@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,26 @@
|
|
|
1
|
+
/** Retained host requests and one response owner per interaction instance. */
|
|
2
|
+
import type { HostEvent } from '../transport/events.ts';
|
|
3
|
+
import type { Json } from '../json.ts';
|
|
4
|
+
import type { AnswerValue, PendingInteraction } from './types.ts';
|
|
5
|
+
export interface InteractionHost {
|
|
6
|
+
focused(): PendingInteraction | undefined;
|
|
7
|
+
admit(sessionId: string, dispatch: () => Promise<void>): Promise<void>;
|
|
8
|
+
reply(eventId: string, outcome: Json): Promise<void>;
|
|
9
|
+
changed(): void;
|
|
10
|
+
}
|
|
11
|
+
export declare class SessionInteractions {
|
|
12
|
+
private readonly host;
|
|
13
|
+
private readonly entries;
|
|
14
|
+
private readonly responding;
|
|
15
|
+
constructor(host: InteractionHost);
|
|
16
|
+
clear(): void;
|
|
17
|
+
settle(): Promise<void>;
|
|
18
|
+
pendingFor(sessionId: string | undefined): PendingInteraction[];
|
|
19
|
+
counts(): ReadonlyMap<string, number>;
|
|
20
|
+
accept(event: HostEvent): boolean;
|
|
21
|
+
cancelled(eventId: string): void;
|
|
22
|
+
answer(value: AnswerValue): Promise<void>;
|
|
23
|
+
approve(allowed: boolean): Promise<void>;
|
|
24
|
+
dismissQuestion(): Promise<void>;
|
|
25
|
+
private respond;
|
|
26
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export class SessionInteractions {
|
|
2
|
+
host;
|
|
3
|
+
entries = new Map();
|
|
4
|
+
responding = new Map();
|
|
5
|
+
constructor(host) {
|
|
6
|
+
this.host = host;
|
|
7
|
+
}
|
|
8
|
+
clear() { this.entries.clear(); }
|
|
9
|
+
async settle() { await Promise.allSettled(this.responding.values()); }
|
|
10
|
+
pendingFor(sessionId) {
|
|
11
|
+
return [...this.entries.values()].filter(frame => frame.sessionId === sessionId);
|
|
12
|
+
}
|
|
13
|
+
counts() {
|
|
14
|
+
const counts = new Map();
|
|
15
|
+
for (const frame of this.entries.values()) {
|
|
16
|
+
if (frame.sessionId !== '')
|
|
17
|
+
counts.set(frame.sessionId, (counts.get(frame.sessionId) ?? 0) + 1);
|
|
18
|
+
}
|
|
19
|
+
return counts;
|
|
20
|
+
}
|
|
21
|
+
accept(event) {
|
|
22
|
+
if (event.kind === 'approval-request') {
|
|
23
|
+
this.entries.set(event.eventId, { kind: 'approval', eventId: event.eventId, sessionId: event.sessionId, description: event.description });
|
|
24
|
+
}
|
|
25
|
+
else if (event.kind === 'question-request') {
|
|
26
|
+
this.entries.set(event.eventId, { kind: 'question', eventId: event.eventId, sessionId: event.sessionId, questions: event.questions });
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
return false;
|
|
30
|
+
this.host.changed();
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
cancelled(eventId) { this.entries.delete(eventId); this.host.changed(); }
|
|
34
|
+
async answer(value) {
|
|
35
|
+
const pending = this.host.focused();
|
|
36
|
+
if (pending?.kind !== 'question')
|
|
37
|
+
throw new Error('No pending question');
|
|
38
|
+
await this.respond(pending, { kind: 'result', value });
|
|
39
|
+
}
|
|
40
|
+
async approve(allowed) {
|
|
41
|
+
const pending = this.host.focused();
|
|
42
|
+
if (pending?.kind !== 'approval')
|
|
43
|
+
throw new Error('No pending approval');
|
|
44
|
+
await this.respond(pending, { kind: 'result', value: allowed ? 'allowed-once' : 'rejected' });
|
|
45
|
+
}
|
|
46
|
+
async dismissQuestion() {
|
|
47
|
+
const pending = this.host.focused();
|
|
48
|
+
if (pending?.kind !== 'question')
|
|
49
|
+
throw new Error('No pending question');
|
|
50
|
+
await this.respond(pending, { kind: 'rejected', error: {
|
|
51
|
+
name: 'UserQuestionError', message: 'the user cancelled ask_user_question', code: 'ASK_CANCELLED',
|
|
52
|
+
} });
|
|
53
|
+
}
|
|
54
|
+
respond(pending, outcome) {
|
|
55
|
+
const existing = this.responding.get(pending);
|
|
56
|
+
if (existing)
|
|
57
|
+
return existing;
|
|
58
|
+
// Reserve synchronously; the next Enter joins this response instead of issuing a conflicting one.
|
|
59
|
+
const response = Promise.resolve().then(() => this.host.admit(pending.sessionId, () => {
|
|
60
|
+
// Admission may have waited. Cancellation or a replayed request invalidates the captured object.
|
|
61
|
+
if (this.entries.get(pending.eventId) !== pending)
|
|
62
|
+
throw new Error('Interaction is no longer pending');
|
|
63
|
+
return this.host.reply(pending.eventId, outcome);
|
|
64
|
+
})).then(() => {
|
|
65
|
+
if (this.entries.get(pending.eventId) === pending) {
|
|
66
|
+
this.entries.delete(pending.eventId);
|
|
67
|
+
this.host.changed();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
this.responding.set(pending, response);
|
|
71
|
+
const release = () => { this.responding.delete(pending); };
|
|
72
|
+
void response.then(release, release);
|
|
73
|
+
return response;
|
|
74
|
+
}
|
|
75
|
+
}
|
package/dist/session/markdown.js
CHANGED
|
@@ -5,7 +5,7 @@ import stringWidth from 'string-width';
|
|
|
5
5
|
import wrapAnsi from 'wrap-ansi';
|
|
6
6
|
import { stripVTControlCharacters } from 'node:util';
|
|
7
7
|
import { decodeHTML } from 'entities';
|
|
8
|
-
import { safeText } from "../
|
|
8
|
+
import { safeText } from "../text.js";
|
|
9
9
|
import { renderMath } from "./math.js";
|
|
10
10
|
const parser = new Marked({ gfm: true });
|
|
11
11
|
// Math is tokenized before Markdown escapes, but after fenced and inline code have claimed their source.
|
package/dist/session/math.js
CHANGED
|
@@ -8,7 +8,7 @@ import { TextNode } from '@mathjax/src/js/core/MmlTree/MmlNode.js';
|
|
|
8
8
|
import { SerializedMmlVisitor } from '@mathjax/src/js/core/MmlTree/SerializedMmlVisitor.js';
|
|
9
9
|
import '@mathjax/src/js/input/tex/base/BaseConfiguration.js';
|
|
10
10
|
import '@mathjax/src/js/input/tex/ams/AmsConfiguration.js';
|
|
11
|
-
import { safeText } from "../
|
|
11
|
+
import { safeText } from "../text.js";
|
|
12
12
|
RegisterHTMLHandler(liteAdaptor());
|
|
13
13
|
const superscripts = Object.fromEntries([...('0123456789+-=()ni')].map((c, i) => [c, [...'⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾ⁿⁱ'][i]]));
|
|
14
14
|
const subscripts = Object.fromEntries([...'0123456789+-=()aehijklmnoprstuvx'].map((c, i) => [c, [...'₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓ'][i]]));
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** One session's mutation admission order, with a control lane that overtakes the normal waiters.
|
|
2
|
+
*
|
|
3
|
+
* The gate serializes the **admission** of a mutation — checking state, deciding, and issuing the
|
|
4
|
+
* request — not the remote mutation itself. A section therefore returns as soon as the request is
|
|
5
|
+
* issued, and the caller awaits the host's answer outside the gate: a `/compact` can take minutes, and
|
|
6
|
+
* holding the gate for it would make `cancel` wait for exactly the operation it exists to interrupt.
|
|
7
|
+
* The `return dispatch()` below is what enforces that: the gate releases when the section *returns*,
|
|
8
|
+
* so even an `async` section holds it only up to its first `await`.
|
|
9
|
+
*
|
|
10
|
+
* Two lanes:
|
|
11
|
+
*
|
|
12
|
+
* * `normal` — a prompt, steering, a host command, an answer, a queue removal: arrival order;
|
|
13
|
+
* * `control` — `cancelTurn`/`interrupt` and `/loop stop`: waits only for the section already running,
|
|
14
|
+
* so it is admitted ahead of every normal admission that is still waiting.
|
|
15
|
+
*
|
|
16
|
+
* The key is the **target** session, not the selected one: cancelling a forked verifier addresses the
|
|
17
|
+
* verifier's own session, and that must not queue behind the reviewed session's writes.
|
|
18
|
+
*/
|
|
19
|
+
/** Which queue one mutation admission joins. */
|
|
20
|
+
export type MutationLane = 'normal' | 'control';
|
|
21
|
+
/** One admission, as the gate reports it so the trace can answer "who dispatched first". */
|
|
22
|
+
export interface MutationAdmission {
|
|
23
|
+
/** Session the mutation targets; never inferred from the selection. */
|
|
24
|
+
readonly sessionId: string;
|
|
25
|
+
/** Lane the admission used. */
|
|
26
|
+
readonly lane: MutationLane;
|
|
27
|
+
/** Whether another admission already held this session's gate when this one arrived. */
|
|
28
|
+
readonly waited: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** Serializes one session's mutation admissions. */
|
|
31
|
+
export declare class SessionMutationGate {
|
|
32
|
+
private readonly report?;
|
|
33
|
+
private readonly lanes;
|
|
34
|
+
/** @param report - Optional observer, called once per admission in dispatch order. */
|
|
35
|
+
constructor(report?: ((admission: MutationAdmission) => void) | undefined);
|
|
36
|
+
/** Wait for this session's turn, then run one section inside the gate.
|
|
37
|
+
*
|
|
38
|
+
* `dispatch` must decide and **issue**: return the promise the caller will await, rather than
|
|
39
|
+
* awaiting it here, or the gate stays held for the whole remote mutation.
|
|
40
|
+
* @param sessionId - Target session the mutation belongs to.
|
|
41
|
+
* @param lane - `control` overtakes waiting normal admissions; `normal` keeps arrival order.
|
|
42
|
+
* @param dispatch - Synchronous decision plus request issue.
|
|
43
|
+
* @returns What `dispatch` returned; a returned promise is adopted, so the caller may await the
|
|
44
|
+
* host's answer without holding the gate.
|
|
45
|
+
*/
|
|
46
|
+
admit<T>(sessionId: string, lane: MutationLane, dispatch: () => T): Promise<T>;
|
|
47
|
+
/** Take the gate now, or queue for it behind the running section. */
|
|
48
|
+
private acquire;
|
|
49
|
+
/** Hand the gate to the next waiter, control first, or free it. */
|
|
50
|
+
private release;
|
|
51
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** One session's mutation admission order, with a control lane that overtakes the normal waiters.
|
|
2
|
+
*
|
|
3
|
+
* The gate serializes the **admission** of a mutation — checking state, deciding, and issuing the
|
|
4
|
+
* request — not the remote mutation itself. A section therefore returns as soon as the request is
|
|
5
|
+
* issued, and the caller awaits the host's answer outside the gate: a `/compact` can take minutes, and
|
|
6
|
+
* holding the gate for it would make `cancel` wait for exactly the operation it exists to interrupt.
|
|
7
|
+
* The `return dispatch()` below is what enforces that: the gate releases when the section *returns*,
|
|
8
|
+
* so even an `async` section holds it only up to its first `await`.
|
|
9
|
+
*
|
|
10
|
+
* Two lanes:
|
|
11
|
+
*
|
|
12
|
+
* * `normal` — a prompt, steering, a host command, an answer, a queue removal: arrival order;
|
|
13
|
+
* * `control` — `cancelTurn`/`interrupt` and `/loop stop`: waits only for the section already running,
|
|
14
|
+
* so it is admitted ahead of every normal admission that is still waiting.
|
|
15
|
+
*
|
|
16
|
+
* The key is the **target** session, not the selected one: cancelling a forked verifier addresses the
|
|
17
|
+
* verifier's own session, and that must not queue behind the reviewed session's writes.
|
|
18
|
+
*/
|
|
19
|
+
/** Serializes one session's mutation admissions. */
|
|
20
|
+
export class SessionMutationGate {
|
|
21
|
+
report;
|
|
22
|
+
lanes = new Map();
|
|
23
|
+
/** @param report - Optional observer, called once per admission in dispatch order. */
|
|
24
|
+
constructor(report) {
|
|
25
|
+
this.report = report;
|
|
26
|
+
}
|
|
27
|
+
/** Wait for this session's turn, then run one section inside the gate.
|
|
28
|
+
*
|
|
29
|
+
* `dispatch` must decide and **issue**: return the promise the caller will await, rather than
|
|
30
|
+
* awaiting it here, or the gate stays held for the whole remote mutation.
|
|
31
|
+
* @param sessionId - Target session the mutation belongs to.
|
|
32
|
+
* @param lane - `control` overtakes waiting normal admissions; `normal` keeps arrival order.
|
|
33
|
+
* @param dispatch - Synchronous decision plus request issue.
|
|
34
|
+
* @returns What `dispatch` returned; a returned promise is adopted, so the caller may await the
|
|
35
|
+
* host's answer without holding the gate.
|
|
36
|
+
*/
|
|
37
|
+
async admit(sessionId, lane, dispatch) {
|
|
38
|
+
const state = this.lanes.get(sessionId) ?? { busy: false, control: [], normal: [] };
|
|
39
|
+
this.lanes.set(sessionId, state);
|
|
40
|
+
const waited = state.busy;
|
|
41
|
+
await this.acquire(state, lane);
|
|
42
|
+
this.report?.({ sessionId, lane, waited });
|
|
43
|
+
try {
|
|
44
|
+
return dispatch();
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
this.release(sessionId, state);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Take the gate now, or queue for it behind the running section. */
|
|
51
|
+
async acquire(state, lane) {
|
|
52
|
+
if (!state.busy) {
|
|
53
|
+
state.busy = true;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// The waiter that is woken inherits the gate: `busy` stays true across the handoff, so an
|
|
57
|
+
// admission arriving during it queues instead of overtaking the waiter already promised the turn.
|
|
58
|
+
await new Promise(resolve => (lane === 'control' ? state.control : state.normal).push(resolve));
|
|
59
|
+
}
|
|
60
|
+
/** Hand the gate to the next waiter, control first, or free it. */
|
|
61
|
+
release(sessionId, state) {
|
|
62
|
+
const next = state.control.shift() ?? state.normal.shift();
|
|
63
|
+
if (next !== undefined) {
|
|
64
|
+
next();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
state.busy = false;
|
|
68
|
+
// A lane nobody is waiting for would otherwise stay behind for the life of the process, and long
|
|
69
|
+
// runs create a session per verification.
|
|
70
|
+
if (this.lanes.get(sessionId) === state)
|
|
71
|
+
this.lanes.delete(sessionId);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -1,91 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
import { type ObjectValue } from '../
|
|
3
|
-
/** Parse workspace and resume navigation, including their long aliases. */
|
|
4
|
-
export declare function navigationCommand(value: string): {
|
|
5
|
-
kind: 'workspace' | 'session';
|
|
6
|
-
query?: string;
|
|
7
|
-
} | undefined;
|
|
8
|
-
/** Resolve the host's title projection, falling back to the session ID. */
|
|
9
|
-
export declare function sessionLabel(session: ObjectValue): string;
|
|
1
|
+
/** Unambiguous slash-command target resolution and host title projection. */
|
|
2
|
+
import { type ObjectValue } from '../json.ts';
|
|
10
3
|
/** Match an exact ID or name before a unique ID prefix; never choose an ambiguous target. */
|
|
11
4
|
export declare function resolveTarget(items: ObjectValue[], query: string, id: string, names: (item: ObjectValue) => string[]): ObjectValue;
|
|
12
|
-
/** User-visible activity of one session, most actionable first.
|
|
13
|
-
*
|
|
14
|
-
* `needs` is the only state that asks the user to do something, and it outranks the host's running
|
|
15
|
-
* flag because a turn waiting on an answer is running only in the mechanical sense. `blank` stays a
|
|
16
|
-
* marker for a session that never sent a turn, but it is not a status and never enters a rollup.
|
|
17
|
-
*/
|
|
18
|
-
export type SessionState = 'needs' | 'running' | 'idle' | 'blank';
|
|
19
|
-
/** Classify one session from the host's list summary; nothing is inferred from silence.
|
|
20
|
-
* @param session - Session summary from `session/list`.
|
|
21
|
-
* @param pending - Whether this client holds an unanswered interaction for that session.
|
|
22
|
-
* @returns Needs-you while an answer is owed, running while its agent works, otherwise idle or blank.
|
|
23
|
-
*/
|
|
24
|
-
export declare function sessionState(session: ObjectValue, pending?: boolean): SessionState;
|
|
25
|
-
/** Leading marker per state: a question mark, a working clock, a filled dot, and an unused circle. */
|
|
26
|
-
export declare const SESSION_MARKERS: Record<SessionState, string>;
|
|
27
|
-
/** One word per state, so every screen names the same state the same way. */
|
|
28
|
-
export declare const STATE_LABELS: Record<SessionState, string>;
|
|
29
|
-
/** Coarse age of a session's last activity, so the column stays steady between list refreshes.
|
|
30
|
-
* @param time - Epoch milliseconds of the last activity, when the summary reported one.
|
|
31
|
-
* @param now - Current epoch milliseconds.
|
|
32
|
-
* @returns `now`, minutes, hours or days.
|
|
33
|
-
*/
|
|
34
|
-
export declare function activityAge(time: number | undefined, now: number): string;
|
|
35
|
-
/** Status cell for one session row: its state marker and the age of its last activity.
|
|
36
|
-
* @param session - Session summary from `session/list`.
|
|
37
|
-
* @param now - Current epoch milliseconds.
|
|
38
|
-
* @param pending - Whether this client holds an unanswered interaction for that session.
|
|
39
|
-
* @returns Marker with an optional age, without a trailing space when unknown.
|
|
40
|
-
*/
|
|
41
|
-
export declare function sessionStatus(session: ObjectValue, now: number, pending?: boolean): string;
|
|
42
|
-
/** States a workspace rollup reports, most actionable first. */
|
|
43
|
-
export declare const ROLLUP_STATES: readonly ["needs", "running", "idle"];
|
|
44
|
-
/** One state a workspace rollup reports. */
|
|
45
|
-
export type RollupState = (typeof ROLLUP_STATES)[number];
|
|
46
|
-
/** One counted state of a workspace rollup. */
|
|
47
|
-
export interface RollupCount {
|
|
48
|
-
state: RollupState;
|
|
49
|
-
count: number;
|
|
50
|
-
}
|
|
51
|
-
/** How much room a rollup has for words. */
|
|
52
|
-
export type RollupStyle = 'words' | 'badges';
|
|
53
|
-
/** Count the sessions of one workspace by the state each reports.
|
|
54
|
-
*
|
|
55
|
-
* Blank sessions are counted by neither a badge nor a word: a session that never sent a turn is the
|
|
56
|
-
* absence of activity, and listing it beside real work only makes the rollup harder to read.
|
|
57
|
-
* @param sessions - Sessions whose `sessionIds` belong to the workspace.
|
|
58
|
-
* @param pending - Session IDs this client holds an unanswered interaction for.
|
|
59
|
-
* @returns One count per state that occurs, most actionable first, or an empty list.
|
|
60
|
-
*/
|
|
61
|
-
export declare function workspaceCounts(sessions: readonly ObjectValue[], pending?: ReadonlySet<string>): RollupCount[];
|
|
62
|
-
/** Render one rollup as separately coloured cells.
|
|
63
|
-
*
|
|
64
|
-
* Each cell after the first carries the separator that joins it to the previous one, so a caller can
|
|
65
|
-
* colour the cells independently without losing the text {@link workspaceStatus} would produce.
|
|
66
|
-
* @param counts - Counts from {@link workspaceCounts}.
|
|
67
|
-
* @param style - `words` spells each state out; `badges` keeps only the marker and the count.
|
|
68
|
-
* @returns The cells in the order given, with their separators.
|
|
69
|
-
*/
|
|
70
|
-
export declare function workspaceSegments(counts: readonly RollupCount[], style?: RollupStyle): {
|
|
71
|
-
state: RollupState;
|
|
72
|
-
text: string;
|
|
73
|
-
}[];
|
|
74
|
-
/** Render one rollup as plain text, the same way every screen and test reads it.
|
|
75
|
-
* @param counts - Counts from {@link workspaceCounts}.
|
|
76
|
-
* @param style - `words` spells each state out; `badges` keeps only the marker and the count.
|
|
77
|
-
* @returns The joined cell text, empty when nothing was counted.
|
|
78
|
-
*/
|
|
79
|
-
export declare function workspaceStatus(counts: readonly RollupCount[], style?: RollupStyle): string;
|
|
80
|
-
/** Marker key for the compact rollup, which has no room for the words. */
|
|
81
|
-
export declare const ROLLUP_LEGEND: string;
|
|
82
|
-
/** Secondary path text for one workspace row.
|
|
83
|
-
*
|
|
84
|
-
* The title is usually the last path segment, so repeating it wastes the row; the parent directory
|
|
85
|
-
* is what distinguishes two checkouts. A title that does not name the last segment keeps the full
|
|
86
|
-
* path, because dropping it would hide where the workspace actually lives.
|
|
87
|
-
* @param path - Registered host directory.
|
|
88
|
-
* @param title - Workspace title as the row already shows it.
|
|
89
|
-
* @returns The path to show beside the row, or an empty string when nothing is left.
|
|
90
|
-
*/
|
|
91
|
-
export declare function workspaceDetail(path: string, title: string): string;
|
|
@@ -1,25 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
import {
|
|
3
|
-
/** Parse workspace and resume navigation, including their long aliases. */
|
|
4
|
-
export function navigationCommand(value) {
|
|
5
|
-
const match = /^\/(ws|workspace|workspaces|resume|session|sessions)(?:\s+(.+))?$/.exec(value);
|
|
6
|
-
if (!match)
|
|
7
|
-
return undefined;
|
|
8
|
-
return { kind: match[1] === 'ws' || match[1].startsWith('workspace') ? 'workspace' : 'session', query: match[2] };
|
|
9
|
-
}
|
|
10
|
-
/** Resolve the host's title projection, falling back to the session ID. */
|
|
11
|
-
export function sessionLabel(session) {
|
|
12
|
-
const projections = session.projections;
|
|
13
|
-
if (projections && typeof projections === 'object' && !Array.isArray(projections)) {
|
|
14
|
-
const values = projections.values;
|
|
15
|
-
const title = values && typeof values === 'object' && !Array.isArray(values) ? values.title : undefined;
|
|
16
|
-
if (typeof title === 'string' && safeText(title).trim())
|
|
17
|
-
return safeText(title).trim();
|
|
18
|
-
if (title && typeof title === 'object' && !Array.isArray(title) && typeof title.title === 'string' && safeText(title.title).trim())
|
|
19
|
-
return safeText(title.title).trim();
|
|
20
|
-
}
|
|
21
|
-
return string(session.sessionId);
|
|
22
|
-
}
|
|
1
|
+
/** Unambiguous slash-command target resolution and host title projection. */
|
|
2
|
+
import { string } from "../json.js";
|
|
23
3
|
/** Match an exact ID or name before a unique ID prefix; never choose an ambiguous target. */
|
|
24
4
|
export function resolveTarget(items, query, id, names) {
|
|
25
5
|
const target = query.replace(/^(["'])(.*)\1$/, '$2');
|
|
@@ -34,110 +14,3 @@ export function resolveTarget(items, query, id, names) {
|
|
|
34
14
|
throw new Error(`Ambiguous target: ${target}. Use a full ID.`);
|
|
35
15
|
throw new Error(`Target not found: ${target}`);
|
|
36
16
|
}
|
|
37
|
-
/** Classify one session from the host's list summary; nothing is inferred from silence.
|
|
38
|
-
* @param session - Session summary from `session/list`.
|
|
39
|
-
* @param pending - Whether this client holds an unanswered interaction for that session.
|
|
40
|
-
* @returns Needs-you while an answer is owed, running while its agent works, otherwise idle or blank.
|
|
41
|
-
*/
|
|
42
|
-
export function sessionState(session, pending = false) {
|
|
43
|
-
if (pending)
|
|
44
|
-
return 'needs';
|
|
45
|
-
if (session.running === true)
|
|
46
|
-
return 'running';
|
|
47
|
-
return session.blank === true ? 'blank' : 'idle';
|
|
48
|
-
}
|
|
49
|
-
/** Leading marker per state: a question mark, a working clock, a filled dot, and an unused circle. */
|
|
50
|
-
export const SESSION_MARKERS = { needs: '?', running: '◐', idle: '●', blank: '○' };
|
|
51
|
-
/** One word per state, so every screen names the same state the same way. */
|
|
52
|
-
export const STATE_LABELS = { needs: 'needs you', running: 'working', idle: 'ready', blank: 'empty' };
|
|
53
|
-
/** Coarse age of a session's last activity, so the column stays steady between list refreshes.
|
|
54
|
-
* @param time - Epoch milliseconds of the last activity, when the summary reported one.
|
|
55
|
-
* @param now - Current epoch milliseconds.
|
|
56
|
-
* @returns `now`, minutes, hours or days.
|
|
57
|
-
*/
|
|
58
|
-
export function activityAge(time, now) {
|
|
59
|
-
if (time === undefined || !Number.isFinite(time))
|
|
60
|
-
return '';
|
|
61
|
-
const seconds = Math.max(0, Math.floor((now - time) / 1000));
|
|
62
|
-
if (seconds < 60)
|
|
63
|
-
return 'now';
|
|
64
|
-
const minutes = Math.floor(seconds / 60);
|
|
65
|
-
if (minutes < 60)
|
|
66
|
-
return `${minutes}m`;
|
|
67
|
-
const hours = Math.floor(minutes / 60);
|
|
68
|
-
return hours < 24 ? `${hours}h` : `${Math.floor(hours / 24)}d`;
|
|
69
|
-
}
|
|
70
|
-
/** Status cell for one session row: its state marker and the age of its last activity.
|
|
71
|
-
* @param session - Session summary from `session/list`.
|
|
72
|
-
* @param now - Current epoch milliseconds.
|
|
73
|
-
* @param pending - Whether this client holds an unanswered interaction for that session.
|
|
74
|
-
* @returns Marker with an optional age, without a trailing space when unknown.
|
|
75
|
-
*/
|
|
76
|
-
export function sessionStatus(session, now, pending = false) {
|
|
77
|
-
const age = activityAge(typeof session.updatedAt === 'number' ? session.updatedAt : undefined, now);
|
|
78
|
-
return `${SESSION_MARKERS[sessionState(session, pending)]}${age === '' ? '' : ` ${age}`}`;
|
|
79
|
-
}
|
|
80
|
-
/** States a workspace rollup reports, most actionable first. */
|
|
81
|
-
export const ROLLUP_STATES = ['needs', 'running', 'idle'];
|
|
82
|
-
/** Count the sessions of one workspace by the state each reports.
|
|
83
|
-
*
|
|
84
|
-
* Blank sessions are counted by neither a badge nor a word: a session that never sent a turn is the
|
|
85
|
-
* absence of activity, and listing it beside real work only makes the rollup harder to read.
|
|
86
|
-
* @param sessions - Sessions whose `sessionIds` belong to the workspace.
|
|
87
|
-
* @param pending - Session IDs this client holds an unanswered interaction for.
|
|
88
|
-
* @returns One count per state that occurs, most actionable first, or an empty list.
|
|
89
|
-
*/
|
|
90
|
-
export function workspaceCounts(sessions, pending = new Set()) {
|
|
91
|
-
const counts = { needs: 0, running: 0, idle: 0 };
|
|
92
|
-
for (const session of sessions) {
|
|
93
|
-
const id = session.sessionId;
|
|
94
|
-
const state = sessionState(session, typeof id === 'string' && pending.has(id));
|
|
95
|
-
if (state !== 'blank')
|
|
96
|
-
counts[state] += 1;
|
|
97
|
-
}
|
|
98
|
-
return ROLLUP_STATES.filter(state => counts[state] > 0).map(state => ({ state, count: counts[state] }));
|
|
99
|
-
}
|
|
100
|
-
/** Render one rollup as separately coloured cells.
|
|
101
|
-
*
|
|
102
|
-
* Each cell after the first carries the separator that joins it to the previous one, so a caller can
|
|
103
|
-
* colour the cells independently without losing the text {@link workspaceStatus} would produce.
|
|
104
|
-
* @param counts - Counts from {@link workspaceCounts}.
|
|
105
|
-
* @param style - `words` spells each state out; `badges` keeps only the marker and the count.
|
|
106
|
-
* @returns The cells in the order given, with their separators.
|
|
107
|
-
*/
|
|
108
|
-
export function workspaceSegments(counts, style = 'words') {
|
|
109
|
-
const separator = style === 'words' ? ' · ' : ' ';
|
|
110
|
-
return counts.map(({ state, count }, index) => ({ state,
|
|
111
|
-
text: `${index === 0 ? '' : separator}${style === 'words' ? `${SESSION_MARKERS[state]} ${count} ${STATE_LABELS[state]}` : `${SESSION_MARKERS[state]}${count}`}` }));
|
|
112
|
-
}
|
|
113
|
-
/** Render one rollup as plain text, the same way every screen and test reads it.
|
|
114
|
-
* @param counts - Counts from {@link workspaceCounts}.
|
|
115
|
-
* @param style - `words` spells each state out; `badges` keeps only the marker and the count.
|
|
116
|
-
* @returns The joined cell text, empty when nothing was counted.
|
|
117
|
-
*/
|
|
118
|
-
export function workspaceStatus(counts, style = 'words') {
|
|
119
|
-
return workspaceSegments(counts, style).map(segment => segment.text).join('');
|
|
120
|
-
}
|
|
121
|
-
/** Marker key for the compact rollup, which has no room for the words. */
|
|
122
|
-
export const ROLLUP_LEGEND = `${SESSION_MARKERS.idle} ${STATE_LABELS.idle} · ${SESSION_MARKERS.running} ${STATE_LABELS.running} · ${SESSION_MARKERS.needs} ${STATE_LABELS.needs}`;
|
|
123
|
-
/** Secondary path text for one workspace row.
|
|
124
|
-
*
|
|
125
|
-
* The title is usually the last path segment, so repeating it wastes the row; the parent directory
|
|
126
|
-
* is what distinguishes two checkouts. A title that does not name the last segment keeps the full
|
|
127
|
-
* path, because dropping it would hide where the workspace actually lives.
|
|
128
|
-
* @param path - Registered host directory.
|
|
129
|
-
* @param title - Workspace title as the row already shows it.
|
|
130
|
-
* @returns The path to show beside the row, or an empty string when nothing is left.
|
|
131
|
-
*/
|
|
132
|
-
export function workspaceDetail(path, title) {
|
|
133
|
-
if (!path)
|
|
134
|
-
return '';
|
|
135
|
-
const trimmed = path.replace(/\/+$/u, '');
|
|
136
|
-
const cut = trimmed.lastIndexOf('/');
|
|
137
|
-
const base = cut < 0 ? trimmed : trimmed.slice(cut + 1);
|
|
138
|
-
if (base !== title)
|
|
139
|
-
return path;
|
|
140
|
-
if (cut > 0)
|
|
141
|
-
return trimmed.slice(0, cut);
|
|
142
|
-
return cut === 0 ? '/' : '';
|
|
143
|
-
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Workspace/session navigation owns view intent independently of background list refreshes. */
|
|
2
|
+
import { type ObjectValue } from '../json.ts';
|
|
3
|
+
import type { HostAccess } from '../transport/host.ts';
|
|
4
|
+
export interface NavigationState {
|
|
5
|
+
screen: 'workspaces' | 'sessions' | 'chat' | 'path';
|
|
6
|
+
workspaces: ObjectValue[];
|
|
7
|
+
sessions: ObjectValue[];
|
|
8
|
+
showAllSessions: boolean;
|
|
9
|
+
workspaceId?: string;
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface NavigationHost extends HostAccess {
|
|
13
|
+
read(): Readonly<NavigationState>;
|
|
14
|
+
publish(patch: Partial<NavigationState>): void;
|
|
15
|
+
selection(): number;
|
|
16
|
+
leave(): void;
|
|
17
|
+
follow(sessionId: string): void;
|
|
18
|
+
}
|
|
19
|
+
export declare class SessionNavigator {
|
|
20
|
+
private readonly host;
|
|
21
|
+
private active?;
|
|
22
|
+
private readonly tasks;
|
|
23
|
+
private listsVersion;
|
|
24
|
+
private revision;
|
|
25
|
+
constructor(host: NavigationHost);
|
|
26
|
+
get intent(): number;
|
|
27
|
+
/** Leaving a view invalidates the request that intended to open it. */
|
|
28
|
+
cancel(): void;
|
|
29
|
+
reset(): void;
|
|
30
|
+
settle(): Promise<void>;
|
|
31
|
+
private run;
|
|
32
|
+
private load;
|
|
33
|
+
private publishLists;
|
|
34
|
+
refreshLists(): Promise<void>;
|
|
35
|
+
showPicker(screen: 'workspaces' | 'sessions', signal?: AbortSignal): Promise<void>;
|
|
36
|
+
showChat(): boolean;
|
|
37
|
+
enterPath(): void;
|
|
38
|
+
private commitWorkspace;
|
|
39
|
+
pickWorkspace(workspaceId?: string): void;
|
|
40
|
+
switchWorkspace(query?: string, signal?: AbortSignal): Promise<void>;
|
|
41
|
+
switchSession(query?: string, signal?: AbortSignal): Promise<void>;
|
|
42
|
+
createWorkspace(path: string, signal?: AbortSignal): Promise<void>;
|
|
43
|
+
createSession(signal?: AbortSignal): Promise<void>;
|
|
44
|
+
get visibleSessions(): ObjectValue[];
|
|
45
|
+
/** Longest registered path wins, with whole-segment matching for nested workspaces. */
|
|
46
|
+
adoptLocalWorkspace(directory: string): string | undefined;
|
|
47
|
+
}
|