dsh-code 1.0.7 → 1.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.en.md +70 -24
- package/README.md +71 -25
- package/bin/deepseek.mjs +202 -39
- package/cordis.patch.yml +13 -4
- package/lib/index.mjs +5002 -1061
- package/lib/session-query.mjs +3 -2
- package/lib/startup.mjs +4 -4
- package/lib/{theme-DCT8Y2xf.mjs → theme-B3orFUYz.mjs} +665 -20
- package/lib/types/app.d.ts +120 -63
- package/lib/types/attachments.d.ts +16 -7
- package/lib/types/authorization-panel.d.ts +3 -3
- package/lib/types/git-workflow.d.ts +91 -2
- package/lib/types/history.d.ts +10 -0
- package/lib/types/i18n.d.ts +39 -0
- package/lib/types/index.d.ts +74 -2
- package/lib/types/input-split.d.ts +1 -1
- package/lib/types/kernel-panels.d.ts +89 -32
- package/lib/types/language-panel.d.ts +12 -0
- package/lib/types/locales/en.d.ts +498 -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/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 +95 -4
- package/lib/types/render/status.d.ts +12 -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 +42 -1
- package/lib/types/session-switch.d.ts +8 -0
- package/lib/types/startup.d.ts +1 -1
- package/lib/types/terminal-title.d.ts +8 -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 +27 -6
- package/lib/types/update.d.ts +10 -1
- package/lib/types/version.d.ts +6 -3
- package/package.json +26 -7
- package/src/app.ts +1426 -627
- package/src/approval.ts +166 -166
- package/src/attachments.ts +65 -19
- package/src/authorization-panel.ts +24 -18
- package/src/editor-keys.ts +371 -371
- package/src/fork.ts +11 -7
- package/src/git-workflow.ts +229 -3
- package/src/history.ts +14 -0
- package/src/i18n.ts +68 -0
- package/src/index.ts +503 -128
- package/src/input-split.ts +27 -7
- package/src/kernel-panels.ts +528 -113
- package/src/keyboard.ts +5 -4
- package/src/language-panel.ts +53 -0
- package/src/locales/en.ts +538 -0
- package/src/locales/zh.ts +537 -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 +218 -0
- package/src/render/animations.ts +104 -6
- package/src/render/editor.ts +20 -20
- package/src/render/export.ts +116 -95
- 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 +429 -19
- package/src/render/status.ts +119 -62
- package/src/render/text.ts +15 -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 +90 -9
- package/src/session-query.ts +8 -4
- package/src/session-switch.ts +14 -0
- package/src/startup.ts +3 -3
- package/src/store.ts +19 -1
- package/src/subagents.ts +229 -229
- package/src/terminal-title.ts +22 -5
- package/src/theme-panel.ts +17 -21
- package/src/theme.ts +281 -33
- package/src/update-panel.ts +148 -31
- package/src/update.ts +19 -3
- package/src/version.ts +63 -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, type MessageKey } 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.
|
|
@@ -37,7 +29,11 @@ export function formatDuration(ms: number): string {
|
|
|
37
29
|
const s = ms / 1_000
|
|
38
30
|
if (s < 60) return String(Math.round(s * 10) / 10) + 's'
|
|
39
31
|
const whole = Math.round(s)
|
|
40
|
-
|
|
32
|
+
const hours = Math.floor(whole / 3_600)
|
|
33
|
+
const minutes = Math.floor((whole % 3_600) / 60)
|
|
34
|
+
const seconds = whole % 60
|
|
35
|
+
if (hours > 0) return `${hours}h${minutes}m${seconds === 0 ? '' : `${seconds}s`}`
|
|
36
|
+
return `${minutes}m${seconds}s`
|
|
41
37
|
}
|
|
42
38
|
|
|
43
39
|
/**
|
|
@@ -53,14 +49,17 @@ function formatRate(n: number): string {
|
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
/**
|
|
56
|
-
* Cache-hit share of billed prompt-side input.
|
|
52
|
+
* Cache-hit share of billed prompt-side input. The denominator is the same
|
|
53
|
+
* billed total the /usage panel shows (uncached input plus both cache
|
|
54
|
+
* buckets), so the two readouts can never disagree.
|
|
57
55
|
* @param usage - cumulative token totals.
|
|
58
56
|
* @returns percent rounded to one decimal place, or null when no input was billed.
|
|
59
57
|
*/
|
|
60
58
|
export function cacheHitPercent(usage: TranscriptStats['usage']): number | null {
|
|
61
|
-
|
|
59
|
+
const billed = billedInputTokens(usage)
|
|
60
|
+
return billed === 0
|
|
62
61
|
? null
|
|
63
|
-
: Math.round(usage.cacheReadTokens /
|
|
62
|
+
: Math.round(usage.cacheReadTokens / billed * 1_000) / 10
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
/**
|
|
@@ -124,8 +123,14 @@ export const STATUS_GROUP_SEPARATOR = ' | '
|
|
|
124
123
|
/** Separator between trailing state spans. */
|
|
125
124
|
export const STATUS_ITEM_SEPARATOR = ' · '
|
|
126
125
|
/** The Codex-style mode cycle hint appended to the permission badge. */
|
|
126
|
+
/** English compatibility value for callers that only measure the default layout. */
|
|
127
127
|
export const STATUS_CYCLE_HINT = ' (shift+tab to cycle)'
|
|
128
128
|
|
|
129
|
+
/** Localized mode-cycle hint used by the live layout. */
|
|
130
|
+
export function statusCycleHint(): string {
|
|
131
|
+
return ` ${t('status.cycleHint')}`
|
|
132
|
+
}
|
|
133
|
+
|
|
129
134
|
/**
|
|
130
135
|
* Interior columns of the context bar. The layout starts every bar at this
|
|
131
136
|
* width so the drop ladder can pre-measure the group, then degrades the
|
|
@@ -185,7 +190,7 @@ export function contextGroupSpans(
|
|
|
185
190
|
barWidth: number,
|
|
186
191
|
readout: ContextReadoutMode,
|
|
187
192
|
): readonly StatusSpan[] {
|
|
188
|
-
const spans: StatusSpan[] = [{ text: 'context ', tone: 'label' }]
|
|
193
|
+
const spans: StatusSpan[] = [{ text: t('status.label.context') + ' ', tone: 'label' }]
|
|
189
194
|
spans.push(...contextBar(usedTokens, contextWindow, barWidth))
|
|
190
195
|
if (readout === 'none' || barWidth <= 0 || contextWindow <= 0) return spans
|
|
191
196
|
const used = Math.max(0, usedTokens)
|
|
@@ -250,6 +255,31 @@ export const STATUS_ITEMS: readonly StatusItemInfo[] = [
|
|
|
250
255
|
{ id: 'sandbox', label: 'sandbox', description: 'divergent sandbox-mode override', side: 'left' },
|
|
251
256
|
]
|
|
252
257
|
|
|
258
|
+
const STATUSLINE_ITEM_KEYS: Record<StatusItemId, { label: MessageKey; description: MessageKey }> = {
|
|
259
|
+
model: { label: 'statusline.item.model', description: 'statusline.item.model.desc' },
|
|
260
|
+
cwd: { label: 'statusline.item.cwd', description: 'statusline.item.cwd.desc' },
|
|
261
|
+
mode: { label: 'statusline.item.mode', description: 'statusline.item.mode.desc' },
|
|
262
|
+
branch: { label: 'statusline.item.branch', description: 'statusline.item.branch.desc' },
|
|
263
|
+
context: { label: 'statusline.item.context', description: 'statusline.item.context.desc' },
|
|
264
|
+
permission: { label: 'statusline.item.permission', description: 'statusline.item.permission.desc' },
|
|
265
|
+
plan: { label: 'statusline.item.plan', description: 'statusline.item.plan.desc' },
|
|
266
|
+
turns: { label: 'statusline.item.turns', description: 'statusline.item.turns.desc' },
|
|
267
|
+
durations: { label: 'statusline.item.durations', description: 'statusline.item.durations.desc' },
|
|
268
|
+
cache: { label: 'statusline.item.cache', description: 'statusline.item.cache.desc' },
|
|
269
|
+
tokens: { label: 'statusline.item.tokens', description: 'statusline.item.tokens.desc' },
|
|
270
|
+
title: { label: 'statusline.item.title', description: 'statusline.item.title.desc' },
|
|
271
|
+
goal: { label: 'statusline.item.goal', description: 'statusline.item.goal.desc' },
|
|
272
|
+
sandbox: { label: 'statusline.item.sandbox', description: 'statusline.item.sandbox.desc' },
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Picker rows with labels/descriptions in the active interface language. */
|
|
276
|
+
export function localizedStatusItems(): readonly StatusItemInfo[] {
|
|
277
|
+
return STATUS_ITEMS.map(item => {
|
|
278
|
+
const keys = STATUSLINE_ITEM_KEYS[item.id]
|
|
279
|
+
return { ...item, label: t(keys.label), description: t(keys.description) }
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
253
283
|
/**
|
|
254
284
|
* Default order: the whole catalog (matches the pre-customization bar).
|
|
255
285
|
* The busy dot is not an item — it always leads the identity cluster.
|
|
@@ -291,9 +321,10 @@ export const STATUS_ROW2_INDENT = 2
|
|
|
291
321
|
const TITLE_BUDGET = 48
|
|
292
322
|
|
|
293
323
|
/**
|
|
294
|
-
* Primary-row drop ranks:
|
|
295
|
-
*
|
|
296
|
-
*
|
|
324
|
+
* Primary-row drop ranks: identity never drops; permission outranks context.
|
|
325
|
+
* The drop ladder peels hint and trailing identity facts before removing
|
|
326
|
+
* the context meter, so occupancy stays visible on a typical 120-column
|
|
327
|
+
* terminal. Secondary-row groups reuse the remaining ranks independently.
|
|
297
328
|
*/
|
|
298
329
|
const RANK_TITLE = 10
|
|
299
330
|
const RANK_TOKENS = 50
|
|
@@ -357,6 +388,30 @@ function sep(): StatusSpan {
|
|
|
357
388
|
return { text: ' · ', tone: 'label' }
|
|
358
389
|
}
|
|
359
390
|
|
|
391
|
+
/** True for the dim ` · ` that joins identity facts. */
|
|
392
|
+
function isIdentitySep(span: StatusSpan): boolean {
|
|
393
|
+
return span.text === ' · ' && span.tone === 'label'
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Drop the trailing identity fact (branch, then mode, then cwd) so the
|
|
398
|
+
* context meter can keep a column. Returns undefined once only the busy
|
|
399
|
+
* dot and model remain.
|
|
400
|
+
*/
|
|
401
|
+
function peelIdentityFact(spans: readonly StatusSpan[]): StatusSpan[] | undefined {
|
|
402
|
+
if (spans.length < 3) return undefined
|
|
403
|
+
const last = spans[spans.length - 1]
|
|
404
|
+
const before = spans[spans.length - 2]
|
|
405
|
+
const modeSep = spans[spans.length - 3]
|
|
406
|
+
if (last?.tone === 'accent' && modeSep !== undefined && isIdentitySep(modeSep) && before?.tone === 'label') {
|
|
407
|
+
return spans.slice(0, -3)
|
|
408
|
+
}
|
|
409
|
+
if ((last?.tone === 'branch' || last?.tone === 'path') && before !== undefined && isIdentitySep(before)) {
|
|
410
|
+
return spans.slice(0, -2)
|
|
411
|
+
}
|
|
412
|
+
return undefined
|
|
413
|
+
}
|
|
414
|
+
|
|
360
415
|
/** Total visible columns of a span list (separators ride inside the spans). */
|
|
361
416
|
function spansWidth(spans: readonly StatusSpan[]): number {
|
|
362
417
|
let width = 0
|
|
@@ -408,7 +463,7 @@ function buildCandidates(
|
|
|
408
463
|
if (cwd !== '' && enabled.has('cwd')) push({ text: cwd, tone: 'path' })
|
|
409
464
|
const mode = safe(facts.mode ?? '')
|
|
410
465
|
if (mode !== '' && enabled.has('mode')) {
|
|
411
|
-
push({ text: '
|
|
466
|
+
push({ text: t('status.label.mode') + ' ', tone: 'label' })
|
|
412
467
|
identity.push({ text: mode, tone: 'accent' })
|
|
413
468
|
}
|
|
414
469
|
const branch = safe(facts.branch)
|
|
@@ -430,8 +485,8 @@ function buildCandidates(
|
|
|
430
485
|
if (counts.length > 0) counts.push(sep())
|
|
431
486
|
counts.push({ text: label + ' ', tone: 'label' }, { text: value, tone: 'value' })
|
|
432
487
|
}
|
|
433
|
-
pair('turns', String(stats.turns))
|
|
434
|
-
pair('steps', String(stats.steps))
|
|
488
|
+
pair(t('status.label.turns'), String(stats.turns))
|
|
489
|
+
pair(t('status.label.steps'), String(stats.steps))
|
|
435
490
|
row2.push({ group: { spans: counts }, rank: RANK_COUNTS, id: 'turns' })
|
|
436
491
|
}
|
|
437
492
|
if (enabled.has('durations')) {
|
|
@@ -443,29 +498,34 @@ function buildCandidates(
|
|
|
443
498
|
if (durations.length > 0) durations.push(sep())
|
|
444
499
|
durations.push({ text: label + ' ', tone: 'label' }, { text: value, tone: 'value' })
|
|
445
500
|
}
|
|
446
|
-
if (stats.llmMs > 0) pair('
|
|
447
|
-
if (stats.ttftSteps > 0) pair('latency', formatDuration(stats.ttftMs / stats.ttftSteps))
|
|
501
|
+
if (stats.llmMs > 0) pair(t('status.label.modelTime'), formatDuration(stats.llmMs))
|
|
502
|
+
if (stats.ttftSteps > 0) pair(t('status.label.latency'), formatDuration(stats.ttftMs / stats.ttftSteps))
|
|
448
503
|
if (stats.decodeMs > 0 && stats.decodeTokens > 0) {
|
|
449
504
|
if (durations.length > 0) durations.push(sep())
|
|
450
505
|
durations.push(
|
|
451
506
|
{ text: formatRate(stats.decodeTokens / (stats.decodeMs / 1_000)), tone: 'value' },
|
|
452
|
-
{ text: '
|
|
507
|
+
{ text: t('status.label.tokensPerSec'), tone: 'label' },
|
|
453
508
|
)
|
|
454
509
|
}
|
|
455
|
-
if (stats.toolMs > 0) pair('tool', formatDuration(stats.toolMs))
|
|
510
|
+
if (stats.toolMs > 0) pair(t('status.label.tool'), formatDuration(stats.toolMs))
|
|
456
511
|
if (durations.length > 0) {
|
|
457
512
|
row2.push({ group: { spans: durations }, rank: RANK2_DURATIONS, id: 'durations' })
|
|
458
513
|
}
|
|
459
514
|
}
|
|
460
515
|
}
|
|
461
516
|
|
|
517
|
+
// The cache group carries both facts about cached prompt tokens: how many
|
|
518
|
+
// were read and what share of the billed prompt that was. The read count
|
|
519
|
+
// lives here rather than in the tokens group so the tokens group keeps
|
|
520
|
+
// meaning "what the provider billed outside the cache".
|
|
462
521
|
const cacheHit = cacheHitPercent(stats.usage)
|
|
463
522
|
if (cacheHit !== null && enabled.has('cache')) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
})
|
|
523
|
+
const spans: StatusSpan[] = [{ text: t('status.label.cache') + ' ', tone: 'label' }]
|
|
524
|
+
if (stats.usage.cacheReadTokens > 0) {
|
|
525
|
+
spans.push({ text: formatTokens(stats.usage.cacheReadTokens), tone: 'value' }, sep())
|
|
526
|
+
}
|
|
527
|
+
spans.push({ text: cacheHit + '%', tone: 'value' })
|
|
528
|
+
row2.push({ group: { spans }, rank: RANK2_CACHE, id: 'cache' })
|
|
469
529
|
}
|
|
470
530
|
// Context occupancy as a purely proportional bar with the usage readout
|
|
471
531
|
// riding outside it: the used total is the most recent reported prompt
|
|
@@ -479,14 +539,14 @@ function buildCandidates(
|
|
|
479
539
|
id: 'context',
|
|
480
540
|
})
|
|
481
541
|
}
|
|
482
|
-
if ((stats.usage.
|
|
542
|
+
if ((stats.usage.uncachedInputTokens > 0 || stats.usage.outputTokens > 0) && enabled.has('tokens')) {
|
|
483
543
|
const tokens: StatusSpan[] = []
|
|
484
544
|
const pair = (label: string, value: string): void => {
|
|
485
545
|
if (tokens.length > 0) tokens.push(sep())
|
|
486
546
|
tokens.push({ text: label + ' ', tone: 'label' }, { text: value, tone: 'value' })
|
|
487
547
|
}
|
|
488
|
-
pair('in', formatTokens(stats.usage.
|
|
489
|
-
pair('out', formatTokens(stats.usage.outputTokens))
|
|
548
|
+
pair(t('status.label.in'), formatTokens(stats.usage.uncachedInputTokens))
|
|
549
|
+
pair(t('status.label.out'), formatTokens(stats.usage.outputTokens))
|
|
490
550
|
row2.push({ group: { spans: tokens }, rank: RANK_TOKENS, id: 'tokens' })
|
|
491
551
|
}
|
|
492
552
|
|
|
@@ -505,8 +565,8 @@ function buildCandidates(
|
|
|
505
565
|
group: {
|
|
506
566
|
spans: [{
|
|
507
567
|
text: facts.goal.phase === 'active'
|
|
508
|
-
? '
|
|
509
|
-
: '
|
|
568
|
+
? t('status.goal.round', { current: facts.goal.rounds, max: facts.goal.max })
|
|
569
|
+
: t('status.goal.phase', { phase: safe(facts.goal.phase) }),
|
|
510
570
|
tone: 'accent',
|
|
511
571
|
}],
|
|
512
572
|
},
|
|
@@ -517,7 +577,7 @@ function buildCandidates(
|
|
|
517
577
|
// The sandbox override stays implicit when it merely echoes the preset.
|
|
518
578
|
const sandbox = safe(facts.sandbox ?? '')
|
|
519
579
|
if (sandbox !== '' && sandbox.toLowerCase() !== facts.permission.toLowerCase() && enabled.has('sandbox')) {
|
|
520
|
-
row2.push({ group: { spans: [{ text: 'sandbox ' + sandbox, tone: 'warn' }] }, rank: RANK_SANDBOX, id: 'sandbox' })
|
|
580
|
+
row2.push({ group: { spans: [{ text: t('status.label.sandbox') + ' ' + sandbox, tone: 'warn' }] }, rank: RANK_SANDBOX, id: 'sandbox' })
|
|
521
581
|
}
|
|
522
582
|
const permission = safe(facts.permission)
|
|
523
583
|
let badge = -1
|
|
@@ -530,7 +590,7 @@ function buildCandidates(
|
|
|
530
590
|
if (permission !== '' && enabled.has('permission')) {
|
|
531
591
|
right.push({
|
|
532
592
|
span: planStation
|
|
533
|
-
? { text: 'plan
|
|
593
|
+
? { text: t('status.plan.on'), tone: 'plan' }
|
|
534
594
|
: { text: permission, tone: permissionTone(permission) },
|
|
535
595
|
rank: RANK_BADGE,
|
|
536
596
|
id: 'permission',
|
|
@@ -538,7 +598,7 @@ function buildCandidates(
|
|
|
538
598
|
badge = right.length - 1
|
|
539
599
|
}
|
|
540
600
|
if (facts.plan && enabled.has('plan')) {
|
|
541
|
-
row2.push({ group: { spans: [{ text: '
|
|
601
|
+
row2.push({ group: { spans: [{ text: t('status.plan.mark'), tone: 'accent' }] }, rank: RANK2_PLAN, id: 'plan' })
|
|
542
602
|
}
|
|
543
603
|
return { left, right, badge, row2 }
|
|
544
604
|
}
|
|
@@ -546,7 +606,8 @@ function buildCandidates(
|
|
|
546
606
|
/**
|
|
547
607
|
* Compose the two-row footer layout under a column budget. Row 1 keeps model,
|
|
548
608
|
* cwd, mode, branch, context, then the right-pinned permission badge and cycle
|
|
549
|
-
* hint. It
|
|
609
|
+
* hint. It shrinks the context bar, drops the hint, and peels trailing
|
|
610
|
+
* identity facts before removing the context group or the permission badge.
|
|
550
611
|
* Row 2 fits all secondary figures and state within its own budget.
|
|
551
612
|
* @param facts - identity facts resolved by the runner.
|
|
552
613
|
* @param stats - session figures folded from the durable log.
|
|
@@ -612,16 +673,15 @@ export function layoutStatusBar(
|
|
|
612
673
|
groupSeparator,
|
|
613
674
|
)
|
|
614
675
|
const rightWidth = joinWidth(rightKept.map(entry => visibleColumns(entry.span.text)), itemSeparator)
|
|
615
|
-
+ (hint ? visibleColumns(
|
|
676
|
+
+ (hint ? visibleColumns(statusCycleHint()) : 0)
|
|
616
677
|
return rightWidth > 0 ? leftWidth + LEFT_RIGHT_GAP + rightWidth : leftWidth
|
|
617
678
|
}
|
|
618
679
|
|
|
619
680
|
while (width() > budget) {
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
// is touched.
|
|
681
|
+
// Occupancy outranks the cycle hint and trailing identity facts
|
|
682
|
+
// (cwd/mode/branch): shrink the bar, drop the hint, then peel those
|
|
683
|
+
// facts before removing the context group. Permission stays until
|
|
684
|
+
// context is already gone.
|
|
625
685
|
if (leftKept.some(entry => entry.id === 'context')) {
|
|
626
686
|
if (contextWidth > CONTEXT_MIN_WIDTH) {
|
|
627
687
|
const overflow = width() - budget
|
|
@@ -634,24 +694,21 @@ export function layoutStatusBar(
|
|
|
634
694
|
rebuildContext()
|
|
635
695
|
continue
|
|
636
696
|
}
|
|
637
|
-
|
|
697
|
+
}
|
|
698
|
+
if (hint) {
|
|
699
|
+
hint = false
|
|
638
700
|
continue
|
|
639
701
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if (identityBudget > 0 && visibleColumns(identityText) > identityBudget) {
|
|
646
|
-
leftKept[0] = {
|
|
647
|
-
...identity,
|
|
648
|
-
group: { spans: [{ text: truncateColumns(identityText, identityBudget), tone: 'model' }] },
|
|
649
|
-
}
|
|
702
|
+
const identityEntry = leftKept[0]
|
|
703
|
+
if (identityEntry?.id === 'identity') {
|
|
704
|
+
const peeled = peelIdentityFact(identityEntry.group.spans)
|
|
705
|
+
if (peeled !== undefined) {
|
|
706
|
+
leftKept[0] = { ...identityEntry, group: { spans: peeled } }
|
|
650
707
|
continue
|
|
651
708
|
}
|
|
652
709
|
}
|
|
653
|
-
if (
|
|
654
|
-
|
|
710
|
+
if (leftKept.some(entry => entry.id === 'context')) {
|
|
711
|
+
leftKept.splice(leftKept.findIndex(entry => entry.id === 'context'), 1)
|
|
655
712
|
continue
|
|
656
713
|
}
|
|
657
714
|
let dropLeft = -1
|
package/src/render/text.ts
CHANGED
|
@@ -14,6 +14,21 @@
|
|
|
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
|
+
// 999,500+ rounds to 1000K; step up to M instead of printing four digits.
|
|
27
|
+
if (n < 999_500) return scaled(n / 1_000) + 'K'
|
|
28
|
+
return scaled(n / 1_000_000) + 'M'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
17
32
|
/** C0 controls except tab (0x09) and newline (0x0a), plus DEL and C1. */
|
|
18
33
|
const CONTROL_ESCAPE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/gu
|
|
19
34
|
|
|
@@ -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
|
+
}
|