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.
- package/README.en.md +123 -26
- package/README.md +124 -27
- package/bin/deepseek.mjs +283 -35
- package/cordis.patch.yml +97 -0
- package/lib/index.mjs +5008 -881
- package/lib/session-query.mjs +150 -0
- package/lib/startup.mjs +4 -4
- package/lib/{theme-DCT8Y2xf.mjs → theme-7u5Qo3dF.mjs} +657 -20
- package/lib/types/app.d.ts +106 -62
- package/lib/types/authorization-panel.d.ts +3 -3
- package/lib/types/git-workflow.d.ts +91 -2
- package/lib/types/i18n.d.ts +39 -0
- package/lib/types/index.d.ts +100 -1
- package/lib/types/input-split.d.ts +1 -1
- package/lib/types/kernel-panels.d.ts +107 -29
- package/lib/types/language-panel.d.ts +12 -0
- package/lib/types/locales/en.d.ts +450 -0
- package/lib/types/locales/zh.d.ts +9 -0
- package/lib/types/mentions.d.ts +7 -3
- package/lib/types/models.d.ts +14 -0
- package/lib/types/panel-accent.d.ts +28 -0
- package/lib/types/rainbow.d.ts +69 -0
- package/lib/types/render/animations.d.ts +42 -0
- package/lib/types/render/editor.d.ts +4 -3
- package/lib/types/render/ime-cursor.d.ts +60 -0
- package/lib/types/render/inspector.d.ts +26 -0
- package/lib/types/render/lines.d.ts +21 -1
- package/lib/types/render/markdown.d.ts +1 -1
- package/lib/types/render/projection.d.ts +130 -4
- package/lib/types/render/status.d.ts +9 -9
- package/lib/types/render/text.d.ts +6 -0
- package/lib/types/render/usage.d.ts +113 -0
- package/lib/types/session-directory.d.ts +17 -0
- package/lib/types/session-query.d.ts +92 -0
- package/lib/types/startup.d.ts +1 -1
- package/lib/types/terminal-title.d.ts +66 -0
- package/lib/types/theme-panel.d.ts +2 -2
- package/lib/types/theme.d.ts +271 -52
- package/lib/types/update-panel.d.ts +49 -0
- package/lib/types/update.d.ts +75 -0
- package/lib/types/version.d.ts +4 -3
- package/package.json +246 -90
- package/src/app.ts +1369 -509
- package/src/approval.ts +166 -166
- package/src/authorization-panel.ts +19 -16
- package/src/editor-keys.ts +371 -371
- package/src/git-workflow.ts +229 -3
- package/src/i18n.ts +68 -0
- package/src/index.ts +534 -80
- package/src/input-split.ts +3 -3
- package/src/internals.ts +5 -0
- package/src/kernel-panels.ts +554 -86
- package/src/keyboard.ts +5 -4
- package/src/language-panel.ts +53 -0
- package/src/locales/en.ts +489 -0
- package/src/locales/zh.ts +488 -0
- package/src/mentions.ts +8 -4
- package/src/models.ts +264 -212
- package/src/panel-accent.ts +41 -0
- package/src/presets.ts +1 -1
- package/src/provider-settings.ts +1 -1
- package/src/rainbow.ts +208 -0
- package/src/render/animations.ts +104 -6
- package/src/render/editor.ts +25 -24
- package/src/render/export.ts +116 -95
- package/src/render/ime-cursor.ts +147 -0
- package/src/render/inspector.ts +42 -0
- package/src/render/lines.ts +628 -415
- package/src/render/markdown.ts +15 -3
- package/src/render/projection.ts +572 -21
- package/src/render/status.ts +59 -39
- package/src/render/text.ts +14 -0
- package/src/render/tool-preview.ts +77 -77
- package/src/render/usage.ts +430 -0
- package/src/render/width.ts +2 -2
- package/src/session-directory.ts +8 -6
- package/src/session-query.ts +239 -0
- package/src/startup.ts +3 -3
- package/src/subagents.ts +229 -229
- package/src/terminal-title.ts +190 -0
- package/src/theme-panel.ts +17 -21
- package/src/theme.ts +281 -33
- package/src/update-panel.ts +256 -0
- package/src/update.ts +126 -0
- package/src/version.ts +58 -20
- package/src/whale-glyph.ts +23 -23
package/src/render/status.ts
CHANGED
|
@@ -12,21 +12,13 @@
|
|
|
12
12
|
|
|
13
13
|
import { visibleColumns } from './markdown.ts'
|
|
14
14
|
import type { TranscriptStats } from './projection.ts'
|
|
15
|
-
import {
|
|
15
|
+
import { billedInputTokens } from './usage.ts'
|
|
16
|
+
import { singleLineText, truncateColumns, formatTokens } from './text.ts'
|
|
17
|
+
|
|
18
|
+
// Re-exported for the callers that have always read the formatter here.
|
|
19
|
+
export { formatTokens }
|
|
20
|
+
import { t } from '../i18n.ts'
|
|
16
21
|
|
|
17
|
-
/**
|
|
18
|
-
* Compact token count: 517 / 12.2K / 517K / 1.2M (one decimal under three
|
|
19
|
-
* digits), mirroring the web composer's StatsLine format.
|
|
20
|
-
* @param n - token count.
|
|
21
|
-
* @returns display string.
|
|
22
|
-
*/
|
|
23
|
-
export function formatTokens(n: number): string {
|
|
24
|
-
const scaled = (v: number): string =>
|
|
25
|
-
v >= 100 ? String(Math.round(v)) : String(Math.round(v * 10) / 10)
|
|
26
|
-
if (n < 1_000) return String(n)
|
|
27
|
-
if (n < 1_000_000) return scaled(n / 1_000) + 'K'
|
|
28
|
-
return scaled(n / 1_000_000) + 'M'
|
|
29
|
-
}
|
|
30
22
|
|
|
31
23
|
/**
|
|
32
24
|
* Compact duration: 45.2s under a minute, 2m42s from there on.
|
|
@@ -53,14 +45,17 @@ function formatRate(n: number): string {
|
|
|
53
45
|
}
|
|
54
46
|
|
|
55
47
|
/**
|
|
56
|
-
* Cache-hit share of billed prompt-side input.
|
|
48
|
+
* Cache-hit share of billed prompt-side input. The denominator is the same
|
|
49
|
+
* billed total the /usage panel shows (uncached input plus both cache
|
|
50
|
+
* buckets), so the two readouts can never disagree.
|
|
57
51
|
* @param usage - cumulative token totals.
|
|
58
52
|
* @returns percent rounded to one decimal place, or null when no input was billed.
|
|
59
53
|
*/
|
|
60
54
|
export function cacheHitPercent(usage: TranscriptStats['usage']): number | null {
|
|
61
|
-
|
|
55
|
+
const billed = billedInputTokens(usage)
|
|
56
|
+
return billed === 0
|
|
62
57
|
? null
|
|
63
|
-
: Math.round(usage.cacheReadTokens /
|
|
58
|
+
: Math.round(usage.cacheReadTokens / billed * 1_000) / 10
|
|
64
59
|
}
|
|
65
60
|
|
|
66
61
|
/**
|
|
@@ -77,6 +72,7 @@ export type StatusTone =
|
|
|
77
72
|
| 'meta'
|
|
78
73
|
| 'accent'
|
|
79
74
|
| 'success'
|
|
75
|
+
| 'plan'
|
|
80
76
|
| 'warn'
|
|
81
77
|
| 'error'
|
|
82
78
|
// Context-bar fill: one DeepSeek blue for the whole occupied run (the free
|
|
@@ -123,8 +119,14 @@ export const STATUS_GROUP_SEPARATOR = ' | '
|
|
|
123
119
|
/** Separator between trailing state spans. */
|
|
124
120
|
export const STATUS_ITEM_SEPARATOR = ' · '
|
|
125
121
|
/** The Codex-style mode cycle hint appended to the permission badge. */
|
|
122
|
+
/** English compatibility value for callers that only measure the default layout. */
|
|
126
123
|
export const STATUS_CYCLE_HINT = ' (shift+tab to cycle)'
|
|
127
124
|
|
|
125
|
+
/** Localized mode-cycle hint used by the live layout. */
|
|
126
|
+
export function statusCycleHint(): string {
|
|
127
|
+
return ` ${t('status.cycleHint')}`
|
|
128
|
+
}
|
|
129
|
+
|
|
128
130
|
/**
|
|
129
131
|
* Interior columns of the context bar. The layout starts every bar at this
|
|
130
132
|
* width so the drop ladder can pre-measure the group, then degrades the
|
|
@@ -184,7 +186,7 @@ export function contextGroupSpans(
|
|
|
184
186
|
barWidth: number,
|
|
185
187
|
readout: ContextReadoutMode,
|
|
186
188
|
): readonly StatusSpan[] {
|
|
187
|
-
const spans: StatusSpan[] = [{ text: 'context ', tone: 'label' }]
|
|
189
|
+
const spans: StatusSpan[] = [{ text: t('status.label.context') + ' ', tone: 'label' }]
|
|
188
190
|
spans.push(...contextBar(usedTokens, contextWindow, barWidth))
|
|
189
191
|
if (readout === 'none' || barWidth <= 0 || contextWindow <= 0) return spans
|
|
190
192
|
const used = Math.max(0, usedTokens)
|
|
@@ -407,7 +409,7 @@ function buildCandidates(
|
|
|
407
409
|
if (cwd !== '' && enabled.has('cwd')) push({ text: cwd, tone: 'path' })
|
|
408
410
|
const mode = safe(facts.mode ?? '')
|
|
409
411
|
if (mode !== '' && enabled.has('mode')) {
|
|
410
|
-
push({ text: '
|
|
412
|
+
push({ text: t('status.label.mode') + ' ', tone: 'label' })
|
|
411
413
|
identity.push({ text: mode, tone: 'accent' })
|
|
412
414
|
}
|
|
413
415
|
const branch = safe(facts.branch)
|
|
@@ -419,9 +421,7 @@ function buildCandidates(
|
|
|
419
421
|
const right: { span: StatusSpan; rank: number; id: string }[] = []
|
|
420
422
|
const row2: { group: StatusGroup; rank: number; id: string }[] = []
|
|
421
423
|
|
|
422
|
-
|
|
423
|
-
row2.push({ group: { spans: [{ text: '⧉ plan', tone: 'accent' }] }, rank: RANK2_PLAN, id: 'plan' })
|
|
424
|
-
}
|
|
424
|
+
|
|
425
425
|
|
|
426
426
|
if (stats.turns > 0 || stats.steps > 0) {
|
|
427
427
|
if (enabled.has('turns')) {
|
|
@@ -431,8 +431,8 @@ function buildCandidates(
|
|
|
431
431
|
if (counts.length > 0) counts.push(sep())
|
|
432
432
|
counts.push({ text: label + ' ', tone: 'label' }, { text: value, tone: 'value' })
|
|
433
433
|
}
|
|
434
|
-
pair('turns', String(stats.turns))
|
|
435
|
-
pair('steps', String(stats.steps))
|
|
434
|
+
pair(t('status.label.turns'), String(stats.turns))
|
|
435
|
+
pair(t('status.label.steps'), String(stats.steps))
|
|
436
436
|
row2.push({ group: { spans: counts }, rank: RANK_COUNTS, id: 'turns' })
|
|
437
437
|
}
|
|
438
438
|
if (enabled.has('durations')) {
|
|
@@ -444,29 +444,34 @@ function buildCandidates(
|
|
|
444
444
|
if (durations.length > 0) durations.push(sep())
|
|
445
445
|
durations.push({ text: label + ' ', tone: 'label' }, { text: value, tone: 'value' })
|
|
446
446
|
}
|
|
447
|
-
if (stats.llmMs > 0) pair('
|
|
448
|
-
if (stats.ttftSteps > 0) pair('latency', formatDuration(stats.ttftMs / stats.ttftSteps))
|
|
447
|
+
if (stats.llmMs > 0) pair(t('status.label.modelTime'), formatDuration(stats.llmMs))
|
|
448
|
+
if (stats.ttftSteps > 0) pair(t('status.label.latency'), formatDuration(stats.ttftMs / stats.ttftSteps))
|
|
449
449
|
if (stats.decodeMs > 0 && stats.decodeTokens > 0) {
|
|
450
450
|
if (durations.length > 0) durations.push(sep())
|
|
451
451
|
durations.push(
|
|
452
452
|
{ text: formatRate(stats.decodeTokens / (stats.decodeMs / 1_000)), tone: 'value' },
|
|
453
|
-
{ text: '
|
|
453
|
+
{ text: t('status.label.tokensPerSec'), tone: 'label' },
|
|
454
454
|
)
|
|
455
455
|
}
|
|
456
|
-
if (stats.toolMs > 0) pair('tool', formatDuration(stats.toolMs))
|
|
456
|
+
if (stats.toolMs > 0) pair(t('status.label.tool'), formatDuration(stats.toolMs))
|
|
457
457
|
if (durations.length > 0) {
|
|
458
458
|
row2.push({ group: { spans: durations }, rank: RANK2_DURATIONS, id: 'durations' })
|
|
459
459
|
}
|
|
460
460
|
}
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
// The cache group carries both facts about cached prompt tokens: how many
|
|
464
|
+
// were read and what share of the billed prompt that was. The read count
|
|
465
|
+
// lives here rather than in the tokens group so the tokens group keeps
|
|
466
|
+
// meaning "what the provider billed outside the cache".
|
|
463
467
|
const cacheHit = cacheHitPercent(stats.usage)
|
|
464
468
|
if (cacheHit !== null && enabled.has('cache')) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
})
|
|
469
|
+
const spans: StatusSpan[] = [{ text: t('status.label.cache') + ' ', tone: 'label' }]
|
|
470
|
+
if (stats.usage.cacheReadTokens > 0) {
|
|
471
|
+
spans.push({ text: formatTokens(stats.usage.cacheReadTokens), tone: 'value' }, sep())
|
|
472
|
+
}
|
|
473
|
+
spans.push({ text: cacheHit + '%', tone: 'value' })
|
|
474
|
+
row2.push({ group: { spans }, rank: RANK2_CACHE, id: 'cache' })
|
|
470
475
|
}
|
|
471
476
|
// Context occupancy as a purely proportional bar with the usage readout
|
|
472
477
|
// riding outside it: the used total is the most recent reported prompt
|
|
@@ -480,14 +485,14 @@ function buildCandidates(
|
|
|
480
485
|
id: 'context',
|
|
481
486
|
})
|
|
482
487
|
}
|
|
483
|
-
if ((stats.usage.
|
|
488
|
+
if ((stats.usage.uncachedInputTokens > 0 || stats.usage.outputTokens > 0) && enabled.has('tokens')) {
|
|
484
489
|
const tokens: StatusSpan[] = []
|
|
485
490
|
const pair = (label: string, value: string): void => {
|
|
486
491
|
if (tokens.length > 0) tokens.push(sep())
|
|
487
492
|
tokens.push({ text: label + ' ', tone: 'label' }, { text: value, tone: 'value' })
|
|
488
493
|
}
|
|
489
|
-
pair('in', formatTokens(stats.usage.
|
|
490
|
-
pair('out', formatTokens(stats.usage.outputTokens))
|
|
494
|
+
pair(t('status.label.in'), formatTokens(stats.usage.uncachedInputTokens))
|
|
495
|
+
pair(t('status.label.out'), formatTokens(stats.usage.outputTokens))
|
|
491
496
|
row2.push({ group: { spans: tokens }, rank: RANK_TOKENS, id: 'tokens' })
|
|
492
497
|
}
|
|
493
498
|
|
|
@@ -522,10 +527,25 @@ function buildCandidates(
|
|
|
522
527
|
}
|
|
523
528
|
const permission = safe(facts.permission)
|
|
524
529
|
let badge = -1
|
|
530
|
+
// The plan STATION names itself in the permission badge: with the most
|
|
531
|
+
// restrictive preset active, plan mode reads as the green fourth cycle
|
|
532
|
+
// station 'plan' (that preset IS the station's permission layer). Plan on
|
|
533
|
+
// any other preset (a typed /plan mid-session) stays orthogonal: the badge
|
|
534
|
+
// keeps naming the preset and row 2 carries the green plan marker.
|
|
535
|
+
const planStation = facts.plan && permissionTone(permission) === 'success'
|
|
525
536
|
if (permission !== '' && enabled.has('permission')) {
|
|
526
|
-
right.push({
|
|
537
|
+
right.push({
|
|
538
|
+
span: planStation
|
|
539
|
+
? { text: 'plan on', tone: 'plan' }
|
|
540
|
+
: { text: permission, tone: permissionTone(permission) },
|
|
541
|
+
rank: RANK_BADGE,
|
|
542
|
+
id: 'permission',
|
|
543
|
+
})
|
|
527
544
|
badge = right.length - 1
|
|
528
545
|
}
|
|
546
|
+
if (facts.plan && enabled.has('plan')) {
|
|
547
|
+
row2.push({ group: { spans: [{ text: '⧉ plan', tone: 'accent' }] }, rank: RANK2_PLAN, id: 'plan' })
|
|
548
|
+
}
|
|
529
549
|
return { left, right, badge, row2 }
|
|
530
550
|
}
|
|
531
551
|
|
|
@@ -598,7 +618,7 @@ export function layoutStatusBar(
|
|
|
598
618
|
groupSeparator,
|
|
599
619
|
)
|
|
600
620
|
const rightWidth = joinWidth(rightKept.map(entry => visibleColumns(entry.span.text)), itemSeparator)
|
|
601
|
-
+ (hint ? visibleColumns(
|
|
621
|
+
+ (hint ? visibleColumns(statusCycleHint()) : 0)
|
|
602
622
|
return rightWidth > 0 ? leftWidth + LEFT_RIGHT_GAP + rightWidth : leftWidth
|
|
603
623
|
}
|
|
604
624
|
|
|
@@ -627,7 +647,7 @@ export function layoutStatusBar(
|
|
|
627
647
|
const identity = leftKept[0]
|
|
628
648
|
const identityText = identity.group.spans.map(span => span.text).join('')
|
|
629
649
|
const rightWidth = joinWidth(rightKept.map(entry => visibleColumns(entry.span.text)), itemSeparator)
|
|
630
|
-
const identityBudget = budget - rightWidth - LEFT_RIGHT_GAP - visibleColumns(
|
|
650
|
+
const identityBudget = budget - rightWidth - LEFT_RIGHT_GAP - visibleColumns(statusCycleHint())
|
|
631
651
|
if (identityBudget > 0 && visibleColumns(identityText) > identityBudget) {
|
|
632
652
|
leftKept[0] = {
|
|
633
653
|
...identity,
|
package/src/render/text.ts
CHANGED
|
@@ -14,6 +14,20 @@
|
|
|
14
14
|
|
|
15
15
|
import { graphemeWidth, splitGraphemes, stringWidth } from './width.ts'
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Compact token count: exact below 1K, then one-decimal-ish K/M.
|
|
19
|
+
* @param n - token count.
|
|
20
|
+
* @returns display string.
|
|
21
|
+
*/
|
|
22
|
+
export function formatTokens(n: number): string {
|
|
23
|
+
const scaled = (v: number): string =>
|
|
24
|
+
v >= 100 ? String(Math.round(v)) : String(Math.round(v * 10) / 10)
|
|
25
|
+
if (n < 1_000) return String(n)
|
|
26
|
+
if (n < 1_000_000) return scaled(n / 1_000) + 'K'
|
|
27
|
+
return scaled(n / 1_000_000) + 'M'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
17
31
|
/** C0 controls except tab (0x09) and newline (0x0a), plus DEL and C1. */
|
|
18
32
|
const CONTROL_ESCAPE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/gu
|
|
19
33
|
|
|
@@ -1,77 +1,77 @@
|
|
|
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. Arguments longer than
|
|
6
|
-
* {@link MAX_PARSE_CHARS} are never parsed: the preview is a display concern,
|
|
7
|
-
* and a synchronous `JSON.parse` plus string copies of an unbounded model
|
|
8
|
-
* payload must not run on the approval or projection paths.
|
|
9
|
-
*
|
|
10
|
-
* @module @deepseek-ai/dsh-code/render/tool-preview
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/** Keys searched in declaration order when building a preview. */
|
|
14
|
-
const PREVIEW_KEYS = ['command', 'cmd', 'description', 'path', 'pattern', 'query'] as const
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Raw arguments longer than this are skipped without parsing and fall back
|
|
18
|
-
* to the bounded raw preview. Well above any realistic command/path/query
|
|
19
|
-
* string while keeping the synchronous parse cost negligible.
|
|
20
|
-
*/
|
|
21
|
-
const MAX_PARSE_CHARS = 4096
|
|
22
|
-
|
|
23
|
-
/** Bounded raw-arguments fallback shared by the skip-parse and parse-failure paths. */
|
|
24
|
-
function boundedRawPreview(args: string): string {
|
|
25
|
-
return args.length > 80 ? `${args.slice(0, 77)}...` : args
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Resolve one bounded preview for raw tool arguments.
|
|
30
|
-
* @param args - raw JSON arguments string as the model produced it.
|
|
31
|
-
* @param toolName - the tool the arguments belong to (fallback label).
|
|
32
|
-
* @returns the preview line; empty when nothing useful resolves.
|
|
33
|
-
*/
|
|
34
|
-
export function toolArgumentsPreview(args: string, toolName: string): string {
|
|
35
|
-
if (args === '') return toolName
|
|
36
|
-
if (args.length > MAX_PARSE_CHARS) return boundedRawPreview(args)
|
|
37
|
-
try {
|
|
38
|
-
const parsed: unknown = JSON.parse(args)
|
|
39
|
-
if (parsed !== null && typeof parsed === 'object') {
|
|
40
|
-
const record = parsed as Record<string, unknown>
|
|
41
|
-
for (const key of PREVIEW_KEYS) {
|
|
42
|
-
const value = record[key]
|
|
43
|
-
if (typeof value === 'string' && value !== '') return value
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
} catch {
|
|
47
|
-
// Raw JSON parse failed: fall through to the bounded raw arguments.
|
|
48
|
-
}
|
|
49
|
-
return boundedRawPreview(args)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Visible budget for one delegation prompt row on the tool card. */
|
|
53
|
-
const MAX_PROMPT_CHARS = 160
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Bounded prompt preview for delegation-style tools (`subagent`): the
|
|
57
|
-
* `prompt` argument rendered as the card's second row, so the transcript
|
|
58
|
-
* shows what the child agent was asked — not just its description label —
|
|
59
|
-
* while it runs (Codex's SpawnAgent card preview). Anything else returns ''.
|
|
60
|
-
* @param toolName - the tool the arguments belong to.
|
|
61
|
-
* @param args - raw JSON arguments string as the model produced it.
|
|
62
|
-
* @returns the one-line prompt preview, or '' when none applies.
|
|
63
|
-
*/
|
|
64
|
-
export function toolPromptPreview(toolName: string, args: string): string {
|
|
65
|
-
if (toolName !== 'subagent' || args === '' || args.length > MAX_PARSE_CHARS) return ''
|
|
66
|
-
try {
|
|
67
|
-
const parsed: unknown = JSON.parse(args)
|
|
68
|
-
if (parsed === null || typeof parsed !== 'object') return ''
|
|
69
|
-
const prompt = (parsed as Record<string, unknown>)['prompt']
|
|
70
|
-
if (typeof prompt !== 'string' || prompt === '') return ''
|
|
71
|
-
const flat = prompt.replace(/\s+/gu, ' ').trim()
|
|
72
|
-
return flat.length > MAX_PROMPT_CHARS ? `${flat.slice(0, MAX_PROMPT_CHARS - 1)}…` : flat
|
|
73
|
-
} catch {
|
|
74
|
-
// Malformed arguments degrade to no prompt row, never a thrown parse.
|
|
75
|
-
return ''
|
|
76
|
-
}
|
|
77
|
-
}
|
|
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. Arguments longer than
|
|
6
|
+
* {@link MAX_PARSE_CHARS} are never parsed: the preview is a display concern,
|
|
7
|
+
* and a synchronous `JSON.parse` plus string copies of an unbounded model
|
|
8
|
+
* payload must not run on the approval or projection paths.
|
|
9
|
+
*
|
|
10
|
+
* @module @deepseek-ai/dsh-code/render/tool-preview
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Keys searched in declaration order when building a preview. */
|
|
14
|
+
const PREVIEW_KEYS = ['command', 'cmd', 'description', 'path', 'pattern', 'query'] as const
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Raw arguments longer than this are skipped without parsing and fall back
|
|
18
|
+
* to the bounded raw preview. Well above any realistic command/path/query
|
|
19
|
+
* string while keeping the synchronous parse cost negligible.
|
|
20
|
+
*/
|
|
21
|
+
const MAX_PARSE_CHARS = 4096
|
|
22
|
+
|
|
23
|
+
/** Bounded raw-arguments fallback shared by the skip-parse and parse-failure paths. */
|
|
24
|
+
function boundedRawPreview(args: string): string {
|
|
25
|
+
return args.length > 80 ? `${args.slice(0, 77)}...` : args
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve one bounded preview for raw tool arguments.
|
|
30
|
+
* @param args - raw JSON arguments string as the model produced it.
|
|
31
|
+
* @param toolName - the tool the arguments belong to (fallback label).
|
|
32
|
+
* @returns the preview line; empty when nothing useful resolves.
|
|
33
|
+
*/
|
|
34
|
+
export function toolArgumentsPreview(args: string, toolName: string): string {
|
|
35
|
+
if (args === '') return toolName
|
|
36
|
+
if (args.length > MAX_PARSE_CHARS) return boundedRawPreview(args)
|
|
37
|
+
try {
|
|
38
|
+
const parsed: unknown = JSON.parse(args)
|
|
39
|
+
if (parsed !== null && typeof parsed === 'object') {
|
|
40
|
+
const record = parsed as Record<string, unknown>
|
|
41
|
+
for (const key of PREVIEW_KEYS) {
|
|
42
|
+
const value = record[key]
|
|
43
|
+
if (typeof value === 'string' && value !== '') return value
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// Raw JSON parse failed: fall through to the bounded raw arguments.
|
|
48
|
+
}
|
|
49
|
+
return boundedRawPreview(args)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Visible budget for one delegation prompt row on the tool card. */
|
|
53
|
+
const MAX_PROMPT_CHARS = 160
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Bounded prompt preview for delegation-style tools (`subagent`): the
|
|
57
|
+
* `prompt` argument rendered as the card's second row, so the transcript
|
|
58
|
+
* shows what the child agent was asked — not just its description label —
|
|
59
|
+
* while it runs (Codex's SpawnAgent card preview). Anything else returns ''.
|
|
60
|
+
* @param toolName - the tool the arguments belong to.
|
|
61
|
+
* @param args - raw JSON arguments string as the model produced it.
|
|
62
|
+
* @returns the one-line prompt preview, or '' when none applies.
|
|
63
|
+
*/
|
|
64
|
+
export function toolPromptPreview(toolName: string, args: string): string {
|
|
65
|
+
if (toolName !== 'subagent' || args === '' || args.length > MAX_PARSE_CHARS) return ''
|
|
66
|
+
try {
|
|
67
|
+
const parsed: unknown = JSON.parse(args)
|
|
68
|
+
if (parsed === null || typeof parsed !== 'object') return ''
|
|
69
|
+
const prompt = (parsed as Record<string, unknown>)['prompt']
|
|
70
|
+
if (typeof prompt !== 'string' || prompt === '') return ''
|
|
71
|
+
const flat = prompt.replace(/\s+/gu, ' ').trim()
|
|
72
|
+
return flat.length > MAX_PROMPT_CHARS ? `${flat.slice(0, MAX_PROMPT_CHARS - 1)}…` : flat
|
|
73
|
+
} catch {
|
|
74
|
+
// Malformed arguments degrade to no prompt row, never a thrown parse.
|
|
75
|
+
return ''
|
|
76
|
+
}
|
|
77
|
+
}
|