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.
Files changed (86) hide show
  1. package/README.en.md +123 -26
  2. package/README.md +124 -27
  3. package/bin/deepseek.mjs +283 -35
  4. package/cordis.patch.yml +97 -0
  5. package/lib/index.mjs +5008 -881
  6. package/lib/session-query.mjs +150 -0
  7. package/lib/startup.mjs +4 -4
  8. package/lib/{theme-DCT8Y2xf.mjs → theme-7u5Qo3dF.mjs} +657 -20
  9. package/lib/types/app.d.ts +106 -62
  10. package/lib/types/authorization-panel.d.ts +3 -3
  11. package/lib/types/git-workflow.d.ts +91 -2
  12. package/lib/types/i18n.d.ts +39 -0
  13. package/lib/types/index.d.ts +100 -1
  14. package/lib/types/input-split.d.ts +1 -1
  15. package/lib/types/kernel-panels.d.ts +107 -29
  16. package/lib/types/language-panel.d.ts +12 -0
  17. package/lib/types/locales/en.d.ts +450 -0
  18. package/lib/types/locales/zh.d.ts +9 -0
  19. package/lib/types/mentions.d.ts +7 -3
  20. package/lib/types/models.d.ts +14 -0
  21. package/lib/types/panel-accent.d.ts +28 -0
  22. package/lib/types/rainbow.d.ts +69 -0
  23. package/lib/types/render/animations.d.ts +42 -0
  24. package/lib/types/render/editor.d.ts +4 -3
  25. package/lib/types/render/ime-cursor.d.ts +60 -0
  26. package/lib/types/render/inspector.d.ts +26 -0
  27. package/lib/types/render/lines.d.ts +21 -1
  28. package/lib/types/render/markdown.d.ts +1 -1
  29. package/lib/types/render/projection.d.ts +130 -4
  30. package/lib/types/render/status.d.ts +9 -9
  31. package/lib/types/render/text.d.ts +6 -0
  32. package/lib/types/render/usage.d.ts +113 -0
  33. package/lib/types/session-directory.d.ts +17 -0
  34. package/lib/types/session-query.d.ts +92 -0
  35. package/lib/types/startup.d.ts +1 -1
  36. package/lib/types/terminal-title.d.ts +66 -0
  37. package/lib/types/theme-panel.d.ts +2 -2
  38. package/lib/types/theme.d.ts +271 -52
  39. package/lib/types/update-panel.d.ts +49 -0
  40. package/lib/types/update.d.ts +75 -0
  41. package/lib/types/version.d.ts +4 -3
  42. package/package.json +246 -90
  43. package/src/app.ts +1369 -509
  44. package/src/approval.ts +166 -166
  45. package/src/authorization-panel.ts +19 -16
  46. package/src/editor-keys.ts +371 -371
  47. package/src/git-workflow.ts +229 -3
  48. package/src/i18n.ts +68 -0
  49. package/src/index.ts +534 -80
  50. package/src/input-split.ts +3 -3
  51. package/src/internals.ts +5 -0
  52. package/src/kernel-panels.ts +554 -86
  53. package/src/keyboard.ts +5 -4
  54. package/src/language-panel.ts +53 -0
  55. package/src/locales/en.ts +489 -0
  56. package/src/locales/zh.ts +488 -0
  57. package/src/mentions.ts +8 -4
  58. package/src/models.ts +264 -212
  59. package/src/panel-accent.ts +41 -0
  60. package/src/presets.ts +1 -1
  61. package/src/provider-settings.ts +1 -1
  62. package/src/rainbow.ts +208 -0
  63. package/src/render/animations.ts +104 -6
  64. package/src/render/editor.ts +25 -24
  65. package/src/render/export.ts +116 -95
  66. package/src/render/ime-cursor.ts +147 -0
  67. package/src/render/inspector.ts +42 -0
  68. package/src/render/lines.ts +628 -415
  69. package/src/render/markdown.ts +15 -3
  70. package/src/render/projection.ts +572 -21
  71. package/src/render/status.ts +59 -39
  72. package/src/render/text.ts +14 -0
  73. package/src/render/tool-preview.ts +77 -77
  74. package/src/render/usage.ts +430 -0
  75. package/src/render/width.ts +2 -2
  76. package/src/session-directory.ts +8 -6
  77. package/src/session-query.ts +239 -0
  78. package/src/startup.ts +3 -3
  79. package/src/subagents.ts +229 -229
  80. package/src/terminal-title.ts +190 -0
  81. package/src/theme-panel.ts +17 -21
  82. package/src/theme.ts +281 -33
  83. package/src/update-panel.ts +256 -0
  84. package/src/update.ts +126 -0
  85. package/src/version.ts +58 -20
  86. package/src/whale-glyph.ts +23 -23
@@ -1,95 +1,116 @@
1
- /**
2
- * Markdown export of one transcript view: the /export command's pure
3
- * formatter. Deterministic and side-effect free — the runner owns the file
4
- * write, so tests drive the builder with folded views directly.
5
- *
6
- * @module @deepseek-ai/dsh-code/render/export
7
- */
8
-
9
- import { assertNever } from '@deepseek-ai/dsh-util-values'
10
- import { fileLabels, imageLabels, type TranscriptView } from './projection.ts'
11
-
12
- /**
13
- * Render the transcript as a standalone markdown document.
14
- * @param view - the folded transcript view to export.
15
- * @param sessionId - the full session identity for the header.
16
- * @returns the complete markdown text.
17
- */
18
- /** Both user and queued rows export the same attachment label block. */
19
- const attachmentLabels = (entry: { images?: readonly unknown[]; files?: readonly unknown[] }): string =>
20
- [imageLabels(entry.images as never), fileLabels(entry.files as never)].filter(label => label !== '').join('\n')
21
-
22
- export function buildExportMarkdown(view: TranscriptView, sessionId: string): string {
23
- const out: string[] = [
24
- view.title === ''
25
- ? `# dsh session ${sessionId}`
26
- : `# ${view.title}`,
27
- `> session ${sessionId}`,
28
- '',
29
- ]
30
- // The effective system prompt (v3 surface nodes) heads the export in a
31
- // collapsed block: visible when audited, out of the way when scrolled.
32
- if (view.systemPrompt !== '') {
33
- out.push('<details><summary>system prompt</summary>', '', view.systemPrompt, '', '</details>', '')
34
- }
35
- for (const entry of view.entries) {
36
- switch (entry.kind) {
37
- case 'user':
38
- if (entry.notice) {
39
- out.push(`> ⤷ context: ${entry.text}`, '')
40
- } else {
41
- const attachments = attachmentLabels(entry)
42
- out.push('## user', '', entry.text, ...(attachments === '' ? [] : [attachments]), '')
43
- }
44
- break
45
- case 'assistant':
46
- if (entry.reasoning !== '') {
47
- out.push('<details><summary>thinking</summary>', '', entry.reasoning, '', '</details>', '')
48
- }
49
- out.push('## assistant', '', entry.text, '')
50
- break
51
- case 'tool':
52
- out.push(`### tool \`${entry.name}\``, '')
53
- if (entry.preview !== '') out.push(`- args: ${entry.preview}`)
54
- if (entry.summary !== '') out.push(`- ${entry.state === 'error' ? 'error' : 'result'}: ${entry.summary}`)
55
- out.push('')
56
- break
57
- case 'command':
58
- out.push(`### /${entry.name}${entry.args === '' ? '' : ` ${entry.args}`}`, '')
59
- if (entry.summary !== '') out.push(`- ${entry.state === 'error' ? 'error' : 'result'}: ${entry.summary}`)
60
- out.push('')
61
- break
62
- case 'error':
63
- out.push(`> ⨯ ${entry.text}`, '')
64
- break
65
- case 'turn-marker':
66
- out.push(`> ${entry.text}`, '')
67
- break
68
- case 'compaction':
69
- out.push(entry.ok
70
- ? `> compacted ~${entry.tokens} tokens`
71
- : `> compaction failed: ${entry.error}`, '')
72
- break
73
- case 'retry':
74
- out.push(`> retry ${entry.attempt}/${entry.max} (${entry.code})`, '')
75
- break
76
- case 'files':
77
- out.push(`> files changed: ${entry.paths.join(', ')}`, '')
78
- break
79
- case 'pending':
80
- // Codex PendingSteer: queued prompts export like ordinary user rows.
81
- out.push('## user', '', entry.text, ...(attachmentLabels(entry) === '' ? [] : [attachmentLabels(entry)]), '')
82
- break
83
- default:
84
- assertNever(entry, 'transcript entry kind')
85
- }
86
- }
87
- if (view.streaming !== '') out.push('## assistant (streaming)', '', view.streaming, '')
88
- const { stats } = view
89
- out.push('---', '')
90
- out.push(`- model: ${view.model === '' ? '(none yet)' : view.model}`)
91
- out.push(`- turns: ${stats.turns} · steps: ${stats.steps}`)
92
- out.push(`- tokens: ↑${stats.usage.inputTokens} ↓${stats.usage.outputTokens} · cache read ${stats.usage.cacheReadTokens}`)
93
- out.push(`- todos: ${view.todos.length}`)
94
- return out.join('\n')
95
- }
1
+ /**
2
+ * Markdown export of one transcript view: the /export command's pure
3
+ * formatter. Deterministic and side-effect free — the runner owns the file
4
+ * write, so tests drive the builder with folded views directly.
5
+ *
6
+ * @module @deepseek-ai/dsh-code/render/export
7
+ */
8
+
9
+ import { assertNever } from '@deepseek-ai/dsh-util-values'
10
+ import { fileLabels, imageLabels, type TranscriptView } from './projection.ts'
11
+ import { t } from '../i18n.ts'
12
+
13
+ /**
14
+ * Render the transcript as a standalone markdown document.
15
+ * @param view - the folded transcript view to export.
16
+ * @param sessionId - the full session identity for the header.
17
+ * @returns the complete markdown text.
18
+ */
19
+ /** Both user and queued rows export the same attachment label block. */
20
+ const attachmentLabels = (entry: { images?: readonly unknown[]; files?: readonly unknown[] }): string =>
21
+ [imageLabels(entry.images as never), fileLabels(entry.files as never)].filter(label => label !== '').join('\n')
22
+
23
+ export function buildExportMarkdown(view: TranscriptView, sessionId: string): string {
24
+ const out: string[] = [
25
+ view.title === ''
26
+ ? `# dsh session ${sessionId}`
27
+ : `# ${view.title}`,
28
+ `> session ${sessionId}`,
29
+ '',
30
+ ]
31
+ // The effective system prompt (v3 surface nodes) heads the export in a
32
+ // collapsed block: visible when audited, out of the way when scrolled.
33
+ if (view.systemPrompt !== '') {
34
+ out.push('<details><summary>system prompt</summary>', '', view.systemPrompt, '', '</details>', '')
35
+ }
36
+ for (const entry of view.entries) {
37
+ switch (entry.kind) {
38
+ case 'user':
39
+ if (entry.notice) {
40
+ out.push(`> context: ${entry.text}`, '')
41
+ } else {
42
+ const attachments = attachmentLabels(entry)
43
+ // How the prompt was delivered is part of the record: a queued or
44
+ // steered message reads differently from one typed into an idle
45
+ // composer. Ordinary prompts keep the bare heading.
46
+ const marker = entry.delivery === undefined ? '' : ` (${t(entry.delivery === 'queued' ? 'entry.delivery.queued' : 'entry.delivery.steered')})`
47
+ out.push(`## user${marker}`, '', entry.text, ...(attachments === '' ? [] : [attachments]), '')
48
+ }
49
+ break
50
+ case 'assistant':
51
+ if (entry.reasoning !== '') {
52
+ out.push('<details><summary>thinking</summary>', '', entry.reasoning, '', '</details>', '')
53
+ }
54
+ out.push('## assistant', '', entry.text, '')
55
+ break
56
+ case 'tool':
57
+ out.push(`### tool \`${entry.name}\``, '')
58
+ if (entry.preview !== '') out.push(`- args: ${entry.preview}`)
59
+ if (entry.subs.length > 0) {
60
+ for (const sub of entry.subs) {
61
+ const label = sub.preview === '' ? sub.name : `${sub.name} ${sub.preview}`
62
+ out.push(`- dispatch: ${sub.state === 'error' ? 'error' : sub.state === 'running' ? 'running' : 'ok'} · ${label}${sub.summary === '' ? '' : ` · ${sub.summary}`}`)
63
+ }
64
+ if (entry.subsDropped > 0) out.push(`- dispatch: … ${entry.subsDropped} earlier dispatch${entry.subsDropped === 1 ? '' : 'es'}`)
65
+ }
66
+ if (entry.summary !== '') out.push(`- ${entry.state === 'error' ? 'error' : 'result'}: ${entry.summary}`)
67
+ out.push('')
68
+ break
69
+ case 'command':
70
+ out.push(`### /${entry.name}${entry.args === '' ? '' : ` ${entry.args}`}`, '')
71
+ if (entry.summary !== '') out.push(`- ${entry.state === 'error' ? 'error' : 'result'}: ${entry.summary}`)
72
+ out.push('')
73
+ break
74
+ case 'error':
75
+ out.push(`> ⨯ ${entry.text}`, '')
76
+ break
77
+ case 'turn-marker':
78
+ out.push(`> ${entry.text}`, '')
79
+ break
80
+ case 'compaction':
81
+ out.push(entry.ok
82
+ ? `> compacted ~${entry.tokens} tokens`
83
+ : `> compaction failed: ${entry.error}`, '')
84
+ break
85
+ case 'retry':
86
+ out.push(`> retry ${entry.attempt}/${entry.max} (${entry.code})`, '')
87
+ break
88
+ case 'files':
89
+ out.push(`> files changed: ${entry.paths.join(', ')}`, '')
90
+ break
91
+ case 'workflow':
92
+ out.push(`### workflow \`${entry.name}\` (${entry.state})`, '')
93
+ for (const member of entry.members) {
94
+ const phase = member.phase === '' ? '' : ` [${member.phase}]`
95
+ out.push(`- ${member.outcome}: ${member.label}${phase}`)
96
+ }
97
+ if (entry.membersDropped > 0) out.push(`- … ${entry.membersDropped} earlier member${entry.membersDropped === 1 ? '' : 's'}`)
98
+ out.push('')
99
+ break
100
+ case 'pending':
101
+ // Codex PendingSteer: queued prompts export like ordinary user rows.
102
+ out.push('## user', '', entry.text, ...(attachmentLabels(entry) === '' ? [] : [attachmentLabels(entry)]), '')
103
+ break
104
+ default:
105
+ assertNever(entry, 'transcript entry kind')
106
+ }
107
+ }
108
+ if (view.streaming !== '') out.push('## assistant (streaming)', '', view.streaming, '')
109
+ const { stats } = view
110
+ out.push('---', '')
111
+ out.push(`- model: ${view.model === '' ? '(none yet)' : view.model}`)
112
+ out.push(`- turns: ${stats.turns} · steps: ${stats.steps}`)
113
+ out.push(`- tokens: ↑${stats.usage.uncachedInputTokens} ↓${stats.usage.outputTokens} · cache read ${stats.usage.cacheReadTokens} · cache write ${stats.usage.cacheWriteTokens}`)
114
+ out.push(`- todos: ${view.todos.length}`)
115
+ return out.join('\n')
116
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * IME cursor anchoring for the composer.
3
+ *
4
+ * Ink keeps the real terminal cursor hidden and parks it just below the
5
+ * dynamic tree (after the status row). IME composition text and candidate
6
+ * windows anchor to that real cursor cell - VS Code's integrated terminal
7
+ * positions its hidden IME textarea there, and Windows consoles behave the
8
+ * same - so CJK input appeared at the bottom of the screen instead of at the
9
+ * caret. The anchor moves the real cursor onto the caret cell without
10
+ * touching Ink's relative erase ledger:
11
+ *
12
+ * - the displacement is owned: before any foreign write or re-anchor, the
13
+ * wrapper cancels it (cursor down, column 1), so every writer - Ink's
14
+ * log-update rewrites, the resize replay, protocol pushes - keeps seeing
15
+ * the cursor exactly where it was left;
16
+ * - log-update frame chunks (which start with the erase-line sequence) get
17
+ * the anchor re-appended inside the same write, so a repaint can never
18
+ * leave the cursor behind - in particular the caret blink keeps the anchor
19
+ * stable while an IME composition is open, because the terminal parses the
20
+ * rewrite and the re-anchor as one atomic update.
21
+ *
22
+ * @module @deepseek-ai/dsh-code/render/ime-cursor
23
+ */
24
+
25
+ import { useEffect, useRef } from 'react'
26
+ import { useStdout } from 'ink'
27
+
28
+ /** Cancel `rows` of owned upward displacement and return to column 1. */
29
+ export function imeCursorRestore(rows: number): string {
30
+ return rows > 0 ? `\x1b[${rows}B\r` : ''
31
+ }
32
+
33
+ /** Move onto the caret cell: `rows` up from Ink's parked row, 0-based column. */
34
+ export function imeCursorMove(rows: number, column: number): string {
35
+ return rows > 0 ? `\x1b[${rows}A\x1b[${column + 1}G` : ''
36
+ }
37
+
38
+ /**
39
+ * Rows between the caret cell and Ink's parked cursor row: the band's bottom
40
+ * blank row, the editor window rows below the caret, the status footer, and
41
+ * Ink's own below-frame row. Rows above the composer (gutter, live content)
42
+ * never enter this distance.
43
+ */
44
+ export function imeCursorRowsUp(input: {
45
+ editorWindowRows: number
46
+ caretRowInWindow: number
47
+ rowsBelowComposer: number
48
+ }): number {
49
+ return Math.max(1, input.editorWindowRows - input.caretRowInWindow + input.rowsBelowComposer)
50
+ }
51
+
52
+ /** log-update chunks start with the erase-line sequence; a chunk without it
53
+ * leaves no frame behind, so the next commit re-applies the anchor. */
54
+ const FRAME_CHUNK_MARKER = '\x1b[2K'
55
+
56
+ /** The installed anchor handle. */
57
+ export interface ImeCursorAnchor {
58
+ /** Anchor on the caret cell: `rows` above Ink's parked row at the 0-based
59
+ * `column`; `rows <= 0` releases the anchor. */
60
+ anchor(rows: number, column: number): void
61
+ /** Cancel the displacement, restore the original write path, and detach. */
62
+ release(): void
63
+ }
64
+
65
+ const ANCHOR_STATE = Symbol.for('dsh-code.ime-cursor-anchor')
66
+
67
+ /**
68
+ * Take over `stream.write` so the anchor displacement stays invisible to every
69
+ * other writer. Idempotent per stream: a second install returns the live
70
+ * handle. Returns `undefined` on non-TTY streams where anchoring is meaningless.
71
+ */
72
+ export function installImeCursorAnchor(stream: NodeJS.WriteStream): ImeCursorAnchor | undefined {
73
+ if (stream?.isTTY !== true) return undefined
74
+ const target = stream as NodeJS.WriteStream & { [ANCHOR_STATE]?: ImeCursorAnchor | undefined }
75
+ const installed = target[ANCHOR_STATE]
76
+ if (installed !== undefined) return installed
77
+ const originalWrite = target.write.bind(target) as (...args: unknown[]) => unknown
78
+ let rows = 0
79
+ let column = -1
80
+ let detached = false
81
+ const anchor: ImeCursorAnchor = {
82
+ anchor(nextRows, nextColumn) {
83
+ if (detached) return
84
+ if (nextRows <= 0) {
85
+ if (rows !== 0) originalWrite(imeCursorRestore(rows))
86
+ rows = 0
87
+ column = -1
88
+ return
89
+ }
90
+ if (rows === nextRows && column === nextColumn) return
91
+ originalWrite(imeCursorRestore(rows) + imeCursorMove(nextRows, nextColumn))
92
+ rows = nextRows
93
+ column = nextColumn
94
+ },
95
+ release() {
96
+ if (detached) return
97
+ detached = true
98
+ if (rows !== 0) originalWrite(imeCursorRestore(rows))
99
+ rows = 0
100
+ column = -1
101
+ target.write = originalWrite as typeof target.write
102
+ delete target[ANCHOR_STATE]
103
+ },
104
+ }
105
+ target.write = ((chunk: unknown, ...rest: unknown[]) => {
106
+ const ownedRows = rows
107
+ const ownedColumn = column
108
+ if (ownedRows === 0 || typeof chunk !== 'string') {
109
+ if (ownedRows !== 0) {
110
+ originalWrite(imeCursorRestore(ownedRows))
111
+ rows = 0
112
+ column = -1
113
+ }
114
+ return originalWrite(chunk, ...rest)
115
+ }
116
+ const reanchor = chunk.startsWith(FRAME_CHUNK_MARKER)
117
+ rows = reanchor ? ownedRows : 0
118
+ column = reanchor ? ownedColumn : -1
119
+ return originalWrite(imeCursorRestore(ownedRows) + chunk + (reanchor ? imeCursorMove(ownedRows, ownedColumn) : ''), ...rest)
120
+ }) as typeof target.write
121
+ target[ANCHOR_STATE] = anchor
122
+ return anchor
123
+ }
124
+
125
+ /**
126
+ * Keep the real terminal cursor on the composer's caret cell while `active`
127
+ * (the editable composer). The second effect runs after every commit without
128
+ * a dep list: frame rewrites restore the anchor themselves, but any other
129
+ * write (protocol push, resize replay) leaves the cursor at Ink's parked
130
+ * position, and the next commit re-anchors it.
131
+ */
132
+ export function useImeCursorAnchor(active: boolean, rows: number, column: number): void {
133
+ const { stdout } = useStdout()
134
+ const anchorRef = useRef<ImeCursorAnchor | undefined>(undefined)
135
+ useEffect(() => {
136
+ if (stdout === undefined) return undefined
137
+ const installed = installImeCursorAnchor(stdout)
138
+ anchorRef.current = installed
139
+ return () => {
140
+ installed?.release()
141
+ if (anchorRef.current === installed) anchorRef.current = undefined
142
+ }
143
+ }, [stdout])
144
+ useEffect(() => {
145
+ anchorRef.current?.anchor(active && rows > 0 ? rows : 0, column)
146
+ })
147
+ }
@@ -24,6 +24,48 @@ export function layoutGutterRows(rows: number): 0 | 1 {
24
24
  return Math.max(1, Math.floor(rows)) >= 14 ? 1 : 0
25
25
  }
26
26
 
27
+ /** Measured chrome that sits below (or in) the live region. */
28
+ export interface LiveRegionChrome {
29
+ /** Terminal rows. */
30
+ readonly terminalRows: number
31
+ /** Editor rows inside the composer band (frozen reports 1). */
32
+ readonly composerRows: number
33
+ /** Status footer rows. */
34
+ readonly statusBarRows: 1 | 2
35
+ /** Completion menu rows (0 when closed). */
36
+ readonly menuRows: number
37
+ /** Transcript-to-composer gutter. */
38
+ readonly gutterRows: 0 | 1
39
+ /** Notice line present. */
40
+ readonly notice: boolean
41
+ /** Todo summary line present. */
42
+ readonly todo: boolean
43
+ /** Agents summary line present. */
44
+ readonly agents: boolean
45
+ }
46
+
47
+ /**
48
+ * Rows left for live transcript and streaming after pinning the composer and
49
+ * status at the bottom. Variable chrome (menu, notice, todos, agents, extra
50
+ * status row, extra editor rows) is deducted here so those rows cover the
51
+ * live region instead of growing the tree and moving the bottom bar.
52
+ */
53
+ export function liveRegionBudget(chrome: LiveRegionChrome): number {
54
+ const terminal = Math.max(1, Math.floor(chrome.terminalRows))
55
+ const editor = Math.max(1, Math.floor(chrome.composerRows))
56
+ const band = editor + 2
57
+ const status = chrome.statusBarRows === 2 ? 2 : 1
58
+ const menu = Math.max(0, Math.floor(chrome.menuRows))
59
+ const gutter = chrome.gutterRows
60
+ const notice = chrome.notice ? 1 : 0
61
+ const todo = chrome.todo ? 1 : 0
62
+ const agents = chrome.agents ? 1 : 0
63
+ // Two spare rows: Ink's parked cursor, plus one so the painted tree never
64
+ // reaches stdout.rows (equality takes the full-terminal clear path).
65
+ const inkSpare = 2
66
+ return Math.max(1, terminal - band - status - menu - gutter - notice - todo - agents - inkSpare)
67
+ }
68
+
27
69
  /**
28
70
  * Keep the inspector plus its persistent status/composer chrome below
29
71
  * `stdout.rows`: at equality Ink clears the terminal and rewrites all