@agentprojectcontext/apx 1.15.5 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +40 -5
- package/src/cli/commands/log.js +113 -0
- package/src/cli/commands/overlay.js +253 -0
- package/src/cli/commands/sys.js +88 -16
- package/src/cli/index.js +23 -1
- package/src/cli/terminal-chat/renderer.js +71 -56
- package/src/cli-ts/commands/agent.ts +173 -0
- package/src/cli-ts/commands/chat.ts +119 -0
- package/src/cli-ts/commands/daemon.ts +112 -0
- package/src/cli-ts/commands/exec.ts +109 -0
- package/src/cli-ts/commands/mcp.ts +235 -0
- package/src/cli-ts/commands/session.ts +224 -0
- package/src/cli-ts/commands/status.ts +61 -0
- package/src/cli-ts/http.ts +36 -0
- package/src/cli-ts/index.ts +73 -0
- package/src/cli-ts/ui.ts +107 -0
- package/src/core/logging.js +81 -0
- package/src/daemon/api.js +58 -0
- package/src/daemon/engines/anthropic.js +60 -1
- package/src/daemon/engines/index.js +2 -1
- package/src/daemon/engines/ollama.js +70 -3
- package/src/daemon/index.js +58 -0
- package/src/daemon/overlay-ws.js +40 -0
- package/src/daemon/plugins/index.js +2 -1
- package/src/daemon/plugins/overlay.js +177 -0
- package/src/daemon/plugins/telegram.js +15 -3
- package/src/daemon/super-agent.js +102 -19
- package/src/daemon/transcription.js +262 -59
- package/src/daemon/wakeup.js +14 -19
- package/src/daemon/whisper-server.py +57 -6
- package/src/overlay/index.html +44 -0
- package/src/overlay/main.js +480 -0
- package/src/overlay/package.json +3 -0
- package/src/overlay/preload.js +34 -0
- package/src/overlay/renderer.js +371 -0
- package/src/overlay/style.css +250 -0
- package/src/tui/_shims/cli-error.ts +6 -0
- package/src/tui/_shims/cli-logo.ts +18 -0
- package/src/tui/_shims/cli-ui.ts +1 -0
- package/src/tui/_shims/config-console-state.ts +7 -0
- package/src/tui/_shims/core-any.ts +30 -0
- package/src/tui/_shims/core-binary.ts +13 -0
- package/src/tui/_shims/core-flag.ts +3 -0
- package/src/tui/_shims/core-log.ts +14 -0
- package/src/tui/_shims/lsp-language.ts +1 -0
- package/src/tui/_shims/opencode-any.ts +135 -0
- package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
- package/src/tui/_shims/plugin-tui.ts +13 -0
- package/src/tui/_shims/provider-provider.ts +10 -0
- package/src/tui/_shims/session-retry.ts +1 -0
- package/src/tui/_shims/session-schema.ts +15 -0
- package/src/tui/_shims/session-session.ts +3 -0
- package/src/tui/_shims/snapshot.ts +4 -0
- package/src/tui/_shims/tool-any.ts +18 -0
- package/src/tui/_shims/util-error.ts +7 -0
- package/src/tui/_shims/util-filesystem.ts +79 -0
- package/src/tui/_shims/util-format.ts +7 -0
- package/src/tui/_shims/util-iife.ts +3 -0
- package/src/tui/_shims/util-locale.ts +10 -0
- package/src/tui/_shims/util-process.ts +38 -0
- package/src/tui/app.tsx +783 -0
- package/src/tui/asset/charge.wav +0 -0
- package/src/tui/asset/pulse-a.wav +0 -0
- package/src/tui/asset/pulse-b.wav +0 -0
- package/src/tui/asset/pulse-c.wav +0 -0
- package/src/tui/attach.ts +100 -0
- package/src/tui/component/bg-pulse-render.ts +436 -0
- package/src/tui/component/bg-pulse.tsx +99 -0
- package/src/tui/component/border.tsx +21 -0
- package/src/tui/component/dialog-agent.tsx +31 -0
- package/src/tui/component/dialog-console-org.tsx +103 -0
- package/src/tui/component/dialog-mcp.tsx +85 -0
- package/src/tui/component/dialog-model.tsx +175 -0
- package/src/tui/component/dialog-provider.tsx +456 -0
- package/src/tui/component/dialog-retry-action.tsx +160 -0
- package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
- package/src/tui/component/dialog-session-list.tsx +323 -0
- package/src/tui/component/dialog-session-rename.tsx +31 -0
- package/src/tui/component/dialog-skill.tsx +36 -0
- package/src/tui/component/dialog-stash.tsx +87 -0
- package/src/tui/component/dialog-status.tsx +168 -0
- package/src/tui/component/dialog-tag.tsx +44 -0
- package/src/tui/component/dialog-theme-list.tsx +50 -0
- package/src/tui/component/dialog-variant.tsx +39 -0
- package/src/tui/component/dialog-workspace-create.tsx +302 -0
- package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
- package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
- package/src/tui/component/error-component.tsx +92 -0
- package/src/tui/component/logo.tsx +896 -0
- package/src/tui/component/plugin-route-missing.tsx +14 -0
- package/src/tui/component/prompt/autocomplete.tsx +869 -0
- package/src/tui/component/prompt/cwd.ts +0 -0
- package/src/tui/component/prompt/frecency.tsx +90 -0
- package/src/tui/component/prompt/history.tsx +108 -0
- package/src/tui/component/prompt/index.tsx +1809 -0
- package/src/tui/component/prompt/part.ts +16 -0
- package/src/tui/component/prompt/stash.tsx +101 -0
- package/src/tui/component/prompt/traits.ts +35 -0
- package/src/tui/component/spinner.tsx +24 -0
- package/src/tui/component/startup-loading.tsx +63 -0
- package/src/tui/component/todo-item.tsx +32 -0
- package/src/tui/component/use-connected.tsx +9 -0
- package/src/tui/component/workspace-label.tsx +19 -0
- package/src/tui/config/cwd.ts +5 -0
- package/src/tui/config/keybind.ts +432 -0
- package/src/tui/config/tui-migrate.ts +154 -0
- package/src/tui/config/tui-schema.ts +34 -0
- package/src/tui/config/tui.ts +46 -0
- package/src/tui/context/aggregate-failures.ts +34 -0
- package/src/tui/context/args.tsx +15 -0
- package/src/tui/context/command-palette.tsx +163 -0
- package/src/tui/context/directory.ts +15 -0
- package/src/tui/context/editor-zed.ts +283 -0
- package/src/tui/context/editor.ts +468 -0
- package/src/tui/context/event-apx.ts +22 -0
- package/src/tui/context/event.ts +6 -0
- package/src/tui/context/exit.tsx +60 -0
- package/src/tui/context/helper.tsx +25 -0
- package/src/tui/context/kv.tsx +81 -0
- package/src/tui/context/local.tsx +608 -0
- package/src/tui/context/path-format.tsx +39 -0
- package/src/tui/context/project-apx.tsx +48 -0
- package/src/tui/context/project.tsx +7 -0
- package/src/tui/context/prompt.tsx +18 -0
- package/src/tui/context/route.tsx +52 -0
- package/src/tui/context/sdk-apx.tsx +185 -0
- package/src/tui/context/sdk.tsx +6 -0
- package/src/tui/context/sync-apx.tsx +178 -0
- package/src/tui/context/sync-v2.tsx +16 -0
- package/src/tui/context/sync.tsx +118 -0
- package/src/tui/context/theme/aura.json +69 -0
- package/src/tui/context/theme/ayu.json +80 -0
- package/src/tui/context/theme/carbonfox.json +248 -0
- package/src/tui/context/theme/catppuccin-frappe.json +230 -0
- package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
- package/src/tui/context/theme/catppuccin.json +112 -0
- package/src/tui/context/theme/cobalt2.json +225 -0
- package/src/tui/context/theme/cursor.json +249 -0
- package/src/tui/context/theme/dracula.json +219 -0
- package/src/tui/context/theme/everforest.json +241 -0
- package/src/tui/context/theme/flexoki.json +237 -0
- package/src/tui/context/theme/github.json +233 -0
- package/src/tui/context/theme/gruvbox.json +242 -0
- package/src/tui/context/theme/kanagawa.json +77 -0
- package/src/tui/context/theme/lucent-orng.json +234 -0
- package/src/tui/context/theme/material.json +235 -0
- package/src/tui/context/theme/matrix.json +77 -0
- package/src/tui/context/theme/mercury.json +252 -0
- package/src/tui/context/theme/monokai.json +221 -0
- package/src/tui/context/theme/nightowl.json +221 -0
- package/src/tui/context/theme/nord.json +223 -0
- package/src/tui/context/theme/one-dark.json +84 -0
- package/src/tui/context/theme/opencode.json +245 -0
- package/src/tui/context/theme/orng.json +249 -0
- package/src/tui/context/theme/osaka-jade.json +93 -0
- package/src/tui/context/theme/palenight.json +222 -0
- package/src/tui/context/theme/rosepine.json +234 -0
- package/src/tui/context/theme/solarized.json +223 -0
- package/src/tui/context/theme/synthwave84.json +226 -0
- package/src/tui/context/theme/tokyonight.json +243 -0
- package/src/tui/context/theme/vercel.json +245 -0
- package/src/tui/context/theme/vesper.json +218 -0
- package/src/tui/context/theme/zenburn.json +223 -0
- package/src/tui/context/theme.tsx +1247 -0
- package/src/tui/context/tui-config.tsx +9 -0
- package/src/tui/event.ts +16 -0
- package/src/tui/feature-plugins/home/footer.tsx +94 -0
- package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
- package/src/tui/feature-plugins/home/tips.tsx +59 -0
- package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
- package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
- package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
- package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
- package/src/tui/feature-plugins/system/plugins.tsx +269 -0
- package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
- package/src/tui/feature-plugins/system/which-key.tsx +608 -0
- package/src/tui/keymap.tsx +166 -0
- package/src/tui/layer.ts +6 -0
- package/src/tui/plugin/api.tsx +381 -0
- package/src/tui/plugin/command-shim.ts +109 -0
- package/src/tui/plugin/internal.ts +33 -0
- package/src/tui/plugin/runtime.ts +1069 -0
- package/src/tui/plugin/slots.tsx +60 -0
- package/src/tui/routes/home.tsx +96 -0
- package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/tui/routes/session/dialog-message.tsx +108 -0
- package/src/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/tui/routes/session/footer.tsx +91 -0
- package/src/tui/routes/session/index.tsx +188 -0
- package/src/tui/routes/session/permission.tsx +722 -0
- package/src/tui/routes/session/question.tsx +490 -0
- package/src/tui/routes/session/sidebar.tsx +102 -0
- package/src/tui/routes/session/subagent-footer.tsx +133 -0
- package/src/tui/run.ts +84 -0
- package/src/tui/thread.ts +261 -0
- package/src/tui/tsconfig.json +40 -0
- package/src/tui/ui/dialog-alert.tsx +66 -0
- package/src/tui/ui/dialog-confirm.tsx +108 -0
- package/src/tui/ui/dialog-export-options.tsx +217 -0
- package/src/tui/ui/dialog-help.tsx +40 -0
- package/src/tui/ui/dialog-prompt.tsx +101 -0
- package/src/tui/ui/dialog-select.tsx +553 -0
- package/src/tui/ui/dialog.tsx +211 -0
- package/src/tui/ui/link.tsx +34 -0
- package/src/tui/ui/spinner.ts +368 -0
- package/src/tui/ui/toast.tsx +111 -0
- package/src/tui/util/clipboard.ts +217 -0
- package/src/tui/util/editor.ts +37 -0
- package/src/tui/util/model.ts +23 -0
- package/src/tui/util/provider-origin.ts +7 -0
- package/src/tui/util/revert-diff.ts +18 -0
- package/src/tui/util/scroll.ts +25 -0
- package/src/tui/util/selection.ts +65 -0
- package/src/tui/util/signal.ts +41 -0
- package/src/tui/util/sound.ts +156 -0
- package/src/tui/util/transcript.ts +112 -0
- package/src/tui/validate-session.ts +29 -0
- package/src/tui/win32.ts +130 -0
- package/src/tui/worker.ts +104 -0
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
import type { BoxRenderable, TextareaRenderable, ScrollBoxRenderable } from "@opentui/core"
|
|
2
|
+
import { pathToFileURL } from "bun"
|
|
3
|
+
import fuzzysort from "fuzzysort"
|
|
4
|
+
import path from "path"
|
|
5
|
+
import { firstBy } from "remeda"
|
|
6
|
+
import { createMemo, createResource, createEffect, onMount, onCleanup, Index, Show, createSignal } from "solid-js"
|
|
7
|
+
import { createStore } from "solid-js/store"
|
|
8
|
+
import { useEditorContext } from "@tui/context/editor"
|
|
9
|
+
import { useSDK } from "@tui/context/sdk"
|
|
10
|
+
import { useSync } from "@tui/context/sync"
|
|
11
|
+
import { getScrollAcceleration } from "../../util/scroll"
|
|
12
|
+
import { useTuiConfig } from "../../context/tui-config"
|
|
13
|
+
import { useTheme, selectedForeground } from "@tui/context/theme"
|
|
14
|
+
import { SplitBorder } from "@tui/component/border"
|
|
15
|
+
import { useCommandPalette } from "../../context/command-palette"
|
|
16
|
+
import { useTerminalDimensions } from "@opentui/solid"
|
|
17
|
+
import { Locale } from "@/util/locale"
|
|
18
|
+
import type { PromptInfo } from "./history"
|
|
19
|
+
import { useFrecency } from "./frecency"
|
|
20
|
+
import { useBindings } from "../../keymap"
|
|
21
|
+
import { Reference } from "@/reference/reference"
|
|
22
|
+
import type { Config } from "@/config/config"
|
|
23
|
+
import { displayCharAt, mentionTriggerIndex } from "@/cli/cmd/prompt-display"
|
|
24
|
+
|
|
25
|
+
function removeLineRange(input: string) {
|
|
26
|
+
const hashIndex = input.lastIndexOf("#")
|
|
27
|
+
return hashIndex !== -1 ? input.substring(0, hashIndex) : input
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function extractLineRange(input: string) {
|
|
31
|
+
const hashIndex = input.lastIndexOf("#")
|
|
32
|
+
if (hashIndex === -1) {
|
|
33
|
+
return { baseQuery: input }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const baseName = input.substring(0, hashIndex)
|
|
37
|
+
const linePart = input.substring(hashIndex + 1)
|
|
38
|
+
const lineMatch = linePart.match(/^(\d+)(?:-(\d*))?$/)
|
|
39
|
+
|
|
40
|
+
if (!lineMatch) {
|
|
41
|
+
return { baseQuery: baseName }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const startLine = Number(lineMatch[1])
|
|
45
|
+
const endLine = lineMatch[2] && startLine < Number(lineMatch[2]) ? Number(lineMatch[2]) : undefined
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
lineRange: {
|
|
49
|
+
baseName,
|
|
50
|
+
startLine,
|
|
51
|
+
endLine,
|
|
52
|
+
},
|
|
53
|
+
baseQuery: baseName,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type AutocompleteRef = {
|
|
58
|
+
onInput: (value: string) => void
|
|
59
|
+
visible: false | "@" | "/"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type AutocompleteOption = {
|
|
63
|
+
display: string
|
|
64
|
+
value?: string
|
|
65
|
+
aliases?: string[]
|
|
66
|
+
disabled?: boolean
|
|
67
|
+
description?: string
|
|
68
|
+
isDirectory?: boolean
|
|
69
|
+
onSelect?: () => void
|
|
70
|
+
path?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function Autocomplete(props: {
|
|
74
|
+
value: string
|
|
75
|
+
sessionID?: string
|
|
76
|
+
setPrompt: (input: (prompt: PromptInfo) => void) => void
|
|
77
|
+
setExtmark: (partIndex: number, extmarkId: number) => void
|
|
78
|
+
anchor: () => BoxRenderable
|
|
79
|
+
input: () => TextareaRenderable
|
|
80
|
+
ref: (ref: AutocompleteRef) => void
|
|
81
|
+
fileStyleId: number
|
|
82
|
+
agentStyleId: number
|
|
83
|
+
promptPartTypeId: () => number
|
|
84
|
+
}) {
|
|
85
|
+
const editor = useEditorContext()
|
|
86
|
+
const sdk = useSDK()
|
|
87
|
+
const sync = useSync()
|
|
88
|
+
const command = useCommandPalette()
|
|
89
|
+
const { theme } = useTheme()
|
|
90
|
+
const dimensions = useTerminalDimensions()
|
|
91
|
+
const frecency = useFrecency()
|
|
92
|
+
const tuiConfig = useTuiConfig()
|
|
93
|
+
const [store, setStore] = createStore({
|
|
94
|
+
index: 0,
|
|
95
|
+
selected: 0,
|
|
96
|
+
visible: false as AutocompleteRef["visible"],
|
|
97
|
+
input: "keyboard" as "keyboard" | "mouse",
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const [positionTick, setPositionTick] = createSignal(0)
|
|
101
|
+
|
|
102
|
+
createEffect(() => {
|
|
103
|
+
if (store.visible) {
|
|
104
|
+
let lastPos = { x: 0, y: 0, width: 0 }
|
|
105
|
+
const interval = setInterval(() => {
|
|
106
|
+
const anchor = props.anchor()
|
|
107
|
+
if (anchor.x !== lastPos.x || anchor.y !== lastPos.y || anchor.width !== lastPos.width) {
|
|
108
|
+
lastPos = { x: anchor.x, y: anchor.y, width: anchor.width }
|
|
109
|
+
setPositionTick((t) => t + 1)
|
|
110
|
+
}
|
|
111
|
+
}, 50)
|
|
112
|
+
|
|
113
|
+
onCleanup(() => clearInterval(interval))
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const position = createMemo(() => {
|
|
118
|
+
if (!store.visible) return { x: 0, y: 0, width: 0 }
|
|
119
|
+
dimensions()
|
|
120
|
+
positionTick()
|
|
121
|
+
const anchor = props.anchor()
|
|
122
|
+
const parent = anchor.parent
|
|
123
|
+
const parentX = parent?.x ?? 0
|
|
124
|
+
const parentY = parent?.y ?? 0
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
x: anchor.x - parentX,
|
|
128
|
+
y: anchor.y - parentY,
|
|
129
|
+
width: anchor.width,
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const filter = createMemo(() => {
|
|
134
|
+
if (!store.visible) return
|
|
135
|
+
// Track props.value to make memo reactive to text changes
|
|
136
|
+
props.value // <- there surely is a better way to do this, like making .input() reactive
|
|
137
|
+
|
|
138
|
+
return props.input().getTextRange(store.index + 1, props.input().cursorOffset)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// filter() reads reactive props.value plus non-reactive cursor/text state.
|
|
142
|
+
// On keypress those can be briefly out of sync, so filter() may return an empty/partial string.
|
|
143
|
+
// Copy it into search in an effect because effects run after reactive updates have been rendered and painted
|
|
144
|
+
// so the input has settled and all consumers read the same stable value.
|
|
145
|
+
const [search, setSearch] = createSignal("")
|
|
146
|
+
createEffect(() => {
|
|
147
|
+
const next = filter()
|
|
148
|
+
setSearch(next ? next : "")
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// When the filter changes due to how TUI works, the mousemove might still be triggered
|
|
152
|
+
// via a synthetic event as the layout moves underneath the cursor. This is a workaround to make sure the input mode remains keyboard so
|
|
153
|
+
// that the mouseover event doesn't trigger when filtering.
|
|
154
|
+
createEffect(() => {
|
|
155
|
+
filter()
|
|
156
|
+
setStore("input", "keyboard")
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
function insertPart(text: string, part: PromptInfo["parts"][number]) {
|
|
160
|
+
const input = props.input()
|
|
161
|
+
const currentCursorOffset = input.cursorOffset
|
|
162
|
+
|
|
163
|
+
const charAfterCursor = displayCharAt(props.value, currentCursorOffset)
|
|
164
|
+
const needsSpace = charAfterCursor !== " "
|
|
165
|
+
const append = "@" + text + (needsSpace ? " " : "")
|
|
166
|
+
|
|
167
|
+
input.cursorOffset = store.index
|
|
168
|
+
const startCursor = input.logicalCursor
|
|
169
|
+
input.cursorOffset = currentCursorOffset
|
|
170
|
+
const endCursor = input.logicalCursor
|
|
171
|
+
|
|
172
|
+
input.deleteRange(startCursor.row, startCursor.col, endCursor.row, endCursor.col)
|
|
173
|
+
input.insertText(append)
|
|
174
|
+
|
|
175
|
+
const virtualText = "@" + text
|
|
176
|
+
const extmarkStart = store.index
|
|
177
|
+
const extmarkEnd = extmarkStart + Bun.stringWidth(virtualText)
|
|
178
|
+
|
|
179
|
+
const styleId = part.type === "file" ? props.fileStyleId : part.type === "agent" ? props.agentStyleId : undefined
|
|
180
|
+
|
|
181
|
+
const extmarkId = input.extmarks.create({
|
|
182
|
+
start: extmarkStart,
|
|
183
|
+
end: extmarkEnd,
|
|
184
|
+
virtual: true,
|
|
185
|
+
styleId,
|
|
186
|
+
typeId: props.promptPartTypeId(),
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
props.setPrompt((draft) => {
|
|
190
|
+
if (part.type === "file") {
|
|
191
|
+
const existingIndex = draft.parts.findIndex((p) => p.type === "file" && "url" in p && p.url === part.url)
|
|
192
|
+
if (existingIndex !== -1) {
|
|
193
|
+
const existing = draft.parts[existingIndex]
|
|
194
|
+
if (
|
|
195
|
+
part.source?.text &&
|
|
196
|
+
existing &&
|
|
197
|
+
"source" in existing &&
|
|
198
|
+
existing.source &&
|
|
199
|
+
"text" in existing.source &&
|
|
200
|
+
existing.source.text
|
|
201
|
+
) {
|
|
202
|
+
existing.source.text.start = extmarkStart
|
|
203
|
+
existing.source.text.end = extmarkEnd
|
|
204
|
+
existing.source.text.value = virtualText
|
|
205
|
+
}
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (part.type === "file" && part.source?.text) {
|
|
211
|
+
part.source.text.start = extmarkStart
|
|
212
|
+
part.source.text.end = extmarkEnd
|
|
213
|
+
part.source.text.value = virtualText
|
|
214
|
+
} else if (part.type === "agent" && part.source) {
|
|
215
|
+
part.source.start = extmarkStart
|
|
216
|
+
part.source.end = extmarkEnd
|
|
217
|
+
part.source.value = virtualText
|
|
218
|
+
}
|
|
219
|
+
const partIndex = draft.parts.length
|
|
220
|
+
draft.parts.push(part)
|
|
221
|
+
props.setExtmark(partIndex, extmarkId)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
if (part.type === "file" && part.source && part.source.type === "file") {
|
|
225
|
+
frecency.updateFrecency(part.source.path)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function createFilePart(item: string, lineRange?: { startLine: number; endLine?: number }) {
|
|
230
|
+
const baseDir = (sync.path.directory || process.cwd()).replace(/\/+$/, "")
|
|
231
|
+
const fullPath = path.isAbsolute(item) ? item : path.join(baseDir, item)
|
|
232
|
+
const urlObj = pathToFileURL(fullPath)
|
|
233
|
+
const filename =
|
|
234
|
+
lineRange && !item.endsWith("/")
|
|
235
|
+
? `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}`
|
|
236
|
+
: item
|
|
237
|
+
|
|
238
|
+
if (lineRange && !item.endsWith("/")) {
|
|
239
|
+
urlObj.searchParams.set("start", String(lineRange.startLine))
|
|
240
|
+
if (lineRange.endLine !== undefined) {
|
|
241
|
+
urlObj.searchParams.set("end", String(lineRange.endLine))
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
filename,
|
|
247
|
+
url: urlObj.href,
|
|
248
|
+
part: {
|
|
249
|
+
type: "file" as const,
|
|
250
|
+
mime: "text/plain",
|
|
251
|
+
filename,
|
|
252
|
+
url: urlObj.href,
|
|
253
|
+
source: {
|
|
254
|
+
type: "file" as const,
|
|
255
|
+
text: {
|
|
256
|
+
start: 0,
|
|
257
|
+
end: 0,
|
|
258
|
+
value: "",
|
|
259
|
+
},
|
|
260
|
+
path: item,
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function createReferenceFilePart(input: {
|
|
267
|
+
alias: string
|
|
268
|
+
root: string
|
|
269
|
+
item: string
|
|
270
|
+
lineRange?: { startLine: number; endLine?: number }
|
|
271
|
+
}) {
|
|
272
|
+
const filename = `${input.alias}/${
|
|
273
|
+
input.lineRange && !input.item.endsWith("/")
|
|
274
|
+
? `${input.item}#${input.lineRange.startLine}${input.lineRange.endLine ? `-${input.lineRange.endLine}` : ""}`
|
|
275
|
+
: input.item
|
|
276
|
+
}`
|
|
277
|
+
const urlObj = pathToFileURL(path.join(input.root, input.item))
|
|
278
|
+
|
|
279
|
+
if (input.lineRange && !input.item.endsWith("/")) {
|
|
280
|
+
urlObj.searchParams.set("start", String(input.lineRange.startLine))
|
|
281
|
+
if (input.lineRange.endLine !== undefined) {
|
|
282
|
+
urlObj.searchParams.set("end", String(input.lineRange.endLine))
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
filename,
|
|
288
|
+
url: urlObj.href,
|
|
289
|
+
part: {
|
|
290
|
+
type: "file" as const,
|
|
291
|
+
mime: input.item.endsWith("/") ? "application/x-directory" : "text/plain",
|
|
292
|
+
filename,
|
|
293
|
+
url: urlObj.href,
|
|
294
|
+
source: {
|
|
295
|
+
type: "file" as const,
|
|
296
|
+
text: {
|
|
297
|
+
start: 0,
|
|
298
|
+
end: 0,
|
|
299
|
+
value: "",
|
|
300
|
+
},
|
|
301
|
+
path: filename,
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function referencePromptText(reference: Reference.Resolved) {
|
|
308
|
+
const problem = reference.kind === "invalid" ? reference.message : undefined
|
|
309
|
+
return [
|
|
310
|
+
`Referenced configured reference @${reference.name}.`,
|
|
311
|
+
...(reference.kind === "local" ? ["Kind: local directory"] : []),
|
|
312
|
+
...(reference.kind === "git" ? ["Kind: git repository"] : []),
|
|
313
|
+
...(reference.kind === "invalid" ? [`Repository: ${reference.repository}`] : []),
|
|
314
|
+
...(reference.kind === "git" ? [`Repository: ${reference.repository}`] : []),
|
|
315
|
+
...(reference.kind === "git" && reference.branch ? [`Branch/ref: ${reference.branch}`] : []),
|
|
316
|
+
...(reference.kind === "invalid" ? [] : [`Reference root: ${reference.path}`]),
|
|
317
|
+
...(problem
|
|
318
|
+
? [`Problem: ${problem}`]
|
|
319
|
+
: [
|
|
320
|
+
"For targeted context, inspect the reference path directly with Read, Glob, and Grep. For broader research, call the task tool with subagent scout and include this reference path.",
|
|
321
|
+
]),
|
|
322
|
+
].join("\n")
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const references = createMemo(() =>
|
|
326
|
+
Reference.resolveAll({
|
|
327
|
+
references: (sync.data.config.reference ?? {}) as NonNullable<Config.Info["reference"]>,
|
|
328
|
+
directory: sync.path.directory || process.cwd(),
|
|
329
|
+
worktree: sync.path.worktree || sync.path.directory || process.cwd(),
|
|
330
|
+
}),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
const referenceSearch = createMemo(() => {
|
|
334
|
+
if (!store.visible || store.visible === "/") return
|
|
335
|
+
const { lineRange, baseQuery } = extractLineRange(search())
|
|
336
|
+
const slash = baseQuery.indexOf("/")
|
|
337
|
+
if (slash === -1) return
|
|
338
|
+
const reference = references().find((item) => item.name === baseQuery.slice(0, slash))
|
|
339
|
+
if (!reference || reference.kind === "invalid") return
|
|
340
|
+
return {
|
|
341
|
+
reference,
|
|
342
|
+
query: baseQuery.slice(slash + 1),
|
|
343
|
+
lineRange,
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
function normalizeMentionPath(filePath: string) {
|
|
348
|
+
const baseDir = sync.path.directory || process.cwd()
|
|
349
|
+
const absolute = path.resolve(filePath)
|
|
350
|
+
const relative = path.relative(baseDir, absolute)
|
|
351
|
+
|
|
352
|
+
if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
353
|
+
return relative.split(path.sep).join("/")
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return absolute.split(path.sep).join("/")
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function insertFileMention(input: { filePath: string; lineStart: number; lineEnd: number }) {
|
|
360
|
+
const item = normalizeMentionPath(input.filePath)
|
|
361
|
+
const lineRange = {
|
|
362
|
+
startLine: input.lineStart,
|
|
363
|
+
endLine: input.lineEnd > input.lineStart ? input.lineEnd : undefined,
|
|
364
|
+
}
|
|
365
|
+
const { filename, part } = createFilePart(item, lineRange)
|
|
366
|
+
const index = store.visible === "@" ? store.index : props.input().cursorOffset
|
|
367
|
+
|
|
368
|
+
command.suspend(false)
|
|
369
|
+
setStore("visible", false)
|
|
370
|
+
setStore("index", index)
|
|
371
|
+
insertPart(filename, part)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const [files] = createResource(
|
|
375
|
+
() => search(),
|
|
376
|
+
async (query) => {
|
|
377
|
+
if (!store.visible || store.visible === "/") return []
|
|
378
|
+
if (referenceSearch()) return []
|
|
379
|
+
|
|
380
|
+
const { lineRange, baseQuery } = extractLineRange(query ?? "")
|
|
381
|
+
|
|
382
|
+
// Get files from SDK
|
|
383
|
+
const result = await sdk.client.find.files({
|
|
384
|
+
query: baseQuery,
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
const options: AutocompleteOption[] = []
|
|
388
|
+
|
|
389
|
+
// Add file options
|
|
390
|
+
if (!result.error && result.data) {
|
|
391
|
+
const sortedFiles = result.data.sort((a, b) => {
|
|
392
|
+
const aScore = frecency.getFrecency(a)
|
|
393
|
+
const bScore = frecency.getFrecency(b)
|
|
394
|
+
if (aScore !== bScore) return bScore - aScore
|
|
395
|
+
const aDepth = a.split("/").length
|
|
396
|
+
const bDepth = b.split("/").length
|
|
397
|
+
if (aDepth !== bDepth) return aDepth - bDepth
|
|
398
|
+
return a.localeCompare(b)
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
const width = props.anchor().width - 4
|
|
402
|
+
options.push(
|
|
403
|
+
...sortedFiles.map((item): AutocompleteOption => {
|
|
404
|
+
const { filename, url, part } = createFilePart(item, lineRange)
|
|
405
|
+
|
|
406
|
+
const isDir = item.endsWith("/")
|
|
407
|
+
return {
|
|
408
|
+
display: Locale.truncateMiddle(filename, width),
|
|
409
|
+
value: filename,
|
|
410
|
+
isDirectory: isDir,
|
|
411
|
+
path: item,
|
|
412
|
+
onSelect: () => {
|
|
413
|
+
insertPart(filename, part)
|
|
414
|
+
},
|
|
415
|
+
}
|
|
416
|
+
}),
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return options
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
initialValue: [],
|
|
424
|
+
},
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
const [referenceFiles] = createResource(
|
|
428
|
+
() => referenceSearch(),
|
|
429
|
+
async (match) => {
|
|
430
|
+
if (!match) return []
|
|
431
|
+
|
|
432
|
+
const result = await sdk.client.find.files({
|
|
433
|
+
directory: match.reference.path,
|
|
434
|
+
query: match.query,
|
|
435
|
+
limit: 50,
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
if (result.error || !result.data) return []
|
|
439
|
+
|
|
440
|
+
const width = props.anchor().width - 4
|
|
441
|
+
return result.data.map((item): AutocompleteOption => {
|
|
442
|
+
const { filename, part } = createReferenceFilePart({
|
|
443
|
+
alias: match.reference.name,
|
|
444
|
+
root: match.reference.path,
|
|
445
|
+
item,
|
|
446
|
+
lineRange: match.lineRange,
|
|
447
|
+
})
|
|
448
|
+
return {
|
|
449
|
+
display: Locale.truncateMiddle(filename, width),
|
|
450
|
+
value: filename,
|
|
451
|
+
isDirectory: item.endsWith("/"),
|
|
452
|
+
path: filename,
|
|
453
|
+
onSelect: () => {
|
|
454
|
+
insertPart(filename, part)
|
|
455
|
+
},
|
|
456
|
+
}
|
|
457
|
+
})
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
initialValue: [],
|
|
461
|
+
},
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
const mcpResources = createMemo(() => {
|
|
465
|
+
if (!store.visible || store.visible === "/") return []
|
|
466
|
+
|
|
467
|
+
const options: AutocompleteOption[] = []
|
|
468
|
+
const width = props.anchor().width - 4
|
|
469
|
+
|
|
470
|
+
for (const res of Object.values(sync.data.mcp_resource)) {
|
|
471
|
+
const text = `${res.name} (${res.uri})`
|
|
472
|
+
options.push({
|
|
473
|
+
display: Locale.truncateMiddle(text, width),
|
|
474
|
+
value: text,
|
|
475
|
+
description: res.description,
|
|
476
|
+
onSelect: () => {
|
|
477
|
+
insertPart(res.name, {
|
|
478
|
+
type: "file",
|
|
479
|
+
mime: res.mimeType ?? "text/plain",
|
|
480
|
+
filename: res.name,
|
|
481
|
+
url: res.uri,
|
|
482
|
+
source: {
|
|
483
|
+
type: "resource",
|
|
484
|
+
text: {
|
|
485
|
+
start: 0,
|
|
486
|
+
end: 0,
|
|
487
|
+
value: "",
|
|
488
|
+
},
|
|
489
|
+
clientName: res.client,
|
|
490
|
+
uri: res.uri,
|
|
491
|
+
},
|
|
492
|
+
})
|
|
493
|
+
},
|
|
494
|
+
})
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return options
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
const agents = createMemo(() => {
|
|
501
|
+
const agents = sync.data.agent
|
|
502
|
+
return agents
|
|
503
|
+
.filter((agent) => !agent.hidden && agent.mode !== "primary")
|
|
504
|
+
.map(
|
|
505
|
+
(agent): AutocompleteOption => ({
|
|
506
|
+
display: "@" + agent.name,
|
|
507
|
+
onSelect: () => {
|
|
508
|
+
insertPart(agent.name, {
|
|
509
|
+
type: "agent",
|
|
510
|
+
name: agent.name,
|
|
511
|
+
source: {
|
|
512
|
+
start: 0,
|
|
513
|
+
end: 0,
|
|
514
|
+
value: "",
|
|
515
|
+
},
|
|
516
|
+
})
|
|
517
|
+
},
|
|
518
|
+
}),
|
|
519
|
+
)
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
const referenceAliases = createMemo(() =>
|
|
523
|
+
references().map(
|
|
524
|
+
(reference): AutocompleteOption => ({
|
|
525
|
+
display: "@" + reference.name,
|
|
526
|
+
description: reference.kind === "invalid" ? reference.message : " configured reference",
|
|
527
|
+
onSelect: () => {
|
|
528
|
+
insertPart(reference.name, {
|
|
529
|
+
type: "text",
|
|
530
|
+
text: referencePromptText(reference),
|
|
531
|
+
synthetic: true,
|
|
532
|
+
})
|
|
533
|
+
},
|
|
534
|
+
}),
|
|
535
|
+
),
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
const commands = createMemo((): AutocompleteOption[] => {
|
|
539
|
+
const results: AutocompleteOption[] = [...command.slashes()]
|
|
540
|
+
|
|
541
|
+
for (const serverCommand of sync.data.command) {
|
|
542
|
+
if (serverCommand.source === "skill") continue
|
|
543
|
+
const label = serverCommand.source === "mcp" ? ":mcp" : ""
|
|
544
|
+
results.push({
|
|
545
|
+
display: "/" + serverCommand.name + label,
|
|
546
|
+
description: serverCommand.description,
|
|
547
|
+
onSelect: () => {
|
|
548
|
+
const newText = "/" + serverCommand.name + " "
|
|
549
|
+
const cursor = props.input().logicalCursor
|
|
550
|
+
props.input().deleteRange(0, 0, cursor.row, cursor.col)
|
|
551
|
+
props.input().insertText(newText)
|
|
552
|
+
props.input().cursorOffset = Bun.stringWidth(newText)
|
|
553
|
+
},
|
|
554
|
+
})
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
results.sort((a, b) => a.display.localeCompare(b.display))
|
|
558
|
+
|
|
559
|
+
const max = firstBy(results, [(x) => x.display.length, "desc"])?.display.length
|
|
560
|
+
if (!max) return results
|
|
561
|
+
return results.map((item) => ({
|
|
562
|
+
...item,
|
|
563
|
+
display: item.display.padEnd(max + 2),
|
|
564
|
+
}))
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
const options = createMemo((prev: AutocompleteOption[] | undefined) => {
|
|
568
|
+
const filesValue = files()
|
|
569
|
+
const referenceFilesValue = referenceFiles()
|
|
570
|
+
const referenceSearchValue = referenceSearch()
|
|
571
|
+
const agentsValue = agents()
|
|
572
|
+
const referenceAliasesValue = referenceAliases()
|
|
573
|
+
const commandsValue = commands()
|
|
574
|
+
|
|
575
|
+
const mixed: AutocompleteOption[] =
|
|
576
|
+
store.visible === "@"
|
|
577
|
+
? referenceSearchValue
|
|
578
|
+
? referenceFilesValue || []
|
|
579
|
+
: [...referenceAliasesValue, ...agentsValue, ...(filesValue || []), ...mcpResources()]
|
|
580
|
+
: [...commandsValue]
|
|
581
|
+
|
|
582
|
+
const searchValue = search()
|
|
583
|
+
|
|
584
|
+
if (!searchValue) {
|
|
585
|
+
return mixed
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if ((files.loading || referenceFiles.loading) && prev && prev.length > 0) {
|
|
589
|
+
return prev
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const result = fuzzysort.go(removeLineRange(searchValue), mixed, {
|
|
593
|
+
keys: [
|
|
594
|
+
(obj) => removeLineRange((obj.value ?? obj.display).trimEnd()),
|
|
595
|
+
"description",
|
|
596
|
+
(obj) => obj.aliases?.join(" ") ?? "",
|
|
597
|
+
],
|
|
598
|
+
limit: 10,
|
|
599
|
+
scoreFn: (objResults) => {
|
|
600
|
+
const displayResult = objResults[0]
|
|
601
|
+
let score = objResults.score
|
|
602
|
+
if (displayResult && displayResult.target.startsWith(store.visible + searchValue)) {
|
|
603
|
+
score *= 2
|
|
604
|
+
}
|
|
605
|
+
const frecencyScore = objResults.obj.path ? frecency.getFrecency(objResults.obj.path) : 0
|
|
606
|
+
return score * (1 + frecencyScore)
|
|
607
|
+
},
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
return result.map((arr) => arr.obj)
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
createEffect(() => {
|
|
614
|
+
filter()
|
|
615
|
+
setStore("selected", 0)
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
function move(direction: -1 | 1) {
|
|
619
|
+
if (!store.visible) return
|
|
620
|
+
if (!options().length) return
|
|
621
|
+
let next = store.selected + direction
|
|
622
|
+
if (next < 0) next = options().length - 1
|
|
623
|
+
if (next >= options().length) next = 0
|
|
624
|
+
moveTo(next)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function moveTo(next: number) {
|
|
628
|
+
setStore("selected", next)
|
|
629
|
+
if (!scroll) return
|
|
630
|
+
const viewportHeight = Math.min(height(), options().length)
|
|
631
|
+
const scrollBottom = scroll.scrollTop + viewportHeight
|
|
632
|
+
if (next < scroll.scrollTop) {
|
|
633
|
+
scroll.scrollBy(next - scroll.scrollTop)
|
|
634
|
+
} else if (next + 1 > scrollBottom) {
|
|
635
|
+
scroll.scrollBy(next + 1 - scrollBottom)
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function select() {
|
|
640
|
+
const selected = options()[store.selected]
|
|
641
|
+
if (!selected) return
|
|
642
|
+
hide()
|
|
643
|
+
selected.onSelect?.()
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function expandDirectory() {
|
|
647
|
+
const selected = options()[store.selected]
|
|
648
|
+
if (!selected) return
|
|
649
|
+
|
|
650
|
+
const input = props.input()
|
|
651
|
+
const currentCursorOffset = input.cursorOffset
|
|
652
|
+
|
|
653
|
+
const displayText = (selected.value ?? selected.display).trimEnd()
|
|
654
|
+
const path = displayText.startsWith("@") ? displayText.slice(1) : displayText
|
|
655
|
+
|
|
656
|
+
input.cursorOffset = store.index
|
|
657
|
+
const startCursor = input.logicalCursor
|
|
658
|
+
input.cursorOffset = currentCursorOffset
|
|
659
|
+
const endCursor = input.logicalCursor
|
|
660
|
+
|
|
661
|
+
input.deleteRange(startCursor.row, startCursor.col, endCursor.row, endCursor.col)
|
|
662
|
+
input.insertText("@" + path)
|
|
663
|
+
|
|
664
|
+
setStore("selected", 0)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
useBindings(() => ({
|
|
668
|
+
target: props.input,
|
|
669
|
+
enabled: () => Boolean(store.visible),
|
|
670
|
+
commands: [
|
|
671
|
+
{
|
|
672
|
+
name: "prompt.autocomplete.prev",
|
|
673
|
+
title: "Previous autocomplete item",
|
|
674
|
+
category: "Autocomplete",
|
|
675
|
+
run() {
|
|
676
|
+
setStore("input", "keyboard")
|
|
677
|
+
move(-1)
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: "prompt.autocomplete.next",
|
|
682
|
+
title: "Next autocomplete item",
|
|
683
|
+
category: "Autocomplete",
|
|
684
|
+
run() {
|
|
685
|
+
setStore("input", "keyboard")
|
|
686
|
+
move(1)
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
name: "prompt.autocomplete.hide",
|
|
691
|
+
title: "Hide autocomplete",
|
|
692
|
+
category: "Autocomplete",
|
|
693
|
+
run() {
|
|
694
|
+
hide()
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
name: "prompt.autocomplete.select",
|
|
699
|
+
title: "Select autocomplete item",
|
|
700
|
+
category: "Autocomplete",
|
|
701
|
+
run() {
|
|
702
|
+
select()
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
name: "prompt.autocomplete.complete",
|
|
707
|
+
title: "Complete autocomplete item",
|
|
708
|
+
category: "Autocomplete",
|
|
709
|
+
run() {
|
|
710
|
+
const selected = options()[store.selected]
|
|
711
|
+
if (selected?.isDirectory) {
|
|
712
|
+
expandDirectory()
|
|
713
|
+
return
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
select()
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
],
|
|
720
|
+
bindings: tuiConfig.keybinds.gather("prompt.autocomplete", [
|
|
721
|
+
"prompt.autocomplete.prev",
|
|
722
|
+
"prompt.autocomplete.next",
|
|
723
|
+
"prompt.autocomplete.hide",
|
|
724
|
+
"prompt.autocomplete.select",
|
|
725
|
+
"prompt.autocomplete.complete",
|
|
726
|
+
]),
|
|
727
|
+
}))
|
|
728
|
+
|
|
729
|
+
function show(mode: "@" | "/") {
|
|
730
|
+
command.suspend(true)
|
|
731
|
+
setStore({
|
|
732
|
+
visible: mode,
|
|
733
|
+
index: props.input().cursorOffset,
|
|
734
|
+
})
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function hide() {
|
|
738
|
+
const text = props.input().plainText
|
|
739
|
+
if (store.visible === "/" && !text.endsWith(" ") && text.startsWith("/")) {
|
|
740
|
+
const cursor = props.input().logicalCursor
|
|
741
|
+
props.input().deleteRange(0, 0, cursor.row, cursor.col)
|
|
742
|
+
// Sync the prompt store immediately since onContentChange is async
|
|
743
|
+
props.setPrompt((draft) => {
|
|
744
|
+
draft.input = props.input().plainText
|
|
745
|
+
})
|
|
746
|
+
}
|
|
747
|
+
command.suspend(false)
|
|
748
|
+
setStore("visible", false)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
onMount(() => {
|
|
752
|
+
const unsubscribeMention = editor.onMention((mention) => {
|
|
753
|
+
insertFileMention(mention)
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
onCleanup(() => {
|
|
757
|
+
unsubscribeMention()
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
props.ref({
|
|
761
|
+
get visible() {
|
|
762
|
+
return store.visible
|
|
763
|
+
},
|
|
764
|
+
onInput(value) {
|
|
765
|
+
if (store.visible) {
|
|
766
|
+
if (
|
|
767
|
+
// Typed text before the trigger
|
|
768
|
+
props.input().cursorOffset <= store.index ||
|
|
769
|
+
// There is a space between the trigger and the cursor
|
|
770
|
+
props.input().getTextRange(store.index, props.input().cursorOffset).match(/\s/) ||
|
|
771
|
+
// "/<command>" is not the sole content
|
|
772
|
+
(store.visible === "/" && value.match(/^\S+\s+\S+\s*$/))
|
|
773
|
+
) {
|
|
774
|
+
hide()
|
|
775
|
+
}
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Check if autocomplete should reopen (e.g., after backspace deleted a space)
|
|
780
|
+
const offset = props.input().cursorOffset
|
|
781
|
+
if (offset === 0) return
|
|
782
|
+
|
|
783
|
+
// Check for "/" at position 0 - reopen slash commands
|
|
784
|
+
if (value.startsWith("/") && !value.slice(0, offset).match(/\s/)) {
|
|
785
|
+
show("/")
|
|
786
|
+
setStore("index", 0)
|
|
787
|
+
return
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Check for "@" trigger - find the nearest "@" before cursor with no whitespace between
|
|
791
|
+
const idx = mentionTriggerIndex(value, offset)
|
|
792
|
+
if (idx !== undefined) {
|
|
793
|
+
show("@")
|
|
794
|
+
setStore("index", idx)
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
})
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
const height = createMemo(() => {
|
|
801
|
+
const count = options().length || 1
|
|
802
|
+
if (!store.visible) return Math.min(10, count)
|
|
803
|
+
positionTick()
|
|
804
|
+
return Math.min(10, count, Math.max(1, props.anchor().y))
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
let scroll: ScrollBoxRenderable
|
|
808
|
+
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
|
|
809
|
+
|
|
810
|
+
return (
|
|
811
|
+
<box
|
|
812
|
+
visible={store.visible !== false}
|
|
813
|
+
position="absolute"
|
|
814
|
+
top={position().y - height()}
|
|
815
|
+
left={position().x}
|
|
816
|
+
width={position().width}
|
|
817
|
+
zIndex={100}
|
|
818
|
+
{...SplitBorder}
|
|
819
|
+
borderColor={theme.border}
|
|
820
|
+
>
|
|
821
|
+
<scrollbox
|
|
822
|
+
ref={(r: ScrollBoxRenderable) => (scroll = r)}
|
|
823
|
+
backgroundColor={theme.backgroundMenu}
|
|
824
|
+
height={height()}
|
|
825
|
+
scrollbarOptions={{ visible: false }}
|
|
826
|
+
scrollAcceleration={scrollAcceleration()}
|
|
827
|
+
>
|
|
828
|
+
<Index
|
|
829
|
+
each={options()}
|
|
830
|
+
fallback={
|
|
831
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
832
|
+
<text fg={theme.textMuted}>No matching items</text>
|
|
833
|
+
</box>
|
|
834
|
+
}
|
|
835
|
+
>
|
|
836
|
+
{(option, index) => (
|
|
837
|
+
<box
|
|
838
|
+
paddingLeft={1}
|
|
839
|
+
paddingRight={1}
|
|
840
|
+
backgroundColor={index === store.selected ? theme.primary : undefined}
|
|
841
|
+
flexDirection="row"
|
|
842
|
+
onMouseMove={() => {
|
|
843
|
+
setStore("input", "mouse")
|
|
844
|
+
}}
|
|
845
|
+
onMouseOver={() => {
|
|
846
|
+
if (store.input !== "mouse") return
|
|
847
|
+
moveTo(index)
|
|
848
|
+
}}
|
|
849
|
+
onMouseDown={() => {
|
|
850
|
+
setStore("input", "mouse")
|
|
851
|
+
moveTo(index)
|
|
852
|
+
}}
|
|
853
|
+
onMouseUp={() => select()}
|
|
854
|
+
>
|
|
855
|
+
<text fg={index === store.selected ? selectedForeground(theme) : theme.text} flexShrink={0}>
|
|
856
|
+
{option().display}
|
|
857
|
+
</text>
|
|
858
|
+
<Show when={option().description}>
|
|
859
|
+
<text fg={index === store.selected ? selectedForeground(theme) : theme.textMuted} wrapMode="none">
|
|
860
|
+
{option().description}
|
|
861
|
+
</text>
|
|
862
|
+
</Show>
|
|
863
|
+
</box>
|
|
864
|
+
)}
|
|
865
|
+
</Index>
|
|
866
|
+
</scrollbox>
|
|
867
|
+
</box>
|
|
868
|
+
)
|
|
869
|
+
}
|