dsh-code 1.0.1 → 1.0.3
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/README.en.md +21 -13
- package/README.md +21 -13
- package/lib/index.mjs +1902 -1092
- package/lib/types/app.d.ts +4 -13
- package/lib/types/attachments.d.ts +1 -1
- package/lib/types/editor-keys.d.ts +105 -0
- package/lib/types/git-workflow.d.ts +6 -2
- package/lib/types/keyboard.d.ts +31 -0
- package/lib/types/mentions.d.ts +2 -0
- package/lib/types/model-capabilities.d.ts +82 -0
- package/lib/types/provider-settings.d.ts +7 -0
- package/lib/types/render/animations.d.ts +27 -11
- package/lib/types/render/editor.d.ts +32 -7
- package/lib/types/render/lines.d.ts +26 -1
- package/lib/types/render/markdown.d.ts +1 -1
- package/lib/types/render/projection.d.ts +15 -1
- package/lib/types/render/text.d.ts +15 -9
- package/lib/types/render/width.d.ts +29 -0
- package/lib/types/session-directory.d.ts +27 -0
- package/lib/types/settings-file.d.ts +33 -0
- package/lib/types/store.d.ts +10 -0
- package/lib/types/subagents.d.ts +13 -3
- package/package.json +159 -159
- package/src/app.ts +920 -764
- package/src/attachments.ts +7 -0
- package/src/editor-keys.ts +371 -0
- package/src/git-workflow.ts +10 -6
- package/src/index.ts +1637 -1523
- package/src/internals.ts +26 -9
- package/src/keyboard.ts +131 -7
- package/src/mentions.ts +6 -1
- package/src/model-capabilities.ts +318 -0
- package/src/provider-settings.ts +16 -0
- package/src/render/animations.ts +64 -17
- package/src/render/editor.ts +125 -25
- package/src/render/lines.ts +403 -342
- package/src/render/markdown.ts +4 -7
- package/src/render/projection.ts +63 -40
- package/src/render/text.ts +152 -150
- package/src/render/width.ts +189 -0
- package/src/session-directory.ts +56 -0
- package/src/settings-file.ts +56 -0
- package/src/store.ts +26 -7
- package/src/subagents.ts +39 -6
package/src/app.ts
CHANGED
|
@@ -14,16 +14,16 @@
|
|
|
14
14
|
* @module @deepseek-ai/dsh-code/app
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { basename } from 'node:path'
|
|
18
|
-
import {
|
|
17
|
+
import { basename } from 'node:path'
|
|
18
|
+
import {
|
|
19
19
|
createElement, memo, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, type ReactElement,
|
|
20
20
|
} from 'react'
|
|
21
21
|
import { Box, Static, Text, useInput, useStdin, useStdout, type Key } from 'ink'
|
|
22
|
-
import type { CommandDescriptor } from '@deepseek-ai/dsh-commands'
|
|
23
|
-
import type { ImageBlock } from '@deepseek-ai/dsh-llm'
|
|
24
|
-
import type { TodoItem } from '@deepseek-ai/dsh-session'
|
|
25
|
-
import type { AskUserQuestionAnswerItem } from '@deepseek-ai/dsh-user-questions'
|
|
26
|
-
import type { AuthorizationInteraction, AuthorizationStatus } from '@deepseek-ai/dsh-authorization'
|
|
22
|
+
import type { CommandDescriptor } from '@deepseek-ai/dsh-commands'
|
|
23
|
+
import type { ImageBlock } from '@deepseek-ai/dsh-llm'
|
|
24
|
+
import type { TodoItem } from '@deepseek-ai/dsh-session'
|
|
25
|
+
import type { AskUserQuestionAnswerItem } from '@deepseek-ai/dsh-user-questions'
|
|
26
|
+
import type { AuthorizationInteraction, AuthorizationStatus } from '@deepseek-ai/dsh-authorization'
|
|
27
27
|
import {
|
|
28
28
|
dim,
|
|
29
29
|
getPalette,
|
|
@@ -41,7 +41,9 @@ import { settledEntryCount, type TranscriptEntry } from './render/projection.ts'
|
|
|
41
41
|
import { type MdSegment, visibleColumns } from './render/markdown.ts'
|
|
42
42
|
import {
|
|
43
43
|
busyChaseFrame,
|
|
44
|
+
BUSY_CHASE_TICK_MS,
|
|
44
45
|
caretVisible,
|
|
46
|
+
DEEP_DIVING_SHIMMER_TICK_MS,
|
|
45
47
|
DEEPSEEK_WAVE_TICK_MS,
|
|
46
48
|
deepseekWaveColumnBg,
|
|
47
49
|
deepseekWaveDuration,
|
|
@@ -50,6 +52,8 @@ import {
|
|
|
50
52
|
deepseekWaveTier,
|
|
51
53
|
deepseekWaveWordHue,
|
|
52
54
|
deepseekWaveWordVisible,
|
|
55
|
+
deepDivingGradientColor,
|
|
56
|
+
deepDivingSparkColor,
|
|
53
57
|
effortAboveHigh,
|
|
54
58
|
isOfficialDeepSeekLabel,
|
|
55
59
|
type DeepseekWaveStyle,
|
|
@@ -61,7 +65,7 @@ import type { ModelDirectory, ModelRow } from './models.ts'
|
|
|
61
65
|
import type { ProviderConfiguration, ProviderSettingsDirectory, ProviderTargetView } from './provider-settings.ts'
|
|
62
66
|
import type { QuestionSnapshot, QuestionStore } from './questions.ts'
|
|
63
67
|
import type { SkillsView, SkillRow } from './skills.ts'
|
|
64
|
-
import type
|
|
68
|
+
import { isPathLikeMentionQuery, type MentionCandidate } from './mentions.ts'
|
|
65
69
|
import type { SubagentFeedView, SubagentRow } from './subagents.ts'
|
|
66
70
|
import { AgentsPanel, EffortPanel, HistoryPanel, JobsPanel, ModePanel, PermissionPanel, PluginPanel, ResumePanel, StatuslinePanel, runClock, SubagentPanel, type JobRow } from './kernel-panels.ts'
|
|
67
71
|
import type { PresetRow } from './presets.ts'
|
|
@@ -76,19 +80,19 @@ import {
|
|
|
76
80
|
type RecallState,
|
|
77
81
|
} from './history.ts'
|
|
78
82
|
import type { SessionDirectoryOptions, SessionRow } from './session-directory.ts'
|
|
79
|
-
import type { GitDiffView } from './git-workflow.ts'
|
|
80
|
-
import {
|
|
81
|
-
authorizationForProvider,
|
|
82
|
-
providerAuthorizationStatus,
|
|
83
|
-
type ProviderAuthorizationDirectory,
|
|
84
|
-
type ProviderAuthorizationRow,
|
|
85
|
-
} from './authorization.ts'
|
|
86
|
-
import { ProviderAuthorizationLogoutPanel, ProviderAuthorizationPanel } from './authorization-panel.ts'
|
|
87
|
-
import {
|
|
88
|
-
looksLikeImagePath,
|
|
89
|
-
parsePastedImagePaths,
|
|
90
|
-
type ImagePathInspection,
|
|
91
|
-
} from './attachments.ts'
|
|
83
|
+
import type { GitDiffView } from './git-workflow.ts'
|
|
84
|
+
import {
|
|
85
|
+
authorizationForProvider,
|
|
86
|
+
providerAuthorizationStatus,
|
|
87
|
+
type ProviderAuthorizationDirectory,
|
|
88
|
+
type ProviderAuthorizationRow,
|
|
89
|
+
} from './authorization.ts'
|
|
90
|
+
import { ProviderAuthorizationLogoutPanel, ProviderAuthorizationPanel } from './authorization-panel.ts'
|
|
91
|
+
import {
|
|
92
|
+
looksLikeImagePath,
|
|
93
|
+
parsePastedImagePaths,
|
|
94
|
+
type ImagePathInspection,
|
|
95
|
+
} from './attachments.ts'
|
|
92
96
|
|
|
93
97
|
/** Match Codex's settled-resize window before rebuilding terminal scrollback. */
|
|
94
98
|
const RESIZE_REFLOW_DELAY_MS = 75
|
|
@@ -140,7 +144,16 @@ import {
|
|
|
140
144
|
type StatusTone,
|
|
141
145
|
} from './render/status.ts'
|
|
142
146
|
import { displayTail, displayText, singleLineText, truncateColumns } from './render/text.ts'
|
|
143
|
-
import {
|
|
147
|
+
import {
|
|
148
|
+
isVsCodeTerminalEnv,
|
|
149
|
+
normalizeKeyboardChunk,
|
|
150
|
+
PASTE_END_MARKER,
|
|
151
|
+
PASTE_START_MARKER,
|
|
152
|
+
stripPasteMarkers,
|
|
153
|
+
stripTerminalFocusEvents,
|
|
154
|
+
tokenizeRawEditorChunk,
|
|
155
|
+
type RawEditorToken,
|
|
156
|
+
} from './keyboard.ts'
|
|
144
157
|
import {
|
|
145
158
|
clampScroll,
|
|
146
159
|
followInspectorCursor,
|
|
@@ -152,6 +165,7 @@ import {
|
|
|
152
165
|
selectionWindow,
|
|
153
166
|
} from './render/inspector.ts'
|
|
154
167
|
import {
|
|
168
|
+
clampLiveAllocation,
|
|
155
169
|
lineSegment,
|
|
156
170
|
markdownLines,
|
|
157
171
|
settledEntryLines,
|
|
@@ -171,52 +185,57 @@ import {
|
|
|
171
185
|
deleteWordBackward,
|
|
172
186
|
deleteWordForward,
|
|
173
187
|
editorModel,
|
|
188
|
+
editorRowParts,
|
|
174
189
|
insertText,
|
|
175
190
|
type EditResult,
|
|
176
191
|
killToLineEnd,
|
|
177
192
|
killToLineStart,
|
|
178
|
-
lineBounds,
|
|
179
193
|
moveCursorBy,
|
|
180
194
|
moveCursorVertically,
|
|
195
|
+
moveToLineEnd,
|
|
196
|
+
moveToLineStart,
|
|
181
197
|
moveWordLeft,
|
|
182
198
|
moveWordRight,
|
|
199
|
+
remapStableRange,
|
|
200
|
+
replaceRangePreservingCursor,
|
|
183
201
|
sanitizeDraftText,
|
|
184
202
|
shouldRecallNavigate,
|
|
185
203
|
splitGraphemes,
|
|
186
204
|
} from './render/editor.ts'
|
|
187
205
|
|
|
188
|
-
/** Visual priority for one bounded local notice. */
|
|
189
|
-
export type NoticeTone = 'info' | 'warning' | 'error'
|
|
190
|
-
|
|
191
|
-
/** One source of truth for TUI-owned slash commands in completion and `/help`. */
|
|
192
|
-
const LOCAL_COMMANDS = [
|
|
193
|
-
{ label: '/help', description: 'show this overlay' },
|
|
194
|
-
{ label: '/model', description: 'switch the model and manage providers' },
|
|
195
|
-
{ label: '/effort', description: 'adjust reasoning effort for the current model' },
|
|
196
|
-
{ label: '/mode', description: 'inspect or select the agent preset (/mode [preset])' },
|
|
197
|
-
{ label: '/permission', description: 'inspect or select the permission preset (/permission [preset])' },
|
|
198
|
-
{ label: '/new', description: 'create and switch to a fresh session (/new [preset])' },
|
|
199
|
-
{ label: '/fork', description: 'fork at the latest completed turn (/fork [event-seq])' },
|
|
200
|
-
{ label: '/resume', description: 'browse or switch root sessions (/resume [id|prefix])' },
|
|
201
|
-
{ label: '/plugin', description: 'inspect the live plugin composition' },
|
|
202
|
-
{ label: '/jobs', description: 'inspect background jobs' },
|
|
203
|
-
{ label: '/statusline', description: 'customize the status line items' },
|
|
204
|
-
{ label: '/theme', description: 'switch the color theme' },
|
|
205
|
-
{ label: '/history', description: 'search and recall past prompts' },
|
|
206
|
-
{ label: '/agents', description: 'inspect subagent sessions of this conversation' },
|
|
207
|
-
{ label: '/todos', description: 'inspect the full todo list' },
|
|
208
|
-
{ label: '/subagent', description: 'choose the model delegated subagents run on' },
|
|
209
|
-
{ label: '/
|
|
210
|
-
{ label: '/
|
|
211
|
-
{ label: '/
|
|
212
|
-
{ label: '/
|
|
213
|
-
{ label: '/
|
|
214
|
-
{ label: '/
|
|
215
|
-
{ label: '/
|
|
216
|
-
{ label: '/
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
206
|
+
/** Visual priority for one bounded local notice. */
|
|
207
|
+
export type NoticeTone = 'info' | 'warning' | 'error'
|
|
208
|
+
|
|
209
|
+
/** One source of truth for TUI-owned slash commands in completion and `/help`. */
|
|
210
|
+
const LOCAL_COMMANDS = [
|
|
211
|
+
{ label: '/help', description: 'show this overlay' },
|
|
212
|
+
{ label: '/model', description: 'switch the model and manage providers' },
|
|
213
|
+
{ label: '/effort', description: 'adjust reasoning effort for the current model' },
|
|
214
|
+
{ label: '/mode', description: 'inspect or select the agent preset (/mode [preset])' },
|
|
215
|
+
{ label: '/permission', description: 'inspect or select the permission preset (/permission [preset])' },
|
|
216
|
+
{ label: '/new', description: 'create and switch to a fresh session (/new [preset])' },
|
|
217
|
+
{ label: '/fork', description: 'fork at the latest completed turn (/fork [event-seq])' },
|
|
218
|
+
{ label: '/resume', description: 'browse or switch root sessions (/resume [id|prefix])' },
|
|
219
|
+
{ label: '/plugin', description: 'inspect the live plugin composition' },
|
|
220
|
+
{ label: '/jobs', description: 'inspect background jobs' },
|
|
221
|
+
{ label: '/statusline', description: 'customize the status line items' },
|
|
222
|
+
{ label: '/theme', description: 'switch the color theme' },
|
|
223
|
+
{ label: '/history', description: 'search and recall past prompts' },
|
|
224
|
+
{ label: '/agents', description: 'inspect subagent sessions of this conversation' },
|
|
225
|
+
{ label: '/todos', description: 'inspect the full todo list' },
|
|
226
|
+
{ label: '/subagent', description: 'choose the model delegated subagents run on' },
|
|
227
|
+
{ label: '/vscode-keys', description: 'pass ctrl+r through the vs code terminal' },
|
|
228
|
+
{ label: '/delete', description: 'delete a session and its subagent threads' },
|
|
229
|
+
{ label: '/clear', description: 'clear the screen' },
|
|
230
|
+
{ label: '/export', description: 'export the transcript to markdown (/export [path])' },
|
|
231
|
+
{ label: '/title', description: 'rename this session (/title <text>)' },
|
|
232
|
+
{ label: '/copy', description: 'copy the latest assistant response' },
|
|
233
|
+
{ label: '/diff', description: 'inspect Git changes (/diff [--staged|ref])' },
|
|
234
|
+
{ label: '/review', description: 'review Git changes under read-only permissions' },
|
|
235
|
+
{ label: '/quit', description: 'exit' },
|
|
236
|
+
] as const
|
|
237
|
+
|
|
238
|
+
const LOCAL_COMMAND_NAMES = new Set(LOCAL_COMMANDS.map(command => command.label.slice(1)))
|
|
220
239
|
|
|
221
240
|
/** Props the runner hands the app; callbacks stay owned by the runner. */
|
|
222
241
|
export interface AppProps {
|
|
@@ -251,9 +270,9 @@ export interface AppProps {
|
|
|
251
270
|
/** Permission preset selected for the current or pending first session. */
|
|
252
271
|
permission: string
|
|
253
272
|
/** Submit one line: slash commands to the registry, other text to the agent. */
|
|
254
|
-
dispatch(text: string, images?: readonly ImageBlock[]): void
|
|
255
|
-
/** Submit steering: consumed at the running turn's next step boundary. */
|
|
256
|
-
steer(text: string, images?: readonly ImageBlock[]): void
|
|
273
|
+
dispatch(text: string, images?: readonly ImageBlock[]): void
|
|
274
|
+
/** Submit steering: consumed at the running turn's next step boundary. */
|
|
275
|
+
steer(text: string, images?: readonly ImageBlock[]): void
|
|
257
276
|
/** Interrupt the running turn (Esc); true when a turn was cancelled. */
|
|
258
277
|
interrupt(): boolean
|
|
259
278
|
/** Quit: unmount, flush, and request process exit. */
|
|
@@ -261,11 +280,11 @@ export interface AppProps {
|
|
|
261
280
|
/** Load the selectable model directory (called when /model opens). */
|
|
262
281
|
loadModels(): Promise<ModelDirectory>
|
|
263
282
|
/** Load @mention candidates for the typed query (files + sessions). */
|
|
264
|
-
loadMentions(query: string, signal?: AbortSignal): Promise<readonly MentionCandidate[]>
|
|
265
|
-
/** Validate draft image paths without committing attachment objects. */
|
|
266
|
-
inspectImages(paths: readonly string[]): Promise<readonly ImagePathInspection[]>
|
|
267
|
-
/** Validate, normalize and persist images immediately before submission. */
|
|
268
|
-
prepareImages(paths: readonly string[]): Promise<readonly ImageBlock[]>
|
|
283
|
+
loadMentions(query: string, signal?: AbortSignal): Promise<readonly MentionCandidate[]>
|
|
284
|
+
/** Validate draft image paths without committing attachment objects. */
|
|
285
|
+
inspectImages(paths: readonly string[]): Promise<readonly ImagePathInspection[]>
|
|
286
|
+
/** Validate, normalize and persist images immediately before submission. */
|
|
287
|
+
prepareImages(paths: readonly string[], signal?: AbortSignal): Promise<readonly ImageBlock[]>
|
|
269
288
|
/** Apply one /model selection (with an advertised reasoning effort, when picked); returns the display label. */
|
|
270
289
|
selectModel(row: ModelRow, effortId?: string): string
|
|
271
290
|
/** The /subagent override label, '' when delegated agents follow the current model. */
|
|
@@ -286,21 +305,21 @@ export interface AppProps {
|
|
|
286
305
|
unsetModelProviderCredential?(target: ProviderTargetView): Promise<void>
|
|
287
306
|
/** Remove one user-owned provider profile and its page-managed credential. */
|
|
288
307
|
removeModelProvider?(target: ProviderTargetView): Promise<void>
|
|
289
|
-
/** Save endpoint and explicit model capacities through the provider profile. */
|
|
290
|
-
saveModelProviderConfiguration?(target: ProviderTargetView, configuration: ProviderConfiguration): Promise<void>
|
|
291
|
-
/** Provider authorization flows and value-free stored-record facts. */
|
|
292
|
-
loadProviderAuthorizations?(): Promise<ProviderAuthorizationDirectory>
|
|
293
|
-
subscribeProviderAuthorizations?(listener: () => void): () => void
|
|
294
|
-
beginProviderAuthorization?(
|
|
295
|
-
row: ProviderAuthorizationRow,
|
|
296
|
-
method: string,
|
|
297
|
-
interaction: AuthorizationInteraction,
|
|
298
|
-
signal: AbortSignal,
|
|
299
|
-
): Promise<AuthorizationStatus>
|
|
300
|
-
cancelProviderAuthorization?(row: ProviderAuthorizationRow): void
|
|
301
|
-
logoutProviderAuthorization?(row: ProviderAuthorizationRow): Promise<void>
|
|
302
|
-
openAuthorizationUrl?(url: string): boolean
|
|
303
|
-
copyTextValue?(text: string): Promise<void>
|
|
308
|
+
/** Save endpoint and explicit model capacities through the provider profile. */
|
|
309
|
+
saveModelProviderConfiguration?(target: ProviderTargetView, configuration: ProviderConfiguration): Promise<void>
|
|
310
|
+
/** Provider authorization flows and value-free stored-record facts. */
|
|
311
|
+
loadProviderAuthorizations?(): Promise<ProviderAuthorizationDirectory>
|
|
312
|
+
subscribeProviderAuthorizations?(listener: () => void): () => void
|
|
313
|
+
beginProviderAuthorization?(
|
|
314
|
+
row: ProviderAuthorizationRow,
|
|
315
|
+
method: string,
|
|
316
|
+
interaction: AuthorizationInteraction,
|
|
317
|
+
signal: AbortSignal,
|
|
318
|
+
): Promise<AuthorizationStatus>
|
|
319
|
+
cancelProviderAuthorization?(row: ProviderAuthorizationRow): void
|
|
320
|
+
logoutProviderAuthorization?(row: ProviderAuthorizationRow): Promise<void>
|
|
321
|
+
openAuthorizationUrl?(url: string): boolean
|
|
322
|
+
copyTextValue?(text: string): Promise<void>
|
|
304
323
|
/** Cycle to the next permission preset (Shift+Tab); returns the new label. */
|
|
305
324
|
cyclePermission(): string
|
|
306
325
|
/** Select or inspect a permission preset without requiring a pre-existing session. */
|
|
@@ -346,6 +365,8 @@ export interface AppProps {
|
|
|
346
365
|
recordHistory(text: string): void
|
|
347
366
|
/** Cancel one queued inbox message by identity (Delete on the empty composer). */
|
|
348
367
|
cancelQueued(messageId: string): void
|
|
368
|
+
/** Apply the Ctrl+R terminal passthrough to the detected editor (/vscode-keys); resolves to a one-line summary. */
|
|
369
|
+
applyEditorKeys(): Promise<string>
|
|
349
370
|
}
|
|
350
371
|
|
|
351
372
|
/** Pad text with spaces to a visible-column target (menu name column). */
|
|
@@ -355,14 +376,15 @@ function padColumns(text: string, width: number): string {
|
|
|
355
376
|
}
|
|
356
377
|
|
|
357
378
|
/** Interval-driven frame counter for one self-contained animated leaf. */
|
|
358
|
-
function useFrames(intervalMs: number): number {
|
|
379
|
+
function useFrames(intervalMs: number, active = true): number {
|
|
359
380
|
const [tick, setTick] = useState(0)
|
|
360
381
|
useEffect(() => {
|
|
382
|
+
if (!active) return
|
|
361
383
|
const id = setInterval(() => setTick(current => current + 1), intervalMs)
|
|
362
384
|
return () => {
|
|
363
385
|
clearInterval(id)
|
|
364
386
|
}
|
|
365
|
-
}, [intervalMs])
|
|
387
|
+
}, [active, intervalMs])
|
|
366
388
|
return tick
|
|
367
389
|
}
|
|
368
390
|
|
|
@@ -381,14 +403,9 @@ function useStableInput(handler: (input: string, key: Key) => void, active: bool
|
|
|
381
403
|
useInput(stableHandler, { isActive: active })
|
|
382
404
|
}
|
|
383
405
|
|
|
384
|
-
/**
|
|
385
|
-
* The web StateDot "ongoing" chase in terminal form: three cells of the 3×3
|
|
386
|
-
* ring trail clockwise around the eight outer positions (8 frames × 125ms =
|
|
387
|
-
* the web's 1s cycle). Replaces the plain busy ellipsis as the composer's
|
|
388
|
-
* prompt marker and leads the Deep-diving line.
|
|
389
|
-
*/
|
|
406
|
+
/** The original web StateDot chase used by the busy composer marker. */
|
|
390
407
|
function BusyChase(): ReactElement {
|
|
391
|
-
const tick = useFrames(
|
|
408
|
+
const tick = useFrames(BUSY_CHASE_TICK_MS)
|
|
392
409
|
return createElement(Text, { color: inkColor(getPalette().brandBright) }, busyChaseFrame(tick) + ' ')
|
|
393
410
|
}
|
|
394
411
|
|
|
@@ -398,33 +415,65 @@ function Caret(): ReactElement {
|
|
|
398
415
|
return createElement(Text, null, caretVisible(tick) ? '▍' : ' ')
|
|
399
416
|
}
|
|
400
417
|
|
|
401
|
-
/**
|
|
402
|
-
function
|
|
403
|
-
const
|
|
404
|
-
|
|
418
|
+
/** One resettable input-caret phase shared by the entire composer. */
|
|
419
|
+
function useCursorBlink(active: boolean): { visible: boolean; reset(): void } {
|
|
420
|
+
const [epoch, setEpoch] = useState(0)
|
|
421
|
+
const [visible, setVisible] = useState(true)
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
setVisible(true)
|
|
424
|
+
if (!active) return
|
|
425
|
+
const id = setInterval(() => setVisible(current => !current), 530)
|
|
426
|
+
return () => {
|
|
427
|
+
clearInterval(id)
|
|
428
|
+
}
|
|
429
|
+
}, [active, epoch])
|
|
430
|
+
const reset = useCallback((): void => {
|
|
431
|
+
setVisible(true)
|
|
432
|
+
setEpoch(current => current + 1)
|
|
433
|
+
}, [])
|
|
434
|
+
return { visible, reset }
|
|
405
435
|
}
|
|
406
436
|
|
|
407
437
|
/**
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
438
|
+
* One bounded line painted with the deep-diving shimmer: a continuously
|
|
439
|
+
* moving blue gradient across graphemes, the `✻` glyph in the breathing
|
|
440
|
+
* spark color. Shared by the busy line and the collapsed thinking marker;
|
|
441
|
+
* always exactly one row (truncate-end) so the live budget stays exact.
|
|
412
442
|
*/
|
|
413
|
-
function
|
|
414
|
-
useFrames(
|
|
415
|
-
const
|
|
443
|
+
function ShimmerLine({ text }: { text: string }): ReactElement {
|
|
444
|
+
const tick = useFrames(DEEP_DIVING_SHIMMER_TICK_MS)
|
|
445
|
+
const palette = getPalette()
|
|
446
|
+
const graphemes = splitGraphemes(text)
|
|
416
447
|
return createElement(
|
|
417
|
-
|
|
418
|
-
{
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
448
|
+
Text,
|
|
449
|
+
{ wrap: 'truncate-end' },
|
|
450
|
+
...graphemes.map((grapheme, index) => {
|
|
451
|
+
const sparkle = grapheme.text === '✻'
|
|
452
|
+
return createElement(
|
|
453
|
+
Text,
|
|
454
|
+
{
|
|
455
|
+
key: `${grapheme.start}-${grapheme.end}`,
|
|
456
|
+
color: inkColor(sparkle ? deepDivingSparkColor(tick, palette.brandDeep, palette.brandBright) : deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep, palette.brandBright)),
|
|
457
|
+
bold: sparkle || undefined,
|
|
458
|
+
},
|
|
459
|
+
grapheme.text,
|
|
460
|
+
)
|
|
461
|
+
}),
|
|
425
462
|
)
|
|
426
463
|
}
|
|
427
464
|
|
|
465
|
+
/**
|
|
466
|
+
* The busy line, web TurnStatus contract: a continuously moving blue gradient
|
|
467
|
+
* paints the complete `Deep diving...` label, with the elapsed clock appended
|
|
468
|
+
* only once the turn has clearly been running (15s) — anchored to `turn/start`
|
|
469
|
+
* so a resumed mid-turn keeps the real time.
|
|
470
|
+
*/
|
|
471
|
+
function DeepDivingLine({ since }: { since: number }): ReactElement {
|
|
472
|
+
const elapsed = since === 0 ? 0 : Date.now() - since
|
|
473
|
+
const text = elapsed >= 15_000 ? `✻ Deep diving... ${runClock(elapsed)}` : '✻ Deep diving...'
|
|
474
|
+
return createElement(ShimmerLine, { text })
|
|
475
|
+
}
|
|
476
|
+
|
|
428
477
|
/**
|
|
429
478
|
* The streaming buffer rendered with a hard size cap: the live region must
|
|
430
479
|
* ALWAYS fit the terminal, or Ink's erase/rewrite of a dynamic tree taller
|
|
@@ -664,11 +713,12 @@ function todoMark(status: TodoItem['status']): string {
|
|
|
664
713
|
|
|
665
714
|
/**
|
|
666
715
|
* One-row live subagent summary (the Codex agent status feed, compressed to
|
|
667
|
-
* the transcript's budget): running count, total
|
|
668
|
-
*
|
|
669
|
-
* the
|
|
716
|
+
* the transcript's budget): running count, the observed total (the row cap
|
|
717
|
+
* is a display budget, not the fan-out size), and the most recently active
|
|
718
|
+
* child's current activity. One line, never more — the full view is the
|
|
719
|
+
* /agents panel.
|
|
670
720
|
*/
|
|
671
|
-
function AgentsLine({ rows }: { rows: readonly SubagentRow[] }): ReactElement | undefined {
|
|
721
|
+
function AgentsLine({ rows, total }: { rows: readonly SubagentRow[]; total: number }): ReactElement | undefined {
|
|
672
722
|
if (rows.length === 0) return undefined
|
|
673
723
|
const running = rows.filter(row => row.state !== 'done').length
|
|
674
724
|
const newest = [...rows].sort((left, right) => right.updatedAt - left.updatedAt)[0]!
|
|
@@ -680,7 +730,7 @@ function AgentsLine({ rows }: { rows: readonly SubagentRow[] }): ReactElement |
|
|
|
680
730
|
Text,
|
|
681
731
|
{ color: inkColor(getPalette().brand), bold: true, wrap: 'truncate-end' },
|
|
682
732
|
`agents ${running} live`,
|
|
683
|
-
createElement(Text, { color: inkColor(getPalette().dim) }, ` · ${
|
|
733
|
+
createElement(Text, { color: inkColor(getPalette().dim) }, ` · ${total} total · /agents`),
|
|
684
734
|
createElement(Text, { color: inkColor(getPalette().text) }, ` · ${mark} ${newest.label} ${newest.activity}`),
|
|
685
735
|
),
|
|
686
736
|
)
|
|
@@ -1488,10 +1538,10 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1488
1538
|
createElement(Text, { color: inkColor(getPalette().brand), bold: true, wrap: 'truncate-end' }, truncateColumns(`/model — select model${rows.length === 0 ? '' : ` · ${cursor + 1}/${rows.length}`}`, viewport.contentColumns)),
|
|
1489
1539
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1490
1540
|
...visibleStateRows,
|
|
1491
|
-
...visible.map((row) => {
|
|
1492
|
-
const index = rows.indexOf(row)
|
|
1493
|
-
const capability = row.inputModalities?.includes('image') === true ? ' · image' : ''
|
|
1494
|
-
const label = displayText(`${row.providerName} · ${row.modelName}${capability}`)
|
|
1541
|
+
...visible.map((row) => {
|
|
1542
|
+
const index = rows.indexOf(row)
|
|
1543
|
+
const capability = row.inputModalities?.includes('image') === true ? ' · image' : ''
|
|
1544
|
+
const label = displayText(`${row.providerName} · ${row.modelName}${capability}`)
|
|
1495
1545
|
return createElement(
|
|
1496
1546
|
Text,
|
|
1497
1547
|
{
|
|
@@ -1521,17 +1571,17 @@ function providerStateLabel(row: ProviderTargetView): string {
|
|
|
1521
1571
|
}
|
|
1522
1572
|
|
|
1523
1573
|
/** The provider-management stage reached from /model with `a`. */
|
|
1524
|
-
function ProviderPanel({ directory, error, authorizations, authorizationError, onCredential, onConfigure, onUnset, onRemove, onLogin, onLogout, onRetry, onBack }: {
|
|
1525
|
-
directory: ProviderSettingsDirectory | undefined
|
|
1526
|
-
error: string | undefined
|
|
1527
|
-
authorizations: ProviderAuthorizationDirectory | undefined
|
|
1528
|
-
authorizationError: string | undefined
|
|
1529
|
-
onCredential(target: ProviderTargetView): void
|
|
1530
|
-
onConfigure(target: ProviderTargetView): void
|
|
1531
|
-
onUnset(target: ProviderTargetView): void
|
|
1532
|
-
onRemove(target: ProviderTargetView): void
|
|
1533
|
-
onLogin(target: ProviderTargetView, authorization: ProviderAuthorizationRow): void
|
|
1534
|
-
onLogout(target: ProviderTargetView, authorization: ProviderAuthorizationRow): void
|
|
1574
|
+
function ProviderPanel({ directory, error, authorizations, authorizationError, onCredential, onConfigure, onUnset, onRemove, onLogin, onLogout, onRetry, onBack }: {
|
|
1575
|
+
directory: ProviderSettingsDirectory | undefined
|
|
1576
|
+
error: string | undefined
|
|
1577
|
+
authorizations: ProviderAuthorizationDirectory | undefined
|
|
1578
|
+
authorizationError: string | undefined
|
|
1579
|
+
onCredential(target: ProviderTargetView): void
|
|
1580
|
+
onConfigure(target: ProviderTargetView): void
|
|
1581
|
+
onUnset(target: ProviderTargetView): void
|
|
1582
|
+
onRemove(target: ProviderTargetView): void
|
|
1583
|
+
onLogin(target: ProviderTargetView, authorization: ProviderAuthorizationRow): void
|
|
1584
|
+
onLogout(target: ProviderTargetView, authorization: ProviderAuthorizationRow): void
|
|
1535
1585
|
onRetry(): void
|
|
1536
1586
|
onBack(): void
|
|
1537
1587
|
}): ReactElement {
|
|
@@ -1598,27 +1648,27 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1598
1648
|
}
|
|
1599
1649
|
return
|
|
1600
1650
|
}
|
|
1601
|
-
if (input === 'x') {
|
|
1651
|
+
if (input === 'x') {
|
|
1602
1652
|
if (!target.removable) {
|
|
1603
1653
|
setActionError('this provider profile is not removable')
|
|
1604
1654
|
} else {
|
|
1605
1655
|
onRemove(target)
|
|
1606
1656
|
}
|
|
1607
|
-
return
|
|
1608
|
-
}
|
|
1609
|
-
const authorization = authorizationForProvider(authorizations, target.provider)
|
|
1610
|
-
if (input === 'l' || input === 'L') {
|
|
1611
|
-
if (authorization === undefined) setActionError('this provider offers no interactive login flow')
|
|
1612
|
-
else if (authorization.inFlight) setActionError('a login attempt is already running for this provider')
|
|
1613
|
-
else onLogin(target, authorization)
|
|
1614
|
-
return
|
|
1615
|
-
}
|
|
1616
|
-
if (input === 'o' || input === 'O') {
|
|
1617
|
-
if (authorization === undefined || !authorization.record.configured) setActionError('this provider has no login record to remove')
|
|
1618
|
-
else if (!authorization.record.writable) setActionError('this login record is read-only')
|
|
1619
|
-
else onLogout(target, authorization)
|
|
1620
|
-
return
|
|
1621
|
-
}
|
|
1657
|
+
return
|
|
1658
|
+
}
|
|
1659
|
+
const authorization = authorizationForProvider(authorizations, target.provider)
|
|
1660
|
+
if (input === 'l' || input === 'L') {
|
|
1661
|
+
if (authorization === undefined) setActionError('this provider offers no interactive login flow')
|
|
1662
|
+
else if (authorization.inFlight) setActionError('a login attempt is already running for this provider')
|
|
1663
|
+
else onLogin(target, authorization)
|
|
1664
|
+
return
|
|
1665
|
+
}
|
|
1666
|
+
if (input === 'o' || input === 'O') {
|
|
1667
|
+
if (authorization === undefined || !authorization.record.configured) setActionError('this provider has no login record to remove')
|
|
1668
|
+
else if (!authorization.record.writable) setActionError('this login record is read-only')
|
|
1669
|
+
else onLogout(target, authorization)
|
|
1670
|
+
return
|
|
1671
|
+
}
|
|
1622
1672
|
if (key.return) {
|
|
1623
1673
|
if (target.settingsNs.length === 0) {
|
|
1624
1674
|
setActionError('this provider is not managed by Harness settings')
|
|
@@ -1646,19 +1696,19 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1646
1696
|
...(actionError === undefined
|
|
1647
1697
|
? []
|
|
1648
1698
|
: [createElement(Text, { key: 'action-error', color: inkColor(getPalette().error), wrap: 'truncate-end' }, truncateColumns(` ${actionError}`, viewport.contentColumns))]),
|
|
1649
|
-
...(directory?.failures ?? []).map((failure, index) => createElement(
|
|
1699
|
+
...(directory?.failures ?? []).map((failure, index) => createElement(
|
|
1650
1700
|
Text,
|
|
1651
1701
|
{ key: `failure-${index}`, color: inkColor(getPalette().warn), wrap: 'truncate-end' },
|
|
1652
|
-
truncateColumns(` ${singleLineText(failure)}`, viewport.contentColumns),
|
|
1653
|
-
)),
|
|
1654
|
-
...(authorizationError === undefined
|
|
1655
|
-
? []
|
|
1656
|
-
: [createElement(Text, { key: 'authorization-error', color: inkColor(getPalette().warn), wrap: 'truncate-end' }, truncateColumns(` login status unavailable: ${singleLineText(authorizationError)}`, viewport.contentColumns))]),
|
|
1657
|
-
...(authorizations?.failures ?? []).map((failure, index) => createElement(
|
|
1658
|
-
Text,
|
|
1659
|
-
{ key: `authorization-failure-${index}`, color: inkColor(getPalette().warn), wrap: 'truncate-end' },
|
|
1660
|
-
truncateColumns(` ${singleLineText(failure)}`, viewport.contentColumns),
|
|
1661
|
-
)),
|
|
1702
|
+
truncateColumns(` ${singleLineText(failure)}`, viewport.contentColumns),
|
|
1703
|
+
)),
|
|
1704
|
+
...(authorizationError === undefined
|
|
1705
|
+
? []
|
|
1706
|
+
: [createElement(Text, { key: 'authorization-error', color: inkColor(getPalette().warn), wrap: 'truncate-end' }, truncateColumns(` login status unavailable: ${singleLineText(authorizationError)}`, viewport.contentColumns))]),
|
|
1707
|
+
...(authorizations?.failures ?? []).map((failure, index) => createElement(
|
|
1708
|
+
Text,
|
|
1709
|
+
{ key: `authorization-failure-${index}`, color: inkColor(getPalette().warn), wrap: 'truncate-end' },
|
|
1710
|
+
truncateColumns(` ${singleLineText(failure)}`, viewport.contentColumns),
|
|
1711
|
+
)),
|
|
1662
1712
|
...(rows.length === 0
|
|
1663
1713
|
? [createElement(Text, { key: 'empty', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, ' no configurable providers')]
|
|
1664
1714
|
: []),
|
|
@@ -1674,13 +1724,13 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1674
1724
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1675
1725
|
...visibleStateRows,
|
|
1676
1726
|
...visible.map((row) => {
|
|
1677
|
-
const index = rows.indexOf(row)
|
|
1678
|
-
const identity = row.displayName === row.provider ? row.provider : `${row.displayName} (${row.provider})`
|
|
1679
|
-
const authorization = authorizationForProvider(authorizations, row.provider)
|
|
1680
|
-
const manualKeyConfigured = row.credential?.kind === 'facts' && row.credential.configured
|
|
1681
|
-
const showAuthorization = !manualKeyConfigured || authorization?.record.configured === true || authorization?.inFlight === true
|
|
1682
|
-
const authLabel = showAuthorization ? ` · ${providerAuthorizationStatus(authorization)}` : ''
|
|
1683
|
-
const label = `${identity} · ${providerStateLabel(row)}${authLabel}${row.removable ? ' · custom' : ''}`
|
|
1727
|
+
const index = rows.indexOf(row)
|
|
1728
|
+
const identity = row.displayName === row.provider ? row.provider : `${row.displayName} (${row.provider})`
|
|
1729
|
+
const authorization = authorizationForProvider(authorizations, row.provider)
|
|
1730
|
+
const manualKeyConfigured = row.credential?.kind === 'facts' && row.credential.configured
|
|
1731
|
+
const showAuthorization = !manualKeyConfigured || authorization?.record.configured === true || authorization?.inFlight === true
|
|
1732
|
+
const authLabel = showAuthorization ? ` · ${providerAuthorizationStatus(authorization)}` : ''
|
|
1733
|
+
const label = `${identity} · ${providerStateLabel(row)}${authLabel}${row.removable ? ' · custom' : ''}`
|
|
1684
1734
|
return createElement(
|
|
1685
1735
|
Text,
|
|
1686
1736
|
{ key: row.provider, color: index === cursor ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim), wrap: 'truncate-end' },
|
|
@@ -1688,7 +1738,7 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1688
1738
|
)
|
|
1689
1739
|
}),
|
|
1690
1740
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1691
|
-
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns('↑↓ move · enter key · l login · o logout · tab configure · d remove key · x remove provider · r retry · esc back', viewport.contentColumns)),
|
|
1741
|
+
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns('↑↓ move · enter key · l login · o logout · tab configure · d remove key · x remove provider · r retry · esc back', viewport.contentColumns)),
|
|
1692
1742
|
)
|
|
1693
1743
|
}
|
|
1694
1744
|
|
|
@@ -1969,7 +2019,7 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
1969
2019
|
createElement(Text, { key: 'keys-title', bold: true, wrap: 'truncate-end' }, ' keys'),
|
|
1970
2020
|
createElement(Text, { key: 'key-submit', dimColor: true, wrap: 'truncate-end' }, ' enter submit · up/down history · tab complete'),
|
|
1971
2021
|
createElement(Text, { key: 'key-mentions', dimColor: true, wrap: 'truncate-end' }, ' @ mentions workspace files and sessions'),
|
|
1972
|
-
createElement(Text, { key: 'key-inspector', dimColor: true, wrap: 'truncate-end' }, ' ctrl+o history details · ctrl+r thinking · shift+tab permission preset'),
|
|
2022
|
+
createElement(Text, { key: 'key-inspector', dimColor: true, wrap: 'truncate-end' }, ' ctrl+o history details · ctrl/alt+r thinking · shift+tab permission preset'),
|
|
1973
2023
|
createElement(Text, { key: 'key-cancel', dimColor: true, wrap: 'truncate-end' }, ' esc interrupt the running turn · ctrl+c cancel / clear / quit · ctrl+d exit'),
|
|
1974
2024
|
createElement(Text, { key: 'key-queue', dimColor: true, wrap: 'truncate-end' }, ' delete on the empty composer cancels the newest queued message'),
|
|
1975
2025
|
createElement(Text, { key: 'key-edit', dimColor: true, wrap: 'truncate-end' }, ' ctrl+k cut to end of line · ctrl+u clear line · ctrl+a / ctrl+e line ends'),
|
|
@@ -1982,12 +2032,12 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
1982
2032
|
{ key: 'commands-error', color: inkColor(getPalette().error), wrap: 'truncate-end' },
|
|
1983
2033
|
truncateColumns(` command catalog unavailable: ${singleLineText(commandError)}`, viewport.contentColumns),
|
|
1984
2034
|
)]),
|
|
1985
|
-
...LOCAL_COMMANDS.map(command => createElement(
|
|
1986
|
-
Box,
|
|
1987
|
-
{ key: `local-${command.label.slice(1)}` },
|
|
1988
|
-
row(command.label, command.description),
|
|
1989
|
-
)),
|
|
1990
|
-
...descriptors.filter(descriptor => !LOCAL_COMMAND_NAMES.has(descriptor.name)).map(descriptor => createElement(
|
|
2035
|
+
...LOCAL_COMMANDS.map(command => createElement(
|
|
2036
|
+
Box,
|
|
2037
|
+
{ key: `local-${command.label.slice(1)}` },
|
|
2038
|
+
row(command.label, command.description),
|
|
2039
|
+
)),
|
|
2040
|
+
...descriptors.filter(descriptor => !LOCAL_COMMAND_NAMES.has(descriptor.name)).map(descriptor => createElement(
|
|
1991
2041
|
Text,
|
|
1992
2042
|
{ key: `command-${descriptor.name}`, dimColor: true, wrap: 'truncate-end' },
|
|
1993
2043
|
` ${padColumns(`/${descriptor.name}`, nameWidth)}${dim(truncateColumns(displayText(descriptor.description), descBudget))}`,
|
|
@@ -2054,83 +2104,13 @@ function verboseLine(text: string, columns: number): string {
|
|
|
2054
2104
|
return truncateColumns(displayText(text).replace(/\n/gu, ' ↵ ').replace(/\t/gu, ' '), Math.max(1, columns))
|
|
2055
2105
|
}
|
|
2056
2106
|
|
|
2057
|
-
/**
|
|
2058
|
-
* Keys Ink 5's parser cannot express at the useInput boundary: Home/End
|
|
2059
|
-
* arrive with `input === ''` and no flag, and Backspace vs Delete both
|
|
2060
|
-
* collapse onto `key.delete`. The composer patches `stdin.read` — the one
|
|
2061
|
-
* choke point every Ink input chunk already passes through — and annotates
|
|
2062
|
-
* the exact sequences the editor must own; Ink's own view of the same chunk
|
|
2063
|
-
* is a no-op for every one of them.
|
|
2064
|
-
*/
|
|
2065
|
-
type RawKeyAnnotation =
|
|
2066
|
-
| 'home'
|
|
2067
|
-
| 'end'
|
|
2068
|
-
| 'delete-backward'
|
|
2069
|
-
| 'delete-word-backward'
|
|
2070
|
-
| 'delete-forward'
|
|
2071
|
-
| 'delete-word-forward'
|
|
2072
|
-
| undefined
|
|
2073
|
-
|
|
2074
|
-
/** Identify one whole-chunk key sequence Ink drops or blurs. */
|
|
2075
|
-
function annotateRawKey(chunk: string): RawKeyAnnotation {
|
|
2076
|
-
switch (chunk) {
|
|
2077
|
-
case '':
|
|
2078
|
-
return 'delete-backward'
|
|
2079
|
-
case '':
|
|
2080
|
-
case '':
|
|
2081
|
-
return 'delete-word-backward'
|
|
2082
|
-
case '[3~':
|
|
2083
|
-
case '[3;2~':
|
|
2084
|
-
return 'delete-forward'
|
|
2085
|
-
case '[3;3~':
|
|
2086
|
-
case '[3;5~':
|
|
2087
|
-
return 'delete-word-forward'
|
|
2088
|
-
case '[H':
|
|
2089
|
-
case '[1~':
|
|
2090
|
-
case '[7~':
|
|
2091
|
-
case 'OH':
|
|
2092
|
-
return 'home'
|
|
2093
|
-
case '[F':
|
|
2094
|
-
case '[4~':
|
|
2095
|
-
case '[8~':
|
|
2096
|
-
case 'OF':
|
|
2097
|
-
return 'end'
|
|
2098
|
-
default:
|
|
2099
|
-
return undefined
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
/**
|
|
2104
|
-
* One-row editor window keeping the logical cursor visible in long drafts.
|
|
2105
|
-
* The caret and its surroundings slice at grapheme boundaries: splitting a
|
|
2106
|
-
* star-plane surrogate pair would render an isolated half under the block
|
|
2107
|
-
* caret with a width the terminal never draws.
|
|
2108
|
-
*/
|
|
2109
|
-
export function editorWindow(value: string, cursor: number, columns: number): { before: string; caret: string; after: string } {
|
|
2110
|
-
const width = Math.max(1, columns)
|
|
2111
|
-
const normalize = (text: string): string => displayText(text).replace(/\n/gu, '↵').replace(/\t/gu, ' ')
|
|
2112
|
-
const site = clampCursor(value, cursor)
|
|
2113
|
-
const caretSpan = splitGraphemes(value).find(span => span.start === site)
|
|
2114
|
-
const caret = caretSpan === undefined ? ' ' : normalize(caretSpan.text)
|
|
2115
|
-
const rest = value.slice(caretSpan === undefined ? site : caretSpan.end)
|
|
2116
|
-
const remaining = Math.max(0, width - visibleColumns(caret))
|
|
2117
|
-
const afterBudget = Math.min(Math.floor(remaining / 3), visibleColumns(normalize(rest)))
|
|
2118
|
-
const beforeBudget = Math.max(0, remaining - afterBudget)
|
|
2119
|
-
const before = beforeBudget === 0
|
|
2120
|
-
? ''
|
|
2121
|
-
: displayTail(normalize(value.slice(0, site)), beforeBudget, 1).text
|
|
2122
|
-
const after = afterBudget === 0
|
|
2123
|
-
? ''
|
|
2124
|
-
: truncateColumns(normalize(rest), afterBudget)
|
|
2125
|
-
return { before, caret, after }
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
2107
|
/** The empty-composer placeholder text (shared by the static and wave paths). */
|
|
2129
2108
|
const COMPOSER_PLACEHOLDER = 'type a message · / commands · @ mentions'
|
|
2130
2109
|
|
|
2131
2110
|
/** One physical cell of the wave-painted composer row: a char plus styles. */
|
|
2132
2111
|
interface ComposerCell {
|
|
2133
2112
|
char: string
|
|
2113
|
+
width?: number
|
|
2134
2114
|
color?: string
|
|
2135
2115
|
backgroundColor?: string
|
|
2136
2116
|
bold?: boolean
|
|
@@ -2360,12 +2340,12 @@ export function completionCandidates(
|
|
|
2360
2340
|
): readonly CompletionCandidate[] {
|
|
2361
2341
|
if (!value.startsWith('/')) return []
|
|
2362
2342
|
const prefix = value.slice(1).split(' ')[0] ?? ''
|
|
2363
|
-
const local: CompletionCandidate[] = LOCAL_COMMANDS.map(command => ({ ...command, origin: 'command' }))
|
|
2343
|
+
const local: CompletionCandidate[] = LOCAL_COMMANDS.map(command => ({ ...command, origin: 'command' }))
|
|
2364
2344
|
// Local commands shadow registry names (e.g. the TUI-local /permission works
|
|
2365
2345
|
// before any session exists, while the registry child needs one), so
|
|
2366
2346
|
// collisions cannot render two rows with the same key.
|
|
2367
|
-
const registry = descriptors
|
|
2368
|
-
.filter(descriptor => !LOCAL_COMMAND_NAMES.has(descriptor.name))
|
|
2347
|
+
const registry = descriptors
|
|
2348
|
+
.filter(descriptor => !LOCAL_COMMAND_NAMES.has(descriptor.name))
|
|
2369
2349
|
.map((descriptor): CompletionCandidate => ({
|
|
2370
2350
|
label: `/${descriptor.name}`,
|
|
2371
2351
|
description: descriptor.description,
|
|
@@ -2455,25 +2435,25 @@ function CompletionMenu({ active, mention, index, rows }: {
|
|
|
2455
2435
|
)
|
|
2456
2436
|
}
|
|
2457
2437
|
|
|
2458
|
-
interface DraftImage extends ImagePathInspection {
|
|
2459
|
-
/** Visible draft token; deleting it also detaches the hidden path. */
|
|
2460
|
-
readonly marker: string
|
|
2461
|
-
}
|
|
2462
|
-
|
|
2463
|
-
/**
|
|
2464
|
-
* The prompt box: TUI-local slash commands handled locally, other lines
|
|
2438
|
+
interface DraftImage extends ImagePathInspection {
|
|
2439
|
+
/** Visible draft token; deleting it also detaches the hidden path. */
|
|
2440
|
+
readonly marker: string
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
/**
|
|
2444
|
+
* The prompt box: TUI-local slash commands handled locally, other lines
|
|
2465
2445
|
* dispatched; input editing keeps a cursor with history and completion.
|
|
2466
2446
|
* While a modal (approval / question / model panel) owns the keys, the
|
|
2467
2447
|
* box passes every key through untouched.
|
|
2468
2448
|
*/
|
|
2469
|
-
function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openJobs, openStatusline, openTheme, openHistory, openAgents, openSubagent, openTodos, openDelete, openDiff, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle, maxRows, onEditorRows }: {
|
|
2449
|
+
function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openJobs, openStatusline, openTheme, openHistory, openAgents, openSubagent, openTodos, openDelete, openDiff, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, applyEditorKeys, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle, maxRows, onEditorRows }: {
|
|
2470
2450
|
active: boolean
|
|
2471
2451
|
frozen: boolean
|
|
2472
2452
|
busy: boolean
|
|
2473
2453
|
descriptors: readonly CommandDescriptor[]
|
|
2474
2454
|
skills: readonly SkillRow[]
|
|
2475
|
-
dispatch(text: string, images?: readonly ImageBlock[]): void
|
|
2476
|
-
steer(text: string, images?: readonly ImageBlock[]): void
|
|
2455
|
+
dispatch(text: string, images?: readonly ImageBlock[]): void
|
|
2456
|
+
steer(text: string, images?: readonly ImageBlock[]): void
|
|
2477
2457
|
interrupt(): boolean
|
|
2478
2458
|
quit(): void
|
|
2479
2459
|
openModel(): void
|
|
@@ -2507,15 +2487,17 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2507
2487
|
forkSession(argument: string): void
|
|
2508
2488
|
cancelSessionSwitch(): boolean
|
|
2509
2489
|
notify(text: string, tone?: NoticeTone): void
|
|
2490
|
+
/** Apply the Ctrl+R passthrough to the detected editor (/vscode-keys); resolves to a one-line summary. */
|
|
2491
|
+
applyEditorKeys(): Promise<string>
|
|
2510
2492
|
hasNotice: boolean
|
|
2511
2493
|
dismissNotice(): void
|
|
2512
2494
|
toggleReasoning(): void
|
|
2513
2495
|
openVerbose(): void
|
|
2514
2496
|
clearView(): void
|
|
2515
2497
|
refresh(): void
|
|
2516
|
-
loadMentions(query: string, signal?: AbortSignal): Promise<readonly MentionCandidate[]>
|
|
2517
|
-
inspectImages(paths: readonly string[]): Promise<readonly ImagePathInspection[]>
|
|
2518
|
-
prepareImages(paths: readonly string[]): Promise<readonly ImageBlock[]>
|
|
2498
|
+
loadMentions(query: string, signal?: AbortSignal): Promise<readonly MentionCandidate[]>
|
|
2499
|
+
inspectImages(paths: readonly string[]): Promise<readonly ImagePathInspection[]>
|
|
2500
|
+
prepareImages(paths: readonly string[], signal?: AbortSignal): Promise<readonly ImageBlock[]>
|
|
2519
2501
|
cyclePermission(): string
|
|
2520
2502
|
exportTranscript(argument: string): Promise<void>
|
|
2521
2503
|
renameTitle(argument: string): string
|
|
@@ -2548,17 +2530,26 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2548
2530
|
onEditorRows(rows: number): void
|
|
2549
2531
|
}): ReactElement {
|
|
2550
2532
|
const columns = useStdout().stdout?.columns ?? 80
|
|
2551
|
-
const
|
|
2552
|
-
const
|
|
2553
|
-
const
|
|
2554
|
-
const
|
|
2555
|
-
const
|
|
2556
|
-
valueRef
|
|
2557
|
-
cursorRef
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
const
|
|
2533
|
+
const editorColumns = Math.max(1, columns - 6)
|
|
2534
|
+
const stdin = useStdin().stdin
|
|
2535
|
+
const focusReporting = isVsCodeTerminalEnv()
|
|
2536
|
+
const [value, setValue] = useState('')
|
|
2537
|
+
const [cursor, setCursor] = useState(0)
|
|
2538
|
+
const valueRef = useRef(value)
|
|
2539
|
+
const cursorRef = useRef(cursor)
|
|
2540
|
+
valueRef.current = value
|
|
2541
|
+
cursorRef.current = cursor
|
|
2542
|
+
const [draftImages, setDraftImages] = useState<readonly DraftImage[]>([])
|
|
2543
|
+
const draftImagesRef = useRef(draftImages)
|
|
2544
|
+
draftImagesRef.current = draftImages
|
|
2545
|
+
const [preparingImages, setPreparingImages] = useState(false)
|
|
2546
|
+
const prepareAbortRef = useRef<AbortController | undefined>(undefined)
|
|
2547
|
+
const prepareEpochRef = useRef(0)
|
|
2548
|
+
const { visible: cursorVisible, reset: resetCursorBlink } = useCursorBlink(active && !frozen && !preparingImages)
|
|
2549
|
+
useEffect(() => () => {
|
|
2550
|
+
prepareEpochRef.current += 1
|
|
2551
|
+
prepareAbortRef.current?.abort()
|
|
2552
|
+
}, [])
|
|
2562
2553
|
// Codex textarea editing state: a single-entry kill buffer, the vertical
|
|
2563
2554
|
// move's preferred display column, the editor's scroll window, and the
|
|
2564
2555
|
// bracketed-paste marker state. All of it is editor-local; nothing here
|
|
@@ -2569,21 +2560,30 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2569
2560
|
const pasteBracketRef = useRef(false)
|
|
2570
2561
|
/** Cancels the pending lost-paste safety timer (undefined when disarmed). */
|
|
2571
2562
|
const pasteBracketCancelRef = useRef<(() => void) | undefined>(undefined)
|
|
2572
|
-
/**
|
|
2573
|
-
const
|
|
2563
|
+
/** Ordered editor tokens from the stdin chunk Ink is about to deliver. */
|
|
2564
|
+
const rawEditorTokens = useRef<readonly RawEditorToken[] | undefined>(undefined)
|
|
2565
|
+
/** VS Code focus state from xterm focus-report events; starts focused. */
|
|
2566
|
+
const terminalFocusedRef = useRef(true)
|
|
2574
2567
|
// Codex shell-style recall: the navigation cursor, the saved draft restored
|
|
2575
2568
|
// on Down past the newest entry, and the boundary-gate anchor.
|
|
2576
2569
|
const recall = useRef<RecallState>(beginRecall([], ''))
|
|
2577
2570
|
|
|
2571
|
+
useEffect(() => {
|
|
2572
|
+
preferredColumnRef.current = null
|
|
2573
|
+
}, [editorColumns])
|
|
2574
|
+
|
|
2578
2575
|
// A /history panel acceptance lands as a fill: place the sanitized text at
|
|
2579
2576
|
// the end of the composer and resume recall from that entry.
|
|
2580
|
-
useEffect(() => {
|
|
2581
|
-
if (historyFill === undefined) return
|
|
2582
|
-
const safe = sanitizeDraftText(historyFill.text)
|
|
2583
|
-
draftImagesRef.current = []
|
|
2584
|
-
setDraftImages([])
|
|
2577
|
+
useEffect(() => {
|
|
2578
|
+
if (historyFill === undefined) return
|
|
2579
|
+
const safe = sanitizeDraftText(historyFill.text)
|
|
2580
|
+
draftImagesRef.current = []
|
|
2581
|
+
setDraftImages([])
|
|
2582
|
+
valueRef.current = safe
|
|
2583
|
+
cursorRef.current = safe.length
|
|
2585
2584
|
setValue(safe)
|
|
2586
2585
|
setCursor(safe.length)
|
|
2586
|
+
resetCursorBlink()
|
|
2587
2587
|
preferredColumnRef.current = null
|
|
2588
2588
|
setDismissedMenuValue(undefined)
|
|
2589
2589
|
recall.current = {
|
|
@@ -2593,25 +2593,24 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2593
2593
|
lastRecalled: safe,
|
|
2594
2594
|
}
|
|
2595
2595
|
historyConsumed()
|
|
2596
|
-
}, [historyFill, recallSpace, historyConsumed])
|
|
2597
|
-
|
|
2598
|
-
useEffect(() => {
|
|
2599
|
-
setDraftImages((current) => {
|
|
2600
|
-
const next = current.filter(image => value.includes(image.marker))
|
|
2601
|
-
draftImagesRef.current = next
|
|
2602
|
-
return next.length === current.length ? current : next
|
|
2603
|
-
})
|
|
2604
|
-
}, [value])
|
|
2596
|
+
}, [historyFill, recallSpace, historyConsumed, resetCursorBlink])
|
|
2597
|
+
|
|
2598
|
+
useEffect(() => {
|
|
2599
|
+
setDraftImages((current) => {
|
|
2600
|
+
const next = current.filter(image => value.includes(image.marker))
|
|
2601
|
+
draftImagesRef.current = next
|
|
2602
|
+
return next.length === current.length ? current : next
|
|
2603
|
+
})
|
|
2604
|
+
}, [value])
|
|
2605
2605
|
|
|
2606
2606
|
// Home/End and the Backspace-vs-Delete family never survive Ink's parser
|
|
2607
2607
|
// as distinct keys, and kitty CSI-u forms parse as unnamed junk Ink would
|
|
2608
2608
|
// insert as draft text.
|
|
2609
2609
|
// Patch stdin.read — the single choke point Ink's input loop pulls every
|
|
2610
2610
|
// chunk through — to first rewrite decodable CSI-u sequences to their
|
|
2611
|
-
// legacy bytes, then
|
|
2612
|
-
// matching
|
|
2613
|
-
//
|
|
2614
|
-
// parses the normalized string; every other byte passes through untouched.
|
|
2611
|
+
// legacy bytes, then tokenize editor-only sequences before Ink emits the
|
|
2612
|
+
// matching input event. Batched Home/End/Delete/Backspace actions remain
|
|
2613
|
+
// ordered even though Ink invokes useInput only once for the whole chunk.
|
|
2615
2614
|
useEffect(() => {
|
|
2616
2615
|
if (stdin === undefined) return
|
|
2617
2616
|
const originalRead = stdin.read.bind(stdin)
|
|
@@ -2619,14 +2618,19 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2619
2618
|
const chunk = originalRead(...args)
|
|
2620
2619
|
if (chunk === null) return chunk
|
|
2621
2620
|
const normalized = normalizeKeyboardChunk(typeof chunk === 'string' ? chunk : String(chunk))
|
|
2622
|
-
|
|
2623
|
-
|
|
2621
|
+
const input = focusReporting
|
|
2622
|
+
? stripTerminalFocusEvents(normalized, focused => {
|
|
2623
|
+
terminalFocusedRef.current = focused
|
|
2624
|
+
})
|
|
2625
|
+
: normalized
|
|
2626
|
+
rawEditorTokens.current = tokenizeRawEditorChunk(input)
|
|
2627
|
+
return input
|
|
2624
2628
|
} as typeof stdin.read
|
|
2625
2629
|
stdin.read = patchedRead
|
|
2626
2630
|
return () => {
|
|
2627
2631
|
stdin.read = originalRead as typeof stdin.read
|
|
2628
2632
|
}
|
|
2629
|
-
}, [stdin])
|
|
2633
|
+
}, [focusReporting, stdin])
|
|
2630
2634
|
|
|
2631
2635
|
// Keep the navigation's recall space fresh while browsing state survives
|
|
2632
2636
|
// (new local submissions extend the space; the index stays valid unless
|
|
@@ -2649,80 +2653,99 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2649
2653
|
const mentionToken = tokenMatch === null
|
|
2650
2654
|
? undefined
|
|
2651
2655
|
: { start: beforeCursor.length - lastLine.length + (tokenMatch.index ?? 0) + (tokenMatch[1]?.length ?? 0), query: tokenMatch[2] ?? '' }
|
|
2652
|
-
const mentionActive = mentionToken !== undefined
|
|
2653
|
-
const [mentionRows, setMentionRows] = useState<readonly MentionCandidate[]>([])
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
const
|
|
2662
|
-
|
|
2663
|
-
let
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
suffix
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
draftImagesRef.current
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
const
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2656
|
+
const mentionActive = mentionToken !== undefined
|
|
2657
|
+
const [mentionRows, setMentionRows] = useState<readonly MentionCandidate[]>([])
|
|
2658
|
+
const mentionRequestRef = useRef(0)
|
|
2659
|
+
|
|
2660
|
+
const sameImagePath = (left: string, right: string): boolean => (
|
|
2661
|
+
process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right
|
|
2662
|
+
)
|
|
2663
|
+
|
|
2664
|
+
const uniqueImageMarker = (name: string, source: 'mention' | 'drop', reserved: readonly string[] = []): string => {
|
|
2665
|
+
const safeName = singleLineText(sanitizeDraftText(name))
|
|
2666
|
+
const base = source === 'mention' ? `@${safeName}` : `[image: ${safeName}]`
|
|
2667
|
+
let marker = base
|
|
2668
|
+
let suffix = 2
|
|
2669
|
+
while (valueRef.current.includes(marker) || draftImagesRef.current.some(image => image.marker === marker) || reserved.includes(marker)) {
|
|
2670
|
+
marker = source === 'mention' ? `@${safeName} (${suffix})` : `[image: ${safeName} ${suffix}]`
|
|
2671
|
+
suffix += 1
|
|
2672
|
+
}
|
|
2673
|
+
return marker
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
const registerDraftImage = (inspection: ImagePathInspection, marker: string): boolean => {
|
|
2677
|
+
if (draftImagesRef.current.some(image => sameImagePath(image.path, inspection.path))) {
|
|
2678
|
+
notify(`${inspection.name} is already attached`, 'warning')
|
|
2679
|
+
return false
|
|
2680
|
+
}
|
|
2681
|
+
const next = [...draftImagesRef.current, { ...inspection, marker }]
|
|
2682
|
+
draftImagesRef.current = next
|
|
2683
|
+
setDraftImages(next)
|
|
2684
|
+
return true
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
const insertDroppedImages = (paths: readonly string[]): void => {
|
|
2688
|
+
const originalValue = valueRef.current
|
|
2689
|
+
const originalCursor = cursorRef.current
|
|
2690
|
+
notify(`checking ${paths.length} image${paths.length === 1 ? '' : 's'}…`)
|
|
2691
|
+
void inspectImages(paths).then((inspected) => {
|
|
2692
|
+
const additions: DraftImage[] = []
|
|
2693
|
+
const markers: string[] = []
|
|
2694
|
+
for (const inspection of inspected) {
|
|
2695
|
+
if ([...draftImagesRef.current, ...additions].some(image => sameImagePath(image.path, inspection.path))) continue
|
|
2696
|
+
const marker = uniqueImageMarker(inspection.name, 'drop', markers)
|
|
2697
|
+
additions.push({ ...inspection, marker })
|
|
2698
|
+
markers.push(marker)
|
|
2699
|
+
}
|
|
2700
|
+
if (additions.length === 0) {
|
|
2701
|
+
notify('those images are already attached', 'warning')
|
|
2702
|
+
return
|
|
2703
|
+
}
|
|
2704
|
+
const current = valueRef.current
|
|
2705
|
+
const anchor = remapStableRange(originalValue, current, { start: originalCursor, end: originalCursor })
|
|
2706
|
+
if (anchor === undefined) {
|
|
2707
|
+
notify('draft changed at the image drop point; drop the images again', 'warning')
|
|
2708
|
+
return
|
|
2709
|
+
}
|
|
2710
|
+
const at = anchor.start
|
|
2711
|
+
const insertion = `${at > 0 && !/\s$/u.test(current.slice(0, at)) ? ' ' : ''}${markers.join(' ')}${current.slice(at) === '' ? '' : ' '}`
|
|
2712
|
+
const edit = replaceRangePreservingCursor(current, cursorRef.current, anchor, insertion)
|
|
2713
|
+
const nextCursor = current === originalValue && cursorRef.current === originalCursor
|
|
2714
|
+
? at + insertion.length
|
|
2715
|
+
: edit.cursor
|
|
2716
|
+
valueRef.current = edit.value
|
|
2717
|
+
cursorRef.current = nextCursor
|
|
2718
|
+
setValue(edit.value)
|
|
2719
|
+
setCursor(nextCursor)
|
|
2720
|
+
resetCursorBlink()
|
|
2721
|
+
const nextImages = [...draftImagesRef.current, ...additions]
|
|
2722
|
+
draftImagesRef.current = nextImages
|
|
2723
|
+
setDraftImages(nextImages)
|
|
2724
|
+
notify(`${additions.length} image${additions.length === 1 ? '' : 's'} ready for the next message`)
|
|
2725
|
+
}, (reason: unknown) => {
|
|
2726
|
+
notify(`image attachment failed: ${reason instanceof Error ? reason.message : String(reason)}`, 'error')
|
|
2727
|
+
})
|
|
2728
|
+
}
|
|
2713
2729
|
|
|
2714
2730
|
useEffect(() => {
|
|
2731
|
+
const requestId = mentionRequestRef.current + 1
|
|
2732
|
+
mentionRequestRef.current = requestId
|
|
2715
2733
|
if (!active || !mentionActive) {
|
|
2716
2734
|
setMentionRows([])
|
|
2717
2735
|
return
|
|
2718
2736
|
}
|
|
2719
2737
|
const controller = new AbortController()
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2738
|
+
const query = mentionToken.query
|
|
2739
|
+
const timer = setTimeout(() => {
|
|
2740
|
+
void loadMentions(query, controller.signal).then(
|
|
2741
|
+
rows => {
|
|
2742
|
+
if (!controller.signal.aborted && mentionRequestRef.current === requestId) setMentionRows(rows)
|
|
2743
|
+
},
|
|
2744
|
+
() => {},
|
|
2745
|
+
)
|
|
2746
|
+
}, 50)
|
|
2725
2747
|
return () => {
|
|
2748
|
+
clearTimeout(timer)
|
|
2726
2749
|
controller.abort()
|
|
2727
2750
|
}
|
|
2728
2751
|
}, [active, mentionActive, mentionToken?.query])
|
|
@@ -2730,9 +2753,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2730
2753
|
// Codex routes keys to the topmost surface first. Completion therefore
|
|
2731
2754
|
// remains available while a turn runs, and Esc dismisses it before the
|
|
2732
2755
|
// same key is allowed to interrupt the turn.
|
|
2733
|
-
const menuActive = (slashActive || mentionActive) && dismissedMenuValue !== value
|
|
2756
|
+
const menuActive = !preparingImages && (slashActive || mentionActive) && dismissedMenuValue !== value
|
|
2757
|
+
const visibleMentionRows = mentionToken !== undefined && isPathLikeMentionQuery(mentionToken.query)
|
|
2758
|
+
? mentionRows.filter(row => row.kind !== 'session')
|
|
2759
|
+
: mentionRows
|
|
2734
2760
|
const menuRows: readonly CompletionCandidate[] = mentionActive
|
|
2735
|
-
?
|
|
2761
|
+
? visibleMentionRows.map(row => ({
|
|
2736
2762
|
label: row.label.startsWith('@')
|
|
2737
2763
|
? row.label
|
|
2738
2764
|
: `@${row.label}${row.kind === 'directory' ? '/' : ''}`,
|
|
@@ -2744,56 +2770,75 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2744
2770
|
/** Accept the highlighted completion-menu candidate into the draft. */
|
|
2745
2771
|
const acceptMenuCandidate = (): void => {
|
|
2746
2772
|
if (mentionActive && mentionToken !== undefined) {
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
const
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
const
|
|
2757
|
-
if (
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2773
|
+
if (visibleMentionRows.length === 0) return
|
|
2774
|
+
const row = visibleMentionRows[completionIndex % visibleMentionRows.length]
|
|
2775
|
+
if (row !== undefined) {
|
|
2776
|
+
if (row.kind === 'file' && row.path !== undefined && looksLikeImagePath(row.path)) {
|
|
2777
|
+
const tokenText = value.slice(mentionToken.start, cursor)
|
|
2778
|
+
const start = mentionToken.start
|
|
2779
|
+
const originalValue = value
|
|
2780
|
+
notify(`checking image ${basename(row.path)}…`)
|
|
2781
|
+
void inspectImages([row.path]).then((inspected) => {
|
|
2782
|
+
const inspection = inspected[0]
|
|
2783
|
+
if (inspection === undefined) return
|
|
2784
|
+
const current = valueRef.current
|
|
2785
|
+
const anchor = remapStableRange(originalValue, current, { start, end: start + tokenText.length })
|
|
2786
|
+
if (anchor === undefined || current.slice(anchor.start, anchor.end) !== tokenText) {
|
|
2787
|
+
notify('draft changed around the image mention; select it again', 'warning')
|
|
2788
|
+
return
|
|
2789
|
+
}
|
|
2790
|
+
if (draftImagesRef.current.some(image => sameImagePath(image.path, inspection.path))) {
|
|
2791
|
+
const edit = replaceRangePreservingCursor(current, cursorRef.current, anchor, '')
|
|
2792
|
+
valueRef.current = edit.value
|
|
2793
|
+
cursorRef.current = edit.cursor
|
|
2794
|
+
setValue(edit.value)
|
|
2795
|
+
setCursor(edit.cursor)
|
|
2796
|
+
resetCursorBlink()
|
|
2797
|
+
setDismissedMenuValue(edit.value)
|
|
2798
|
+
notify(`${inspection.name} is already attached`, 'warning')
|
|
2799
|
+
return
|
|
2800
|
+
}
|
|
2801
|
+
const marker = uniqueImageMarker(inspection.name, 'mention')
|
|
2802
|
+
const edit = replaceRangePreservingCursor(current, cursorRef.current, anchor, marker)
|
|
2803
|
+
valueRef.current = edit.value
|
|
2804
|
+
cursorRef.current = edit.cursor
|
|
2805
|
+
setValue(edit.value)
|
|
2806
|
+
setCursor(edit.cursor)
|
|
2807
|
+
resetCursorBlink()
|
|
2808
|
+
setDismissedMenuValue(edit.value)
|
|
2809
|
+
registerDraftImage(inspection, marker)
|
|
2810
|
+
notify(`${inspection.name} ready for the next message`)
|
|
2811
|
+
}, (reason: unknown) => {
|
|
2812
|
+
notify(`image attachment failed: ${reason instanceof Error ? reason.message : String(reason)}`, 'error')
|
|
2813
|
+
})
|
|
2814
|
+
setCompletionIndex(0)
|
|
2815
|
+
setDismissedMenuValue(undefined)
|
|
2816
|
+
return
|
|
2817
|
+
}
|
|
2818
|
+
// Session rows carry the canonical @[label](dsh-session:…) token;
|
|
2785
2819
|
// file rows insert `@path` (directories keep their trailing slash).
|
|
2786
2820
|
const insertion = row.label.startsWith('@')
|
|
2787
2821
|
? row.label
|
|
2788
2822
|
: `@${row.label}${row.kind === 'directory' ? '/' : ''}`
|
|
2789
|
-
|
|
2790
|
-
|
|
2823
|
+
const nextValue = value.slice(0, mentionToken.start) + insertion + value.slice(cursor)
|
|
2824
|
+
const nextCursor = mentionToken.start + insertion.length
|
|
2825
|
+
valueRef.current = nextValue
|
|
2826
|
+
cursorRef.current = nextCursor
|
|
2827
|
+
setValue(nextValue)
|
|
2828
|
+
setCursor(nextCursor)
|
|
2829
|
+
resetCursorBlink()
|
|
2791
2830
|
}
|
|
2792
2831
|
} else {
|
|
2832
|
+
if (candidates.length === 0) return
|
|
2793
2833
|
const candidate = candidates[completionIndex % candidates.length]
|
|
2794
2834
|
if (candidate !== undefined) {
|
|
2795
|
-
|
|
2796
|
-
|
|
2835
|
+
const nextValue = `${candidate.label} `
|
|
2836
|
+
const nextCursor = candidate.label.length + 1
|
|
2837
|
+
valueRef.current = nextValue
|
|
2838
|
+
cursorRef.current = nextCursor
|
|
2839
|
+
setValue(nextValue)
|
|
2840
|
+
setCursor(nextCursor)
|
|
2841
|
+
resetCursorBlink()
|
|
2797
2842
|
}
|
|
2798
2843
|
}
|
|
2799
2844
|
setCompletionIndex(0)
|
|
@@ -2803,8 +2848,11 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2803
2848
|
/** Apply one editor edit: draft, cursor, kill buffer, menu reset. */
|
|
2804
2849
|
const applyEdit = (edit: EditResult): void => {
|
|
2805
2850
|
if (edit.killed !== undefined && edit.killed !== '') killRef.current = edit.killed
|
|
2851
|
+
valueRef.current = edit.value
|
|
2852
|
+
cursorRef.current = edit.cursor
|
|
2806
2853
|
setValue(edit.value)
|
|
2807
2854
|
setCursor(edit.cursor)
|
|
2855
|
+
resetCursorBlink()
|
|
2808
2856
|
preferredColumnRef.current = null
|
|
2809
2857
|
setCompletionIndex(0)
|
|
2810
2858
|
setDismissedMenuValue(undefined)
|
|
@@ -2812,15 +2860,106 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2812
2860
|
|
|
2813
2861
|
/** Move the cursor without editing; horizontal moves clear the column preference. */
|
|
2814
2862
|
const moveCursorTo = (next: number): void => {
|
|
2815
|
-
|
|
2863
|
+
resetCursorBlink()
|
|
2864
|
+
if (next === cursorRef.current) return
|
|
2865
|
+
cursorRef.current = next
|
|
2816
2866
|
setCursor(next)
|
|
2817
2867
|
preferredColumnRef.current = null
|
|
2818
2868
|
}
|
|
2819
2869
|
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2870
|
+
/** Apply an ordered raw-key batch against one current draft snapshot. */
|
|
2871
|
+
const applyRawEditorTokens = (tokens: readonly RawEditorToken[]): void => {
|
|
2872
|
+
let nextValue = valueRef.current
|
|
2873
|
+
let nextCursor = cursorRef.current
|
|
2874
|
+
for (const token of tokens) {
|
|
2875
|
+
if (token.kind === 'text') {
|
|
2876
|
+
const edit = insertText(nextValue, nextCursor, token.text)
|
|
2877
|
+
nextValue = edit.value
|
|
2878
|
+
nextCursor = edit.cursor
|
|
2879
|
+
continue
|
|
2880
|
+
}
|
|
2881
|
+
if (token.kind === 'home') {
|
|
2882
|
+
nextCursor = moveToLineStart(nextValue, nextCursor, false)
|
|
2883
|
+
continue
|
|
2884
|
+
}
|
|
2885
|
+
if (token.kind === 'end') {
|
|
2886
|
+
nextCursor = moveToLineEnd(nextValue, nextCursor, false)
|
|
2887
|
+
continue
|
|
2888
|
+
}
|
|
2889
|
+
const edit = token.kind === 'delete-backward'
|
|
2890
|
+
? deleteBackward(nextValue, nextCursor)
|
|
2891
|
+
: token.kind === 'delete-word-backward'
|
|
2892
|
+
? deleteWordBackward(nextValue, nextCursor)
|
|
2893
|
+
: token.kind === 'delete-forward'
|
|
2894
|
+
? deleteForward(nextValue, nextCursor)
|
|
2895
|
+
: deleteWordForward(nextValue, nextCursor)
|
|
2896
|
+
if (edit.killed !== undefined && edit.killed !== '') killRef.current = edit.killed
|
|
2897
|
+
nextValue = edit.value
|
|
2898
|
+
nextCursor = edit.cursor
|
|
2899
|
+
}
|
|
2900
|
+
valueRef.current = nextValue
|
|
2901
|
+
cursorRef.current = nextCursor
|
|
2902
|
+
setValue(nextValue)
|
|
2903
|
+
setCursor(nextCursor)
|
|
2904
|
+
resetCursorBlink()
|
|
2905
|
+
preferredColumnRef.current = null
|
|
2906
|
+
setCompletionIndex(0)
|
|
2907
|
+
setDismissedMenuValue(undefined)
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
const cancelImageSubmission = (): void => {
|
|
2911
|
+
prepareEpochRef.current += 1
|
|
2912
|
+
prepareAbortRef.current?.abort()
|
|
2913
|
+
prepareAbortRef.current = undefined
|
|
2914
|
+
setPreparingImages(false)
|
|
2915
|
+
dismissNotice()
|
|
2916
|
+
notify('image submission cancelled', 'warning')
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
/** Move through visual rows first, then cross history at the true edge. */
|
|
2920
|
+
const navigateVertical = (direction: -1 | 1): void => {
|
|
2921
|
+
const currentValue = valueRef.current
|
|
2922
|
+
const currentCursor = cursorRef.current
|
|
2923
|
+
const model = editorModel(currentValue, editorColumns)
|
|
2924
|
+
const preferred = preferredColumnRef.current ?? caretSite(model, currentCursor).column
|
|
2925
|
+
const next = moveCursorVertically(model, currentCursor, preferred, direction)
|
|
2926
|
+
if (next !== currentCursor) {
|
|
2927
|
+
cursorRef.current = next
|
|
2928
|
+
setCursor(next)
|
|
2929
|
+
resetCursorBlink()
|
|
2930
|
+
preferredColumnRef.current = preferred
|
|
2931
|
+
return
|
|
2932
|
+
}
|
|
2933
|
+
if (recall.current.entries.length > 0
|
|
2934
|
+
&& shouldRecallNavigate(currentValue, currentCursor, recall.current.lastRecalled, direction)) {
|
|
2935
|
+
const step = direction < 0 ? recallOlder(recall.current, currentValue) : recallNewer(recall.current)
|
|
2936
|
+
recall.current = step.state
|
|
2937
|
+
if (step.entry !== undefined) {
|
|
2938
|
+
const safe = sanitizeDraftText(step.entry)
|
|
2939
|
+
valueRef.current = safe
|
|
2940
|
+
cursorRef.current = safe.length
|
|
2941
|
+
setValue(safe)
|
|
2942
|
+
setCursor(safe.length)
|
|
2943
|
+
preferredColumnRef.current = null
|
|
2944
|
+
setDismissedMenuValue(undefined)
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
resetCursorBlink()
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
useStableInput((input, key) => {
|
|
2951
|
+
// Modal ownership: approval/question/model dialogs consume all keys.
|
|
2952
|
+
if (!active) return
|
|
2953
|
+
// React may not have committed the previous Tab completion render before
|
|
2954
|
+
// the next terminal byte arrives. Read the synchronous editor refs so a
|
|
2955
|
+
// completion followed immediately by text edits never uses stale closure
|
|
2956
|
+
// state.
|
|
2957
|
+
const liveValue = valueRef.current
|
|
2958
|
+
const liveCursor = cursorRef.current
|
|
2959
|
+
if (preparingImages) {
|
|
2960
|
+
if (key.escape || (key.ctrl && input === 'c')) cancelImageSubmission()
|
|
2961
|
+
return
|
|
2962
|
+
}
|
|
2824
2963
|
// Deletion confirm owns the box: y proceeds, anything else cancels.
|
|
2825
2964
|
// Typed in the INPUT BOX (codex delete-confirm): the keystroke is echoed
|
|
2826
2965
|
// as the box's own prompt, not an invisible panel keypress.
|
|
@@ -2843,7 +2982,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2843
2982
|
return
|
|
2844
2983
|
}
|
|
2845
2984
|
// Ctrl+R toggles the thinking display (Claude-Code reasoning fold).
|
|
2846
|
-
|
|
2985
|
+
// Alt+R is the zero-config alias: VS Code never intercepts Alt chords,
|
|
2986
|
+
// so the toggle stays reachable before /vscode-keys has been applied.
|
|
2987
|
+
if ((key.ctrl || key.meta) && input === 'r') {
|
|
2988
|
+
if (focusReporting && !terminalFocusedRef.current) return
|
|
2847
2989
|
toggleReasoning()
|
|
2848
2990
|
return
|
|
2849
2991
|
}
|
|
@@ -2860,11 +3002,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2860
3002
|
if (key.ctrl && input === 'c') {
|
|
2861
3003
|
if (busy) {
|
|
2862
3004
|
interrupt()
|
|
2863
|
-
} else if (
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
3005
|
+
} else if (liveValue !== '') {
|
|
3006
|
+
valueRef.current = ''
|
|
3007
|
+
cursorRef.current = 0
|
|
3008
|
+
setValue('')
|
|
3009
|
+
setCursor(0)
|
|
3010
|
+
resetCursorBlink()
|
|
3011
|
+
draftImagesRef.current = []
|
|
3012
|
+
setDraftImages([])
|
|
2868
3013
|
setCompletionIndex(0)
|
|
2869
3014
|
setDismissedMenuValue(undefined)
|
|
2870
3015
|
} else {
|
|
@@ -2875,8 +3020,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2875
3020
|
if (key.ctrl && input === 'd') {
|
|
2876
3021
|
// Codex: Ctrl+D deletes forward while a draft exists; the app-level
|
|
2877
3022
|
// exit only fires from an empty composer.
|
|
2878
|
-
if (
|
|
2879
|
-
applyEdit(deleteForward(
|
|
3023
|
+
if (liveValue !== '') {
|
|
3024
|
+
applyEdit(deleteForward(liveValue, liveCursor))
|
|
2880
3025
|
return
|
|
2881
3026
|
}
|
|
2882
3027
|
if (busy) notify('cancel the running turn before exiting (Esc or Ctrl+C)', 'warning')
|
|
@@ -2885,7 +3030,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2885
3030
|
}
|
|
2886
3031
|
if (key.escape) {
|
|
2887
3032
|
if (menuActive) {
|
|
2888
|
-
setDismissedMenuValue(
|
|
3033
|
+
setDismissedMenuValue(liveValue)
|
|
2889
3034
|
return
|
|
2890
3035
|
}
|
|
2891
3036
|
if (hasNotice) {
|
|
@@ -2897,14 +3042,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2897
3042
|
}
|
|
2898
3043
|
// Delete on the empty composer cancels the newest queued message (the
|
|
2899
3044
|
// web queue-mirror contract: the durable splice drops the pending row).
|
|
2900
|
-
if (key.delete &&
|
|
3045
|
+
if (key.delete && liveValue === '' && queued.length > 0) {
|
|
2901
3046
|
cancelQueued(queued[queued.length - 1]!.messageId)
|
|
2902
3047
|
return
|
|
2903
3048
|
}
|
|
2904
3049
|
if (key.return) {
|
|
2905
3050
|
// A newline inside an open bracketed paste inserts; it never submits.
|
|
2906
3051
|
if (pasteBracketRef.current) {
|
|
2907
|
-
applyEdit(insertText(
|
|
3052
|
+
applyEdit(insertText(liveValue, liveCursor, '\n'))
|
|
2908
3053
|
return
|
|
2909
3054
|
}
|
|
2910
3055
|
// Enter on an open completion menu accepts the highlighted candidate
|
|
@@ -2913,43 +3058,54 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
2913
3058
|
// exactly, in which case Enter submits it (typing a full "/effort" and
|
|
2914
3059
|
// pressing return must run the command, not re-accept its own text).
|
|
2915
3060
|
if (menuActive) {
|
|
2916
|
-
const exactSlash = !mentionActive && candidates.some(candidate => candidate.label ===
|
|
3061
|
+
const exactSlash = !mentionActive && candidates.some(candidate => candidate.label === liveValue)
|
|
2917
3062
|
if (!exactSlash) {
|
|
2918
3063
|
acceptMenuCandidate()
|
|
2919
3064
|
return
|
|
2920
3065
|
}
|
|
2921
|
-
}
|
|
2922
|
-
const text =
|
|
2923
|
-
if (draftImagesRef.current.length > 0) {
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
3066
|
+
}
|
|
3067
|
+
const text = liveValue.trim()
|
|
3068
|
+
if (draftImagesRef.current.length > 0) {
|
|
3069
|
+
const controller = new AbortController()
|
|
3070
|
+
const epoch = prepareEpochRef.current + 1
|
|
3071
|
+
prepareEpochRef.current = epoch
|
|
3072
|
+
prepareAbortRef.current = controller
|
|
3073
|
+
setPreparingImages(true)
|
|
3074
|
+
notify(`processing ${draftImagesRef.current.length} image${draftImagesRef.current.length === 1 ? '' : 's'}…`)
|
|
3075
|
+
const snapshot = draftImagesRef.current
|
|
3076
|
+
void prepareImages(snapshot.map(image => image.path), controller.signal).then((images) => {
|
|
3077
|
+
if (controller.signal.aborted || prepareEpochRef.current !== epoch) return
|
|
3078
|
+
prepareAbortRef.current = undefined
|
|
3079
|
+
setPreparingImages(false)
|
|
3080
|
+
valueRef.current = ''
|
|
3081
|
+
cursorRef.current = 0
|
|
3082
|
+
setValue('')
|
|
3083
|
+
setCursor(0)
|
|
3084
|
+
draftImagesRef.current = []
|
|
3085
|
+
setDraftImages([])
|
|
3086
|
+
setCompletionIndex(0)
|
|
3087
|
+
setDismissedMenuValue(undefined)
|
|
3088
|
+
dismissNotice()
|
|
3089
|
+
if (text !== '') {
|
|
3090
|
+
recordLocal(text)
|
|
3091
|
+
recordHistory(text)
|
|
3092
|
+
}
|
|
3093
|
+
recall.current = beginRecall(recallSpace, '')
|
|
3094
|
+
if (busy) steer(text, images)
|
|
3095
|
+
else dispatch(text, images)
|
|
3096
|
+
}, (reason: unknown) => {
|
|
3097
|
+
if (controller.signal.aborted || prepareEpochRef.current !== epoch) return
|
|
3098
|
+
prepareAbortRef.current = undefined
|
|
3099
|
+
setPreparingImages(false)
|
|
3100
|
+
notify(`image submission failed: ${reason instanceof Error ? reason.message : String(reason)}`, 'error')
|
|
3101
|
+
})
|
|
3102
|
+
return
|
|
3103
|
+
}
|
|
3104
|
+
valueRef.current = ''
|
|
3105
|
+
cursorRef.current = 0
|
|
3106
|
+
setValue('')
|
|
2952
3107
|
setCursor(0)
|
|
3108
|
+
resetCursorBlink()
|
|
2953
3109
|
setCompletionIndex(0)
|
|
2954
3110
|
setDismissedMenuValue(undefined)
|
|
2955
3111
|
if (text === '') return
|
|
@@ -3076,6 +3232,13 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3076
3232
|
openTodos()
|
|
3077
3233
|
return
|
|
3078
3234
|
}
|
|
3235
|
+
if (text === '/vscode-keys' || text.startsWith('/vscode-keys ')) {
|
|
3236
|
+
void applyEditorKeys().then(
|
|
3237
|
+
summary => notify(summary),
|
|
3238
|
+
error => notify(`vscode-keys failed: ${error instanceof Error ? error.message : String(error)}`, 'error'),
|
|
3239
|
+
)
|
|
3240
|
+
return
|
|
3241
|
+
}
|
|
3079
3242
|
if (text === '/subagent') {
|
|
3080
3243
|
openSubagent()
|
|
3081
3244
|
return
|
|
@@ -3097,6 +3260,15 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3097
3260
|
// Ink exposes Ctrl+J as a bare LF and Alt+Enter as a bare CR after
|
|
3098
3261
|
// stripping the leading escape. Neither is a multiline shortcut.
|
|
3099
3262
|
if (input === '\n' || input === '\r') return
|
|
3263
|
+
// A fast Tab followed by text can arrive as one readable chunk in an
|
|
3264
|
+
// integrated terminal. Accept the candidate first, then apply the
|
|
3265
|
+
// remaining characters against the synchronously updated editor refs.
|
|
3266
|
+
if (menuActive && (key.tab || input.startsWith('\t'))) {
|
|
3267
|
+
const remainder = key.tab ? '' : input.slice(1)
|
|
3268
|
+
acceptMenuCandidate()
|
|
3269
|
+
if (remainder !== '') applyEdit(insertText(valueRef.current, cursorRef.current, remainder))
|
|
3270
|
+
return
|
|
3271
|
+
}
|
|
3100
3272
|
if (menuActive && key.upArrow) {
|
|
3101
3273
|
setCompletionIndex(index => (index + menuRows.length - 1) % menuRows.length)
|
|
3102
3274
|
return
|
|
@@ -3105,135 +3277,86 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3105
3277
|
setCompletionIndex(index => (index + 1) % menuRows.length)
|
|
3106
3278
|
return
|
|
3107
3279
|
}
|
|
3108
|
-
//
|
|
3109
|
-
//
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
if (
|
|
3113
|
-
|
|
3114
|
-
else if (rawKey === 'end') moveCursorTo(lineBounds(value, cursor).end)
|
|
3115
|
-
else if (rawKey === 'delete-backward') applyEdit(deleteBackward(value, cursor))
|
|
3116
|
-
else if (rawKey === 'delete-word-backward') applyEdit(deleteWordBackward(value, cursor))
|
|
3117
|
-
else if (rawKey === 'delete-forward') applyEdit(deleteForward(value, cursor))
|
|
3118
|
-
else applyEdit(deleteWordForward(value, cursor))
|
|
3280
|
+
// Batched Home/End/Delete/Backspace sequences bypass Ink's one-key parser
|
|
3281
|
+
// and reduce against one current editor snapshot in their original order.
|
|
3282
|
+
const rawTokens = rawEditorTokens.current
|
|
3283
|
+
rawEditorTokens.current = undefined
|
|
3284
|
+
if (rawTokens !== undefined) {
|
|
3285
|
+
applyRawEditorTokens(rawTokens)
|
|
3119
3286
|
return
|
|
3120
3287
|
}
|
|
3121
3288
|
if (key.upArrow || key.downArrow) {
|
|
3122
|
-
|
|
3123
|
-
// a boundary of a draft that still matches the last recalled entry;
|
|
3124
|
-
// every interior Up/Down moves the caret across the multiline draft.
|
|
3125
|
-
if (recall.current.entries.length > 0 && shouldRecallNavigate(value, cursor, recall.current.lastRecalled)) {
|
|
3126
|
-
const step = key.upArrow ? recallOlder(recall.current, value) : recallNewer(recall.current)
|
|
3127
|
-
recall.current = step.state
|
|
3128
|
-
if (step.entry !== undefined) {
|
|
3129
|
-
const safe = sanitizeDraftText(step.entry)
|
|
3130
|
-
setValue(safe)
|
|
3131
|
-
setCursor(safe.length)
|
|
3132
|
-
preferredColumnRef.current = null
|
|
3133
|
-
setDismissedMenuValue(undefined)
|
|
3134
|
-
}
|
|
3135
|
-
return
|
|
3136
|
-
}
|
|
3137
|
-
const model = editorModel(value, Math.max(1, columns - 6))
|
|
3138
|
-
const preferred = preferredColumnRef.current ?? caretSite(model, cursor).column
|
|
3139
|
-
const next = moveCursorVertically(model, cursor, preferred, key.upArrow ? -1 : 1)
|
|
3140
|
-
if (next !== cursor) {
|
|
3141
|
-
setCursor(next)
|
|
3142
|
-
preferredColumnRef.current = preferred
|
|
3143
|
-
}
|
|
3289
|
+
navigateVertical(key.upArrow ? -1 : 1)
|
|
3144
3290
|
return
|
|
3145
3291
|
}
|
|
3146
3292
|
// Ctrl+P / Ctrl+N share the Up/Down contract (Codex binds them to
|
|
3147
3293
|
// move_up/move_down, so the history gate applies first).
|
|
3148
3294
|
if (key.ctrl && (input === 'p' || input === 'n')) {
|
|
3149
|
-
|
|
3150
|
-
if (recall.current.entries.length > 0 && shouldRecallNavigate(value, cursor, recall.current.lastRecalled)) {
|
|
3151
|
-
const step = up ? recallOlder(recall.current, value) : recallNewer(recall.current)
|
|
3152
|
-
recall.current = step.state
|
|
3153
|
-
if (step.entry !== undefined) {
|
|
3154
|
-
const safe = sanitizeDraftText(step.entry)
|
|
3155
|
-
setValue(safe)
|
|
3156
|
-
setCursor(safe.length)
|
|
3157
|
-
preferredColumnRef.current = null
|
|
3158
|
-
setDismissedMenuValue(undefined)
|
|
3159
|
-
}
|
|
3160
|
-
return
|
|
3161
|
-
}
|
|
3162
|
-
const model = editorModel(value, Math.max(1, columns - 6))
|
|
3163
|
-
const preferred = preferredColumnRef.current ?? caretSite(model, cursor).column
|
|
3164
|
-
const next = moveCursorVertically(model, cursor, preferred, up ? -1 : 1)
|
|
3165
|
-
if (next !== cursor) {
|
|
3166
|
-
setCursor(next)
|
|
3167
|
-
preferredColumnRef.current = preferred
|
|
3168
|
-
}
|
|
3169
|
-
return
|
|
3170
|
-
}
|
|
3171
|
-
if (key.tab && menuActive) {
|
|
3172
|
-
acceptMenuCandidate()
|
|
3295
|
+
navigateVertical(input === 'p' ? -1 : 1)
|
|
3173
3296
|
return
|
|
3174
3297
|
}
|
|
3175
3298
|
// Codex editor keymap: Alt/Ctrl+arrows and Alt+B/F move by word pieces;
|
|
3176
3299
|
// plain arrows and Ctrl+B/F move by grapheme.
|
|
3177
3300
|
if (key.leftArrow) {
|
|
3178
|
-
moveCursorTo(key.meta || key.ctrl ? moveWordLeft(
|
|
3301
|
+
moveCursorTo(key.meta || key.ctrl ? moveWordLeft(liveValue, liveCursor) : moveCursorBy(liveValue, liveCursor, -1))
|
|
3179
3302
|
return
|
|
3180
3303
|
}
|
|
3181
3304
|
if (key.rightArrow) {
|
|
3182
|
-
moveCursorTo(key.meta || key.ctrl ? moveWordRight(
|
|
3305
|
+
moveCursorTo(key.meta || key.ctrl ? moveWordRight(liveValue, liveCursor) : moveCursorBy(liveValue, liveCursor, 1))
|
|
3183
3306
|
return
|
|
3184
3307
|
}
|
|
3185
3308
|
if (key.meta && input === 'b') {
|
|
3186
|
-
moveCursorTo(moveWordLeft(
|
|
3309
|
+
moveCursorTo(moveWordLeft(liveValue, liveCursor))
|
|
3187
3310
|
return
|
|
3188
3311
|
}
|
|
3189
3312
|
if (key.meta && input === 'f') {
|
|
3190
|
-
moveCursorTo(moveWordRight(
|
|
3313
|
+
moveCursorTo(moveWordRight(liveValue, liveCursor))
|
|
3191
3314
|
return
|
|
3192
3315
|
}
|
|
3193
3316
|
if (key.ctrl && input === 'b') {
|
|
3194
|
-
moveCursorTo(moveCursorBy(
|
|
3317
|
+
moveCursorTo(moveCursorBy(liveValue, liveCursor, -1))
|
|
3195
3318
|
return
|
|
3196
3319
|
}
|
|
3197
3320
|
if (key.ctrl && input === 'f') {
|
|
3198
|
-
moveCursorTo(moveCursorBy(
|
|
3321
|
+
moveCursorTo(moveCursorBy(liveValue, liveCursor, 1))
|
|
3199
3322
|
return
|
|
3200
3323
|
}
|
|
3201
3324
|
// Ctrl+W and Alt+Backspace delete the previous word piece into the kill
|
|
3202
3325
|
// buffer; Alt+D and the raw Ctrl/Alt+Delete variants kill forward.
|
|
3203
3326
|
if (key.ctrl && input === 'w') {
|
|
3204
|
-
applyEdit(deleteWordBackward(
|
|
3327
|
+
applyEdit(deleteWordBackward(liveValue, liveCursor))
|
|
3205
3328
|
return
|
|
3206
3329
|
}
|
|
3207
3330
|
if (key.meta && input === 'd') {
|
|
3208
|
-
applyEdit(deleteWordForward(
|
|
3331
|
+
applyEdit(deleteWordForward(liveValue, liveCursor))
|
|
3209
3332
|
return
|
|
3210
3333
|
}
|
|
3211
3334
|
// Un-annotated backspace/delete (Ink maps both and here):
|
|
3212
3335
|
// delete the grapheme before the cursor.
|
|
3213
3336
|
if (key.backspace || key.delete) {
|
|
3214
|
-
applyEdit(deleteBackward(
|
|
3337
|
+
applyEdit(deleteBackward(liveValue, liveCursor))
|
|
3215
3338
|
return
|
|
3216
3339
|
}
|
|
3217
3340
|
// Readline parity over the LOGICAL line: A/E to its ends, U/K kill to
|
|
3218
3341
|
// them (filling the single kill buffer), Y yanks it back.
|
|
3219
3342
|
if (key.ctrl && input === 'a') {
|
|
3220
|
-
moveCursorTo(
|
|
3343
|
+
moveCursorTo(moveToLineStart(liveValue, liveCursor, true))
|
|
3221
3344
|
return
|
|
3222
3345
|
}
|
|
3223
3346
|
if (key.ctrl && input === 'e') {
|
|
3224
|
-
moveCursorTo(
|
|
3347
|
+
moveCursorTo(moveToLineEnd(liveValue, liveCursor, true))
|
|
3225
3348
|
return
|
|
3226
3349
|
}
|
|
3227
3350
|
if (key.ctrl && input === 'u') {
|
|
3228
|
-
applyEdit(killToLineStart(
|
|
3351
|
+
applyEdit(killToLineStart(liveValue, liveCursor))
|
|
3229
3352
|
return
|
|
3230
3353
|
}
|
|
3231
3354
|
if (key.ctrl && input === 'k') {
|
|
3232
|
-
applyEdit(killToLineEnd(
|
|
3355
|
+
applyEdit(killToLineEnd(liveValue, liveCursor))
|
|
3233
3356
|
return
|
|
3234
3357
|
}
|
|
3235
3358
|
if (key.ctrl && input === 'y') {
|
|
3236
|
-
if (killRef.current !== '') applyEdit(insertText(
|
|
3359
|
+
if (killRef.current !== '') applyEdit(insertText(liveValue, liveCursor, killRef.current))
|
|
3237
3360
|
return
|
|
3238
3361
|
}
|
|
3239
3362
|
// Ctrl+L refreshes the screen (readline convention): raw ANSI clear
|
|
@@ -3268,16 +3391,16 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3268
3391
|
pasteBracketRef.current = false
|
|
3269
3392
|
pasteBracketCancelRef.current?.()
|
|
3270
3393
|
text = text.replaceAll(PASTE_END_MARKER, '')
|
|
3271
|
-
}
|
|
3272
|
-
if (text === '') return
|
|
3273
|
-
const droppedPaths = text.length > 1 ? parsePastedImagePaths(text) : []
|
|
3274
|
-
if (droppedPaths.length > 0) {
|
|
3275
|
-
insertDroppedImages(droppedPaths)
|
|
3276
|
-
return
|
|
3277
|
-
}
|
|
3278
|
-
applyEdit(insertText(
|
|
3394
|
+
}
|
|
3395
|
+
if (text === '') return
|
|
3396
|
+
const droppedPaths = text.length > 1 ? parsePastedImagePaths(text) : []
|
|
3397
|
+
if (droppedPaths.length > 0) {
|
|
3398
|
+
insertDroppedImages(droppedPaths)
|
|
3399
|
+
return
|
|
3400
|
+
}
|
|
3401
|
+
applyEdit(insertText(valueRef.current, cursorRef.current, text))
|
|
3279
3402
|
}
|
|
3280
|
-
})
|
|
3403
|
+
}, active)
|
|
3281
3404
|
|
|
3282
3405
|
// The DeepSeek easter-egg wave owns its 33ms tick HERE instead of in App:
|
|
3283
3406
|
// the interval re-renders only the composer band at 30fps, never the whole
|
|
@@ -3299,7 +3422,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3299
3422
|
setWaveTick(0)
|
|
3300
3423
|
}
|
|
3301
3424
|
}, [waveTier, waveStyle])
|
|
3302
|
-
const waveActive = waveTick !== null && waveTier !== null && waveStyle !== null
|
|
3425
|
+
const waveActive = !preparingImages && waveTick !== null && waveTier !== null && waveStyle !== null
|
|
3303
3426
|
&& waveTick * DEEPSEEK_WAVE_TICK_MS < deepseekWaveDuration(waveTier, waveStyle)
|
|
3304
3427
|
useEffect(() => {
|
|
3305
3428
|
if (!waveActive) return
|
|
@@ -3325,7 +3448,6 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3325
3448
|
// column-safe physical rows, with the caret mapped to its exact row and
|
|
3326
3449
|
// column. Computed before the frozen path so the row report below runs
|
|
3327
3450
|
// unconditionally.
|
|
3328
|
-
const editorColumns = Math.max(1, columns - 6)
|
|
3329
3451
|
const editorViewModel = editorModel(value, editorColumns)
|
|
3330
3452
|
const clampedCursor = clampCursor(value, cursor)
|
|
3331
3453
|
const caret = caretSite(editorViewModel, clampedCursor)
|
|
@@ -3390,131 +3512,126 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3390
3512
|
rows: menuRows,
|
|
3391
3513
|
})
|
|
3392
3514
|
|
|
3393
|
-
//
|
|
3394
|
-
//
|
|
3395
|
-
//
|
|
3396
|
-
// column — the same gutter reply rows use. The caret is the inverse block
|
|
3397
|
-
// on its exact grapheme, so wide CJK cells and emoji clusters position the
|
|
3398
|
-
// block precisely. The prompt marker keeps the tier accent while an
|
|
3399
|
-
// official DeepSeek model is applied, restoring the static brand ❯ on any
|
|
3400
|
-
// other route.
|
|
3515
|
+
// Every state reuses this exact multiline editor window. Only the caret row
|
|
3516
|
+
// owns an inverse block; non-caret rows render their text without a hidden
|
|
3517
|
+
// spacer or a second blink timer.
|
|
3401
3518
|
const editorRows: ReactElement[] = []
|
|
3402
3519
|
for (let index = editorWindowStart; index < Math.min(editorViewModel.rows.length, editorWindowStart + editorWindowRows); index += 1) {
|
|
3403
3520
|
const row = editorViewModel.rows[index]!
|
|
3404
|
-
const
|
|
3405
|
-
const
|
|
3406
|
-
const
|
|
3407
|
-
const
|
|
3408
|
-
? row.text
|
|
3409
|
-
: caretAt < row.cuts.length - 1
|
|
3410
|
-
? row.text.slice(row.cuts[caretAt + 1]!)
|
|
3411
|
-
: ''
|
|
3412
|
-
const placeholder = index === 0 && value === '' && !busy
|
|
3413
|
-
const tail = placeholder ? COMPOSER_PLACEHOLDER : after
|
|
3414
|
-
const consumed = 2 + visibleColumns(before) + visibleColumns(caretChar) + visibleColumns(tail)
|
|
3521
|
+
const parts = editorRowParts(row, index, caret.row, clampedCursor, !preparingImages)
|
|
3522
|
+
const placeholder = index === 0 && value === '' && !busy && !preparingImages
|
|
3523
|
+
const tail = placeholder ? COMPOSER_PLACEHOLDER : parts.after
|
|
3524
|
+
const consumed = 2 + visibleColumns(parts.before) + visibleColumns(parts.caret) + visibleColumns(tail)
|
|
3415
3525
|
editorRows.push(createElement(
|
|
3416
3526
|
Text,
|
|
3417
3527
|
{ key: index, backgroundColor: bandBg, wrap: 'truncate-end' },
|
|
3418
3528
|
index === 0
|
|
3419
|
-
?
|
|
3420
|
-
? createElement(
|
|
3421
|
-
:
|
|
3529
|
+
? preparingImages
|
|
3530
|
+
? createElement(Text, { color: inkColor(getPalette().warn), bold: true }, '… ')
|
|
3531
|
+
: busy
|
|
3532
|
+
? createElement(BusyChase)
|
|
3533
|
+
: createElement(Text, { color: promptColor, bold: tierActive ? true : undefined }, `${promptGlyph} `)
|
|
3422
3534
|
: ' ',
|
|
3423
|
-
before,
|
|
3424
|
-
|
|
3535
|
+
parts.before,
|
|
3536
|
+
parts.hasCaret
|
|
3537
|
+
? createElement(Text, { key: 'caret', inverse: cursorVisible || undefined }, parts.caret)
|
|
3538
|
+
: null,
|
|
3425
3539
|
placeholder
|
|
3426
3540
|
? createElement(Text, { dimColor: true }, COMPOSER_PLACEHOLDER)
|
|
3427
|
-
: after,
|
|
3541
|
+
: parts.after,
|
|
3428
3542
|
bandFill(consumed),
|
|
3429
3543
|
))
|
|
3430
3544
|
}
|
|
3431
3545
|
const staticEditor = createElement(Box, { flexDirection: 'column' }, ...editorRows)
|
|
3432
3546
|
|
|
3433
|
-
//
|
|
3434
|
-
//
|
|
3435
|
-
//
|
|
3436
|
-
// draft, cursor, placeholder, and the trailing fill — with a per-row phase
|
|
3437
|
-
// offset that flows the wave down the band. The deepseek tier drops the
|
|
3438
|
-
// `· ✦ ✧` sparkles into the rightmost blank cell from 900ms on.
|
|
3547
|
+
// The wave paints the SAME visible rows and caret site as the static path.
|
|
3548
|
+
// Graphemes remain atomic and every background sample advances by terminal
|
|
3549
|
+
// display columns, so CJK and emoji cannot move the caret or wrap the band.
|
|
3439
3550
|
const waveRow = (): ReactElement => {
|
|
3440
3551
|
const hues = deepseekWaveHues(waveTier!)
|
|
3441
3552
|
const style = waveStyle!
|
|
3442
3553
|
const bandRgb = getPalette().composerBand
|
|
3554
|
+
const visibleRows = editorViewModel.rows.slice(editorWindowStart, editorWindowStart + editorWindowRows)
|
|
3555
|
+
const totalBandRows = visibleRows.length + 2
|
|
3443
3556
|
const waveBg = (row: number, column: number): string => {
|
|
3444
|
-
const rgb = deepseekWaveColumnBg(waveTick!, column, bandWidth, waveTier!, style, hues, bandRgb, row,
|
|
3557
|
+
const rgb = deepseekWaveColumnBg(waveTick!, column, bandWidth, waveTier!, style, hues, bandRgb, row, totalBandRows)
|
|
3445
3558
|
return rgb === null ? bandBg : inkColor(rgb)
|
|
3446
3559
|
}
|
|
3447
3560
|
const blankBandRow = (row: number): ReactElement => {
|
|
3448
3561
|
const blanks: ComposerCell[] = []
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
}
|
|
3452
|
-
const waveEditor = editorWindow(value, cursor, Math.max(1, columns - 7))
|
|
3453
|
-
const cells: ComposerCell[] = [
|
|
3454
|
-
{ char: ' ', backgroundColor: waveBg(1, 0) },
|
|
3455
|
-
{ char: ' ', backgroundColor: waveBg(1, 1) },
|
|
3456
|
-
{ char: promptGlyph, color: promptColor, bold: true, backgroundColor: waveBg(1, 2) },
|
|
3457
|
-
{ char: ' ', color: promptColor, backgroundColor: waveBg(1, 3) },
|
|
3458
|
-
]
|
|
3459
|
-
for (const char of waveEditor.before) {
|
|
3460
|
-
cells.push({ char, backgroundColor: waveBg(1, cells.length) })
|
|
3461
|
-
}
|
|
3462
|
-
cells.push({ char: waveEditor.caret, inverse: true, backgroundColor: waveBg(1, cells.length) })
|
|
3463
|
-
if (value === '' && !busy) {
|
|
3464
|
-
for (let at = 0; at < COMPOSER_PLACEHOLDER.length; at += 1) {
|
|
3465
|
-
cells.push({ char: COMPOSER_PLACEHOLDER[at]!, dim: true, backgroundColor: waveBg(1, cells.length) })
|
|
3562
|
+
for (let column = 0; column < bandWidth; column += 1) {
|
|
3563
|
+
blanks.push({ char: ' ', width: 1, backgroundColor: waveBg(row, column) })
|
|
3466
3564
|
}
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3565
|
+
return createElement(Text, { key: `blank-${row}` }, ...waveRowSpans(blanks))
|
|
3566
|
+
}
|
|
3567
|
+
const cellIndexAtColumn = (cells: readonly ComposerCell[], target: number): number | undefined => {
|
|
3568
|
+
let column = 0
|
|
3569
|
+
for (let index = 0; index < cells.length; index += 1) {
|
|
3570
|
+
if (column === target) return index
|
|
3571
|
+
column += cells[index]!.width ?? visibleColumns(cells[index]!.char)
|
|
3572
|
+
if (column > target) return undefined
|
|
3470
3573
|
}
|
|
3574
|
+
return undefined
|
|
3471
3575
|
}
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3576
|
+
const editorWaveRows = visibleRows.map((row, visibleIndex) => {
|
|
3577
|
+
const sourceIndex = editorWindowStart + visibleIndex
|
|
3578
|
+
const bandRow = visibleIndex + 1
|
|
3579
|
+
const parts = editorRowParts(row, sourceIndex, caret.row, clampedCursor)
|
|
3580
|
+
const placeholder = sourceIndex === 0 && value === '' && !busy
|
|
3581
|
+
const cells: ComposerCell[] = []
|
|
3582
|
+
let usedColumns = 0
|
|
3583
|
+
const push = (char: string, extra: Omit<ComposerCell, 'char' | 'width' | 'backgroundColor'> = {}): void => {
|
|
3584
|
+
const width = visibleColumns(char)
|
|
3585
|
+
cells.push({ char, width, backgroundColor: waveBg(bandRow, usedColumns), ...extra })
|
|
3586
|
+
usedColumns += width
|
|
3587
|
+
}
|
|
3588
|
+
if (sourceIndex === 0) {
|
|
3589
|
+
push(promptGlyph, { color: promptColor, bold: true })
|
|
3590
|
+
push(' ', { color: promptColor })
|
|
3591
|
+
} else {
|
|
3592
|
+
push(' ')
|
|
3593
|
+
push(' ')
|
|
3486
3594
|
}
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3595
|
+
for (const span of splitGraphemes(parts.before)) push(span.text)
|
|
3596
|
+
if (parts.hasCaret) push(parts.caret, { inverse: cursorVisible })
|
|
3597
|
+
const tail = placeholder ? COMPOSER_PLACEHOLDER : parts.after
|
|
3598
|
+
for (const span of splitGraphemes(tail)) push(span.text, placeholder ? { dim: true } : {})
|
|
3599
|
+
while (usedColumns < bandWidth) push(' ')
|
|
3600
|
+
|
|
3601
|
+
const middleBandRow = Math.floor(totalBandRows / 2)
|
|
3602
|
+
if (bandRow === middleBandRow && deepseekWaveWordVisible(waveTick!, waveTier!, style)) {
|
|
3603
|
+
const word = waveTier === 'unknown' ? 'Into the Unknown' : 'deepseek'
|
|
3604
|
+
const start = Math.max(2, Math.floor((bandWidth - word.length) / 2))
|
|
3605
|
+
const indices = Array.from({ length: word.length }, (_, at) => cellIndexAtColumn(cells, start + at))
|
|
3606
|
+
if (indices.every(index => index !== undefined && (cells[index]!.char === ' ' || cells[index]!.dim === true))) {
|
|
3607
|
+
for (let at = 0; at < word.length; at += 1) {
|
|
3608
|
+
const cell = cells[indices[at]!]!
|
|
3609
|
+
cell.char = word[at]!
|
|
3610
|
+
cell.width = 1
|
|
3611
|
+
cell.color = inkColor(deepseekWaveWordHue(at, hues))
|
|
3612
|
+
cell.bold = true
|
|
3613
|
+
cell.dim = false
|
|
3614
|
+
}
|
|
3494
3615
|
}
|
|
3495
3616
|
}
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
if (last !== undefined && last.char === ' ') {
|
|
3505
|
-
last.char = spark
|
|
3506
|
-
last.color = promptColor
|
|
3507
|
-
last.bold = true
|
|
3508
|
-
last.dim = false
|
|
3617
|
+
if (bandRow === middleBandRow && (waveTier === 'deepseek' || waveTier === 'unknown') && style === 'wave') {
|
|
3618
|
+
const spark = deepseekWaveSpark(waveTick!)
|
|
3619
|
+
const lastIndex = cellIndexAtColumn(cells, bandWidth - 1)
|
|
3620
|
+
if (spark !== null && lastIndex !== undefined && cells[lastIndex]!.char === ' ') {
|
|
3621
|
+
cells[lastIndex]!.char = spark
|
|
3622
|
+
cells[lastIndex]!.color = promptColor
|
|
3623
|
+
cells[lastIndex]!.bold = true
|
|
3624
|
+
cells[lastIndex]!.dim = false
|
|
3509
3625
|
}
|
|
3510
3626
|
}
|
|
3511
|
-
|
|
3627
|
+
return createElement(Text, { key: `editor-${sourceIndex}`, wrap: 'truncate-end' }, ...waveRowSpans(cells))
|
|
3628
|
+
})
|
|
3512
3629
|
return createElement(
|
|
3513
3630
|
Box,
|
|
3514
3631
|
{ flexDirection: 'column', width: bandWidth },
|
|
3515
3632
|
blankBandRow(0),
|
|
3516
|
-
|
|
3517
|
-
blankBandRow(
|
|
3633
|
+
...editorWaveRows,
|
|
3634
|
+
blankBandRow(totalBandRows - 1),
|
|
3518
3635
|
)
|
|
3519
3636
|
}
|
|
3520
3637
|
|
|
@@ -3522,7 +3639,9 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3522
3639
|
Box,
|
|
3523
3640
|
{ flexDirection: 'column' },
|
|
3524
3641
|
menu,
|
|
3525
|
-
waveTick !== null && waveTier !== null && !busy
|
|
3642
|
+
waveTick !== null && waveTier !== null && waveStyle !== null && !busy && !preparingImages
|
|
3643
|
+
? waveRow()
|
|
3644
|
+
: band(staticEditor),
|
|
3526
3645
|
)
|
|
3527
3646
|
}
|
|
3528
3647
|
|
|
@@ -3764,13 +3883,13 @@ export function App(props: AppProps): ReactElement {
|
|
|
3764
3883
|
const skills = useSyncExternalStore(props.skills.subscribe, readSkills)
|
|
3765
3884
|
const [modelLabel, setModelLabel] = useState(props.model)
|
|
3766
3885
|
const [modelOpen, setModelOpen] = useState(false)
|
|
3767
|
-
/** Nested /model stages; only one owns terminal input at a time. */
|
|
3768
|
-
const [providerOpen, setProviderOpen] = useState(false)
|
|
3769
|
-
const [providerAction, setProviderAction] = useState<
|
|
3770
|
-
| { kind: 'credential' | 'configure' | 'unset' | 'remove'; target: ProviderTargetView }
|
|
3771
|
-
| { kind: 'login' | 'logout'; target: ProviderTargetView; authorization: ProviderAuthorizationRow }
|
|
3772
|
-
| undefined
|
|
3773
|
-
>(undefined)
|
|
3886
|
+
/** Nested /model stages; only one owns terminal input at a time. */
|
|
3887
|
+
const [providerOpen, setProviderOpen] = useState(false)
|
|
3888
|
+
const [providerAction, setProviderAction] = useState<
|
|
3889
|
+
| { kind: 'credential' | 'configure' | 'unset' | 'remove'; target: ProviderTargetView }
|
|
3890
|
+
| { kind: 'login' | 'logout'; target: ProviderTargetView; authorization: ProviderAuthorizationRow }
|
|
3891
|
+
| undefined
|
|
3892
|
+
>(undefined)
|
|
3774
3893
|
/** The model row whose effort levels the /model stage lists; undefined shows the model list. */
|
|
3775
3894
|
const [effortFor, setEffortFor] = useState<ModelRow | undefined>(undefined)
|
|
3776
3895
|
/** Effective reasoning effort, shown in the /model picker and switch notice. */
|
|
@@ -3820,10 +3939,10 @@ export function App(props: AppProps): ReactElement {
|
|
|
3820
3939
|
}, [modelLabel, effortLabel])
|
|
3821
3940
|
const [directory, setDirectory] = useState<ModelDirectory | undefined>(undefined)
|
|
3822
3941
|
const [modelError, setModelError] = useState<string | undefined>(undefined)
|
|
3823
|
-
const [providerDirectory, setProviderDirectory] = useState<ProviderSettingsDirectory | undefined>(undefined)
|
|
3824
|
-
const [providerError, setProviderError] = useState<string | undefined>(undefined)
|
|
3825
|
-
const [authorizationDirectory, setAuthorizationDirectory] = useState<ProviderAuthorizationDirectory | undefined>(undefined)
|
|
3826
|
-
const [authorizationError, setAuthorizationError] = useState<string | undefined>(undefined)
|
|
3942
|
+
const [providerDirectory, setProviderDirectory] = useState<ProviderSettingsDirectory | undefined>(undefined)
|
|
3943
|
+
const [providerError, setProviderError] = useState<string | undefined>(undefined)
|
|
3944
|
+
const [authorizationDirectory, setAuthorizationDirectory] = useState<ProviderAuthorizationDirectory | undefined>(undefined)
|
|
3945
|
+
const [authorizationError, setAuthorizationError] = useState<string | undefined>(undefined)
|
|
3827
3946
|
const [modelLoadEpoch, setModelLoadEpoch] = useState(0)
|
|
3828
3947
|
const [notice, setNotice] = useState<{ text: string; tone: NoticeTone } | undefined>(undefined)
|
|
3829
3948
|
const notify = useCallback((text: string, tone: NoticeTone = 'info'): void => {
|
|
@@ -3863,21 +3982,21 @@ export function App(props: AppProps): ReactElement {
|
|
|
3863
3982
|
return () => {
|
|
3864
3983
|
cancelled = true
|
|
3865
3984
|
}
|
|
3866
|
-
}, [modelOpen, modelLoadEpoch, props.loadModelProviders])
|
|
3867
|
-
useEffect(() => {
|
|
3868
|
-
if (!modelOpen || props.loadProviderAuthorizations === undefined) return
|
|
3869
|
-
let cancelled = false
|
|
3870
|
-
setAuthorizationDirectory(undefined)
|
|
3871
|
-
setAuthorizationError(undefined)
|
|
3872
|
-
Promise.resolve().then(() => props.loadProviderAuthorizations!()).then((loaded) => {
|
|
3873
|
-
if (!cancelled) setAuthorizationDirectory(loaded)
|
|
3874
|
-
}, (error: unknown) => {
|
|
3875
|
-
if (!cancelled) setAuthorizationError(error instanceof Error ? error.message : String(error))
|
|
3876
|
-
})
|
|
3877
|
-
return () => {
|
|
3878
|
-
cancelled = true
|
|
3879
|
-
}
|
|
3880
|
-
}, [modelOpen, modelLoadEpoch, props.loadProviderAuthorizations])
|
|
3985
|
+
}, [modelOpen, modelLoadEpoch, props.loadModelProviders])
|
|
3986
|
+
useEffect(() => {
|
|
3987
|
+
if (!modelOpen || props.loadProviderAuthorizations === undefined) return
|
|
3988
|
+
let cancelled = false
|
|
3989
|
+
setAuthorizationDirectory(undefined)
|
|
3990
|
+
setAuthorizationError(undefined)
|
|
3991
|
+
Promise.resolve().then(() => props.loadProviderAuthorizations!()).then((loaded) => {
|
|
3992
|
+
if (!cancelled) setAuthorizationDirectory(loaded)
|
|
3993
|
+
}, (error: unknown) => {
|
|
3994
|
+
if (!cancelled) setAuthorizationError(error instanceof Error ? error.message : String(error))
|
|
3995
|
+
})
|
|
3996
|
+
return () => {
|
|
3997
|
+
cancelled = true
|
|
3998
|
+
}
|
|
3999
|
+
}, [modelOpen, modelLoadEpoch, props.loadProviderAuthorizations])
|
|
3881
4000
|
useEffect(() => {
|
|
3882
4001
|
const subscribe = props.subscribeModelProviders
|
|
3883
4002
|
if (!modelOpen || subscribe === undefined) return
|
|
@@ -3886,19 +4005,21 @@ export function App(props: AppProps): ReactElement {
|
|
|
3886
4005
|
} catch (error: unknown) {
|
|
3887
4006
|
setProviderError(error instanceof Error ? error.message : String(error))
|
|
3888
4007
|
}
|
|
3889
|
-
}, [modelOpen, props.subscribeModelProviders])
|
|
3890
|
-
useEffect(() => {
|
|
3891
|
-
const subscribe = props.subscribeProviderAuthorizations
|
|
3892
|
-
if (!modelOpen || subscribe === undefined) return
|
|
3893
|
-
try {
|
|
3894
|
-
return subscribe(() => setModelLoadEpoch(epoch => epoch + 1))
|
|
3895
|
-
} catch (error: unknown) {
|
|
3896
|
-
setAuthorizationError(error instanceof Error ? error.message : String(error))
|
|
3897
|
-
}
|
|
3898
|
-
}, [modelOpen, props.subscribeProviderAuthorizations])
|
|
4008
|
+
}, [modelOpen, props.subscribeModelProviders])
|
|
4009
|
+
useEffect(() => {
|
|
4010
|
+
const subscribe = props.subscribeProviderAuthorizations
|
|
4011
|
+
if (!modelOpen || subscribe === undefined) return
|
|
4012
|
+
try {
|
|
4013
|
+
return subscribe(() => setModelLoadEpoch(epoch => epoch + 1))
|
|
4014
|
+
} catch (error: unknown) {
|
|
4015
|
+
setAuthorizationError(error instanceof Error ? error.message : String(error))
|
|
4016
|
+
}
|
|
4017
|
+
}, [modelOpen, props.subscribeProviderAuthorizations])
|
|
3899
4018
|
|
|
3900
4019
|
const busy = view.busy
|
|
3901
4020
|
const [showReasoning, setShowReasoning] = useState(false)
|
|
4021
|
+
// Dedupe for the dynamic-budget tripwire: one warning per distinct shape.
|
|
4022
|
+
const budgetWarnRef = useRef<string | undefined>(undefined)
|
|
3902
4023
|
const [verboseOpen, setVerboseOpen] = useState(false)
|
|
3903
4024
|
const [diffView, setDiffView] = useState<GitDiffView | undefined>(undefined)
|
|
3904
4025
|
const [helpOpen, setHelpOpen] = useState(false)
|
|
@@ -4131,6 +4252,22 @@ export function App(props: AppProps): ReactElement {
|
|
|
4131
4252
|
? Math.max(1, Math.floor(streamRows / 3))
|
|
4132
4253
|
: 1
|
|
4133
4254
|
const answerRows = view.streaming === '' ? 0 : Math.max(1, streamRows - reasoningRows)
|
|
4255
|
+
// Dynamic-height tripwire: the allocation must fit dynamicRows by
|
|
4256
|
+
// construction; a future edit that breaks the derivation clamps here
|
|
4257
|
+
// (answer, then reasoning, then settled live rows) and warns once.
|
|
4258
|
+
const liveAudit = clampLiveAllocation(
|
|
4259
|
+
{ live: visibleLiveLines.length, reasoning: reasoningRows, answer: answerRows },
|
|
4260
|
+
dynamicRows,
|
|
4261
|
+
)
|
|
4262
|
+
if (liveAudit.warning !== undefined && budgetWarnRef.current !== liveAudit.warning) {
|
|
4263
|
+
budgetWarnRef.current = liveAudit.warning
|
|
4264
|
+
console.warn(`[dsh-code] ${liveAudit.warning}`)
|
|
4265
|
+
}
|
|
4266
|
+
const auditedLiveLines = liveAudit.allocation.live === visibleLiveLines.length
|
|
4267
|
+
? visibleLiveLines
|
|
4268
|
+
: visibleLiveLines.slice(-liveAudit.allocation.live)
|
|
4269
|
+
const auditedReasoningRows = liveAudit.allocation.reasoning
|
|
4270
|
+
const auditedAnswerRows = liveAudit.allocation.answer
|
|
4134
4271
|
const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !jobsOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !todosOpen && !verboseOpen && diffView === undefined && !approvalPending && !questionPending
|
|
4135
4272
|
const inspectorVisible = verboseOpen && !approvalPending && !questionPending
|
|
4136
4273
|
const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || jobsOpen || statuslineOpen || themeOpen || historyOpen || agentsOpen || subagentOpen || todosOpen || inspectorVisible || diffView !== undefined || approvalPending || questionPending
|
|
@@ -4155,7 +4292,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
4155
4292
|
appStdout.write(SYNCHRONIZED_UPDATE_END)
|
|
4156
4293
|
}, [appStdout, refreshEpoch])
|
|
4157
4294
|
// An idle Ctrl+R fold toggle joins resize and explicit Ctrl+L as a deliberate
|
|
4158
|
-
// source-backed rebuild of native scrollback
|
|
4295
|
+
// source-backed rebuild of native scrollback.
|
|
4159
4296
|
|
|
4160
4297
|
// Rendered-history cap: when the settled window overflows the trim
|
|
4161
4298
|
// hysteresis, one source-backed replay re-windows it (the rebuild branch
|
|
@@ -4168,21 +4305,21 @@ export function App(props: AppProps): ReactElement {
|
|
|
4168
4305
|
refreshScreen()
|
|
4169
4306
|
}, [settledNeedsTrim, busy, streamingActive])
|
|
4170
4307
|
|
|
4171
|
-
const sessionHasImages = useMemo(() => view.entries.some(entry =>
|
|
4172
|
-
(entry.kind === 'user' || entry.kind === 'pending') && (entry.images?.length ?? 0) > 0), [view.entries])
|
|
4173
|
-
|
|
4174
|
-
/** Apply one /model pick: record the selection, close the panel, report via notice. */
|
|
4175
|
-
const applyModel = (row: ModelRow, effortId: string | undefined): void => {
|
|
4176
|
-
try {
|
|
4177
|
-
const label = props.selectModel(row, effortId)
|
|
4178
|
-
setModelLabel(label)
|
|
4179
|
-
setEffortLabel(effortId)
|
|
4180
|
-
const selected = `${label}${effortId === undefined || effortId === '' ? '' : `@${effortId}`}`
|
|
4181
|
-
if (sessionHasImages && row.inputModalities !== undefined && !row.inputModalities.includes('image')) {
|
|
4182
|
-
notify(`model → ${selected} · image history will be sent as text placeholders`, 'warning')
|
|
4183
|
-
} else {
|
|
4184
|
-
notify(`model → next step uses ${selected}`)
|
|
4185
|
-
}
|
|
4308
|
+
const sessionHasImages = useMemo(() => view.entries.some(entry =>
|
|
4309
|
+
(entry.kind === 'user' || entry.kind === 'pending') && (entry.images?.length ?? 0) > 0), [view.entries])
|
|
4310
|
+
|
|
4311
|
+
/** Apply one /model pick: record the selection, close the panel, report via notice. */
|
|
4312
|
+
const applyModel = (row: ModelRow, effortId: string | undefined): void => {
|
|
4313
|
+
try {
|
|
4314
|
+
const label = props.selectModel(row, effortId)
|
|
4315
|
+
setModelLabel(label)
|
|
4316
|
+
setEffortLabel(effortId)
|
|
4317
|
+
const selected = `${label}${effortId === undefined || effortId === '' ? '' : `@${effortId}`}`
|
|
4318
|
+
if (sessionHasImages && row.inputModalities !== undefined && !row.inputModalities.includes('image')) {
|
|
4319
|
+
notify(`model → ${selected} · image history will be sent as text placeholders`, 'warning')
|
|
4320
|
+
} else {
|
|
4321
|
+
notify(`model → next step uses ${selected}`)
|
|
4322
|
+
}
|
|
4186
4323
|
setModelOpen(false)
|
|
4187
4324
|
setProviderOpen(false)
|
|
4188
4325
|
setProviderAction(undefined)
|
|
@@ -4202,44 +4339,44 @@ export function App(props: AppProps): ReactElement {
|
|
|
4202
4339
|
setEffortFor(undefined)
|
|
4203
4340
|
}
|
|
4204
4341
|
let modelSurface: ReactElement | undefined
|
|
4205
|
-
if (modelOpen && !approvalPending && !questionPending) {
|
|
4206
|
-
if (providerAction?.kind === 'login'
|
|
4207
|
-
&& props.beginProviderAuthorization !== undefined
|
|
4208
|
-
&& props.cancelProviderAuthorization !== undefined
|
|
4209
|
-
&& props.openAuthorizationUrl !== undefined
|
|
4210
|
-
&& props.copyTextValue !== undefined) {
|
|
4211
|
-
modelSurface = createElement(ProviderAuthorizationPanel, {
|
|
4212
|
-
row: providerAction.authorization,
|
|
4213
|
-
begin: props.beginProviderAuthorization,
|
|
4214
|
-
cancel: () => props.cancelProviderAuthorization!(providerAction.authorization),
|
|
4215
|
-
openUrl: props.openAuthorizationUrl,
|
|
4216
|
-
copy: props.copyTextValue,
|
|
4217
|
-
done: () => {
|
|
4218
|
-
const authorization = providerAction.authorization
|
|
4219
|
-
setProviderAction(undefined)
|
|
4220
|
-
setProviderOpen(false)
|
|
4221
|
-
reloadModelSurfaces()
|
|
4222
|
-
notify(`logged in to ${authorization.label}; select a model`)
|
|
4223
|
-
},
|
|
4224
|
-
back: () => {
|
|
4225
|
-
setProviderAction(undefined)
|
|
4226
|
-
setProviderOpen(true)
|
|
4227
|
-
},
|
|
4228
|
-
})
|
|
4229
|
-
} else if (providerAction?.kind === 'logout' && props.logoutProviderAuthorization !== undefined) {
|
|
4230
|
-
modelSurface = createElement(ProviderAuthorizationLogoutPanel, {
|
|
4231
|
-
row: providerAction.authorization,
|
|
4232
|
-
confirm: props.logoutProviderAuthorization,
|
|
4233
|
-
done: () => {
|
|
4234
|
-
const authorization = providerAction.authorization
|
|
4235
|
-
setProviderAction(undefined)
|
|
4236
|
-
setProviderOpen(true)
|
|
4237
|
-
reloadModelSurfaces()
|
|
4238
|
-
notify(`logged out from ${authorization.label}`)
|
|
4239
|
-
},
|
|
4240
|
-
back: () => setProviderAction(undefined),
|
|
4241
|
-
})
|
|
4242
|
-
} else if (providerAction?.kind === 'configure' && props.saveModelProviderConfiguration !== undefined) {
|
|
4342
|
+
if (modelOpen && !approvalPending && !questionPending) {
|
|
4343
|
+
if (providerAction?.kind === 'login'
|
|
4344
|
+
&& props.beginProviderAuthorization !== undefined
|
|
4345
|
+
&& props.cancelProviderAuthorization !== undefined
|
|
4346
|
+
&& props.openAuthorizationUrl !== undefined
|
|
4347
|
+
&& props.copyTextValue !== undefined) {
|
|
4348
|
+
modelSurface = createElement(ProviderAuthorizationPanel, {
|
|
4349
|
+
row: providerAction.authorization,
|
|
4350
|
+
begin: props.beginProviderAuthorization,
|
|
4351
|
+
cancel: () => props.cancelProviderAuthorization!(providerAction.authorization),
|
|
4352
|
+
openUrl: props.openAuthorizationUrl,
|
|
4353
|
+
copy: props.copyTextValue,
|
|
4354
|
+
done: () => {
|
|
4355
|
+
const authorization = providerAction.authorization
|
|
4356
|
+
setProviderAction(undefined)
|
|
4357
|
+
setProviderOpen(false)
|
|
4358
|
+
reloadModelSurfaces()
|
|
4359
|
+
notify(`logged in to ${authorization.label}; select a model`)
|
|
4360
|
+
},
|
|
4361
|
+
back: () => {
|
|
4362
|
+
setProviderAction(undefined)
|
|
4363
|
+
setProviderOpen(true)
|
|
4364
|
+
},
|
|
4365
|
+
})
|
|
4366
|
+
} else if (providerAction?.kind === 'logout' && props.logoutProviderAuthorization !== undefined) {
|
|
4367
|
+
modelSurface = createElement(ProviderAuthorizationLogoutPanel, {
|
|
4368
|
+
row: providerAction.authorization,
|
|
4369
|
+
confirm: props.logoutProviderAuthorization,
|
|
4370
|
+
done: () => {
|
|
4371
|
+
const authorization = providerAction.authorization
|
|
4372
|
+
setProviderAction(undefined)
|
|
4373
|
+
setProviderOpen(true)
|
|
4374
|
+
reloadModelSurfaces()
|
|
4375
|
+
notify(`logged out from ${authorization.label}`)
|
|
4376
|
+
},
|
|
4377
|
+
back: () => setProviderAction(undefined),
|
|
4378
|
+
})
|
|
4379
|
+
} else if (providerAction?.kind === 'configure' && props.saveModelProviderConfiguration !== undefined) {
|
|
4243
4380
|
modelSurface = createElement(ProviderConfigurationPanel, {
|
|
4244
4381
|
target: providerAction.target,
|
|
4245
4382
|
catalog: directory?.rows ?? [],
|
|
@@ -4295,11 +4432,11 @@ export function App(props: AppProps): ReactElement {
|
|
|
4295
4432
|
back: () => setProviderAction(undefined),
|
|
4296
4433
|
})
|
|
4297
4434
|
} else if (providerOpen) {
|
|
4298
|
-
modelSurface = createElement(ProviderPanel, {
|
|
4299
|
-
directory: providerDirectory,
|
|
4300
|
-
error: providerError,
|
|
4301
|
-
authorizations: authorizationDirectory,
|
|
4302
|
-
authorizationError,
|
|
4435
|
+
modelSurface = createElement(ProviderPanel, {
|
|
4436
|
+
directory: providerDirectory,
|
|
4437
|
+
error: providerError,
|
|
4438
|
+
authorizations: authorizationDirectory,
|
|
4439
|
+
authorizationError,
|
|
4303
4440
|
onCredential: (target: ProviderTargetView) => {
|
|
4304
4441
|
if (props.saveModelProviderCredential === undefined) {
|
|
4305
4442
|
notify('API key storage is unavailable in this profile', 'warning')
|
|
@@ -4321,34 +4458,34 @@ export function App(props: AppProps): ReactElement {
|
|
|
4321
4458
|
}
|
|
4322
4459
|
setProviderAction({ kind: 'unset', target })
|
|
4323
4460
|
},
|
|
4324
|
-
onRemove: (target: ProviderTargetView) => {
|
|
4461
|
+
onRemove: (target: ProviderTargetView) => {
|
|
4325
4462
|
if (props.removeModelProvider === undefined) {
|
|
4326
4463
|
notify('provider removal is unavailable in this profile', 'warning')
|
|
4327
4464
|
return
|
|
4328
4465
|
}
|
|
4329
|
-
setProviderAction({ kind: 'remove', target })
|
|
4330
|
-
},
|
|
4331
|
-
onLogin: (target: ProviderTargetView, authorization: ProviderAuthorizationRow) => {
|
|
4332
|
-
if (busy) {
|
|
4333
|
-
notify('provider login is available only while the agent is idle', 'warning')
|
|
4334
|
-
return
|
|
4335
|
-
}
|
|
4336
|
-
if (props.beginProviderAuthorization === undefined
|
|
4337
|
-
|| props.cancelProviderAuthorization === undefined
|
|
4338
|
-
|| props.openAuthorizationUrl === undefined
|
|
4339
|
-
|| props.copyTextValue === undefined) {
|
|
4340
|
-
notify('provider login is unavailable in this profile', 'warning')
|
|
4341
|
-
return
|
|
4342
|
-
}
|
|
4343
|
-
setProviderAction({ kind: 'login', target, authorization })
|
|
4344
|
-
},
|
|
4345
|
-
onLogout: (target: ProviderTargetView, authorization: ProviderAuthorizationRow) => {
|
|
4346
|
-
if (props.logoutProviderAuthorization === undefined) {
|
|
4347
|
-
notify('provider logout is unavailable in this profile', 'warning')
|
|
4348
|
-
return
|
|
4349
|
-
}
|
|
4350
|
-
setProviderAction({ kind: 'logout', target, authorization })
|
|
4351
|
-
},
|
|
4466
|
+
setProviderAction({ kind: 'remove', target })
|
|
4467
|
+
},
|
|
4468
|
+
onLogin: (target: ProviderTargetView, authorization: ProviderAuthorizationRow) => {
|
|
4469
|
+
if (busy) {
|
|
4470
|
+
notify('provider login is available only while the agent is idle', 'warning')
|
|
4471
|
+
return
|
|
4472
|
+
}
|
|
4473
|
+
if (props.beginProviderAuthorization === undefined
|
|
4474
|
+
|| props.cancelProviderAuthorization === undefined
|
|
4475
|
+
|| props.openAuthorizationUrl === undefined
|
|
4476
|
+
|| props.copyTextValue === undefined) {
|
|
4477
|
+
notify('provider login is unavailable in this profile', 'warning')
|
|
4478
|
+
return
|
|
4479
|
+
}
|
|
4480
|
+
setProviderAction({ kind: 'login', target, authorization })
|
|
4481
|
+
},
|
|
4482
|
+
onLogout: (target: ProviderTargetView, authorization: ProviderAuthorizationRow) => {
|
|
4483
|
+
if (props.logoutProviderAuthorization === undefined) {
|
|
4484
|
+
notify('provider logout is unavailable in this profile', 'warning')
|
|
4485
|
+
return
|
|
4486
|
+
}
|
|
4487
|
+
setProviderAction({ kind: 'logout', target, authorization })
|
|
4488
|
+
},
|
|
4352
4489
|
onRetry: reloadModelSurfaces,
|
|
4353
4490
|
onBack: () => setProviderOpen(false),
|
|
4354
4491
|
})
|
|
@@ -4401,22 +4538,39 @@ export function App(props: AppProps): ReactElement {
|
|
|
4401
4538
|
// prefix, so streaming text lands exactly where the composer's input
|
|
4402
4539
|
// text and the settled reply both render (Codex LIVE_PREFIX).
|
|
4403
4540
|
{ flexDirection: 'column' },
|
|
4404
|
-
|
|
4405
|
-
view.streamingReasoning !== '' &&
|
|
4406
|
-
?
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4541
|
+
auditedLiveLines.length === 0 ? undefined : createElement(StyledRows, { lines: auditedLiveLines }),
|
|
4542
|
+
view.streamingReasoning !== '' && auditedReasoningRows > 0
|
|
4543
|
+
? showReasoning
|
|
4544
|
+
? createElement(StreamTail, {
|
|
4545
|
+
text: view.streamingReasoning,
|
|
4546
|
+
prefix: '✻ ',
|
|
4547
|
+
continuationPrefix: ' ',
|
|
4548
|
+
dim: true,
|
|
4549
|
+
maxRows: auditedReasoningRows,
|
|
4550
|
+
})
|
|
4551
|
+
// The collapsed marker shimmers only while reasoning streams
|
|
4552
|
+
// alone: once answer text flows, a periodically re-rendered
|
|
4553
|
+
// animation component would race the store's frame-throttled
|
|
4554
|
+
// notifications and could defer the answer paint by tens to
|
|
4555
|
+
// hundreds of milliseconds (stream-burst contract), so the
|
|
4556
|
+
// marker falls back to the static dim row — same as Deep diving
|
|
4557
|
+
// always yields the live region to streaming content.
|
|
4558
|
+
: view.streaming === ''
|
|
4559
|
+
? createElement(ShimmerLine, { text: '✻ Thinking… (Ctrl/Alt+R to expand)' })
|
|
4560
|
+
: createElement(StreamTail, {
|
|
4561
|
+
text: 'Thinking… (Ctrl/Alt+R to expand)',
|
|
4562
|
+
prefix: '✻ ',
|
|
4563
|
+
continuationPrefix: ' ',
|
|
4564
|
+
dim: true,
|
|
4565
|
+
maxRows: auditedReasoningRows,
|
|
4566
|
+
})
|
|
4413
4567
|
: undefined,
|
|
4414
|
-
view.streaming !== '' &&
|
|
4568
|
+
view.streaming !== '' && auditedAnswerRows > 0
|
|
4415
4569
|
? createElement(
|
|
4416
4570
|
StreamTail,
|
|
4417
4571
|
// The same two-column gutter as settled replies: streamed text
|
|
4418
4572
|
// lands exactly where the assembled message will render.
|
|
4419
|
-
{ text: view.streaming, dim: false, maxRows:
|
|
4573
|
+
{ text: view.streaming, dim: false, maxRows: auditedAnswerRows, prefix: ' ' },
|
|
4420
4574
|
busy ? createElement(Caret) : undefined,
|
|
4421
4575
|
)
|
|
4422
4576
|
: undefined,
|
|
@@ -4424,7 +4578,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
4424
4578
|
)
|
|
4425
4579
|
: undefined,
|
|
4426
4580
|
transcriptVisible ? createElement(TodoPanel, { todos: view.todos }) : undefined,
|
|
4427
|
-
transcriptVisible ? createElement(AgentsLine, { rows: agentRows }) : undefined,
|
|
4581
|
+
transcriptVisible ? createElement(AgentsLine, { rows: agentRows, total: props.subagents.getTotalSeen() }) : undefined,
|
|
4428
4582
|
todosOpen && !approvalPending && !questionPending
|
|
4429
4583
|
? createElement(MemoTodoListPanel, {
|
|
4430
4584
|
todos: view.todos,
|
|
@@ -4593,16 +4747,17 @@ export function App(props: AppProps): ReactElement {
|
|
|
4593
4747
|
descriptors,
|
|
4594
4748
|
skills,
|
|
4595
4749
|
dispatch: props.dispatch,
|
|
4750
|
+
applyEditorKeys: props.applyEditorKeys,
|
|
4596
4751
|
steer: props.steer,
|
|
4597
4752
|
interrupt: props.interrupt,
|
|
4598
4753
|
quit: props.quit,
|
|
4599
4754
|
openModel: () => {
|
|
4600
4755
|
setDirectory(undefined)
|
|
4601
4756
|
setModelError(undefined)
|
|
4602
|
-
setProviderDirectory(undefined)
|
|
4603
|
-
setProviderError(undefined)
|
|
4604
|
-
setAuthorizationDirectory(undefined)
|
|
4605
|
-
setAuthorizationError(undefined)
|
|
4757
|
+
setProviderDirectory(undefined)
|
|
4758
|
+
setProviderError(undefined)
|
|
4759
|
+
setAuthorizationDirectory(undefined)
|
|
4760
|
+
setAuthorizationError(undefined)
|
|
4606
4761
|
setProviderOpen(false)
|
|
4607
4762
|
setProviderAction(undefined)
|
|
4608
4763
|
setEffortFor(undefined)
|
|
@@ -4692,19 +4847,20 @@ export function App(props: AppProps): ReactElement {
|
|
|
4692
4847
|
props.store.reset()
|
|
4693
4848
|
},
|
|
4694
4849
|
refresh: refreshScreen,
|
|
4695
|
-
// Ctrl+R flips the reasoning fold.
|
|
4696
|
-
//
|
|
4697
|
-
//
|
|
4698
|
-
//
|
|
4699
|
-
//
|
|
4700
|
-
//
|
|
4850
|
+
// Ctrl+R flips the reasoning fold. Rows already emitted through
|
|
4851
|
+
// Static are native scrollback, so the fold state of past entries can
|
|
4852
|
+
// only change through the source-backed replay (one clear + rebuild,
|
|
4853
|
+
// wrapped in a synchronized frame). Every toggle replays globally and
|
|
4854
|
+
// immediately — including mid-turn — so the whole transcript stays at
|
|
4855
|
+
// one fold state; the resize path already proves replaying during a
|
|
4856
|
+
// stream is safe.
|
|
4701
4857
|
toggleReasoning: () => {
|
|
4702
4858
|
setShowReasoning(current => !current)
|
|
4703
|
-
|
|
4859
|
+
refreshScreen()
|
|
4704
4860
|
},
|
|
4705
|
-
loadMentions: props.loadMentions,
|
|
4706
|
-
inspectImages: props.inspectImages,
|
|
4707
|
-
prepareImages: props.prepareImages,
|
|
4861
|
+
loadMentions: props.loadMentions,
|
|
4862
|
+
inspectImages: props.inspectImages,
|
|
4863
|
+
prepareImages: props.prepareImages,
|
|
4708
4864
|
cyclePermission: props.cyclePermission,
|
|
4709
4865
|
exportTranscript: props.exportTranscript,
|
|
4710
4866
|
renameTitle: props.renameTitle,
|