@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
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/** Workspace/session navigation owns view intent independently of background list refreshes. */
|
|
2
|
+
import { array, object, string } from "../json.js";
|
|
3
|
+
import { sessionLabel } from "../session-title.js";
|
|
4
|
+
import { resolveTarget } from "./navigation.js";
|
|
5
|
+
export class SessionNavigator {
|
|
6
|
+
host;
|
|
7
|
+
active;
|
|
8
|
+
tasks = new Map();
|
|
9
|
+
listsVersion = 0;
|
|
10
|
+
revision = 0;
|
|
11
|
+
constructor(host) {
|
|
12
|
+
this.host = host;
|
|
13
|
+
}
|
|
14
|
+
get intent() { return this.revision; }
|
|
15
|
+
/** Leaving a view invalidates the request that intended to open it. */
|
|
16
|
+
cancel() { this.revision++; this.active?.abort(); }
|
|
17
|
+
reset() { this.revision++; for (const abort of this.tasks.keys())
|
|
18
|
+
abort.abort(); }
|
|
19
|
+
async settle() { await Promise.allSettled(this.tasks.values()); }
|
|
20
|
+
run(caller, navigate, work) {
|
|
21
|
+
if (navigate)
|
|
22
|
+
this.cancel();
|
|
23
|
+
const abort = new AbortController();
|
|
24
|
+
if (navigate)
|
|
25
|
+
this.active = abort;
|
|
26
|
+
const selection = this.host.selection();
|
|
27
|
+
const signal = AbortSignal.any([abort.signal, this.host.signal(), ...(caller ? [caller] : [])]);
|
|
28
|
+
const task = Promise.resolve().then(async () => {
|
|
29
|
+
signal.throwIfAborted();
|
|
30
|
+
const client = this.host.require();
|
|
31
|
+
const check = () => {
|
|
32
|
+
signal.throwIfAborted();
|
|
33
|
+
if (client !== this.host.client() || navigate && selection !== this.host.selection()) {
|
|
34
|
+
throw new Error('Navigation changed while loading');
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
check();
|
|
38
|
+
return work({ client, signal, check });
|
|
39
|
+
}).finally(() => {
|
|
40
|
+
abort.abort();
|
|
41
|
+
this.tasks.delete(abort);
|
|
42
|
+
if (this.active === abort)
|
|
43
|
+
this.active = undefined;
|
|
44
|
+
});
|
|
45
|
+
this.tasks.set(abort, task);
|
|
46
|
+
return task;
|
|
47
|
+
}
|
|
48
|
+
async load(context) {
|
|
49
|
+
const version = ++this.listsVersion;
|
|
50
|
+
const [workspaces, sessions] = await Promise.all([
|
|
51
|
+
context.client.listWorkspaces(context.signal), context.client.listSessions(undefined, context.signal),
|
|
52
|
+
]);
|
|
53
|
+
context.check();
|
|
54
|
+
return { workspaces, sessions, version };
|
|
55
|
+
}
|
|
56
|
+
publishLists(lists) {
|
|
57
|
+
if (lists.version === this.listsVersion)
|
|
58
|
+
this.host.publish({ workspaces: lists.workspaces, sessions: lists.sessions });
|
|
59
|
+
}
|
|
60
|
+
refreshLists() {
|
|
61
|
+
return this.run(undefined, false, async (context) => { this.publishLists(await this.load(context)); });
|
|
62
|
+
}
|
|
63
|
+
showPicker(screen, signal) {
|
|
64
|
+
return this.run(signal, true, async (context) => {
|
|
65
|
+
const lists = await this.load(context);
|
|
66
|
+
this.publishLists(lists);
|
|
67
|
+
context.check();
|
|
68
|
+
this.host.publish({ screen });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
showChat() {
|
|
72
|
+
if (this.host.read().sessionId === undefined)
|
|
73
|
+
return false;
|
|
74
|
+
this.cancel();
|
|
75
|
+
this.host.publish({ screen: 'chat' });
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
enterPath() { this.cancel(); this.host.publish({ screen: 'path' }); }
|
|
79
|
+
commitWorkspace(workspaceId) {
|
|
80
|
+
this.host.leave();
|
|
81
|
+
this.host.publish({ workspaceId, sessionId: undefined, showAllSessions: false, screen: 'sessions' });
|
|
82
|
+
}
|
|
83
|
+
pickWorkspace(workspaceId) { this.cancel(); this.commitWorkspace(workspaceId); }
|
|
84
|
+
switchWorkspace(query, signal) {
|
|
85
|
+
if (!query)
|
|
86
|
+
return this.showPicker('workspaces', signal);
|
|
87
|
+
return this.run(signal, true, async (context) => {
|
|
88
|
+
const lists = await this.load(context);
|
|
89
|
+
const workspace = resolveTarget(lists.workspaces, query, 'workspaceId', row => [string(row.title), string(row.path)]);
|
|
90
|
+
this.publishLists(lists);
|
|
91
|
+
context.check();
|
|
92
|
+
this.commitWorkspace(string(workspace.workspaceId));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
switchSession(query, signal) {
|
|
96
|
+
return this.run(signal, true, async (context) => {
|
|
97
|
+
const lists = await this.load(context);
|
|
98
|
+
if (!query || query === 'all') {
|
|
99
|
+
this.publishLists(lists);
|
|
100
|
+
context.check();
|
|
101
|
+
this.host.publish({ screen: query === 'all' || this.host.read().workspaceId ? 'sessions' : 'workspaces', showAllSessions: query === 'all' });
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const session = resolveTarget(lists.sessions, query, 'sessionId', row => [sessionLabel(row)]);
|
|
105
|
+
this.publishLists(lists);
|
|
106
|
+
context.check();
|
|
107
|
+
this.host.follow(string(session.sessionId));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
createWorkspace(path, signal) {
|
|
112
|
+
return this.run(signal, true, async (context) => {
|
|
113
|
+
const result = object(await context.client.call('workspace/create', { request: { path } }, context.signal));
|
|
114
|
+
context.check();
|
|
115
|
+
const lists = await this.load(context);
|
|
116
|
+
this.publishLists(lists);
|
|
117
|
+
context.check();
|
|
118
|
+
this.commitWorkspace(string(object(result.workspace).workspaceId));
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
createSession(signal) {
|
|
122
|
+
return this.run(signal, true, async (context) => {
|
|
123
|
+
const workspaceId = this.host.read().workspaceId;
|
|
124
|
+
if (!workspaceId)
|
|
125
|
+
throw new Error('Select a workspace before creating a session');
|
|
126
|
+
const result = object(await context.client.call('session/create', { request: { workspaceId } }, context.signal));
|
|
127
|
+
context.check();
|
|
128
|
+
this.host.follow(string(result.sessionId));
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
get visibleSessions() {
|
|
132
|
+
const state = this.host.read();
|
|
133
|
+
const sessions = state.sessions.filter(item => !this.host.client()?.archivedSessionIds.has(string(item.sessionId)));
|
|
134
|
+
if (state.showAllSessions || !state.workspaceId)
|
|
135
|
+
return sessions;
|
|
136
|
+
const workspace = state.workspaces.find(item => item.workspaceId === state.workspaceId);
|
|
137
|
+
const ids = new Set(array(workspace?.sessionIds ?? []).map(string));
|
|
138
|
+
return sessions.filter(item => ids.has(string(item.sessionId)));
|
|
139
|
+
}
|
|
140
|
+
/** Longest registered path wins, with whole-segment matching for nested workspaces. */
|
|
141
|
+
adoptLocalWorkspace(directory) {
|
|
142
|
+
const slashed = (value) => value.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
143
|
+
const target = slashed(directory);
|
|
144
|
+
if (!target)
|
|
145
|
+
return undefined;
|
|
146
|
+
let best;
|
|
147
|
+
for (const workspace of this.host.read().workspaces) {
|
|
148
|
+
const path = slashed(string(workspace.path));
|
|
149
|
+
if (!path || target !== path && !target.startsWith(`${path}/`))
|
|
150
|
+
continue;
|
|
151
|
+
if (!best || path.length > best.length)
|
|
152
|
+
best = { id: string(workspace.workspaceId), length: path.length };
|
|
153
|
+
}
|
|
154
|
+
if (best)
|
|
155
|
+
this.pickWorkspace(best.id);
|
|
156
|
+
return best?.id;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { HostAccess } from '../transport/host.ts';
|
|
2
|
+
import type { ObjectValue } from '../transport/wire.ts';
|
|
3
|
+
import { Transcript } from './transcript.ts';
|
|
4
|
+
/** One followed session's read-only state. */
|
|
5
|
+
export interface PeekState {
|
|
6
|
+
/** The transcript as the host has streamed it. */
|
|
7
|
+
readonly transcript: Transcript;
|
|
8
|
+
/** True once the stream ended, so a view stops calling it live. */
|
|
9
|
+
readonly ended: boolean;
|
|
10
|
+
/** Set when no address form could be followed. */
|
|
11
|
+
readonly error?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Follows one session address form at a time, read-only. */
|
|
14
|
+
export declare class SessionPeek {
|
|
15
|
+
private readonly host;
|
|
16
|
+
private subscription;
|
|
17
|
+
private state;
|
|
18
|
+
/** Address forms left to try, in order; a host may refuse the child form for one delivery mode. */
|
|
19
|
+
private pending;
|
|
20
|
+
/** Bumped on every open and close, so a cancelled stream's late frame cannot revive the state. */
|
|
21
|
+
private generation;
|
|
22
|
+
/** @param host - Transport access this client already holds; the peek borrows it, never owns it. */
|
|
23
|
+
constructor(host: HostAccess);
|
|
24
|
+
/** Start following the first address that answers, releasing whatever was followed before.
|
|
25
|
+
* @param addresses - Address forms to try in order.
|
|
26
|
+
* @param onChange - Called when the transcript or the state moves.
|
|
27
|
+
*/
|
|
28
|
+
open(addresses: readonly ObjectValue[], onChange: () => void): void;
|
|
29
|
+
/** Stop following and release the transcript; safe to call when nothing is open. */
|
|
30
|
+
close(): void;
|
|
31
|
+
/** @returns What a view renders, or undefined when nothing is followed. */
|
|
32
|
+
get snapshot(): PeekState | undefined;
|
|
33
|
+
/** Follow the next address form, or report that none worked.
|
|
34
|
+
* @param generation - The open this attempt belongs to; a superseded one stops silently.
|
|
35
|
+
* @param onChange - Called when the transcript or the state moves.
|
|
36
|
+
*/
|
|
37
|
+
private next;
|
|
38
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { releaseHistoryLayout } from "./history.js";
|
|
2
|
+
import { Transcript } from "./transcript.js";
|
|
3
|
+
/** How much of a followed session the first snapshot asks for; the live tail appends after it. */
|
|
4
|
+
const PEEK_MESSAGES = 120;
|
|
5
|
+
/** Follows one session address form at a time, read-only. */
|
|
6
|
+
export class SessionPeek {
|
|
7
|
+
host;
|
|
8
|
+
subscription;
|
|
9
|
+
state;
|
|
10
|
+
/** Address forms left to try, in order; a host may refuse the child form for one delivery mode. */
|
|
11
|
+
pending = [];
|
|
12
|
+
/** Bumped on every open and close, so a cancelled stream's late frame cannot revive the state. */
|
|
13
|
+
generation = 0;
|
|
14
|
+
/** @param host - Transport access this client already holds; the peek borrows it, never owns it. */
|
|
15
|
+
constructor(host) {
|
|
16
|
+
this.host = host;
|
|
17
|
+
}
|
|
18
|
+
/** Start following the first address that answers, releasing whatever was followed before.
|
|
19
|
+
* @param addresses - Address forms to try in order.
|
|
20
|
+
* @param onChange - Called when the transcript or the state moves.
|
|
21
|
+
*/
|
|
22
|
+
open(addresses, onChange) {
|
|
23
|
+
this.close();
|
|
24
|
+
// `close` bumped the generation; this open owns the next one and every callback checks it.
|
|
25
|
+
const generation = ++this.generation;
|
|
26
|
+
this.pending = [...addresses];
|
|
27
|
+
this.next(generation, onChange);
|
|
28
|
+
}
|
|
29
|
+
/** Stop following and release the transcript; safe to call when nothing is open. */
|
|
30
|
+
close() {
|
|
31
|
+
this.generation += 1;
|
|
32
|
+
this.subscription?.cancel();
|
|
33
|
+
this.subscription = undefined;
|
|
34
|
+
this.pending = [];
|
|
35
|
+
if (this.state === undefined)
|
|
36
|
+
return;
|
|
37
|
+
releaseHistoryLayout(this.state.transcript);
|
|
38
|
+
this.state.transcript.dispose();
|
|
39
|
+
this.state = undefined;
|
|
40
|
+
}
|
|
41
|
+
/** @returns What a view renders, or undefined when nothing is followed. */
|
|
42
|
+
get snapshot() { return this.state; }
|
|
43
|
+
/** Follow the next address form, or report that none worked.
|
|
44
|
+
* @param generation - The open this attempt belongs to; a superseded one stops silently.
|
|
45
|
+
* @param onChange - Called when the transcript or the state moves.
|
|
46
|
+
*/
|
|
47
|
+
next(generation, onChange) {
|
|
48
|
+
if (generation !== this.generation)
|
|
49
|
+
return;
|
|
50
|
+
const address = this.pending.shift();
|
|
51
|
+
if (address === undefined) {
|
|
52
|
+
this.state = { transcript: new Transcript(), ended: true, error: 'the host refused every address form' };
|
|
53
|
+
onChange();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const transcript = new Transcript();
|
|
57
|
+
this.state = { transcript, ended: false };
|
|
58
|
+
let attempted = false;
|
|
59
|
+
let subscription;
|
|
60
|
+
try {
|
|
61
|
+
subscription = this.host.require().subscribe('session/follow', {
|
|
62
|
+
request: { address, maxMessages: PEEK_MESSAGES, assistantStream: true },
|
|
63
|
+
}, {
|
|
64
|
+
item: value => {
|
|
65
|
+
if (generation !== this.generation)
|
|
66
|
+
return;
|
|
67
|
+
// A peek is a diagnostic surface: a malformed frame ends it with a reason rather than taking
|
|
68
|
+
// the whole client down the way a selected session's stream would.
|
|
69
|
+
try {
|
|
70
|
+
transcript.accept(value);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
this.state = { transcript, ended: true, error: 'the session stream sent a frame this client cannot read' };
|
|
74
|
+
}
|
|
75
|
+
onChange();
|
|
76
|
+
},
|
|
77
|
+
end: error => {
|
|
78
|
+
// A cancelled stream still reports its end; only the live attempt may act on it.
|
|
79
|
+
if (attempted || generation !== this.generation)
|
|
80
|
+
return;
|
|
81
|
+
attempted = true;
|
|
82
|
+
if (error !== undefined && this.pending.length > 0) {
|
|
83
|
+
this.next(generation, onChange);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.state = { transcript, ended: true, ...(error === undefined ? {} : { error: errorTextOf(error) }) };
|
|
87
|
+
onChange();
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
// Offline, or between connection generations: the view says so instead of failing the caller.
|
|
93
|
+
this.state = { transcript, ended: true, error: errorTextOf(error) };
|
|
94
|
+
onChange();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.subscription = subscription;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** One error as displayable text, without importing the whole wire module. */
|
|
101
|
+
function errorTextOf(error) {
|
|
102
|
+
return error instanceof Error ? error.message : String(error);
|
|
103
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { HostAccess } from '../transport/host.ts';
|
|
2
|
+
import { Transcript } from './transcript.ts';
|
|
3
|
+
import type { PromptCache, PromptIndex } from './info.ts';
|
|
4
|
+
interface Selection {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
revision: number;
|
|
7
|
+
record: Transcript;
|
|
8
|
+
prompts: PromptIndex;
|
|
9
|
+
}
|
|
10
|
+
interface BackfillHost extends Pick<HostAccess, 'require' | 'signal' | 'online'> {
|
|
11
|
+
current(selection: Selection): boolean;
|
|
12
|
+
changed(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare class PromptBackfill {
|
|
15
|
+
private readonly host;
|
|
16
|
+
private readonly cache;
|
|
17
|
+
private readonly tasks;
|
|
18
|
+
constructor(host: BackfillHost, cache: PromptCache);
|
|
19
|
+
cancel(): void;
|
|
20
|
+
settle(): Promise<void>;
|
|
21
|
+
start(selection: Selection): void;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Bounded background recall indexing, owned by one selected record and connection lifetime. */
|
|
2
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
+
import { object } from "../json.js";
|
|
4
|
+
import { Transcript } from "./transcript.js";
|
|
5
|
+
const PAGE_LIMIT = 200;
|
|
6
|
+
export class PromptBackfill {
|
|
7
|
+
host;
|
|
8
|
+
cache;
|
|
9
|
+
tasks = new Set();
|
|
10
|
+
constructor(host, cache) {
|
|
11
|
+
this.host = host;
|
|
12
|
+
this.cache = cache;
|
|
13
|
+
}
|
|
14
|
+
cancel() { for (const task of this.tasks)
|
|
15
|
+
task.abort.abort(); }
|
|
16
|
+
async settle() { await Promise.allSettled([...this.tasks].map(task => task.promise)); }
|
|
17
|
+
start(selection) {
|
|
18
|
+
this.cancel();
|
|
19
|
+
const abort = new AbortController();
|
|
20
|
+
const signal = AbortSignal.any([abort.signal, this.host.signal()]);
|
|
21
|
+
const task = { abort, promise: Promise.resolve() };
|
|
22
|
+
// Register before dispatch, including before any reentrant host callback.
|
|
23
|
+
this.tasks.add(task);
|
|
24
|
+
task.promise = Promise.resolve().then(async () => {
|
|
25
|
+
const check = () => {
|
|
26
|
+
signal.throwIfAborted();
|
|
27
|
+
if (!this.host.online() || !this.host.current(selection))
|
|
28
|
+
throw new Error('Session changed while indexing prompts');
|
|
29
|
+
};
|
|
30
|
+
check();
|
|
31
|
+
const client = this.host.require();
|
|
32
|
+
const deadline = Date.now() + client.timeoutMs;
|
|
33
|
+
const { record, prompts, sessionId } = selection;
|
|
34
|
+
while (!record.ready) {
|
|
35
|
+
check();
|
|
36
|
+
if (Date.now() >= deadline)
|
|
37
|
+
throw new Error('Session snapshot timed out');
|
|
38
|
+
await delay(20, undefined, { signal });
|
|
39
|
+
}
|
|
40
|
+
check();
|
|
41
|
+
const adopt = () => {
|
|
42
|
+
const cached = this.cache.get(sessionId);
|
|
43
|
+
if (!cached?.complete)
|
|
44
|
+
return false;
|
|
45
|
+
prompts.prepend(cached.prompts);
|
|
46
|
+
prompts.settle();
|
|
47
|
+
prompts.markComplete();
|
|
48
|
+
this.host.changed();
|
|
49
|
+
return true;
|
|
50
|
+
};
|
|
51
|
+
if (adopt())
|
|
52
|
+
return;
|
|
53
|
+
const throughSeq = record.readThrough;
|
|
54
|
+
let beforeSeq = record.beforeSeq, hasMore = record.hasMore;
|
|
55
|
+
for (let page = 0; hasMore && beforeSeq !== undefined && page < PAGE_LIMIT; page++) {
|
|
56
|
+
check();
|
|
57
|
+
const result = object(await client.call('session/page', { request: {
|
|
58
|
+
address: { kind: 'session', sessionId }, throughSeq, beforeSeq, maxMessages: 80,
|
|
59
|
+
} }, signal));
|
|
60
|
+
check();
|
|
61
|
+
const temporary = new Transcript();
|
|
62
|
+
try {
|
|
63
|
+
temporary.accept({ type: 'snapshot', cursor: throughSeq, assistantStream: { revision: 0 }, records: result.records, hasMore: result.hasMore });
|
|
64
|
+
const next = temporary.beforeSeq;
|
|
65
|
+
if (temporary.hasMore && (next === undefined || next >= beforeSeq))
|
|
66
|
+
throw new Error('Host history page did not advance');
|
|
67
|
+
prompts.prepend(temporary.promptsSince(-1).prompts);
|
|
68
|
+
// Bound each admitted page, even when the next request fails or never returns.
|
|
69
|
+
prompts.settle();
|
|
70
|
+
beforeSeq = next;
|
|
71
|
+
hasMore = temporary.hasMore;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
temporary.dispose();
|
|
75
|
+
}
|
|
76
|
+
if (adopt())
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (!hasMore) {
|
|
80
|
+
prompts.markComplete();
|
|
81
|
+
this.cache.put(sessionId, { prompts: prompts.durableItems, complete: prompts.exhausted });
|
|
82
|
+
}
|
|
83
|
+
this.host.changed();
|
|
84
|
+
}).catch(() => {
|
|
85
|
+
// Cancellation or unavailable history leaves older prompts reachable through lazy paging.
|
|
86
|
+
}).finally(() => { this.tasks.delete(task); });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -1,23 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
path: string;
|
|
4
|
-
kind: 'file' | 'directory';
|
|
5
|
-
}
|
|
6
|
-
/** Find the unfinished reference at the end of the draft; email addresses do not trigger it.
|
|
7
|
-
* @param text - Complete composer draft.
|
|
8
|
-
* @returns The replaceable token and host query, or undefined outside a reference.
|
|
9
|
-
*/
|
|
10
|
-
export declare function activeReference(text: string): {
|
|
11
|
-
prefix: string;
|
|
12
|
-
query: string;
|
|
13
|
-
quoted: boolean;
|
|
14
|
-
} | undefined;
|
|
15
|
-
/** Encode a candidate in Harness prompt syntax; directories keep completion open.
|
|
16
|
-
* @param candidate - Remote path and entry kind.
|
|
17
|
-
* @param quoted - Preserve an explicitly opened quote.
|
|
18
|
-
* @returns Mention text, or undefined for paths the mention grammar cannot encode.
|
|
19
|
-
*/
|
|
20
|
-
export declare function fileMention(candidate: FileReference, quoted?: boolean): string | undefined;
|
|
1
|
+
import { type FileReference } from '../references.ts';
|
|
2
|
+
export type { FileReference } from '../references.ts';
|
|
21
3
|
/** Validate remote candidates and omit paths that cannot be inserted safely.
|
|
22
4
|
* @param value - Decoded fileReferences/list result.
|
|
23
5
|
* @returns Ordered file and directory candidates.
|
|
@@ -1,31 +1,6 @@
|
|
|
1
1
|
/** Independent adapter for Harness path-only @ references; no file bytes are read. */
|
|
2
2
|
import { array, object, string } from "../transport/wire.js";
|
|
3
|
-
|
|
4
|
-
* @param text - Complete composer draft.
|
|
5
|
-
* @returns The replaceable token and host query, or undefined outside a reference.
|
|
6
|
-
*/
|
|
7
|
-
export function activeReference(text) {
|
|
8
|
-
const quoted = /(?:^|\s)(@"([^"]*))$/u.exec(text);
|
|
9
|
-
if (quoted)
|
|
10
|
-
return { prefix: quoted[1], query: quoted[2], quoted: true };
|
|
11
|
-
const plain = /(?:^|\s)(@([^\s"]*))$/u.exec(text);
|
|
12
|
-
if (plain)
|
|
13
|
-
return { prefix: plain[1], query: plain[2], quoted: false };
|
|
14
|
-
return undefined;
|
|
15
|
-
}
|
|
16
|
-
/** Encode a candidate in Harness prompt syntax; directories keep completion open.
|
|
17
|
-
* @param candidate - Remote path and entry kind.
|
|
18
|
-
* @param quoted - Preserve an explicitly opened quote.
|
|
19
|
-
* @returns Mention text, or undefined for paths the mention grammar cannot encode.
|
|
20
|
-
*/
|
|
21
|
-
export function fileMention(candidate, quoted = false) {
|
|
22
|
-
const path = candidate.path + (candidate.kind === 'directory' ? '/' : '');
|
|
23
|
-
if (/[\u0000-\u001f\u007f-\u009f"]/u.test(path))
|
|
24
|
-
return undefined;
|
|
25
|
-
if (!quoted && !/\s/u.test(path))
|
|
26
|
-
return `@${path}`;
|
|
27
|
-
return `@"${path}${candidate.kind === 'file' ? '"' : ''}`;
|
|
28
|
-
}
|
|
3
|
+
import { fileMention } from "../references.js";
|
|
29
4
|
/** Validate remote candidates and omit paths that cannot be inserted safely.
|
|
30
5
|
* @param value - Decoded fileReferences/list result.
|
|
31
6
|
* @returns Ordered file and directory candidates.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Per-session host runtime mirrors that are not connection state.
|
|
2
|
+
*
|
|
3
|
+
* `running` and the observation start change while the network stays perfectly healthy
|
|
4
|
+
* (`ready → running → waiting → running`), so they belong to the session runtime rather than to the
|
|
5
|
+
* connection. Both maps are keyed by `sessionId` and cleared at each connection generation.
|
|
6
|
+
*/
|
|
7
|
+
import type { ControlFrame } from '../transport/events.ts';
|
|
8
|
+
import { Telemetry } from './telemetry.ts';
|
|
9
|
+
export declare class SessionRuntime {
|
|
10
|
+
/** Host projection values for every session of the current generation. */
|
|
11
|
+
telemetry: Telemetry;
|
|
12
|
+
private readonly running;
|
|
13
|
+
private readonly observed;
|
|
14
|
+
/** Cached host running flag for one session, or undefined when never reported. */
|
|
15
|
+
runningFor(sessionId: string): boolean | undefined;
|
|
16
|
+
/** When this client first observed the session running, for the elapsed-time fallback. */
|
|
17
|
+
observedAt(sessionId: string): number | undefined;
|
|
18
|
+
/** Remember when a session this client just opened was first seen. */
|
|
19
|
+
observe(sessionId: string): void;
|
|
20
|
+
/** Apply one host running-state notification for a session. */
|
|
21
|
+
accept(sessionId: string, running: boolean): void;
|
|
22
|
+
/** Apply one normalized projection/queue/job frame. */
|
|
23
|
+
acceptControl(frame: ControlFrame): void;
|
|
24
|
+
/** Drop every generation-scoped mirror, including the projection store. */
|
|
25
|
+
reset(): void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Telemetry } from "./telemetry.js";
|
|
2
|
+
/** Projection capabilities this client consumes; the host retains every other key. */
|
|
3
|
+
const RETAINED_PROJECTIONS = new Set(['title', 'modelSelection', 'contextPressure', 'tokenUsage', 'sessionStats', 'agentPreset']);
|
|
4
|
+
export class SessionRuntime {
|
|
5
|
+
/** Host projection values for every session of the current generation. */
|
|
6
|
+
telemetry = new Telemetry(RETAINED_PROJECTIONS);
|
|
7
|
+
running = new Map();
|
|
8
|
+
observed = new Map();
|
|
9
|
+
/** Cached host running flag for one session, or undefined when never reported. */
|
|
10
|
+
runningFor(sessionId) { return this.running.get(sessionId); }
|
|
11
|
+
/** When this client first observed the session running, for the elapsed-time fallback. */
|
|
12
|
+
observedAt(sessionId) { return this.observed.get(sessionId); }
|
|
13
|
+
/** Remember when a session this client just opened was first seen. */
|
|
14
|
+
observe(sessionId) { if (!this.observed.has(sessionId))
|
|
15
|
+
this.observed.set(sessionId, Date.now()); }
|
|
16
|
+
/** Apply one host running-state notification for a session. */
|
|
17
|
+
accept(sessionId, running) {
|
|
18
|
+
if (running && !this.running.get(sessionId))
|
|
19
|
+
this.observed.set(sessionId, Date.now());
|
|
20
|
+
if (!running)
|
|
21
|
+
this.observed.delete(sessionId);
|
|
22
|
+
this.running.set(sessionId, running);
|
|
23
|
+
}
|
|
24
|
+
/** Apply one normalized projection/queue/job frame. */
|
|
25
|
+
acceptControl(frame) { this.telemetry.accept(frame); }
|
|
26
|
+
/** Drop every generation-scoped mirror, including the projection store. */
|
|
27
|
+
reset() { this.running.clear(); this.observed.clear(); this.telemetry = new Telemetry(RETAINED_PROJECTIONS); }
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** State and publication capabilities owned by the session domain. */
|
|
2
|
+
import type { NavigationState } from './navigator.ts';
|
|
3
|
+
import type { SessionInfo } from './info.ts';
|
|
4
|
+
import type { PendingInteraction } from './types.ts';
|
|
5
|
+
export interface SessionState extends NavigationState {
|
|
6
|
+
online: boolean;
|
|
7
|
+
status: string;
|
|
8
|
+
lastFailure: string;
|
|
9
|
+
pending: PendingInteraction[];
|
|
10
|
+
session: SessionInfo;
|
|
11
|
+
}
|
|
12
|
+
/** The connection and application derive online/pending; session actions cannot overwrite them. */
|
|
13
|
+
export type SessionUpdate = Partial<Omit<SessionState, 'online' | 'pending' | 'session'>>;
|
|
14
|
+
export interface SessionStore {
|
|
15
|
+
readonly state: Readonly<SessionState>;
|
|
16
|
+
update(patch: SessionUpdate): void;
|
|
17
|
+
selection(): number;
|
|
18
|
+
bumpSelection(): void;
|
|
19
|
+
busy(): boolean;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,42 +1,50 @@
|
|
|
1
|
-
/** Host projection values with per-key watermarks; snapshots never roll back newer updates.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
/** Host projection values with per-key watermarks; snapshots never roll back newer updates.
|
|
2
|
+
*
|
|
3
|
+
* The wire shape is decoded by `transport/events.ts`; this class only applies semantic control frames
|
|
4
|
+
* to its own maps, so a host field rename never reaches the session domain.
|
|
5
|
+
*/
|
|
6
|
+
import type { ControlFrame, ProjectionSnapshot, ProjectionValue, QueuedInput } from '../transport/events.ts';
|
|
7
|
+
export type { QueuedInput } from '../transport/events.ts';
|
|
8
|
+
export interface TelemetrySnapshot {
|
|
9
|
+
readonly values: Readonly<Record<string, ProjectionValue>>;
|
|
10
|
+
readonly queued?: number;
|
|
11
|
+
readonly jobs?: number;
|
|
12
|
+
}
|
|
13
|
+
/** The public reader has no projection acceptance or baseline replacement capability. */
|
|
14
|
+
export interface TelemetryReader {
|
|
15
|
+
readonly ready: boolean;
|
|
16
|
+
view(id?: string): TelemetrySnapshot;
|
|
17
|
+
pending(id?: string): readonly Readonly<QueuedInput>[];
|
|
8
18
|
}
|
|
9
19
|
/** Generation-local projection, inbox and background-job data for every session. */
|
|
10
20
|
export declare class Telemetry {
|
|
11
21
|
private readonly retainedKeys?;
|
|
12
22
|
private entries;
|
|
13
23
|
private queues;
|
|
24
|
+
private readonly views;
|
|
25
|
+
readonly reader: TelemetryReader;
|
|
14
26
|
private jobs;
|
|
15
27
|
ready: boolean;
|
|
16
28
|
/** Optionally retain only projection capabilities consumed by this client. */
|
|
17
29
|
constructor(retainedKeys?: ReadonlySet<string> | undefined);
|
|
18
30
|
/** Replace all control state on a new stream baseline, then accept replacement frames.
|
|
19
|
-
* @param
|
|
31
|
+
* @param frame - One normalized session/control frame.
|
|
20
32
|
*/
|
|
21
|
-
accept(
|
|
33
|
+
accept(frame: ControlFrame): void;
|
|
22
34
|
/** Merge a complete follow snapshot without restoring absent or older projection values.
|
|
23
35
|
* @param id - Session identity.
|
|
24
|
-
* @param
|
|
36
|
+
* @param baseline - Projection baseline, when supplied by the host.
|
|
25
37
|
*/
|
|
26
|
-
snapshot(id: string,
|
|
38
|
+
snapshot(id: string, baseline: ProjectionSnapshot | undefined): void;
|
|
27
39
|
/** Read current values for one session; missing capabilities remain absent.
|
|
28
40
|
* @param id - Selected session identity, if any.
|
|
29
41
|
* @returns Projection values and known queue/job counts.
|
|
30
42
|
*/
|
|
31
|
-
view(id?: string):
|
|
32
|
-
values: ObjectValue;
|
|
33
|
-
queued?: number;
|
|
34
|
-
jobs?: number;
|
|
35
|
-
};
|
|
43
|
+
view(id?: string): TelemetrySnapshot;
|
|
36
44
|
/** Read the selected session's authoritative pending inputs; absence means no observed queue.
|
|
37
45
|
* @param id - Session identity.
|
|
38
46
|
* @returns Pending inputs in host order.
|
|
39
47
|
*/
|
|
40
|
-
pending(id?: string): readonly QueuedInput[];
|
|
48
|
+
pending(id?: string): readonly Readonly<QueuedInput>[];
|
|
41
49
|
private entry;
|
|
42
50
|
}
|