Adam пре 4 недеља
родитељ
комит
82f718b3cf

+ 106 - 79
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<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[] = []
+
+    for (const tab of order) {
+      const path = files.pathFromTab(tab)
+      if (!path) continue
+      if (seen.has(path)) continue
+      seen.add(path)
+      paths.push(path)
+    }
 
-  const activeFileSelection = createMemo(() => {
-    const path = activeFile()
-    if (!path) return
-    const range = files.selectedLines(path)
-    if (!range) return
-    return selectionFromLines(range)
+    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 addContextFile = (path: string, selection?: FileSelection) => {
-      const absolute = toAbsolutePath(path)
-      const query = selection ? `?start=${selection.startLine}&end=${selection.endLine}` : ""
+    const context = prompt.context.items().slice()
+
+    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))

+ 0 - 12
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),
       },

+ 0 - 101
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<number | undefined>()
-
                     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() {
                       </div>
                     )
 
-                    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}
                       >
-                        <Show when={activeTab() === tab}>
-                          <Show when={selectionPopoverTop() !== undefined && selection()}>
-                            {(sel) => (
-                              <div class="absolute z-20 right-6" style={{ top: `${selectionPopoverTop() ?? 0}px` }}>
-                                <TooltipKeybind
-                                  placement="bottom"
-                                  title="Add selection to context"
-                                  keybind={command.keybind("context.addSelection")}
-                                >
-                                  <button
-                                    type="button"
-                                    class="group relative flex items-center gap-2 h-6 px-2.5 rounded-md bg-surface-raised-stronger-non-alpha border border-border-weak-base text-12-medium text-text-strong shadow-xs-border whitespace-nowrap hover:bg-surface-raised-stronger-hover hover:border-border-hover focus:outline-none focus-visible:shadow-xs-border-focus"
-                                    onClick={() => {
-                                      const p = path()
-                                      if (!p) return
-                                      addSelectionToContext(p, sel())
-                                    }}
-                                  >
-                                    <span class="pointer-events-none absolute -left-1 top-1/2 size-2.5 -translate-y-1/2 rotate-45 bg-surface-raised-stronger-non-alpha border-l border-b border-border-weak-base group-hover:bg-surface-raised-stronger-hover group-hover:border-border-hover" />
-                                    <Icon name="plus-small" size="small" />
-                                    <span>
-                                      {language.t("session.context.addToContext", {
-                                        selection: selectionLabel() ?? "",
-                                      })}
-                                    </span>
-                                  </button>
-                                </TooltipKeybind>
-                              </div>
-                            )}
-                          </Show>
-                        </Show>
                         <Switch>
                           <Match when={state()?.loaded && isImage()}>
                             <div class="px-6 py-4 pb-40">

+ 2 - 2
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"] {