@itookit/dsht 0.3.0 → 0.3.3
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.i18n.yaml +2 -2
- package/README.md +39 -23
- package/README.zh.md +40 -24
- package/dist/catalog/controller.d.ts +32 -0
- package/dist/catalog/controller.js +88 -0
- package/dist/catalog/index.d.ts +2 -0
- package/dist/catalog/index.js +2 -0
- package/dist/{cli.js → cli/index.js} +42 -29
- package/dist/controller/connection.d.ts +80 -0
- package/dist/controller/connection.js +190 -0
- package/dist/controller/controller.d.ts +269 -0
- package/dist/controller/controller.js +372 -0
- package/dist/controller/index.d.ts +5 -0
- package/dist/controller/index.js +3 -0
- package/dist/controller/memory-log.d.ts +35 -0
- package/dist/controller/memory-log.js +95 -0
- package/dist/cost/config.d.ts +17 -0
- package/dist/cost/config.js +68 -0
- package/dist/cost/controller.d.ts +41 -0
- package/dist/cost/controller.js +115 -0
- package/dist/cost/index.d.ts +10 -0
- package/dist/cost/index.js +8 -0
- package/dist/cost/ledger-files.d.ts +16 -0
- package/dist/cost/ledger-files.js +103 -0
- package/dist/cost/ledger.d.ts +78 -0
- package/dist/cost/ledger.js +146 -0
- package/dist/cost/pricing.d.ts +86 -0
- package/dist/cost/pricing.js +223 -0
- package/dist/cost/records.d.ts +17 -0
- package/dist/cost/records.js +79 -0
- package/dist/cost/scanner.d.ts +22 -0
- package/dist/cost/scanner.js +95 -0
- package/dist/cost/types.d.ts +85 -0
- package/dist/cost/types.js +7 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/session/connection-view.d.ts +18 -0
- package/dist/session/connection-view.js +1 -0
- package/dist/{controller.d.ts → session/controller.d.ts} +102 -136
- package/dist/session/controller.js +616 -0
- package/dist/session/export-html.d.ts +9 -0
- package/dist/session/export-html.js +39 -0
- package/dist/{export.d.ts → session/export.d.ts} +1 -1
- package/dist/{export.js → session/export.js} +6 -20
- package/dist/{history.d.ts → session/history.d.ts} +17 -0
- package/dist/{history.js → session/history.js} +163 -2
- package/dist/session/index.d.ts +17 -0
- package/dist/session/index.js +10 -0
- package/dist/session/markdown.d.ts +39 -0
- package/dist/session/markdown.js +255 -0
- package/dist/session/math.d.ts +11 -0
- package/dist/session/math.js +82 -0
- package/dist/session/navigation.d.ts +37 -0
- package/dist/session/navigation.js +84 -0
- package/dist/{references.js → session/references.js} +1 -1
- package/dist/{telemetry.d.ts → session/telemetry.d.ts} +1 -1
- package/dist/{telemetry.js → session/telemetry.js} +1 -1
- package/dist/{transcript.d.ts → session/transcript.d.ts} +65 -1
- package/dist/{transcript.js → session/transcript.js} +141 -20
- package/dist/session/types.d.ts +18 -0
- package/dist/session/types.js +2 -0
- package/dist/state.d.ts +41 -0
- package/dist/state.js +9 -0
- package/dist/storage/directories.d.ts +14 -0
- package/dist/storage/directories.js +24 -0
- package/dist/storage/files.d.ts +51 -0
- package/dist/storage/files.js +152 -0
- package/dist/storage/heap-snapshot.d.ts +19 -0
- package/dist/storage/heap-snapshot.js +29 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +4 -0
- package/dist/transport/auth.js +65 -0
- package/dist/transport/host.d.ts +13 -0
- package/dist/transport/host.js +1 -0
- package/dist/ui/app.d.ts +11 -0
- package/dist/ui/app.js +790 -0
- package/dist/ui/chat/header.d.ts +11 -0
- package/dist/ui/chat/header.js +14 -0
- package/dist/ui/chat/history-view.d.ts +12 -0
- package/dist/ui/chat/history-view.js +17 -0
- package/dist/ui/chat/status.d.ts +91 -0
- package/dist/ui/chat/status.js +386 -0
- package/dist/ui/chat/viewport.d.ts +17 -0
- package/dist/ui/chat/viewport.js +14 -0
- package/dist/ui/commands/parse.d.ts +99 -0
- package/dist/ui/commands/parse.js +126 -0
- package/dist/ui/commands/registry.d.ts +33 -0
- package/dist/ui/commands/registry.js +73 -0
- package/dist/ui/copy-mode.d.ts +4 -0
- package/dist/ui/copy-mode.js +6 -0
- package/dist/{cost-view.d.ts → ui/dialogs/cost.d.ts} +1 -1
- package/dist/{cost-view.js → ui/dialogs/cost.js} +6 -6
- package/dist/ui/dialogs/index.d.ts +120 -0
- package/dist/ui/dialogs/index.js +113 -0
- package/dist/ui/dialogs/picker.d.ts +18 -0
- package/dist/ui/dialogs/picker.js +38 -0
- package/dist/ui/frozen.d.ts +8 -0
- package/dist/ui/frozen.js +7 -0
- package/dist/ui/input/references.d.ts +12 -0
- package/dist/ui/input/references.js +15 -0
- package/dist/ui/mount.d.ts +6 -0
- package/dist/ui/mount.js +11 -0
- package/dist/{theme.d.ts → ui/theme/index.d.ts} +1 -1
- package/package.json +19 -13
- package/dist/app.d.ts +0 -21
- package/dist/app.js +0 -805
- package/dist/auth.js +0 -108
- package/dist/controller.js +0 -961
- package/dist/cost.d.ts +0 -119
- package/dist/cost.js +0 -313
- package/dist/history-view.d.ts +0 -8
- package/dist/history-view.js +0 -12
- package/dist/navigation.d.ts +0 -11
- package/dist/navigation.js +0 -36
- package/dist/status.d.ts +0 -28
- package/dist/status.js +0 -157
- /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
- /package/dist/{memory.d.ts → session/memory.d.ts} +0 -0
- /package/dist/{memory.js → session/memory.js} +0 -0
- /package/dist/{references.d.ts → session/references.d.ts} +0 -0
- /package/dist/{auth.d.ts → transport/auth.d.ts} +0 -0
- /package/dist/{client.d.ts → transport/client.d.ts} +0 -0
- /package/dist/{client.js → transport/client.js} +0 -0
- /package/dist/{endpoint.d.ts → transport/endpoint.d.ts} +0 -0
- /package/dist/{endpoint.js → transport/endpoint.js} +0 -0
- /package/dist/{wire.d.ts → transport/wire.d.ts} +0 -0
- /package/dist/{wire.js → transport/wire.js} +0 -0
- /package/dist/{input-history.d.ts → ui/input/history.d.ts} +0 -0
- /package/dist/{input-history.js → ui/input/history.js} +0 -0
- /package/dist/{input.d.ts → ui/input/input.d.ts} +0 -0
- /package/dist/{input.js → ui/input/input.js} +0 -0
- /package/dist/{mouse.d.ts → ui/input/mouse.d.ts} +0 -0
- /package/dist/{mouse.js → ui/input/mouse.js} +0 -0
- /package/dist/{theme.js → ui/theme/index.js} +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** MathJax compiles TeX locally; terminal output keeps explicit grouping when Unicode cannot express it. */
|
|
2
|
+
import { mathjax } from '@mathjax/src/js/mathjax.js';
|
|
3
|
+
import { TeX } from '@mathjax/src/js/input/tex.js';
|
|
4
|
+
import { liteAdaptor } from '@mathjax/src/js/adaptors/liteAdaptor.js';
|
|
5
|
+
import { RegisterHTMLHandler } from '@mathjax/src/js/handlers/html.js';
|
|
6
|
+
import { STATE } from '@mathjax/src/js/core/MathItem.js';
|
|
7
|
+
import { TextNode } from '@mathjax/src/js/core/MmlTree/MmlNode.js';
|
|
8
|
+
import { SerializedMmlVisitor } from '@mathjax/src/js/core/MmlTree/SerializedMmlVisitor.js';
|
|
9
|
+
import '@mathjax/src/js/input/tex/base/BaseConfiguration.js';
|
|
10
|
+
import '@mathjax/src/js/input/tex/ams/AmsConfiguration.js';
|
|
11
|
+
import { safeText } from "../transport/wire.js";
|
|
12
|
+
RegisterHTMLHandler(liteAdaptor());
|
|
13
|
+
const superscripts = Object.fromEntries([...('0123456789+-=()ni')].map((c, i) => [c, [...'⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾ⁿⁱ'][i]]));
|
|
14
|
+
const subscripts = Object.fromEntries([...'0123456789+-=()aehijklmnoprstuvx'].map((c, i) => [c, [...'₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓ'][i]]));
|
|
15
|
+
function script(text, sub) {
|
|
16
|
+
const alphabet = sub ? subscripts : superscripts;
|
|
17
|
+
return [...text].every(c => alphabet[c]) ? [...text].map(c => alphabet[c]).join('') : `${sub ? '_' : '^'}(${text})`;
|
|
18
|
+
}
|
|
19
|
+
function readable(node) {
|
|
20
|
+
if (node instanceof TextNode)
|
|
21
|
+
return node.getText();
|
|
22
|
+
const children = node.childNodes.map(readable);
|
|
23
|
+
const [a = '', b = '', c = ''] = children;
|
|
24
|
+
let base = node.childNodes[0];
|
|
25
|
+
while (base && ['TeXAtom', 'mrow', 'inferredMrow', 'mstyle'].includes(base.kind) && base.childNodes.length === 1)
|
|
26
|
+
base = base.childNodes[0];
|
|
27
|
+
const grouped = base && !base.isToken && base.childNodes.length > 1 ? `(${a})` : a;
|
|
28
|
+
switch (node.kind) {
|
|
29
|
+
case 'mfrac': return `(${a})/(${b})`;
|
|
30
|
+
case 'msqrt': return `√(${children.join('')})`;
|
|
31
|
+
case 'mroot': return `root(${b}, ${a})`;
|
|
32
|
+
case 'msup': return `${grouped}${script(b, false)}`;
|
|
33
|
+
case 'msub': return `${grouped}${script(b, true)}`;
|
|
34
|
+
case 'msubsup': return `${grouped}${script(b, true)}${script(c, false)}`;
|
|
35
|
+
case 'munder': return `${a}_(${b})`;
|
|
36
|
+
case 'mover': return `${a}^(${b})`;
|
|
37
|
+
case 'munderover': return `${a}_(${b})^(${c})`;
|
|
38
|
+
case 'mtable': return `[${children.join('; ')}]`;
|
|
39
|
+
case 'mtr':
|
|
40
|
+
case 'mlabeledtr': return children.join(', ');
|
|
41
|
+
case 'mspace': return ' ';
|
|
42
|
+
case 'mphantom': return '';
|
|
43
|
+
case 'math':
|
|
44
|
+
case 'mrow':
|
|
45
|
+
case 'inferredMrow':
|
|
46
|
+
case 'TeXAtom':
|
|
47
|
+
case 'mstyle':
|
|
48
|
+
case 'mi':
|
|
49
|
+
case 'mn':
|
|
50
|
+
case 'mo':
|
|
51
|
+
case 'mtext':
|
|
52
|
+
case 'mtd': return children.join('');
|
|
53
|
+
default: throw new Error(`Unsupported terminal math node: ${node.kind}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Convert one TeX expression, without extension autoloads, network access or shared macro state.
|
|
57
|
+
* @param source - TeX without delimiters.
|
|
58
|
+
* @param display - Block rather than inline mathematics.
|
|
59
|
+
* @returns Unicode approximation and MathJax-generated MathML, or undefined for invalid TeX.
|
|
60
|
+
*/
|
|
61
|
+
export function renderMath(source, display) {
|
|
62
|
+
if (source.length > 16 * 1024)
|
|
63
|
+
return undefined;
|
|
64
|
+
try {
|
|
65
|
+
const tex = new TeX({ packages: ['base', 'ams'], maxBuffer: 16 * 1024,
|
|
66
|
+
formatError(_jax, error) { throw error; } });
|
|
67
|
+
const document = mathjax.document('', { InputJax: tex });
|
|
68
|
+
const node = document.convert(source, { display, end: STATE.CONVERT });
|
|
69
|
+
const html = new SerializedMmlVisitor().visitTree(node);
|
|
70
|
+
let text;
|
|
71
|
+
try {
|
|
72
|
+
text = readable(node);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
text = source; /* Valid MathML with no faithful terminal notation retains its TeX. */
|
|
76
|
+
}
|
|
77
|
+
return { text: safeText(text), html };
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return undefined; /* Incomplete or invalid model-authored TeX remains visible as source. */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Shared display names and unambiguous slash-command target resolution. */
|
|
2
|
+
import { type ObjectValue } from '../transport/wire.ts';
|
|
3
|
+
/** Parse workspace and resume navigation, including their long aliases. */
|
|
4
|
+
export declare function navigationCommand(value: string): {
|
|
5
|
+
kind: 'workspace' | 'session';
|
|
6
|
+
query?: string;
|
|
7
|
+
} | undefined;
|
|
8
|
+
/** Resolve the host's title projection, falling back to the session ID. */
|
|
9
|
+
export declare function sessionLabel(session: ObjectValue): string;
|
|
10
|
+
/** Match an exact ID or name before a unique ID prefix; never choose an ambiguous target. */
|
|
11
|
+
export declare function resolveTarget(items: ObjectValue[], query: string, id: string, names: (item: ObjectValue) => string[]): ObjectValue;
|
|
12
|
+
/** Activity a session summary reports on its own, without loading that session's history. */
|
|
13
|
+
export type SessionState = 'running' | 'idle' | 'blank';
|
|
14
|
+
/** Classify one session from the host's list summary; nothing is inferred from silence.
|
|
15
|
+
* @param session - Session summary from `session/list`.
|
|
16
|
+
* @returns Running while its agent works, blank before its first turn, otherwise idle.
|
|
17
|
+
*/
|
|
18
|
+
export declare function sessionState(session: ObjectValue): SessionState;
|
|
19
|
+
/** Leading marker per state: a working clock, a filled idle dot, and an empty unused circle. */
|
|
20
|
+
export declare const SESSION_MARKERS: Record<SessionState, string>;
|
|
21
|
+
/** Coarse age of a session's last activity, so the column stays steady between list refreshes.
|
|
22
|
+
* @param time - Epoch milliseconds of the last activity, when the summary reported one.
|
|
23
|
+
* @param now - Current epoch milliseconds.
|
|
24
|
+
* @returns `now`, minutes, hours or days.
|
|
25
|
+
*/
|
|
26
|
+
export declare function activityAge(time: number | undefined, now: number): string;
|
|
27
|
+
/** Status cell for one session row: its state marker and the age of its last activity.
|
|
28
|
+
* @param session - Session summary from `session/list`.
|
|
29
|
+
* @param now - Current epoch milliseconds.
|
|
30
|
+
* @returns Marker with an optional age, without a trailing space when unknown.
|
|
31
|
+
*/
|
|
32
|
+
export declare function sessionStatus(session: ObjectValue, now: number): string;
|
|
33
|
+
/** Count the sessions of one workspace by the state each reports.
|
|
34
|
+
* @param sessions - Sessions whose `sessionIds` belong to the workspace.
|
|
35
|
+
* @returns One `marker count` cell per state that occurs, running first, or an empty string.
|
|
36
|
+
*/
|
|
37
|
+
export declare function workspaceStatus(sessions: readonly ObjectValue[]): string;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Shared display names and unambiguous slash-command target resolution. */
|
|
2
|
+
import { safeText, string } from "../transport/wire.js";
|
|
3
|
+
/** Parse workspace and resume navigation, including their long aliases. */
|
|
4
|
+
export function navigationCommand(value) {
|
|
5
|
+
const match = /^\/(ws|workspace|workspaces|resume|session|sessions)(?:\s+(.+))?$/.exec(value);
|
|
6
|
+
if (!match)
|
|
7
|
+
return undefined;
|
|
8
|
+
return { kind: match[1] === 'ws' || match[1].startsWith('workspace') ? 'workspace' : 'session', query: match[2] };
|
|
9
|
+
}
|
|
10
|
+
/** Resolve the host's title projection, falling back to the session ID. */
|
|
11
|
+
export function sessionLabel(session) {
|
|
12
|
+
const projections = session.projections;
|
|
13
|
+
if (projections && typeof projections === 'object' && !Array.isArray(projections)) {
|
|
14
|
+
const values = projections.values;
|
|
15
|
+
const title = values && typeof values === 'object' && !Array.isArray(values) ? values.title : undefined;
|
|
16
|
+
if (typeof title === 'string' && safeText(title).trim())
|
|
17
|
+
return safeText(title).trim();
|
|
18
|
+
if (title && typeof title === 'object' && !Array.isArray(title) && typeof title.title === 'string' && safeText(title.title).trim())
|
|
19
|
+
return safeText(title.title).trim();
|
|
20
|
+
}
|
|
21
|
+
return string(session.sessionId);
|
|
22
|
+
}
|
|
23
|
+
/** Match an exact ID or name before a unique ID prefix; never choose an ambiguous target. */
|
|
24
|
+
export function resolveTarget(items, query, id, names) {
|
|
25
|
+
const target = query.replace(/^(["'])(.*)\1$/, '$2');
|
|
26
|
+
const exactId = items.find(item => item[id] === target);
|
|
27
|
+
if (exactId)
|
|
28
|
+
return exactId;
|
|
29
|
+
const exactNames = items.filter(item => names(item).includes(target));
|
|
30
|
+
const matches = exactNames.length ? exactNames : items.filter(item => string(item[id]).startsWith(target));
|
|
31
|
+
if (matches.length === 1)
|
|
32
|
+
return matches[0];
|
|
33
|
+
if (matches.length > 1)
|
|
34
|
+
throw new Error(`Ambiguous target: ${target}. Use a full ID.`);
|
|
35
|
+
throw new Error(`Target not found: ${target}`);
|
|
36
|
+
}
|
|
37
|
+
/** Classify one session from the host's list summary; nothing is inferred from silence.
|
|
38
|
+
* @param session - Session summary from `session/list`.
|
|
39
|
+
* @returns Running while its agent works, blank before its first turn, otherwise idle.
|
|
40
|
+
*/
|
|
41
|
+
export function sessionState(session) {
|
|
42
|
+
if (session.running === true)
|
|
43
|
+
return 'running';
|
|
44
|
+
return session.blank === true ? 'blank' : 'idle';
|
|
45
|
+
}
|
|
46
|
+
/** Leading marker per state: a working clock, a filled idle dot, and an empty unused circle. */
|
|
47
|
+
export const SESSION_MARKERS = { running: '◐', idle: '●', blank: '○' };
|
|
48
|
+
/** Coarse age of a session's last activity, so the column stays steady between list refreshes.
|
|
49
|
+
* @param time - Epoch milliseconds of the last activity, when the summary reported one.
|
|
50
|
+
* @param now - Current epoch milliseconds.
|
|
51
|
+
* @returns `now`, minutes, hours or days.
|
|
52
|
+
*/
|
|
53
|
+
export function activityAge(time, now) {
|
|
54
|
+
if (time === undefined || !Number.isFinite(time))
|
|
55
|
+
return '';
|
|
56
|
+
const seconds = Math.max(0, Math.floor((now - time) / 1000));
|
|
57
|
+
if (seconds < 60)
|
|
58
|
+
return 'now';
|
|
59
|
+
const minutes = Math.floor(seconds / 60);
|
|
60
|
+
if (minutes < 60)
|
|
61
|
+
return `${minutes}m`;
|
|
62
|
+
const hours = Math.floor(minutes / 60);
|
|
63
|
+
return hours < 24 ? `${hours}h` : `${Math.floor(hours / 24)}d`;
|
|
64
|
+
}
|
|
65
|
+
/** Status cell for one session row: its state marker and the age of its last activity.
|
|
66
|
+
* @param session - Session summary from `session/list`.
|
|
67
|
+
* @param now - Current epoch milliseconds.
|
|
68
|
+
* @returns Marker with an optional age, without a trailing space when unknown.
|
|
69
|
+
*/
|
|
70
|
+
export function sessionStatus(session, now) {
|
|
71
|
+
const age = activityAge(typeof session.updatedAt === 'number' ? session.updatedAt : undefined, now);
|
|
72
|
+
return `${SESSION_MARKERS[sessionState(session)]}${age === '' ? '' : ` ${age}`}`;
|
|
73
|
+
}
|
|
74
|
+
/** Count the sessions of one workspace by the state each reports.
|
|
75
|
+
* @param sessions - Sessions whose `sessionIds` belong to the workspace.
|
|
76
|
+
* @returns One `marker count` cell per state that occurs, running first, or an empty string.
|
|
77
|
+
*/
|
|
78
|
+
export function workspaceStatus(sessions) {
|
|
79
|
+
const counts = { running: 0, idle: 0, blank: 0 };
|
|
80
|
+
for (const session of sessions)
|
|
81
|
+
counts[sessionState(session)] += 1;
|
|
82
|
+
return ['running', 'idle', 'blank'].filter(state => counts[state] > 0)
|
|
83
|
+
.map(state => `${SESSION_MARKERS[state]} ${counts[state]}`).join(' ');
|
|
84
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Independent adapter for Harness path-only @ references; no file bytes are read. */
|
|
2
|
-
import { array, object, string } from "
|
|
2
|
+
import { array, object, string } from "../transport/wire.js";
|
|
3
3
|
/** Find the unfinished reference at the end of the draft; email addresses do not trigger it.
|
|
4
4
|
* @param text - Complete composer draft.
|
|
5
5
|
* @returns The replaceable token and host query, or undefined outside a reference.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Host projection values with per-key watermarks; snapshots never roll back newer updates. */
|
|
2
|
-
import { type ObjectValue } from '
|
|
2
|
+
import { type ObjectValue } from '../transport/wire.ts';
|
|
3
3
|
/** One host-owned pending input, removable only while its occurrence remains queued. */
|
|
4
4
|
export interface QueuedInput {
|
|
5
5
|
id: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Host projection values with per-key watermarks; snapshots never roll back newer updates. */
|
|
2
|
-
import { array, object, string } from "
|
|
2
|
+
import { array, object, string } from "../transport/wire.js";
|
|
3
3
|
function queuedInputs(value) {
|
|
4
4
|
return array(value).map(value => {
|
|
5
5
|
const item = object(value);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HistoryLimits } from './memory.ts';
|
|
2
|
-
import { type Json } from '
|
|
2
|
+
import { type Json } from '../transport/wire.ts';
|
|
3
3
|
interface ToolSummary {
|
|
4
4
|
name: string;
|
|
5
5
|
operation?: string;
|
|
@@ -25,6 +25,17 @@ export interface MessagePart {
|
|
|
25
25
|
text: string;
|
|
26
26
|
/** Closed live reasoning can fold before the assistant message is committed. */
|
|
27
27
|
closed?: boolean;
|
|
28
|
+
/** Stable identity of a live part, so its rows can be wrapped incrementally as it grows. */
|
|
29
|
+
key?: string;
|
|
30
|
+
}
|
|
31
|
+
/** The stripe of work the live attempt last moved into; the status bar times it until a newer one. */
|
|
32
|
+
export interface LivePhase {
|
|
33
|
+
/** Which kind of work the newest event named. */
|
|
34
|
+
kind: 'thinking' | 'tool' | 'output';
|
|
35
|
+
/** Tool name when `kind` is `tool`, so the bar can name what is running. */
|
|
36
|
+
name?: string;
|
|
37
|
+
/** Epoch milliseconds when this phase began. */
|
|
38
|
+
startedAt: number;
|
|
28
39
|
}
|
|
29
40
|
/** A displayed message retains the durable sequence for stable reconciliation. */
|
|
30
41
|
export interface Message {
|
|
@@ -60,7 +71,14 @@ export declare class Transcript {
|
|
|
60
71
|
private closedBlocks;
|
|
61
72
|
private oldestSeq;
|
|
62
73
|
private turnMarker;
|
|
74
|
+
/** Pending-tool lookup memo, keyed by the revision that produced it. */
|
|
75
|
+
private runningCache;
|
|
63
76
|
private attempt;
|
|
77
|
+
private phase;
|
|
78
|
+
/** Whether the newest delta still extends `phase`; attempt boundaries close it without dropping it. */
|
|
79
|
+
private phaseOpen;
|
|
80
|
+
/** Set when an added record can change which call the open turn is still waiting on. */
|
|
81
|
+
private pendingDirty;
|
|
64
82
|
private nextIndex;
|
|
65
83
|
private revision;
|
|
66
84
|
private legacyStream;
|
|
@@ -97,6 +115,29 @@ export declare class Transcript {
|
|
|
97
115
|
private dropProjections;
|
|
98
116
|
private storeEvent;
|
|
99
117
|
private deleteEvent;
|
|
118
|
+
/** The stripe of work the live attempt is in: the newest event the bar can time.
|
|
119
|
+
*
|
|
120
|
+
* Its age answers "what is it doing, and for how long" without inferring anything from silence,
|
|
121
|
+
* so the answer outlives the stream that supplied it: a tool keeps its age while it runs and after
|
|
122
|
+
* it answers, until the next stripe starts or the turn closes. Only the turn's own boundaries
|
|
123
|
+
* withdraw the phase, because everything inside a turn is still work the bar should be timing.
|
|
124
|
+
* @returns Phase kind, the tool name when the phase is a tool call, and when the phase began.
|
|
125
|
+
*/
|
|
126
|
+
get livePhase(): LivePhase | undefined;
|
|
127
|
+
/** The tool the open turn asked for that has not answered yet, oldest first.
|
|
128
|
+
* @returns Call id, tool name and the moment the request was recorded, or undefined when none is in flight.
|
|
129
|
+
*/
|
|
130
|
+
get runningTool(): {
|
|
131
|
+
id: string;
|
|
132
|
+
name: string;
|
|
133
|
+
startedAt: number;
|
|
134
|
+
} | undefined;
|
|
135
|
+
/** Find the open turn's unanswered tool call, which is the work a bar can still be waiting on.
|
|
136
|
+
*
|
|
137
|
+
* The host's `tool/call` event is not retained — only user-visible content is — so this reads the
|
|
138
|
+
* tool-call blocks a retained assistant message carries and the results that answer them.
|
|
139
|
+
*/
|
|
140
|
+
private findPendingTool;
|
|
100
141
|
/** Start of the open durable turn, when its timestamp is present in the retained window. */
|
|
101
142
|
get activeTurnStartedAt(): number | undefined;
|
|
102
143
|
/** Cached prompt/reasoning summaries, oldest first; stream deltas never invalidate this index. */
|
|
@@ -144,6 +185,29 @@ export declare class Transcript {
|
|
|
144
185
|
*/
|
|
145
186
|
private foldLegacy;
|
|
146
187
|
private validatePackedTiming;
|
|
188
|
+
/** Drop the live attempt's blocks; the phase they named stays until something newer replaces it. */
|
|
189
|
+
private clearBlocks;
|
|
190
|
+
/** Withdraw the phase at a turn boundary, where no work is running under it any more. */
|
|
191
|
+
private clearPhase;
|
|
192
|
+
/** Move the phase to the call the open turn is still waiting on, which is work in progress.
|
|
193
|
+
*
|
|
194
|
+
* A tool runs after the assistant stream that asked for it has ended, so the live block no longer
|
|
195
|
+
* covers it; the call the host committed but did not answer is what keeps a long command visible
|
|
196
|
+
* instead of showing an idle bar. While deltas are still arriving they are the newer evidence, and
|
|
197
|
+
* a turn with nothing unanswered keeps the phase it has rather than dropping back to silence.
|
|
198
|
+
*/
|
|
199
|
+
private settlePhase;
|
|
200
|
+
/** Record the stripe a delta moved the attempt into; the status bar shows its age.
|
|
201
|
+
*
|
|
202
|
+
* A delta that continues the open stripe keeps the original start, because the phase is one
|
|
203
|
+
* stretch of work rather than one chunk. A stripe that returns after the attempt closed it — a
|
|
204
|
+
* second `think` in a later step — starts over, and the call id is what distinguishes a repeated
|
|
205
|
+
* tool from the call that already ran.
|
|
206
|
+
* @param kind - Stripe the delta belongs to.
|
|
207
|
+
* @param name - Tool name, when the stripe is a tool call.
|
|
208
|
+
* @param id - Tool call id, when the stripe is a tool call.
|
|
209
|
+
*/
|
|
210
|
+
private notePhase;
|
|
147
211
|
private chunk;
|
|
148
212
|
}
|
|
149
213
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Human transcript projection from durable events and ephemeral assistant chunks. */
|
|
2
2
|
import sliceAnsi from 'slice-ansi';
|
|
3
|
-
import { array, object, safeText, string } from "
|
|
3
|
+
import { array, object, safeText, string } from "../transport/wire.js";
|
|
4
4
|
function toolSummary(block) {
|
|
5
5
|
let args = {};
|
|
6
6
|
if (typeof block.arguments === 'string' && block.arguments.trimStart().startsWith('{')) {
|
|
@@ -76,7 +76,14 @@ export class Transcript {
|
|
|
76
76
|
closedBlocks = new Set();
|
|
77
77
|
oldestSeq;
|
|
78
78
|
turnMarker;
|
|
79
|
+
/** Pending-tool lookup memo, keyed by the revision that produced it. */
|
|
80
|
+
runningCache;
|
|
79
81
|
attempt;
|
|
82
|
+
phase;
|
|
83
|
+
/** Whether the newest delta still extends `phase`; attempt boundaries close it without dropping it. */
|
|
84
|
+
phaseOpen = false;
|
|
85
|
+
/** Set when an added record can change which call the open turn is still waiting on. */
|
|
86
|
+
pendingDirty = false;
|
|
80
87
|
nextIndex = 0;
|
|
81
88
|
revision = 0;
|
|
82
89
|
legacyStream = false;
|
|
@@ -114,7 +121,8 @@ export class Transcript {
|
|
|
114
121
|
this.closedBlocks.clear();
|
|
115
122
|
this.oldestSeq = undefined;
|
|
116
123
|
this.turnMarker = undefined;
|
|
117
|
-
this.
|
|
124
|
+
this.clearBlocks();
|
|
125
|
+
this.clearPhase();
|
|
118
126
|
this.keysDirty = true;
|
|
119
127
|
this.projectionRevision++;
|
|
120
128
|
this.legacyDirty = true;
|
|
@@ -157,8 +165,7 @@ export class Transcript {
|
|
|
157
165
|
throw new Error('Assistant stream revision gap');
|
|
158
166
|
this.revision = number(live.revision);
|
|
159
167
|
if (live.type === 'start') {
|
|
160
|
-
this.
|
|
161
|
-
this.closedBlocks.clear();
|
|
168
|
+
this.clearBlocks();
|
|
162
169
|
this.attempt = string(live.attemptId);
|
|
163
170
|
this.nextIndex = 0;
|
|
164
171
|
}
|
|
@@ -172,7 +179,8 @@ export class Transcript {
|
|
|
172
179
|
}
|
|
173
180
|
else if (live.type === 'end') {
|
|
174
181
|
this.attempt = undefined;
|
|
175
|
-
this.
|
|
182
|
+
this.clearBlocks();
|
|
183
|
+
this.settlePhase();
|
|
176
184
|
}
|
|
177
185
|
else
|
|
178
186
|
throw new Error('Unknown assistant stream frame');
|
|
@@ -181,6 +189,10 @@ export class Transcript {
|
|
|
181
189
|
}
|
|
182
190
|
default: throw new Error('Unknown session follow frame');
|
|
183
191
|
}
|
|
192
|
+
if (this.pendingDirty) {
|
|
193
|
+
this.pendingDirty = false;
|
|
194
|
+
this.settlePhase();
|
|
195
|
+
}
|
|
184
196
|
}
|
|
185
197
|
/** Add an older page without replacing the live tail. */
|
|
186
198
|
addPage(value) {
|
|
@@ -190,6 +202,10 @@ export class Transcript {
|
|
|
190
202
|
const page = object(value);
|
|
191
203
|
this.addRecords(array(page.records));
|
|
192
204
|
this.hasMore = page.hasMore === true;
|
|
205
|
+
if (this.pendingDirty) {
|
|
206
|
+
this.pendingDirty = false;
|
|
207
|
+
this.settlePhase();
|
|
208
|
+
}
|
|
193
209
|
}
|
|
194
210
|
/** Durable read cutoff includes followed records that were not present in the opening snapshot. */
|
|
195
211
|
get readThrough() { return Math.max(this.cursor, this.throughSeq); }
|
|
@@ -257,8 +273,8 @@ export class Transcript {
|
|
|
257
273
|
this.events.clear();
|
|
258
274
|
this.sizes.clear();
|
|
259
275
|
this.storedBytes = 0;
|
|
260
|
-
this.
|
|
261
|
-
this.
|
|
276
|
+
this.clearBlocks();
|
|
277
|
+
this.clearPhase();
|
|
262
278
|
this.transientSeqs.clear();
|
|
263
279
|
this.sortedSeqs = [];
|
|
264
280
|
this.displayed = [];
|
|
@@ -296,6 +312,58 @@ export class Transcript {
|
|
|
296
312
|
this.sizes.delete(seq);
|
|
297
313
|
this.events.delete(seq);
|
|
298
314
|
}
|
|
315
|
+
/** The stripe of work the live attempt is in: the newest event the bar can time.
|
|
316
|
+
*
|
|
317
|
+
* Its age answers "what is it doing, and for how long" without inferring anything from silence,
|
|
318
|
+
* so the answer outlives the stream that supplied it: a tool keeps its age while it runs and after
|
|
319
|
+
* it answers, until the next stripe starts or the turn closes. Only the turn's own boundaries
|
|
320
|
+
* withdraw the phase, because everything inside a turn is still work the bar should be timing.
|
|
321
|
+
* @returns Phase kind, the tool name when the phase is a tool call, and when the phase began.
|
|
322
|
+
*/
|
|
323
|
+
get livePhase() {
|
|
324
|
+
const phase = this.phase;
|
|
325
|
+
return phase === undefined ? undefined
|
|
326
|
+
: { kind: phase.kind, ...(phase.name === undefined ? {} : { name: phase.name }), startedAt: phase.startedAt };
|
|
327
|
+
}
|
|
328
|
+
/** The tool the open turn asked for that has not answered yet, oldest first.
|
|
329
|
+
* @returns Call id, tool name and the moment the request was recorded, or undefined when none is in flight.
|
|
330
|
+
*/
|
|
331
|
+
get runningTool() {
|
|
332
|
+
if (this.runningCache?.version === this.version)
|
|
333
|
+
return this.runningCache.value;
|
|
334
|
+
const value = this.findPendingTool();
|
|
335
|
+
this.runningCache = { version: this.version, value };
|
|
336
|
+
return value;
|
|
337
|
+
}
|
|
338
|
+
/** Find the open turn's unanswered tool call, which is the work a bar can still be waiting on.
|
|
339
|
+
*
|
|
340
|
+
* The host's `tool/call` event is not retained — only user-visible content is — so this reads the
|
|
341
|
+
* tool-call blocks a retained assistant message carries and the results that answer them.
|
|
342
|
+
*/
|
|
343
|
+
findPendingTool() {
|
|
344
|
+
const marker = this.turnMarker;
|
|
345
|
+
// A turn that ended cannot have a tool in flight, and `turn/end` clears the marker's start.
|
|
346
|
+
if (marker?.start === undefined)
|
|
347
|
+
return undefined;
|
|
348
|
+
const calls = [];
|
|
349
|
+
const answered = new Set();
|
|
350
|
+
for (const seq of this.sortedKeys()) {
|
|
351
|
+
if (seq <= marker.seq)
|
|
352
|
+
continue;
|
|
353
|
+
const event = this.events.get(seq);
|
|
354
|
+
if (event.type !== 'assistant/message' && event.type !== 'tool/result')
|
|
355
|
+
continue;
|
|
356
|
+
const startedAt = typeof event.time === 'number' ? event.time : marker.start;
|
|
357
|
+
for (const block of array(object(object(event.data).message).content).map(object)) {
|
|
358
|
+
if (block.type === 'tool-call')
|
|
359
|
+
calls.push({ id: string(block.id), name: string(block.name ?? 'tool'), startedAt });
|
|
360
|
+
else if (block.type === 'tool-result')
|
|
361
|
+
answered.add(string(block.toolCallId));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const pending = calls.find(call => !answered.has(call.id));
|
|
365
|
+
return pending === undefined ? undefined : { id: pending.id, name: pending.name, startedAt: pending.startedAt };
|
|
366
|
+
}
|
|
299
367
|
/** Start of the open durable turn, when its timestamp is present in the retained window. */
|
|
300
368
|
get activeTurnStartedAt() {
|
|
301
369
|
return this.turnMarker?.start;
|
|
@@ -449,6 +517,7 @@ export class Transcript {
|
|
|
449
517
|
* @returns Ordered blocks without terminal style escapes.
|
|
450
518
|
*/
|
|
451
519
|
liveParts(width) {
|
|
520
|
+
const attempt = this.liveAttemptKey ?? 'live';
|
|
452
521
|
return [...this.blocks].sort(([a], [b]) => a - b).map(([index, block]) => {
|
|
453
522
|
const closed = this.closedBlocks.has(index);
|
|
454
523
|
const cached = this.liveProjection.get(block);
|
|
@@ -457,6 +526,7 @@ export class Transcript {
|
|
|
457
526
|
const part = {
|
|
458
527
|
kind: block.type === 'reasoning' ? 'reasoning' : block.type === 'tool-call' ? 'tool' : 'text',
|
|
459
528
|
text: cached?.width === width ? cached.part.text : contentText([block], undefined, width), closed,
|
|
529
|
+
key: `${attempt}:${index}`,
|
|
460
530
|
};
|
|
461
531
|
this.liveProjection.set(block, { width, part });
|
|
462
532
|
return part;
|
|
@@ -478,9 +548,16 @@ export class Transcript {
|
|
|
478
548
|
this.throughSeq = Math.max(this.throughSeq, seq);
|
|
479
549
|
if ((event.type === 'turn/start' || event.type === 'turn/end') && seq >= (this.turnMarker?.seq ?? -1)) {
|
|
480
550
|
this.turnMarker = { seq, start: event.type === 'turn/start' && typeof event.time === 'number' && Number.isFinite(event.time) ? event.time : undefined };
|
|
551
|
+
// A turn is the span a phase belongs to: the closed one has nothing left running and the
|
|
552
|
+
// opening one has not moved yet, so neither may show the other's last event.
|
|
553
|
+
this.clearPhase();
|
|
481
554
|
}
|
|
482
555
|
const current = this.events.get(seq);
|
|
483
556
|
this.storeEvent(seq, retainedEvent(event));
|
|
557
|
+
// Only these records can answer "which call is the open turn still waiting on".
|
|
558
|
+
if (event.type === 'assistant/message' || event.type === 'tool/result'
|
|
559
|
+
|| event.type === 'turn/start' || event.type === 'turn/end')
|
|
560
|
+
this.pendingDirty = true;
|
|
484
561
|
if (!DISPLAY_EVENTS.has(string(event.type)))
|
|
485
562
|
this.transientSeqs.add(seq);
|
|
486
563
|
else
|
|
@@ -520,8 +597,7 @@ export class Transcript {
|
|
|
520
597
|
foldLegacy(added) {
|
|
521
598
|
let events;
|
|
522
599
|
if (this.legacyDirty) {
|
|
523
|
-
this.
|
|
524
|
-
this.closedBlocks.clear();
|
|
600
|
+
this.clearBlocks();
|
|
525
601
|
this.legacyPosition = '';
|
|
526
602
|
this.legacySeq = -1;
|
|
527
603
|
this.legacyDirty = false;
|
|
@@ -533,17 +609,14 @@ export class Transcript {
|
|
|
533
609
|
for (const event of events) {
|
|
534
610
|
this.legacySeq = Math.max(this.legacySeq, number(event.seq));
|
|
535
611
|
if (['step/start', 'step/end', 'turn/end', 'assistant/message', 'assistant/attempt'].includes(string(event.type))) {
|
|
536
|
-
this.
|
|
537
|
-
this.closedBlocks.clear();
|
|
612
|
+
this.clearBlocks();
|
|
538
613
|
this.legacyPosition = '';
|
|
539
614
|
}
|
|
540
615
|
else if (event.type === 'assistant/chunk' || string(event.type).startsWith('chunkrow/')) {
|
|
541
616
|
const data = object(event.data);
|
|
542
617
|
const nextPosition = `${number(data.turn)}:${number(data.step)}`;
|
|
543
|
-
if (this.legacyPosition !== nextPosition)
|
|
544
|
-
this.
|
|
545
|
-
this.closedBlocks.clear();
|
|
546
|
-
}
|
|
618
|
+
if (this.legacyPosition !== nextPosition)
|
|
619
|
+
this.clearBlocks();
|
|
547
620
|
this.legacyPosition = nextPosition;
|
|
548
621
|
switch (event.type) {
|
|
549
622
|
case 'assistant/chunk':
|
|
@@ -572,6 +645,48 @@ export class Transcript {
|
|
|
572
645
|
if (count === 0 || array(data.dt).length !== count - 1)
|
|
573
646
|
throw new Error('Invalid packed history member count');
|
|
574
647
|
}
|
|
648
|
+
/** Drop the live attempt's blocks; the phase they named stays until something newer replaces it. */
|
|
649
|
+
clearBlocks() { this.blocks.clear(); this.closedBlocks.clear(); this.phaseOpen = false; }
|
|
650
|
+
/** Withdraw the phase at a turn boundary, where no work is running under it any more. */
|
|
651
|
+
clearPhase() { this.phase = undefined; this.phaseOpen = false; }
|
|
652
|
+
/** Move the phase to the call the open turn is still waiting on, which is work in progress.
|
|
653
|
+
*
|
|
654
|
+
* A tool runs after the assistant stream that asked for it has ended, so the live block no longer
|
|
655
|
+
* covers it; the call the host committed but did not answer is what keeps a long command visible
|
|
656
|
+
* instead of showing an idle bar. While deltas are still arriving they are the newer evidence, and
|
|
657
|
+
* a turn with nothing unanswered keeps the phase it has rather than dropping back to silence.
|
|
658
|
+
*/
|
|
659
|
+
settlePhase() {
|
|
660
|
+
if (this.phaseOpen)
|
|
661
|
+
return;
|
|
662
|
+
const tool = this.runningTool;
|
|
663
|
+
if (tool === undefined)
|
|
664
|
+
return;
|
|
665
|
+
const key = toolPhaseKey(tool.id, tool.name);
|
|
666
|
+
if (this.phase?.key === key)
|
|
667
|
+
return;
|
|
668
|
+
this.phase = { key, kind: 'tool', name: tool.name, startedAt: tool.startedAt };
|
|
669
|
+
}
|
|
670
|
+
/** Record the stripe a delta moved the attempt into; the status bar shows its age.
|
|
671
|
+
*
|
|
672
|
+
* A delta that continues the open stripe keeps the original start, because the phase is one
|
|
673
|
+
* stretch of work rather than one chunk. A stripe that returns after the attempt closed it — a
|
|
674
|
+
* second `think` in a later step — starts over, and the call id is what distinguishes a repeated
|
|
675
|
+
* tool from the call that already ran.
|
|
676
|
+
* @param kind - Stripe the delta belongs to.
|
|
677
|
+
* @param name - Tool name, when the stripe is a tool call.
|
|
678
|
+
* @param id - Tool call id, when the stripe is a tool call.
|
|
679
|
+
*/
|
|
680
|
+
notePhase(kind, name, id) {
|
|
681
|
+
const key = kind === 'tool' ? toolPhaseKey(id ?? '', name) : kind;
|
|
682
|
+
if (this.phase?.key === key && this.phaseOpen) {
|
|
683
|
+
if (name !== undefined && this.phase.name !== name)
|
|
684
|
+
this.phase = { key, kind, name, startedAt: this.phase.startedAt };
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
this.phase = { key, kind, ...(name === undefined ? {} : { name }), startedAt: Date.now() };
|
|
688
|
+
this.phaseOpen = true;
|
|
689
|
+
}
|
|
575
690
|
chunk(chunk) {
|
|
576
691
|
if (chunk.type === 'text-delta' || chunk.type === 'tool-call-delta') {
|
|
577
692
|
for (const [index, block] of this.blocks)
|
|
@@ -583,13 +698,15 @@ export class Transcript {
|
|
|
583
698
|
const block = this.blocks.get(index);
|
|
584
699
|
this.blocks.set(index, { type: chunk.type === 'text-delta' ? 'text' : 'reasoning',
|
|
585
700
|
text: (block ? string(block.text) : '') + string(chunk.text) });
|
|
701
|
+
this.notePhase(chunk.type === 'text-delta' ? 'output' : 'thinking');
|
|
586
702
|
}
|
|
587
703
|
else if (chunk.type === 'tool-call-delta') {
|
|
588
704
|
const index = number(chunk.index);
|
|
589
705
|
const block = this.blocks.get(index);
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
706
|
+
const name = typeof chunk.name === 'string' ? chunk.name : typeof block?.name === 'string' ? block.name : '';
|
|
707
|
+
const id = typeof chunk.id === 'string' ? chunk.id : typeof block?.id === 'string' ? block.id : '';
|
|
708
|
+
this.blocks.set(index, { type: 'tool-call', id, name, arguments: '' });
|
|
709
|
+
this.notePhase('tool', name === '' ? undefined : name, id);
|
|
593
710
|
}
|
|
594
711
|
else if (chunk.type === 'block-end') {
|
|
595
712
|
this.blocks.set(number(chunk.index), object(chunk.block));
|
|
@@ -602,12 +719,16 @@ function number(value) {
|
|
|
602
719
|
throw new Error('Invalid sequence number');
|
|
603
720
|
return value;
|
|
604
721
|
}
|
|
722
|
+
/** Identity of one tool call, so two calls of the same tool do not share one phase. */
|
|
723
|
+
function toolPhaseKey(id, name) {
|
|
724
|
+
return `tool:${id === '' ? name ?? '' : id}`;
|
|
725
|
+
}
|
|
605
726
|
/** Keep only user-visible content in durable client memory; the host owns raw tool results. */
|
|
606
727
|
function retainedEvent(event) {
|
|
607
728
|
if (!DISPLAY_EVENTS.has(string(event.type)))
|
|
608
729
|
return event;
|
|
609
730
|
if (event.surfaceOp !== 'append')
|
|
610
|
-
return { seq: event.seq, type: event.type };
|
|
731
|
+
return { seq: event.seq, type: event.type, ...(typeof event.time === 'number' ? { time: event.time } : {}) };
|
|
611
732
|
const data = object(event.data);
|
|
612
733
|
const clean = (value) => {
|
|
613
734
|
const block = object(value);
|
|
@@ -617,7 +738,7 @@ function retainedEvent(event) {
|
|
|
617
738
|
return { type: 'tool-result', toolCallId: block.toolCallId, isError: block.isError === true };
|
|
618
739
|
return block;
|
|
619
740
|
};
|
|
620
|
-
return { seq: event.seq, type: event.type, surfaceOp: 'append', data: event.type === 'user/message'
|
|
741
|
+
return { seq: event.seq, type: event.type, ...(typeof event.time === 'number' ? { time: event.time } : {}), surfaceOp: 'append', data: event.type === 'user/message'
|
|
621
742
|
? { content: data.content, ...(data.source ? { source: data.source } : {}) }
|
|
622
743
|
: { message: { content: array(object(data.message).content).filter(block => event.type !== 'tool/result' || object(block).type === 'tool-result').map(clean) } } };
|
|
623
744
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Session-domain result types shared by the controller facade and the terminal UI. */
|
|
2
|
+
/** Resolved navigation-removal identity; empty marks a fresh blank, idle session eligible for immediate archival. */
|
|
3
|
+
export interface RemovalTarget {
|
|
4
|
+
kind: 'workspace' | 'session';
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
empty?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Bounded search results contain navigation summaries, never complete message bodies. */
|
|
11
|
+
export interface HistorySearch {
|
|
12
|
+
items: {
|
|
13
|
+
seq: number;
|
|
14
|
+
role: string;
|
|
15
|
+
preview: string;
|
|
16
|
+
}[];
|
|
17
|
+
truncated: boolean;
|
|
18
|
+
}
|