dsh-code 1.0.7 → 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 (81) hide show
  1. package/README.en.md +70 -24
  2. package/README.md +71 -25
  3. package/bin/deepseek.mjs +202 -39
  4. package/cordis.patch.yml +13 -4
  5. package/lib/index.mjs +4063 -844
  6. package/lib/session-query.mjs +3 -2
  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 +100 -63
  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 +73 -1
  14. package/lib/types/input-split.d.ts +1 -1
  15. package/lib/types/kernel-panels.d.ts +86 -31
  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/inspector.d.ts +26 -0
  25. package/lib/types/render/lines.d.ts +21 -1
  26. package/lib/types/render/markdown.d.ts +1 -1
  27. package/lib/types/render/projection.d.ts +95 -4
  28. package/lib/types/render/status.d.ts +8 -8
  29. package/lib/types/render/text.d.ts +6 -0
  30. package/lib/types/render/usage.d.ts +113 -0
  31. package/lib/types/session-directory.d.ts +17 -0
  32. package/lib/types/startup.d.ts +1 -1
  33. package/lib/types/terminal-title.d.ts +8 -0
  34. package/lib/types/theme-panel.d.ts +2 -2
  35. package/lib/types/theme.d.ts +271 -52
  36. package/lib/types/update-panel.d.ts +5 -5
  37. package/lib/types/update.d.ts +10 -1
  38. package/lib/types/version.d.ts +4 -3
  39. package/package.json +24 -7
  40. package/src/app.ts +1155 -478
  41. package/src/approval.ts +166 -166
  42. package/src/authorization-panel.ts +19 -16
  43. package/src/editor-keys.ts +371 -371
  44. package/src/git-workflow.ts +229 -3
  45. package/src/i18n.ts +68 -0
  46. package/src/index.ts +412 -76
  47. package/src/input-split.ts +3 -3
  48. package/src/kernel-panels.ts +471 -89
  49. package/src/keyboard.ts +5 -4
  50. package/src/language-panel.ts +53 -0
  51. package/src/locales/en.ts +489 -0
  52. package/src/locales/zh.ts +488 -0
  53. package/src/mentions.ts +8 -4
  54. package/src/models.ts +264 -212
  55. package/src/panel-accent.ts +41 -0
  56. package/src/presets.ts +1 -1
  57. package/src/provider-settings.ts +1 -1
  58. package/src/rainbow.ts +208 -0
  59. package/src/render/animations.ts +104 -6
  60. package/src/render/editor.ts +20 -20
  61. package/src/render/export.ts +116 -95
  62. package/src/render/inspector.ts +42 -0
  63. package/src/render/lines.ts +628 -415
  64. package/src/render/markdown.ts +15 -3
  65. package/src/render/projection.ts +429 -19
  66. package/src/render/status.ts +41 -35
  67. package/src/render/text.ts +14 -0
  68. package/src/render/tool-preview.ts +77 -77
  69. package/src/render/usage.ts +430 -0
  70. package/src/render/width.ts +2 -2
  71. package/src/session-directory.ts +8 -6
  72. package/src/session-query.ts +8 -4
  73. package/src/startup.ts +3 -3
  74. package/src/subagents.ts +229 -229
  75. package/src/terminal-title.ts +22 -5
  76. package/src/theme-panel.ts +17 -21
  77. package/src/theme.ts +281 -33
  78. package/src/update-panel.ts +37 -27
  79. package/src/update.ts +19 -3
  80. package/src/version.ts +58 -20
  81. package/src/whale-glyph.ts +23 -23
@@ -12,21 +12,13 @@
12
12
 
13
13
  import { visibleColumns } from './markdown.ts'
14
14
  import type { TranscriptStats } from './projection.ts'
15
- import { singleLineText, truncateColumns } from './text.ts'
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
- return usage.inputTokens === 0
55
+ const billed = billedInputTokens(usage)
56
+ return billed === 0
62
57
  ? null
63
- : Math.round(usage.cacheReadTokens / usage.inputTokens * 1_000) / 10
58
+ : Math.round(usage.cacheReadTokens / billed * 1_000) / 10
64
59
  }
65
60
 
66
61
  /**
@@ -124,8 +119,14 @@ export const STATUS_GROUP_SEPARATOR = ' | '
124
119
  /** Separator between trailing state spans. */
125
120
  export const STATUS_ITEM_SEPARATOR = ' · '
126
121
  /** The Codex-style mode cycle hint appended to the permission badge. */
122
+ /** English compatibility value for callers that only measure the default layout. */
127
123
  export const STATUS_CYCLE_HINT = ' (shift+tab to cycle)'
128
124
 
125
+ /** Localized mode-cycle hint used by the live layout. */
126
+ export function statusCycleHint(): string {
127
+ return ` ${t('status.cycleHint')}`
128
+ }
129
+
129
130
  /**
130
131
  * Interior columns of the context bar. The layout starts every bar at this
131
132
  * width so the drop ladder can pre-measure the group, then degrades the
@@ -185,7 +186,7 @@ export function contextGroupSpans(
185
186
  barWidth: number,
186
187
  readout: ContextReadoutMode,
187
188
  ): readonly StatusSpan[] {
188
- const spans: StatusSpan[] = [{ text: 'context ', tone: 'label' }]
189
+ const spans: StatusSpan[] = [{ text: t('status.label.context') + ' ', tone: 'label' }]
189
190
  spans.push(...contextBar(usedTokens, contextWindow, barWidth))
190
191
  if (readout === 'none' || barWidth <= 0 || contextWindow <= 0) return spans
191
192
  const used = Math.max(0, usedTokens)
@@ -408,7 +409,7 @@ function buildCandidates(
408
409
  if (cwd !== '' && enabled.has('cwd')) push({ text: cwd, tone: 'path' })
409
410
  const mode = safe(facts.mode ?? '')
410
411
  if (mode !== '' && enabled.has('mode')) {
411
- push({ text: '/mode ', tone: 'label' })
412
+ push({ text: t('status.label.mode') + ' ', tone: 'label' })
412
413
  identity.push({ text: mode, tone: 'accent' })
413
414
  }
414
415
  const branch = safe(facts.branch)
@@ -430,8 +431,8 @@ function buildCandidates(
430
431
  if (counts.length > 0) counts.push(sep())
431
432
  counts.push({ text: label + ' ', tone: 'label' }, { text: value, tone: 'value' })
432
433
  }
433
- pair('turns', String(stats.turns))
434
- pair('steps', String(stats.steps))
434
+ pair(t('status.label.turns'), String(stats.turns))
435
+ pair(t('status.label.steps'), String(stats.steps))
435
436
  row2.push({ group: { spans: counts }, rank: RANK_COUNTS, id: 'turns' })
436
437
  }
437
438
  if (enabled.has('durations')) {
@@ -443,29 +444,34 @@ function buildCandidates(
443
444
  if (durations.length > 0) durations.push(sep())
444
445
  durations.push({ text: label + ' ', tone: 'label' }, { text: value, tone: 'value' })
445
446
  }
446
- if (stats.llmMs > 0) pair('model', formatDuration(stats.llmMs))
447
- 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))
448
449
  if (stats.decodeMs > 0 && stats.decodeTokens > 0) {
449
450
  if (durations.length > 0) durations.push(sep())
450
451
  durations.push(
451
452
  { text: formatRate(stats.decodeTokens / (stats.decodeMs / 1_000)), tone: 'value' },
452
- { text: ' tokens/s', tone: 'label' },
453
+ { text: t('status.label.tokensPerSec'), tone: 'label' },
453
454
  )
454
455
  }
455
- if (stats.toolMs > 0) pair('tool', formatDuration(stats.toolMs))
456
+ if (stats.toolMs > 0) pair(t('status.label.tool'), formatDuration(stats.toolMs))
456
457
  if (durations.length > 0) {
457
458
  row2.push({ group: { spans: durations }, rank: RANK2_DURATIONS, id: 'durations' })
458
459
  }
459
460
  }
460
461
  }
461
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".
462
467
  const cacheHit = cacheHitPercent(stats.usage)
463
468
  if (cacheHit !== null && enabled.has('cache')) {
464
- row2.push({
465
- group: { spans: [{ text: 'cache ', tone: 'label' }, { text: cacheHit + '%', tone: 'value' }] },
466
- rank: RANK2_CACHE,
467
- id: 'cache',
468
- })
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' })
469
475
  }
470
476
  // Context occupancy as a purely proportional bar with the usage readout
471
477
  // riding outside it: the used total is the most recent reported prompt
@@ -479,14 +485,14 @@ function buildCandidates(
479
485
  id: 'context',
480
486
  })
481
487
  }
482
- if ((stats.usage.inputTokens > 0 || stats.usage.outputTokens > 0) && enabled.has('tokens')) {
488
+ if ((stats.usage.uncachedInputTokens > 0 || stats.usage.outputTokens > 0) && enabled.has('tokens')) {
483
489
  const tokens: StatusSpan[] = []
484
490
  const pair = (label: string, value: string): void => {
485
491
  if (tokens.length > 0) tokens.push(sep())
486
492
  tokens.push({ text: label + ' ', tone: 'label' }, { text: value, tone: 'value' })
487
493
  }
488
- pair('in', formatTokens(stats.usage.inputTokens))
489
- 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))
490
496
  row2.push({ group: { spans: tokens }, rank: RANK_TOKENS, id: 'tokens' })
491
497
  }
492
498
 
@@ -612,7 +618,7 @@ export function layoutStatusBar(
612
618
  groupSeparator,
613
619
  )
614
620
  const rightWidth = joinWidth(rightKept.map(entry => visibleColumns(entry.span.text)), itemSeparator)
615
- + (hint ? visibleColumns(STATUS_CYCLE_HINT) : 0)
621
+ + (hint ? visibleColumns(statusCycleHint()) : 0)
616
622
  return rightWidth > 0 ? leftWidth + LEFT_RIGHT_GAP + rightWidth : leftWidth
617
623
  }
618
624
 
@@ -641,7 +647,7 @@ export function layoutStatusBar(
641
647
  const identity = leftKept[0]
642
648
  const identityText = identity.group.spans.map(span => span.text).join('')
643
649
  const rightWidth = joinWidth(rightKept.map(entry => visibleColumns(entry.span.text)), itemSeparator)
644
- const identityBudget = budget - rightWidth - LEFT_RIGHT_GAP - visibleColumns(STATUS_CYCLE_HINT)
650
+ const identityBudget = budget - rightWidth - LEFT_RIGHT_GAP - visibleColumns(statusCycleHint())
645
651
  if (identityBudget > 0 && visibleColumns(identityText) > identityBudget) {
646
652
  leftKept[0] = {
647
653
  ...identity,
@@ -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
+ }