dsh-code 1.3.0 → 1.5.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 +10 -8
- package/README.md +10 -8
- package/lib/index.mjs +6980 -5942
- package/lib/session-query.mjs +18 -1
- package/lib/types/app.d.ts +38 -65
- package/lib/types/completion.d.ts +29 -0
- package/lib/types/composer.d.ts +150 -0
- package/lib/types/git-workflow.d.ts +6 -0
- package/lib/types/index.d.ts +6 -188
- package/lib/types/locales/en.d.ts +20 -8
- package/lib/types/{authorization-panel.d.ts → panels/authorization-panel.d.ts} +6 -1
- package/lib/types/panels/completion-panel.d.ts +13 -0
- package/lib/types/panels/interaction-bars.d.ts +38 -0
- package/lib/types/{kernel-panels.d.ts → panels/kernel-panels.d.ts} +16 -37
- package/lib/types/{language-panel.d.ts → panels/language-panel.d.ts} +1 -1
- package/lib/types/panels/model-panels.d.ts +86 -0
- package/lib/types/{theme-panel.d.ts → panels/theme-panel.d.ts} +1 -1
- package/lib/types/{update-panel.d.ts → panels/update-panel.d.ts} +1 -1
- package/lib/types/provider-settings.d.ts +11 -0
- package/lib/types/render/inspector.d.ts +16 -0
- package/lib/types/render/projection-events.d.ts +33 -0
- package/lib/types/render/text.d.ts +4 -0
- package/lib/types/render/transcript-viewport.d.ts +32 -0
- package/lib/types/runner/harness-gate.d.ts +83 -0
- package/lib/types/runner/input-history.d.ts +31 -0
- package/lib/types/runner/mode-cycle.d.ts +44 -0
- package/lib/types/runner/preferences.d.ts +40 -0
- package/lib/types/runner/quit.d.ts +27 -0
- package/lib/types/runner/search-rows.d.ts +38 -0
- package/lib/types/runner/session-io.d.ts +57 -0
- package/lib/types/runner/session-target.d.ts +42 -0
- package/lib/types/runner/startup-config.d.ts +33 -0
- package/lib/types/runner/submissions.d.ts +87 -0
- package/lib/types/session/attach.d.ts +39 -0
- package/lib/types/{store.d.ts → session/store.d.ts} +1 -1
- package/lib/types/{subagents.d.ts → session/subagents.d.ts} +8 -1
- package/lib/types/session-query.d.ts +11 -0
- package/lib/types/settings-file.d.ts +10 -0
- package/lib/types/{panel-accent.d.ts → ui/panel-accent.d.ts} +1 -1
- package/lib/types/ui/panel-gap.d.ts +6 -0
- package/lib/types/ui/query-editor.d.ts +10 -0
- package/lib/types/ui/styled-rows.d.ts +8 -0
- package/lib/types/{terminal-title.d.ts → ui/terminal-title.d.ts} +1 -1
- package/lib/types/ui/ui-contract.d.ts +12 -0
- package/lib/types/ui/use-frames.d.ts +6 -0
- package/lib/types/ui/use-stable-input.d.ts +7 -0
- package/package.json +3 -2
- package/src/app.ts +802 -4162
- package/src/completion.ts +117 -0
- package/src/composer.ts +1956 -0
- package/src/git-workflow.ts +18 -0
- package/src/index.ts +113 -633
- package/src/internals.ts +1 -1
- package/src/locales/en.ts +20 -8
- package/src/locales/zh.ts +20 -8
- package/src/{authorization-panel.ts → panels/authorization-panel.ts} +25 -6
- package/src/panels/completion-panel.ts +79 -0
- package/src/panels/interaction-bars.ts +567 -0
- package/src/{kernel-panels.ts → panels/kernel-panels.ts} +84 -65
- package/src/{language-panel.ts → panels/language-panel.ts} +4 -4
- package/src/panels/model-panels.ts +1021 -0
- package/src/{theme-panel.ts → panels/theme-panel.ts} +5 -5
- package/src/{update-panel.ts → panels/update-panel.ts} +6 -6
- package/src/provider-settings.ts +38 -0
- package/src/render/inspector.ts +35 -0
- package/src/render/lines.ts +45 -18
- package/src/render/projection-events.ts +109 -0
- package/src/render/projection.ts +11 -2
- package/src/render/text.ts +8 -0
- package/src/render/tool-preview.ts +35 -2
- package/src/render/transcript-viewport.ts +61 -0
- package/src/render/usage.ts +1 -2
- package/src/runner/harness-gate.ts +168 -0
- package/src/runner/input-history.ts +77 -0
- package/src/runner/mode-cycle.ts +49 -0
- package/src/runner/preferences.ts +67 -0
- package/src/runner/quit.ts +53 -0
- package/src/runner/search-rows.ts +55 -0
- package/src/runner/session-io.ts +245 -0
- package/src/runner/session-target.ts +81 -0
- package/src/runner/startup-config.ts +54 -0
- package/src/runner/submissions.ts +157 -0
- package/src/session/attach.ts +87 -0
- package/src/{session-directory.ts → session/session-directory.ts} +1 -1
- package/src/{store.ts → session/store.ts} +1 -1
- package/src/{subagents.ts → session/subagents.ts} +12 -1
- package/src/session-query.ts +22 -0
- package/src/settings-file.ts +19 -1
- package/src/{panel-accent.ts → ui/panel-accent.ts} +1 -1
- package/src/ui/panel-gap.ts +9 -0
- package/src/ui/query-editor.ts +16 -0
- package/src/ui/styled-rows.ts +124 -0
- package/src/{terminal-title.ts → ui/terminal-title.ts} +1 -1
- package/src/ui/ui-contract.ts +10 -0
- package/src/ui/use-frames.ts +23 -0
- package/src/ui/use-stable-input.ts +17 -0
- /package/lib/types/{fork.d.ts → session/fork.d.ts} +0 -0
- /package/lib/types/{history.d.ts → session/history.d.ts} +0 -0
- /package/lib/types/{session-directory.d.ts → session/session-directory.d.ts} +0 -0
- /package/lib/types/{session-switch.d.ts → session/session-switch.d.ts} +0 -0
- /package/src/{fork.ts → session/fork.ts} +0 -0
- /package/src/{history.ts → session/history.ts} +0 -0
- /package/src/{session-switch.ts → session/session-switch.ts} +0 -0
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import { createElement, useState, type ReactElement } from 'react'
|
|
11
11
|
import { Box, Text, useInput, useStdout } from 'ink'
|
|
12
|
-
import { clampScroll, panelViewport } from '
|
|
13
|
-
import { truncateColumns } from '
|
|
14
|
-
import { panelAccent } from '
|
|
15
|
-
import { getPalette, inkColor, THEMES, type ThemeName } from '
|
|
16
|
-
import { t } from '
|
|
12
|
+
import { clampScroll, panelViewport } from '../render/inspector.ts'
|
|
13
|
+
import { truncateColumns } from '../render/text.ts'
|
|
14
|
+
import { panelAccent } from '../ui/panel-accent.ts'
|
|
15
|
+
import { getPalette, inkColor, THEMES, type ThemeName } from '../theme.ts'
|
|
16
|
+
import { t } from '../i18n.ts'
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* The /theme list: one row per theme, the current one marked with ●, the
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
|
|
12
12
|
import { createElement, useEffect, useState, type ReactElement } from 'react'
|
|
13
13
|
import { Box, Text, useInput, useStdout } from 'ink'
|
|
14
|
-
import type { LauncherUpdatePlan, LauncherUpdateStatus } from '
|
|
15
|
-
import { clampScroll, panelViewport } from '
|
|
16
|
-
import { singleLineText, truncateColumns } from '
|
|
17
|
-
import { panelAccent } from '
|
|
18
|
-
import { getPalette, inkColor } from '
|
|
19
|
-
import { t } from '
|
|
14
|
+
import type { LauncherUpdatePlan, LauncherUpdateStatus } from '../update.ts'
|
|
15
|
+
import { clampScroll, panelViewport } from '../render/inspector.ts'
|
|
16
|
+
import { singleLineText, truncateColumns } from '../render/text.ts'
|
|
17
|
+
import { panelAccent } from '../ui/panel-accent.ts'
|
|
18
|
+
import { getPalette, inkColor } from '../theme.ts'
|
|
19
|
+
import { t } from '../i18n.ts'
|
|
20
20
|
|
|
21
21
|
/** Retained apply-progress lines (ring tail; npm output is ephemeral). */
|
|
22
22
|
export const UPDATE_OUTPUT_CAP = 800
|
package/src/provider-settings.ts
CHANGED
|
@@ -666,6 +666,44 @@ export async function saveProviderConfiguration(
|
|
|
666
666
|
}
|
|
667
667
|
}
|
|
668
668
|
|
|
669
|
+
/**
|
|
670
|
+
* Switch one provider route to its subscription (plan sign-in) channel: the
|
|
671
|
+
* profile keeps neither a key reference nor an explicit model list, so the
|
|
672
|
+
* official catalog endpoint serves requests and the stored OAuth record — not
|
|
673
|
+
* a settings-named key override — authenticates them. Enabling a route that
|
|
674
|
+
* has no profile yet creates the empty profile that registers it.
|
|
675
|
+
* @param ctx - context carrying the `settings` service.
|
|
676
|
+
* @param target - provider row whose settings namespace owns the profile.
|
|
677
|
+
* @throws when the route is unmanaged or settings are read-only.
|
|
678
|
+
*/
|
|
679
|
+
export async function enableProviderSubscription(ctx: Context, target: ProviderTargetView): Promise<void> {
|
|
680
|
+
if (target.settingsNs.length === 0) {
|
|
681
|
+
throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings namespace; configure it in settings.yaml`)
|
|
682
|
+
}
|
|
683
|
+
const settings = ctx.get('settings') as SettingsFace | undefined
|
|
684
|
+
if (settings === undefined || settings.writable !== true) {
|
|
685
|
+
throw new ProviderSettingsError('settings are read-only; provider configuration cannot be changed here')
|
|
686
|
+
}
|
|
687
|
+
const root = target.settingsPath
|
|
688
|
+
const ops: SettingsPathOpFace[] = target.configured
|
|
689
|
+
? [
|
|
690
|
+
// The key reference would resolve as a request-level override BEFORE
|
|
691
|
+
// the stored sign-in record, so the subscription channel must drop it;
|
|
692
|
+
// the endpoint and the explicit model list belong to the key channel.
|
|
693
|
+
{ op: 'unset', path: [...root, 'apiKeyEnv'] },
|
|
694
|
+
{ op: 'unset', path: [...root, 'baseURL'] },
|
|
695
|
+
{ op: 'unset', path: [...root, 'models'] },
|
|
696
|
+
]
|
|
697
|
+
// A route with no profile stays dormant: an empty profile registers it
|
|
698
|
+
// with the catalog endpoint and every catalog model.
|
|
699
|
+
: [{ op: 'set', path: [...root], value: {} }]
|
|
700
|
+
try {
|
|
701
|
+
await settings.mutate(target.settingsNs, ops, target.settingsRevision)
|
|
702
|
+
} catch (error) {
|
|
703
|
+
throw new ProviderSettingsError(singleLine(messageOf(error)))
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
669
707
|
/**
|
|
670
708
|
* Interrogate a provider endpoint for the models it really serves, through
|
|
671
709
|
* the model-discovery capability the provider's settings namespace
|
package/src/render/inspector.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
/** Pure viewport, selection, and scrolling rules for exclusive TUI panels. */
|
|
2
2
|
|
|
3
|
+
import type { TranscriptEntry } from './projection.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Remove assistant settlements that contain reasoning but no final response.
|
|
7
|
+
* They remain in the durable transcript and in the Ctrl/Alt+R fold; excluding
|
|
8
|
+
* them only from Ctrl+O navigation prevents short progress thoughts from
|
|
9
|
+
* appearing as a long run of misleading `reply` entries.
|
|
10
|
+
*/
|
|
11
|
+
export function inspectableTranscriptEntries(entries: readonly TranscriptEntry[]): readonly TranscriptEntry[] {
|
|
12
|
+
return entries.filter(entry => !(entry.kind === 'assistant' && entry.text.trim() === '' && entry.reasoning.trim() !== ''))
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
/** Terminal-space allocation for the inspector's one dynamic screen. */
|
|
4
16
|
export interface InspectorViewport {
|
|
5
17
|
/** Maximum dynamic rows, kept strictly below the terminal height. */
|
|
@@ -99,6 +111,29 @@ export function panelViewport(columns: number, rows: number): InspectorViewport
|
|
|
99
111
|
}
|
|
100
112
|
}
|
|
101
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Viewport for an expanded (near-fullscreen) document overlay: the document
|
|
116
|
+
* replaces the transcript area entirely, so the half-screen overlay cap and
|
|
117
|
+
* the inter-section gaps drop away while the bottom live region (composer,
|
|
118
|
+
* status) and the Static-rewrite margin stay reserved. The result stays
|
|
119
|
+
* below terminal height, which is what keeps Ink off its full-rewrite path.
|
|
120
|
+
*/
|
|
121
|
+
export function expandedDocumentViewport(columns: number, rows: number): InspectorViewport {
|
|
122
|
+
const safeColumns = Math.max(1, Math.floor(columns))
|
|
123
|
+
const safeRows = Math.max(1, Math.floor(rows))
|
|
124
|
+
const maxHeight = Math.max(0, safeRows - 2 - INSPECTOR_CHROME_ROWS - layoutGutterRows(safeRows))
|
|
125
|
+
const compact = maxHeight < 5 || safeColumns < 8
|
|
126
|
+
const outerColumns = compact ? safeColumns : Math.max(1, safeColumns - 1)
|
|
127
|
+
return {
|
|
128
|
+
maxHeight,
|
|
129
|
+
bodyRows: compact ? 0 : maxHeight - 4,
|
|
130
|
+
gapRows: 0,
|
|
131
|
+
contentColumns: compact ? Math.max(1, safeColumns - 1) : Math.max(1, outerColumns - 4),
|
|
132
|
+
outerColumns,
|
|
133
|
+
compact,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
102
137
|
/** Backward-compatible name for the Ctrl+O-specific caller and tests. */
|
|
103
138
|
export function inspectorViewport(columns: number, rows: number): InspectorViewport {
|
|
104
139
|
return panelViewport(columns, rows)
|
package/src/render/lines.ts
CHANGED
|
@@ -243,7 +243,7 @@ export function reasoningLines(text: string, columns: number): readonly StyledLi
|
|
|
243
243
|
* diff/read/web/raw rows align under them instead of floating two columns
|
|
244
244
|
* shallower.
|
|
245
245
|
*/
|
|
246
|
-
function toolDetailLines(detail: ToolDetail, columns: number): readonly StyledLine[] {
|
|
246
|
+
function toolDetailLines(detail: ToolDetail, columns: number, rawStyle: LineStyle): readonly StyledLine[] {
|
|
247
247
|
switch (detail.kind) {
|
|
248
248
|
case 'diff':
|
|
249
249
|
return detail.diffs.flatMap(diff => [
|
|
@@ -280,8 +280,8 @@ function toolDetailLines(detail: ToolDetail, columns: number): readonly StyledLi
|
|
|
280
280
|
return prefixedTextLines(`${detail.url} · HTTP ${detail.statusCode}`, columns, ' ', 'dim')
|
|
281
281
|
case 'raw':
|
|
282
282
|
return [
|
|
283
|
-
...prefixedTextLines(detail.text, columns, ' ',
|
|
284
|
-
...prefixedTextLines(detail.truncated ? '… (output truncated)' : '(end of output)', columns, ' ',
|
|
283
|
+
...prefixedTextLines(detail.text, columns, ' ', rawStyle),
|
|
284
|
+
...prefixedTextLines(detail.truncated ? '… (output truncated)' : '(end of output)', columns, ' ', rawStyle),
|
|
285
285
|
]
|
|
286
286
|
default: {
|
|
287
287
|
const exhaustive: never = detail
|
|
@@ -410,12 +410,14 @@ function subDispatchLines(entry: ToolEntry, columns: number): readonly StyledLin
|
|
|
410
410
|
const width = Math.max(1, Math.floor(columns))
|
|
411
411
|
const rows = entry.subs.flatMap(sub => {
|
|
412
412
|
const mark = sub.state === 'running' ? '●' : sub.state === 'error' ? '⨯' : '⏺'
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
413
|
+
const markStyle: LineStyle = sub.state === 'running' ? 'brand' : sub.state === 'error' ? 'error' : 'success'
|
|
414
|
+
return hangingStyledLines([
|
|
415
|
+
lineSegment(`${mark} `, markStyle),
|
|
416
|
+
lineSegment(sub.name, 'brand'),
|
|
417
|
+
lineSegment(sub.preview === '' ? '' : ` ${sub.preview}`, 'dim'),
|
|
418
|
+
lineSegment(sub.state === 'error' && sub.summary !== '' ? ` · ${sub.summary}` : '', sub.state === 'error' ? 'error' : 'dim'),
|
|
419
|
+
lineSegment(subDispatchSeconds(sub.durationMs), 'dim'),
|
|
420
|
+
], width, ' ┆ ', 'dim', ' ┆ ', 'dim')
|
|
419
421
|
})
|
|
420
422
|
if (entry.subsDropped === 0) return rows
|
|
421
423
|
return [...rows, ...textLines(` ┆ … ${entry.subsDropped} earlier dispatch${entry.subsDropped === 1 ? '' : 'es'}`, width, 'dim')]
|
|
@@ -491,7 +493,7 @@ export function transcriptEntryLines(
|
|
|
491
493
|
const mark = entry.state === 'running' ? '●' : entry.state === 'error' ? '⨯' : '⏺'
|
|
492
494
|
const markStyle: LineStyle = entry.state === 'running' ? 'brand' : entry.state === 'error' ? 'error' : 'success'
|
|
493
495
|
const summaryStyle: LineStyle = entry.state === 'error' ? 'error' : 'dim'
|
|
494
|
-
const
|
|
496
|
+
const invocation = [
|
|
495
497
|
// The invocation row hangs wrapped previews under the call badge.
|
|
496
498
|
...hangingStyledLines([
|
|
497
499
|
// Global call ordinal — the same number an error line references.
|
|
@@ -505,13 +507,35 @@ export function transcriptEntryLines(
|
|
|
505
507
|
// Nested PTC sub-dispatches (run_code): one bounded row per child
|
|
506
508
|
// call, live while the parent card itself is still running.
|
|
507
509
|
...subDispatchLines(entry, width),
|
|
508
|
-
...(entry.summary === '' ? [] : hangingTextLines(
|
|
509
|
-
entry.state === 'error' ? `call ${entry.ordinal}: ${entry.summary}` : entry.summary,
|
|
510
|
-
width, ' ⎿ ', summaryStyle, ' ', summaryStyle,
|
|
511
|
-
)),
|
|
512
|
-
...(entry.detail === undefined ? [] : toolDetailLines(entry.detail, width)),
|
|
513
510
|
]
|
|
514
|
-
|
|
511
|
+
const summary = entry.summary === '' ? [] : hangingTextLines(
|
|
512
|
+
entry.state === 'error' ? `call ${entry.ordinal}: ${entry.summary}` : entry.summary,
|
|
513
|
+
width, ' ⎿ ', summaryStyle, ' ', summaryStyle,
|
|
514
|
+
)
|
|
515
|
+
// Compact cards carry the bounded result summary only. Keep one fold
|
|
516
|
+
// hint when richer detail exists, but not when a complete one-line raw
|
|
517
|
+
// result is byte-for-byte the summary (that false hint was one symptom
|
|
518
|
+
// of rendering summary and detail together).
|
|
519
|
+
if (!showToolDetails) {
|
|
520
|
+
const rawFullySummarized = entry.detail?.kind === 'raw'
|
|
521
|
+
&& !entry.detail.truncated
|
|
522
|
+
&& entry.detail.text.trim() === entry.summary.trim()
|
|
523
|
+
const hiddenDetail = entry.detail !== undefined && !rawFullySummarized
|
|
524
|
+
? textLines(' … output hidden · Ctrl/Alt+R', width, 'dim').slice(0, 1)
|
|
525
|
+
: []
|
|
526
|
+
return compactToolLines([...invocation, ...summary, ...hiddenDetail], width)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const detail = entry.detail === undefined
|
|
530
|
+
? []
|
|
531
|
+
: toolDetailLines(entry.detail, width, entry.state === 'error' ? 'error' : 'dim')
|
|
532
|
+
// Raw output and structured read/diff views already contain the result
|
|
533
|
+
// represented by `summary`; web details are supplemental metadata, so
|
|
534
|
+
// those keep both the answer summary and their source/status rows.
|
|
535
|
+
const detailSupersedesSummary = entry.detail?.kind === 'raw'
|
|
536
|
+
|| entry.detail?.kind === 'read'
|
|
537
|
+
|| entry.detail?.kind === 'diff'
|
|
538
|
+
return [...invocation, ...(detailSupersedesSummary ? [] : summary), ...detail]
|
|
515
539
|
}
|
|
516
540
|
case 'command': {
|
|
517
541
|
const mark = entry.state === 'running' ? '●' : entry.state === 'error' ? '⨯' : '⏺'
|
|
@@ -537,9 +561,12 @@ export function transcriptEntryLines(
|
|
|
537
561
|
// subagent feed's live rows for the same agent.
|
|
538
562
|
...entry.members.flatMap(member => {
|
|
539
563
|
const state = member.outcome === 'running' ? '●' : member.outcome === 'failed' ? '⨯' : member.outcome === 'cancelled' ? '⏹' : '⏺'
|
|
540
|
-
const
|
|
564
|
+
const stateStyle: LineStyle = member.outcome === 'running' ? 'brand' : member.outcome === 'failed' ? 'error' : member.outcome === 'cancelled' ? 'dim' : 'success'
|
|
541
565
|
const phase = member.phase === '' ? '' : ` [${member.phase}]`
|
|
542
|
-
return
|
|
566
|
+
return hangingStyledLines([
|
|
567
|
+
lineSegment(`${state} `, stateStyle),
|
|
568
|
+
lineSegment(`${member.label}${phase}`, 'dim'),
|
|
569
|
+
], width, ' ┆ ', 'dim', ' ┆ ', 'dim')
|
|
543
570
|
}),
|
|
544
571
|
...(entry.membersDropped === 0 ? [] : textLines(` ┆ … ${entry.membersDropped} earlier member${entry.membersDropped === 1 ? '' : 's'}`, width, 'dim')),
|
|
545
572
|
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit compatibility policy for durable session events entering the TUI.
|
|
3
|
+
*
|
|
4
|
+
* The Harness event map is merge-extensible, so a reducer default branch is
|
|
5
|
+
* unavoidable: deployments may mount plugins this bundle does not know. The
|
|
6
|
+
* default must not, however, let a NEW first-party event silently bypass
|
|
7
|
+
* review. Tests compare this policy with `KNOWN_SESSION_EVENT_TYPES`; every
|
|
8
|
+
* kernel event must be deliberately classified before a Harness-line bump.
|
|
9
|
+
*
|
|
10
|
+
* @module @deepseek-ai/dsh-tui/render/projection-events
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** How one durable event relates to the terminal transcript projection. */
|
|
14
|
+
export type SessionEventDisposition =
|
|
15
|
+
| 'transcript'
|
|
16
|
+
| 'other-surface'
|
|
17
|
+
| 'ignored'
|
|
18
|
+
| 'unknown'
|
|
19
|
+
|
|
20
|
+
/** Events folded directly into `TranscriptView`. */
|
|
21
|
+
export const TRANSCRIPT_SESSION_EVENT_TYPES = [
|
|
22
|
+
'agent/inbox/spliced',
|
|
23
|
+
'assistant/attempt',
|
|
24
|
+
'assistant/message',
|
|
25
|
+
'command/done',
|
|
26
|
+
'command/run',
|
|
27
|
+
'compaction/end',
|
|
28
|
+
'compaction/prune',
|
|
29
|
+
'compaction/summary',
|
|
30
|
+
'goal/change',
|
|
31
|
+
'llm/retry',
|
|
32
|
+
'llm/retry-started',
|
|
33
|
+
'permission/preset',
|
|
34
|
+
'plan/mode',
|
|
35
|
+
'request/context',
|
|
36
|
+
'request/header',
|
|
37
|
+
'sandbox/mode',
|
|
38
|
+
'schedule/change',
|
|
39
|
+
'session/title',
|
|
40
|
+
'step/start',
|
|
41
|
+
'system/message',
|
|
42
|
+
'todo/write',
|
|
43
|
+
'tool-workflow/agent-end',
|
|
44
|
+
'tool-workflow/agent-start',
|
|
45
|
+
'tool-workflow/run-end',
|
|
46
|
+
'tool-workflow/run-start',
|
|
47
|
+
'tool/call',
|
|
48
|
+
'tool/ptc-dispatch',
|
|
49
|
+
'tool/ptc-dispatch-start',
|
|
50
|
+
'tool/result',
|
|
51
|
+
'turn/end',
|
|
52
|
+
'turn/start',
|
|
53
|
+
'user/message',
|
|
54
|
+
] as const
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Events consumed by another TUI adapter rather than the transcript fold.
|
|
58
|
+
* Keeping these separate documents that they are visible behavior, not
|
|
59
|
+
* accidental no-ops.
|
|
60
|
+
*/
|
|
61
|
+
export const OTHER_SURFACE_SESSION_EVENT_TYPES = [
|
|
62
|
+
'agent-preset/selected',
|
|
63
|
+
'model/selection',
|
|
64
|
+
'subagent/catalog',
|
|
65
|
+
] as const
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Kernel events deliberately omitted from terminal presentation. Their
|
|
69
|
+
* model-visible consequences arrive through projected message/tool/session
|
|
70
|
+
* events, while audit-only facts do not need a second transcript row.
|
|
71
|
+
*/
|
|
72
|
+
export const IGNORED_SESSION_EVENT_TYPES = [
|
|
73
|
+
'approval/asked',
|
|
74
|
+
'approval/decided',
|
|
75
|
+
'approval/policy',
|
|
76
|
+
'compaction/start',
|
|
77
|
+
'deliverables/presented',
|
|
78
|
+
'feedback/message-delete',
|
|
79
|
+
'feedback/message-put',
|
|
80
|
+
'feedback/record',
|
|
81
|
+
'hook/invoked',
|
|
82
|
+
'hook/result',
|
|
83
|
+
'session-log-deepseek/delivery-accepted',
|
|
84
|
+
'session/end-seed',
|
|
85
|
+
'session/title-llm-request',
|
|
86
|
+
'step/end',
|
|
87
|
+
'subagent/descriptor',
|
|
88
|
+
'subagent/model-selection-policy',
|
|
89
|
+
'team/member',
|
|
90
|
+
'team/message/delivered',
|
|
91
|
+
'team/message/queued',
|
|
92
|
+
'team/task',
|
|
93
|
+
'web/deepseek-search-llm-request',
|
|
94
|
+
] as const
|
|
95
|
+
|
|
96
|
+
const EVENT_DISPOSITIONS = new Map<string, Exclude<SessionEventDisposition, 'unknown'>>([
|
|
97
|
+
...TRANSCRIPT_SESSION_EVENT_TYPES.map(type => [type, 'transcript'] as const),
|
|
98
|
+
...OTHER_SURFACE_SESSION_EVENT_TYPES.map(type => [type, 'other-surface'] as const),
|
|
99
|
+
...IGNORED_SESSION_EVENT_TYPES.map(type => [type, 'ignored'] as const),
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Classify a durable event without rejecting merge-extensible plugin events.
|
|
104
|
+
* `unknown` remains a runtime no-op for forward/plugin compatibility; the
|
|
105
|
+
* known-event coverage test is what makes a first-party addition fail CI.
|
|
106
|
+
*/
|
|
107
|
+
export function sessionEventDisposition(type: string): SessionEventDisposition {
|
|
108
|
+
return EVENT_DISPOSITIONS.get(type) ?? 'unknown'
|
|
109
|
+
}
|
package/src/render/projection.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { FileAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
|
|
12
12
|
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
13
13
|
import type { TodoItem } from '@deepseek-ai/dsh-tool-todo'
|
|
14
14
|
import { graphemeWidth, splitGraphemes } from './width.ts'
|
|
15
|
+
import { sessionEventDisposition } from './projection-events.ts'
|
|
15
16
|
// Type-only imports merge the plugin-owned SessionEventMap variants
|
|
16
17
|
// (agent/inbox/spliced, command/*, compaction/*, goal/change, llm/retry*,
|
|
17
18
|
// plan/mode, permission/preset, sandbox/mode, session/title) into the union
|
|
@@ -796,6 +797,10 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
796
797
|
stats: { ...view.stats, contextSegments: { ...view.stats.contextSegments, system: estimateTokens(shadow.prompt) } },
|
|
797
798
|
}
|
|
798
799
|
}
|
|
800
|
+
// Known non-transcript events are deliberate no-ops. Unknown plugin events
|
|
801
|
+
// remain tolerated at runtime, while the known-event coverage test makes a
|
|
802
|
+
// new first-party kernel event fail CI until its disposition is reviewed.
|
|
803
|
+
if (sessionEventDisposition(event.type) !== 'transcript') return view
|
|
799
804
|
switch (event.type) {
|
|
800
805
|
case 'user/message': {
|
|
801
806
|
// A queued row retires when its durable user message lands (the agent
|
|
@@ -1477,7 +1482,10 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
1477
1482
|
: { ...entry, state: data.stopReason })
|
|
1478
1483
|
return { ...view, entries }
|
|
1479
1484
|
}
|
|
1480
|
-
|
|
1485
|
+
// The policy classified this event as transcript-visible, so reaching
|
|
1486
|
+
// the end means its reducer branch was omitted. Fail loudly instead of
|
|
1487
|
+
// shipping a silently stale terminal projection.
|
|
1488
|
+
throw new Error(`missing transcript projection for session event "${type}"`)
|
|
1481
1489
|
}
|
|
1482
1490
|
}
|
|
1483
1491
|
}
|
|
@@ -1694,6 +1702,7 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
|
|
|
1694
1702
|
acc.systemPrompt = shadow.prompt
|
|
1695
1703
|
acc.stats = { ...acc.stats, contextSegments: { ...acc.stats.contextSegments, system: estimateTokens(shadow.prompt) } }
|
|
1696
1704
|
}
|
|
1705
|
+
if (sessionEventDisposition(event.type) !== 'transcript') return shadow.changed
|
|
1697
1706
|
switch (event.type) {
|
|
1698
1707
|
case 'user/message': {
|
|
1699
1708
|
const message = event.data
|
|
@@ -2211,7 +2220,7 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
|
|
|
2211
2220
|
({ ...entry, state: data.stopReason }))
|
|
2212
2221
|
return true
|
|
2213
2222
|
}
|
|
2214
|
-
|
|
2223
|
+
throw new Error(`missing transcript replay projection for session event "${type}"`)
|
|
2215
2224
|
}
|
|
2216
2225
|
}
|
|
2217
2226
|
}
|
package/src/render/text.ts
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import { graphemeWidth, splitGraphemes, stringWidth } from './width.ts'
|
|
16
16
|
|
|
17
|
+
export { stringWidth }
|
|
18
|
+
|
|
17
19
|
/**
|
|
18
20
|
* Compact token count: exact below 1K, then one-decimal-ish K/M.
|
|
19
21
|
* @param n - token count.
|
|
@@ -84,6 +86,12 @@ export function truncateColumns(text: string, columns: number): string {
|
|
|
84
86
|
return `${result}…`
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
/** Sanitize, clip, and right-pad one cell to an exact terminal-column width. */
|
|
90
|
+
export function padColumns(text: string, columns: number): string {
|
|
91
|
+
const clipped = truncateColumns(singleLineText(text), columns)
|
|
92
|
+
return clipped + ' '.repeat(Math.max(0, columns - stringWidth(clipped)))
|
|
93
|
+
}
|
|
94
|
+
|
|
87
95
|
/** A display-safe suffix bounded by terminal rows and columns. */
|
|
88
96
|
export interface DisplayTail {
|
|
89
97
|
/** Sanitized suffix suitable for direct terminal rendering. */
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/** Keys searched in declaration order when building a preview. */
|
|
14
|
-
const PREVIEW_KEYS = ['command', 'cmd', 'description', 'path', 'pattern', 'query'] as const
|
|
14
|
+
const PREVIEW_KEYS = ['command', 'cmd', 'file_path', 'description', 'path', 'pattern', 'query', 'name'] as const
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Raw arguments longer than this are skipped without parsing and fall back
|
|
@@ -25,6 +25,33 @@ function boundedRawPreview(args: string): string {
|
|
|
25
25
|
return args.length > 80 ? `${args.slice(0, 77)}...` : args
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Escape a fixed preview key before embedding it in a bounded regex. */
|
|
29
|
+
function regexKey(key: string): string {
|
|
30
|
+
return key.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Recover one complete JSON string property from the bounded head/tail of a
|
|
35
|
+
* large argument object. Write/edit payloads commonly put a multi-kilobyte
|
|
36
|
+
* `content` first and `file_path` last; parsing the whole payload is forbidden
|
|
37
|
+
* on the render path, but the short trailing path is still safe to decode.
|
|
38
|
+
*/
|
|
39
|
+
function boundedStringProperty(args: string, key: string): string | undefined {
|
|
40
|
+
const slices = [args.slice(0, MAX_PARSE_CHARS), args.slice(-MAX_PARSE_CHARS)]
|
|
41
|
+
const pattern = new RegExp(`"${regexKey(key)}"\\s*:\\s*("(?:\\\\.|[^"\\\\])*")`, 'u')
|
|
42
|
+
for (const slice of slices) {
|
|
43
|
+
const literal = pattern.exec(slice)?.[1]
|
|
44
|
+
if (literal === undefined) continue
|
|
45
|
+
try {
|
|
46
|
+
const value: unknown = JSON.parse(literal)
|
|
47
|
+
if (typeof value === 'string' && value !== '') return value
|
|
48
|
+
} catch {
|
|
49
|
+
// A property crossing the bounded slice edge is ignored.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
28
55
|
/**
|
|
29
56
|
* Resolve one bounded preview for raw tool arguments.
|
|
30
57
|
* @param args - raw JSON arguments string as the model produced it.
|
|
@@ -33,7 +60,13 @@ function boundedRawPreview(args: string): string {
|
|
|
33
60
|
*/
|
|
34
61
|
export function toolArgumentsPreview(args: string, toolName: string): string {
|
|
35
62
|
if (args === '') return toolName
|
|
36
|
-
if (args.length > MAX_PARSE_CHARS)
|
|
63
|
+
if (args.length > MAX_PARSE_CHARS) {
|
|
64
|
+
for (const key of PREVIEW_KEYS) {
|
|
65
|
+
const value = boundedStringProperty(args, key)
|
|
66
|
+
if (value !== undefined) return value
|
|
67
|
+
}
|
|
68
|
+
return boundedRawPreview(args)
|
|
69
|
+
}
|
|
37
70
|
try {
|
|
38
71
|
const parsed: unknown = JSON.parse(args)
|
|
39
72
|
if (parsed !== null && typeof parsed === 'object') {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Physical-row viewport state for native scrollback plus a bottom-anchored tail. */
|
|
2
|
+
|
|
3
|
+
export interface TranscriptViewportCursor {
|
|
4
|
+
readonly sessionKey: string
|
|
5
|
+
readonly epoch: number
|
|
6
|
+
readonly flushedRows: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TranscriptViewportInput {
|
|
10
|
+
readonly sessionKey: string
|
|
11
|
+
readonly epoch: number
|
|
12
|
+
/** Physical durable transcript rows in the current rendered-history window. */
|
|
13
|
+
readonly totalRows: number
|
|
14
|
+
/** Maximum durable rows retained outside Static for the live viewport. */
|
|
15
|
+
readonly retainedRows: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TranscriptViewportStep {
|
|
19
|
+
readonly cursor: TranscriptViewportCursor
|
|
20
|
+
/** Durable physical rows emitted through Static. */
|
|
21
|
+
readonly staticRows: number
|
|
22
|
+
/** Durable physical rows still available to the live viewport. */
|
|
23
|
+
readonly liveRows: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Advance the monotonic physical-row flush cursor.
|
|
28
|
+
*
|
|
29
|
+
* Ordinary renders never pull a row back out of native scrollback. A source-
|
|
30
|
+
* backed replay (epoch change) flushes the complete durable window exactly
|
|
31
|
+
* once after the caller clears/remounts Static; subsequent durable rows start
|
|
32
|
+
* a fresh retained tail. Session/reset shrink starts from the new source.
|
|
33
|
+
*/
|
|
34
|
+
export function advanceTranscriptViewport(
|
|
35
|
+
previous: TranscriptViewportCursor,
|
|
36
|
+
input: TranscriptViewportInput,
|
|
37
|
+
): TranscriptViewportStep {
|
|
38
|
+
const total = Math.max(0, Math.floor(input.totalRows))
|
|
39
|
+
const retained = Math.max(0, Math.floor(input.retainedRows))
|
|
40
|
+
const desiredStatic = Math.max(0, total - retained)
|
|
41
|
+
const staticRows = previous.sessionKey !== input.sessionKey
|
|
42
|
+
? desiredStatic
|
|
43
|
+
: previous.epoch !== input.epoch
|
|
44
|
+
? total
|
|
45
|
+
: total < previous.flushedRows
|
|
46
|
+
? desiredStatic
|
|
47
|
+
: Math.max(previous.flushedRows, desiredStatic)
|
|
48
|
+
return {
|
|
49
|
+
cursor: { sessionKey: input.sessionKey, epoch: input.epoch, flushedRows: staticRows },
|
|
50
|
+
staticRows,
|
|
51
|
+
liveRows: total - staticRows,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Real history rows that fit beside the currently painted stream/tool rows. */
|
|
56
|
+
export function visibleTranscriptRows(historyRows: number, occupiedRows: number, capacity: number): number {
|
|
57
|
+
const history = Math.max(0, Math.floor(historyRows))
|
|
58
|
+
const occupied = Math.max(0, Math.floor(occupiedRows))
|
|
59
|
+
const available = Math.max(0, Math.floor(capacity) - occupied)
|
|
60
|
+
return Math.min(history, available)
|
|
61
|
+
}
|
package/src/render/usage.ts
CHANGED
|
@@ -15,8 +15,7 @@ import type { TokenUsageProjection, TurnTokenUsage } from '@deepseek-ai/dsh-toke
|
|
|
15
15
|
import { t } from '../i18n.ts'
|
|
16
16
|
import { lineSegment, type LineStyle, type StyledLine, type StyledSegment } from './lines.ts'
|
|
17
17
|
import { visibleColumns } from './markdown.ts'
|
|
18
|
-
import { formatTokens } from './
|
|
19
|
-
import { truncateColumns } from './text.ts'
|
|
18
|
+
import { formatTokens, truncateColumns } from './text.ts'
|
|
20
19
|
|
|
21
20
|
/** One completed turn's exact provider-reported accounting. */
|
|
22
21
|
export interface UsageTurn {
|