@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,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** The full-screen read-only view of one output source.
|
|
3
|
+
*
|
|
4
|
+
* A reader opens this to watch work that is not their conversation — a verifier's session, a subagent
|
|
5
|
+
* child, a local `!` run — without selecting it. The application follows the source and hands over laid
|
|
6
|
+
* out rows; this only draws them, so nothing here can write to what it is showing.
|
|
7
|
+
*/
|
|
8
|
+
import { Box, Text } from 'ink';
|
|
9
|
+
import { HistoryViewport } from "../chat/history-view.js";
|
|
10
|
+
import { elapsedTime } from "../chat/status.js";
|
|
11
|
+
import { useTheme } from "../theme/index.js";
|
|
12
|
+
/** One source's identity line: what it is, who made it, how it is doing. */
|
|
13
|
+
function metaLine(source, now) {
|
|
14
|
+
const parts = [
|
|
15
|
+
source.detail ?? (source.kind === 'session' ? 'session' : 'local process'),
|
|
16
|
+
source.state,
|
|
17
|
+
source.parentSessionId === undefined ? undefined : `from ${source.parentSessionId}`,
|
|
18
|
+
source.startedAt === undefined ? undefined
|
|
19
|
+
: source.state === 'running' ? `started ${new Date(source.startedAt).toLocaleTimeString()}`
|
|
20
|
+
: `ran ${elapsedTime((source.endedAt ?? now) - source.startedAt)}`,
|
|
21
|
+
];
|
|
22
|
+
return parts.filter((part) => part !== undefined).join(' · ');
|
|
23
|
+
}
|
|
24
|
+
/** Draw one read-only source: its header, its content viewport, and the scroll line.
|
|
25
|
+
* @param props - The source, its already-laid-out rows and the reader's position in them.
|
|
26
|
+
* @returns The panel, filling the body area it is given.
|
|
27
|
+
*/
|
|
28
|
+
export function PeekPanel({ source, error, rows, position, total, width, now, boxRef }) {
|
|
29
|
+
const theme = useTheme();
|
|
30
|
+
const end = Math.max(0, total - position);
|
|
31
|
+
const first = Math.max(1, end - rows.length + 1);
|
|
32
|
+
return _jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflowY: "hidden", marginY: 1, children: [_jsx(Text, { bold: true, color: theme.accent, wrap: "truncate-end", children: source.label }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [source.id, " \u00B7 ", metaLine(source, now)] }), error !== undefined && _jsx(Text, { color: theme.colors.error, wrap: "truncate-end", children: error }), _jsx(Text, { dimColor: true, children: '─'.repeat(Math.max(0, width - 4)) }), _jsx(Box, { ref: boxRef, flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflowY: "hidden", children: rows.length === 0
|
|
33
|
+
? _jsx(Text, { color: theme.colors.muted, children: source.state === 'running' ? 'No output yet.' : 'This source has no output.' })
|
|
34
|
+
: _jsx(HistoryViewport, { rows: rows }) }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: total === 0 ? 'Esc closes' : `Esc closes · ↑ ↓ / PgUp PgDn / wheel scroll · lines ${first}–${end} of ${total}` })] });
|
|
35
|
+
}
|
|
@@ -19,6 +19,8 @@ export interface Choice {
|
|
|
19
19
|
/** Trailing column, right-aligned and truncated from its start so a path keeps its tail. */
|
|
20
20
|
detail?: string;
|
|
21
21
|
action(): void;
|
|
22
|
+
/** `e` edits the row's own content, on surfaces that keep editable entries. */
|
|
23
|
+
edit?(): void;
|
|
22
24
|
remove?(): void;
|
|
23
25
|
}
|
|
24
26
|
/** Keyboard-driven list with paging and optional removal.
|
|
@@ -3,7 +3,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { Box, Text, useInput, useStdin, useStdout } from 'ink';
|
|
5
5
|
import stringWidth from 'string-width';
|
|
6
|
-
import { safeText } from "../../
|
|
6
|
+
import { safeText } from "../../text.js";
|
|
7
7
|
import { useCopyMode } from "../copy-mode.js";
|
|
8
8
|
import { useTheme } from "../theme/index.js";
|
|
9
9
|
/** Display width of one cell list, so wide characters count as two columns. */
|
|
@@ -34,6 +34,8 @@ export function Picker({ choices, enabled, canSelect, pageSize = 12, hint, width
|
|
|
34
34
|
setSelected(Math.max(0, current - 1));
|
|
35
35
|
else if (key.downArrow)
|
|
36
36
|
setSelected(Math.min(choices.length - 1, current + 1));
|
|
37
|
+
else if (_input === 'e' && !key.ctrl && !key.meta)
|
|
38
|
+
choices[current]?.edit?.();
|
|
37
39
|
else if (_input === 'd' && !key.ctrl && !key.meta || key.delete && /^\x1b\[3(?:;\d+)?~$/.test(rawKey.current))
|
|
38
40
|
choices[current]?.remove?.();
|
|
39
41
|
else if (key.return)
|
|
@@ -66,5 +68,5 @@ export function Picker({ choices, enabled, canSelect, pageSize = 12, hint, width
|
|
|
66
68
|
if (!table || !isTableRow)
|
|
67
69
|
return _jsxs(Text, { color: isCurrent ? theme.accent : undefined, wrap: "truncate-end", children: [cursor, safeText(choice.label)] }, choice.key);
|
|
68
70
|
return _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: titleColumn, flexShrink: 0, children: _jsxs(Text, { color: isCurrent ? theme.accent : undefined, wrap: "truncate-end", children: [cursor, safeText(choice.title ?? choice.label)] }) }), _jsx(Box, { width: cellColumn, flexShrink: 0, children: _jsx(Text, { wrap: "truncate-end", children: choice.cells?.map((cell, position) => _jsx(Text, { color: cell.color, bold: cell.bold, children: safeText(cell.text) }, position)) }) }), detailShown && choice.detail !== undefined && _jsx(Box, { width: detailColumn, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { dimColor: true, wrap: "truncate-start", children: safeText(choice.detail) }) })] }, choice.key);
|
|
69
|
-
}), _jsx(Text, { dimColor: true, children: hint ?? `↑ ↓ select · Enter open${choices.some(choice => choice.remove) ? ' · d/Delete remove / archive' : ''} · Ctrl+C stop / exit` })] });
|
|
71
|
+
}), _jsx(Text, { dimColor: true, children: hint ?? `↑ ↓ select · Enter open${choices.some(choice => choice.edit) ? ' · e edit' : ''}${choices.some(choice => choice.remove) ? ' · d/Delete remove / archive' : ''} · Ctrl+C stop / exit` })] });
|
|
70
72
|
}
|
|
@@ -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
|
+
}
|
package/dist/ui/input/mouse.d.ts
CHANGED
|
@@ -8,9 +8,19 @@ export declare function isMouseReport(raw: string): boolean;
|
|
|
8
8
|
* @returns Positive for older history, negative for newer history, or zero.
|
|
9
9
|
*/
|
|
10
10
|
export declare function wheelDirection(raw: string): number;
|
|
11
|
+
/** One unmodified left press, in the terminal's own 1-based cells. */
|
|
12
|
+
export interface MousePress {
|
|
13
|
+
column: number;
|
|
14
|
+
row: number;
|
|
15
|
+
}
|
|
16
|
+
/** Decode an unmodified left press; releases, motion and modified presses are ignored.
|
|
17
|
+
* @param raw - One complete SGR mouse report.
|
|
18
|
+
* @returns The pressed cell, or undefined.
|
|
19
|
+
*/
|
|
20
|
+
export declare function mousePress(raw: string): MousePress | undefined;
|
|
11
21
|
/** Enable cell-based mouse reports for this mount and restore normal terminal behavior on exit.
|
|
12
22
|
* @param scroll - Current transcript scrolling callback.
|
|
13
23
|
* @param enabled - False restores native terminal selection while display updates are paused.
|
|
14
|
-
* @param
|
|
24
|
+
* @param press - Left-press callback, given the terminal cell it landed on.
|
|
15
25
|
*/
|
|
16
|
-
export declare function useMouseWheel(scroll: (direction: number) => void, enabled?: boolean,
|
|
26
|
+
export declare function useMouseWheel(scroll: (direction: number) => void, enabled?: boolean, press?: (cell: MousePress) => void): void;
|
package/dist/ui/input/mouse.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** SGR mouse reporting, shared by transcript scrolling and input suppression. */
|
|
1
|
+
/** SGR mouse reporting, shared by transcript scrolling, row clicks and input suppression. */
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
3
|
import { useStdin, useStdout } from 'ink';
|
|
4
4
|
/** Recognize complete SGR reports so clicks and wheel bytes never become prompt text.
|
|
@@ -18,18 +18,28 @@ export function wheelDirection(raw) {
|
|
|
18
18
|
const base = button & ~28;
|
|
19
19
|
return base === 64 ? 1 : base === 65 ? -1 : 0;
|
|
20
20
|
}
|
|
21
|
+
/** Decode an unmodified left press; releases, motion and modified presses are ignored.
|
|
22
|
+
* @param raw - One complete SGR mouse report.
|
|
23
|
+
* @returns The pressed cell, or undefined.
|
|
24
|
+
*/
|
|
25
|
+
export function mousePress(raw) {
|
|
26
|
+
const match = /^\x1b\[<0;(\d+);(\d+)M$/.exec(raw);
|
|
27
|
+
if (!match)
|
|
28
|
+
return undefined;
|
|
29
|
+
return { column: Number(match[1]), row: Number(match[2]) };
|
|
30
|
+
}
|
|
21
31
|
/** Enable cell-based mouse reports for this mount and restore normal terminal behavior on exit.
|
|
22
32
|
* @param scroll - Current transcript scrolling callback.
|
|
23
33
|
* @param enabled - False restores native terminal selection while display updates are paused.
|
|
24
|
-
* @param
|
|
34
|
+
* @param press - Left-press callback, given the terminal cell it landed on.
|
|
25
35
|
*/
|
|
26
|
-
export function useMouseWheel(scroll, enabled = true,
|
|
36
|
+
export function useMouseWheel(scroll, enabled = true, press) {
|
|
27
37
|
const { internal_eventEmitter } = useStdin();
|
|
28
38
|
const { stdout } = useStdout();
|
|
29
39
|
const callback = useRef(scroll);
|
|
30
40
|
callback.current = scroll;
|
|
31
|
-
const
|
|
32
|
-
|
|
41
|
+
const onPress = useRef(press);
|
|
42
|
+
onPress.current = press;
|
|
33
43
|
useEffect(() => {
|
|
34
44
|
if (!enabled)
|
|
35
45
|
return;
|
|
@@ -37,8 +47,11 @@ export function useMouseWheel(scroll, enabled = true, select) {
|
|
|
37
47
|
const direction = wheelDirection(raw);
|
|
38
48
|
if (direction)
|
|
39
49
|
callback.current(direction);
|
|
40
|
-
else
|
|
41
|
-
|
|
50
|
+
else {
|
|
51
|
+
const cell = mousePress(raw);
|
|
52
|
+
if (cell !== undefined)
|
|
53
|
+
onPress.current?.(cell);
|
|
54
|
+
}
|
|
42
55
|
};
|
|
43
56
|
internal_eventEmitter.on('input', onInput);
|
|
44
57
|
if (stdout.isTTY)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FileReference } from '../../
|
|
1
|
+
import type { FileReference } from '../../contracts.ts';
|
|
2
2
|
/** Candidates for the unfinished reference at the end of the draft.
|
|
3
3
|
* @param props - Lookup result and the highlighted candidate.
|
|
4
4
|
* @returns The completion menu.
|
|
@@ -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 {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Lazy prompt recall owns its scan; the composer decides whether the result still belongs to it. */
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
import { errorText } from "../../text.js";
|
|
4
|
+
/** Bound one key press even when many tool-only pages separate user messages. */
|
|
5
|
+
const PAGE_SCAN_LIMIT = 20;
|
|
6
|
+
/** No Controller or Transcript dependency: only the paging and recall capabilities it needs. */
|
|
7
|
+
export function useHistoryRecall(options) {
|
|
8
|
+
const loading = useRef(false);
|
|
9
|
+
return async () => {
|
|
10
|
+
if (loading.current)
|
|
11
|
+
return;
|
|
12
|
+
const receipt = options.capture();
|
|
13
|
+
const { source, draft } = options;
|
|
14
|
+
const restore = () => options.restore(receipt, () => options.previous(draft));
|
|
15
|
+
if (options.refill() || options.hasPrevious() || !source.hasMore) {
|
|
16
|
+
restore();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
loading.current = true;
|
|
20
|
+
try {
|
|
21
|
+
const found = await options.run(async (signal) => {
|
|
22
|
+
for (let page = 0; page < PAGE_SCAN_LIMIT; page++) {
|
|
23
|
+
signal.throwIfAborted();
|
|
24
|
+
if (source.beforeSeq === undefined || !source.hasMore)
|
|
25
|
+
break;
|
|
26
|
+
if (!await options.older(signal, source))
|
|
27
|
+
break;
|
|
28
|
+
signal.throwIfAborted();
|
|
29
|
+
// Startup backfill may have inserted the same prompts while this page was in flight.
|
|
30
|
+
if (options.refill() || options.hasPrevious())
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
});
|
|
35
|
+
if (found)
|
|
36
|
+
restore();
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
// Foreground cancellation is an expected end to a user-triggered scan.
|
|
40
|
+
if (!(error instanceof Error && error.name === 'AbortError'))
|
|
41
|
+
options.notify(errorText(error));
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
loading.current = false;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Presentation helpers for the status and cost panels. */
|
|
2
|
+
import type { CostTotal } from '../../contracts.ts';
|
|
3
|
+
/** Render a cost total with a marker while any record behind it is unpriced.
|
|
4
|
+
* @param total - Folded session or day total.
|
|
5
|
+
* @returns The estimate, prefixed `~` because it is derived from usage, not an invoice.
|
|
6
|
+
*/
|
|
7
|
+
export declare function costText(total: CostTotal): string;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Render a cost total with a marker while any record behind it is unpriced.
|
|
2
|
+
* @param total - Folded session or day total.
|
|
3
|
+
* @returns The estimate, prefixed `~` because it is derived from usage, not an invoice.
|
|
4
|
+
*/
|
|
5
|
+
export function costText(total) { return `~¥${total.amount.toFixed(4)}${total.unknown ? '*' : ''}`; }
|
package/dist/ui/theme/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itookit/dsht",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Remote-first TUI for DeepSeek Harness with SSH-friendly mobile access and request-level cost tracking",
|
|
6
6
|
"engines": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"start": "tsx src/cli/index.ts",
|
|
29
29
|
"start:profile": "node -e \"require('node:fs').mkdirSync('.diagnostics',{recursive:true})\" && NODE_OPTIONS=\"--expose-gc --heapsnapshot-signal=SIGUSR2 --diagnostic-dir=.diagnostics\" tsx src/cli/index.ts",
|
|
30
|
-
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
|
|
30
|
+
"build": "npm run build:prompts && node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
|
|
31
31
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.tests.json",
|
|
32
32
|
"test": "tsx --test tests/*/*.test.ts tests/*/*.test.tsx",
|
|
33
33
|
"test:terminal": "node scripts/test/terminal.mjs",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"prepack": "npm run build",
|
|
36
36
|
"prepublishOnly": "npm run typecheck && npm run test:terminal",
|
|
37
37
|
"bench:input": "tsx scripts/benchmarks/input.tsx",
|
|
38
|
-
"bench:history": "node --import tsx scripts/benchmarks/history.tsx"
|
|
38
|
+
"bench:history": "node --import tsx scripts/benchmarks/history.tsx",
|
|
39
|
+
"build:prompts": "tsx scripts/build/loop-prompts.mts"
|
|
39
40
|
},
|
|
40
41
|
"keywords": [
|
|
41
42
|
"deepseek",
|
|
@@ -84,6 +85,7 @@
|
|
|
84
85
|
"@types/ws": "^8.18.1",
|
|
85
86
|
"ink-testing-library": "^4.0.0",
|
|
86
87
|
"tsx": "^4.22.0",
|
|
87
|
-
"typescript": "^5.9.3"
|
|
88
|
+
"typescript": "^5.9.3",
|
|
89
|
+
"yaml": "^2.9.1"
|
|
88
90
|
}
|
|
89
91
|
}
|