dsh-code 1.0.7 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +70 -24
- package/README.md +71 -25
- package/bin/deepseek.mjs +202 -39
- package/cordis.patch.yml +13 -4
- package/lib/index.mjs +5002 -1061
- package/lib/session-query.mjs +3 -2
- package/lib/startup.mjs +4 -4
- package/lib/{theme-DCT8Y2xf.mjs → theme-B3orFUYz.mjs} +665 -20
- package/lib/types/app.d.ts +120 -63
- package/lib/types/attachments.d.ts +16 -7
- package/lib/types/authorization-panel.d.ts +3 -3
- package/lib/types/git-workflow.d.ts +91 -2
- package/lib/types/history.d.ts +10 -0
- package/lib/types/i18n.d.ts +39 -0
- package/lib/types/index.d.ts +74 -2
- package/lib/types/input-split.d.ts +1 -1
- package/lib/types/kernel-panels.d.ts +89 -32
- package/lib/types/language-panel.d.ts +12 -0
- package/lib/types/locales/en.d.ts +498 -0
- package/lib/types/locales/zh.d.ts +9 -0
- package/lib/types/mentions.d.ts +7 -3
- package/lib/types/models.d.ts +14 -0
- package/lib/types/panel-accent.d.ts +28 -0
- package/lib/types/rainbow.d.ts +69 -0
- package/lib/types/render/animations.d.ts +42 -0
- package/lib/types/render/inspector.d.ts +26 -0
- package/lib/types/render/lines.d.ts +21 -1
- package/lib/types/render/markdown.d.ts +1 -1
- package/lib/types/render/projection.d.ts +95 -4
- package/lib/types/render/status.d.ts +12 -9
- package/lib/types/render/text.d.ts +6 -0
- package/lib/types/render/usage.d.ts +113 -0
- package/lib/types/session-directory.d.ts +42 -1
- package/lib/types/session-switch.d.ts +8 -0
- package/lib/types/startup.d.ts +1 -1
- package/lib/types/terminal-title.d.ts +8 -0
- package/lib/types/theme-panel.d.ts +2 -2
- package/lib/types/theme.d.ts +271 -52
- package/lib/types/update-panel.d.ts +27 -6
- package/lib/types/update.d.ts +10 -1
- package/lib/types/version.d.ts +6 -3
- package/package.json +26 -7
- package/src/app.ts +1426 -627
- package/src/approval.ts +166 -166
- package/src/attachments.ts +65 -19
- package/src/authorization-panel.ts +24 -18
- package/src/editor-keys.ts +371 -371
- package/src/fork.ts +11 -7
- package/src/git-workflow.ts +229 -3
- package/src/history.ts +14 -0
- package/src/i18n.ts +68 -0
- package/src/index.ts +503 -128
- package/src/input-split.ts +27 -7
- package/src/kernel-panels.ts +528 -113
- package/src/keyboard.ts +5 -4
- package/src/language-panel.ts +53 -0
- package/src/locales/en.ts +538 -0
- package/src/locales/zh.ts +537 -0
- package/src/mentions.ts +8 -4
- package/src/models.ts +264 -212
- package/src/panel-accent.ts +41 -0
- package/src/presets.ts +1 -1
- package/src/provider-settings.ts +1 -1
- package/src/rainbow.ts +218 -0
- package/src/render/animations.ts +104 -6
- package/src/render/editor.ts +20 -20
- package/src/render/export.ts +116 -95
- package/src/render/inspector.ts +42 -0
- package/src/render/lines.ts +628 -415
- package/src/render/markdown.ts +15 -3
- package/src/render/projection.ts +429 -19
- package/src/render/status.ts +119 -62
- package/src/render/text.ts +15 -0
- package/src/render/tool-preview.ts +77 -77
- package/src/render/usage.ts +430 -0
- package/src/render/width.ts +2 -2
- package/src/session-directory.ts +90 -9
- package/src/session-query.ts +8 -4
- package/src/session-switch.ts +14 -0
- package/src/startup.ts +3 -3
- package/src/store.ts +19 -1
- package/src/subagents.ts +229 -229
- package/src/terminal-title.ts +22 -5
- package/src/theme-panel.ts +17 -21
- package/src/theme.ts +281 -33
- package/src/update-panel.ts +148 -31
- package/src/update.ts +19 -3
- package/src/version.ts +63 -20
- package/src/whale-glyph.ts +23 -23
package/src/app.ts
CHANGED
|
@@ -26,18 +26,29 @@ import type { AskUserQuestionAnswerItem, AskUserQuestionItem } from '@deepseek-a
|
|
|
26
26
|
import type { AuthorizationInteraction, AuthorizationStatus } from '@deepseek-ai/dsh-authorization'
|
|
27
27
|
import {
|
|
28
28
|
dim,
|
|
29
|
+
diffBackground,
|
|
30
|
+
promptRowTokens,
|
|
31
|
+
rowBackground,
|
|
32
|
+
FLOW_ANCHORS,
|
|
29
33
|
getPalette,
|
|
30
34
|
getTheme,
|
|
31
35
|
inkColor,
|
|
36
|
+
isPrismatic,
|
|
37
|
+
isRainbow,
|
|
38
|
+
themeFlow,
|
|
32
39
|
setTheme,
|
|
33
40
|
type RgbTriple,
|
|
34
41
|
type ThemeName,
|
|
35
42
|
} from './theme.ts'
|
|
43
|
+
import { panelAccent } from './panel-accent.ts'
|
|
44
|
+
import { parseRainbowArgument, rainbowRoll, rainbowSeedLabel, rerollRainbow } from './rainbow.ts'
|
|
36
45
|
import { ThemePanel } from './theme-panel.ts'
|
|
37
|
-
import {
|
|
46
|
+
import { LanguagePanel } from './language-panel.ts'
|
|
47
|
+
import { getLanguage, parseLanguageName, t, type LanguageName, type MessageKey } from './i18n.ts'
|
|
48
|
+
import { UpdatePanel, subscribeUpdateApplyRunning } from './update-panel.ts'
|
|
38
49
|
import type { LauncherUpdateStatus } from './update.ts'
|
|
39
50
|
import { WHALE_GLYPH, WHALE_GLYPH_COLUMNS } from './whale-glyph.ts'
|
|
40
|
-
import {
|
|
51
|
+
import { dshKernelVersion, headerBrandTitle } from './version.ts'
|
|
41
52
|
import type { TranscriptStore } from './store.ts'
|
|
42
53
|
import { DEFAULT_TERMINAL_TITLE, sanitizeTerminalTitle, terminalTitleSequence, useTerminalTitle } from './terminal-title.ts'
|
|
43
54
|
import { settledEntryCount, type TranscriptEntry } from './render/projection.ts'
|
|
@@ -59,9 +70,14 @@ import {
|
|
|
59
70
|
deepseekWaveWordVisible,
|
|
60
71
|
deepDivingGradientColor,
|
|
61
72
|
deepDivingSparkColor,
|
|
73
|
+
flowColor,
|
|
62
74
|
effortAboveHigh,
|
|
63
75
|
isOfficialDeepSeekLabel,
|
|
64
76
|
parseAnimationsArgument,
|
|
77
|
+
RAINBOW_BURST_DURATION_MS,
|
|
78
|
+
RAINBOW_BURST_TICK_MS,
|
|
79
|
+
rainbowBurstColumnBg,
|
|
80
|
+
rainbowSpectrumHue,
|
|
65
81
|
type DeepseekWaveStyle,
|
|
66
82
|
type DeepseekWaveTier,
|
|
67
83
|
} from './render/animations.ts'
|
|
@@ -84,7 +100,8 @@ import type { QuestionSnapshot, QuestionStore } from './questions.ts'
|
|
|
84
100
|
import type { SkillsView, SkillRow } from './skills.ts'
|
|
85
101
|
import { isPathLikeMentionQuery, type MentionCandidate } from './mentions.ts'
|
|
86
102
|
import type { SubagentFeedView, SubagentRow } from './subagents.ts'
|
|
87
|
-
import {
|
|
103
|
+
import type { UsageView } from './render/usage.ts'
|
|
104
|
+
import { AgentsPanel, editQuery, EffortPanel, HistoryPanel, JobsPanel, ModePanel, PermissionPanel, PluginPanel, ResumePanel, ReviewPickerPanel, SchedulePanel, SearchPanel, StatuslinePanel, runClock, SubagentPanel, UsagePanel, type JobRow, type SearchRow } from './kernel-panels.ts'
|
|
88
105
|
import type { PresetRow } from './presets.ts'
|
|
89
106
|
import type { PermissionRow } from './permissions.ts'
|
|
90
107
|
import type { PluginRow } from './plugin-inventory.ts'
|
|
@@ -95,9 +112,10 @@ import {
|
|
|
95
112
|
recallOlder,
|
|
96
113
|
recordLocalEntry,
|
|
97
114
|
type RecallState,
|
|
115
|
+
appendRecall,
|
|
98
116
|
} from './history.ts'
|
|
99
117
|
import type { SessionDirectoryOptions, SessionRow } from './session-directory.ts'
|
|
100
|
-
import type
|
|
118
|
+
import { parseReviewArgument, type GitDiffView, type ReviewBranch, type ReviewCommit, type ReviewSelection } from './git-workflow.ts'
|
|
101
119
|
import {
|
|
102
120
|
authorizationForProvider,
|
|
103
121
|
providerAuthorizationStatus,
|
|
@@ -108,6 +126,7 @@ import { ProviderAuthorizationLogoutPanel, ProviderAuthorizationPanel } from './
|
|
|
108
126
|
import {
|
|
109
127
|
looksLikeImagePath,
|
|
110
128
|
parsePastedAttachmentPaths,
|
|
129
|
+
looksLikePathDraft,
|
|
111
130
|
type FilePathInspection,
|
|
112
131
|
type ImagePathInspection,
|
|
113
132
|
} from './attachments.ts'
|
|
@@ -141,6 +160,12 @@ function readSettledRowCap(): number {
|
|
|
141
160
|
|
|
142
161
|
/** Reset region/style, clear the visible screen and scrollback, then home. */
|
|
143
162
|
const RESIZE_REFLOW_CLEAR = '\x1b[r\x1b[0m\x1b[H\x1b[2J\x1b[3J\x1b[H'
|
|
163
|
+
/**
|
|
164
|
+
* Same as {@link RESIZE_REFLOW_CLEAR} without wiping native scrollback.
|
|
165
|
+
* History-cap trims remount `<Static>` but must not `\x1b[3J` a user who is
|
|
166
|
+
* reading earlier messages above the fold.
|
|
167
|
+
*/
|
|
168
|
+
const TRIM_REFLOW_CLEAR = '\x1b[r\x1b[0m\x1b[H\x1b[2J\x1b[H'
|
|
144
169
|
/** Ask terminals supporting DEC synchronized updates to hold the frame. */
|
|
145
170
|
const SYNCHRONIZED_UPDATE_BEGIN = '\x1b[?2026h'
|
|
146
171
|
/** Release the held frame after Ink has replayed the source-backed Static rows. */
|
|
@@ -148,7 +173,7 @@ const SYNCHRONIZED_UPDATE_END = '\x1b[?2026l'
|
|
|
148
173
|
import {
|
|
149
174
|
layoutStatusBar,
|
|
150
175
|
parseStatuslineItems,
|
|
151
|
-
|
|
176
|
+
statusCycleHint,
|
|
152
177
|
STATUS_GROUP_SEPARATOR,
|
|
153
178
|
STATUS_ITEM_SEPARATOR,
|
|
154
179
|
STATUS_ROW2_INDENT,
|
|
@@ -175,6 +200,7 @@ import {
|
|
|
175
200
|
followInspectorCursor,
|
|
176
201
|
inspectorViewport,
|
|
177
202
|
layoutGutterRows,
|
|
203
|
+
liveRegionBudget,
|
|
178
204
|
moveScroll,
|
|
179
205
|
panelViewport,
|
|
180
206
|
revealRow,
|
|
@@ -182,6 +208,8 @@ import {
|
|
|
182
208
|
} from './render/inspector.ts'
|
|
183
209
|
import {
|
|
184
210
|
clampLiveAllocation,
|
|
211
|
+
diffLineStyle,
|
|
212
|
+
fillDiffLineBars,
|
|
185
213
|
lineSegment,
|
|
186
214
|
markdownLines,
|
|
187
215
|
settledEntryLines,
|
|
@@ -224,39 +252,69 @@ import {
|
|
|
224
252
|
export type NoticeTone = 'info' | 'warning' | 'error'
|
|
225
253
|
|
|
226
254
|
/** One source of truth for TUI-owned slash commands in completion and `/help`. */
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
{ label: '/
|
|
232
|
-
{ label: '/
|
|
233
|
-
{ label: '/
|
|
234
|
-
{ label: '/
|
|
235
|
-
{ label: '/
|
|
236
|
-
{ label: '/
|
|
237
|
-
{ label: '/
|
|
238
|
-
{ label: '/
|
|
239
|
-
{ label: '/
|
|
240
|
-
{ label: '/
|
|
241
|
-
{ label: '/
|
|
242
|
-
{ label: '/
|
|
243
|
-
{ label: '/
|
|
244
|
-
{ label: '/
|
|
245
|
-
{ label: '/
|
|
246
|
-
{ label: '/
|
|
247
|
-
{ label: '/
|
|
248
|
-
{ label: '/
|
|
249
|
-
{ label: '/
|
|
250
|
-
{ label: '/
|
|
251
|
-
{ label: '/
|
|
252
|
-
{ label: '/
|
|
253
|
-
{ label: '/
|
|
254
|
-
{ label: '/
|
|
255
|
-
{ label: '/
|
|
255
|
+
/** One TUI-owned slash command: label plus its i18n description key. */
|
|
256
|
+
interface LocalCommand { readonly label: string; readonly descriptionKey: MessageKey }
|
|
257
|
+
|
|
258
|
+
const LOCAL_COMMANDS: readonly LocalCommand[] = [
|
|
259
|
+
{ label: '/help', descriptionKey: 'cmd.help' },
|
|
260
|
+
{ label: '/model', descriptionKey: 'cmd.model' },
|
|
261
|
+
{ label: '/effort', descriptionKey: 'cmd.effort' },
|
|
262
|
+
{ label: '/mode', descriptionKey: 'cmd.mode' },
|
|
263
|
+
{ label: '/permission', descriptionKey: 'cmd.permission' },
|
|
264
|
+
{ label: '/new', descriptionKey: 'cmd.new' },
|
|
265
|
+
{ label: '/fork', descriptionKey: 'cmd.fork' },
|
|
266
|
+
{ label: '/resume', descriptionKey: 'cmd.resume' },
|
|
267
|
+
{ label: '/search', descriptionKey: 'cmd.search' },
|
|
268
|
+
{ label: '/plugin', descriptionKey: 'cmd.plugin' },
|
|
269
|
+
{ label: '/update', descriptionKey: 'cmd.update' },
|
|
270
|
+
{ label: '/jobs', descriptionKey: 'cmd.jobs' },
|
|
271
|
+
{ label: '/schedule', descriptionKey: 'cmd.schedule' },
|
|
272
|
+
{ label: '/statusline', descriptionKey: 'cmd.statusline' },
|
|
273
|
+
{ label: '/theme', descriptionKey: 'cmd.theme' },
|
|
274
|
+
{ label: '/language', descriptionKey: 'cmd.language' },
|
|
275
|
+
{ label: '/rainbow', descriptionKey: 'cmd.rainbow' },
|
|
276
|
+
{ label: '/animation', descriptionKey: 'cmd.animation' },
|
|
277
|
+
{ label: '/history', descriptionKey: 'cmd.history' },
|
|
278
|
+
{ label: '/queue', descriptionKey: 'cmd.queue' },
|
|
279
|
+
{ label: '/usage', descriptionKey: 'cmd.usage' },
|
|
280
|
+
{ label: '/agents', descriptionKey: 'cmd.agents' },
|
|
281
|
+
{ label: '/todos', descriptionKey: 'cmd.todos' },
|
|
282
|
+
{ label: '/subagent', descriptionKey: 'cmd.subagent' },
|
|
283
|
+
{ label: '/vscode-keys', descriptionKey: 'cmd.vscode-keys' },
|
|
284
|
+
{ label: '/delete', descriptionKey: 'cmd.delete' },
|
|
285
|
+
{ label: '/clear', descriptionKey: 'cmd.clear' },
|
|
286
|
+
{ label: '/export', descriptionKey: 'cmd.export' },
|
|
287
|
+
{ label: '/title', descriptionKey: 'cmd.title' },
|
|
288
|
+
{ label: '/copy', descriptionKey: 'cmd.copy' },
|
|
289
|
+
{ label: '/diff', descriptionKey: 'cmd.diff' },
|
|
290
|
+
{ label: '/review', descriptionKey: 'cmd.review' },
|
|
291
|
+
{ label: '/quit', descriptionKey: 'cmd.quit' },
|
|
256
292
|
] as const
|
|
257
293
|
|
|
258
294
|
const LOCAL_COMMAND_NAMES = new Set(LOCAL_COMMANDS.map(command => command.label.slice(1)))
|
|
259
295
|
|
|
296
|
+
/**
|
|
297
|
+
* TUI-local commands that take no input. Extra tokens used to fall through
|
|
298
|
+
* as a prompt (`/queue clear` reached the model); they now surface usage.
|
|
299
|
+
*/
|
|
300
|
+
const BARE_LOCAL_COMMANDS = new Set([
|
|
301
|
+
'quit', 'help', 'clear', 'copy', 'update', 'schedule', 'statusline', 'theme',
|
|
302
|
+
'history', 'queue', 'usage', 'agents', 'todos', 'subagent',
|
|
303
|
+
])
|
|
304
|
+
|
|
305
|
+
/** Split a slash line into the command name and any trailing input. */
|
|
306
|
+
function slashNameAndArgs(text: string): { readonly name: string; readonly args: string } | undefined {
|
|
307
|
+
const match = /^\/([a-z][a-z0-9_-]*)(?:$|[\t ](.*))$/u.exec(text)
|
|
308
|
+
if (match === null || match[1] === undefined) return undefined
|
|
309
|
+
return { name: match[1], args: (match[2] ?? '').trim() }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** One mutation the terminal may request for a pending next-turn inbox item. */
|
|
313
|
+
export type QueueMutation =
|
|
314
|
+
| { readonly kind: 'remove' }
|
|
315
|
+
| { readonly kind: 'edit'; readonly text: string }
|
|
316
|
+
| { readonly kind: 'steer' }
|
|
317
|
+
|
|
260
318
|
/** Props the runner hands the app; callbacks stay owned by the runner. */
|
|
261
319
|
export interface AppProps {
|
|
262
320
|
/** Event-fed transcript store for the live session. */
|
|
@@ -295,133 +353,151 @@ export interface AppProps {
|
|
|
295
353
|
* an attachment prepare resolves after the app remounted onto another
|
|
296
354
|
* session, and the runner drops the stale delivery then.
|
|
297
355
|
*/
|
|
298
|
-
dispatch(text: string, attachments?: readonly ContentBlock[], origin?: string)
|
|
299
|
-
/**
|
|
300
|
-
|
|
356
|
+
dispatch: (text: string, attachments?: readonly ContentBlock[], origin?: string) => void
|
|
357
|
+
/**
|
|
358
|
+
* Submit one line as steering: it joins the turn already running at its next
|
|
359
|
+
* step boundary instead of waiting for the next turn. Same stale-delivery
|
|
360
|
+
* guard as {@link dispatch}.
|
|
361
|
+
*/
|
|
362
|
+
steer: (text: string, attachments?: readonly ContentBlock[], origin?: string) => void
|
|
301
363
|
/**
|
|
302
364
|
* The FULL current session identity ('' while the first session is pending)
|
|
303
365
|
* — the stale-delivery origin above. Distinct from the short display id.
|
|
304
366
|
*/
|
|
305
367
|
sessionKey: string
|
|
306
368
|
/** Interrupt the running turn (Esc); true when a turn was cancelled. */
|
|
307
|
-
interrupt()
|
|
369
|
+
interrupt: () => boolean
|
|
308
370
|
/** Quit: unmount, flush, and request process exit. */
|
|
309
|
-
quit()
|
|
371
|
+
quit: () => void
|
|
310
372
|
/** Load the selectable model directory (called when /model opens). */
|
|
311
|
-
loadModels()
|
|
373
|
+
loadModels: () => Promise<ModelDirectory>
|
|
312
374
|
/** Load @mention candidates for the typed query (files + sessions). */
|
|
313
|
-
loadMentions(query: string, signal?: AbortSignal)
|
|
375
|
+
loadMentions: (query: string, signal?: AbortSignal) => Promise<readonly MentionCandidate[]>
|
|
314
376
|
/** Validate draft image paths without committing attachment objects. */
|
|
315
|
-
inspectImages(paths: readonly string[])
|
|
377
|
+
inspectImages: (paths: readonly string[]) => Promise<readonly ImagePathInspection[]>
|
|
316
378
|
/** Validate, normalize and persist images immediately before submission. */
|
|
317
|
-
prepareImages(paths: readonly string[], signal?: AbortSignal)
|
|
379
|
+
prepareImages: (paths: readonly string[], signal?: AbortSignal) => Promise<readonly ImageBlock[]>
|
|
318
380
|
/** Validate draft non-image file paths without committing attachment objects. */
|
|
319
|
-
inspectFiles(paths: readonly string[])
|
|
381
|
+
inspectFiles: (paths: readonly string[]) => Promise<readonly FilePathInspection[]>
|
|
320
382
|
/** Persist non-image files immediately before submission as durable file blocks. */
|
|
321
|
-
prepareFiles(paths: readonly string[], signal?: AbortSignal)
|
|
383
|
+
prepareFiles: (paths: readonly string[], signal?: AbortSignal) => Promise<readonly FileBlock[]>
|
|
322
384
|
/** Apply one /model selection (with an advertised reasoning effort, when picked); returns the display label. */
|
|
323
|
-
selectModel(row: ModelRow, effortId?: string)
|
|
385
|
+
selectModel: (row: ModelRow, effortId?: string) => string
|
|
324
386
|
/** The /subagent override label, '' when delegated agents follow the current model. */
|
|
325
387
|
subagentModel: string
|
|
326
388
|
/** Apply one /subagent model pick; returns the override label. */
|
|
327
|
-
setSubagentModel(row: ModelRow, effortId?: string)
|
|
389
|
+
setSubagentModel: (row: ModelRow, effortId?: string) => string
|
|
328
390
|
/** Drop the /subagent override (delegated agents follow the current model). */
|
|
329
|
-
clearSubagentModel()
|
|
391
|
+
clearSubagentModel: () => void
|
|
330
392
|
/** Delete one session subtree; resolves with the outcome line. */
|
|
331
|
-
deleteSession(id: string)
|
|
393
|
+
deleteSession: (id: string) => Promise<string>
|
|
332
394
|
/** Load provider/settings/credential facts for the optional /model provider stage. */
|
|
333
|
-
loadModelProviders
|
|
395
|
+
loadModelProviders?: () => Promise<ProviderSettingsDirectory>
|
|
334
396
|
/** Subscribe to Harness credential/settings/adapter invalidations while /model is open. */
|
|
335
|
-
subscribeModelProviders
|
|
397
|
+
subscribeModelProviders?: (listener: () => void) => () => void
|
|
336
398
|
/** Store or rotate one provider credential through the Harness credential service. */
|
|
337
|
-
saveModelProviderCredential
|
|
399
|
+
saveModelProviderCredential?: (target: ProviderTargetView, key: string) => Promise<void>
|
|
338
400
|
/** Remove one writable provider credential without removing its settings profile. */
|
|
339
|
-
unsetModelProviderCredential
|
|
401
|
+
unsetModelProviderCredential?: (target: ProviderTargetView) => Promise<void>
|
|
340
402
|
/** Remove one user-owned provider profile and its page-managed credential. */
|
|
341
|
-
removeModelProvider
|
|
403
|
+
removeModelProvider?: (target: ProviderTargetView) => Promise<void>
|
|
342
404
|
/** Save endpoint and explicit model capacities through the provider profile. */
|
|
343
|
-
saveModelProviderConfiguration
|
|
405
|
+
saveModelProviderConfiguration?: (target: ProviderTargetView, configuration: ProviderConfiguration) => Promise<void>
|
|
344
406
|
/**
|
|
345
407
|
* Interrogate the provider's real endpoint (typed key wins over the stored
|
|
346
408
|
* credential) for the models it actually serves — the discovery stage of
|
|
347
409
|
* the provider setup page.
|
|
348
410
|
*/
|
|
349
|
-
discoverModelProvider
|
|
411
|
+
discoverModelProvider?: (
|
|
350
412
|
target: ProviderTargetView,
|
|
351
413
|
request: { readonly apiKey?: string; readonly baseURL?: string },
|
|
352
414
|
signal?: AbortSignal,
|
|
353
|
-
)
|
|
415
|
+
) => Promise<readonly DiscoveredModelView[]>
|
|
354
416
|
/** Provider authorization flows and value-free stored-record facts. */
|
|
355
|
-
loadProviderAuthorizations
|
|
356
|
-
subscribeProviderAuthorizations
|
|
357
|
-
beginProviderAuthorization
|
|
417
|
+
loadProviderAuthorizations?: () => Promise<ProviderAuthorizationDirectory>
|
|
418
|
+
subscribeProviderAuthorizations?: (listener: () => void) => () => void
|
|
419
|
+
beginProviderAuthorization?: (
|
|
358
420
|
row: ProviderAuthorizationRow,
|
|
359
421
|
method: string,
|
|
360
422
|
interaction: AuthorizationInteraction,
|
|
361
423
|
signal: AbortSignal,
|
|
362
|
-
)
|
|
363
|
-
cancelProviderAuthorization
|
|
364
|
-
logoutProviderAuthorization
|
|
365
|
-
openAuthorizationUrl
|
|
366
|
-
copyTextValue
|
|
424
|
+
) => Promise<AuthorizationStatus>
|
|
425
|
+
cancelProviderAuthorization?: (row: ProviderAuthorizationRow) => void
|
|
426
|
+
logoutProviderAuthorization?: (row: ProviderAuthorizationRow) => Promise<void>
|
|
427
|
+
openAuthorizationUrl?: (url: string) => boolean
|
|
428
|
+
copyTextValue?: (text: string) => Promise<void>
|
|
367
429
|
/** Cycle to the next mode station (Shift+Tab): a permission preset or a plan switch; returns the notice label. */
|
|
368
|
-
cycleMode()
|
|
430
|
+
cycleMode: () => string
|
|
369
431
|
/** Pre-session plan choice: shows the plan badge before the first session exists. */
|
|
370
432
|
pendingPlan?: boolean
|
|
371
433
|
/** Select or inspect a permission preset without requiring a pre-existing session. */
|
|
372
|
-
setPermission(id: string)
|
|
434
|
+
setPermission: (id: string) => string
|
|
373
435
|
/** Export the transcript to a markdown file (/export [path]); reports via notices. */
|
|
374
|
-
exportTranscript(argument: string)
|
|
436
|
+
exportTranscript: (argument: string) => Promise<void>
|
|
375
437
|
/** Rename the session (/title <text>); returns the outcome line for the notice. */
|
|
376
|
-
renameTitle(argument: string)
|
|
438
|
+
renameTitle: (argument: string) => string
|
|
377
439
|
/** Copy the latest complete assistant response; resolves to notice text. */
|
|
378
|
-
copyLastResponse()
|
|
440
|
+
copyLastResponse: () => Promise<string>
|
|
379
441
|
/** Load a complete read-only Git diff for the file-oriented viewport. */
|
|
380
|
-
loadGitDiff(argument: string)
|
|
442
|
+
loadGitDiff: (argument: string) => Promise<GitDiffView>
|
|
443
|
+
/** Local branches for the /review picker (absent: the picker hides the branch phase's list). */
|
|
444
|
+
listReviewBranches?: (signal?: AbortSignal) => Promise<readonly ReviewBranch[]>
|
|
445
|
+
/** Recent commits on the current branch for the /review picker. */
|
|
446
|
+
listReviewCommits?: (signal?: AbortSignal) => Promise<readonly ReviewCommit[]>
|
|
381
447
|
/** Start a model review after applying the read-only permission preset. */
|
|
382
|
-
reviewChanges(
|
|
448
|
+
reviewChanges: (selection: ReviewSelection) => void
|
|
383
449
|
/** Preset/session/plugin kernel operations. */
|
|
384
|
-
loadPresets()
|
|
385
|
-
switchMode(id: string)
|
|
450
|
+
loadPresets: () => Promise<readonly PresetRow[]>
|
|
451
|
+
switchMode: (id: string) => Promise<string>
|
|
386
452
|
/** Load the switchable permission presets for the /permission panel. */
|
|
387
|
-
loadPermissions()
|
|
388
|
-
createSession(mode?: string)
|
|
453
|
+
loadPermissions: () => Promise<readonly PermissionRow[]>
|
|
454
|
+
createSession: (mode?: string) => void
|
|
389
455
|
/** Fork the active session at a completed-turn boundary. */
|
|
390
|
-
forkSession(argument: string)
|
|
391
|
-
loadSessions(options: SessionDirectoryOptions, signal?: AbortSignal)
|
|
392
|
-
loadSessionTranscript(id: string, signal?: AbortSignal)
|
|
456
|
+
forkSession: (argument: string) => void
|
|
457
|
+
loadSessions: (options: SessionDirectoryOptions, signal?: AbortSignal) => Promise<readonly SessionRow[]>
|
|
458
|
+
loadSessionTranscript: (id: string, signal?: AbortSignal) => Promise<string>
|
|
459
|
+
/** Read the current session's usage blocks (projections plus per-turn fold). */
|
|
460
|
+
loadUsage: () => Promise<UsageView>
|
|
461
|
+
/**
|
|
462
|
+
* Full-text search over every persisted session (the in-process
|
|
463
|
+
* session-query engine). Absent when the deployment disabled the row;
|
|
464
|
+
* /search degrades to a notice instead of opening the panel.
|
|
465
|
+
*/
|
|
466
|
+
searchSessions?: (query: string, signal?: AbortSignal) => Promise<readonly SearchRow[]>
|
|
393
467
|
/** Load this session's subagent conversations (children by lineage). */
|
|
394
|
-
loadSubagents()
|
|
395
|
-
switchSession(row: SessionRow)
|
|
396
|
-
cancelSessionSwitch()
|
|
397
|
-
loadPlugins()
|
|
468
|
+
loadSubagents: () => Promise<readonly SessionRow[]>
|
|
469
|
+
switchSession: (row: SessionRow) => void
|
|
470
|
+
cancelSessionSwitch: () => boolean
|
|
471
|
+
loadPlugins: () => readonly PluginRow[]
|
|
398
472
|
/** Caller-visible background jobs (the host jobs registry, read-only). */
|
|
399
|
-
loadJobs()
|
|
473
|
+
loadJobs: () => readonly JobRow[]
|
|
400
474
|
/** Probe the launcher's aligned update plan (read-only; never installs). */
|
|
401
|
-
probeUpdate()
|
|
475
|
+
probeUpdate: () => Promise<LauncherUpdateStatus>
|
|
402
476
|
/** Run the launcher's aligned update; streams sanitized lines; resolves with the exit code. */
|
|
403
|
-
applyUpdate(onLine: (line: string) => void
|
|
477
|
+
applyUpdate: (onLine: (line: string) => void, plan?: { readonly dshSpec: string; readonly codeSpec: string; readonly pluginSpecs: readonly string[] }) => Promise<number>
|
|
404
478
|
/** Registers the app's notice channel with the runner (called once on mount). */
|
|
405
|
-
onBridgeReady(bridge: { notify(text: string, tone?: NoticeTone)
|
|
479
|
+
onBridgeReady: (bridge: { notify: (text: string, tone?: NoticeTone) => void }) => void
|
|
406
480
|
/** Ordered enabled status items (/statusline config); the runner owns persistence. */
|
|
407
481
|
statusline: readonly string[]
|
|
408
482
|
/** Persist a new statusline item set; the runner surfaces IO failures as notices. */
|
|
409
|
-
saveStatusline(items: readonly string[])
|
|
483
|
+
saveStatusline: (items: readonly string[]) => void
|
|
484
|
+
/** Apply and persist one /language selection; the runner owns the language.json file. */
|
|
485
|
+
saveLanguage: (name: LanguageName) => void
|
|
410
486
|
/** Apply and persist one /theme selection; the runner owns the theme.json file. */
|
|
411
|
-
saveTheme
|
|
487
|
+
saveTheme?: (name: ThemeName) => void
|
|
412
488
|
/** Whether timed animations run at startup (animations.json; on by default
|
|
413
489
|
* — like parseAnimationsPref, only an explicit false disables them). */
|
|
414
490
|
animations?: boolean
|
|
415
491
|
/** Apply and persist one /animation toggle; the runner owns the file. */
|
|
416
|
-
saveAnimations
|
|
492
|
+
saveAnimations?: (enabled: boolean) => void
|
|
417
493
|
/** Persistent cross-session input history (oldest first); the runner owns the file. */
|
|
418
494
|
history: readonly string[]
|
|
419
495
|
/** Persist one submitted prompt to the global history file. */
|
|
420
|
-
recordHistory(text: string)
|
|
421
|
-
/**
|
|
422
|
-
|
|
496
|
+
recordHistory: (text: string) => void
|
|
497
|
+
/** Mutate one next-turn inbox message; durable inbox splices reconcile the result. */
|
|
498
|
+
updateQueued?: (messageId: string, action: QueueMutation) => void
|
|
423
499
|
/** Apply the Ctrl+R terminal passthrough to the detected editor (/vscode-keys); resolves to a one-line summary. */
|
|
424
|
-
applyEditorKeys()
|
|
500
|
+
applyEditorKeys: () => Promise<string>
|
|
425
501
|
}
|
|
426
502
|
|
|
427
503
|
/** Pad text with spaces to a visible-column target (menu name column). */
|
|
@@ -475,7 +551,13 @@ function useStableInput(handler: (input: string, key: Key) => void, active: bool
|
|
|
475
551
|
*/
|
|
476
552
|
function BusyChase({ animated = true }: { animated?: boolean }): ReactElement {
|
|
477
553
|
const tick = useFrames(BUSY_CHASE_TICK_MS, animated)
|
|
478
|
-
|
|
554
|
+
// Flowing themes (prismatic, rainbow) ride their anchor walk while busy;
|
|
555
|
+
// every other theme (and the frozen state) keeps the palette's live accent.
|
|
556
|
+
const flow = themeFlow()
|
|
557
|
+
const marker = flow !== undefined && animated
|
|
558
|
+
? flowColor(tick * BUSY_CHASE_TICK_MS + flow.phaseMs, flow.anchors)
|
|
559
|
+
: getPalette().brandBright
|
|
560
|
+
return createElement(Text, { color: inkColor(marker) }, busyChaseFrame(tick) + ' ')
|
|
479
561
|
}
|
|
480
562
|
|
|
481
563
|
/** Blinking block caret appended to streaming text; solid when frozen. */
|
|
@@ -485,7 +567,7 @@ function Caret({ animated = true }: { animated?: boolean }): ReactElement {
|
|
|
485
567
|
}
|
|
486
568
|
|
|
487
569
|
/** One resettable input-caret phase shared by the entire composer. */
|
|
488
|
-
function useCursorBlink(active: boolean): { visible: boolean; reset()
|
|
570
|
+
function useCursorBlink(active: boolean): { visible: boolean; reset: () => void } {
|
|
489
571
|
const [epoch, setEpoch] = useState(0)
|
|
490
572
|
const [visible, setVisible] = useState(true)
|
|
491
573
|
useEffect(() => {
|
|
@@ -514,6 +596,13 @@ function useCursorBlink(active: boolean): { visible: boolean; reset(): void } {
|
|
|
514
596
|
function ShimmerLine({ text, animated = true }: { text: string; animated?: boolean }): ReactElement {
|
|
515
597
|
const tick = useFrames(DEEP_DIVING_SHIMMER_TICK_MS, animated)
|
|
516
598
|
const palette = getPalette()
|
|
599
|
+
// Flowing themes walk their anchors for the shimmer highlight so
|
|
600
|
+
// streaming text glows along the spectrum; other themes keep the bright
|
|
601
|
+
// accent.
|
|
602
|
+
const flow = themeFlow()
|
|
603
|
+
const highlight = flow !== undefined && animated
|
|
604
|
+
? flowColor(tick * DEEP_DIVING_SHIMMER_TICK_MS + flow.phaseMs, flow.anchors)
|
|
605
|
+
: palette.brandBright
|
|
517
606
|
const graphemes = splitGraphemes(text)
|
|
518
607
|
return createElement(
|
|
519
608
|
Text,
|
|
@@ -527,8 +616,8 @@ function ShimmerLine({ text, animated = true }: { text: string; animated?: boole
|
|
|
527
616
|
color: inkColor(!animated
|
|
528
617
|
? (sparkle ? palette.brandBright : palette.brandDeep)
|
|
529
618
|
: sparkle
|
|
530
|
-
? deepDivingSparkColor(tick, palette.brandDeep,
|
|
531
|
-
: deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep,
|
|
619
|
+
? deepDivingSparkColor(tick, palette.brandDeep, highlight)
|
|
620
|
+
: deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep, highlight)),
|
|
532
621
|
bold: sparkle || undefined,
|
|
533
622
|
},
|
|
534
623
|
grapheme.text,
|
|
@@ -556,27 +645,33 @@ function DeepDivingLine({ since, animated = true }: { since: number; animated?:
|
|
|
556
645
|
* cap counts explicit newlines and terminal wrapping, slicing from the END so
|
|
557
646
|
* the freshest tokens stay visible while a long reply streams; the complete
|
|
558
647
|
* text lands in the flushed scrollback once the turn assembles it.
|
|
648
|
+
*
|
|
649
|
+
* Body wrap width for a streaming tail. `rowColumns` is the same width
|
|
650
|
+
* passed to `transcriptEntryLines` (terminal minus the last-column safety);
|
|
651
|
+
* the hanging prefix then shrinks the body so streamed text and settled
|
|
652
|
+
* markdown wrap on the same column.
|
|
559
653
|
*/
|
|
560
|
-
function
|
|
654
|
+
export function streamTailBodyColumns(rowColumns: number, prefix: string, continuationPrefix = prefix): number {
|
|
655
|
+
const width = Math.max(1, Math.floor(rowColumns))
|
|
656
|
+
const prefixColumns = Math.max(visibleColumns(prefix), visibleColumns(continuationPrefix))
|
|
657
|
+
return Math.max(1, width - prefixColumns)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function StreamTail({ text, dim, maxRows, prefix = '', continuationPrefix = prefix, children, columns }: {
|
|
561
661
|
text: string
|
|
562
662
|
dim: boolean
|
|
563
663
|
maxRows: number
|
|
564
664
|
prefix?: string
|
|
565
665
|
continuationPrefix?: string
|
|
566
666
|
children?: ReactElement
|
|
667
|
+
/** Same physical row width `transcriptEntryLines` uses (terminal minus 2). */
|
|
668
|
+
columns: number
|
|
567
669
|
}): ReactElement {
|
|
568
|
-
const columns = useStdout().stdout?.columns ?? 80
|
|
569
670
|
const safeRows = Math.max(1, maxRows)
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
const
|
|
574
|
-
// Content takes the full physical row minus prefixes and the final wrap
|
|
575
|
-
// column — a forced 10-column FLOOR on a narrower terminal made every row
|
|
576
|
-
// autowrap onto a second, unbudgeted row (the live budget then
|
|
577
|
-
// under-counted and the tree overflowed), so the width now shrinks with
|
|
578
|
-
// the real terminal instead of flooring at 10.
|
|
579
|
-
const contentColumns = Math.max(1, columns - 1 - prefixColumns)
|
|
671
|
+
// Both prefixes participate because every physical row repeats its hanging
|
|
672
|
+
// indent. The wrap matches settled markdown (row width minus prefix), so
|
|
673
|
+
// the flush at turn end does not reflow the last paragraph.
|
|
674
|
+
const contentColumns = streamTailBodyColumns(columns, prefix, continuationPrefix)
|
|
580
675
|
const initial = displayTail(text, contentColumns, safeRows)
|
|
581
676
|
// Reserve one row for the omission marker only when a marker is needed.
|
|
582
677
|
const tail = initial.truncated && safeRows > 1
|
|
@@ -625,6 +720,18 @@ function segmentProps(style: MdSegment['style']): {
|
|
|
625
720
|
return { color: undefined, bold: true, italic: true, strikethrough: undefined }
|
|
626
721
|
case 'strike':
|
|
627
722
|
return { color: inkColor(getPalette().dim), bold: undefined, italic: undefined, strikethrough: true }
|
|
723
|
+
case 'diffAdd':
|
|
724
|
+
case 'diffDel':
|
|
725
|
+
// Inline-markdown twin of lineStyleProps' diff cases: tinted rows for
|
|
726
|
+
// ```diff fences rendered through the markdown span path. The diff
|
|
727
|
+
// foreground tokens stay AA-legible both on the row tints (when the
|
|
728
|
+
// background rides along) and on the plain terminal background.
|
|
729
|
+
return {
|
|
730
|
+
color: inkColor(style === 'diffAdd' ? getPalette().diffAddFg : getPalette().diffDelFg),
|
|
731
|
+
bold: undefined,
|
|
732
|
+
italic: undefined,
|
|
733
|
+
strikethrough: undefined,
|
|
734
|
+
}
|
|
628
735
|
default:
|
|
629
736
|
return { color: undefined, bold: undefined, italic: undefined, strikethrough: undefined }
|
|
630
737
|
}
|
|
@@ -637,20 +744,46 @@ function lineStyleProps(style: LineStyle): {
|
|
|
637
744
|
italic: boolean | undefined
|
|
638
745
|
strikethrough: boolean | undefined
|
|
639
746
|
dimColor: boolean | undefined
|
|
747
|
+
backgroundColor: string | undefined
|
|
640
748
|
} {
|
|
641
749
|
switch (style) {
|
|
642
750
|
case 'brand':
|
|
643
|
-
return { color: inkColor(getPalette().brandBright), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined }
|
|
751
|
+
return { color: inkColor(getPalette().brandBright), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: undefined }
|
|
644
752
|
case 'success':
|
|
645
|
-
return { color: inkColor(getPalette().success), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined }
|
|
753
|
+
return { color: inkColor(getPalette().success), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: undefined }
|
|
646
754
|
case 'error':
|
|
647
|
-
return { color: inkColor(getPalette().error), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined }
|
|
755
|
+
return { color: inkColor(getPalette().error), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: undefined }
|
|
648
756
|
case 'warn':
|
|
649
|
-
return { color: inkColor(getPalette().warn), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined }
|
|
757
|
+
return { color: inkColor(getPalette().warn), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: undefined }
|
|
650
758
|
case 'dimItalic':
|
|
651
|
-
return { color: inkColor(getPalette().dim), bold: undefined, italic: true, strikethrough: undefined, dimColor: undefined }
|
|
759
|
+
return { color: inkColor(getPalette().dim), bold: undefined, italic: true, strikethrough: undefined, dimColor: undefined, backgroundColor: undefined }
|
|
760
|
+
// Codex diff rendering: added/removed lines carry a theme tint behind
|
|
761
|
+
// the sign and text, with the AA-tuned diff foreground tokens on top; the
|
|
762
|
+
// depth gate turns this into plain foreground styling on 16-color
|
|
763
|
+
// terminals.
|
|
764
|
+
case 'diffAdd':
|
|
765
|
+
return { color: inkColor(getPalette().diffAddFg), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: diffBackground('diffAdd') }
|
|
766
|
+
case 'diffDel':
|
|
767
|
+
return { color: inkColor(getPalette().diffDelFg), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: diffBackground('diffDel') }
|
|
768
|
+
// Prompt rows: one full-width bar per delivery kind. The tint comes from
|
|
769
|
+
// the row token and the foreground from its AA-tuned twin; the depth gate
|
|
770
|
+
// inside rowBackground degrades this to foreground-only on 16-color
|
|
771
|
+
// terminals, matching the diff rows.
|
|
772
|
+
case 'promptRow':
|
|
773
|
+
case 'promptQueuedRow':
|
|
774
|
+
case 'promptSteeredRow': {
|
|
775
|
+
const tokens = promptRowTokens(style === 'promptQueuedRow' ? 'queued' : style === 'promptSteeredRow' ? 'steered' : undefined)
|
|
776
|
+
return {
|
|
777
|
+
color: inkColor(getPalette()[tokens.fg]),
|
|
778
|
+
bold: undefined,
|
|
779
|
+
italic: undefined,
|
|
780
|
+
strikethrough: undefined,
|
|
781
|
+
dimColor: undefined,
|
|
782
|
+
backgroundColor: rowBackground(tokens.fg),
|
|
783
|
+
}
|
|
784
|
+
}
|
|
652
785
|
default:
|
|
653
|
-
return { ...segmentProps(style), dimColor: undefined }
|
|
786
|
+
return { ...segmentProps(style), dimColor: undefined, backgroundColor: undefined }
|
|
654
787
|
}
|
|
655
788
|
}
|
|
656
789
|
|
|
@@ -674,7 +807,7 @@ function StyledRows({ lines }: { lines: readonly StyledLine[] }): ReactElement {
|
|
|
674
807
|
}
|
|
675
808
|
|
|
676
809
|
/** File-oriented, color-coded unified diff viewport. */
|
|
677
|
-
function DiffPanel({ view, onClose }: { view: GitDiffView; onClose()
|
|
810
|
+
function DiffPanel({ view, onClose }: { view: GitDiffView; onClose: () => void }): ReactElement {
|
|
678
811
|
const stdout = useStdout().stdout
|
|
679
812
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
680
813
|
const [fileIndex, setFileIndex] = useState(0)
|
|
@@ -682,15 +815,7 @@ function DiffPanel({ view, onClose }: { view: GitDiffView; onClose(): void }): R
|
|
|
682
815
|
const file = view.files[fileIndex]
|
|
683
816
|
const lines = useMemo(() => {
|
|
684
817
|
if (file === undefined) return textLines(' (no changes)', viewport.contentColumns, 'dim')
|
|
685
|
-
return file.lines.flatMap(line => styledLines([
|
|
686
|
-
lineSegment(line, line.startsWith('+') && !line.startsWith('+++')
|
|
687
|
-
? 'success'
|
|
688
|
-
: line.startsWith('-') && !line.startsWith('---')
|
|
689
|
-
? 'error'
|
|
690
|
-
: line.startsWith('@@') || line.startsWith('diff --git') || line.startsWith('index ')
|
|
691
|
-
? 'brand'
|
|
692
|
-
: 'dim'),
|
|
693
|
-
], viewport.contentColumns))
|
|
818
|
+
return fillDiffLineBars(file.lines.flatMap(line => styledLines([lineSegment(line, diffLineStyle(line))], viewport.contentColumns)), viewport.contentColumns)
|
|
694
819
|
}, [file, viewport.contentColumns])
|
|
695
820
|
const visibleScroll = clampScroll(scroll, lines.length, viewport.bodyRows)
|
|
696
821
|
useInput((input, key) => {
|
|
@@ -709,15 +834,16 @@ function DiffPanel({ view, onClose }: { view: GitDiffView; onClose(): void }): R
|
|
|
709
834
|
else if (key.pageUp) setScroll(current => moveScroll(current, -viewport.bodyRows, lines.length, viewport.bodyRows))
|
|
710
835
|
else if (key.pageDown) setScroll(current => moveScroll(current, viewport.bodyRows, lines.length, viewport.bodyRows))
|
|
711
836
|
})
|
|
712
|
-
if (viewport.compact) return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(
|
|
837
|
+
if (viewport.compact) return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.diff.compact', { title: view.title, files: view.files.length }), viewport.contentColumns))
|
|
838
|
+
const accent = panelAccent('diff', getPalette().dim, getPalette().brand)
|
|
713
839
|
return createElement(
|
|
714
840
|
Box,
|
|
715
|
-
{ flexDirection: 'column', borderStyle: 'round', borderColor: inkColor(
|
|
716
|
-
createElement(Text, { color: inkColor(
|
|
841
|
+
{ flexDirection: 'column', borderStyle: 'round', borderColor: inkColor(accent.border), paddingX: 1 },
|
|
842
|
+
createElement(Text, { color: inkColor(accent.title), bold: true, wrap: 'truncate-end' }, truncateColumns(`${view.title} · ${view.files.length === 0 ? t('panel.diff.noFiles') : `${fileIndex + 1}/${view.files.length} ${file?.path ?? ''}`} · rows ${lines.length === 0 ? 0 : visibleScroll + 1}-${Math.min(lines.length, visibleScroll + viewport.bodyRows)}/${lines.length}`, viewport.contentColumns)),
|
|
717
843
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
718
844
|
createElement(StyledRows, { lines: lines.slice(visibleScroll, visibleScroll + viewport.bodyRows) }),
|
|
719
845
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
720
|
-
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns('
|
|
846
|
+
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(t('panel.diff.footer'), viewport.contentColumns)),
|
|
721
847
|
)
|
|
722
848
|
}
|
|
723
849
|
|
|
@@ -735,6 +861,32 @@ function PanelGap({ visible }: { visible: boolean }): ReactElement | undefined {
|
|
|
735
861
|
* keeps its historical three lines. Short or narrow terminals keep a one-line
|
|
736
862
|
* form without the kernel line.
|
|
737
863
|
*/
|
|
864
|
+
/**
|
|
865
|
+
* One whale-glyph row painted as a frozen seven-color spectrum (column 0
|
|
866
|
+
* red, column 25 violet). Spaces stay uncolored so the silhouette punches
|
|
867
|
+
* through; adjacent same-hue blocks merge into one span.
|
|
868
|
+
*/
|
|
869
|
+
function rainbowGlyphRow(row: string, rowKey: number): ReactElement {
|
|
870
|
+
const span = Math.max(1, WHALE_GLYPH_COLUMNS - 1)
|
|
871
|
+
const children: ReactElement[] = []
|
|
872
|
+
let start = 0
|
|
873
|
+
while (start < row.length) {
|
|
874
|
+
if (row[start] === ' ') {
|
|
875
|
+
let end = start + 1
|
|
876
|
+
while (end < row.length && row[end] === ' ') end += 1
|
|
877
|
+
children.push(createElement(Text, { key: start }, row.slice(start, end)))
|
|
878
|
+
start = end
|
|
879
|
+
continue
|
|
880
|
+
}
|
|
881
|
+
const color = inkColor(rainbowSpectrumHue(start / span))
|
|
882
|
+
let end = start + 1
|
|
883
|
+
while (end < row.length && row[end] !== ' ' && inkColor(rainbowSpectrumHue(end / span)) === color) end += 1
|
|
884
|
+
children.push(createElement(Text, { key: start, color }, row.slice(start, end)))
|
|
885
|
+
start = end
|
|
886
|
+
}
|
|
887
|
+
return createElement(Text, { key: rowKey }, ...children)
|
|
888
|
+
}
|
|
889
|
+
|
|
738
890
|
function Header({ resumed }: { resumed: boolean }): ReactElement {
|
|
739
891
|
const stdout = useStdout().stdout
|
|
740
892
|
const rows = stdout?.rows ?? 40
|
|
@@ -743,13 +895,13 @@ function Header({ resumed }: { resumed: boolean }): ReactElement {
|
|
|
743
895
|
const version = dshKernelVersion()
|
|
744
896
|
return version === undefined ? undefined : `dsh-v${version}`
|
|
745
897
|
})()
|
|
746
|
-
const title =
|
|
898
|
+
const title = headerBrandTitle()
|
|
747
899
|
const slogan = 'Into the Unknown 探索未至之境'
|
|
748
|
-
const hint = resumed ? '
|
|
900
|
+
const hint = resumed ? t('header.hintResumed') : t('header.hint')
|
|
749
901
|
const copyWidths = [visibleColumns(title), visibleColumns(slogan), visibleColumns(hint)]
|
|
750
902
|
if (kernelLine !== undefined) copyWidths.push(visibleColumns(kernelLine))
|
|
751
903
|
const copyColumns = Math.max(...copyWidths)
|
|
752
|
-
const compact = `${title} · ${hint}`
|
|
904
|
+
const compact = kernelLine === undefined ? `${title} · ${hint}` : `${title} · ${kernelLine} · ${hint}`
|
|
753
905
|
if (rows < 20 || columns < WHALE_GLYPH_COLUMNS + copyColumns + 10) {
|
|
754
906
|
return createElement(
|
|
755
907
|
Box,
|
|
@@ -768,7 +920,10 @@ function Header({ resumed }: { resumed: boolean }): ReactElement {
|
|
|
768
920
|
createElement(
|
|
769
921
|
Box,
|
|
770
922
|
{ flexDirection: 'column', width: WHALE_GLYPH_COLUMNS, justifyContent: 'center' },
|
|
771
|
-
...WHALE_GLYPH.map((row, index) =>
|
|
923
|
+
...WHALE_GLYPH.map((row, index) =>
|
|
924
|
+
isRainbow()
|
|
925
|
+
? rainbowGlyphRow(row, index)
|
|
926
|
+
: createElement(Text, { key: index, color: inkColor(getPalette().brand) }, row)),
|
|
772
927
|
),
|
|
773
928
|
createElement(
|
|
774
929
|
Box,
|
|
@@ -803,7 +958,7 @@ function todoMark(status: TodoItem['status']): string {
|
|
|
803
958
|
function AgentsLine({ rows, total }: { rows: readonly SubagentRow[]; total: number }): ReactElement | undefined {
|
|
804
959
|
if (rows.length === 0) return undefined
|
|
805
960
|
const running = rows.filter(row => row.state !== 'done').length
|
|
806
|
-
const newest = [...rows].sort((left, right) => right.updatedAt - left.updatedAt)[0]
|
|
961
|
+
const newest = [...rows].sort((left, right) => right.updatedAt - left.updatedAt)[0]
|
|
807
962
|
const mark = newest.state === 'done' ? '✓' : newest.state === 'idle' ? '⏸' : '●'
|
|
808
963
|
return createElement(
|
|
809
964
|
Box,
|
|
@@ -856,7 +1011,7 @@ function TodoListPanel({ todos, onClose }: { todos: readonly TodoItem[]; onClose
|
|
|
856
1011
|
const inProgress = todos.filter(todo => todo.status === 'in_progress').length
|
|
857
1012
|
const pending = todos.length - completed - inProgress
|
|
858
1013
|
const rows = todos.length === 0
|
|
859
|
-
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' }, '
|
|
1014
|
+
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' }, ` ${t('panel.todos.empty')}`)]
|
|
860
1015
|
: todos.map(todo => createElement(
|
|
861
1016
|
Text,
|
|
862
1017
|
{ key: todo.content, dimColor: true, wrap: 'truncate-end' },
|
|
@@ -885,40 +1040,202 @@ function TodoListPanel({ todos, onClose }: { todos: readonly TodoItem[]; onClose
|
|
|
885
1040
|
})
|
|
886
1041
|
|
|
887
1042
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
888
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('todos
|
|
1043
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.todos.compact'), viewport.contentColumns))
|
|
889
1044
|
}
|
|
890
1045
|
|
|
1046
|
+
const accent = panelAccent('todos', getPalette().brand)
|
|
891
1047
|
return createElement(
|
|
892
1048
|
Box,
|
|
893
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
894
|
-
createElement(Text, { color: inkColor(
|
|
1049
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
1050
|
+
createElement(Text, { color: inkColor(accent.title), bold: true, wrap: 'truncate-end' }, truncateColumns(t('panel.todos.title', { done: completed, total: todos.length, active: inProgress, pending, from: rows.length === 0 ? 0 : visibleScroll + 1, to: Math.min(rows.length, visibleScroll + viewport.bodyRows), rows: rows.length }), viewport.contentColumns)),
|
|
895
1051
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
896
1052
|
...rows.slice(visibleScroll, visibleScroll + viewport.bodyRows),
|
|
897
1053
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
898
|
-
createElement(Text, {
|
|
1054
|
+
createElement(Text, { wrap: 'truncate-end' }, dim(truncateColumns(t('panel.todos.footer'), viewport.contentColumns))),
|
|
899
1055
|
)
|
|
900
1056
|
}
|
|
901
1057
|
|
|
902
1058
|
const MemoTodoListPanel = memo(TodoListPanel)
|
|
903
1059
|
|
|
1060
|
+
/** Rows in the exact next-turn inbox order, never transcript append order. */
|
|
1061
|
+
export function queuedInboxRows(
|
|
1062
|
+
entries: readonly TranscriptEntry[],
|
|
1063
|
+
ids: readonly string[],
|
|
1064
|
+
): readonly Extract<TranscriptEntry, { kind: 'pending' }>[] {
|
|
1065
|
+
const byId = new Map<string, Extract<TranscriptEntry, { kind: 'pending' }>>()
|
|
1066
|
+
for (const entry of entries) {
|
|
1067
|
+
if (entry.kind === 'pending' && entry.target === 'next-turn') byId.set(entry.messageId, entry)
|
|
1068
|
+
}
|
|
1069
|
+
return ids.flatMap(id => {
|
|
1070
|
+
const row = byId.get(id)
|
|
1071
|
+
return row === undefined ? [] : [row]
|
|
1072
|
+
})
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/** A bounded, keyboard-owned management surface for the durable next-turn inbox. */
|
|
1076
|
+
function QueuePanel({ rows, busy, update, onClose }: {
|
|
1077
|
+
rows: readonly Extract<TranscriptEntry, { kind: 'pending' }>[]
|
|
1078
|
+
busy: boolean
|
|
1079
|
+
update?: (messageId: string, action: QueueMutation) => void
|
|
1080
|
+
onClose: () => void
|
|
1081
|
+
}): ReactElement {
|
|
1082
|
+
const stdout = useStdout().stdout
|
|
1083
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
1084
|
+
const [selected, setSelected] = useState(0)
|
|
1085
|
+
const [scroll, setScroll] = useState(0)
|
|
1086
|
+
const [editing, setEditing] = useState<{ messageId: string; text: string; cursor: number } | undefined>(undefined)
|
|
1087
|
+
// Ink can deliver a following key before its effect swaps the input
|
|
1088
|
+
// listener after an edit-mode render; this ref keeps the editor's key
|
|
1089
|
+
// stream coherent while the visible state catches up.
|
|
1090
|
+
const editingRef = useRef(editing)
|
|
1091
|
+
const current = rows[selected]
|
|
1092
|
+
const visibleScroll = revealRow(clampScroll(scroll, rows.length, viewport.bodyRows), selected, rows.length, viewport.bodyRows)
|
|
1093
|
+
const move = (delta: number): void => {
|
|
1094
|
+
setSelected(current => Math.max(0, Math.min(rows.length - 1, current + delta)))
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
useEffect(() => {
|
|
1098
|
+
setSelected(current => Math.max(0, Math.min(rows.length - 1, current)))
|
|
1099
|
+
if (editing !== undefined && !rows.some(row => row.messageId === editing.messageId)) {
|
|
1100
|
+
editingRef.current = undefined
|
|
1101
|
+
setEditing(undefined)
|
|
1102
|
+
}
|
|
1103
|
+
}, [rows, editing])
|
|
1104
|
+
useEffect(() => {
|
|
1105
|
+
if (visibleScroll !== scroll) setScroll(visibleScroll)
|
|
1106
|
+
}, [visibleScroll, scroll])
|
|
1107
|
+
|
|
1108
|
+
useStableInput((input, key) => {
|
|
1109
|
+
const activeEdit = editingRef.current
|
|
1110
|
+
if (activeEdit !== undefined) {
|
|
1111
|
+
if (key.escape) {
|
|
1112
|
+
editingRef.current = undefined
|
|
1113
|
+
setEditing(undefined)
|
|
1114
|
+
return
|
|
1115
|
+
}
|
|
1116
|
+
if (key.return) {
|
|
1117
|
+
if (activeEdit.text.trim() !== '') update?.(activeEdit.messageId, { kind: 'edit', text: activeEdit.text })
|
|
1118
|
+
editingRef.current = undefined
|
|
1119
|
+
setEditing(undefined)
|
|
1120
|
+
return
|
|
1121
|
+
}
|
|
1122
|
+
if (key.leftArrow) {
|
|
1123
|
+
const next = { ...activeEdit, cursor: moveCursorBy(activeEdit.text, activeEdit.cursor, -1) }
|
|
1124
|
+
editingRef.current = next
|
|
1125
|
+
setEditing(next)
|
|
1126
|
+
return
|
|
1127
|
+
}
|
|
1128
|
+
if (key.rightArrow) {
|
|
1129
|
+
const next = { ...activeEdit, cursor: moveCursorBy(activeEdit.text, activeEdit.cursor, 1) }
|
|
1130
|
+
editingRef.current = next
|
|
1131
|
+
setEditing(next)
|
|
1132
|
+
return
|
|
1133
|
+
}
|
|
1134
|
+
// Ink 5 reports 0x7F (backspace) and the forward-delete sequence as the
|
|
1135
|
+
// same `key.delete`, so a bare Delete binding here would erase on a
|
|
1136
|
+
// habitual Backspace. This management surface keeps `d` as its only
|
|
1137
|
+
// removal key instead of guessing which byte arrived.
|
|
1138
|
+
if (key.backspace || key.delete) {
|
|
1139
|
+
const edit = deleteBackward(activeEdit.text, activeEdit.cursor)
|
|
1140
|
+
const next = { ...activeEdit, text: edit.value, cursor: edit.cursor }
|
|
1141
|
+
editingRef.current = next
|
|
1142
|
+
setEditing(next)
|
|
1143
|
+
return
|
|
1144
|
+
}
|
|
1145
|
+
if (input !== '' && !key.ctrl && !key.meta) {
|
|
1146
|
+
const edit = insertText(activeEdit.text, activeEdit.cursor, input)
|
|
1147
|
+
const next = { ...activeEdit, text: edit.value, cursor: edit.cursor }
|
|
1148
|
+
editingRef.current = next
|
|
1149
|
+
setEditing(next)
|
|
1150
|
+
}
|
|
1151
|
+
return
|
|
1152
|
+
}
|
|
1153
|
+
if (key.escape || input === 'q') {
|
|
1154
|
+
onClose()
|
|
1155
|
+
return
|
|
1156
|
+
}
|
|
1157
|
+
if (key.upArrow) move(-1)
|
|
1158
|
+
else if (key.downArrow) move(1)
|
|
1159
|
+
else if (key.pageUp) move(-Math.max(1, viewport.bodyRows - 1))
|
|
1160
|
+
else if (key.pageDown) move(Math.max(1, viewport.bodyRows - 1))
|
|
1161
|
+
else if (input === 'g') setSelected(0)
|
|
1162
|
+
else if (input === 'G') setSelected(Math.max(0, rows.length - 1))
|
|
1163
|
+
else if (input === 'e' && current !== undefined) {
|
|
1164
|
+
// Text is editable on every row: an edit rewrites what the user typed
|
|
1165
|
+
// and carries the row's attachments through untouched, which is exactly
|
|
1166
|
+
// what the row's read-only attachment marker promises.
|
|
1167
|
+
const next = { messageId: current.messageId, text: current.text, cursor: current.text.length }
|
|
1168
|
+
editingRef.current = next
|
|
1169
|
+
setEditing(next)
|
|
1170
|
+
}
|
|
1171
|
+
else if (input === 'd' && current !== undefined) update?.(current.messageId, { kind: 'remove' })
|
|
1172
|
+
else if (key.return && current !== undefined && busy) update?.(current.messageId, { kind: 'steer' })
|
|
1173
|
+
}, true)
|
|
1174
|
+
|
|
1175
|
+
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
1176
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.queue.compact'), viewport.contentColumns))
|
|
1177
|
+
}
|
|
1178
|
+
const body = rows.length === 0
|
|
1179
|
+
? [createElement(Text, { key: 'empty', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(t('panel.queue.empty'), viewport.contentColumns))]
|
|
1180
|
+
: rows.map((row, index) => {
|
|
1181
|
+
const selectedRow = index === selected
|
|
1182
|
+
const suffix = (row.images?.length ?? 0) + (row.files?.length ?? 0) > 0 ? ` ${t('panel.queue.attachments')}` : ''
|
|
1183
|
+
if (editing?.messageId === row.messageId) {
|
|
1184
|
+
const before = editing.text.slice(0, editing.cursor)
|
|
1185
|
+
const caret = editing.text.slice(editing.cursor, editing.cursor + 1) || ' '
|
|
1186
|
+
const after = editing.text.slice(editing.cursor + caret.length)
|
|
1187
|
+
return createElement(Text, { key: row.messageId, color: inkColor(getPalette().brandBright), wrap: 'truncate-end' }, truncateColumns(`✎ ${before}[${caret}]${after}`, viewport.contentColumns))
|
|
1188
|
+
}
|
|
1189
|
+
return createElement(Text, { key: row.messageId, color: inkColor(selectedRow ? getPalette().brandBright : getPalette().dim), bold: selectedRow || undefined, wrap: 'truncate-end' }, truncateColumns(`${selectedRow ? '›' : ' '} ${index + 1}. ${singleLineText(row.text)}${suffix}`, viewport.contentColumns))
|
|
1190
|
+
})
|
|
1191
|
+
const footer = editing !== undefined
|
|
1192
|
+
? t('panel.queue.editFooter')
|
|
1193
|
+
: busy
|
|
1194
|
+
? t('panel.queue.footerBusy')
|
|
1195
|
+
: t('panel.queue.footerIdle')
|
|
1196
|
+
const accent = panelAccent('queue', getPalette().brand)
|
|
1197
|
+
return createElement(
|
|
1198
|
+
Box,
|
|
1199
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
1200
|
+
createElement(Text, { color: inkColor(accent.title), bold: true, wrap: 'truncate-end' }, truncateColumns(t('panel.queue.title', { count: rows.length, from: rows.length === 0 ? 0 : visibleScroll + 1, to: Math.min(rows.length, visibleScroll + viewport.bodyRows) }), viewport.contentColumns)),
|
|
1201
|
+
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1202
|
+
...body.slice(visibleScroll, visibleScroll + viewport.bodyRows),
|
|
1203
|
+
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1204
|
+
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(footer, viewport.contentColumns)),
|
|
1205
|
+
)
|
|
1206
|
+
}
|
|
1207
|
+
|
|
904
1208
|
/**
|
|
905
1209
|
* Ink props for one status tone: the Codex status-line accent mapping over
|
|
906
1210
|
* the DeepSeek palette, all blue by design — the status bar speaks only in
|
|
907
1211
|
* degrees of blue (deep accent, primary figures, model identity, sky
|
|
908
1212
|
* paths and done states), with amber/red reserved for warnings and errors.
|
|
909
1213
|
*/
|
|
910
|
-
function statusToneProps(tone: StatusTone): {
|
|
1214
|
+
function statusToneProps(tone: StatusTone, flowMs?: number): {
|
|
911
1215
|
color: string | undefined
|
|
912
1216
|
bold: boolean | undefined
|
|
913
1217
|
dimColor: boolean | undefined
|
|
914
1218
|
} {
|
|
1219
|
+
if (isRainbow()) {
|
|
1220
|
+
// Carnival roll: every tone carries its rolled color (adjacent tones in
|
|
1221
|
+
// canonical on-screen order never match, the row boundary included);
|
|
1222
|
+
// the live dot rides the flow walk while busy. Bold emphasis carries
|
|
1223
|
+
// the semantic hierarchy so randomness never hides importance.
|
|
1224
|
+
const emphasized = tone === 'model' || tone === 'success' || tone === 'plan' || tone === 'warn' || tone === 'error'
|
|
1225
|
+
const color = tone === 'live' && flowMs !== undefined
|
|
1226
|
+
? flowColor(flowMs, themeFlow()?.anchors ?? FLOW_ANCHORS)
|
|
1227
|
+
: rainbowRoll().toneColors[tone]
|
|
1228
|
+
return { color: inkColor(color), bold: emphasized || undefined, dimColor: undefined }
|
|
1229
|
+
}
|
|
915
1230
|
switch (tone) {
|
|
916
1231
|
case 'model':
|
|
917
1232
|
// Same tone as the working-directory segment: the model name reads as
|
|
918
1233
|
// a path fact, not a brand accent.
|
|
919
1234
|
return { color: inkColor(getPalette().code), bold: true, dimColor: undefined }
|
|
920
1235
|
case 'live':
|
|
921
|
-
|
|
1236
|
+
// With a flow sample (flowing theme while busy), the live dot rides
|
|
1237
|
+
// the anchor walk; otherwise the palette's live accent.
|
|
1238
|
+
return { color: inkColor(flowMs === undefined ? getPalette().brandBright : flowColor(flowMs, themeFlow()?.anchors ?? FLOW_ANCHORS)), bold: undefined, dimColor: undefined }
|
|
922
1239
|
case 'path':
|
|
923
1240
|
return { color: inkColor(getPalette().code), bold: undefined, dimColor: undefined }
|
|
924
1241
|
case 'branch':
|
|
@@ -980,12 +1297,22 @@ function statusToneProps(tone: StatusTone): {
|
|
|
980
1297
|
* the prompt keeps is always hues[0]. */
|
|
981
1298
|
function deepseekWaveHues(tier: DeepseekWaveTier): readonly [RgbTriple, RgbTriple, RgbTriple] {
|
|
982
1299
|
const palette = getPalette()
|
|
1300
|
+
// Rainbow waves pick three SPECTRALLY SPREAD anchors from the rolled
|
|
1301
|
+
// pool — spectral neighbors would wash into one hue across the band.
|
|
1302
|
+
if (isRainbow()) {
|
|
1303
|
+
const anchors = rainbowRoll().flowAnchors
|
|
1304
|
+
const stride = Math.max(1, Math.floor(anchors.length / 3))
|
|
1305
|
+
return [anchors[0], anchors[stride], anchors[stride * 2]]
|
|
1306
|
+
}
|
|
1307
|
+
// Prismatic waves ride the flow anchors for both tiers — the model-switch
|
|
1308
|
+
// easter egg becomes a violet→fuchsia→cyan sweep.
|
|
1309
|
+
if (isPrismatic()) return [FLOW_ANCHORS[0], FLOW_ANCHORS[1], FLOW_ANCHORS[2]]
|
|
983
1310
|
return tier === 'flash'
|
|
984
1311
|
? [palette.brandBright, palette.brand, palette.brandMid]
|
|
985
1312
|
: [palette.brandBright, palette.code, palette.brandMid]
|
|
986
1313
|
}
|
|
987
1314
|
|
|
988
|
-
function StatusLine({ facts, stats, busy, columns, items, onRows }: {
|
|
1315
|
+
function StatusLine({ facts, stats, busy, columns, items, onRows, animated }: {
|
|
989
1316
|
facts: StatusFacts
|
|
990
1317
|
stats: Parameters<typeof layoutStatusBar>[1]
|
|
991
1318
|
busy: boolean
|
|
@@ -994,30 +1321,35 @@ function StatusLine({ facts, stats, busy, columns, items, onRows }: {
|
|
|
994
1321
|
/** Reports the footer's exact physical row count (1 or 2) so the IME
|
|
995
1322
|
* anchor ledger below the composer stays exact. */
|
|
996
1323
|
onRows?: (rows: 1 | 2) => void
|
|
1324
|
+
/** Whether timed animations run (the persisted preference). */
|
|
1325
|
+
animated: boolean
|
|
997
1326
|
}): ReactElement {
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
facts.
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1327
|
+
// Flowing-theme busy flow: the identity cluster's live dot cycles the
|
|
1328
|
+
// anchor walk while a turn runs; static themes never start the timer.
|
|
1329
|
+
const flow = themeFlow()
|
|
1330
|
+
const flowActive = animated && busy && flow !== undefined
|
|
1331
|
+
const flowTick = useFrames(BUSY_CHASE_TICK_MS, flowActive)
|
|
1332
|
+
const flowMs = flowActive ? flowTick * BUSY_CHASE_TICK_MS + (flow?.phaseMs ?? 0) : undefined
|
|
1333
|
+
const language = getLanguage()
|
|
1334
|
+
const layout = useMemo(() => {
|
|
1335
|
+
// The layout reads translations through t(); naming the current language
|
|
1336
|
+
// here makes that external store value an explicit cache invalidator.
|
|
1337
|
+
void language
|
|
1338
|
+
return layoutStatusBar(facts, stats, Math.max(8, columns - 2), {
|
|
1339
|
+
busy,
|
|
1340
|
+
items,
|
|
1341
|
+
// Match the composer content budget: border + horizontal padding are
|
|
1342
|
+
// already excluded, and layoutStatusBar shrinks this ceiling as needed.
|
|
1343
|
+
contextWidth: Math.max(5, columns - 6),
|
|
1344
|
+
})
|
|
1345
|
+
}, [
|
|
1346
|
+
facts,
|
|
1017
1347
|
stats,
|
|
1018
1348
|
busy,
|
|
1019
1349
|
columns,
|
|
1020
1350
|
items,
|
|
1351
|
+
// Labels come from t(); a language switch must rebuild the rows.
|
|
1352
|
+
language,
|
|
1021
1353
|
])
|
|
1022
1354
|
// The IME anchor below the composer counts every row between the caret and
|
|
1023
1355
|
// Ink's parked cursor, so the footer reports its exact row count one-way
|
|
@@ -1036,7 +1368,7 @@ function StatusLine({ facts, stats, busy, columns, items, onRows }: {
|
|
|
1036
1368
|
group.spans.forEach((span, spanIndex) => {
|
|
1037
1369
|
leftParts.push(createElement(
|
|
1038
1370
|
Text,
|
|
1039
|
-
{ key: key + 'g' + groupIndex + 's' + spanIndex, wrap: 'truncate-end', ...statusToneProps(span.tone) },
|
|
1371
|
+
{ key: key + 'g' + groupIndex + 's' + spanIndex, wrap: 'truncate-end', ...statusToneProps(span.tone, flowMs) },
|
|
1040
1372
|
span.text,
|
|
1041
1373
|
))
|
|
1042
1374
|
})
|
|
@@ -1048,12 +1380,12 @@ function StatusLine({ facts, stats, busy, columns, items, onRows }: {
|
|
|
1048
1380
|
}
|
|
1049
1381
|
rightParts.push(createElement(
|
|
1050
1382
|
Text,
|
|
1051
|
-
{ key: key + 'r' + index, wrap: 'truncate-end', ...statusToneProps(span.tone) },
|
|
1383
|
+
{ key: key + 'r' + index, wrap: 'truncate-end', ...statusToneProps(span.tone, flowMs) },
|
|
1052
1384
|
span.text,
|
|
1053
1385
|
))
|
|
1054
1386
|
})
|
|
1055
1387
|
if (row.hint) {
|
|
1056
|
-
rightParts.push(createElement(Text, { key: key + 'hint', color: inkColor(getPalette().dim) },
|
|
1388
|
+
rightParts.push(createElement(Text, { key: key + 'hint', color: inkColor(getPalette().dim) }, statusCycleHint()))
|
|
1057
1389
|
}
|
|
1058
1390
|
// Each row already fits the column budget; truncate-end stays as the
|
|
1059
1391
|
// terminal-measurement backstop so a drifting cell count clips instead
|
|
@@ -1131,9 +1463,9 @@ const APPROVAL_OPTIONS: readonly ApprovalOption[] = [
|
|
|
1131
1463
|
function ApprovalBar({ snapshot, locked, notify, interrupt, summarize }: {
|
|
1132
1464
|
snapshot: ApprovalSnapshot
|
|
1133
1465
|
locked: boolean
|
|
1134
|
-
notify(text: string, tone?: NoticeTone)
|
|
1466
|
+
notify: (text: string, tone?: NoticeTone) => void
|
|
1135
1467
|
/** Cancel the running turn (Ctrl+C), matching the composer's busy branch. */
|
|
1136
|
-
interrupt()
|
|
1468
|
+
interrupt: () => boolean
|
|
1137
1469
|
/** Render as the bounded one-line form even on tall terminals (another
|
|
1138
1470
|
* human-asked surface already owns the full panel budget). */
|
|
1139
1471
|
summarize?: boolean
|
|
@@ -1160,7 +1492,7 @@ function ApprovalBar({ snapshot, locked, notify, interrupt, summarize }: {
|
|
|
1160
1492
|
}
|
|
1161
1493
|
ask.answer('rejected')
|
|
1162
1494
|
if (option.key === 'reject-note') {
|
|
1163
|
-
notify('rejected
|
|
1495
|
+
notify(t('notice.rejected'), 'warning')
|
|
1164
1496
|
}
|
|
1165
1497
|
}
|
|
1166
1498
|
|
|
@@ -1184,35 +1516,35 @@ function ApprovalBar({ snapshot, locked, notify, interrupt, summarize }: {
|
|
|
1184
1516
|
return
|
|
1185
1517
|
}
|
|
1186
1518
|
if (key.return) {
|
|
1187
|
-
decide(APPROVAL_OPTIONS[cursor]
|
|
1519
|
+
decide(APPROVAL_OPTIONS[cursor])
|
|
1188
1520
|
return
|
|
1189
1521
|
}
|
|
1190
1522
|
if (key.escape) {
|
|
1191
|
-
decide(APPROVAL_OPTIONS[2]
|
|
1523
|
+
decide(APPROVAL_OPTIONS[2])
|
|
1192
1524
|
return
|
|
1193
1525
|
}
|
|
1194
1526
|
if (input === 'y' || input === 'Y') {
|
|
1195
|
-
decide(APPROVAL_OPTIONS[0]
|
|
1527
|
+
decide(APPROVAL_OPTIONS[0])
|
|
1196
1528
|
return
|
|
1197
1529
|
}
|
|
1198
1530
|
if (input === 'n' || input === 'N') {
|
|
1199
|
-
decide(APPROVAL_OPTIONS[1]
|
|
1531
|
+
decide(APPROVAL_OPTIONS[1])
|
|
1200
1532
|
return
|
|
1201
1533
|
}
|
|
1202
1534
|
if (input === 'd' || input === 'D') {
|
|
1203
|
-
decide(APPROVAL_OPTIONS[2]
|
|
1535
|
+
decide(APPROVAL_OPTIONS[2])
|
|
1204
1536
|
return
|
|
1205
1537
|
}
|
|
1206
1538
|
if (/^[1-9]$/u.test(input)) {
|
|
1207
1539
|
const index = Number(input) - 1
|
|
1208
|
-
if (index < APPROVAL_OPTIONS.length) decide(APPROVAL_OPTIONS[index]
|
|
1540
|
+
if (index < APPROVAL_OPTIONS.length) decide(APPROVAL_OPTIONS[index])
|
|
1209
1541
|
}
|
|
1210
1542
|
}, { isActive: active })
|
|
1211
1543
|
|
|
1212
1544
|
if (pending === undefined) return undefined
|
|
1213
1545
|
const queuedSuffix = snapshot.queued > 0 ? ` · +${snapshot.queued} queued` : ''
|
|
1214
1546
|
if (viewport.maxHeight === 0 || viewport.compact || summarize === true) {
|
|
1215
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(
|
|
1547
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('approval.compact', { queued: queuedSuffix }), viewport.contentColumns))
|
|
1216
1548
|
}
|
|
1217
1549
|
// Body budget: title + options + footer consume fixed rows; the command
|
|
1218
1550
|
// preview shrinks with an explicit overflow marker (Codex's "[… N lines]").
|
|
@@ -1231,7 +1563,7 @@ function ApprovalBar({ snapshot, locked, notify, interrupt, summarize }: {
|
|
|
1231
1563
|
createElement(PanelGap, { visible: viewport.gapRows > 0 && body.length > 0 }),
|
|
1232
1564
|
...visibleBody.map((line, index) => createElement(StyledRows, { key: `body-${index}`, lines: [line] })),
|
|
1233
1565
|
...(overflow > 0
|
|
1234
|
-
? [createElement(Text, { key: 'overflow', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(
|
|
1566
|
+
? [createElement(Text, { key: 'overflow', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(t('approval.overflow', { count: overflow }), viewport.contentColumns))]
|
|
1235
1567
|
: []),
|
|
1236
1568
|
...(body.length > 0 ? [createElement(PanelGap, { visible: viewport.gapRows > 0 })] : []),
|
|
1237
1569
|
...APPROVAL_OPTIONS.map((option, index) => {
|
|
@@ -1248,8 +1580,8 @@ function ApprovalBar({ snapshot, locked, notify, interrupt, summarize }: {
|
|
|
1248
1580
|
)
|
|
1249
1581
|
}),
|
|
1250
1582
|
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(snapshot.answered
|
|
1251
|
-
? 'submitted
|
|
1252
|
-
: '
|
|
1583
|
+
? t('approval.submitted')
|
|
1584
|
+
: t('approval.footer'), viewport.contentColumns)),
|
|
1253
1585
|
)
|
|
1254
1586
|
}
|
|
1255
1587
|
|
|
@@ -1332,7 +1664,7 @@ function QuestionBar({ store, snapshot, locked }: { store: QuestionStore; snapsh
|
|
|
1332
1664
|
}, [request])
|
|
1333
1665
|
|
|
1334
1666
|
const question = pending?.request.questions[index]
|
|
1335
|
-
const options = question?.options ?? []
|
|
1667
|
+
const options = useMemo(() => question?.options ?? [], [question])
|
|
1336
1668
|
const isPlan = question?.intent?.kind === 'plan-review'
|
|
1337
1669
|
const isMulti = question?.multiSelect === true
|
|
1338
1670
|
const currentDraft = drafts[index] ?? initialQuestionDraft(question)
|
|
@@ -1622,21 +1954,21 @@ function QuestionBar({ store, snapshot, locked }: { store: QuestionStore; snapsh
|
|
|
1622
1954
|
|
|
1623
1955
|
if (pending === undefined || question === undefined) return undefined
|
|
1624
1956
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
1625
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(isPlan ? 'plan
|
|
1957
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(isPlan ? t('question.compact.plan') : t('question.compact.normal'), viewport.contentColumns))
|
|
1626
1958
|
}
|
|
1627
1959
|
const footerBase = submitted
|
|
1628
|
-
? 'submitted
|
|
1960
|
+
? t('question.submitted')
|
|
1629
1961
|
: mode === 'custom'
|
|
1630
1962
|
? options.length === 0
|
|
1631
|
-
? '
|
|
1632
|
-
: '
|
|
1963
|
+
? t('question.customNoOptions')
|
|
1964
|
+
: t('question.customOptions')
|
|
1633
1965
|
: options.length === 0
|
|
1634
|
-
? '
|
|
1966
|
+
? t('question.customNoOptions')
|
|
1635
1967
|
: isMulti
|
|
1636
|
-
? '
|
|
1637
|
-
: '
|
|
1968
|
+
? t('question.multiOptions')
|
|
1969
|
+
: t('question.singleOptions')
|
|
1638
1970
|
const footer = pending.request.questions.length > 1 && !submitted
|
|
1639
|
-
? `${footerBase}
|
|
1971
|
+
? `${footerBase}${t('question.switch')}`
|
|
1640
1972
|
: footerBase
|
|
1641
1973
|
return createElement(
|
|
1642
1974
|
Box,
|
|
@@ -1644,12 +1976,12 @@ function QuestionBar({ store, snapshot, locked }: { store: QuestionStore; snapsh
|
|
|
1644
1976
|
createElement(
|
|
1645
1977
|
Text,
|
|
1646
1978
|
{ color: inkColor(isPlan ? getPalette().brand : getPalette().brandDeep), bold: true, wrap: 'truncate-end' },
|
|
1647
|
-
truncateColumns(`${isPlan ? '
|
|
1979
|
+
truncateColumns(`${isPlan ? t('question.title.plan') : t('question.title.normal')} ${index + 1}/${pending.request.questions.length} · lines ${rendered.lines.length === 0 ? 0 : visibleScroll + 1}-${Math.min(rendered.lines.length, visibleScroll + viewport.bodyRows)}/${rendered.lines.length}`, viewport.contentColumns),
|
|
1648
1980
|
),
|
|
1649
1981
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1650
1982
|
createElement(StyledRows, { lines: rendered.lines.slice(visibleScroll, visibleScroll + viewport.bodyRows) }),
|
|
1651
1983
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1652
|
-
createElement(Text, {
|
|
1984
|
+
createElement(Text, { wrap: 'truncate-end' }, dim(truncateColumns(footer, viewport.contentColumns))),
|
|
1653
1985
|
)
|
|
1654
1986
|
}
|
|
1655
1987
|
|
|
@@ -1659,16 +1991,16 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1659
1991
|
error: string | undefined
|
|
1660
1992
|
/** `provider/model` label of the applied model: the cursor lands on it once. */
|
|
1661
1993
|
current?: string
|
|
1662
|
-
onSelect(row: ModelRow)
|
|
1663
|
-
onProviders
|
|
1664
|
-
onRetry()
|
|
1665
|
-
onClose()
|
|
1994
|
+
onSelect: (row: ModelRow) => void
|
|
1995
|
+
onProviders?: () => void
|
|
1996
|
+
onRetry: () => void
|
|
1997
|
+
onClose: () => void
|
|
1666
1998
|
}): ReactElement {
|
|
1667
1999
|
const [query, setQuery] = useState('')
|
|
1668
2000
|
const [cursor, setCursor] = useState(0)
|
|
1669
2001
|
const stdout = useStdout().stdout
|
|
1670
2002
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
1671
|
-
const rows = directory?.rows ?? []
|
|
2003
|
+
const rows = useMemo(() => directory?.rows ?? [], [directory])
|
|
1672
2004
|
// Direct-typing filter over provider and model names (the /mode contract):
|
|
1673
2005
|
// printable keys edit the query, so a long directory is searchable without
|
|
1674
2006
|
// a separate search mode. With a query active, q/r/g/G stop acting as
|
|
@@ -1679,6 +2011,13 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1679
2011
|
return rows.filter(row => `${row.provider} ${row.providerName ?? ''} ${row.model} ${row.modelName}`.toLowerCase().includes(needle))
|
|
1680
2012
|
}, [rows, query])
|
|
1681
2013
|
const positioned = useRef(false)
|
|
2014
|
+
// Latest-value ref: Ink re-subscribes useInput when its effect flushes,
|
|
2015
|
+
// which can lag a committed render (a directory that just landed paints
|
|
2016
|
+
// before the subscription swaps). A keystroke in that window would meet a
|
|
2017
|
+
// stale closure — Enter died as an empty-filter no-op right after "2 of 4
|
|
2018
|
+
// match" painted. The handler reads render-fresh values through the ref.
|
|
2019
|
+
const liveRef = useRef({ filtered, query, cursor })
|
|
2020
|
+
liveRef.current = { filtered, query, cursor }
|
|
1682
2021
|
|
|
1683
2022
|
useEffect(() => {
|
|
1684
2023
|
// Open ON the applied model (Codex resumes the previous pick): the first
|
|
@@ -1697,7 +2036,7 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1697
2036
|
// Position within the ACTIVE filter: the full-row index means nothing
|
|
1698
2037
|
// when the query already narrowed the list while the directory loaded
|
|
1699
2038
|
// (a late resolve must not place the cursor outside `filtered`).
|
|
1700
|
-
const filteredIndex = filtered.indexOf(rows[index]
|
|
2039
|
+
const filteredIndex = filtered.indexOf(rows[index])
|
|
1701
2040
|
setCursor(filteredIndex >= 0 ? filteredIndex : 0)
|
|
1702
2041
|
} else if (cursor >= filtered.length) {
|
|
1703
2042
|
setCursor(Math.max(0, filtered.length - 1))
|
|
@@ -1705,11 +2044,12 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1705
2044
|
}, [rows, filtered, cursor, current])
|
|
1706
2045
|
|
|
1707
2046
|
useInput((input, key) => {
|
|
1708
|
-
|
|
2047
|
+
const { filtered: list, query: text, cursor: at } = liveRef.current
|
|
2048
|
+
if (key.escape || (input === 'q' && text === '')) {
|
|
1709
2049
|
onClose()
|
|
1710
2050
|
return
|
|
1711
2051
|
}
|
|
1712
|
-
if (input === 'r' &&
|
|
2052
|
+
if (input === 'r' && text === '') {
|
|
1713
2053
|
onRetry()
|
|
1714
2054
|
return
|
|
1715
2055
|
}
|
|
@@ -1722,19 +2062,19 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1722
2062
|
onClose()
|
|
1723
2063
|
return
|
|
1724
2064
|
}
|
|
1725
|
-
const next = editQuery(
|
|
2065
|
+
const next = editQuery(text, input, key)
|
|
1726
2066
|
if (next !== undefined) {
|
|
1727
2067
|
setQuery(next)
|
|
1728
2068
|
setCursor(0)
|
|
1729
2069
|
return
|
|
1730
2070
|
}
|
|
1731
|
-
if (
|
|
2071
|
+
if (list.length === 0) return
|
|
1732
2072
|
if (key.upArrow) {
|
|
1733
|
-
setCursor(
|
|
2073
|
+
setCursor(at > 0 ? at - 1 : list.length - 1)
|
|
1734
2074
|
return
|
|
1735
2075
|
}
|
|
1736
2076
|
if (key.downArrow) {
|
|
1737
|
-
setCursor(
|
|
2077
|
+
setCursor(at < list.length - 1 ? at + 1 : 0)
|
|
1738
2078
|
return
|
|
1739
2079
|
}
|
|
1740
2080
|
if (key.pageUp) {
|
|
@@ -1742,11 +2082,11 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1742
2082
|
return
|
|
1743
2083
|
}
|
|
1744
2084
|
if (key.pageDown) {
|
|
1745
|
-
setCursor(current => Math.min(
|
|
2085
|
+
setCursor(current => Math.min(list.length - 1, current + Math.max(1, viewport.bodyRows - 1)))
|
|
1746
2086
|
return
|
|
1747
2087
|
}
|
|
1748
|
-
if (key.return &&
|
|
1749
|
-
onSelect(
|
|
2088
|
+
if (key.return && list[at] !== undefined) {
|
|
2089
|
+
onSelect(list[at])
|
|
1750
2090
|
}
|
|
1751
2091
|
})
|
|
1752
2092
|
|
|
@@ -1754,19 +2094,19 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1754
2094
|
const providers = onProviders === undefined ? '' : ' · tab providers'
|
|
1755
2095
|
const state = filtered.length === 0
|
|
1756
2096
|
? directory === undefined && error === undefined
|
|
1757
|
-
? 'loading
|
|
2097
|
+
? t('panel.model.loading')
|
|
1758
2098
|
: error !== undefined
|
|
1759
|
-
? 'error'
|
|
1760
|
-
: query === '' ? '
|
|
2099
|
+
? t('panel.model.error')
|
|
2100
|
+
: query === '' ? t('panel.model.noModels') : t('panel.model.compactNoMatch', { query: singleLineText(query) })
|
|
1761
2101
|
: `❯ ${filtered[cursor]?.modelName ?? filtered[cursor]?.model ?? ''}`
|
|
1762
2102
|
const tail = query === ''
|
|
1763
|
-
? '
|
|
1764
|
-
: '
|
|
1765
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(
|
|
2103
|
+
? t('panel.model.footer.filter')
|
|
2104
|
+
: t('panel.model.footer.filtered')
|
|
2105
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.model.compact', { state, providers, tail }), viewport.contentColumns))
|
|
1766
2106
|
}
|
|
1767
2107
|
|
|
1768
2108
|
const stateRows: ReactElement[] = directory === undefined && error === undefined
|
|
1769
|
-
? [createElement(Text, { key: 'loading', dimColor: true, wrap: 'truncate-end' }, '
|
|
2109
|
+
? [createElement(Text, { key: 'loading', dimColor: true, wrap: 'truncate-end' }, ` ${t('panel.model.loading')}`)]
|
|
1770
2110
|
: error !== undefined
|
|
1771
2111
|
? [createElement(
|
|
1772
2112
|
Text,
|
|
@@ -1779,12 +2119,12 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1779
2119
|
: [createElement(
|
|
1780
2120
|
Text,
|
|
1781
2121
|
{ key: 'failures', color: inkColor(getPalette().warn), wrap: 'truncate-end' },
|
|
1782
|
-
truncateColumns(`
|
|
2122
|
+
truncateColumns(` ${t('panel.provider.failure', { providers: directory?.failures.join(', ') ?? '' })}`, viewport.contentColumns),
|
|
1783
2123
|
)]),
|
|
1784
2124
|
...(rows.length === 0
|
|
1785
|
-
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' },
|
|
2125
|
+
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' }, ` ${t('panel.model.noModels')}`)]
|
|
1786
2126
|
: filtered.length === 0
|
|
1787
|
-
? [createElement(Text, { key: 'no-match', dimColor: true, wrap: 'truncate-end' }, truncateColumns(`
|
|
2127
|
+
? [createElement(Text, { key: 'no-match', dimColor: true, wrap: 'truncate-end' }, truncateColumns(` ${t('panel.model.noMatch', { query: singleLineText(query) })}`, viewport.contentColumns))]
|
|
1788
2128
|
: []),
|
|
1789
2129
|
]
|
|
1790
2130
|
// Measurement and rendering share the same physical-row budget: state
|
|
@@ -1794,12 +2134,13 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1794
2134
|
const rowBudget = Math.max(0, viewport.bodyRows - visibleStateRows.length)
|
|
1795
2135
|
const first = selectionWindow(cursor, filtered.length, rowBudget)
|
|
1796
2136
|
const visible = rowBudget === 0 ? [] : filtered.slice(first, first + rowBudget)
|
|
2137
|
+
const accent = panelAccent('model', getPalette().brand)
|
|
1797
2138
|
return createElement(
|
|
1798
2139
|
Box,
|
|
1799
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
1800
|
-
createElement(Text, { color: inkColor(
|
|
1801
|
-
?
|
|
1802
|
-
:
|
|
2140
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
2141
|
+
createElement(Text, { color: inkColor(accent.title), bold: true, wrap: 'truncate-end' }, truncateColumns(query === ''
|
|
2142
|
+
? rows.length === 0 ? t('panel.model.title') : t('panel.model.titleCount', { index: cursor + 1, total: rows.length })
|
|
2143
|
+
: t('panel.model.titleMatches', { filtered: filtered.length, total: rows.length, query: singleLineText(query) }), viewport.contentColumns)),
|
|
1803
2144
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1804
2145
|
...visibleStateRows,
|
|
1805
2146
|
...visible.map((row) => {
|
|
@@ -1817,23 +2158,23 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1817
2158
|
)
|
|
1818
2159
|
}),
|
|
1819
2160
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1820
|
-
createElement(Text, {
|
|
1821
|
-
?
|
|
1822
|
-
:
|
|
2161
|
+
createElement(Text, { wrap: 'truncate-end' }, dim(truncateColumns(query === ''
|
|
2162
|
+
? t('panel.model.footer.select', { providers: onProviders === undefined ? '' : ` · ${t('panel.model.providers')}` })
|
|
2163
|
+
: t('panel.model.footer.filteredSelect'), viewport.contentColumns))),
|
|
1823
2164
|
)
|
|
1824
2165
|
}
|
|
1825
2166
|
|
|
1826
2167
|
/** Compact provider-state copy; only value-free credential facts cross this boundary. */
|
|
1827
2168
|
function providerStateLabel(row: ProviderTargetView): string {
|
|
1828
|
-
const route = row.active ? 'active' : 'dormant'
|
|
2169
|
+
const route = row.active ? t('panel.provider.active') : t('panel.provider.dormant')
|
|
1829
2170
|
const credential = row.credential
|
|
1830
|
-
if (credential?.kind === 'error') return
|
|
2171
|
+
if (credential?.kind === 'error') return t('panel.provider.state', { route, value: t('panel.provider.keyStatusUnavailable') })
|
|
1831
2172
|
if (credential?.kind === 'facts') {
|
|
1832
|
-
if (!credential.configured) return
|
|
1833
|
-
const source = credential.source === undefined ? 'configured' : singleLineText(credential.source)
|
|
1834
|
-
return
|
|
2173
|
+
if (!credential.configured) return t('panel.provider.state', { route, value: t('panel.provider.noKey') })
|
|
2174
|
+
const source = credential.source === undefined ? t('panel.provider.configured') : singleLineText(credential.source)
|
|
2175
|
+
return t('panel.provider.state', { route, value: `${t('panel.provider.key', { value: source })}${credential.writable ? '' : ` · ${t('panel.provider.readOnly')}`}` })
|
|
1835
2176
|
}
|
|
1836
|
-
return
|
|
2177
|
+
return t('panel.provider.state', { route, value: row.configured ? t('panel.provider.authConfigured') : t('panel.provider.noLogin') })
|
|
1837
2178
|
}
|
|
1838
2179
|
|
|
1839
2180
|
/** The provider-management stage reached from /model with `a`. */
|
|
@@ -1842,15 +2183,15 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1842
2183
|
error: string | undefined
|
|
1843
2184
|
authorizations: ProviderAuthorizationDirectory | undefined
|
|
1844
2185
|
authorizationError: string | undefined
|
|
1845
|
-
onConfigure(target: ProviderTargetView)
|
|
1846
|
-
onUnset(target: ProviderTargetView)
|
|
1847
|
-
onRemove(target: ProviderTargetView)
|
|
1848
|
-
onLogin(target: ProviderTargetView, authorization: ProviderAuthorizationRow)
|
|
1849
|
-
onLogout(target: ProviderTargetView, authorization: ProviderAuthorizationRow)
|
|
1850
|
-
onRetry()
|
|
1851
|
-
onBack()
|
|
2186
|
+
onConfigure: (target: ProviderTargetView) => void
|
|
2187
|
+
onUnset: (target: ProviderTargetView) => void
|
|
2188
|
+
onRemove: (target: ProviderTargetView) => void
|
|
2189
|
+
onLogin: (target: ProviderTargetView, authorization: ProviderAuthorizationRow) => void
|
|
2190
|
+
onLogout: (target: ProviderTargetView, authorization: ProviderAuthorizationRow) => void
|
|
2191
|
+
onRetry: () => void
|
|
2192
|
+
onBack: () => void
|
|
1852
2193
|
/** Leave the whole /model flow (Ctrl+C), not just this stage. */
|
|
1853
|
-
onExit()
|
|
2194
|
+
onExit: () => void
|
|
1854
2195
|
}): ReactElement {
|
|
1855
2196
|
const stdout = useStdout().stdout
|
|
1856
2197
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
@@ -1912,9 +2253,9 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1912
2253
|
if (input === 'd') {
|
|
1913
2254
|
const facts = target.credential
|
|
1914
2255
|
if (facts?.kind !== 'facts' || !facts.configured) {
|
|
1915
|
-
setActionError('
|
|
2256
|
+
setActionError(t('panel.provider.noConfiguredKey'))
|
|
1916
2257
|
} else if (!facts.writable) {
|
|
1917
|
-
setActionError('
|
|
2258
|
+
setActionError(t('panel.provider.readOnlyKey'))
|
|
1918
2259
|
} else {
|
|
1919
2260
|
onUnset(target)
|
|
1920
2261
|
}
|
|
@@ -1922,7 +2263,7 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1922
2263
|
}
|
|
1923
2264
|
if (input === 'x') {
|
|
1924
2265
|
if (!target.removable) {
|
|
1925
|
-
setActionError('
|
|
2266
|
+
setActionError(t('panel.provider.notRemovable'))
|
|
1926
2267
|
} else {
|
|
1927
2268
|
onRemove(target)
|
|
1928
2269
|
}
|
|
@@ -1930,14 +2271,14 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1930
2271
|
}
|
|
1931
2272
|
const authorization = authorizationForProvider(authorizations, target.provider)
|
|
1932
2273
|
if (input === 'l' || input === 'L') {
|
|
1933
|
-
if (authorization === undefined) setActionError('
|
|
1934
|
-
else if (authorization.inFlight) setActionError('
|
|
2274
|
+
if (authorization === undefined) setActionError(t('panel.provider.noLoginFlow'))
|
|
2275
|
+
else if (authorization.inFlight) setActionError(t('panel.provider.loginRunning'))
|
|
1935
2276
|
else onLogin(target, authorization)
|
|
1936
2277
|
return
|
|
1937
2278
|
}
|
|
1938
2279
|
if (input === 'o' || input === 'O') {
|
|
1939
|
-
if (authorization === undefined || !authorization.record.configured) setActionError('
|
|
1940
|
-
else if (!authorization.record.writable) setActionError('
|
|
2280
|
+
if (authorization === undefined || !authorization.record.configured) setActionError(t('panel.provider.noLoginRecord'))
|
|
2281
|
+
else if (!authorization.record.writable) setActionError(t('panel.provider.readOnlyLogin'))
|
|
1941
2282
|
else onLogout(target, authorization)
|
|
1942
2283
|
return
|
|
1943
2284
|
}
|
|
@@ -1946,7 +2287,7 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1946
2287
|
// the configuration surface behind an undiscoverable chord.
|
|
1947
2288
|
if (key.return) {
|
|
1948
2289
|
if (target.settingsNs.length === 0) {
|
|
1949
|
-
setActionError('
|
|
2290
|
+
setActionError(t('panel.provider.notManaged'))
|
|
1950
2291
|
} else {
|
|
1951
2292
|
onConfigure(target)
|
|
1952
2293
|
}
|
|
@@ -1954,10 +2295,10 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1954
2295
|
}, true)
|
|
1955
2296
|
|
|
1956
2297
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
1957
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('
|
|
2298
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.providers.compact'), viewport.contentColumns))
|
|
1958
2299
|
}
|
|
1959
2300
|
const stateRows: ReactElement[] = directory === undefined && error === undefined
|
|
1960
|
-
? [createElement(Text, { key: 'loading', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, '
|
|
2301
|
+
? [createElement(Text, { key: 'loading', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, ` ${t('panel.provider.loading')}`)]
|
|
1961
2302
|
: error !== undefined
|
|
1962
2303
|
? [createElement(Text, { key: 'error', color: inkColor(getPalette().error), wrap: 'truncate-end' }, truncateColumns(` ${singleLineText(error)}`, viewport.contentColumns))]
|
|
1963
2304
|
: [
|
|
@@ -1971,14 +2312,14 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1971
2312
|
)),
|
|
1972
2313
|
...(authorizationError === undefined
|
|
1973
2314
|
? []
|
|
1974
|
-
: [createElement(Text, { key: 'authorization-error', color: inkColor(getPalette().warn), wrap: 'truncate-end' }, truncateColumns(`
|
|
2315
|
+
: [createElement(Text, { key: 'authorization-error', color: inkColor(getPalette().warn), wrap: 'truncate-end' }, truncateColumns(` ${t('panel.provider.loginStatusUnavailable', { message: singleLineText(authorizationError) })}`, viewport.contentColumns))]),
|
|
1975
2316
|
...(authorizations?.failures ?? []).map((failure, index) => createElement(
|
|
1976
2317
|
Text,
|
|
1977
2318
|
{ key: `authorization-failure-${index}`, color: inkColor(getPalette().warn), wrap: 'truncate-end' },
|
|
1978
2319
|
truncateColumns(` ${singleLineText(failure)}`, viewport.contentColumns),
|
|
1979
2320
|
)),
|
|
1980
2321
|
...(rows.length === 0
|
|
1981
|
-
? [createElement(Text, { key: 'empty', color: inkColor(getPalette().dim), wrap: 'truncate-end' },
|
|
2322
|
+
? [createElement(Text, { key: 'empty', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, ` ${t('panel.provider.empty')}`)]
|
|
1982
2323
|
: []),
|
|
1983
2324
|
]
|
|
1984
2325
|
const visibleStateRows = stateRows.slice(0, viewport.bodyRows)
|
|
@@ -1989,7 +2330,7 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1989
2330
|
const itemRows: ReactElement[] = []
|
|
1990
2331
|
for (let display = first; display < first + rowBudget && display < displayLength; display += 1) {
|
|
1991
2332
|
if (hasSeparator && display === configuredCount) {
|
|
1992
|
-
itemRows.push(createElement(Text, { key: 'separator', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(
|
|
2333
|
+
itemRows.push(createElement(Text, { key: 'separator', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(` ${t('panel.provider.notConfiguredDivider')}`, viewport.contentColumns)))
|
|
1993
2334
|
continue
|
|
1994
2335
|
}
|
|
1995
2336
|
const index = hasSeparator && display > configuredCount ? display - 1 : display
|
|
@@ -2013,15 +2354,16 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
2013
2354
|
truncateColumns((index === cursor ? '❯ ' : ' ') + displayText(label), viewport.contentColumns),
|
|
2014
2355
|
))
|
|
2015
2356
|
}
|
|
2357
|
+
const accent = panelAccent('model-providers', getPalette().brand)
|
|
2016
2358
|
return createElement(
|
|
2017
2359
|
Box,
|
|
2018
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
2019
|
-
createElement(Text, { color: inkColor(
|
|
2360
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
2361
|
+
createElement(Text, { color: inkColor(accent.title), bold: true, wrap: 'truncate-end' }, truncateColumns(rows.length === 0 ? t('panel.provider.title') : t('panel.provider.titleCount', { index: cursor + 1, total: rows.length }), viewport.contentColumns)),
|
|
2020
2362
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2021
2363
|
...visibleStateRows,
|
|
2022
2364
|
...itemRows,
|
|
2023
2365
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2024
|
-
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns('
|
|
2366
|
+
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(t('panel.providers.footer'), viewport.contentColumns)),
|
|
2025
2367
|
)
|
|
2026
2368
|
}
|
|
2027
2369
|
|
|
@@ -2049,14 +2391,14 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2049
2391
|
target: ProviderTargetView
|
|
2050
2392
|
/** Models with declared efforts (settings first, catalog-advertised after) a model row can copy from. */
|
|
2051
2393
|
effortDonors: readonly EffortDonor[]
|
|
2052
|
-
save(target: ProviderTargetView, configuration: ProviderConfiguration)
|
|
2394
|
+
save: (target: ProviderTargetView, configuration: ProviderConfiguration) => Promise<void>
|
|
2053
2395
|
saveCredential: ((target: ProviderTargetView, key: string) => Promise<void>) | undefined
|
|
2054
|
-
discover(target: ProviderTargetView, request: { readonly apiKey?: string; readonly baseURL?: string }, signal?: AbortSignal)
|
|
2396
|
+
discover: (target: ProviderTargetView, request: { readonly apiKey?: string; readonly baseURL?: string }, signal?: AbortSignal) => Promise<readonly DiscoveredModelView[]>
|
|
2055
2397
|
/** Report a successful save so the surface can notice the key rotation. */
|
|
2056
|
-
done(result: { readonly key: boolean })
|
|
2057
|
-
back()
|
|
2398
|
+
done: (result: { readonly key: boolean }) => void
|
|
2399
|
+
back: () => void
|
|
2058
2400
|
/** Leave the whole /model flow (Ctrl+C), not just this page. */
|
|
2059
|
-
onExit()
|
|
2401
|
+
onExit: () => void
|
|
2060
2402
|
}): ReactElement {
|
|
2061
2403
|
const stdout = useStdout().stdout
|
|
2062
2404
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
@@ -2110,7 +2452,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2110
2452
|
|
|
2111
2453
|
/** Compact declaration summary for the row label: count, off, or inherit. */
|
|
2112
2454
|
const effortsSummary = (model: ProviderModelSettings): string => {
|
|
2113
|
-
const raw = (model.extras
|
|
2455
|
+
const raw = (model.extras)?.reasoningEfforts
|
|
2114
2456
|
if (raw === false) return 'off'
|
|
2115
2457
|
if (isDeclaredReasoningEfforts(raw)) return String(Object.keys(raw).length)
|
|
2116
2458
|
return '~'
|
|
@@ -2263,7 +2605,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2263
2605
|
}
|
|
2264
2606
|
if (input === 'e' && selected !== undefined) {
|
|
2265
2607
|
setError(undefined)
|
|
2266
|
-
setEffDraft(serializeReasoningEfforts((selected.extras
|
|
2608
|
+
setEffDraft(serializeReasoningEfforts((selected.extras)?.reasoningEfforts))
|
|
2267
2609
|
setEffEditing(true)
|
|
2268
2610
|
return
|
|
2269
2611
|
}
|
|
@@ -2291,19 +2633,20 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2291
2633
|
// Never hide a live input surface: one visible row keeps the escape
|
|
2292
2634
|
// route honest on extremely short terminals (the three fixed rows - key,
|
|
2293
2635
|
// url, add-by-id - cannot fit below a three-row body).
|
|
2294
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('
|
|
2636
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.setup.compact'), viewport.contentColumns))
|
|
2295
2637
|
}
|
|
2296
2638
|
if (page === 'donor') {
|
|
2297
2639
|
const stateRow = donorRows.length === 0
|
|
2298
|
-
? createElement(Text, { key: 'empty', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(
|
|
2299
|
-
: createElement(Text, { key: 'hint', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns('
|
|
2640
|
+
? createElement(Text, { key: 'empty', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(` ${t('panel.setup.noDonors')}`, viewport.contentColumns))
|
|
2641
|
+
: createElement(Text, { key: 'hint', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(` ${t('panel.setup.copyInto', { id: displayText(selected?.id ?? '') })}`, viewport.contentColumns))
|
|
2300
2642
|
const donorBudget = Math.max(0, viewport.bodyRows - 2)
|
|
2301
2643
|
const donorFirst = selectionWindow(donorIndex, donorRows.length, donorBudget)
|
|
2302
2644
|
const donorVisible = donorRows.slice(donorFirst, donorFirst + donorBudget)
|
|
2645
|
+
const accent = panelAccent('model-efforts', getPalette().brand)
|
|
2303
2646
|
return createElement(
|
|
2304
2647
|
Box,
|
|
2305
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
2306
|
-
createElement(Text, { color: inkColor(
|
|
2648
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
2649
|
+
createElement(Text, { color: inkColor(accent.title), bold: true, wrap: 'truncate-end' }, truncateColumns(t('panel.setup.copyTitle'), viewport.contentColumns)),
|
|
2307
2650
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2308
2651
|
stateRow,
|
|
2309
2652
|
...donorVisible.map((donor, index) => {
|
|
@@ -2312,7 +2655,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2312
2655
|
return createElement(Text, { key: donor.provider + '/' + donor.id, color: active ? inkColor(getPalette().brandBright) : inkColor(getPalette().text), wrap: 'truncate-end' }, truncateColumns(label, viewport.contentColumns))
|
|
2313
2656
|
}),
|
|
2314
2657
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2315
|
-
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns('
|
|
2658
|
+
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(t('panel.setup.copyFooter'), viewport.contentColumns)),
|
|
2316
2659
|
)
|
|
2317
2660
|
}
|
|
2318
2661
|
if (page === 'discover') {
|
|
@@ -2354,7 +2697,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2354
2697
|
modelRows.push(createElement(Text, { key: 'add', color: cursor === index ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(' ' + (cursor === index ? '>' : ' ') + ' + add by id' + (addDraft === '' ? '' : ' ' + addDraft + '▏'), viewport.contentColumns)))
|
|
2355
2698
|
continue
|
|
2356
2699
|
}
|
|
2357
|
-
const model = models[index]
|
|
2700
|
+
const model = models[index]
|
|
2358
2701
|
const active = index === cursor
|
|
2359
2702
|
const context = model.contextWindow === undefined ? '-' : String(model.contextWindow)
|
|
2360
2703
|
const output = model.maxTokens === undefined ? '-' : String(model.maxTokens)
|
|
@@ -2364,10 +2707,11 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2364
2707
|
: ' in:' + (active && field === 'ctx' ? '[' + context + ']' : context) + ' out:' + (active && field === 'out' ? '[' + output + ']' : output) + ' eff:' + effortsSummary(model)
|
|
2365
2708
|
modelRows.push(createElement(Text, { key: model.id, color: active ? inkColor(getPalette().brandBright) : inkColor(getPalette().success), wrap: 'truncate-end' }, truncateColumns(' ' + (active ? '>' : ' ') + ' [x] ' + displayText(model.id) + tail, viewport.contentColumns)))
|
|
2366
2709
|
}
|
|
2710
|
+
const accent = panelAccent('model-configure', getPalette().brand)
|
|
2367
2711
|
return createElement(
|
|
2368
2712
|
Box,
|
|
2369
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
2370
|
-
createElement(Text, { color: inkColor(
|
|
2713
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
2714
|
+
createElement(Text, { color: inkColor(accent.title), bold: true, wrap: 'truncate-end' }, truncateColumns(t('panel.setup.title', { provider: target.displayName }), viewport.contentColumns)),
|
|
2371
2715
|
// The adapter's configuration diagnostic heads the editor: the provider
|
|
2372
2716
|
// is here precisely because it stayed listed for repair.
|
|
2373
2717
|
...(target.diagnostic === undefined ? [] : [createElement(
|
|
@@ -2381,7 +2725,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2381
2725
|
...stateRows,
|
|
2382
2726
|
...modelRows,
|
|
2383
2727
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2384
|
-
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns('
|
|
2728
|
+
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(t('panel.setup.footer'), viewport.contentColumns)),
|
|
2385
2729
|
)
|
|
2386
2730
|
}
|
|
2387
2731
|
|
|
@@ -2397,11 +2741,11 @@ function ProviderDiscoveryPanel({ target, baseURL, apiKey, configured, discover,
|
|
|
2397
2741
|
baseURL: string
|
|
2398
2742
|
apiKey: string
|
|
2399
2743
|
configured: readonly string[]
|
|
2400
|
-
discover(target: ProviderTargetView, request: { readonly apiKey?: string; readonly baseURL?: string }, signal?: AbortSignal)
|
|
2401
|
-
onAdopt(models: readonly DiscoveredModelView[])
|
|
2402
|
-
back()
|
|
2744
|
+
discover: (target: ProviderTargetView, request: { readonly apiKey?: string; readonly baseURL?: string }, signal?: AbortSignal) => Promise<readonly DiscoveredModelView[]>
|
|
2745
|
+
onAdopt: (models: readonly DiscoveredModelView[]) => void
|
|
2746
|
+
back: () => void
|
|
2403
2747
|
/** Leave the whole /model flow (Ctrl+C). */
|
|
2404
|
-
onExit()
|
|
2748
|
+
onExit: () => void
|
|
2405
2749
|
}): ReactElement {
|
|
2406
2750
|
const stdout = useStdout().stdout
|
|
2407
2751
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
@@ -2462,24 +2806,25 @@ function ProviderDiscoveryPanel({ target, baseURL, apiKey, configured, discover,
|
|
|
2462
2806
|
}
|
|
2463
2807
|
}, true)
|
|
2464
2808
|
if (viewport.maxHeight === 0) {
|
|
2465
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('
|
|
2809
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.discovery.compact'), viewport.contentColumns))
|
|
2466
2810
|
}
|
|
2467
2811
|
const stateRows = loading
|
|
2468
|
-
? [createElement(Text, { key: 'loading', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(
|
|
2812
|
+
? [createElement(Text, { key: 'loading', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(` ${t('panel.discovery.loading')}`, viewport.contentColumns))]
|
|
2469
2813
|
: error !== undefined
|
|
2470
2814
|
? [createElement(Text, { key: 'error', color: inkColor(getPalette().error), wrap: 'truncate-end' }, truncateColumns(' ' + error, viewport.contentColumns))]
|
|
2471
2815
|
: rows.length === 0
|
|
2472
|
-
? [createElement(Text, { key: 'empty', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(
|
|
2473
|
-
: [createElement(Text, { key: 'summary', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(
|
|
2816
|
+
? [createElement(Text, { key: 'empty', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(` ${t('panel.discovery.empty')}`, viewport.contentColumns))]
|
|
2817
|
+
: [createElement(Text, { key: 'summary', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(` ${t('panel.discovery.summary', { advertised: rows.length, newCount: rows.filter(model => !known.has(model.id)).length, checked: checked.size })}`, viewport.contentColumns))]
|
|
2474
2818
|
// One spare row keeps the panel strictly below maxHeight even with the
|
|
2475
2819
|
// gap collapsed (the at-equality regime makes Ink rewrite Static).
|
|
2476
2820
|
const rowBudget = Math.max(0, viewport.bodyRows - stateRows.length - 1)
|
|
2477
2821
|
const first = selectionWindow(cursor, rows.length, rowBudget)
|
|
2478
2822
|
const visible = rows.slice(first, first + rowBudget)
|
|
2823
|
+
const accent = panelAccent('model-discover', getPalette().brand)
|
|
2479
2824
|
return createElement(
|
|
2480
2825
|
Box,
|
|
2481
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
2482
|
-
createElement(Text, { color: inkColor(
|
|
2826
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
2827
|
+
createElement(Text, { color: inkColor(accent.title), bold: true, wrap: 'truncate-end' }, truncateColumns(t('panel.discovery.title', { provider: target.displayName }), viewport.contentColumns)),
|
|
2483
2828
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2484
2829
|
...stateRows,
|
|
2485
2830
|
...visible.map((model, index) => {
|
|
@@ -2491,7 +2836,7 @@ function ProviderDiscoveryPanel({ target, baseURL, apiKey, configured, discover,
|
|
|
2491
2836
|
return createElement(Text, { key: model.id, color: added ? inkColor(getPalette().dim) : active ? inkColor(getPalette().brandBright) : inkColor(getPalette().text), wrap: 'truncate-end' }, truncateColumns(label, viewport.contentColumns))
|
|
2492
2837
|
}),
|
|
2493
2838
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2494
|
-
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns('
|
|
2839
|
+
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(t('panel.discovery.footer'), viewport.contentColumns)),
|
|
2495
2840
|
)
|
|
2496
2841
|
}
|
|
2497
2842
|
|
|
@@ -2499,9 +2844,9 @@ function ProviderDiscoveryPanel({ target, baseURL, apiKey, configured, discover,
|
|
|
2499
2844
|
function ProviderConfirmPanel({ target, kind, confirm, done, back }: {
|
|
2500
2845
|
target: ProviderTargetView
|
|
2501
2846
|
kind: 'credential' | 'provider'
|
|
2502
|
-
confirm(target: ProviderTargetView)
|
|
2503
|
-
done()
|
|
2504
|
-
back()
|
|
2847
|
+
confirm: (target: ProviderTargetView) => Promise<void>
|
|
2848
|
+
done: () => void
|
|
2849
|
+
back: () => void
|
|
2505
2850
|
}): ReactElement {
|
|
2506
2851
|
const stdout = useStdout().stdout
|
|
2507
2852
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
@@ -2559,7 +2904,7 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
2559
2904
|
skills: readonly SkillRow[]
|
|
2560
2905
|
commandError: string | undefined
|
|
2561
2906
|
skillError: string | undefined
|
|
2562
|
-
onClose()
|
|
2907
|
+
onClose: () => void
|
|
2563
2908
|
}): ReactElement {
|
|
2564
2909
|
const stdout = useStdout().stdout
|
|
2565
2910
|
const columns = stdout?.columns ?? 80
|
|
@@ -2569,19 +2914,19 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
2569
2914
|
const descBudget = Math.max(0, viewport.contentColumns - nameWidth - 2)
|
|
2570
2915
|
const row = (label: string, description: string): ReactElement => createElement(
|
|
2571
2916
|
Text,
|
|
2572
|
-
{
|
|
2573
|
-
` ${padColumns(label, nameWidth)}${
|
|
2917
|
+
{ color: inkColor(getPalette().dim), wrap: 'truncate-end' },
|
|
2918
|
+
` ${padColumns(label, nameWidth)}${truncateColumns(displayText(description), descBudget)}`,
|
|
2574
2919
|
)
|
|
2575
2920
|
const content: ReactElement[] = [
|
|
2576
|
-
createElement(Text, { key: 'keys-title', bold: true, wrap: 'truncate-end' }, '
|
|
2577
|
-
createElement(Text, { key: 'key-submit', dimColor: true, wrap: 'truncate-end' }, '
|
|
2578
|
-
createElement(Text, { key: 'key-mentions', dimColor: true, wrap: 'truncate-end' }, '
|
|
2579
|
-
createElement(Text, { key: 'key-inspector', dimColor: true, wrap: 'truncate-end' },
|
|
2580
|
-
createElement(Text, { key: 'key-cancel', dimColor: true, wrap: 'truncate-end' }, '
|
|
2581
|
-
createElement(Text, { key: 'key-queue', dimColor: true, wrap: 'truncate-end' },
|
|
2582
|
-
createElement(Text, { key: 'key-edit', dimColor: true, wrap: 'truncate-end' },
|
|
2921
|
+
createElement(Text, { key: 'keys-title', bold: true, wrap: 'truncate-end' }, t('help.keysTitle')),
|
|
2922
|
+
createElement(Text, { key: 'key-submit', dimColor: true, wrap: 'truncate-end' }, ` ${t('help.key.submit')}`),
|
|
2923
|
+
createElement(Text, { key: 'key-mentions', dimColor: true, wrap: 'truncate-end' }, ` ${t('help.key.mentions')}`),
|
|
2924
|
+
createElement(Text, { key: 'key-inspector', dimColor: true, wrap: 'truncate-end' }, ` ${t('help.key.inspector')}`),
|
|
2925
|
+
createElement(Text, { key: 'key-cancel', dimColor: true, wrap: 'truncate-end' }, ` ${t('help.key.cancel')}`),
|
|
2926
|
+
createElement(Text, { key: 'key-queue', dimColor: true, wrap: 'truncate-end' }, ` ${t('help.key.queue')}`),
|
|
2927
|
+
createElement(Text, { key: 'key-edit', dimColor: true, wrap: 'truncate-end' }, ` ${t('help.key.edit')}`),
|
|
2583
2928
|
createElement(Text, { key: 'commands-gap' }, ' '),
|
|
2584
|
-
createElement(Text, { key: 'commands-title', bold: true, wrap: 'truncate-end' }, '
|
|
2929
|
+
createElement(Text, { key: 'commands-title', bold: true, wrap: 'truncate-end' }, t('help.commandsTitle')),
|
|
2585
2930
|
...(commandError === undefined
|
|
2586
2931
|
? []
|
|
2587
2932
|
: [createElement(
|
|
@@ -2592,18 +2937,18 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
2592
2937
|
...LOCAL_COMMANDS.map(command => createElement(
|
|
2593
2938
|
Box,
|
|
2594
2939
|
{ key: `local-${command.label.slice(1)}` },
|
|
2595
|
-
row(command.label, command.
|
|
2940
|
+
row(command.label, t(command.descriptionKey)),
|
|
2596
2941
|
)),
|
|
2597
2942
|
...descriptors.filter(descriptor => !LOCAL_COMMAND_NAMES.has(descriptor.name)).map(descriptor => createElement(
|
|
2598
2943
|
Text,
|
|
2599
|
-
{ key: `command-${descriptor.name}`,
|
|
2600
|
-
` ${padColumns(`/${descriptor.name}`, nameWidth)}${
|
|
2944
|
+
{ key: `command-${descriptor.name}`, color: inkColor(getPalette().dim), wrap: 'truncate-end' },
|
|
2945
|
+
` ${padColumns(`/${descriptor.name}`, nameWidth)}${truncateColumns(displayText(descriptor.description), descBudget)}`,
|
|
2601
2946
|
)),
|
|
2602
2947
|
...(skills.length === 0 && skillError === undefined
|
|
2603
2948
|
? []
|
|
2604
2949
|
: [
|
|
2605
2950
|
createElement(Text, { key: 'skills-gap' }, ' '),
|
|
2606
|
-
createElement(Text, { key: 'skills-title', bold: true, wrap: 'truncate-end' }, '
|
|
2951
|
+
createElement(Text, { key: 'skills-title', bold: true, wrap: 'truncate-end' }, t('help.skillsTitle')),
|
|
2607
2952
|
]),
|
|
2608
2953
|
...(skillError === undefined
|
|
2609
2954
|
? []
|
|
@@ -2614,8 +2959,8 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
2614
2959
|
)]),
|
|
2615
2960
|
...skills.map(skill => createElement(
|
|
2616
2961
|
Text,
|
|
2617
|
-
{ key: `skill-${skill.name}`,
|
|
2618
|
-
` ${padColumns(`/${skill.name}`, nameWidth)}${
|
|
2962
|
+
{ key: `skill-${skill.name}`, color: inkColor(getPalette().dim), wrap: 'truncate-end' },
|
|
2963
|
+
` ${padColumns(`/${skill.name}`, nameWidth)}${truncateColumns(displayText(skill.description), descBudget)}`,
|
|
2619
2964
|
)),
|
|
2620
2965
|
]
|
|
2621
2966
|
const visibleScroll = clampScroll(scroll, content.length, viewport.bodyRows)
|
|
@@ -2641,17 +2986,18 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
2641
2986
|
})
|
|
2642
2987
|
|
|
2643
2988
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
2644
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('
|
|
2989
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('help.compact'), viewport.contentColumns))
|
|
2645
2990
|
}
|
|
2646
2991
|
|
|
2992
|
+
const accent = panelAccent('help', getPalette().brand)
|
|
2647
2993
|
return createElement(
|
|
2648
2994
|
Box,
|
|
2649
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
2650
|
-
createElement(Text, { color: inkColor(
|
|
2995
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
2996
|
+
createElement(Text, { color: inkColor(accent.title), bold: true, wrap: 'truncate-end' }, truncateColumns(`/help — keys and commands · rows ${content.length === 0 ? 0 : visibleScroll + 1}-${Math.min(content.length, visibleScroll + viewport.bodyRows)}/${content.length}`, viewport.contentColumns)),
|
|
2651
2997
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2652
2998
|
...content.slice(visibleScroll, visibleScroll + viewport.bodyRows),
|
|
2653
2999
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2654
|
-
createElement(Text, {
|
|
3000
|
+
createElement(Text, { wrap: 'truncate-end' }, dim(truncateColumns(t('help.footer'), viewport.contentColumns))),
|
|
2655
3001
|
)
|
|
2656
3002
|
}
|
|
2657
3003
|
|
|
@@ -2661,7 +3007,8 @@ function verboseLine(text: string, columns: number): string {
|
|
|
2661
3007
|
}
|
|
2662
3008
|
|
|
2663
3009
|
/** The empty-composer placeholder text (shared by the static and wave paths). */
|
|
2664
|
-
const
|
|
3010
|
+
const composerPlaceholder = (mode: 'queue' | 'steer'): string =>
|
|
3011
|
+
t(mode === 'steer' ? 'composer.placeholderSteer' : 'composer.placeholder')
|
|
2665
3012
|
|
|
2666
3013
|
/** One physical cell of the wave-painted composer row: a char plus styles. */
|
|
2667
3014
|
interface ComposerCell {
|
|
@@ -2694,9 +3041,9 @@ function waveRowSpans(cells: readonly ComposerCell[]): ReactElement[] {
|
|
|
2694
3041
|
const spans: ReactElement[] = []
|
|
2695
3042
|
let start = 0
|
|
2696
3043
|
while (start < cells.length) {
|
|
2697
|
-
const cell = cells[start]
|
|
3044
|
+
const cell = cells[start]
|
|
2698
3045
|
let end = start + 1
|
|
2699
|
-
while (end < cells.length && sameCellStyle(cells[end]
|
|
3046
|
+
while (end < cells.length && sameCellStyle(cells[end], cell)) end += 1
|
|
2700
3047
|
spans.push(createElement(
|
|
2701
3048
|
Text,
|
|
2702
3049
|
{
|
|
@@ -2719,7 +3066,7 @@ function cellIndexAtColumn(cells: readonly ComposerCell[], target: number): numb
|
|
|
2719
3066
|
let column = 0
|
|
2720
3067
|
for (let index = 0; index < cells.length; index += 1) {
|
|
2721
3068
|
if (column === target) return index
|
|
2722
|
-
column += cells[index]
|
|
3069
|
+
column += cells[index].width ?? visibleColumns(cells[index].char)
|
|
2723
3070
|
if (column > target) return undefined
|
|
2724
3071
|
}
|
|
2725
3072
|
return undefined
|
|
@@ -2792,11 +3139,13 @@ interface ComposerWaveProps {
|
|
|
2792
3139
|
value: string
|
|
2793
3140
|
/** Tier prompt glyph and accent color (persistent, like Codex's charge). */
|
|
2794
3141
|
promptGlyph: string
|
|
3142
|
+
/** Empty-composer placeholder for the delivery mode in force. */
|
|
3143
|
+
placeholder: string
|
|
2795
3144
|
promptColor: string
|
|
2796
3145
|
/** Fires EXACTLY ONCE when this sweep ends for any reason — completed,
|
|
2797
3146
|
* cancelled by the gate, or unmounted (a modal panel froze the composer) —
|
|
2798
3147
|
* so Input's played-key latch survives the leaf's unmount/remount cycle. */
|
|
2799
|
-
onSettled()
|
|
3148
|
+
onSettled: () => void
|
|
2800
3149
|
}
|
|
2801
3150
|
|
|
2802
3151
|
/**
|
|
@@ -2864,7 +3213,7 @@ function ComposerWave(props: ComposerWaveProps): ReactElement {
|
|
|
2864
3213
|
}
|
|
2865
3214
|
for (const span of splitGraphemes(parts.before)) push(span.text)
|
|
2866
3215
|
if (parts.hasCaret) push(parts.caret, { inverse: props.caretVisible })
|
|
2867
|
-
const tail = placeholder ?
|
|
3216
|
+
const tail = placeholder ? props.placeholder : parts.after
|
|
2868
3217
|
for (const span of splitGraphemes(tail)) push(span.text, placeholder ? { dim: true } : {})
|
|
2869
3218
|
while (usedColumns < props.bandWidth) push(' ')
|
|
2870
3219
|
|
|
@@ -2873,9 +3222,9 @@ function ComposerWave(props: ComposerWaveProps): ReactElement {
|
|
|
2873
3222
|
const word = tier === 'unknown' ? 'Into the Unknown' : 'deepseek'
|
|
2874
3223
|
const start = Math.max(2, Math.floor((props.bandWidth - word.length) / 2))
|
|
2875
3224
|
const indices = Array.from({ length: word.length }, (_, at) => cellIndexAtColumn(cells, start + at))
|
|
2876
|
-
if (indices.every(index => index !== undefined && (cells[index]
|
|
3225
|
+
if (indices.every(index => index !== undefined && (cells[index].char === ' ' || cells[index].dim === true))) {
|
|
2877
3226
|
for (let at = 0; at < word.length; at += 1) {
|
|
2878
|
-
const cell = cells[indices[at]!]
|
|
3227
|
+
const cell = cells[indices[at]!]
|
|
2879
3228
|
cell.char = word[at]!
|
|
2880
3229
|
cell.width = 1
|
|
2881
3230
|
cell.color = inkColor(deepseekWaveWordHue(at, hues))
|
|
@@ -2887,11 +3236,11 @@ function ComposerWave(props: ComposerWaveProps): ReactElement {
|
|
|
2887
3236
|
if (bandRow === middleBandRow && (tier === 'deepseek' || tier === 'unknown') && style === 'wave') {
|
|
2888
3237
|
const spark = deepseekWaveSpark(tick)
|
|
2889
3238
|
const lastIndex = cellIndexAtColumn(cells, props.bandWidth - 1)
|
|
2890
|
-
if (spark !== null && lastIndex !== undefined && cells[lastIndex]
|
|
2891
|
-
cells[lastIndex]
|
|
2892
|
-
cells[lastIndex]
|
|
2893
|
-
cells[lastIndex]
|
|
2894
|
-
cells[lastIndex]
|
|
3239
|
+
if (spark !== null && lastIndex !== undefined && cells[lastIndex].char === ' ') {
|
|
3240
|
+
cells[lastIndex].char = spark
|
|
3241
|
+
cells[lastIndex].color = props.promptColor
|
|
3242
|
+
cells[lastIndex].bold = true
|
|
3243
|
+
cells[lastIndex].dim = false
|
|
2895
3244
|
}
|
|
2896
3245
|
}
|
|
2897
3246
|
return createElement(Text, { key: `editor-${sourceIndex}`, wrap: 'truncate-end' }, ...waveRowSpans(cells))
|
|
@@ -2905,16 +3254,110 @@ function ComposerWave(props: ComposerWaveProps): ReactElement {
|
|
|
2905
3254
|
)
|
|
2906
3255
|
}
|
|
2907
3256
|
|
|
3257
|
+
/**
|
|
3258
|
+
* The /rainbow celebration leaf: a FIXED seven-color ribbon that slides
|
|
3259
|
+
* across the three-row composer band. Same cell model as ComposerWave so
|
|
3260
|
+
* CJK/emoji stay atomic; no wordmark, no sparkles — the spectrum is the
|
|
3261
|
+
* show. Strictly one-shot per burst id (Input latches onSettled).
|
|
3262
|
+
*/
|
|
3263
|
+
function ComposerRainbowBurst(props: Omit<ComposerWaveProps, 'tier' | 'style'>): ReactElement {
|
|
3264
|
+
const durationMs = RAINBOW_BURST_DURATION_MS
|
|
3265
|
+
const { tick, done } = useWaveFrames(props.active, durationMs)
|
|
3266
|
+
const settledRef = useRef(false)
|
|
3267
|
+
const onSettledRef = useRef(props.onSettled)
|
|
3268
|
+
onSettledRef.current = props.onSettled
|
|
3269
|
+
const settle = (): void => {
|
|
3270
|
+
if (settledRef.current) return
|
|
3271
|
+
settledRef.current = true
|
|
3272
|
+
onSettledRef.current()
|
|
3273
|
+
}
|
|
3274
|
+
useEffect(() => {
|
|
3275
|
+
if (done) settle()
|
|
3276
|
+
}, [done])
|
|
3277
|
+
useEffect(() => () => {
|
|
3278
|
+
settle()
|
|
3279
|
+
}, [])
|
|
3280
|
+
if (!props.active || done || tick * RAINBOW_BURST_TICK_MS >= durationMs) return props.fallback
|
|
3281
|
+
const bandRgb = getPalette().composerBand
|
|
3282
|
+
const totalBandRows = props.rows.length + 2
|
|
3283
|
+
const burstBg = (row: number, column: number): string => {
|
|
3284
|
+
const rgb = rainbowBurstColumnBg(tick, column, props.bandWidth, bandRgb, row, totalBandRows)
|
|
3285
|
+
return rgb === null ? props.bandBg : inkColor(rgb)
|
|
3286
|
+
}
|
|
3287
|
+
const blankBandRow = (row: number): ReactElement => {
|
|
3288
|
+
const blanks: ComposerCell[] = []
|
|
3289
|
+
for (let column = 0; column < props.bandWidth; column += 1) {
|
|
3290
|
+
blanks.push({ char: ' ', width: 1, backgroundColor: burstBg(row, column) })
|
|
3291
|
+
}
|
|
3292
|
+
return createElement(Text, { key: `blank-${row}` }, ...waveRowSpans(blanks))
|
|
3293
|
+
}
|
|
3294
|
+
const editorBurstRows = props.rows.map((row, visibleIndex) => {
|
|
3295
|
+
const sourceIndex = props.windowStart + visibleIndex
|
|
3296
|
+
const bandRow = visibleIndex + 1
|
|
3297
|
+
const parts = editorRowParts(row, sourceIndex, props.caretRow, props.cursor)
|
|
3298
|
+
const placeholder = sourceIndex === 0 && props.value === ''
|
|
3299
|
+
const cells: ComposerCell[] = []
|
|
3300
|
+
let usedColumns = 0
|
|
3301
|
+
const push = (char: string, extra: Omit<ComposerCell, 'char' | 'width' | 'backgroundColor'> = {}): void => {
|
|
3302
|
+
const width = visibleColumns(char)
|
|
3303
|
+
cells.push({ char, width, backgroundColor: burstBg(bandRow, usedColumns), ...extra })
|
|
3304
|
+
usedColumns += width
|
|
3305
|
+
}
|
|
3306
|
+
if (sourceIndex === 0) {
|
|
3307
|
+
push(props.promptGlyph, { color: props.promptColor, bold: true })
|
|
3308
|
+
push(' ', { color: props.promptColor })
|
|
3309
|
+
} else {
|
|
3310
|
+
push(' ')
|
|
3311
|
+
push(' ')
|
|
3312
|
+
}
|
|
3313
|
+
for (const span of splitGraphemes(parts.before)) push(span.text)
|
|
3314
|
+
if (parts.hasCaret) push(parts.caret, { inverse: props.caretVisible })
|
|
3315
|
+
const tail = placeholder ? props.placeholder : parts.after
|
|
3316
|
+
for (const span of splitGraphemes(tail)) push(span.text, placeholder ? { dim: true } : {})
|
|
3317
|
+
while (usedColumns < props.bandWidth) push(' ')
|
|
3318
|
+
return createElement(Text, { key: `editor-${sourceIndex}`, wrap: 'truncate-end' }, ...waveRowSpans(cells))
|
|
3319
|
+
})
|
|
3320
|
+
return createElement(
|
|
3321
|
+
Box,
|
|
3322
|
+
{ flexDirection: 'column', width: props.bandWidth },
|
|
3323
|
+
blankBandRow(0),
|
|
3324
|
+
...editorBurstRows,
|
|
3325
|
+
blankBandRow(totalBandRows - 1),
|
|
3326
|
+
)
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
/** One-word kind label per entry, so the inspector's ←→ walk names what
|
|
3330
|
+
* each step is instead of leaving the reader to infer it from the body. */
|
|
3331
|
+
function entryKindLabel(entry: TranscriptEntry | undefined): string {
|
|
3332
|
+
switch (entry?.kind) {
|
|
3333
|
+
case 'user': return 'user prompt'
|
|
3334
|
+
case 'pending': return 'queued prompt'
|
|
3335
|
+
case 'assistant': return 'reply'
|
|
3336
|
+
case 'tool': return 'tool call'
|
|
3337
|
+
case 'command': return 'command'
|
|
3338
|
+
case 'error': return 'turn error'
|
|
3339
|
+
case 'turn-marker': return 'turn end'
|
|
3340
|
+
case 'compaction': return 'compaction'
|
|
3341
|
+
case 'retry': return 'retry'
|
|
3342
|
+
case 'files': return 'files changed'
|
|
3343
|
+
case 'workflow': return 'workflow run'
|
|
3344
|
+
default: return 'empty'
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
|
|
2908
3348
|
/**
|
|
2909
3349
|
* The Ctrl+O transcript inspector: one selected durable entry at a time,
|
|
2910
3350
|
* with independent history selection and content scrolling. The complete
|
|
2911
3351
|
* retained entry is converted to physical rows, but only one viewport slice
|
|
2912
3352
|
* reaches Ink, so even a huge reasoning block cannot grow the dynamic tree.
|
|
2913
3353
|
*/
|
|
2914
|
-
function VerbosePanel({ entries, onClose
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
3354
|
+
function VerbosePanel({ entries, onClose, columns, rows }: {
|
|
3355
|
+
entries: readonly TranscriptEntry[]
|
|
3356
|
+
onClose: () => void
|
|
3357
|
+
/** Live terminal columns from App's resize store — not useStdout, so memo cannot skip a reflow. */
|
|
3358
|
+
columns: number
|
|
3359
|
+
rows: number
|
|
3360
|
+
}): ReactElement {
|
|
2918
3361
|
const viewport = inspectorViewport(columns, rows)
|
|
2919
3362
|
const [cursor, setCursor] = useState(() => Math.max(0, entries.length - 1))
|
|
2920
3363
|
const [scroll, setScroll] = useState(0)
|
|
@@ -2927,6 +3370,8 @@ function VerbosePanel({ entries, onClose }: { entries: readonly TranscriptEntry[
|
|
|
2927
3370
|
[entry, viewport.contentColumns],
|
|
2928
3371
|
)
|
|
2929
3372
|
const visibleScroll = clampScroll(scroll, allLines.length, viewport.bodyRows)
|
|
3373
|
+
const visibleScrollRef = useRef(visibleScroll)
|
|
3374
|
+
visibleScrollRef.current = visibleScroll
|
|
2930
3375
|
|
|
2931
3376
|
useEffect(() => {
|
|
2932
3377
|
cursorRef.current = cursor
|
|
@@ -2936,7 +3381,7 @@ function VerbosePanel({ entries, onClose }: { entries: readonly TranscriptEntry[
|
|
|
2936
3381
|
const current = cursorRef.current
|
|
2937
3382
|
const next = followInspectorCursor(current, previousLength.current, entries.length)
|
|
2938
3383
|
if (next !== current) {
|
|
2939
|
-
savedScroll.current.set(current,
|
|
3384
|
+
savedScroll.current.set(current, visibleScrollRef.current)
|
|
2940
3385
|
setCursor(next)
|
|
2941
3386
|
setScroll(savedScroll.current.get(next) ?? 0)
|
|
2942
3387
|
}
|
|
@@ -3005,14 +3450,15 @@ function VerbosePanel({ entries, onClose }: { entries: readonly TranscriptEntry[
|
|
|
3005
3450
|
return createElement(
|
|
3006
3451
|
Text,
|
|
3007
3452
|
{ wrap: 'truncate-end' },
|
|
3008
|
-
truncateColumns('
|
|
3453
|
+
truncateColumns(t('panel.verbose.compact'), viewport.contentColumns),
|
|
3009
3454
|
)
|
|
3010
3455
|
}
|
|
3011
3456
|
|
|
3012
3457
|
const title = entries.length === 0
|
|
3013
3458
|
? 'history details · empty'
|
|
3014
|
-
: `history details · entry ${cursor + 1}/${entries.length} · lines ${allLines.length === 0 ? 0 : visibleScroll + 1}-${Math.min(allLines.length, visibleScroll + viewport.bodyRows)}/${allLines.length}`
|
|
3459
|
+
: `history details · entry ${cursor + 1}/${entries.length} · ${entryKindLabel(entry)} · lines ${allLines.length === 0 ? 0 : visibleScroll + 1}-${Math.min(allLines.length, visibleScroll + viewport.bodyRows)}/${allLines.length}`
|
|
3015
3460
|
const visible = allLines.slice(visibleScroll, visibleScroll + viewport.bodyRows)
|
|
3461
|
+
const accent = panelAccent('history-inspector', getPalette().brand)
|
|
3016
3462
|
return createElement(
|
|
3017
3463
|
Box,
|
|
3018
3464
|
{
|
|
@@ -3020,11 +3466,11 @@ function VerbosePanel({ entries, onClose }: { entries: readonly TranscriptEntry[
|
|
|
3020
3466
|
width: viewport.outerColumns,
|
|
3021
3467
|
paddingX: 1,
|
|
3022
3468
|
borderStyle: 'round',
|
|
3023
|
-
borderColor: inkColor(
|
|
3469
|
+
borderColor: inkColor(accent.border),
|
|
3024
3470
|
},
|
|
3025
3471
|
createElement(
|
|
3026
3472
|
Text,
|
|
3027
|
-
{ color: inkColor(
|
|
3473
|
+
{ color: inkColor(accent.title), bold: true, wrap: 'truncate-end' },
|
|
3028
3474
|
truncateColumns(title, viewport.contentColumns),
|
|
3029
3475
|
),
|
|
3030
3476
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
@@ -3038,8 +3484,8 @@ function VerbosePanel({ entries, onClose }: { entries: readonly TranscriptEntry[
|
|
|
3038
3484
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
3039
3485
|
createElement(
|
|
3040
3486
|
Text,
|
|
3041
|
-
{
|
|
3042
|
-
dim(truncateColumns('
|
|
3487
|
+
{ wrap: 'truncate-end' },
|
|
3488
|
+
dim(truncateColumns(t('panel.verbose.footer'), viewport.contentColumns)),
|
|
3043
3489
|
),
|
|
3044
3490
|
)
|
|
3045
3491
|
}
|
|
@@ -3068,6 +3514,16 @@ interface CompletionCandidate {
|
|
|
3068
3514
|
origin: 'command' | 'skill' | 'mention'
|
|
3069
3515
|
}
|
|
3070
3516
|
|
|
3517
|
+
/**
|
|
3518
|
+
* Wrap one completion-menu cursor step. An empty menu keeps the index at 0:
|
|
3519
|
+
* `% 0` is NaN, and a NaN index silently disables the highlight, the accept
|
|
3520
|
+
* key, and any later Enter that runs through the menu.
|
|
3521
|
+
*/
|
|
3522
|
+
export function stepCompletionIndex(index: number, delta: number, count: number): number {
|
|
3523
|
+
if (count <= 0) return 0
|
|
3524
|
+
return ((index + delta) % count + count) % count
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3071
3527
|
/**
|
|
3072
3528
|
* Resolve completion candidates for the current input: TUI-local commands,
|
|
3073
3529
|
* the live registry descriptors, and user-invocable skills, filtered by the
|
|
@@ -3086,7 +3542,7 @@ export function completionCandidates(
|
|
|
3086
3542
|
): readonly CompletionCandidate[] {
|
|
3087
3543
|
if (!value.startsWith('/')) return []
|
|
3088
3544
|
const prefix = value.slice(1).split(' ')[0] ?? ''
|
|
3089
|
-
const local: CompletionCandidate[] = LOCAL_COMMANDS.map(command => ({
|
|
3545
|
+
const local: CompletionCandidate[] = LOCAL_COMMANDS.map(command => ({ label: command.label, description: t(command.descriptionKey), origin: 'command' }))
|
|
3090
3546
|
// Local commands shadow registry names (e.g. the TUI-local /permission works
|
|
3091
3547
|
// before any session exists, while the registry child needs one), so
|
|
3092
3548
|
// collisions cannot render two rows with the same key.
|
|
@@ -3151,10 +3607,10 @@ function completionMenuRowCount(terminalRows: number, rowCount: number): number
|
|
|
3151
3607
|
|
|
3152
3608
|
/**
|
|
3153
3609
|
* The completion menu, rendered inside the composer's subtree directly above
|
|
3154
|
-
* the composer band — attached the way Claude-Code anchors its dropdown.
|
|
3155
|
-
*
|
|
3156
|
-
*
|
|
3157
|
-
*
|
|
3610
|
+
* the composer band — attached the way Claude-Code anchors its dropdown.
|
|
3611
|
+
* Its height is deducted from the live transcript budget so the menu covers
|
|
3612
|
+
* live rows instead of growing the tree and moving the composer/status.
|
|
3613
|
+
* Props-only (no lifted state): the menu is a pure view of the input
|
|
3158
3614
|
* editor's live completion state, so no cross-component effect ever resyncs
|
|
3159
3615
|
* it (a state lift here previously deadlocked the menu after a resize).
|
|
3160
3616
|
*/
|
|
@@ -3212,7 +3668,7 @@ function CompletionMenu({ active, mention, index, rows, error }: {
|
|
|
3212
3668
|
// Scroll affordance: with the full merged catalog (commands + registry +
|
|
3213
3669
|
// skills) the six-row window rarely shows the tail — count and hint keep
|
|
3214
3670
|
// the rest discoverable without inflating the menu budget.
|
|
3215
|
-
hidden > 0 ? createElement(Text, { key: 'more', color: inkColor(getPalette().dim), wrap: 'truncate-end' },
|
|
3671
|
+
hidden > 0 ? createElement(Text, { key: 'more', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, ` … +${hidden} more`) : undefined,
|
|
3216
3672
|
showFooter ? createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, dim(mention ? `↑↓ choose · ${rows.length} items · tab insert` : `↑↓ choose · ${rows.length} items · tab complete`)) : undefined,
|
|
3217
3673
|
)
|
|
3218
3674
|
}
|
|
@@ -3234,7 +3690,7 @@ interface DraftFile extends FilePathInspection {
|
|
|
3234
3690
|
* While a modal (approval / question / model panel) owns the keys, the
|
|
3235
3691
|
* box passes every key through untouched.
|
|
3236
3692
|
*/
|
|
3237
|
-
function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openUpdate, openSchedule, 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, inspectFiles, prepareFiles, cycleMode, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued,
|
|
3693
|
+
function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch, steer, submitMode, cycleSubmitMode, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openSearch, openPlugin, openUpdate, openSchedule, openJobs, openStatusline, openTheme, openLanguage, saveLanguage, openHistory, openQueue, openAgents, openSubagent, openTodos, openUsage, openDelete, openDiff, openReviewPicker, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, applyEditorKeys, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, inspectFiles, prepareFiles, cycleMode, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, updateQueued, historyFill, historyConsumed, animations, applyAnimations, applyRainbow, rainbowBurstId, waveTier, waveStyle, maxRows, anchorRowsBelow, tabTitle, onEditorRows, onMenuRows, sessionKey }: {
|
|
3238
3694
|
active: boolean
|
|
3239
3695
|
frozen: boolean
|
|
3240
3696
|
/** Frozen-band hint naming the surface that owns the keyboard; an empty
|
|
@@ -3243,82 +3699,100 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3243
3699
|
busy: boolean
|
|
3244
3700
|
descriptors: readonly CommandDescriptor[]
|
|
3245
3701
|
skills: readonly SkillRow[]
|
|
3246
|
-
dispatch(text: string, attachments?: readonly ContentBlock[], origin?: string)
|
|
3247
|
-
|
|
3702
|
+
dispatch: (text: string, attachments?: readonly ContentBlock[], origin?: string) => void
|
|
3703
|
+
/** Submit as steering into the running turn (see {@link AppProps.steer}). */
|
|
3704
|
+
steer: (text: string, attachments?: readonly ContentBlock[], origin?: string) => void
|
|
3705
|
+
/** Delivery mode the next submission uses; Tab on an empty composer flips it. */
|
|
3706
|
+
submitMode: 'queue' | 'steer'
|
|
3707
|
+
/** Flip {@link submitMode} and report the new mode. */
|
|
3708
|
+
cycleSubmitMode: () => void
|
|
3248
3709
|
/** The full current session identity ('' while pending); the delivery origin. */
|
|
3249
3710
|
sessionKey: string
|
|
3250
|
-
interrupt()
|
|
3251
|
-
quit()
|
|
3252
|
-
openModel()
|
|
3253
|
-
openEffort()
|
|
3254
|
-
openHelp()
|
|
3255
|
-
openMode()
|
|
3256
|
-
openPermission()
|
|
3257
|
-
openResume()
|
|
3258
|
-
|
|
3711
|
+
interrupt: () => boolean
|
|
3712
|
+
quit: () => void
|
|
3713
|
+
openModel: () => void
|
|
3714
|
+
openEffort: () => void
|
|
3715
|
+
openHelp: () => void
|
|
3716
|
+
openMode: () => void
|
|
3717
|
+
openPermission: () => void
|
|
3718
|
+
openResume: () => void
|
|
3719
|
+
/** Open the /search panel with an optional seed query. */
|
|
3720
|
+
openSearch: (query: string) => void
|
|
3721
|
+
openPlugin: (query?: string) => void
|
|
3259
3722
|
/** Open the /update panel (aligned upgrade surface). */
|
|
3260
|
-
openUpdate()
|
|
3723
|
+
openUpdate: () => void
|
|
3261
3724
|
/** Open the /schedule reminder panel (read-only catalog). */
|
|
3262
|
-
openSchedule()
|
|
3263
|
-
openJobs()
|
|
3264
|
-
openStatusline()
|
|
3265
|
-
openTheme()
|
|
3266
|
-
|
|
3725
|
+
openSchedule: () => void
|
|
3726
|
+
openJobs: () => void
|
|
3727
|
+
openStatusline: () => void
|
|
3728
|
+
openTheme: () => void
|
|
3729
|
+
/** Open the /language picker (bare /language). */
|
|
3730
|
+
openLanguage: () => void
|
|
3731
|
+
/** Apply and persist a language chosen by argument. */
|
|
3732
|
+
saveLanguage: (name: LanguageName) => void
|
|
3733
|
+
openHistory: () => void
|
|
3734
|
+
openQueue: () => void
|
|
3267
3735
|
/** Open the /agents panel (live subagent feed + transcript entry). */
|
|
3268
|
-
openAgents()
|
|
3736
|
+
openAgents: () => void
|
|
3269
3737
|
/** Open the /subagent model panel. */
|
|
3270
|
-
openSubagent()
|
|
3738
|
+
openSubagent: () => void
|
|
3271
3739
|
/** Open the /todos subpage (full todo list in one bounded panel). */
|
|
3272
|
-
openTodos()
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3740
|
+
openTodos: () => void
|
|
3741
|
+
openUsage: () => void
|
|
3742
|
+
/** Open the dedicated /delete picker, optionally pre-armed on one id. */
|
|
3743
|
+
openDelete: (id?: string) => void
|
|
3744
|
+
openDiff: (argument: string) => void
|
|
3745
|
+
reviewChanges: (selection: ReviewSelection) => void
|
|
3746
|
+
/** Open the /review candidate picker (bare /review). */
|
|
3747
|
+
openReviewPicker: () => void
|
|
3277
3748
|
/** The row id awaiting y/n in this box, when a deletion is pending. */
|
|
3278
3749
|
deleteConfirm?: string
|
|
3279
3750
|
/** Confirm the pending deletion (y in the box). */
|
|
3280
|
-
confirmDelete()
|
|
3751
|
+
confirmDelete: () => void
|
|
3281
3752
|
/** Cancel the pending deletion (any other key in the box). */
|
|
3282
|
-
cancelDelete()
|
|
3283
|
-
createSession(mode?: string)
|
|
3284
|
-
forkSession(argument: string)
|
|
3285
|
-
cancelSessionSwitch()
|
|
3286
|
-
notify(text: string, tone?: NoticeTone)
|
|
3753
|
+
cancelDelete: () => void
|
|
3754
|
+
createSession: (mode?: string) => void
|
|
3755
|
+
forkSession: (argument: string) => void
|
|
3756
|
+
cancelSessionSwitch: () => boolean
|
|
3757
|
+
notify: (text: string, tone?: NoticeTone) => void
|
|
3287
3758
|
/** Apply the Ctrl+R passthrough to the detected editor (/vscode-keys); resolves to a one-line summary. */
|
|
3288
|
-
applyEditorKeys()
|
|
3759
|
+
applyEditorKeys: () => Promise<string>
|
|
3289
3760
|
hasNotice: boolean
|
|
3290
|
-
dismissNotice()
|
|
3291
|
-
toggleReasoning()
|
|
3292
|
-
openVerbose()
|
|
3293
|
-
clearView()
|
|
3294
|
-
refresh()
|
|
3295
|
-
loadMentions(query: string, signal?: AbortSignal)
|
|
3296
|
-
inspectImages(paths: readonly string[])
|
|
3297
|
-
prepareImages(paths: readonly string[], signal?: AbortSignal)
|
|
3298
|
-
inspectFiles(paths: readonly string[])
|
|
3299
|
-
prepareFiles(paths: readonly string[], signal?: AbortSignal)
|
|
3300
|
-
cycleMode()
|
|
3301
|
-
exportTranscript(argument: string)
|
|
3302
|
-
renameTitle(argument: string)
|
|
3303
|
-
copyLastResponse()
|
|
3761
|
+
dismissNotice: () => void
|
|
3762
|
+
toggleReasoning: () => void
|
|
3763
|
+
openVerbose: () => void
|
|
3764
|
+
clearView: () => void
|
|
3765
|
+
refresh: () => void
|
|
3766
|
+
loadMentions: (query: string, signal?: AbortSignal) => Promise<readonly MentionCandidate[]>
|
|
3767
|
+
inspectImages: (paths: readonly string[]) => Promise<readonly ImagePathInspection[]>
|
|
3768
|
+
prepareImages: (paths: readonly string[], signal?: AbortSignal) => Promise<readonly ImageBlock[]>
|
|
3769
|
+
inspectFiles: (paths: readonly string[]) => Promise<readonly FilePathInspection[]>
|
|
3770
|
+
prepareFiles: (paths: readonly string[], signal?: AbortSignal) => Promise<readonly FileBlock[]>
|
|
3771
|
+
cycleMode: () => string
|
|
3772
|
+
exportTranscript: (argument: string) => Promise<void>
|
|
3773
|
+
renameTitle: (argument: string) => string
|
|
3774
|
+
copyLastResponse: () => Promise<string>
|
|
3304
3775
|
/** Newest-first recall space (persistent + in-session, deduped). */
|
|
3305
3776
|
recallSpace: readonly string[]
|
|
3306
3777
|
/** Record one in-session submission (deduped, local only). */
|
|
3307
|
-
recordLocal(text: string)
|
|
3778
|
+
recordLocal: (text: string) => void
|
|
3308
3779
|
/** Persist one submission to the global history file. */
|
|
3309
|
-
recordHistory(text: string)
|
|
3310
|
-
/**
|
|
3311
|
-
queued: readonly {
|
|
3312
|
-
|
|
3313
|
-
cancelQueued(messageId: string): void
|
|
3780
|
+
recordHistory: (text: string) => void
|
|
3781
|
+
/** Next-turn inbox rows, ordered exactly as the durable inbox. */
|
|
3782
|
+
queued: readonly Extract<TranscriptEntry, { kind: 'pending' }>[]
|
|
3783
|
+
updateQueued?: (messageId: string, action: QueueMutation) => void
|
|
3314
3784
|
/** Accepted /history entry waiting to be placed into the composer. */
|
|
3315
3785
|
historyFill: { text: string; index: number } | undefined
|
|
3316
3786
|
/** Marks the accepted entry consumed (called after the fill is applied). */
|
|
3317
|
-
historyConsumed()
|
|
3787
|
+
historyConsumed: () => void
|
|
3318
3788
|
/** Whether timed animations run (shimmer, chase, blink, wave). */
|
|
3319
3789
|
animations: boolean
|
|
3320
3790
|
/** Apply and report one /animation toggle (App persists through the runner). */
|
|
3321
|
-
applyAnimations(enabled: boolean)
|
|
3791
|
+
applyAnimations: (enabled: boolean) => void
|
|
3792
|
+
/** Reroll or pin the rainbow palette (switches to rainbow if needed). */
|
|
3793
|
+
applyRainbow: (seed?: number) => void
|
|
3794
|
+
/** Monotonic id of the in-flight /rainbow composer burst; 0 means none. */
|
|
3795
|
+
rainbowBurstId: number
|
|
3322
3796
|
/** DeepSeek easter-egg wave tier of the applied route (null otherwise):
|
|
3323
3797
|
* official DeepSeek models drive their flash/pro tiers, non-DeepSeek
|
|
3324
3798
|
* models running an effort above high drive the "Into the Unknown"
|
|
@@ -3337,10 +3811,10 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3337
3811
|
* background process sharing the console cannot keep it overwritten. */
|
|
3338
3812
|
tabTitle: string
|
|
3339
3813
|
/** Reports the editor's current physical row count so the live budget stays exact. */
|
|
3340
|
-
onEditorRows(rows: number)
|
|
3814
|
+
onEditorRows: (rows: number) => void
|
|
3341
3815
|
/** Reports the open completion menu's physical row count (0 when closed)
|
|
3342
3816
|
* for the same reason: the dynamic budget must reserve it, not overflow. */
|
|
3343
|
-
onMenuRows(rows: number)
|
|
3817
|
+
onMenuRows: (rows: number) => void
|
|
3344
3818
|
}): ReactElement {
|
|
3345
3819
|
const { stdout: inputStdout } = useStdout()
|
|
3346
3820
|
const columns = inputStdout?.columns ?? 80
|
|
@@ -3393,27 +3867,28 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3393
3867
|
preferredColumnRef.current = null
|
|
3394
3868
|
}, [editorColumns])
|
|
3395
3869
|
|
|
3396
|
-
// A /history panel acceptance lands as a fill:
|
|
3397
|
-
// the
|
|
3870
|
+
// A /history panel acceptance lands as a fill: append the sanitized text to
|
|
3871
|
+
// the composer and resume recall from that entry. Appending (never
|
|
3872
|
+
// replacing) is what keeps a half-written draft and its prepared
|
|
3873
|
+
// attachments from being destroyed by picking a history entry.
|
|
3398
3874
|
useEffect(() => {
|
|
3399
3875
|
if (historyFill === undefined) return
|
|
3400
3876
|
const safe = sanitizeDraftText(historyFill.text)
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
setValue(safe)
|
|
3408
|
-
setCursor(safe.length)
|
|
3877
|
+
const current = valueRef.current
|
|
3878
|
+
const joined = appendRecall(current, safe)
|
|
3879
|
+
valueRef.current = joined
|
|
3880
|
+
cursorRef.current = joined.length
|
|
3881
|
+
setValue(joined)
|
|
3882
|
+
setCursor(joined.length)
|
|
3409
3883
|
resetCursorBlink()
|
|
3410
3884
|
preferredColumnRef.current = null
|
|
3411
3885
|
setDismissedMenuValue(undefined)
|
|
3412
3886
|
recall.current = {
|
|
3413
3887
|
entries: recallSpace,
|
|
3414
3888
|
index: historyFill.index,
|
|
3415
|
-
|
|
3416
|
-
|
|
3889
|
+
// The draft this fill appended to stays reachable: Down walks back to it.
|
|
3890
|
+
savedDraft: current,
|
|
3891
|
+
lastRecalled: joined,
|
|
3417
3892
|
}
|
|
3418
3893
|
historyConsumed()
|
|
3419
3894
|
}, [historyFill, recallSpace, historyConsumed, resetCursorBlink])
|
|
@@ -3443,7 +3918,11 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3443
3918
|
if (stdin === undefined) return
|
|
3444
3919
|
const originalRead = stdin.read.bind(stdin)
|
|
3445
3920
|
const patchedRead = function patchedRead(this: typeof stdin, ...args: Parameters<typeof originalRead>) {
|
|
3446
|
-
|
|
3921
|
+
// `Readable.read` is declared `any`; the assertion names its real result
|
|
3922
|
+
// union once so the normalization and the focus-event stripping below
|
|
3923
|
+
// stay type-checked. Node hands back a Buffer unless an encoding was set,
|
|
3924
|
+
// and null once the stream ends.
|
|
3925
|
+
const chunk = originalRead(...args) as string | Buffer | null
|
|
3447
3926
|
if (chunk === null) return chunk
|
|
3448
3927
|
const normalized = normalizeKeyboardChunk(typeof chunk === 'string' ? chunk : String(chunk))
|
|
3449
3928
|
const input = focusReporting
|
|
@@ -3464,9 +3943,9 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3464
3943
|
} as typeof stdin.read
|
|
3465
3944
|
stdin.read = patchedRead
|
|
3466
3945
|
return () => {
|
|
3467
|
-
stdin.read = originalRead
|
|
3946
|
+
stdin.read = originalRead
|
|
3468
3947
|
}
|
|
3469
|
-
}, [focusReporting, stdin])
|
|
3948
|
+
}, [focusReporting, inputStdout, stdin])
|
|
3470
3949
|
|
|
3471
3950
|
// Keep the navigation's recall space fresh while browsing state survives
|
|
3472
3951
|
// (new local submissions extend the space; the index stays valid unless
|
|
@@ -3480,7 +3959,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3480
3959
|
const [completionIndex, setCompletionIndex] = useState(0)
|
|
3481
3960
|
const [dismissedMenuValue, setDismissedMenuValue] = useState<string | undefined>(undefined)
|
|
3482
3961
|
const candidates = completionCandidates(value, descriptors, skills)
|
|
3483
|
-
const slashActive = candidates.length > 0 && value.startsWith('/') && !value.includes(' ') && !value.includes('\n')
|
|
3962
|
+
const slashActive = candidates.length > 0 && value.startsWith('/') && !value.includes(' ') && !value.includes('\n') && !looksLikePathDraft(value)
|
|
3484
3963
|
|
|
3485
3964
|
// @mention token: the last `@word` on the cursor's line before the cursor.
|
|
3486
3965
|
const beforeCursor = value.slice(0, cursor)
|
|
@@ -3519,7 +3998,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3519
3998
|
|
|
3520
3999
|
const registerDraftImage = (inspection: ImagePathInspection, marker: string): boolean => {
|
|
3521
4000
|
if (draftImagesRef.current.some(image => sameImagePath(image.path, inspection.path))) {
|
|
3522
|
-
notify(
|
|
4001
|
+
notify(t('notice.attachmentAlready', { name: inspection.name }), 'warning')
|
|
3523
4002
|
return false
|
|
3524
4003
|
}
|
|
3525
4004
|
const next = [...draftImagesRef.current, { ...inspection, marker }]
|
|
@@ -3538,7 +4017,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3538
4017
|
const originalCursor = cursorRef.current
|
|
3539
4018
|
const total = imagePaths.length + filePaths.length
|
|
3540
4019
|
if (total === 0) return
|
|
3541
|
-
notify(
|
|
4020
|
+
notify(t('notice.attachmentsChecking', { count: total, plural: total === 1 ? '' : 's' }))
|
|
3542
4021
|
void Promise.all([
|
|
3543
4022
|
imagePaths.length === 0 ? Promise.resolve([]) : inspectImages(imagePaths),
|
|
3544
4023
|
filePaths.length === 0 ? Promise.resolve([]) : inspectFiles(filePaths),
|
|
@@ -3559,13 +4038,13 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3559
4038
|
markers.push(marker)
|
|
3560
4039
|
}
|
|
3561
4040
|
if (imageAdditions.length === 0 && fileAdditions.length === 0) {
|
|
3562
|
-
notify('
|
|
4041
|
+
notify(t('notice.attachmentsAlready'), 'warning')
|
|
3563
4042
|
return
|
|
3564
4043
|
}
|
|
3565
4044
|
const current = valueRef.current
|
|
3566
4045
|
const anchor = remapStableRange(originalValue, current, { start: originalCursor, end: originalCursor })
|
|
3567
4046
|
if (anchor === undefined) {
|
|
3568
|
-
notify('
|
|
4047
|
+
notify(t('notice.attachmentDraftChanged'), 'warning')
|
|
3569
4048
|
return
|
|
3570
4049
|
}
|
|
3571
4050
|
const at = anchor.start
|
|
@@ -3586,9 +4065,9 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3586
4065
|
draftFilesRef.current = nextFiles
|
|
3587
4066
|
setDraftFiles(nextFiles)
|
|
3588
4067
|
const count = imageAdditions.length + fileAdditions.length
|
|
3589
|
-
notify(
|
|
4068
|
+
notify(t('notice.attachmentsReady', { count, plural: count === 1 ? '' : 's' }))
|
|
3590
4069
|
}, (reason: unknown) => {
|
|
3591
|
-
notify(
|
|
4070
|
+
notify(t('notice.attachmentFailed', { message: reason instanceof Error ? reason.message : String(reason) }), 'error')
|
|
3592
4071
|
})
|
|
3593
4072
|
}
|
|
3594
4073
|
|
|
@@ -3596,8 +4075,8 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3596
4075
|
const requestId = mentionRequestRef.current + 1
|
|
3597
4076
|
mentionRequestRef.current = requestId
|
|
3598
4077
|
if (!active || !mentionActive) {
|
|
3599
|
-
setMentionRows([])
|
|
3600
|
-
setMentionError(undefined)
|
|
4078
|
+
setMentionRows(current => current.length === 0 ? current : [])
|
|
4079
|
+
setMentionError(current => current === undefined ? current : undefined)
|
|
3601
4080
|
return
|
|
3602
4081
|
}
|
|
3603
4082
|
setMentionError(undefined)
|
|
@@ -3620,7 +4099,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3620
4099
|
clearTimeout(timer)
|
|
3621
4100
|
controller.abort()
|
|
3622
4101
|
}
|
|
3623
|
-
}, [active, mentionActive, mentionToken?.query])
|
|
4102
|
+
}, [active, loadMentions, mentionActive, mentionToken?.query])
|
|
3624
4103
|
|
|
3625
4104
|
// Codex routes keys to the topmost surface first. Completion therefore
|
|
3626
4105
|
// remains available while a turn runs, and Esc dismisses it before the
|
|
@@ -3656,6 +4135,23 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3656
4135
|
// the App's dynamic budget can reserve it instead of overflowing.
|
|
3657
4136
|
const menuHeightRows = menuActive ? completionMenuRowCount(inputTerminalRows, menuRows.length) : 0
|
|
3658
4137
|
|
|
4138
|
+
/**
|
|
4139
|
+
* What accepting the highlighted completion candidate would insert, or ''
|
|
4140
|
+
* when nothing can be accepted (no rows, or an image row that resolves
|
|
4141
|
+
* asynchronously). The Enter branch uses it to tell a real accept from a
|
|
4142
|
+
* no-op that must fall through to submission.
|
|
4143
|
+
*/
|
|
4144
|
+
const highlightedCompletion = (): string => {
|
|
4145
|
+
if (mentionActive) {
|
|
4146
|
+
const row = rankedMentionRows[completionIndex % Math.max(1, rankedMentionRows.length)]
|
|
4147
|
+
if (row === undefined) return ''
|
|
4148
|
+
if (row.kind === 'file' && row.path !== undefined && looksLikeImagePath(row.path)) return ''
|
|
4149
|
+
return row.label.startsWith('@') ? row.label : `@${row.label}${row.kind === 'directory' ? '/' : ''}`
|
|
4150
|
+
}
|
|
4151
|
+
const candidate = candidates[completionIndex % Math.max(1, candidates.length)]
|
|
4152
|
+
return candidate === undefined ? '' : `${candidate.label} `
|
|
4153
|
+
}
|
|
4154
|
+
|
|
3659
4155
|
/** Accept the highlighted completion-menu candidate into the draft. */
|
|
3660
4156
|
const acceptMenuCandidate = (): void => {
|
|
3661
4157
|
if (mentionActive && mentionToken !== undefined) {
|
|
@@ -3673,7 +4169,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3673
4169
|
const current = valueRef.current
|
|
3674
4170
|
const anchor = remapStableRange(originalValue, current, { start, end: start + tokenText.length })
|
|
3675
4171
|
if (anchor === undefined || current.slice(anchor.start, anchor.end) !== tokenText) {
|
|
3676
|
-
notify('
|
|
4172
|
+
notify(t('notice.imageDraftChanged'), 'warning')
|
|
3677
4173
|
return
|
|
3678
4174
|
}
|
|
3679
4175
|
if (draftImagesRef.current.some(image => sameImagePath(image.path, inspection.path))) {
|
|
@@ -3684,7 +4180,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3684
4180
|
setCursor(edit.cursor)
|
|
3685
4181
|
resetCursorBlink()
|
|
3686
4182
|
setDismissedMenuValue(edit.value)
|
|
3687
|
-
notify(
|
|
4183
|
+
notify(t('notice.attachmentAlready', { name: inspection.name }), 'warning')
|
|
3688
4184
|
return
|
|
3689
4185
|
}
|
|
3690
4186
|
const marker = uniqueImageMarker(inspection.name, 'mention')
|
|
@@ -3696,9 +4192,9 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3696
4192
|
resetCursorBlink()
|
|
3697
4193
|
setDismissedMenuValue(edit.value)
|
|
3698
4194
|
registerDraftImage(inspection, marker)
|
|
3699
|
-
notify(
|
|
4195
|
+
notify(t('notice.imageReady', { name: inspection.name }))
|
|
3700
4196
|
}, (reason: unknown) => {
|
|
3701
|
-
notify(
|
|
4197
|
+
notify(t('notice.imageFailed', { message: reason instanceof Error ? reason.message : String(reason) }), 'error')
|
|
3702
4198
|
})
|
|
3703
4199
|
setCompletionIndex(0)
|
|
3704
4200
|
setDismissedMenuValue(undefined)
|
|
@@ -3745,6 +4241,18 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3745
4241
|
preferredColumnRef.current = null
|
|
3746
4242
|
setCompletionIndex(0)
|
|
3747
4243
|
setDismissedMenuValue(undefined)
|
|
4244
|
+
// A file drag (any OS / terminal) often writes the path without
|
|
4245
|
+
// bracketed-paste wrappers, so it lands as ordinary insert. If the whole
|
|
4246
|
+
// draft is one drop path, attach it the same way a copied pathname paste
|
|
4247
|
+
// would.
|
|
4248
|
+
const dropped = parsePastedAttachmentPaths(edit.value)
|
|
4249
|
+
if (dropped.images.length > 0 || dropped.files.length > 0) {
|
|
4250
|
+
valueRef.current = ''
|
|
4251
|
+
cursorRef.current = 0
|
|
4252
|
+
setValue('')
|
|
4253
|
+
setCursor(0)
|
|
4254
|
+
insertDroppedAttachments(dropped.images, dropped.files)
|
|
4255
|
+
}
|
|
3748
4256
|
}
|
|
3749
4257
|
|
|
3750
4258
|
/** Move the cursor without editing; horizontal moves clear the column preference. */
|
|
@@ -3802,7 +4310,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3802
4310
|
prepareAbortRef.current = undefined
|
|
3803
4311
|
setPreparingImages(false)
|
|
3804
4312
|
dismissNotice()
|
|
3805
|
-
notify('
|
|
4313
|
+
notify(t('notice.imageCancelled'), 'warning')
|
|
3806
4314
|
}
|
|
3807
4315
|
|
|
3808
4316
|
/** Cross history while an unchanged recalled draft rests its caret on
|
|
@@ -3877,10 +4385,18 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3877
4385
|
const label = cycleMode()
|
|
3878
4386
|
if (label !== '') notify(label)
|
|
3879
4387
|
} catch (error: unknown) {
|
|
3880
|
-
notify(
|
|
4388
|
+
notify(t('notice.permissionChangeFailed', { message: error instanceof Error ? error.message : String(error) }), 'error')
|
|
3881
4389
|
}
|
|
3882
4390
|
return
|
|
3883
4391
|
}
|
|
4392
|
+
// Tab on an EMPTY composer picks how the next submission is delivered:
|
|
4393
|
+
// queue for the next turn, or steer into the turn already running. With a
|
|
4394
|
+
// draft present Tab stays the completion key (handled with the menu
|
|
4395
|
+
// below), so this only claims the keypress when nothing is being typed.
|
|
4396
|
+
if (key.tab && liveValue === '' && !menuActive) {
|
|
4397
|
+
cycleSubmitMode()
|
|
4398
|
+
return
|
|
4399
|
+
}
|
|
3884
4400
|
// Ctrl+R toggles the thinking display (Claude-Code reasoning fold).
|
|
3885
4401
|
// Alt+R is the zero-config alias: VS Code never intercepts Alt chords,
|
|
3886
4402
|
// so the toggle stays reachable before /vscode-keys has been applied.
|
|
@@ -3926,7 +4442,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3926
4442
|
applyEdit(deleteForward(liveValue, liveCursor))
|
|
3927
4443
|
return
|
|
3928
4444
|
}
|
|
3929
|
-
if (busy) notify('
|
|
4445
|
+
if (busy) notify(t('notice.cancelBeforeExit'), 'warning')
|
|
3930
4446
|
else quit()
|
|
3931
4447
|
return
|
|
3932
4448
|
}
|
|
@@ -3952,7 +4468,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3952
4468
|
const forwardDelete = rawEditorTokens.current?.some(token =>
|
|
3953
4469
|
token.kind === 'delete-forward' || token.kind === 'delete-word-forward') === true
|
|
3954
4470
|
if (forwardDelete) {
|
|
3955
|
-
|
|
4471
|
+
updateQueued?.(queued[queued.length - 1].messageId, { kind: 'remove' })
|
|
3956
4472
|
return
|
|
3957
4473
|
}
|
|
3958
4474
|
}
|
|
@@ -3968,8 +4484,19 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3968
4484
|
// exactly, in which case Enter submits it (typing a full "/effort" and
|
|
3969
4485
|
// pressing return must run the command, not re-accept its own text).
|
|
3970
4486
|
if (menuActive) {
|
|
4487
|
+
// Accepting can be a no-op: the menu is open with no matches yet, the
|
|
4488
|
+
// slash line already spells its candidate, or a mention insertion
|
|
4489
|
+
// repeats the token already in the draft. Enter must reach the submit
|
|
4490
|
+
// path in every one of those cases, or the message cannot be sent.
|
|
4491
|
+
const highlighted = highlightedCompletion()
|
|
4492
|
+
const repeatsToken = mentionActive && mentionToken !== undefined && cursor === liveValue.length
|
|
4493
|
+
&& liveValue.slice(mentionToken.start, cursor) === highlighted
|
|
4494
|
+
// The slash rule is a whole-list exact match, not a highlighted-row
|
|
4495
|
+
// comparison: typing `/mode` once the list is open must run the
|
|
4496
|
+
// command even when the highlight happens to rest on `/model`.
|
|
3971
4497
|
const exactSlash = !mentionActive && candidates.some(candidate => candidate.label === liveValue)
|
|
3972
|
-
|
|
4498
|
+
const acceptNoop = highlighted === '' || repeatsToken || exactSlash
|
|
4499
|
+
if (!acceptNoop) {
|
|
3973
4500
|
acceptMenuCandidate()
|
|
3974
4501
|
return
|
|
3975
4502
|
}
|
|
@@ -3984,7 +4511,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3984
4511
|
// Slash semantics with attachments are unchanged: commands cannot
|
|
3985
4512
|
// carry attachments, so the line goes to the model as a prompt —
|
|
3986
4513
|
// warn instead of surprising the user with a literal "/export".
|
|
3987
|
-
if (isSlashLine(text)) notify('
|
|
4514
|
+
if (isSlashLine(text)) notify(t('notice.commandAttachments'), 'warning')
|
|
3988
4515
|
// Attachment prepares resolve asynchronously; the app remounts onto
|
|
3989
4516
|
// another session in the meantime, and this (old) instance's unmount
|
|
3990
4517
|
// cleanup runs too late on the microtask timeline. Tag the delivery
|
|
@@ -4023,7 +4550,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4023
4550
|
}
|
|
4024
4551
|
recall.current = beginRecall(recallSpace, '')
|
|
4025
4552
|
const blocks: readonly ContentBlock[] = [...images, ...files]
|
|
4026
|
-
if (
|
|
4553
|
+
if (submitMode === 'steer') steer(text, blocks, originSession)
|
|
4027
4554
|
else dispatch(text, blocks, originSession)
|
|
4028
4555
|
}, (reason: unknown) => {
|
|
4029
4556
|
if (controller.signal.aborted || prepareEpochRef.current !== epoch) return
|
|
@@ -4083,7 +4610,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4083
4610
|
if (text === '/copy') {
|
|
4084
4611
|
void copyLastResponse().then(
|
|
4085
4612
|
outcome => notify(outcome),
|
|
4086
|
-
error => notify(
|
|
4613
|
+
error => notify(t('notice.copyFailed', { message: error instanceof Error ? error.message : String(error) }), 'error'),
|
|
4087
4614
|
)
|
|
4088
4615
|
return
|
|
4089
4616
|
}
|
|
@@ -4092,7 +4619,16 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4092
4619
|
return
|
|
4093
4620
|
}
|
|
4094
4621
|
if (text === '/review' || text.startsWith('/review ')) {
|
|
4095
|
-
|
|
4622
|
+
const argument = text.slice(7).trim()
|
|
4623
|
+
if (argument === '') {
|
|
4624
|
+
openReviewPicker()
|
|
4625
|
+
return
|
|
4626
|
+
}
|
|
4627
|
+
try {
|
|
4628
|
+
reviewChanges(parseReviewArgument(argument))
|
|
4629
|
+
} catch (error: unknown) {
|
|
4630
|
+
notify(error instanceof Error ? error.message : String(error), 'warning')
|
|
4631
|
+
}
|
|
4096
4632
|
return
|
|
4097
4633
|
}
|
|
4098
4634
|
if (text === '/model' || text.startsWith('/model ')) {
|
|
@@ -4127,6 +4663,10 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4127
4663
|
else dispatch(text)
|
|
4128
4664
|
return
|
|
4129
4665
|
}
|
|
4666
|
+
if (text === '/search' || text.startsWith('/search ')) {
|
|
4667
|
+
openSearch(text.slice(7).trim())
|
|
4668
|
+
return
|
|
4669
|
+
}
|
|
4130
4670
|
if (text === '/new' || text.startsWith('/new ')) {
|
|
4131
4671
|
createSession(text.slice(4).trim() || undefined)
|
|
4132
4672
|
return
|
|
@@ -4159,17 +4699,41 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4159
4699
|
openTheme()
|
|
4160
4700
|
return
|
|
4161
4701
|
}
|
|
4702
|
+
if (text === '/language' || text.startsWith('/language ')) {
|
|
4703
|
+
const argument = text.slice('/language'.length).trim()
|
|
4704
|
+
if (argument === '') openLanguage()
|
|
4705
|
+
else if (argument === 'en' || argument === 'zh') {
|
|
4706
|
+
saveLanguage(parseLanguageName(argument))
|
|
4707
|
+
notify(t('notice.languageSaved', { name: argument }))
|
|
4708
|
+
refresh()
|
|
4709
|
+
} else notify(t('notice.usage.language'), 'warning')
|
|
4710
|
+
return
|
|
4711
|
+
}
|
|
4162
4712
|
if (text === '/animation' || text.startsWith('/animation ')) {
|
|
4163
4713
|
const parsed = parseAnimationsArgument(text.slice('/animation'.length))
|
|
4164
4714
|
if (parsed === 'toggle') applyAnimations(!animations)
|
|
4165
|
-
else if (parsed === 'usage') notify('usage
|
|
4715
|
+
else if (parsed === 'usage') notify(t('notice.usage.animation'), 'info')
|
|
4166
4716
|
else applyAnimations(parsed.enabled)
|
|
4167
4717
|
return
|
|
4168
4718
|
}
|
|
4719
|
+
if (text === '/rainbow' || text.startsWith('/rainbow ')) {
|
|
4720
|
+
const parsed = parseRainbowArgument(text.slice('/rainbow'.length))
|
|
4721
|
+
if (parsed === 'usage') notify(t('notice.usage.rainbow'), 'warning')
|
|
4722
|
+
else applyRainbow(parsed === 'random' ? undefined : parsed.seed)
|
|
4723
|
+
return
|
|
4724
|
+
}
|
|
4169
4725
|
if (text === '/history') {
|
|
4170
4726
|
openHistory()
|
|
4171
4727
|
return
|
|
4172
4728
|
}
|
|
4729
|
+
if (text === '/queue') {
|
|
4730
|
+
openQueue()
|
|
4731
|
+
return
|
|
4732
|
+
}
|
|
4733
|
+
if (text === '/usage') {
|
|
4734
|
+
openUsage()
|
|
4735
|
+
return
|
|
4736
|
+
}
|
|
4173
4737
|
if (text === '/agents') {
|
|
4174
4738
|
openAgents()
|
|
4175
4739
|
return
|
|
@@ -4193,19 +4757,30 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4193
4757
|
openDelete(text.slice(7).trim())
|
|
4194
4758
|
return
|
|
4195
4759
|
}
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
// Slash lines keep the registry path — commands run out of band.
|
|
4200
|
-
steer(text)
|
|
4760
|
+
const slash = slashNameAndArgs(text)
|
|
4761
|
+
if (slash !== undefined && BARE_LOCAL_COMMANDS.has(slash.name) && slash.args !== '') {
|
|
4762
|
+
notify(t('notice.usage.bareCommand', { name: slash.name }), 'warning')
|
|
4201
4763
|
return
|
|
4202
4764
|
}
|
|
4203
|
-
|
|
4765
|
+
// Delivery mode: everything above this point is a local command or a
|
|
4766
|
+
// panel opener and always runs out of band. A real prompt follows the
|
|
4767
|
+
// composer's Tab choice — `steer` joins the running turn, the default
|
|
4768
|
+
// queues it for the next one.
|
|
4769
|
+
if (submitMode === 'steer') steer(text)
|
|
4770
|
+
else dispatch(text)
|
|
4771
|
+
return
|
|
4772
|
+
}
|
|
4773
|
+
// The modified-Enter newline family: Ctrl+J arrives as a bare LF (Ink
|
|
4774
|
+
// names it 'enter', not 'return'), and the kitty layer normalizes
|
|
4775
|
+
// Ctrl/Shift+Enter to the same byte. Alt+Enter reaches here as a bare CR
|
|
4776
|
+
// with no flags — Ink's parser drops the escape and reports no meta, and
|
|
4777
|
+
// plain Enter always carries key.return — so a flagless CR is Alt+Enter.
|
|
4778
|
+
// Only plain Enter submits. app.spec's "modified-Enter family" test pins
|
|
4779
|
+
// this exact parser shape; an Ink upgrade that changes it fails there.
|
|
4780
|
+
if (input === '\n' || input === '\r') {
|
|
4781
|
+
applyEdit(insertText(liveValue, liveCursor, '\n'))
|
|
4204
4782
|
return
|
|
4205
4783
|
}
|
|
4206
|
-
// Ink exposes Ctrl+J as a bare LF and Alt+Enter as a bare CR after
|
|
4207
|
-
// stripping the leading escape. Neither is a multiline shortcut.
|
|
4208
|
-
if (input === '\n' || input === '\r') return
|
|
4209
4784
|
// A fast Tab followed by text can arrive as one readable chunk in an
|
|
4210
4785
|
// integrated terminal. Accept the candidate first, then apply the
|
|
4211
4786
|
// remaining characters against the synchronously updated editor refs.
|
|
@@ -4216,11 +4791,11 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4216
4791
|
return
|
|
4217
4792
|
}
|
|
4218
4793
|
if (menuActive && key.upArrow) {
|
|
4219
|
-
setCompletionIndex(index => (index
|
|
4794
|
+
setCompletionIndex(index => stepCompletionIndex(index, -1, menuRows.length))
|
|
4220
4795
|
return
|
|
4221
4796
|
}
|
|
4222
4797
|
if (menuActive && key.downArrow) {
|
|
4223
|
-
setCompletionIndex(index => (index
|
|
4798
|
+
setCompletionIndex(index => stepCompletionIndex(index, 1, menuRows.length))
|
|
4224
4799
|
return
|
|
4225
4800
|
}
|
|
4226
4801
|
// Batched Home/End/Delete/Backspace sequences bypass Ink's one-key parser
|
|
@@ -4313,10 +4888,13 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4313
4888
|
return
|
|
4314
4889
|
}
|
|
4315
4890
|
if (input !== '' && !key.ctrl && !key.meta) {
|
|
4316
|
-
// Bracketed-paste wrappers arrive as
|
|
4317
|
-
//
|
|
4318
|
-
//
|
|
4319
|
-
//
|
|
4891
|
+
// Bracketed-paste wrappers arrive as escape sequences Ink has stripped
|
|
4892
|
+
// only ONE leading ESC from, so the tail marker still carries its own:
|
|
4893
|
+
// strip both spellings before the payload is read, or a dragged path
|
|
4894
|
+
// reaches the attachment parser with a trailing control byte and fails.
|
|
4895
|
+
// Markers may ride their own chunk or the edges of a content chunk; the
|
|
4896
|
+
// open-paste flag is tracked so a chunk that is exactly LF inserts
|
|
4897
|
+
// instead of submitting.
|
|
4320
4898
|
let text = input
|
|
4321
4899
|
if (text.includes(PASTE_START_MARKER)) {
|
|
4322
4900
|
pasteBracketRef.current = true
|
|
@@ -4331,13 +4909,12 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4331
4909
|
clearTimeout(timer)
|
|
4332
4910
|
pasteBracketCancelRef.current = undefined
|
|
4333
4911
|
}
|
|
4334
|
-
text = text.replaceAll(PASTE_START_MARKER, '')
|
|
4335
4912
|
}
|
|
4336
4913
|
if (text.includes(PASTE_END_MARKER)) {
|
|
4337
4914
|
pasteBracketRef.current = false
|
|
4338
4915
|
pasteBracketCancelRef.current?.()
|
|
4339
|
-
text = text.replaceAll(PASTE_END_MARKER, '')
|
|
4340
4916
|
}
|
|
4917
|
+
text = stripPasteMarkers(text)
|
|
4341
4918
|
if (text === '') return
|
|
4342
4919
|
if (text.length > 1) {
|
|
4343
4920
|
// A path-list paste splits into images and files; prose falls through
|
|
@@ -4377,13 +4954,23 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4377
4954
|
if (!animations && waveKey !== null && waveKey !== wavePlayedKey) setWavePlayedKey(waveKey)
|
|
4378
4955
|
}, [animations, waveKey, wavePlayedKey])
|
|
4379
4956
|
const waveArmed = waveKey !== null && waveKey !== wavePlayedKey
|
|
4957
|
+
const burstKey = rainbowBurstId > 0 ? `rainbow:${rainbowBurstId}` : null
|
|
4958
|
+
const [burstPlayedKey, setBurstPlayedKey] = useState<string | null>(null)
|
|
4959
|
+
useEffect(() => {
|
|
4960
|
+
if (!animations && burstKey !== null && burstKey !== burstPlayedKey) setBurstPlayedKey(burstKey)
|
|
4961
|
+
}, [animations, burstKey, burstPlayedKey])
|
|
4962
|
+
const burstArmed = burstKey !== null && burstKey !== burstPlayedKey
|
|
4380
4963
|
|
|
4381
4964
|
// Every exclusive panel keeps the composer as a stable visual anchor, but
|
|
4382
4965
|
// freezes it to one row: no menu, multiline wrap, or animation.
|
|
4383
4966
|
const tierActive = waveTier !== null
|
|
4384
4967
|
const tierHues = waveTier === null ? null : deepseekWaveHues(waveTier)
|
|
4385
4968
|
const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0])
|
|
4386
|
-
const
|
|
4969
|
+
const waveGlyph = waveTier === 'flash' ? '›' : waveTier === 'deepseek' ? '»' : '❯'
|
|
4970
|
+
// Steer mode owns the prompt glyph in every paint path (static band, wave,
|
|
4971
|
+
// rainbow burst), so the mode is visible without reading the placeholder.
|
|
4972
|
+
const promptGlyph = submitMode === 'steer' ? '↳' : waveGlyph
|
|
4973
|
+
const placeholderText = composerPlaceholder(submitMode)
|
|
4387
4974
|
// The multiline editor model: the sanitized draft hard-wrapped into
|
|
4388
4975
|
// column-safe physical rows, with the caret mapped to its exact row and
|
|
4389
4976
|
// column. Computed before the frozen path so the row report below runs
|
|
@@ -4421,7 +5008,9 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4421
5008
|
// the reserve from outliving the menu (unmount or inactive handoff).
|
|
4422
5009
|
useEffect(() => {
|
|
4423
5010
|
onMenuRows(menuHeightRows)
|
|
4424
|
-
return () =>
|
|
5011
|
+
return () => {
|
|
5012
|
+
if (menuHeightRows !== 0) onMenuRows(0)
|
|
5013
|
+
}
|
|
4425
5014
|
}, [menuHeightRows, onMenuRows])
|
|
4426
5015
|
// The composer band: the old border's three-row footprint repainted as a
|
|
4427
5016
|
// background-color band (the Codex-style shaded composer strip) — one
|
|
@@ -4457,7 +5046,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4457
5046
|
return band(createElement(
|
|
4458
5047
|
Text,
|
|
4459
5048
|
{ backgroundColor: bandBg, wrap: 'truncate-end' },
|
|
4460
|
-
createElement(Text, { color: promptColor, bold: tierActive ? true : undefined }, busy ? '… ' : `${promptGlyph} `),
|
|
5049
|
+
createElement(Text, { color: promptColor, bold: tierActive ? true : undefined }, submitMode === 'steer' ? '↳ ' : busy ? '… ' : `${promptGlyph} `),
|
|
4461
5050
|
frozenLine,
|
|
4462
5051
|
bandFill(2 + visibleColumns(frozenLine)),
|
|
4463
5052
|
))
|
|
@@ -4475,10 +5064,10 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4475
5064
|
// spacer or a second blink timer.
|
|
4476
5065
|
const editorRows: ReactElement[] = []
|
|
4477
5066
|
for (let index = editorWindowStart; index < Math.min(editorViewModel.rows.length, editorWindowStart + editorWindowRows); index += 1) {
|
|
4478
|
-
const row = editorViewModel.rows[index]
|
|
5067
|
+
const row = editorViewModel.rows[index]
|
|
4479
5068
|
const parts = editorRowParts(row, index, caret.row, clampedCursor, !preparingImages)
|
|
4480
5069
|
const placeholder = index === 0 && value === '' && !busy && !preparingImages
|
|
4481
|
-
const tail = placeholder ?
|
|
5070
|
+
const tail = placeholder ? placeholderText : parts.after
|
|
4482
5071
|
const consumed = 2 + visibleColumns(parts.before) + visibleColumns(parts.caret) + visibleColumns(tail)
|
|
4483
5072
|
editorRows.push(createElement(
|
|
4484
5073
|
Text,
|
|
@@ -4486,16 +5075,18 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4486
5075
|
index === 0
|
|
4487
5076
|
? preparingImages
|
|
4488
5077
|
? createElement(Text, { color: inkColor(getPalette().warn), bold: true }, '… ')
|
|
4489
|
-
:
|
|
4490
|
-
? createElement(
|
|
4491
|
-
:
|
|
5078
|
+
: submitMode === 'steer'
|
|
5079
|
+
? createElement(Text, { color: promptColor, bold: true }, '↳ ')
|
|
5080
|
+
: busy
|
|
5081
|
+
? createElement(BusyChase, { animated: animations })
|
|
5082
|
+
: createElement(Text, { color: promptColor, bold: tierActive ? true : undefined }, `${promptGlyph} `)
|
|
4492
5083
|
: ' ',
|
|
4493
5084
|
parts.before,
|
|
4494
5085
|
parts.hasCaret
|
|
4495
5086
|
? createElement(Text, { key: 'caret', inverse: cursorVisible || undefined }, parts.caret)
|
|
4496
5087
|
: null,
|
|
4497
5088
|
placeholder
|
|
4498
|
-
? createElement(Text, { dimColor: true },
|
|
5089
|
+
? createElement(Text, { dimColor: true }, tail)
|
|
4499
5090
|
: parts.after,
|
|
4500
5091
|
bandFill(consumed),
|
|
4501
5092
|
))
|
|
@@ -4511,26 +5102,47 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4511
5102
|
Box,
|
|
4512
5103
|
{ flexDirection: 'column' },
|
|
4513
5104
|
menu,
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
5105
|
+
burstArmed
|
|
5106
|
+
? createElement(ComposerRainbowBurst, {
|
|
5107
|
+
key: burstKey,
|
|
5108
|
+
active: !busy && !preparingImages && animations,
|
|
5109
|
+
onSettled: () => {
|
|
5110
|
+
if (burstKey !== null) setBurstPlayedKey(burstKey)
|
|
5111
|
+
},
|
|
5112
|
+
fallback: band(staticEditor),
|
|
5113
|
+
bandWidth,
|
|
5114
|
+
bandBg,
|
|
5115
|
+
rows: editorViewModel.rows.slice(editorWindowStart, editorWindowStart + editorWindowRows),
|
|
5116
|
+
windowStart: editorWindowStart,
|
|
5117
|
+
caretRow: caret.row,
|
|
5118
|
+
cursor: clampedCursor,
|
|
5119
|
+
caretVisible: cursorVisible,
|
|
5120
|
+
value,
|
|
5121
|
+
promptGlyph,
|
|
5122
|
+
placeholder: placeholderText,
|
|
5123
|
+
promptColor,
|
|
5124
|
+
})
|
|
5125
|
+
: createElement(ComposerWave, {
|
|
5126
|
+
key: waveKey ?? 'static',
|
|
5127
|
+
tier: waveTier ?? 'deepseek',
|
|
5128
|
+
style: waveStyle ?? 'wave',
|
|
5129
|
+
active: waveTier !== null && waveStyle !== null && !busy && !preparingImages && animations && waveArmed,
|
|
5130
|
+
onSettled: () => {
|
|
5131
|
+
if (waveKey !== null) setWavePlayedKey(waveKey)
|
|
5132
|
+
},
|
|
5133
|
+
fallback: band(staticEditor),
|
|
5134
|
+
bandWidth,
|
|
5135
|
+
bandBg,
|
|
5136
|
+
rows: editorViewModel.rows.slice(editorWindowStart, editorWindowStart + editorWindowRows),
|
|
5137
|
+
windowStart: editorWindowStart,
|
|
5138
|
+
caretRow: caret.row,
|
|
5139
|
+
cursor: clampedCursor,
|
|
5140
|
+
caretVisible: cursorVisible,
|
|
5141
|
+
value,
|
|
5142
|
+
promptGlyph,
|
|
5143
|
+
placeholder: placeholderText,
|
|
5144
|
+
promptColor,
|
|
5145
|
+
}),
|
|
4534
5146
|
)
|
|
4535
5147
|
}
|
|
4536
5148
|
|
|
@@ -4755,7 +5367,13 @@ export function computeSettledRows(
|
|
|
4755
5367
|
|
|
4756
5368
|
/** The whole terminal app; state arrives via the store, output via Ink. */
|
|
4757
5369
|
export function App(props: AppProps): ReactElement {
|
|
4758
|
-
|
|
5370
|
+
// The stores are closure-backed singletons whose methods never touch `this`,
|
|
5371
|
+
// but a bare method reference still detaches it from its receiver. One stable
|
|
5372
|
+
// wrapper per store keeps both the receiver and the reference identity the
|
|
5373
|
+
// `useSyncExternalStore` contract requires.
|
|
5374
|
+
const subscribeTranscript = useCallback((listener: () => void) => props.store.subscribe(listener), [props.store])
|
|
5375
|
+
const readTranscript = useCallback(() => props.store.getView(), [props.store])
|
|
5376
|
+
const view = useSyncExternalStore(subscribeTranscript, readTranscript)
|
|
4759
5377
|
// Terminal input anchor: Ink reference-counts raw mode across every active
|
|
4760
5378
|
// `useInput` hook, so mutually exclusive surfaces (composer <-> approval
|
|
4761
5379
|
// bar <-> panels) drop the count to zero inside each handoff commit — the
|
|
@@ -4777,8 +5395,10 @@ export function App(props: AppProps): ReactElement {
|
|
|
4777
5395
|
// process-stable, so one callback per view identity is enough.
|
|
4778
5396
|
const readDescriptors = useCallback(() => props.commands.descriptors, [props.commands])
|
|
4779
5397
|
const readSkills = useCallback(() => props.skills.rows, [props.skills])
|
|
4780
|
-
const
|
|
4781
|
-
const
|
|
5398
|
+
const subscribeCommands = useCallback((listener: () => void) => props.commands.subscribe(listener), [props.commands])
|
|
5399
|
+
const subscribeSkills = useCallback((listener: () => void) => props.skills.subscribe(listener), [props.skills])
|
|
5400
|
+
const descriptors = useSyncExternalStore(subscribeCommands, readDescriptors)
|
|
5401
|
+
const skills = useSyncExternalStore(subscribeSkills, readSkills)
|
|
4782
5402
|
const [modelLabel, setModelLabel] = useState(props.model)
|
|
4783
5403
|
const [modelOpen, setModelOpen] = useState(false)
|
|
4784
5404
|
/** Nested /model stages; only one owns terminal input at a time. */
|
|
@@ -4807,13 +5427,17 @@ export function App(props: AppProps): ReactElement {
|
|
|
4807
5427
|
* (ordinary turns, image preparation, /animation toggles) never replays. */
|
|
4808
5428
|
const [waveTier, setWaveTier] = useState<DeepseekWaveTier | null>(null)
|
|
4809
5429
|
const [waveStyle, setWaveStyle] = useState<DeepseekWaveStyle | null>(null)
|
|
5430
|
+
const [rainbowBurstId, setRainbowBurstId] = useState(0)
|
|
5431
|
+
const fireRainbowBurst = (): void => {
|
|
5432
|
+
setRainbowBurstId(id => id + 1)
|
|
5433
|
+
}
|
|
4810
5434
|
// /animation toggle: applies immediately, persists through the runner, and
|
|
4811
5435
|
// gates every timed leaf (shimmer, chase, blink, wave) for this render.
|
|
4812
5436
|
const [animations, setAnimations] = useState(props.animations ?? true)
|
|
4813
5437
|
const applyAnimations = (enabled: boolean): void => {
|
|
4814
5438
|
setAnimations(enabled)
|
|
4815
5439
|
props.saveAnimations?.(enabled)
|
|
4816
|
-
notify(
|
|
5440
|
+
notify(t('notice.animationState', { state: enabled ? 'on' : 'off' }))
|
|
4817
5441
|
}
|
|
4818
5442
|
const previousModel = useRef<string | undefined>(undefined)
|
|
4819
5443
|
const previousEffort = useRef<string | undefined>(props.effort)
|
|
@@ -4851,14 +5475,22 @@ export function App(props: AppProps): ReactElement {
|
|
|
4851
5475
|
const [authorizationDirectory, setAuthorizationDirectory] = useState<ProviderAuthorizationDirectory | undefined>(undefined)
|
|
4852
5476
|
const [authorizationError, setAuthorizationError] = useState<string | undefined>(undefined)
|
|
4853
5477
|
const [modelLoadEpoch, setModelLoadEpoch] = useState(0)
|
|
5478
|
+
/** Bumped when /effort's catalog lookup must be ignored (panel closed or superseded). */
|
|
5479
|
+
const effortLookupEpoch = useRef(0)
|
|
4854
5480
|
const [notice, setNotice] = useState<{ text: string; tone: NoticeTone } | undefined>(undefined)
|
|
4855
5481
|
const notify = useCallback((text: string, tone: NoticeTone = 'info'): void => {
|
|
4856
5482
|
setNotice({ text, tone })
|
|
4857
5483
|
}, [])
|
|
4858
5484
|
|
|
5485
|
+
const {
|
|
5486
|
+
loadModels,
|
|
5487
|
+
loadModelProviders,
|
|
5488
|
+
loadProviderAuthorizations,
|
|
5489
|
+
onBridgeReady,
|
|
5490
|
+
} = props
|
|
4859
5491
|
useEffect(() => {
|
|
4860
|
-
|
|
4861
|
-
}, [])
|
|
5492
|
+
onBridgeReady({ notify })
|
|
5493
|
+
}, [notify, onBridgeReady])
|
|
4862
5494
|
useEffect(() => {
|
|
4863
5495
|
if (!modelOpen) return
|
|
4864
5496
|
let cancelled = false
|
|
@@ -4867,7 +5499,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
4867
5499
|
// Enter the promise chain before invoking the loader so a provider that
|
|
4868
5500
|
// throws synchronously becomes an in-panel error instead of escaping the
|
|
4869
5501
|
// React effect and tearing down Ink.
|
|
4870
|
-
Promise.resolve().then(() =>
|
|
5502
|
+
Promise.resolve().then(() => loadModels()).then((loaded) => {
|
|
4871
5503
|
if (!cancelled) setDirectory(loaded)
|
|
4872
5504
|
}, (error: unknown) => {
|
|
4873
5505
|
if (!cancelled) setModelError(error instanceof Error ? error.message : String(error))
|
|
@@ -4875,13 +5507,13 @@ export function App(props: AppProps): ReactElement {
|
|
|
4875
5507
|
return () => {
|
|
4876
5508
|
cancelled = true
|
|
4877
5509
|
}
|
|
4878
|
-
}, [modelOpen, modelLoadEpoch
|
|
5510
|
+
}, [loadModels, modelOpen, modelLoadEpoch])
|
|
4879
5511
|
useEffect(() => {
|
|
4880
|
-
if (!modelOpen ||
|
|
5512
|
+
if (!modelOpen || loadModelProviders === undefined) return
|
|
4881
5513
|
let cancelled = false
|
|
4882
5514
|
setProviderDirectory(undefined)
|
|
4883
5515
|
setProviderError(undefined)
|
|
4884
|
-
Promise.resolve().then(() =>
|
|
5516
|
+
Promise.resolve().then(() => loadModelProviders()).then((loaded) => {
|
|
4885
5517
|
if (!cancelled) setProviderDirectory(loaded)
|
|
4886
5518
|
}, (error: unknown) => {
|
|
4887
5519
|
if (!cancelled) setProviderError(error instanceof Error ? error.message : String(error))
|
|
@@ -4889,13 +5521,13 @@ export function App(props: AppProps): ReactElement {
|
|
|
4889
5521
|
return () => {
|
|
4890
5522
|
cancelled = true
|
|
4891
5523
|
}
|
|
4892
|
-
}, [modelOpen, modelLoadEpoch
|
|
5524
|
+
}, [loadModelProviders, modelOpen, modelLoadEpoch])
|
|
4893
5525
|
useEffect(() => {
|
|
4894
|
-
if (!modelOpen ||
|
|
5526
|
+
if (!modelOpen || loadProviderAuthorizations === undefined) return
|
|
4895
5527
|
let cancelled = false
|
|
4896
5528
|
setAuthorizationDirectory(undefined)
|
|
4897
5529
|
setAuthorizationError(undefined)
|
|
4898
|
-
Promise.resolve().then(() =>
|
|
5530
|
+
Promise.resolve().then(() => loadProviderAuthorizations()).then((loaded) => {
|
|
4899
5531
|
if (!cancelled) setAuthorizationDirectory(loaded)
|
|
4900
5532
|
}, (error: unknown) => {
|
|
4901
5533
|
if (!cancelled) setAuthorizationError(error instanceof Error ? error.message : String(error))
|
|
@@ -4903,7 +5535,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
4903
5535
|
return () => {
|
|
4904
5536
|
cancelled = true
|
|
4905
5537
|
}
|
|
4906
|
-
}, [modelOpen, modelLoadEpoch
|
|
5538
|
+
}, [loadProviderAuthorizations, modelOpen, modelLoadEpoch])
|
|
4907
5539
|
useEffect(() => {
|
|
4908
5540
|
const subscribe = props.subscribeModelProviders
|
|
4909
5541
|
if (!modelOpen || subscribe === undefined) return
|
|
@@ -4928,24 +5560,39 @@ export function App(props: AppProps): ReactElement {
|
|
|
4928
5560
|
// Dedupe for the dynamic-budget tripwire: one warning per distinct shape.
|
|
4929
5561
|
const budgetWarnRef = useRef<string | undefined>(undefined)
|
|
4930
5562
|
const [verboseOpen, setVerboseOpen] = useState(false)
|
|
5563
|
+
const [queueOpen, setQueueOpen] = useState(false)
|
|
5564
|
+
/**
|
|
5565
|
+
* How the composer delivers its next submission: `queue` waits for the next
|
|
5566
|
+
* turn, `steer` joins the turn already running. Tab on an empty composer
|
|
5567
|
+
* flips it; the prompt glyph and the placeholder both name the current mode.
|
|
5568
|
+
*/
|
|
5569
|
+
const [submitMode, setSubmitMode] = useState<'queue' | 'steer'>('queue')
|
|
4931
5570
|
const [diffView, setDiffView] = useState<GitDiffView | undefined>(undefined)
|
|
5571
|
+
const [reviewPickerOpen, setReviewPickerOpen] = useState(false)
|
|
4932
5572
|
const [helpOpen, setHelpOpen] = useState(false)
|
|
4933
5573
|
const [modeOpen, setModeOpen] = useState(false)
|
|
4934
5574
|
const [permissionOpen, setPermissionOpen] = useState(false)
|
|
4935
5575
|
const [resumeOpen, setResumeOpen] = useState(false)
|
|
5576
|
+
const [searchOpen, setSearchOpen] = useState(false)
|
|
5577
|
+
/** /search seed: the query from `/search <text>` (cleared on open). */
|
|
5578
|
+
const [searchSeed, setSearchSeed] = useState('')
|
|
4936
5579
|
const [pluginOpen, setPluginOpen] = useState(false)
|
|
4937
5580
|
const [pluginQuery, setPluginQuery] = useState('')
|
|
4938
5581
|
const [updateOpen, setUpdateOpen] = useState(false)
|
|
5582
|
+
const [updateApplying, setUpdateApplying] = useState(false)
|
|
5583
|
+
useEffect(() => subscribeUpdateApplyRunning(setUpdateApplying), [])
|
|
4939
5584
|
const [scheduleOpen, setScheduleOpen] = useState(false)
|
|
4940
5585
|
const [jobsOpen, setJobsOpen] = useState(false)
|
|
4941
5586
|
const [statuslineOpen, setStatuslineOpen] = useState(false)
|
|
4942
5587
|
const [statuslineItems, setStatuslineItems] = useState<readonly StatusItemId[]>(() => parseStatuslineItems(props.statusline))
|
|
4943
5588
|
const [themeOpen, setThemeOpen] = useState(false)
|
|
5589
|
+
const [languageOpen, setLanguageOpen] = useState(false)
|
|
4944
5590
|
const [historyOpen, setHistoryOpen] = useState(false)
|
|
4945
5591
|
const [agentsOpen, setAgentsOpen] = useState(false)
|
|
4946
5592
|
const [subagentOpen, setSubagentOpen] = useState(false)
|
|
4947
5593
|
const [todosOpen, setTodosOpen] = useState(false)
|
|
4948
|
-
|
|
5594
|
+
const [usageOpen, setUsageOpen] = useState(false)
|
|
5595
|
+
/** /delete state: dedicated picker mode plus an optional pre-armed row id. */
|
|
4949
5596
|
const [resumeDelete, setResumeDelete] = useState<{ mode: boolean; id?: string }>({ mode: false })
|
|
4950
5597
|
/** The row id awaiting y/n in the COMPOSER (codex delete confirm): the
|
|
4951
5598
|
* composer takes the keys, the resume panel yields until it settles. */
|
|
@@ -4958,19 +5605,20 @@ export function App(props: AppProps): ReactElement {
|
|
|
4958
5605
|
const cancelDelete = useCallback((): void => {
|
|
4959
5606
|
setDeleteConfirmId(undefined)
|
|
4960
5607
|
}, [])
|
|
5608
|
+
const deleteSession = props.deleteSession
|
|
4961
5609
|
const confirmDelete = useCallback((): void => {
|
|
4962
5610
|
const id = deleteConfirmId
|
|
4963
5611
|
if (id === undefined) return
|
|
4964
5612
|
setDeleteConfirmId(undefined)
|
|
4965
|
-
void
|
|
5613
|
+
void deleteSession(id).then(outcome => {
|
|
4966
5614
|
notify(outcome)
|
|
4967
5615
|
// Keep the picker open and reload: a successful deletion must vanish
|
|
4968
5616
|
// from the list immediately, not look like a no-op.
|
|
4969
5617
|
setDeleteReloadToken(token => token + 1)
|
|
4970
5618
|
}, (reason: unknown) => {
|
|
4971
|
-
notify(
|
|
5619
|
+
notify(t('notice.deleteFailed', { message: reason instanceof Error ? reason.message : String(reason) }), 'error')
|
|
4972
5620
|
})
|
|
4973
|
-
}, [
|
|
5621
|
+
}, [deleteSession, deleteConfirmId, notify])
|
|
4974
5622
|
/** The /history panel's accepted entry: text plus its recall-space index. */
|
|
4975
5623
|
const [historyFill, setHistoryFill] = useState<{ text: string; index: number } | undefined>(undefined)
|
|
4976
5624
|
/** Submissions recorded in this process (Codex local history; persistent file stays in the runner). */
|
|
@@ -4988,60 +5636,90 @@ export function App(props: AppProps): ReactElement {
|
|
|
4988
5636
|
}, [])
|
|
4989
5637
|
/** The append-only flush boundary (see `settledEntryCount`): entries below
|
|
4990
5638
|
* this index are final and ride the `<Static>` scrollback; everything at or
|
|
4991
|
-
* beyond stays in the live tree.
|
|
4992
|
-
* index >= settled, so the queued-inbox scan below only walks the mutable
|
|
4993
|
-
* tail instead of the whole history. */
|
|
5639
|
+
* beyond stays in the live tree. */
|
|
4994
5640
|
const settled = useMemo(() => settledEntryCount(view.entries), [view.entries])
|
|
4995
|
-
/**
|
|
4996
|
-
*
|
|
4997
|
-
*
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
const entry = view.entries[index]
|
|
5003
|
-
if (entry.kind === 'pending') rows.push(entry)
|
|
5004
|
-
}
|
|
5005
|
-
return rows
|
|
5006
|
-
}, [view.entries, settled])
|
|
5641
|
+
/** Next-turn rows in durable inbox order: a running tool row can split the
|
|
5642
|
+
* pending rows, so this maps the inbox id list onto the folded entries
|
|
5643
|
+
* instead of scanning the mutable tail. */
|
|
5644
|
+
const queuedRows = useMemo(
|
|
5645
|
+
() => queuedInboxRows(view.entries, view.pending['next-turn']),
|
|
5646
|
+
[view.entries, view.pending],
|
|
5647
|
+
)
|
|
5007
5648
|
const [refreshEpoch, setRefreshEpoch] = useState(0)
|
|
5008
|
-
const
|
|
5009
|
-
const
|
|
5010
|
-
const
|
|
5649
|
+
const subscribeApproval = useCallback((listener: () => void) => props.approval.subscribe(listener), [props.approval])
|
|
5650
|
+
const readApprovalSnapshot = useCallback(() => props.approval.getSnapshot(), [props.approval])
|
|
5651
|
+
const subscribeQuestions = useCallback((listener: () => void) => props.questions.subscribe(listener), [props.questions])
|
|
5652
|
+
const readQuestionSnapshot = useCallback(() => props.questions.getSnapshot(), [props.questions])
|
|
5653
|
+
const subscribeSubagents = useCallback((listener: () => void) => props.subagents.subscribe(listener), [props.subagents])
|
|
5654
|
+
const readAgentRows = useCallback(() => props.subagents.getSnapshot(), [props.subagents])
|
|
5655
|
+
const approvalSnapshot = useSyncExternalStore(subscribeApproval, readApprovalSnapshot)
|
|
5656
|
+
const questionSnapshot = useSyncExternalStore(subscribeQuestions, readQuestionSnapshot)
|
|
5657
|
+
const agentRows = useSyncExternalStore(subscribeSubagents, readAgentRows)
|
|
5011
5658
|
const approvalPending = approvalSnapshot.pending !== undefined
|
|
5012
5659
|
const questionPending = questionSnapshot.pending !== undefined
|
|
5013
|
-
|
|
5660
|
+
/**
|
|
5661
|
+
* Every keyboard-owning surface that is mutually exclusive with the composer,
|
|
5662
|
+
* in precedence order. This ONE list drives the composer gate, the transcript
|
|
5663
|
+
* visibility, the frozen band's hint, and the hand-off when a human approval
|
|
5664
|
+
* or question arrives, so a panel cannot be wired into one of them and
|
|
5665
|
+
* forgotten in the others.
|
|
5666
|
+
*/
|
|
5667
|
+
const panelSurfaces: readonly { readonly hint: string; readonly open: boolean; readonly close: () => void }[] = [
|
|
5668
|
+
{ hint: 'the diff review', open: diffView !== undefined, close: () => setDiffView(undefined) },
|
|
5669
|
+
{ hint: 'the review picker', open: reviewPickerOpen, close: () => setReviewPickerOpen(false) },
|
|
5670
|
+
{
|
|
5671
|
+
hint: '/model',
|
|
5672
|
+
open: modelOpen,
|
|
5673
|
+
close: () => {
|
|
5674
|
+
setModelOpen(false)
|
|
5675
|
+
setProviderOpen(false)
|
|
5676
|
+
setProviderAction(undefined)
|
|
5677
|
+
setEffortFor(undefined)
|
|
5678
|
+
},
|
|
5679
|
+
},
|
|
5680
|
+
{ hint: '/help', open: helpOpen, close: () => setHelpOpen(false) },
|
|
5681
|
+
{ hint: '/mode', open: modeOpen, close: () => setModeOpen(false) },
|
|
5682
|
+
{ hint: '/permission', open: permissionOpen, close: () => setPermissionOpen(false) },
|
|
5683
|
+
{ hint: '/resume', open: resumeOpen, close: () => setResumeOpen(false) },
|
|
5684
|
+
{ hint: '/search', open: searchOpen, close: () => setSearchOpen(false) },
|
|
5685
|
+
{ hint: '/plugin', open: pluginOpen, close: () => setPluginOpen(false) },
|
|
5686
|
+
{ hint: '/update', open: updateOpen, close: () => setUpdateOpen(false) },
|
|
5687
|
+
{ hint: '/schedule', open: scheduleOpen, close: () => setScheduleOpen(false) },
|
|
5688
|
+
{ hint: '/jobs', open: jobsOpen, close: () => setJobsOpen(false) },
|
|
5689
|
+
{ hint: '/statusline', open: statuslineOpen, close: () => setStatuslineOpen(false) },
|
|
5690
|
+
{ hint: '/theme', open: themeOpen, close: () => setThemeOpen(false) },
|
|
5691
|
+
{ hint: '/language', open: languageOpen, close: () => setLanguageOpen(false) },
|
|
5692
|
+
{ hint: '/history', open: historyOpen, close: () => setHistoryOpen(false) },
|
|
5693
|
+
{ hint: '/queue', open: queueOpen, close: () => setQueueOpen(false) },
|
|
5694
|
+
{ hint: '/agents', open: agentsOpen, close: () => setAgentsOpen(false) },
|
|
5695
|
+
{ hint: '/subagent', open: subagentOpen, close: () => setSubagentOpen(false) },
|
|
5696
|
+
{ hint: '/todos', open: todosOpen, close: () => setTodosOpen(false) },
|
|
5697
|
+
{ hint: '/usage', open: usageOpen, close: () => setUsageOpen(false) },
|
|
5698
|
+
]
|
|
5699
|
+
const panelSurfacesRef = useRef(panelSurfaces)
|
|
5700
|
+
panelSurfacesRef.current = panelSurfaces
|
|
5701
|
+
const openPanel = panelSurfaces.find(surface => surface.open)
|
|
5702
|
+
// The Ctrl+O inspector is the one surface the composer already yields to
|
|
5703
|
+
// through verboseOpen; it rides the same gate without a panel row.
|
|
5704
|
+
const inspectorVisible = verboseOpen && !approvalPending && !questionPending
|
|
5705
|
+
const modalVisible = openPanel !== undefined || inspectorVisible || approvalPending || questionPending
|
|
5014
5706
|
// While a deletion waits for y/n, the composer takes the keys (the resume
|
|
5015
5707
|
// panel yields): the confirm is typed IN the input box, not as an invisible
|
|
5016
5708
|
// panel keypress.
|
|
5017
5709
|
const inputActive = deleteConfirmId !== undefined
|
|
5018
5710
|
? !approvalPending && !questionPending
|
|
5019
|
-
: !
|
|
5711
|
+
: !modalVisible
|
|
5712
|
+
const transcriptVisible = !modalVisible
|
|
5020
5713
|
|
|
5021
|
-
// Human questions outrank local inspectors. Close
|
|
5022
|
-
// of leaving
|
|
5714
|
+
// Human questions outrank local inspectors. Close every open surface instead
|
|
5715
|
+
// of leaving it visible but keyboard-locked behind the approval.
|
|
5023
5716
|
useEffect(() => {
|
|
5024
5717
|
if (!approvalPending && !questionPending) return
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
setEffortFor(undefined)
|
|
5029
|
-
setHelpOpen(false)
|
|
5030
|
-
setModeOpen(false)
|
|
5031
|
-
setPermissionOpen(false)
|
|
5032
|
-
setResumeOpen(false)
|
|
5033
|
-
setPluginOpen(false)
|
|
5034
|
-
setUpdateOpen(false)
|
|
5035
|
-
setScheduleOpen(false)
|
|
5036
|
-
setStatuslineOpen(false)
|
|
5037
|
-
setThemeOpen(false)
|
|
5038
|
-
setHistoryOpen(false)
|
|
5039
|
-
setAgentsOpen(false)
|
|
5040
|
-
setSubagentOpen(false)
|
|
5041
|
-
setTodosOpen(false)
|
|
5718
|
+
for (const surface of panelSurfacesRef.current) {
|
|
5719
|
+
if (surface.open) surface.close()
|
|
5720
|
+
}
|
|
5042
5721
|
setDeleteConfirmId(undefined)
|
|
5043
5722
|
setVerboseOpen(false)
|
|
5044
|
-
setDiffView(undefined)
|
|
5045
5723
|
}, [approvalPending, questionPending])
|
|
5046
5724
|
|
|
5047
5725
|
// Append-only transcript: everything up to the first still-mutable entry
|
|
@@ -5085,6 +5763,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5085
5763
|
// One pending synchronized frame covers a debounced resize or explicit
|
|
5086
5764
|
// source-backed replay. It is closed after the corresponding React commit.
|
|
5087
5765
|
const synchronizedReplayPending = useRef(false)
|
|
5766
|
+
const resizeBurstHeld = useRef(false)
|
|
5088
5767
|
useEffect(() => {
|
|
5089
5768
|
if (appStdout === undefined) return
|
|
5090
5769
|
let replayTimer: ReturnType<typeof setTimeout> | undefined
|
|
@@ -5096,24 +5775,31 @@ export function App(props: AppProps): ReactElement {
|
|
|
5096
5775
|
if (next.columns === terminalSizeRef.current.columns && next.rows === terminalSizeRef.current.rows) return
|
|
5097
5776
|
terminalSizeRef.current = next
|
|
5098
5777
|
|
|
5099
|
-
//
|
|
5100
|
-
//
|
|
5101
|
-
//
|
|
5102
|
-
//
|
|
5103
|
-
|
|
5104
|
-
|
|
5778
|
+
// Hold the visible frame for the whole burst so intermediate Ink
|
|
5779
|
+
// relayouts (new width against still-old Static rows) never flash as
|
|
5780
|
+
// doubled borders. One clear + Static remount still runs after the
|
|
5781
|
+
// burst settles.
|
|
5782
|
+
if (!resizeBurstHeld.current) {
|
|
5783
|
+
resizeBurstHeld.current = true
|
|
5784
|
+
appStdout.write(SYNCHRONIZED_UPDATE_BEGIN)
|
|
5785
|
+
}
|
|
5105
5786
|
setTerminalSize(next)
|
|
5106
5787
|
if (replayTimer !== undefined) clearTimeout(replayTimer)
|
|
5107
5788
|
replayTimer = setTimeout(() => {
|
|
5108
5789
|
synchronizedReplayPending.current = true
|
|
5109
|
-
appStdout.write(
|
|
5790
|
+
appStdout.write(RESIZE_REFLOW_CLEAR)
|
|
5110
5791
|
setRefreshEpoch(epoch => epoch + 1)
|
|
5792
|
+
resizeBurstHeld.current = false
|
|
5111
5793
|
}, RESIZE_REFLOW_DELAY_MS)
|
|
5112
5794
|
}
|
|
5113
5795
|
appStdout.on('resize', handleResize)
|
|
5114
5796
|
return () => {
|
|
5115
5797
|
appStdout.off('resize', handleResize)
|
|
5116
5798
|
if (replayTimer !== undefined) clearTimeout(replayTimer)
|
|
5799
|
+
if (resizeBurstHeld.current) {
|
|
5800
|
+
appStdout.write(SYNCHRONIZED_UPDATE_END)
|
|
5801
|
+
resizeBurstHeld.current = false
|
|
5802
|
+
}
|
|
5117
5803
|
}
|
|
5118
5804
|
}, [appStdout])
|
|
5119
5805
|
const terminalRows = terminalSize.rows
|
|
@@ -5146,14 +5832,19 @@ export function App(props: AppProps): ReactElement {
|
|
|
5146
5832
|
}, [])
|
|
5147
5833
|
const imeRowsBelowComposer = statusBarRows + 1
|
|
5148
5834
|
const composerEditorCap = composerMaxRows(terminalRows)
|
|
5149
|
-
//
|
|
5150
|
-
//
|
|
5151
|
-
//
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5835
|
+
// Pin the composer and status at the bottom: every extra chrome row
|
|
5836
|
+
// (completion menu, notice, todos, agents, extra editor/status rows)
|
|
5837
|
+
// covers live transcript instead of growing the tree.
|
|
5838
|
+
const dynamicRows = liveRegionBudget({
|
|
5839
|
+
terminalRows,
|
|
5840
|
+
composerRows,
|
|
5841
|
+
statusBarRows,
|
|
5842
|
+
menuRows,
|
|
5843
|
+
gutterRows: composerGutterRows,
|
|
5844
|
+
notice: notice !== undefined,
|
|
5845
|
+
todo: transcriptVisible && view.todos.length > 0,
|
|
5846
|
+
agents: transcriptVisible && agentRows.length > 0,
|
|
5847
|
+
})
|
|
5157
5848
|
const streamingActive = view.streaming !== '' || view.streamingReasoning !== ''
|
|
5158
5849
|
const deepDivingVisible = busy && !streamingActive
|
|
5159
5850
|
// Terminal tab label: "deepseek" until the session carries a name, then the
|
|
@@ -5201,6 +5892,10 @@ export function App(props: AppProps): ReactElement {
|
|
|
5201
5892
|
)
|
|
5202
5893
|
if (liveAudit.warning !== undefined && budgetWarnRef.current !== liveAudit.warning) {
|
|
5203
5894
|
budgetWarnRef.current = liveAudit.warning
|
|
5895
|
+
// The budget tripwire is a last-resort diagnostic: it fires only when the
|
|
5896
|
+
// live/static allocation already broke its contract, and Ink owns/shadows
|
|
5897
|
+
// console output. Everywhere else the TUI must never write to stdout.
|
|
5898
|
+
// eslint-disable-next-line no-console -- see the tripwire note above
|
|
5204
5899
|
console.warn(`[dsh-code] ${liveAudit.warning}`)
|
|
5205
5900
|
}
|
|
5206
5901
|
const auditedLiveLines = liveAudit.allocation.live === visibleLiveLines.length
|
|
@@ -5208,9 +5903,6 @@ export function App(props: AppProps): ReactElement {
|
|
|
5208
5903
|
: visibleLiveLines.slice(-liveAudit.allocation.live)
|
|
5209
5904
|
const auditedReasoningRows = liveAudit.allocation.reasoning
|
|
5210
5905
|
const auditedAnswerRows = liveAudit.allocation.answer
|
|
5211
|
-
const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !updateOpen && !scheduleOpen && !jobsOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !todosOpen && !verboseOpen && diffView === undefined && !approvalPending && !questionPending
|
|
5212
|
-
const inspectorVisible = verboseOpen && !approvalPending && !questionPending
|
|
5213
|
-
const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || updateOpen || scheduleOpen || jobsOpen || statuslineOpen || themeOpen || historyOpen || agentsOpen || subagentOpen || todosOpen || inspectorVisible || diffView !== undefined || approvalPending || questionPending
|
|
5214
5906
|
// The surface that currently owns the keyboard, named in the frozen band:
|
|
5215
5907
|
// an empty composer under a panel must not advertise typing it cannot
|
|
5216
5908
|
// accept — every key actually feeds the panel (which may or may not
|
|
@@ -5219,58 +5911,54 @@ export function App(props: AppProps): ReactElement {
|
|
|
5219
5911
|
? 'the approval prompt'
|
|
5220
5912
|
: questionPending
|
|
5221
5913
|
? 'the question'
|
|
5222
|
-
:
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
? '/resume'
|
|
5234
|
-
: pluginOpen
|
|
5235
|
-
? '/plugin'
|
|
5236
|
-
: updateOpen
|
|
5237
|
-
? '/update'
|
|
5238
|
-
: scheduleOpen
|
|
5239
|
-
? '/schedule'
|
|
5240
|
-
: jobsOpen
|
|
5241
|
-
? '/jobs'
|
|
5242
|
-
: statuslineOpen
|
|
5243
|
-
? '/statusline'
|
|
5244
|
-
: themeOpen
|
|
5245
|
-
? '/theme'
|
|
5246
|
-
: historyOpen
|
|
5247
|
-
? '/history'
|
|
5248
|
-
: agentsOpen
|
|
5249
|
-
? '/agents'
|
|
5250
|
-
: subagentOpen
|
|
5251
|
-
? '/subagent'
|
|
5252
|
-
: todosOpen
|
|
5253
|
-
? '/todos'
|
|
5254
|
-
: inspectorVisible
|
|
5255
|
-
? 'history details'
|
|
5256
|
-
: undefined
|
|
5914
|
+
: openPanel?.hint ?? (inspectorVisible ? 'history details' : undefined)
|
|
5915
|
+
// One way out of every panel, matching Esc: while a panel owns the keys,
|
|
5916
|
+
// Ctrl+C closes it instead of silently doing nothing. The composer keeps its
|
|
5917
|
+
// own three states (interrupt the turn / clear the draft / quit) whenever no
|
|
5918
|
+
// panel is open, and the approval and question bars keep theirs.
|
|
5919
|
+
useStableInput((input, key) => {
|
|
5920
|
+
if (!(key.ctrl && input === 'c')) return
|
|
5921
|
+
if (approvalPending || questionPending || deleteConfirmId !== undefined) return
|
|
5922
|
+
if (openPanel !== undefined) openPanel.close()
|
|
5923
|
+
else if (inspectorVisible) setVerboseOpen(false)
|
|
5924
|
+
}, true)
|
|
5257
5925
|
const frozenHint = keyboardOwner === undefined
|
|
5258
5926
|
? undefined
|
|
5259
|
-
:
|
|
5927
|
+
: t('frozen.keysGoTo', {
|
|
5928
|
+
owner: keyboardOwner,
|
|
5929
|
+
action: approvalPending
|
|
5930
|
+
? t('frozen.action.rejects')
|
|
5931
|
+
: questionPending
|
|
5932
|
+
? t('frozen.action.cancels')
|
|
5933
|
+
: updateApplying && updateOpen
|
|
5934
|
+
? t('frozen.action.waits')
|
|
5935
|
+
: t('frozen.action.closes'),
|
|
5936
|
+
})
|
|
5260
5937
|
const closeInspector = useCallback((): void => {
|
|
5261
5938
|
setVerboseOpen(false)
|
|
5262
5939
|
}, [])
|
|
5263
|
-
const refreshScreen = (): void => {
|
|
5264
|
-
//
|
|
5265
|
-
//
|
|
5266
|
-
//
|
|
5267
|
-
|
|
5268
|
-
// stale-position flicker where the screen keeps redrawing.
|
|
5940
|
+
const refreshScreen = useCallback((opts?: { wipeScrollback?: boolean }): void => {
|
|
5941
|
+
// Resize / Ctrl+L wipe screen AND scrollback. A history-cap trim remounts
|
|
5942
|
+
// Static at the current width, so native scrollback must stay — the user
|
|
5943
|
+
// may be reading messages above the fold.
|
|
5944
|
+
const clear = opts?.wipeScrollback === false ? TRIM_REFLOW_CLEAR : RESIZE_REFLOW_CLEAR
|
|
5269
5945
|
if (appStdout !== undefined) {
|
|
5270
5946
|
synchronizedReplayPending.current = true
|
|
5271
|
-
appStdout.write(SYNCHRONIZED_UPDATE_BEGIN +
|
|
5947
|
+
appStdout.write(SYNCHRONIZED_UPDATE_BEGIN + clear)
|
|
5272
5948
|
}
|
|
5273
5949
|
setRefreshEpoch(epoch => epoch + 1)
|
|
5950
|
+
}, [appStdout])
|
|
5951
|
+
const applyRainbow = (seed?: number): void => {
|
|
5952
|
+
// Replace the memoized roll, then setTheme so getPalette() and the
|
|
5953
|
+
// painters pick the new values; persist rainbow as the active theme
|
|
5954
|
+
// so a mid-session /rainbow from dark/light actually sticks. The
|
|
5955
|
+
// source-backed rebuild (same as /theme) repaints Static history too.
|
|
5956
|
+
rerollRainbow(seed)
|
|
5957
|
+
setTheme('rainbow')
|
|
5958
|
+
props.saveTheme?.('rainbow')
|
|
5959
|
+
notify(t('notice.rainbowRolled', { seed: rainbowSeedLabel() }))
|
|
5960
|
+
fireRainbowBurst()
|
|
5961
|
+
refreshScreen()
|
|
5274
5962
|
}
|
|
5275
5963
|
useEffect(() => {
|
|
5276
5964
|
if (!synchronizedReplayPending.current || appStdout === undefined) return
|
|
@@ -5288,8 +5976,8 @@ export function App(props: AppProps): ReactElement {
|
|
|
5288
5976
|
const settledNeedsTrim = settledRowsCache.current?.needsTrim === true
|
|
5289
5977
|
useEffect(() => {
|
|
5290
5978
|
if (!settledNeedsTrim || busy || streamingActive) return
|
|
5291
|
-
refreshScreen()
|
|
5292
|
-
}, [
|
|
5979
|
+
refreshScreen({ wipeScrollback: false })
|
|
5980
|
+
}, [busy, refreshScreen, settledNeedsTrim, streamingActive])
|
|
5293
5981
|
|
|
5294
5982
|
const sessionHasImages = useMemo(() => view.entries.some(entry =>
|
|
5295
5983
|
(entry.kind === 'user' || entry.kind === 'pending') && (entry.images?.length ?? 0) > 0), [view.entries])
|
|
@@ -5302,16 +5990,16 @@ export function App(props: AppProps): ReactElement {
|
|
|
5302
5990
|
setEffortLabel(effortId)
|
|
5303
5991
|
const selected = `${label}${effortId === undefined || effortId === '' ? '' : `@${effortId}`}`
|
|
5304
5992
|
if (sessionHasImages && row.inputModalities !== undefined && !row.inputModalities.includes('image')) {
|
|
5305
|
-
notify(
|
|
5993
|
+
notify(t('notice.modelChangedPlaceholder', { model: selected }), 'warning')
|
|
5306
5994
|
} else {
|
|
5307
|
-
notify(
|
|
5995
|
+
notify(t('notice.modelNextStep', { model: selected }))
|
|
5308
5996
|
}
|
|
5309
5997
|
setModelOpen(false)
|
|
5310
5998
|
setProviderOpen(false)
|
|
5311
5999
|
setProviderAction(undefined)
|
|
5312
6000
|
setEffortFor(undefined)
|
|
5313
6001
|
} catch (error: unknown) {
|
|
5314
|
-
notify(
|
|
6002
|
+
notify(t('notice.modelSwitchFailed', { message: error instanceof Error ? error.message : String(error) }), 'error')
|
|
5315
6003
|
}
|
|
5316
6004
|
}
|
|
5317
6005
|
|
|
@@ -5319,6 +6007,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5319
6007
|
setModelLoadEpoch(epoch => epoch + 1)
|
|
5320
6008
|
}
|
|
5321
6009
|
const closeModelSurface = (): void => {
|
|
6010
|
+
effortLookupEpoch.current += 1
|
|
5322
6011
|
setModelOpen(false)
|
|
5323
6012
|
setProviderOpen(false)
|
|
5324
6013
|
setProviderAction(undefined)
|
|
@@ -5332,7 +6021,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5332
6021
|
const seen = new Set<string>()
|
|
5333
6022
|
for (const row of providerDirectory?.rows ?? []) {
|
|
5334
6023
|
for (const model of row.configuration.models) {
|
|
5335
|
-
const raw = (model.extras
|
|
6024
|
+
const raw = (model.extras)?.reasoningEfforts
|
|
5336
6025
|
if (!isDeclaredReasoningEfforts(raw)) continue
|
|
5337
6026
|
const key = row.provider + '/' + model.id
|
|
5338
6027
|
if (seen.has(key)) continue
|
|
@@ -5370,7 +6059,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5370
6059
|
setProviderAction(undefined)
|
|
5371
6060
|
setProviderOpen(false)
|
|
5372
6061
|
reloadModelSurfaces()
|
|
5373
|
-
notify(
|
|
6062
|
+
notify(t('notice.loggedIn', { provider: authorization.label }))
|
|
5374
6063
|
},
|
|
5375
6064
|
back: () => {
|
|
5376
6065
|
setProviderAction(undefined)
|
|
@@ -5386,7 +6075,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5386
6075
|
setProviderAction(undefined)
|
|
5387
6076
|
setProviderOpen(true)
|
|
5388
6077
|
reloadModelSurfaces()
|
|
5389
|
-
notify(
|
|
6078
|
+
notify(t('notice.loggedOut', { provider: authorization.label }))
|
|
5390
6079
|
},
|
|
5391
6080
|
back: () => setProviderAction(undefined),
|
|
5392
6081
|
})
|
|
@@ -5397,13 +6086,13 @@ export function App(props: AppProps): ReactElement {
|
|
|
5397
6086
|
save: props.saveModelProviderConfiguration,
|
|
5398
6087
|
saveCredential: props.saveModelProviderCredential,
|
|
5399
6088
|
discover: props.discoverModelProvider
|
|
5400
|
-
?? (
|
|
6089
|
+
?? (() => Promise.reject(new Error('model discovery is unavailable in this profile; enter models by hand'))),
|
|
5401
6090
|
done: result => {
|
|
5402
6091
|
const target = providerAction.target
|
|
5403
6092
|
setProviderAction(undefined)
|
|
5404
6093
|
setProviderOpen(true)
|
|
5405
6094
|
reloadModelSurfaces()
|
|
5406
|
-
notify(
|
|
6095
|
+
notify(t('notice.providerSaved', { provider: target.displayName, suffix: result.key ? ' · API key updated' : '' }))
|
|
5407
6096
|
},
|
|
5408
6097
|
back: () => setProviderAction(undefined),
|
|
5409
6098
|
onExit: closeModelSurface,
|
|
@@ -5418,7 +6107,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5418
6107
|
setProviderAction(undefined)
|
|
5419
6108
|
setProviderOpen(true)
|
|
5420
6109
|
reloadModelSurfaces()
|
|
5421
|
-
notify(
|
|
6110
|
+
notify(t('notice.apiKeyRemoved', { provider: target.displayName }))
|
|
5422
6111
|
},
|
|
5423
6112
|
back: () => setProviderAction(undefined),
|
|
5424
6113
|
})
|
|
@@ -5432,7 +6121,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5432
6121
|
setProviderAction(undefined)
|
|
5433
6122
|
setProviderOpen(true)
|
|
5434
6123
|
reloadModelSurfaces()
|
|
5435
|
-
notify(
|
|
6124
|
+
notify(t('notice.providerRemoved', { provider: target.displayName }))
|
|
5436
6125
|
},
|
|
5437
6126
|
back: () => setProviderAction(undefined),
|
|
5438
6127
|
})
|
|
@@ -5444,42 +6133,42 @@ export function App(props: AppProps): ReactElement {
|
|
|
5444
6133
|
authorizationError,
|
|
5445
6134
|
onConfigure: (target: ProviderTargetView) => {
|
|
5446
6135
|
if (props.saveModelProviderConfiguration === undefined) {
|
|
5447
|
-
notify('
|
|
6136
|
+
notify(t('notice.providerUnavailable'), 'warning')
|
|
5448
6137
|
return
|
|
5449
6138
|
}
|
|
5450
6139
|
setProviderAction({ kind: 'configure', target })
|
|
5451
6140
|
},
|
|
5452
6141
|
onUnset: (target: ProviderTargetView) => {
|
|
5453
6142
|
if (props.unsetModelProviderCredential === undefined) {
|
|
5454
|
-
notify('
|
|
6143
|
+
notify(t('notice.apiKeyUnavailable'), 'warning')
|
|
5455
6144
|
return
|
|
5456
6145
|
}
|
|
5457
6146
|
setProviderAction({ kind: 'unset', target })
|
|
5458
6147
|
},
|
|
5459
6148
|
onRemove: (target: ProviderTargetView) => {
|
|
5460
6149
|
if (props.removeModelProvider === undefined) {
|
|
5461
|
-
notify('
|
|
6150
|
+
notify(t('notice.providerRemovalUnavailable'), 'warning')
|
|
5462
6151
|
return
|
|
5463
6152
|
}
|
|
5464
6153
|
setProviderAction({ kind: 'remove', target })
|
|
5465
6154
|
},
|
|
5466
6155
|
onLogin: (target: ProviderTargetView, authorization: ProviderAuthorizationRow) => {
|
|
5467
6156
|
if (busy) {
|
|
5468
|
-
notify('
|
|
6157
|
+
notify(t('notice.loginIdleOnly'), 'warning')
|
|
5469
6158
|
return
|
|
5470
6159
|
}
|
|
5471
6160
|
if (props.beginProviderAuthorization === undefined
|
|
5472
6161
|
|| props.cancelProviderAuthorization === undefined
|
|
5473
6162
|
|| props.openAuthorizationUrl === undefined
|
|
5474
6163
|
|| props.copyTextValue === undefined) {
|
|
5475
|
-
notify('
|
|
6164
|
+
notify(t('notice.loginUnavailable'), 'warning')
|
|
5476
6165
|
return
|
|
5477
6166
|
}
|
|
5478
6167
|
setProviderAction({ kind: 'login', target, authorization })
|
|
5479
6168
|
},
|
|
5480
6169
|
onLogout: (target: ProviderTargetView, authorization: ProviderAuthorizationRow) => {
|
|
5481
6170
|
if (props.logoutProviderAuthorization === undefined) {
|
|
5482
|
-
notify('
|
|
6171
|
+
notify(t('notice.logoutUnavailable'), 'warning')
|
|
5483
6172
|
return
|
|
5484
6173
|
}
|
|
5485
6174
|
setProviderAction({ kind: 'logout', target, authorization })
|
|
@@ -5512,7 +6201,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5512
6201
|
setEffortFor(row)
|
|
5513
6202
|
return
|
|
5514
6203
|
}
|
|
5515
|
-
const effortId = row.reasoning?.efforts.length === 1 ? row.reasoning.efforts[0]
|
|
6204
|
+
const effortId = row.reasoning?.efforts.length === 1 ? row.reasoning.efforts[0].id : undefined
|
|
5516
6205
|
applyModel(row, effortId)
|
|
5517
6206
|
},
|
|
5518
6207
|
...(props.loadModelProviders === undefined || props.saveModelProviderConfiguration === undefined
|
|
@@ -5547,6 +6236,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5547
6236
|
continuationPrefix: ' ',
|
|
5548
6237
|
dim: true,
|
|
5549
6238
|
maxRows: auditedReasoningRows,
|
|
6239
|
+
columns: Math.max(1, terminalColumns - 2),
|
|
5550
6240
|
})
|
|
5551
6241
|
// The collapsed marker shimmers only while reasoning streams
|
|
5552
6242
|
// alone: once answer text flows, a periodically re-rendered
|
|
@@ -5563,6 +6253,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5563
6253
|
continuationPrefix: ' ',
|
|
5564
6254
|
dim: true,
|
|
5565
6255
|
maxRows: auditedReasoningRows,
|
|
6256
|
+
columns: Math.max(1, terminalColumns - 2),
|
|
5566
6257
|
})
|
|
5567
6258
|
: undefined,
|
|
5568
6259
|
view.streaming !== '' && auditedAnswerRows > 0
|
|
@@ -5570,7 +6261,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5570
6261
|
StreamTail,
|
|
5571
6262
|
// The same two-column gutter as settled replies: streamed text
|
|
5572
6263
|
// lands exactly where the assembled message will render.
|
|
5573
|
-
{ text: view.streaming, dim: false, maxRows: auditedAnswerRows, prefix: ' ' },
|
|
6264
|
+
{ text: view.streaming, dim: false, maxRows: auditedAnswerRows, prefix: ' ', columns: Math.max(1, terminalColumns - 2) },
|
|
5574
6265
|
busy ? createElement(Caret, { animated: animations }) : undefined,
|
|
5575
6266
|
)
|
|
5576
6267
|
: undefined,
|
|
@@ -5579,6 +6270,13 @@ export function App(props: AppProps): ReactElement {
|
|
|
5579
6270
|
: undefined,
|
|
5580
6271
|
transcriptVisible ? createElement(TodoPanel, { todos: view.todos }) : undefined,
|
|
5581
6272
|
transcriptVisible ? createElement(AgentsLine, { rows: agentRows, total: props.subagents.getTotalSeen() }) : undefined,
|
|
6273
|
+
usageOpen && !approvalPending && !questionPending
|
|
6274
|
+
? createElement(UsagePanel, {
|
|
6275
|
+
key: props.sessionKey,
|
|
6276
|
+
load: props.loadUsage,
|
|
6277
|
+
close: () => setUsageOpen(false),
|
|
6278
|
+
})
|
|
6279
|
+
: undefined,
|
|
5582
6280
|
todosOpen && !approvalPending && !questionPending
|
|
5583
6281
|
? createElement(MemoTodoListPanel, {
|
|
5584
6282
|
todos: view.todos,
|
|
@@ -5587,6 +6285,14 @@ export function App(props: AppProps): ReactElement {
|
|
|
5587
6285
|
},
|
|
5588
6286
|
})
|
|
5589
6287
|
: undefined,
|
|
6288
|
+
queueOpen && !approvalPending && !questionPending
|
|
6289
|
+
? createElement(QueuePanel, {
|
|
6290
|
+
rows: queuedRows,
|
|
6291
|
+
busy,
|
|
6292
|
+
update: props.updateQueued,
|
|
6293
|
+
onClose: () => setQueueOpen(false),
|
|
6294
|
+
})
|
|
6295
|
+
: undefined,
|
|
5590
6296
|
createElement(QuestionBar, { store: props.questions, snapshot: questionSnapshot, locked: false }),
|
|
5591
6297
|
createElement(ApprovalBar, { snapshot: approvalSnapshot, locked: questionPending, notify, interrupt: props.interrupt, summarize: questionPending }),
|
|
5592
6298
|
modelSurface,
|
|
@@ -5607,10 +6313,23 @@ export function App(props: AppProps): ReactElement {
|
|
|
5607
6313
|
onClose: () => setDiffView(undefined),
|
|
5608
6314
|
})
|
|
5609
6315
|
: undefined,
|
|
6316
|
+
reviewPickerOpen && !approvalPending && !questionPending && props.listReviewBranches !== undefined && props.listReviewCommits !== undefined
|
|
6317
|
+
? createElement(ReviewPickerPanel, {
|
|
6318
|
+
loadBranches: props.listReviewBranches,
|
|
6319
|
+
loadCommits: props.listReviewCommits,
|
|
6320
|
+
choose: argument => {
|
|
6321
|
+
setReviewPickerOpen(false)
|
|
6322
|
+
props.reviewChanges(argument)
|
|
6323
|
+
},
|
|
6324
|
+
close: () => setReviewPickerOpen(false),
|
|
6325
|
+
})
|
|
6326
|
+
: undefined,
|
|
5610
6327
|
verboseOpen && !approvalPending && !questionPending
|
|
5611
6328
|
? createElement(MemoVerbosePanel, {
|
|
5612
6329
|
entries: view.entries,
|
|
5613
6330
|
onClose: closeInspector,
|
|
6331
|
+
columns: terminalColumns,
|
|
6332
|
+
rows: terminalRows,
|
|
5614
6333
|
})
|
|
5615
6334
|
: undefined,
|
|
5616
6335
|
modeOpen && !approvalPending && !questionPending
|
|
@@ -5619,9 +6338,9 @@ export function App(props: AppProps): ReactElement {
|
|
|
5619
6338
|
load: props.loadPresets,
|
|
5620
6339
|
select: (id: string) => {
|
|
5621
6340
|
void props.switchMode(id).then(label => {
|
|
5622
|
-
notify(
|
|
6341
|
+
notify(t('notice.modeChangedSimple', { value: label }))
|
|
5623
6342
|
setModeOpen(false)
|
|
5624
|
-
}, (reason: unknown) => notify(
|
|
6343
|
+
}, (reason: unknown) => notify(t('notice.modeSwitchFailed', { message: reason instanceof Error ? reason.message : String(reason) }), 'error'))
|
|
5625
6344
|
},
|
|
5626
6345
|
close: () => setModeOpen(false),
|
|
5627
6346
|
})
|
|
@@ -5633,7 +6352,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5633
6352
|
select: (id: string) => {
|
|
5634
6353
|
try {
|
|
5635
6354
|
const selected = props.setPermission(id)
|
|
5636
|
-
notify(
|
|
6355
|
+
notify(t('notice.permissionChangedSimple', { value: selected }))
|
|
5637
6356
|
setPermissionOpen(false)
|
|
5638
6357
|
} catch (reason: unknown) {
|
|
5639
6358
|
notify(`permission change failed: ${reason instanceof Error ? reason.message : String(reason)}`, 'error')
|
|
@@ -5647,14 +6366,43 @@ export function App(props: AppProps): ReactElement {
|
|
|
5647
6366
|
currentCwd: props.workspaceRoot,
|
|
5648
6367
|
load: props.loadSessions,
|
|
5649
6368
|
readTranscript: props.loadSessionTranscript,
|
|
5650
|
-
requestDelete,
|
|
6369
|
+
requestDelete: resumeDelete.mode ? requestDelete : undefined,
|
|
5651
6370
|
deleteConfirmId,
|
|
5652
6371
|
reloadToken: deleteReloadToken,
|
|
5653
6372
|
deleteMode: resumeDelete.mode,
|
|
5654
|
-
|
|
6373
|
+
presetId: resumeDelete.id,
|
|
6374
|
+
select: (row: SessionRow) => {
|
|
6375
|
+
// Defense in depth: the dedicated delete picker must never turn a
|
|
6376
|
+
// selection into a session switch, even if its key routing regresses.
|
|
6377
|
+
if (resumeDelete.mode) return
|
|
6378
|
+
props.switchSession(row)
|
|
6379
|
+
setResumeOpen(false)
|
|
6380
|
+
},
|
|
5655
6381
|
close: () => setResumeOpen(false),
|
|
5656
6382
|
})
|
|
5657
6383
|
: undefined,
|
|
6384
|
+
searchOpen && !approvalPending && !questionPending && props.searchSessions !== undefined
|
|
6385
|
+
? createElement(SearchPanel, {
|
|
6386
|
+
load: props.searchSessions,
|
|
6387
|
+
initialQuery: searchSeed,
|
|
6388
|
+
select: (row: SearchRow) => {
|
|
6389
|
+
setSearchOpen(false)
|
|
6390
|
+
props.switchSession({
|
|
6391
|
+
id: row.id,
|
|
6392
|
+
createdAt: row.updatedAt,
|
|
6393
|
+
updatedAt: row.updatedAt,
|
|
6394
|
+
cwd: '',
|
|
6395
|
+
workspace: '',
|
|
6396
|
+
subagent: row.subagent,
|
|
6397
|
+
resumable: row.resumable,
|
|
6398
|
+
live: false,
|
|
6399
|
+
persisted: true,
|
|
6400
|
+
preset: '',
|
|
6401
|
+
})
|
|
6402
|
+
},
|
|
6403
|
+
close: () => setSearchOpen(false),
|
|
6404
|
+
})
|
|
6405
|
+
: undefined,
|
|
5658
6406
|
pluginOpen && !approvalPending && !questionPending
|
|
5659
6407
|
? createElement(PluginPanel, { load: props.loadPlugins, initialQuery: pluginQuery, close: () => setPluginOpen(false) })
|
|
5660
6408
|
: undefined,
|
|
@@ -5691,12 +6439,36 @@ export function App(props: AppProps): ReactElement {
|
|
|
5691
6439
|
// palette. `auto` stores as requested; detection is a later step.
|
|
5692
6440
|
setTheme(name)
|
|
5693
6441
|
props.saveTheme?.(name)
|
|
5694
|
-
|
|
6442
|
+
// Rainbow prints its roll seed so a lucky launch can be reproduced
|
|
6443
|
+
// with RAINBOW_SEED=<seed>.
|
|
6444
|
+
notify(name === 'rainbow'
|
|
6445
|
+
? t('notice.themeRainbow', { seed: rainbowSeedLabel() })
|
|
6446
|
+
: t('notice.themeSaved', { name }))
|
|
5695
6447
|
setThemeOpen(false)
|
|
6448
|
+
// The header whale and settled history live in the Static region,
|
|
6449
|
+
// which renders once and would keep the old palette's colors; the
|
|
6450
|
+
// same source-backed rebuild resize and Ctrl+L use repaints the
|
|
6451
|
+
// whole screen (scrollback included) from the new palette.
|
|
6452
|
+
if (name === 'rainbow') fireRainbowBurst()
|
|
6453
|
+
refreshScreen()
|
|
5696
6454
|
},
|
|
5697
6455
|
close: () => setThemeOpen(false),
|
|
5698
6456
|
})
|
|
5699
6457
|
: undefined,
|
|
6458
|
+
languageOpen && !approvalPending && !questionPending
|
|
6459
|
+
? createElement(LanguagePanel, {
|
|
6460
|
+
current: getLanguage(),
|
|
6461
|
+
select: (name: LanguageName) => {
|
|
6462
|
+
props.saveLanguage(name)
|
|
6463
|
+
notify(t('notice.languageSaved', { name }))
|
|
6464
|
+
setLanguageOpen(false)
|
|
6465
|
+
// The Static region renders once; the same source-backed rebuild
|
|
6466
|
+
// the theme switch uses repaints translated text everywhere.
|
|
6467
|
+
refreshScreen()
|
|
6468
|
+
},
|
|
6469
|
+
close: () => setLanguageOpen(false),
|
|
6470
|
+
})
|
|
6471
|
+
: undefined,
|
|
5700
6472
|
historyOpen && !approvalPending && !questionPending
|
|
5701
6473
|
? createElement(HistoryPanel, {
|
|
5702
6474
|
entries: recallSpace,
|
|
@@ -5724,15 +6496,15 @@ export function App(props: AppProps): ReactElement {
|
|
|
5724
6496
|
// The runner's label already carries the effort suffix
|
|
5725
6497
|
// (`provider/model@effort`), so no second append here.
|
|
5726
6498
|
const label = props.setSubagentModel(row, effortId)
|
|
5727
|
-
notify(
|
|
6499
|
+
notify(t('notice.subagentsChanged', { value: label }))
|
|
5728
6500
|
setSubagentOpen(false)
|
|
5729
6501
|
} catch (reason: unknown) {
|
|
5730
|
-
notify(
|
|
6502
|
+
notify(t('notice.subagentChangeFailed', { message: reason instanceof Error ? reason.message : String(reason) }), 'error')
|
|
5731
6503
|
}
|
|
5732
6504
|
},
|
|
5733
6505
|
inherit: () => {
|
|
5734
6506
|
props.clearSubagentModel()
|
|
5735
|
-
notify('
|
|
6507
|
+
notify(t('notice.subagentsInherited'))
|
|
5736
6508
|
setSubagentOpen(false)
|
|
5737
6509
|
},
|
|
5738
6510
|
close: () => setSubagentOpen(false),
|
|
@@ -5759,11 +6531,18 @@ export function App(props: AppProps): ReactElement {
|
|
|
5759
6531
|
descriptors,
|
|
5760
6532
|
skills,
|
|
5761
6533
|
dispatch: props.dispatch,
|
|
5762
|
-
applyEditorKeys: props.applyEditorKeys,
|
|
5763
6534
|
steer: props.steer,
|
|
6535
|
+
submitMode,
|
|
6536
|
+
cycleSubmitMode: () => {
|
|
6537
|
+
const next = submitMode === 'queue' ? 'steer' : 'queue'
|
|
6538
|
+
setSubmitMode(next)
|
|
6539
|
+
notify(t(next === 'steer' ? 'notice.submitMode.steer' : 'notice.submitMode.queue'))
|
|
6540
|
+
},
|
|
6541
|
+
applyEditorKeys: props.applyEditorKeys,
|
|
5764
6542
|
interrupt: props.interrupt,
|
|
5765
6543
|
quit: props.quit,
|
|
5766
6544
|
openModel: () => {
|
|
6545
|
+
effortLookupEpoch.current += 1
|
|
5767
6546
|
setDirectory(undefined)
|
|
5768
6547
|
setModelError(undefined)
|
|
5769
6548
|
setProviderDirectory(undefined)
|
|
@@ -5786,18 +6565,20 @@ export function App(props: AppProps): ReactElement {
|
|
|
5786
6565
|
// never as "the model has no efforts" — the adapter advertises
|
|
5787
6566
|
// levels for every deepseek model, so "no efforts" is almost
|
|
5788
6567
|
// always a failed resolveModelInfo, not a fact.
|
|
6568
|
+
const epoch = ++effortLookupEpoch.current
|
|
5789
6569
|
void props.loadModels().then((loaded) => {
|
|
6570
|
+
if (epoch !== effortLookupEpoch.current) return
|
|
5790
6571
|
const [provider, model] = modelLabel.split('/')
|
|
5791
6572
|
const row = loaded.rows.find(candidate => candidate.provider === provider && candidate.model === model)
|
|
5792
6573
|
?? loaded.rows.find(candidate => candidate.model === model && candidate.reasoning !== undefined)
|
|
5793
6574
|
?? loaded.rows.find(candidate => candidate.model === model)
|
|
5794
6575
|
if (row === undefined) {
|
|
5795
|
-
notify('
|
|
6576
|
+
notify(t('notice.modelMissing'), 'warning')
|
|
5796
6577
|
return
|
|
5797
6578
|
}
|
|
5798
6579
|
const rowTag = `${row.provider}/${row.model}`
|
|
5799
6580
|
if (loaded.reasoningFailures?.includes(rowTag) === true) {
|
|
5800
|
-
notify('
|
|
6581
|
+
notify(t('notice.effortUnavailable'), 'warning')
|
|
5801
6582
|
return
|
|
5802
6583
|
}
|
|
5803
6584
|
if (row.reasoning === undefined || row.reasoning.efforts.length === 0) {
|
|
@@ -5821,20 +6602,34 @@ export function App(props: AppProps): ReactElement {
|
|
|
5821
6602
|
openMode: () => setModeOpen(true),
|
|
5822
6603
|
openPermission: () => setPermissionOpen(true),
|
|
5823
6604
|
openResume: () => { setResumeDelete({ mode: false }); setResumeOpen(true) },
|
|
6605
|
+
openSearch: (query: string) => {
|
|
6606
|
+
if (props.searchSessions === undefined) {
|
|
6607
|
+
notify(t('notice.sessionSearchUnavailable'), 'warning')
|
|
6608
|
+
return
|
|
6609
|
+
}
|
|
6610
|
+
setSearchSeed(query)
|
|
6611
|
+
setSearchOpen(true)
|
|
6612
|
+
},
|
|
5824
6613
|
openPlugin: (query = '') => { setPluginQuery(query); setPluginOpen(true) },
|
|
5825
6614
|
openUpdate: () => setUpdateOpen(true),
|
|
5826
6615
|
openSchedule: () => setScheduleOpen(true),
|
|
5827
6616
|
openJobs: () => setJobsOpen(true),
|
|
5828
6617
|
openStatusline: () => setStatuslineOpen(true),
|
|
5829
6618
|
openTheme: () => setThemeOpen(true),
|
|
6619
|
+
openLanguage: () => setLanguageOpen(true),
|
|
6620
|
+
saveLanguage: props.saveLanguage,
|
|
5830
6621
|
openHistory: () => setHistoryOpen(true),
|
|
6622
|
+
openQueue: () => setQueueOpen(true),
|
|
5831
6623
|
openAgents: () => setAgentsOpen(true),
|
|
5832
6624
|
openSubagent: () => setSubagentOpen(true),
|
|
5833
6625
|
openTodos: () => setTodosOpen(true),
|
|
6626
|
+
openUsage: () => setUsageOpen(true),
|
|
5834
6627
|
openDelete: (id?: string) => {
|
|
5835
6628
|
const armed = id === undefined || id === '' ? undefined : id
|
|
5836
6629
|
setResumeDelete({ mode: true, ...armed === undefined ? {} : { id: armed } })
|
|
5837
|
-
|
|
6630
|
+
// Confirm after the picker resolves the id against the listing —
|
|
6631
|
+
// the argument may be a suffix of the displayed id, not the row key.
|
|
6632
|
+
setDeleteConfirmId(undefined)
|
|
5838
6633
|
setResumeOpen(true)
|
|
5839
6634
|
},
|
|
5840
6635
|
openDiff: (argument: string) => {
|
|
@@ -5843,6 +6638,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5843
6638
|
})
|
|
5844
6639
|
},
|
|
5845
6640
|
reviewChanges: props.reviewChanges,
|
|
6641
|
+
openReviewPicker: () => setReviewPickerOpen(true),
|
|
5846
6642
|
deleteConfirm: deleteConfirmId,
|
|
5847
6643
|
confirmDelete,
|
|
5848
6644
|
cancelDelete,
|
|
@@ -5886,11 +6682,13 @@ export function App(props: AppProps): ReactElement {
|
|
|
5886
6682
|
recordLocal,
|
|
5887
6683
|
recordHistory: props.recordHistory,
|
|
5888
6684
|
queued: queuedRows,
|
|
5889
|
-
|
|
6685
|
+
updateQueued: props.updateQueued,
|
|
5890
6686
|
historyFill,
|
|
5891
6687
|
historyConsumed,
|
|
5892
6688
|
animations,
|
|
5893
6689
|
applyAnimations,
|
|
6690
|
+
applyRainbow,
|
|
6691
|
+
rainbowBurstId,
|
|
5894
6692
|
waveTier,
|
|
5895
6693
|
waveStyle,
|
|
5896
6694
|
maxRows: composerEditorCap,
|
|
@@ -5914,6 +6712,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5914
6712
|
},
|
|
5915
6713
|
stats: view.stats,
|
|
5916
6714
|
busy,
|
|
6715
|
+
animated: animations,
|
|
5917
6716
|
columns: terminalColumns,
|
|
5918
6717
|
items: statuslineItems,
|
|
5919
6718
|
onRows: handleStatusRows,
|