@itookit/dsht 0.5.1 → 0.6.0
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 +10 -4
- package/README.zh.md +12 -6
- 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 +22 -2
- package/dist/cli/startup.js +30 -11
- package/dist/cli/verifier.d.ts +4 -0
- package/dist/cli/verifier.js +28 -5
- package/dist/contracts.d.ts +42 -5
- package/dist/controller/connection-streams.d.ts +22 -0
- package/dist/controller/connection-streams.js +105 -0
- package/dist/controller/connection.d.ts +14 -3
- package/dist/controller/connection.js +40 -69
- package/dist/controller/controller.d.ts +20 -234
- package/dist/controller/controller.js +113 -811
- package/dist/controller/foreground.d.ts +44 -0
- package/dist/controller/foreground.js +79 -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 +16 -2
- package/dist/controller/loop-prompts-schema.js +106 -27
- package/dist/controller/loop-prompts.d.ts +17 -2
- package/dist/controller/loop-prompts.generated.js +2 -1
- package/dist/controller/loop-prompts.js +35 -9
- package/dist/controller/loop-protocols.d.ts +3 -1
- package/dist/controller/loop-protocols.js +8 -3
- package/dist/controller/loop-source.d.ts +74 -0
- package/dist/controller/loop-source.js +224 -0
- package/dist/controller/verifier.d.ts +4 -0
- package/dist/cost/controller.d.ts +3 -1
- package/dist/cost/controller.js +26 -7
- package/dist/cost/index.d.ts +1 -1
- package/dist/cost/index.js +1 -1
- package/dist/cost/ledger-files.d.ts +20 -0
- package/dist/cost/ledger-files.js +115 -15
- package/dist/cost/ledger.d.ts +31 -6
- package/dist/cost/ledger.js +74 -22
- package/dist/cost/pricing.d.ts +39 -0
- package/dist/cost/pricing.js +46 -0
- package/dist/cost/scanner.js +1 -0
- package/dist/cost/types.d.ts +9 -3
- package/dist/session/controller.d.ts +23 -35
- package/dist/session/controller.js +113 -363
- package/dist/session/history-reader.d.ts +32 -0
- package/dist/session/history-reader.js +170 -0
- package/dist/session/index.d.ts +1 -1
- package/dist/session/info.d.ts +3 -38
- package/dist/session/info.js +14 -1
- package/dist/session/interactions.d.ts +26 -0
- package/dist/session/interactions.js +75 -0
- package/dist/session/navigator.d.ts +47 -0
- package/dist/session/navigator.js +158 -0
- package/dist/session/prompt-backfill.d.ts +23 -0
- package/dist/session/prompt-backfill.js +88 -0
- package/dist/session/state.d.ts +20 -0
- package/dist/session/state.js +1 -0
- package/dist/session/telemetry.d.ts +15 -6
- package/dist/session/telemetry.js +44 -7
- package/dist/session/transcript.d.ts +5 -1
- package/dist/slash/index.d.ts +1 -1
- package/dist/slash/parse.d.ts +2 -126
- package/dist/slash/registry.d.ts +1 -1
- package/dist/slash/types.d.ts +126 -0
- package/dist/slash/types.js +1 -0
- package/dist/state.d.ts +5 -17
- package/dist/state.js +1 -1
- package/dist/storage/files.d.ts +8 -0
- package/dist/storage/files.js +18 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/transport/client.d.ts +4 -3
- package/dist/transport/client.js +71 -25
- package/dist/ui/app.js +88 -301
- package/dist/ui/chat/shell-view.d.ts +2 -0
- package/dist/ui/chat/shell-view.js +8 -0
- package/dist/ui/chat/use-history-view.d.ts +69 -0
- package/dist/ui/chat/use-history-view.js +123 -0
- package/dist/ui/dialogs/cost.d.ts +6 -0
- package/dist/ui/dialogs/cost.js +5 -1
- package/dist/ui/dialogs/loop.d.ts +5 -4
- package/dist/ui/dialogs/loop.js +14 -6
- package/dist/ui/dialogs/use-panels.d.ts +53 -0
- package/dist/ui/dialogs/use-panels.js +51 -0
- 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/loop.yaml +230 -0
- package/package.json +5 -4
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Reading position, folds and asynchronous navigation belong to this mounted conversation view. */
|
|
2
|
+
import { type SetStateAction } from 'react';
|
|
3
|
+
import type { Reasoning, SessionRender, ShellBlock } from '../../contracts.ts';
|
|
4
|
+
interface HistorySource {
|
|
5
|
+
readonly version: number;
|
|
6
|
+
readonly memoryRevision: number;
|
|
7
|
+
readonly beforeSeq: number | undefined;
|
|
8
|
+
readonly ready: boolean;
|
|
9
|
+
readonly hasMore: boolean;
|
|
10
|
+
readonly liveAttemptKey: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
interface HistoryViewOptions<S extends HistorySource> {
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
source: S;
|
|
15
|
+
display: S;
|
|
16
|
+
detached: boolean;
|
|
17
|
+
width: number;
|
|
18
|
+
rows: number;
|
|
19
|
+
showHint: boolean;
|
|
20
|
+
protect: boolean;
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
online: boolean;
|
|
23
|
+
busy: boolean;
|
|
24
|
+
revision: number;
|
|
25
|
+
blocks: readonly ShellBlock[];
|
|
26
|
+
current(source: S): boolean;
|
|
27
|
+
render(input: {
|
|
28
|
+
transcript: S;
|
|
29
|
+
width: number;
|
|
30
|
+
folds: ReadonlySet<number>;
|
|
31
|
+
liveReasoning: Reasoning;
|
|
32
|
+
}): SessionRender;
|
|
33
|
+
run<T>(work: (signal: AbortSignal) => Promise<T>, label: string): Promise<T | undefined>;
|
|
34
|
+
older(signal: AbortSignal, source: S): Promise<boolean>;
|
|
35
|
+
open(target: number, signal: AbortSignal): Promise<boolean>;
|
|
36
|
+
pin(value: boolean): void;
|
|
37
|
+
closePanels(): void;
|
|
38
|
+
notify(message: string): void;
|
|
39
|
+
}
|
|
40
|
+
export declare function useHistoryView<S extends HistorySource>(options: HistoryViewOptions<S>): {
|
|
41
|
+
layout: {
|
|
42
|
+
messages: import("../../contracts.ts").Message[];
|
|
43
|
+
offsets: Map<number, number>;
|
|
44
|
+
first: number | undefined;
|
|
45
|
+
length: number;
|
|
46
|
+
liveOffset: number;
|
|
47
|
+
viewport: (start: number, end: number) => import("../../contracts.ts").HistoryRow[];
|
|
48
|
+
readonly cachedRowCount: number;
|
|
49
|
+
readonly lines: string[];
|
|
50
|
+
};
|
|
51
|
+
merged: {
|
|
52
|
+
total: number;
|
|
53
|
+
hostRow(row: number): number;
|
|
54
|
+
sources: ReadonlyMap<number, string>;
|
|
55
|
+
viewport(start: number, end: number): import("../../contracts.ts").HistoryRow[];
|
|
56
|
+
};
|
|
57
|
+
totalRows: number;
|
|
58
|
+
pageSize: number;
|
|
59
|
+
position: number;
|
|
60
|
+
reasoningOverrides: ReadonlySet<number>;
|
|
61
|
+
liveReasoning: Reasoning;
|
|
62
|
+
setScroll: (value: SetStateAction<number>) => void;
|
|
63
|
+
setReasoningOverrides: (value: SetStateAction<ReadonlySet<number>>) => void;
|
|
64
|
+
setLiveReasoning: (value: SetStateAction<Reasoning>) => void;
|
|
65
|
+
scrollHistory: (delta: number) => void;
|
|
66
|
+
jumpHistory: (target: number) => Promise<void>;
|
|
67
|
+
historyOperation: <T>(work: (signal: AbortSignal) => Promise<T>, label?: string) => Promise<T | undefined>;
|
|
68
|
+
};
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** Reading position, folds and asynchronous navigation belong to this mounted conversation view. */
|
|
2
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { errorText } from "../../text.js";
|
|
4
|
+
import { mergeShellRuns } from "./shell-view.js";
|
|
5
|
+
export function useHistoryView(options) {
|
|
6
|
+
const [scroll, setScroll] = useState(0);
|
|
7
|
+
const [folds, setFolds] = useState(new Set());
|
|
8
|
+
const [liveReasoning, setLive] = useState('row');
|
|
9
|
+
const [anchor, setAnchor] = useState();
|
|
10
|
+
const latest = useRef(options);
|
|
11
|
+
latest.current = options;
|
|
12
|
+
const mounted = useRef(true);
|
|
13
|
+
const intent = useRef(0);
|
|
14
|
+
const jumpAbort = useRef(undefined);
|
|
15
|
+
const pageAbort = useRef(undefined);
|
|
16
|
+
const lifetime = useRef(new AbortController());
|
|
17
|
+
const paging = useRef(false);
|
|
18
|
+
const invalidate = () => { intent.current++; jumpAbort.current?.abort(); setAnchor(undefined); };
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (lifetime.current.signal.aborted)
|
|
21
|
+
lifetime.current = new AbortController();
|
|
22
|
+
mounted.current = true;
|
|
23
|
+
return () => {
|
|
24
|
+
mounted.current = false;
|
|
25
|
+
intent.current++;
|
|
26
|
+
lifetime.current.abort();
|
|
27
|
+
jumpAbort.current?.abort();
|
|
28
|
+
pageAbort.current?.abort();
|
|
29
|
+
latest.current.pin(false);
|
|
30
|
+
};
|
|
31
|
+
}, []);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
invalidate();
|
|
34
|
+
pageAbort.current?.abort();
|
|
35
|
+
setScroll(0);
|
|
36
|
+
}, [options.source]);
|
|
37
|
+
useEffect(() => { setFolds(new Set()); setLive('row'); }, [options.sessionId]);
|
|
38
|
+
useEffect(() => { setLive('row'); }, [options.source, options.source.liveAttemptKey]);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const first = options.display.beforeSeq;
|
|
41
|
+
if (first !== undefined)
|
|
42
|
+
setFolds(previous => {
|
|
43
|
+
const next = new Set([...previous].filter(seq => seq >= first));
|
|
44
|
+
return next.size === previous.size ? previous : next;
|
|
45
|
+
});
|
|
46
|
+
}, [options.display, options.display.memoryRevision]);
|
|
47
|
+
const { source, display, width } = options;
|
|
48
|
+
const layout = useMemo(() => options.render({ transcript: display, width, folds, liveReasoning }), [display, display.version, width, folds, liveReasoning]);
|
|
49
|
+
const merged = useMemo(() => mergeShellRuns(layout, options.blocks, width), [layout, options.blocks, options.revision, width]);
|
|
50
|
+
const pageSize = Math.max(1, options.rows - (options.showHint ? 1 : 0));
|
|
51
|
+
const totalRows = merged.total;
|
|
52
|
+
const maxScroll = Math.max(0, totalRows - pageSize);
|
|
53
|
+
const previousView = useRef({ source, display, count: totalRows, first: layout.first, folds, liveReasoning });
|
|
54
|
+
const previous = previousView.current;
|
|
55
|
+
const prepended = previous.first !== undefined && layout.first !== undefined && layout.first < previous.first;
|
|
56
|
+
const adjusted = previous.source !== source ? 0
|
|
57
|
+
: scroll > 0 && previous.display === display && !prepended && previous.folds === folds && previous.liveReasoning === liveReasoning
|
|
58
|
+
? Math.max(0, scroll + totalRows - previous.count) : scroll;
|
|
59
|
+
const row = anchor?.source === source ? layout.offsets.get(anchor.target) : undefined;
|
|
60
|
+
const position = Math.max(0, Math.min(row === undefined ? adjusted : totalRows - pageSize - merged.hostRow(row), maxScroll));
|
|
61
|
+
useLayoutEffect(() => {
|
|
62
|
+
previousView.current = { source, display, count: totalRows, first: layout.first, folds, liveReasoning };
|
|
63
|
+
if (position !== scroll)
|
|
64
|
+
setScroll(position);
|
|
65
|
+
if (anchor)
|
|
66
|
+
setAnchor(undefined);
|
|
67
|
+
}, [source, display, totalRows, position, scroll, folds, liveReasoning, anchor]);
|
|
68
|
+
useLayoutEffect(() => { options.pin(!options.detached && (position > 0 || options.protect)); }, [options.detached, position, options.protect, source]);
|
|
69
|
+
function scrollTo(value) { invalidate(); setScroll(value); }
|
|
70
|
+
function setReasoningOverrides(value) { invalidate(); setFolds(value); }
|
|
71
|
+
function setLiveReasoning(value) { invalidate(); setLive(value); }
|
|
72
|
+
function historyOperation(work, label = 'Loading history…') {
|
|
73
|
+
return options.run(signal => work(AbortSignal.any([signal, lifetime.current.signal])), label);
|
|
74
|
+
}
|
|
75
|
+
function scrollHistory(delta) {
|
|
76
|
+
if (!options.enabled)
|
|
77
|
+
return;
|
|
78
|
+
invalidate();
|
|
79
|
+
const own = intent.current;
|
|
80
|
+
const next = Math.max(0, Math.min(maxScroll, position + delta));
|
|
81
|
+
setScroll(next);
|
|
82
|
+
if (delta <= 0 || next < maxScroll || paging.current || options.busy || !options.online || !display.ready || !display.hasMore)
|
|
83
|
+
return;
|
|
84
|
+
paging.current = true;
|
|
85
|
+
const abort = new AbortController();
|
|
86
|
+
pageAbort.current = abort;
|
|
87
|
+
void historyOperation(signal => options.older(AbortSignal.any([signal, abort.signal]), display)).then(accepted => {
|
|
88
|
+
if (accepted && mounted.current && latest.current.display === display && options.current(source) && intent.current === own) {
|
|
89
|
+
setScroll(position + delta);
|
|
90
|
+
}
|
|
91
|
+
}).catch(error => {
|
|
92
|
+
if (mounted.current && !(error instanceof Error && error.name === 'AbortError'))
|
|
93
|
+
latest.current.notify(errorText(error));
|
|
94
|
+
}).finally(() => { paging.current = false; if (pageAbort.current === abort)
|
|
95
|
+
pageAbort.current = undefined; });
|
|
96
|
+
}
|
|
97
|
+
async function jumpHistory(target) {
|
|
98
|
+
if (!options.enabled)
|
|
99
|
+
throw new Error('Select a session first');
|
|
100
|
+
invalidate();
|
|
101
|
+
const own = intent.current;
|
|
102
|
+
const abort = new AbortController();
|
|
103
|
+
jumpAbort.current = abort;
|
|
104
|
+
try {
|
|
105
|
+
await historyOperation(async (foreground) => {
|
|
106
|
+
const signal = AbortSignal.any([foreground, abort.signal]);
|
|
107
|
+
if (!await options.open(target, signal))
|
|
108
|
+
return;
|
|
109
|
+
signal.throwIfAborted();
|
|
110
|
+
if (!mounted.current || intent.current !== own || !options.current(source))
|
|
111
|
+
return;
|
|
112
|
+
setAnchor({ source, target });
|
|
113
|
+
latest.current.closePanels();
|
|
114
|
+
}, 'Jumping to a message…');
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
if (jumpAbort.current === abort)
|
|
118
|
+
jumpAbort.current = undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { layout, merged, totalRows, pageSize, position, reasoningOverrides: folds, liveReasoning,
|
|
122
|
+
setScroll: scrollTo, setReasoningOverrides, setLiveReasoning, scrollHistory, jumpHistory, historyOperation };
|
|
123
|
+
}
|
|
@@ -9,6 +9,8 @@ export interface CostLine {
|
|
|
9
9
|
export interface CostSource {
|
|
10
10
|
session?: CostLine;
|
|
11
11
|
today: CostLine;
|
|
12
|
+
week: CostLine;
|
|
13
|
+
month: CostLine;
|
|
12
14
|
scanning: boolean;
|
|
13
15
|
coverage: Coverage;
|
|
14
16
|
scannedAt?: number;
|
|
@@ -17,6 +19,10 @@ export interface CostSource {
|
|
|
17
19
|
missing: readonly string[];
|
|
18
20
|
}
|
|
19
21
|
/** Render cached totals while the independent HTTP cost scan refreshes.
|
|
22
|
+
*
|
|
23
|
+
* The three periods are natural Beijing periods counted from their own start through today, not
|
|
24
|
+
* sliding windows, so "this week" and "this month" match what an invoice would name. They only reach
|
|
25
|
+
* back as far as the retained ledger does, which is why the panel says how old its cached totals are.
|
|
20
26
|
* @param source - Plain billing summary, absent when this run has no ledger.
|
|
21
27
|
* @returns Billing panel, including unpriced models and refresh errors.
|
|
22
28
|
*/
|
package/dist/ui/dialogs/cost.js
CHANGED
|
@@ -4,6 +4,10 @@ import { useTheme } from "../theme/index.js";
|
|
|
4
4
|
import { Box, Text } from 'ink';
|
|
5
5
|
import { safeText } from "../../text.js";
|
|
6
6
|
/** Render cached totals while the independent HTTP cost scan refreshes.
|
|
7
|
+
*
|
|
8
|
+
* The three periods are natural Beijing periods counted from their own start through today, not
|
|
9
|
+
* sliding windows, so "this week" and "this month" match what an invoice would name. They only reach
|
|
10
|
+
* back as far as the retained ledger does, which is why the panel says how old its cached totals are.
|
|
7
11
|
* @param source - Plain billing summary, absent when this run has no ledger.
|
|
8
12
|
* @returns Billing panel, including unpriced models and refresh errors.
|
|
9
13
|
*/
|
|
@@ -12,7 +16,7 @@ export function CostPanel({ source }) {
|
|
|
12
16
|
const costs = source;
|
|
13
17
|
if (!costs)
|
|
14
18
|
return _jsx(Text, { children: "Cost tracking is unavailable" });
|
|
15
|
-
const rows = [['Session', costs.session], ['Today', costs.today]];
|
|
19
|
+
const rows = [['Session', costs.session], ['Today', costs.today], ['This week', costs.week], ['This month', costs.month]];
|
|
16
20
|
return _jsxs(Box, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Cost \u00B7 CNY estimate \u00B7 Asia/Shanghai \u00B7 /cost closes" }), rows.map(([label, total]) => _jsxs(Text, { children: [label, ": ", total ? `${total.text} · ${total.unknown} unpriced / ${total.records} requests` : '?'] }, label)), _jsx(Text, { dimColor: true, children: costs.scanning ? 'Refreshing all visible sessions…'
|
|
17
21
|
: costs.coverage === 'partial' ? 'Partial totals · awaiting a complete scan'
|
|
18
22
|
: costs.scannedAt ? `Last refresh: ${new Date(costs.scannedAt).toISOString()}`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LoopLimits, LoopRecord } from '../../contracts.ts';
|
|
1
|
+
import type { LoopLimits, LoopRecord, LoopSourceInfo } from '../../contracts.ts';
|
|
2
2
|
/** The numeric fields the form edits, in the order a run reads them. */
|
|
3
3
|
export type LoopField = 'from' | 'to' | 'score' | 'tries';
|
|
4
4
|
/** What the form hands to the runner: the four numbers and the record's variables as confirmed. */
|
|
@@ -15,12 +15,13 @@ export declare function loopRecordDefaults(record: LoopRecord): LoopLimits;
|
|
|
15
15
|
*
|
|
16
16
|
* It offers the same names and defaults the runner reads, so choosing from it cannot start a run
|
|
17
17
|
* other than the one the row describes.
|
|
18
|
-
* @param props - Matching records and the
|
|
19
|
-
* @returns The key line and up to six record rows.
|
|
18
|
+
* @param props - Matching records, the highlighted row, and where the records came from.
|
|
19
|
+
* @returns The key line, the source line, any warning, and up to six record rows.
|
|
20
20
|
*/
|
|
21
|
-
export declare function LoopMenu({ records, index }: {
|
|
21
|
+
export declare function LoopMenu({ records, index, source }: {
|
|
22
22
|
records: readonly LoopRecord[];
|
|
23
23
|
index: number;
|
|
24
|
+
source?: LoopSourceInfo;
|
|
24
25
|
}): import("react").JSX.Element;
|
|
25
26
|
/** The loop parameter form: one record's variables and defaults, editable in place, then a Start.
|
|
26
27
|
*
|
package/dist/ui/dialogs/loop.js
CHANGED
|
@@ -47,20 +47,28 @@ function labelWidth(rows) {
|
|
|
47
47
|
*
|
|
48
48
|
* It offers the same names and defaults the runner reads, so choosing from it cannot start a run
|
|
49
49
|
* other than the one the row describes.
|
|
50
|
-
* @param props - Matching records and the
|
|
51
|
-
* @returns The key line and up to six record rows.
|
|
50
|
+
* @param props - Matching records, the highlighted row, and where the records came from.
|
|
51
|
+
* @returns The key line, the source line, any warning, and up to six record rows.
|
|
52
52
|
*/
|
|
53
|
-
export function LoopMenu({ records, index }) {
|
|
53
|
+
export function LoopMenu({ records, index, source }) {
|
|
54
54
|
const theme = useTheme();
|
|
55
55
|
const start = Math.max(0, index - 5);
|
|
56
|
-
|
|
56
|
+
// Records are configuration now: whenever a user file supplied or replaced one, the list says so,
|
|
57
|
+
// because an operator who overrode a shipped record cannot tell the two apart from the name alone.
|
|
58
|
+
const origin = source?.file === undefined ? undefined : [
|
|
59
|
+
`Records from ${source.file}`,
|
|
60
|
+
...(source.overridden.length === 0 ? [] : [`replaced: ${source.overridden.join(', ')}`]),
|
|
61
|
+
...(source.added.length === 0 ? [] : [`added: ${source.added.join(', ')}`]),
|
|
62
|
+
].join(' · ');
|
|
63
|
+
return _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Loop records \u00B7 \u2191 \u2193 select \u00B7 Enter confirm defaults \u00B7 Tab finish the name \u00B7 Esc close" }), origin !== undefined && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: safeText(origin) }), (source?.warnings ?? []).map(warning => _jsx(Text, { color: theme.colors.error, wrap: "truncate-end", children: safeText(warning) }, warning)), records.slice(start, start + 6).map((record, offset) => {
|
|
57
64
|
const current = start + offset === index;
|
|
58
65
|
// The record's own inputs come before the artifact file: they are what the form will edit, and
|
|
59
66
|
// the first thing a narrow row must not lose.
|
|
60
67
|
const vars = Object.entries(record.vars).map(([name, value]) => `${name} ${value}`).join(' · ');
|
|
61
68
|
const target = vars === '' ? '' : ` · ${vars}`;
|
|
62
69
|
const artifact = record.artifact === undefined ? '' : ` · ${record.artifact}`;
|
|
63
|
-
|
|
70
|
+
const mine = record.fromFile === true ? ' · yours' : '';
|
|
71
|
+
return _jsxs(Text, { color: current ? theme.accent : undefined, wrap: "truncate-end", children: [current ? '❯ ' : ' ', record.name, " \u00B7 ", record.title, " \u00B7 ", record.steps, " rounds \u00B7 pass ", record.defaultScore, " \u00B7 \u2264", record.defaultTries, " tries", target, artifact, mine] }, record.name);
|
|
64
72
|
})] });
|
|
65
73
|
}
|
|
66
74
|
/** The loop parameter form: one record's variables and defaults, editable in place, then a Start.
|
|
@@ -207,7 +215,7 @@ export function LoopDialog({ record, enabled, onStart, onBack, onClose }) {
|
|
|
207
215
|
}
|
|
208
216
|
onStart({ limits: values, vars });
|
|
209
217
|
}, { isActive: !copyMode });
|
|
210
|
-
return _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { bold: true, children: ["Run loop record \u00B7 ", safeText(record.name), " \u00B7 Esc close"] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [record.steps, " rounds", record.artifact === undefined ? '' : ` · ${safeText(record.artifact)}`, " \u00B7 \u2191 \u2193 to a row, then type"] }), rows.map((item, index) => {
|
|
218
|
+
return _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { bold: true, children: ["Run loop record \u00B7 ", safeText(record.name), record.fromFile === true ? ' · your file' : '', " \u00B7 Esc close"] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [record.steps, " rounds", record.artifact === undefined ? '' : ` · ${safeText(record.artifact)}`, " \u00B7 \u2191 \u2193 to a row, then type"] }), rows.map((item, index) => {
|
|
211
219
|
const selected = index === row;
|
|
212
220
|
const cursor = selected ? '❯ ' : ' ';
|
|
213
221
|
if (item.kind === 'start' || item.kind === 'back')
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** UI-owned dialog state and the keyboard traits shared by routing and rendering. */
|
|
2
|
+
import { type SetStateAction } from 'react';
|
|
3
|
+
import type { HistoryPanel, ModelState, PanelName, RemovalTarget, SearchPanel } from '../../contracts.ts';
|
|
4
|
+
type SurfaceName = PanelName | 'peek';
|
|
5
|
+
interface PanelState {
|
|
6
|
+
thoughts: boolean;
|
|
7
|
+
queue: boolean;
|
|
8
|
+
prompts: boolean;
|
|
9
|
+
help: boolean;
|
|
10
|
+
cost: boolean;
|
|
11
|
+
status: boolean;
|
|
12
|
+
model?: ModelState;
|
|
13
|
+
history?: HistoryPanel;
|
|
14
|
+
search?: SearchPanel;
|
|
15
|
+
removal?: RemovalTarget;
|
|
16
|
+
loop?: {
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
interface Surface {
|
|
21
|
+
name: SurfaceName;
|
|
22
|
+
open: boolean;
|
|
23
|
+
arrows?: boolean;
|
|
24
|
+
blocksKeys?: boolean;
|
|
25
|
+
reserved?: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
/** The externally owned peek view participates in routing without duplicating its state. */
|
|
28
|
+
export declare function usePanels(options: {
|
|
29
|
+
sessionId: string | undefined;
|
|
30
|
+
pendingId: string | undefined;
|
|
31
|
+
peekOpen: boolean;
|
|
32
|
+
closePeek(): void;
|
|
33
|
+
}): {
|
|
34
|
+
state: PanelState;
|
|
35
|
+
openSurfaces: Surface[];
|
|
36
|
+
close: (name: SurfaceName) => void;
|
|
37
|
+
closeExcept: (keep?: SurfaceName) => void;
|
|
38
|
+
reservedKeys: string[];
|
|
39
|
+
openThoughts: (value: SetStateAction<boolean>) => void;
|
|
40
|
+
openQueue: (value: SetStateAction<boolean>) => void;
|
|
41
|
+
openPrompts: (value: SetStateAction<boolean>) => void;
|
|
42
|
+
setModelPanel: (value: SetStateAction<ModelState | undefined>) => void;
|
|
43
|
+
setHistoryPanel: (value: SetStateAction<HistoryPanel | undefined>) => void;
|
|
44
|
+
setSearchPanel: (value: SetStateAction<SearchPanel | undefined>) => void;
|
|
45
|
+
setRemoval: (value: SetStateAction<RemovalTarget | undefined>) => void;
|
|
46
|
+
setLoopForm: (value: SetStateAction<{
|
|
47
|
+
name: string;
|
|
48
|
+
} | undefined>) => void;
|
|
49
|
+
setHelp: (value: SetStateAction<boolean>) => void;
|
|
50
|
+
setCostExpanded: (value: SetStateAction<boolean>) => void;
|
|
51
|
+
setStatusExpanded: (value: SetStateAction<boolean>) => void;
|
|
52
|
+
};
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** UI-owned dialog state and the keyboard traits shared by routing and rendering. */
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
const EMPTY = { thoughts: false, queue: false, prompts: false, help: false, cost: false, status: false };
|
|
4
|
+
const TRAITS = [
|
|
5
|
+
{ name: 'queue', reserved: ['d'] },
|
|
6
|
+
{ name: 'prompts', arrows: true, blocksKeys: true, reserved: ['d', 'e'] },
|
|
7
|
+
...['removal', 'model', 'loop', 'thoughts', 'history', 'search'].map(name => ({ name, arrows: true, blocksKeys: true })),
|
|
8
|
+
...['help', 'cost', 'status'].map(name => ({ name, blocksKeys: true })),
|
|
9
|
+
];
|
|
10
|
+
/** The externally owned peek view participates in routing without duplicating its state. */
|
|
11
|
+
export function usePanels(options) {
|
|
12
|
+
const [state, setState] = useState(EMPTY);
|
|
13
|
+
const setter = (key) => (value) => {
|
|
14
|
+
setState(current => {
|
|
15
|
+
const next = typeof value === 'function' ? value(current[key]) : value;
|
|
16
|
+
return current[key] === next ? current : { ...current, [key]: next };
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
// Read-only client panels and a chosen removal target survive navigation as before.
|
|
21
|
+
setState(current => ({ ...EMPTY, help: current.help, cost: current.cost, status: current.status, removal: current.removal }));
|
|
22
|
+
}, [options.sessionId]);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
setState(current => ({ ...current, queue: false, loop: options.pendingId === undefined ? current.loop : undefined }));
|
|
25
|
+
}, [options.sessionId, options.pendingId]);
|
|
26
|
+
const surfaces = [
|
|
27
|
+
{ name: 'peek', open: options.peekOpen, arrows: true, blocksKeys: true },
|
|
28
|
+
...TRAITS.map(trait => ({ ...trait, open: trait.name === 'queue' ? state.queue && options.pendingId === undefined
|
|
29
|
+
: !!state[trait.name] })),
|
|
30
|
+
];
|
|
31
|
+
const openSurfaces = surfaces.filter(surface => surface.open);
|
|
32
|
+
function close(name) {
|
|
33
|
+
if (name === 'peek')
|
|
34
|
+
options.closePeek();
|
|
35
|
+
else
|
|
36
|
+
setState(current => ({ ...current, [name]: EMPTY[name] }));
|
|
37
|
+
}
|
|
38
|
+
function closeExcept(keep) {
|
|
39
|
+
if (keep !== 'peek')
|
|
40
|
+
options.closePeek();
|
|
41
|
+
setState(current => keep === undefined || keep === 'peek' ? EMPTY : { ...EMPTY, [keep]: current[keep] });
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
state, openSurfaces, close, closeExcept,
|
|
45
|
+
reservedKeys: [...new Set(openSurfaces.flatMap(surface => surface.reserved ?? []))],
|
|
46
|
+
openThoughts: setter('thoughts'), openQueue: setter('queue'), openPrompts: setter('prompts'),
|
|
47
|
+
setModelPanel: setter('model'), setHistoryPanel: setter('history'), setSearchPanel: setter('search'),
|
|
48
|
+
setRemoval: setter('removal'), setLoopForm: setter('loop'),
|
|
49
|
+
setHelp: setter('help'), setCostExpanded: setter('cost'), setStatusExpanded: setter('status'),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** A local editor borrows Enter until it is saved or dismissed. */
|
|
2
|
+
export interface ComposerIntent {
|
|
3
|
+
hint: string;
|
|
4
|
+
emptyNotice: string;
|
|
5
|
+
successNotice?: string;
|
|
6
|
+
commit(text: string): Promise<boolean>;
|
|
7
|
+
}
|
|
8
|
+
/** Identity of a submitted draft, even if a later edit has identical text. */
|
|
9
|
+
export interface DraftReceipt {
|
|
10
|
+
readonly sessionId: string | undefined;
|
|
11
|
+
readonly revision: number;
|
|
12
|
+
}
|
|
13
|
+
interface ComposerOptions {
|
|
14
|
+
sessionId: string | undefined;
|
|
15
|
+
/** Read at completion too, before React necessarily renders a session change. */
|
|
16
|
+
currentSession(): string | undefined;
|
|
17
|
+
resetRecall(): void;
|
|
18
|
+
messageStarted(): void;
|
|
19
|
+
notify(message: string): void;
|
|
20
|
+
}
|
|
21
|
+
/** Owns editor state only; command interpretation and keyboard routing stay in App. */
|
|
22
|
+
export declare function useComposer(options: ComposerOptions): {
|
|
23
|
+
input: string;
|
|
24
|
+
cursor: number;
|
|
25
|
+
intent: ComposerIntent | undefined;
|
|
26
|
+
setInput: (value: string, recalled?: boolean) => void;
|
|
27
|
+
setCursor: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
28
|
+
setIntent: (value: ComposerIntent | undefined) => void;
|
|
29
|
+
capture: () => DraftReceipt;
|
|
30
|
+
consume: (receipt: DraftReceipt) => void;
|
|
31
|
+
recallIfCurrent: (receipt: DraftReceipt, read: () => string) => void;
|
|
32
|
+
commit: (raw: string) => Promise<boolean>;
|
|
33
|
+
interactionChanged: (answerPending: boolean, pending: boolean) => void;
|
|
34
|
+
};
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** Session-owned draft, temporary editors and async submission ownership. */
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { errorText } from "../../text.js";
|
|
4
|
+
/** Owns editor state only; command interpretation and keyboard routing stay in App. */
|
|
5
|
+
export function useComposer(options) {
|
|
6
|
+
const [input, setInputValue] = useState('');
|
|
7
|
+
const [cursor, setCursor] = useState(0);
|
|
8
|
+
const [intent, publishIntent] = useState();
|
|
9
|
+
const current = useRef({ sessionId: options.sessionId, revision: 0, input: '', parked: '', intent: undefined });
|
|
10
|
+
const mounted = useRef(false);
|
|
11
|
+
const saving = useRef(new WeakSet());
|
|
12
|
+
function replace(value) {
|
|
13
|
+
current.current.input = value;
|
|
14
|
+
current.current.revision += 1;
|
|
15
|
+
setInputValue(value);
|
|
16
|
+
setCursor(value.length);
|
|
17
|
+
}
|
|
18
|
+
function setIntent(value) {
|
|
19
|
+
current.current.intent = value;
|
|
20
|
+
publishIntent(value);
|
|
21
|
+
}
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
mounted.current = true;
|
|
24
|
+
return () => { mounted.current = false; };
|
|
25
|
+
}, []);
|
|
26
|
+
useLayoutEffect(() => {
|
|
27
|
+
if (current.current.sessionId === options.sessionId)
|
|
28
|
+
return;
|
|
29
|
+
current.current.sessionId = options.sessionId;
|
|
30
|
+
current.current.parked = '';
|
|
31
|
+
setIntent(undefined);
|
|
32
|
+
replace('');
|
|
33
|
+
}, [options.sessionId]);
|
|
34
|
+
function setInput(value, recalled = false) {
|
|
35
|
+
if (!recalled)
|
|
36
|
+
options.resetRecall();
|
|
37
|
+
if (current.current.input === '' && value !== '' && !value.startsWith('/'))
|
|
38
|
+
options.messageStarted();
|
|
39
|
+
replace(value);
|
|
40
|
+
}
|
|
41
|
+
function capture() {
|
|
42
|
+
return { sessionId: current.current.sessionId, revision: current.current.revision };
|
|
43
|
+
}
|
|
44
|
+
function owns(receipt) {
|
|
45
|
+
return mounted.current && receipt.sessionId === options.currentSession()
|
|
46
|
+
&& receipt.sessionId === current.current.sessionId && receipt.revision === current.current.revision;
|
|
47
|
+
}
|
|
48
|
+
function consume(receipt) {
|
|
49
|
+
if (owns(receipt))
|
|
50
|
+
setInput('');
|
|
51
|
+
}
|
|
52
|
+
/** A slow recall must neither replace newer typing nor move its recall cursor. */
|
|
53
|
+
function recallIfCurrent(receipt, read) {
|
|
54
|
+
if (owns(receipt))
|
|
55
|
+
setInput(read(), true);
|
|
56
|
+
}
|
|
57
|
+
/** Returns true whenever an editor handled Enter, including an already pending save. */
|
|
58
|
+
async function commit(raw) {
|
|
59
|
+
const editor = current.current.intent;
|
|
60
|
+
if (!editor)
|
|
61
|
+
return false;
|
|
62
|
+
if (saving.current.has(editor))
|
|
63
|
+
return true;
|
|
64
|
+
const text = raw.trim();
|
|
65
|
+
if (!text) {
|
|
66
|
+
options.notify(editor.emptyNotice);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
const receipt = capture();
|
|
70
|
+
const active = () => mounted.current && current.current.intent === editor
|
|
71
|
+
&& receipt.sessionId === options.currentSession() && receipt.sessionId === current.current.sessionId;
|
|
72
|
+
saving.current.add(editor);
|
|
73
|
+
try {
|
|
74
|
+
if (await editor.commit(text) && active()) {
|
|
75
|
+
if (editor.successNotice)
|
|
76
|
+
options.notify(editor.successNotice);
|
|
77
|
+
// Continued typing remains in edit mode for the next explicit save.
|
|
78
|
+
if (owns(receipt)) {
|
|
79
|
+
setIntent(undefined);
|
|
80
|
+
consume(receipt);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (active())
|
|
86
|
+
options.notify(errorText(error));
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
saving.current.delete(editor);
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
/** Parking/restoring is not a recall or a new message, so it does not move the reader. */
|
|
94
|
+
function interactionChanged(answerPending, pending) {
|
|
95
|
+
if (answerPending) {
|
|
96
|
+
setIntent(undefined);
|
|
97
|
+
if (current.current.input !== '') {
|
|
98
|
+
current.current.parked = current.current.input;
|
|
99
|
+
replace('');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (!pending && current.current.parked !== '') {
|
|
103
|
+
const parked = current.current.parked;
|
|
104
|
+
current.current.parked = '';
|
|
105
|
+
replace(parked);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { input, cursor, intent, setInput, setCursor, setIntent, capture, consume, recallIfCurrent, commit, interactionChanged };
|
|
109
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type AuthorizeFacts, type DeferReason, type LineCommand } from '../../slash/index.ts';
|
|
2
|
+
interface DeferredFacts extends AuthorizeFacts {
|
|
3
|
+
sessionId: string | undefined;
|
|
4
|
+
/** Both the connection baseline and the selected conversation snapshot must be restored. */
|
|
5
|
+
ready: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface DeferredOptions {
|
|
8
|
+
facts(): DeferredFacts;
|
|
9
|
+
run(command: LineCommand): Promise<void>;
|
|
10
|
+
notify(message: string): void;
|
|
11
|
+
}
|
|
12
|
+
/** Owns FIFO and in-flight state, independent of Controller and the composer's current draft. */
|
|
13
|
+
export declare function useDeferredLines(options: DeferredOptions): {
|
|
14
|
+
enqueue(command: LineCommand, defer: DeferReason): void;
|
|
15
|
+
};
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Commands explicitly accepted by the operator, waiting for their execution policy. */
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { authorize } from "../../slash/index.js";
|
|
4
|
+
import { errorText } from "../../text.js";
|
|
5
|
+
/** Owns FIFO and in-flight state, independent of Controller and the composer's current draft. */
|
|
6
|
+
export function useDeferredLines(options) {
|
|
7
|
+
const [lines, setLines] = useState([]);
|
|
8
|
+
const draining = useRef(false);
|
|
9
|
+
const mounted = useRef(false);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
mounted.current = true;
|
|
12
|
+
return () => { mounted.current = false; };
|
|
13
|
+
}, []);
|
|
14
|
+
// Re-evaluate on UI publications, including connection/snapshot readiness and a completed run.
|
|
15
|
+
// A synchronous reservation prevents a re-render or StrictMode replay from starting it twice.
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const next = lines[0];
|
|
18
|
+
if (!next || draining.current)
|
|
19
|
+
return;
|
|
20
|
+
const facts = options.facts();
|
|
21
|
+
if (next.sessionId !== facts.sessionId) {
|
|
22
|
+
setLines(current => current.filter(line => line.sessionId === facts.sessionId));
|
|
23
|
+
options.notify('Dropped a queued line: the selected session changed');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!facts.ready || facts.foreground || (next.defer !== 'busy' && facts.during !== 'idle'))
|
|
27
|
+
return;
|
|
28
|
+
const verdict = authorize(next.command, facts);
|
|
29
|
+
if (verdict.allow && verdict.defer !== undefined)
|
|
30
|
+
return;
|
|
31
|
+
draining.current = true;
|
|
32
|
+
setLines(current => current.slice(1));
|
|
33
|
+
void (async () => {
|
|
34
|
+
try {
|
|
35
|
+
await options.run(verdict.allow ? verdict.command : verdict.error);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (mounted.current)
|
|
39
|
+
options.notify(errorText(error));
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
draining.current = false;
|
|
43
|
+
if (mounted.current)
|
|
44
|
+
setLines(current => [...current]);
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
enqueue(command, defer) {
|
|
50
|
+
const sessionId = options.facts().sessionId;
|
|
51
|
+
setLines(current => [...current, { command, defer, sessionId }]);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DraftReceipt } from './use-composer.ts';
|
|
2
|
+
interface RecallSource {
|
|
3
|
+
readonly hasMore: boolean;
|
|
4
|
+
readonly beforeSeq: number | undefined;
|
|
5
|
+
}
|
|
6
|
+
interface RecallOptions<Source extends RecallSource> {
|
|
7
|
+
source: Source;
|
|
8
|
+
draft: string;
|
|
9
|
+
capture(): DraftReceipt;
|
|
10
|
+
restore(receipt: DraftReceipt, read: () => string): void;
|
|
11
|
+
previous(draft: string): string;
|
|
12
|
+
refill(): boolean;
|
|
13
|
+
hasPrevious(): boolean;
|
|
14
|
+
older(signal: AbortSignal, source: Source): Promise<boolean>;
|
|
15
|
+
run(work: (signal: AbortSignal) => Promise<boolean>): Promise<boolean | undefined>;
|
|
16
|
+
notify(message: string): void;
|
|
17
|
+
}
|
|
18
|
+
/** No Controller or Transcript dependency: only the paging and recall capabilities it needs. */
|
|
19
|
+
export declare function useHistoryRecall<Source extends RecallSource>(options: RecallOptions<Source>): () => Promise<void>;
|
|
20
|
+
export {};
|