@itookit/dsht 0.3.8 → 0.5.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 +33 -12
- package/README.zh.md +35 -14
- package/dist/catalog/controller.d.ts +26 -6
- package/dist/catalog/controller.js +73 -45
- package/dist/catalog/index.d.ts +1 -0
- package/dist/cli/dsht.js +206 -18
- package/dist/cli/startup.d.ts +40 -0
- package/dist/cli/startup.js +314 -0
- package/dist/cli/trace-summary.d.ts +78 -0
- package/dist/cli/trace-summary.js +241 -0
- package/dist/cli/verifier.d.ts +64 -0
- package/dist/cli/verifier.js +265 -0
- package/dist/contracts.d.ts +359 -0
- package/dist/contracts.js +1 -0
- package/dist/controller/commands.d.ts +47 -0
- package/dist/controller/commands.js +322 -0
- package/dist/controller/connection-streams.d.ts +22 -0
- package/dist/controller/connection-streams.js +105 -0
- package/dist/controller/connection.d.ts +24 -31
- package/dist/controller/connection.js +48 -111
- package/dist/controller/controller.d.ts +412 -178
- package/dist/controller/controller.js +713 -167
- package/dist/controller/foreground.d.ts +44 -0
- package/dist/controller/foreground.js +79 -0
- package/dist/controller/index.d.ts +8 -1
- package/dist/controller/index.js +5 -0
- package/dist/controller/loop-contract.d.ts +136 -0
- package/dist/controller/loop-contract.js +308 -0
- package/dist/controller/loop-coordinator.d.ts +48 -0
- package/dist/controller/loop-coordinator.js +647 -0
- package/dist/controller/loop-prompts-schema.d.ts +56 -0
- package/dist/controller/loop-prompts-schema.js +144 -0
- package/dist/controller/loop-prompts.d.ts +55 -0
- package/dist/controller/loop-prompts.generated.d.ts +104 -0
- package/dist/controller/loop-prompts.generated.js +185 -0
- package/dist/controller/loop-prompts.js +104 -0
- package/dist/controller/loop-protocols.d.ts +39 -0
- package/dist/controller/loop-protocols.js +115 -0
- package/dist/controller/loop.d.ts +275 -0
- package/dist/controller/loop.js +378 -0
- package/dist/controller/prompts.d.ts +54 -0
- package/dist/controller/prompts.js +162 -0
- package/dist/controller/trace-log.d.ts +45 -0
- package/dist/controller/trace-log.js +144 -0
- package/dist/controller/verifier.d.ts +130 -0
- package/dist/controller/verifier.js +75 -0
- package/dist/cost/controller.d.ts +1 -1
- package/dist/cost/controller.js +12 -5
- package/dist/cost/index.d.ts +1 -1
- package/dist/cost/index.js +1 -1
- package/dist/cost/ledger.d.ts +0 -1
- package/dist/cost/ledger.js +0 -1
- package/dist/cost/scanner.js +1 -0
- package/dist/json.d.ts +18 -0
- package/dist/json.js +19 -0
- package/dist/references.d.ts +25 -0
- package/dist/references.js +26 -0
- package/dist/session/connection-view.d.ts +2 -11
- package/dist/session/controller.d.ts +94 -105
- package/dist/session/controller.js +262 -536
- package/dist/session/history-reader.d.ts +32 -0
- package/dist/session/history-reader.js +170 -0
- package/dist/session/history.d.ts +6 -18
- package/dist/session/history.js +1 -24
- package/dist/session/index.d.ts +9 -4
- package/dist/session/index.js +7 -3
- package/dist/session/info.d.ts +20 -82
- package/dist/session/info.js +52 -25
- package/dist/session/interactions.d.ts +26 -0
- package/dist/session/interactions.js +75 -0
- package/dist/session/markdown.js +1 -1
- package/dist/session/math.js +1 -1
- package/dist/session/mutation-gate.d.ts +51 -0
- package/dist/session/mutation-gate.js +73 -0
- package/dist/session/navigation.d.ts +2 -89
- package/dist/session/navigation.js +2 -129
- package/dist/session/navigator.d.ts +47 -0
- package/dist/session/navigator.js +158 -0
- package/dist/session/peek.d.ts +38 -0
- package/dist/session/peek.js +103 -0
- package/dist/session/prompt-backfill.d.ts +23 -0
- package/dist/session/prompt-backfill.js +88 -0
- package/dist/session/references.d.ts +2 -20
- package/dist/session/references.js +1 -26
- package/dist/session/runtime.d.ts +26 -0
- package/dist/session/runtime.js +28 -0
- package/dist/session/state.d.ts +20 -0
- package/dist/session/state.js +1 -0
- package/dist/session/telemetry.d.ts +25 -17
- package/dist/session/telemetry.js +66 -60
- package/dist/session/transcript.d.ts +5 -7
- package/dist/session/transcript.js +2 -15
- package/dist/session/types.d.ts +25 -0
- package/dist/session/types.js +0 -1
- package/dist/session-title.d.ts +9 -0
- package/dist/session-title.js +21 -0
- package/dist/shell/controller.d.ts +31 -1
- package/dist/shell/controller.js +34 -2
- package/dist/shell/index.d.ts +3 -3
- package/dist/shell/index.js +2 -2
- package/dist/shell/runner.d.ts +10 -0
- package/dist/shell/runner.js +48 -9
- package/dist/slash/index.d.ts +10 -0
- package/dist/slash/index.js +7 -0
- package/dist/slash/parse.d.ts +42 -0
- package/dist/slash/parse.js +259 -0
- package/dist/slash/pipeline.d.ts +140 -0
- package/dist/slash/pipeline.js +115 -0
- package/dist/slash/registry.d.ts +88 -0
- package/dist/slash/registry.js +177 -0
- package/dist/slash/types.d.ts +126 -0
- package/dist/slash/types.js +1 -0
- package/dist/state.d.ts +16 -18
- package/dist/state.js +4 -3
- package/dist/text.d.ts +28 -0
- package/dist/text.js +55 -0
- package/dist/transport/client.d.ts +4 -3
- package/dist/transport/client.js +71 -25
- package/dist/transport/events.d.ts +104 -0
- package/dist/transport/events.js +149 -0
- package/dist/transport/wire.d.ts +9 -17
- package/dist/transport/wire.js +2 -27
- package/dist/ui/app.js +750 -550
- package/dist/ui/chat/header.js +1 -1
- package/dist/ui/chat/history-view.d.ts +1 -1
- package/dist/ui/chat/loop-status.d.ts +11 -0
- package/dist/ui/chat/loop-status.js +28 -0
- package/dist/ui/chat/navigation-model.d.ts +86 -0
- package/dist/ui/chat/navigation-model.js +107 -0
- package/dist/ui/chat/shell-view.d.ts +17 -2
- package/dist/ui/chat/shell-view.js +45 -3
- package/dist/ui/chat/status.d.ts +47 -3
- package/dist/ui/chat/status.js +65 -50
- package/dist/ui/chat/use-history-view.d.ts +69 -0
- package/dist/ui/chat/use-history-view.js +123 -0
- package/dist/ui/chat/viewport.d.ts +1 -1
- package/dist/ui/dialogs/cost.d.ts +21 -4
- package/dist/ui/dialogs/cost.js +7 -12
- package/dist/ui/dialogs/index.d.ts +22 -5
- package/dist/ui/dialogs/index.js +19 -3
- package/dist/ui/dialogs/loop.d.ts +43 -0
- package/dist/ui/dialogs/loop.js +224 -0
- package/dist/ui/dialogs/peek.d.ts +25 -0
- package/dist/ui/dialogs/peek.js +35 -0
- package/dist/ui/dialogs/picker.d.ts +2 -0
- package/dist/ui/dialogs/picker.js +4 -2
- package/dist/ui/dialogs/use-panels.d.ts +53 -0
- package/dist/ui/dialogs/use-panels.js +51 -0
- package/dist/ui/input/mouse.d.ts +12 -2
- package/dist/ui/input/mouse.js +20 -7
- package/dist/ui/input/references.d.ts +1 -1
- package/dist/ui/input/use-composer.d.ts +35 -0
- package/dist/ui/input/use-composer.js +109 -0
- package/dist/ui/input/use-deferred-lines.d.ts +16 -0
- package/dist/ui/input/use-deferred-lines.js +54 -0
- package/dist/ui/input/use-history-recall.d.ts +20 -0
- package/dist/ui/input/use-history-recall.js +47 -0
- package/dist/ui/status/model.d.ts +7 -0
- package/dist/ui/status/model.js +5 -0
- package/dist/ui/theme/index.d.ts +1 -1
- package/package.json +6 -4
- package/dist/ui/commands/parse.d.ts +0 -104
- package/dist/ui/commands/parse.js +0 -135
- package/dist/ui/commands/registry.d.ts +0 -33
- package/dist/ui/commands/registry.js +0 -73
|
@@ -1,106 +1,120 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
const EMPTY_VALUES = Object.freeze({});
|
|
2
|
+
const EMPTY_QUEUE = Object.freeze([]);
|
|
3
|
+
const EMPTY_VIEW = Object.freeze({ values: EMPTY_VALUES, queued: undefined, jobs: undefined });
|
|
4
|
+
/** Copy on admission, not on render; no published nested object aliases a wire frame. */
|
|
5
|
+
function retain(value) {
|
|
6
|
+
const copy = structuredClone(value);
|
|
7
|
+
const pending = [copy];
|
|
8
|
+
while (pending.length) {
|
|
9
|
+
const item = pending.pop();
|
|
10
|
+
if (item === null || typeof item !== 'object' || Object.isFrozen(item))
|
|
11
|
+
continue;
|
|
12
|
+
Object.freeze(item);
|
|
13
|
+
for (const child of Object.values(item))
|
|
14
|
+
pending.push(child);
|
|
15
|
+
}
|
|
16
|
+
return copy;
|
|
17
|
+
}
|
|
18
|
+
function retainQueue(items) {
|
|
19
|
+
return Object.freeze(items.map(item => Object.freeze({ ...item })));
|
|
11
20
|
}
|
|
12
21
|
/** Generation-local projection, inbox and background-job data for every session. */
|
|
13
22
|
export class Telemetry {
|
|
14
23
|
retainedKeys;
|
|
15
24
|
entries = new Map();
|
|
16
25
|
queues = new Map();
|
|
26
|
+
views = new Map();
|
|
27
|
+
reader;
|
|
17
28
|
jobs = new Map();
|
|
18
29
|
ready = false;
|
|
19
30
|
/** Optionally retain only projection capabilities consumed by this client. */
|
|
20
31
|
constructor(retainedKeys) {
|
|
21
32
|
this.retainedKeys = retainedKeys;
|
|
33
|
+
const telemetry = this;
|
|
34
|
+
this.reader = Object.freeze({
|
|
35
|
+
get ready() { return telemetry.ready; },
|
|
36
|
+
view: (id) => this.view(id), pending: (id) => this.pending(id),
|
|
37
|
+
});
|
|
22
38
|
}
|
|
23
39
|
/** Replace all control state on a new stream baseline, then accept replacement frames.
|
|
24
|
-
* @param
|
|
40
|
+
* @param frame - One normalized session/control frame.
|
|
25
41
|
*/
|
|
26
|
-
accept(
|
|
27
|
-
|
|
28
|
-
if (frame.type === 'baseline') {
|
|
29
|
-
const baseline = object(frame.value);
|
|
42
|
+
accept(frame) {
|
|
43
|
+
if (frame.kind === 'baseline') {
|
|
30
44
|
this.entries.clear();
|
|
31
45
|
this.queues.clear();
|
|
32
46
|
this.jobs.clear();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
this.views.clear();
|
|
48
|
+
for (const [id, snapshot] of frame.projections)
|
|
49
|
+
this.snapshot(id, snapshot);
|
|
50
|
+
for (const [id, items] of frame.queues)
|
|
51
|
+
this.queues.set(id, retainQueue(items));
|
|
52
|
+
for (const [id, count] of frame.jobs)
|
|
53
|
+
this.jobs.set(id, count);
|
|
39
54
|
this.ready = true;
|
|
40
55
|
return;
|
|
41
56
|
}
|
|
42
57
|
if (!this.ready)
|
|
43
58
|
throw new Error('Session control update before baseline');
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const key = string(frame.key);
|
|
48
|
-
const seq = sequence(frame.seq);
|
|
49
|
-
if (seq < (entry.revisions.get(key) ?? entry.baseline))
|
|
59
|
+
if (frame.kind === 'projection') {
|
|
60
|
+
const entry = this.entry(frame.sessionId);
|
|
61
|
+
if (frame.seq < (entry.revisions.get(frame.key) ?? entry.baseline))
|
|
50
62
|
return;
|
|
51
|
-
if (
|
|
52
|
-
throw new Error('Missing projection value');
|
|
53
|
-
if (this.retainedKeys && !this.retainedKeys.has(key))
|
|
63
|
+
if (this.retainedKeys && !this.retainedKeys.has(frame.key))
|
|
54
64
|
return;
|
|
55
|
-
entry.values[key] = frame.value;
|
|
56
|
-
entry.revisions.set(key, seq);
|
|
65
|
+
entry.values[frame.key] = retain(frame.value);
|
|
66
|
+
entry.revisions.set(frame.key, frame.seq);
|
|
57
67
|
}
|
|
58
|
-
else if (frame.
|
|
59
|
-
this.queues.set(
|
|
60
|
-
else if (frame.type === 'jobs')
|
|
61
|
-
this.jobs.set(id, activeJobs(frame.items));
|
|
68
|
+
else if (frame.kind === 'queue')
|
|
69
|
+
this.queues.set(frame.sessionId, retainQueue(frame.items));
|
|
62
70
|
else
|
|
63
|
-
|
|
71
|
+
this.jobs.set(frame.sessionId, frame.count);
|
|
72
|
+
this.views.delete(frame.sessionId);
|
|
64
73
|
}
|
|
65
74
|
/** Merge a complete follow snapshot without restoring absent or older projection values.
|
|
66
75
|
* @param id - Session identity.
|
|
67
|
-
* @param
|
|
76
|
+
* @param baseline - Projection baseline, when supplied by the host.
|
|
68
77
|
*/
|
|
69
|
-
snapshot(id,
|
|
70
|
-
if (
|
|
78
|
+
snapshot(id, baseline) {
|
|
79
|
+
if (baseline === undefined)
|
|
71
80
|
return;
|
|
72
|
-
const baseline = object(value);
|
|
73
|
-
const seq = sequence(baseline.asOfSeq);
|
|
74
|
-
const values = object(baseline.values);
|
|
75
81
|
const entry = this.entry(id);
|
|
76
|
-
if (
|
|
82
|
+
if (baseline.asOfSeq < entry.baseline)
|
|
77
83
|
return;
|
|
78
|
-
for (const key of new Set([...Object.keys(entry.values), ...Object.keys(values)])) {
|
|
84
|
+
for (const key of new Set([...Object.keys(entry.values), ...Object.keys(baseline.values)])) {
|
|
79
85
|
if (this.retainedKeys && !this.retainedKeys.has(key))
|
|
80
86
|
continue;
|
|
81
|
-
if ((entry.revisions.get(key) ?? -1) >
|
|
87
|
+
if ((entry.revisions.get(key) ?? -1) > baseline.asOfSeq)
|
|
82
88
|
continue;
|
|
83
|
-
if (Object.hasOwn(values, key))
|
|
84
|
-
entry.values[key] = values[key];
|
|
89
|
+
if (Object.hasOwn(baseline.values, key))
|
|
90
|
+
entry.values[key] = retain(baseline.values[key]);
|
|
85
91
|
else
|
|
86
92
|
delete entry.values[key];
|
|
87
93
|
entry.revisions.delete(key);
|
|
88
94
|
}
|
|
89
|
-
entry.baseline =
|
|
95
|
+
entry.baseline = baseline.asOfSeq;
|
|
96
|
+
this.views.delete(id);
|
|
90
97
|
}
|
|
91
98
|
/** Read current values for one session; missing capabilities remain absent.
|
|
92
99
|
* @param id - Selected session identity, if any.
|
|
93
100
|
* @returns Projection values and known queue/job counts.
|
|
94
101
|
*/
|
|
95
102
|
view(id) {
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
if (!id || !this.entries.has(id) && !this.queues.has(id) && !this.jobs.has(id))
|
|
104
|
+
return EMPTY_VIEW;
|
|
105
|
+
let view = this.views.get(id);
|
|
106
|
+
if (!view) {
|
|
107
|
+
view = Object.freeze({ values: Object.freeze({ ...this.entries.get(id)?.values }),
|
|
108
|
+
queued: this.queues.get(id)?.length, jobs: this.jobs.get(id) });
|
|
109
|
+
this.views.set(id, view);
|
|
110
|
+
}
|
|
111
|
+
return view;
|
|
98
112
|
}
|
|
99
113
|
/** Read the selected session's authoritative pending inputs; absence means no observed queue.
|
|
100
114
|
* @param id - Session identity.
|
|
101
115
|
* @returns Pending inputs in host order.
|
|
102
116
|
*/
|
|
103
|
-
pending(id) { return id ? this.queues.get(id) ??
|
|
117
|
+
pending(id) { return id ? this.queues.get(id) ?? EMPTY_QUEUE : EMPTY_QUEUE; }
|
|
104
118
|
entry(id) {
|
|
105
119
|
let entry = this.entries.get(id);
|
|
106
120
|
if (!entry) {
|
|
@@ -110,11 +124,3 @@ export class Telemetry {
|
|
|
110
124
|
return entry;
|
|
111
125
|
}
|
|
112
126
|
}
|
|
113
|
-
function sequence(value) {
|
|
114
|
-
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < -1)
|
|
115
|
-
throw new Error('Invalid projection watermark');
|
|
116
|
-
return value;
|
|
117
|
-
}
|
|
118
|
-
function activeJobs(value) {
|
|
119
|
-
return array(value).filter(item => ['running', 'stopping'].includes(string(object(item).status))).length;
|
|
120
|
-
}
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import type { HistoryLimits } from './memory.ts';
|
|
2
|
-
import type { PromptRecord } from './info.ts';
|
|
3
2
|
import { type Json, type ObjectValue } from '../transport/wire.ts';
|
|
3
|
+
/** A durable user prompt projected from the transcript, before recall retention. */
|
|
4
|
+
export interface PromptRecord {
|
|
5
|
+
seq: number;
|
|
6
|
+
text: string;
|
|
7
|
+
}
|
|
4
8
|
interface ToolSummary {
|
|
5
9
|
name: string;
|
|
6
10
|
operation?: string;
|
|
7
11
|
command?: string;
|
|
8
12
|
}
|
|
9
|
-
/** Fit a tool operation to one terminal row without exposing the result body.
|
|
10
|
-
* @param text - Tool name, status icon and optional operation.
|
|
11
|
-
* @param width - Available terminal columns.
|
|
12
|
-
* @returns A single line with an ellipsis when shortened.
|
|
13
|
-
*/
|
|
14
|
-
export declare function toolLine(text: string, width: number): string;
|
|
15
13
|
/** Render known content blocks and preserve unknown plugin blocks as JSON.
|
|
16
14
|
* @param content - Message content blocks.
|
|
17
15
|
* @param tools - Tool summaries by call ID, when retained history provides them.
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import { array, object, safeText, string } from "../transport/wire.js";
|
|
1
|
+
import { array, object, string } from "../transport/wire.js";
|
|
2
|
+
import { safeText, toolLine } from "../text.js";
|
|
4
3
|
function toolSummary(block) {
|
|
5
4
|
let args = {};
|
|
6
5
|
if (typeof block.arguments === 'string' && block.arguments.trimStart().startsWith('{')) {
|
|
@@ -19,18 +18,6 @@ function toolSummary(block) {
|
|
|
19
18
|
return { name: string(block.name), ...(typeof operation === 'string' ? { operation } : {}),
|
|
20
19
|
...(typeof command === 'string' && command !== operation ? { command: command.split(/\r?\n/)[0] } : {}) };
|
|
21
20
|
}
|
|
22
|
-
/** Fit a tool operation to one terminal row without exposing the result body.
|
|
23
|
-
* @param text - Tool name, status icon and optional operation.
|
|
24
|
-
* @param width - Available terminal columns.
|
|
25
|
-
* @returns A single line with an ellipsis when shortened.
|
|
26
|
-
*/
|
|
27
|
-
export function toolLine(text, width) {
|
|
28
|
-
const clean = safeText(text).replace(/\s+/gu, ' ').trim();
|
|
29
|
-
if (width < 2)
|
|
30
|
-
return width === 1 ? '…' : '';
|
|
31
|
-
const clipped = sliceAnsi(clean, 0, width);
|
|
32
|
-
return clipped.length < clean.length ? sliceAnsi(clean, 0, width - 1) + '…' : clean;
|
|
33
|
-
}
|
|
34
21
|
/** Render known content blocks and preserve unknown plugin blocks as JSON.
|
|
35
22
|
* @param content - Message content blocks.
|
|
36
23
|
* @param tools - Tool summaries by call ID, when retained history provides them.
|
package/dist/session/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Session-domain result types shared by the controller facade and the terminal UI. */
|
|
2
|
+
import type { QuestionItem } from '../transport/events.ts';
|
|
2
3
|
/** Resolved navigation-removal identity; empty marks a fresh blank, idle session eligible for immediate archival. */
|
|
3
4
|
export interface RemovalTarget {
|
|
4
5
|
kind: 'workspace' | 'session';
|
|
@@ -16,3 +17,27 @@ export interface HistorySearch {
|
|
|
16
17
|
}[];
|
|
17
18
|
truncated: boolean;
|
|
18
19
|
}
|
|
20
|
+
/** A structured question answer as the UI collects it; the host receives it inside an outcome. */
|
|
21
|
+
export type AnswerValue = {
|
|
22
|
+
answers: {
|
|
23
|
+
id: string;
|
|
24
|
+
selected: string[];
|
|
25
|
+
custom?: string;
|
|
26
|
+
}[];
|
|
27
|
+
};
|
|
28
|
+
/** One unanswered host interaction of the selected session.
|
|
29
|
+
*
|
|
30
|
+
* A discriminated union rather than the raw waterfall: the UI switches on `kind` and reads named
|
|
31
|
+
* fields, so it never needs to know how the host shaped its request.
|
|
32
|
+
*/
|
|
33
|
+
export type PendingInteraction = {
|
|
34
|
+
kind: 'approval';
|
|
35
|
+
eventId: string;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
description: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'question';
|
|
40
|
+
eventId: string;
|
|
41
|
+
sessionId: string;
|
|
42
|
+
questions: readonly QuestionItem[];
|
|
43
|
+
};
|
package/dist/session/types.js
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Read a session summary's display title.
|
|
2
|
+
*
|
|
3
|
+
* Host rows carry their title inside a projection, and both the domain (matching a target by name)
|
|
4
|
+
* and the UI (labelling a row) need the same reading, so it lives in its own leaf rather than in
|
|
5
|
+
* either layer.
|
|
6
|
+
*/
|
|
7
|
+
import { type ObjectValue } from './json.ts';
|
|
8
|
+
/** Resolve the host's title projection, falling back to the session ID. */
|
|
9
|
+
export declare function sessionLabel(session: ObjectValue): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Read a session summary's display title.
|
|
2
|
+
*
|
|
3
|
+
* Host rows carry their title inside a projection, and both the domain (matching a target by name)
|
|
4
|
+
* and the UI (labelling a row) need the same reading, so it lives in its own leaf rather than in
|
|
5
|
+
* either layer.
|
|
6
|
+
*/
|
|
7
|
+
import { string } from "./json.js";
|
|
8
|
+
import { safeText } from "./text.js";
|
|
9
|
+
/** Resolve the host's title projection, falling back to the session ID. */
|
|
10
|
+
export function sessionLabel(session) {
|
|
11
|
+
const projections = session.projections;
|
|
12
|
+
if (projections && typeof projections === 'object' && !Array.isArray(projections)) {
|
|
13
|
+
const values = projections.values;
|
|
14
|
+
const title = values && typeof values === 'object' && !Array.isArray(values) ? values.title : undefined;
|
|
15
|
+
if (typeof title === 'string' && safeText(title).trim())
|
|
16
|
+
return safeText(title).trim();
|
|
17
|
+
if (title && typeof title === 'object' && !Array.isArray(title) && typeof title.title === 'string' && safeText(title.title).trim())
|
|
18
|
+
return safeText(title.title).trim();
|
|
19
|
+
}
|
|
20
|
+
return string(session.sessionId);
|
|
21
|
+
}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Readable id of one `!` run's own output, so its transcript bar and its source entry agree. */
|
|
2
|
+
export declare function localSourceId(id: number): string;
|
|
3
|
+
/** One local command and the output retained for it.
|
|
4
|
+
*
|
|
5
|
+
* A block is either a `!` process (`shell`) or a client command echoed so its bar can be read and
|
|
6
|
+
* clicked (`note`). A note runs nothing; it carries the readable source its bar opens, which the
|
|
7
|
+
* application links once the thing it announced exists.
|
|
8
|
+
*/
|
|
2
9
|
export interface ShellBlock {
|
|
3
10
|
id: number;
|
|
11
|
+
/** What produced the block: a local process, or a command echoed for reading. */
|
|
12
|
+
kind: 'shell' | 'note';
|
|
13
|
+
/** Readable source this block's bar opens: its own lines, or the session a note announces. */
|
|
14
|
+
source?: string;
|
|
4
15
|
command: string;
|
|
5
16
|
/** Retained output lines, oldest first. */
|
|
6
17
|
lines: string[];
|
|
@@ -15,6 +26,11 @@ export interface ShellBlock {
|
|
|
15
26
|
startedAt: number;
|
|
16
27
|
endedAt?: number;
|
|
17
28
|
}
|
|
29
|
+
/** Plain read-only view of the local `!` runs, so the UI never reads the service object. */
|
|
30
|
+
export interface ShellSnapshot {
|
|
31
|
+
running: boolean;
|
|
32
|
+
blocks: readonly ShellBlock[];
|
|
33
|
+
}
|
|
18
34
|
/** What one shell controller needs from its owner. */
|
|
19
35
|
export interface ShellHost {
|
|
20
36
|
/** Repaint after output or a status change. */
|
|
@@ -41,6 +57,8 @@ export declare class ShellController {
|
|
|
41
57
|
get runs(): readonly ShellBlock[];
|
|
42
58
|
/** Whether a command is still running. */
|
|
43
59
|
get running(): boolean;
|
|
60
|
+
/** The plain snapshot `AppState.shell` publishes; blocks stay owned here. */
|
|
61
|
+
snapshot(): ShellSnapshot;
|
|
44
62
|
/** Start one command.
|
|
45
63
|
*
|
|
46
64
|
* One command runs at a time: a second `!` while the first is live is refused rather than queued,
|
|
@@ -53,6 +71,18 @@ export declare class ShellController {
|
|
|
53
71
|
* @returns Whether a run was stopped.
|
|
54
72
|
*/
|
|
55
73
|
cancel(): boolean;
|
|
74
|
+
/** Echo one client command as a local block, so the transcript holds a bar for it.
|
|
75
|
+
*
|
|
76
|
+
* Nothing runs and nothing is retained: the bar exists to be read and clicked, and the application
|
|
77
|
+
* links it to a readable source afterwards (`link`). A command that only fills a composer frame
|
|
78
|
+
* never reaches here, so the transcript does not collect bars for forms that were never run.
|
|
79
|
+
* @param command - The command line as the operator submitted it.
|
|
80
|
+
* @param source - Readable source the bar opens, when one already exists; `link` adds a later one.
|
|
81
|
+
* @returns The block created for it.
|
|
82
|
+
*/
|
|
83
|
+
note(command: string, source?: string): ShellBlock;
|
|
84
|
+
/** Point the newest note at a readable source, so its bar opens what the run is doing now. */
|
|
85
|
+
link(source: string): void;
|
|
56
86
|
/** One run's retained output, with dropped lines made explicit.
|
|
57
87
|
* @param id - Run to read; defaults to the newest.
|
|
58
88
|
* @returns The text, or undefined when the run is unknown.
|
package/dist/shell/controller.js
CHANGED
|
@@ -14,6 +14,8 @@ const BLOCK_BYTES = 64 * 1024;
|
|
|
14
14
|
const BLOCK_LIMIT = 20;
|
|
15
15
|
/** Fastest repaint cadence while output streams; status changes always publish. */
|
|
16
16
|
const PUBLISH_INTERVAL_MS = 80;
|
|
17
|
+
/** Readable id of one `!` run's own output, so its transcript bar and its source entry agree. */
|
|
18
|
+
export function localSourceId(id) { return `shell:${id}`; }
|
|
17
19
|
/** Owns the `!` commands of this client process: one at a time, bounded, killable. */
|
|
18
20
|
export class ShellController {
|
|
19
21
|
host;
|
|
@@ -32,6 +34,8 @@ export class ShellController {
|
|
|
32
34
|
get runs() { return this.blocks; }
|
|
33
35
|
/** Whether a command is still running. */
|
|
34
36
|
get running() { return this.blocks.some(block => block.status === 'running'); }
|
|
37
|
+
/** The plain snapshot `AppState.shell` publishes; blocks stay owned here. */
|
|
38
|
+
snapshot() { return { running: this.running, blocks: this.blocks }; }
|
|
35
39
|
/** Start one command.
|
|
36
40
|
*
|
|
37
41
|
* One command runs at a time: a second `!` while the first is live is refused rather than queued,
|
|
@@ -44,8 +48,9 @@ export class ShellController {
|
|
|
44
48
|
throw new Error('Shell commands are disabled (--no-shell or DSHT_NO_SHELL=1)');
|
|
45
49
|
if (this.task)
|
|
46
50
|
throw new Error('A shell command is already running; Ctrl+C stops it');
|
|
47
|
-
const
|
|
48
|
-
|
|
51
|
+
const id = this.nextId++;
|
|
52
|
+
const block = { id, kind: 'shell', source: localSourceId(id), command, lines: [], dropped: 0,
|
|
53
|
+
status: 'running', startedAt: Date.now(), anchor: this.host.anchor() };
|
|
49
54
|
this.blocks.push(block);
|
|
50
55
|
while (this.blocks.length > BLOCK_LIMIT)
|
|
51
56
|
this.blocks.shift();
|
|
@@ -84,6 +89,33 @@ export class ShellController {
|
|
|
84
89
|
this.abort.abort();
|
|
85
90
|
return true;
|
|
86
91
|
}
|
|
92
|
+
/** Echo one client command as a local block, so the transcript holds a bar for it.
|
|
93
|
+
*
|
|
94
|
+
* Nothing runs and nothing is retained: the bar exists to be read and clicked, and the application
|
|
95
|
+
* links it to a readable source afterwards (`link`). A command that only fills a composer frame
|
|
96
|
+
* never reaches here, so the transcript does not collect bars for forms that were never run.
|
|
97
|
+
* @param command - The command line as the operator submitted it.
|
|
98
|
+
* @param source - Readable source the bar opens, when one already exists; `link` adds a later one.
|
|
99
|
+
* @returns The block created for it.
|
|
100
|
+
*/
|
|
101
|
+
note(command, source) {
|
|
102
|
+
const block = { id: this.nextId++, kind: 'note', command, lines: [], dropped: 0,
|
|
103
|
+
status: 'exited', startedAt: Date.now(), endedAt: Date.now(), anchor: this.host.anchor(),
|
|
104
|
+
...(source === undefined ? {} : { source }) };
|
|
105
|
+
this.blocks.push(block);
|
|
106
|
+
while (this.blocks.length > BLOCK_LIMIT)
|
|
107
|
+
this.blocks.shift();
|
|
108
|
+
this.publish(true);
|
|
109
|
+
return block;
|
|
110
|
+
}
|
|
111
|
+
/** Point the newest note at a readable source, so its bar opens what the run is doing now. */
|
|
112
|
+
link(source) {
|
|
113
|
+
const note = [...this.blocks].reverse().find(block => block.kind === 'note');
|
|
114
|
+
if (note === undefined || note.source === source)
|
|
115
|
+
return;
|
|
116
|
+
note.source = source;
|
|
117
|
+
this.publish(true);
|
|
118
|
+
}
|
|
87
119
|
/** One run's retained output, with dropped lines made explicit.
|
|
88
120
|
* @param id - Run to read; defaults to the newest.
|
|
89
121
|
* @returns The text, or undefined when the run is unknown.
|
package/dist/shell/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Shell domain: the local `!` command runner and the bounded blocks it produces. */
|
|
2
|
-
export { ShellController } from './controller.ts';
|
|
3
|
-
export type { ShellBlock, ShellHost } from './controller.ts';
|
|
4
|
-
export { runShell } from './runner.ts';
|
|
2
|
+
export { ShellController, localSourceId } from './controller.ts';
|
|
3
|
+
export type { ShellBlock, ShellHost, ShellSnapshot } from './controller.ts';
|
|
4
|
+
export { runProcess, runShell } from './runner.ts';
|
|
5
5
|
export type { ShellExit, ShellRunOptions, ShellStream } from './runner.ts';
|
package/dist/shell/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** Shell domain: the local `!` command runner and the bounded blocks it produces. */
|
|
2
|
-
export { ShellController } from "./controller.js";
|
|
3
|
-
export { runShell } from "./runner.js";
|
|
2
|
+
export { ShellController, localSourceId } from "./controller.js";
|
|
3
|
+
export { runProcess, runShell } from "./runner.js";
|
package/dist/shell/runner.d.ts
CHANGED
|
@@ -26,3 +26,13 @@ export interface ShellRunOptions {
|
|
|
26
26
|
* @returns How the command ended.
|
|
27
27
|
*/
|
|
28
28
|
export declare function runShell(command: string, options: ShellRunOptions): Promise<ShellExit>;
|
|
29
|
+
/** Run one program with its own arguments, without a shell.
|
|
30
|
+
*
|
|
31
|
+
* A forked verifier is spawned this way: arguments reach the child verbatim, so a prompt can never be
|
|
32
|
+
* re-read as shell syntax.
|
|
33
|
+
* @param file - Program to run.
|
|
34
|
+
* @param args - Arguments, passed verbatim.
|
|
35
|
+
* @param options - Directory, environment, cancellation and the line sink.
|
|
36
|
+
* @returns How the program ended.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runProcess(file: string, args: readonly string[], options: ShellRunOptions): Promise<ShellExit>;
|
package/dist/shell/runner.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
*
|
|
3
3
|
* `!cmd` executes on the machine this client runs on — the operator's laptop or a jump host — never
|
|
4
4
|
* on the host the agent works in. The host's shell belongs to the model's own tools; this module is
|
|
5
|
-
* a separate, local facility, and it is the only place in `src/` allowed to spawn a process
|
|
5
|
+
* a separate, local facility, and it is the only place in `src/` allowed to spawn a process — including
|
|
6
|
+
* the `dsht` child that verifies a scored round in a session of its own.
|
|
6
7
|
*/
|
|
7
8
|
import { spawn } from 'node:child_process';
|
|
8
9
|
/** Longest single output line kept while it is still being assembled. */
|
|
@@ -45,15 +46,55 @@ function signalGroup(child, signal) {
|
|
|
45
46
|
* @returns How the command ended.
|
|
46
47
|
*/
|
|
47
48
|
export function runShell(command, options) {
|
|
49
|
+
return spawnLines(shellPath(), ['-c', command], options);
|
|
50
|
+
}
|
|
51
|
+
/** Run one program with its own arguments, without a shell.
|
|
52
|
+
*
|
|
53
|
+
* A forked verifier is spawned this way: arguments reach the child verbatim, so a prompt can never be
|
|
54
|
+
* re-read as shell syntax.
|
|
55
|
+
* @param file - Program to run.
|
|
56
|
+
* @param args - Arguments, passed verbatim.
|
|
57
|
+
* @param options - Directory, environment, cancellation and the line sink.
|
|
58
|
+
* @returns How the program ended.
|
|
59
|
+
*/
|
|
60
|
+
export function runProcess(file, args, options) {
|
|
61
|
+
return spawnLines(file, [...args], options);
|
|
62
|
+
}
|
|
63
|
+
/** Spawn one program, merge both pipes into a single line stream, and resolve when it closes.
|
|
64
|
+
* @param file - Program to run.
|
|
65
|
+
* @param args - Arguments, passed verbatim.
|
|
66
|
+
* @param options - Directory, environment, cancellation and the line sink.
|
|
67
|
+
* @returns How the program ended.
|
|
68
|
+
*/
|
|
69
|
+
function spawnLines(file, args, options) {
|
|
48
70
|
return new Promise(resolve => {
|
|
49
|
-
|
|
71
|
+
// Nothing to cancel yet: never spawn a process the caller already abandoned.
|
|
72
|
+
if (options.signal.aborted) {
|
|
73
|
+
resolve({ code: null, signal: 'SIGTERM' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const child = spawn(file, [...args], {
|
|
50
77
|
cwd: options.cwd, env: options.env, detached: true, stdio: ['ignore', 'pipe', 'pipe'],
|
|
51
78
|
});
|
|
52
79
|
const carry = { stdout: '', stderr: '' };
|
|
53
80
|
const discarding = { stdout: false, stderr: false };
|
|
54
81
|
let settled = false;
|
|
55
82
|
let graceTimer;
|
|
56
|
-
/** Emit one assembled line,
|
|
83
|
+
/** Emit one assembled line, truncated to the budget.
|
|
84
|
+
*
|
|
85
|
+
* The bound is applied to every emitted line, not only to a partial remainder that overflowed:
|
|
86
|
+
* pipe chunking decides whether one huge line arrives whole or in pieces, so a bound applied only
|
|
87
|
+
* to the remainder would make the client's memory depend on the operating system's read size.
|
|
88
|
+
*/
|
|
89
|
+
const emit = (line, stream) => {
|
|
90
|
+
if (discarding[stream]) {
|
|
91
|
+
discarding[stream] = false;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const clean = line.replace(/\r$/, '');
|
|
95
|
+
options.onLine(clean.length > MAX_LINE_CHARS ? `${clean.slice(0, MAX_LINE_CHARS)}…` : clean, stream);
|
|
96
|
+
};
|
|
97
|
+
/** Split one chunk into lines, keeping the partial remainder for the next chunk. */
|
|
57
98
|
const feed = (stream, chunk) => {
|
|
58
99
|
carry[stream] += chunk;
|
|
59
100
|
for (;;) {
|
|
@@ -62,12 +103,10 @@ export function runShell(command, options) {
|
|
|
62
103
|
break;
|
|
63
104
|
const line = carry[stream].slice(0, newline);
|
|
64
105
|
carry[stream] = carry[stream].slice(newline + 1);
|
|
65
|
-
|
|
66
|
-
discarding[stream] = false;
|
|
67
|
-
else
|
|
68
|
-
options.onLine(line.replace(/\r$/, ''), stream);
|
|
106
|
+
emit(line, stream);
|
|
69
107
|
}
|
|
70
108
|
if (carry[stream].length > MAX_LINE_CHARS) {
|
|
109
|
+
// No newline in sight: emit the head once and drop the rest of this line.
|
|
71
110
|
if (!discarding[stream]) {
|
|
72
111
|
options.onLine(`${carry[stream].slice(0, MAX_LINE_CHARS)}…`, stream);
|
|
73
112
|
discarding[stream] = true;
|
|
@@ -76,8 +115,8 @@ export function runShell(command, options) {
|
|
|
76
115
|
}
|
|
77
116
|
};
|
|
78
117
|
const flush = (stream) => {
|
|
79
|
-
if (carry[stream] !== ''
|
|
80
|
-
|
|
118
|
+
if (carry[stream] !== '')
|
|
119
|
+
emit(carry[stream], stream);
|
|
81
120
|
carry[stream] = '';
|
|
82
121
|
};
|
|
83
122
|
const finish = (exit) => {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Slash-command domain: the command catalog, its syntax and the completion helpers.
|
|
2
|
+
*
|
|
3
|
+
* A pure leaf: it imports nothing from the application, the features or the UI.
|
|
4
|
+
*/
|
|
5
|
+
export { COMMAND_HINTS, COMMAND_LABELS, COMMAND_LABEL_WIDTH, COMMANDS, COMMAND_POLICY, argumentHint, commandMatches, commonPrefix, completeCommand, resolveCommand, suggestedCommands } from './registry.ts';
|
|
6
|
+
export type { CommandHint, CommandPolicy } from './registry.ts';
|
|
7
|
+
export { parseCommand, loopNameQuery, validLoopOption, LOOP_ABORT_USAGE, LOOP_ANSWER_USAGE, LOOP_STOP_USAGE, LOOP_USAGE } from './parse.ts';
|
|
8
|
+
export type { Command, LoopOptions } from './types.ts';
|
|
9
|
+
export { interpret, normalize, authorize } from './pipeline.ts';
|
|
10
|
+
export type { AuthorizeFacts, DeferReason, ExecutableSubmission, InterpretFacts, LineCommand, NormalizeFacts, Submission, UiAction, Verdict } from './pipeline.ts';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Slash-command domain: the command catalog, its syntax and the completion helpers.
|
|
2
|
+
*
|
|
3
|
+
* A pure leaf: it imports nothing from the application, the features or the UI.
|
|
4
|
+
*/
|
|
5
|
+
export { COMMAND_HINTS, COMMAND_LABELS, COMMAND_LABEL_WIDTH, COMMANDS, COMMAND_POLICY, argumentHint, commandMatches, commonPrefix, completeCommand, resolveCommand, suggestedCommands } from "./registry.js";
|
|
6
|
+
export { parseCommand, loopNameQuery, validLoopOption, LOOP_ABORT_USAGE, LOOP_ANSWER_USAGE, LOOP_STOP_USAGE, LOOP_USAGE } from "./parse.js";
|
|
7
|
+
export { interpret, normalize, authorize } from "./pipeline.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Command } from './types.ts';
|
|
2
|
+
export type { Command, LoopOptions } from './types.ts';
|
|
3
|
+
/** The numeric options a flag can carry; `vars` is not one, so it stays out of the flag table. */
|
|
4
|
+
type LoopNumberOption = 'from' | 'to' | 'score' | 'tries';
|
|
5
|
+
/** Whether one numeric value is acceptable for one loop option.
|
|
6
|
+
*
|
|
7
|
+
* The command line and the interactive form share this rule, so a value the form accepts is exactly
|
|
8
|
+
* one the syntax would have accepted if it had been typed.
|
|
9
|
+
* @param key - Option being set.
|
|
10
|
+
* @param value - Candidate number.
|
|
11
|
+
* @returns True when the option may carry that value.
|
|
12
|
+
*/
|
|
13
|
+
export declare function validLoopOption(key: LoopNumberOption, value: number): boolean;
|
|
14
|
+
/** The record name the composer is currently typing after `/loop`, if any.
|
|
15
|
+
*
|
|
16
|
+
* The loop-name menu appears while the line is `/loop` or `/loop <one unfinished token>`; a second
|
|
17
|
+
* token means the operator moved on to the flags, so the menu stays out of the way. A trailing space
|
|
18
|
+
* after a complete name still counts: the menu then confirms that name rather than filtering it out.
|
|
19
|
+
* @param line - Composer draft exactly as typed.
|
|
20
|
+
* @returns The unfinished name (empty when none was started), or undefined for any other line.
|
|
21
|
+
*/
|
|
22
|
+
export declare function loopNameQuery(line: string): string | undefined;
|
|
23
|
+
/** The one message every malformed `/loop` line receives. */
|
|
24
|
+
export declare const LOOP_USAGE = "Use /loop <name> [score] [tries] [--from N] [--to N] [--score X] [--tries N]";
|
|
25
|
+
/** The one message a malformed `/loop stop` line receives. */
|
|
26
|
+
export declare const LOOP_STOP_USAGE = "Use /loop stop (no arguments)";
|
|
27
|
+
/** The one message a `/loop answer` line without an answer receives. */
|
|
28
|
+
export declare const LOOP_ANSWER_USAGE = "Use /loop answer <text>";
|
|
29
|
+
/** The one message a malformed `/loop abort` line receives. */
|
|
30
|
+
export declare const LOOP_ABORT_USAGE = "Use /loop abort (no arguments)";
|
|
31
|
+
/** Parse one composer line into a command without performing any of its effects.
|
|
32
|
+
*
|
|
33
|
+
* The order of the checks is the command precedence: the in-place panels, navigation and removal,
|
|
34
|
+
* then the session and host commands, then a plain prompt. Facts about the current screen or a
|
|
35
|
+
* pending interaction are deliberately absent: they decide whether a command may run, not what it is.
|
|
36
|
+
*
|
|
37
|
+
* A leading token that names exactly one command is resolved first, so `/pro Add tests` runs
|
|
38
|
+
* `/prompt Add tests`; an ambiguous token is left alone and reported with its candidates.
|
|
39
|
+
* @param line - Draft exactly as submitted.
|
|
40
|
+
* @returns The parsed command.
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseCommand(line: string): Command;
|