dsh-code 1.0.6 → 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 +123 -26
- package/README.md +124 -27
- package/bin/deepseek.mjs +283 -35
- package/cordis.patch.yml +97 -0
- package/lib/index.mjs +5008 -881
- package/lib/session-query.mjs +150 -0
- package/lib/startup.mjs +4 -4
- package/lib/{theme-DCT8Y2xf.mjs → theme-7u5Qo3dF.mjs} +657 -20
- package/lib/types/app.d.ts +106 -62
- 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 +100 -1
- package/lib/types/input-split.d.ts +1 -1
- package/lib/types/kernel-panels.d.ts +107 -29
- 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/editor.d.ts +4 -3
- package/lib/types/render/ime-cursor.d.ts +60 -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 +130 -4
- package/lib/types/render/status.d.ts +9 -9
- package/lib/types/render/text.d.ts +6 -0
- package/lib/types/render/usage.d.ts +113 -0
- package/lib/types/session-directory.d.ts +17 -0
- package/lib/types/session-query.d.ts +92 -0
- package/lib/types/startup.d.ts +1 -1
- package/lib/types/terminal-title.d.ts +66 -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 +49 -0
- package/lib/types/update.d.ts +75 -0
- package/lib/types/version.d.ts +4 -3
- package/package.json +246 -90
- package/src/app.ts +1369 -509
- 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 +534 -80
- package/src/input-split.ts +3 -3
- package/src/internals.ts +5 -0
- package/src/kernel-panels.ts +554 -86
- 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 +25 -24
- package/src/render/export.ts +116 -95
- package/src/render/ime-cursor.ts +147 -0
- 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 +572 -21
- package/src/render/status.ts +59 -39
- 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 +239 -0
- package/src/startup.ts +3 -3
- package/src/subagents.ts +229 -229
- package/src/terminal-title.ts +190 -0
- package/src/theme-panel.ts +17 -21
- package/src/theme.ts +281 -33
- package/src/update-panel.ts +256 -0
- package/src/update.ts +126 -0
- package/src/version.ts +58 -20
- package/src/whale-glyph.ts +23 -23
package/src/app.ts
CHANGED
|
@@ -26,18 +26,33 @@ 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'
|
|
48
|
+
import { UpdatePanel } from './update-panel.ts'
|
|
49
|
+
import type { LauncherUpdateStatus } from './update.ts'
|
|
37
50
|
import { WHALE_GLYPH, WHALE_GLYPH_COLUMNS } from './whale-glyph.ts'
|
|
38
51
|
import { DSH_CODE_VERSION, dshKernelVersion } from './version.ts'
|
|
39
52
|
import type { TranscriptStore } from './store.ts'
|
|
53
|
+
import { DEFAULT_TERMINAL_TITLE, sanitizeTerminalTitle, terminalTitleSequence, useTerminalTitle } from './terminal-title.ts'
|
|
40
54
|
import { settledEntryCount, type TranscriptEntry } from './render/projection.ts'
|
|
55
|
+
import { imeCursorRowsUp, useImeCursorAnchor } from './render/ime-cursor.ts'
|
|
41
56
|
import { type MdSegment, visibleColumns } from './render/markdown.ts'
|
|
42
57
|
import {
|
|
43
58
|
busyChaseFrame,
|
|
@@ -55,9 +70,14 @@ import {
|
|
|
55
70
|
deepseekWaveWordVisible,
|
|
56
71
|
deepDivingGradientColor,
|
|
57
72
|
deepDivingSparkColor,
|
|
73
|
+
flowColor,
|
|
58
74
|
effortAboveHigh,
|
|
59
75
|
isOfficialDeepSeekLabel,
|
|
60
76
|
parseAnimationsArgument,
|
|
77
|
+
RAINBOW_BURST_DURATION_MS,
|
|
78
|
+
RAINBOW_BURST_TICK_MS,
|
|
79
|
+
rainbowBurstColumnBg,
|
|
80
|
+
rainbowSpectrumHue,
|
|
61
81
|
type DeepseekWaveStyle,
|
|
62
82
|
type DeepseekWaveTier,
|
|
63
83
|
} from './render/animations.ts'
|
|
@@ -80,7 +100,8 @@ import type { QuestionSnapshot, QuestionStore } from './questions.ts'
|
|
|
80
100
|
import type { SkillsView, SkillRow } from './skills.ts'
|
|
81
101
|
import { isPathLikeMentionQuery, type MentionCandidate } from './mentions.ts'
|
|
82
102
|
import type { SubagentFeedView, SubagentRow } from './subagents.ts'
|
|
83
|
-
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'
|
|
84
105
|
import type { PresetRow } from './presets.ts'
|
|
85
106
|
import type { PermissionRow } from './permissions.ts'
|
|
86
107
|
import type { PluginRow } from './plugin-inventory.ts'
|
|
@@ -93,7 +114,7 @@ import {
|
|
|
93
114
|
type RecallState,
|
|
94
115
|
} from './history.ts'
|
|
95
116
|
import type { SessionDirectoryOptions, SessionRow } from './session-directory.ts'
|
|
96
|
-
import type
|
|
117
|
+
import { parseReviewArgument, type GitDiffView, type ReviewBranch, type ReviewCommit, type ReviewSelection } from './git-workflow.ts'
|
|
97
118
|
import {
|
|
98
119
|
authorizationForProvider,
|
|
99
120
|
providerAuthorizationStatus,
|
|
@@ -144,7 +165,7 @@ const SYNCHRONIZED_UPDATE_END = '\x1b[?2026l'
|
|
|
144
165
|
import {
|
|
145
166
|
layoutStatusBar,
|
|
146
167
|
parseStatuslineItems,
|
|
147
|
-
|
|
168
|
+
statusCycleHint,
|
|
148
169
|
STATUS_GROUP_SEPARATOR,
|
|
149
170
|
STATUS_ITEM_SEPARATOR,
|
|
150
171
|
STATUS_ROW2_INDENT,
|
|
@@ -171,6 +192,7 @@ import {
|
|
|
171
192
|
followInspectorCursor,
|
|
172
193
|
inspectorViewport,
|
|
173
194
|
layoutGutterRows,
|
|
195
|
+
liveRegionBudget,
|
|
174
196
|
moveScroll,
|
|
175
197
|
panelViewport,
|
|
176
198
|
revealRow,
|
|
@@ -178,6 +200,8 @@ import {
|
|
|
178
200
|
} from './render/inspector.ts'
|
|
179
201
|
import {
|
|
180
202
|
clampLiveAllocation,
|
|
203
|
+
diffLineStyle,
|
|
204
|
+
fillDiffLineBars,
|
|
181
205
|
lineSegment,
|
|
182
206
|
markdownLines,
|
|
183
207
|
settledEntryLines,
|
|
@@ -220,37 +244,53 @@ import {
|
|
|
220
244
|
export type NoticeTone = 'info' | 'warning' | 'error'
|
|
221
245
|
|
|
222
246
|
/** One source of truth for TUI-owned slash commands in completion and `/help`. */
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
{ label: '/
|
|
228
|
-
{ label: '/
|
|
229
|
-
{ label: '/
|
|
230
|
-
{ label: '/
|
|
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: '/
|
|
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' },
|
|
250
284
|
] as const
|
|
251
285
|
|
|
252
286
|
const LOCAL_COMMAND_NAMES = new Set(LOCAL_COMMANDS.map(command => command.label.slice(1)))
|
|
253
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
|
+
|
|
254
294
|
/** Props the runner hands the app; callbacks stay owned by the runner. */
|
|
255
295
|
export interface AppProps {
|
|
256
296
|
/** Event-fed transcript store for the live session. */
|
|
@@ -289,127 +329,151 @@ export interface AppProps {
|
|
|
289
329
|
* an attachment prepare resolves after the app remounted onto another
|
|
290
330
|
* session, and the runner drops the stale delivery then.
|
|
291
331
|
*/
|
|
292
|
-
dispatch(text: string, attachments?: readonly ContentBlock[], origin?: string)
|
|
293
|
-
/**
|
|
294
|
-
|
|
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
|
|
295
339
|
/**
|
|
296
340
|
* The FULL current session identity ('' while the first session is pending)
|
|
297
341
|
* — the stale-delivery origin above. Distinct from the short display id.
|
|
298
342
|
*/
|
|
299
343
|
sessionKey: string
|
|
300
344
|
/** Interrupt the running turn (Esc); true when a turn was cancelled. */
|
|
301
|
-
interrupt()
|
|
345
|
+
interrupt: () => boolean
|
|
302
346
|
/** Quit: unmount, flush, and request process exit. */
|
|
303
|
-
quit()
|
|
347
|
+
quit: () => void
|
|
304
348
|
/** Load the selectable model directory (called when /model opens). */
|
|
305
|
-
loadModels()
|
|
349
|
+
loadModels: () => Promise<ModelDirectory>
|
|
306
350
|
/** Load @mention candidates for the typed query (files + sessions). */
|
|
307
|
-
loadMentions(query: string, signal?: AbortSignal)
|
|
351
|
+
loadMentions: (query: string, signal?: AbortSignal) => Promise<readonly MentionCandidate[]>
|
|
308
352
|
/** Validate draft image paths without committing attachment objects. */
|
|
309
|
-
inspectImages(paths: readonly string[])
|
|
353
|
+
inspectImages: (paths: readonly string[]) => Promise<readonly ImagePathInspection[]>
|
|
310
354
|
/** Validate, normalize and persist images immediately before submission. */
|
|
311
|
-
prepareImages(paths: readonly string[], signal?: AbortSignal)
|
|
355
|
+
prepareImages: (paths: readonly string[], signal?: AbortSignal) => Promise<readonly ImageBlock[]>
|
|
312
356
|
/** Validate draft non-image file paths without committing attachment objects. */
|
|
313
|
-
inspectFiles(paths: readonly string[])
|
|
357
|
+
inspectFiles: (paths: readonly string[]) => Promise<readonly FilePathInspection[]>
|
|
314
358
|
/** Persist non-image files immediately before submission as durable file blocks. */
|
|
315
|
-
prepareFiles(paths: readonly string[], signal?: AbortSignal)
|
|
359
|
+
prepareFiles: (paths: readonly string[], signal?: AbortSignal) => Promise<readonly FileBlock[]>
|
|
316
360
|
/** Apply one /model selection (with an advertised reasoning effort, when picked); returns the display label. */
|
|
317
|
-
selectModel(row: ModelRow, effortId?: string)
|
|
361
|
+
selectModel: (row: ModelRow, effortId?: string) => string
|
|
318
362
|
/** The /subagent override label, '' when delegated agents follow the current model. */
|
|
319
363
|
subagentModel: string
|
|
320
364
|
/** Apply one /subagent model pick; returns the override label. */
|
|
321
|
-
setSubagentModel(row: ModelRow, effortId?: string)
|
|
365
|
+
setSubagentModel: (row: ModelRow, effortId?: string) => string
|
|
322
366
|
/** Drop the /subagent override (delegated agents follow the current model). */
|
|
323
|
-
clearSubagentModel()
|
|
367
|
+
clearSubagentModel: () => void
|
|
324
368
|
/** Delete one session subtree; resolves with the outcome line. */
|
|
325
|
-
deleteSession(id: string)
|
|
369
|
+
deleteSession: (id: string) => Promise<string>
|
|
326
370
|
/** Load provider/settings/credential facts for the optional /model provider stage. */
|
|
327
|
-
loadModelProviders
|
|
371
|
+
loadModelProviders?: () => Promise<ProviderSettingsDirectory>
|
|
328
372
|
/** Subscribe to Harness credential/settings/adapter invalidations while /model is open. */
|
|
329
|
-
subscribeModelProviders
|
|
373
|
+
subscribeModelProviders?: (listener: () => void) => () => void
|
|
330
374
|
/** Store or rotate one provider credential through the Harness credential service. */
|
|
331
|
-
saveModelProviderCredential
|
|
375
|
+
saveModelProviderCredential?: (target: ProviderTargetView, key: string) => Promise<void>
|
|
332
376
|
/** Remove one writable provider credential without removing its settings profile. */
|
|
333
|
-
unsetModelProviderCredential
|
|
377
|
+
unsetModelProviderCredential?: (target: ProviderTargetView) => Promise<void>
|
|
334
378
|
/** Remove one user-owned provider profile and its page-managed credential. */
|
|
335
|
-
removeModelProvider
|
|
379
|
+
removeModelProvider?: (target: ProviderTargetView) => Promise<void>
|
|
336
380
|
/** Save endpoint and explicit model capacities through the provider profile. */
|
|
337
|
-
saveModelProviderConfiguration
|
|
381
|
+
saveModelProviderConfiguration?: (target: ProviderTargetView, configuration: ProviderConfiguration) => Promise<void>
|
|
338
382
|
/**
|
|
339
383
|
* Interrogate the provider's real endpoint (typed key wins over the stored
|
|
340
384
|
* credential) for the models it actually serves — the discovery stage of
|
|
341
385
|
* the provider setup page.
|
|
342
386
|
*/
|
|
343
|
-
discoverModelProvider
|
|
387
|
+
discoverModelProvider?: (
|
|
344
388
|
target: ProviderTargetView,
|
|
345
389
|
request: { readonly apiKey?: string; readonly baseURL?: string },
|
|
346
390
|
signal?: AbortSignal,
|
|
347
|
-
)
|
|
391
|
+
) => Promise<readonly DiscoveredModelView[]>
|
|
348
392
|
/** Provider authorization flows and value-free stored-record facts. */
|
|
349
|
-
loadProviderAuthorizations
|
|
350
|
-
subscribeProviderAuthorizations
|
|
351
|
-
beginProviderAuthorization
|
|
393
|
+
loadProviderAuthorizations?: () => Promise<ProviderAuthorizationDirectory>
|
|
394
|
+
subscribeProviderAuthorizations?: (listener: () => void) => () => void
|
|
395
|
+
beginProviderAuthorization?: (
|
|
352
396
|
row: ProviderAuthorizationRow,
|
|
353
397
|
method: string,
|
|
354
398
|
interaction: AuthorizationInteraction,
|
|
355
399
|
signal: AbortSignal,
|
|
356
|
-
)
|
|
357
|
-
cancelProviderAuthorization
|
|
358
|
-
logoutProviderAuthorization
|
|
359
|
-
openAuthorizationUrl
|
|
360
|
-
copyTextValue
|
|
361
|
-
/** Cycle to the next
|
|
362
|
-
|
|
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>
|
|
405
|
+
/** Cycle to the next mode station (Shift+Tab): a permission preset or a plan switch; returns the notice label. */
|
|
406
|
+
cycleMode: () => string
|
|
407
|
+
/** Pre-session plan choice: shows the plan badge before the first session exists. */
|
|
408
|
+
pendingPlan?: boolean
|
|
363
409
|
/** Select or inspect a permission preset without requiring a pre-existing session. */
|
|
364
|
-
setPermission(id: string)
|
|
410
|
+
setPermission: (id: string) => string
|
|
365
411
|
/** Export the transcript to a markdown file (/export [path]); reports via notices. */
|
|
366
|
-
exportTranscript(argument: string)
|
|
412
|
+
exportTranscript: (argument: string) => Promise<void>
|
|
367
413
|
/** Rename the session (/title <text>); returns the outcome line for the notice. */
|
|
368
|
-
renameTitle(argument: string)
|
|
414
|
+
renameTitle: (argument: string) => string
|
|
369
415
|
/** Copy the latest complete assistant response; resolves to notice text. */
|
|
370
|
-
copyLastResponse()
|
|
416
|
+
copyLastResponse: () => Promise<string>
|
|
371
417
|
/** Load a complete read-only Git diff for the file-oriented viewport. */
|
|
372
|
-
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[]>
|
|
373
423
|
/** Start a model review after applying the read-only permission preset. */
|
|
374
|
-
reviewChanges(
|
|
424
|
+
reviewChanges: (selection: ReviewSelection) => void
|
|
375
425
|
/** Preset/session/plugin kernel operations. */
|
|
376
|
-
loadPresets()
|
|
377
|
-
switchMode(id: string)
|
|
426
|
+
loadPresets: () => Promise<readonly PresetRow[]>
|
|
427
|
+
switchMode: (id: string) => Promise<string>
|
|
378
428
|
/** Load the switchable permission presets for the /permission panel. */
|
|
379
|
-
loadPermissions()
|
|
380
|
-
createSession(mode?: string)
|
|
429
|
+
loadPermissions: () => Promise<readonly PermissionRow[]>
|
|
430
|
+
createSession: (mode?: string) => void
|
|
381
431
|
/** Fork the active session at a completed-turn boundary. */
|
|
382
|
-
forkSession(argument: string)
|
|
383
|
-
loadSessions(options: SessionDirectoryOptions, signal?: AbortSignal)
|
|
384
|
-
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[]>
|
|
385
443
|
/** Load this session's subagent conversations (children by lineage). */
|
|
386
|
-
loadSubagents()
|
|
387
|
-
switchSession(row: SessionRow)
|
|
388
|
-
cancelSessionSwitch()
|
|
389
|
-
loadPlugins()
|
|
444
|
+
loadSubagents: () => Promise<readonly SessionRow[]>
|
|
445
|
+
switchSession: (row: SessionRow) => void
|
|
446
|
+
cancelSessionSwitch: () => boolean
|
|
447
|
+
loadPlugins: () => readonly PluginRow[]
|
|
390
448
|
/** Caller-visible background jobs (the host jobs registry, read-only). */
|
|
391
|
-
loadJobs()
|
|
449
|
+
loadJobs: () => readonly JobRow[]
|
|
450
|
+
/** Probe the launcher's aligned update plan (read-only; never installs). */
|
|
451
|
+
probeUpdate: () => Promise<LauncherUpdateStatus>
|
|
452
|
+
/** Run the launcher's aligned update; streams sanitized lines; resolves with the exit code. */
|
|
453
|
+
applyUpdate: (onLine: (line: string) => void, plan?: { readonly dshSpec: string; readonly codeSpec: string; readonly pluginSpecs: readonly string[] }) => Promise<number>
|
|
392
454
|
/** Registers the app's notice channel with the runner (called once on mount). */
|
|
393
|
-
onBridgeReady(bridge: { notify(text: string, tone?: NoticeTone)
|
|
455
|
+
onBridgeReady: (bridge: { notify: (text: string, tone?: NoticeTone) => void }) => void
|
|
394
456
|
/** Ordered enabled status items (/statusline config); the runner owns persistence. */
|
|
395
457
|
statusline: readonly string[]
|
|
396
458
|
/** Persist a new statusline item set; the runner surfaces IO failures as notices. */
|
|
397
|
-
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
|
|
398
462
|
/** Apply and persist one /theme selection; the runner owns the theme.json file. */
|
|
399
|
-
saveTheme
|
|
463
|
+
saveTheme?: (name: ThemeName) => void
|
|
400
464
|
/** Whether timed animations run at startup (animations.json; on by default
|
|
401
465
|
* — like parseAnimationsPref, only an explicit false disables them). */
|
|
402
466
|
animations?: boolean
|
|
403
467
|
/** Apply and persist one /animation toggle; the runner owns the file. */
|
|
404
|
-
saveAnimations
|
|
468
|
+
saveAnimations?: (enabled: boolean) => void
|
|
405
469
|
/** Persistent cross-session input history (oldest first); the runner owns the file. */
|
|
406
470
|
history: readonly string[]
|
|
407
471
|
/** Persist one submitted prompt to the global history file. */
|
|
408
|
-
recordHistory(text: string)
|
|
409
|
-
/**
|
|
410
|
-
|
|
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
|
|
411
475
|
/** Apply the Ctrl+R terminal passthrough to the detected editor (/vscode-keys); resolves to a one-line summary. */
|
|
412
|
-
applyEditorKeys()
|
|
476
|
+
applyEditorKeys: () => Promise<string>
|
|
413
477
|
}
|
|
414
478
|
|
|
415
479
|
/** Pad text with spaces to a visible-column target (menu name column). */
|
|
@@ -463,7 +527,13 @@ function useStableInput(handler: (input: string, key: Key) => void, active: bool
|
|
|
463
527
|
*/
|
|
464
528
|
function BusyChase({ animated = true }: { animated?: boolean }): ReactElement {
|
|
465
529
|
const tick = useFrames(BUSY_CHASE_TICK_MS, animated)
|
|
466
|
-
|
|
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) + ' ')
|
|
467
537
|
}
|
|
468
538
|
|
|
469
539
|
/** Blinking block caret appended to streaming text; solid when frozen. */
|
|
@@ -473,7 +543,7 @@ function Caret({ animated = true }: { animated?: boolean }): ReactElement {
|
|
|
473
543
|
}
|
|
474
544
|
|
|
475
545
|
/** One resettable input-caret phase shared by the entire composer. */
|
|
476
|
-
function useCursorBlink(active: boolean): { visible: boolean; reset()
|
|
546
|
+
function useCursorBlink(active: boolean): { visible: boolean; reset: () => void } {
|
|
477
547
|
const [epoch, setEpoch] = useState(0)
|
|
478
548
|
const [visible, setVisible] = useState(true)
|
|
479
549
|
useEffect(() => {
|
|
@@ -502,6 +572,13 @@ function useCursorBlink(active: boolean): { visible: boolean; reset(): void } {
|
|
|
502
572
|
function ShimmerLine({ text, animated = true }: { text: string; animated?: boolean }): ReactElement {
|
|
503
573
|
const tick = useFrames(DEEP_DIVING_SHIMMER_TICK_MS, animated)
|
|
504
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
|
|
505
582
|
const graphemes = splitGraphemes(text)
|
|
506
583
|
return createElement(
|
|
507
584
|
Text,
|
|
@@ -515,8 +592,8 @@ function ShimmerLine({ text, animated = true }: { text: string; animated?: boole
|
|
|
515
592
|
color: inkColor(!animated
|
|
516
593
|
? (sparkle ? palette.brandBright : palette.brandDeep)
|
|
517
594
|
: sparkle
|
|
518
|
-
? deepDivingSparkColor(tick, palette.brandDeep,
|
|
519
|
-
: deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep,
|
|
595
|
+
? deepDivingSparkColor(tick, palette.brandDeep, highlight)
|
|
596
|
+
: deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep, highlight)),
|
|
520
597
|
bold: sparkle || undefined,
|
|
521
598
|
},
|
|
522
599
|
grapheme.text,
|
|
@@ -613,6 +690,18 @@ function segmentProps(style: MdSegment['style']): {
|
|
|
613
690
|
return { color: undefined, bold: true, italic: true, strikethrough: undefined }
|
|
614
691
|
case 'strike':
|
|
615
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
|
+
}
|
|
616
705
|
default:
|
|
617
706
|
return { color: undefined, bold: undefined, italic: undefined, strikethrough: undefined }
|
|
618
707
|
}
|
|
@@ -625,20 +714,46 @@ function lineStyleProps(style: LineStyle): {
|
|
|
625
714
|
italic: boolean | undefined
|
|
626
715
|
strikethrough: boolean | undefined
|
|
627
716
|
dimColor: boolean | undefined
|
|
717
|
+
backgroundColor: string | undefined
|
|
628
718
|
} {
|
|
629
719
|
switch (style) {
|
|
630
720
|
case 'brand':
|
|
631
|
-
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 }
|
|
632
722
|
case 'success':
|
|
633
|
-
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 }
|
|
634
724
|
case 'error':
|
|
635
|
-
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 }
|
|
636
726
|
case 'warn':
|
|
637
|
-
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 }
|
|
638
728
|
case 'dimItalic':
|
|
639
|
-
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
|
+
}
|
|
640
755
|
default:
|
|
641
|
-
return { ...segmentProps(style), dimColor: undefined }
|
|
756
|
+
return { ...segmentProps(style), dimColor: undefined, backgroundColor: undefined }
|
|
642
757
|
}
|
|
643
758
|
}
|
|
644
759
|
|
|
@@ -662,7 +777,7 @@ function StyledRows({ lines }: { lines: readonly StyledLine[] }): ReactElement {
|
|
|
662
777
|
}
|
|
663
778
|
|
|
664
779
|
/** File-oriented, color-coded unified diff viewport. */
|
|
665
|
-
function DiffPanel({ view, onClose }: { view: GitDiffView; onClose()
|
|
780
|
+
function DiffPanel({ view, onClose }: { view: GitDiffView; onClose: () => void }): ReactElement {
|
|
666
781
|
const stdout = useStdout().stdout
|
|
667
782
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
668
783
|
const [fileIndex, setFileIndex] = useState(0)
|
|
@@ -670,15 +785,7 @@ function DiffPanel({ view, onClose }: { view: GitDiffView; onClose(): void }): R
|
|
|
670
785
|
const file = view.files[fileIndex]
|
|
671
786
|
const lines = useMemo(() => {
|
|
672
787
|
if (file === undefined) return textLines(' (no changes)', viewport.contentColumns, 'dim')
|
|
673
|
-
return file.lines.flatMap(line => styledLines([
|
|
674
|
-
lineSegment(line, line.startsWith('+') && !line.startsWith('+++')
|
|
675
|
-
? 'success'
|
|
676
|
-
: line.startsWith('-') && !line.startsWith('---')
|
|
677
|
-
? 'error'
|
|
678
|
-
: line.startsWith('@@') || line.startsWith('diff --git') || line.startsWith('index ')
|
|
679
|
-
? 'brand'
|
|
680
|
-
: 'dim'),
|
|
681
|
-
], viewport.contentColumns))
|
|
788
|
+
return fillDiffLineBars(file.lines.flatMap(line => styledLines([lineSegment(line, diffLineStyle(line))], viewport.contentColumns)), viewport.contentColumns)
|
|
682
789
|
}, [file, viewport.contentColumns])
|
|
683
790
|
const visibleScroll = clampScroll(scroll, lines.length, viewport.bodyRows)
|
|
684
791
|
useInput((input, key) => {
|
|
@@ -697,15 +804,16 @@ function DiffPanel({ view, onClose }: { view: GitDiffView; onClose(): void }): R
|
|
|
697
804
|
else if (key.pageUp) setScroll(current => moveScroll(current, -viewport.bodyRows, lines.length, viewport.bodyRows))
|
|
698
805
|
else if (key.pageDown) setScroll(current => moveScroll(current, viewport.bodyRows, lines.length, viewport.bodyRows))
|
|
699
806
|
})
|
|
700
|
-
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)
|
|
701
809
|
return createElement(
|
|
702
810
|
Box,
|
|
703
|
-
{ flexDirection: 'column', borderStyle: 'round', borderColor: inkColor(
|
|
704
|
-
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)),
|
|
705
813
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
706
814
|
createElement(StyledRows, { lines: lines.slice(visibleScroll, visibleScroll + viewport.bodyRows) }),
|
|
707
815
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
708
|
-
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)),
|
|
709
817
|
)
|
|
710
818
|
}
|
|
711
819
|
|
|
@@ -723,6 +831,32 @@ function PanelGap({ visible }: { visible: boolean }): ReactElement | undefined {
|
|
|
723
831
|
* keeps its historical three lines. Short or narrow terminals keep a one-line
|
|
724
832
|
* form without the kernel line.
|
|
725
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
|
+
|
|
726
860
|
function Header({ resumed }: { resumed: boolean }): ReactElement {
|
|
727
861
|
const stdout = useStdout().stdout
|
|
728
862
|
const rows = stdout?.rows ?? 40
|
|
@@ -737,7 +871,7 @@ function Header({ resumed }: { resumed: boolean }): ReactElement {
|
|
|
737
871
|
const copyWidths = [visibleColumns(title), visibleColumns(slogan), visibleColumns(hint)]
|
|
738
872
|
if (kernelLine !== undefined) copyWidths.push(visibleColumns(kernelLine))
|
|
739
873
|
const copyColumns = Math.max(...copyWidths)
|
|
740
|
-
const compact = `${title} · ${hint}`
|
|
874
|
+
const compact = kernelLine === undefined ? `${title} · ${hint}` : `${title} · ${kernelLine} · ${hint}`
|
|
741
875
|
if (rows < 20 || columns < WHALE_GLYPH_COLUMNS + copyColumns + 10) {
|
|
742
876
|
return createElement(
|
|
743
877
|
Box,
|
|
@@ -756,7 +890,10 @@ function Header({ resumed }: { resumed: boolean }): ReactElement {
|
|
|
756
890
|
createElement(
|
|
757
891
|
Box,
|
|
758
892
|
{ flexDirection: 'column', width: WHALE_GLYPH_COLUMNS, justifyContent: 'center' },
|
|
759
|
-
...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)),
|
|
760
897
|
),
|
|
761
898
|
createElement(
|
|
762
899
|
Box,
|
|
@@ -791,7 +928,7 @@ function todoMark(status: TodoItem['status']): string {
|
|
|
791
928
|
function AgentsLine({ rows, total }: { rows: readonly SubagentRow[]; total: number }): ReactElement | undefined {
|
|
792
929
|
if (rows.length === 0) return undefined
|
|
793
930
|
const running = rows.filter(row => row.state !== 'done').length
|
|
794
|
-
const newest = [...rows].sort((left, right) => right.updatedAt - left.updatedAt)[0]
|
|
931
|
+
const newest = [...rows].sort((left, right) => right.updatedAt - left.updatedAt)[0]
|
|
795
932
|
const mark = newest.state === 'done' ? '✓' : newest.state === 'idle' ? '⏸' : '●'
|
|
796
933
|
return createElement(
|
|
797
934
|
Box,
|
|
@@ -844,7 +981,7 @@ function TodoListPanel({ todos, onClose }: { todos: readonly TodoItem[]; onClose
|
|
|
844
981
|
const inProgress = todos.filter(todo => todo.status === 'in_progress').length
|
|
845
982
|
const pending = todos.length - completed - inProgress
|
|
846
983
|
const rows = todos.length === 0
|
|
847
|
-
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' }, '
|
|
984
|
+
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' }, ` ${t('panel.todos.empty')}`)]
|
|
848
985
|
: todos.map(todo => createElement(
|
|
849
986
|
Text,
|
|
850
987
|
{ key: todo.content, dimColor: true, wrap: 'truncate-end' },
|
|
@@ -873,40 +1010,202 @@ function TodoListPanel({ todos, onClose }: { todos: readonly TodoItem[]; onClose
|
|
|
873
1010
|
})
|
|
874
1011
|
|
|
875
1012
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
876
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('todos
|
|
1013
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.todos.compact'), viewport.contentColumns))
|
|
877
1014
|
}
|
|
878
1015
|
|
|
1016
|
+
const accent = panelAccent('todos', getPalette().brand)
|
|
879
1017
|
return createElement(
|
|
880
1018
|
Box,
|
|
881
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
882
|
-
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)),
|
|
883
1021
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
884
1022
|
...rows.slice(visibleScroll, visibleScroll + viewport.bodyRows),
|
|
885
1023
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
886
|
-
createElement(Text, {
|
|
1024
|
+
createElement(Text, { wrap: 'truncate-end' }, dim(truncateColumns(t('panel.todos.footer'), viewport.contentColumns))),
|
|
887
1025
|
)
|
|
888
1026
|
}
|
|
889
1027
|
|
|
890
1028
|
const MemoTodoListPanel = memo(TodoListPanel)
|
|
891
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
|
+
|
|
892
1178
|
/**
|
|
893
1179
|
* Ink props for one status tone: the Codex status-line accent mapping over
|
|
894
1180
|
* the DeepSeek palette, all blue by design — the status bar speaks only in
|
|
895
1181
|
* degrees of blue (deep accent, primary figures, model identity, sky
|
|
896
1182
|
* paths and done states), with amber/red reserved for warnings and errors.
|
|
897
1183
|
*/
|
|
898
|
-
function statusToneProps(tone: StatusTone): {
|
|
1184
|
+
function statusToneProps(tone: StatusTone, flowMs?: number): {
|
|
899
1185
|
color: string | undefined
|
|
900
1186
|
bold: boolean | undefined
|
|
901
1187
|
dimColor: boolean | undefined
|
|
902
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
|
+
}
|
|
903
1200
|
switch (tone) {
|
|
904
1201
|
case 'model':
|
|
905
1202
|
// Same tone as the working-directory segment: the model name reads as
|
|
906
1203
|
// a path fact, not a brand accent.
|
|
907
1204
|
return { color: inkColor(getPalette().code), bold: true, dimColor: undefined }
|
|
908
1205
|
case 'live':
|
|
909
|
-
|
|
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 }
|
|
910
1209
|
case 'path':
|
|
911
1210
|
return { color: inkColor(getPalette().code), bold: undefined, dimColor: undefined }
|
|
912
1211
|
case 'branch':
|
|
@@ -928,6 +1227,10 @@ function statusToneProps(tone: StatusTone): {
|
|
|
928
1227
|
return { color: inkColor(getPalette().brand), bold: undefined, dimColor: undefined }
|
|
929
1228
|
case 'success':
|
|
930
1229
|
return { color: inkColor(getPalette().code), bold: true, dimColor: undefined }
|
|
1230
|
+
// The plan station's dedicated green: the status bar otherwise speaks in
|
|
1231
|
+
// blues, but the fourth cycle station IS a distinct green mode marker.
|
|
1232
|
+
case 'plan':
|
|
1233
|
+
return { color: inkColor(getPalette().success), bold: true, dimColor: undefined }
|
|
931
1234
|
case 'warn':
|
|
932
1235
|
return { color: inkColor(getPalette().warn), bold: true, dimColor: undefined }
|
|
933
1236
|
case 'error':
|
|
@@ -964,18 +1267,40 @@ function statusToneProps(tone: StatusTone): {
|
|
|
964
1267
|
* the prompt keeps is always hues[0]. */
|
|
965
1268
|
function deepseekWaveHues(tier: DeepseekWaveTier): readonly [RgbTriple, RgbTriple, RgbTriple] {
|
|
966
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]]
|
|
967
1280
|
return tier === 'flash'
|
|
968
1281
|
? [palette.brandBright, palette.brand, palette.brandMid]
|
|
969
1282
|
: [palette.brandBright, palette.code, palette.brandMid]
|
|
970
1283
|
}
|
|
971
1284
|
|
|
972
|
-
function StatusLine({ facts, stats, busy, columns, items }: {
|
|
1285
|
+
function StatusLine({ facts, stats, busy, columns, items, onRows, animated }: {
|
|
973
1286
|
facts: StatusFacts
|
|
974
1287
|
stats: Parameters<typeof layoutStatusBar>[1]
|
|
975
1288
|
busy: boolean
|
|
976
1289
|
columns: number
|
|
977
1290
|
items: readonly string[]
|
|
1291
|
+
/** Reports the footer's exact physical row count (1 or 2) so the IME
|
|
1292
|
+
* anchor ledger below the composer stays exact. */
|
|
1293
|
+
onRows?: (rows: 1 | 2) => void
|
|
1294
|
+
/** Whether timed animations run (the persisted preference). */
|
|
1295
|
+
animated: boolean
|
|
978
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()
|
|
979
1304
|
const layout = useMemo(() => layoutStatusBar(facts, stats, Math.max(8, columns - 2), {
|
|
980
1305
|
busy,
|
|
981
1306
|
items,
|
|
@@ -999,7 +1324,16 @@ function StatusLine({ facts, stats, busy, columns, items }: {
|
|
|
999
1324
|
busy,
|
|
1000
1325
|
columns,
|
|
1001
1326
|
items,
|
|
1327
|
+
// Labels come from t(); a language switch must rebuild the rows.
|
|
1328
|
+
language,
|
|
1002
1329
|
])
|
|
1330
|
+
// The IME anchor below the composer counts every row between the caret and
|
|
1331
|
+
// Ink's parked cursor, so the footer reports its exact row count one-way
|
|
1332
|
+
// (same contract as the composer's row report).
|
|
1333
|
+
const statusRowCount: 1 | 2 = layout.row2.left.length > 0 ? 2 : 1
|
|
1334
|
+
useEffect(() => {
|
|
1335
|
+
onRows?.(statusRowCount)
|
|
1336
|
+
}, [onRows, statusRowCount])
|
|
1003
1337
|
|
|
1004
1338
|
const renderRow = (row: { left: readonly StatusGroup[]; right: readonly StatusSpan[]; hint: boolean }, key: string, indent = 0): ReactElement => {
|
|
1005
1339
|
const leftParts: ReactElement[] = []
|
|
@@ -1010,7 +1344,7 @@ function StatusLine({ facts, stats, busy, columns, items }: {
|
|
|
1010
1344
|
group.spans.forEach((span, spanIndex) => {
|
|
1011
1345
|
leftParts.push(createElement(
|
|
1012
1346
|
Text,
|
|
1013
|
-
{ 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) },
|
|
1014
1348
|
span.text,
|
|
1015
1349
|
))
|
|
1016
1350
|
})
|
|
@@ -1022,12 +1356,12 @@ function StatusLine({ facts, stats, busy, columns, items }: {
|
|
|
1022
1356
|
}
|
|
1023
1357
|
rightParts.push(createElement(
|
|
1024
1358
|
Text,
|
|
1025
|
-
{ key: key + 'r' + index, wrap: 'truncate-end', ...statusToneProps(span.tone) },
|
|
1359
|
+
{ key: key + 'r' + index, wrap: 'truncate-end', ...statusToneProps(span.tone, flowMs) },
|
|
1026
1360
|
span.text,
|
|
1027
1361
|
))
|
|
1028
1362
|
})
|
|
1029
1363
|
if (row.hint) {
|
|
1030
|
-
rightParts.push(createElement(Text, { key: key + 'hint', color: inkColor(getPalette().dim) },
|
|
1364
|
+
rightParts.push(createElement(Text, { key: key + 'hint', color: inkColor(getPalette().dim) }, statusCycleHint()))
|
|
1031
1365
|
}
|
|
1032
1366
|
// Each row already fits the column budget; truncate-end stays as the
|
|
1033
1367
|
// terminal-measurement backstop so a drifting cell count clips instead
|
|
@@ -1105,9 +1439,9 @@ const APPROVAL_OPTIONS: readonly ApprovalOption[] = [
|
|
|
1105
1439
|
function ApprovalBar({ snapshot, locked, notify, interrupt, summarize }: {
|
|
1106
1440
|
snapshot: ApprovalSnapshot
|
|
1107
1441
|
locked: boolean
|
|
1108
|
-
notify(text: string, tone?: NoticeTone)
|
|
1442
|
+
notify: (text: string, tone?: NoticeTone) => void
|
|
1109
1443
|
/** Cancel the running turn (Ctrl+C), matching the composer's busy branch. */
|
|
1110
|
-
interrupt()
|
|
1444
|
+
interrupt: () => boolean
|
|
1111
1445
|
/** Render as the bounded one-line form even on tall terminals (another
|
|
1112
1446
|
* human-asked surface already owns the full panel budget). */
|
|
1113
1447
|
summarize?: boolean
|
|
@@ -1134,7 +1468,7 @@ function ApprovalBar({ snapshot, locked, notify, interrupt, summarize }: {
|
|
|
1134
1468
|
}
|
|
1135
1469
|
ask.answer('rejected')
|
|
1136
1470
|
if (option.key === 'reject-note') {
|
|
1137
|
-
notify('rejected
|
|
1471
|
+
notify(t('notice.rejected'), 'warning')
|
|
1138
1472
|
}
|
|
1139
1473
|
}
|
|
1140
1474
|
|
|
@@ -1158,35 +1492,35 @@ function ApprovalBar({ snapshot, locked, notify, interrupt, summarize }: {
|
|
|
1158
1492
|
return
|
|
1159
1493
|
}
|
|
1160
1494
|
if (key.return) {
|
|
1161
|
-
decide(APPROVAL_OPTIONS[cursor]
|
|
1495
|
+
decide(APPROVAL_OPTIONS[cursor])
|
|
1162
1496
|
return
|
|
1163
1497
|
}
|
|
1164
1498
|
if (key.escape) {
|
|
1165
|
-
decide(APPROVAL_OPTIONS[2]
|
|
1499
|
+
decide(APPROVAL_OPTIONS[2])
|
|
1166
1500
|
return
|
|
1167
1501
|
}
|
|
1168
1502
|
if (input === 'y' || input === 'Y') {
|
|
1169
|
-
decide(APPROVAL_OPTIONS[0]
|
|
1503
|
+
decide(APPROVAL_OPTIONS[0])
|
|
1170
1504
|
return
|
|
1171
1505
|
}
|
|
1172
1506
|
if (input === 'n' || input === 'N') {
|
|
1173
|
-
decide(APPROVAL_OPTIONS[1]
|
|
1507
|
+
decide(APPROVAL_OPTIONS[1])
|
|
1174
1508
|
return
|
|
1175
1509
|
}
|
|
1176
1510
|
if (input === 'd' || input === 'D') {
|
|
1177
|
-
decide(APPROVAL_OPTIONS[2]
|
|
1511
|
+
decide(APPROVAL_OPTIONS[2])
|
|
1178
1512
|
return
|
|
1179
1513
|
}
|
|
1180
1514
|
if (/^[1-9]$/u.test(input)) {
|
|
1181
1515
|
const index = Number(input) - 1
|
|
1182
|
-
if (index < APPROVAL_OPTIONS.length) decide(APPROVAL_OPTIONS[index]
|
|
1516
|
+
if (index < APPROVAL_OPTIONS.length) decide(APPROVAL_OPTIONS[index])
|
|
1183
1517
|
}
|
|
1184
1518
|
}, { isActive: active })
|
|
1185
1519
|
|
|
1186
1520
|
if (pending === undefined) return undefined
|
|
1187
1521
|
const queuedSuffix = snapshot.queued > 0 ? ` · +${snapshot.queued} queued` : ''
|
|
1188
1522
|
if (viewport.maxHeight === 0 || viewport.compact || summarize === true) {
|
|
1189
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(
|
|
1523
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('approval.compact', { queued: queuedSuffix }), viewport.contentColumns))
|
|
1190
1524
|
}
|
|
1191
1525
|
// Body budget: title + options + footer consume fixed rows; the command
|
|
1192
1526
|
// preview shrinks with an explicit overflow marker (Codex's "[… N lines]").
|
|
@@ -1205,7 +1539,7 @@ function ApprovalBar({ snapshot, locked, notify, interrupt, summarize }: {
|
|
|
1205
1539
|
createElement(PanelGap, { visible: viewport.gapRows > 0 && body.length > 0 }),
|
|
1206
1540
|
...visibleBody.map((line, index) => createElement(StyledRows, { key: `body-${index}`, lines: [line] })),
|
|
1207
1541
|
...(overflow > 0
|
|
1208
|
-
? [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))]
|
|
1209
1543
|
: []),
|
|
1210
1544
|
...(body.length > 0 ? [createElement(PanelGap, { visible: viewport.gapRows > 0 })] : []),
|
|
1211
1545
|
...APPROVAL_OPTIONS.map((option, index) => {
|
|
@@ -1222,8 +1556,8 @@ function ApprovalBar({ snapshot, locked, notify, interrupt, summarize }: {
|
|
|
1222
1556
|
)
|
|
1223
1557
|
}),
|
|
1224
1558
|
createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(snapshot.answered
|
|
1225
|
-
? 'submitted
|
|
1226
|
-
: '
|
|
1559
|
+
? t('approval.submitted')
|
|
1560
|
+
: t('approval.footer'), viewport.contentColumns)),
|
|
1227
1561
|
)
|
|
1228
1562
|
}
|
|
1229
1563
|
|
|
@@ -1596,21 +1930,21 @@ function QuestionBar({ store, snapshot, locked }: { store: QuestionStore; snapsh
|
|
|
1596
1930
|
|
|
1597
1931
|
if (pending === undefined || question === undefined) return undefined
|
|
1598
1932
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
1599
|
-
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))
|
|
1600
1934
|
}
|
|
1601
1935
|
const footerBase = submitted
|
|
1602
|
-
? 'submitted
|
|
1936
|
+
? t('question.submitted')
|
|
1603
1937
|
: mode === 'custom'
|
|
1604
1938
|
? options.length === 0
|
|
1605
|
-
? '
|
|
1606
|
-
: '
|
|
1939
|
+
? t('question.customNoOptions')
|
|
1940
|
+
: t('question.customOptions')
|
|
1607
1941
|
: options.length === 0
|
|
1608
|
-
? '
|
|
1942
|
+
? t('question.customNoOptions')
|
|
1609
1943
|
: isMulti
|
|
1610
|
-
? '
|
|
1611
|
-
: '
|
|
1944
|
+
? t('question.multiOptions')
|
|
1945
|
+
: t('question.singleOptions')
|
|
1612
1946
|
const footer = pending.request.questions.length > 1 && !submitted
|
|
1613
|
-
? `${footerBase}
|
|
1947
|
+
? `${footerBase}${t('question.switch')}`
|
|
1614
1948
|
: footerBase
|
|
1615
1949
|
return createElement(
|
|
1616
1950
|
Box,
|
|
@@ -1618,12 +1952,12 @@ function QuestionBar({ store, snapshot, locked }: { store: QuestionStore; snapsh
|
|
|
1618
1952
|
createElement(
|
|
1619
1953
|
Text,
|
|
1620
1954
|
{ color: inkColor(isPlan ? getPalette().brand : getPalette().brandDeep), bold: true, wrap: 'truncate-end' },
|
|
1621
|
-
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),
|
|
1622
1956
|
),
|
|
1623
1957
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1624
1958
|
createElement(StyledRows, { lines: rendered.lines.slice(visibleScroll, visibleScroll + viewport.bodyRows) }),
|
|
1625
1959
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1626
|
-
createElement(Text, {
|
|
1960
|
+
createElement(Text, { wrap: 'truncate-end' }, dim(truncateColumns(footer, viewport.contentColumns))),
|
|
1627
1961
|
)
|
|
1628
1962
|
}
|
|
1629
1963
|
|
|
@@ -1633,43 +1967,65 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1633
1967
|
error: string | undefined
|
|
1634
1968
|
/** `provider/model` label of the applied model: the cursor lands on it once. */
|
|
1635
1969
|
current?: string
|
|
1636
|
-
onSelect(row: ModelRow)
|
|
1637
|
-
onProviders
|
|
1638
|
-
onRetry()
|
|
1639
|
-
onClose()
|
|
1970
|
+
onSelect: (row: ModelRow) => void
|
|
1971
|
+
onProviders?: () => void
|
|
1972
|
+
onRetry: () => void
|
|
1973
|
+
onClose: () => void
|
|
1640
1974
|
}): ReactElement {
|
|
1975
|
+
const [query, setQuery] = useState('')
|
|
1641
1976
|
const [cursor, setCursor] = useState(0)
|
|
1642
1977
|
const stdout = useStdout().stdout
|
|
1643
1978
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
1644
1979
|
const rows = directory?.rows ?? []
|
|
1980
|
+
// Direct-typing filter over provider and model names (the /mode contract):
|
|
1981
|
+
// printable keys edit the query, so a long directory is searchable without
|
|
1982
|
+
// a separate search mode. With a query active, q/r/g/G stop acting as
|
|
1983
|
+
// commands and become query text instead.
|
|
1984
|
+
const filtered = useMemo(() => {
|
|
1985
|
+
if (query === '') return rows
|
|
1986
|
+
const needle = query.toLowerCase()
|
|
1987
|
+
return rows.filter(row => `${row.provider} ${row.providerName ?? ''} ${row.model} ${row.modelName}`.toLowerCase().includes(needle))
|
|
1988
|
+
}, [rows, query])
|
|
1645
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 }
|
|
1646
1997
|
|
|
1647
1998
|
useEffect(() => {
|
|
1648
1999
|
// Open ON the applied model (Codex resumes the previous pick): the first
|
|
1649
2000
|
// non-empty directory positions the cursor once, never on later refreshes.
|
|
1650
2001
|
if (positioned.current || rows.length === 0 || current === undefined) {
|
|
1651
|
-
if (
|
|
2002
|
+
if (filtered.length === 0) {
|
|
1652
2003
|
if (cursor !== 0) setCursor(0)
|
|
1653
2004
|
return
|
|
1654
2005
|
}
|
|
1655
|
-
if (cursor >=
|
|
2006
|
+
if (cursor >= filtered.length) setCursor(filtered.length - 1)
|
|
1656
2007
|
return
|
|
1657
2008
|
}
|
|
1658
2009
|
const index = rows.findIndex(row => `${row.provider}/${row.model}` === current)
|
|
1659
2010
|
if (index >= 0) {
|
|
1660
2011
|
positioned.current = true
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
2012
|
+
// Position within the ACTIVE filter: the full-row index means nothing
|
|
2013
|
+
// when the query already narrowed the list while the directory loaded
|
|
2014
|
+
// (a late resolve must not place the cursor outside `filtered`).
|
|
2015
|
+
const filteredIndex = filtered.indexOf(rows[index])
|
|
2016
|
+
setCursor(filteredIndex >= 0 ? filteredIndex : 0)
|
|
2017
|
+
} else if (cursor >= filtered.length) {
|
|
2018
|
+
setCursor(Math.max(0, filtered.length - 1))
|
|
1664
2019
|
}
|
|
1665
|
-
}, [rows, cursor, current])
|
|
2020
|
+
}, [rows, filtered, cursor, current])
|
|
1666
2021
|
|
|
1667
2022
|
useInput((input, key) => {
|
|
1668
|
-
|
|
2023
|
+
const { filtered: list, query: text, cursor: at } = liveRef.current
|
|
2024
|
+
if (key.escape || (input === 'q' && text === '')) {
|
|
1669
2025
|
onClose()
|
|
1670
2026
|
return
|
|
1671
2027
|
}
|
|
1672
|
-
if (input === 'r') {
|
|
2028
|
+
if (input === 'r' && text === '') {
|
|
1673
2029
|
onRetry()
|
|
1674
2030
|
return
|
|
1675
2031
|
}
|
|
@@ -1682,13 +2038,19 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1682
2038
|
onClose()
|
|
1683
2039
|
return
|
|
1684
2040
|
}
|
|
1685
|
-
|
|
2041
|
+
const next = editQuery(text, input, key)
|
|
2042
|
+
if (next !== undefined) {
|
|
2043
|
+
setQuery(next)
|
|
2044
|
+
setCursor(0)
|
|
2045
|
+
return
|
|
2046
|
+
}
|
|
2047
|
+
if (list.length === 0) return
|
|
1686
2048
|
if (key.upArrow) {
|
|
1687
|
-
setCursor(
|
|
2049
|
+
setCursor(at > 0 ? at - 1 : list.length - 1)
|
|
1688
2050
|
return
|
|
1689
2051
|
}
|
|
1690
2052
|
if (key.downArrow) {
|
|
1691
|
-
setCursor(
|
|
2053
|
+
setCursor(at < list.length - 1 ? at + 1 : 0)
|
|
1692
2054
|
return
|
|
1693
2055
|
}
|
|
1694
2056
|
if (key.pageUp) {
|
|
@@ -1696,36 +2058,31 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1696
2058
|
return
|
|
1697
2059
|
}
|
|
1698
2060
|
if (key.pageDown) {
|
|
1699
|
-
setCursor(current => Math.min(
|
|
2061
|
+
setCursor(current => Math.min(list.length - 1, current + Math.max(1, viewport.bodyRows - 1)))
|
|
1700
2062
|
return
|
|
1701
2063
|
}
|
|
1702
|
-
if (
|
|
1703
|
-
|
|
1704
|
-
return
|
|
1705
|
-
}
|
|
1706
|
-
if (input === 'G') {
|
|
1707
|
-
setCursor(rows.length - 1)
|
|
1708
|
-
return
|
|
1709
|
-
}
|
|
1710
|
-
if (key.return && rows[cursor] !== undefined) {
|
|
1711
|
-
onSelect(rows[cursor])
|
|
2064
|
+
if (key.return && list[at] !== undefined) {
|
|
2065
|
+
onSelect(list[at])
|
|
1712
2066
|
}
|
|
1713
2067
|
})
|
|
1714
2068
|
|
|
1715
2069
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
1716
2070
|
const providers = onProviders === undefined ? '' : ' · tab providers'
|
|
1717
|
-
const state =
|
|
2071
|
+
const state = filtered.length === 0
|
|
1718
2072
|
? directory === undefined && error === undefined
|
|
1719
|
-
? 'loading
|
|
2073
|
+
? t('panel.model.loading')
|
|
1720
2074
|
: error !== undefined
|
|
1721
|
-
? 'error'
|
|
1722
|
-
: '
|
|
1723
|
-
: `❯ ${
|
|
1724
|
-
|
|
2075
|
+
? t('panel.model.error')
|
|
2076
|
+
: query === '' ? t('panel.model.noModels') : t('panel.model.compactNoMatch', { query: singleLineText(query) })
|
|
2077
|
+
: `❯ ${filtered[cursor]?.modelName ?? filtered[cursor]?.model ?? ''}`
|
|
2078
|
+
const tail = query === ''
|
|
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))
|
|
1725
2082
|
}
|
|
1726
2083
|
|
|
1727
2084
|
const stateRows: ReactElement[] = directory === undefined && error === undefined
|
|
1728
|
-
? [createElement(Text, { key: 'loading', dimColor: true, wrap: 'truncate-end' }, '
|
|
2085
|
+
? [createElement(Text, { key: 'loading', dimColor: true, wrap: 'truncate-end' }, ` ${t('panel.model.loading')}`)]
|
|
1729
2086
|
: error !== undefined
|
|
1730
2087
|
? [createElement(
|
|
1731
2088
|
Text,
|
|
@@ -1738,27 +2095,32 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1738
2095
|
: [createElement(
|
|
1739
2096
|
Text,
|
|
1740
2097
|
{ key: 'failures', color: inkColor(getPalette().warn), wrap: 'truncate-end' },
|
|
1741
|
-
truncateColumns(`
|
|
2098
|
+
truncateColumns(` ${t('panel.provider.failure', { providers: directory?.failures.join(', ') ?? '' })}`, viewport.contentColumns),
|
|
1742
2099
|
)]),
|
|
1743
2100
|
...(rows.length === 0
|
|
1744
|
-
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' },
|
|
1745
|
-
:
|
|
2101
|
+
? [createElement(Text, { key: 'empty', dimColor: true, wrap: 'truncate-end' }, ` ${t('panel.model.noModels')}`)]
|
|
2102
|
+
: filtered.length === 0
|
|
2103
|
+
? [createElement(Text, { key: 'no-match', dimColor: true, wrap: 'truncate-end' }, truncateColumns(` ${t('panel.model.noMatch', { query: singleLineText(query) })}`, viewport.contentColumns))]
|
|
2104
|
+
: []),
|
|
1746
2105
|
]
|
|
1747
2106
|
// Measurement and rendering share the same physical-row budget: state
|
|
1748
2107
|
// messages consume body rows before selectable entries, as in Codex's
|
|
1749
2108
|
// list-selection views.
|
|
1750
2109
|
const visibleStateRows = stateRows.slice(0, viewport.bodyRows)
|
|
1751
2110
|
const rowBudget = Math.max(0, viewport.bodyRows - visibleStateRows.length)
|
|
1752
|
-
const first = selectionWindow(cursor,
|
|
1753
|
-
const visible = rowBudget === 0 ? [] :
|
|
2111
|
+
const first = selectionWindow(cursor, filtered.length, rowBudget)
|
|
2112
|
+
const visible = rowBudget === 0 ? [] : filtered.slice(first, first + rowBudget)
|
|
2113
|
+
const accent = panelAccent('model', getPalette().brand)
|
|
1754
2114
|
return createElement(
|
|
1755
2115
|
Box,
|
|
1756
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
1757
|
-
createElement(Text, { color: inkColor(
|
|
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)),
|
|
1758
2120
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1759
2121
|
...visibleStateRows,
|
|
1760
2122
|
...visible.map((row) => {
|
|
1761
|
-
const index =
|
|
2123
|
+
const index = filtered.indexOf(row)
|
|
1762
2124
|
const capability = row.inputModalities?.includes('image') === true ? ' · image' : ''
|
|
1763
2125
|
const label = displayText(`${row.providerName} · ${row.modelName}${capability}`)
|
|
1764
2126
|
return createElement(
|
|
@@ -1772,21 +2134,23 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
1772
2134
|
)
|
|
1773
2135
|
}),
|
|
1774
2136
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1775
|
-
createElement(Text, {
|
|
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))),
|
|
1776
2140
|
)
|
|
1777
2141
|
}
|
|
1778
2142
|
|
|
1779
2143
|
/** Compact provider-state copy; only value-free credential facts cross this boundary. */
|
|
1780
2144
|
function providerStateLabel(row: ProviderTargetView): string {
|
|
1781
|
-
const route = row.active ? 'active' : 'dormant'
|
|
2145
|
+
const route = row.active ? t('panel.provider.active') : t('panel.provider.dormant')
|
|
1782
2146
|
const credential = row.credential
|
|
1783
|
-
if (credential?.kind === 'error') return
|
|
2147
|
+
if (credential?.kind === 'error') return t('panel.provider.state', { route, value: t('panel.provider.keyStatusUnavailable') })
|
|
1784
2148
|
if (credential?.kind === 'facts') {
|
|
1785
|
-
if (!credential.configured) return
|
|
1786
|
-
const source = credential.source === undefined ? 'configured' : singleLineText(credential.source)
|
|
1787
|
-
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')}`}` })
|
|
1788
2152
|
}
|
|
1789
|
-
return
|
|
2153
|
+
return t('panel.provider.state', { route, value: row.configured ? t('panel.provider.authConfigured') : t('panel.provider.noLogin') })
|
|
1790
2154
|
}
|
|
1791
2155
|
|
|
1792
2156
|
/** The provider-management stage reached from /model with `a`. */
|
|
@@ -1795,15 +2159,15 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1795
2159
|
error: string | undefined
|
|
1796
2160
|
authorizations: ProviderAuthorizationDirectory | undefined
|
|
1797
2161
|
authorizationError: string | undefined
|
|
1798
|
-
onConfigure(target: ProviderTargetView)
|
|
1799
|
-
onUnset(target: ProviderTargetView)
|
|
1800
|
-
onRemove(target: ProviderTargetView)
|
|
1801
|
-
onLogin(target: ProviderTargetView, authorization: ProviderAuthorizationRow)
|
|
1802
|
-
onLogout(target: ProviderTargetView, authorization: ProviderAuthorizationRow)
|
|
1803
|
-
onRetry()
|
|
1804
|
-
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
|
|
1805
2169
|
/** Leave the whole /model flow (Ctrl+C), not just this stage. */
|
|
1806
|
-
onExit()
|
|
2170
|
+
onExit: () => void
|
|
1807
2171
|
}): ReactElement {
|
|
1808
2172
|
const stdout = useStdout().stdout
|
|
1809
2173
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
@@ -1865,9 +2229,9 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1865
2229
|
if (input === 'd') {
|
|
1866
2230
|
const facts = target.credential
|
|
1867
2231
|
if (facts?.kind !== 'facts' || !facts.configured) {
|
|
1868
|
-
setActionError('
|
|
2232
|
+
setActionError(t('panel.provider.noConfiguredKey'))
|
|
1869
2233
|
} else if (!facts.writable) {
|
|
1870
|
-
setActionError('
|
|
2234
|
+
setActionError(t('panel.provider.readOnlyKey'))
|
|
1871
2235
|
} else {
|
|
1872
2236
|
onUnset(target)
|
|
1873
2237
|
}
|
|
@@ -1875,7 +2239,7 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1875
2239
|
}
|
|
1876
2240
|
if (input === 'x') {
|
|
1877
2241
|
if (!target.removable) {
|
|
1878
|
-
setActionError('
|
|
2242
|
+
setActionError(t('panel.provider.notRemovable'))
|
|
1879
2243
|
} else {
|
|
1880
2244
|
onRemove(target)
|
|
1881
2245
|
}
|
|
@@ -1883,14 +2247,14 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1883
2247
|
}
|
|
1884
2248
|
const authorization = authorizationForProvider(authorizations, target.provider)
|
|
1885
2249
|
if (input === 'l' || input === 'L') {
|
|
1886
|
-
if (authorization === undefined) setActionError('
|
|
1887
|
-
else if (authorization.inFlight) setActionError('
|
|
2250
|
+
if (authorization === undefined) setActionError(t('panel.provider.noLoginFlow'))
|
|
2251
|
+
else if (authorization.inFlight) setActionError(t('panel.provider.loginRunning'))
|
|
1888
2252
|
else onLogin(target, authorization)
|
|
1889
2253
|
return
|
|
1890
2254
|
}
|
|
1891
2255
|
if (input === 'o' || input === 'O') {
|
|
1892
|
-
if (authorization === undefined || !authorization.record.configured) setActionError('
|
|
1893
|
-
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'))
|
|
1894
2258
|
else onLogout(target, authorization)
|
|
1895
2259
|
return
|
|
1896
2260
|
}
|
|
@@ -1899,7 +2263,7 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1899
2263
|
// the configuration surface behind an undiscoverable chord.
|
|
1900
2264
|
if (key.return) {
|
|
1901
2265
|
if (target.settingsNs.length === 0) {
|
|
1902
|
-
setActionError('
|
|
2266
|
+
setActionError(t('panel.provider.notManaged'))
|
|
1903
2267
|
} else {
|
|
1904
2268
|
onConfigure(target)
|
|
1905
2269
|
}
|
|
@@ -1907,10 +2271,10 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1907
2271
|
}, true)
|
|
1908
2272
|
|
|
1909
2273
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
1910
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('
|
|
2274
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.providers.compact'), viewport.contentColumns))
|
|
1911
2275
|
}
|
|
1912
2276
|
const stateRows: ReactElement[] = directory === undefined && error === undefined
|
|
1913
|
-
? [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')}`)]
|
|
1914
2278
|
: error !== undefined
|
|
1915
2279
|
? [createElement(Text, { key: 'error', color: inkColor(getPalette().error), wrap: 'truncate-end' }, truncateColumns(` ${singleLineText(error)}`, viewport.contentColumns))]
|
|
1916
2280
|
: [
|
|
@@ -1924,14 +2288,14 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1924
2288
|
)),
|
|
1925
2289
|
...(authorizationError === undefined
|
|
1926
2290
|
? []
|
|
1927
|
-
: [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))]),
|
|
1928
2292
|
...(authorizations?.failures ?? []).map((failure, index) => createElement(
|
|
1929
2293
|
Text,
|
|
1930
2294
|
{ key: `authorization-failure-${index}`, color: inkColor(getPalette().warn), wrap: 'truncate-end' },
|
|
1931
2295
|
truncateColumns(` ${singleLineText(failure)}`, viewport.contentColumns),
|
|
1932
2296
|
)),
|
|
1933
2297
|
...(rows.length === 0
|
|
1934
|
-
? [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')}`)]
|
|
1935
2299
|
: []),
|
|
1936
2300
|
]
|
|
1937
2301
|
const visibleStateRows = stateRows.slice(0, viewport.bodyRows)
|
|
@@ -1942,7 +2306,7 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1942
2306
|
const itemRows: ReactElement[] = []
|
|
1943
2307
|
for (let display = first; display < first + rowBudget && display < displayLength; display += 1) {
|
|
1944
2308
|
if (hasSeparator && display === configuredCount) {
|
|
1945
|
-
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)))
|
|
1946
2310
|
continue
|
|
1947
2311
|
}
|
|
1948
2312
|
const index = hasSeparator && display > configuredCount ? display - 1 : display
|
|
@@ -1966,15 +2330,16 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
|
|
|
1966
2330
|
truncateColumns((index === cursor ? '❯ ' : ' ') + displayText(label), viewport.contentColumns),
|
|
1967
2331
|
))
|
|
1968
2332
|
}
|
|
2333
|
+
const accent = panelAccent('model-providers', getPalette().brand)
|
|
1969
2334
|
return createElement(
|
|
1970
2335
|
Box,
|
|
1971
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
1972
|
-
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)),
|
|
1973
2338
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1974
2339
|
...visibleStateRows,
|
|
1975
2340
|
...itemRows,
|
|
1976
2341
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
1977
|
-
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)),
|
|
1978
2343
|
)
|
|
1979
2344
|
}
|
|
1980
2345
|
|
|
@@ -2002,14 +2367,14 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2002
2367
|
target: ProviderTargetView
|
|
2003
2368
|
/** Models with declared efforts (settings first, catalog-advertised after) a model row can copy from. */
|
|
2004
2369
|
effortDonors: readonly EffortDonor[]
|
|
2005
|
-
save(target: ProviderTargetView, configuration: ProviderConfiguration)
|
|
2370
|
+
save: (target: ProviderTargetView, configuration: ProviderConfiguration) => Promise<void>
|
|
2006
2371
|
saveCredential: ((target: ProviderTargetView, key: string) => Promise<void>) | undefined
|
|
2007
|
-
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[]>
|
|
2008
2373
|
/** Report a successful save so the surface can notice the key rotation. */
|
|
2009
|
-
done(result: { readonly key: boolean })
|
|
2010
|
-
back()
|
|
2374
|
+
done: (result: { readonly key: boolean }) => void
|
|
2375
|
+
back: () => void
|
|
2011
2376
|
/** Leave the whole /model flow (Ctrl+C), not just this page. */
|
|
2012
|
-
onExit()
|
|
2377
|
+
onExit: () => void
|
|
2013
2378
|
}): ReactElement {
|
|
2014
2379
|
const stdout = useStdout().stdout
|
|
2015
2380
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
@@ -2063,7 +2428,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2063
2428
|
|
|
2064
2429
|
/** Compact declaration summary for the row label: count, off, or inherit. */
|
|
2065
2430
|
const effortsSummary = (model: ProviderModelSettings): string => {
|
|
2066
|
-
const raw = (model.extras
|
|
2431
|
+
const raw = (model.extras)?.reasoningEfforts
|
|
2067
2432
|
if (raw === false) return 'off'
|
|
2068
2433
|
if (isDeclaredReasoningEfforts(raw)) return String(Object.keys(raw).length)
|
|
2069
2434
|
return '~'
|
|
@@ -2216,7 +2581,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2216
2581
|
}
|
|
2217
2582
|
if (input === 'e' && selected !== undefined) {
|
|
2218
2583
|
setError(undefined)
|
|
2219
|
-
setEffDraft(serializeReasoningEfforts((selected.extras
|
|
2584
|
+
setEffDraft(serializeReasoningEfforts((selected.extras)?.reasoningEfforts))
|
|
2220
2585
|
setEffEditing(true)
|
|
2221
2586
|
return
|
|
2222
2587
|
}
|
|
@@ -2244,19 +2609,20 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2244
2609
|
// Never hide a live input surface: one visible row keeps the escape
|
|
2245
2610
|
// route honest on extremely short terminals (the three fixed rows - key,
|
|
2246
2611
|
// url, add-by-id - cannot fit below a three-row body).
|
|
2247
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('
|
|
2612
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.setup.compact'), viewport.contentColumns))
|
|
2248
2613
|
}
|
|
2249
2614
|
if (page === 'donor') {
|
|
2250
2615
|
const stateRow = donorRows.length === 0
|
|
2251
|
-
? createElement(Text, { key: 'empty', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(
|
|
2252
|
-
: 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))
|
|
2253
2618
|
const donorBudget = Math.max(0, viewport.bodyRows - 2)
|
|
2254
2619
|
const donorFirst = selectionWindow(donorIndex, donorRows.length, donorBudget)
|
|
2255
2620
|
const donorVisible = donorRows.slice(donorFirst, donorFirst + donorBudget)
|
|
2621
|
+
const accent = panelAccent('model-efforts', getPalette().brand)
|
|
2256
2622
|
return createElement(
|
|
2257
2623
|
Box,
|
|
2258
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
2259
|
-
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)),
|
|
2260
2626
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2261
2627
|
stateRow,
|
|
2262
2628
|
...donorVisible.map((donor, index) => {
|
|
@@ -2265,7 +2631,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2265
2631
|
return createElement(Text, { key: donor.provider + '/' + donor.id, color: active ? inkColor(getPalette().brandBright) : inkColor(getPalette().text), wrap: 'truncate-end' }, truncateColumns(label, viewport.contentColumns))
|
|
2266
2632
|
}),
|
|
2267
2633
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2268
|
-
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)),
|
|
2269
2635
|
)
|
|
2270
2636
|
}
|
|
2271
2637
|
if (page === 'discover') {
|
|
@@ -2307,7 +2673,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2307
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)))
|
|
2308
2674
|
continue
|
|
2309
2675
|
}
|
|
2310
|
-
const model = models[index]
|
|
2676
|
+
const model = models[index]
|
|
2311
2677
|
const active = index === cursor
|
|
2312
2678
|
const context = model.contextWindow === undefined ? '-' : String(model.contextWindow)
|
|
2313
2679
|
const output = model.maxTokens === undefined ? '-' : String(model.maxTokens)
|
|
@@ -2317,10 +2683,11 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2317
2683
|
: ' in:' + (active && field === 'ctx' ? '[' + context + ']' : context) + ' out:' + (active && field === 'out' ? '[' + output + ']' : output) + ' eff:' + effortsSummary(model)
|
|
2318
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)))
|
|
2319
2685
|
}
|
|
2686
|
+
const accent = panelAccent('model-configure', getPalette().brand)
|
|
2320
2687
|
return createElement(
|
|
2321
2688
|
Box,
|
|
2322
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
2323
|
-
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)),
|
|
2324
2691
|
// The adapter's configuration diagnostic heads the editor: the provider
|
|
2325
2692
|
// is here precisely because it stayed listed for repair.
|
|
2326
2693
|
...(target.diagnostic === undefined ? [] : [createElement(
|
|
@@ -2334,7 +2701,7 @@ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDono
|
|
|
2334
2701
|
...stateRows,
|
|
2335
2702
|
...modelRows,
|
|
2336
2703
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2337
|
-
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)),
|
|
2338
2705
|
)
|
|
2339
2706
|
}
|
|
2340
2707
|
|
|
@@ -2350,11 +2717,11 @@ function ProviderDiscoveryPanel({ target, baseURL, apiKey, configured, discover,
|
|
|
2350
2717
|
baseURL: string
|
|
2351
2718
|
apiKey: string
|
|
2352
2719
|
configured: readonly string[]
|
|
2353
|
-
discover(target: ProviderTargetView, request: { readonly apiKey?: string; readonly baseURL?: string }, signal?: AbortSignal)
|
|
2354
|
-
onAdopt(models: readonly DiscoveredModelView[])
|
|
2355
|
-
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
|
|
2356
2723
|
/** Leave the whole /model flow (Ctrl+C). */
|
|
2357
|
-
onExit()
|
|
2724
|
+
onExit: () => void
|
|
2358
2725
|
}): ReactElement {
|
|
2359
2726
|
const stdout = useStdout().stdout
|
|
2360
2727
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
@@ -2415,24 +2782,25 @@ function ProviderDiscoveryPanel({ target, baseURL, apiKey, configured, discover,
|
|
|
2415
2782
|
}
|
|
2416
2783
|
}, true)
|
|
2417
2784
|
if (viewport.maxHeight === 0) {
|
|
2418
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('
|
|
2785
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.discovery.compact'), viewport.contentColumns))
|
|
2419
2786
|
}
|
|
2420
2787
|
const stateRows = loading
|
|
2421
|
-
? [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))]
|
|
2422
2789
|
: error !== undefined
|
|
2423
2790
|
? [createElement(Text, { key: 'error', color: inkColor(getPalette().error), wrap: 'truncate-end' }, truncateColumns(' ' + error, viewport.contentColumns))]
|
|
2424
2791
|
: rows.length === 0
|
|
2425
|
-
? [createElement(Text, { key: 'empty', color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(
|
|
2426
|
-
: [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))]
|
|
2427
2794
|
// One spare row keeps the panel strictly below maxHeight even with the
|
|
2428
2795
|
// gap collapsed (the at-equality regime makes Ink rewrite Static).
|
|
2429
2796
|
const rowBudget = Math.max(0, viewport.bodyRows - stateRows.length - 1)
|
|
2430
2797
|
const first = selectionWindow(cursor, rows.length, rowBudget)
|
|
2431
2798
|
const visible = rows.slice(first, first + rowBudget)
|
|
2799
|
+
const accent = panelAccent('model-discover', getPalette().brand)
|
|
2432
2800
|
return createElement(
|
|
2433
2801
|
Box,
|
|
2434
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
2435
|
-
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)),
|
|
2436
2804
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2437
2805
|
...stateRows,
|
|
2438
2806
|
...visible.map((model, index) => {
|
|
@@ -2444,7 +2812,7 @@ function ProviderDiscoveryPanel({ target, baseURL, apiKey, configured, discover,
|
|
|
2444
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))
|
|
2445
2813
|
}),
|
|
2446
2814
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2447
|
-
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)),
|
|
2448
2816
|
)
|
|
2449
2817
|
}
|
|
2450
2818
|
|
|
@@ -2452,9 +2820,9 @@ function ProviderDiscoveryPanel({ target, baseURL, apiKey, configured, discover,
|
|
|
2452
2820
|
function ProviderConfirmPanel({ target, kind, confirm, done, back }: {
|
|
2453
2821
|
target: ProviderTargetView
|
|
2454
2822
|
kind: 'credential' | 'provider'
|
|
2455
|
-
confirm(target: ProviderTargetView)
|
|
2456
|
-
done()
|
|
2457
|
-
back()
|
|
2823
|
+
confirm: (target: ProviderTargetView) => Promise<void>
|
|
2824
|
+
done: () => void
|
|
2825
|
+
back: () => void
|
|
2458
2826
|
}): ReactElement {
|
|
2459
2827
|
const stdout = useStdout().stdout
|
|
2460
2828
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
|
|
@@ -2512,7 +2880,7 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
2512
2880
|
skills: readonly SkillRow[]
|
|
2513
2881
|
commandError: string | undefined
|
|
2514
2882
|
skillError: string | undefined
|
|
2515
|
-
onClose()
|
|
2883
|
+
onClose: () => void
|
|
2516
2884
|
}): ReactElement {
|
|
2517
2885
|
const stdout = useStdout().stdout
|
|
2518
2886
|
const columns = stdout?.columns ?? 80
|
|
@@ -2522,19 +2890,19 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
2522
2890
|
const descBudget = Math.max(0, viewport.contentColumns - nameWidth - 2)
|
|
2523
2891
|
const row = (label: string, description: string): ReactElement => createElement(
|
|
2524
2892
|
Text,
|
|
2525
|
-
{
|
|
2526
|
-
` ${padColumns(label, nameWidth)}${
|
|
2893
|
+
{ color: inkColor(getPalette().dim), wrap: 'truncate-end' },
|
|
2894
|
+
` ${padColumns(label, nameWidth)}${truncateColumns(displayText(description), descBudget)}`,
|
|
2527
2895
|
)
|
|
2528
2896
|
const content: ReactElement[] = [
|
|
2529
|
-
createElement(Text, { key: 'keys-title', bold: true, wrap: 'truncate-end' }, '
|
|
2530
|
-
createElement(Text, { key: 'key-submit', dimColor: true, wrap: 'truncate-end' }, '
|
|
2531
|
-
createElement(Text, { key: 'key-mentions', dimColor: true, wrap: 'truncate-end' }, '
|
|
2532
|
-
createElement(Text, { key: 'key-inspector', dimColor: true, wrap: 'truncate-end' },
|
|
2533
|
-
createElement(Text, { key: 'key-cancel', dimColor: true, wrap: 'truncate-end' }, '
|
|
2534
|
-
createElement(Text, { key: 'key-queue', dimColor: true, wrap: 'truncate-end' },
|
|
2535
|
-
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')}`),
|
|
2536
2904
|
createElement(Text, { key: 'commands-gap' }, ' '),
|
|
2537
|
-
createElement(Text, { key: 'commands-title', bold: true, wrap: 'truncate-end' }, '
|
|
2905
|
+
createElement(Text, { key: 'commands-title', bold: true, wrap: 'truncate-end' }, t('help.commandsTitle')),
|
|
2538
2906
|
...(commandError === undefined
|
|
2539
2907
|
? []
|
|
2540
2908
|
: [createElement(
|
|
@@ -2545,18 +2913,18 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
2545
2913
|
...LOCAL_COMMANDS.map(command => createElement(
|
|
2546
2914
|
Box,
|
|
2547
2915
|
{ key: `local-${command.label.slice(1)}` },
|
|
2548
|
-
row(command.label, command.
|
|
2916
|
+
row(command.label, t(command.descriptionKey)),
|
|
2549
2917
|
)),
|
|
2550
2918
|
...descriptors.filter(descriptor => !LOCAL_COMMAND_NAMES.has(descriptor.name)).map(descriptor => createElement(
|
|
2551
2919
|
Text,
|
|
2552
|
-
{ key: `command-${descriptor.name}`,
|
|
2553
|
-
` ${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)}`,
|
|
2554
2922
|
)),
|
|
2555
2923
|
...(skills.length === 0 && skillError === undefined
|
|
2556
2924
|
? []
|
|
2557
2925
|
: [
|
|
2558
2926
|
createElement(Text, { key: 'skills-gap' }, ' '),
|
|
2559
|
-
createElement(Text, { key: 'skills-title', bold: true, wrap: 'truncate-end' }, '
|
|
2927
|
+
createElement(Text, { key: 'skills-title', bold: true, wrap: 'truncate-end' }, t('help.skillsTitle')),
|
|
2560
2928
|
]),
|
|
2561
2929
|
...(skillError === undefined
|
|
2562
2930
|
? []
|
|
@@ -2567,8 +2935,8 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
2567
2935
|
)]),
|
|
2568
2936
|
...skills.map(skill => createElement(
|
|
2569
2937
|
Text,
|
|
2570
|
-
{ key: `skill-${skill.name}`,
|
|
2571
|
-
` ${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)}`,
|
|
2572
2940
|
)),
|
|
2573
2941
|
]
|
|
2574
2942
|
const visibleScroll = clampScroll(scroll, content.length, viewport.bodyRows)
|
|
@@ -2594,17 +2962,18 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }: {
|
|
|
2594
2962
|
})
|
|
2595
2963
|
|
|
2596
2964
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
2597
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('
|
|
2965
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('help.compact'), viewport.contentColumns))
|
|
2598
2966
|
}
|
|
2599
2967
|
|
|
2968
|
+
const accent = panelAccent('help', getPalette().brand)
|
|
2600
2969
|
return createElement(
|
|
2601
2970
|
Box,
|
|
2602
|
-
{ flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(
|
|
2603
|
-
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)),
|
|
2604
2973
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2605
2974
|
...content.slice(visibleScroll, visibleScroll + viewport.bodyRows),
|
|
2606
2975
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2607
|
-
createElement(Text, {
|
|
2976
|
+
createElement(Text, { wrap: 'truncate-end' }, dim(truncateColumns(t('help.footer'), viewport.contentColumns))),
|
|
2608
2977
|
)
|
|
2609
2978
|
}
|
|
2610
2979
|
|
|
@@ -2614,7 +2983,8 @@ function verboseLine(text: string, columns: number): string {
|
|
|
2614
2983
|
}
|
|
2615
2984
|
|
|
2616
2985
|
/** The empty-composer placeholder text (shared by the static and wave paths). */
|
|
2617
|
-
const
|
|
2986
|
+
const composerPlaceholder = (mode: 'queue' | 'steer'): string =>
|
|
2987
|
+
t(mode === 'steer' ? 'composer.placeholderSteer' : 'composer.placeholder')
|
|
2618
2988
|
|
|
2619
2989
|
/** One physical cell of the wave-painted composer row: a char plus styles. */
|
|
2620
2990
|
interface ComposerCell {
|
|
@@ -2647,9 +3017,9 @@ function waveRowSpans(cells: readonly ComposerCell[]): ReactElement[] {
|
|
|
2647
3017
|
const spans: ReactElement[] = []
|
|
2648
3018
|
let start = 0
|
|
2649
3019
|
while (start < cells.length) {
|
|
2650
|
-
const cell = cells[start]
|
|
3020
|
+
const cell = cells[start]
|
|
2651
3021
|
let end = start + 1
|
|
2652
|
-
while (end < cells.length && sameCellStyle(cells[end]
|
|
3022
|
+
while (end < cells.length && sameCellStyle(cells[end], cell)) end += 1
|
|
2653
3023
|
spans.push(createElement(
|
|
2654
3024
|
Text,
|
|
2655
3025
|
{
|
|
@@ -2672,7 +3042,7 @@ function cellIndexAtColumn(cells: readonly ComposerCell[], target: number): numb
|
|
|
2672
3042
|
let column = 0
|
|
2673
3043
|
for (let index = 0; index < cells.length; index += 1) {
|
|
2674
3044
|
if (column === target) return index
|
|
2675
|
-
column += cells[index]
|
|
3045
|
+
column += cells[index].width ?? visibleColumns(cells[index].char)
|
|
2676
3046
|
if (column > target) return undefined
|
|
2677
3047
|
}
|
|
2678
3048
|
return undefined
|
|
@@ -2745,11 +3115,13 @@ interface ComposerWaveProps {
|
|
|
2745
3115
|
value: string
|
|
2746
3116
|
/** Tier prompt glyph and accent color (persistent, like Codex's charge). */
|
|
2747
3117
|
promptGlyph: string
|
|
3118
|
+
/** Empty-composer placeholder for the delivery mode in force. */
|
|
3119
|
+
placeholder: string
|
|
2748
3120
|
promptColor: string
|
|
2749
3121
|
/** Fires EXACTLY ONCE when this sweep ends for any reason — completed,
|
|
2750
3122
|
* cancelled by the gate, or unmounted (a modal panel froze the composer) —
|
|
2751
3123
|
* so Input's played-key latch survives the leaf's unmount/remount cycle. */
|
|
2752
|
-
onSettled()
|
|
3124
|
+
onSettled: () => void
|
|
2753
3125
|
}
|
|
2754
3126
|
|
|
2755
3127
|
/**
|
|
@@ -2817,7 +3189,7 @@ function ComposerWave(props: ComposerWaveProps): ReactElement {
|
|
|
2817
3189
|
}
|
|
2818
3190
|
for (const span of splitGraphemes(parts.before)) push(span.text)
|
|
2819
3191
|
if (parts.hasCaret) push(parts.caret, { inverse: props.caretVisible })
|
|
2820
|
-
const tail = placeholder ?
|
|
3192
|
+
const tail = placeholder ? props.placeholder : parts.after
|
|
2821
3193
|
for (const span of splitGraphemes(tail)) push(span.text, placeholder ? { dim: true } : {})
|
|
2822
3194
|
while (usedColumns < props.bandWidth) push(' ')
|
|
2823
3195
|
|
|
@@ -2826,9 +3198,9 @@ function ComposerWave(props: ComposerWaveProps): ReactElement {
|
|
|
2826
3198
|
const word = tier === 'unknown' ? 'Into the Unknown' : 'deepseek'
|
|
2827
3199
|
const start = Math.max(2, Math.floor((props.bandWidth - word.length) / 2))
|
|
2828
3200
|
const indices = Array.from({ length: word.length }, (_, at) => cellIndexAtColumn(cells, start + at))
|
|
2829
|
-
if (indices.every(index => index !== undefined && (cells[index]
|
|
3201
|
+
if (indices.every(index => index !== undefined && (cells[index].char === ' ' || cells[index].dim === true))) {
|
|
2830
3202
|
for (let at = 0; at < word.length; at += 1) {
|
|
2831
|
-
const cell = cells[indices[at]!]
|
|
3203
|
+
const cell = cells[indices[at]!]
|
|
2832
3204
|
cell.char = word[at]!
|
|
2833
3205
|
cell.width = 1
|
|
2834
3206
|
cell.color = inkColor(deepseekWaveWordHue(at, hues))
|
|
@@ -2840,11 +3212,11 @@ function ComposerWave(props: ComposerWaveProps): ReactElement {
|
|
|
2840
3212
|
if (bandRow === middleBandRow && (tier === 'deepseek' || tier === 'unknown') && style === 'wave') {
|
|
2841
3213
|
const spark = deepseekWaveSpark(tick)
|
|
2842
3214
|
const lastIndex = cellIndexAtColumn(cells, props.bandWidth - 1)
|
|
2843
|
-
if (spark !== null && lastIndex !== undefined && cells[lastIndex]
|
|
2844
|
-
cells[lastIndex]
|
|
2845
|
-
cells[lastIndex]
|
|
2846
|
-
cells[lastIndex]
|
|
2847
|
-
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
|
|
2848
3220
|
}
|
|
2849
3221
|
}
|
|
2850
3222
|
return createElement(Text, { key: `editor-${sourceIndex}`, wrap: 'truncate-end' }, ...waveRowSpans(cells))
|
|
@@ -2858,13 +3230,104 @@ function ComposerWave(props: ComposerWaveProps): ReactElement {
|
|
|
2858
3230
|
)
|
|
2859
3231
|
}
|
|
2860
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
|
+
|
|
2861
3324
|
/**
|
|
2862
3325
|
* The Ctrl+O transcript inspector: one selected durable entry at a time,
|
|
2863
3326
|
* with independent history selection and content scrolling. The complete
|
|
2864
3327
|
* retained entry is converted to physical rows, but only one viewport slice
|
|
2865
3328
|
* reaches Ink, so even a huge reasoning block cannot grow the dynamic tree.
|
|
2866
3329
|
*/
|
|
2867
|
-
function VerbosePanel({ entries, onClose }: { entries: readonly TranscriptEntry[]; onClose()
|
|
3330
|
+
function VerbosePanel({ entries, onClose }: { entries: readonly TranscriptEntry[]; onClose: () => void }): ReactElement {
|
|
2868
3331
|
const stdout = useStdout().stdout
|
|
2869
3332
|
const columns = stdout?.columns ?? 80
|
|
2870
3333
|
const rows = stdout?.rows ?? 30
|
|
@@ -2958,14 +3421,15 @@ function VerbosePanel({ entries, onClose }: { entries: readonly TranscriptEntry[
|
|
|
2958
3421
|
return createElement(
|
|
2959
3422
|
Text,
|
|
2960
3423
|
{ wrap: 'truncate-end' },
|
|
2961
|
-
truncateColumns('
|
|
3424
|
+
truncateColumns(t('panel.verbose.compact'), viewport.contentColumns),
|
|
2962
3425
|
)
|
|
2963
3426
|
}
|
|
2964
3427
|
|
|
2965
3428
|
const title = entries.length === 0
|
|
2966
3429
|
? 'history details · empty'
|
|
2967
|
-
: `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}`
|
|
2968
3431
|
const visible = allLines.slice(visibleScroll, visibleScroll + viewport.bodyRows)
|
|
3432
|
+
const accent = panelAccent('history-inspector', getPalette().brand)
|
|
2969
3433
|
return createElement(
|
|
2970
3434
|
Box,
|
|
2971
3435
|
{
|
|
@@ -2973,11 +3437,11 @@ function VerbosePanel({ entries, onClose }: { entries: readonly TranscriptEntry[
|
|
|
2973
3437
|
width: viewport.outerColumns,
|
|
2974
3438
|
paddingX: 1,
|
|
2975
3439
|
borderStyle: 'round',
|
|
2976
|
-
borderColor: inkColor(
|
|
3440
|
+
borderColor: inkColor(accent.border),
|
|
2977
3441
|
},
|
|
2978
3442
|
createElement(
|
|
2979
3443
|
Text,
|
|
2980
|
-
{ color: inkColor(
|
|
3444
|
+
{ color: inkColor(accent.title), bold: true, wrap: 'truncate-end' },
|
|
2981
3445
|
truncateColumns(title, viewport.contentColumns),
|
|
2982
3446
|
),
|
|
2983
3447
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
@@ -2991,8 +3455,8 @@ function VerbosePanel({ entries, onClose }: { entries: readonly TranscriptEntry[
|
|
|
2991
3455
|
createElement(PanelGap, { visible: viewport.gapRows > 0 }),
|
|
2992
3456
|
createElement(
|
|
2993
3457
|
Text,
|
|
2994
|
-
{
|
|
2995
|
-
dim(truncateColumns('
|
|
3458
|
+
{ wrap: 'truncate-end' },
|
|
3459
|
+
dim(truncateColumns(t('panel.verbose.footer'), viewport.contentColumns)),
|
|
2996
3460
|
),
|
|
2997
3461
|
)
|
|
2998
3462
|
}
|
|
@@ -3039,7 +3503,7 @@ export function completionCandidates(
|
|
|
3039
3503
|
): readonly CompletionCandidate[] {
|
|
3040
3504
|
if (!value.startsWith('/')) return []
|
|
3041
3505
|
const prefix = value.slice(1).split(' ')[0] ?? ''
|
|
3042
|
-
const local: CompletionCandidate[] = LOCAL_COMMANDS.map(command => ({
|
|
3506
|
+
const local: CompletionCandidate[] = LOCAL_COMMANDS.map(command => ({ label: command.label, description: t(command.descriptionKey), origin: 'command' }))
|
|
3043
3507
|
// Local commands shadow registry names (e.g. the TUI-local /permission works
|
|
3044
3508
|
// before any session exists, while the registry child needs one), so
|
|
3045
3509
|
// collisions cannot render two rows with the same key.
|
|
@@ -3104,10 +3568,10 @@ function completionMenuRowCount(terminalRows: number, rowCount: number): number
|
|
|
3104
3568
|
|
|
3105
3569
|
/**
|
|
3106
3570
|
* The completion menu, rendered inside the composer's subtree directly above
|
|
3107
|
-
* the composer band — attached the way Claude-Code anchors its dropdown.
|
|
3108
|
-
*
|
|
3109
|
-
*
|
|
3110
|
-
*
|
|
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
|
|
3111
3575
|
* editor's live completion state, so no cross-component effect ever resyncs
|
|
3112
3576
|
* it (a state lift here previously deadlocked the menu after a resize).
|
|
3113
3577
|
*/
|
|
@@ -3165,7 +3629,7 @@ function CompletionMenu({ active, mention, index, rows, error }: {
|
|
|
3165
3629
|
// Scroll affordance: with the full merged catalog (commands + registry +
|
|
3166
3630
|
// skills) the six-row window rarely shows the tail — count and hint keep
|
|
3167
3631
|
// the rest discoverable without inflating the menu budget.
|
|
3168
|
-
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,
|
|
3169
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,
|
|
3170
3634
|
)
|
|
3171
3635
|
}
|
|
@@ -3187,84 +3651,109 @@ interface DraftFile extends FilePathInspection {
|
|
|
3187
3651
|
* While a modal (approval / question / model panel) owns the keys, the
|
|
3188
3652
|
* box passes every key through untouched.
|
|
3189
3653
|
*/
|
|
3190
|
-
function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openJobs, openStatusline, openTheme, openHistory, openAgents, openSubagent, openTodos, openDelete, openDiff, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, applyEditorKeys, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, inspectFiles, prepareFiles,
|
|
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 }: {
|
|
3191
3655
|
active: boolean
|
|
3192
3656
|
frozen: boolean
|
|
3657
|
+
/** Frozen-band hint naming the surface that owns the keyboard; an empty
|
|
3658
|
+
* draft otherwise advertises typing that the composer cannot accept. */
|
|
3659
|
+
frozenHint?: string
|
|
3193
3660
|
busy: boolean
|
|
3194
3661
|
descriptors: readonly CommandDescriptor[]
|
|
3195
3662
|
skills: readonly SkillRow[]
|
|
3196
|
-
dispatch(text: string, attachments?: readonly ContentBlock[], origin?: string)
|
|
3197
|
-
|
|
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
|
|
3198
3670
|
/** The full current session identity ('' while pending); the delivery origin. */
|
|
3199
3671
|
sessionKey: string
|
|
3200
|
-
interrupt()
|
|
3201
|
-
quit()
|
|
3202
|
-
openModel()
|
|
3203
|
-
openEffort()
|
|
3204
|
-
openHelp()
|
|
3205
|
-
openMode()
|
|
3206
|
-
openPermission()
|
|
3207
|
-
openResume()
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
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
|
|
3683
|
+
/** Open the /update panel (aligned upgrade surface). */
|
|
3684
|
+
openUpdate: () => void
|
|
3685
|
+
/** Open the /schedule reminder panel (read-only catalog). */
|
|
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
|
|
3213
3696
|
/** Open the /agents panel (live subagent feed + transcript entry). */
|
|
3214
|
-
openAgents()
|
|
3697
|
+
openAgents: () => void
|
|
3215
3698
|
/** Open the /subagent model panel. */
|
|
3216
|
-
openSubagent()
|
|
3699
|
+
openSubagent: () => void
|
|
3217
3700
|
/** Open the /todos subpage (full todo list in one bounded panel). */
|
|
3218
|
-
openTodos()
|
|
3701
|
+
openTodos: () => void
|
|
3702
|
+
openUsage: () => void
|
|
3219
3703
|
/** Open the /resume picker in delete mode, optionally pre-armed on one id. */
|
|
3220
|
-
openDelete(id?: string)
|
|
3221
|
-
openDiff(argument: string)
|
|
3222
|
-
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
|
|
3223
3709
|
/** The row id awaiting y/n in this box, when a deletion is pending. */
|
|
3224
3710
|
deleteConfirm?: string
|
|
3225
3711
|
/** Confirm the pending deletion (y in the box). */
|
|
3226
|
-
confirmDelete()
|
|
3712
|
+
confirmDelete: () => void
|
|
3227
3713
|
/** Cancel the pending deletion (any other key in the box). */
|
|
3228
|
-
cancelDelete()
|
|
3229
|
-
createSession(mode?: string)
|
|
3230
|
-
forkSession(argument: string)
|
|
3231
|
-
cancelSessionSwitch()
|
|
3232
|
-
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
|
|
3233
3719
|
/** Apply the Ctrl+R passthrough to the detected editor (/vscode-keys); resolves to a one-line summary. */
|
|
3234
|
-
applyEditorKeys()
|
|
3720
|
+
applyEditorKeys: () => Promise<string>
|
|
3235
3721
|
hasNotice: boolean
|
|
3236
|
-
dismissNotice()
|
|
3237
|
-
toggleReasoning()
|
|
3238
|
-
openVerbose()
|
|
3239
|
-
clearView()
|
|
3240
|
-
refresh()
|
|
3241
|
-
loadMentions(query: string, signal?: AbortSignal)
|
|
3242
|
-
inspectImages(paths: readonly string[])
|
|
3243
|
-
prepareImages(paths: readonly string[], signal?: AbortSignal)
|
|
3244
|
-
inspectFiles(paths: readonly string[])
|
|
3245
|
-
prepareFiles(paths: readonly string[], signal?: AbortSignal)
|
|
3246
|
-
|
|
3247
|
-
exportTranscript(argument: string)
|
|
3248
|
-
renameTitle(argument: string)
|
|
3249
|
-
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>
|
|
3250
3736
|
/** Newest-first recall space (persistent + in-session, deduped). */
|
|
3251
3737
|
recallSpace: readonly string[]
|
|
3252
3738
|
/** Record one in-session submission (deduped, local only). */
|
|
3253
|
-
recordLocal(text: string)
|
|
3739
|
+
recordLocal: (text: string) => void
|
|
3254
3740
|
/** Persist one submission to the global history file. */
|
|
3255
|
-
recordHistory(text: string)
|
|
3256
|
-
/**
|
|
3257
|
-
queued: readonly {
|
|
3258
|
-
|
|
3259
|
-
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
|
|
3260
3745
|
/** Accepted /history entry waiting to be placed into the composer. */
|
|
3261
3746
|
historyFill: { text: string; index: number } | undefined
|
|
3262
3747
|
/** Marks the accepted entry consumed (called after the fill is applied). */
|
|
3263
|
-
historyConsumed()
|
|
3748
|
+
historyConsumed: () => void
|
|
3264
3749
|
/** Whether timed animations run (shimmer, chase, blink, wave). */
|
|
3265
3750
|
animations: boolean
|
|
3266
3751
|
/** Apply and report one /animation toggle (App persists through the runner). */
|
|
3267
|
-
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
|
|
3268
3757
|
/** DeepSeek easter-egg wave tier of the applied route (null otherwise):
|
|
3269
3758
|
* official DeepSeek models drive their flash/pro tiers, non-DeepSeek
|
|
3270
3759
|
* models running an effort above high drive the "Into the Unknown"
|
|
@@ -3275,14 +3764,25 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3275
3764
|
waveStyle: DeepseekWaveStyle | null
|
|
3276
3765
|
/** Maximum physical editor rows the composer may occupy (see composerMaxRows). */
|
|
3277
3766
|
maxRows: number
|
|
3767
|
+
/** Terminal rows below the composer the editor does not own: the status
|
|
3768
|
+
* footer and Ink's parked cursor row. The IME anchor adds these to the
|
|
3769
|
+
* caret's in-band offset to reach that parked position. */
|
|
3770
|
+
anchorRowsBelow: number
|
|
3771
|
+
/** The managed terminal tab label; re-asserted on terminal focus-in so a
|
|
3772
|
+
* background process sharing the console cannot keep it overwritten. */
|
|
3773
|
+
tabTitle: string
|
|
3278
3774
|
/** Reports the editor's current physical row count so the live budget stays exact. */
|
|
3279
|
-
onEditorRows(rows: number)
|
|
3775
|
+
onEditorRows: (rows: number) => void
|
|
3280
3776
|
/** Reports the open completion menu's physical row count (0 when closed)
|
|
3281
3777
|
* for the same reason: the dynamic budget must reserve it, not overflow. */
|
|
3282
|
-
onMenuRows(rows: number)
|
|
3778
|
+
onMenuRows: (rows: number) => void
|
|
3283
3779
|
}): ReactElement {
|
|
3284
|
-
const
|
|
3285
|
-
const
|
|
3780
|
+
const { stdout: inputStdout } = useStdout()
|
|
3781
|
+
const columns = inputStdout?.columns ?? 80
|
|
3782
|
+
const inputTerminalRows = inputStdout?.rows ?? 30
|
|
3783
|
+
// The managed tab label, kept current for the focus-in re-assert below.
|
|
3784
|
+
const tabTitleRef = useRef(tabTitle)
|
|
3785
|
+
tabTitleRef.current = tabTitle
|
|
3286
3786
|
const editorColumns = Math.max(1, columns - 6)
|
|
3287
3787
|
const stdin = useStdin().stdin
|
|
3288
3788
|
const focusReporting = isVsCodeTerminalEnv()
|
|
@@ -3378,12 +3878,24 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3378
3878
|
if (stdin === undefined) return
|
|
3379
3879
|
const originalRead = stdin.read.bind(stdin)
|
|
3380
3880
|
const patchedRead = function patchedRead(this: typeof stdin, ...args: Parameters<typeof originalRead>) {
|
|
3381
|
-
|
|
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
|
|
3382
3886
|
if (chunk === null) return chunk
|
|
3383
3887
|
const normalized = normalizeKeyboardChunk(typeof chunk === 'string' ? chunk : String(chunk))
|
|
3384
3888
|
const input = focusReporting
|
|
3385
3889
|
? stripTerminalFocusEvents(normalized, focused => {
|
|
3386
3890
|
terminalFocusedRef.current = focused
|
|
3891
|
+
// Focus-in re-asserts the managed tab label on both channels: a
|
|
3892
|
+
// background process sharing this console (a test-runner worker,
|
|
3893
|
+
// for example) may have overwritten the console title while the
|
|
3894
|
+
// terminal was unfocused.
|
|
3895
|
+
if (focused && inputStdout !== undefined) {
|
|
3896
|
+
inputStdout.write(terminalTitleSequence(tabTitleRef.current))
|
|
3897
|
+
process.title = sanitizeTerminalTitle(tabTitleRef.current)
|
|
3898
|
+
}
|
|
3387
3899
|
})
|
|
3388
3900
|
: normalized
|
|
3389
3901
|
rawEditorTokens.current = tokenizeRawEditorChunk(input)
|
|
@@ -3391,7 +3903,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3391
3903
|
} as typeof stdin.read
|
|
3392
3904
|
stdin.read = patchedRead
|
|
3393
3905
|
return () => {
|
|
3394
|
-
stdin.read = originalRead
|
|
3906
|
+
stdin.read = originalRead
|
|
3395
3907
|
}
|
|
3396
3908
|
}, [focusReporting, stdin])
|
|
3397
3909
|
|
|
@@ -3446,7 +3958,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3446
3958
|
|
|
3447
3959
|
const registerDraftImage = (inspection: ImagePathInspection, marker: string): boolean => {
|
|
3448
3960
|
if (draftImagesRef.current.some(image => sameImagePath(image.path, inspection.path))) {
|
|
3449
|
-
notify(
|
|
3961
|
+
notify(t('notice.attachmentAlready', { name: inspection.name }), 'warning')
|
|
3450
3962
|
return false
|
|
3451
3963
|
}
|
|
3452
3964
|
const next = [...draftImagesRef.current, { ...inspection, marker }]
|
|
@@ -3465,7 +3977,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3465
3977
|
const originalCursor = cursorRef.current
|
|
3466
3978
|
const total = imagePaths.length + filePaths.length
|
|
3467
3979
|
if (total === 0) return
|
|
3468
|
-
notify(
|
|
3980
|
+
notify(t('notice.attachmentsChecking', { count: total, plural: total === 1 ? '' : 's' }))
|
|
3469
3981
|
void Promise.all([
|
|
3470
3982
|
imagePaths.length === 0 ? Promise.resolve([]) : inspectImages(imagePaths),
|
|
3471
3983
|
filePaths.length === 0 ? Promise.resolve([]) : inspectFiles(filePaths),
|
|
@@ -3486,13 +3998,13 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3486
3998
|
markers.push(marker)
|
|
3487
3999
|
}
|
|
3488
4000
|
if (imageAdditions.length === 0 && fileAdditions.length === 0) {
|
|
3489
|
-
notify('
|
|
4001
|
+
notify(t('notice.attachmentsAlready'), 'warning')
|
|
3490
4002
|
return
|
|
3491
4003
|
}
|
|
3492
4004
|
const current = valueRef.current
|
|
3493
4005
|
const anchor = remapStableRange(originalValue, current, { start: originalCursor, end: originalCursor })
|
|
3494
4006
|
if (anchor === undefined) {
|
|
3495
|
-
notify('
|
|
4007
|
+
notify(t('notice.attachmentDraftChanged'), 'warning')
|
|
3496
4008
|
return
|
|
3497
4009
|
}
|
|
3498
4010
|
const at = anchor.start
|
|
@@ -3513,9 +4025,9 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3513
4025
|
draftFilesRef.current = nextFiles
|
|
3514
4026
|
setDraftFiles(nextFiles)
|
|
3515
4027
|
const count = imageAdditions.length + fileAdditions.length
|
|
3516
|
-
notify(
|
|
4028
|
+
notify(t('notice.attachmentsReady', { count, plural: count === 1 ? '' : 's' }))
|
|
3517
4029
|
}, (reason: unknown) => {
|
|
3518
|
-
notify(
|
|
4030
|
+
notify(t('notice.attachmentFailed', { message: reason instanceof Error ? reason.message : String(reason) }), 'error')
|
|
3519
4031
|
})
|
|
3520
4032
|
}
|
|
3521
4033
|
|
|
@@ -3600,7 +4112,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3600
4112
|
const current = valueRef.current
|
|
3601
4113
|
const anchor = remapStableRange(originalValue, current, { start, end: start + tokenText.length })
|
|
3602
4114
|
if (anchor === undefined || current.slice(anchor.start, anchor.end) !== tokenText) {
|
|
3603
|
-
notify('
|
|
4115
|
+
notify(t('notice.imageDraftChanged'), 'warning')
|
|
3604
4116
|
return
|
|
3605
4117
|
}
|
|
3606
4118
|
if (draftImagesRef.current.some(image => sameImagePath(image.path, inspection.path))) {
|
|
@@ -3611,7 +4123,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3611
4123
|
setCursor(edit.cursor)
|
|
3612
4124
|
resetCursorBlink()
|
|
3613
4125
|
setDismissedMenuValue(edit.value)
|
|
3614
|
-
notify(
|
|
4126
|
+
notify(t('notice.attachmentAlready', { name: inspection.name }), 'warning')
|
|
3615
4127
|
return
|
|
3616
4128
|
}
|
|
3617
4129
|
const marker = uniqueImageMarker(inspection.name, 'mention')
|
|
@@ -3623,9 +4135,9 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3623
4135
|
resetCursorBlink()
|
|
3624
4136
|
setDismissedMenuValue(edit.value)
|
|
3625
4137
|
registerDraftImage(inspection, marker)
|
|
3626
|
-
notify(
|
|
4138
|
+
notify(t('notice.imageReady', { name: inspection.name }))
|
|
3627
4139
|
}, (reason: unknown) => {
|
|
3628
|
-
notify(
|
|
4140
|
+
notify(t('notice.imageFailed', { message: reason instanceof Error ? reason.message : String(reason) }), 'error')
|
|
3629
4141
|
})
|
|
3630
4142
|
setCompletionIndex(0)
|
|
3631
4143
|
setDismissedMenuValue(undefined)
|
|
@@ -3729,23 +4241,19 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3729
4241
|
prepareAbortRef.current = undefined
|
|
3730
4242
|
setPreparingImages(false)
|
|
3731
4243
|
dismissNotice()
|
|
3732
|
-
notify('
|
|
4244
|
+
notify(t('notice.imageCancelled'), 'warning')
|
|
3733
4245
|
}
|
|
3734
4246
|
|
|
3735
|
-
/**
|
|
4247
|
+
/** Cross history while an unchanged recalled draft rests its caret on
|
|
4248
|
+
* either text edge; between the edges (or inside ordinary drafts) the
|
|
4249
|
+
* arrows move through visual rows first. */
|
|
3736
4250
|
const navigateVertical = (direction: -1 | 1): void => {
|
|
3737
4251
|
const currentValue = valueRef.current
|
|
3738
4252
|
const currentCursor = cursorRef.current
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
cursorRef.current = next
|
|
3744
|
-
setCursor(next)
|
|
3745
|
-
resetCursorBlink()
|
|
3746
|
-
preferredColumnRef.current = preferred
|
|
3747
|
-
return
|
|
3748
|
-
}
|
|
4253
|
+
// History owns the arrows while an unchanged recalled draft rests its
|
|
4254
|
+
// caret on either text edge (start or end). Everywhere else - edited
|
|
4255
|
+
// drafts, interior carets, ordinary typing - the arrows move through
|
|
4256
|
+
// visual rows as plain editing.
|
|
3749
4257
|
if (recall.current.entries.length > 0
|
|
3750
4258
|
&& shouldRecallNavigate(currentValue, currentCursor, recall.current.lastRecalled, direction)) {
|
|
3751
4259
|
const step = direction < 0 ? recallOlder(recall.current, currentValue) : recallNewer(recall.current)
|
|
@@ -3757,10 +4265,24 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3757
4265
|
setValue(safe)
|
|
3758
4266
|
setCursor(safe.length)
|
|
3759
4267
|
preferredColumnRef.current = null
|
|
3760
|
-
|
|
4268
|
+
// Suppress the completion menu for the recalled text: a recalled
|
|
4269
|
+
// command would otherwise reopen the menu, whose Up/Down navigation
|
|
4270
|
+
// then traps the walk before it reaches older history entries. Any
|
|
4271
|
+
// edit re-opens the menu; submitting resets the dismissal.
|
|
4272
|
+
setDismissedMenuValue(safe)
|
|
3761
4273
|
}
|
|
4274
|
+
resetCursorBlink()
|
|
4275
|
+
return
|
|
4276
|
+
}
|
|
4277
|
+
const model = editorModel(currentValue, editorColumns)
|
|
4278
|
+
const preferred = preferredColumnRef.current ?? caretSite(model, currentCursor).column
|
|
4279
|
+
const next = moveCursorVertically(model, currentCursor, preferred, direction)
|
|
4280
|
+
if (next !== currentCursor) {
|
|
4281
|
+
cursorRef.current = next
|
|
4282
|
+
setCursor(next)
|
|
4283
|
+
resetCursorBlink()
|
|
4284
|
+
preferredColumnRef.current = preferred
|
|
3762
4285
|
}
|
|
3763
|
-
resetCursorBlink()
|
|
3764
4286
|
}
|
|
3765
4287
|
|
|
3766
4288
|
useStableInput((input, key) => {
|
|
@@ -3787,16 +4309,25 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3787
4309
|
}
|
|
3788
4310
|
return
|
|
3789
4311
|
}
|
|
3790
|
-
// Shift+Tab cycles the permission
|
|
4312
|
+
// Shift+Tab cycles the mode stations: permission presets, then the
|
|
4313
|
+
// plan station when the composition offers it (Claude-Code convention).
|
|
3791
4314
|
if (key.tab && key.shift) {
|
|
3792
4315
|
try {
|
|
3793
|
-
const
|
|
3794
|
-
if (
|
|
4316
|
+
const label = cycleMode()
|
|
4317
|
+
if (label !== '') notify(label)
|
|
3795
4318
|
} catch (error: unknown) {
|
|
3796
|
-
notify(
|
|
4319
|
+
notify(t('notice.permissionChangeFailed', { message: error instanceof Error ? error.message : String(error) }), 'error')
|
|
3797
4320
|
}
|
|
3798
4321
|
return
|
|
3799
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
|
+
}
|
|
3800
4331
|
// Ctrl+R toggles the thinking display (Claude-Code reasoning fold).
|
|
3801
4332
|
// Alt+R is the zero-config alias: VS Code never intercepts Alt chords,
|
|
3802
4333
|
// so the toggle stays reachable before /vscode-keys has been applied.
|
|
@@ -3842,7 +4373,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3842
4373
|
applyEdit(deleteForward(liveValue, liveCursor))
|
|
3843
4374
|
return
|
|
3844
4375
|
}
|
|
3845
|
-
if (busy) notify('
|
|
4376
|
+
if (busy) notify(t('notice.cancelBeforeExit'), 'warning')
|
|
3846
4377
|
else quit()
|
|
3847
4378
|
return
|
|
3848
4379
|
}
|
|
@@ -3868,7 +4399,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3868
4399
|
const forwardDelete = rawEditorTokens.current?.some(token =>
|
|
3869
4400
|
token.kind === 'delete-forward' || token.kind === 'delete-word-forward') === true
|
|
3870
4401
|
if (forwardDelete) {
|
|
3871
|
-
|
|
4402
|
+
updateQueued?.(queued[queued.length - 1].messageId, { kind: 'remove' })
|
|
3872
4403
|
return
|
|
3873
4404
|
}
|
|
3874
4405
|
}
|
|
@@ -3900,7 +4431,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3900
4431
|
// Slash semantics with attachments are unchanged: commands cannot
|
|
3901
4432
|
// carry attachments, so the line goes to the model as a prompt —
|
|
3902
4433
|
// warn instead of surprising the user with a literal "/export".
|
|
3903
|
-
if (isSlashLine(text)) notify('
|
|
4434
|
+
if (isSlashLine(text)) notify(t('notice.commandAttachments'), 'warning')
|
|
3904
4435
|
// Attachment prepares resolve asynchronously; the app remounts onto
|
|
3905
4436
|
// another session in the meantime, and this (old) instance's unmount
|
|
3906
4437
|
// cleanup runs too late on the microtask timeline. Tag the delivery
|
|
@@ -3939,7 +4470,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3939
4470
|
}
|
|
3940
4471
|
recall.current = beginRecall(recallSpace, '')
|
|
3941
4472
|
const blocks: readonly ContentBlock[] = [...images, ...files]
|
|
3942
|
-
if (
|
|
4473
|
+
if (submitMode === 'steer') steer(text, blocks, originSession)
|
|
3943
4474
|
else dispatch(text, blocks, originSession)
|
|
3944
4475
|
}, (reason: unknown) => {
|
|
3945
4476
|
if (controller.signal.aborted || prepareEpochRef.current !== epoch) return
|
|
@@ -3958,13 +4489,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
3958
4489
|
setDismissedMenuValue(undefined)
|
|
3959
4490
|
if (trimmed === '') return
|
|
3960
4491
|
dismissNotice()
|
|
3961
|
-
// Global recall records
|
|
3962
|
-
// commands,
|
|
3963
|
-
// the submission resets any active recall
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
}
|
|
4492
|
+
// Global recall records every submission - prompts and typed slash
|
|
4493
|
+
// commands share one history, so Up/Down and /history recall commands
|
|
4494
|
+
// exactly like prompts; the submission resets any active recall
|
|
4495
|
+
// browsing.
|
|
4496
|
+
recordLocal(text)
|
|
4497
|
+
recordHistory(text)
|
|
3968
4498
|
recall.current = beginRecall(recallSpace, '')
|
|
3969
4499
|
if (text === '/quit') {
|
|
3970
4500
|
quit()
|
|
@@ -4009,7 +4539,16 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
4009
4539
|
return
|
|
4010
4540
|
}
|
|
4011
4541
|
if (text === '/review' || text.startsWith('/review ')) {
|
|
4012
|
-
|
|
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
|
+
}
|
|
4013
4552
|
return
|
|
4014
4553
|
}
|
|
4015
4554
|
if (text === '/model' || text.startsWith('/model ')) {
|
|
@@ -4044,6 +4583,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
4044
4583
|
else dispatch(text)
|
|
4045
4584
|
return
|
|
4046
4585
|
}
|
|
4586
|
+
if (text === '/search' || text.startsWith('/search ')) {
|
|
4587
|
+
openSearch(text.slice(7).trim())
|
|
4588
|
+
return
|
|
4589
|
+
}
|
|
4047
4590
|
if (text === '/new' || text.startsWith('/new ')) {
|
|
4048
4591
|
createSession(text.slice(4).trim() || undefined)
|
|
4049
4592
|
return
|
|
@@ -4056,6 +4599,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
4056
4599
|
openPlugin(text.slice(7).trim())
|
|
4057
4600
|
return
|
|
4058
4601
|
}
|
|
4602
|
+
if (text === '/update') {
|
|
4603
|
+
openUpdate()
|
|
4604
|
+
return
|
|
4605
|
+
}
|
|
4606
|
+
if (text === '/schedule') {
|
|
4607
|
+
openSchedule()
|
|
4608
|
+
return
|
|
4609
|
+
}
|
|
4059
4610
|
if (text === '/jobs' || text.startsWith('/jobs ')) {
|
|
4060
4611
|
openJobs()
|
|
4061
4612
|
return
|
|
@@ -4068,17 +4619,41 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
4068
4619
|
openTheme()
|
|
4069
4620
|
return
|
|
4070
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
|
+
}
|
|
4071
4632
|
if (text === '/animation' || text.startsWith('/animation ')) {
|
|
4072
4633
|
const parsed = parseAnimationsArgument(text.slice('/animation'.length))
|
|
4073
4634
|
if (parsed === 'toggle') applyAnimations(!animations)
|
|
4074
|
-
else if (parsed === 'usage') notify('usage
|
|
4635
|
+
else if (parsed === 'usage') notify(t('notice.usage.animation'), 'info')
|
|
4075
4636
|
else applyAnimations(parsed.enabled)
|
|
4076
4637
|
return
|
|
4077
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
|
+
}
|
|
4078
4645
|
if (text === '/history') {
|
|
4079
4646
|
openHistory()
|
|
4080
4647
|
return
|
|
4081
4648
|
}
|
|
4649
|
+
if (text === '/queue') {
|
|
4650
|
+
openQueue()
|
|
4651
|
+
return
|
|
4652
|
+
}
|
|
4653
|
+
if (text === '/usage') {
|
|
4654
|
+
openUsage()
|
|
4655
|
+
return
|
|
4656
|
+
}
|
|
4082
4657
|
if (text === '/agents') {
|
|
4083
4658
|
openAgents()
|
|
4084
4659
|
return
|
|
@@ -4102,19 +4677,25 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
4102
4677
|
openDelete(text.slice(7).trim())
|
|
4103
4678
|
return
|
|
4104
4679
|
}
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
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'))
|
|
4113
4697
|
return
|
|
4114
4698
|
}
|
|
4115
|
-
// Ink exposes Ctrl+J as a bare LF and Alt+Enter as a bare CR after
|
|
4116
|
-
// stripping the leading escape. Neither is a multiline shortcut.
|
|
4117
|
-
if (input === '\n' || input === '\r') return
|
|
4118
4699
|
// A fast Tab followed by text can arrive as one readable chunk in an
|
|
4119
4700
|
// integrated terminal. Accept the candidate first, then apply the
|
|
4120
4701
|
// remaining characters against the synchronously updated editor refs.
|
|
@@ -4286,13 +4867,23 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
4286
4867
|
if (!animations && waveKey !== null && waveKey !== wavePlayedKey) setWavePlayedKey(waveKey)
|
|
4287
4868
|
}, [animations, waveKey, wavePlayedKey])
|
|
4288
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
|
|
4289
4876
|
|
|
4290
4877
|
// Every exclusive panel keeps the composer as a stable visual anchor, but
|
|
4291
4878
|
// freezes it to one row: no menu, multiline wrap, or animation.
|
|
4292
4879
|
const tierActive = waveTier !== null
|
|
4293
4880
|
const tierHues = waveTier === null ? null : deepseekWaveHues(waveTier)
|
|
4294
4881
|
const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0])
|
|
4295
|
-
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)
|
|
4296
4887
|
// The multiline editor model: the sanitized draft hard-wrapped into
|
|
4297
4888
|
// column-safe physical rows, with the caret mapped to its exact row and
|
|
4298
4889
|
// column. Computed before the frozen path so the row report below runs
|
|
@@ -4315,6 +4906,17 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
4315
4906
|
useEffect(() => {
|
|
4316
4907
|
onEditorRows(editorRowCount)
|
|
4317
4908
|
}, [editorRowCount, onEditorRows])
|
|
4909
|
+
// IME anchor: park the real terminal cursor on the caret cell while the
|
|
4910
|
+
// composer accepts input. IME composition and candidate windows anchor to
|
|
4911
|
+
// that real cursor cell, which otherwise sits below the status row where
|
|
4912
|
+
// Ink leaves it, so Chinese input never appears at the caret. Frozen bands
|
|
4913
|
+
// release the anchor; the wrapper keeps Ink's relative erase ledger exact.
|
|
4914
|
+
const caretRowInWindow = Math.max(0, Math.min(caret.row - editorWindowStart, editorWindowRows - 1))
|
|
4915
|
+
useImeCursorAnchor(
|
|
4916
|
+
!frozen,
|
|
4917
|
+
imeCursorRowsUp({ editorWindowRows, caretRowInWindow, rowsBelowComposer: anchorRowsBelow }),
|
|
4918
|
+
2 + caret.column,
|
|
4919
|
+
)
|
|
4318
4920
|
// The menu's physical rows ride the same one-way report; the cleanup keeps
|
|
4319
4921
|
// the reserve from outliving the menu (unmount or inactive handoff).
|
|
4320
4922
|
useEffect(() => {
|
|
@@ -4350,12 +4952,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
4350
4952
|
))
|
|
4351
4953
|
}
|
|
4352
4954
|
const frozenLine = value === ''
|
|
4353
|
-
? 'type a message'
|
|
4955
|
+
? frozenHint ?? 'type a message'
|
|
4354
4956
|
: verboseLine(value, Math.max(1, columns - 6))
|
|
4355
4957
|
return band(createElement(
|
|
4356
4958
|
Text,
|
|
4357
4959
|
{ backgroundColor: bandBg, wrap: 'truncate-end' },
|
|
4358
|
-
createElement(Text, { color: promptColor, bold: tierActive ? true : undefined }, busy ? '… ' : `${promptGlyph} `),
|
|
4960
|
+
createElement(Text, { color: promptColor, bold: tierActive ? true : undefined }, submitMode === 'steer' ? '↳ ' : busy ? '… ' : `${promptGlyph} `),
|
|
4359
4961
|
frozenLine,
|
|
4360
4962
|
bandFill(2 + visibleColumns(frozenLine)),
|
|
4361
4963
|
))
|
|
@@ -4373,10 +4975,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
4373
4975
|
// spacer or a second blink timer.
|
|
4374
4976
|
const editorRows: ReactElement[] = []
|
|
4375
4977
|
for (let index = editorWindowStart; index < Math.min(editorViewModel.rows.length, editorWindowStart + editorWindowRows); index += 1) {
|
|
4376
|
-
const row = editorViewModel.rows[index]
|
|
4978
|
+
const row = editorViewModel.rows[index]
|
|
4377
4979
|
const parts = editorRowParts(row, index, caret.row, clampedCursor, !preparingImages)
|
|
4378
4980
|
const placeholder = index === 0 && value === '' && !busy && !preparingImages
|
|
4379
|
-
const tail = placeholder ?
|
|
4981
|
+
const tail = placeholder ? placeholderText : parts.after
|
|
4380
4982
|
const consumed = 2 + visibleColumns(parts.before) + visibleColumns(parts.caret) + visibleColumns(tail)
|
|
4381
4983
|
editorRows.push(createElement(
|
|
4382
4984
|
Text,
|
|
@@ -4384,16 +4986,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
4384
4986
|
index === 0
|
|
4385
4987
|
? preparingImages
|
|
4386
4988
|
? createElement(Text, { color: inkColor(getPalette().warn), bold: true }, '… ')
|
|
4387
|
-
:
|
|
4388
|
-
? createElement(
|
|
4389
|
-
:
|
|
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} `)
|
|
4390
4994
|
: ' ',
|
|
4391
4995
|
parts.before,
|
|
4392
4996
|
parts.hasCaret
|
|
4393
4997
|
? createElement(Text, { key: 'caret', inverse: cursorVisible || undefined }, parts.caret)
|
|
4394
4998
|
: null,
|
|
4395
4999
|
placeholder
|
|
4396
|
-
? createElement(Text, { dimColor: true },
|
|
5000
|
+
? createElement(Text, { dimColor: true }, tail)
|
|
4397
5001
|
: parts.after,
|
|
4398
5002
|
bandFill(consumed),
|
|
4399
5003
|
))
|
|
@@ -4409,26 +5013,47 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
|
|
|
4409
5013
|
Box,
|
|
4410
5014
|
{ flexDirection: 'column' },
|
|
4411
5015
|
menu,
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
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
|
+
}),
|
|
4432
5057
|
)
|
|
4433
5058
|
}
|
|
4434
5059
|
|
|
@@ -4653,7 +5278,13 @@ export function computeSettledRows(
|
|
|
4653
5278
|
|
|
4654
5279
|
/** The whole terminal app; state arrives via the store, output via Ink. */
|
|
4655
5280
|
export function App(props: AppProps): ReactElement {
|
|
4656
|
-
|
|
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)
|
|
4657
5288
|
// Terminal input anchor: Ink reference-counts raw mode across every active
|
|
4658
5289
|
// `useInput` hook, so mutually exclusive surfaces (composer <-> approval
|
|
4659
5290
|
// bar <-> panels) drop the count to zero inside each handoff commit — the
|
|
@@ -4675,8 +5306,10 @@ export function App(props: AppProps): ReactElement {
|
|
|
4675
5306
|
// process-stable, so one callback per view identity is enough.
|
|
4676
5307
|
const readDescriptors = useCallback(() => props.commands.descriptors, [props.commands])
|
|
4677
5308
|
const readSkills = useCallback(() => props.skills.rows, [props.skills])
|
|
4678
|
-
const
|
|
4679
|
-
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)
|
|
4680
5313
|
const [modelLabel, setModelLabel] = useState(props.model)
|
|
4681
5314
|
const [modelOpen, setModelOpen] = useState(false)
|
|
4682
5315
|
/** Nested /model stages; only one owns terminal input at a time. */
|
|
@@ -4705,13 +5338,17 @@ export function App(props: AppProps): ReactElement {
|
|
|
4705
5338
|
* (ordinary turns, image preparation, /animation toggles) never replays. */
|
|
4706
5339
|
const [waveTier, setWaveTier] = useState<DeepseekWaveTier | null>(null)
|
|
4707
5340
|
const [waveStyle, setWaveStyle] = useState<DeepseekWaveStyle | null>(null)
|
|
5341
|
+
const [rainbowBurstId, setRainbowBurstId] = useState(0)
|
|
5342
|
+
const fireRainbowBurst = (): void => {
|
|
5343
|
+
setRainbowBurstId(id => id + 1)
|
|
5344
|
+
}
|
|
4708
5345
|
// /animation toggle: applies immediately, persists through the runner, and
|
|
4709
5346
|
// gates every timed leaf (shimmer, chase, blink, wave) for this render.
|
|
4710
5347
|
const [animations, setAnimations] = useState(props.animations ?? true)
|
|
4711
5348
|
const applyAnimations = (enabled: boolean): void => {
|
|
4712
5349
|
setAnimations(enabled)
|
|
4713
5350
|
props.saveAnimations?.(enabled)
|
|
4714
|
-
notify(
|
|
5351
|
+
notify(t('notice.animationState', { state: enabled ? 'on' : 'off' }))
|
|
4715
5352
|
}
|
|
4716
5353
|
const previousModel = useRef<string | undefined>(undefined)
|
|
4717
5354
|
const previousEffort = useRef<string | undefined>(props.effort)
|
|
@@ -4826,21 +5463,36 @@ export function App(props: AppProps): ReactElement {
|
|
|
4826
5463
|
// Dedupe for the dynamic-budget tripwire: one warning per distinct shape.
|
|
4827
5464
|
const budgetWarnRef = useRef<string | undefined>(undefined)
|
|
4828
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')
|
|
4829
5473
|
const [diffView, setDiffView] = useState<GitDiffView | undefined>(undefined)
|
|
5474
|
+
const [reviewPickerOpen, setReviewPickerOpen] = useState(false)
|
|
4830
5475
|
const [helpOpen, setHelpOpen] = useState(false)
|
|
4831
5476
|
const [modeOpen, setModeOpen] = useState(false)
|
|
4832
5477
|
const [permissionOpen, setPermissionOpen] = useState(false)
|
|
4833
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('')
|
|
4834
5482
|
const [pluginOpen, setPluginOpen] = useState(false)
|
|
4835
5483
|
const [pluginQuery, setPluginQuery] = useState('')
|
|
5484
|
+
const [updateOpen, setUpdateOpen] = useState(false)
|
|
5485
|
+
const [scheduleOpen, setScheduleOpen] = useState(false)
|
|
4836
5486
|
const [jobsOpen, setJobsOpen] = useState(false)
|
|
4837
5487
|
const [statuslineOpen, setStatuslineOpen] = useState(false)
|
|
4838
5488
|
const [statuslineItems, setStatuslineItems] = useState<readonly StatusItemId[]>(() => parseStatuslineItems(props.statusline))
|
|
4839
5489
|
const [themeOpen, setThemeOpen] = useState(false)
|
|
5490
|
+
const [languageOpen, setLanguageOpen] = useState(false)
|
|
4840
5491
|
const [historyOpen, setHistoryOpen] = useState(false)
|
|
4841
5492
|
const [agentsOpen, setAgentsOpen] = useState(false)
|
|
4842
5493
|
const [subagentOpen, setSubagentOpen] = useState(false)
|
|
4843
5494
|
const [todosOpen, setTodosOpen] = useState(false)
|
|
5495
|
+
const [usageOpen, setUsageOpen] = useState(false)
|
|
4844
5496
|
/** /delete state: delete-mode hint plus an optional pre-armed row id. */
|
|
4845
5497
|
const [resumeDelete, setResumeDelete] = useState<{ mode: boolean; id?: string }>({ mode: false })
|
|
4846
5498
|
/** The row id awaiting y/n in the COMPOSER (codex delete confirm): the
|
|
@@ -4864,7 +5516,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
4864
5516
|
// from the list immediately, not look like a no-op.
|
|
4865
5517
|
setDeleteReloadToken(token => token + 1)
|
|
4866
5518
|
}, (reason: unknown) => {
|
|
4867
|
-
notify(
|
|
5519
|
+
notify(t('notice.deleteFailed', { message: reason instanceof Error ? reason.message : String(reason) }), 'error')
|
|
4868
5520
|
})
|
|
4869
5521
|
}, [deleteConfirmId, props.deleteSession, notify])
|
|
4870
5522
|
/** The /history panel's accepted entry: text plus its recall-space index. */
|
|
@@ -4884,26 +5536,25 @@ export function App(props: AppProps): ReactElement {
|
|
|
4884
5536
|
}, [])
|
|
4885
5537
|
/** The append-only flush boundary (see `settledEntryCount`): entries below
|
|
4886
5538
|
* this index are final and ride the `<Static>` scrollback; everything at or
|
|
4887
|
-
* beyond stays in the live tree.
|
|
4888
|
-
* index >= settled, so the queued-inbox scan below only walks the mutable
|
|
4889
|
-
* tail instead of the whole history. */
|
|
5539
|
+
* beyond stays in the live tree. */
|
|
4890
5540
|
const settled = useMemo(() => settledEntryCount(view.entries), [view.entries])
|
|
4891
|
-
/**
|
|
4892
|
-
*
|
|
4893
|
-
*
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
const entry = view.entries[index]
|
|
4899
|
-
if (entry.kind === 'pending') rows.push(entry)
|
|
4900
|
-
}
|
|
4901
|
-
return rows
|
|
4902
|
-
}, [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
|
+
)
|
|
4903
5548
|
const [refreshEpoch, setRefreshEpoch] = useState(0)
|
|
4904
|
-
const
|
|
4905
|
-
const
|
|
4906
|
-
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)
|
|
4907
5558
|
const approvalPending = approvalSnapshot.pending !== undefined
|
|
4908
5559
|
const questionPending = questionSnapshot.pending !== undefined
|
|
4909
5560
|
// While any modal owns the keys, the prompt box passes everything through.
|
|
@@ -4912,7 +5563,8 @@ export function App(props: AppProps): ReactElement {
|
|
|
4912
5563
|
// panel keypress.
|
|
4913
5564
|
const inputActive = deleteConfirmId !== undefined
|
|
4914
5565
|
? !approvalPending && !questionPending
|
|
4915
|
-
: !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !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
|
|
4916
5568
|
|
|
4917
5569
|
// Human questions outrank local inspectors. Close the lower modal instead
|
|
4918
5570
|
// of leaving an approval/question visible but keyboard-locked behind it.
|
|
@@ -4927,9 +5579,12 @@ export function App(props: AppProps): ReactElement {
|
|
|
4927
5579
|
setPermissionOpen(false)
|
|
4928
5580
|
setResumeOpen(false)
|
|
4929
5581
|
setPluginOpen(false)
|
|
5582
|
+
setUpdateOpen(false)
|
|
5583
|
+
setScheduleOpen(false)
|
|
4930
5584
|
setStatuslineOpen(false)
|
|
4931
5585
|
setThemeOpen(false)
|
|
4932
5586
|
setHistoryOpen(false)
|
|
5587
|
+
setQueueOpen(false)
|
|
4933
5588
|
setAgentsOpen(false)
|
|
4934
5589
|
setSubagentOpen(false)
|
|
4935
5590
|
setTodosOpen(false)
|
|
@@ -4979,6 +5634,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
4979
5634
|
// One pending synchronized frame covers a debounced resize or explicit
|
|
4980
5635
|
// source-backed replay. It is closed after the corresponding React commit.
|
|
4981
5636
|
const synchronizedReplayPending = useRef(false)
|
|
5637
|
+
const resizeBurstHeld = useRef(false)
|
|
4982
5638
|
useEffect(() => {
|
|
4983
5639
|
if (appStdout === undefined) return
|
|
4984
5640
|
let replayTimer: ReturnType<typeof setTimeout> | undefined
|
|
@@ -4990,24 +5646,31 @@ export function App(props: AppProps): ReactElement {
|
|
|
4990
5646
|
if (next.columns === terminalSizeRef.current.columns && next.rows === terminalSizeRef.current.rows) return
|
|
4991
5647
|
terminalSizeRef.current = next
|
|
4992
5648
|
|
|
4993
|
-
//
|
|
4994
|
-
//
|
|
4995
|
-
//
|
|
4996
|
-
//
|
|
4997
|
-
|
|
4998
|
-
|
|
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
|
+
}
|
|
4999
5657
|
setTerminalSize(next)
|
|
5000
5658
|
if (replayTimer !== undefined) clearTimeout(replayTimer)
|
|
5001
5659
|
replayTimer = setTimeout(() => {
|
|
5002
5660
|
synchronizedReplayPending.current = true
|
|
5003
|
-
appStdout.write(
|
|
5661
|
+
appStdout.write(RESIZE_REFLOW_CLEAR)
|
|
5004
5662
|
setRefreshEpoch(epoch => epoch + 1)
|
|
5663
|
+
resizeBurstHeld.current = false
|
|
5005
5664
|
}, RESIZE_REFLOW_DELAY_MS)
|
|
5006
5665
|
}
|
|
5007
5666
|
appStdout.on('resize', handleResize)
|
|
5008
5667
|
return () => {
|
|
5009
5668
|
appStdout.off('resize', handleResize)
|
|
5010
5669
|
if (replayTimer !== undefined) clearTimeout(replayTimer)
|
|
5670
|
+
if (resizeBurstHeld.current) {
|
|
5671
|
+
appStdout.write(SYNCHRONIZED_UPDATE_END)
|
|
5672
|
+
resizeBurstHeld.current = false
|
|
5673
|
+
}
|
|
5011
5674
|
}
|
|
5012
5675
|
}, [appStdout])
|
|
5013
5676
|
const terminalRows = terminalSize.rows
|
|
@@ -5029,17 +5692,36 @@ export function App(props: AppProps): ReactElement {
|
|
|
5029
5692
|
const handleMenuRows = useCallback((rows: number): void => {
|
|
5030
5693
|
setMenuRows(current => (current === rows ? current : rows))
|
|
5031
5694
|
}, [])
|
|
5695
|
+
// The status footer's exact row count, reported one-way by StatusLine (the
|
|
5696
|
+
// second row renders only while it has content). The IME cursor anchor
|
|
5697
|
+
// counts every row between the composer caret and Ink's parked cursor: the
|
|
5698
|
+
// status footer plus Ink's own below-frame row. The gutter rows sit ABOVE
|
|
5699
|
+
// the composer and never enter this distance.
|
|
5700
|
+
const [statusBarRows, setStatusBarRows] = useState<1 | 2>(1)
|
|
5701
|
+
const handleStatusRows = useCallback((rows: 1 | 2): void => {
|
|
5702
|
+
setStatusBarRows(current => (current === rows ? current : rows))
|
|
5703
|
+
}, [])
|
|
5704
|
+
const imeRowsBelowComposer = statusBarRows + 1
|
|
5032
5705
|
const composerEditorCap = composerMaxRows(terminalRows)
|
|
5033
|
-
//
|
|
5034
|
-
//
|
|
5035
|
-
//
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
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
|
+
})
|
|
5041
5719
|
const streamingActive = view.streaming !== '' || view.streamingReasoning !== ''
|
|
5042
5720
|
const deepDivingVisible = busy && !streamingActive
|
|
5721
|
+
// Terminal tab label: "deepseek" until the session carries a name, then the
|
|
5722
|
+
// session title; cleared on unmount so the host shell regains its default.
|
|
5723
|
+
const tabTitle = view.title === '' ? DEFAULT_TERMINAL_TITLE : view.title
|
|
5724
|
+
useTerminalTitle(tabTitle)
|
|
5043
5725
|
const allLiveLines = useMemo(
|
|
5044
5726
|
() => view.entries.slice(settled).flatMap(
|
|
5045
5727
|
// Width shrinks with the real terminal (no 10-column floor: on a
|
|
@@ -5081,6 +5763,10 @@ export function App(props: AppProps): ReactElement {
|
|
|
5081
5763
|
)
|
|
5082
5764
|
if (liveAudit.warning !== undefined && budgetWarnRef.current !== liveAudit.warning) {
|
|
5083
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
|
|
5084
5770
|
console.warn(`[dsh-code] ${liveAudit.warning}`)
|
|
5085
5771
|
}
|
|
5086
5772
|
const auditedLiveLines = liveAudit.allocation.live === visibleLiveLines.length
|
|
@@ -5088,9 +5774,60 @@ export function App(props: AppProps): ReactElement {
|
|
|
5088
5774
|
: visibleLiveLines.slice(-liveAudit.allocation.live)
|
|
5089
5775
|
const auditedReasoningRows = liveAudit.allocation.reasoning
|
|
5090
5776
|
const auditedAnswerRows = liveAudit.allocation.answer
|
|
5091
|
-
const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !jobsOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !todosOpen && !verboseOpen && diffView === undefined && !approvalPending && !questionPending
|
|
5092
5777
|
const inspectorVisible = verboseOpen && !approvalPending && !questionPending
|
|
5093
|
-
const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || 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
|
|
5779
|
+
// The surface that currently owns the keyboard, named in the frozen band:
|
|
5780
|
+
// an empty composer under a panel must not advertise typing it cannot
|
|
5781
|
+
// accept — every key actually feeds the panel (which may or may not
|
|
5782
|
+
// filter with it), so the honest hint names the owner and the way out.
|
|
5783
|
+
const keyboardOwner = approvalPending
|
|
5784
|
+
? 'the approval prompt'
|
|
5785
|
+
: questionPending
|
|
5786
|
+
? 'the question'
|
|
5787
|
+
: diffView !== undefined
|
|
5788
|
+
? 'the diff review'
|
|
5789
|
+
: reviewPickerOpen
|
|
5790
|
+
? 'the review picker'
|
|
5791
|
+
: modelOpen
|
|
5792
|
+
? '/model'
|
|
5793
|
+
: helpOpen
|
|
5794
|
+
? '/help'
|
|
5795
|
+
: modeOpen
|
|
5796
|
+
? '/mode'
|
|
5797
|
+
: permissionOpen
|
|
5798
|
+
? '/permission'
|
|
5799
|
+
: resumeOpen
|
|
5800
|
+
? '/resume'
|
|
5801
|
+
: pluginOpen
|
|
5802
|
+
? '/plugin'
|
|
5803
|
+
: updateOpen
|
|
5804
|
+
? '/update'
|
|
5805
|
+
: scheduleOpen
|
|
5806
|
+
? '/schedule'
|
|
5807
|
+
: jobsOpen
|
|
5808
|
+
? '/jobs'
|
|
5809
|
+
: statuslineOpen
|
|
5810
|
+
? '/statusline'
|
|
5811
|
+
: themeOpen
|
|
5812
|
+
? '/theme'
|
|
5813
|
+
: languageOpen
|
|
5814
|
+
? '/language'
|
|
5815
|
+
: historyOpen
|
|
5816
|
+
? '/history'
|
|
5817
|
+
: agentsOpen
|
|
5818
|
+
? '/agents'
|
|
5819
|
+
: subagentOpen
|
|
5820
|
+
? '/subagent'
|
|
5821
|
+
: todosOpen
|
|
5822
|
+
? '/todos'
|
|
5823
|
+
: usageOpen
|
|
5824
|
+
? '/usage'
|
|
5825
|
+
: inspectorVisible
|
|
5826
|
+
? 'history details'
|
|
5827
|
+
: undefined
|
|
5828
|
+
const frozenHint = keyboardOwner === undefined
|
|
5829
|
+
? undefined
|
|
5830
|
+
: `keys go to ${keyboardOwner} · esc ${approvalPending ? 'rejects' : questionPending ? 'cancels' : 'closes'}`
|
|
5094
5831
|
const closeInspector = useCallback((): void => {
|
|
5095
5832
|
setVerboseOpen(false)
|
|
5096
5833
|
}, [])
|
|
@@ -5106,6 +5843,18 @@ export function App(props: AppProps): ReactElement {
|
|
|
5106
5843
|
}
|
|
5107
5844
|
setRefreshEpoch(epoch => epoch + 1)
|
|
5108
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
|
+
}
|
|
5109
5858
|
useEffect(() => {
|
|
5110
5859
|
if (!synchronizedReplayPending.current || appStdout === undefined) return
|
|
5111
5860
|
synchronizedReplayPending.current = false
|
|
@@ -5136,16 +5885,16 @@ export function App(props: AppProps): ReactElement {
|
|
|
5136
5885
|
setEffortLabel(effortId)
|
|
5137
5886
|
const selected = `${label}${effortId === undefined || effortId === '' ? '' : `@${effortId}`}`
|
|
5138
5887
|
if (sessionHasImages && row.inputModalities !== undefined && !row.inputModalities.includes('image')) {
|
|
5139
|
-
notify(
|
|
5888
|
+
notify(t('notice.modelChangedPlaceholder', { model: selected }), 'warning')
|
|
5140
5889
|
} else {
|
|
5141
|
-
notify(
|
|
5890
|
+
notify(t('notice.modelNextStep', { model: selected }))
|
|
5142
5891
|
}
|
|
5143
5892
|
setModelOpen(false)
|
|
5144
5893
|
setProviderOpen(false)
|
|
5145
5894
|
setProviderAction(undefined)
|
|
5146
5895
|
setEffortFor(undefined)
|
|
5147
5896
|
} catch (error: unknown) {
|
|
5148
|
-
notify(
|
|
5897
|
+
notify(t('notice.modelSwitchFailed', { message: error instanceof Error ? error.message : String(error) }), 'error')
|
|
5149
5898
|
}
|
|
5150
5899
|
}
|
|
5151
5900
|
|
|
@@ -5166,7 +5915,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5166
5915
|
const seen = new Set<string>()
|
|
5167
5916
|
for (const row of providerDirectory?.rows ?? []) {
|
|
5168
5917
|
for (const model of row.configuration.models) {
|
|
5169
|
-
const raw = (model.extras
|
|
5918
|
+
const raw = (model.extras)?.reasoningEfforts
|
|
5170
5919
|
if (!isDeclaredReasoningEfforts(raw)) continue
|
|
5171
5920
|
const key = row.provider + '/' + model.id
|
|
5172
5921
|
if (seen.has(key)) continue
|
|
@@ -5204,7 +5953,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5204
5953
|
setProviderAction(undefined)
|
|
5205
5954
|
setProviderOpen(false)
|
|
5206
5955
|
reloadModelSurfaces()
|
|
5207
|
-
notify(
|
|
5956
|
+
notify(t('notice.loggedIn', { provider: authorization.label }))
|
|
5208
5957
|
},
|
|
5209
5958
|
back: () => {
|
|
5210
5959
|
setProviderAction(undefined)
|
|
@@ -5220,7 +5969,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5220
5969
|
setProviderAction(undefined)
|
|
5221
5970
|
setProviderOpen(true)
|
|
5222
5971
|
reloadModelSurfaces()
|
|
5223
|
-
notify(
|
|
5972
|
+
notify(t('notice.loggedOut', { provider: authorization.label }))
|
|
5224
5973
|
},
|
|
5225
5974
|
back: () => setProviderAction(undefined),
|
|
5226
5975
|
})
|
|
@@ -5231,13 +5980,13 @@ export function App(props: AppProps): ReactElement {
|
|
|
5231
5980
|
save: props.saveModelProviderConfiguration,
|
|
5232
5981
|
saveCredential: props.saveModelProviderCredential,
|
|
5233
5982
|
discover: props.discoverModelProvider
|
|
5234
|
-
?? (
|
|
5983
|
+
?? (() => Promise.reject(new Error('model discovery is unavailable in this profile; enter models by hand'))),
|
|
5235
5984
|
done: result => {
|
|
5236
5985
|
const target = providerAction.target
|
|
5237
5986
|
setProviderAction(undefined)
|
|
5238
5987
|
setProviderOpen(true)
|
|
5239
5988
|
reloadModelSurfaces()
|
|
5240
|
-
notify(
|
|
5989
|
+
notify(t('notice.providerSaved', { provider: target.displayName, suffix: result.key ? ' · API key updated' : '' }))
|
|
5241
5990
|
},
|
|
5242
5991
|
back: () => setProviderAction(undefined),
|
|
5243
5992
|
onExit: closeModelSurface,
|
|
@@ -5252,7 +6001,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5252
6001
|
setProviderAction(undefined)
|
|
5253
6002
|
setProviderOpen(true)
|
|
5254
6003
|
reloadModelSurfaces()
|
|
5255
|
-
notify(
|
|
6004
|
+
notify(t('notice.apiKeyRemoved', { provider: target.displayName }))
|
|
5256
6005
|
},
|
|
5257
6006
|
back: () => setProviderAction(undefined),
|
|
5258
6007
|
})
|
|
@@ -5266,7 +6015,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5266
6015
|
setProviderAction(undefined)
|
|
5267
6016
|
setProviderOpen(true)
|
|
5268
6017
|
reloadModelSurfaces()
|
|
5269
|
-
notify(
|
|
6018
|
+
notify(t('notice.providerRemoved', { provider: target.displayName }))
|
|
5270
6019
|
},
|
|
5271
6020
|
back: () => setProviderAction(undefined),
|
|
5272
6021
|
})
|
|
@@ -5278,42 +6027,42 @@ export function App(props: AppProps): ReactElement {
|
|
|
5278
6027
|
authorizationError,
|
|
5279
6028
|
onConfigure: (target: ProviderTargetView) => {
|
|
5280
6029
|
if (props.saveModelProviderConfiguration === undefined) {
|
|
5281
|
-
notify('
|
|
6030
|
+
notify(t('notice.providerUnavailable'), 'warning')
|
|
5282
6031
|
return
|
|
5283
6032
|
}
|
|
5284
6033
|
setProviderAction({ kind: 'configure', target })
|
|
5285
6034
|
},
|
|
5286
6035
|
onUnset: (target: ProviderTargetView) => {
|
|
5287
6036
|
if (props.unsetModelProviderCredential === undefined) {
|
|
5288
|
-
notify('
|
|
6037
|
+
notify(t('notice.apiKeyUnavailable'), 'warning')
|
|
5289
6038
|
return
|
|
5290
6039
|
}
|
|
5291
6040
|
setProviderAction({ kind: 'unset', target })
|
|
5292
6041
|
},
|
|
5293
6042
|
onRemove: (target: ProviderTargetView) => {
|
|
5294
6043
|
if (props.removeModelProvider === undefined) {
|
|
5295
|
-
notify('
|
|
6044
|
+
notify(t('notice.providerRemovalUnavailable'), 'warning')
|
|
5296
6045
|
return
|
|
5297
6046
|
}
|
|
5298
6047
|
setProviderAction({ kind: 'remove', target })
|
|
5299
6048
|
},
|
|
5300
6049
|
onLogin: (target: ProviderTargetView, authorization: ProviderAuthorizationRow) => {
|
|
5301
6050
|
if (busy) {
|
|
5302
|
-
notify('
|
|
6051
|
+
notify(t('notice.loginIdleOnly'), 'warning')
|
|
5303
6052
|
return
|
|
5304
6053
|
}
|
|
5305
6054
|
if (props.beginProviderAuthorization === undefined
|
|
5306
6055
|
|| props.cancelProviderAuthorization === undefined
|
|
5307
6056
|
|| props.openAuthorizationUrl === undefined
|
|
5308
6057
|
|| props.copyTextValue === undefined) {
|
|
5309
|
-
notify('
|
|
6058
|
+
notify(t('notice.loginUnavailable'), 'warning')
|
|
5310
6059
|
return
|
|
5311
6060
|
}
|
|
5312
6061
|
setProviderAction({ kind: 'login', target, authorization })
|
|
5313
6062
|
},
|
|
5314
6063
|
onLogout: (target: ProviderTargetView, authorization: ProviderAuthorizationRow) => {
|
|
5315
6064
|
if (props.logoutProviderAuthorization === undefined) {
|
|
5316
|
-
notify('
|
|
6065
|
+
notify(t('notice.logoutUnavailable'), 'warning')
|
|
5317
6066
|
return
|
|
5318
6067
|
}
|
|
5319
6068
|
setProviderAction({ kind: 'logout', target, authorization })
|
|
@@ -5346,7 +6095,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5346
6095
|
setEffortFor(row)
|
|
5347
6096
|
return
|
|
5348
6097
|
}
|
|
5349
|
-
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
|
|
5350
6099
|
applyModel(row, effortId)
|
|
5351
6100
|
},
|
|
5352
6101
|
...(props.loadModelProviders === undefined || props.saveModelProviderConfiguration === undefined
|
|
@@ -5413,6 +6162,13 @@ export function App(props: AppProps): ReactElement {
|
|
|
5413
6162
|
: undefined,
|
|
5414
6163
|
transcriptVisible ? createElement(TodoPanel, { todos: view.todos }) : undefined,
|
|
5415
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,
|
|
5416
6172
|
todosOpen && !approvalPending && !questionPending
|
|
5417
6173
|
? createElement(MemoTodoListPanel, {
|
|
5418
6174
|
todos: view.todos,
|
|
@@ -5421,6 +6177,14 @@ export function App(props: AppProps): ReactElement {
|
|
|
5421
6177
|
},
|
|
5422
6178
|
})
|
|
5423
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,
|
|
5424
6188
|
createElement(QuestionBar, { store: props.questions, snapshot: questionSnapshot, locked: false }),
|
|
5425
6189
|
createElement(ApprovalBar, { snapshot: approvalSnapshot, locked: questionPending, notify, interrupt: props.interrupt, summarize: questionPending }),
|
|
5426
6190
|
modelSurface,
|
|
@@ -5441,6 +6205,17 @@ export function App(props: AppProps): ReactElement {
|
|
|
5441
6205
|
onClose: () => setDiffView(undefined),
|
|
5442
6206
|
})
|
|
5443
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,
|
|
5444
6219
|
verboseOpen && !approvalPending && !questionPending
|
|
5445
6220
|
? createElement(MemoVerbosePanel, {
|
|
5446
6221
|
entries: view.entries,
|
|
@@ -5453,9 +6228,9 @@ export function App(props: AppProps): ReactElement {
|
|
|
5453
6228
|
load: props.loadPresets,
|
|
5454
6229
|
select: (id: string) => {
|
|
5455
6230
|
void props.switchMode(id).then(label => {
|
|
5456
|
-
notify(
|
|
6231
|
+
notify(t('notice.modeChangedSimple', { value: label }))
|
|
5457
6232
|
setModeOpen(false)
|
|
5458
|
-
}, (reason: unknown) => notify(
|
|
6233
|
+
}, (reason: unknown) => notify(t('notice.modeSwitchFailed', { message: reason instanceof Error ? reason.message : String(reason) }), 'error'))
|
|
5459
6234
|
},
|
|
5460
6235
|
close: () => setModeOpen(false),
|
|
5461
6236
|
})
|
|
@@ -5467,7 +6242,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5467
6242
|
select: (id: string) => {
|
|
5468
6243
|
try {
|
|
5469
6244
|
const selected = props.setPermission(id)
|
|
5470
|
-
notify(
|
|
6245
|
+
notify(t('notice.permissionChangedSimple', { value: selected }))
|
|
5471
6246
|
setPermissionOpen(false)
|
|
5472
6247
|
} catch (reason: unknown) {
|
|
5473
6248
|
notify(`permission change failed: ${reason instanceof Error ? reason.message : String(reason)}`, 'error')
|
|
@@ -5489,9 +6264,42 @@ export function App(props: AppProps): ReactElement {
|
|
|
5489
6264
|
close: () => setResumeOpen(false),
|
|
5490
6265
|
})
|
|
5491
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,
|
|
5492
6289
|
pluginOpen && !approvalPending && !questionPending
|
|
5493
6290
|
? createElement(PluginPanel, { load: props.loadPlugins, initialQuery: pluginQuery, close: () => setPluginOpen(false) })
|
|
5494
6291
|
: undefined,
|
|
6292
|
+
updateOpen && !approvalPending && !questionPending
|
|
6293
|
+
? createElement(UpdatePanel, {
|
|
6294
|
+
probe: props.probeUpdate,
|
|
6295
|
+
apply: props.applyUpdate,
|
|
6296
|
+
notify: (text: string, tone?: NoticeTone) => notify(text, tone),
|
|
6297
|
+
close: () => setUpdateOpen(false),
|
|
6298
|
+
})
|
|
6299
|
+
: undefined,
|
|
6300
|
+
scheduleOpen && !approvalPending && !questionPending
|
|
6301
|
+
? createElement(SchedulePanel, { rows: () => view.schedules, close: () => setScheduleOpen(false) })
|
|
6302
|
+
: undefined,
|
|
5495
6303
|
jobsOpen && !approvalPending && !questionPending
|
|
5496
6304
|
? createElement(JobsPanel, { load: props.loadJobs, close: () => setJobsOpen(false) })
|
|
5497
6305
|
: undefined,
|
|
@@ -5514,12 +6322,36 @@ export function App(props: AppProps): ReactElement {
|
|
|
5514
6322
|
// palette. `auto` stores as requested; detection is a later step.
|
|
5515
6323
|
setTheme(name)
|
|
5516
6324
|
props.saveTheme?.(name)
|
|
5517
|
-
|
|
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 }))
|
|
5518
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()
|
|
5519
6337
|
},
|
|
5520
6338
|
close: () => setThemeOpen(false),
|
|
5521
6339
|
})
|
|
5522
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,
|
|
5523
6355
|
historyOpen && !approvalPending && !questionPending
|
|
5524
6356
|
? createElement(HistoryPanel, {
|
|
5525
6357
|
entries: recallSpace,
|
|
@@ -5547,15 +6379,15 @@ export function App(props: AppProps): ReactElement {
|
|
|
5547
6379
|
// The runner's label already carries the effort suffix
|
|
5548
6380
|
// (`provider/model@effort`), so no second append here.
|
|
5549
6381
|
const label = props.setSubagentModel(row, effortId)
|
|
5550
|
-
notify(
|
|
6382
|
+
notify(t('notice.subagentsChanged', { value: label }))
|
|
5551
6383
|
setSubagentOpen(false)
|
|
5552
6384
|
} catch (reason: unknown) {
|
|
5553
|
-
notify(
|
|
6385
|
+
notify(t('notice.subagentChangeFailed', { message: reason instanceof Error ? reason.message : String(reason) }), 'error')
|
|
5554
6386
|
}
|
|
5555
6387
|
},
|
|
5556
6388
|
inherit: () => {
|
|
5557
6389
|
props.clearSubagentModel()
|
|
5558
|
-
notify('
|
|
6390
|
+
notify(t('notice.subagentsInherited'))
|
|
5559
6391
|
setSubagentOpen(false)
|
|
5560
6392
|
},
|
|
5561
6393
|
close: () => setSubagentOpen(false),
|
|
@@ -5577,12 +6409,19 @@ export function App(props: AppProps): ReactElement {
|
|
|
5577
6409
|
createElement(Input, {
|
|
5578
6410
|
active: inputActive,
|
|
5579
6411
|
frozen: modalVisible,
|
|
6412
|
+
frozenHint,
|
|
5580
6413
|
busy,
|
|
5581
6414
|
descriptors,
|
|
5582
6415
|
skills,
|
|
5583
6416
|
dispatch: props.dispatch,
|
|
5584
|
-
applyEditorKeys: props.applyEditorKeys,
|
|
5585
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,
|
|
5586
6425
|
interrupt: props.interrupt,
|
|
5587
6426
|
quit: props.quit,
|
|
5588
6427
|
openModel: () => {
|
|
@@ -5614,12 +6453,12 @@ export function App(props: AppProps): ReactElement {
|
|
|
5614
6453
|
?? loaded.rows.find(candidate => candidate.model === model && candidate.reasoning !== undefined)
|
|
5615
6454
|
?? loaded.rows.find(candidate => candidate.model === model)
|
|
5616
6455
|
if (row === undefined) {
|
|
5617
|
-
notify('
|
|
6456
|
+
notify(t('notice.modelMissing'), 'warning')
|
|
5618
6457
|
return
|
|
5619
6458
|
}
|
|
5620
6459
|
const rowTag = `${row.provider}/${row.model}`
|
|
5621
6460
|
if (loaded.reasoningFailures?.includes(rowTag) === true) {
|
|
5622
|
-
notify('
|
|
6461
|
+
notify(t('notice.effortUnavailable'), 'warning')
|
|
5623
6462
|
return
|
|
5624
6463
|
}
|
|
5625
6464
|
if (row.reasoning === undefined || row.reasoning.efforts.length === 0) {
|
|
@@ -5643,14 +6482,28 @@ export function App(props: AppProps): ReactElement {
|
|
|
5643
6482
|
openMode: () => setModeOpen(true),
|
|
5644
6483
|
openPermission: () => setPermissionOpen(true),
|
|
5645
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
|
+
},
|
|
5646
6493
|
openPlugin: (query = '') => { setPluginQuery(query); setPluginOpen(true) },
|
|
6494
|
+
openUpdate: () => setUpdateOpen(true),
|
|
6495
|
+
openSchedule: () => setScheduleOpen(true),
|
|
5647
6496
|
openJobs: () => setJobsOpen(true),
|
|
5648
6497
|
openStatusline: () => setStatuslineOpen(true),
|
|
5649
6498
|
openTheme: () => setThemeOpen(true),
|
|
6499
|
+
openLanguage: () => setLanguageOpen(true),
|
|
6500
|
+
saveLanguage: props.saveLanguage,
|
|
5650
6501
|
openHistory: () => setHistoryOpen(true),
|
|
6502
|
+
openQueue: () => setQueueOpen(true),
|
|
5651
6503
|
openAgents: () => setAgentsOpen(true),
|
|
5652
6504
|
openSubagent: () => setSubagentOpen(true),
|
|
5653
6505
|
openTodos: () => setTodosOpen(true),
|
|
6506
|
+
openUsage: () => setUsageOpen(true),
|
|
5654
6507
|
openDelete: (id?: string) => {
|
|
5655
6508
|
const armed = id === undefined || id === '' ? undefined : id
|
|
5656
6509
|
setResumeDelete({ mode: true, ...armed === undefined ? {} : { id: armed } })
|
|
@@ -5663,6 +6516,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5663
6516
|
})
|
|
5664
6517
|
},
|
|
5665
6518
|
reviewChanges: props.reviewChanges,
|
|
6519
|
+
openReviewPicker: () => setReviewPickerOpen(true),
|
|
5666
6520
|
deleteConfirm: deleteConfirmId,
|
|
5667
6521
|
confirmDelete,
|
|
5668
6522
|
cancelDelete,
|
|
@@ -5698,7 +6552,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
5698
6552
|
inspectFiles: props.inspectFiles,
|
|
5699
6553
|
prepareFiles: props.prepareFiles,
|
|
5700
6554
|
sessionKey: props.sessionKey,
|
|
5701
|
-
|
|
6555
|
+
cycleMode: props.cycleMode,
|
|
5702
6556
|
exportTranscript: props.exportTranscript,
|
|
5703
6557
|
renameTitle: props.renameTitle,
|
|
5704
6558
|
copyLastResponse: props.copyLastResponse,
|
|
@@ -5706,14 +6560,18 @@ export function App(props: AppProps): ReactElement {
|
|
|
5706
6560
|
recordLocal,
|
|
5707
6561
|
recordHistory: props.recordHistory,
|
|
5708
6562
|
queued: queuedRows,
|
|
5709
|
-
|
|
6563
|
+
updateQueued: props.updateQueued,
|
|
5710
6564
|
historyFill,
|
|
5711
6565
|
historyConsumed,
|
|
5712
6566
|
animations,
|
|
5713
6567
|
applyAnimations,
|
|
6568
|
+
applyRainbow,
|
|
6569
|
+
rainbowBurstId,
|
|
5714
6570
|
waveTier,
|
|
5715
6571
|
waveStyle,
|
|
5716
6572
|
maxRows: composerEditorCap,
|
|
6573
|
+
anchorRowsBelow: imeRowsBelowComposer,
|
|
6574
|
+
tabTitle,
|
|
5717
6575
|
onEditorRows: handleEditorRows,
|
|
5718
6576
|
onMenuRows: handleMenuRows,
|
|
5719
6577
|
}),
|
|
@@ -5725,15 +6583,17 @@ export function App(props: AppProps): ReactElement {
|
|
|
5725
6583
|
branch: props.branch,
|
|
5726
6584
|
sessionId: props.sessionId,
|
|
5727
6585
|
title: view.title,
|
|
5728
|
-
plan: view.plan,
|
|
6586
|
+
plan: view.plan || props.pendingPlan === true,
|
|
5729
6587
|
permission: view.permission !== '' ? view.permission : props.permission,
|
|
5730
6588
|
sandbox: view.sandbox,
|
|
5731
6589
|
goal: view.goal === undefined ? undefined : { phase: view.goal.phase, rounds: view.goal.rounds, max: view.goal.max },
|
|
5732
6590
|
},
|
|
5733
6591
|
stats: view.stats,
|
|
5734
6592
|
busy,
|
|
6593
|
+
animated: animations,
|
|
5735
6594
|
columns: terminalColumns,
|
|
5736
6595
|
items: statuslineItems,
|
|
6596
|
+
onRows: handleStatusRows,
|
|
5737
6597
|
}),
|
|
5738
6598
|
),
|
|
5739
6599
|
)
|