diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 8dc64b428..f0ac47632 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -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 = (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() + 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 = (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 = (props) => { } = useFilteredList({ 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 = (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 = (props) => { const requestParts = [ textPart, ...fileAttachmentParts, - ...contextFileParts, + ...contextParts, ...agentAttachmentParts, ...imageAttachmentParts, ] @@ -1345,6 +1399,10 @@ export const PromptInput: Component = (props) => { ) } + for (const item of commentItems) { + prompt.context.remove(item.key) + } + clearInput() addOptimisticMessage() @@ -1363,6 +1421,16 @@ export const PromptInput: Component = (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 = (props) => { - 0 || !!activeFile()}> + 0}>
- - {(path) => ( -
-
- -
- {getDirectory(path())} - {getFilename(path())} - - {(sel) => ( - - {sel().startLine === sel().endLine - ? `:${sel().startLine}` - : `:${sel().startLine}-${sel().endLine}`} - - )} - - {language.t("prompt.context.active")} -
- prompt.context.removeActive()} - aria-label={language.t("prompt.context.removeActiveFile")} - /> -
-
- )} -
- - - {(item) => { const preview = createMemo(() => selectionPreview(item.path, item.selection, item.preview)) diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx index 40baa0ef5..6b9d5cc0d 100644 --- a/packages/app/src/context/prompt.tsx +++ b/packages/app/src/context/prompt.tsx @@ -122,14 +122,12 @@ function createPromptSession(dir: string, id: string | undefined) { prompt: Prompt cursor?: number context: { - activeTab: boolean items: (ContextItem & { key: string })[] } }>({ prompt: clonePrompt(DEFAULT_PROMPT), cursor: undefined, context: { - activeTab: true, items: [], }, }), @@ -157,14 +155,7 @@ function createPromptSession(dir: string, id: string | undefined) { cursor: createMemo(() => store.cursor), dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)), context: { - activeTab: createMemo(() => store.context.activeTab), items: createMemo(() => store.context.items), - addActive() { - setStore("context", "activeTab", true) - }, - removeActive() { - setStore("context", "activeTab", false) - }, add(item: ContextItem) { const key = keyForItem(item) if (store.context.items.find((x) => x.key === key)) return @@ -243,10 +234,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext( cursor: () => session().cursor(), dirty: () => session().dirty(), context: { - activeTab: () => session().context.activeTab(), items: () => session().context.items(), - addActive: () => session().context.addActive(), - removeActive: () => session().context.removeActive(), add: (item: ContextItem) => session().context.add(item), remove: (key: string) => session().context.remove(key), }, diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 96de3f117..9470a032f 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1810,8 +1810,6 @@ export default function Page() { let pending: { x: number; y: number } | undefined let codeScroll: HTMLElement[] = [] - const [selectionPopoverTop, setSelectionPopoverTop] = createSignal() - const path = createMemo(() => file.pathFromTab(tab)) const state = createMemo(() => { const p = path() @@ -1855,17 +1853,6 @@ export default function Page() { if (file.ready()) return file.selectedLines(p) ?? null return handoff.files[p] ?? null }) - const selection = createMemo(() => { - const range = selectedLines() - if (!range) return - return selectionFromLines(range) - }) - const selectionLabel = createMemo(() => { - const sel = selection() - if (!sel) return - if (sel.startLine === sel.endLine) return `L${sel.startLine}` - return `L${sel.startLine}-${sel.endLine}` - }) let wrap: HTMLDivElement | undefined let textarea: HTMLTextAreaElement | undefined @@ -1991,7 +1978,6 @@ export default function Page() { commentedLines={commentedLines()} onRendered={() => { requestAnimationFrame(restoreScroll) - requestAnimationFrame(updateSelectionPopover) requestAnimationFrame(scheduleComments) }} onLineSelected={(range: SelectedLineRange | null) => { @@ -2119,61 +2105,6 @@ export default function Page() {
) - const updateSelectionPopover = () => { - const el = scroll - if (!el) { - setSelectionPopoverTop(undefined) - return - } - - const sel = selection() - if (!sel) { - setSelectionPopoverTop(undefined) - return - } - - const host = el.querySelector("diffs-container") - if (!(host instanceof HTMLElement)) { - setSelectionPopoverTop(undefined) - return - } - - const root = host.shadowRoot - if (!root) { - setSelectionPopoverTop(undefined) - return - } - - const marker = - (root.querySelector( - '[data-selected-line="last"], [data-selected-line="single"]', - ) as HTMLElement | null) ?? (root.querySelector("[data-selected-line]") as HTMLElement | null) - - if (!marker) { - setSelectionPopoverTop(undefined) - return - } - - const containerRect = el.getBoundingClientRect() - const markerRect = marker.getBoundingClientRect() - setSelectionPopoverTop(markerRect.bottom - containerRect.top + el.scrollTop + 8) - } - - createEffect( - on( - selection, - (sel) => { - if (!sel) { - setSelectionPopoverTop(undefined) - return - } - - requestAnimationFrame(updateSelectionPopover) - }, - { defer: true }, - ), - ) - const getCodeScroll = () => { const el = scroll if (!el) return [] @@ -2312,41 +2243,9 @@ export default function Page() { ref={(el: HTMLDivElement) => { scroll = el restoreScroll() - updateSelectionPopover() }} onScroll={handleScroll} > - - - {(sel) => ( -
- - - -
- )} -
-
diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index f34c8b446..b5eb78753 100644 --- a/packages/ui/src/components/session-review.css +++ b/packages/ui/src/components/session-review.css @@ -79,7 +79,7 @@ position: absolute; top: 0; right: calc(100% + 12px); - z-index: 40; + z-index: 6; min-width: 200px; max-width: min(320px, calc(100vw - 48px)); border-radius: var(--radius-md); @@ -223,7 +223,7 @@ [data-slot="session-review-comment-anchor"] { position: absolute; right: 12px; - z-index: 30; + z-index: 5; } [data-slot="session-review-comment-button"] {