@itookit/dsht 0.3.0 → 0.3.2
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 +30 -15
- package/README.zh.md +30 -15
- 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} +40 -24
- package/dist/controller/connection.d.ts +80 -0
- package/dist/controller/connection.js +190 -0
- package/dist/controller/controller.d.ts +260 -0
- package/dist/controller/controller.js +362 -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/controller.d.ts +34 -0
- package/dist/cost/controller.js +107 -0
- package/dist/cost/index.d.ts +9 -0
- package/dist/cost/index.js +7 -0
- package/dist/cost/ledger-files.d.ts +20 -0
- package/dist/cost/ledger-files.js +128 -0
- package/dist/cost/ledger.d.ts +65 -0
- package/dist/cost/ledger.js +136 -0
- package/dist/cost/pricing.d.ts +48 -0
- package/dist/cost/pricing.js +141 -0
- package/dist/cost/records.d.ts +17 -0
- package/dist/cost/records.js +66 -0
- package/dist/cost/scanner.d.ts +22 -0
- package/dist/cost/scanner.js +95 -0
- package/dist/cost/types.d.ts +69 -0
- package/dist/cost/types.js +3 -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/{navigation.d.ts → session/navigation.d.ts} +1 -1
- package/dist/{navigation.js → session/navigation.js} +1 -1
- 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} +23 -1
- package/dist/{transcript.js → session/transcript.js} +36 -18
- 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 +43 -0
- package/dist/storage/files.js +135 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.js +3 -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 +726 -0
- package/dist/ui/chat/header.d.ts +11 -0
- package/dist/ui/chat/header.js +14 -0
- package/dist/{history-view.d.ts → ui/chat/history-view.d.ts} +1 -1
- package/dist/{history-view.js → ui/chat/history-view.js} +3 -3
- package/dist/ui/chat/status.d.ts +79 -0
- package/dist/ui/chat/status.js +311 -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 +96 -0
- package/dist/ui/commands/parse.js +121 -0
- package/dist/ui/commands/registry.d.ts +33 -0
- package/dist/ui/commands/registry.js +72 -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} +3 -3
- 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/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
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Shared display names and unambiguous slash-command target resolution. */
|
|
2
|
-
import { type ObjectValue } from '
|
|
2
|
+
import { type ObjectValue } from '../transport/wire.ts';
|
|
3
3
|
/** Parse workspace and resume navigation, including their long aliases. */
|
|
4
4
|
export declare function navigationCommand(value: string): {
|
|
5
5
|
kind: 'workspace' | 'session';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Shared display names and unambiguous slash-command target resolution. */
|
|
2
|
-
import { safeText, string } from "
|
|
2
|
+
import { safeText, string } from "../transport/wire.js";
|
|
3
3
|
/** Parse workspace and resume navigation, including their long aliases. */
|
|
4
4
|
export function navigationCommand(value) {
|
|
5
5
|
const match = /^\/(ws|workspace|workspaces|resume|session|sessions)(?:\s+(.+))?$/.exec(value);
|
|
@@ -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 live work the unfinished attempt is in. */
|
|
32
|
+
export interface LivePhase {
|
|
33
|
+
/** Which kind of delta last moved the attempt forward. */
|
|
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 {
|
|
@@ -61,6 +72,7 @@ export declare class Transcript {
|
|
|
61
72
|
private oldestSeq;
|
|
62
73
|
private turnMarker;
|
|
63
74
|
private attempt;
|
|
75
|
+
private phase;
|
|
64
76
|
private nextIndex;
|
|
65
77
|
private revision;
|
|
66
78
|
private legacyStream;
|
|
@@ -97,6 +109,12 @@ export declare class Transcript {
|
|
|
97
109
|
private dropProjections;
|
|
98
110
|
private storeEvent;
|
|
99
111
|
private deleteEvent;
|
|
112
|
+
/** The stripe of work the live attempt is in: the phase of the block that last took a delta.
|
|
113
|
+
*
|
|
114
|
+
* Its age answers "what is it doing, and for how long" without inferring anything from silence.
|
|
115
|
+
* @returns Phase kind, the tool name when the phase is a tool call, and when the phase began.
|
|
116
|
+
*/
|
|
117
|
+
get livePhase(): LivePhase | undefined;
|
|
100
118
|
/** Start of the open durable turn, when its timestamp is present in the retained window. */
|
|
101
119
|
get activeTurnStartedAt(): number | undefined;
|
|
102
120
|
/** Cached prompt/reasoning summaries, oldest first; stream deltas never invalidate this index. */
|
|
@@ -144,6 +162,10 @@ export declare class Transcript {
|
|
|
144
162
|
*/
|
|
145
163
|
private foldLegacy;
|
|
146
164
|
private validatePackedTiming;
|
|
165
|
+
/** Drop the live attempt's blocks and the phase derived from them. */
|
|
166
|
+
private clearBlocks;
|
|
167
|
+
/** Record the phase a delta moved the attempt into; the status bar shows its age. */
|
|
168
|
+
private notePhase;
|
|
147
169
|
private chunk;
|
|
148
170
|
}
|
|
149
171
|
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('{')) {
|
|
@@ -77,6 +77,7 @@ export class Transcript {
|
|
|
77
77
|
oldestSeq;
|
|
78
78
|
turnMarker;
|
|
79
79
|
attempt;
|
|
80
|
+
phase;
|
|
80
81
|
nextIndex = 0;
|
|
81
82
|
revision = 0;
|
|
82
83
|
legacyStream = false;
|
|
@@ -114,7 +115,7 @@ export class Transcript {
|
|
|
114
115
|
this.closedBlocks.clear();
|
|
115
116
|
this.oldestSeq = undefined;
|
|
116
117
|
this.turnMarker = undefined;
|
|
117
|
-
this.
|
|
118
|
+
this.clearBlocks();
|
|
118
119
|
this.keysDirty = true;
|
|
119
120
|
this.projectionRevision++;
|
|
120
121
|
this.legacyDirty = true;
|
|
@@ -157,8 +158,7 @@ export class Transcript {
|
|
|
157
158
|
throw new Error('Assistant stream revision gap');
|
|
158
159
|
this.revision = number(live.revision);
|
|
159
160
|
if (live.type === 'start') {
|
|
160
|
-
this.
|
|
161
|
-
this.closedBlocks.clear();
|
|
161
|
+
this.clearBlocks();
|
|
162
162
|
this.attempt = string(live.attemptId);
|
|
163
163
|
this.nextIndex = 0;
|
|
164
164
|
}
|
|
@@ -172,7 +172,7 @@ export class Transcript {
|
|
|
172
172
|
}
|
|
173
173
|
else if (live.type === 'end') {
|
|
174
174
|
this.attempt = undefined;
|
|
175
|
-
this.
|
|
175
|
+
this.clearBlocks();
|
|
176
176
|
}
|
|
177
177
|
else
|
|
178
178
|
throw new Error('Unknown assistant stream frame');
|
|
@@ -257,8 +257,7 @@ export class Transcript {
|
|
|
257
257
|
this.events.clear();
|
|
258
258
|
this.sizes.clear();
|
|
259
259
|
this.storedBytes = 0;
|
|
260
|
-
this.
|
|
261
|
-
this.closedBlocks.clear();
|
|
260
|
+
this.clearBlocks();
|
|
262
261
|
this.transientSeqs.clear();
|
|
263
262
|
this.sortedSeqs = [];
|
|
264
263
|
this.displayed = [];
|
|
@@ -296,6 +295,17 @@ export class Transcript {
|
|
|
296
295
|
this.sizes.delete(seq);
|
|
297
296
|
this.events.delete(seq);
|
|
298
297
|
}
|
|
298
|
+
/** The stripe of work the live attempt is in: the phase of the block that last took a delta.
|
|
299
|
+
*
|
|
300
|
+
* Its age answers "what is it doing, and for how long" without inferring anything from silence.
|
|
301
|
+
* @returns Phase kind, the tool name when the phase is a tool call, and when the phase began.
|
|
302
|
+
*/
|
|
303
|
+
get livePhase() {
|
|
304
|
+
if (this.phase === undefined || this.blocks.size === 0)
|
|
305
|
+
return undefined;
|
|
306
|
+
const { kind, name, startedAt } = this.phase;
|
|
307
|
+
return { kind, ...(name === undefined ? {} : { name }), startedAt };
|
|
308
|
+
}
|
|
299
309
|
/** Start of the open durable turn, when its timestamp is present in the retained window. */
|
|
300
310
|
get activeTurnStartedAt() {
|
|
301
311
|
return this.turnMarker?.start;
|
|
@@ -449,6 +459,7 @@ export class Transcript {
|
|
|
449
459
|
* @returns Ordered blocks without terminal style escapes.
|
|
450
460
|
*/
|
|
451
461
|
liveParts(width) {
|
|
462
|
+
const attempt = this.liveAttemptKey ?? 'live';
|
|
452
463
|
return [...this.blocks].sort(([a], [b]) => a - b).map(([index, block]) => {
|
|
453
464
|
const closed = this.closedBlocks.has(index);
|
|
454
465
|
const cached = this.liveProjection.get(block);
|
|
@@ -457,6 +468,7 @@ export class Transcript {
|
|
|
457
468
|
const part = {
|
|
458
469
|
kind: block.type === 'reasoning' ? 'reasoning' : block.type === 'tool-call' ? 'tool' : 'text',
|
|
459
470
|
text: cached?.width === width ? cached.part.text : contentText([block], undefined, width), closed,
|
|
471
|
+
key: `${attempt}:${index}`,
|
|
460
472
|
};
|
|
461
473
|
this.liveProjection.set(block, { width, part });
|
|
462
474
|
return part;
|
|
@@ -520,8 +532,7 @@ export class Transcript {
|
|
|
520
532
|
foldLegacy(added) {
|
|
521
533
|
let events;
|
|
522
534
|
if (this.legacyDirty) {
|
|
523
|
-
this.
|
|
524
|
-
this.closedBlocks.clear();
|
|
535
|
+
this.clearBlocks();
|
|
525
536
|
this.legacyPosition = '';
|
|
526
537
|
this.legacySeq = -1;
|
|
527
538
|
this.legacyDirty = false;
|
|
@@ -533,17 +544,14 @@ export class Transcript {
|
|
|
533
544
|
for (const event of events) {
|
|
534
545
|
this.legacySeq = Math.max(this.legacySeq, number(event.seq));
|
|
535
546
|
if (['step/start', 'step/end', 'turn/end', 'assistant/message', 'assistant/attempt'].includes(string(event.type))) {
|
|
536
|
-
this.
|
|
537
|
-
this.closedBlocks.clear();
|
|
547
|
+
this.clearBlocks();
|
|
538
548
|
this.legacyPosition = '';
|
|
539
549
|
}
|
|
540
550
|
else if (event.type === 'assistant/chunk' || string(event.type).startsWith('chunkrow/')) {
|
|
541
551
|
const data = object(event.data);
|
|
542
552
|
const nextPosition = `${number(data.turn)}:${number(data.step)}`;
|
|
543
|
-
if (this.legacyPosition !== nextPosition)
|
|
544
|
-
this.
|
|
545
|
-
this.closedBlocks.clear();
|
|
546
|
-
}
|
|
553
|
+
if (this.legacyPosition !== nextPosition)
|
|
554
|
+
this.clearBlocks();
|
|
547
555
|
this.legacyPosition = nextPosition;
|
|
548
556
|
switch (event.type) {
|
|
549
557
|
case 'assistant/chunk':
|
|
@@ -572,6 +580,15 @@ export class Transcript {
|
|
|
572
580
|
if (count === 0 || array(data.dt).length !== count - 1)
|
|
573
581
|
throw new Error('Invalid packed history member count');
|
|
574
582
|
}
|
|
583
|
+
/** Drop the live attempt's blocks and the phase derived from them. */
|
|
584
|
+
clearBlocks() { this.blocks.clear(); this.closedBlocks.clear(); this.phase = undefined; }
|
|
585
|
+
/** Record the phase a delta moved the attempt into; the status bar shows its age. */
|
|
586
|
+
notePhase(kind, name) {
|
|
587
|
+
const key = kind === 'tool' ? `tool:${name ?? ''}` : kind;
|
|
588
|
+
if (this.phase?.key === key)
|
|
589
|
+
return;
|
|
590
|
+
this.phase = { key, kind, ...(name === undefined ? {} : { name }), startedAt: Date.now() };
|
|
591
|
+
}
|
|
575
592
|
chunk(chunk) {
|
|
576
593
|
if (chunk.type === 'text-delta' || chunk.type === 'tool-call-delta') {
|
|
577
594
|
for (const [index, block] of this.blocks)
|
|
@@ -583,13 +600,14 @@ export class Transcript {
|
|
|
583
600
|
const block = this.blocks.get(index);
|
|
584
601
|
this.blocks.set(index, { type: chunk.type === 'text-delta' ? 'text' : 'reasoning',
|
|
585
602
|
text: (block ? string(block.text) : '') + string(chunk.text) });
|
|
603
|
+
this.notePhase(chunk.type === 'text-delta' ? 'output' : 'thinking');
|
|
586
604
|
}
|
|
587
605
|
else if (chunk.type === 'tool-call-delta') {
|
|
588
606
|
const index = number(chunk.index);
|
|
589
607
|
const block = this.blocks.get(index);
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
608
|
+
const name = typeof chunk.name === 'string' ? chunk.name : typeof block?.name === 'string' ? block.name : '';
|
|
609
|
+
this.blocks.set(index, { type: 'tool-call', id: string(chunk.id), name, arguments: '' });
|
|
610
|
+
this.notePhase('tool', name === '' ? undefined : name);
|
|
593
611
|
}
|
|
594
612
|
else if (chunk.type === 'block-end') {
|
|
595
613
|
this.blocks.set(number(chunk.index), object(chunk.block));
|
|
@@ -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
|
+
}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Application state shared by the controller facade and every domain controller. */
|
|
2
|
+
import { Transcript } from './session/transcript.ts';
|
|
3
|
+
import type { HistorySearch, RemovalTarget } from './session/types.ts';
|
|
4
|
+
import type { ObjectValue } from './transport/wire.ts';
|
|
5
|
+
/** State shared by the picker and conversation view. */
|
|
6
|
+
export interface State {
|
|
7
|
+
version: number;
|
|
8
|
+
online: boolean;
|
|
9
|
+
busy: boolean;
|
|
10
|
+
screen: 'workspaces' | 'sessions' | 'chat' | 'path';
|
|
11
|
+
status: string;
|
|
12
|
+
error: string;
|
|
13
|
+
workspaces: ObjectValue[];
|
|
14
|
+
sessions: ObjectValue[];
|
|
15
|
+
showAllSessions: boolean;
|
|
16
|
+
workspaceId?: string;
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
pending: ObjectValue[];
|
|
19
|
+
controlError?: string;
|
|
20
|
+
modelError?: string;
|
|
21
|
+
presetError?: string;
|
|
22
|
+
presets?: ObjectValue[];
|
|
23
|
+
defaultModel?: ObjectValue;
|
|
24
|
+
transcript: Transcript;
|
|
25
|
+
}
|
|
26
|
+
/** The state contract every domain controller writes through. */
|
|
27
|
+
export interface ControllerStore {
|
|
28
|
+
/** Current immutable state snapshot. */
|
|
29
|
+
readonly state: State;
|
|
30
|
+
/** Publish a state patch and notify observers. */
|
|
31
|
+
update(patch: Partial<State>): void;
|
|
32
|
+
/** Monotonic selector generation; a changed value discards in-flight session work. */
|
|
33
|
+
selection(): number;
|
|
34
|
+
/** Advance the selector generation when the selected workspace or session changes. */
|
|
35
|
+
bumpSelection(): void;
|
|
36
|
+
}
|
|
37
|
+
/** Build the initial state before any connection exists.
|
|
38
|
+
* @returns A fresh state whose transcript is empty and disconnected.
|
|
39
|
+
*/
|
|
40
|
+
export declare function initialState(): State;
|
|
41
|
+
export type { HistorySearch, RemovalTarget };
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Application state shared by the controller facade and every domain controller. */
|
|
2
|
+
import { Transcript } from "./session/transcript.js";
|
|
3
|
+
/** Build the initial state before any connection exists.
|
|
4
|
+
* @returns A fresh state whose transcript is empty and disconnected.
|
|
5
|
+
*/
|
|
6
|
+
export function initialState() {
|
|
7
|
+
return { version: 0, online: false, busy: false, screen: 'workspaces', status: 'Connecting…',
|
|
8
|
+
error: '', workspaces: [], sessions: [], showAllSessions: false, pending: [], transcript: new Transcript() };
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Create a directory tree with owner-only permissions, leaving an existing directory as it is.
|
|
2
|
+
* @param path - Directory path.
|
|
3
|
+
*/
|
|
4
|
+
export declare function ensureDirectory(path: string): Promise<void>;
|
|
5
|
+
/** Create a directory tree and require it to stay owner-only.
|
|
6
|
+
* @param path - Directory path.
|
|
7
|
+
* @param label - Name used in the validation error, such as `Cookie directory`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ensurePrivateDirectory(path: string, label: string): Promise<void>;
|
|
10
|
+
/** List the entry names of one directory.
|
|
11
|
+
* @param path - Directory path.
|
|
12
|
+
* @returns Entry names in filesystem order.
|
|
13
|
+
*/
|
|
14
|
+
export declare function listEntries(path: string): Promise<string[]>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Private directory creation and listing for the client's configuration and state. */
|
|
2
|
+
import { lstat, mkdir, readdir } from 'node:fs/promises';
|
|
3
|
+
/** Create a directory tree with owner-only permissions, leaving an existing directory as it is.
|
|
4
|
+
* @param path - Directory path.
|
|
5
|
+
*/
|
|
6
|
+
export async function ensureDirectory(path) {
|
|
7
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
8
|
+
}
|
|
9
|
+
/** Create a directory tree and require it to stay owner-only.
|
|
10
|
+
* @param path - Directory path.
|
|
11
|
+
* @param label - Name used in the validation error, such as `Cookie directory`.
|
|
12
|
+
*/
|
|
13
|
+
export async function ensurePrivateDirectory(path, label) {
|
|
14
|
+
await ensureDirectory(path);
|
|
15
|
+
const stat = await lstat(path);
|
|
16
|
+
if (!stat.isDirectory() || process.platform !== 'win32' && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())) {
|
|
17
|
+
throw new Error(`${label} must be owned by the current user with permissions 0700`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** List the entry names of one directory.
|
|
21
|
+
* @param path - Directory path.
|
|
22
|
+
* @returns Entry names in filesystem order.
|
|
23
|
+
*/
|
|
24
|
+
export async function listEntries(path) { return readdir(path); }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Read a UTF-8 file; an absent file is an ordinary result, not a failure.
|
|
2
|
+
* @param path - Absolute file path.
|
|
3
|
+
* @returns File contents, or undefined when the file does not exist.
|
|
4
|
+
*/
|
|
5
|
+
export declare function readText(path: string): Promise<string | undefined>;
|
|
6
|
+
/** Read a user-private file, rejecting a symlink, a foreign owner, or group/other access.
|
|
7
|
+
* @param path - Absolute file path.
|
|
8
|
+
* @param label - Name used in the validation error, such as `Cookie file`.
|
|
9
|
+
* @returns File contents, or undefined when the file does not exist.
|
|
10
|
+
*/
|
|
11
|
+
export declare function readPrivateFile(path: string, label: string): Promise<string | undefined>;
|
|
12
|
+
/** Replace a file atomically with owner-only contents, leaving no partial file behind.
|
|
13
|
+
* @param path - Destination path.
|
|
14
|
+
* @param contents - Complete file contents.
|
|
15
|
+
*/
|
|
16
|
+
export declare function writePrivateFile(path: string, contents: string): Promise<void>;
|
|
17
|
+
/** Create an owner-only file only when it does not exist.
|
|
18
|
+
* @param path - Destination path.
|
|
19
|
+
* @param contents - Complete file contents.
|
|
20
|
+
* @returns False when the file already existed, leaving it untouched.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createPrivateFile(path: string, contents: string): Promise<boolean>;
|
|
23
|
+
/** Stream bytes into a new owner-only file, removing it unless the write completed.
|
|
24
|
+
*
|
|
25
|
+
* The destination is created before the source is requested, so an existing file fails without
|
|
26
|
+
* contacting the host, and a source or streaming failure removes the partial file.
|
|
27
|
+
* @param path - Destination path; an existing file is never replaced.
|
|
28
|
+
* @param source - Produces the byte stream once the destination exists.
|
|
29
|
+
* @param signal - Cancels the write and removes the partial file.
|
|
30
|
+
*/
|
|
31
|
+
export declare function writeExclusiveStream(path: string, source: () => Promise<AsyncIterable<Uint8Array>>, signal?: AbortSignal): Promise<void>;
|
|
32
|
+
/** Append to an owner-only file, creating it when absent.
|
|
33
|
+
*
|
|
34
|
+
* Appending keeps a long-lived diagnostic file cheap to extend; callers that need a bounded file
|
|
35
|
+
* rewrite it periodically with `writePrivateFile`.
|
|
36
|
+
* @param path - Destination path.
|
|
37
|
+
* @param contents - Text to append.
|
|
38
|
+
*/
|
|
39
|
+
export declare function appendPrivateFile(path: string, contents: string): Promise<void>;
|
|
40
|
+
/** Remove a file, treating an already absent file as success.
|
|
41
|
+
* @param path - File to remove.
|
|
42
|
+
*/
|
|
43
|
+
export declare function removeFile(path: string): Promise<void>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** Every filesystem read and write the client performs; no other module opens a file. */
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { open, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
/** Read a UTF-8 file; an absent file is an ordinary result, not a failure.
|
|
7
|
+
* @param path - Absolute file path.
|
|
8
|
+
* @returns File contents, or undefined when the file does not exist.
|
|
9
|
+
*/
|
|
10
|
+
export async function readText(path) {
|
|
11
|
+
try {
|
|
12
|
+
return await readFile(path, 'utf8');
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (isMissing(error))
|
|
16
|
+
return undefined;
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Read a user-private file, rejecting a symlink, a foreign owner, or group/other access.
|
|
21
|
+
* @param path - Absolute file path.
|
|
22
|
+
* @param label - Name used in the validation error, such as `Cookie file`.
|
|
23
|
+
* @returns File contents, or undefined when the file does not exist.
|
|
24
|
+
*/
|
|
25
|
+
export async function readPrivateFile(path, label) {
|
|
26
|
+
let handle;
|
|
27
|
+
try {
|
|
28
|
+
handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (isMissing(error))
|
|
32
|
+
return undefined;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const stat = await handle.stat();
|
|
37
|
+
if (!stat.isFile() || process.platform !== 'win32' && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())) {
|
|
38
|
+
throw new Error(`${label} must be owned by the current user with permissions 0600`);
|
|
39
|
+
}
|
|
40
|
+
return await handle.readFile('utf8');
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await handle.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Replace a file atomically with owner-only contents, leaving no partial file behind.
|
|
47
|
+
* @param path - Destination path.
|
|
48
|
+
* @param contents - Complete file contents.
|
|
49
|
+
*/
|
|
50
|
+
export async function writePrivateFile(path, contents) {
|
|
51
|
+
const temporary = join(dirname(path), `${randomUUID()}.tmp`);
|
|
52
|
+
try {
|
|
53
|
+
await writeFile(temporary, contents, { mode: 0o600, flag: 'wx' });
|
|
54
|
+
await rename(temporary, path);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
await removeFile(temporary);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Create an owner-only file only when it does not exist.
|
|
61
|
+
* @param path - Destination path.
|
|
62
|
+
* @param contents - Complete file contents.
|
|
63
|
+
* @returns False when the file already existed, leaving it untouched.
|
|
64
|
+
*/
|
|
65
|
+
export async function createPrivateFile(path, contents) {
|
|
66
|
+
try {
|
|
67
|
+
await writeFile(path, contents, { mode: 0o600, flag: 'wx' });
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (isCode(error, 'EEXIST'))
|
|
72
|
+
return false;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Stream bytes into a new owner-only file, removing it unless the write completed.
|
|
77
|
+
*
|
|
78
|
+
* The destination is created before the source is requested, so an existing file fails without
|
|
79
|
+
* contacting the host, and a source or streaming failure removes the partial file.
|
|
80
|
+
* @param path - Destination path; an existing file is never replaced.
|
|
81
|
+
* @param source - Produces the byte stream once the destination exists.
|
|
82
|
+
* @param signal - Cancels the write and removes the partial file.
|
|
83
|
+
*/
|
|
84
|
+
export async function writeExclusiveStream(path, source, signal) {
|
|
85
|
+
const file = await open(path, 'wx', 0o600);
|
|
86
|
+
let complete = false;
|
|
87
|
+
try {
|
|
88
|
+
await file.writeFile(await source(), { signal });
|
|
89
|
+
signal?.throwIfAborted();
|
|
90
|
+
complete = true;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
try {
|
|
94
|
+
await file.close();
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
if (!complete)
|
|
98
|
+
await removeFile(path);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Append to an owner-only file, creating it when absent.
|
|
103
|
+
*
|
|
104
|
+
* Appending keeps a long-lived diagnostic file cheap to extend; callers that need a bounded file
|
|
105
|
+
* rewrite it periodically with `writePrivateFile`.
|
|
106
|
+
* @param path - Destination path.
|
|
107
|
+
* @param contents - Text to append.
|
|
108
|
+
*/
|
|
109
|
+
export async function appendPrivateFile(path, contents) {
|
|
110
|
+
const handle = await open(path, 'a', 0o600);
|
|
111
|
+
try {
|
|
112
|
+
await handle.writeFile(contents);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await handle.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Remove a file, treating an already absent file as success.
|
|
119
|
+
* @param path - File to remove.
|
|
120
|
+
*/
|
|
121
|
+
export async function removeFile(path) {
|
|
122
|
+
try {
|
|
123
|
+
await unlink(path);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (!isMissing(error))
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Recognize one error code without assuming the error's prototype. */
|
|
131
|
+
function isCode(error, code) {
|
|
132
|
+
return error instanceof Error && 'code' in error && error.code === code;
|
|
133
|
+
}
|
|
134
|
+
/** Whether an operation failed because the path does not exist. */
|
|
135
|
+
function isMissing(error) { return isCode(error, 'ENOENT'); }
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Storage domain: every filesystem operation the client performs. */
|
|
2
|
+
export { appendPrivateFile, createPrivateFile, readPrivateFile, readText, removeFile, writeExclusiveStream, writePrivateFile } from './files.ts';
|
|
3
|
+
export { ensureDirectory, ensurePrivateDirectory, listEntries } from './directories.ts';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Storage domain: every filesystem operation the client performs. */
|
|
2
|
+
export { appendPrivateFile, createPrivateFile, readPrivateFile, readText, removeFile, writeExclusiveStream, writePrivateFile } from "./files.js";
|
|
3
|
+
export { ensureDirectory, ensurePrivateDirectory, listEntries } from "./directories.js";
|