|
@@ -15,7 +15,7 @@ import {
|
|
|
import { createStore, produce } from "solid-js/store"
|
|
import { createStore, produce } from "solid-js/store"
|
|
|
import { createFocusSignal } from "@solid-primitives/active-element"
|
|
import { createFocusSignal } from "@solid-primitives/active-element"
|
|
|
import { useLocal } from "@/context/local"
|
|
import { useLocal } from "@/context/local"
|
|
|
-import { selectionFromLines, useFile, type FileSelection } from "@/context/file"
|
|
|
|
|
|
|
+import { useFile, type FileSelection } from "@/context/file"
|
|
|
import {
|
|
import {
|
|
|
ContentPart,
|
|
ContentPart,
|
|
|
DEFAULT_PROMPT,
|
|
DEFAULT_PROMPT,
|
|
@@ -161,18 +161,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
|
|
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
|
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
|
|
const view = createMemo(() => layout.view(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 info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
|
|
const status = createMemo(
|
|
const status = createMemo(
|
|
@@ -393,7 +397,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
if (!isFocused()) setComposing(false)
|
|
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(() =>
|
|
const agentList = createMemo(() =>
|
|
|
sync.data.agent
|
|
sync.data.agent
|
|
@@ -424,12 +430,30 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
} = useFilteredList<AtOption>({
|
|
} = useFilteredList<AtOption>({
|
|
|
items: async (query) => {
|
|
items: async (query) => {
|
|
|
const agents = agentList()
|
|
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 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,
|
|
key: atKey,
|
|
|
filterKeys: ["display"],
|
|
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,
|
|
onSelect: handleAtSelect,
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -1242,37 +1266,67 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
|
|
|
|
|
const usedUrls = new Set(fileAttachmentParts.map((part) => part.url))
|
|
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}`
|
|
const url = `file://${absolute}${query}`
|
|
|
- if (usedUrls.has(url)) return
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const comment = input.comment?.trim()
|
|
|
|
|
+ if (!comment && usedUrls.has(url)) return
|
|
|
usedUrls.add(url)
|
|
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"),
|
|
id: Identifier.ascending("part"),
|
|
|
type: "file",
|
|
type: "file",
|
|
|
mime: "text/plain",
|
|
mime: "text/plain",
|
|
|
url,
|
|
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
|
|
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) => ({
|
|
const imageAttachmentParts = images.map((attachment) => ({
|
|
@@ -1292,7 +1346,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
const requestParts = [
|
|
const requestParts = [
|
|
|
textPart,
|
|
textPart,
|
|
|
...fileAttachmentParts,
|
|
...fileAttachmentParts,
|
|
|
- ...contextFileParts,
|
|
|
|
|
|
|
+ ...contextParts,
|
|
|
...agentAttachmentParts,
|
|
...agentAttachmentParts,
|
|
|
...imageAttachmentParts,
|
|
...imageAttachmentParts,
|
|
|
]
|
|
]
|
|
@@ -1345,6 +1399,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ for (const item of commentItems) {
|
|
|
|
|
+ prompt.context.remove(item.key)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
clearInput()
|
|
clearInput()
|
|
|
addOptimisticMessage()
|
|
addOptimisticMessage()
|
|
|
|
|
|
|
@@ -1363,6 +1421,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
description: errorMessage(err),
|
|
description: errorMessage(err),
|
|
|
})
|
|
})
|
|
|
removeOptimisticMessage()
|
|
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()
|
|
restoreInput()
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
@@ -1487,49 +1555,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</Show>
|
|
</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">
|
|
<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()}>
|
|
<For each={prompt.context.items()}>
|
|
|
{(item) => {
|
|
{(item) => {
|
|
|
const preview = createMemo(() => selectionPreview(item.path, item.selection, item.preview))
|
|
const preview = createMemo(() => selectionPreview(item.path, item.selection, item.preview))
|