wip(app): line selection

This commit is contained in:
Adam
2026-01-22 19:57:51 -06:00
parent 0eb523631d
commit 82f718b3cf
4 changed files with 107 additions and 193 deletions

View File

@@ -15,7 +15,7 @@ import {
import { createStore, produce } from "solid-js/store"
import { createFocusSignal } from "@solid-primitives/active-element"
import { useLocal } from "@/context/local"
import { selectionFromLines, useFile, type FileSelection } from "@/context/file"
import { useFile, type FileSelection } from "@/context/file"
import {
ContentPart,
DEFAULT_PROMPT,
@@ -161,18 +161,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey()))
const view = createMemo(() => layout.view(sessionKey()))
const activeFile = createMemo(() => {
const tab = tabs().active()
if (!tab) return
return files.pathFromTab(tab)
})
const recent = createMemo(() => {
const all = tabs().all()
const active = tabs().active()
const order = active ? [active, ...all.filter((x) => x !== active)] : all
const seen = new Set<string>()
const paths: string[] = []
const activeFileSelection = createMemo(() => {
const path = activeFile()
if (!path) return
const range = files.selectedLines(path)
if (!range) return
return selectionFromLines(range)
for (const tab of order) {
const path = files.pathFromTab(tab)
if (!path) continue
if (seen.has(path)) continue
seen.add(path)
paths.push(path)
}
return paths
})
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const status = createMemo(
@@ -393,7 +397,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (!isFocused()) setComposing(false)
})
type AtOption = { type: "agent"; name: string; display: string } | { type: "file"; path: string; display: string }
type AtOption =
| { type: "agent"; name: string; display: string }
| { type: "file"; path: string; display: string; recent?: boolean }
const agentList = createMemo(() =>
sync.data.agent
@@ -424,12 +430,30 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
} = useFilteredList<AtOption>({
items: async (query) => {
const agents = agentList()
const open = recent()
const seen = new Set(open)
const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
const paths = await files.searchFilesAndDirectories(query)
const fileOptions: AtOption[] = paths.map((path) => ({ type: "file", path, display: path }))
return [...agents, ...fileOptions]
const fileOptions: AtOption[] = paths
.filter((path) => !seen.has(path))
.map((path) => ({ type: "file", path, display: path }))
return [...agents, ...pinned, ...fileOptions]
},
key: atKey,
filterKeys: ["display"],
groupBy: (item) => {
if (item.type === "agent") return "agent"
if (item.recent) return "recent"
return "file"
},
sortGroupsBy: (a, b) => {
const rank = (category: string) => {
if (category === "agent") return 0
if (category === "recent") return 1
return 2
}
return rank(a.category) - rank(b.category)
},
onSelect: handleAtSelect,
})
@@ -1242,37 +1266,67 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const usedUrls = new Set(fileAttachmentParts.map((part) => part.url))
const contextFileParts: Array<{
id: string
type: "file"
mime: string
url: string
filename?: string
}> = []
const context = prompt.context.items().slice()
const addContextFile = (path: string, selection?: FileSelection) => {
const absolute = toAbsolutePath(path)
const query = selection ? `?start=${selection.startLine}&end=${selection.endLine}` : ""
const commentItems = context.filter((item) => item.type === "file" && !!item.comment?.trim())
const contextParts: Array<
| {
id: string
type: "text"
text: string
}
| {
id: string
type: "file"
mime: string
url: string
filename?: string
}
> = []
const commentNote = (path: string, selection: FileSelection | undefined, comment: string) => {
const start = selection ? Math.min(selection.startLine, selection.endLine) : undefined
const end = selection ? Math.max(selection.startLine, selection.endLine) : undefined
const range =
start === undefined || end === undefined
? "this file"
: start === end
? `line ${start}`
: `lines ${start} through ${end}`
return `The user made the following comment regarding ${range} of ${path}: ${comment}`
}
const addContextFile = (input: { path: string; selection?: FileSelection; comment?: string }) => {
const absolute = toAbsolutePath(input.path)
const query = input.selection ? `?start=${input.selection.startLine}&end=${input.selection.endLine}` : ""
const url = `file://${absolute}${query}`
if (usedUrls.has(url)) return
const comment = input.comment?.trim()
if (!comment && usedUrls.has(url)) return
usedUrls.add(url)
contextFileParts.push({
if (comment) {
contextParts.push({
id: Identifier.ascending("part"),
type: "text",
text: commentNote(input.path, input.selection, comment),
})
}
contextParts.push({
id: Identifier.ascending("part"),
type: "file",
mime: "text/plain",
url,
filename: getFilename(path),
filename: getFilename(input.path),
})
}
const activePath = activeFile()
if (activePath && prompt.context.activeTab()) {
addContextFile(activePath, activeFileSelection())
}
for (const item of prompt.context.items()) {
for (const item of context) {
if (item.type !== "file") continue
addContextFile(item.path, item.selection)
addContextFile({ path: item.path, selection: item.selection, comment: item.comment })
}
const imageAttachmentParts = images.map((attachment) => ({
@@ -1292,7 +1346,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const requestParts = [
textPart,
...fileAttachmentParts,
...contextFileParts,
...contextParts,
...agentAttachmentParts,
...imageAttachmentParts,
]
@@ -1345,6 +1399,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
)
}
for (const item of commentItems) {
prompt.context.remove(item.key)
}
clearInput()
addOptimisticMessage()
@@ -1363,6 +1421,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
description: errorMessage(err),
})
removeOptimisticMessage()
for (const item of commentItems) {
prompt.context.add({
type: "file",
path: item.path,
selection: item.selection,
comment: item.comment,
commentID: item.commentID,
preview: item.preview,
})
}
restoreInput()
})
}
@@ -1487,49 +1555,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</div>
</div>
</Show>
<Show when={prompt.context.items().length > 0 || !!activeFile()}>
<Show when={prompt.context.items().length > 0}>
<div class="flex flex-nowrap items-start gap-1.5 px-3 pt-3 overflow-x-auto no-scrollbar">
<Show when={prompt.context.activeTab() ? activeFile() : undefined}>
{(path) => (
<div class="shrink-0 flex flex-col gap-1 rounded-md bg-surface-base border border-border-base px-2 py-1 max-w-[320px]">
<div class="flex items-center gap-1.5">
<FileIcon node={{ path: path(), type: "file" }} class="shrink-0 size-3.5" />
<div class="flex items-center text-11-regular min-w-0">
<span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(path())}</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(path())}</span>
<Show when={activeFileSelection()}>
{(sel) => (
<span class="text-text-weak whitespace-nowrap ml-1">
{sel().startLine === sel().endLine
? `:${sel().startLine}`
: `:${sel().startLine}-${sel().endLine}`}
</span>
)}
</Show>
<span class="text-text-weak whitespace-nowrap ml-1">{language.t("prompt.context.active")}</span>
</div>
<IconButton
type="button"
icon="close"
variant="ghost"
class="h-5 w-5"
onClick={() => prompt.context.removeActive()}
aria-label={language.t("prompt.context.removeActiveFile")}
/>
</div>
</div>
)}
</Show>
<Show when={!prompt.context.activeTab() && !!activeFile()}>
<button
type="button"
class="shrink-0 flex items-center gap-1.5 px-1.5 py-0.5 rounded-md bg-surface-base border border-border-base text-11-regular text-text-weak hover:bg-surface-raised-base-hover"
onClick={() => prompt.context.addActive()}
>
<Icon name="plus-small" size="small" />
<span>{language.t("prompt.context.includeActiveFile")}</span>
</button>
</Show>
<For each={prompt.context.items()}>
{(item) => {
const preview = createMemo(() => selectionPreview(item.path, item.selection, item.preview))