dsh-code 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  /** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
2
2
 
3
- import { createElement, useEffect, useMemo, useRef, useState, type ReactElement } from 'react'
3
+ import { createElement, useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from 'react'
4
4
  import { Box, Text, useInput, useStdout } from 'ink'
5
5
  import type { ModelDirectory, ModelRow } from './models.ts'
6
6
  import type { SubagentRow } from './subagents.ts'
@@ -9,14 +9,14 @@ import type { PermissionRow } from './permissions.ts'
9
9
  import type { PresetRow } from './presets.ts'
10
10
  import type { PluginRow } from './plugin-inventory.ts'
11
11
  import type { SessionDirectoryOptions, SessionRow } from './session-directory.ts'
12
- import { formatRelativeTime } from './session-directory.ts'
12
+ import { formatRelativeTime, matchSessionRow } from './session-directory.ts'
13
13
  import type { ReviewBranch, ReviewCommit, ReviewSelection } from './git-workflow.ts'
14
14
  import { panelViewport, revealRow } from './render/inspector.ts'
15
15
  import { markdownLines, textLines, type LineStyle, type StyledLine } from './render/lines.ts'
16
16
  import { usageLines, type UsageView } from './render/usage.ts'
17
17
  import { deleteLastGrapheme } from './render/editor.ts'
18
18
  import { stripPasteMarkers } from './keyboard.ts'
19
- import { DEFAULT_STATUSLINE_ITEMS, STATUS_ITEMS, type StatusItemId } from './render/status.ts'
19
+ import { DEFAULT_STATUSLINE_ITEMS, localizedStatusItems, type StatusItemId } from './render/status.ts'
20
20
  import { singleLineText, truncateColumns } from './render/text.ts'
21
21
  import { panelAccent } from './panel-accent.ts'
22
22
  import { t } from './i18n.ts'
@@ -119,13 +119,13 @@ export function ModePanel({ current, load, select, close }: {
119
119
  const [cursor, setCursor] = useState(0)
120
120
  const [loading, setLoading] = useState(true)
121
121
  const [error, setError] = useState<string>()
122
- const refresh = (): void => {
122
+ const refresh = useCallback((): void => {
123
123
  setLoading(true); setError(undefined)
124
124
  Promise.resolve().then(load).then(value => { setRows(value); setLoading(false) }, reason => {
125
125
  setError(reason instanceof Error ? reason.message : String(reason)); setLoading(false)
126
126
  })
127
- }
128
- useEffect(refresh, [])
127
+ }, [load])
128
+ useEffect(refresh, [refresh])
129
129
  const visible = useMemo(() => rows.filter(row => `${row.id} ${row.name ?? ''} ${row.description ?? ''}`.toLowerCase().includes(query.toLowerCase())), [rows, query])
130
130
  useEffect(() => setCursor(value => Math.min(value, Math.max(0, visible.length - 1))), [visible.length])
131
131
  useInput((input, key) => {
@@ -160,13 +160,13 @@ export function PermissionPanel({ current, load, select, close }: {
160
160
  const [cursor, setCursor] = useState(0)
161
161
  const [loading, setLoading] = useState(true)
162
162
  const [error, setError] = useState<string>()
163
- const refresh = (): void => {
163
+ const refresh = useCallback((): void => {
164
164
  setLoading(true); setError(undefined)
165
165
  Promise.resolve().then(load).then(value => { setRows(value); setLoading(false) }, reason => {
166
166
  setError(reason instanceof Error ? reason.message : String(reason)); setLoading(false)
167
167
  })
168
- }
169
- useEffect(refresh, [])
168
+ }, [load])
169
+ useEffect(refresh, [refresh])
170
170
  const visible = useMemo(() => rows.filter(row => `${row.id} ${row.description ?? ''}`.toLowerCase().includes(query.toLowerCase())), [rows, query])
171
171
  useEffect(() => setCursor(value => Math.min(value, Math.max(0, visible.length - 1))), [visible.length])
172
172
  useInput((input, key) => {
@@ -192,7 +192,8 @@ export function PluginPanel({ load, close, initialQuery = '' }: { load: () => re
192
192
  const [query, setQuery] = useState(initialQuery)
193
193
  const [cursor, setCursor] = useState(0)
194
194
  const [expanded, setExpanded] = useState(false)
195
- const rows = useMemo(() => load().filter(row => `${row.entryId} ${row.moduleName} ${row.phase ?? ''}`.toLowerCase().includes(query.toLowerCase())), [epoch, query])
195
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- epoch is the panel's explicit registry refresh trigger
196
+ const rows = useMemo(() => load().filter(row => `${row.entryId} ${row.moduleName} ${row.phase ?? ''}`.toLowerCase().includes(query.toLowerCase())), [epoch, load, query])
196
197
  useEffect(() => setCursor(value => Math.min(value, Math.max(0, rows.length - 1))), [rows.length])
197
198
  useInput((input, key) => {
198
199
  if (key.escape) return close()
@@ -283,7 +284,7 @@ export function JobsPanel({ load, close }: { load: () => readonly JobRow[]; clos
283
284
  })
284
285
  }
285
286
 
286
- export function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete, deleteConfirmId, reloadToken = 0, deleteMode = false, close }: {
287
+ export function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete, deleteConfirmId, reloadToken = 0, deleteMode = false, presetId, close }: {
287
288
  currentCwd: string
288
289
  load: (options: SessionDirectoryOptions, signal?: AbortSignal) => Promise<readonly SessionRow[]>
289
290
  readTranscript: (id: string, signal?: AbortSignal) => Promise<string>
@@ -296,12 +297,20 @@ export function ResumePanel({ currentCwd, load, readTranscript, select, requestD
296
297
  reloadToken?: number
297
298
  /** Opened via /delete: hint-first delete mode. */
298
299
  deleteMode?: boolean
300
+ /** `/delete <id>` argument to resolve after the listing loads. */
301
+ presetId?: string
299
302
  close: () => void
300
303
  }): ReactElement {
301
304
  // Codex resume-picker default: the CURRENT directory's root sessions; the
302
305
  // cwd filter widens to all only on request (the old default leaked every
303
306
  // directory's sessions into what read as a current-directory view).
304
- const [options, setOptions] = useState<SessionDirectoryOptions>({ sessions: 'roots', cwd: 'current', sort: 'newest', currentCwd, query: '' })
307
+ const [options, setOptions] = useState<SessionDirectoryOptions>({
308
+ sessions: presetId === undefined || presetId === '' ? 'roots' : 'all',
309
+ cwd: presetId === undefined || presetId === '' ? 'current' : 'all',
310
+ sort: 'newest',
311
+ currentCwd,
312
+ query: '',
313
+ })
305
314
  const [focus, setFocus] = useState(0)
306
315
  const [density, setDensity] = useState<'comfortable' | 'dense'>('comfortable')
307
316
  const [rows, setRows] = useState<readonly SessionRow[]>([])
@@ -313,6 +322,7 @@ export function ResumePanel({ currentCwd, load, readTranscript, select, requestD
313
322
  /** Ctrl+F-gated search: typing filters only while searching (codex). */
314
323
  const [searching, setSearching] = useState(false)
315
324
  /** Reference clock pinned per row render, so relative times never drift mid-list. */
325
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally re-pin when the listing or its options change
316
326
  const now = useMemo(() => Date.now(), [rows, options])
317
327
  const transcriptLoad = useRef<AbortController>()
318
328
  useEffect(() => () => transcriptLoad.current?.abort(), [])
@@ -325,8 +335,22 @@ export function ResumePanel({ currentCwd, load, readTranscript, select, requestD
325
335
  if (!controller.signal.aborted) { setError(reason instanceof Error ? reason.message : String(reason)); setLoading(false) }
326
336
  })
327
337
  return () => controller.abort()
328
- }, [options, reloadToken])
338
+ }, [load, options, reloadToken])
329
339
  useEffect(() => setCursor(value => Math.min(value, Math.max(0, rows.length - 1))), [rows.length])
340
+ const presetArmed = useRef(false)
341
+ useEffect(() => {
342
+ if (presetArmed.current || presetId === undefined || presetId === '' || rows.length === 0 || requestDelete === undefined) return
343
+ try {
344
+ const row = matchSessionRow(rows, presetId)
345
+ presetArmed.current = true
346
+ const index = rows.findIndex(candidate => candidate.id === row.id)
347
+ if (index >= 0) setCursor(index)
348
+ requestDelete(row)
349
+ } catch (reason: unknown) {
350
+ presetArmed.current = true
351
+ setError(reason instanceof Error ? reason.message : String(reason))
352
+ }
353
+ }, [rows, presetId, requestDelete])
330
354
  const cycle = (): void => {
331
355
  if (focus === 3) {
332
356
  setDensity(current => current === 'comfortable' ? 'dense' : 'comfortable')
@@ -363,7 +387,6 @@ export function ResumePanel({ currentCwd, load, readTranscript, select, requestD
363
387
  if (key.pageDown) return setCursor(value => Math.min(rows.length - 1, value + 8))
364
388
  if (input === 'g') return setCursor(0)
365
389
  if (input === 'G') return setCursor(Math.max(0, rows.length - 1))
366
- if (input === 'd' && rows[cursor] !== undefined && requestDelete !== undefined) return requestDelete(rows[cursor])
367
390
  if (input === 'e' && rows[cursor] !== undefined) {
368
391
  return setExpanded(value => value === rows[cursor].id ? undefined : rows[cursor].id)
369
392
  }
@@ -379,7 +402,13 @@ export function ResumePanel({ currentCwd, load, readTranscript, select, requestD
379
402
  )
380
403
  return
381
404
  }
382
- if (key.return && rows[cursor]?.resumable === true) return select(rows[cursor])
405
+ if (key.return && rows[cursor] !== undefined) {
406
+ if (deleteMode) {
407
+ if (requestDelete !== undefined && !rows[cursor].live) requestDelete(rows[cursor])
408
+ return
409
+ }
410
+ if (rows[cursor].resumable) select(rows[cursor])
411
+ }
383
412
  }, { isActive: transcript === undefined })
384
413
  if (transcript !== undefined) {
385
414
  return createElement(DocumentPanel, {
@@ -392,15 +421,17 @@ export function ResumePanel({ currentCwd, load, readTranscript, select, requestD
392
421
  const pendingRow = deleteConfirmId === undefined ? undefined : rows.find(row => row.id === deleteConfirmId)
393
422
  const toolbar = `[${focus === 0 ? '>' : ''}${options.sessions}] [${focus === 1 ? '>' : ''}${options.cwd} cwd] [${focus === 2 ? '>' : ''}${options.sort}] [${focus === 3 ? '>' : ''}${density}]`
394
423
  return createElement(ListFrame, {
395
- title: deleteConfirmId === undefined
396
- ? `/resume${deleteMode ? ' delete mode' : ''}${searching ? ' searching' : ''} · ${toolbar}`
397
- : `permanently delete ${pendingRow === undefined ? deleteConfirmId.slice(-12) : pendingRow.title ?? pendingRow.id}? this cannot be undone · subagent threads go too`,
424
+ title: deleteConfirmId !== undefined
425
+ ? t('panel.resume.deleteTitle', { target: pendingRow === undefined ? deleteConfirmId.slice(-12) : pendingRow.title ?? pendingRow.id })
426
+ : deleteMode
427
+ ? t('panel.delete.title', { search: searching ? ` — ${t('panel.searching')}` : '', toolbar })
428
+ : t('panel.resume.title', { mode: '', search: searching ? ` — ${t('panel.searching')}` : '', toolbar }),
398
429
  rows: rows.map(row => ({
399
430
  key: row.id,
400
- disabled: !row.resumable,
431
+ disabled: deleteMode ? row.live : !row.resumable,
401
432
  text: `${row.subagent ? '↳' : '○'} ${row.title ?? row.id.slice(-12)}${density === 'comfortable' ? ` · ${formatRelativeTime(row.updatedAt ?? row.createdAt, now)} · ${row.workspace} · ${row.preset}` : ''}${row.live ? ' · live' : ''}${expanded === row.id ? ` · ${row.id} · ${row.cwd}${row.parent === undefined ? '' : ` · parent ${row.parent}`}` : ''}`,
402
433
  })), cursor, loading, error, query: options.query, searching,
403
- footer: t('panel.footer.resume'),
434
+ footer: t(deleteMode ? 'panel.footer.delete' : 'panel.footer.resume'),
404
435
  })
405
436
  }
406
437
 
@@ -580,6 +611,7 @@ export function ReviewPickerPanel({ loadBranches, loadCommits, choose, close }:
580
611
  const [loading, setLoading] = useState(false)
581
612
  const [error, setError] = useState<string>()
582
613
  const loadRef = useRef<AbortController>()
614
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally re-pin when review rows change
583
615
  const now = useMemo(() => Date.now(), [rows])
584
616
 
585
617
  useEffect(() => {
@@ -815,6 +847,7 @@ export function SearchPanel({ load, select, initialQuery = '', close }: {
815
847
  })
816
848
  const stdout = useStdout().stdout
817
849
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
850
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally re-pin when a search result set lands
818
851
  const now = useMemo(() => Date.now(), [rows, searched])
819
852
  if (viewport.maxHeight === 0 || viewport.compact) {
820
853
  const state = loading ? 'searching…' : error !== undefined ? `error: ${error}` : rows.length === 0 ? 'no results yet' : `❯ ${rows[cursor]?.label ?? ''}`
@@ -929,7 +962,7 @@ export function StatuslinePanel({ enabled, change, close }: {
929
962
  const bodyRows = Math.max(1, viewport.bodyRows - 1)
930
963
  const offset = revealRow(0, cursor, order.length, bodyRows)
931
964
  const visible = order.slice(offset, offset + bodyRows)
932
- const meta = new Map(STATUS_ITEMS.map(item => [item.id, item]))
965
+ const meta = new Map(localizedStatusItems().map(item => [item.id, item]))
933
966
  const accent = panelAccent('statusline', getPalette().dim, getPalette().brandBright)
934
967
  return createElement(
935
968
  Box,
@@ -1075,7 +1108,7 @@ export function AgentsPanel({ live, load, readTranscript, close }: {
1075
1108
  const [transcript, setTranscript] = useState<{ id: string; text?: string; error?: string }>()
1076
1109
  const transcriptLoad = useRef<AbortController>()
1077
1110
  useEffect(() => () => transcriptLoad.current?.abort(), [])
1078
- const refresh = (): void => {
1111
+ const refresh = useCallback((): void => {
1079
1112
  setLoading(true)
1080
1113
  setError(undefined)
1081
1114
  Promise.resolve().then(load).then(value => {
@@ -1085,8 +1118,8 @@ export function AgentsPanel({ live, load, readTranscript, close }: {
1085
1118
  setError(reason instanceof Error ? reason.message : String(reason))
1086
1119
  setLoading(false)
1087
1120
  })
1088
- }
1089
- useEffect(refresh, [])
1121
+ }, [load])
1122
+ useEffect(refresh, [refresh])
1090
1123
  // Live feed rows first (they carry the running state), then persisted
1091
1124
  // children only the directory knows — settled subagents from earlier turns.
1092
1125
  const rows = useMemo<readonly AgentsEntry[]>(() => {
@@ -1187,7 +1220,7 @@ export function SubagentPanel({ current, load, pick, inherit, close }: {
1187
1220
  const [loading, setLoading] = useState(true)
1188
1221
  const [cursor, setCursor] = useState(0)
1189
1222
  const [effortFor, setEffortFor] = useState<ModelRow | undefined>(undefined)
1190
- const refresh = (): void => {
1223
+ const refresh = useCallback((): void => {
1191
1224
  setLoading(true)
1192
1225
  setError(undefined)
1193
1226
  Promise.resolve().then(load).then(value => {
@@ -1197,8 +1230,8 @@ export function SubagentPanel({ current, load, pick, inherit, close }: {
1197
1230
  setError(reason instanceof Error ? reason.message : String(reason))
1198
1231
  setLoading(false)
1199
1232
  })
1200
- }
1201
- useEffect(refresh, [])
1233
+ }, [load])
1234
+ useEffect(refresh, [refresh])
1202
1235
  const rows = useMemo(() => directory?.rows ?? [], [directory])
1203
1236
  // The list opens on the override's own row (index 0 is the inherit row).
1204
1237
  useEffect(() => {
package/src/locales/en.ts CHANGED
@@ -63,7 +63,38 @@ export const en = {
63
63
  'search.footer': '↑↓ move · enter search/resume · esc close',
64
64
 
65
65
  // /statusline panel
66
+ 'header.hint': '/help · Esc interrupt · Ctrl+C quit',
67
+ 'header.hintResumed': 'resumed · /help · Esc interrupt',
68
+
66
69
  'statusline.title': '/statusline · items apply to the live status line below',
70
+ 'statusline.item.model': 'model',
71
+ 'statusline.item.model.desc': 'provider/model serving this session',
72
+ 'statusline.item.cwd': 'cwd',
73
+ 'statusline.item.cwd.desc': 'working-directory basename',
74
+ 'statusline.item.mode': 'mode',
75
+ 'statusline.item.mode.desc': 'agent preset composing the session',
76
+ 'statusline.item.branch': 'branch',
77
+ 'statusline.item.branch.desc': 'git branch inside a repository',
78
+ 'statusline.item.context': 'context',
79
+ 'statusline.item.context.desc': 'context-window occupancy meter',
80
+ 'statusline.item.permission': 'permission',
81
+ 'statusline.item.permission.desc': 'permission preset badge with cycle hint',
82
+ 'statusline.item.plan': 'plan',
83
+ 'statusline.item.plan.desc': 'plan-mode state mark',
84
+ 'statusline.item.turns': 'turns',
85
+ 'statusline.item.turns.desc': 'turn and step counters',
86
+ 'statusline.item.durations': 'durations',
87
+ 'statusline.item.durations.desc': 'llm/ttft/decode/tool wall time',
88
+ 'statusline.item.cache': 'cache',
89
+ 'statusline.item.cache.desc': 'cache-hit share of billed input',
90
+ 'statusline.item.tokens': 'tokens',
91
+ 'statusline.item.tokens.desc': 'cumulative input/output tokens',
92
+ 'statusline.item.title': 'title',
93
+ 'statusline.item.title.desc': 'session title or short id',
94
+ 'statusline.item.goal': 'goal',
95
+ 'statusline.item.goal.desc': 'live goal phase and round progress',
96
+ 'statusline.item.sandbox': 'sandbox',
97
+ 'statusline.item.sandbox.desc': 'divergent sandbox-mode override',
67
98
 
68
99
  // /schedule panel
69
100
  'schedule.title': '/schedule · {count} active reminder',
@@ -227,12 +258,18 @@ export const en = {
227
258
  'status.label.out': 'out',
228
259
  'status.label.mode': '/mode',
229
260
  'status.label.context': 'context',
261
+ 'status.label.sandbox': 'sandbox',
262
+ 'status.plan.on': 'plan on',
263
+ 'status.plan.mark': '⧉ plan',
264
+ 'status.goal.round': '◎ round {current}/{max}',
265
+ 'status.goal.phase': '◎ {phase}',
230
266
 
231
267
  // Frozen band
232
268
  'frozen.keysGoTo': 'keys go to {owner} · esc {action}',
233
269
  'frozen.action.rejects': 'rejects',
234
270
  'frozen.action.cancels': 'cancels',
235
271
  'frozen.action.closes': 'closes',
272
+ 'frozen.action.waits': 'waits',
236
273
 
237
274
  // Relative time (panel list timestamps)
238
275
  'time.justNow': 'now',
@@ -248,6 +285,7 @@ export const en = {
248
285
  'notice.themeSaveFailed': 'theme save failed: {message}',
249
286
  'notice.languageSaveFailed': 'language save failed: {message}',
250
287
  'notice.alreadyActive': 'that session is already active',
288
+ 'notice.permissionPresetsUnmounted': 'permission presets are not mounted in this composition',
251
289
  'notice.queueCancelled': 'queued message cancelled',
252
290
  'notice.queueActionFailed': 'queue action failed: {message}',
253
291
  'notice.queueUnavailable': 'queued message is no longer pending',
@@ -262,6 +300,8 @@ export const en = {
262
300
  'notice.usage.animation': 'usage: /animation [on|off]',
263
301
  'notice.usage.language': 'usage: /language [en|zh]',
264
302
  'notice.usage.rainbow': 'usage: /rainbow [seed]',
303
+ 'notice.usage.bareCommand': 'usage: /{name}',
304
+ 'notice.switchInProgress': 'switching sessions — send again once the new session is up, or /resume cancel',
265
305
  'notice.imageCancelled': 'image submission cancelled',
266
306
  'notice.themeConfigUnreadable': 'theme config unreadable, using dark: {message}',
267
307
  'notice.languageConfigUnreadable': 'language config unreadable, using english: {message}',
@@ -292,6 +332,13 @@ export const en = {
292
332
  'notice.resumeFailed': 'resume failed: {message}',
293
333
  'notice.forkFailed': 'fork failed: {message}',
294
334
  'notice.sessionSwitchFailed': 'session switch failed: {message}',
335
+ 'notice.sessionCreated': 'created {id} · mode {mode}',
336
+ 'notice.sessionResumed': 'resumed {id} · mode {mode}',
337
+ 'notice.sessionSwitchedDirty': 'switched to {id}, but {detail}',
338
+ 'notice.flushFailed': 'previous session flush failed: {message}',
339
+ 'notice.agentReleaseFailed': 'previous agent release failed: {message}',
340
+ 'notice.copyEmpty': 'nothing to copy yet',
341
+ 'notice.copied': 'copied latest response',
295
342
  'notice.switchQueued': 'will switch to {label} when the current turn finishes · /resume cancel to abort',
296
343
  'notice.reviewStarted': 'review started under read-only permissions',
297
344
  'notice.reviewFailed': 'review failed: {message}',
@@ -345,7 +392,8 @@ export const en = {
345
392
  'panel.footer.chooseSelect': '↑↓ choose · enter select · r refresh · esc close',
346
393
  'panel.footer.inspectDetails': '↑↓ inspect · enter details · r refresh · esc close',
347
394
  'panel.footer.inspectRefresh': '↑↓ inspect · r refresh · esc close',
348
- 'panel.footer.resume': 'tab/←→ filters · ↑↓/pg navigate · ctrl+f search · e details · t transcript · d delete · enter resume',
395
+ 'panel.footer.resume': 'tab/←→ filters · ↑↓/pg navigate · ctrl+f search · e details · t transcript · enter resume',
396
+ 'panel.footer.delete': 'tab/←→ filters · ↑↓/pg navigate · ctrl+f search · e details · t transcript · enter delete · esc close',
349
397
  'panel.footer.transcript': 'lines {from}-{to}/{total} · ↑↓/pg/g/G · t/esc close',
350
398
  'panel.footer.history': '↑↓ move · g/G ends · enter fill · esc close',
351
399
  'panel.footer.review': '↑↓ move · enter select · esc back · q close',
@@ -357,6 +405,7 @@ export const en = {
357
405
  'panel.jobs.title': '/jobs · background tasks · {count}',
358
406
  'panel.resume.deleteTitle': 'permanently delete {target}? this cannot be undone · subagent threads go too',
359
407
  'panel.resume.title': '/resume{mode}{search} · {toolbar}',
408
+ 'panel.delete.title': '/delete{search} · {toolbar}',
360
409
  'panel.noActiveReminders': 'no active reminders — the model schedules via schedule_create',
361
410
  'panel.notMounted': 'not mounted',
362
411
  'panel.broken': 'broken: {message}',
package/src/locales/zh.ts CHANGED
@@ -65,7 +65,38 @@ export const zh: MessageCatalog = {
65
65
  'search.footer': '↑↓ 移动 · 回车 搜索/恢复 · esc 关闭',
66
66
 
67
67
  // /statusline panel
68
+ 'header.hint': '/help · Esc 中断 · Ctrl+C 退出',
69
+ 'header.hintResumed': '已恢复 · /help · Esc 中断',
70
+
68
71
  'statusline.title': '/statusline · 选项实时应用到下方状态栏',
72
+ 'statusline.item.model': '模型',
73
+ 'statusline.item.model.desc': '本会话使用的 provider/model',
74
+ 'statusline.item.cwd': '目录',
75
+ 'statusline.item.cwd.desc': '工作目录基名',
76
+ 'statusline.item.mode': '模式',
77
+ 'statusline.item.mode.desc': '当前 agent 预设',
78
+ 'statusline.item.branch': '分支',
79
+ 'statusline.item.branch.desc': '仓库内 git 分支',
80
+ 'statusline.item.context': '上下文',
81
+ 'statusline.item.context.desc': '上下文窗口占用',
82
+ 'statusline.item.permission': '权限',
83
+ 'statusline.item.permission.desc': '权限预设徽章与循环提示',
84
+ 'statusline.item.plan': '计划',
85
+ 'statusline.item.plan.desc': '计划模式标记',
86
+ 'statusline.item.turns': '回合',
87
+ 'statusline.item.turns.desc': '回合与步骤计数',
88
+ 'statusline.item.durations': '耗时',
89
+ 'statusline.item.durations.desc': '模型/首字/解码/工具墙钟',
90
+ 'statusline.item.cache': '缓存',
91
+ 'statusline.item.cache.desc': '计费输入的缓存命中占比',
92
+ 'statusline.item.tokens': '用量',
93
+ 'statusline.item.tokens.desc': '累计输入/输出 token',
94
+ 'statusline.item.title': '标题',
95
+ 'statusline.item.title.desc': '会话标题或短 id',
96
+ 'statusline.item.goal': '目标',
97
+ 'statusline.item.goal.desc': '进行中的目标阶段与轮次',
98
+ 'statusline.item.sandbox': '沙箱',
99
+ 'statusline.item.sandbox.desc': '与预设不同的沙箱覆盖',
69
100
 
70
101
  // /schedule panel
71
102
  'schedule.title': '/schedule · {count} 条活动提醒',
@@ -229,12 +260,18 @@ export const zh: MessageCatalog = {
229
260
  'status.label.out': '出',
230
261
  'status.label.mode': '/mode',
231
262
  'status.label.context': '上下文',
263
+ 'status.label.sandbox': '沙箱',
264
+ 'status.plan.on': '计划开',
265
+ 'status.plan.mark': '⧉ 计划',
266
+ 'status.goal.round': '◎ 第 {current}/{max} 轮',
267
+ 'status.goal.phase': '◎ {phase}',
232
268
 
233
269
  // Frozen band
234
270
  'frozen.keysGoTo': '按键交给{owner} · esc {action}',
235
271
  'frozen.action.rejects': '拒绝',
236
272
  'frozen.action.cancels': '取消',
237
273
  'frozen.action.closes': '关闭',
274
+ 'frozen.action.waits': '等待',
238
275
 
239
276
  // Relative time (panel list timestamps)
240
277
  'time.justNow': '刚刚',
@@ -250,6 +287,7 @@ export const zh: MessageCatalog = {
250
287
  'notice.themeSaveFailed': '主题保存失败:{message}',
251
288
  'notice.languageSaveFailed': '语言保存失败:{message}',
252
289
  'notice.alreadyActive': '该会话已是当前会话',
290
+ 'notice.permissionPresetsUnmounted': '当前组合未挂载权限预设',
253
291
  'notice.queueCancelled': '已取消排队消息',
254
292
  'notice.queueActionFailed': '队列操作失败:{message}',
255
293
  'notice.queueUnavailable': '该消息已不在队列中',
@@ -264,6 +302,8 @@ export const zh: MessageCatalog = {
264
302
  'notice.usage.animation': '用法:/animation [on|off]',
265
303
  'notice.usage.language': '用法:/language [en|zh]',
266
304
  'notice.usage.rainbow': '用法:/rainbow [seed]',
305
+ 'notice.usage.bareCommand': '用法:/{name}',
306
+ 'notice.switchInProgress': '正在切换会话 —— 等新会话就绪后再发送,或用 /resume cancel 取消',
267
307
  'notice.imageCancelled': '图片提交已取消',
268
308
  'notice.themeConfigUnreadable': '主题配置无法读取,使用暗色:{message}',
269
309
  'notice.languageConfigUnreadable': '语言配置无法读取,使用英文:{message}',
@@ -294,6 +334,13 @@ export const zh: MessageCatalog = {
294
334
  'notice.resumeFailed': '恢复失败:{message}',
295
335
  'notice.forkFailed': '分叉失败:{message}',
296
336
  'notice.sessionSwitchFailed': '会话切换失败:{message}',
337
+ 'notice.sessionCreated': '已创建 {id} · 模式 {mode}',
338
+ 'notice.sessionResumed': '已恢复 {id} · 模式 {mode}',
339
+ 'notice.sessionSwitchedDirty': '已切换到 {id},但 {detail}',
340
+ 'notice.flushFailed': '上一会话刷新失败:{message}',
341
+ 'notice.agentReleaseFailed': '上一代理释放失败:{message}',
342
+ 'notice.copyEmpty': '还没有可复制的回复',
343
+ 'notice.copied': '已复制最近一条回复',
297
344
  'notice.switchQueued': '当前回合结束后将切换到 {label} · 可用 /resume cancel 取消',
298
345
  'notice.reviewStarted': '审查已在只读权限下开始',
299
346
  'notice.reviewFailed': '审查失败:{message}',
@@ -347,7 +394,8 @@ export const zh: MessageCatalog = {
347
394
  'panel.footer.chooseSelect': '↑↓ 移动 · 回车 选择 · r 刷新 · esc 关闭',
348
395
  'panel.footer.inspectDetails': '↑↓ 查看 · 回车 详情 · r 刷新 · esc 关闭',
349
396
  'panel.footer.inspectRefresh': '↑↓ 查看 · r 刷新 · esc 关闭',
350
- 'panel.footer.resume': 'tab/←→ 筛选 · ↑↓/pg 翻页 · ctrl+f 搜索 · e 详情 · t 转录 · d 删除 · 回车恢复',
397
+ 'panel.footer.resume': 'tab/←→ 筛选 · ↑↓/pg 翻页 · ctrl+f 搜索 · e 详情 · t 转录 · 回车恢复',
398
+ 'panel.footer.delete': 'tab/←→ 筛选 · ↑↓/pg 翻页 · ctrl+f 搜索 · e 详情 · t 转录 · 回车删除 · esc 关闭',
351
399
  'panel.footer.transcript': '行 {from}-{to}/{total} · ↑↓/pg/g/G · t/esc 关闭',
352
400
  'panel.footer.history': '↑↓ 移动 · g/G 首尾 · 回车填入 · esc 关闭',
353
401
  'panel.footer.review': '↑↓ 移动 · 回车选择 · esc 返回 · q 关闭',
@@ -359,6 +407,7 @@ export const zh: MessageCatalog = {
359
407
  'panel.jobs.title': '/jobs · 后台任务 · {count}',
360
408
  'panel.resume.deleteTitle': '永久删除 {target}?此操作无法撤销,子代理线程也会删除',
361
409
  'panel.resume.title': '/resume{mode}{search} · {toolbar}',
410
+ 'panel.delete.title': '/delete{search} · {toolbar}',
362
411
  'panel.noActiveReminders': '暂无活动提醒——模型通过 schedule_create 创建',
363
412
  'panel.notMounted': '未加载',
364
413
  'panel.broken': '异常:{message}',
package/src/rainbow.ts CHANGED
@@ -115,6 +115,11 @@ function hueOf([r, g, b]: RgbTriple): number {
115
115
  return (60 * (r - g) / span + 240) % 360
116
116
  }
117
117
 
118
+ /** Strict RGB equality for the wrap-collision repair. */
119
+ function sameRgb(left: RgbTriple, right: RgbTriple): boolean {
120
+ return left[0] === right[0] && left[1] === right[1] && left[2] === right[2]
121
+ }
122
+
118
123
  /** A dark row tint for diff backgrounds: the hue at 22% strength over black. */
119
124
  function darkTint(hue: RgbTriple): RgbTriple {
120
125
  return [Math.round(hue[0] * 0.22), Math.round(hue[1] * 0.22), Math.round(hue[2] * 0.22)]
@@ -160,14 +165,19 @@ export function rollRainbow(seed: number): RainbowRoll {
160
165
  steered: code,
161
166
  }
162
167
  const ring = shuffle(RAINBOW_POOL, rng).slice(0, 4)
163
- // Adjacency by construction: consecutive tones take consecutive slots of a
164
- // 12-distinct shuffle, so neighbors can never match (13 tones wrap the
165
- // first color onto 'error', which is never adjacent to 'live' on screen).
168
+ // Consecutive tones take consecutive slots of a 12-distinct shuffle, so
169
+ // on-screen neighbors never match. 13 tones wrap: live (index 0) and error
170
+ // (index 12) would share a color. Reassign error to a pool hue that is
171
+ // neither live nor its neighbor warn — error must not look like the busy dot.
166
172
  const offset = Math.floor(rng() * RAINBOW_POOL.length)
167
173
  const toneColors = {} as Record<StatusTone, RgbTriple>
168
174
  TONE_ORDER.forEach((tone, index) => {
169
175
  toneColors[tone] = pool[(offset + index) % pool.length]!
170
176
  })
177
+ if (sameRgb(toneColors.live, toneColors.error)) {
178
+ const replacement = pool.find(hue => !sameRgb(hue, toneColors.live) && !sameRgb(hue, toneColors.warn))
179
+ if (replacement !== undefined) toneColors.error = replacement
180
+ }
171
181
  const flowAnchors = [...RAINBOW_POOL].sort((a, b) => hueOf(a) - hueOf(b))
172
182
  const flowPhaseMs = Math.floor(rng() * 2400)
173
183
  return { seed, palette, ring, toneColors, flowAnchors, flowPhaseMs }