dsh-code 1.0.7 → 1.2.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 +4063 -844
- package/lib/session-query.mjs +3 -2
- package/lib/startup.mjs +4 -4
- package/lib/{theme-DCT8Y2xf.mjs → theme-7u5Qo3dF.mjs} +657 -20
- package/lib/types/app.d.ts +100 -63
- package/lib/types/authorization-panel.d.ts +3 -3
- package/lib/types/git-workflow.d.ts +91 -2
- package/lib/types/i18n.d.ts +39 -0
- package/lib/types/index.d.ts +73 -1
- package/lib/types/input-split.d.ts +1 -1
- package/lib/types/kernel-panels.d.ts +86 -31
- package/lib/types/language-panel.d.ts +12 -0
- package/lib/types/locales/en.d.ts +450 -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 +8 -8
- 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 +17 -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 +5 -5
- package/lib/types/update.d.ts +10 -1
- package/lib/types/version.d.ts +4 -3
- package/package.json +24 -7
- package/src/app.ts +1155 -478
- package/src/approval.ts +166 -166
- package/src/authorization-panel.ts +19 -16
- package/src/editor-keys.ts +371 -371
- package/src/git-workflow.ts +229 -3
- package/src/i18n.ts +68 -0
- package/src/index.ts +412 -76
- package/src/input-split.ts +3 -3
- package/src/kernel-panels.ts +471 -89
- package/src/keyboard.ts +5 -4
- package/src/language-panel.ts +53 -0
- package/src/locales/en.ts +489 -0
- package/src/locales/zh.ts +488 -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 +208 -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 +41 -35
- package/src/render/text.ts +14 -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 +8 -6
- package/src/session-query.ts +8 -4
- package/src/startup.ts +3 -3
- 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 +37 -27
- package/src/update.ts +19 -3
- package/src/version.ts +58 -20
- package/src/whale-glyph.ts +23 -23
package/src/app.ts
CHANGED
|
@@ -26,14 +26,25 @@ 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'
|
|
46
|
+
import { LanguagePanel } from './language-panel.ts'
|
|
47
|
+
import { getLanguage, parseLanguageName, t, type LanguageName, type MessageKey } from './i18n.ts'
|
|
37
48
|
import { UpdatePanel } from './update-panel.ts'
|
|
38
49
|
import type { LauncherUpdateStatus } from './update.ts'
|
|
39
50
|
import { WHALE_GLYPH, WHALE_GLYPH_COLUMNS } from './whale-glyph.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'
|
|
@@ -97,7 +114,7 @@ import {
|
|
|
97
114
|
type RecallState,
|
|
98
115
|
} from './history.ts'
|
|
99
116
|
import type { SessionDirectoryOptions, SessionRow } from './session-directory.ts'
|
|
100
|
-
import type
|
|
117
|
+
import { parseReviewArgument, type GitDiffView, type ReviewBranch, type ReviewCommit, type ReviewSelection } from './git-workflow.ts'
|
|
101
118
|
import {
|
|
102
119
|
authorizationForProvider,
|
|
103
120
|
providerAuthorizationStatus,
|
|
@@ -148,7 +165,7 @@ const SYNCHRONIZED_UPDATE_END = '\x1b[?2026l'
|
|
|
148
165
|
import {
|
|
149
166
|
layoutStatusBar,
|
|
150
167
|
parseStatuslineItems,
|
|
151
|
-
|
|
168
|
+
statusCycleHint,
|
|
152
169
|
STATUS_GROUP_SEPARATOR,
|
|
153
170
|
STATUS_ITEM_SEPARATOR,
|
|
154
171
|
STATUS_ROW2_INDENT,
|
|
@@ -175,6 +192,7 @@ import {
|
|
|
175
192
|
followInspectorCursor,
|
|
176
193
|
inspectorViewport,
|
|
177
194
|
layoutGutterRows,
|
|
195
|
+
liveRegionBudget,
|
|
178
196
|
moveScroll,
|
|
179
197
|
panelViewport,
|
|
180
198
|
revealRow,
|
|
@@ -182,6 +200,8 @@ import {
|
|
|
182
200
|
} from './render/inspector.ts'
|
|
183
201
|
import {
|
|
184
202
|
clampLiveAllocation,
|
|
203
|
+
diffLineStyle,
|
|
204
|
+
fillDiffLineBars,
|
|
185
205
|
lineSegment,
|
|
186
206
|
markdownLines,
|
|
187
207
|
settledEntryLines,
|
|
@@ -224,39 +244,53 @@ import {
|
|
|
224
244
|
export type NoticeTone = 'info' | 'warning' | 'error'
|
|
225
245
|
|
|
226
246
|
/** 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: '/
|
|
247
|
+
/** One TUI-owned slash command: label plus its i18n description key. */
|
|
248
|
+
interface LocalCommand { readonly label: string; readonly descriptionKey: MessageKey }
|
|
249
|
+
|
|
250
|
+
const LOCAL_COMMANDS: readonly LocalCommand[] = [
|
|
251
|
+
{ label: '/help', descriptionKey: 'cmd.help' },
|
|
252
|
+
{ label: '/model', descriptionKey: 'cmd.model' },
|
|
253
|
+
{ label: '/effort', descriptionKey: 'cmd.effort' },
|
|
254
|
+
{ label: '/mode', descriptionKey: 'cmd.mode' },
|
|
255
|
+
{ label: '/permission', descriptionKey: 'cmd.permission' },
|
|
256
|
+
{ label: '/new', descriptionKey: 'cmd.new' },
|
|
257
|
+
{ label: '/fork', descriptionKey: 'cmd.fork' },
|
|
258
|
+
{ label: '/resume', descriptionKey: 'cmd.resume' },
|
|
259
|
+
{ label: '/search', descriptionKey: 'cmd.search' },
|
|
260
|
+
{ label: '/plugin', descriptionKey: 'cmd.plugin' },
|
|
261
|
+
{ label: '/update', descriptionKey: 'cmd.update' },
|
|
262
|
+
{ label: '/jobs', descriptionKey: 'cmd.jobs' },
|
|
263
|
+
{ label: '/schedule', descriptionKey: 'cmd.schedule' },
|
|
264
|
+
{ label: '/statusline', descriptionKey: 'cmd.statusline' },
|
|
265
|
+
{ label: '/theme', descriptionKey: 'cmd.theme' },
|
|
266
|
+
{ label: '/language', descriptionKey: 'cmd.language' },
|
|
267
|
+
{ label: '/rainbow', descriptionKey: 'cmd.rainbow' },
|
|
268
|
+
{ label: '/animation', descriptionKey: 'cmd.animation' },
|
|
269
|
+
{ label: '/history', descriptionKey: 'cmd.history' },
|
|
270
|
+
{ label: '/queue', descriptionKey: 'cmd.queue' },
|
|
271
|
+
{ label: '/usage', descriptionKey: 'cmd.usage' },
|
|
272
|
+
{ label: '/agents', descriptionKey: 'cmd.agents' },
|
|
273
|
+
{ label: '/todos', descriptionKey: 'cmd.todos' },
|
|
274
|
+
{ label: '/subagent', descriptionKey: 'cmd.subagent' },
|
|
275
|
+
{ label: '/vscode-keys', descriptionKey: 'cmd.vscode-keys' },
|
|
276
|
+
{ label: '/delete', descriptionKey: 'cmd.delete' },
|
|
277
|
+
{ label: '/clear', descriptionKey: 'cmd.clear' },
|
|
278
|
+
{ label: '/export', descriptionKey: 'cmd.export' },
|
|
279
|
+
{ label: '/title', descriptionKey: 'cmd.title' },
|
|
280
|
+
{ label: '/copy', descriptionKey: 'cmd.copy' },
|
|
281
|
+
{ label: '/diff', descriptionKey: 'cmd.diff' },
|
|
282
|
+
{ label: '/review', descriptionKey: 'cmd.review' },
|
|
283
|
+
{ label: '/quit', descriptionKey: 'cmd.quit' },
|
|
256
284
|
] as const
|
|
257
285
|
|
|
258
286
|
const LOCAL_COMMAND_NAMES = new Set(LOCAL_COMMANDS.map(command => command.label.slice(1)))
|
|
259
287
|
|
|
288
|
+
/** One mutation the terminal may request for a pending next-turn inbox item. */
|
|
289
|
+
export type QueueMutation =
|
|
290
|
+
| { readonly kind: 'remove' }
|
|
291
|
+
| { readonly kind: 'edit'; readonly text: string }
|
|
292
|
+
| { readonly kind: 'steer' }
|
|
293
|
+
|
|
260
294
|
/** Props the runner hands the app; callbacks stay owned by the runner. */
|
|
261
295
|
export interface AppProps {
|
|
262
296
|
/** Event-fed transcript store for the live session. */
|
|
@@ -295,133 +329,151 @@ export interface AppProps {
|
|
|
295
329
|
* an attachment prepare resolves after the app remounted onto another
|
|
296
330
|
* session, and the runner drops the stale delivery then.
|
|
297
331
|
*/
|
|
298
|
-
dispatch(text: string, attachments?: readonly ContentBlock[], origin?: string)
|
|
299
|
-
/**
|
|
300
|
-
|
|
332
|
+
dispatch: (text: string, attachments?: readonly ContentBlock[], origin?: string) => void
|
|
333
|
+
/**
|
|
334
|
+
* Submit one line as steering: it joins the turn already running at its next
|
|
335
|
+
* step boundary instead of waiting for the next turn. Same stale-delivery
|
|
336
|
+
* guard as {@link dispatch}.
|
|
337
|
+
*/
|
|
338
|
+
steer: (text: string, attachments?: readonly ContentBlock[], origin?: string) => void
|
|
301
339
|
/**
|
|
302
340
|
* The FULL current session identity ('' while the first session is pending)
|
|
303
341
|
* — the stale-delivery origin above. Distinct from the short display id.
|
|
304
342
|
*/
|
|
305
343
|
sessionKey: string
|
|
306
344
|
/** Interrupt the running turn (Esc); true when a turn was cancelled. */
|
|
307
|
-
interrupt()
|
|
345
|
+
interrupt: () => boolean
|
|
308
346
|
/** Quit: unmount, flush, and request process exit. */
|
|
309
|
-
quit()
|
|
347
|
+
quit: () => void
|
|
310
348
|
/** Load the selectable model directory (called when /model opens). */
|
|
311
|
-
loadModels()
|
|
349
|
+
loadModels: () => Promise<ModelDirectory>
|
|
312
350
|
/** Load @mention candidates for the typed query (files + sessions). */
|
|
313
|
-
loadMentions(query: string, signal?: AbortSignal)
|
|
351
|
+
loadMentions: (query: string, signal?: AbortSignal) => Promise<readonly MentionCandidate[]>
|
|
314
352
|
/** Validate draft image paths without committing attachment objects. */
|
|
315
|
-
inspectImages(paths: readonly string[])
|
|
353
|
+
inspectImages: (paths: readonly string[]) => Promise<readonly ImagePathInspection[]>
|
|
316
354
|
/** Validate, normalize and persist images immediately before submission. */
|
|
317
|
-
prepareImages(paths: readonly string[], signal?: AbortSignal)
|
|
355
|
+
prepareImages: (paths: readonly string[], signal?: AbortSignal) => Promise<readonly ImageBlock[]>
|
|
318
356
|
/** Validate draft non-image file paths without committing attachment objects. */
|
|
319
|
-
inspectFiles(paths: readonly string[])
|
|
357
|
+
inspectFiles: (paths: readonly string[]) => Promise<readonly FilePathInspection[]>
|
|
320
358
|
/** Persist non-image files immediately before submission as durable file blocks. */
|
|
321
|
-
prepareFiles(paths: readonly string[], signal?: AbortSignal)
|
|
359
|
+
prepareFiles: (paths: readonly string[], signal?: AbortSignal) => Promise<readonly FileBlock[]>
|
|
322
360
|
/** Apply one /model selection (with an advertised reasoning effort, when picked); returns the display label. */
|
|
323
|
-
selectModel(row: ModelRow, effortId?: string)
|
|
361
|
+
selectModel: (row: ModelRow, effortId?: string) => string
|
|
324
362
|
/** The /subagent override label, '' when delegated agents follow the current model. */
|
|
325
363
|
subagentModel: string
|
|
326
364
|
/** Apply one /subagent model pick; returns the override label. */
|
|
327
|
-
setSubagentModel(row: ModelRow, effortId?: string)
|
|
365
|
+
setSubagentModel: (row: ModelRow, effortId?: string) => string
|
|
328
366
|
/** Drop the /subagent override (delegated agents follow the current model). */
|
|
329
|
-
clearSubagentModel()
|
|
367
|
+
clearSubagentModel: () => void
|
|
330
368
|
/** Delete one session subtree; resolves with the outcome line. */
|
|
331
|
-
deleteSession(id: string)
|
|
369
|
+
deleteSession: (id: string) => Promise<string>
|
|
332
370
|
/** Load provider/settings/credential facts for the optional /model provider stage. */
|
|
333
|
-
loadModelProviders
|
|
371
|
+
loadModelProviders?: () => Promise<ProviderSettingsDirectory>
|
|
334
372
|
/** Subscribe to Harness credential/settings/adapter invalidations while /model is open. */
|
|
335
|
-
subscribeModelProviders
|
|
373
|
+
subscribeModelProviders?: (listener: () => void) => () => void
|
|
336
374
|
/** Store or rotate one provider credential through the Harness credential service. */
|
|
337
|
-
saveModelProviderCredential
|
|
375
|
+
saveModelProviderCredential?: (target: ProviderTargetView, key: string) => Promise<void>
|
|
338
376
|
/** Remove one writable provider credential without removing its settings profile. */
|
|
339
|
-
unsetModelProviderCredential
|
|
377
|
+
unsetModelProviderCredential?: (target: ProviderTargetView) => Promise<void>
|
|
340
378
|
/** Remove one user-owned provider profile and its page-managed credential. */
|
|
341
|
-
removeModelProvider
|
|
379
|
+
removeModelProvider?: (target: ProviderTargetView) => Promise<void>
|
|
342
380
|
/** Save endpoint and explicit model capacities through the provider profile. */
|
|
343
|
-
saveModelProviderConfiguration
|
|
381
|
+
saveModelProviderConfiguration?: (target: ProviderTargetView, configuration: ProviderConfiguration) => Promise<void>
|
|
344
382
|
/**
|
|
345
383
|
* Interrogate the provider's real endpoint (typed key wins over the stored
|
|
346
384
|
* credential) for the models it actually serves — the discovery stage of
|
|
347
385
|
* the provider setup page.
|
|
348
386
|
*/
|
|
349
|
-
discoverModelProvider
|
|
387
|
+
discoverModelProvider?: (
|
|
350
388
|
target: ProviderTargetView,
|
|
351
389
|
request: { readonly apiKey?: string; readonly baseURL?: string },
|
|
352
390
|
signal?: AbortSignal,
|
|
353
|
-
)
|
|
391
|
+
) => Promise<readonly DiscoveredModelView[]>
|
|
354
392
|
/** Provider authorization flows and value-free stored-record facts. */
|
|
355
|
-
loadProviderAuthorizations
|
|
356
|
-
subscribeProviderAuthorizations
|
|
357
|
-
beginProviderAuthorization
|
|
393
|
+
loadProviderAuthorizations?: () => Promise<ProviderAuthorizationDirectory>
|
|
394
|
+
subscribeProviderAuthorizations?: (listener: () => void) => () => void
|
|
395
|
+
beginProviderAuthorization?: (
|
|
358
396
|
row: ProviderAuthorizationRow,
|
|
359
397
|
method: string,
|
|
360
398
|
interaction: AuthorizationInteraction,
|
|
361
399
|
signal: AbortSignal,
|
|
362
|
-
)
|
|
363
|
-
cancelProviderAuthorization
|
|
364
|
-
logoutProviderAuthorization
|
|
365
|
-
openAuthorizationUrl
|
|
366
|
-
copyTextValue
|
|
400
|
+
) => Promise<AuthorizationStatus>
|
|
401
|
+
cancelProviderAuthorization?: (row: ProviderAuthorizationRow) => void
|
|
402
|
+
logoutProviderAuthorization?: (row: ProviderAuthorizationRow) => Promise<void>
|
|
403
|
+
openAuthorizationUrl?: (url: string) => boolean
|
|
404
|
+
copyTextValue?: (text: string) => Promise<void>
|
|
367
405
|
/** Cycle to the next mode station (Shift+Tab): a permission preset or a plan switch; returns the notice label. */
|
|
368
|
-
cycleMode()
|
|
406
|
+
cycleMode: () => string
|
|
369
407
|
/** Pre-session plan choice: shows the plan badge before the first session exists. */
|
|
370
408
|
pendingPlan?: boolean
|
|
371
409
|
/** Select or inspect a permission preset without requiring a pre-existing session. */
|
|
372
|
-
setPermission(id: string)
|
|
410
|
+
setPermission: (id: string) => string
|
|
373
411
|
/** Export the transcript to a markdown file (/export [path]); reports via notices. */
|
|
374
|
-
exportTranscript(argument: string)
|
|
412
|
+
exportTranscript: (argument: string) => Promise<void>
|
|
375
413
|
/** Rename the session (/title <text>); returns the outcome line for the notice. */
|
|
376
|
-
renameTitle(argument: string)
|
|
414
|
+
renameTitle: (argument: string) => string
|
|
377
415
|
/** Copy the latest complete assistant response; resolves to notice text. */
|
|
378
|
-
copyLastResponse()
|
|
416
|
+
copyLastResponse: () => Promise<string>
|
|
379
417
|
/** Load a complete read-only Git diff for the file-oriented viewport. */
|
|
380
|
-
loadGitDiff(argument: string)
|
|
418
|
+
loadGitDiff: (argument: string) => Promise<GitDiffView>
|
|
419
|
+
/** Local branches for the /review picker (absent: the picker hides the branch phase's list). */
|
|
420
|
+
listReviewBranches?: (signal?: AbortSignal) => Promise<readonly ReviewBranch[]>
|
|
421
|
+
/** Recent commits on the current branch for the /review picker. */
|
|
422
|
+
listReviewCommits?: (signal?: AbortSignal) => Promise<readonly ReviewCommit[]>
|
|
381
423
|
/** Start a model review after applying the read-only permission preset. */
|
|
382
|
-
reviewChanges(
|
|
424
|
+
reviewChanges: (selection: ReviewSelection) => void
|
|
383
425
|
/** Preset/session/plugin kernel operations. */
|
|
384
|
-
loadPresets()
|
|
385
|
-
switchMode(id: string)
|
|
426
|
+
loadPresets: () => Promise<readonly PresetRow[]>
|
|
427
|
+
switchMode: (id: string) => Promise<string>
|
|
386
428
|
/** Load the switchable permission presets for the /permission panel. */
|
|
387
|
-
loadPermissions()
|
|
388
|
-
createSession(mode?: string)
|
|
429
|
+
loadPermissions: () => Promise<readonly PermissionRow[]>
|
|
430
|
+
createSession: (mode?: string) => void
|
|
389
431
|
/** 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)
|
|
432
|
+
forkSession: (argument: string) => void
|
|
433
|
+
loadSessions: (options: SessionDirectoryOptions, signal?: AbortSignal) => Promise<readonly SessionRow[]>
|
|
434
|
+
loadSessionTranscript: (id: string, signal?: AbortSignal) => Promise<string>
|
|
435
|
+
/** Read the current session's usage blocks (projections plus per-turn fold). */
|
|
436
|
+
loadUsage: () => Promise<UsageView>
|
|
437
|
+
/**
|
|
438
|
+
* Full-text search over every persisted session (the in-process
|
|
439
|
+
* session-query engine). Absent when the deployment disabled the row;
|
|
440
|
+
* /search degrades to a notice instead of opening the panel.
|
|
441
|
+
*/
|
|
442
|
+
searchSessions?: (query: string, signal?: AbortSignal) => Promise<readonly SearchRow[]>
|
|
393
443
|
/** Load this session's subagent conversations (children by lineage). */
|
|
394
|
-
loadSubagents()
|
|
395
|
-
switchSession(row: SessionRow)
|
|
396
|
-
cancelSessionSwitch()
|
|
397
|
-
loadPlugins()
|
|
444
|
+
loadSubagents: () => Promise<readonly SessionRow[]>
|
|
445
|
+
switchSession: (row: SessionRow) => void
|
|
446
|
+
cancelSessionSwitch: () => boolean
|
|
447
|
+
loadPlugins: () => readonly PluginRow[]
|
|
398
448
|
/** Caller-visible background jobs (the host jobs registry, read-only). */
|
|
399
|
-
loadJobs()
|
|
449
|
+
loadJobs: () => readonly JobRow[]
|
|
400
450
|
/** Probe the launcher's aligned update plan (read-only; never installs). */
|
|
401
|
-
probeUpdate()
|
|
451
|
+
probeUpdate: () => Promise<LauncherUpdateStatus>
|
|
402
452
|
/** Run the launcher's aligned update; streams sanitized lines; resolves with the exit code. */
|
|
403
|
-
applyUpdate(onLine: (line: string) => void
|
|
453
|
+
applyUpdate: (onLine: (line: string) => void, plan?: { readonly dshSpec: string; readonly codeSpec: string; readonly pluginSpecs: readonly string[] }) => Promise<number>
|
|
404
454
|
/** Registers the app's notice channel with the runner (called once on mount). */
|
|
405
|
-
onBridgeReady(bridge: { notify(text: string, tone?: NoticeTone)
|
|
455
|
+
onBridgeReady: (bridge: { notify: (text: string, tone?: NoticeTone) => void }) => void
|
|
406
456
|
/** Ordered enabled status items (/statusline config); the runner owns persistence. */
|
|
407
457
|
statusline: readonly string[]
|
|
408
458
|
/** Persist a new statusline item set; the runner surfaces IO failures as notices. */
|
|
409
|
-
saveStatusline(items: readonly string[])
|
|
459
|
+
saveStatusline: (items: readonly string[]) => void
|
|
460
|
+
/** Apply and persist one /language selection; the runner owns the language.json file. */
|
|
461
|
+
saveLanguage: (name: LanguageName) => void
|
|
410
462
|
/** Apply and persist one /theme selection; the runner owns the theme.json file. */
|
|
411
|
-
saveTheme
|
|
463
|
+
saveTheme?: (name: ThemeName) => void
|
|
412
464
|
/** Whether timed animations run at startup (animations.json; on by default
|
|
413
465
|
* — like parseAnimationsPref, only an explicit false disables them). */
|
|
414
466
|
animations?: boolean
|
|
415
467
|
/** Apply and persist one /animation toggle; the runner owns the file. */
|
|
416
|
-
saveAnimations
|
|
468
|
+
saveAnimations?: (enabled: boolean) => void
|
|
417
469
|
/** Persistent cross-session input history (oldest first); the runner owns the file. */
|
|
418
470
|
history: readonly string[]
|
|
419
471
|
/** Persist one submitted prompt to the global history file. */
|
|
420
|
-
recordHistory(text: string)
|
|
421
|
-
/**
|
|
422
|
-
|
|
472
|
+
recordHistory: (text: string) => void
|
|
473
|
+
/** Mutate one next-turn inbox message; durable inbox splices reconcile the result. */
|
|
474
|
+
updateQueued?: (messageId: string, action: QueueMutation) => void
|
|
423
475
|
/** Apply the Ctrl+R terminal passthrough to the detected editor (/vscode-keys); resolves to a one-line summary. */
|
|
424
|
-
applyEditorKeys()
|
|
476
|
+
applyEditorKeys: () => Promise<string>
|
|
425
477
|
}
|
|
426
478
|
|
|
427
479
|
/** Pad text with spaces to a visible-column target (menu name column). */
|
|
@@ -475,7 +527,13 @@ function useStableInput(handler: (input: string, key: Key) => void, active: bool
|
|
|
475
527
|
*/
|
|
476
528
|
function BusyChase({ animated = true }: { animated?: boolean }): ReactElement {
|
|
477
529
|
const tick = useFrames(BUSY_CHASE_TICK_MS, animated)
|
|
478
|
-
|
|
530
|
+
// Flowing themes (prismatic, rainbow) ride their anchor walk while busy;
|
|
531
|
+
// every other theme (and the frozen state) keeps the palette's live accent.
|
|
532
|
+
const flow = themeFlow()
|
|
533
|
+
const marker = flow !== undefined && animated
|
|
534
|
+
? flowColor(tick * BUSY_CHASE_TICK_MS + flow.phaseMs, flow.anchors)
|
|
535
|
+
: getPalette().brandBright
|
|
536
|
+
return createElement(Text, { color: inkColor(marker) }, busyChaseFrame(tick) + ' ')
|
|
479
537
|
}
|
|
480
538
|
|
|
481
539
|
/** Blinking block caret appended to streaming text; solid when frozen. */
|
|
@@ -485,7 +543,7 @@ function Caret({ animated = true }: { animated?: boolean }): ReactElement {
|
|
|
485
543
|
}
|
|
486
544
|
|
|
487
545
|
/** One resettable input-caret phase shared by the entire composer. */
|
|
488
|
-
function useCursorBlink(active: boolean): { visible: boolean; reset()
|
|
546
|
+
function useCursorBlink(active: boolean): { visible: boolean; reset: () => void } {
|
|
489
547
|
const [epoch, setEpoch] = useState(0)
|
|
490
548
|
const [visible, setVisible] = useState(true)
|
|
491
549
|
useEffect(() => {
|
|
@@ -514,6 +572,13 @@ function useCursorBlink(active: boolean): { visible: boolean; reset(): void } {
|
|
|
514
572
|
function ShimmerLine({ text, animated = true }: { text: string; animated?: boolean }): ReactElement {
|
|
515
573
|
const tick = useFrames(DEEP_DIVING_SHIMMER_TICK_MS, animated)
|
|
516
574
|
const palette = getPalette()
|
|
575
|
+
// Flowing themes walk their anchors for the shimmer highlight so
|
|
576
|
+
// streaming text glows along the spectrum; other themes keep the bright
|
|
577
|
+
// accent.
|
|
578
|
+
const flow = themeFlow()
|
|
579
|
+
const highlight = flow !== undefined && animated
|
|
580
|
+
? flowColor(tick * DEEP_DIVING_SHIMMER_TICK_MS + flow.phaseMs, flow.anchors)
|
|
581
|
+
: palette.brandBright
|
|
517
582
|
const graphemes = splitGraphemes(text)
|
|
518
583
|
return createElement(
|
|
519
584
|
Text,
|
|
@@ -527,8 +592,8 @@ function ShimmerLine({ text, animated = true }: { text: string; animated?: boole
|
|
|
527
592
|
color: inkColor(!animated
|
|
528
593
|
? (sparkle ? palette.brandBright : palette.brandDeep)
|
|
529
594
|
: sparkle
|
|
530
|
-
? deepDivingSparkColor(tick, palette.brandDeep,
|
|
531
|
-
: deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep,
|
|
595
|
+
? deepDivingSparkColor(tick, palette.brandDeep, highlight)
|
|
596
|
+
: deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep, highlight)),
|
|
532
597
|
bold: sparkle || undefined,
|
|
533
598
|
},
|
|
534
599
|
grapheme.text,
|
|
@@ -625,6 +690,18 @@ function segmentProps(style: MdSegment['style']): {
|
|
|
625
690
|
return { color: undefined, bold: true, italic: true, strikethrough: undefined }
|
|
626
691
|
case 'strike':
|
|
627
692
|
return { color: inkColor(getPalette().dim), bold: undefined, italic: undefined, strikethrough: true }
|
|
693
|
+
case 'diffAdd':
|
|
694
|
+
case 'diffDel':
|
|
695
|
+
// Inline-markdown twin of lineStyleProps' diff cases: tinted rows for
|
|
696
|
+
// ```diff fences rendered through the markdown span path. The diff
|
|
697
|
+
// foreground tokens stay AA-legible both on the row tints (when the
|
|
698
|
+
// background rides along) and on the plain terminal background.
|
|
699
|
+
return {
|
|
700
|
+
color: inkColor(style === 'diffAdd' ? getPalette().diffAddFg : getPalette().diffDelFg),
|
|
701
|
+
bold: undefined,
|
|
702
|
+
italic: undefined,
|
|
703
|
+
strikethrough: undefined,
|
|
704
|
+
}
|
|
628
705
|
default:
|
|
629
706
|
return { color: undefined, bold: undefined, italic: undefined, strikethrough: undefined }
|
|
630
707
|
}
|
|
@@ -637,20 +714,46 @@ function lineStyleProps(style: LineStyle): {
|
|
|
637
714
|
italic: boolean | undefined
|
|
638
715
|
strikethrough: boolean | undefined
|
|
639
716
|
dimColor: boolean | undefined
|
|
717
|
+
backgroundColor: string | undefined
|
|
640
718
|
} {
|
|
641
719
|
switch (style) {
|
|
642
720
|
case 'brand':
|
|
643
|
-
return { color: inkColor(getPalette().brandBright), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined }
|
|
721
|
+
return { color: inkColor(getPalette().brandBright), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: undefined }
|
|
644
722
|
case 'success':
|
|
645
|
-
return { color: inkColor(getPalette().success), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined }
|
|
723
|
+
return { color: inkColor(getPalette().success), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: undefined }
|
|
646
724
|
case 'error':
|
|
647
|
-
return { color: inkColor(getPalette().error), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined }
|
|
725
|
+
return { color: inkColor(getPalette().error), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: undefined }
|
|
648
726
|
case 'warn':
|
|
649
|
-
return { color: inkColor(getPalette().warn), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined }
|
|
727
|
+
return { color: inkColor(getPalette().warn), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: undefined }
|
|
650
728
|
case 'dimItalic':
|
|
651
|
-
return { color: inkColor(getPalette().dim), bold: undefined, italic: true, strikethrough: undefined, dimColor: undefined }
|
|
729
|
+
return { color: inkColor(getPalette().dim), bold: undefined, italic: true, strikethrough: undefined, dimColor: undefined, backgroundColor: undefined }
|
|
730
|
+
// Codex diff rendering: added/removed lines carry a theme tint behind
|
|
731
|
+
// the sign and text, with the AA-tuned diff foreground tokens on top; the
|
|
732
|
+
// depth gate turns this into plain foreground styling on 16-color
|
|
733
|
+
// terminals.
|
|
734
|
+
case 'diffAdd':
|
|
735
|
+
return { color: inkColor(getPalette().diffAddFg), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: diffBackground('diffAdd') }
|
|
736
|
+
case 'diffDel':
|
|
737
|
+
return { color: inkColor(getPalette().diffDelFg), bold: undefined, italic: undefined, strikethrough: undefined, dimColor: undefined, backgroundColor: diffBackground('diffDel') }
|
|
738
|
+
// Prompt rows: one full-width bar per delivery kind. The tint comes from
|
|
739
|
+
// the row token and the foreground from its AA-tuned twin; the depth gate
|
|
740
|
+
// inside rowBackground degrades this to foreground-only on 16-color
|
|
741
|
+
// terminals, matching the diff rows.
|
|
742
|
+
case 'promptRow':
|
|
743
|
+
case 'promptQueuedRow':
|
|
744
|
+
case 'promptSteeredRow': {
|
|
745
|
+
const tokens = promptRowTokens(style === 'promptQueuedRow' ? 'queued' : style === 'promptSteeredRow' ? 'steered' : undefined)
|
|
746
|
+
return {
|
|
747
|
+
color: inkColor(getPalette()[tokens.fg]),
|
|
748
|
+
bold: undefined,
|
|
749
|
+
italic: undefined,
|
|
750
|
+
strikethrough: undefined,
|
|
751
|
+
dimColor: undefined,
|
|
752
|
+
backgroundColor: rowBackground(tokens.fg),
|
|
753
|
+
}
|
|
754
|
+
}
|
|
652
755
|
default:
|
|
653
|
-
return { ...segmentProps(style), dimColor: undefined }
|
|
756
|
+
return { ...segmentProps(style), dimColor: undefined, backgroundColor: undefined }
|
|
654
757
|
}
|
|
655
758
|
}
|
|
656
759
|
|
|
@@ -674,7 +777,7 @@ function StyledRows({ lines }: { lines: readonly StyledLine[] }): ReactElement {
|
|
|
674
777
|
}
|
|
675
778
|
|
|
676
779
|
/** File-oriented, color-coded unified diff viewport. */
|
|
677
|
-
function DiffPanel({ view, onClose }: { view: GitDiffView; onClose()
|
|
780
|
+
function DiffPanel({ view, onClose }: { view: GitDiffView; onClose: () => void }): ReactElement {
|
|
678
781
|
const stdout = useStdout().stdout
|
|
679
782
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
680
783
|
const [fileIndex, setFileIndex] = useState(0)
|
|
@@ -682,15 +785,7 @@ function DiffPanel({ view, onClose }: { view: GitDiffView; onClose(): void }): R
|
|
|
682
785
|
const file = view.files[fileIndex]
|
|
683
786
|
const lines = useMemo(() => {
|
|
684
787
|
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))
|
|
788
|
+
return fillDiffLineBars(file.lines.flatMap(line => styledLines([lineSegment(line, diffLineStyle(line))], viewport.contentColumns)), viewport.contentColumns)
|
|
694
789
|
}, [file, viewport.contentColumns])
|
|
695
790
|
const visibleScroll = clampScroll(scroll, lines.length, viewport.bodyRows)
|
|
696
791
|
useInput((input, key) => {
|
|
@@ -709,15 +804,16 @@ function DiffPanel({ view, onClose }: { view: GitDiffView; onClose(): void }): R
|
|
|
709
804
|
else if (key.pageUp) setScroll(current => moveScroll(current, -viewport.bodyRows, lines.length, viewport.bodyRows))
|
|
710
805
|
else if (key.pageDown) setScroll(current => moveScroll(current, viewport.bodyRows, lines.length, viewport.bodyRows))
|
|
711
806
|
})
|
|
712
|
-
if (viewport.compact) return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(
|
|
807
|
+
if (viewport.compact) return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.diff.compact', { title: view.title, files: view.files.length }), viewport.contentColumns))
|
|
808
|
+
const accent = panelAccent('diff', getPalette().dim, getPalette().brand)
|
|
713
809
|
return createElement(
|
|
714
810
|
Box,
|
|
715
|
-
{ flexDirection: 'column', borderStyle: 'round', borderColor: inkColor(
|
|
716
|
-
createElement(Text, { color: inkColor(
|
|
811
|
+
{ flexDirection: 'column', borderStyle: 'round', borderColor: inkColor(accent.border), paddingX: 1 },
|
|
812
|
+
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
813
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
718
814
|
createElement(StyledRows, { lines: lines.slice(visibleScroll, visibleScroll + viewport.bodyRows) }),
|
|
719
815
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
720
|
-
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns('
|
|
816
|
+
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(t('panel.diff.footer'), viewport.contentColumns)),
|
|
721
817
|
)
|
|
722
818
|
}
|
|
723
819
|
|
|
@@ -735,6 +831,32 @@ function PanelGap({ visible }: { visible: boolean }): ReactElement | undefined {
|
|
|
735
831
|
* keeps its historical three lines. Short or narrow terminals keep a one-line
|
|
736
832
|
* form without the kernel line.
|
|
737
833
|
*/
|
|
834
|
+
/**
|
|
835
|
+
* One whale-glyph row painted as a frozen seven-color spectrum (column 0
|
|
836
|
+
* red, column 25 violet). Spaces stay uncolored so the silhouette punches
|
|
837
|
+
* through; adjacent same-hue blocks merge into one span.
|
|
838
|
+
*/
|
|
839
|
+
function rainbowGlyphRow(row: string, rowKey: number): ReactElement {
|
|
840
|
+
const span = Math.max(1, WHALE_GLYPH_COLUMNS - 1)
|
|
841
|
+
const children: ReactElement[] = []
|
|
842
|
+
let start = 0
|
|
843
|
+
while (start < row.length) {
|
|
844
|
+
if (row[start] === ' ') {
|
|
845
|
+
let end = start + 1
|
|
846
|
+
while (end < row.length && row[end] === ' ') end += 1
|
|
847
|
+
children.push(createElement(Text, { key: start }, row.slice(start, end)))
|
|
848
|
+
start = end
|
|
849
|
+
continue
|
|
850
|
+
}
|
|
851
|
+
const color = inkColor(rainbowSpectrumHue(start / span))
|
|
852
|
+
let end = start + 1
|
|
853
|
+
while (end < row.length && row[end] !== ' ' && inkColor(rainbowSpectrumHue(end / span)) === color) end += 1
|
|
854
|
+
children.push(createElement(Text, { key: start, color }, row.slice(start, end)))
|
|
855
|
+
start = end
|
|
856
|
+
}
|
|
857
|
+
return createElement(Text, { key: rowKey }, ...children)
|
|
858
|
+
}
|
|
859
|
+
|
|
738
860
|
function Header({ resumed }: { resumed: boolean }): ReactElement {
|
|
739
861
|
const stdout = useStdout().stdout
|
|
740
862
|
const rows = stdout?.rows ?? 40
|
|
@@ -749,7 +871,7 @@ function Header({ resumed }: { resumed: boolean }): ReactElement {
|
|
|
749
871
|
const copyWidths = [visibleColumns(title), visibleColumns(slogan), visibleColumns(hint)]
|
|
750
872
|
if (kernelLine !== undefined) copyWidths.push(visibleColumns(kernelLine))
|
|
751
873
|
const copyColumns = Math.max(...copyWidths)
|
|
752
|
-
const compact = `${title} · ${hint}`
|
|
874
|
+
const compact = kernelLine === undefined ? `${title} · ${hint}` : `${title} · ${kernelLine} · ${hint}`
|
|
753
875
|
if (rows < 20 || columns < WHALE_GLYPH_COLUMNS + copyColumns + 10) {
|
|
754
876
|
return createElement(
|
|
755
877
|
Box,
|
|
@@ -768,7 +890,10 @@ function Header({ resumed }: { resumed: boolean }): ReactElement {
|
|
|
768
890
|
createElement(
|
|
769
891
|
Box,
|
|
770
892
|
{ flexDirection: 'column', width: WHALE_GLYPH_COLUMNS, justifyContent: 'center' },
|
|
771
|
-
...WHALE_GLYPH.map((row, index) =>
|
|
893
|
+
...WHALE_GLYPH.map((row, index) =>
|
|
894
|
+
isRainbow()
|
|
895
|
+
? rainbowGlyphRow(row, index)
|
|
896
|
+
: createElement(Text, { key: index, color: inkColor(getPalette().brand) }, row)),
|
|
772
897
|
),
|
|
773
898
|
createElement(
|
|
774
899
|
Box,
|
|
@@ -803,7 +928,7 @@ function todoMark(status: TodoItem['status']): string {
|
|
|
803
928
|
function AgentsLine({ rows, total }: { rows: readonly SubagentRow[]; total: number }): ReactElement | undefined {
|
|
804
929
|
if (rows.length === 0) return undefined
|
|
805
930
|
const running = rows.filter(row => row.state !== 'done').length
|
|
806
|
-
const newest = [...rows].sort((left, right) => right.updatedAt - left.updatedAt)[0]
|
|
931
|
+
const newest = [...rows].sort((left, right) => right.updatedAt - left.updatedAt)[0]
|
|
807
932
|
const mark = newest.state === 'done' ? '✓' : newest.state === 'idle' ? '⏸' : '●'
|
|
808
933
|
return createElement(
|
|
809
934
|
Box,
|
|
@@ -856,7 +981,7 @@ function TodoListPanel({ todos, onClose }: { todos: readonly TodoItem[]; onClose
|
|
|
856
981
|
const inProgress = todos.filter(todo => todo.status === 'in_progress').length
|
|
857
982
|
const pending = todos.length - completed - inProgress
|
|
858
983
|
const rows = todos.length === 0
|
|
859
|
-
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' }, '
|
|
984
|
+
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' }, ` ${t('panel.todos.empty')}`)]
|
|
860
985
|
: todos.map(todo => createElement(
|
|
861
986
|
Text,
|
|
862
987
|
{ key: todo.content, dimColor: true, wrap: 'truncate-end' },
|
|
@@ -885,40 +1010,202 @@ function TodoListPanel({ todos, onClose }: { todos: readonly TodoItem[]; onClose
|
|
|
885
1010
|
})
|
|
886
1011
|
|
|
887
1012
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
888
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('todos
|
|
1013
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.todos.compact'), viewport.contentColumns))
|
|
889
1014
|
}
|
|
890
1015
|
|
|
1016
|
+
const accent = panelAccent('todos', getPalette().brand)
|
|
891
1017
|
return createElement(
|
|
892
1018
|
Box,
|
|
893
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
894
|
-
createElement(Text, { color: inkColor(
|
|
1019
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
1020
|
+
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
1021
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
896
1022
|
...rows.slice(visibleScroll, visibleScroll + viewport.bodyRows),
|
|
897
1023
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
898
|
-
createElement(Text, {
|
|
1024
|
+
createElement(Text, { wrap: 'truncate-end' }, dim(truncateColumns(t('panel.todos.footer'), viewport.contentColumns))),
|
|
899
1025
|
)
|
|
900
1026
|
}
|
|
901
1027
|
|
|
902
1028
|
const MemoTodoListPanel = memo(TodoListPanel)
|
|
903
1029
|
|
|
1030
|
+
/** Rows in the exact next-turn inbox order, never transcript append order. */
|
|
1031
|
+
export function queuedInboxRows(
|
|
1032
|
+
entries: readonly TranscriptEntry[],
|
|
1033
|
+
ids: readonly string[],
|
|
1034
|
+
): readonly Extract<TranscriptEntry, { kind: 'pending' }>[] {
|
|
1035
|
+
const byId = new Map<string, Extract<TranscriptEntry, { kind: 'pending' }>>()
|
|
1036
|
+
for (const entry of entries) {
|
|
1037
|
+
if (entry.kind === 'pending' && entry.target === 'next-turn') byId.set(entry.messageId, entry)
|
|
1038
|
+
}
|
|
1039
|
+
return ids.flatMap(id => {
|
|
1040
|
+
const row = byId.get(id)
|
|
1041
|
+
return row === undefined ? [] : [row]
|
|
1042
|
+
})
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/** A bounded, keyboard-owned management surface for the durable next-turn inbox. */
|
|
1046
|
+
function QueuePanel({ rows, busy, update, onClose }: {
|
|
1047
|
+
rows: readonly Extract<TranscriptEntry, { kind: 'pending' }>[]
|
|
1048
|
+
busy: boolean
|
|
1049
|
+
update?: (messageId: string, action: QueueMutation) => void
|
|
1050
|
+
onClose: () => void
|
|
1051
|
+
}): ReactElement {
|
|
1052
|
+
const stdout = useStdout().stdout
|
|
1053
|
+
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
1054
|
+
const [selected, setSelected] = useState(0)
|
|
1055
|
+
const [scroll, setScroll] = useState(0)
|
|
1056
|
+
const [editing, setEditing] = useState<{ messageId: string; text: string; cursor: number } | undefined>(undefined)
|
|
1057
|
+
// Ink can deliver a following key before its effect swaps the input
|
|
1058
|
+
// listener after an edit-mode render; this ref keeps the editor's key
|
|
1059
|
+
// stream coherent while the visible state catches up.
|
|
1060
|
+
const editingRef = useRef(editing)
|
|
1061
|
+
const current = rows[selected]
|
|
1062
|
+
const visibleScroll = revealRow(clampScroll(scroll, rows.length, viewport.bodyRows), selected, rows.length, viewport.bodyRows)
|
|
1063
|
+
const move = (delta: number): void => {
|
|
1064
|
+
setSelected(current => Math.max(0, Math.min(rows.length - 1, current + delta)))
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
useEffect(() => {
|
|
1068
|
+
setSelected(current => Math.max(0, Math.min(rows.length - 1, current)))
|
|
1069
|
+
if (editing !== undefined && !rows.some(row => row.messageId === editing.messageId)) {
|
|
1070
|
+
editingRef.current = undefined
|
|
1071
|
+
setEditing(undefined)
|
|
1072
|
+
}
|
|
1073
|
+
}, [rows, editing])
|
|
1074
|
+
useEffect(() => {
|
|
1075
|
+
if (visibleScroll !== scroll) setScroll(visibleScroll)
|
|
1076
|
+
}, [visibleScroll, scroll])
|
|
1077
|
+
|
|
1078
|
+
useStableInput((input, key) => {
|
|
1079
|
+
const activeEdit = editingRef.current
|
|
1080
|
+
if (activeEdit !== undefined) {
|
|
1081
|
+
if (key.escape) {
|
|
1082
|
+
editingRef.current = undefined
|
|
1083
|
+
setEditing(undefined)
|
|
1084
|
+
return
|
|
1085
|
+
}
|
|
1086
|
+
if (key.return) {
|
|
1087
|
+
if (activeEdit.text.trim() !== '') update?.(activeEdit.messageId, { kind: 'edit', text: activeEdit.text })
|
|
1088
|
+
editingRef.current = undefined
|
|
1089
|
+
setEditing(undefined)
|
|
1090
|
+
return
|
|
1091
|
+
}
|
|
1092
|
+
if (key.leftArrow) {
|
|
1093
|
+
const next = { ...activeEdit, cursor: moveCursorBy(activeEdit.text, activeEdit.cursor, -1) }
|
|
1094
|
+
editingRef.current = next
|
|
1095
|
+
setEditing(next)
|
|
1096
|
+
return
|
|
1097
|
+
}
|
|
1098
|
+
if (key.rightArrow) {
|
|
1099
|
+
const next = { ...activeEdit, cursor: moveCursorBy(activeEdit.text, activeEdit.cursor, 1) }
|
|
1100
|
+
editingRef.current = next
|
|
1101
|
+
setEditing(next)
|
|
1102
|
+
return
|
|
1103
|
+
}
|
|
1104
|
+
// Ink 5 reports 0x7F (backspace) and the forward-delete sequence as the
|
|
1105
|
+
// same `key.delete`, so a bare Delete binding here would erase on a
|
|
1106
|
+
// habitual Backspace. This management surface keeps `d` as its only
|
|
1107
|
+
// removal key instead of guessing which byte arrived.
|
|
1108
|
+
if (key.backspace || key.delete) {
|
|
1109
|
+
const edit = deleteBackward(activeEdit.text, activeEdit.cursor)
|
|
1110
|
+
const next = { ...activeEdit, text: edit.value, cursor: edit.cursor }
|
|
1111
|
+
editingRef.current = next
|
|
1112
|
+
setEditing(next)
|
|
1113
|
+
return
|
|
1114
|
+
}
|
|
1115
|
+
if (input !== '' && !key.ctrl && !key.meta) {
|
|
1116
|
+
const edit = insertText(activeEdit.text, activeEdit.cursor, input)
|
|
1117
|
+
const next = { ...activeEdit, text: edit.value, cursor: edit.cursor }
|
|
1118
|
+
editingRef.current = next
|
|
1119
|
+
setEditing(next)
|
|
1120
|
+
}
|
|
1121
|
+
return
|
|
1122
|
+
}
|
|
1123
|
+
if (key.escape || input === 'q') {
|
|
1124
|
+
onClose()
|
|
1125
|
+
return
|
|
1126
|
+
}
|
|
1127
|
+
if (key.upArrow) move(-1)
|
|
1128
|
+
else if (key.downArrow) move(1)
|
|
1129
|
+
else if (key.pageUp) move(-Math.max(1, viewport.bodyRows - 1))
|
|
1130
|
+
else if (key.pageDown) move(Math.max(1, viewport.bodyRows - 1))
|
|
1131
|
+
else if (input === 'g') setSelected(0)
|
|
1132
|
+
else if (input === 'G') setSelected(Math.max(0, rows.length - 1))
|
|
1133
|
+
else if (input === 'e' && current !== undefined) {
|
|
1134
|
+
// Text is editable on every row: an edit rewrites what the user typed
|
|
1135
|
+
// and carries the row's attachments through untouched, which is exactly
|
|
1136
|
+
// what the row's read-only attachment marker promises.
|
|
1137
|
+
const next = { messageId: current.messageId, text: current.text, cursor: current.text.length }
|
|
1138
|
+
editingRef.current = next
|
|
1139
|
+
setEditing(next)
|
|
1140
|
+
}
|
|
1141
|
+
else if (input === 'd' && current !== undefined) update?.(current.messageId, { kind: 'remove' })
|
|
1142
|
+
else if (key.return && current !== undefined && busy) update?.(current.messageId, { kind: 'steer' })
|
|
1143
|
+
}, true)
|
|
1144
|
+
|
|
1145
|
+
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
1146
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.queue.compact'), viewport.contentColumns))
|
|
1147
|
+
}
|
|
1148
|
+
const body = rows.length === 0
|
|
1149
|
+
? [createElement(Text, { key: 'empty', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(t('panel.queue.empty'), viewport.contentColumns))]
|
|
1150
|
+
: rows.map((row, index) => {
|
|
1151
|
+
const selectedRow = index === selected
|
|
1152
|
+
const suffix = (row.images?.length ?? 0) + (row.files?.length ?? 0) > 0 ? ` ${t('panel.queue.attachments')}` : ''
|
|
1153
|
+
if (editing?.messageId === row.messageId) {
|
|
1154
|
+
const before = editing.text.slice(0, editing.cursor)
|
|
1155
|
+
const caret = editing.text.slice(editing.cursor, editing.cursor + 1) || ' '
|
|
1156
|
+
const after = editing.text.slice(editing.cursor + caret.length)
|
|
1157
|
+
return createElement(Text, { key: row.messageId, color: inkColor(getPalette().brandBright), wrap: 'truncate-end' }, truncateColumns(`✎ ${before}[${caret}]${after}`, viewport.contentColumns))
|
|
1158
|
+
}
|
|
1159
|
+
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))
|
|
1160
|
+
})
|
|
1161
|
+
const footer = editing !== undefined
|
|
1162
|
+
? t('panel.queue.editFooter')
|
|
1163
|
+
: busy
|
|
1164
|
+
? t('panel.queue.footerBusy')
|
|
1165
|
+
: t('panel.queue.footerIdle')
|
|
1166
|
+
const accent = panelAccent('queue', getPalette().brand)
|
|
1167
|
+
return createElement(
|
|
1168
|
+
Box,
|
|
1169
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
1170
|
+
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)),
|
|
1171
|
+
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1172
|
+
...body.slice(visibleScroll, visibleScroll + viewport.bodyRows),
|
|
1173
|
+
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1174
|
+
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(footer, viewport.contentColumns)),
|
|
1175
|
+
)
|
|
1176
|
+
}
|
|
1177
|
+
|
|
904
1178
|
/**
|
|
905
1179
|
* Ink props for one status tone: the Codex status-line accent mapping over
|
|
906
1180
|
* the DeepSeek palette, all blue by design — the status bar speaks only in
|
|
907
1181
|
* degrees of blue (deep accent, primary figures, model identity, sky
|
|
908
1182
|
* paths and done states), with amber/red reserved for warnings and errors.
|
|
909
1183
|
*/
|
|
910
|
-
function statusToneProps(tone: StatusTone): {
|
|
1184
|
+
function statusToneProps(tone: StatusTone, flowMs?: number): {
|
|
911
1185
|
color: string | undefined
|
|
912
1186
|
bold: boolean | undefined
|
|
913
1187
|
dimColor: boolean | undefined
|
|
914
1188
|
} {
|
|
1189
|
+
if (isRainbow()) {
|
|
1190
|
+
// Carnival roll: every tone carries its rolled color (adjacent tones in
|
|
1191
|
+
// canonical on-screen order never match, the row boundary included);
|
|
1192
|
+
// the live dot rides the flow walk while busy. Bold emphasis carries
|
|
1193
|
+
// the semantic hierarchy so randomness never hides importance.
|
|
1194
|
+
const emphasized = tone === 'model' || tone === 'success' || tone === 'plan' || tone === 'warn' || tone === 'error'
|
|
1195
|
+
const color = tone === 'live' && flowMs !== undefined
|
|
1196
|
+
? flowColor(flowMs, themeFlow()?.anchors ?? FLOW_ANCHORS)
|
|
1197
|
+
: rainbowRoll().toneColors[tone]
|
|
1198
|
+
return { color: inkColor(color), bold: emphasized || undefined, dimColor: undefined }
|
|
1199
|
+
}
|
|
915
1200
|
switch (tone) {
|
|
916
1201
|
case 'model':
|
|
917
1202
|
// Same tone as the working-directory segment: the model name reads as
|
|
918
1203
|
// a path fact, not a brand accent.
|
|
919
1204
|
return { color: inkColor(getPalette().code), bold: true, dimColor: undefined }
|
|
920
1205
|
case 'live':
|
|
921
|
-
|
|
1206
|
+
// With a flow sample (flowing theme while busy), the live dot rides
|
|
1207
|
+
// the anchor walk; otherwise the palette's live accent.
|
|
1208
|
+
return { color: inkColor(flowMs === undefined ? getPalette().brandBright : flowColor(flowMs, themeFlow()?.anchors ?? FLOW_ANCHORS)), bold: undefined, dimColor: undefined }
|
|
922
1209
|
case 'path':
|
|
923
1210
|
return { color: inkColor(getPalette().code), bold: undefined, dimColor: undefined }
|
|
924
1211
|
case 'branch':
|
|
@@ -980,12 +1267,22 @@ function statusToneProps(tone: StatusTone): {
|
|
|
980
1267
|
* the prompt keeps is always hues[0]. */
|
|
981
1268
|
function deepseekWaveHues(tier: DeepseekWaveTier): readonly [RgbTriple, RgbTriple, RgbTriple] {
|
|
982
1269
|
const palette = getPalette()
|
|
1270
|
+
// Rainbow waves pick three SPECTRALLY SPREAD anchors from the rolled
|
|
1271
|
+
// pool — spectral neighbors would wash into one hue across the band.
|
|
1272
|
+
if (isRainbow()) {
|
|
1273
|
+
const anchors = rainbowRoll().flowAnchors
|
|
1274
|
+
const stride = Math.max(1, Math.floor(anchors.length / 3))
|
|
1275
|
+
return [anchors[0], anchors[stride], anchors[stride * 2]]
|
|
1276
|
+
}
|
|
1277
|
+
// Prismatic waves ride the flow anchors for both tiers — the model-switch
|
|
1278
|
+
// easter egg becomes a violet→fuchsia→cyan sweep.
|
|
1279
|
+
if (isPrismatic()) return [FLOW_ANCHORS[0], FLOW_ANCHORS[1], FLOW_ANCHORS[2]]
|
|
983
1280
|
return tier === 'flash'
|
|
984
1281
|
? [palette.brandBright, palette.brand, palette.brandMid]
|
|
985
1282
|
: [palette.brandBright, palette.code, palette.brandMid]
|
|
986
1283
|
}
|
|
987
1284
|
|
|
988
|
-
function StatusLine({ facts, stats, busy, columns, items, onRows }: {
|
|
1285
|
+
function StatusLine({ facts, stats, busy, columns, items, onRows, animated }: {
|
|
989
1286
|
facts: StatusFacts
|
|
990
1287
|
stats: Parameters<typeof layoutStatusBar>[1]
|
|
991
1288
|
busy: boolean
|
|
@@ -994,7 +1291,16 @@ function StatusLine({ facts, stats, busy, columns, items, onRows }: {
|
|
|
994
1291
|
/** Reports the footer's exact physical row count (1 or 2) so the IME
|
|
995
1292
|
* anchor ledger below the composer stays exact. */
|
|
996
1293
|
onRows?: (rows: 1 | 2) => void
|
|
1294
|
+
/** Whether timed animations run (the persisted preference). */
|
|
1295
|
+
animated: boolean
|
|
997
1296
|
}): ReactElement {
|
|
1297
|
+
// Flowing-theme busy flow: the identity cluster's live dot cycles the
|
|
1298
|
+
// anchor walk while a turn runs; static themes never start the timer.
|
|
1299
|
+
const flow = themeFlow()
|
|
1300
|
+
const flowActive = animated && busy && flow !== undefined
|
|
1301
|
+
const flowTick = useFrames(BUSY_CHASE_TICK_MS, flowActive)
|
|
1302
|
+
const flowMs = flowActive ? flowTick * BUSY_CHASE_TICK_MS + (flow?.phaseMs ?? 0) : undefined
|
|
1303
|
+
const language = getLanguage()
|
|
998
1304
|
const layout = useMemo(() => layoutStatusBar(facts, stats, Math.max(8, columns - 2), {
|
|
999
1305
|
busy,
|
|
1000
1306
|
items,
|
|
@@ -1018,6 +1324,8 @@ function StatusLine({ facts, stats, busy, columns, items, onRows }: {
|
|
|
1018
1324
|
busy,
|
|
1019
1325
|
columns,
|
|
1020
1326
|
items,
|
|
1327
|
+
// Labels come from t(); a language switch must rebuild the rows.
|
|
1328
|
+
language,
|
|
1021
1329
|
])
|
|
1022
1330
|
// The IME anchor below the composer counts every row between the caret and
|
|
1023
1331
|
// Ink's parked cursor, so the footer reports its exact row count one-way
|
|
@@ -1036,7 +1344,7 @@ function StatusLine({ facts, stats, busy, columns, items, onRows }: {
|
|
|
1036
1344
|
group.spans.forEach((span, spanIndex) => {
|
|
1037
1345
|
leftParts.push(createElement(
|
|
1038
1346
|
Text,
|
|
1039
|
-
{ key: key + 'g' + groupIndex + 's' + spanIndex, wrap: 'truncate-end', ...statusToneProps(span.tone) },
|
|
1347
|
+
{ key: key + 'g' + groupIndex + 's' + spanIndex, wrap: 'truncate-end', ...statusToneProps(span.tone, flowMs) },
|
|
1040
1348
|
span.text,
|
|
1041
1349
|
))
|
|
1042
1350
|
})
|
|
@@ -1048,12 +1356,12 @@ function StatusLine({ facts, stats, busy, columns, items, onRows }: {
|
|
|
1048
1356
|
}
|
|
1049
1357
|
rightParts.push(createElement(
|
|
1050
1358
|
Text,
|
|
1051
|
-
{ key: key + 'r' + index, wrap: 'truncate-end', ...statusToneProps(span.tone) },
|
|
1359
|
+
{ key: key + 'r' + index, wrap: 'truncate-end', ...statusToneProps(span.tone, flowMs) },
|
|
1052
1360
|
span.text,
|
|
1053
1361
|
))
|
|
1054
1362
|
})
|
|
1055
1363
|
if (row.hint) {
|
|
1056
|
-
rightParts.push(createElement(Text, { key: key + 'hint', color: inkColor(getPalette().dim) },
|
|
1364
|
+
rightParts.push(createElement(Text, { key: key + 'hint', color: inkColor(getPalette().dim) }, statusCycleHint()))
|
|
1057
1365
|
}
|
|
1058
1366
|
// Each row already fits the column budget; truncate-end stays as the
|
|
1059
1367
|
// terminal-measurement backstop so a drifting cell count clips instead
|
|
@@ -1131,9 +1439,9 @@ const APPROVAL_OPTIONS: readonly ApprovalOption[] = [
|
|
|
1131
1439
|
function ApprovalBar({ snapshot, locked, notify, interrupt, summarize }: {
|
|
1132
1440
|
snapshot: ApprovalSnapshot
|
|
1133
1441
|
locked: boolean
|
|
1134
|
-
notify(text: string, tone?: NoticeTone)
|
|
1442
|
+
notify: (text: string, tone?: NoticeTone) => void
|
|
1135
1443
|
/** Cancel the running turn (Ctrl+C), matching the composer's busy branch. */
|
|
1136
|
-
interrupt()
|
|
1444
|
+
interrupt: () => boolean
|
|
1137
1445
|
/** Render as the bounded one-line form even on tall terminals (another
|
|
1138
1446
|
* human-asked surface already owns the full panel budget). */
|
|
1139
1447
|
summarize?: boolean
|
|
@@ -1160,7 +1468,7 @@ function ApprovalBar({ snapshot, locked, notify, interrupt, summarize }: {
|
|
|
1160
1468
|
}
|
|
1161
1469
|
ask.answer('rejected')
|
|
1162
1470
|
if (option.key === 'reject-note') {
|
|
1163
|
-
notify('rejected
|
|
1471
|
+
notify(t('notice.rejected'), 'warning')
|
|
1164
1472
|
}
|
|
1165
1473
|
}
|
|
1166
1474
|
|
|
@@ -1184,35 +1492,35 @@ function ApprovalBar({ snapshot, locked, notify, interrupt, summarize }: {
|
|
|
1184
1492
|
return
|
|
1185
1493
|
}
|
|
1186
1494
|
if (key.return) {
|
|
1187
|
-
decide(APPROVAL_OPTIONS[cursor]
|
|
1495
|
+
decide(APPROVAL_OPTIONS[cursor])
|
|
1188
1496
|
return
|
|
1189
1497
|
}
|
|
1190
1498
|
if (key.escape) {
|
|
1191
|
-
decide(APPROVAL_OPTIONS[2]
|
|
1499
|
+
decide(APPROVAL_OPTIONS[2])
|
|
1192
1500
|
return
|
|
1193
1501
|
}
|
|
1194
1502
|
if (input === 'y' || input === 'Y') {
|
|
1195
|
-
decide(APPROVAL_OPTIONS[0]
|
|
1503
|
+
decide(APPROVAL_OPTIONS[0])
|
|
1196
1504
|
return
|
|
1197
1505
|
}
|
|
1198
1506
|
if (input === 'n' || input === 'N') {
|
|
1199
|
-
decide(APPROVAL_OPTIONS[1]
|
|
1507
|
+
decide(APPROVAL_OPTIONS[1])
|
|
1200
1508
|
return
|
|
1201
1509
|
}
|
|
1202
1510
|
if (input === 'd' || input === 'D') {
|
|
1203
|
-
decide(APPROVAL_OPTIONS[2]
|
|
1511
|
+
decide(APPROVAL_OPTIONS[2])
|
|
1204
1512
|
return
|
|
1205
1513
|
}
|
|
1206
1514
|
if (/^[1-9]$/u.test(input)) {
|
|
1207
1515
|
const index = Number(input) - 1
|
|
1208
|
-
if (index < APPROVAL_OPTIONS.length) decide(APPROVAL_OPTIONS[index]
|
|
1516
|
+
if (index < APPROVAL_OPTIONS.length) decide(APPROVAL_OPTIONS[index])
|
|
1209
1517
|
}
|
|
1210
1518
|
}, { isActive: active })
|
|
1211
1519
|
|
|
1212
1520
|
if (pending === undefined) return undefined
|
|
1213
1521
|
const queuedSuffix = snapshot.queued > 0 ? ` · +${snapshot.queued} queued` : ''
|
|
1214
1522
|
if (viewport.maxHeight === 0 || viewport.compact || summarize === true) {
|
|
1215
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(
|
|
1523
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('approval.compact', { queued: queuedSuffix }), viewport.contentColumns))
|
|
1216
1524
|
}
|
|
1217
1525
|
// Body budget: title + options + footer consume fixed rows; the command
|
|
1218
1526
|
// preview shrinks with an explicit overflow marker (Codex's "[… N lines]").
|
|
@@ -1231,7 +1539,7 @@ function ApprovalBar({ snapshot, locked, notify, interrupt, summarize }: {
|
|
|
1231
1539
|
createElement(PanelGap, { visible: viewport.gapRows > 0 && body.length > 0 }),
|
|
1232
1540
|
...visibleBody.map((line, index) => createElement(StyledRows, { key: `body-${index}`, lines: [line] })),
|
|
1233
1541
|
...(overflow > 0
|
|
1234
|
-
? [createElement(Text, { key: 'overflow', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(
|
|
1542
|
+
? [createElement(Text, { key: 'overflow', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(t('approval.overflow', { count: overflow }), viewport.contentColumns))]
|
|
1235
1543
|
: []),
|
|
1236
1544
|
...(body.length > 0 ? [createElement(PanelGap, { visible: viewport.gapRows > 0 })] : []),
|
|
1237
1545
|
...APPROVAL_OPTIONS.map((option, index) => {
|
|
@@ -1248,8 +1556,8 @@ function ApprovalBar({ snapshot, locked, notify, interrupt, summarize }: {
|
|
|
1248
1556
|
)
|
|
1249
1557
|
}),
|
|
1250
1558
|
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(snapshot.answered
|
|
1251
|
-
? 'submitted
|
|
1252
|
-
: '
|
|
1559
|
+
? t('approval.submitted')
|
|
1560
|
+
: t('approval.footer'), viewport.contentColumns)),
|
|
1253
1561
|
)
|
|
1254
1562
|
}
|
|
1255
1563
|
|
|
@@ -1622,21 +1930,21 @@ function QuestionBar({ store, snapshot, locked }: { store: QuestionStore; snapsh
|
|
|
1622
1930
|
|
|
1623
1931
|
if (pending === undefined || question === undefined) return undefined
|
|
1624
1932
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
1625
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(isPlan ? 'plan
|
|
1933
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(isPlan ? t('question.compact.plan') : t('question.compact.normal'), viewport.contentColumns))
|
|
1626
1934
|
}
|
|
1627
1935
|
const footerBase = submitted
|
|
1628
|
-
? 'submitted
|
|
1936
|
+
? t('question.submitted')
|
|
1629
1937
|
: mode === 'custom'
|
|
1630
1938
|
? options.length === 0
|
|
1631
|
-
? '
|
|
1632
|
-
: '
|
|
1939
|
+
? t('question.customNoOptions')
|
|
1940
|
+
: t('question.customOptions')
|
|
1633
1941
|
: options.length === 0
|
|
1634
|
-
? '
|
|
1942
|
+
? t('question.customNoOptions')
|
|
1635
1943
|
: isMulti
|
|
1636
|
-
? '
|
|
1637
|
-
: '
|
|
1944
|
+
? t('question.multiOptions')
|
|
1945
|
+
: t('question.singleOptions')
|
|
1638
1946
|
const footer = pending.request.questions.length > 1 && !submitted
|
|
1639
|
-
? `${footerBase}
|
|
1947
|
+
? `${footerBase}${t('question.switch')}`
|
|
1640
1948
|
: footerBase
|
|
1641
1949
|
return createElement(
|
|
1642
1950
|
Box,
|
|
@@ -1644,12 +1952,12 @@ function QuestionBar({ store, snapshot, locked }: { store: QuestionStore; snapsh
|
|
|
1644
1952
|
createElement(
|
|
1645
1953
|
Text,
|
|
1646
1954
|
{ color: inkColor(isPlan ? getPalette().brand : getPalette().brandDeep), bold: true, wrap: 'truncate-end' },
|
|
1647
|
-
truncateColumns(`${isPlan ? '
|
|
1955
|
+
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
1956
|
),
|
|
1649
1957
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1650
1958
|
createElement(StyledRows, { lines: rendered.lines.slice(visibleScroll, visibleScroll + viewport.bodyRows) }),
|
|
1651
1959
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1652
|
-
createElement(Text, {
|
|
1960
|
+
createElement(Text, { wrap: 'truncate-end' }, dim(truncateColumns(footer, viewport.contentColumns))),
|
|
1653
1961
|
)
|
|
1654
1962
|
}
|
|
1655
1963
|
|
|
@@ -1659,10 +1967,10 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1659
1967
|
error: string | undefined
|
|
1660
1968
|
/** `provider/model` label of the applied model: the cursor lands on it once. */
|
|
1661
1969
|
current?: string
|
|
1662
|
-
onSelect(row: ModelRow)
|
|
1663
|
-
onProviders
|
|
1664
|
-
onRetry()
|
|
1665
|
-
onClose()
|
|
1970
|
+
onSelect: (row: ModelRow) => void
|
|
1971
|
+
onProviders?: () => void
|
|
1972
|
+
onRetry: () => void
|
|
1973
|
+
onClose: () => void
|
|
1666
1974
|
}): ReactElement {
|
|
1667
1975
|
const [query, setQuery] = useState('')
|
|
1668
1976
|
const [cursor, setCursor] = useState(0)
|
|
@@ -1679,6 +1987,13 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1679
1987
|
return rows.filter(row => `${row.provider} ${row.providerName ?? ''} ${row.model} ${row.modelName}`.toLowerCase().includes(needle))
|
|
1680
1988
|
}, [rows, query])
|
|
1681
1989
|
const positioned = useRef(false)
|
|
1990
|
+
// Latest-value ref: Ink re-subscribes useInput when its effect flushes,
|
|
1991
|
+
// which can lag a committed render (a directory that just landed paints
|
|
1992
|
+
// before the subscription swaps). A keystroke in that window would meet a
|
|
1993
|
+
// stale closure — Enter died as an empty-filter no-op right after "2 of 4
|
|
1994
|
+
// match" painted. The handler reads render-fresh values through the ref.
|
|
1995
|
+
const liveRef = useRef({ filtered, query, cursor })
|
|
1996
|
+
liveRef.current = { filtered, query, cursor }
|
|
1682
1997
|
|
|
1683
1998
|
useEffect(() => {
|
|
1684
1999
|
// Open ON the applied model (Codex resumes the previous pick): the first
|
|
@@ -1697,7 +2012,7 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1697
2012
|
// Position within the ACTIVE filter: the full-row index means nothing
|
|
1698
2013
|
// when the query already narrowed the list while the directory loaded
|
|
1699
2014
|
// (a late resolve must not place the cursor outside `filtered`).
|
|
1700
|
-
const filteredIndex = filtered.indexOf(rows[index]
|
|
2015
|
+
const filteredIndex = filtered.indexOf(rows[index])
|
|
1701
2016
|
setCursor(filteredIndex >= 0 ? filteredIndex : 0)
|
|
1702
2017
|
} else if (cursor >= filtered.length) {
|
|
1703
2018
|
setCursor(Math.max(0, filtered.length - 1))
|
|
@@ -1705,11 +2020,12 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1705
2020
|
}, [rows, filtered, cursor, current])
|
|
1706
2021
|
|
|
1707
2022
|
useInput((input, key) => {
|
|
1708
|
-
|
|
2023
|
+
const { filtered: list, query: text, cursor: at } = liveRef.current
|
|
2024
|
+
if (key.escape || (input === 'q' && text === '')) {
|
|
1709
2025
|
onClose()
|
|
1710
2026
|
return
|
|
1711
2027
|
}
|
|
1712
|
-
if (input === 'r' &&
|
|
2028
|
+
if (input === 'r' && text === '') {
|
|
1713
2029
|
onRetry()
|
|
1714
2030
|
return
|
|
1715
2031
|
}
|
|
@@ -1722,19 +2038,19 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1722
2038
|
onClose()
|
|
1723
2039
|
return
|
|
1724
2040
|
}
|
|
1725
|
-
const next = editQuery(
|
|
2041
|
+
const next = editQuery(text, input, key)
|
|
1726
2042
|
if (next !== undefined) {
|
|
1727
2043
|
setQuery(next)
|
|
1728
2044
|
setCursor(0)
|
|
1729
2045
|
return
|
|
1730
2046
|
}
|
|
1731
|
-
if (
|
|
2047
|
+
if (list.length === 0) return
|
|
1732
2048
|
if (key.upArrow) {
|
|
1733
|
-
setCursor(
|
|
2049
|
+
setCursor(at > 0 ? at - 1 : list.length - 1)
|
|
1734
2050
|
return
|
|
1735
2051
|
}
|
|
1736
2052
|
if (key.downArrow) {
|
|
1737
|
-
setCursor(
|
|
2053
|
+
setCursor(at < list.length - 1 ? at + 1 : 0)
|
|
1738
2054
|
return
|
|
1739
2055
|
}
|
|
1740
2056
|
if (key.pageUp) {
|
|
@@ -1742,11 +2058,11 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1742
2058
|
return
|
|
1743
2059
|
}
|
|
1744
2060
|
if (key.pageDown) {
|
|
1745
|
-
setCursor(current => Math.min(
|
|
2061
|
+
setCursor(current => Math.min(list.length - 1, current + Math.max(1, viewport.bodyRows - 1)))
|
|
1746
2062
|
return
|
|
1747
2063
|
}
|
|
1748
|
-
if (key.return &&
|
|
1749
|
-
onSelect(
|
|
2064
|
+
if (key.return && list[at] !== undefined) {
|
|
2065
|
+
onSelect(list[at])
|
|
1750
2066
|
}
|
|
1751
2067
|
})
|
|
1752
2068
|
|
|
@@ -1754,19 +2070,19 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1754
2070
|
const providers = onProviders === undefined ? '' : ' · tab providers'
|
|
1755
2071
|
const state = filtered.length === 0
|
|
1756
2072
|
? directory === undefined && error === undefined
|
|
1757
|
-
? 'loading
|
|
2073
|
+
? t('panel.model.loading')
|
|
1758
2074
|
: error !== undefined
|
|
1759
|
-
? 'error'
|
|
1760
|
-
: query === '' ? '
|
|
2075
|
+
? t('panel.model.error')
|
|
2076
|
+
: query === '' ? t('panel.model.noModels') : t('panel.model.compactNoMatch', { query: singleLineText(query) })
|
|
1761
2077
|
: `❯ ${filtered[cursor]?.modelName ?? filtered[cursor]?.model ?? ''}`
|
|
1762
2078
|
const tail = query === ''
|
|
1763
|
-
? '
|
|
1764
|
-
: '
|
|
1765
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(
|
|
2079
|
+
? t('panel.model.footer.filter')
|
|
2080
|
+
: t('panel.model.footer.filtered')
|
|
2081
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.model.compact', { state, providers, tail }), viewport.contentColumns))
|
|
1766
2082
|
}
|
|
1767
2083
|
|
|
1768
2084
|
const stateRows: ReactElement[] = directory === undefined && error === undefined
|
|
1769
|
-
? [createElement(Text, { key: 'loading', dimColor: true, wrap: 'truncate-end' }, '
|
|
2085
|
+
? [createElement(Text, { key: 'loading', dimColor: true, wrap: 'truncate-end' }, ` ${t('panel.model.loading')}`)]
|
|
1770
2086
|
: error !== undefined
|
|
1771
2087
|
? [createElement(
|
|
1772
2088
|
Text,
|
|
@@ -1779,12 +2095,12 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1779
2095
|
: [createElement(
|
|
1780
2096
|
Text,
|
|
1781
2097
|
{ key: 'failures', color: inkColor(getPalette().warn), wrap: 'truncate-end' },
|
|
1782
|
-
truncateColumns(`
|
|
2098
|
+
truncateColumns(` ${t('panel.provider.failure', { providers: directory?.failures.join(', ') ?? '' })}`, viewport.contentColumns),
|
|
1783
2099
|
)]),
|
|
1784
2100
|
...(rows.length === 0
|
|
1785
|
-
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' },
|
|
2101
|
+
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' }, ` ${t('panel.model.noModels')}`)]
|
|
1786
2102
|
: filtered.length === 0
|
|
1787
|
-
? [createElement(Text, { key: 'no-match', dimColor: true, wrap: 'truncate-end' }, truncateColumns(`
|
|
2103
|
+
? [createElement(Text, { key: 'no-match', dimColor: true, wrap: 'truncate-end' }, truncateColumns(` ${t('panel.model.noMatch', { query: singleLineText(query) })}`, viewport.contentColumns))]
|
|
1788
2104
|
: []),
|
|
1789
2105
|
]
|
|
1790
2106
|
// Measurement and rendering share the same physical-row budget: state
|
|
@@ -1794,12 +2110,13 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1794
2110
|
const rowBudget = Math.max(0, viewport.bodyRows - visibleStateRows.length)
|
|
1795
2111
|
const first = selectionWindow(cursor, filtered.length, rowBudget)
|
|
1796
2112
|
const visible = rowBudget === 0 ? [] : filtered.slice(first, first + rowBudget)
|
|
2113
|
+
const accent = panelAccent('model', getPalette().brand)
|
|
1797
2114
|
return createElement(
|
|
1798
2115
|
Box,
|
|
1799
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
1800
|
-
createElement(Text, { color: inkColor(
|
|
1801
|
-
?
|
|
1802
|
-
:
|
|
2116
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
2117
|
+
createElement(Text, { color: inkColor(accent.title), bold: true, wrap: 'truncate-end' }, truncateColumns(query === ''
|
|
2118
|
+
? rows.length === 0 ? t('panel.model.title') : t('panel.model.titleCount', { index: cursor + 1, total: rows.length })
|
|
2119
|
+
: t('panel.model.titleMatches', { filtered: filtered.length, total: rows.length, query: singleLineText(query) }), viewport.contentColumns)),
|
|
1803
2120
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1804
2121
|
...visibleStateRows,
|
|
1805
2122
|
...visible.map((row) => {
|
|
@@ -1817,23 +2134,23 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1817
2134
|
)
|
|
1818
2135
|
}),
|
|
1819
2136
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1820
|
-
createElement(Text, {
|
|
1821
|
-
?
|
|
1822
|
-
:
|
|
2137
|
+
createElement(Text, { wrap: 'truncate-end' }, dim(truncateColumns(query === ''
|
|
2138
|
+
? t('panel.model.footer.select', { providers: onProviders === undefined ? '' : ` · ${t('panel.model.providers')}` })
|
|
2139
|
+
: t('panel.model.footer.filteredSelect'), viewport.contentColumns))),
|
|
1823
2140
|
)
|
|
1824
2141
|
}
|
|
1825
2142
|
|
|
1826
2143
|
/** Compact provider-state copy; only value-free credential facts cross this boundary. */
|
|
1827
2144
|
function providerStateLabel(row: ProviderTargetView): string {
|
|
1828
|
-
const route = row.active ? 'active' : 'dormant'
|
|
2145
|
+
const route = row.active ? t('panel.provider.active') : t('panel.provider.dormant')
|
|
1829
2146
|
const credential = row.credential
|
|
1830
|
-
if (credential?.kind === 'error') return
|
|
2147
|
+
if (credential?.kind === 'error') return t('panel.provider.state', { route, value: t('panel.provider.keyStatusUnavailable') })
|
|
1831
2148
|
if (credential?.kind === 'facts') {
|
|
1832
|
-
if (!credential.configured) return
|
|
1833
|
-
const source = credential.source === undefined ? 'configured' : singleLineText(credential.source)
|
|
1834
|
-
return
|
|
2149
|
+
if (!credential.configured) return t('panel.provider.state', { route, value: t('panel.provider.noKey') })
|
|
2150
|
+
const source = credential.source === undefined ? t('panel.provider.configured') : singleLineText(credential.source)
|
|
2151
|
+
return t('panel.provider.state', { route, value: `${t('panel.provider.key', { value: source })}${credential.writable ? '' : ` · ${t('panel.provider.readOnly')}`}` })
|
|
1835
2152
|
}
|
|
1836
|
-
return
|
|
2153
|
+
return t('panel.provider.state', { route, value: row.configured ? t('panel.provider.authConfigured') : t('panel.provider.noLogin') })
|
|
1837
2154
|
}
|
|
1838
2155
|
|
|
1839
2156
|
/** The provider-management stage reached from /model with `a`. */
|
|
@@ -1842,15 +2159,15 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1842
2159
|
error: string | undefined
|
|
1843
2160
|
authorizations: ProviderAuthorizationDirectory | undefined
|
|
1844
2161
|
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()
|
|
2162
|
+
onConfigure: (target: ProviderTargetView) => void
|
|
2163
|
+
onUnset: (target: ProviderTargetView) => void
|
|
2164
|
+
onRemove: (target: ProviderTargetView) => void
|
|
2165
|
+
onLogin: (target: ProviderTargetView, authorization: ProviderAuthorizationRow) => void
|
|
2166
|
+
onLogout: (target: ProviderTargetView, authorization: ProviderAuthorizationRow) => void
|
|
2167
|
+
onRetry: () => void
|
|
2168
|
+
onBack: () => void
|
|
1852
2169
|
/** Leave the whole /model flow (Ctrl+C), not just this stage. */
|
|
1853
|
-
onExit()
|
|
2170
|
+
onExit: () => void
|
|
1854
2171
|
}): ReactElement {
|
|
1855
2172
|
const stdout = useStdout().stdout
|
|
1856
2173
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
@@ -1912,9 +2229,9 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1912
2229
|
if (input === 'd') {
|
|
1913
2230
|
const facts = target.credential
|
|
1914
2231
|
if (facts?.kind !== 'facts' || !facts.configured) {
|
|
1915
|
-
setActionError('
|
|
2232
|
+
setActionError(t('panel.provider.noConfiguredKey'))
|
|
1916
2233
|
} else if (!facts.writable) {
|
|
1917
|
-
setActionError('
|
|
2234
|
+
setActionError(t('panel.provider.readOnlyKey'))
|
|
1918
2235
|
} else {
|
|
1919
2236
|
onUnset(target)
|
|
1920
2237
|
}
|
|
@@ -1922,7 +2239,7 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1922
2239
|
}
|
|
1923
2240
|
if (input === 'x') {
|
|
1924
2241
|
if (!target.removable) {
|
|
1925
|
-
setActionError('
|
|
2242
|
+
setActionError(t('panel.provider.notRemovable'))
|
|
1926
2243
|
} else {
|
|
1927
2244
|
onRemove(target)
|
|
1928
2245
|
}
|
|
@@ -1930,14 +2247,14 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1930
2247
|
}
|
|
1931
2248
|
const authorization = authorizationForProvider(authorizations, target.provider)
|
|
1932
2249
|
if (input === 'l' || input === 'L') {
|
|
1933
|
-
if (authorization === undefined) setActionError('
|
|
1934
|
-
else if (authorization.inFlight) setActionError('
|
|
2250
|
+
if (authorization === undefined) setActionError(t('panel.provider.noLoginFlow'))
|
|
2251
|
+
else if (authorization.inFlight) setActionError(t('panel.provider.loginRunning'))
|
|
1935
2252
|
else onLogin(target, authorization)
|
|
1936
2253
|
return
|
|
1937
2254
|
}
|
|
1938
2255
|
if (input === 'o' || input === 'O') {
|
|
1939
|
-
if (authorization === undefined || !authorization.record.configured) setActionError('
|
|
1940
|
-
else if (!authorization.record.writable) setActionError('
|
|
2256
|
+
if (authorization === undefined || !authorization.record.configured) setActionError(t('panel.provider.noLoginRecord'))
|
|
2257
|
+
else if (!authorization.record.writable) setActionError(t('panel.provider.readOnlyLogin'))
|
|
1941
2258
|
else onLogout(target, authorization)
|
|
1942
2259
|
return
|
|
1943
2260
|
}
|
|
@@ -1946,7 +2263,7 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1946
2263
|
// the configuration surface behind an undiscoverable chord.
|
|
1947
2264
|
if (key.return) {
|
|
1948
2265
|
if (target.settingsNs.length === 0) {
|
|
1949
|
-
setActionError('
|
|
2266
|
+
setActionError(t('panel.provider.notManaged'))
|
|
1950
2267
|
} else {
|
|
1951
2268
|
onConfigure(target)
|
|
1952
2269
|
}
|
|
@@ -1954,10 +2271,10 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1954
2271
|
}, true)
|
|
1955
2272
|
|
|
1956
2273
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
1957
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('
|
|
2274
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.providers.compact'), viewport.contentColumns))
|
|
1958
2275
|
}
|
|
1959
2276
|
const stateRows: ReactElement[] = directory === undefined && error === undefined
|
|
1960
|
-
? [createElement(Text, { key: 'loading', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, '
|
|
2277
|
+
? [createElement(Text, { key: 'loading', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, ` ${t('panel.provider.loading')}`)]
|
|
1961
2278
|
: error !== undefined
|
|
1962
2279
|
? [createElement(Text, { key: 'error', color: inkColor(getPalette().error), wrap: 'truncate-end' }, truncateColumns(` ${singleLineText(error)}`, viewport.contentColumns))]
|
|
1963
2280
|
: [
|
|
@@ -1971,14 +2288,14 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1971
2288
|
)),
|
|
1972
2289
|
...(authorizationError === undefined
|
|
1973
2290
|
? []
|
|
1974
|
-
: [createElement(Text, { key: 'authorization-error', color: inkColor(getPalette().warn), wrap: 'truncate-end' }, truncateColumns(`
|
|
2291
|
+
: [createElement(Text, { key: 'authorization-error', color: inkColor(getPalette().warn), wrap: 'truncate-end' }, truncateColumns(` ${t('panel.provider.loginStatusUnavailable', { message: singleLineText(authorizationError) })}`, viewport.contentColumns))]),
|
|
1975
2292
|
...(authorizations?.failures ?? []).map((failure, index) => createElement(
|
|
1976
2293
|
Text,
|
|
1977
2294
|
{ key: `authorization-failure-${index}`, color: inkColor(getPalette().warn), wrap: 'truncate-end' },
|
|
1978
2295
|
truncateColumns(` ${singleLineText(failure)}`, viewport.contentColumns),
|
|
1979
2296
|
)),
|
|
1980
2297
|
...(rows.length === 0
|
|
1981
|
-
? [createElement(Text, { key: 'empty', color: inkColor(getPalette().dim), wrap: 'truncate-end' },
|
|
2298
|
+
? [createElement(Text, { key: 'empty', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, ` ${t('panel.provider.empty')}`)]
|
|
1982
2299
|
: []),
|
|
1983
2300
|
]
|
|
1984
2301
|
const visibleStateRows = stateRows.slice(0, viewport.bodyRows)
|
|
@@ -1989,7 +2306,7 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1989
2306
|
const itemRows: ReactElement[] = []
|
|
1990
2307
|
for (let display = first; display < first + rowBudget && display < displayLength; display += 1) {
|
|
1991
2308
|
if (hasSeparator && display === configuredCount) {
|
|
1992
|
-
itemRows.push(createElement(Text, { key: 'separator', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(
|
|
2309
|
+
itemRows.push(createElement(Text, { key: 'separator', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(` ${t('panel.provider.notConfiguredDivider')}`, viewport.contentColumns)))
|
|
1993
2310
|
continue
|
|
1994
2311
|
}
|
|
1995
2312
|
const index = hasSeparator && display > configuredCount ? display - 1 : display
|
|
@@ -2013,15 +2330,16 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
2013
2330
|
truncateColumns((index === cursor ? '❯ ' : ' ') + displayText(label), viewport.contentColumns),
|
|
2014
2331
|
))
|
|
2015
2332
|
}
|
|
2333
|
+
const accent = panelAccent('model-providers', getPalette().brand)
|
|
2016
2334
|
return createElement(
|
|
2017
2335
|
Box,
|
|
2018
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
2019
|
-
createElement(Text, { color: inkColor(
|
|
2336
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
2337
|
+
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
2338
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2021
2339
|
...visibleStateRows,
|
|
2022
2340
|
...itemRows,
|
|
2023
2341
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2024
|
-
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns('
|
|
2342
|
+
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(t('panel.providers.footer'), viewport.contentColumns)),
|
|
2025
2343
|
)
|
|
2026
2344
|
}
|
|
2027
2345
|
|
|
@@ -2049,14 +2367,14 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2049
2367
|
target: ProviderTargetView
|
|
2050
2368
|
/** Models with declared efforts (settings first, catalog-advertised after) a model row can copy from. */
|
|
2051
2369
|
effortDonors: readonly EffortDonor[]
|
|
2052
|
-
save(target: ProviderTargetView, configuration: ProviderConfiguration)
|
|
2370
|
+
save: (target: ProviderTargetView, configuration: ProviderConfiguration) => Promise<void>
|
|
2053
2371
|
saveCredential: ((target: ProviderTargetView, key: string) => Promise<void>) | undefined
|
|
2054
|
-
discover(target: ProviderTargetView, request: { readonly apiKey?: string; readonly baseURL?: string }, signal?: AbortSignal)
|
|
2372
|
+
discover: (target: ProviderTargetView, request: { readonly apiKey?: string; readonly baseURL?: string }, signal?: AbortSignal) => Promise<readonly DiscoveredModelView[]>
|
|
2055
2373
|
/** Report a successful save so the surface can notice the key rotation. */
|
|
2056
|
-
done(result: { readonly key: boolean })
|
|
2057
|
-
back()
|
|
2374
|
+
done: (result: { readonly key: boolean }) => void
|
|
2375
|
+
back: () => void
|
|
2058
2376
|
/** Leave the whole /model flow (Ctrl+C), not just this page. */
|
|
2059
|
-
onExit()
|
|
2377
|
+
onExit: () => void
|
|
2060
2378
|
}): ReactElement {
|
|
2061
2379
|
const stdout = useStdout().stdout
|
|
2062
2380
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
@@ -2110,7 +2428,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2110
2428
|
|
|
2111
2429
|
/** Compact declaration summary for the row label: count, off, or inherit. */
|
|
2112
2430
|
const effortsSummary = (model: ProviderModelSettings): string => {
|
|
2113
|
-
const raw = (model.extras
|
|
2431
|
+
const raw = (model.extras)?.reasoningEfforts
|
|
2114
2432
|
if (raw === false) return 'off'
|
|
2115
2433
|
if (isDeclaredReasoningEfforts(raw)) return String(Object.keys(raw).length)
|
|
2116
2434
|
return '~'
|
|
@@ -2263,7 +2581,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2263
2581
|
}
|
|
2264
2582
|
if (input === 'e' && selected !== undefined) {
|
|
2265
2583
|
setError(undefined)
|
|
2266
|
-
setEffDraft(serializeReasoningEfforts((selected.extras
|
|
2584
|
+
setEffDraft(serializeReasoningEfforts((selected.extras)?.reasoningEfforts))
|
|
2267
2585
|
setEffEditing(true)
|
|
2268
2586
|
return
|
|
2269
2587
|
}
|
|
@@ -2291,19 +2609,20 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2291
2609
|
// Never hide a live input surface: one visible row keeps the escape
|
|
2292
2610
|
// route honest on extremely short terminals (the three fixed rows - key,
|
|
2293
2611
|
// url, add-by-id - cannot fit below a three-row body).
|
|
2294
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('
|
|
2612
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.setup.compact'), viewport.contentColumns))
|
|
2295
2613
|
}
|
|
2296
2614
|
if (page === 'donor') {
|
|
2297
2615
|
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('
|
|
2616
|
+
? createElement(Text, { key: 'empty', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(` ${t('panel.setup.noDonors')}`, viewport.contentColumns))
|
|
2617
|
+
: createElement(Text, { key: 'hint', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(` ${t('panel.setup.copyInto', { id: displayText(selected?.id ?? '') })}`, viewport.contentColumns))
|
|
2300
2618
|
const donorBudget = Math.max(0, viewport.bodyRows - 2)
|
|
2301
2619
|
const donorFirst = selectionWindow(donorIndex, donorRows.length, donorBudget)
|
|
2302
2620
|
const donorVisible = donorRows.slice(donorFirst, donorFirst + donorBudget)
|
|
2621
|
+
const accent = panelAccent('model-efforts', getPalette().brand)
|
|
2303
2622
|
return createElement(
|
|
2304
2623
|
Box,
|
|
2305
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
2306
|
-
createElement(Text, { color: inkColor(
|
|
2624
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
2625
|
+
createElement(Text, { color: inkColor(accent.title), bold: true, wrap: 'truncate-end' }, truncateColumns(t('panel.setup.copyTitle'), viewport.contentColumns)),
|
|
2307
2626
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2308
2627
|
stateRow,
|
|
2309
2628
|
...donorVisible.map((donor, index) => {
|
|
@@ -2312,7 +2631,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2312
2631
|
return createElement(Text, { key: donor.provider + '/' + donor.id, color: active ? inkColor(getPalette().brandBright) : inkColor(getPalette().text), wrap: 'truncate-end' }, truncateColumns(label, viewport.contentColumns))
|
|
2313
2632
|
}),
|
|
2314
2633
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2315
|
-
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns('
|
|
2634
|
+
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(t('panel.setup.copyFooter'), viewport.contentColumns)),
|
|
2316
2635
|
)
|
|
2317
2636
|
}
|
|
2318
2637
|
if (page === 'discover') {
|
|
@@ -2354,7 +2673,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2354
2673
|
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
2674
|
continue
|
|
2356
2675
|
}
|
|
2357
|
-
const model = models[index]
|
|
2676
|
+
const model = models[index]
|
|
2358
2677
|
const active = index === cursor
|
|
2359
2678
|
const context = model.contextWindow === undefined ? '-' : String(model.contextWindow)
|
|
2360
2679
|
const output = model.maxTokens === undefined ? '-' : String(model.maxTokens)
|
|
@@ -2364,10 +2683,11 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2364
2683
|
: ' in:' + (active && field === 'ctx' ? '[' + context + ']' : context) + ' out:' + (active && field === 'out' ? '[' + output + ']' : output) + ' eff:' + effortsSummary(model)
|
|
2365
2684
|
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
2685
|
}
|
|
2686
|
+
const accent = panelAccent('model-configure', getPalette().brand)
|
|
2367
2687
|
return createElement(
|
|
2368
2688
|
Box,
|
|
2369
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
2370
|
-
createElement(Text, { color: inkColor(
|
|
2689
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
2690
|
+
createElement(Text, { color: inkColor(accent.title), bold: true, wrap: 'truncate-end' }, truncateColumns(t('panel.setup.title', { provider: target.displayName }), viewport.contentColumns)),
|
|
2371
2691
|
// The adapter's configuration diagnostic heads the editor: the provider
|
|
2372
2692
|
// is here precisely because it stayed listed for repair.
|
|
2373
2693
|
...(target.diagnostic === undefined ? [] : [createElement(
|
|
@@ -2381,7 +2701,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2381
2701
|
...stateRows,
|
|
2382
2702
|
...modelRows,
|
|
2383
2703
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2384
|
-
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns('
|
|
2704
|
+
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(t('panel.setup.footer'), viewport.contentColumns)),
|
|
2385
2705
|
)
|
|
2386
2706
|
}
|
|
2387
2707
|
|
|
@@ -2397,11 +2717,11 @@ function ProviderDiscoveryPanel({ target, baseURL, apiKey, configured, discover,
|
|
|
2397
2717
|
baseURL: string
|
|
2398
2718
|
apiKey: string
|
|
2399
2719
|
configured: readonly string[]
|
|
2400
|
-
discover(target: ProviderTargetView, request: { readonly apiKey?: string; readonly baseURL?: string }, signal?: AbortSignal)
|
|
2401
|
-
onAdopt(models: readonly DiscoveredModelView[])
|
|
2402
|
-
back()
|
|
2720
|
+
discover: (target: ProviderTargetView, request: { readonly apiKey?: string; readonly baseURL?: string }, signal?: AbortSignal) => Promise<readonly DiscoveredModelView[]>
|
|
2721
|
+
onAdopt: (models: readonly DiscoveredModelView[]) => void
|
|
2722
|
+
back: () => void
|
|
2403
2723
|
/** Leave the whole /model flow (Ctrl+C). */
|
|
2404
|
-
onExit()
|
|
2724
|
+
onExit: () => void
|
|
2405
2725
|
}): ReactElement {
|
|
2406
2726
|
const stdout = useStdout().stdout
|
|
2407
2727
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
@@ -2462,24 +2782,25 @@ function ProviderDiscoveryPanel({ target, baseURL, apiKey, configured, discover,
|
|
|
2462
2782
|
}
|
|
2463
2783
|
}, true)
|
|
2464
2784
|
if (viewport.maxHeight === 0) {
|
|
2465
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('
|
|
2785
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.discovery.compact'), viewport.contentColumns))
|
|
2466
2786
|
}
|
|
2467
2787
|
const stateRows = loading
|
|
2468
|
-
? [createElement(Text, { key: 'loading', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(
|
|
2788
|
+
? [createElement(Text, { key: 'loading', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(` ${t('panel.discovery.loading')}`, viewport.contentColumns))]
|
|
2469
2789
|
: error !== undefined
|
|
2470
2790
|
? [createElement(Text, { key: 'error', color: inkColor(getPalette().error), wrap: 'truncate-end' }, truncateColumns(' ' + error, viewport.contentColumns))]
|
|
2471
2791
|
: 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(
|
|
2792
|
+
? [createElement(Text, { key: 'empty', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(` ${t('panel.discovery.empty')}`, viewport.contentColumns))]
|
|
2793
|
+
: [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
2794
|
// One spare row keeps the panel strictly below maxHeight even with the
|
|
2475
2795
|
// gap collapsed (the at-equality regime makes Ink rewrite Static).
|
|
2476
2796
|
const rowBudget = Math.max(0, viewport.bodyRows - stateRows.length - 1)
|
|
2477
2797
|
const first = selectionWindow(cursor, rows.length, rowBudget)
|
|
2478
2798
|
const visible = rows.slice(first, first + rowBudget)
|
|
2799
|
+
const accent = panelAccent('model-discover', getPalette().brand)
|
|
2479
2800
|
return createElement(
|
|
2480
2801
|
Box,
|
|
2481
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
2482
|
-
createElement(Text, { color: inkColor(
|
|
2802
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
2803
|
+
createElement(Text, { color: inkColor(accent.title), bold: true, wrap: 'truncate-end' }, truncateColumns(t('panel.discovery.title', { provider: target.displayName }), viewport.contentColumns)),
|
|
2483
2804
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2484
2805
|
...stateRows,
|
|
2485
2806
|
...visible.map((model, index) => {
|
|
@@ -2491,7 +2812,7 @@ function ProviderDiscoveryPanel({ target, baseURL, apiKey, configured, discover,
|
|
|
2491
2812
|
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
2813
|
}),
|
|
2493
2814
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2494
|
-
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns('
|
|
2815
|
+
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(t('panel.discovery.footer'), viewport.contentColumns)),
|
|
2495
2816
|
)
|
|
2496
2817
|
}
|
|
2497
2818
|
|
|
@@ -2499,9 +2820,9 @@ function ProviderDiscoveryPanel({ target, baseURL, apiKey, configured, discover,
|
|
|
2499
2820
|
function ProviderConfirmPanel({ target, kind, confirm, done, back }: {
|
|
2500
2821
|
target: ProviderTargetView
|
|
2501
2822
|
kind: 'credential' | 'provider'
|
|
2502
|
-
confirm(target: ProviderTargetView)
|
|
2503
|
-
done()
|
|
2504
|
-
back()
|
|
2823
|
+
confirm: (target: ProviderTargetView) => Promise<void>
|
|
2824
|
+
done: () => void
|
|
2825
|
+
back: () => void
|
|
2505
2826
|
}): ReactElement {
|
|
2506
2827
|
const stdout = useStdout().stdout
|
|
2507
2828
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
@@ -2559,7 +2880,7 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
2559
2880
|
skills: readonly SkillRow[]
|
|
2560
2881
|
commandError: string | undefined
|
|
2561
2882
|
skillError: string | undefined
|
|
2562
|
-
onClose()
|
|
2883
|
+
onClose: () => void
|
|
2563
2884
|
}): ReactElement {
|
|
2564
2885
|
const stdout = useStdout().stdout
|
|
2565
2886
|
const columns = stdout?.columns ?? 80
|
|
@@ -2569,19 +2890,19 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
2569
2890
|
const descBudget = Math.max(0, viewport.contentColumns - nameWidth - 2)
|
|
2570
2891
|
const row = (label: string, description: string): ReactElement => createElement(
|
|
2571
2892
|
Text,
|
|
2572
|
-
{
|
|
2573
|
-
` ${padColumns(label, nameWidth)}${
|
|
2893
|
+
{ color: inkColor(getPalette().dim), wrap: 'truncate-end' },
|
|
2894
|
+
` ${padColumns(label, nameWidth)}${truncateColumns(displayText(description), descBudget)}`,
|
|
2574
2895
|
)
|
|
2575
2896
|
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' },
|
|
2897
|
+
createElement(Text, { key: 'keys-title', bold: true, wrap: 'truncate-end' }, t('help.keysTitle')),
|
|
2898
|
+
createElement(Text, { key: 'key-submit', dimColor: true, wrap: 'truncate-end' }, ` ${t('help.key.submit')}`),
|
|
2899
|
+
createElement(Text, { key: 'key-mentions', dimColor: true, wrap: 'truncate-end' }, ` ${t('help.key.mentions')}`),
|
|
2900
|
+
createElement(Text, { key: 'key-inspector', dimColor: true, wrap: 'truncate-end' }, ` ${t('help.key.inspector')}`),
|
|
2901
|
+
createElement(Text, { key: 'key-cancel', dimColor: true, wrap: 'truncate-end' }, ` ${t('help.key.cancel')}`),
|
|
2902
|
+
createElement(Text, { key: 'key-queue', dimColor: true, wrap: 'truncate-end' }, ` ${t('help.key.queue')}`),
|
|
2903
|
+
createElement(Text, { key: 'key-edit', dimColor: true, wrap: 'truncate-end' }, ` ${t('help.key.edit')}`),
|
|
2583
2904
|
createElement(Text, { key: 'commands-gap' }, ' '),
|
|
2584
|
-
createElement(Text, { key: 'commands-title', bold: true, wrap: 'truncate-end' }, '
|
|
2905
|
+
createElement(Text, { key: 'commands-title', bold: true, wrap: 'truncate-end' }, t('help.commandsTitle')),
|
|
2585
2906
|
...(commandError === undefined
|
|
2586
2907
|
? []
|
|
2587
2908
|
: [createElement(
|
|
@@ -2592,18 +2913,18 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
2592
2913
|
...LOCAL_COMMANDS.map(command => createElement(
|
|
2593
2914
|
Box,
|
|
2594
2915
|
{ key: `local-${command.label.slice(1)}` },
|
|
2595
|
-
row(command.label, command.
|
|
2916
|
+
row(command.label, t(command.descriptionKey)),
|
|
2596
2917
|
)),
|
|
2597
2918
|
...descriptors.filter(descriptor => !LOCAL_COMMAND_NAMES.has(descriptor.name)).map(descriptor => createElement(
|
|
2598
2919
|
Text,
|
|
2599
|
-
{ key: `command-${descriptor.name}`,
|
|
2600
|
-
` ${padColumns(`/${descriptor.name}`, nameWidth)}${
|
|
2920
|
+
{ key: `command-${descriptor.name}`, color: inkColor(getPalette().dim), wrap: 'truncate-end' },
|
|
2921
|
+
` ${padColumns(`/${descriptor.name}`, nameWidth)}${truncateColumns(displayText(descriptor.description), descBudget)}`,
|
|
2601
2922
|
)),
|
|
2602
2923
|
...(skills.length === 0 && skillError === undefined
|
|
2603
2924
|
? []
|
|
2604
2925
|
: [
|
|
2605
2926
|
createElement(Text, { key: 'skills-gap' }, ' '),
|
|
2606
|
-
createElement(Text, { key: 'skills-title', bold: true, wrap: 'truncate-end' }, '
|
|
2927
|
+
createElement(Text, { key: 'skills-title', bold: true, wrap: 'truncate-end' }, t('help.skillsTitle')),
|
|
2607
2928
|
]),
|
|
2608
2929
|
...(skillError === undefined
|
|
2609
2930
|
? []
|
|
@@ -2614,8 +2935,8 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
2614
2935
|
)]),
|
|
2615
2936
|
...skills.map(skill => createElement(
|
|
2616
2937
|
Text,
|
|
2617
|
-
{ key: `skill-${skill.name}`,
|
|
2618
|
-
` ${padColumns(`/${skill.name}`, nameWidth)}${
|
|
2938
|
+
{ key: `skill-${skill.name}`, color: inkColor(getPalette().dim), wrap: 'truncate-end' },
|
|
2939
|
+
` ${padColumns(`/${skill.name}`, nameWidth)}${truncateColumns(displayText(skill.description), descBudget)}`,
|
|
2619
2940
|
)),
|
|
2620
2941
|
]
|
|
2621
2942
|
const visibleScroll = clampScroll(scroll, content.length, viewport.bodyRows)
|
|
@@ -2641,17 +2962,18 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
2641
2962
|
})
|
|
2642
2963
|
|
|
2643
2964
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
2644
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('
|
|
2965
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('help.compact'), viewport.contentColumns))
|
|
2645
2966
|
}
|
|
2646
2967
|
|
|
2968
|
+
const accent = panelAccent('help', getPalette().brand)
|
|
2647
2969
|
return createElement(
|
|
2648
2970
|
Box,
|
|
2649
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
2650
|
-
createElement(Text, { color: inkColor(
|
|
2971
|
+
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
|
|
2972
|
+
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
2973
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2652
2974
|
...content.slice(visibleScroll, visibleScroll + viewport.bodyRows),
|
|
2653
2975
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2654
|
-
createElement(Text, {
|
|
2976
|
+
createElement(Text, { wrap: 'truncate-end' }, dim(truncateColumns(t('help.footer'), viewport.contentColumns))),
|
|
2655
2977
|
)
|
|
2656
2978
|
}
|
|
2657
2979
|
|
|
@@ -2661,7 +2983,8 @@ function verboseLine(text: string, columns: number): string {
|
|
|
2661
2983
|
}
|
|
2662
2984
|
|
|
2663
2985
|
/** The empty-composer placeholder text (shared by the static and wave paths). */
|
|
2664
|
-
const
|
|
2986
|
+
const composerPlaceholder = (mode: 'queue' | 'steer'): string =>
|
|
2987
|
+
t(mode === 'steer' ? 'composer.placeholderSteer' : 'composer.placeholder')
|
|
2665
2988
|
|
|
2666
2989
|
/** One physical cell of the wave-painted composer row: a char plus styles. */
|
|
2667
2990
|
interface ComposerCell {
|
|
@@ -2694,9 +3017,9 @@ function waveRowSpans(cells: readonly ComposerCell[]): ReactElement[] {
|
|
|
2694
3017
|
const spans: ReactElement[] = []
|
|
2695
3018
|
let start = 0
|
|
2696
3019
|
while (start < cells.length) {
|
|
2697
|
-
const cell = cells[start]
|
|
3020
|
+
const cell = cells[start]
|
|
2698
3021
|
let end = start + 1
|
|
2699
|
-
while (end < cells.length && sameCellStyle(cells[end]
|
|
3022
|
+
while (end < cells.length && sameCellStyle(cells[end], cell)) end += 1
|
|
2700
3023
|
spans.push(createElement(
|
|
2701
3024
|
Text,
|
|
2702
3025
|
{
|
|
@@ -2719,7 +3042,7 @@ function cellIndexAtColumn(cells: readonly ComposerCell[], target: number): numb
|
|
|
2719
3042
|
let column = 0
|
|
2720
3043
|
for (let index = 0; index < cells.length; index += 1) {
|
|
2721
3044
|
if (column === target) return index
|
|
2722
|
-
column += cells[index]
|
|
3045
|
+
column += cells[index].width ?? visibleColumns(cells[index].char)
|
|
2723
3046
|
if (column > target) return undefined
|
|
2724
3047
|
}
|
|
2725
3048
|
return undefined
|
|
@@ -2792,11 +3115,13 @@ interface ComposerWaveProps {
|
|
|
2792
3115
|
value: string
|
|
2793
3116
|
/** Tier prompt glyph and accent color (persistent, like Codex's charge). */
|
|
2794
3117
|
promptGlyph: string
|
|
3118
|
+
/** Empty-composer placeholder for the delivery mode in force. */
|
|
3119
|
+
placeholder: string
|
|
2795
3120
|
promptColor: string
|
|
2796
3121
|
/** Fires EXACTLY ONCE when this sweep ends for any reason — completed,
|
|
2797
3122
|
* cancelled by the gate, or unmounted (a modal panel froze the composer) —
|
|
2798
3123
|
* so Input's played-key latch survives the leaf's unmount/remount cycle. */
|
|
2799
|
-
onSettled()
|
|
3124
|
+
onSettled: () => void
|
|
2800
3125
|
}
|
|
2801
3126
|
|
|
2802
3127
|
/**
|
|
@@ -2864,7 +3189,7 @@ function ComposerWave(props: ComposerWaveProps): ReactElement {
|
|
|
2864
3189
|
}
|
|
2865
3190
|
for (const span of splitGraphemes(parts.before)) push(span.text)
|
|
2866
3191
|
if (parts.hasCaret) push(parts.caret, { inverse: props.caretVisible })
|
|
2867
|
-
const tail = placeholder ?
|
|
3192
|
+
const tail = placeholder ? props.placeholder : parts.after
|
|
2868
3193
|
for (const span of splitGraphemes(tail)) push(span.text, placeholder ? { dim: true } : {})
|
|
2869
3194
|
while (usedColumns < props.bandWidth) push(' ')
|
|
2870
3195
|
|
|
@@ -2873,9 +3198,9 @@ function ComposerWave(props: ComposerWaveProps): ReactElement {
|
|
|
2873
3198
|
const word = tier === 'unknown' ? 'Into the Unknown' : 'deepseek'
|
|
2874
3199
|
const start = Math.max(2, Math.floor((props.bandWidth - word.length) / 2))
|
|
2875
3200
|
const indices = Array.from({ length: word.length }, (_, at) => cellIndexAtColumn(cells, start + at))
|
|
2876
|
-
if (indices.every(index => index !== undefined && (cells[index]
|
|
3201
|
+
if (indices.every(index => index !== undefined && (cells[index].char === ' ' || cells[index].dim === true))) {
|
|
2877
3202
|
for (let at = 0; at < word.length; at += 1) {
|
|
2878
|
-
const cell = cells[indices[at]!]
|
|
3203
|
+
const cell = cells[indices[at]!]
|
|
2879
3204
|
cell.char = word[at]!
|
|
2880
3205
|
cell.width = 1
|
|
2881
3206
|
cell.color = inkColor(deepseekWaveWordHue(at, hues))
|
|
@@ -2887,11 +3212,11 @@ function ComposerWave(props: ComposerWaveProps): ReactElement {
|
|
|
2887
3212
|
if (bandRow === middleBandRow && (tier === 'deepseek' || tier === 'unknown') && style === 'wave') {
|
|
2888
3213
|
const spark = deepseekWaveSpark(tick)
|
|
2889
3214
|
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]
|
|
3215
|
+
if (spark !== null && lastIndex !== undefined && cells[lastIndex].char === ' ') {
|
|
3216
|
+
cells[lastIndex].char = spark
|
|
3217
|
+
cells[lastIndex].color = props.promptColor
|
|
3218
|
+
cells[lastIndex].bold = true
|
|
3219
|
+
cells[lastIndex].dim = false
|
|
2895
3220
|
}
|
|
2896
3221
|
}
|
|
2897
3222
|
return createElement(Text, { key: `editor-${sourceIndex}`, wrap: 'truncate-end' }, ...waveRowSpans(cells))
|
|
@@ -2905,13 +3230,104 @@ function ComposerWave(props: ComposerWaveProps): ReactElement {
|
|
|
2905
3230
|
)
|
|
2906
3231
|
}
|
|
2907
3232
|
|
|
3233
|
+
/**
|
|
3234
|
+
* The /rainbow celebration leaf: a FIXED seven-color ribbon that slides
|
|
3235
|
+
* across the three-row composer band. Same cell model as ComposerWave so
|
|
3236
|
+
* CJK/emoji stay atomic; no wordmark, no sparkles — the spectrum is the
|
|
3237
|
+
* show. Strictly one-shot per burst id (Input latches onSettled).
|
|
3238
|
+
*/
|
|
3239
|
+
function ComposerRainbowBurst(props: Omit<ComposerWaveProps, 'tier' | 'style'>): ReactElement {
|
|
3240
|
+
const durationMs = RAINBOW_BURST_DURATION_MS
|
|
3241
|
+
const { tick, done } = useWaveFrames(props.active, durationMs)
|
|
3242
|
+
const settledRef = useRef(false)
|
|
3243
|
+
const onSettledRef = useRef(props.onSettled)
|
|
3244
|
+
onSettledRef.current = props.onSettled
|
|
3245
|
+
const settle = (): void => {
|
|
3246
|
+
if (settledRef.current) return
|
|
3247
|
+
settledRef.current = true
|
|
3248
|
+
onSettledRef.current()
|
|
3249
|
+
}
|
|
3250
|
+
useEffect(() => {
|
|
3251
|
+
if (done) settle()
|
|
3252
|
+
}, [done])
|
|
3253
|
+
useEffect(() => () => {
|
|
3254
|
+
settle()
|
|
3255
|
+
}, [])
|
|
3256
|
+
if (!props.active || done || tick * RAINBOW_BURST_TICK_MS >= durationMs) return props.fallback
|
|
3257
|
+
const bandRgb = getPalette().composerBand
|
|
3258
|
+
const totalBandRows = props.rows.length + 2
|
|
3259
|
+
const burstBg = (row: number, column: number): string => {
|
|
3260
|
+
const rgb = rainbowBurstColumnBg(tick, column, props.bandWidth, bandRgb, row, totalBandRows)
|
|
3261
|
+
return rgb === null ? props.bandBg : inkColor(rgb)
|
|
3262
|
+
}
|
|
3263
|
+
const blankBandRow = (row: number): ReactElement => {
|
|
3264
|
+
const blanks: ComposerCell[] = []
|
|
3265
|
+
for (let column = 0; column < props.bandWidth; column += 1) {
|
|
3266
|
+
blanks.push({ char: ' ', width: 1, backgroundColor: burstBg(row, column) })
|
|
3267
|
+
}
|
|
3268
|
+
return createElement(Text, { key: `blank-${row}` }, ...waveRowSpans(blanks))
|
|
3269
|
+
}
|
|
3270
|
+
const editorBurstRows = props.rows.map((row, visibleIndex) => {
|
|
3271
|
+
const sourceIndex = props.windowStart + visibleIndex
|
|
3272
|
+
const bandRow = visibleIndex + 1
|
|
3273
|
+
const parts = editorRowParts(row, sourceIndex, props.caretRow, props.cursor)
|
|
3274
|
+
const placeholder = sourceIndex === 0 && props.value === ''
|
|
3275
|
+
const cells: ComposerCell[] = []
|
|
3276
|
+
let usedColumns = 0
|
|
3277
|
+
const push = (char: string, extra: Omit<ComposerCell, 'char' | 'width' | 'backgroundColor'> = {}): void => {
|
|
3278
|
+
const width = visibleColumns(char)
|
|
3279
|
+
cells.push({ char, width, backgroundColor: burstBg(bandRow, usedColumns), ...extra })
|
|
3280
|
+
usedColumns += width
|
|
3281
|
+
}
|
|
3282
|
+
if (sourceIndex === 0) {
|
|
3283
|
+
push(props.promptGlyph, { color: props.promptColor, bold: true })
|
|
3284
|
+
push(' ', { color: props.promptColor })
|
|
3285
|
+
} else {
|
|
3286
|
+
push(' ')
|
|
3287
|
+
push(' ')
|
|
3288
|
+
}
|
|
3289
|
+
for (const span of splitGraphemes(parts.before)) push(span.text)
|
|
3290
|
+
if (parts.hasCaret) push(parts.caret, { inverse: props.caretVisible })
|
|
3291
|
+
const tail = placeholder ? props.placeholder : parts.after
|
|
3292
|
+
for (const span of splitGraphemes(tail)) push(span.text, placeholder ? { dim: true } : {})
|
|
3293
|
+
while (usedColumns < props.bandWidth) push(' ')
|
|
3294
|
+
return createElement(Text, { key: `editor-${sourceIndex}`, wrap: 'truncate-end' }, ...waveRowSpans(cells))
|
|
3295
|
+
})
|
|
3296
|
+
return createElement(
|
|
3297
|
+
Box,
|
|
3298
|
+
{ flexDirection: 'column', width: props.bandWidth },
|
|
3299
|
+
blankBandRow(0),
|
|
3300
|
+
...editorBurstRows,
|
|
3301
|
+
blankBandRow(totalBandRows - 1),
|
|
3302
|
+
)
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
/** One-word kind label per entry, so the inspector's ←→ walk names what
|
|
3306
|
+
* each step is instead of leaving the reader to infer it from the body. */
|
|
3307
|
+
function entryKindLabel(entry: TranscriptEntry | undefined): string {
|
|
3308
|
+
switch (entry?.kind) {
|
|
3309
|
+
case 'user': return 'user prompt'
|
|
3310
|
+
case 'pending': return 'queued prompt'
|
|
3311
|
+
case 'assistant': return 'reply'
|
|
3312
|
+
case 'tool': return 'tool call'
|
|
3313
|
+
case 'command': return 'command'
|
|
3314
|
+
case 'error': return 'turn error'
|
|
3315
|
+
case 'turn-marker': return 'turn end'
|
|
3316
|
+
case 'compaction': return 'compaction'
|
|
3317
|
+
case 'retry': return 'retry'
|
|
3318
|
+
case 'files': return 'files changed'
|
|
3319
|
+
case 'workflow': return 'workflow run'
|
|
3320
|
+
default: return 'empty'
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
|
|
2908
3324
|
/**
|
|
2909
3325
|
* The Ctrl+O transcript inspector: one selected durable entry at a time,
|
|
2910
3326
|
* with independent history selection and content scrolling. The complete
|
|
2911
3327
|
* retained entry is converted to physical rows, but only one viewport slice
|
|
2912
3328
|
* reaches Ink, so even a huge reasoning block cannot grow the dynamic tree.
|
|
2913
3329
|
*/
|
|
2914
|
-
function VerbosePanel({ entries, onClose }: { entries: readonly TranscriptEntry[]; onClose()
|
|
3330
|
+
function VerbosePanel({ entries, onClose }: { entries: readonly TranscriptEntry[]; onClose: () => void }): ReactElement {
|
|
2915
3331
|
const stdout = useStdout().stdout
|
|
2916
3332
|
const columns = stdout?.columns ?? 80
|
|
2917
3333
|
const rows = stdout?.rows ?? 30
|
|
@@ -3005,14 +3421,15 @@ function VerbosePanel({ entries, onClose }: { entries: readonly TranscriptEntry[
|
|
|
3005
3421
|
return createElement(
|
|
3006
3422
|
Text,
|
|
3007
3423
|
{ wrap: 'truncate-end' },
|
|
3008
|
-
truncateColumns('
|
|
3424
|
+
truncateColumns(t('panel.verbose.compact'), viewport.contentColumns),
|
|
3009
3425
|
)
|
|
3010
3426
|
}
|
|
3011
3427
|
|
|
3012
3428
|
const title = entries.length === 0
|
|
3013
3429
|
? '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}`
|
|
3430
|
+
: `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
3431
|
const visible = allLines.slice(visibleScroll, visibleScroll + viewport.bodyRows)
|
|
3432
|
+
const accent = panelAccent('history-inspector', getPalette().brand)
|
|
3016
3433
|
return createElement(
|
|
3017
3434
|
Box,
|
|
3018
3435
|
{
|
|
@@ -3020,11 +3437,11 @@ function VerbosePanel({ entries, onClose }: { entries: readonly TranscriptEntry[
|
|
|
3020
3437
|
width: viewport.outerColumns,
|
|
3021
3438
|
paddingX: 1,
|
|
3022
3439
|
borderStyle: 'round',
|
|
3023
|
-
borderColor: inkColor(
|
|
3440
|
+
borderColor: inkColor(accent.border),
|
|
3024
3441
|
},
|
|
3025
3442
|
createElement(
|
|
3026
3443
|
Text,
|
|
3027
|
-
{ color: inkColor(
|
|
3444
|
+
{ color: inkColor(accent.title), bold: true, wrap: 'truncate-end' },
|
|
3028
3445
|
truncateColumns(title, viewport.contentColumns),
|
|
3029
3446
|
),
|
|
3030
3447
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
@@ -3038,8 +3455,8 @@ function VerbosePanel({ entries, onClose }: { entries: readonly TranscriptEntry[
|
|
|
3038
3455
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
3039
3456
|
createElement(
|
|
3040
3457
|
Text,
|
|
3041
|
-
{
|
|
3042
|
-
dim(truncateColumns('
|
|
3458
|
+
{ wrap: 'truncate-end' },
|
|
3459
|
+
dim(truncateColumns(t('panel.verbose.footer'), viewport.contentColumns)),
|
|
3043
3460
|
),
|
|
3044
3461
|
)
|
|
3045
3462
|
}
|
|
@@ -3086,7 +3503,7 @@ export function completionCandidates(
|
|
|
3086
3503
|
): readonly CompletionCandidate[] {
|
|
3087
3504
|
if (!value.startsWith('/')) return []
|
|
3088
3505
|
const prefix = value.slice(1).split(' ')[0] ?? ''
|
|
3089
|
-
const local: CompletionCandidate[] = LOCAL_COMMANDS.map(command => ({
|
|
3506
|
+
const local: CompletionCandidate[] = LOCAL_COMMANDS.map(command => ({ label: command.label, description: t(command.descriptionKey), origin: 'command' }))
|
|
3090
3507
|
// Local commands shadow registry names (e.g. the TUI-local /permission works
|
|
3091
3508
|
// before any session exists, while the registry child needs one), so
|
|
3092
3509
|
// collisions cannot render two rows with the same key.
|
|
@@ -3151,10 +3568,10 @@ function completionMenuRowCount(terminalRows: number, rowCount: number): number
|
|
|
3151
3568
|
|
|
3152
3569
|
/**
|
|
3153
3570
|
* 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
|
-
*
|
|
3571
|
+
* the composer band — attached the way Claude-Code anchors its dropdown.
|
|
3572
|
+
* Its height is deducted from the live transcript budget so the menu covers
|
|
3573
|
+
* live rows instead of growing the tree and moving the composer/status.
|
|
3574
|
+
* Props-only (no lifted state): the menu is a pure view of the input
|
|
3158
3575
|
* editor's live completion state, so no cross-component effect ever resyncs
|
|
3159
3576
|
* it (a state lift here previously deadlocked the menu after a resize).
|
|
3160
3577
|
*/
|
|
@@ -3212,7 +3629,7 @@ function CompletionMenu({ active, mention, index, rows, error }: {
|
|
|
3212
3629
|
// Scroll affordance: with the full merged catalog (commands + registry +
|
|
3213
3630
|
// skills) the six-row window rarely shows the tail — count and hint keep
|
|
3214
3631
|
// the rest discoverable without inflating the menu budget.
|
|
3215
|
-
hidden > 0 ? createElement(Text, { key: 'more', color: inkColor(getPalette().dim), wrap: 'truncate-end' },
|
|
3632
|
+
hidden > 0 ? createElement(Text, { key: 'more', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, ` … +${hidden} more`) : undefined,
|
|
3216
3633
|
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
3634
|
)
|
|
3218
3635
|
}
|
|
@@ -3234,7 +3651,7 @@ interface DraftFile extends FilePathInspection {
|
|
|
3234
3651
|
* While a modal (approval / question / model panel) owns the keys, the
|
|
3235
3652
|
* box passes every key through untouched.
|
|
3236
3653
|
*/
|
|
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,
|
|
3654
|
+
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
3655
|
active: boolean
|
|
3239
3656
|
frozen: boolean
|
|
3240
3657
|
/** Frozen-band hint naming the surface that owns the keyboard; an empty
|
|
@@ -3243,82 +3660,100 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3243
3660
|
busy: boolean
|
|
3244
3661
|
descriptors: readonly CommandDescriptor[]
|
|
3245
3662
|
skills: readonly SkillRow[]
|
|
3246
|
-
dispatch(text: string, attachments?: readonly ContentBlock[], origin?: string)
|
|
3247
|
-
|
|
3663
|
+
dispatch: (text: string, attachments?: readonly ContentBlock[], origin?: string) => void
|
|
3664
|
+
/** Submit as steering into the running turn (see {@link AppProps.steer}). */
|
|
3665
|
+
steer: (text: string, attachments?: readonly ContentBlock[], origin?: string) => void
|
|
3666
|
+
/** Delivery mode the next submission uses; Tab on an empty composer flips it. */
|
|
3667
|
+
submitMode: 'queue' | 'steer'
|
|
3668
|
+
/** Flip {@link submitMode} and report the new mode. */
|
|
3669
|
+
cycleSubmitMode: () => void
|
|
3248
3670
|
/** The full current session identity ('' while pending); the delivery origin. */
|
|
3249
3671
|
sessionKey: string
|
|
3250
|
-
interrupt()
|
|
3251
|
-
quit()
|
|
3252
|
-
openModel()
|
|
3253
|
-
openEffort()
|
|
3254
|
-
openHelp()
|
|
3255
|
-
openMode()
|
|
3256
|
-
openPermission()
|
|
3257
|
-
openResume()
|
|
3258
|
-
|
|
3672
|
+
interrupt: () => boolean
|
|
3673
|
+
quit: () => void
|
|
3674
|
+
openModel: () => void
|
|
3675
|
+
openEffort: () => void
|
|
3676
|
+
openHelp: () => void
|
|
3677
|
+
openMode: () => void
|
|
3678
|
+
openPermission: () => void
|
|
3679
|
+
openResume: () => void
|
|
3680
|
+
/** Open the /search panel with an optional seed query. */
|
|
3681
|
+
openSearch: (query: string) => void
|
|
3682
|
+
openPlugin: (query?: string) => void
|
|
3259
3683
|
/** Open the /update panel (aligned upgrade surface). */
|
|
3260
|
-
openUpdate()
|
|
3684
|
+
openUpdate: () => void
|
|
3261
3685
|
/** Open the /schedule reminder panel (read-only catalog). */
|
|
3262
|
-
openSchedule()
|
|
3263
|
-
openJobs()
|
|
3264
|
-
openStatusline()
|
|
3265
|
-
openTheme()
|
|
3266
|
-
|
|
3686
|
+
openSchedule: () => void
|
|
3687
|
+
openJobs: () => void
|
|
3688
|
+
openStatusline: () => void
|
|
3689
|
+
openTheme: () => void
|
|
3690
|
+
/** Open the /language picker (bare /language). */
|
|
3691
|
+
openLanguage: () => void
|
|
3692
|
+
/** Apply and persist a language chosen by argument. */
|
|
3693
|
+
saveLanguage: (name: LanguageName) => void
|
|
3694
|
+
openHistory: () => void
|
|
3695
|
+
openQueue: () => void
|
|
3267
3696
|
/** Open the /agents panel (live subagent feed + transcript entry). */
|
|
3268
|
-
openAgents()
|
|
3697
|
+
openAgents: () => void
|
|
3269
3698
|
/** Open the /subagent model panel. */
|
|
3270
|
-
openSubagent()
|
|
3699
|
+
openSubagent: () => void
|
|
3271
3700
|
/** Open the /todos subpage (full todo list in one bounded panel). */
|
|
3272
|
-
openTodos()
|
|
3701
|
+
openTodos: () => void
|
|
3702
|
+
openUsage: () => void
|
|
3273
3703
|
/** Open the /resume picker in delete mode, optionally pre-armed on one id. */
|
|
3274
|
-
openDelete(id?: string)
|
|
3275
|
-
openDiff(argument: string)
|
|
3276
|
-
reviewChanges(
|
|
3704
|
+
openDelete: (id?: string) => void
|
|
3705
|
+
openDiff: (argument: string) => void
|
|
3706
|
+
reviewChanges: (selection: ReviewSelection) => void
|
|
3707
|
+
/** Open the /review candidate picker (bare /review). */
|
|
3708
|
+
openReviewPicker: () => void
|
|
3277
3709
|
/** The row id awaiting y/n in this box, when a deletion is pending. */
|
|
3278
3710
|
deleteConfirm?: string
|
|
3279
3711
|
/** Confirm the pending deletion (y in the box). */
|
|
3280
|
-
confirmDelete()
|
|
3712
|
+
confirmDelete: () => void
|
|
3281
3713
|
/** 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)
|
|
3714
|
+
cancelDelete: () => void
|
|
3715
|
+
createSession: (mode?: string) => void
|
|
3716
|
+
forkSession: (argument: string) => void
|
|
3717
|
+
cancelSessionSwitch: () => boolean
|
|
3718
|
+
notify: (text: string, tone?: NoticeTone) => void
|
|
3287
3719
|
/** Apply the Ctrl+R passthrough to the detected editor (/vscode-keys); resolves to a one-line summary. */
|
|
3288
|
-
applyEditorKeys()
|
|
3720
|
+
applyEditorKeys: () => Promise<string>
|
|
3289
3721
|
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()
|
|
3722
|
+
dismissNotice: () => void
|
|
3723
|
+
toggleReasoning: () => void
|
|
3724
|
+
openVerbose: () => void
|
|
3725
|
+
clearView: () => void
|
|
3726
|
+
refresh: () => void
|
|
3727
|
+
loadMentions: (query: string, signal?: AbortSignal) => Promise<readonly MentionCandidate[]>
|
|
3728
|
+
inspectImages: (paths: readonly string[]) => Promise<readonly ImagePathInspection[]>
|
|
3729
|
+
prepareImages: (paths: readonly string[], signal?: AbortSignal) => Promise<readonly ImageBlock[]>
|
|
3730
|
+
inspectFiles: (paths: readonly string[]) => Promise<readonly FilePathInspection[]>
|
|
3731
|
+
prepareFiles: (paths: readonly string[], signal?: AbortSignal) => Promise<readonly FileBlock[]>
|
|
3732
|
+
cycleMode: () => string
|
|
3733
|
+
exportTranscript: (argument: string) => Promise<void>
|
|
3734
|
+
renameTitle: (argument: string) => string
|
|
3735
|
+
copyLastResponse: () => Promise<string>
|
|
3304
3736
|
/** Newest-first recall space (persistent + in-session, deduped). */
|
|
3305
3737
|
recallSpace: readonly string[]
|
|
3306
3738
|
/** Record one in-session submission (deduped, local only). */
|
|
3307
|
-
recordLocal(text: string)
|
|
3739
|
+
recordLocal: (text: string) => void
|
|
3308
3740
|
/** Persist one submission to the global history file. */
|
|
3309
|
-
recordHistory(text: string)
|
|
3310
|
-
/**
|
|
3311
|
-
queued: readonly {
|
|
3312
|
-
|
|
3313
|
-
cancelQueued(messageId: string): void
|
|
3741
|
+
recordHistory: (text: string) => void
|
|
3742
|
+
/** Next-turn inbox rows, ordered exactly as the durable inbox. */
|
|
3743
|
+
queued: readonly Extract<TranscriptEntry, { kind: 'pending' }>[]
|
|
3744
|
+
updateQueued?: (messageId: string, action: QueueMutation) => void
|
|
3314
3745
|
/** Accepted /history entry waiting to be placed into the composer. */
|
|
3315
3746
|
historyFill: { text: string; index: number } | undefined
|
|
3316
3747
|
/** Marks the accepted entry consumed (called after the fill is applied). */
|
|
3317
|
-
historyConsumed()
|
|
3748
|
+
historyConsumed: () => void
|
|
3318
3749
|
/** Whether timed animations run (shimmer, chase, blink, wave). */
|
|
3319
3750
|
animations: boolean
|
|
3320
3751
|
/** Apply and report one /animation toggle (App persists through the runner). */
|
|
3321
|
-
applyAnimations(enabled: boolean)
|
|
3752
|
+
applyAnimations: (enabled: boolean) => void
|
|
3753
|
+
/** Reroll or pin the rainbow palette (switches to rainbow if needed). */
|
|
3754
|
+
applyRainbow: (seed?: number) => void
|
|
3755
|
+
/** Monotonic id of the in-flight /rainbow composer burst; 0 means none. */
|
|
3756
|
+
rainbowBurstId: number
|
|
3322
3757
|
/** DeepSeek easter-egg wave tier of the applied route (null otherwise):
|
|
3323
3758
|
* official DeepSeek models drive their flash/pro tiers, non-DeepSeek
|
|
3324
3759
|
* models running an effort above high drive the "Into the Unknown"
|
|
@@ -3337,10 +3772,10 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3337
3772
|
* background process sharing the console cannot keep it overwritten. */
|
|
3338
3773
|
tabTitle: string
|
|
3339
3774
|
/** Reports the editor's current physical row count so the live budget stays exact. */
|
|
3340
|
-
onEditorRows(rows: number)
|
|
3775
|
+
onEditorRows: (rows: number) => void
|
|
3341
3776
|
/** Reports the open completion menu's physical row count (0 when closed)
|
|
3342
3777
|
* for the same reason: the dynamic budget must reserve it, not overflow. */
|
|
3343
|
-
onMenuRows(rows: number)
|
|
3778
|
+
onMenuRows: (rows: number) => void
|
|
3344
3779
|
}): ReactElement {
|
|
3345
3780
|
const { stdout: inputStdout } = useStdout()
|
|
3346
3781
|
const columns = inputStdout?.columns ?? 80
|
|
@@ -3443,7 +3878,11 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3443
3878
|
if (stdin === undefined) return
|
|
3444
3879
|
const originalRead = stdin.read.bind(stdin)
|
|
3445
3880
|
const patchedRead = function patchedRead(this: typeof stdin, ...args: Parameters<typeof originalRead>) {
|
|
3446
|
-
|
|
3881
|
+
// `Readable.read` is declared `any`; the assertion names its real result
|
|
3882
|
+
// union once so the normalization and the focus-event stripping below
|
|
3883
|
+
// stay type-checked. Node hands back a Buffer unless an encoding was set,
|
|
3884
|
+
// and null once the stream ends.
|
|
3885
|
+
const chunk = originalRead(...args) as string | Buffer | null
|
|
3447
3886
|
if (chunk === null) return chunk
|
|
3448
3887
|
const normalized = normalizeKeyboardChunk(typeof chunk === 'string' ? chunk : String(chunk))
|
|
3449
3888
|
const input = focusReporting
|
|
@@ -3464,7 +3903,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3464
3903
|
} as typeof stdin.read
|
|
3465
3904
|
stdin.read = patchedRead
|
|
3466
3905
|
return () => {
|
|
3467
|
-
stdin.read = originalRead
|
|
3906
|
+
stdin.read = originalRead
|
|
3468
3907
|
}
|
|
3469
3908
|
}, [focusReporting, stdin])
|
|
3470
3909
|
|
|
@@ -3519,7 +3958,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3519
3958
|
|
|
3520
3959
|
const registerDraftImage = (inspection: ImagePathInspection, marker: string): boolean => {
|
|
3521
3960
|
if (draftImagesRef.current.some(image => sameImagePath(image.path, inspection.path))) {
|
|
3522
|
-
notify(
|
|
3961
|
+
notify(t('notice.attachmentAlready', { name: inspection.name }), 'warning')
|
|
3523
3962
|
return false
|
|
3524
3963
|
}
|
|
3525
3964
|
const next = [...draftImagesRef.current, { ...inspection, marker }]
|
|
@@ -3538,7 +3977,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3538
3977
|
const originalCursor = cursorRef.current
|
|
3539
3978
|
const total = imagePaths.length + filePaths.length
|
|
3540
3979
|
if (total === 0) return
|
|
3541
|
-
notify(
|
|
3980
|
+
notify(t('notice.attachmentsChecking', { count: total, plural: total === 1 ? '' : 's' }))
|
|
3542
3981
|
void Promise.all([
|
|
3543
3982
|
imagePaths.length === 0 ? Promise.resolve([]) : inspectImages(imagePaths),
|
|
3544
3983
|
filePaths.length === 0 ? Promise.resolve([]) : inspectFiles(filePaths),
|
|
@@ -3559,13 +3998,13 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3559
3998
|
markers.push(marker)
|
|
3560
3999
|
}
|
|
3561
4000
|
if (imageAdditions.length === 0 && fileAdditions.length === 0) {
|
|
3562
|
-
notify('
|
|
4001
|
+
notify(t('notice.attachmentsAlready'), 'warning')
|
|
3563
4002
|
return
|
|
3564
4003
|
}
|
|
3565
4004
|
const current = valueRef.current
|
|
3566
4005
|
const anchor = remapStableRange(originalValue, current, { start: originalCursor, end: originalCursor })
|
|
3567
4006
|
if (anchor === undefined) {
|
|
3568
|
-
notify('
|
|
4007
|
+
notify(t('notice.attachmentDraftChanged'), 'warning')
|
|
3569
4008
|
return
|
|
3570
4009
|
}
|
|
3571
4010
|
const at = anchor.start
|
|
@@ -3586,9 +4025,9 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3586
4025
|
draftFilesRef.current = nextFiles
|
|
3587
4026
|
setDraftFiles(nextFiles)
|
|
3588
4027
|
const count = imageAdditions.length + fileAdditions.length
|
|
3589
|
-
notify(
|
|
4028
|
+
notify(t('notice.attachmentsReady', { count, plural: count === 1 ? '' : 's' }))
|
|
3590
4029
|
}, (reason: unknown) => {
|
|
3591
|
-
notify(
|
|
4030
|
+
notify(t('notice.attachmentFailed', { message: reason instanceof Error ? reason.message : String(reason) }), 'error')
|
|
3592
4031
|
})
|
|
3593
4032
|
}
|
|
3594
4033
|
|
|
@@ -3673,7 +4112,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3673
4112
|
const current = valueRef.current
|
|
3674
4113
|
const anchor = remapStableRange(originalValue, current, { start, end: start + tokenText.length })
|
|
3675
4114
|
if (anchor === undefined || current.slice(anchor.start, anchor.end) !== tokenText) {
|
|
3676
|
-
notify('
|
|
4115
|
+
notify(t('notice.imageDraftChanged'), 'warning')
|
|
3677
4116
|
return
|
|
3678
4117
|
}
|
|
3679
4118
|
if (draftImagesRef.current.some(image => sameImagePath(image.path, inspection.path))) {
|
|
@@ -3684,7 +4123,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3684
4123
|
setCursor(edit.cursor)
|
|
3685
4124
|
resetCursorBlink()
|
|
3686
4125
|
setDismissedMenuValue(edit.value)
|
|
3687
|
-
notify(
|
|
4126
|
+
notify(t('notice.attachmentAlready', { name: inspection.name }), 'warning')
|
|
3688
4127
|
return
|
|
3689
4128
|
}
|
|
3690
4129
|
const marker = uniqueImageMarker(inspection.name, 'mention')
|
|
@@ -3696,9 +4135,9 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3696
4135
|
resetCursorBlink()
|
|
3697
4136
|
setDismissedMenuValue(edit.value)
|
|
3698
4137
|
registerDraftImage(inspection, marker)
|
|
3699
|
-
notify(
|
|
4138
|
+
notify(t('notice.imageReady', { name: inspection.name }))
|
|
3700
4139
|
}, (reason: unknown) => {
|
|
3701
|
-
notify(
|
|
4140
|
+
notify(t('notice.imageFailed', { message: reason instanceof Error ? reason.message : String(reason) }), 'error')
|
|
3702
4141
|
})
|
|
3703
4142
|
setCompletionIndex(0)
|
|
3704
4143
|
setDismissedMenuValue(undefined)
|
|
@@ -3802,7 +4241,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3802
4241
|
prepareAbortRef.current = undefined
|
|
3803
4242
|
setPreparingImages(false)
|
|
3804
4243
|
dismissNotice()
|
|
3805
|
-
notify('
|
|
4244
|
+
notify(t('notice.imageCancelled'), 'warning')
|
|
3806
4245
|
}
|
|
3807
4246
|
|
|
3808
4247
|
/** Cross history while an unchanged recalled draft rests its caret on
|
|
@@ -3877,10 +4316,18 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3877
4316
|
const label = cycleMode()
|
|
3878
4317
|
if (label !== '') notify(label)
|
|
3879
4318
|
} catch (error: unknown) {
|
|
3880
|
-
notify(
|
|
4319
|
+
notify(t('notice.permissionChangeFailed', { message: error instanceof Error ? error.message : String(error) }), 'error')
|
|
3881
4320
|
}
|
|
3882
4321
|
return
|
|
3883
4322
|
}
|
|
4323
|
+
// Tab on an EMPTY composer picks how the next submission is delivered:
|
|
4324
|
+
// queue for the next turn, or steer into the turn already running. With a
|
|
4325
|
+
// draft present Tab stays the completion key (handled with the menu
|
|
4326
|
+
// below), so this only claims the keypress when nothing is being typed.
|
|
4327
|
+
if (key.tab && liveValue === '' && !menuActive) {
|
|
4328
|
+
cycleSubmitMode()
|
|
4329
|
+
return
|
|
4330
|
+
}
|
|
3884
4331
|
// Ctrl+R toggles the thinking display (Claude-Code reasoning fold).
|
|
3885
4332
|
// Alt+R is the zero-config alias: VS Code never intercepts Alt chords,
|
|
3886
4333
|
// so the toggle stays reachable before /vscode-keys has been applied.
|
|
@@ -3926,7 +4373,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3926
4373
|
applyEdit(deleteForward(liveValue, liveCursor))
|
|
3927
4374
|
return
|
|
3928
4375
|
}
|
|
3929
|
-
if (busy) notify('
|
|
4376
|
+
if (busy) notify(t('notice.cancelBeforeExit'), 'warning')
|
|
3930
4377
|
else quit()
|
|
3931
4378
|
return
|
|
3932
4379
|
}
|
|
@@ -3952,7 +4399,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3952
4399
|
const forwardDelete = rawEditorTokens.current?.some(token =>
|
|
3953
4400
|
token.kind === 'delete-forward' || token.kind === 'delete-word-forward') === true
|
|
3954
4401
|
if (forwardDelete) {
|
|
3955
|
-
|
|
4402
|
+
updateQueued?.(queued[queued.length - 1].messageId, { kind: 'remove' })
|
|
3956
4403
|
return
|
|
3957
4404
|
}
|
|
3958
4405
|
}
|
|
@@ -3984,7 +4431,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
3984
4431
|
// Slash semantics with attachments are unchanged: commands cannot
|
|
3985
4432
|
// carry attachments, so the line goes to the model as a prompt —
|
|
3986
4433
|
// warn instead of surprising the user with a literal "/export".
|
|
3987
|
-
if (isSlashLine(text)) notify('
|
|
4434
|
+
if (isSlashLine(text)) notify(t('notice.commandAttachments'), 'warning')
|
|
3988
4435
|
// Attachment prepares resolve asynchronously; the app remounts onto
|
|
3989
4436
|
// another session in the meantime, and this (old) instance's unmount
|
|
3990
4437
|
// cleanup runs too late on the microtask timeline. Tag the delivery
|
|
@@ -4023,7 +4470,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4023
4470
|
}
|
|
4024
4471
|
recall.current = beginRecall(recallSpace, '')
|
|
4025
4472
|
const blocks: readonly ContentBlock[] = [...images, ...files]
|
|
4026
|
-
if (
|
|
4473
|
+
if (submitMode === 'steer') steer(text, blocks, originSession)
|
|
4027
4474
|
else dispatch(text, blocks, originSession)
|
|
4028
4475
|
}, (reason: unknown) => {
|
|
4029
4476
|
if (controller.signal.aborted || prepareEpochRef.current !== epoch) return
|
|
@@ -4092,7 +4539,16 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4092
4539
|
return
|
|
4093
4540
|
}
|
|
4094
4541
|
if (text === '/review' || text.startsWith('/review ')) {
|
|
4095
|
-
|
|
4542
|
+
const argument = text.slice(7).trim()
|
|
4543
|
+
if (argument === '') {
|
|
4544
|
+
openReviewPicker()
|
|
4545
|
+
return
|
|
4546
|
+
}
|
|
4547
|
+
try {
|
|
4548
|
+
reviewChanges(parseReviewArgument(argument))
|
|
4549
|
+
} catch (error: unknown) {
|
|
4550
|
+
notify(error instanceof Error ? error.message : String(error), 'warning')
|
|
4551
|
+
}
|
|
4096
4552
|
return
|
|
4097
4553
|
}
|
|
4098
4554
|
if (text === '/model' || text.startsWith('/model ')) {
|
|
@@ -4127,6 +4583,10 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4127
4583
|
else dispatch(text)
|
|
4128
4584
|
return
|
|
4129
4585
|
}
|
|
4586
|
+
if (text === '/search' || text.startsWith('/search ')) {
|
|
4587
|
+
openSearch(text.slice(7).trim())
|
|
4588
|
+
return
|
|
4589
|
+
}
|
|
4130
4590
|
if (text === '/new' || text.startsWith('/new ')) {
|
|
4131
4591
|
createSession(text.slice(4).trim() || undefined)
|
|
4132
4592
|
return
|
|
@@ -4159,17 +4619,41 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4159
4619
|
openTheme()
|
|
4160
4620
|
return
|
|
4161
4621
|
}
|
|
4622
|
+
if (text === '/language' || text.startsWith('/language ')) {
|
|
4623
|
+
const argument = text.slice('/language'.length).trim()
|
|
4624
|
+
if (argument === '') openLanguage()
|
|
4625
|
+
else if (argument === 'en' || argument === 'zh') {
|
|
4626
|
+
saveLanguage(parseLanguageName(argument))
|
|
4627
|
+
notify(t('notice.languageSaved', { name: argument }))
|
|
4628
|
+
refresh()
|
|
4629
|
+
} else notify(t('notice.usage.language'), 'warning')
|
|
4630
|
+
return
|
|
4631
|
+
}
|
|
4162
4632
|
if (text === '/animation' || text.startsWith('/animation ')) {
|
|
4163
4633
|
const parsed = parseAnimationsArgument(text.slice('/animation'.length))
|
|
4164
4634
|
if (parsed === 'toggle') applyAnimations(!animations)
|
|
4165
|
-
else if (parsed === 'usage') notify('usage
|
|
4635
|
+
else if (parsed === 'usage') notify(t('notice.usage.animation'), 'info')
|
|
4166
4636
|
else applyAnimations(parsed.enabled)
|
|
4167
4637
|
return
|
|
4168
4638
|
}
|
|
4639
|
+
if (text === '/rainbow' || text.startsWith('/rainbow ')) {
|
|
4640
|
+
const parsed = parseRainbowArgument(text.slice('/rainbow'.length))
|
|
4641
|
+
if (parsed === 'usage') notify(t('notice.usage.rainbow'), 'warning')
|
|
4642
|
+
else applyRainbow(parsed === 'random' ? undefined : parsed.seed)
|
|
4643
|
+
return
|
|
4644
|
+
}
|
|
4169
4645
|
if (text === '/history') {
|
|
4170
4646
|
openHistory()
|
|
4171
4647
|
return
|
|
4172
4648
|
}
|
|
4649
|
+
if (text === '/queue') {
|
|
4650
|
+
openQueue()
|
|
4651
|
+
return
|
|
4652
|
+
}
|
|
4653
|
+
if (text === '/usage') {
|
|
4654
|
+
openUsage()
|
|
4655
|
+
return
|
|
4656
|
+
}
|
|
4173
4657
|
if (text === '/agents') {
|
|
4174
4658
|
openAgents()
|
|
4175
4659
|
return
|
|
@@ -4193,19 +4677,25 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4193
4677
|
openDelete(text.slice(7).trim())
|
|
4194
4678
|
return
|
|
4195
4679
|
}
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4680
|
+
// Delivery mode: everything above this point is a local command or a
|
|
4681
|
+
// panel opener and always runs out of band. A real prompt follows the
|
|
4682
|
+
// composer's Tab choice — `steer` joins the running turn, the default
|
|
4683
|
+
// queues it for the next one.
|
|
4684
|
+
if (submitMode === 'steer') steer(text)
|
|
4685
|
+
else dispatch(text)
|
|
4686
|
+
return
|
|
4687
|
+
}
|
|
4688
|
+
// The modified-Enter newline family: Ctrl+J arrives as a bare LF (Ink
|
|
4689
|
+
// names it 'enter', not 'return'), and the kitty layer normalizes
|
|
4690
|
+
// Ctrl/Shift+Enter to the same byte. Alt+Enter reaches here as a bare CR
|
|
4691
|
+
// with no flags — Ink's parser drops the escape and reports no meta, and
|
|
4692
|
+
// plain Enter always carries key.return — so a flagless CR is Alt+Enter.
|
|
4693
|
+
// Only plain Enter submits. app.spec's "modified-Enter family" test pins
|
|
4694
|
+
// this exact parser shape; an Ink upgrade that changes it fails there.
|
|
4695
|
+
if (input === '\n' || input === '\r') {
|
|
4696
|
+
applyEdit(insertText(liveValue, liveCursor, '\n'))
|
|
4204
4697
|
return
|
|
4205
4698
|
}
|
|
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
4699
|
// A fast Tab followed by text can arrive as one readable chunk in an
|
|
4210
4700
|
// integrated terminal. Accept the candidate first, then apply the
|
|
4211
4701
|
// remaining characters against the synchronously updated editor refs.
|
|
@@ -4377,13 +4867,23 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4377
4867
|
if (!animations && waveKey !== null && waveKey !== wavePlayedKey) setWavePlayedKey(waveKey)
|
|
4378
4868
|
}, [animations, waveKey, wavePlayedKey])
|
|
4379
4869
|
const waveArmed = waveKey !== null && waveKey !== wavePlayedKey
|
|
4870
|
+
const burstKey = rainbowBurstId > 0 ? `rainbow:${rainbowBurstId}` : null
|
|
4871
|
+
const [burstPlayedKey, setBurstPlayedKey] = useState<string | null>(null)
|
|
4872
|
+
useEffect(() => {
|
|
4873
|
+
if (!animations && burstKey !== null && burstKey !== burstPlayedKey) setBurstPlayedKey(burstKey)
|
|
4874
|
+
}, [animations, burstKey, burstPlayedKey])
|
|
4875
|
+
const burstArmed = burstKey !== null && burstKey !== burstPlayedKey
|
|
4380
4876
|
|
|
4381
4877
|
// Every exclusive panel keeps the composer as a stable visual anchor, but
|
|
4382
4878
|
// freezes it to one row: no menu, multiline wrap, or animation.
|
|
4383
4879
|
const tierActive = waveTier !== null
|
|
4384
4880
|
const tierHues = waveTier === null ? null : deepseekWaveHues(waveTier)
|
|
4385
4881
|
const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0])
|
|
4386
|
-
const
|
|
4882
|
+
const waveGlyph = waveTier === 'flash' ? '›' : waveTier === 'deepseek' ? '»' : '❯'
|
|
4883
|
+
// Steer mode owns the prompt glyph in every paint path (static band, wave,
|
|
4884
|
+
// rainbow burst), so the mode is visible without reading the placeholder.
|
|
4885
|
+
const promptGlyph = submitMode === 'steer' ? '↳' : waveGlyph
|
|
4886
|
+
const placeholderText = composerPlaceholder(submitMode)
|
|
4387
4887
|
// The multiline editor model: the sanitized draft hard-wrapped into
|
|
4388
4888
|
// column-safe physical rows, with the caret mapped to its exact row and
|
|
4389
4889
|
// column. Computed before the frozen path so the row report below runs
|
|
@@ -4457,7 +4957,7 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4457
4957
|
return band(createElement(
|
|
4458
4958
|
Text,
|
|
4459
4959
|
{ backgroundColor: bandBg, wrap: 'truncate-end' },
|
|
4460
|
-
createElement(Text, { color: promptColor, bold: tierActive ? true : undefined }, busy ? '… ' : `${promptGlyph} `),
|
|
4960
|
+
createElement(Text, { color: promptColor, bold: tierActive ? true : undefined }, submitMode === 'steer' ? '↳ ' : busy ? '… ' : `${promptGlyph} `),
|
|
4461
4961
|
frozenLine,
|
|
4462
4962
|
bandFill(2 + visibleColumns(frozenLine)),
|
|
4463
4963
|
))
|
|
@@ -4475,10 +4975,10 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4475
4975
|
// spacer or a second blink timer.
|
|
4476
4976
|
const editorRows: ReactElement[] = []
|
|
4477
4977
|
for (let index = editorWindowStart; index < Math.min(editorViewModel.rows.length, editorWindowStart + editorWindowRows); index += 1) {
|
|
4478
|
-
const row = editorViewModel.rows[index]
|
|
4978
|
+
const row = editorViewModel.rows[index]
|
|
4479
4979
|
const parts = editorRowParts(row, index, caret.row, clampedCursor, !preparingImages)
|
|
4480
4980
|
const placeholder = index === 0 && value === '' && !busy && !preparingImages
|
|
4481
|
-
const tail = placeholder ?
|
|
4981
|
+
const tail = placeholder ? placeholderText : parts.after
|
|
4482
4982
|
const consumed = 2 + visibleColumns(parts.before) + visibleColumns(parts.caret) + visibleColumns(tail)
|
|
4483
4983
|
editorRows.push(createElement(
|
|
4484
4984
|
Text,
|
|
@@ -4486,16 +4986,18 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4486
4986
|
index === 0
|
|
4487
4987
|
? preparingImages
|
|
4488
4988
|
? createElement(Text, { color: inkColor(getPalette().warn), bold: true }, '… ')
|
|
4489
|
-
:
|
|
4490
|
-
? createElement(
|
|
4491
|
-
:
|
|
4989
|
+
: submitMode === 'steer'
|
|
4990
|
+
? createElement(Text, { color: promptColor, bold: true }, '↳ ')
|
|
4991
|
+
: busy
|
|
4992
|
+
? createElement(BusyChase, { animated: animations })
|
|
4993
|
+
: createElement(Text, { color: promptColor, bold: tierActive ? true : undefined }, `${promptGlyph} `)
|
|
4492
4994
|
: ' ',
|
|
4493
4995
|
parts.before,
|
|
4494
4996
|
parts.hasCaret
|
|
4495
4997
|
? createElement(Text, { key: 'caret', inverse: cursorVisible || undefined }, parts.caret)
|
|
4496
4998
|
: null,
|
|
4497
4999
|
placeholder
|
|
4498
|
-
? createElement(Text, { dimColor: true },
|
|
5000
|
+
? createElement(Text, { dimColor: true }, tail)
|
|
4499
5001
|
: parts.after,
|
|
4500
5002
|
bandFill(consumed),
|
|
4501
5003
|
))
|
|
@@ -4511,26 +5013,47 @@ function Input({ active, frozen, frozenHint, busy, descriptors, skills, dispatch
|
|
|
4511
5013
|
Box,
|
|
4512
5014
|
{ flexDirection: 'column' },
|
|
4513
5015
|
menu,
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
5016
|
+
burstArmed
|
|
5017
|
+
? createElement(ComposerRainbowBurst, {
|
|
5018
|
+
key: burstKey,
|
|
5019
|
+
active: !busy && !preparingImages && animations,
|
|
5020
|
+
onSettled: () => {
|
|
5021
|
+
if (burstKey !== null) setBurstPlayedKey(burstKey)
|
|
5022
|
+
},
|
|
5023
|
+
fallback: band(staticEditor),
|
|
5024
|
+
bandWidth,
|
|
5025
|
+
bandBg,
|
|
5026
|
+
rows: editorViewModel.rows.slice(editorWindowStart, editorWindowStart + editorWindowRows),
|
|
5027
|
+
windowStart: editorWindowStart,
|
|
5028
|
+
caretRow: caret.row,
|
|
5029
|
+
cursor: clampedCursor,
|
|
5030
|
+
caretVisible: cursorVisible,
|
|
5031
|
+
value,
|
|
5032
|
+
promptGlyph,
|
|
5033
|
+
placeholder: placeholderText,
|
|
5034
|
+
promptColor,
|
|
5035
|
+
})
|
|
5036
|
+
: createElement(ComposerWave, {
|
|
5037
|
+
key: waveKey ?? 'static',
|
|
5038
|
+
tier: waveTier ?? 'deepseek',
|
|
5039
|
+
style: waveStyle ?? 'wave',
|
|
5040
|
+
active: waveTier !== null && waveStyle !== null && !busy && !preparingImages && animations && waveArmed,
|
|
5041
|
+
onSettled: () => {
|
|
5042
|
+
if (waveKey !== null) setWavePlayedKey(waveKey)
|
|
5043
|
+
},
|
|
5044
|
+
fallback: band(staticEditor),
|
|
5045
|
+
bandWidth,
|
|
5046
|
+
bandBg,
|
|
5047
|
+
rows: editorViewModel.rows.slice(editorWindowStart, editorWindowStart + editorWindowRows),
|
|
5048
|
+
windowStart: editorWindowStart,
|
|
5049
|
+
caretRow: caret.row,
|
|
5050
|
+
cursor: clampedCursor,
|
|
5051
|
+
caretVisible: cursorVisible,
|
|
5052
|
+
value,
|
|
5053
|
+
promptGlyph,
|
|
5054
|
+
placeholder: placeholderText,
|
|
5055
|
+
promptColor,
|
|
5056
|
+
}),
|
|
4534
5057
|
)
|
|
4535
5058
|
}
|
|
4536
5059
|
|
|
@@ -4755,7 +5278,13 @@ export function computeSettledRows(
|
|
|
4755
5278
|
|
|
4756
5279
|
/** The whole terminal app; state arrives via the store, output via Ink. */
|
|
4757
5280
|
export function App(props: AppProps): ReactElement {
|
|
4758
|
-
|
|
5281
|
+
// The stores are closure-backed singletons whose methods never touch `this`,
|
|
5282
|
+
// but a bare method reference still detaches it from its receiver. One stable
|
|
5283
|
+
// wrapper per store keeps both the receiver and the reference identity the
|
|
5284
|
+
// `useSyncExternalStore` contract requires.
|
|
5285
|
+
const subscribeTranscript = useCallback((listener: () => void) => props.store.subscribe(listener), [props.store])
|
|
5286
|
+
const readTranscript = useCallback(() => props.store.getView(), [props.store])
|
|
5287
|
+
const view = useSyncExternalStore(subscribeTranscript, readTranscript)
|
|
4759
5288
|
// Terminal input anchor: Ink reference-counts raw mode across every active
|
|
4760
5289
|
// `useInput` hook, so mutually exclusive surfaces (composer <-> approval
|
|
4761
5290
|
// bar <-> panels) drop the count to zero inside each handoff commit — the
|
|
@@ -4777,8 +5306,10 @@ export function App(props: AppProps): ReactElement {
|
|
|
4777
5306
|
// process-stable, so one callback per view identity is enough.
|
|
4778
5307
|
const readDescriptors = useCallback(() => props.commands.descriptors, [props.commands])
|
|
4779
5308
|
const readSkills = useCallback(() => props.skills.rows, [props.skills])
|
|
4780
|
-
const
|
|
4781
|
-
const
|
|
5309
|
+
const subscribeCommands = useCallback((listener: () => void) => props.commands.subscribe(listener), [props.commands])
|
|
5310
|
+
const subscribeSkills = useCallback((listener: () => void) => props.skills.subscribe(listener), [props.skills])
|
|
5311
|
+
const descriptors = useSyncExternalStore(subscribeCommands, readDescriptors)
|
|
5312
|
+
const skills = useSyncExternalStore(subscribeSkills, readSkills)
|
|
4782
5313
|
const [modelLabel, setModelLabel] = useState(props.model)
|
|
4783
5314
|
const [modelOpen, setModelOpen] = useState(false)
|
|
4784
5315
|
/** Nested /model stages; only one owns terminal input at a time. */
|
|
@@ -4807,13 +5338,17 @@ export function App(props: AppProps): ReactElement {
|
|
|
4807
5338
|
* (ordinary turns, image preparation, /animation toggles) never replays. */
|
|
4808
5339
|
const [waveTier, setWaveTier] = useState<DeepseekWaveTier | null>(null)
|
|
4809
5340
|
const [waveStyle, setWaveStyle] = useState<DeepseekWaveStyle | null>(null)
|
|
5341
|
+
const [rainbowBurstId, setRainbowBurstId] = useState(0)
|
|
5342
|
+
const fireRainbowBurst = (): void => {
|
|
5343
|
+
setRainbowBurstId(id => id + 1)
|
|
5344
|
+
}
|
|
4810
5345
|
// /animation toggle: applies immediately, persists through the runner, and
|
|
4811
5346
|
// gates every timed leaf (shimmer, chase, blink, wave) for this render.
|
|
4812
5347
|
const [animations, setAnimations] = useState(props.animations ?? true)
|
|
4813
5348
|
const applyAnimations = (enabled: boolean): void => {
|
|
4814
5349
|
setAnimations(enabled)
|
|
4815
5350
|
props.saveAnimations?.(enabled)
|
|
4816
|
-
notify(
|
|
5351
|
+
notify(t('notice.animationState', { state: enabled ? 'on' : 'off' }))
|
|
4817
5352
|
}
|
|
4818
5353
|
const previousModel = useRef<string | undefined>(undefined)
|
|
4819
5354
|
const previousEffort = useRef<string | undefined>(props.effort)
|
|
@@ -4928,11 +5463,22 @@ export function App(props: AppProps): ReactElement {
|
|
|
4928
5463
|
// Dedupe for the dynamic-budget tripwire: one warning per distinct shape.
|
|
4929
5464
|
const budgetWarnRef = useRef<string | undefined>(undefined)
|
|
4930
5465
|
const [verboseOpen, setVerboseOpen] = useState(false)
|
|
5466
|
+
const [queueOpen, setQueueOpen] = useState(false)
|
|
5467
|
+
/**
|
|
5468
|
+
* How the composer delivers its next submission: `queue` waits for the next
|
|
5469
|
+
* turn, `steer` joins the turn already running. Tab on an empty composer
|
|
5470
|
+
* flips it; the prompt glyph and the placeholder both name the current mode.
|
|
5471
|
+
*/
|
|
5472
|
+
const [submitMode, setSubmitMode] = useState<'queue' | 'steer'>('queue')
|
|
4931
5473
|
const [diffView, setDiffView] = useState<GitDiffView | undefined>(undefined)
|
|
5474
|
+
const [reviewPickerOpen, setReviewPickerOpen] = useState(false)
|
|
4932
5475
|
const [helpOpen, setHelpOpen] = useState(false)
|
|
4933
5476
|
const [modeOpen, setModeOpen] = useState(false)
|
|
4934
5477
|
const [permissionOpen, setPermissionOpen] = useState(false)
|
|
4935
5478
|
const [resumeOpen, setResumeOpen] = useState(false)
|
|
5479
|
+
const [searchOpen, setSearchOpen] = useState(false)
|
|
5480
|
+
/** /search seed: the query from `/search <text>` (cleared on open). */
|
|
5481
|
+
const [searchSeed, setSearchSeed] = useState('')
|
|
4936
5482
|
const [pluginOpen, setPluginOpen] = useState(false)
|
|
4937
5483
|
const [pluginQuery, setPluginQuery] = useState('')
|
|
4938
5484
|
const [updateOpen, setUpdateOpen] = useState(false)
|
|
@@ -4941,10 +5487,12 @@ export function App(props: AppProps): ReactElement {
|
|
|
4941
5487
|
const [statuslineOpen, setStatuslineOpen] = useState(false)
|
|
4942
5488
|
const [statuslineItems, setStatuslineItems] = useState<readonly StatusItemId[]>(() => parseStatuslineItems(props.statusline))
|
|
4943
5489
|
const [themeOpen, setThemeOpen] = useState(false)
|
|
5490
|
+
const [languageOpen, setLanguageOpen] = useState(false)
|
|
4944
5491
|
const [historyOpen, setHistoryOpen] = useState(false)
|
|
4945
5492
|
const [agentsOpen, setAgentsOpen] = useState(false)
|
|
4946
5493
|
const [subagentOpen, setSubagentOpen] = useState(false)
|
|
4947
5494
|
const [todosOpen, setTodosOpen] = useState(false)
|
|
5495
|
+
const [usageOpen, setUsageOpen] = useState(false)
|
|
4948
5496
|
/** /delete state: delete-mode hint plus an optional pre-armed row id. */
|
|
4949
5497
|
const [resumeDelete, setResumeDelete] = useState<{ mode: boolean; id?: string }>({ mode: false })
|
|
4950
5498
|
/** The row id awaiting y/n in the COMPOSER (codex delete confirm): the
|
|
@@ -4968,7 +5516,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
4968
5516
|
// from the list immediately, not look like a no-op.
|
|
4969
5517
|
setDeleteReloadToken(token => token + 1)
|
|
4970
5518
|
}, (reason: unknown) => {
|
|
4971
|
-
notify(
|
|
5519
|
+
notify(t('notice.deleteFailed', { message: reason instanceof Error ? reason.message : String(reason) }), 'error')
|
|
4972
5520
|
})
|
|
4973
5521
|
}, [deleteConfirmId, props.deleteSession, notify])
|
|
4974
5522
|
/** The /history panel's accepted entry: text plus its recall-space index. */
|
|
@@ -4988,26 +5536,25 @@ export function App(props: AppProps): ReactElement {
|
|
|
4988
5536
|
}, [])
|
|
4989
5537
|
/** The append-only flush boundary (see `settledEntryCount`): entries below
|
|
4990
5538
|
* 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. */
|
|
5539
|
+
* beyond stays in the live tree. */
|
|
4994
5540
|
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])
|
|
5541
|
+
/** Next-turn rows in durable inbox order: a running tool row can split the
|
|
5542
|
+
* pending rows, so this maps the inbox id list onto the folded entries
|
|
5543
|
+
* instead of scanning the mutable tail. */
|
|
5544
|
+
const queuedRows = useMemo(
|
|
5545
|
+
() => queuedInboxRows(view.entries, view.pending['next-turn']),
|
|
5546
|
+
[view.entries, view.pending],
|
|
5547
|
+
)
|
|
5007
5548
|
const [refreshEpoch, setRefreshEpoch] = useState(0)
|
|
5008
|
-
const
|
|
5009
|
-
const
|
|
5010
|
-
const
|
|
5549
|
+
const subscribeApproval = useCallback((listener: () => void) => props.approval.subscribe(listener), [props.approval])
|
|
5550
|
+
const readApprovalSnapshot = useCallback(() => props.approval.getSnapshot(), [props.approval])
|
|
5551
|
+
const subscribeQuestions = useCallback((listener: () => void) => props.questions.subscribe(listener), [props.questions])
|
|
5552
|
+
const readQuestionSnapshot = useCallback(() => props.questions.getSnapshot(), [props.questions])
|
|
5553
|
+
const subscribeSubagents = useCallback((listener: () => void) => props.subagents.subscribe(listener), [props.subagents])
|
|
5554
|
+
const readAgentRows = useCallback(() => props.subagents.getSnapshot(), [props.subagents])
|
|
5555
|
+
const approvalSnapshot = useSyncExternalStore(subscribeApproval, readApprovalSnapshot)
|
|
5556
|
+
const questionSnapshot = useSyncExternalStore(subscribeQuestions, readQuestionSnapshot)
|
|
5557
|
+
const agentRows = useSyncExternalStore(subscribeSubagents, readAgentRows)
|
|
5011
5558
|
const approvalPending = approvalSnapshot.pending !== undefined
|
|
5012
5559
|
const questionPending = questionSnapshot.pending !== undefined
|
|
5013
5560
|
// While any modal owns the keys, the prompt box passes everything through.
|
|
@@ -5016,7 +5563,8 @@ export function App(props: AppProps): ReactElement {
|
|
|
5016
5563
|
// panel keypress.
|
|
5017
5564
|
const inputActive = deleteConfirmId !== undefined
|
|
5018
5565
|
? !approvalPending && !questionPending
|
|
5019
|
-
: !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !updateOpen && !scheduleOpen && !jobsOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !todosOpen && !verboseOpen && diffView === undefined && !approvalPending && !questionPending
|
|
5566
|
+
: !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !updateOpen && !scheduleOpen && !jobsOpen && !statuslineOpen && !themeOpen && !languageOpen && !historyOpen && !queueOpen && !agentsOpen && !subagentOpen && !todosOpen && !usageOpen && !verboseOpen && diffView === undefined && !reviewPickerOpen && !approvalPending && !questionPending
|
|
5567
|
+
const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !updateOpen && !scheduleOpen && !jobsOpen && !statuslineOpen && !themeOpen && !languageOpen && !historyOpen && !queueOpen && !agentsOpen && !subagentOpen && !todosOpen && !usageOpen && !verboseOpen && diffView === undefined && !reviewPickerOpen && !approvalPending && !questionPending
|
|
5020
5568
|
|
|
5021
5569
|
// Human questions outrank local inspectors. Close the lower modal instead
|
|
5022
5570
|
// of leaving an approval/question visible but keyboard-locked behind it.
|
|
@@ -5036,6 +5584,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5036
5584
|
setStatuslineOpen(false)
|
|
5037
5585
|
setThemeOpen(false)
|
|
5038
5586
|
setHistoryOpen(false)
|
|
5587
|
+
setQueueOpen(false)
|
|
5039
5588
|
setAgentsOpen(false)
|
|
5040
5589
|
setSubagentOpen(false)
|
|
5041
5590
|
setTodosOpen(false)
|
|
@@ -5085,6 +5634,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5085
5634
|
// One pending synchronized frame covers a debounced resize or explicit
|
|
5086
5635
|
// source-backed replay. It is closed after the corresponding React commit.
|
|
5087
5636
|
const synchronizedReplayPending = useRef(false)
|
|
5637
|
+
const resizeBurstHeld = useRef(false)
|
|
5088
5638
|
useEffect(() => {
|
|
5089
5639
|
if (appStdout === undefined) return
|
|
5090
5640
|
let replayTimer: ReturnType<typeof setTimeout> | undefined
|
|
@@ -5096,24 +5646,31 @@ export function App(props: AppProps): ReactElement {
|
|
|
5096
5646
|
if (next.columns === terminalSizeRef.current.columns && next.rows === terminalSizeRef.current.rows) return
|
|
5097
5647
|
terminalSizeRef.current = next
|
|
5098
5648
|
|
|
5099
|
-
//
|
|
5100
|
-
//
|
|
5101
|
-
//
|
|
5102
|
-
//
|
|
5103
|
-
|
|
5104
|
-
|
|
5649
|
+
// Hold the visible frame for the whole burst so intermediate Ink
|
|
5650
|
+
// relayouts (new width against still-old Static rows) never flash as
|
|
5651
|
+
// doubled borders. One clear + Static remount still runs after the
|
|
5652
|
+
// burst settles.
|
|
5653
|
+
if (!resizeBurstHeld.current) {
|
|
5654
|
+
resizeBurstHeld.current = true
|
|
5655
|
+
appStdout.write(SYNCHRONIZED_UPDATE_BEGIN)
|
|
5656
|
+
}
|
|
5105
5657
|
setTerminalSize(next)
|
|
5106
5658
|
if (replayTimer !== undefined) clearTimeout(replayTimer)
|
|
5107
5659
|
replayTimer = setTimeout(() => {
|
|
5108
5660
|
synchronizedReplayPending.current = true
|
|
5109
|
-
appStdout.write(
|
|
5661
|
+
appStdout.write(RESIZE_REFLOW_CLEAR)
|
|
5110
5662
|
setRefreshEpoch(epoch => epoch + 1)
|
|
5663
|
+
resizeBurstHeld.current = false
|
|
5111
5664
|
}, RESIZE_REFLOW_DELAY_MS)
|
|
5112
5665
|
}
|
|
5113
5666
|
appStdout.on('resize', handleResize)
|
|
5114
5667
|
return () => {
|
|
5115
5668
|
appStdout.off('resize', handleResize)
|
|
5116
5669
|
if (replayTimer !== undefined) clearTimeout(replayTimer)
|
|
5670
|
+
if (resizeBurstHeld.current) {
|
|
5671
|
+
appStdout.write(SYNCHRONIZED_UPDATE_END)
|
|
5672
|
+
resizeBurstHeld.current = false
|
|
5673
|
+
}
|
|
5117
5674
|
}
|
|
5118
5675
|
}, [appStdout])
|
|
5119
5676
|
const terminalRows = terminalSize.rows
|
|
@@ -5146,14 +5703,19 @@ export function App(props: AppProps): ReactElement {
|
|
|
5146
5703
|
}, [])
|
|
5147
5704
|
const imeRowsBelowComposer = statusBarRows + 1
|
|
5148
5705
|
const composerEditorCap = composerMaxRows(terminalRows)
|
|
5149
|
-
//
|
|
5150
|
-
//
|
|
5151
|
-
//
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5706
|
+
// Pin the composer and status at the bottom: every extra chrome row
|
|
5707
|
+
// (completion menu, notice, todos, agents, extra editor/status rows)
|
|
5708
|
+
// covers live transcript instead of growing the tree.
|
|
5709
|
+
const dynamicRows = liveRegionBudget({
|
|
5710
|
+
terminalRows,
|
|
5711
|
+
composerRows,
|
|
5712
|
+
statusBarRows,
|
|
5713
|
+
menuRows,
|
|
5714
|
+
gutterRows: composerGutterRows,
|
|
5715
|
+
notice: notice !== undefined,
|
|
5716
|
+
todo: transcriptVisible && view.todos.length > 0,
|
|
5717
|
+
agents: transcriptVisible && agentRows.length > 0,
|
|
5718
|
+
})
|
|
5157
5719
|
const streamingActive = view.streaming !== '' || view.streamingReasoning !== ''
|
|
5158
5720
|
const deepDivingVisible = busy && !streamingActive
|
|
5159
5721
|
// Terminal tab label: "deepseek" until the session carries a name, then the
|
|
@@ -5201,6 +5763,10 @@ export function App(props: AppProps): ReactElement {
|
|
|
5201
5763
|
)
|
|
5202
5764
|
if (liveAudit.warning !== undefined && budgetWarnRef.current !== liveAudit.warning) {
|
|
5203
5765
|
budgetWarnRef.current = liveAudit.warning
|
|
5766
|
+
// The budget tripwire is a last-resort diagnostic: it fires only when the
|
|
5767
|
+
// live/static allocation already broke its contract, and Ink owns/shadows
|
|
5768
|
+
// console output. Everywhere else the TUI must never write to stdout.
|
|
5769
|
+
// eslint-disable-next-line no-console -- see the tripwire note above
|
|
5204
5770
|
console.warn(`[dsh-code] ${liveAudit.warning}`)
|
|
5205
5771
|
}
|
|
5206
5772
|
const auditedLiveLines = liveAudit.allocation.live === visibleLiveLines.length
|
|
@@ -5208,9 +5774,8 @@ export function App(props: AppProps): ReactElement {
|
|
|
5208
5774
|
: visibleLiveLines.slice(-liveAudit.allocation.live)
|
|
5209
5775
|
const auditedReasoningRows = liveAudit.allocation.reasoning
|
|
5210
5776
|
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
5777
|
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
|
|
5778
|
+
const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || updateOpen || scheduleOpen || jobsOpen || statuslineOpen || themeOpen || languageOpen || historyOpen || queueOpen || agentsOpen || subagentOpen || todosOpen || usageOpen || inspectorVisible || diffView !== undefined || reviewPickerOpen || approvalPending || questionPending
|
|
5214
5779
|
// The surface that currently owns the keyboard, named in the frozen band:
|
|
5215
5780
|
// an empty composer under a panel must not advertise typing it cannot
|
|
5216
5781
|
// accept — every key actually feeds the panel (which may or may not
|
|
@@ -5221,6 +5786,8 @@ export function App(props: AppProps): ReactElement {
|
|
|
5221
5786
|
? 'the question'
|
|
5222
5787
|
: diffView !== undefined
|
|
5223
5788
|
? 'the diff review'
|
|
5789
|
+
: reviewPickerOpen
|
|
5790
|
+
? 'the review picker'
|
|
5224
5791
|
: modelOpen
|
|
5225
5792
|
? '/model'
|
|
5226
5793
|
: helpOpen
|
|
@@ -5243,6 +5810,8 @@ export function App(props: AppProps): ReactElement {
|
|
|
5243
5810
|
? '/statusline'
|
|
5244
5811
|
: themeOpen
|
|
5245
5812
|
? '/theme'
|
|
5813
|
+
: languageOpen
|
|
5814
|
+
? '/language'
|
|
5246
5815
|
: historyOpen
|
|
5247
5816
|
? '/history'
|
|
5248
5817
|
: agentsOpen
|
|
@@ -5251,9 +5820,11 @@ export function App(props: AppProps): ReactElement {
|
|
|
5251
5820
|
? '/subagent'
|
|
5252
5821
|
: todosOpen
|
|
5253
5822
|
? '/todos'
|
|
5254
|
-
:
|
|
5255
|
-
? '
|
|
5256
|
-
:
|
|
5823
|
+
: usageOpen
|
|
5824
|
+
? '/usage'
|
|
5825
|
+
: inspectorVisible
|
|
5826
|
+
? 'history details'
|
|
5827
|
+
: undefined
|
|
5257
5828
|
const frozenHint = keyboardOwner === undefined
|
|
5258
5829
|
? undefined
|
|
5259
5830
|
: `keys go to ${keyboardOwner} · esc ${approvalPending ? 'rejects' : questionPending ? 'cancels' : 'closes'}`
|
|
@@ -5272,6 +5843,18 @@ export function App(props: AppProps): ReactElement {
|
|
|
5272
5843
|
}
|
|
5273
5844
|
setRefreshEpoch(epoch => epoch + 1)
|
|
5274
5845
|
}
|
|
5846
|
+
const applyRainbow = (seed?: number): void => {
|
|
5847
|
+
// Replace the memoized roll, then setTheme so getPalette() and the
|
|
5848
|
+
// painters pick the new values; persist rainbow as the active theme
|
|
5849
|
+
// so a mid-session /rainbow from dark/light actually sticks. The
|
|
5850
|
+
// source-backed rebuild (same as /theme) repaints Static history too.
|
|
5851
|
+
rerollRainbow(seed)
|
|
5852
|
+
setTheme('rainbow')
|
|
5853
|
+
props.saveTheme?.('rainbow')
|
|
5854
|
+
notify(t('notice.rainbowRolled', { seed: rainbowSeedLabel() }))
|
|
5855
|
+
fireRainbowBurst()
|
|
5856
|
+
refreshScreen()
|
|
5857
|
+
}
|
|
5275
5858
|
useEffect(() => {
|
|
5276
5859
|
if (!synchronizedReplayPending.current || appStdout === undefined) return
|
|
5277
5860
|
synchronizedReplayPending.current = false
|
|
@@ -5302,16 +5885,16 @@ export function App(props: AppProps): ReactElement {
|
|
|
5302
5885
|
setEffortLabel(effortId)
|
|
5303
5886
|
const selected = `${label}${effortId === undefined || effortId === '' ? '' : `@${effortId}`}`
|
|
5304
5887
|
if (sessionHasImages && row.inputModalities !== undefined && !row.inputModalities.includes('image')) {
|
|
5305
|
-
notify(
|
|
5888
|
+
notify(t('notice.modelChangedPlaceholder', { model: selected }), 'warning')
|
|
5306
5889
|
} else {
|
|
5307
|
-
notify(
|
|
5890
|
+
notify(t('notice.modelNextStep', { model: selected }))
|
|
5308
5891
|
}
|
|
5309
5892
|
setModelOpen(false)
|
|
5310
5893
|
setProviderOpen(false)
|
|
5311
5894
|
setProviderAction(undefined)
|
|
5312
5895
|
setEffortFor(undefined)
|
|
5313
5896
|
} catch (error: unknown) {
|
|
5314
|
-
notify(
|
|
5897
|
+
notify(t('notice.modelSwitchFailed', { message: error instanceof Error ? error.message : String(error) }), 'error')
|
|
5315
5898
|
}
|
|
5316
5899
|
}
|
|
5317
5900
|
|
|
@@ -5332,7 +5915,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5332
5915
|
const seen = new Set<string>()
|
|
5333
5916
|
for (const row of providerDirectory?.rows ?? []) {
|
|
5334
5917
|
for (const model of row.configuration.models) {
|
|
5335
|
-
const raw = (model.extras
|
|
5918
|
+
const raw = (model.extras)?.reasoningEfforts
|
|
5336
5919
|
if (!isDeclaredReasoningEfforts(raw)) continue
|
|
5337
5920
|
const key = row.provider + '/' + model.id
|
|
5338
5921
|
if (seen.has(key)) continue
|
|
@@ -5370,7 +5953,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5370
5953
|
setProviderAction(undefined)
|
|
5371
5954
|
setProviderOpen(false)
|
|
5372
5955
|
reloadModelSurfaces()
|
|
5373
|
-
notify(
|
|
5956
|
+
notify(t('notice.loggedIn', { provider: authorization.label }))
|
|
5374
5957
|
},
|
|
5375
5958
|
back: () => {
|
|
5376
5959
|
setProviderAction(undefined)
|
|
@@ -5386,7 +5969,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5386
5969
|
setProviderAction(undefined)
|
|
5387
5970
|
setProviderOpen(true)
|
|
5388
5971
|
reloadModelSurfaces()
|
|
5389
|
-
notify(
|
|
5972
|
+
notify(t('notice.loggedOut', { provider: authorization.label }))
|
|
5390
5973
|
},
|
|
5391
5974
|
back: () => setProviderAction(undefined),
|
|
5392
5975
|
})
|
|
@@ -5397,13 +5980,13 @@ export function App(props: AppProps): ReactElement {
|
|
|
5397
5980
|
save: props.saveModelProviderConfiguration,
|
|
5398
5981
|
saveCredential: props.saveModelProviderCredential,
|
|
5399
5982
|
discover: props.discoverModelProvider
|
|
5400
|
-
?? (
|
|
5983
|
+
?? (() => Promise.reject(new Error('model discovery is unavailable in this profile; enter models by hand'))),
|
|
5401
5984
|
done: result => {
|
|
5402
5985
|
const target = providerAction.target
|
|
5403
5986
|
setProviderAction(undefined)
|
|
5404
5987
|
setProviderOpen(true)
|
|
5405
5988
|
reloadModelSurfaces()
|
|
5406
|
-
notify(
|
|
5989
|
+
notify(t('notice.providerSaved', { provider: target.displayName, suffix: result.key ? ' · API key updated' : '' }))
|
|
5407
5990
|
},
|
|
5408
5991
|
back: () => setProviderAction(undefined),
|
|
5409
5992
|
onExit: closeModelSurface,
|
|
@@ -5418,7 +6001,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5418
6001
|
setProviderAction(undefined)
|
|
5419
6002
|
setProviderOpen(true)
|
|
5420
6003
|
reloadModelSurfaces()
|
|
5421
|
-
notify(
|
|
6004
|
+
notify(t('notice.apiKeyRemoved', { provider: target.displayName }))
|
|
5422
6005
|
},
|
|
5423
6006
|
back: () => setProviderAction(undefined),
|
|
5424
6007
|
})
|
|
@@ -5432,7 +6015,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5432
6015
|
setProviderAction(undefined)
|
|
5433
6016
|
setProviderOpen(true)
|
|
5434
6017
|
reloadModelSurfaces()
|
|
5435
|
-
notify(
|
|
6018
|
+
notify(t('notice.providerRemoved', { provider: target.displayName }))
|
|
5436
6019
|
},
|
|
5437
6020
|
back: () => setProviderAction(undefined),
|
|
5438
6021
|
})
|
|
@@ -5444,42 +6027,42 @@ export function App(props: AppProps): ReactElement {
|
|
|
5444
6027
|
authorizationError,
|
|
5445
6028
|
onConfigure: (target: ProviderTargetView) => {
|
|
5446
6029
|
if (props.saveModelProviderConfiguration === undefined) {
|
|
5447
|
-
notify('
|
|
6030
|
+
notify(t('notice.providerUnavailable'), 'warning')
|
|
5448
6031
|
return
|
|
5449
6032
|
}
|
|
5450
6033
|
setProviderAction({ kind: 'configure', target })
|
|
5451
6034
|
},
|
|
5452
6035
|
onUnset: (target: ProviderTargetView) => {
|
|
5453
6036
|
if (props.unsetModelProviderCredential === undefined) {
|
|
5454
|
-
notify('
|
|
6037
|
+
notify(t('notice.apiKeyUnavailable'), 'warning')
|
|
5455
6038
|
return
|
|
5456
6039
|
}
|
|
5457
6040
|
setProviderAction({ kind: 'unset', target })
|
|
5458
6041
|
},
|
|
5459
6042
|
onRemove: (target: ProviderTargetView) => {
|
|
5460
6043
|
if (props.removeModelProvider === undefined) {
|
|
5461
|
-
notify('
|
|
6044
|
+
notify(t('notice.providerRemovalUnavailable'), 'warning')
|
|
5462
6045
|
return
|
|
5463
6046
|
}
|
|
5464
6047
|
setProviderAction({ kind: 'remove', target })
|
|
5465
6048
|
},
|
|
5466
6049
|
onLogin: (target: ProviderTargetView, authorization: ProviderAuthorizationRow) => {
|
|
5467
6050
|
if (busy) {
|
|
5468
|
-
notify('
|
|
6051
|
+
notify(t('notice.loginIdleOnly'), 'warning')
|
|
5469
6052
|
return
|
|
5470
6053
|
}
|
|
5471
6054
|
if (props.beginProviderAuthorization === undefined
|
|
5472
6055
|
|| props.cancelProviderAuthorization === undefined
|
|
5473
6056
|
|| props.openAuthorizationUrl === undefined
|
|
5474
6057
|
|| props.copyTextValue === undefined) {
|
|
5475
|
-
notify('
|
|
6058
|
+
notify(t('notice.loginUnavailable'), 'warning')
|
|
5476
6059
|
return
|
|
5477
6060
|
}
|
|
5478
6061
|
setProviderAction({ kind: 'login', target, authorization })
|
|
5479
6062
|
},
|
|
5480
6063
|
onLogout: (target: ProviderTargetView, authorization: ProviderAuthorizationRow) => {
|
|
5481
6064
|
if (props.logoutProviderAuthorization === undefined) {
|
|
5482
|
-
notify('
|
|
6065
|
+
notify(t('notice.logoutUnavailable'), 'warning')
|
|
5483
6066
|
return
|
|
5484
6067
|
}
|
|
5485
6068
|
setProviderAction({ kind: 'logout', target, authorization })
|
|
@@ -5512,7 +6095,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5512
6095
|
setEffortFor(row)
|
|
5513
6096
|
return
|
|
5514
6097
|
}
|
|
5515
|
-
const effortId = row.reasoning?.efforts.length === 1 ? row.reasoning.efforts[0]
|
|
6098
|
+
const effortId = row.reasoning?.efforts.length === 1 ? row.reasoning.efforts[0].id : undefined
|
|
5516
6099
|
applyModel(row, effortId)
|
|
5517
6100
|
},
|
|
5518
6101
|
...(props.loadModelProviders === undefined || props.saveModelProviderConfiguration === undefined
|
|
@@ -5579,6 +6162,13 @@ export function App(props: AppProps): ReactElement {
|
|
|
5579
6162
|
: undefined,
|
|
5580
6163
|
transcriptVisible ? createElement(TodoPanel, { todos: view.todos }) : undefined,
|
|
5581
6164
|
transcriptVisible ? createElement(AgentsLine, { rows: agentRows, total: props.subagents.getTotalSeen() }) : undefined,
|
|
6165
|
+
usageOpen && !approvalPending && !questionPending
|
|
6166
|
+
? createElement(UsagePanel, {
|
|
6167
|
+
key: props.sessionKey,
|
|
6168
|
+
load: props.loadUsage,
|
|
6169
|
+
close: () => setUsageOpen(false),
|
|
6170
|
+
})
|
|
6171
|
+
: undefined,
|
|
5582
6172
|
todosOpen && !approvalPending && !questionPending
|
|
5583
6173
|
? createElement(MemoTodoListPanel, {
|
|
5584
6174
|
todos: view.todos,
|
|
@@ -5587,6 +6177,14 @@ export function App(props: AppProps): ReactElement {
|
|
|
5587
6177
|
},
|
|
5588
6178
|
})
|
|
5589
6179
|
: undefined,
|
|
6180
|
+
queueOpen && !approvalPending && !questionPending
|
|
6181
|
+
? createElement(QueuePanel, {
|
|
6182
|
+
rows: queuedRows,
|
|
6183
|
+
busy,
|
|
6184
|
+
update: props.updateQueued,
|
|
6185
|
+
onClose: () => setQueueOpen(false),
|
|
6186
|
+
})
|
|
6187
|
+
: undefined,
|
|
5590
6188
|
createElement(QuestionBar, { store: props.questions, snapshot: questionSnapshot, locked: false }),
|
|
5591
6189
|
createElement(ApprovalBar, { snapshot: approvalSnapshot, locked: questionPending, notify, interrupt: props.interrupt, summarize: questionPending }),
|
|
5592
6190
|
modelSurface,
|
|
@@ -5607,6 +6205,17 @@ export function App(props: AppProps): ReactElement {
|
|
|
5607
6205
|
onClose: () => setDiffView(undefined),
|
|
5608
6206
|
})
|
|
5609
6207
|
: undefined,
|
|
6208
|
+
reviewPickerOpen && !approvalPending && !questionPending && props.listReviewBranches !== undefined && props.listReviewCommits !== undefined
|
|
6209
|
+
? createElement(ReviewPickerPanel, {
|
|
6210
|
+
loadBranches: props.listReviewBranches,
|
|
6211
|
+
loadCommits: props.listReviewCommits,
|
|
6212
|
+
choose: argument => {
|
|
6213
|
+
setReviewPickerOpen(false)
|
|
6214
|
+
props.reviewChanges(argument)
|
|
6215
|
+
},
|
|
6216
|
+
close: () => setReviewPickerOpen(false),
|
|
6217
|
+
})
|
|
6218
|
+
: undefined,
|
|
5610
6219
|
verboseOpen && !approvalPending && !questionPending
|
|
5611
6220
|
? createElement(MemoVerbosePanel, {
|
|
5612
6221
|
entries: view.entries,
|
|
@@ -5619,9 +6228,9 @@ export function App(props: AppProps): ReactElement {
|
|
|
5619
6228
|
load: props.loadPresets,
|
|
5620
6229
|
select: (id: string) => {
|
|
5621
6230
|
void props.switchMode(id).then(label => {
|
|
5622
|
-
notify(
|
|
6231
|
+
notify(t('notice.modeChangedSimple', { value: label }))
|
|
5623
6232
|
setModeOpen(false)
|
|
5624
|
-
}, (reason: unknown) => notify(
|
|
6233
|
+
}, (reason: unknown) => notify(t('notice.modeSwitchFailed', { message: reason instanceof Error ? reason.message : String(reason) }), 'error'))
|
|
5625
6234
|
},
|
|
5626
6235
|
close: () => setModeOpen(false),
|
|
5627
6236
|
})
|
|
@@ -5633,7 +6242,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5633
6242
|
select: (id: string) => {
|
|
5634
6243
|
try {
|
|
5635
6244
|
const selected = props.setPermission(id)
|
|
5636
|
-
notify(
|
|
6245
|
+
notify(t('notice.permissionChangedSimple', { value: selected }))
|
|
5637
6246
|
setPermissionOpen(false)
|
|
5638
6247
|
} catch (reason: unknown) {
|
|
5639
6248
|
notify(`permission change failed: ${reason instanceof Error ? reason.message : String(reason)}`, 'error')
|
|
@@ -5655,6 +6264,28 @@ export function App(props: AppProps): ReactElement {
|
|
|
5655
6264
|
close: () => setResumeOpen(false),
|
|
5656
6265
|
})
|
|
5657
6266
|
: undefined,
|
|
6267
|
+
searchOpen && !approvalPending && !questionPending && props.searchSessions !== undefined
|
|
6268
|
+
? createElement(SearchPanel, {
|
|
6269
|
+
load: props.searchSessions,
|
|
6270
|
+
initialQuery: searchSeed,
|
|
6271
|
+
select: (row: SearchRow) => {
|
|
6272
|
+
setSearchOpen(false)
|
|
6273
|
+
props.switchSession({
|
|
6274
|
+
id: row.id,
|
|
6275
|
+
createdAt: row.updatedAt,
|
|
6276
|
+
updatedAt: row.updatedAt,
|
|
6277
|
+
cwd: '',
|
|
6278
|
+
workspace: '',
|
|
6279
|
+
subagent: row.subagent,
|
|
6280
|
+
resumable: row.resumable,
|
|
6281
|
+
live: false,
|
|
6282
|
+
persisted: true,
|
|
6283
|
+
preset: '',
|
|
6284
|
+
})
|
|
6285
|
+
},
|
|
6286
|
+
close: () => setSearchOpen(false),
|
|
6287
|
+
})
|
|
6288
|
+
: undefined,
|
|
5658
6289
|
pluginOpen && !approvalPending && !questionPending
|
|
5659
6290
|
? createElement(PluginPanel, { load: props.loadPlugins, initialQuery: pluginQuery, close: () => setPluginOpen(false) })
|
|
5660
6291
|
: undefined,
|
|
@@ -5691,12 +6322,36 @@ export function App(props: AppProps): ReactElement {
|
|
|
5691
6322
|
// palette. `auto` stores as requested; detection is a later step.
|
|
5692
6323
|
setTheme(name)
|
|
5693
6324
|
props.saveTheme?.(name)
|
|
5694
|
-
|
|
6325
|
+
// Rainbow prints its roll seed so a lucky launch can be reproduced
|
|
6326
|
+
// with RAINBOW_SEED=<seed>.
|
|
6327
|
+
notify(name === 'rainbow'
|
|
6328
|
+
? t('notice.themeRainbow', { seed: rainbowSeedLabel() })
|
|
6329
|
+
: t('notice.themeSaved', { name }))
|
|
5695
6330
|
setThemeOpen(false)
|
|
6331
|
+
// The header whale and settled history live in the Static region,
|
|
6332
|
+
// which renders once and would keep the old palette's colors; the
|
|
6333
|
+
// same source-backed rebuild resize and Ctrl+L use repaints the
|
|
6334
|
+
// whole screen (scrollback included) from the new palette.
|
|
6335
|
+
if (name === 'rainbow') fireRainbowBurst()
|
|
6336
|
+
refreshScreen()
|
|
5696
6337
|
},
|
|
5697
6338
|
close: () => setThemeOpen(false),
|
|
5698
6339
|
})
|
|
5699
6340
|
: undefined,
|
|
6341
|
+
languageOpen && !approvalPending && !questionPending
|
|
6342
|
+
? createElement(LanguagePanel, {
|
|
6343
|
+
current: getLanguage(),
|
|
6344
|
+
select: (name: LanguageName) => {
|
|
6345
|
+
props.saveLanguage(name)
|
|
6346
|
+
notify(t('notice.languageSaved', { name }))
|
|
6347
|
+
setLanguageOpen(false)
|
|
6348
|
+
// The Static region renders once; the same source-backed rebuild
|
|
6349
|
+
// the theme switch uses repaints translated text everywhere.
|
|
6350
|
+
refreshScreen()
|
|
6351
|
+
},
|
|
6352
|
+
close: () => setLanguageOpen(false),
|
|
6353
|
+
})
|
|
6354
|
+
: undefined,
|
|
5700
6355
|
historyOpen && !approvalPending && !questionPending
|
|
5701
6356
|
? createElement(HistoryPanel, {
|
|
5702
6357
|
entries: recallSpace,
|
|
@@ -5724,15 +6379,15 @@ export function App(props: AppProps): ReactElement {
|
|
|
5724
6379
|
// The runner's label already carries the effort suffix
|
|
5725
6380
|
// (`provider/model@effort`), so no second append here.
|
|
5726
6381
|
const label = props.setSubagentModel(row, effortId)
|
|
5727
|
-
notify(
|
|
6382
|
+
notify(t('notice.subagentsChanged', { value: label }))
|
|
5728
6383
|
setSubagentOpen(false)
|
|
5729
6384
|
} catch (reason: unknown) {
|
|
5730
|
-
notify(
|
|
6385
|
+
notify(t('notice.subagentChangeFailed', { message: reason instanceof Error ? reason.message : String(reason) }), 'error')
|
|
5731
6386
|
}
|
|
5732
6387
|
},
|
|
5733
6388
|
inherit: () => {
|
|
5734
6389
|
props.clearSubagentModel()
|
|
5735
|
-
notify('
|
|
6390
|
+
notify(t('notice.subagentsInherited'))
|
|
5736
6391
|
setSubagentOpen(false)
|
|
5737
6392
|
},
|
|
5738
6393
|
close: () => setSubagentOpen(false),
|
|
@@ -5759,8 +6414,14 @@ export function App(props: AppProps): ReactElement {
|
|
|
5759
6414
|
descriptors,
|
|
5760
6415
|
skills,
|
|
5761
6416
|
dispatch: props.dispatch,
|
|
5762
|
-
applyEditorKeys: props.applyEditorKeys,
|
|
5763
6417
|
steer: props.steer,
|
|
6418
|
+
submitMode,
|
|
6419
|
+
cycleSubmitMode: () => {
|
|
6420
|
+
const next = submitMode === 'queue' ? 'steer' : 'queue'
|
|
6421
|
+
setSubmitMode(next)
|
|
6422
|
+
notify(t(next === 'steer' ? 'notice.submitMode.steer' : 'notice.submitMode.queue'))
|
|
6423
|
+
},
|
|
6424
|
+
applyEditorKeys: props.applyEditorKeys,
|
|
5764
6425
|
interrupt: props.interrupt,
|
|
5765
6426
|
quit: props.quit,
|
|
5766
6427
|
openModel: () => {
|
|
@@ -5792,12 +6453,12 @@ export function App(props: AppProps): ReactElement {
|
|
|
5792
6453
|
?? loaded.rows.find(candidate => candidate.model === model && candidate.reasoning !== undefined)
|
|
5793
6454
|
?? loaded.rows.find(candidate => candidate.model === model)
|
|
5794
6455
|
if (row === undefined) {
|
|
5795
|
-
notify('
|
|
6456
|
+
notify(t('notice.modelMissing'), 'warning')
|
|
5796
6457
|
return
|
|
5797
6458
|
}
|
|
5798
6459
|
const rowTag = `${row.provider}/${row.model}`
|
|
5799
6460
|
if (loaded.reasoningFailures?.includes(rowTag) === true) {
|
|
5800
|
-
notify('
|
|
6461
|
+
notify(t('notice.effortUnavailable'), 'warning')
|
|
5801
6462
|
return
|
|
5802
6463
|
}
|
|
5803
6464
|
if (row.reasoning === undefined || row.reasoning.efforts.length === 0) {
|
|
@@ -5821,16 +6482,28 @@ export function App(props: AppProps): ReactElement {
|
|
|
5821
6482
|
openMode: () => setModeOpen(true),
|
|
5822
6483
|
openPermission: () => setPermissionOpen(true),
|
|
5823
6484
|
openResume: () => { setResumeDelete({ mode: false }); setResumeOpen(true) },
|
|
6485
|
+
openSearch: (query: string) => {
|
|
6486
|
+
if (props.searchSessions === undefined) {
|
|
6487
|
+
notify(t('notice.sessionSearchUnavailable'), 'warning')
|
|
6488
|
+
return
|
|
6489
|
+
}
|
|
6490
|
+
setSearchSeed(query)
|
|
6491
|
+
setSearchOpen(true)
|
|
6492
|
+
},
|
|
5824
6493
|
openPlugin: (query = '') => { setPluginQuery(query); setPluginOpen(true) },
|
|
5825
6494
|
openUpdate: () => setUpdateOpen(true),
|
|
5826
6495
|
openSchedule: () => setScheduleOpen(true),
|
|
5827
6496
|
openJobs: () => setJobsOpen(true),
|
|
5828
6497
|
openStatusline: () => setStatuslineOpen(true),
|
|
5829
6498
|
openTheme: () => setThemeOpen(true),
|
|
6499
|
+
openLanguage: () => setLanguageOpen(true),
|
|
6500
|
+
saveLanguage: props.saveLanguage,
|
|
5830
6501
|
openHistory: () => setHistoryOpen(true),
|
|
6502
|
+
openQueue: () => setQueueOpen(true),
|
|
5831
6503
|
openAgents: () => setAgentsOpen(true),
|
|
5832
6504
|
openSubagent: () => setSubagentOpen(true),
|
|
5833
6505
|
openTodos: () => setTodosOpen(true),
|
|
6506
|
+
openUsage: () => setUsageOpen(true),
|
|
5834
6507
|
openDelete: (id?: string) => {
|
|
5835
6508
|
const armed = id === undefined || id === '' ? undefined : id
|
|
5836
6509
|
setResumeDelete({ mode: true, ...armed === undefined ? {} : { id: armed } })
|
|
@@ -5843,6 +6516,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5843
6516
|
})
|
|
5844
6517
|
},
|
|
5845
6518
|
reviewChanges: props.reviewChanges,
|
|
6519
|
+
openReviewPicker: () => setReviewPickerOpen(true),
|
|
5846
6520
|
deleteConfirm: deleteConfirmId,
|
|
5847
6521
|
confirmDelete,
|
|
5848
6522
|
cancelDelete,
|
|
@@ -5886,11 +6560,13 @@ export function App(props: AppProps): ReactElement {
|
|
|
5886
6560
|
recordLocal,
|
|
5887
6561
|
recordHistory: props.recordHistory,
|
|
5888
6562
|
queued: queuedRows,
|
|
5889
|
-
|
|
6563
|
+
updateQueued: props.updateQueued,
|
|
5890
6564
|
historyFill,
|
|
5891
6565
|
historyConsumed,
|
|
5892
6566
|
animations,
|
|
5893
6567
|
applyAnimations,
|
|
6568
|
+
applyRainbow,
|
|
6569
|
+
rainbowBurstId,
|
|
5894
6570
|
waveTier,
|
|
5895
6571
|
waveStyle,
|
|
5896
6572
|
maxRows: composerEditorCap,
|
|
@@ -5914,6 +6590,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5914
6590
|
},
|
|
5915
6591
|
stats: view.stats,
|
|
5916
6592
|
busy,
|
|
6593
|
+
animated: animations,
|
|
5917
6594
|
columns: terminalColumns,
|
|
5918
6595
|
items: statuslineItems,
|
|
5919
6596
|
onRows: handleStatusRows,
|