dsh-code 0.9.0 → 1.0.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 +31 -8
- package/README.md +264 -241
- package/bin/deepseek.mjs +100 -6
- package/cordis.patch.yml +29 -1
- package/lib/index.mjs +2317 -708
- package/lib/startup.mjs +21 -11
- package/lib/{theme-BEi4i_aN.mjs → theme-DCT8Y2xf.mjs} +13 -9
- package/lib/types/app.d.ts +66 -14
- package/lib/types/attachments.d.ts +7 -0
- package/lib/types/editor.d.ts +6 -0
- package/lib/types/fork.d.ts +8 -0
- package/lib/types/git-workflow.d.ts +23 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/kernel-panels.d.ts +39 -0
- package/lib/types/keyboard.d.ts +43 -0
- package/lib/types/mentions.d.ts +28 -38
- package/lib/types/presets.d.ts +1 -3
- package/lib/types/provider-settings.d.ts +16 -0
- package/lib/types/render/animations.d.ts +24 -41
- package/lib/types/render/editor.d.ts +137 -0
- package/lib/types/render/export.d.ts +1 -1
- package/lib/types/render/lines.d.ts +6 -2
- package/lib/types/render/markdown.d.ts +3 -1
- package/lib/types/render/projection.d.ts +29 -3
- package/lib/types/render/status.d.ts +5 -12
- package/lib/types/session-directory.d.ts +1 -3
- package/lib/types/startup.d.ts +14 -11
- package/lib/types/store.d.ts +11 -9
- package/lib/types/subagents.d.ts +3 -3
- package/lib/types/theme.d.ts +14 -1
- package/lib/types/version.d.ts +15 -2
- package/package.json +153 -117
- package/src/app.ts +4455 -3904
- package/src/attachments.ts +44 -0
- package/src/editor.ts +51 -0
- package/src/fork.ts +31 -0
- package/src/git-workflow.ts +87 -0
- package/src/index.ts +1510 -1374
- package/src/internals.ts +14 -1
- package/src/kernel-panels.ts +914 -798
- package/src/keyboard.ts +125 -0
- package/src/mentions.ts +72 -117
- package/src/presets.ts +1 -4
- package/src/provider-settings.ts +94 -0
- package/src/render/animations.ts +74 -60
- package/src/render/editor.ts +398 -0
- package/src/render/export.ts +79 -79
- package/src/render/lines.ts +342 -236
- package/src/render/markdown.ts +99 -26
- package/src/render/projection.ts +102 -19
- package/src/render/status.ts +713 -650
- package/src/render/text.ts +150 -150
- package/src/render/tool-detail.ts +3 -1
- package/src/session-directory.ts +3 -3
- package/src/startup.ts +136 -119
- package/src/store.ts +23 -11
- package/src/subagents.ts +13 -5
- package/src/theme.ts +214 -206
- package/src/version.ts +58 -1
package/src/render/text.ts
CHANGED
|
@@ -1,150 +1,150 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Display-boundary sanitization for externally sourced text (model output,
|
|
3
|
-
* tool payloads, skill descriptions). Control characters — including ANSI
|
|
4
|
-
* CSI/OSC escape sequences — would otherwise pass through Ink into the
|
|
5
|
-
* terminal, letting output rewrite the screen or inject prompts. Newlines
|
|
6
|
-
* survive; everything else in C0/C1 plus DEL becomes a visible `\xNN`
|
|
7
|
-
* escape, and bidi overrides / invisible format controls / Unicode line and
|
|
8
|
-
* paragraph separators become a visible `\uXXXX` escape (terminal emulators
|
|
9
|
-
* that render bidirectional text would otherwise reorder the displayed
|
|
10
|
-
* glyphs and let a command read as something it is not).
|
|
11
|
-
*
|
|
12
|
-
* @module @deepseek-ai/dsh-code/render/text
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/** C0 controls except tab (0x09) and newline (0x0a), plus DEL and C1. */
|
|
16
|
-
const CONTROL_ESCAPE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/gu
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Bidi overrides and isolates (U+202A-202E, U+2066-2069), the Arabic Letter
|
|
20
|
-
* Mark (U+061C), directional and zero-width format characters (U+200B,
|
|
21
|
-
* U+200E/200F, U+2060-2064, U+FEFF), and Unicode line/paragraph separators
|
|
22
|
-
* (U+2028/2029). Terminal emulators with bidi support (Windows Terminal,
|
|
23
|
-
* iTerm2, kitty, WezTerm) reorder or hide these, so they must never reach
|
|
24
|
-
* the terminal raw.
|
|
25
|
-
*/
|
|
26
|
-
const INVISIBLE_ESCAPE = /[\u061c\u200b\u200e\u200f\u2028\u2029\u202a-\u202e\u2060-\u2064\u2066-\u206f\ufeff]/gu
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Escape control and deceptive characters so externally sourced text cannot
|
|
30
|
-
* drive the terminal. C0/C1/DEL render as a literal `\xNN` escape; bidi,
|
|
31
|
-
* invisible-format, and separator controls render as a literal `\uXXXX`
|
|
32
|
-
* escape. Newlines and tabs survive (budgeted callers normalize tabs).
|
|
33
|
-
* @param text - raw text from a session event, tool payload, or catalog.
|
|
34
|
-
* @returns display-safe text with every injectable character made visible.
|
|
35
|
-
*/
|
|
36
|
-
export function displayText(text: string): string {
|
|
37
|
-
return text
|
|
38
|
-
.replace(CONTROL_ESCAPE, char => `\\x${char.charCodeAt(0).toString(16).padStart(2, '0')}`)
|
|
39
|
-
.replace(INVISIBLE_ESCAPE, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Collapse external text to one terminal-safe logical row. */
|
|
43
|
-
export function singleLineText(text: string): string {
|
|
44
|
-
return displayText(text).replace(/\r?\n/gu, ' ↵ ').replace(/\t/gu, ' ')
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Terminal-cell width matching the TUI's existing CJK-aware wrapping rule. */
|
|
48
|
-
function cellWidth(text: string): number {
|
|
49
|
-
let columns = 0
|
|
50
|
-
for (const char of text) {
|
|
51
|
-
columns += (char.codePointAt(0) ?? 0) > 0x2e7f ? 2 : 1
|
|
52
|
-
}
|
|
53
|
-
return columns
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Truncate one display-safe row without ever exceeding its physical-column
|
|
58
|
-
* budget. The ellipsis is included inside the budget, matching Codex's popup
|
|
59
|
-
* truncation contract; the previous app-local helper appended it after the
|
|
60
|
-
* row was already full and could force an extra terminal wrap.
|
|
61
|
-
*/
|
|
62
|
-
export function truncateColumns(text: string, columns: number): string {
|
|
63
|
-
const limit = Math.max(0, Math.floor(columns))
|
|
64
|
-
if (limit === 0) return ''
|
|
65
|
-
if (cellWidth(text) <= limit) return text
|
|
66
|
-
|
|
67
|
-
const contentLimit = limit - 1
|
|
68
|
-
let used = 0
|
|
69
|
-
let result = ''
|
|
70
|
-
for (const char of text) {
|
|
71
|
-
const width = cellWidth(char)
|
|
72
|
-
if (used + width > contentLimit) break
|
|
73
|
-
result += char
|
|
74
|
-
used += width
|
|
75
|
-
}
|
|
76
|
-
return `${result}…`
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** A display-safe suffix bounded by terminal rows and columns. */
|
|
80
|
-
export interface DisplayTail {
|
|
81
|
-
/** Sanitized suffix suitable for direct terminal rendering. */
|
|
82
|
-
text: string
|
|
83
|
-
/** Whether content before the returned suffix was omitted. */
|
|
84
|
-
truncated: boolean
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Read one Unicode character immediately before `end`. */
|
|
88
|
-
function previousCharacter(text: string, end: number): { char: string; start: number } {
|
|
89
|
-
const last = text.charCodeAt(end - 1)
|
|
90
|
-
if (last >= 0xdc00 && last <= 0xdfff && end >= 2) {
|
|
91
|
-
const first = text.charCodeAt(end - 2)
|
|
92
|
-
if (first >= 0xd800 && first <= 0xdbff) {
|
|
93
|
-
return { char: text.slice(end - 2, end), start: end - 2 }
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return { char: text.slice(end - 1, end), start: end - 1 }
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Keep only the newest display-safe text that fits a terminal rectangle.
|
|
101
|
-
* The scan walks backward and stops as soon as the suffix is full, so a long
|
|
102
|
-
* reasoning stream does not rescan its entire accumulated prefix per chunk.
|
|
103
|
-
* Explicit newlines and terminal wrapping both consume rows; tabs expand to
|
|
104
|
-
* two spaces so terminal tab stops (which render at contextual column 8
|
|
105
|
-
* boundaries, not at the budgeted cell count) cannot inflate the physical
|
|
106
|
-
* row count of the live region.
|
|
107
|
-
* @param text - raw externally sourced text.
|
|
108
|
-
* @param columns - available terminal columns.
|
|
109
|
-
* @param rows - available terminal rows.
|
|
110
|
-
* @returns a sanitized bounded suffix and whether an earlier prefix was cut.
|
|
111
|
-
*/
|
|
112
|
-
export function displayTail(text: string, columns: number, rows: number): DisplayTail {
|
|
113
|
-
const columnLimit = Math.max(1, Math.floor(columns))
|
|
114
|
-
const rowLimit = Math.max(1, Math.floor(rows))
|
|
115
|
-
const reversed: string[] = []
|
|
116
|
-
let row = 1
|
|
117
|
-
let used = 0
|
|
118
|
-
let end = text.length
|
|
119
|
-
|
|
120
|
-
while (end > 0) {
|
|
121
|
-
const previous = previousCharacter(text, end)
|
|
122
|
-
if (previous.char === '\n') {
|
|
123
|
-
if (row >= rowLimit) break
|
|
124
|
-
reversed.push('\n')
|
|
125
|
-
row += 1
|
|
126
|
-
used = 0
|
|
127
|
-
end = previous.start
|
|
128
|
-
continue
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const safe = previous.char === '\t' ? ' ' : displayText(previous.char)
|
|
132
|
-
const width = cellWidth(safe)
|
|
133
|
-
if (used > 0 && used + width > columnLimit) {
|
|
134
|
-
if (row >= rowLimit) break
|
|
135
|
-
// Materialize the soft wrap. Ink otherwise reflows at word boundaries
|
|
136
|
-
// and can turn a cell-counted two-row suffix into three rendered rows.
|
|
137
|
-
reversed.push('\n')
|
|
138
|
-
row += 1
|
|
139
|
-
used = 0
|
|
140
|
-
}
|
|
141
|
-
const extraRows = Math.floor(Math.max(0, width - 1) / columnLimit)
|
|
142
|
-
if (row + extraRows > rowLimit) break
|
|
143
|
-
row += extraRows
|
|
144
|
-
reversed.push(safe)
|
|
145
|
-
used = extraRows === 0 ? used + width : width - extraRows * columnLimit
|
|
146
|
-
end = previous.start
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return { text: reversed.reverse().join(''), truncated: end > 0 }
|
|
150
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Display-boundary sanitization for externally sourced text (model output,
|
|
3
|
+
* tool payloads, skill descriptions). Control characters — including ANSI
|
|
4
|
+
* CSI/OSC escape sequences — would otherwise pass through Ink into the
|
|
5
|
+
* terminal, letting output rewrite the screen or inject prompts. Newlines
|
|
6
|
+
* survive; everything else in C0/C1 plus DEL becomes a visible `\xNN`
|
|
7
|
+
* escape, and bidi overrides / invisible format controls / Unicode line and
|
|
8
|
+
* paragraph separators become a visible `\uXXXX` escape (terminal emulators
|
|
9
|
+
* that render bidirectional text would otherwise reorder the displayed
|
|
10
|
+
* glyphs and let a command read as something it is not).
|
|
11
|
+
*
|
|
12
|
+
* @module @deepseek-ai/dsh-code/render/text
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** C0 controls except tab (0x09) and newline (0x0a), plus DEL and C1. */
|
|
16
|
+
const CONTROL_ESCAPE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/gu
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Bidi overrides and isolates (U+202A-202E, U+2066-2069), the Arabic Letter
|
|
20
|
+
* Mark (U+061C), directional and zero-width format characters (U+200B,
|
|
21
|
+
* U+200E/200F, U+2060-2064, U+FEFF), and Unicode line/paragraph separators
|
|
22
|
+
* (U+2028/2029). Terminal emulators with bidi support (Windows Terminal,
|
|
23
|
+
* iTerm2, kitty, WezTerm) reorder or hide these, so they must never reach
|
|
24
|
+
* the terminal raw.
|
|
25
|
+
*/
|
|
26
|
+
const INVISIBLE_ESCAPE = /[\u061c\u200b\u200e\u200f\u2028\u2029\u202a-\u202e\u2060-\u2064\u2066-\u206f\ufeff]/gu
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Escape control and deceptive characters so externally sourced text cannot
|
|
30
|
+
* drive the terminal. C0/C1/DEL render as a literal `\xNN` escape; bidi,
|
|
31
|
+
* invisible-format, and separator controls render as a literal `\uXXXX`
|
|
32
|
+
* escape. Newlines and tabs survive (budgeted callers normalize tabs).
|
|
33
|
+
* @param text - raw text from a session event, tool payload, or catalog.
|
|
34
|
+
* @returns display-safe text with every injectable character made visible.
|
|
35
|
+
*/
|
|
36
|
+
export function displayText(text: string): string {
|
|
37
|
+
return text
|
|
38
|
+
.replace(CONTROL_ESCAPE, char => `\\x${char.charCodeAt(0).toString(16).padStart(2, '0')}`)
|
|
39
|
+
.replace(INVISIBLE_ESCAPE, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Collapse external text to one terminal-safe logical row. */
|
|
43
|
+
export function singleLineText(text: string): string {
|
|
44
|
+
return displayText(text).replace(/\r?\n/gu, ' ↵ ').replace(/\t/gu, ' ')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Terminal-cell width matching the TUI's existing CJK-aware wrapping rule. */
|
|
48
|
+
function cellWidth(text: string): number {
|
|
49
|
+
let columns = 0
|
|
50
|
+
for (const char of text) {
|
|
51
|
+
columns += (char.codePointAt(0) ?? 0) > 0x2e7f ? 2 : 1
|
|
52
|
+
}
|
|
53
|
+
return columns
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Truncate one display-safe row without ever exceeding its physical-column
|
|
58
|
+
* budget. The ellipsis is included inside the budget, matching Codex's popup
|
|
59
|
+
* truncation contract; the previous app-local helper appended it after the
|
|
60
|
+
* row was already full and could force an extra terminal wrap.
|
|
61
|
+
*/
|
|
62
|
+
export function truncateColumns(text: string, columns: number): string {
|
|
63
|
+
const limit = Math.max(0, Math.floor(columns))
|
|
64
|
+
if (limit === 0) return ''
|
|
65
|
+
if (cellWidth(text) <= limit) return text
|
|
66
|
+
|
|
67
|
+
const contentLimit = limit - 1
|
|
68
|
+
let used = 0
|
|
69
|
+
let result = ''
|
|
70
|
+
for (const char of text) {
|
|
71
|
+
const width = cellWidth(char)
|
|
72
|
+
if (used + width > contentLimit) break
|
|
73
|
+
result += char
|
|
74
|
+
used += width
|
|
75
|
+
}
|
|
76
|
+
return `${result}…`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A display-safe suffix bounded by terminal rows and columns. */
|
|
80
|
+
export interface DisplayTail {
|
|
81
|
+
/** Sanitized suffix suitable for direct terminal rendering. */
|
|
82
|
+
text: string
|
|
83
|
+
/** Whether content before the returned suffix was omitted. */
|
|
84
|
+
truncated: boolean
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Read one Unicode character immediately before `end`. */
|
|
88
|
+
function previousCharacter(text: string, end: number): { char: string; start: number } {
|
|
89
|
+
const last = text.charCodeAt(end - 1)
|
|
90
|
+
if (last >= 0xdc00 && last <= 0xdfff && end >= 2) {
|
|
91
|
+
const first = text.charCodeAt(end - 2)
|
|
92
|
+
if (first >= 0xd800 && first <= 0xdbff) {
|
|
93
|
+
return { char: text.slice(end - 2, end), start: end - 2 }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { char: text.slice(end - 1, end), start: end - 1 }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Keep only the newest display-safe text that fits a terminal rectangle.
|
|
101
|
+
* The scan walks backward and stops as soon as the suffix is full, so a long
|
|
102
|
+
* reasoning stream does not rescan its entire accumulated prefix per chunk.
|
|
103
|
+
* Explicit newlines and terminal wrapping both consume rows; tabs expand to
|
|
104
|
+
* two spaces so terminal tab stops (which render at contextual column 8
|
|
105
|
+
* boundaries, not at the budgeted cell count) cannot inflate the physical
|
|
106
|
+
* row count of the live region.
|
|
107
|
+
* @param text - raw externally sourced text.
|
|
108
|
+
* @param columns - available terminal columns.
|
|
109
|
+
* @param rows - available terminal rows.
|
|
110
|
+
* @returns a sanitized bounded suffix and whether an earlier prefix was cut.
|
|
111
|
+
*/
|
|
112
|
+
export function displayTail(text: string, columns: number, rows: number): DisplayTail {
|
|
113
|
+
const columnLimit = Math.max(1, Math.floor(columns))
|
|
114
|
+
const rowLimit = Math.max(1, Math.floor(rows))
|
|
115
|
+
const reversed: string[] = []
|
|
116
|
+
let row = 1
|
|
117
|
+
let used = 0
|
|
118
|
+
let end = text.length
|
|
119
|
+
|
|
120
|
+
while (end > 0) {
|
|
121
|
+
const previous = previousCharacter(text, end)
|
|
122
|
+
if (previous.char === '\n') {
|
|
123
|
+
if (row >= rowLimit) break
|
|
124
|
+
reversed.push('\n')
|
|
125
|
+
row += 1
|
|
126
|
+
used = 0
|
|
127
|
+
end = previous.start
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const safe = previous.char === '\t' ? ' ' : displayText(previous.char)
|
|
132
|
+
const width = cellWidth(safe)
|
|
133
|
+
if (used > 0 && used + width > columnLimit) {
|
|
134
|
+
if (row >= rowLimit) break
|
|
135
|
+
// Materialize the soft wrap. Ink otherwise reflows at word boundaries
|
|
136
|
+
// and can turn a cell-counted two-row suffix into three rendered rows.
|
|
137
|
+
reversed.push('\n')
|
|
138
|
+
row += 1
|
|
139
|
+
used = 0
|
|
140
|
+
}
|
|
141
|
+
const extraRows = Math.floor(Math.max(0, width - 1) / columnLimit)
|
|
142
|
+
if (row + extraRows > rowLimit) break
|
|
143
|
+
row += extraRows
|
|
144
|
+
reversed.push(safe)
|
|
145
|
+
used = extraRows === 0 ? used + width : width - extraRows * columnLimit
|
|
146
|
+
end = previous.start
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { text: reversed.reverse().join(''), truncated: end > 0 }
|
|
150
|
+
}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* @module @deepseek-ai/dsh-code/render/tool-detail
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { displayText, truncateColumns } from './text.ts'
|
|
15
|
+
|
|
14
16
|
/** Budgets keeping one expanded card bounded on a terminal. */
|
|
15
17
|
const MAX_DIFF_LINES = 200
|
|
16
18
|
const MAX_READ_LINES = 120
|
|
@@ -67,7 +69,7 @@ export type ToolDetail =
|
|
|
67
69
|
|
|
68
70
|
/** Truncate one line to the visible-column budget with an ellipsis marker. */
|
|
69
71
|
function clipLine(text: string): string {
|
|
70
|
-
return text
|
|
72
|
+
return truncateColumns(displayText(text), MAX_LINE_COLUMNS)
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
/** Split text into lines, dropping the trailing empty element of a final newline. */
|
package/src/session-directory.ts
CHANGED
|
@@ -59,9 +59,9 @@ export interface SessionRow {
|
|
|
59
59
|
/** Case-insensitive filesystems (Windows, macOS) compare paths by lowercased form. */
|
|
60
60
|
const CASE_INSENSITIVE_FS = process.platform === 'win32' || process.platform === 'darwin'
|
|
61
61
|
|
|
62
|
-
/** True
|
|
62
|
+
/** True only for delegated subagents; ordinary forks also carry lineage. */
|
|
63
63
|
export function isSubagentSession(header: SessionHeader): boolean {
|
|
64
|
-
return header.origin === 'subagent'
|
|
64
|
+
return header.origin === 'subagent'
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
function comparablePath(value: string): string {
|
|
@@ -79,7 +79,7 @@ function comparablePath(value: string): string {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/** Platform-consistent path equality for session cwd comparisons. */
|
|
82
|
-
|
|
82
|
+
function samePath(left: string | undefined, right: string): boolean {
|
|
83
83
|
if (left === undefined) return false
|
|
84
84
|
return comparablePath(left) === comparablePath(right)
|
|
85
85
|
}
|
package/src/startup.ts
CHANGED
|
@@ -1,119 +1,136 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The interactive terminal app's command-line provider: parses `--resume`,
|
|
3
|
-
* `--continue`, `--session`, `--mode`, `--theme`,
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* a
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
| { readonly kind: '
|
|
41
|
-
| { readonly kind: '
|
|
42
|
-
| { readonly kind: '
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
readonly
|
|
47
|
-
readonly
|
|
48
|
-
readonly
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The interactive terminal app's command-line provider: parses `--resume`,
|
|
3
|
+
* `--continue`, `--session`, `--mode`, `--theme`, `--image`, an optional
|
|
4
|
+
* initial prompt, and `--help`, then
|
|
5
|
+
* publishes {@link TUI_STARTUP_SERVICE} for the runner to consume lazily.
|
|
6
|
+
* Follows the headless bundle's startup shape (a commander action publishing
|
|
7
|
+
* a service through {@link parseCmdline}).
|
|
8
|
+
*
|
|
9
|
+
* Semantics:
|
|
10
|
+
* - `--resume <id|prefix>` — continue the persisted session whose id or unique
|
|
11
|
+
* id-prefix matches; the TUI replays its transcript and appends to the same
|
|
12
|
+
* durable log.
|
|
13
|
+
* - `--continue` / `-c` — resume the most recently modified persisted session
|
|
14
|
+
* whose project directory matches the current working directory.
|
|
15
|
+
* - `--session <id>` — create a new session under an explicit identity (the
|
|
16
|
+
* id must not exist yet).
|
|
17
|
+
* - `--theme <dark|light|auto>` — the color palette; auto follows the
|
|
18
|
+
* terminal (dark fallback until OSC-11 detection lands).
|
|
19
|
+
* - no flags — a fresh session with a minted id.
|
|
20
|
+
*
|
|
21
|
+
* @module @deepseek-ai/dsh-tui/startup
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { Command } from 'commander'
|
|
25
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
26
|
+
import { parseCmdline } from '@deepseek-ai/dsh-cmdline'
|
|
27
|
+
import { THEME_NAMES, type ThemeName } from './theme.ts'
|
|
28
|
+
|
|
29
|
+
/** Stable Cordis plugin name. */
|
|
30
|
+
export const name = 'tui-startup'
|
|
31
|
+
|
|
32
|
+
/** Services required before the invocation can be resolved. */
|
|
33
|
+
export const inject = ['cmdlineArgs']
|
|
34
|
+
|
|
35
|
+
/** Service provided by this plugin and injected by the terminal runner. */
|
|
36
|
+
const TUI_STARTUP_SERVICE = 'tuiStartup'
|
|
37
|
+
|
|
38
|
+
/** How the runner obtains its session identity. */
|
|
39
|
+
export type TuiStartup =
|
|
40
|
+
| ({ readonly kind: 'fresh'; readonly mode?: string } & TuiStartupInput)
|
|
41
|
+
| ({ readonly kind: 'named'; readonly sessionId: string; readonly mode?: string } & TuiStartupInput)
|
|
42
|
+
| ({ readonly kind: 'resume'; readonly sessionId: string } & TuiStartupInput)
|
|
43
|
+
| ({ readonly kind: 'latest' } & TuiStartupInput)
|
|
44
|
+
|
|
45
|
+
interface TuiStartupInput {
|
|
46
|
+
readonly theme?: ThemeName
|
|
47
|
+
readonly prompt?: string
|
|
48
|
+
readonly images?: readonly string[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface TuiStartupOptions {
|
|
52
|
+
readonly resume?: string
|
|
53
|
+
readonly continue?: boolean
|
|
54
|
+
readonly session?: string
|
|
55
|
+
readonly mode?: string
|
|
56
|
+
readonly theme?: ThemeName
|
|
57
|
+
readonly prompt?: string
|
|
58
|
+
readonly images?: readonly string[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Pure option policy shared by Commander and tests. */
|
|
62
|
+
export function resolveTuiStartup(options: TuiStartupOptions): TuiStartup {
|
|
63
|
+
const selected = [options.resume !== undefined, options.continue === true, options.session !== undefined]
|
|
64
|
+
if (selected.filter(Boolean).length > 1) throw new Error('--resume, --continue, and --session are mutually exclusive')
|
|
65
|
+
if (options.session === '') throw new Error('--session needs an id')
|
|
66
|
+
if (options.resume === '') throw new Error('--resume needs a session id or id prefix')
|
|
67
|
+
if (options.mode === '') throw new Error('--mode needs a preset id')
|
|
68
|
+
if (options.mode !== undefined && (options.resume !== undefined || options.continue === true)) {
|
|
69
|
+
throw new Error('--mode applies only to a new session; it cannot be combined with --resume or --continue')
|
|
70
|
+
}
|
|
71
|
+
if (options.theme !== undefined && !THEME_NAMES.includes(options.theme)) {
|
|
72
|
+
throw new Error('--theme must be dark, light, or auto')
|
|
73
|
+
}
|
|
74
|
+
const input = {
|
|
75
|
+
...(options.theme === undefined ? {} : { theme: options.theme }),
|
|
76
|
+
...(options.prompt === undefined || options.prompt.trim() === '' ? {} : { prompt: options.prompt.trim() }),
|
|
77
|
+
...(options.images === undefined || options.images.length === 0 ? {} : { images: [...options.images] }),
|
|
78
|
+
}
|
|
79
|
+
return options.resume !== undefined
|
|
80
|
+
? { kind: 'resume', sessionId: options.resume, ...input }
|
|
81
|
+
: options.continue === true
|
|
82
|
+
? { kind: 'latest', ...input }
|
|
83
|
+
: options.session !== undefined
|
|
84
|
+
? { kind: 'named', sessionId: options.session, ...options.mode === undefined ? {} : { mode: options.mode }, ...input }
|
|
85
|
+
: { kind: 'fresh', ...options.mode === undefined ? {} : { mode: options.mode }, ...input }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* This app's command: the launcher's flags this app owns, its description,
|
|
90
|
+
* and its help text.
|
|
91
|
+
* @returns a fresh program, so one process can parse more than once (tests).
|
|
92
|
+
*/
|
|
93
|
+
function tuiCommand(): Command {
|
|
94
|
+
return new Command()
|
|
95
|
+
.name('dsh --profile cli')
|
|
96
|
+
.description('DeepSeek Harness CLI core: the interactive coding terminal.')
|
|
97
|
+
.helpOption('-h, --help', 'show this help')
|
|
98
|
+
.option('-r, --resume <session>', 'resume the persisted session with this id (or unique id prefix)')
|
|
99
|
+
.option('-c, --continue', 'resume the most recent persisted session for this working directory')
|
|
100
|
+
.option('--session <id>', 'create a new session under this explicit id')
|
|
101
|
+
.option('--mode <preset>', 'agent preset for a newly created session')
|
|
102
|
+
.option('--theme <name>', 'color theme: dark (default), light, or auto')
|
|
103
|
+
.option('-i, --image <path>', 'attach an image to the initial prompt (repeatable)', (path, paths: string[]) => [...paths, path], [])
|
|
104
|
+
.argument('[prompt...]', 'initial prompt; sends immediately after startup')
|
|
105
|
+
.addHelpText('after', `
|
|
106
|
+
Examples:
|
|
107
|
+
dsh --profile cli fresh session, minted id
|
|
108
|
+
dsh --profile cli --resume abc123 resume session by id prefix
|
|
109
|
+
dsh --profile cli --continue resume the latest local session
|
|
110
|
+
dsh --profile cli --mode minimal fresh session using the minimal preset
|
|
111
|
+
dsh --profile cli --theme light light palette for bright terminals
|
|
112
|
+
dsh --profile cli "explain this repo" start and send an initial prompt
|
|
113
|
+
dsh --profile cli -i diagram.png "review this diagram"
|
|
114
|
+
`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Parse the invocation and publish the startup service. Mutual exclusions are
|
|
119
|
+
* usage errors rejected from the action before anything is provided.
|
|
120
|
+
* @param ctx - plugin context carrying the command line and exit request.
|
|
121
|
+
*/
|
|
122
|
+
export function apply(ctx: Context): void {
|
|
123
|
+
const program = tuiCommand()
|
|
124
|
+
program.action((prompt: string[]) => {
|
|
125
|
+
const options = { ...program.opts<TuiStartupOptions>(), prompt: prompt.join(' ') }
|
|
126
|
+
let startup: TuiStartup | undefined
|
|
127
|
+
try {
|
|
128
|
+
startup = resolveTuiStartup(options)
|
|
129
|
+
} catch (error: unknown) {
|
|
130
|
+
program.error(`error: ${error instanceof Error ? error.message : String(error)}`)
|
|
131
|
+
}
|
|
132
|
+
if (startup === undefined) return
|
|
133
|
+
ctx.provide(TUI_STARTUP_SERVICE, { startup } satisfies { startup: TuiStartup })
|
|
134
|
+
})
|
|
135
|
+
parseCmdline(ctx, program)
|
|
136
|
+
}
|