|
|
@@ -12,7 +12,7 @@ import { useRoute } from "@tui/context/route"
|
|
|
import { useProject } from "@tui/context/project"
|
|
|
import { useSync } from "@tui/context/sync"
|
|
|
import { useEvent } from "@tui/context/event"
|
|
|
-import { useEditorContext, type EditorSelection } from "@tui/context/editor"
|
|
|
+import { editorSelectionKey, useEditorContext, type EditorSelection } from "@tui/context/editor"
|
|
|
import { MessageID, PartID } from "@/session/schema"
|
|
|
import { createStore, produce, unwrap } from "solid-js/store"
|
|
|
import { useKeybind } from "@tui/context/keybind"
|
|
|
@@ -84,16 +84,30 @@ function fadeColor(color: RGBA, alpha: number) {
|
|
|
return RGBA.fromValues(color.r, color.g, color.b, color.a * alpha)
|
|
|
}
|
|
|
|
|
|
-function getEditorSelectionKey(selection: EditorSelection) {
|
|
|
- return [
|
|
|
- selection.filePath,
|
|
|
- selection.text,
|
|
|
- selection.source ?? "",
|
|
|
- selection.selection.start.line,
|
|
|
- selection.selection.start.character,
|
|
|
- selection.selection.end.line,
|
|
|
- selection.selection.end.character,
|
|
|
- ].join("-")
|
|
|
+function hasEditorRangeSelection(selection: EditorSelection["ranges"][number]) {
|
|
|
+ return (
|
|
|
+ selection.selection.start.line !== selection.selection.end.line ||
|
|
|
+ selection.selection.start.character !== selection.selection.end.character
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function getEditorRangeLabel(selection: EditorSelection["ranges"][number]) {
|
|
|
+ if (!hasEditorRangeSelection(selection)) return
|
|
|
+ if (selection.selection.start.line === selection.selection.end.line) return `#${selection.selection.start.line}`
|
|
|
+ return `#${selection.selection.start.line}-${selection.selection.end.line}`
|
|
|
+}
|
|
|
+
|
|
|
+function formatEditorContext(selection: EditorSelection) {
|
|
|
+ const selected = selection.ranges.filter(hasEditorRangeSelection)
|
|
|
+ if (selected.length === 0)
|
|
|
+ return `<system-reminder>Note: The user opened the file "${selection.filePath}". This may or may not be relevant to the current task.</system-reminder>\n`
|
|
|
+
|
|
|
+ const ranges = selected.map((range, index) => {
|
|
|
+ const prefix = selected.length > 1 ? `Selection ${index + 1}: ` : ""
|
|
|
+ return `Note: The user selected ${prefix}${getEditorRangeLabel(range)} from "${selection.filePath}". \`\`\`${range.text}\`\`\`\n\n`
|
|
|
+ })
|
|
|
+
|
|
|
+ return `<system-reminder>${ranges.join("\n")} This may or may not be relevant to the current task.</system-reminder>\n`
|
|
|
}
|
|
|
|
|
|
let stashed: { prompt: PromptInfo; cursor: number } | undefined
|
|
|
@@ -125,13 +139,21 @@ export function Prompt(props: PromptProps) {
|
|
|
const list = createMemo(() => props.placeholders?.normal ?? [])
|
|
|
const shell = createMemo(() => props.placeholders?.shell ?? [])
|
|
|
const fileContextEnabled = createMemo(() => kv.get("file_context_enabled", true))
|
|
|
- const editorPath = createMemo(() => (fileContextEnabled() ? editor.selection()?.filePath : undefined))
|
|
|
- const editorSelectionLabel = createMemo(() => {
|
|
|
- const selection = fileContextEnabled() ? editor.selection()?.selection : undefined
|
|
|
+ const [dismissedEditorSelectionKey, setDismissedEditorSelectionKey] = createSignal<string>()
|
|
|
+ const editorContext = createMemo(() => {
|
|
|
+ const selection = fileContextEnabled() ? editor.selection() : undefined
|
|
|
if (!selection) return
|
|
|
- if (selection.start.line === selection.end.line && selection.start.character === selection.end.character) return
|
|
|
- if (selection.start.line === selection.end.line) return `#${selection.start.line}`
|
|
|
- return `#${selection.start.line}-${selection.end.line}`
|
|
|
+ return editorSelectionKey(selection) === dismissedEditorSelectionKey() ? undefined : selection
|
|
|
+ })
|
|
|
+ const editorPath = createMemo(() => editorContext()?.filePath)
|
|
|
+ const editorSelectionLabel = createMemo(() => {
|
|
|
+ const ranges = editorContext()?.ranges
|
|
|
+ if (!ranges) return
|
|
|
+ const first = ranges.find(hasEditorRangeSelection) ?? ranges[0]
|
|
|
+ if (!first) return
|
|
|
+ return [getEditorRangeLabel(first), ranges.length > 1 ? `+${ranges.length - 1}` : undefined]
|
|
|
+ .filter(Boolean)
|
|
|
+ .join(" ")
|
|
|
})
|
|
|
const editorFileLabel = createMemo(() => {
|
|
|
const value = editorPath()
|
|
|
@@ -147,6 +169,7 @@ export function Prompt(props: PromptProps) {
|
|
|
if (!file) return
|
|
|
return Locale.truncateMiddle(file, Math.max(12, Math.min(48, Math.floor(dimensions().width / 3))))
|
|
|
})
|
|
|
+ const [editorContextHover, setEditorContextHover] = createSignal(false)
|
|
|
let lastSubmittedEditorSelectionKey: string | undefined
|
|
|
const [auto, setAuto] = createSignal<AutocompleteRef>()
|
|
|
const currentProviderLabel = createMemo(() => local.model.parsed().provider)
|
|
|
@@ -163,6 +186,11 @@ export function Prompt(props: PromptProps) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ function dismissEditorContext() {
|
|
|
+ setDismissedEditorSelectionKey(editorSelectionKey(editorContext()))
|
|
|
+ editor.clearSelection()
|
|
|
+ }
|
|
|
+
|
|
|
const textareaKeybindings = useTextareaKeybindings()
|
|
|
|
|
|
const fileStyleId = syntax().getStyleId("extmark.file")!
|
|
|
@@ -292,6 +320,16 @@ export function Prompt(props: PromptProps) {
|
|
|
dialog.clear()
|
|
|
},
|
|
|
},
|
|
|
+ {
|
|
|
+ title: "Remove editor context",
|
|
|
+ value: "prompt.editor_context.clear",
|
|
|
+ category: "Prompt",
|
|
|
+ enabled: Boolean(editorContext()),
|
|
|
+ onSelect: (dialog) => {
|
|
|
+ dismissEditorContext()
|
|
|
+ dialog.clear()
|
|
|
+ },
|
|
|
+ },
|
|
|
{
|
|
|
title: "Paste",
|
|
|
value: "prompt.paste",
|
|
|
@@ -760,35 +798,21 @@ export function Prompt(props: PromptProps) {
|
|
|
// Capture mode before it gets reset
|
|
|
const currentMode = store.mode
|
|
|
const variant = local.model.variant.current()
|
|
|
- const editorSelection = fileContextEnabled() ? editor.selection() : undefined
|
|
|
- const editorSelectionKey = editorSelection ? getEditorSelectionKey(editorSelection) : undefined
|
|
|
+ const editorSelection = editorContext()
|
|
|
+ const currentEditorSelectionKey = editorSelectionKey(editorSelection)
|
|
|
const editorParts =
|
|
|
- editorSelection && editorSelectionKey !== lastSubmittedEditorSelectionKey
|
|
|
+ editorSelection && currentEditorSelectionKey !== lastSubmittedEditorSelectionKey
|
|
|
? [
|
|
|
{
|
|
|
id: PartID.ascending(),
|
|
|
type: "text" as const,
|
|
|
- text: (() => {
|
|
|
- const start = editorSelection.selection.start
|
|
|
- const end = editorSelection.selection.end
|
|
|
-
|
|
|
- let text = ""
|
|
|
- if (start.line === end.line && start.character === end.character) {
|
|
|
- text = `Note: The user opened the file "${editorSelection.filePath}".`
|
|
|
- } else if (start.line === end.line) {
|
|
|
- text = `Note: The user selected line ${start.line + 1} from "${editorSelection.filePath}". \`\`\`${editorSelection.text}\`\`\`\n\n`
|
|
|
- } else {
|
|
|
- text = `Note: The user selected lines ${start.line + 1} to ${end.line + 1} from "${editorSelection.filePath}". \`\`\`${editorSelection.text}\`\`\`\n\n`
|
|
|
- }
|
|
|
-
|
|
|
- return `<system-reminder>${text} This may or may not be relevant to the current task.</system-reminder>\n`
|
|
|
- })(),
|
|
|
+ text: formatEditorContext(editorSelection),
|
|
|
synthetic: true,
|
|
|
metadata: {
|
|
|
kind: "editor_context",
|
|
|
source: editorSelection.source ?? "editor",
|
|
|
filePath: editorSelection.filePath,
|
|
|
- selection: editorSelection.selection,
|
|
|
+ ranges: editorSelection.ranges,
|
|
|
},
|
|
|
},
|
|
|
]
|
|
|
@@ -855,7 +879,7 @@ export function Prompt(props: PromptProps) {
|
|
|
],
|
|
|
})
|
|
|
.catch(() => {})
|
|
|
- lastSubmittedEditorSelectionKey = editorSelectionKey
|
|
|
+ lastSubmittedEditorSelectionKey = currentEditorSelectionKey
|
|
|
}
|
|
|
history.append({
|
|
|
...store.prompt,
|
|
|
@@ -1406,7 +1430,18 @@ export function Prompt(props: PromptProps) {
|
|
|
</Show>
|
|
|
<Show when={status().type !== "retry"}>
|
|
|
<box gap={2} flexDirection="row">
|
|
|
- <Show when={editorFileLabelDisplay()}>{(file) => <text fg={theme.secondary}>{file()}</text>}</Show>
|
|
|
+ <Show when={editorFileLabelDisplay()}>
|
|
|
+ {(file) => (
|
|
|
+ <text
|
|
|
+ fg={theme.secondary}
|
|
|
+ onMouseOver={() => setEditorContextHover(true)}
|
|
|
+ onMouseOut={() => setEditorContextHover(false)}
|
|
|
+ onMouseUp={dismissEditorContext}
|
|
|
+ >
|
|
|
+ {editorContextHover() ? `x ${file()}` : file()}
|
|
|
+ </text>
|
|
|
+ )}
|
|
|
+ </Show>
|
|
|
<Switch>
|
|
|
<Match when={store.mode === "normal"}>
|
|
|
<Switch>
|