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.
- package/README.en.md +5 -5
- package/README.md +5 -5
- package/lib/index.mjs +955 -233
- package/lib/startup.mjs +1 -1
- package/lib/{theme-7u5Qo3dF.mjs → theme-B3orFUYz.mjs} +8 -0
- package/lib/types/app.d.ts +20 -0
- package/lib/types/attachments.d.ts +16 -7
- package/lib/types/history.d.ts +10 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/kernel-panels.d.ts +3 -1
- package/lib/types/locales/en.d.ts +49 -1
- package/lib/types/render/status.d.ts +4 -1
- package/lib/types/session-directory.d.ts +25 -1
- package/lib/types/session-switch.d.ts +8 -0
- package/lib/types/update-panel.d.ts +22 -1
- package/lib/types/version.d.ts +2 -0
- package/package.json +7 -5
- package/src/app.ts +288 -166
- package/src/attachments.ts +65 -19
- package/src/authorization-panel.ts +5 -2
- package/src/fork.ts +11 -7
- package/src/history.ts +14 -0
- package/src/index.ts +92 -53
- package/src/input-split.ts +24 -4
- package/src/kernel-panels.ts +60 -27
- package/src/locales/en.ts +50 -1
- package/src/locales/zh.ts +50 -1
- package/src/rainbow.ts +13 -3
- package/src/render/status.ts +80 -29
- package/src/render/text.ts +2 -1
- package/src/session-directory.ts +82 -3
- package/src/session-switch.ts +14 -0
- package/src/store.ts +19 -1
- package/src/update-panel.ts +112 -5
- package/src/version.ts +5 -0
package/src/kernel-panels.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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>({
|
|
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]
|
|
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
|
|
396
|
-
?
|
|
397
|
-
:
|
|
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(
|
|
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 ·
|
|
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 转录 ·
|
|
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
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
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 }
|