Skip to content

Commit 82f718b

Browse files
committed
wip(app): line selection
1 parent 0eb5236 commit 82f718b

4 files changed

Lines changed: 108 additions & 194 deletions

File tree

packages/app/src/components/prompt-input.tsx

Lines changed: 106 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
import { createStore, produce } from "solid-js/store"
1616
import { createFocusSignal } from "@solid-primitives/active-element"
1717
import { useLocal } from "@/context/local"
18-
import { selectionFromLines, useFile, type FileSelection } from "@/context/file"
18+
import { useFile, type FileSelection } from "@/context/file"
1919
import {
2020
ContentPart,
2121
DEFAULT_PROMPT,
@@ -161,18 +161,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
161161
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
162162
const tabs = createMemo(() => layout.tabs(sessionKey()))
163163
const view = createMemo(() => layout.view(sessionKey()))
164-
const activeFile = createMemo(() => {
165-
const tab = tabs().active()
166-
if (!tab) return
167-
return files.pathFromTab(tab)
168-
})
164+
const recent = createMemo(() => {
165+
const all = tabs().all()
166+
const active = tabs().active()
167+
const order = active ? [active, ...all.filter((x) => x !== active)] : all
168+
const seen = new Set<string>()
169+
const paths: string[] = []
170+
171+
for (const tab of order) {
172+
const path = files.pathFromTab(tab)
173+
if (!path) continue
174+
if (seen.has(path)) continue
175+
seen.add(path)
176+
paths.push(path)
177+
}
169178

170-
const activeFileSelection = createMemo(() => {
171-
const path = activeFile()
172-
if (!path) return
173-
const range = files.selectedLines(path)
174-
if (!range) return
175-
return selectionFromLines(range)
179+
return paths
176180
})
177181
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
178182
const status = createMemo(
@@ -393,7 +397,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
393397
if (!isFocused()) setComposing(false)
394398
})
395399

396-
type AtOption = { type: "agent"; name: string; display: string } | { type: "file"; path: string; display: string }
400+
type AtOption =
401+
| { type: "agent"; name: string; display: string }
402+
| { type: "file"; path: string; display: string; recent?: boolean }
397403

398404
const agentList = createMemo(() =>
399405
sync.data.agent
@@ -424,12 +430,30 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
424430
} = useFilteredList<AtOption>({
425431
items: async (query) => {
426432
const agents = agentList()
433+
const open = recent()
434+
const seen = new Set(open)
435+
const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
427436
const paths = await files.searchFilesAndDirectories(query)
428-
const fileOptions: AtOption[] = paths.map((path) => ({ type: "file", path, display: path }))
429-
return [...agents, ...fileOptions]
437+
const fileOptions: AtOption[] = paths
438+
.filter((path) => !seen.has(path))
439+
.map((path) => ({ type: "file", path, display: path }))
440+
return [...agents, ...pinned, ...fileOptions]
430441
},
431442
key: atKey,
432443
filterKeys: ["display"],
444+
groupBy: (item) => {
445+
if (item.type === "agent") return "agent"
446+
if (item.recent) return "recent"
447+
return "file"
448+
},
449+
sortGroupsBy: (a, b) => {
450+
const rank = (category: string) => {
451+
if (category === "agent") return 0
452+
if (category === "recent") return 1
453+
return 2
454+
}
455+
return rank(a.category) - rank(b.category)
456+
},
433457
onSelect: handleAtSelect,
434458
})
435459

@@ -1242,37 +1266,67 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
12421266

12431267
const usedUrls = new Set(fileAttachmentParts.map((part) => part.url))
12441268

1245-
const contextFileParts: Array<{
1246-
id: string
1247-
type: "file"
1248-
mime: string
1249-
url: string
1250-
filename?: string
1251-
}> = []
1252-
1253-
const addContextFile = (path: string, selection?: FileSelection) => {
1254-
const absolute = toAbsolutePath(path)
1255-
const query = selection ? `?start=${selection.startLine}&end=${selection.endLine}` : ""
1269+
const context = prompt.context.items().slice()
1270+
1271+
const commentItems = context.filter((item) => item.type === "file" && !!item.comment?.trim())
1272+
1273+
const contextParts: Array<
1274+
| {
1275+
id: string
1276+
type: "text"
1277+
text: string
1278+
}
1279+
| {
1280+
id: string
1281+
type: "file"
1282+
mime: string
1283+
url: string
1284+
filename?: string
1285+
}
1286+
> = []
1287+
1288+
const commentNote = (path: string, selection: FileSelection | undefined, comment: string) => {
1289+
const start = selection ? Math.min(selection.startLine, selection.endLine) : undefined
1290+
const end = selection ? Math.max(selection.startLine, selection.endLine) : undefined
1291+
const range =
1292+
start === undefined || end === undefined
1293+
? "this file"
1294+
: start === end
1295+
? `line ${start}`
1296+
: `lines ${start} through ${end}`
1297+
1298+
return `The user made the following comment regarding ${range} of ${path}: ${comment}`
1299+
}
1300+
1301+
const addContextFile = (input: { path: string; selection?: FileSelection; comment?: string }) => {
1302+
const absolute = toAbsolutePath(input.path)
1303+
const query = input.selection ? `?start=${input.selection.startLine}&end=${input.selection.endLine}` : ""
12561304
const url = `file://${absolute}${query}`
1257-
if (usedUrls.has(url)) return
1305+
1306+
const comment = input.comment?.trim()
1307+
if (!comment && usedUrls.has(url)) return
12581308
usedUrls.add(url)
1259-
contextFileParts.push({
1309+
1310+
if (comment) {
1311+
contextParts.push({
1312+
id: Identifier.ascending("part"),
1313+
type: "text",
1314+
text: commentNote(input.path, input.selection, comment),
1315+
})
1316+
}
1317+
1318+
contextParts.push({
12601319
id: Identifier.ascending("part"),
12611320
type: "file",
12621321
mime: "text/plain",
12631322
url,
1264-
filename: getFilename(path),
1323+
filename: getFilename(input.path),
12651324
})
12661325
}
12671326

1268-
const activePath = activeFile()
1269-
if (activePath && prompt.context.activeTab()) {
1270-
addContextFile(activePath, activeFileSelection())
1271-
}
1272-
1273-
for (const item of prompt.context.items()) {
1327+
for (const item of context) {
12741328
if (item.type !== "file") continue
1275-
addContextFile(item.path, item.selection)
1329+
addContextFile({ path: item.path, selection: item.selection, comment: item.comment })
12761330
}
12771331

12781332
const imageAttachmentParts = images.map((attachment) => ({
@@ -1292,7 +1346,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
12921346
const requestParts = [
12931347
textPart,
12941348
...fileAttachmentParts,
1295-
...contextFileParts,
1349+
...contextParts,
12961350
...agentAttachmentParts,
12971351
...imageAttachmentParts,
12981352
]
@@ -1345,6 +1399,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
13451399
)
13461400
}
13471401

1402+
for (const item of commentItems) {
1403+
prompt.context.remove(item.key)
1404+
}
1405+
13481406
clearInput()
13491407
addOptimisticMessage()
13501408

@@ -1363,6 +1421,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
13631421
description: errorMessage(err),
13641422
})
13651423
removeOptimisticMessage()
1424+
for (const item of commentItems) {
1425+
prompt.context.add({
1426+
type: "file",
1427+
path: item.path,
1428+
selection: item.selection,
1429+
comment: item.comment,
1430+
commentID: item.commentID,
1431+
preview: item.preview,
1432+
})
1433+
}
13661434
restoreInput()
13671435
})
13681436
}
@@ -1487,49 +1555,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
14871555
</div>
14881556
</div>
14891557
</Show>
1490-
<Show when={prompt.context.items().length > 0 || !!activeFile()}>
1558+
<Show when={prompt.context.items().length > 0}>
14911559
<div class="flex flex-nowrap items-start gap-1.5 px-3 pt-3 overflow-x-auto no-scrollbar">
1492-
<Show when={prompt.context.activeTab() ? activeFile() : undefined}>
1493-
{(path) => (
1494-
<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]">
1495-
<div class="flex items-center gap-1.5">
1496-
<FileIcon node={{ path: path(), type: "file" }} class="shrink-0 size-3.5" />
1497-
<div class="flex items-center text-11-regular min-w-0">
1498-
<span class="text-text-weak whitespace-nowrap truncate min-w-0">{getDirectory(path())}</span>
1499-
<span class="text-text-strong whitespace-nowrap">{getFilename(path())}</span>
1500-
<Show when={activeFileSelection()}>
1501-
{(sel) => (
1502-
<span class="text-text-weak whitespace-nowrap ml-1">
1503-
{sel().startLine === sel().endLine
1504-
? `:${sel().startLine}`
1505-
: `:${sel().startLine}-${sel().endLine}`}
1506-
</span>
1507-
)}
1508-
</Show>
1509-
<span class="text-text-weak whitespace-nowrap ml-1">{language.t("prompt.context.active")}</span>
1510-
</div>
1511-
<IconButton
1512-
type="button"
1513-
icon="close"
1514-
variant="ghost"
1515-
class="h-5 w-5"
1516-
onClick={() => prompt.context.removeActive()}
1517-
aria-label={language.t("prompt.context.removeActiveFile")}
1518-
/>
1519-
</div>
1520-
</div>
1521-
)}
1522-
</Show>
1523-
<Show when={!prompt.context.activeTab() && !!activeFile()}>
1524-
<button
1525-
type="button"
1526-
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"
1527-
onClick={() => prompt.context.addActive()}
1528-
>
1529-
<Icon name="plus-small" size="small" />
1530-
<span>{language.t("prompt.context.includeActiveFile")}</span>
1531-
</button>
1532-
</Show>
15331560
<For each={prompt.context.items()}>
15341561
{(item) => {
15351562
const preview = createMemo(() => selectionPreview(item.path, item.selection, item.preview))

packages/app/src/context/prompt.tsx

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,12 @@ function createPromptSession(dir: string, id: string | undefined) {
122122
prompt: Prompt
123123
cursor?: number
124124
context: {
125-
activeTab: boolean
126125
items: (ContextItem & { key: string })[]
127126
}
128127
}>({
129128
prompt: clonePrompt(DEFAULT_PROMPT),
130129
cursor: undefined,
131130
context: {
132-
activeTab: true,
133131
items: [],
134132
},
135133
}),
@@ -157,14 +155,7 @@ function createPromptSession(dir: string, id: string | undefined) {
157155
cursor: createMemo(() => store.cursor),
158156
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
159157
context: {
160-
activeTab: createMemo(() => store.context.activeTab),
161158
items: createMemo(() => store.context.items),
162-
addActive() {
163-
setStore("context", "activeTab", true)
164-
},
165-
removeActive() {
166-
setStore("context", "activeTab", false)
167-
},
168159
add(item: ContextItem) {
169160
const key = keyForItem(item)
170161
if (store.context.items.find((x) => x.key === key)) return
@@ -243,10 +234,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
243234
cursor: () => session().cursor(),
244235
dirty: () => session().dirty(),
245236
context: {
246-
activeTab: () => session().context.activeTab(),
247237
items: () => session().context.items(),
248-
addActive: () => session().context.addActive(),
249-
removeActive: () => session().context.removeActive(),
250238
add: (item: ContextItem) => session().context.add(item),
251239
remove: (key: string) => session().context.remove(key),
252240
},

0 commit comments

Comments
 (0)