dsh-code 0.2.0 → 0.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.md +7 -2
- package/README.zh.md +7 -2
- package/cordis.patch.yml +6 -0
- package/lib/index.mjs +1091 -105
- package/lib/types/app.d.ts +29 -0
- package/lib/types/mentions.d.ts +70 -0
- package/lib/types/questions.d.ts +48 -0
- package/lib/types/render/animations.d.ts +15 -0
- package/lib/types/render/markdown.d.ts +27 -0
- package/lib/types/render/projection.d.ts +13 -0
- package/lib/types/render/status.d.ts +4 -0
- package/lib/types/render/tool-preview.d.ts +15 -0
- package/lib/types/theme.d.ts +4 -0
- package/package.json +10 -2
- package/src/app.ts +582 -78
- package/src/index.ts +110 -46
- package/src/mentions.ts +193 -0
- package/src/questions.ts +143 -0
- package/src/render/animations.ts +22 -0
- package/src/render/markdown.ts +235 -0
- package/src/render/projection.ts +48 -7
- package/src/render/status.ts +14 -2
- package/src/render/tool-preview.ts +34 -0
- package/src/theme.ts +4 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal markdown renderer for assistant replies: a pure GFM-subset
|
|
3
|
+
* block/inline parser producing styled line segments the Ink renderer maps
|
|
4
|
+
* to colored text. No ANSI here — the app owns color mapping, tests own the
|
|
5
|
+
* structure. The subset mirrors what agent replies actually emit: headings,
|
|
6
|
+
* emphasis, inline/fenced code, flat lists, blockquotes, links, rules, and
|
|
7
|
+
* wrapped paragraphs. Unknown syntax degrades to plain text (never throws).
|
|
8
|
+
*
|
|
9
|
+
* @module @deepseek-ai/dsh-code/render/markdown
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Style classes the renderer emits; the app maps them to colors/props. */
|
|
13
|
+
export type MdStyle = 'plain' | 'bold' | 'italic' | 'boldItalic' | 'code' | 'accent' | 'dim' | 'strike'
|
|
14
|
+
|
|
15
|
+
/** One styled run of text. */
|
|
16
|
+
export interface MdSegment {
|
|
17
|
+
/** Visible text (no ANSI). */
|
|
18
|
+
text: string
|
|
19
|
+
/** Presentation class for the app's color map. */
|
|
20
|
+
style: MdStyle
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** One rendered line: a sequence of styled runs. */
|
|
24
|
+
export interface MdLine {
|
|
25
|
+
segments: readonly MdSegment[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Plain segment helper. */
|
|
29
|
+
function seg(text: string, style: MdStyle = 'plain'): MdSegment {
|
|
30
|
+
return { text, style }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Visible width of a run in columns (CJK counts double). */
|
|
34
|
+
export function visibleColumns(text: string): number {
|
|
35
|
+
let columns = 0
|
|
36
|
+
for (const char of text) {
|
|
37
|
+
const code = char.codePointAt(0) ?? 0
|
|
38
|
+
columns += code > 0x2e7f ? 2 : 1
|
|
39
|
+
}
|
|
40
|
+
return columns
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Walk a segment list, breaking it into lines that fit `width` columns. */
|
|
44
|
+
function wrapSegments(segments: readonly MdSegment[], width: number): readonly MdSegment[][] {
|
|
45
|
+
const lines: MdSegment[][] = []
|
|
46
|
+
let current: MdSegment[] = []
|
|
47
|
+
let used = 0
|
|
48
|
+
for (const segment of segments) {
|
|
49
|
+
// Break the segment at spaces into words so long runs wrap mid-text.
|
|
50
|
+
const words = segment.text.split(/( )/u)
|
|
51
|
+
for (const word of words) {
|
|
52
|
+
if (word === '') continue
|
|
53
|
+
const columns = visibleColumns(word)
|
|
54
|
+
if (used + columns > width && used > 0) {
|
|
55
|
+
lines.push(current)
|
|
56
|
+
current = []
|
|
57
|
+
used = 0
|
|
58
|
+
}
|
|
59
|
+
// A single word wider than the line still goes on its own line.
|
|
60
|
+
current.push({ text: word, style: segment.style })
|
|
61
|
+
used += columns
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (current.length > 0) lines.push(current)
|
|
65
|
+
// Drop the trailing space a wrapped line picked up before the break.
|
|
66
|
+
return lines.map(line => {
|
|
67
|
+
const last = line[line.length - 1]
|
|
68
|
+
if (last !== undefined && last.text === ' ' && line.length > 1) return line.slice(0, -1)
|
|
69
|
+
return line
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Join adjacent same-style runs so the app renders fewer elements. */
|
|
74
|
+
function merge(segments: readonly MdSegment[]): readonly MdSegment[] {
|
|
75
|
+
const merged: MdSegment[] = []
|
|
76
|
+
for (const segment of segments) {
|
|
77
|
+
const last = merged[merged.length - 1]
|
|
78
|
+
if (last !== undefined && last.style === segment.style) {
|
|
79
|
+
merged[merged.length - 1] = { text: last.text + segment.text, style: last.style }
|
|
80
|
+
} else {
|
|
81
|
+
merged.push({ ...segment })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return merged
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** One parsed inline run before wrapping. */
|
|
88
|
+
interface InlineRun {
|
|
89
|
+
text: string
|
|
90
|
+
style: MdStyle
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse inline markdown in one line of text. Link destinations render as a
|
|
95
|
+
* dim `(url)` suffix — the visible text keeps the accent.
|
|
96
|
+
*/
|
|
97
|
+
function parseInline(text: string): readonly InlineRun[] {
|
|
98
|
+
const runs: InlineRun[] = []
|
|
99
|
+
let rest = text
|
|
100
|
+
while (rest !== '') {
|
|
101
|
+
const code = /^`([^`]+)`/u.exec(rest)
|
|
102
|
+
if (code !== null) {
|
|
103
|
+
runs.push({ text: code[1] ?? '', style: 'code' })
|
|
104
|
+
rest = rest.slice(code[0].length)
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
const boldItalic = /^\*\*\*([^*]+)\*\*\*/u.exec(rest)
|
|
108
|
+
if (boldItalic !== null) {
|
|
109
|
+
runs.push({ text: boldItalic[1] ?? '', style: 'boldItalic' })
|
|
110
|
+
rest = rest.slice(boldItalic[0].length)
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
const bold = /^\*\*([^*]+)\*\*/u.exec(rest)
|
|
114
|
+
if (bold !== null) {
|
|
115
|
+
runs.push({ text: bold[1] ?? '', style: 'bold' })
|
|
116
|
+
rest = rest.slice(bold[0].length)
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
const italic = /^\*([^*]+)\*/u.exec(rest) ?? /^_([^_]+)_/u.exec(rest)
|
|
120
|
+
if (italic !== null) {
|
|
121
|
+
runs.push({ text: italic[1] ?? '', style: 'italic' })
|
|
122
|
+
rest = rest.slice(italic[0].length)
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
const strike = /^~~([^~]+)~~/u.exec(rest)
|
|
126
|
+
if (strike !== null) {
|
|
127
|
+
runs.push({ text: strike[1] ?? '', style: 'strike' })
|
|
128
|
+
rest = rest.slice(strike[0].length)
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
const link = /^\[([^\]]+)\]\(([^)\s]+)\)/u.exec(rest)
|
|
132
|
+
if (link !== null) {
|
|
133
|
+
const label = link[1] ?? ''
|
|
134
|
+
const url = link[2] ?? ''
|
|
135
|
+
runs.push({ text: label, style: 'accent' })
|
|
136
|
+
runs.push({ text: ` (${url})`, style: 'dim' })
|
|
137
|
+
rest = rest.slice(link[0].length)
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
// Plain run up to the next special opener.
|
|
141
|
+
const next = rest.search(/[*_`~[]/u)
|
|
142
|
+
if (next === -1) {
|
|
143
|
+
runs.push({ text: rest, style: 'plain' })
|
|
144
|
+
break
|
|
145
|
+
}
|
|
146
|
+
if (next > 0) {
|
|
147
|
+
runs.push({ text: rest.slice(0, next), style: 'plain' })
|
|
148
|
+
rest = rest.slice(next)
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
// A special opener at position 0 that no pattern consumed: emit it
|
|
152
|
+
// literally and advance, so unbalanced syntax never loops.
|
|
153
|
+
runs.push({ text: rest.slice(0, 1), style: 'plain' })
|
|
154
|
+
rest = rest.slice(1)
|
|
155
|
+
}
|
|
156
|
+
return runs
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const HEADING = /^(#{1,6})\s+(.*)$/u
|
|
160
|
+
const FENCE = /^```([^\s`]*)\s*$/u
|
|
161
|
+
const RULE = /^(?:---|\*\*\*|___)\s*$/u
|
|
162
|
+
const QUOTE = /^>\s?(.*)$/u
|
|
163
|
+
const UNORDERED = /^\s*[-*+]\s+(.*)$/u
|
|
164
|
+
const ORDERED = /^\s*(\d+)[.)]\s+(.*)$/u
|
|
165
|
+
|
|
166
|
+
/** Render markdown text into styled lines of at most `width` columns. */
|
|
167
|
+
export function renderMarkdown(text: string, width: number): readonly MdLine[] {
|
|
168
|
+
const lines: MdLine[] = []
|
|
169
|
+
const push = (segments: readonly MdSegment[]): void => {
|
|
170
|
+
for (const wrapped of wrapSegments(segments, Math.max(10, width))) {
|
|
171
|
+
lines.push({ segments: merge(wrapped) })
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const raw = text.replaceAll('\r', '')
|
|
175
|
+
const source = raw.split('\n')
|
|
176
|
+
let index = 0
|
|
177
|
+
while (index < source.length) {
|
|
178
|
+
const line = source[index] ?? ''
|
|
179
|
+
index += 1
|
|
180
|
+
|
|
181
|
+
// Fenced code block: verbatim lines in code style, language label first.
|
|
182
|
+
const fence = FENCE.exec(line)
|
|
183
|
+
if (fence !== null) {
|
|
184
|
+
const language = fence[1] ?? ''
|
|
185
|
+
if (language !== '') push([seg(` ${language}`, 'dim')])
|
|
186
|
+
while (index < source.length && !FENCE.test(source[index] ?? '')) {
|
|
187
|
+
push([seg(` ${source[index] ?? ''}`, 'code')])
|
|
188
|
+
index += 1
|
|
189
|
+
}
|
|
190
|
+
index += 1 // closing fence
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (line.trim() === '') continue
|
|
195
|
+
if (RULE.test(line.trim())) {
|
|
196
|
+
push([seg(` ${'─'.repeat(Math.max(1, Math.floor(width / 4)))}`, 'dim')])
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
const heading = HEADING.exec(line)
|
|
200
|
+
if (heading !== null) {
|
|
201
|
+
push([seg(heading[2] ?? '', 'accent')])
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
const quote = QUOTE.exec(line)
|
|
205
|
+
if (quote !== null) {
|
|
206
|
+
push([seg(' │ ', 'accent'), ...parseInline(quote[1] ?? '').map(run => seg(run.text, run.style === 'plain' ? 'dim' : run.style))])
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
const ordered = ORDERED.exec(line)
|
|
210
|
+
if (ordered !== null) {
|
|
211
|
+
push([seg(` ${ordered[1] ?? ''}. `, 'accent'), ...parseInline(ordered[2] ?? '').map(run => seg(run.text, run.style))])
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
const unordered = UNORDERED.exec(line)
|
|
215
|
+
if (unordered !== null) {
|
|
216
|
+
push([seg(' • ', 'accent'), ...parseInline(unordered[1] ?? '').map(run => seg(run.text, run.style))])
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Paragraph: gather until a blank line, then wrap as one flow. Line
|
|
221
|
+
// breaks inside a paragraph join as a single space (GFM soft breaks).
|
|
222
|
+
const paragraph = [line]
|
|
223
|
+
while (index < source.length && (source[index] ?? '').trim() !== '') {
|
|
224
|
+
paragraph.push(source[index] ?? '')
|
|
225
|
+
index += 1
|
|
226
|
+
}
|
|
227
|
+
const runs: InlineRun[] = []
|
|
228
|
+
for (let at = 0; at < paragraph.length; at += 1) {
|
|
229
|
+
if (at > 0) runs.push({ text: ' ', style: 'plain' })
|
|
230
|
+
runs.push(...parseInline(paragraph[at] ?? ''))
|
|
231
|
+
}
|
|
232
|
+
push(runs.map(run => seg(run.text, run.style)))
|
|
233
|
+
}
|
|
234
|
+
return lines
|
|
235
|
+
}
|
package/src/render/projection.ts
CHANGED
|
@@ -10,14 +10,21 @@
|
|
|
10
10
|
import { boundContextSummary, type ContentBlock } from '@deepseek-ai/dsh-llm'
|
|
11
11
|
import type { SessionEvent, TodoItem } from '@deepseek-ai/dsh-session'
|
|
12
12
|
// Type-only imports merge the plugin-owned SessionEventMap variants (command/*
|
|
13
|
-
// from dsh-commands) into the union this reducer
|
|
13
|
+
// from dsh-commands, plan/mode, permission/preset) into the union this reducer
|
|
14
|
+
// switches on.
|
|
14
15
|
import type {} from '@deepseek-ai/dsh-commands'
|
|
16
|
+
import type {} from '@deepseek-ai/dsh-plan-mode'
|
|
17
|
+
import type {} from '@deepseek-ai/dsh-permission-presets'
|
|
18
|
+
import { toolArgumentsPreview } from './tool-preview.ts'
|
|
15
19
|
|
|
16
20
|
/** One user prompt line. */
|
|
17
21
|
export interface UserEntry {
|
|
18
22
|
kind: 'user'
|
|
19
23
|
/** Joined text blocks of the user message. */
|
|
20
24
|
text: string
|
|
25
|
+
/** True for collapsed injected context (plugin/continuation notices), which
|
|
26
|
+
* the renderer marks with a dim ↳ instead of the user ❯ prompt. */
|
|
27
|
+
notice: boolean
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
/** One assembled assistant reply. */
|
|
@@ -25,6 +32,8 @@ export interface AssistantEntry {
|
|
|
25
32
|
kind: 'assistant'
|
|
26
33
|
/** Joined text blocks of the assistant message. */
|
|
27
34
|
text: string
|
|
35
|
+
/** Joined reasoning blocks of the same message, empty when the model thought out loud. */
|
|
36
|
+
reasoning: string
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
/** One model-requested tool invocation and its settled state. */
|
|
@@ -36,6 +45,8 @@ export interface ToolEntry {
|
|
|
36
45
|
name: string
|
|
37
46
|
/** Raw arguments JSON string exactly as the model produced it. */
|
|
38
47
|
arguments: string
|
|
48
|
+
/** Bounded human-meaningful arguments preview for the tool card. */
|
|
49
|
+
preview: string
|
|
39
50
|
/** Execution state; `running` until the paired result lands. */
|
|
40
51
|
state: 'running' | 'done' | 'error'
|
|
41
52
|
/** Bounded first text block of the result, empty until it lands. */
|
|
@@ -97,6 +108,8 @@ export interface TranscriptView {
|
|
|
97
108
|
entries: readonly TranscriptEntry[]
|
|
98
109
|
/** Text accumulated from `assistant/chunk` deltas since the last flush. */
|
|
99
110
|
streaming: string
|
|
111
|
+
/** Thinking accumulated from `assistant/chunk` reasoning deltas since the last flush. */
|
|
112
|
+
streamingReasoning: string
|
|
100
113
|
/** Latest whole-list todo snapshot from `todo/write`, empty when none. */
|
|
101
114
|
todos: readonly TodoItem[]
|
|
102
115
|
/** True while a durable turn is open (`turn/start` … `turn/end`). */
|
|
@@ -110,6 +123,10 @@ export interface TranscriptView {
|
|
|
110
123
|
* Empty before the session's first request.
|
|
111
124
|
*/
|
|
112
125
|
model: string
|
|
126
|
+
/** Plan mode state folded from the last `plan/mode` event. */
|
|
127
|
+
plan: boolean
|
|
128
|
+
/** Active permission preset folded from the last `permission/preset` event, empty before one. */
|
|
129
|
+
permission: string
|
|
113
130
|
/**
|
|
114
131
|
* Fold-internal timing anchors, never rendered: open step and tool-call
|
|
115
132
|
* start timestamps the next `assistant/message` / `tool/result` resolves
|
|
@@ -123,14 +140,22 @@ function textOf(content: readonly ContentBlock[]): string {
|
|
|
123
140
|
return content.filter(block => block.type === 'text').map(block => block.text).join('')
|
|
124
141
|
}
|
|
125
142
|
|
|
143
|
+
/** Join the reasoning blocks of a content list; non-reasoning blocks contribute nothing. */
|
|
144
|
+
function reasoningOf(content: readonly ContentBlock[]): string {
|
|
145
|
+
return content.filter(block => block.type === 'reasoning').map(block => block.text).join('')
|
|
146
|
+
}
|
|
147
|
+
|
|
126
148
|
/** A fresh, empty transcript view. */
|
|
127
149
|
export function createTranscriptView(): TranscriptView {
|
|
128
150
|
return {
|
|
129
151
|
entries: [],
|
|
130
152
|
streaming: '',
|
|
153
|
+
streamingReasoning: '',
|
|
131
154
|
todos: [],
|
|
132
155
|
busy: false,
|
|
133
156
|
model: '',
|
|
157
|
+
plan: false,
|
|
158
|
+
permission: '',
|
|
134
159
|
stats: { turns: 0, steps: 0, llmMs: 0, toolMs: 0, usage: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 } },
|
|
135
160
|
anchors: { stepStart: new Map(), toolStart: new Map() },
|
|
136
161
|
}
|
|
@@ -150,20 +175,25 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
150
175
|
// elsewhere in the product; only direct human prompts render in full.
|
|
151
176
|
const message = event.data
|
|
152
177
|
if (message.source.kind === 'user') {
|
|
153
|
-
return { ...view, entries: [...view.entries, { kind: 'user', text: textOf(message.content) }] }
|
|
178
|
+
return { ...view, entries: [...view.entries, { kind: 'user', text: textOf(message.content), notice: false }] }
|
|
154
179
|
}
|
|
155
180
|
const notice = message.source.kind === 'plugin' && message.source.form === 'notice'
|
|
156
181
|
? message.source.summary
|
|
157
182
|
: message.source.kind
|
|
158
|
-
return { ...view, entries: [...view.entries, { kind: 'user', text: boundContextSummary(notice) }] }
|
|
183
|
+
return { ...view, entries: [...view.entries, { kind: 'user', text: boundContextSummary(notice), notice: true }] }
|
|
159
184
|
}
|
|
160
185
|
case 'assistant/chunk': {
|
|
161
186
|
const chunk = event.data.chunk
|
|
162
|
-
if (chunk.type
|
|
163
|
-
|
|
187
|
+
if (chunk.type === 'text-delta') {
|
|
188
|
+
return { ...view, streaming: view.streaming + chunk.text }
|
|
189
|
+
}
|
|
190
|
+
if (chunk.type === 'reasoning-delta') {
|
|
191
|
+
return { ...view, streamingReasoning: view.streamingReasoning + chunk.text }
|
|
192
|
+
}
|
|
193
|
+
return view
|
|
164
194
|
}
|
|
165
195
|
case 'assistant/message': {
|
|
166
|
-
// The assembled message is authoritative; drop the streamed
|
|
196
|
+
// The assembled message is authoritative; drop the streamed buffers.
|
|
167
197
|
const key = `${event.data.turn}:${event.data.step}`
|
|
168
198
|
const started = view.anchors.stepStart.get(key)
|
|
169
199
|
view.anchors.stepStart.delete(key)
|
|
@@ -172,7 +202,12 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
172
202
|
return {
|
|
173
203
|
...view,
|
|
174
204
|
streaming: '',
|
|
175
|
-
|
|
205
|
+
streamingReasoning: '',
|
|
206
|
+
entries: [...view.entries, {
|
|
207
|
+
kind: 'assistant',
|
|
208
|
+
text: textOf(event.data.message.content),
|
|
209
|
+
reasoning: reasoningOf(event.data.message.content),
|
|
210
|
+
}],
|
|
176
211
|
stats: {
|
|
177
212
|
...view.stats,
|
|
178
213
|
llmMs: view.stats.llmMs + (started === undefined ? 0 : Math.max(0, event.time - started)),
|
|
@@ -194,6 +229,7 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
194
229
|
callId: data.callId,
|
|
195
230
|
name: data.name,
|
|
196
231
|
arguments: data.arguments,
|
|
232
|
+
preview: toolArgumentsPreview(data.arguments, data.name),
|
|
197
233
|
state: 'running',
|
|
198
234
|
summary: '',
|
|
199
235
|
}],
|
|
@@ -247,6 +283,11 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
247
283
|
const config = event.data.header.config
|
|
248
284
|
return { ...view, model: `${config.provider}/${config.model}` }
|
|
249
285
|
}
|
|
286
|
+
case 'plan/mode':
|
|
287
|
+
// Whole-value replace; the last one wins (upstream fold semantics).
|
|
288
|
+
return { ...view, plan: event.data.active }
|
|
289
|
+
case 'permission/preset':
|
|
290
|
+
return { ...view, permission: event.data.preset }
|
|
250
291
|
case 'command/run': {
|
|
251
292
|
const data = event.data
|
|
252
293
|
return {
|
package/src/render/status.ts
CHANGED
|
@@ -57,6 +57,10 @@ export interface StatusFacts {
|
|
|
57
57
|
branch: string
|
|
58
58
|
/** Short session identifier (last dash-separated segment or tail). */
|
|
59
59
|
sessionId: string
|
|
60
|
+
/** Whether plan mode is active (folded from `plan/mode`). */
|
|
61
|
+
plan: boolean
|
|
62
|
+
/** Active permission preset (folded from `permission/preset`), empty when unknown. */
|
|
63
|
+
permission: string
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
/**
|
|
@@ -67,8 +71,12 @@ export interface StatusFacts {
|
|
|
67
71
|
*/
|
|
68
72
|
export function buildStatusGroups(facts: StatusFacts, stats: TranscriptStats): string[] {
|
|
69
73
|
const groups: string[] = []
|
|
70
|
-
const identity = [
|
|
71
|
-
.
|
|
74
|
+
const identity = [
|
|
75
|
+
facts.model,
|
|
76
|
+
facts.cwd,
|
|
77
|
+
facts.branch === '' ? undefined : `⑂ ${facts.branch}`,
|
|
78
|
+
facts.plan ? '⧉ plan' : undefined,
|
|
79
|
+
].filter(part => part !== undefined && part !== '')
|
|
72
80
|
if (identity.length > 0) groups.push(identity.join(' · '))
|
|
73
81
|
if (stats.turns > 0 || stats.steps > 0) {
|
|
74
82
|
groups.push(`T${stats.turns} · S${stats.steps}`)
|
|
@@ -83,5 +91,9 @@ export function buildStatusGroups(facts: StatusFacts, stats: TranscriptStats): s
|
|
|
83
91
|
groups.push(`↑${formatTokens(stats.usage.inputTokens)} ↓${formatTokens(stats.usage.outputTokens)}`)
|
|
84
92
|
}
|
|
85
93
|
if (facts.sessionId !== '') groups.push(facts.sessionId)
|
|
94
|
+
// The permission preset trails the line: switching it changes only the
|
|
95
|
+
// tail, so the left-aligned bar never shifts its other groups. Plain text,
|
|
96
|
+
// the Claude-Code permission-mode display (no glyphs).
|
|
97
|
+
if (facts.permission !== undefined && facts.permission !== '') groups.push(facts.permission)
|
|
86
98
|
return groups
|
|
87
99
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded preview line for a tool invocation's raw JSON arguments: the first
|
|
3
|
+
* human-meaningful string among the well-known keys (command, path, query, …)
|
|
4
|
+
* with a fallback to the bounded raw JSON. Shared by the tool card in the
|
|
5
|
+
* transcript and the approval bar's command preview.
|
|
6
|
+
*
|
|
7
|
+
* @module @deepseek-ai/dsh-code/render/tool-preview
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Keys searched in declaration order when building a preview. */
|
|
11
|
+
const PREVIEW_KEYS = ['command', 'cmd', 'description', 'path', 'pattern', 'query'] as const
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve one bounded preview for raw tool arguments.
|
|
15
|
+
* @param args - raw JSON arguments string as the model produced it.
|
|
16
|
+
* @param toolName - the tool the arguments belong to (fallback label).
|
|
17
|
+
* @returns the preview line; empty when nothing useful resolves.
|
|
18
|
+
*/
|
|
19
|
+
export function toolArgumentsPreview(args: string, toolName: string): string {
|
|
20
|
+
if (args === '') return toolName
|
|
21
|
+
try {
|
|
22
|
+
const parsed: unknown = JSON.parse(args)
|
|
23
|
+
if (parsed !== null && typeof parsed === 'object') {
|
|
24
|
+
const record = parsed as Record<string, unknown>
|
|
25
|
+
for (const key of PREVIEW_KEYS) {
|
|
26
|
+
const value = record[key]
|
|
27
|
+
if (typeof value === 'string' && value !== '') return value
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Raw JSON parse failed: fall through to the bounded raw arguments.
|
|
32
|
+
}
|
|
33
|
+
return args.length > 80 ? `${args.slice(0, 77)}...` : args
|
|
34
|
+
}
|
package/src/theme.ts
CHANGED
|
@@ -28,6 +28,10 @@ export const TUI_RGB = {
|
|
|
28
28
|
error: [239, 68, 68],
|
|
29
29
|
/** Warning amber — `--dsw-static-amber-500`. */
|
|
30
30
|
warn: [245, 158, 11],
|
|
31
|
+
/** Default foreground text — `--dsw-static-neutral-50`. */
|
|
32
|
+
text: [236, 240, 246],
|
|
33
|
+
/** Inline/fenced code — soft sky blue, distinct from brand accents. */
|
|
34
|
+
code: [125, 211, 252],
|
|
31
35
|
} as const satisfies Record<string, readonly [number, number, number]>
|
|
32
36
|
|
|
33
37
|
/** Paint with the primary brand blue: whale, wordmark, tool names, accents. */
|