@agentprojectcontext/apx 1.15.6 → 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/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,1809 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BoxRenderable,
|
|
3
|
+
RGBA,
|
|
4
|
+
TextareaRenderable,
|
|
5
|
+
MouseEvent,
|
|
6
|
+
PasteEvent,
|
|
7
|
+
decodePasteBytes,
|
|
8
|
+
type KeyEvent,
|
|
9
|
+
type Renderable,
|
|
10
|
+
} from "@opentui/core"
|
|
11
|
+
import type { CommandContext } from "@opentui/keymap"
|
|
12
|
+
import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js"
|
|
13
|
+
import "opentui-spinner/solid"
|
|
14
|
+
import path from "path"
|
|
15
|
+
import { fileURLToPath } from "url"
|
|
16
|
+
import { Filesystem } from "@/util/filesystem"
|
|
17
|
+
import { useLocal } from "@tui/context/local"
|
|
18
|
+
import { tint, useTheme } from "@tui/context/theme"
|
|
19
|
+
import { EmptyBorder, SplitBorder } from "@tui/component/border"
|
|
20
|
+
import { Spinner } from "@tui/component/spinner"
|
|
21
|
+
import { useSDK } from "@tui/context/sdk"
|
|
22
|
+
import { useRoute } from "@tui/context/route"
|
|
23
|
+
import { useProject } from "@tui/context/project"
|
|
24
|
+
import { useSync } from "@tui/context/sync"
|
|
25
|
+
import { useEvent } from "@tui/context/event"
|
|
26
|
+
import { editorSelectionKey, useEditorContext, type EditorSelection } from "@tui/context/editor"
|
|
27
|
+
import { MessageID, PartID } from "@/session/schema"
|
|
28
|
+
import { createStore, produce, unwrap } from "solid-js/store"
|
|
29
|
+
import { usePromptHistory, type PromptInfo } from "./history"
|
|
30
|
+
import { computePromptTraits } from "./traits"
|
|
31
|
+
import { assign } from "./part"
|
|
32
|
+
import { usePromptStash } from "./stash"
|
|
33
|
+
import { DialogStash } from "../dialog-stash"
|
|
34
|
+
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
|
|
35
|
+
import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
|
36
|
+
import * as Editor from "@tui/util/editor"
|
|
37
|
+
import { useExit } from "../../context/exit"
|
|
38
|
+
import * as Clipboard from "../../util/clipboard"
|
|
39
|
+
import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v2"
|
|
40
|
+
import { TuiEvent } from "../../event"
|
|
41
|
+
import { iife } from "@/util/iife"
|
|
42
|
+
import { Locale } from "@/util/locale"
|
|
43
|
+
import { formatDuration } from "@/util/format"
|
|
44
|
+
import { createColors, createFrames } from "../../ui/spinner.ts"
|
|
45
|
+
import { useDialog } from "@tui/ui/dialog"
|
|
46
|
+
import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
|
|
47
|
+
import { DialogAlert } from "../../ui/dialog-alert"
|
|
48
|
+
import { useToast } from "../../ui/toast"
|
|
49
|
+
import { useKV } from "../../context/kv"
|
|
50
|
+
import { createFadeIn } from "../../util/signal"
|
|
51
|
+
import { DialogSkill } from "../dialog-skill"
|
|
52
|
+
import {
|
|
53
|
+
confirmWorkspaceFileChanges,
|
|
54
|
+
openWorkspaceSelect,
|
|
55
|
+
warpWorkspaceSession,
|
|
56
|
+
type WorkspaceSelection,
|
|
57
|
+
} from "../dialog-workspace-create"
|
|
58
|
+
import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable"
|
|
59
|
+
import { useArgs } from "@tui/context/args"
|
|
60
|
+
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
61
|
+
import { type WorkspaceStatus } from "../workspace-label"
|
|
62
|
+
import { useCommandPalette } from "../../context/command-palette"
|
|
63
|
+
import { useBindings, useCommandShortcut, useLeaderActive, useOpencodeKeymap } from "../../keymap"
|
|
64
|
+
import { useTuiConfig } from "../../context/tui-config"
|
|
65
|
+
|
|
66
|
+
export type PromptProps = {
|
|
67
|
+
sessionID?: string
|
|
68
|
+
workspaceID?: string
|
|
69
|
+
visible?: boolean
|
|
70
|
+
disabled?: boolean
|
|
71
|
+
onSubmit?: () => void
|
|
72
|
+
ref?: (ref: PromptRef | undefined) => void
|
|
73
|
+
hint?: JSX.Element
|
|
74
|
+
right?: JSX.Element
|
|
75
|
+
showPlaceholder?: boolean
|
|
76
|
+
placeholders?: {
|
|
77
|
+
normal?: string[]
|
|
78
|
+
shell?: string[]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type PromptRef = {
|
|
83
|
+
focused: boolean
|
|
84
|
+
current: PromptInfo
|
|
85
|
+
set(prompt: PromptInfo): void
|
|
86
|
+
reset(): void
|
|
87
|
+
blur(): void
|
|
88
|
+
focus(): void
|
|
89
|
+
submit(): void
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const money = new Intl.NumberFormat("en-US", {
|
|
93
|
+
style: "currency",
|
|
94
|
+
currency: "USD",
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const DRAFT_RETENTION_MIN_CHARS = 20
|
|
98
|
+
|
|
99
|
+
function randomIndex(count: number) {
|
|
100
|
+
if (count <= 0) return 0
|
|
101
|
+
return Math.floor(Math.random() * count)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function fadeColor(color: RGBA, alpha: number) {
|
|
105
|
+
return RGBA.fromValues(color.r, color.g, color.b, color.a * alpha)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function hasEditorRangeSelection(selection: EditorSelection["ranges"][number]) {
|
|
109
|
+
return (
|
|
110
|
+
selection.selection.start.line !== selection.selection.end.line ||
|
|
111
|
+
selection.selection.start.character !== selection.selection.end.character
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getEditorRangeLabel(selection: EditorSelection["ranges"][number]) {
|
|
116
|
+
if (!hasEditorRangeSelection(selection)) return
|
|
117
|
+
if (selection.selection.start.line === selection.selection.end.line) return `#${selection.selection.start.line}`
|
|
118
|
+
return `#${selection.selection.start.line}-${selection.selection.end.line}`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function formatEditorContext(selection: EditorSelection) {
|
|
122
|
+
const selected = selection.ranges.filter(hasEditorRangeSelection)
|
|
123
|
+
if (selected.length === 0)
|
|
124
|
+
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`
|
|
125
|
+
|
|
126
|
+
const ranges = selected.map((range, index) => {
|
|
127
|
+
const prefix = selected.length > 1 ? `Selection ${index + 1}: ` : ""
|
|
128
|
+
return `Note: The user selected ${prefix}${getEditorRangeLabel(range)} from "${selection.filePath}". \`\`\`${range.text}\`\`\`\n\n`
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
return `<system-reminder>${ranges.join("\n")} This may or may not be relevant to the current task.</system-reminder>\n`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let stashed: { prompt: PromptInfo; cursor: number } | undefined
|
|
135
|
+
|
|
136
|
+
export function Prompt(props: PromptProps) {
|
|
137
|
+
let input: TextareaRenderable
|
|
138
|
+
let anchor: BoxRenderable
|
|
139
|
+
const [inputTarget, setInputTarget] = createSignal<TextareaRenderable | undefined>()
|
|
140
|
+
|
|
141
|
+
const leader = useLeaderActive()
|
|
142
|
+
const local = useLocal()
|
|
143
|
+
const args = useArgs()
|
|
144
|
+
const sdk = useSDK()
|
|
145
|
+
const editor = useEditorContext()
|
|
146
|
+
const route = useRoute()
|
|
147
|
+
const project = useProject()
|
|
148
|
+
const sync = useSync()
|
|
149
|
+
const tuiConfig = useTuiConfig()
|
|
150
|
+
const dialog = useDialog()
|
|
151
|
+
const toast = useToast()
|
|
152
|
+
const status = createMemo(() => sync.data.session_status?.[props.sessionID ?? ""] ?? { type: "idle" })
|
|
153
|
+
const history = usePromptHistory()
|
|
154
|
+
const stash = usePromptStash()
|
|
155
|
+
const command = useCommandPalette()
|
|
156
|
+
const keymap = useOpencodeKeymap()
|
|
157
|
+
const agentShortcut = useCommandShortcut("agent.cycle")
|
|
158
|
+
const paletteShortcut = useCommandShortcut("command.palette.show")
|
|
159
|
+
const renderer = useRenderer()
|
|
160
|
+
const dimensions = useTerminalDimensions()
|
|
161
|
+
const { theme, syntax } = useTheme()
|
|
162
|
+
const kv = useKV()
|
|
163
|
+
const animationsEnabled = createMemo(() => kv.get("animations_enabled", true))
|
|
164
|
+
const list = createMemo(() => props.placeholders?.normal ?? [])
|
|
165
|
+
const shell = createMemo(() => props.placeholders?.shell ?? [])
|
|
166
|
+
const fileContextEnabled = createMemo(() => kv.get("file_context_enabled", true))
|
|
167
|
+
const [dismissedEditorSelectionKey, setDismissedEditorSelectionKey] = createSignal<string>()
|
|
168
|
+
const editorContext = createMemo(() => {
|
|
169
|
+
const selection = fileContextEnabled() ? editor.selection() : undefined
|
|
170
|
+
if (!selection) return
|
|
171
|
+
return editorSelectionKey(selection) === dismissedEditorSelectionKey() ? undefined : selection
|
|
172
|
+
})
|
|
173
|
+
const editorPath = createMemo(() => editorContext()?.filePath)
|
|
174
|
+
const editorSelectionLabel = createMemo(() => {
|
|
175
|
+
const ranges = editorContext()?.ranges
|
|
176
|
+
if (!ranges) return
|
|
177
|
+
const first = ranges.find(hasEditorRangeSelection) ?? ranges[0]
|
|
178
|
+
if (!first) return
|
|
179
|
+
return [getEditorRangeLabel(first), ranges.length > 1 ? `+${ranges.length - 1}` : undefined]
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.join(" ")
|
|
182
|
+
})
|
|
183
|
+
const editorFileLabel = createMemo(() => {
|
|
184
|
+
const value = editorPath()
|
|
185
|
+
if (!value) return
|
|
186
|
+
const filename = path.basename(value)
|
|
187
|
+
const file = /^index\.[^./]+$/.test(filename)
|
|
188
|
+
? [path.basename(path.dirname(value)), filename].filter(Boolean).join("/")
|
|
189
|
+
: filename
|
|
190
|
+
return `${file.split(path.sep).join("/")}${editorSelectionLabel() ?? ""}`
|
|
191
|
+
})
|
|
192
|
+
const editorFileLabelDisplay = createMemo(() => {
|
|
193
|
+
const file = editorFileLabel()
|
|
194
|
+
if (!file) return
|
|
195
|
+
return Locale.truncateMiddle(file, Math.max(12, Math.min(48, Math.floor(dimensions().width / 3))))
|
|
196
|
+
})
|
|
197
|
+
const editorContextLabelState = createMemo(() => editor.labelState())
|
|
198
|
+
const [auto, setAuto] = createSignal<AutocompleteRef>()
|
|
199
|
+
const [workspaceSelection, setWorkspaceSelection] = createSignal<WorkspaceSelection>()
|
|
200
|
+
const [workspaceCreating, setWorkspaceCreating] = createSignal(false)
|
|
201
|
+
const [workspaceCreatingDots, setWorkspaceCreatingDots] = createSignal(3)
|
|
202
|
+
const [warpNotice, setWarpNotice] = createSignal<string>()
|
|
203
|
+
const [cursorVersion, setCursorVersion] = createSignal(0)
|
|
204
|
+
const currentProviderLabel = createMemo(() => local.model.parsed().provider)
|
|
205
|
+
const hasRightContent = createMemo(() => Boolean(props.right))
|
|
206
|
+
const defaultWorkspaceID = createMemo(() => props.workspaceID ?? project.workspace.current())
|
|
207
|
+
|
|
208
|
+
function selectWorkspace(selection: WorkspaceSelection | undefined) {
|
|
209
|
+
setWorkspaceSelection(selection)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function setCreatingWorkspace(creating: boolean) {
|
|
213
|
+
setWorkspaceCreating(creating)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function showWarpNotice(name: string) {
|
|
217
|
+
setWarpNotice(`Warped to ${name}`)
|
|
218
|
+
setTimeout(() => setWarpNotice(undefined), 4000)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function createWorkspace(selection: Extract<WorkspaceSelection, { type: "new" }>) {
|
|
222
|
+
setCreatingWorkspace(true)
|
|
223
|
+
const result = await sdk.client.experimental.workspace
|
|
224
|
+
.create({ type: selection.workspaceType, branch: null })
|
|
225
|
+
.catch(() => undefined)
|
|
226
|
+
if (result == undefined || result.error || !result.data) {
|
|
227
|
+
selectWorkspace(undefined)
|
|
228
|
+
setCreatingWorkspace(false)
|
|
229
|
+
toast.show({
|
|
230
|
+
message: "Creating workspace failed",
|
|
231
|
+
variant: "error",
|
|
232
|
+
})
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await project.workspace.sync()
|
|
237
|
+
const workspace = result.data
|
|
238
|
+
selectWorkspace({
|
|
239
|
+
type: "existing",
|
|
240
|
+
workspaceID: workspace.id,
|
|
241
|
+
workspaceType: workspace.type,
|
|
242
|
+
workspaceName: workspace.name,
|
|
243
|
+
})
|
|
244
|
+
setCreatingWorkspace(false)
|
|
245
|
+
return workspace
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function warpSession(selection: WorkspaceSelection) {
|
|
249
|
+
if (!props.sessionID) {
|
|
250
|
+
selectWorkspace(selection)
|
|
251
|
+
dialog.clear()
|
|
252
|
+
if (selection.type === "new") void createWorkspace(selection)
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
const sourceWorkspaceID = project.workspace.current()
|
|
256
|
+
const copyChanges = await confirmWorkspaceFileChanges({ dialog, sdk, sourceWorkspaceID })
|
|
257
|
+
if (copyChanges === undefined) return
|
|
258
|
+
selectWorkspace(selection)
|
|
259
|
+
dialog.clear()
|
|
260
|
+
|
|
261
|
+
const workspace =
|
|
262
|
+
selection.type === "none"
|
|
263
|
+
? { id: null, name: "local project" }
|
|
264
|
+
: selection.type === "existing"
|
|
265
|
+
? { id: selection.workspaceID, name: selection.workspaceName }
|
|
266
|
+
: await createWorkspace(selection)
|
|
267
|
+
if (!workspace) return
|
|
268
|
+
|
|
269
|
+
const warped = await warpWorkspaceSession({
|
|
270
|
+
dialog,
|
|
271
|
+
sdk,
|
|
272
|
+
sync,
|
|
273
|
+
project,
|
|
274
|
+
toast,
|
|
275
|
+
sourceWorkspaceID,
|
|
276
|
+
workspaceID: workspace.id,
|
|
277
|
+
sessionID: props.sessionID,
|
|
278
|
+
copyChanges,
|
|
279
|
+
})
|
|
280
|
+
if (warped) showWarpNotice(workspace.name)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
createEffect(() => {
|
|
284
|
+
if (!workspaceCreating()) {
|
|
285
|
+
setWorkspaceCreatingDots(3)
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
const timer = setInterval(() => setWorkspaceCreatingDots((dots) => (dots % 3) + 1), 1000)
|
|
289
|
+
onCleanup(() => clearInterval(timer))
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
function promptModelWarning() {
|
|
293
|
+
toast.show({
|
|
294
|
+
variant: "warning",
|
|
295
|
+
message: "Connect a provider to send prompts",
|
|
296
|
+
duration: 3000,
|
|
297
|
+
})
|
|
298
|
+
if (sync.data.provider.length === 0) {
|
|
299
|
+
dialog.replace(() => <DialogProviderConnect />)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function dismissEditorContext() {
|
|
304
|
+
setDismissedEditorSelectionKey(editorSelectionKey(editorContext()))
|
|
305
|
+
editor.clearSelection()
|
|
306
|
+
}
|
|
307
|
+
const fileStyleId = syntax().getStyleId("extmark.file")!
|
|
308
|
+
const agentStyleId = syntax().getStyleId("extmark.agent")!
|
|
309
|
+
const pasteStyleId = syntax().getStyleId("extmark.paste")!
|
|
310
|
+
let promptPartTypeId = 0
|
|
311
|
+
const event = useEvent()
|
|
312
|
+
|
|
313
|
+
event.on(TuiEvent.PromptAppend.type, (evt) => {
|
|
314
|
+
if (!input || input.isDestroyed) return
|
|
315
|
+
input.insertText(evt.properties.text)
|
|
316
|
+
setTimeout(() => {
|
|
317
|
+
// setTimeout is a workaround and needs to be addressed properly
|
|
318
|
+
if (!input || input.isDestroyed) return
|
|
319
|
+
input.getLayoutNode().markDirty()
|
|
320
|
+
input.gotoBufferEnd()
|
|
321
|
+
renderer.requestRender()
|
|
322
|
+
}, 0)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
createEffect(() => {
|
|
326
|
+
if (!input || input.isDestroyed) return
|
|
327
|
+
if (props.disabled) input.cursorColor = theme.backgroundElement
|
|
328
|
+
if (!props.disabled) input.cursorColor = theme.text
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
const lastUserMessage = createMemo(() => {
|
|
332
|
+
if (!props.sessionID) return undefined
|
|
333
|
+
const messages = sync.data.message[props.sessionID]
|
|
334
|
+
if (!messages) return undefined
|
|
335
|
+
return messages.findLast((m): m is UserMessage => m.role === "user")
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
const usage = createMemo(() => {
|
|
339
|
+
if (!props.sessionID) return
|
|
340
|
+
const session = sync.session.get(props.sessionID)
|
|
341
|
+
const msg = sync.data.message[props.sessionID] ?? []
|
|
342
|
+
const last = msg.findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0)
|
|
343
|
+
if (!last) return
|
|
344
|
+
|
|
345
|
+
const tokens =
|
|
346
|
+
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
|
347
|
+
if (tokens <= 0) return
|
|
348
|
+
|
|
349
|
+
const model = sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
|
|
350
|
+
const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined
|
|
351
|
+
const cost = session?.cost ?? 0
|
|
352
|
+
return {
|
|
353
|
+
context: pct ? `${Locale.number(tokens)} (${pct})` : Locale.number(tokens),
|
|
354
|
+
cost: cost > 0 ? money.format(cost) : undefined,
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
const [store, setStore] = createStore<{
|
|
359
|
+
prompt: PromptInfo
|
|
360
|
+
mode: "normal" | "shell"
|
|
361
|
+
extmarkToPartIndex: Map<number, number>
|
|
362
|
+
interrupt: number
|
|
363
|
+
placeholder: number
|
|
364
|
+
}>({
|
|
365
|
+
placeholder: randomIndex(list().length),
|
|
366
|
+
prompt: {
|
|
367
|
+
input: "",
|
|
368
|
+
parts: [],
|
|
369
|
+
},
|
|
370
|
+
mode: "normal",
|
|
371
|
+
extmarkToPartIndex: new Map(),
|
|
372
|
+
interrupt: 0,
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
createEffect(
|
|
376
|
+
on(
|
|
377
|
+
() => props.sessionID,
|
|
378
|
+
() => {
|
|
379
|
+
setStore("placeholder", randomIndex(list().length))
|
|
380
|
+
},
|
|
381
|
+
{ defer: true },
|
|
382
|
+
),
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
// Initialize agent/model/variant from last user message when session changes
|
|
386
|
+
let syncedSessionID: string | undefined
|
|
387
|
+
createEffect(() => {
|
|
388
|
+
const sessionID = props.sessionID
|
|
389
|
+
const msg = lastUserMessage()
|
|
390
|
+
|
|
391
|
+
if (sessionID !== syncedSessionID) {
|
|
392
|
+
if (!sessionID || !msg) return
|
|
393
|
+
|
|
394
|
+
syncedSessionID = sessionID
|
|
395
|
+
|
|
396
|
+
// Only set agent if it's a primary agent (not a subagent)
|
|
397
|
+
const isPrimaryAgent = local.agent.list().some((x) => x.name === msg.agent)
|
|
398
|
+
if (msg.agent && isPrimaryAgent) {
|
|
399
|
+
// Keep command line --agent if specified.
|
|
400
|
+
if (!args.agent) local.agent.set(msg.agent)
|
|
401
|
+
if (msg.model) {
|
|
402
|
+
local.model.set(msg.model)
|
|
403
|
+
local.model.variant.set(msg.model.variant)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
const promptCommands = createMemo(() =>
|
|
410
|
+
[
|
|
411
|
+
{
|
|
412
|
+
title: "Clear prompt",
|
|
413
|
+
name: "prompt.clear",
|
|
414
|
+
category: "Prompt",
|
|
415
|
+
hidden: true,
|
|
416
|
+
run: () => {
|
|
417
|
+
clearPrompt()
|
|
418
|
+
dialog.clear()
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
title: "Submit prompt",
|
|
423
|
+
name: "prompt.submit",
|
|
424
|
+
category: "Prompt",
|
|
425
|
+
hidden: true,
|
|
426
|
+
run: async () => {
|
|
427
|
+
if (!input.focused) return
|
|
428
|
+
const handled = await submit()
|
|
429
|
+
if (!handled) return
|
|
430
|
+
|
|
431
|
+
dialog.clear()
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
title: "Remove editor context",
|
|
436
|
+
name: "prompt.editor_context.clear",
|
|
437
|
+
category: "Prompt",
|
|
438
|
+
enabled: Boolean(editorContext()),
|
|
439
|
+
run: () => {
|
|
440
|
+
dismissEditorContext()
|
|
441
|
+
dialog.clear()
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
title: "Paste",
|
|
446
|
+
name: "prompt.paste",
|
|
447
|
+
category: "Prompt",
|
|
448
|
+
hidden: true,
|
|
449
|
+
run: async (ctx: CommandContext<Renderable, KeyEvent>) => {
|
|
450
|
+
ctx.event.preventDefault()
|
|
451
|
+
ctx.event.stopPropagation()
|
|
452
|
+
const content = await Clipboard.read()
|
|
453
|
+
if (content?.mime.startsWith("image/")) {
|
|
454
|
+
await pasteAttachment({
|
|
455
|
+
filename: "clipboard",
|
|
456
|
+
mime: content.mime,
|
|
457
|
+
content: content.data,
|
|
458
|
+
})
|
|
459
|
+
return
|
|
460
|
+
}
|
|
461
|
+
if (content?.mime === "text/plain") {
|
|
462
|
+
await pasteInputText(content.data)
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
title: "Interrupt session",
|
|
468
|
+
name: "session.interrupt",
|
|
469
|
+
category: "Session",
|
|
470
|
+
hidden: true,
|
|
471
|
+
enabled: status().type !== "idle",
|
|
472
|
+
run: () => {
|
|
473
|
+
if (auto()?.visible) return
|
|
474
|
+
if (!input.focused) return
|
|
475
|
+
// TODO: this should be its own command
|
|
476
|
+
if (store.mode === "shell") {
|
|
477
|
+
setStore("mode", "normal")
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
if (!props.sessionID) return
|
|
481
|
+
|
|
482
|
+
setStore("interrupt", store.interrupt + 1)
|
|
483
|
+
|
|
484
|
+
setTimeout(() => {
|
|
485
|
+
setStore("interrupt", 0)
|
|
486
|
+
}, 5000)
|
|
487
|
+
|
|
488
|
+
if (store.interrupt >= 2) {
|
|
489
|
+
void sdk.client.session.abort({
|
|
490
|
+
sessionID: props.sessionID,
|
|
491
|
+
})
|
|
492
|
+
setStore("interrupt", 0)
|
|
493
|
+
}
|
|
494
|
+
dialog.clear()
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
title: "Open editor",
|
|
499
|
+
category: "Session",
|
|
500
|
+
name: "prompt.editor",
|
|
501
|
+
slashName: "editor",
|
|
502
|
+
run: async () => {
|
|
503
|
+
dialog.clear()
|
|
504
|
+
|
|
505
|
+
// replace summarized text parts with the actual text
|
|
506
|
+
const text = store.prompt.parts
|
|
507
|
+
.filter((p) => p.type === "text")
|
|
508
|
+
.reduce((acc, p) => {
|
|
509
|
+
if (!p.source) return acc
|
|
510
|
+
return acc.replace(p.source.text.value, p.text)
|
|
511
|
+
}, store.prompt.input)
|
|
512
|
+
|
|
513
|
+
const nonTextParts = store.prompt.parts.filter((p) => p.type !== "text")
|
|
514
|
+
|
|
515
|
+
const value = text
|
|
516
|
+
const content = await Editor.open({ value, renderer })
|
|
517
|
+
if (!content) return
|
|
518
|
+
|
|
519
|
+
input.setText(content)
|
|
520
|
+
|
|
521
|
+
// Update positions for nonTextParts based on their location in new content
|
|
522
|
+
// Filter out parts whose virtual text was deleted
|
|
523
|
+
// this handles a case where the user edits the text in the editor
|
|
524
|
+
// such that the virtual text moves around or is deleted
|
|
525
|
+
const updatedNonTextParts = nonTextParts
|
|
526
|
+
.map((part) => {
|
|
527
|
+
let virtualText = ""
|
|
528
|
+
if (part.type === "file" && part.source?.text) {
|
|
529
|
+
virtualText = part.source.text.value
|
|
530
|
+
} else if (part.type === "agent" && part.source) {
|
|
531
|
+
virtualText = part.source.value
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (!virtualText) return part
|
|
535
|
+
|
|
536
|
+
const newStart = content.indexOf(virtualText)
|
|
537
|
+
// if the virtual text is deleted, remove the part
|
|
538
|
+
if (newStart === -1) return null
|
|
539
|
+
|
|
540
|
+
const newEnd = newStart + virtualText.length
|
|
541
|
+
|
|
542
|
+
if (part.type === "file" && part.source?.text) {
|
|
543
|
+
return {
|
|
544
|
+
...part,
|
|
545
|
+
source: {
|
|
546
|
+
...part.source,
|
|
547
|
+
text: {
|
|
548
|
+
...part.source.text,
|
|
549
|
+
start: newStart,
|
|
550
|
+
end: newEnd,
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (part.type === "agent" && part.source) {
|
|
557
|
+
return {
|
|
558
|
+
...part,
|
|
559
|
+
source: {
|
|
560
|
+
...part.source,
|
|
561
|
+
start: newStart,
|
|
562
|
+
end: newEnd,
|
|
563
|
+
},
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return part
|
|
568
|
+
})
|
|
569
|
+
.filter((part) => part !== null)
|
|
570
|
+
|
|
571
|
+
setStore("prompt", {
|
|
572
|
+
input: content,
|
|
573
|
+
// keep only the non-text parts because the text parts were
|
|
574
|
+
// already expanded inline
|
|
575
|
+
parts: updatedNonTextParts,
|
|
576
|
+
})
|
|
577
|
+
restoreExtmarksFromParts(updatedNonTextParts)
|
|
578
|
+
input.cursorOffset = Bun.stringWidth(content)
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
title: "Skills",
|
|
583
|
+
name: "prompt.skills",
|
|
584
|
+
category: "Prompt",
|
|
585
|
+
slashName: "skills",
|
|
586
|
+
run: () => {
|
|
587
|
+
dialog.replace(() => (
|
|
588
|
+
<DialogSkill
|
|
589
|
+
onSelect={(skill) => {
|
|
590
|
+
input.setText(`/${skill} `)
|
|
591
|
+
setStore("prompt", {
|
|
592
|
+
input: `/${skill} `,
|
|
593
|
+
parts: [],
|
|
594
|
+
})
|
|
595
|
+
input.gotoBufferEnd()
|
|
596
|
+
}}
|
|
597
|
+
/>
|
|
598
|
+
))
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
title: "Warp",
|
|
603
|
+
desc: "Change the workspace for the session",
|
|
604
|
+
name: "workspace.set",
|
|
605
|
+
category: "Session",
|
|
606
|
+
enabled: Flag.OPENCODE_EXPERIMENTAL_WORKSPACES,
|
|
607
|
+
slashName: "warp",
|
|
608
|
+
run: () => {
|
|
609
|
+
void openWorkspaceSelect({
|
|
610
|
+
dialog,
|
|
611
|
+
sdk,
|
|
612
|
+
sync,
|
|
613
|
+
project,
|
|
614
|
+
toast,
|
|
615
|
+
onSelect: (selection) => {
|
|
616
|
+
void warpSession(selection)
|
|
617
|
+
},
|
|
618
|
+
})
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
].map((entry) => ({
|
|
622
|
+
namespace: "palette",
|
|
623
|
+
...entry,
|
|
624
|
+
})),
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
useBindings(() => ({
|
|
628
|
+
commands: promptCommands(),
|
|
629
|
+
}))
|
|
630
|
+
|
|
631
|
+
useBindings(() => ({
|
|
632
|
+
enabled: command.matcher,
|
|
633
|
+
bindings: tuiConfig.keybinds.gather("prompt.palette", [
|
|
634
|
+
"prompt.submit",
|
|
635
|
+
"prompt.editor",
|
|
636
|
+
"prompt.editor_context.clear",
|
|
637
|
+
"prompt.stash",
|
|
638
|
+
"prompt.stash.pop",
|
|
639
|
+
"prompt.stash.list",
|
|
640
|
+
"session.interrupt",
|
|
641
|
+
"workspace.set",
|
|
642
|
+
]),
|
|
643
|
+
}))
|
|
644
|
+
|
|
645
|
+
const ref: PromptRef = {
|
|
646
|
+
get focused() {
|
|
647
|
+
return input.focused
|
|
648
|
+
},
|
|
649
|
+
get current() {
|
|
650
|
+
return store.prompt
|
|
651
|
+
},
|
|
652
|
+
focus() {
|
|
653
|
+
input.focus()
|
|
654
|
+
},
|
|
655
|
+
blur() {
|
|
656
|
+
input.blur()
|
|
657
|
+
},
|
|
658
|
+
set(prompt) {
|
|
659
|
+
input.setText(prompt.input)
|
|
660
|
+
setStore("prompt", prompt)
|
|
661
|
+
restoreExtmarksFromParts(prompt.parts)
|
|
662
|
+
input.gotoBufferEnd()
|
|
663
|
+
},
|
|
664
|
+
reset() {
|
|
665
|
+
input.clear()
|
|
666
|
+
input.extmarks.clear()
|
|
667
|
+
setStore("prompt", {
|
|
668
|
+
input: "",
|
|
669
|
+
parts: [],
|
|
670
|
+
})
|
|
671
|
+
setStore("extmarkToPartIndex", new Map())
|
|
672
|
+
},
|
|
673
|
+
submit() {
|
|
674
|
+
void submit()
|
|
675
|
+
},
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
onMount(() => {
|
|
679
|
+
const saved = stashed
|
|
680
|
+
stashed = undefined
|
|
681
|
+
if (store.prompt.input) return
|
|
682
|
+
if (saved && saved.prompt.input) {
|
|
683
|
+
input.setText(saved.prompt.input)
|
|
684
|
+
setStore("prompt", saved.prompt)
|
|
685
|
+
restoreExtmarksFromParts(saved.prompt.parts)
|
|
686
|
+
input.cursorOffset = saved.cursor
|
|
687
|
+
}
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
onCleanup(() => {
|
|
691
|
+
if (store.prompt.input) {
|
|
692
|
+
stashed = { prompt: unwrap(store.prompt), cursor: input.cursorOffset }
|
|
693
|
+
}
|
|
694
|
+
setInputTarget(undefined)
|
|
695
|
+
props.ref?.(undefined)
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
createEffect(() => {
|
|
699
|
+
if (!input || input.isDestroyed) return
|
|
700
|
+
if (props.visible === false || dialog.stack.length > 0) {
|
|
701
|
+
if (input.focused) input.blur()
|
|
702
|
+
return
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Slot/plugin updates can remount the background prompt while a dialog is open.
|
|
706
|
+
// Keep focus with the dialog and let the prompt reclaim it after the dialog closes.
|
|
707
|
+
if (!input.focused) input.focus()
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
createEffect(() => {
|
|
711
|
+
if (!input || input.isDestroyed) return
|
|
712
|
+
input.traits = {
|
|
713
|
+
...input.traits,
|
|
714
|
+
...computePromptTraits({
|
|
715
|
+
mode: store.mode,
|
|
716
|
+
autocompleteVisible: !!auto()?.visible,
|
|
717
|
+
}),
|
|
718
|
+
}
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
function restoreExtmarksFromParts(parts: PromptInfo["parts"]) {
|
|
722
|
+
input.extmarks.clear()
|
|
723
|
+
setStore("extmarkToPartIndex", new Map())
|
|
724
|
+
|
|
725
|
+
parts.forEach((part, partIndex) => {
|
|
726
|
+
let start = 0
|
|
727
|
+
let end = 0
|
|
728
|
+
let virtualText = ""
|
|
729
|
+
let styleId: number | undefined
|
|
730
|
+
|
|
731
|
+
if (part.type === "file" && part.source?.text) {
|
|
732
|
+
start = part.source.text.start
|
|
733
|
+
end = part.source.text.end
|
|
734
|
+
virtualText = part.source.text.value
|
|
735
|
+
styleId = fileStyleId
|
|
736
|
+
} else if (part.type === "agent" && part.source) {
|
|
737
|
+
start = part.source.start
|
|
738
|
+
end = part.source.end
|
|
739
|
+
virtualText = part.source.value
|
|
740
|
+
styleId = agentStyleId
|
|
741
|
+
} else if (part.type === "text" && part.source?.text) {
|
|
742
|
+
start = part.source.text.start
|
|
743
|
+
end = part.source.text.end
|
|
744
|
+
virtualText = part.source.text.value
|
|
745
|
+
styleId = pasteStyleId
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (virtualText) {
|
|
749
|
+
const extmarkId = input.extmarks.create({
|
|
750
|
+
start,
|
|
751
|
+
end,
|
|
752
|
+
virtual: true,
|
|
753
|
+
styleId,
|
|
754
|
+
typeId: promptPartTypeId,
|
|
755
|
+
})
|
|
756
|
+
setStore("extmarkToPartIndex", (map: Map<number, number>) => {
|
|
757
|
+
const newMap = new Map(map)
|
|
758
|
+
newMap.set(extmarkId, partIndex)
|
|
759
|
+
return newMap
|
|
760
|
+
})
|
|
761
|
+
}
|
|
762
|
+
})
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function syncExtmarksWithPromptParts() {
|
|
766
|
+
const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId)
|
|
767
|
+
setStore(
|
|
768
|
+
produce((draft) => {
|
|
769
|
+
const newMap = new Map<number, number>()
|
|
770
|
+
const newParts: typeof draft.prompt.parts = []
|
|
771
|
+
|
|
772
|
+
for (const extmark of allExtmarks) {
|
|
773
|
+
const partIndex = draft.extmarkToPartIndex.get(extmark.id)
|
|
774
|
+
if (partIndex !== undefined) {
|
|
775
|
+
const part = draft.prompt.parts[partIndex]
|
|
776
|
+
if (part) {
|
|
777
|
+
if (part.type === "agent" && part.source) {
|
|
778
|
+
part.source.start = extmark.start
|
|
779
|
+
part.source.end = extmark.end
|
|
780
|
+
} else if (part.type === "file" && part.source?.text) {
|
|
781
|
+
part.source.text.start = extmark.start
|
|
782
|
+
part.source.text.end = extmark.end
|
|
783
|
+
} else if (part.type === "text" && part.source?.text) {
|
|
784
|
+
part.source.text.start = extmark.start
|
|
785
|
+
part.source.text.end = extmark.end
|
|
786
|
+
}
|
|
787
|
+
newMap.set(extmark.id, newParts.length)
|
|
788
|
+
newParts.push(part)
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
draft.extmarkToPartIndex = newMap
|
|
794
|
+
draft.prompt.parts = newParts
|
|
795
|
+
}),
|
|
796
|
+
)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const stashCommands = createMemo(() =>
|
|
800
|
+
[
|
|
801
|
+
{
|
|
802
|
+
title: "Stash prompt",
|
|
803
|
+
name: "prompt.stash",
|
|
804
|
+
category: "Prompt",
|
|
805
|
+
enabled: !!store.prompt.input,
|
|
806
|
+
run: () => {
|
|
807
|
+
if (!store.prompt.input) return
|
|
808
|
+
stash.push({
|
|
809
|
+
input: store.prompt.input,
|
|
810
|
+
parts: store.prompt.parts,
|
|
811
|
+
})
|
|
812
|
+
input.extmarks.clear()
|
|
813
|
+
input.clear()
|
|
814
|
+
setStore("prompt", { input: "", parts: [] })
|
|
815
|
+
setStore("extmarkToPartIndex", new Map())
|
|
816
|
+
dialog.clear()
|
|
817
|
+
},
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
title: "Stash pop",
|
|
821
|
+
name: "prompt.stash.pop",
|
|
822
|
+
category: "Prompt",
|
|
823
|
+
enabled: stash.list().length > 0,
|
|
824
|
+
run: () => {
|
|
825
|
+
const entry = stash.pop()
|
|
826
|
+
if (entry) {
|
|
827
|
+
input.setText(entry.input)
|
|
828
|
+
setStore("prompt", { input: entry.input, parts: entry.parts })
|
|
829
|
+
restoreExtmarksFromParts(entry.parts)
|
|
830
|
+
input.gotoBufferEnd()
|
|
831
|
+
}
|
|
832
|
+
dialog.clear()
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
title: "Stash list",
|
|
837
|
+
name: "prompt.stash.list",
|
|
838
|
+
category: "Prompt",
|
|
839
|
+
enabled: stash.list().length > 0,
|
|
840
|
+
run: () => {
|
|
841
|
+
dialog.replace(() => (
|
|
842
|
+
<DialogStash
|
|
843
|
+
onSelect={(entry) => {
|
|
844
|
+
input.setText(entry.input)
|
|
845
|
+
setStore("prompt", { input: entry.input, parts: entry.parts })
|
|
846
|
+
restoreExtmarksFromParts(entry.parts)
|
|
847
|
+
input.gotoBufferEnd()
|
|
848
|
+
}}
|
|
849
|
+
/>
|
|
850
|
+
))
|
|
851
|
+
},
|
|
852
|
+
},
|
|
853
|
+
].map((entry) => ({
|
|
854
|
+
namespace: "palette",
|
|
855
|
+
...entry,
|
|
856
|
+
})),
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
useBindings(() => ({
|
|
860
|
+
commands: stashCommands(),
|
|
861
|
+
}))
|
|
862
|
+
|
|
863
|
+
useBindings(() => {
|
|
864
|
+
return {
|
|
865
|
+
target: inputTarget,
|
|
866
|
+
enabled: inputTarget() !== undefined && !props.disabled,
|
|
867
|
+
bindings: tuiConfig.keybinds.get("prompt.paste"),
|
|
868
|
+
}
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
useBindings(() => {
|
|
872
|
+
return {
|
|
873
|
+
target: inputTarget,
|
|
874
|
+
enabled: inputTarget() !== undefined && !props.disabled && store.prompt.input !== "",
|
|
875
|
+
bindings: tuiConfig.keybinds.get("prompt.clear"),
|
|
876
|
+
}
|
|
877
|
+
})
|
|
878
|
+
|
|
879
|
+
useBindings(() => {
|
|
880
|
+
return {
|
|
881
|
+
target: inputTarget,
|
|
882
|
+
enabled: (() => {
|
|
883
|
+
cursorVersion()
|
|
884
|
+
return (
|
|
885
|
+
inputTarget() !== undefined &&
|
|
886
|
+
!props.disabled &&
|
|
887
|
+
store.mode === "normal" &&
|
|
888
|
+
!auto()?.visible &&
|
|
889
|
+
input?.visualCursor.offset === 0
|
|
890
|
+
)
|
|
891
|
+
})(),
|
|
892
|
+
bindings: [
|
|
893
|
+
{
|
|
894
|
+
key: "!",
|
|
895
|
+
desc: "Shell mode",
|
|
896
|
+
group: "Prompt",
|
|
897
|
+
cmd: () => {
|
|
898
|
+
setStore("placeholder", randomIndex(shell().length))
|
|
899
|
+
setStore("mode", "shell")
|
|
900
|
+
},
|
|
901
|
+
},
|
|
902
|
+
],
|
|
903
|
+
}
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
useBindings(() => {
|
|
907
|
+
return {
|
|
908
|
+
target: inputTarget,
|
|
909
|
+
enabled: inputTarget() !== undefined && store.mode === "shell",
|
|
910
|
+
bindings: [{ key: "escape", desc: "Exit shell mode", group: "Prompt", cmd: () => setStore("mode", "normal") }],
|
|
911
|
+
}
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
useBindings(() => {
|
|
915
|
+
return {
|
|
916
|
+
target: inputTarget,
|
|
917
|
+
enabled: (() => {
|
|
918
|
+
cursorVersion()
|
|
919
|
+
return inputTarget() !== undefined && store.mode === "shell" && input?.visualCursor.offset === 0
|
|
920
|
+
})(),
|
|
921
|
+
bindings: [{ key: "backspace", desc: "Exit shell mode", group: "Prompt", cmd: () => setStore("mode", "normal") }],
|
|
922
|
+
}
|
|
923
|
+
})
|
|
924
|
+
|
|
925
|
+
useBindings(() => {
|
|
926
|
+
return {
|
|
927
|
+
target: inputTarget,
|
|
928
|
+
enabled: (() => {
|
|
929
|
+
cursorVersion()
|
|
930
|
+
return inputTarget() !== undefined && !props.disabled && !auto()?.visible && input !== undefined
|
|
931
|
+
})(),
|
|
932
|
+
commands: [
|
|
933
|
+
{
|
|
934
|
+
name: "prompt.history.previous",
|
|
935
|
+
title: "Previous prompt history",
|
|
936
|
+
category: "Prompt",
|
|
937
|
+
run() {
|
|
938
|
+
if (input.cursorOffset !== 0) {
|
|
939
|
+
if (input.scrollY + input.visualCursor.visualRow === 0) input.cursorOffset = 0
|
|
940
|
+
return false
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const item = history.move(-1, input.plainText)
|
|
944
|
+
if (!item) return false
|
|
945
|
+
input.setText(item.input)
|
|
946
|
+
setStore("prompt", item)
|
|
947
|
+
setStore("mode", item.mode ?? "normal")
|
|
948
|
+
restoreExtmarksFromParts(item.parts)
|
|
949
|
+
input.cursorOffset = 0
|
|
950
|
+
},
|
|
951
|
+
},
|
|
952
|
+
],
|
|
953
|
+
bindings: tuiConfig.keybinds.get("prompt.history.previous"),
|
|
954
|
+
}
|
|
955
|
+
})
|
|
956
|
+
|
|
957
|
+
useBindings(() => {
|
|
958
|
+
return {
|
|
959
|
+
target: inputTarget,
|
|
960
|
+
enabled: (() => {
|
|
961
|
+
cursorVersion()
|
|
962
|
+
return inputTarget() !== undefined && !props.disabled && !auto()?.visible && input !== undefined
|
|
963
|
+
})(),
|
|
964
|
+
commands: [
|
|
965
|
+
{
|
|
966
|
+
name: "prompt.history.next",
|
|
967
|
+
title: "Next prompt history",
|
|
968
|
+
category: "Prompt",
|
|
969
|
+
run() {
|
|
970
|
+
if (input.cursorOffset !== input.plainText.length) {
|
|
971
|
+
if (
|
|
972
|
+
input.scrollY + input.visualCursor.visualRow ===
|
|
973
|
+
Math.max(0, input.editorView.getTotalVirtualLineCount() - 1)
|
|
974
|
+
)
|
|
975
|
+
input.cursorOffset = input.plainText.length
|
|
976
|
+
return false
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const item = history.move(1, input.plainText)
|
|
980
|
+
if (!item) return false
|
|
981
|
+
input.setText(item.input)
|
|
982
|
+
setStore("prompt", item)
|
|
983
|
+
setStore("mode", item.mode ?? "normal")
|
|
984
|
+
restoreExtmarksFromParts(item.parts)
|
|
985
|
+
input.cursorOffset = input.plainText.length
|
|
986
|
+
},
|
|
987
|
+
},
|
|
988
|
+
],
|
|
989
|
+
bindings: tuiConfig.keybinds.get("prompt.history.next"),
|
|
990
|
+
}
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
let submitting = false
|
|
994
|
+
async function submit() {
|
|
995
|
+
// Prevent overlapping invocations (e.g. a double-pressed Enter, or the
|
|
996
|
+
// input's native onSubmit racing another dispatch). Without this guard,
|
|
997
|
+
// a second call slips past the empty-input check before the first call
|
|
998
|
+
// clears `store.prompt.input`, then awaits its own `session.create` and
|
|
999
|
+
// ultimately reads the now-empty store — sending a phantom empty prompt
|
|
1000
|
+
// to a freshly created session.
|
|
1001
|
+
if (submitting) return false
|
|
1002
|
+
submitting = true
|
|
1003
|
+
try {
|
|
1004
|
+
return await submitInner()
|
|
1005
|
+
} finally {
|
|
1006
|
+
submitting = false
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
async function submitInner() {
|
|
1011
|
+
setWarpNotice(undefined)
|
|
1012
|
+
|
|
1013
|
+
// IME: double-defer may fire before onContentChange flushes the last
|
|
1014
|
+
// composed character (e.g. Korean hangul) to the store, so read
|
|
1015
|
+
// plainText directly and sync before any downstream reads.
|
|
1016
|
+
if (input && !input.isDestroyed && input.plainText !== store.prompt.input) {
|
|
1017
|
+
setStore("prompt", "input", input.plainText)
|
|
1018
|
+
syncExtmarksWithPromptParts()
|
|
1019
|
+
}
|
|
1020
|
+
if (props.disabled) return false
|
|
1021
|
+
if (workspaceCreating()) return false
|
|
1022
|
+
if (auto()?.visible) return false
|
|
1023
|
+
if (!store.prompt.input) return false
|
|
1024
|
+
const agent = local.agent.current()
|
|
1025
|
+
if (!agent) return false
|
|
1026
|
+
const trimmed = store.prompt.input.trim()
|
|
1027
|
+
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
|
|
1028
|
+
void exit()
|
|
1029
|
+
return true
|
|
1030
|
+
}
|
|
1031
|
+
const selectedModel = local.model.current()
|
|
1032
|
+
if (!selectedModel) {
|
|
1033
|
+
void promptModelWarning()
|
|
1034
|
+
return false
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const workspaceSession = props.sessionID ? sync.session.get(props.sessionID) : undefined
|
|
1038
|
+
const workspaceID = workspaceSession?.workspaceID
|
|
1039
|
+
const workspaceStatus = workspaceID ? (project.workspace.status(workspaceID) ?? "error") : undefined
|
|
1040
|
+
if (props.sessionID && workspaceID && workspaceStatus !== "connected") {
|
|
1041
|
+
dialog.replace(() => (
|
|
1042
|
+
<DialogWorkspaceUnavailable
|
|
1043
|
+
onRestore={() => {
|
|
1044
|
+
void openWorkspaceSelect({
|
|
1045
|
+
dialog,
|
|
1046
|
+
sdk,
|
|
1047
|
+
sync,
|
|
1048
|
+
project,
|
|
1049
|
+
toast,
|
|
1050
|
+
onSelect: (selection) => {
|
|
1051
|
+
void warpSession(selection)
|
|
1052
|
+
},
|
|
1053
|
+
})
|
|
1054
|
+
return false
|
|
1055
|
+
}}
|
|
1056
|
+
/>
|
|
1057
|
+
))
|
|
1058
|
+
return false
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const variant = local.model.variant.current()
|
|
1062
|
+
let sessionID = props.sessionID
|
|
1063
|
+
if (sessionID == null) {
|
|
1064
|
+
const workspace = workspaceSelection()
|
|
1065
|
+
const workspaceID = iife(() => {
|
|
1066
|
+
if (!workspace) return defaultWorkspaceID()
|
|
1067
|
+
if (workspace.type === "none") return undefined
|
|
1068
|
+
if (workspace.type === "existing") return workspace.workspaceID
|
|
1069
|
+
return undefined
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
const res = await sdk.client.session.create({
|
|
1073
|
+
workspace: workspaceID,
|
|
1074
|
+
agent: agent.name,
|
|
1075
|
+
model: {
|
|
1076
|
+
providerID: selectedModel.providerID,
|
|
1077
|
+
id: selectedModel.modelID,
|
|
1078
|
+
variant,
|
|
1079
|
+
},
|
|
1080
|
+
})
|
|
1081
|
+
|
|
1082
|
+
if (res.error) {
|
|
1083
|
+
console.log("Creating a session failed:", res.error)
|
|
1084
|
+
|
|
1085
|
+
toast.show({
|
|
1086
|
+
message: "Creating a session failed. Open console for more details.",
|
|
1087
|
+
variant: "error",
|
|
1088
|
+
})
|
|
1089
|
+
|
|
1090
|
+
return true
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
sessionID = res.data.id
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const messageID = MessageID.ascending()
|
|
1097
|
+
let inputText = store.prompt.input
|
|
1098
|
+
|
|
1099
|
+
// Expand pasted text inline before submitting
|
|
1100
|
+
const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId)
|
|
1101
|
+
const sortedExtmarks = allExtmarks.sort((a: { start: number }, b: { start: number }) => b.start - a.start)
|
|
1102
|
+
|
|
1103
|
+
for (const extmark of sortedExtmarks) {
|
|
1104
|
+
const partIndex = store.extmarkToPartIndex.get(extmark.id)
|
|
1105
|
+
if (partIndex !== undefined) {
|
|
1106
|
+
const part = store.prompt.parts[partIndex]
|
|
1107
|
+
if (part?.type === "text" && part.text) {
|
|
1108
|
+
const before = inputText.slice(0, extmark.start)
|
|
1109
|
+
const after = inputText.slice(extmark.end)
|
|
1110
|
+
inputText = before + part.text + after
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Filter out text parts (pasted content) since they're now expanded inline
|
|
1116
|
+
const nonTextParts = store.prompt.parts.filter((part) => part.type !== "text")
|
|
1117
|
+
|
|
1118
|
+
// Capture mode before it gets reset
|
|
1119
|
+
const currentMode = store.mode
|
|
1120
|
+
const editorSelection = editorContext()
|
|
1121
|
+
const editorParts =
|
|
1122
|
+
editorSelection && editor.labelState() === "pending"
|
|
1123
|
+
? [
|
|
1124
|
+
{
|
|
1125
|
+
id: PartID.ascending(),
|
|
1126
|
+
type: "text" as const,
|
|
1127
|
+
text: formatEditorContext(editorSelection),
|
|
1128
|
+
synthetic: true,
|
|
1129
|
+
metadata: {
|
|
1130
|
+
kind: "editor_context",
|
|
1131
|
+
source: editorSelection.source ?? "editor",
|
|
1132
|
+
filePath: editorSelection.filePath,
|
|
1133
|
+
ranges: editorSelection.ranges,
|
|
1134
|
+
},
|
|
1135
|
+
},
|
|
1136
|
+
]
|
|
1137
|
+
: []
|
|
1138
|
+
|
|
1139
|
+
if (store.mode === "shell") {
|
|
1140
|
+
void sdk.client.session.shell({
|
|
1141
|
+
sessionID,
|
|
1142
|
+
agent: agent.name,
|
|
1143
|
+
model: {
|
|
1144
|
+
providerID: selectedModel.providerID,
|
|
1145
|
+
modelID: selectedModel.modelID,
|
|
1146
|
+
},
|
|
1147
|
+
command: inputText,
|
|
1148
|
+
})
|
|
1149
|
+
setStore("mode", "normal")
|
|
1150
|
+
} else if (
|
|
1151
|
+
inputText.startsWith("/") &&
|
|
1152
|
+
iife(() => {
|
|
1153
|
+
const firstLine = inputText.split("\n")[0]
|
|
1154
|
+
const command = firstLine.split(" ")[0].slice(1)
|
|
1155
|
+
return sync.data.command.some((x) => x.name === command)
|
|
1156
|
+
})
|
|
1157
|
+
) {
|
|
1158
|
+
// Parse command from first line, preserve multi-line content in arguments
|
|
1159
|
+
const firstLineEnd = inputText.indexOf("\n")
|
|
1160
|
+
const firstLine = firstLineEnd === -1 ? inputText : inputText.slice(0, firstLineEnd)
|
|
1161
|
+
const [command, ...firstLineArgs] = firstLine.split(" ")
|
|
1162
|
+
const restOfInput = firstLineEnd === -1 ? "" : inputText.slice(firstLineEnd + 1)
|
|
1163
|
+
const args = firstLineArgs.join(" ") + (restOfInput ? "\n" + restOfInput : "")
|
|
1164
|
+
|
|
1165
|
+
void sdk.client.session.command({
|
|
1166
|
+
sessionID,
|
|
1167
|
+
command: command.slice(1),
|
|
1168
|
+
arguments: args,
|
|
1169
|
+
agent: agent.name,
|
|
1170
|
+
model: `${selectedModel.providerID}/${selectedModel.modelID}`,
|
|
1171
|
+
messageID,
|
|
1172
|
+
variant,
|
|
1173
|
+
parts: nonTextParts
|
|
1174
|
+
.filter((x) => x.type === "file")
|
|
1175
|
+
.map((x) => ({
|
|
1176
|
+
id: PartID.ascending(),
|
|
1177
|
+
...x,
|
|
1178
|
+
})),
|
|
1179
|
+
})
|
|
1180
|
+
} else {
|
|
1181
|
+
sdk.client.session
|
|
1182
|
+
.prompt({
|
|
1183
|
+
sessionID,
|
|
1184
|
+
...selectedModel,
|
|
1185
|
+
messageID,
|
|
1186
|
+
agent: agent.name,
|
|
1187
|
+
model: selectedModel,
|
|
1188
|
+
variant,
|
|
1189
|
+
parts: [
|
|
1190
|
+
...editorParts,
|
|
1191
|
+
{
|
|
1192
|
+
id: PartID.ascending(),
|
|
1193
|
+
type: "text",
|
|
1194
|
+
text: inputText,
|
|
1195
|
+
},
|
|
1196
|
+
...nonTextParts.map(assign),
|
|
1197
|
+
],
|
|
1198
|
+
})
|
|
1199
|
+
.catch(() => {})
|
|
1200
|
+
if (editorParts.length > 0) editor.markSelectionSent()
|
|
1201
|
+
}
|
|
1202
|
+
history.append({
|
|
1203
|
+
...store.prompt,
|
|
1204
|
+
mode: currentMode,
|
|
1205
|
+
})
|
|
1206
|
+
input.extmarks.clear()
|
|
1207
|
+
setStore("prompt", {
|
|
1208
|
+
input: "",
|
|
1209
|
+
parts: [],
|
|
1210
|
+
})
|
|
1211
|
+
setStore("extmarkToPartIndex", new Map())
|
|
1212
|
+
props.onSubmit?.()
|
|
1213
|
+
|
|
1214
|
+
// temporary hack to make sure the message is sent
|
|
1215
|
+
if (!props.sessionID) {
|
|
1216
|
+
if (editorParts.length > 0) editor.preserveSelectionFromNewSession()
|
|
1217
|
+
setTimeout(() => {
|
|
1218
|
+
route.navigate({
|
|
1219
|
+
type: "session",
|
|
1220
|
+
sessionID,
|
|
1221
|
+
})
|
|
1222
|
+
}, 50)
|
|
1223
|
+
}
|
|
1224
|
+
input.clear()
|
|
1225
|
+
return true
|
|
1226
|
+
}
|
|
1227
|
+
const exit = useExit()
|
|
1228
|
+
|
|
1229
|
+
function pasteText(text: string, virtualText: string) {
|
|
1230
|
+
const currentOffset = input.visualCursor.offset
|
|
1231
|
+
const extmarkStart = currentOffset
|
|
1232
|
+
const extmarkEnd = extmarkStart + virtualText.length
|
|
1233
|
+
|
|
1234
|
+
input.insertText(virtualText + " ")
|
|
1235
|
+
|
|
1236
|
+
const extmarkId = input.extmarks.create({
|
|
1237
|
+
start: extmarkStart,
|
|
1238
|
+
end: extmarkEnd,
|
|
1239
|
+
virtual: true,
|
|
1240
|
+
styleId: pasteStyleId,
|
|
1241
|
+
typeId: promptPartTypeId,
|
|
1242
|
+
})
|
|
1243
|
+
|
|
1244
|
+
setStore(
|
|
1245
|
+
produce((draft) => {
|
|
1246
|
+
const partIndex = draft.prompt.parts.length
|
|
1247
|
+
draft.prompt.parts.push({
|
|
1248
|
+
type: "text" as const,
|
|
1249
|
+
text,
|
|
1250
|
+
source: {
|
|
1251
|
+
text: {
|
|
1252
|
+
start: extmarkStart,
|
|
1253
|
+
end: extmarkEnd,
|
|
1254
|
+
value: virtualText,
|
|
1255
|
+
},
|
|
1256
|
+
},
|
|
1257
|
+
})
|
|
1258
|
+
draft.extmarkToPartIndex.set(extmarkId, partIndex)
|
|
1259
|
+
}),
|
|
1260
|
+
)
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
async function pasteInputText(text: string) {
|
|
1264
|
+
const normalizedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
|
|
1265
|
+
const pastedContent = normalizedText.trim()
|
|
1266
|
+
const filepath = iife(() => {
|
|
1267
|
+
const raw = pastedContent.replace(/^['"]+|['"]+$/g, "")
|
|
1268
|
+
if (raw.startsWith("file://")) {
|
|
1269
|
+
try {
|
|
1270
|
+
return fileURLToPath(raw)
|
|
1271
|
+
} catch {}
|
|
1272
|
+
}
|
|
1273
|
+
if (process.platform === "win32") return raw
|
|
1274
|
+
return raw.replace(/\\(.)/g, "$1")
|
|
1275
|
+
})
|
|
1276
|
+
const isUrl = /^(https?):\/\//.test(filepath)
|
|
1277
|
+
if (!isUrl) {
|
|
1278
|
+
try {
|
|
1279
|
+
const mime = await Filesystem.mimeType(filepath)
|
|
1280
|
+
const filename = path.basename(filepath)
|
|
1281
|
+
if (mime === "image/svg+xml") {
|
|
1282
|
+
const content = await Filesystem.readText(filepath).catch(() => {})
|
|
1283
|
+
if (content) {
|
|
1284
|
+
pasteText(content, `[SVG: ${filename ?? "image"}]`)
|
|
1285
|
+
return
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
if (mime.startsWith("image/") || mime === "application/pdf") {
|
|
1289
|
+
const content = await Filesystem.readArrayBuffer(filepath)
|
|
1290
|
+
.then((buffer) => Buffer.from(buffer).toString("base64"))
|
|
1291
|
+
.catch(() => {})
|
|
1292
|
+
if (content) {
|
|
1293
|
+
await pasteAttachment({
|
|
1294
|
+
filename,
|
|
1295
|
+
filepath,
|
|
1296
|
+
mime,
|
|
1297
|
+
content,
|
|
1298
|
+
})
|
|
1299
|
+
return
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
} catch {}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const lineCount = (pastedContent.match(/\n/g)?.length ?? 0) + 1
|
|
1306
|
+
if (
|
|
1307
|
+
(lineCount >= 3 || pastedContent.length > 150) &&
|
|
1308
|
+
kv.get("paste_summary_enabled", !sync.data.config.experimental?.disable_paste_summary)
|
|
1309
|
+
) {
|
|
1310
|
+
pasteText(pastedContent, `[Pasted ~${lineCount} lines]`)
|
|
1311
|
+
return
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
input.insertText(normalizedText)
|
|
1315
|
+
|
|
1316
|
+
setTimeout(() => {
|
|
1317
|
+
if (!input || input.isDestroyed) return
|
|
1318
|
+
input.getLayoutNode().markDirty()
|
|
1319
|
+
renderer.requestRender()
|
|
1320
|
+
}, 0)
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
async function pasteAttachment(file: { filename?: string; filepath?: string; content: string; mime: string }) {
|
|
1324
|
+
const currentOffset = input.visualCursor.offset
|
|
1325
|
+
const extmarkStart = currentOffset
|
|
1326
|
+
const pdf = file.mime === "application/pdf"
|
|
1327
|
+
const count = store.prompt.parts.filter((x) => {
|
|
1328
|
+
if (x.type !== "file") return false
|
|
1329
|
+
if (pdf) return x.mime === "application/pdf"
|
|
1330
|
+
return x.mime.startsWith("image/")
|
|
1331
|
+
}).length
|
|
1332
|
+
const virtualText = pdf ? `[PDF ${count + 1}]` : `[Image ${count + 1}]`
|
|
1333
|
+
const extmarkEnd = extmarkStart + virtualText.length
|
|
1334
|
+
const textToInsert = virtualText + " "
|
|
1335
|
+
|
|
1336
|
+
input.insertText(textToInsert)
|
|
1337
|
+
|
|
1338
|
+
const extmarkId = input.extmarks.create({
|
|
1339
|
+
start: extmarkStart,
|
|
1340
|
+
end: extmarkEnd,
|
|
1341
|
+
virtual: true,
|
|
1342
|
+
styleId: pasteStyleId,
|
|
1343
|
+
typeId: promptPartTypeId,
|
|
1344
|
+
})
|
|
1345
|
+
|
|
1346
|
+
const part: Omit<FilePart, "id" | "messageID" | "sessionID"> = {
|
|
1347
|
+
type: "file" as const,
|
|
1348
|
+
mime: file.mime,
|
|
1349
|
+
filename: file.filename,
|
|
1350
|
+
url: `data:${file.mime};base64,${file.content}`,
|
|
1351
|
+
source: {
|
|
1352
|
+
type: "file",
|
|
1353
|
+
path: file.filepath ?? file.filename ?? "",
|
|
1354
|
+
text: {
|
|
1355
|
+
start: extmarkStart,
|
|
1356
|
+
end: extmarkEnd,
|
|
1357
|
+
value: virtualText,
|
|
1358
|
+
},
|
|
1359
|
+
},
|
|
1360
|
+
}
|
|
1361
|
+
setStore(
|
|
1362
|
+
produce((draft) => {
|
|
1363
|
+
const partIndex = draft.prompt.parts.length
|
|
1364
|
+
draft.prompt.parts.push(part)
|
|
1365
|
+
draft.extmarkToPartIndex.set(extmarkId, partIndex)
|
|
1366
|
+
}),
|
|
1367
|
+
)
|
|
1368
|
+
return
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function clearPrompt() {
|
|
1372
|
+
if (store.prompt.input.trim().length >= DRAFT_RETENTION_MIN_CHARS || store.prompt.parts.length > 0) {
|
|
1373
|
+
history.append({
|
|
1374
|
+
...store.prompt,
|
|
1375
|
+
mode: store.mode,
|
|
1376
|
+
})
|
|
1377
|
+
}
|
|
1378
|
+
input.clear()
|
|
1379
|
+
input.extmarks.clear()
|
|
1380
|
+
setStore("prompt", {
|
|
1381
|
+
input: "",
|
|
1382
|
+
parts: [],
|
|
1383
|
+
})
|
|
1384
|
+
setStore("extmarkToPartIndex", new Map())
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
const highlight = createMemo(() => {
|
|
1388
|
+
if (leader()) return theme.border
|
|
1389
|
+
if (store.mode === "shell") return theme.primary
|
|
1390
|
+
const agent = local.agent.current()
|
|
1391
|
+
if (!agent) return theme.border
|
|
1392
|
+
return local.agent.color(agent.name)
|
|
1393
|
+
})
|
|
1394
|
+
|
|
1395
|
+
const showVariant = createMemo(() => {
|
|
1396
|
+
const variants = local.model.variant.list()
|
|
1397
|
+
if (variants.length === 0) return false
|
|
1398
|
+
const current = local.model.variant.current()
|
|
1399
|
+
return !!current
|
|
1400
|
+
})
|
|
1401
|
+
|
|
1402
|
+
const agentMetaAlpha = createFadeIn(() => !!local.agent.current(), animationsEnabled)
|
|
1403
|
+
const modelMetaAlpha = createFadeIn(() => !!local.agent.current() && store.mode === "normal", animationsEnabled)
|
|
1404
|
+
const variantMetaAlpha = createFadeIn(
|
|
1405
|
+
() => !!local.agent.current() && store.mode === "normal" && showVariant(),
|
|
1406
|
+
animationsEnabled,
|
|
1407
|
+
)
|
|
1408
|
+
const borderHighlight = createMemo(() => tint(theme.border, highlight(), agentMetaAlpha()))
|
|
1409
|
+
|
|
1410
|
+
const placeholderText = createMemo(() => {
|
|
1411
|
+
if (props.showPlaceholder === false) return undefined
|
|
1412
|
+
if (store.mode === "shell") {
|
|
1413
|
+
if (!shell().length) return undefined
|
|
1414
|
+
const example = shell()[store.placeholder % shell().length]
|
|
1415
|
+
return `Run a command... "${example}"`
|
|
1416
|
+
}
|
|
1417
|
+
if (!list().length) return undefined
|
|
1418
|
+
return `Ask anything... "${list()[store.placeholder % list().length]}"`
|
|
1419
|
+
})
|
|
1420
|
+
|
|
1421
|
+
const workspaceLabel = createMemo<
|
|
1422
|
+
| { type: "new"; workspaceType: string }
|
|
1423
|
+
| { type: "existing"; workspaceType: string; workspaceName: string; status?: WorkspaceStatus }
|
|
1424
|
+
| undefined
|
|
1425
|
+
>(() => {
|
|
1426
|
+
const selected = workspaceSelection()
|
|
1427
|
+
if (!selected) {
|
|
1428
|
+
const workspaceID = defaultWorkspaceID()
|
|
1429
|
+
if (props.sessionID || !workspaceID) return
|
|
1430
|
+
const workspace = project.workspace.get(workspaceID)
|
|
1431
|
+
return {
|
|
1432
|
+
type: "existing",
|
|
1433
|
+
workspaceType: workspace?.type ?? "unknown",
|
|
1434
|
+
workspaceName: workspace?.name ?? workspaceID,
|
|
1435
|
+
status: project.workspace.status(workspaceID) ?? "error",
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (selected.type === "none") return
|
|
1439
|
+
if (props.sessionID && !workspaceCreating()) return
|
|
1440
|
+
if (selected.type === "new") {
|
|
1441
|
+
return {
|
|
1442
|
+
type: "new",
|
|
1443
|
+
workspaceType: selected.workspaceType,
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
return {
|
|
1447
|
+
type: "existing",
|
|
1448
|
+
workspaceType: selected.workspaceType,
|
|
1449
|
+
workspaceName: selected.workspaceName,
|
|
1450
|
+
status: selected.type === "existing" ? "connected" : undefined,
|
|
1451
|
+
}
|
|
1452
|
+
})
|
|
1453
|
+
|
|
1454
|
+
const spinnerDef = createMemo(() => {
|
|
1455
|
+
const agent = local.agent.current()
|
|
1456
|
+
const color = agent ? local.agent.color(agent.name) : theme.border
|
|
1457
|
+
return {
|
|
1458
|
+
frames: createFrames({
|
|
1459
|
+
color,
|
|
1460
|
+
style: "blocks",
|
|
1461
|
+
inactiveFactor: 0.6,
|
|
1462
|
+
// enableFading: false,
|
|
1463
|
+
minAlpha: 0.3,
|
|
1464
|
+
}),
|
|
1465
|
+
color: createColors({
|
|
1466
|
+
color,
|
|
1467
|
+
style: "blocks",
|
|
1468
|
+
inactiveFactor: 0.6,
|
|
1469
|
+
// enableFading: false,
|
|
1470
|
+
minAlpha: 0.3,
|
|
1471
|
+
}),
|
|
1472
|
+
}
|
|
1473
|
+
})
|
|
1474
|
+
|
|
1475
|
+
return (
|
|
1476
|
+
<>
|
|
1477
|
+
<box ref={(r: BoxRenderable) => (anchor = r)} visible={props.visible !== false}>
|
|
1478
|
+
<box
|
|
1479
|
+
border={["left"]}
|
|
1480
|
+
borderColor={borderHighlight()}
|
|
1481
|
+
customBorderChars={{
|
|
1482
|
+
...SplitBorder.customBorderChars,
|
|
1483
|
+
bottomLeft: "╹",
|
|
1484
|
+
}}
|
|
1485
|
+
>
|
|
1486
|
+
<box
|
|
1487
|
+
paddingLeft={2}
|
|
1488
|
+
paddingRight={2}
|
|
1489
|
+
paddingTop={1}
|
|
1490
|
+
flexShrink={0}
|
|
1491
|
+
backgroundColor={theme.backgroundElement}
|
|
1492
|
+
flexGrow={1}
|
|
1493
|
+
>
|
|
1494
|
+
<textarea
|
|
1495
|
+
placeholder={placeholderText()}
|
|
1496
|
+
placeholderColor={theme.textMuted}
|
|
1497
|
+
textColor={leader() ? theme.textMuted : theme.text}
|
|
1498
|
+
focusedTextColor={leader() ? theme.textMuted : theme.text}
|
|
1499
|
+
minHeight={1}
|
|
1500
|
+
maxHeight={6}
|
|
1501
|
+
onContentChange={() => {
|
|
1502
|
+
const value = input.plainText
|
|
1503
|
+
setStore("prompt", "input", value)
|
|
1504
|
+
auto()?.onInput(value)
|
|
1505
|
+
syncExtmarksWithPromptParts()
|
|
1506
|
+
setCursorVersion((value) => value + 1)
|
|
1507
|
+
}}
|
|
1508
|
+
onCursorChange={() => setCursorVersion((value) => value + 1)}
|
|
1509
|
+
onKeyDown={(e: { preventDefault(): void }) => {
|
|
1510
|
+
if (props.disabled) {
|
|
1511
|
+
e.preventDefault()
|
|
1512
|
+
return
|
|
1513
|
+
}
|
|
1514
|
+
}}
|
|
1515
|
+
onSubmit={() => {
|
|
1516
|
+
// IME: double-defer so the last composed character (e.g. Korean
|
|
1517
|
+
// hangul) is flushed to plainText before we read it for submission.
|
|
1518
|
+
setTimeout(() => setTimeout(() => submit(), 0), 0)
|
|
1519
|
+
}}
|
|
1520
|
+
onPaste={async (event: PasteEvent) => {
|
|
1521
|
+
if (props.disabled) {
|
|
1522
|
+
event.preventDefault()
|
|
1523
|
+
return
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Normalize line endings at the boundary
|
|
1527
|
+
// Windows ConPTY/Terminal often sends CR-only newlines in bracketed paste
|
|
1528
|
+
// Replace CRLF first, then any remaining CR
|
|
1529
|
+
const normalizedText = decodePasteBytes(event.bytes).replace(/\r\n/g, "\n").replace(/\r/g, "\n")
|
|
1530
|
+
const pastedContent = normalizedText.trim()
|
|
1531
|
+
|
|
1532
|
+
// Windows Terminal <1.25 can surface image-only clipboard as an
|
|
1533
|
+
// empty bracketed paste. Windows Terminal 1.25+ does not.
|
|
1534
|
+
if (!pastedContent) {
|
|
1535
|
+
keymap.dispatchCommand("prompt.paste")
|
|
1536
|
+
return
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// Once we cross an async boundary below, the terminal may perform its
|
|
1540
|
+
// default paste unless we suppress it first and handle insertion ourselves.
|
|
1541
|
+
event.preventDefault()
|
|
1542
|
+
|
|
1543
|
+
await pasteInputText(normalizedText)
|
|
1544
|
+
}}
|
|
1545
|
+
ref={(r: TextareaRenderable) => {
|
|
1546
|
+
input = r
|
|
1547
|
+
setInputTarget(r)
|
|
1548
|
+
if (promptPartTypeId === 0) {
|
|
1549
|
+
promptPartTypeId = input.extmarks.registerType("prompt-part")
|
|
1550
|
+
}
|
|
1551
|
+
props.ref?.(ref)
|
|
1552
|
+
setTimeout(() => {
|
|
1553
|
+
// setTimeout is a workaround and needs to be addressed properly
|
|
1554
|
+
if (!input || input.isDestroyed) return
|
|
1555
|
+
input.cursorColor = theme.text
|
|
1556
|
+
}, 0)
|
|
1557
|
+
}}
|
|
1558
|
+
onMouseDown={(r: MouseEvent) => r.target?.focus()}
|
|
1559
|
+
focusedBackgroundColor={theme.backgroundElement}
|
|
1560
|
+
cursorColor={props.disabled ? theme.backgroundElement : theme.text}
|
|
1561
|
+
syntaxStyle={syntax()}
|
|
1562
|
+
/>
|
|
1563
|
+
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1} justifyContent="space-between">
|
|
1564
|
+
<box flexDirection="row" gap={1}>
|
|
1565
|
+
<Show when={local.agent.current()} fallback={<box height={1} />}>
|
|
1566
|
+
{(agent) => (
|
|
1567
|
+
<>
|
|
1568
|
+
<text fg={fadeColor(highlight(), agentMetaAlpha())}>
|
|
1569
|
+
{store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)}
|
|
1570
|
+
</text>
|
|
1571
|
+
<Show when={store.mode === "normal"}>
|
|
1572
|
+
<box flexDirection="row" gap={1}>
|
|
1573
|
+
<text fg={fadeColor(theme.textMuted, modelMetaAlpha())}>·</text>
|
|
1574
|
+
<text
|
|
1575
|
+
flexShrink={0}
|
|
1576
|
+
fg={fadeColor(leader() ? theme.textMuted : theme.text, modelMetaAlpha())}
|
|
1577
|
+
>
|
|
1578
|
+
{local.model.parsed().model}
|
|
1579
|
+
</text>
|
|
1580
|
+
<text fg={fadeColor(theme.textMuted, modelMetaAlpha())}>{currentProviderLabel()}</text>
|
|
1581
|
+
<Show when={showVariant()}>
|
|
1582
|
+
<text fg={fadeColor(theme.textMuted, variantMetaAlpha())}>·</text>
|
|
1583
|
+
<text>
|
|
1584
|
+
<span style={{ fg: fadeColor(theme.warning, variantMetaAlpha()), bold: true }}>
|
|
1585
|
+
{local.model.variant.current()}
|
|
1586
|
+
</span>
|
|
1587
|
+
</text>
|
|
1588
|
+
</Show>
|
|
1589
|
+
</box>
|
|
1590
|
+
</Show>
|
|
1591
|
+
</>
|
|
1592
|
+
)}
|
|
1593
|
+
</Show>
|
|
1594
|
+
</box>
|
|
1595
|
+
<Show when={hasRightContent()}>
|
|
1596
|
+
<box flexDirection="row" gap={1} alignItems="center">
|
|
1597
|
+
{props.right}
|
|
1598
|
+
</box>
|
|
1599
|
+
</Show>
|
|
1600
|
+
</box>
|
|
1601
|
+
</box>
|
|
1602
|
+
</box>
|
|
1603
|
+
<box
|
|
1604
|
+
height={1}
|
|
1605
|
+
border={["left"]}
|
|
1606
|
+
borderColor={borderHighlight()}
|
|
1607
|
+
customBorderChars={{
|
|
1608
|
+
...EmptyBorder,
|
|
1609
|
+
vertical: theme.backgroundElement.a !== 0 ? "╹" : " ",
|
|
1610
|
+
}}
|
|
1611
|
+
>
|
|
1612
|
+
<box
|
|
1613
|
+
height={1}
|
|
1614
|
+
border={["bottom"]}
|
|
1615
|
+
borderColor={theme.backgroundElement}
|
|
1616
|
+
customBorderChars={
|
|
1617
|
+
theme.backgroundElement.a !== 0
|
|
1618
|
+
? {
|
|
1619
|
+
...EmptyBorder,
|
|
1620
|
+
horizontal: "▀",
|
|
1621
|
+
}
|
|
1622
|
+
: {
|
|
1623
|
+
...EmptyBorder,
|
|
1624
|
+
horizontal: " ",
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
/>
|
|
1628
|
+
</box>
|
|
1629
|
+
<box width="100%" flexDirection="row" justifyContent="space-between">
|
|
1630
|
+
<Switch>
|
|
1631
|
+
<Match when={status().type !== "idle"}>
|
|
1632
|
+
<box
|
|
1633
|
+
flexDirection="row"
|
|
1634
|
+
gap={1}
|
|
1635
|
+
flexGrow={1}
|
|
1636
|
+
justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
|
|
1637
|
+
>
|
|
1638
|
+
<box flexShrink={0} flexDirection="row" gap={1}>
|
|
1639
|
+
<box marginLeft={1}>
|
|
1640
|
+
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={theme.textMuted}>[⋯]</text>}>
|
|
1641
|
+
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
|
|
1642
|
+
</Show>
|
|
1643
|
+
</box>
|
|
1644
|
+
<box flexDirection="row" gap={1} flexShrink={0}>
|
|
1645
|
+
{(() => {
|
|
1646
|
+
const retry = createMemo(() => {
|
|
1647
|
+
const s = status()
|
|
1648
|
+
if (s.type !== "retry") return
|
|
1649
|
+
return s
|
|
1650
|
+
})
|
|
1651
|
+
const message = createMemo(() => {
|
|
1652
|
+
const r = retry()
|
|
1653
|
+
if (!r) return
|
|
1654
|
+
if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
|
|
1655
|
+
return "gemini is way too hot right now"
|
|
1656
|
+
if (r.message.length > 80) return r.message.slice(0, 80) + "..."
|
|
1657
|
+
return r.message
|
|
1658
|
+
})
|
|
1659
|
+
const isTruncated = createMemo(() => {
|
|
1660
|
+
const r = retry()
|
|
1661
|
+
if (!r) return false
|
|
1662
|
+
return r.message.length > 120
|
|
1663
|
+
})
|
|
1664
|
+
const [seconds, setSeconds] = createSignal(0)
|
|
1665
|
+
onMount(() => {
|
|
1666
|
+
const timer = setInterval(() => {
|
|
1667
|
+
const next = retry()?.next
|
|
1668
|
+
if (next) setSeconds(Math.round((next - Date.now()) / 1000))
|
|
1669
|
+
}, 1000)
|
|
1670
|
+
|
|
1671
|
+
onCleanup(() => {
|
|
1672
|
+
clearInterval(timer)
|
|
1673
|
+
})
|
|
1674
|
+
})
|
|
1675
|
+
const handleMessageClick = () => {
|
|
1676
|
+
const r = retry()
|
|
1677
|
+
if (!r) return
|
|
1678
|
+
if (isTruncated()) {
|
|
1679
|
+
void DialogAlert.show(dialog, "Retry Error", r.message)
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
const retryText = () => {
|
|
1684
|
+
const r = retry()
|
|
1685
|
+
if (!r) return ""
|
|
1686
|
+
const baseMessage = message()
|
|
1687
|
+
const truncatedHint = isTruncated() ? " (click to expand)" : ""
|
|
1688
|
+
const duration = formatDuration(seconds())
|
|
1689
|
+
const retryInfo = ` [retrying ${duration ? `in ${duration} ` : ""}attempt #${r.attempt}]`
|
|
1690
|
+
return baseMessage + truncatedHint + retryInfo
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
return (
|
|
1694
|
+
<Show when={retry()}>
|
|
1695
|
+
<box onMouseUp={handleMessageClick}>
|
|
1696
|
+
<text fg={theme.error}>{retryText()}</text>
|
|
1697
|
+
</box>
|
|
1698
|
+
</Show>
|
|
1699
|
+
)
|
|
1700
|
+
})()}
|
|
1701
|
+
</box>
|
|
1702
|
+
</box>
|
|
1703
|
+
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
|
|
1704
|
+
esc{" "}
|
|
1705
|
+
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
|
|
1706
|
+
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
|
|
1707
|
+
</span>
|
|
1708
|
+
</text>
|
|
1709
|
+
</box>
|
|
1710
|
+
</Match>
|
|
1711
|
+
<Match when={warpNotice()}>
|
|
1712
|
+
{(notice) => (
|
|
1713
|
+
<box paddingLeft={3}>
|
|
1714
|
+
<text fg={theme.accent}>{notice()}</text>
|
|
1715
|
+
</box>
|
|
1716
|
+
)}
|
|
1717
|
+
</Match>
|
|
1718
|
+
<Match when={workspaceLabel()}>
|
|
1719
|
+
{(workspace) => (
|
|
1720
|
+
<box paddingLeft={3} flexDirection="row" gap={1}>
|
|
1721
|
+
<Show when={workspaceCreating()}>
|
|
1722
|
+
<Spinner color={theme.accent} />
|
|
1723
|
+
</Show>
|
|
1724
|
+
<text fg={workspaceCreating() ? theme.accent : theme.text}>
|
|
1725
|
+
{(() => {
|
|
1726
|
+
const item = workspace()
|
|
1727
|
+
if (item.type === "new") {
|
|
1728
|
+
if (workspaceCreating())
|
|
1729
|
+
return `Creating ${item.workspaceType}${".".repeat(workspaceCreatingDots())}`
|
|
1730
|
+
return (
|
|
1731
|
+
<>
|
|
1732
|
+
Workspace <span style={{ fg: theme.textMuted }}>(new {item.workspaceType})</span>
|
|
1733
|
+
</>
|
|
1734
|
+
)
|
|
1735
|
+
}
|
|
1736
|
+
return (
|
|
1737
|
+
<>
|
|
1738
|
+
Workspace <span style={{ fg: theme.textMuted }}>{item.workspaceName}</span>
|
|
1739
|
+
</>
|
|
1740
|
+
)
|
|
1741
|
+
})()}
|
|
1742
|
+
</text>
|
|
1743
|
+
</box>
|
|
1744
|
+
)}
|
|
1745
|
+
</Match>
|
|
1746
|
+
<Match when={true}>{props.hint ?? <text />}</Match>
|
|
1747
|
+
</Switch>
|
|
1748
|
+
<Show when={status().type !== "retry"}>
|
|
1749
|
+
<box gap={2} flexDirection="row">
|
|
1750
|
+
<Show when={editorContextLabelState() !== "none" ? editorFileLabelDisplay() : undefined}>
|
|
1751
|
+
{(file) => (
|
|
1752
|
+
<text fg={editorContextLabelState() === "pending" ? theme.secondary : theme.textMuted}>{file()}</text>
|
|
1753
|
+
)}
|
|
1754
|
+
</Show>
|
|
1755
|
+
<Switch>
|
|
1756
|
+
<Match when={store.mode === "normal"}>
|
|
1757
|
+
<Switch>
|
|
1758
|
+
<Match when={usage()}>
|
|
1759
|
+
{(item) => (
|
|
1760
|
+
<text fg={theme.textMuted} wrapMode="none">
|
|
1761
|
+
{[item().context, item().cost].filter(Boolean).join(" · ")}
|
|
1762
|
+
</text>
|
|
1763
|
+
)}
|
|
1764
|
+
</Match>
|
|
1765
|
+
<Match when={true}>
|
|
1766
|
+
<text fg={theme.text}>
|
|
1767
|
+
{agentShortcut()} <span style={{ fg: theme.textMuted }}>agents</span>
|
|
1768
|
+
</text>
|
|
1769
|
+
</Match>
|
|
1770
|
+
</Switch>
|
|
1771
|
+
<text fg={theme.text}>
|
|
1772
|
+
{paletteShortcut()} <span style={{ fg: theme.textMuted }}>commands</span>
|
|
1773
|
+
</text>
|
|
1774
|
+
</Match>
|
|
1775
|
+
<Match when={store.mode === "shell"}>
|
|
1776
|
+
<text fg={theme.text}>
|
|
1777
|
+
esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
|
|
1778
|
+
</text>
|
|
1779
|
+
</Match>
|
|
1780
|
+
</Switch>
|
|
1781
|
+
</box>
|
|
1782
|
+
</Show>
|
|
1783
|
+
</box>
|
|
1784
|
+
</box>
|
|
1785
|
+
<Autocomplete
|
|
1786
|
+
sessionID={props.sessionID}
|
|
1787
|
+
ref={(r) => {
|
|
1788
|
+
setAuto(() => r)
|
|
1789
|
+
}}
|
|
1790
|
+
anchor={() => anchor}
|
|
1791
|
+
input={() => input}
|
|
1792
|
+
setPrompt={(cb) => {
|
|
1793
|
+
setStore("prompt", produce(cb))
|
|
1794
|
+
}}
|
|
1795
|
+
setExtmark={(partIndex, extmarkId) => {
|
|
1796
|
+
setStore("extmarkToPartIndex", (map: Map<number, number>) => {
|
|
1797
|
+
const newMap = new Map(map)
|
|
1798
|
+
newMap.set(extmarkId, partIndex)
|
|
1799
|
+
return newMap
|
|
1800
|
+
})
|
|
1801
|
+
}}
|
|
1802
|
+
value={store.prompt.input}
|
|
1803
|
+
fileStyleId={fileStyleId}
|
|
1804
|
+
agentStyleId={agentStyleId}
|
|
1805
|
+
promptPartTypeId={() => promptPartTypeId}
|
|
1806
|
+
/>
|
|
1807
|
+
</>
|
|
1808
|
+
)
|
|
1809
|
+
}
|