dsh-ssh-tui 0.6.4 → 0.7.0-rc.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.en.md +14 -0
- package/README.md +14 -0
- package/docs/remote-ops.md +133 -0
- package/docs/screenshots/compare.png +0 -0
- package/docs/screenshots/headless.png +0 -0
- package/docs/screenshots/workspace.png +0 -0
- package/lib/approval-cache.js +158 -0
- package/lib/approval-cache.js.map +1 -0
- package/lib/attach.js +66 -7
- package/lib/attach.js.map +1 -1
- package/lib/auto-approval.js +13 -0
- package/lib/auto-approval.js.map +1 -1
- package/lib/color-depth.js +192 -0
- package/lib/color-depth.js.map +1 -0
- package/lib/commands.js +5 -0
- package/lib/commands.js.map +1 -1
- package/lib/copy-text.js +1 -0
- package/lib/copy-text.js.map +1 -1
- package/lib/dialogs.js +81 -8
- package/lib/dialogs.js.map +1 -1
- package/lib/display-sock.js +24 -8
- package/lib/display-sock.js.map +1 -1
- package/lib/doctor.js +380 -0
- package/lib/doctor.js.map +1 -0
- package/lib/footer.js +76 -6
- package/lib/footer.js.map +1 -1
- package/lib/i18n/en.js +136 -3
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/zh.js +137 -4
- package/lib/i18n/zh.js.map +1 -1
- package/lib/index.js +8 -1
- package/lib/index.js.map +1 -1
- package/lib/keymap.js +172 -0
- package/lib/keymap.js.map +1 -0
- package/lib/line-diff.js +224 -0
- package/lib/line-diff.js.map +1 -0
- package/lib/line-mode.js +86 -0
- package/lib/line-mode.js.map +1 -0
- package/lib/paint.js +158 -26
- package/lib/paint.js.map +1 -1
- package/lib/plan.js +51 -8
- package/lib/plan.js.map +1 -1
- package/lib/preset-authoring.js +110 -0
- package/lib/preset-authoring.js.map +1 -0
- package/lib/preset-picker.js +105 -0
- package/lib/preset-picker.js.map +1 -0
- package/lib/preset-rows.js +288 -34
- package/lib/preset-rows.js.map +1 -1
- package/lib/selection.js +154 -0
- package/lib/selection.js.map +1 -0
- package/lib/term-text.js +106 -1
- package/lib/term-text.js.map +1 -1
- package/lib/tool-present.js +157 -21
- package/lib/tool-present.js.map +1 -1
- package/lib/tui.js +1428 -97
- package/lib/tui.js.map +1 -1
- package/lib/types/approval-cache.d.ts +125 -0
- package/lib/types/attach.d.ts +29 -0
- package/lib/types/auto-approval.d.ts +11 -0
- package/lib/types/color-depth.d.ts +50 -0
- package/lib/types/commands.d.ts +9 -0
- package/lib/types/dialogs.d.ts +36 -5
- package/lib/types/display-sock.d.ts +6 -0
- package/lib/types/doctor.d.ts +118 -0
- package/lib/types/footer.d.ts +35 -0
- package/lib/types/keymap.d.ts +68 -0
- package/lib/types/line-diff.d.ts +107 -0
- package/lib/types/line-mode.d.ts +36 -0
- package/lib/types/paint.d.ts +89 -4
- package/lib/types/plan.d.ts +7 -0
- package/lib/types/preset-authoring.d.ts +135 -0
- package/lib/types/preset-picker.d.ts +64 -0
- package/lib/types/preset-rows.d.ts +113 -9
- package/lib/types/selection.d.ts +81 -0
- package/lib/types/term-text.d.ts +29 -0
- package/lib/types/tool-present.d.ts +53 -3
- package/lib/types/transcript-types.d.ts +12 -2
- package/lib/types/tui.d.ts +231 -2
- package/package.json +33 -32
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A TTL cache for approval verdicts the AI reviewer already produced.
|
|
3
|
+
*
|
|
4
|
+
* `/approval auto` hands every shape the rule table cannot judge to a
|
|
5
|
+
* subagent-configured model: one `llm.stream` call, up to 15 seconds, and the
|
|
6
|
+
* user's quota. The same shape repeats inside one session — `pytest -q`,
|
|
7
|
+
* `npm install`, `docker compose up` — and each repeat paid the full review
|
|
8
|
+
* again. This module remembers a verdict for a bounded time so an identical
|
|
9
|
+
* request is decided from the cache.
|
|
10
|
+
*
|
|
11
|
+
* Two rules keep that safe, and both are structural rather than heuristic:
|
|
12
|
+
*
|
|
13
|
+
* 1. Only requests the classifier marked `ask` are ever looked up or stored.
|
|
14
|
+
* Rule allows and denials are free, and `DANGER_PATTERNS` returns before the
|
|
15
|
+
* cache is consulted at all, so a remembered verdict can never widen what
|
|
16
|
+
* the rule table refuses.
|
|
17
|
+
* 2. The key is an exact identity, not a prefix: the tool, the canonical
|
|
18
|
+
* command, its raw arguments, the sandbox mode, the workspace, the locale,
|
|
19
|
+
* the reviewer that produced the verdict, and a fingerprint of the user
|
|
20
|
+
* message that authorized it. A verdict handed out while `npm publish` was
|
|
21
|
+
* authorized never answers `npm publish --tag next`, and an escalation
|
|
22
|
+
* justified in one message is not replayed for the next one.
|
|
23
|
+
*
|
|
24
|
+
* Opaque interpreter payloads (`bash -c`, `python -e`) are excluded on purpose:
|
|
25
|
+
* the review result there depends entirely on a script the key cannot see.
|
|
26
|
+
* @module dsh-ssh-tui/approval-cache
|
|
27
|
+
*/
|
|
28
|
+
import type { ReviewVerdict } from './approval-reviewer.js';
|
|
29
|
+
/** How long one remembered verdict stays usable. */
|
|
30
|
+
export declare const DEFAULT_VERDICT_TTL_MS: number;
|
|
31
|
+
/** How many verdicts are remembered at once; the least recently used go first. */
|
|
32
|
+
export declare const DEFAULT_VERDICT_CAPACITY = 32;
|
|
33
|
+
/**
|
|
34
|
+
* Everything the reviewer's verdict actually depends on.
|
|
35
|
+
*
|
|
36
|
+
* `args` and `authorization` are load-bearing: the `workspace-write` escalation
|
|
37
|
+
* is allowed purely because the model read a justification out of the request,
|
|
38
|
+
* and `latestUserAuthorizationText` is what the reviewer weighs when it decides
|
|
39
|
+
* whether the user asked for this. Dropping either would let one authorization
|
|
40
|
+
* cover a later, different call.
|
|
41
|
+
*/
|
|
42
|
+
export interface VerdictKeyInput {
|
|
43
|
+
toolName: string;
|
|
44
|
+
command: string;
|
|
45
|
+
args?: string | undefined;
|
|
46
|
+
reason?: string | undefined;
|
|
47
|
+
sandboxMode?: string | undefined;
|
|
48
|
+
/** The agent the request came from: a subagent's authorization is its own. */
|
|
49
|
+
agentId: string;
|
|
50
|
+
workspaceCwd: string;
|
|
51
|
+
locale: string;
|
|
52
|
+
/** The model that produced the verdict; a different reviewer may differ. */
|
|
53
|
+
reviewer: {
|
|
54
|
+
provider: string;
|
|
55
|
+
model: string;
|
|
56
|
+
};
|
|
57
|
+
/** One-way fingerprint of the user message that authorized the request. */
|
|
58
|
+
authorization: string;
|
|
59
|
+
}
|
|
60
|
+
/** Whitespace-insensitive form of a command, so spacing alone cannot miss. */
|
|
61
|
+
export declare function canonicalCommand(command: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Whether a verdict for this request may be remembered.
|
|
64
|
+
*
|
|
65
|
+
* A request without a command has nothing to key on, and an interpreter
|
|
66
|
+
* wrapper hides its payload behind `-c`/`-e`, so its review says nothing about
|
|
67
|
+
* the next invocation of the same wrapper.
|
|
68
|
+
* @param input - the identity a verdict would be stored under.
|
|
69
|
+
* @returns whether the cache may serve and store this shape.
|
|
70
|
+
*/
|
|
71
|
+
export declare function cacheableShape(input: VerdictKeyInput): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* The cache key for one reviewed request.
|
|
74
|
+
* @param input - the identity a verdict would be stored under.
|
|
75
|
+
* @returns a stable, collision-resistant digest.
|
|
76
|
+
*/
|
|
77
|
+
export declare function verdictKey(input: VerdictKeyInput): string;
|
|
78
|
+
export interface CachedVerdict {
|
|
79
|
+
verdict: ReviewVerdict;
|
|
80
|
+
storedAt: number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A bounded, time-limited map from a reviewed request to its verdict.
|
|
84
|
+
*
|
|
85
|
+
* Both limits are injectable so tests can drive the clock instead of sleeping.
|
|
86
|
+
* The instance is per TUI: a fresh session starts with an empty cache, and
|
|
87
|
+
* nothing is written to disk.
|
|
88
|
+
*/
|
|
89
|
+
export declare class ApprovalVerdictCache {
|
|
90
|
+
private readonly options;
|
|
91
|
+
private readonly entries;
|
|
92
|
+
private hitCount;
|
|
93
|
+
constructor(options?: {
|
|
94
|
+
ttlMs?: number;
|
|
95
|
+
capacity?: number;
|
|
96
|
+
now?: () => number;
|
|
97
|
+
});
|
|
98
|
+
private get ttlMs();
|
|
99
|
+
private get capacity();
|
|
100
|
+
private get now();
|
|
101
|
+
/**
|
|
102
|
+
* The remembered verdict for a key, if it is still inside its TTL.
|
|
103
|
+
* @param key - {@link verdictKey} of the request being decided.
|
|
104
|
+
* @returns the verdict and its age, or `undefined` for a miss or an expiry.
|
|
105
|
+
*/
|
|
106
|
+
lookup(key: string): {
|
|
107
|
+
verdict: ReviewVerdict;
|
|
108
|
+
ageMs: number;
|
|
109
|
+
} | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* Remember one reviewed verdict, evicting the least recently used entry past
|
|
112
|
+
* the capacity.
|
|
113
|
+
* @param key - {@link verdictKey} of the reviewed request.
|
|
114
|
+
* @param verdict - what the reviewer concluded.
|
|
115
|
+
*/
|
|
116
|
+
store(key: string, verdict: ReviewVerdict): void;
|
|
117
|
+
/** Drop every remembered verdict; returns how many were held. */
|
|
118
|
+
clear(): number;
|
|
119
|
+
/** How many verdicts are held right now. */
|
|
120
|
+
get size(): number;
|
|
121
|
+
/** How many lookups were served from the cache over this instance's life. */
|
|
122
|
+
get hits(): number;
|
|
123
|
+
/** The TTL as whole minutes, for the status line. */
|
|
124
|
+
get ttlMinutes(): number;
|
|
125
|
+
}
|
package/lib/types/attach.d.ts
CHANGED
|
@@ -22,6 +22,26 @@ export declare const RECOVERY_BURST_WINDOW_MS = 10000;
|
|
|
22
22
|
export declare const RECOVERY_BURST_LIMIT = 3;
|
|
23
23
|
/** How long to wait for a freshly spawned Host to accept on its channel. */
|
|
24
24
|
export declare const HOST_START_TIMEOUT_MS = 15000;
|
|
25
|
+
/**
|
|
26
|
+
* Waits between the automatic re-attach attempts of one run.
|
|
27
|
+
*
|
|
28
|
+
* A single retry was not enough for a link that flaps for half a minute: the
|
|
29
|
+
* burst breaker cut the run at three tries inside ten seconds and told the user
|
|
30
|
+
* to come back later, when one more try would have caught the link. One run now
|
|
31
|
+
* backs off — 250ms, 500ms, 1s, 2s, 4s — and only then gives up; the breaker
|
|
32
|
+
* still stops *runs* from fighting each other, which is what it was for.
|
|
33
|
+
*/
|
|
34
|
+
export declare const RECOVERY_BACKOFF_MS: readonly [250, 500, 1000, 2000, 4000];
|
|
35
|
+
/**
|
|
36
|
+
* How long to wait before attempt `attempt` (0-based), with jitter.
|
|
37
|
+
*
|
|
38
|
+
* Two windows that drop together must not retry in lockstep, so each wait is
|
|
39
|
+
* taken from the upper half of its base: 0.5–1× the nominal delay.
|
|
40
|
+
* @param attempt - 0 for the first retry after the initial failure.
|
|
41
|
+
* @param random - injectable for tests.
|
|
42
|
+
* @returns milliseconds to wait.
|
|
43
|
+
*/
|
|
44
|
+
export declare function recoveryDelay(attempt: number, random?: () => number): number;
|
|
25
45
|
/** True when a relay error means the peer vanished rather than a real fault. */
|
|
26
46
|
export declare function attachPeerVanished(error: unknown, elapsedMs: number): boolean;
|
|
27
47
|
export interface LiveHost {
|
|
@@ -86,6 +106,15 @@ export interface AttacherDeps {
|
|
|
86
106
|
windowMs?: number;
|
|
87
107
|
limit?: number;
|
|
88
108
|
};
|
|
109
|
+
/** Override the backoff schedule (tests); defaults to {@link RECOVERY_BACKOFF_MS}. */
|
|
110
|
+
backoff?: readonly number[];
|
|
111
|
+
/** Jitter source for {@link recoveryDelay} (tests). */
|
|
112
|
+
random?: () => number;
|
|
113
|
+
/** One run recovered after at least one retry; the Host side reports it. */
|
|
114
|
+
onReconnected?(info: {
|
|
115
|
+
attempts: number;
|
|
116
|
+
elapsedMs: number;
|
|
117
|
+
}): void;
|
|
89
118
|
}
|
|
90
119
|
export interface Attacher {
|
|
91
120
|
/**
|
|
@@ -47,6 +47,17 @@ export declare function touchesSensitivePath(text: string): boolean;
|
|
|
47
47
|
* pattern — unknown shapes ask (and detach to a rejection when unattended).
|
|
48
48
|
*/
|
|
49
49
|
export declare function classifyCommand(command: string): ApprovalDecision;
|
|
50
|
+
/**
|
|
51
|
+
* Whether a command is an opaque interpreter payload (`bash -c …`, `python -e …`).
|
|
52
|
+
*
|
|
53
|
+
* The rule table asks about these instead of denying them (a quoted `rm -rf`
|
|
54
|
+
* must not match `DANGER_PATTERNS`), and the AI reviewer judges the script it is
|
|
55
|
+
* shown — so its verdict says nothing about the next invocation of the same
|
|
56
|
+
* wrapper. The approval cache uses this to refuse to remember them.
|
|
57
|
+
* @param command - the command string a request carried.
|
|
58
|
+
* @returns whether the payload, not the command line, decides the risk.
|
|
59
|
+
*/
|
|
60
|
+
export declare function isOpaqueInterpreterCommand(command: string): boolean;
|
|
50
61
|
export declare function classifyCommandDetailed(command: string): ClassifiedApproval;
|
|
51
62
|
export interface ClassifyApprovalInput {
|
|
52
63
|
toolName: string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much colour the terminal can take, and what to do when it cannot take
|
|
3
|
+
* what a caller asked for.
|
|
4
|
+
*
|
|
5
|
+
* A truecolor pair (`38;2;…`) on an 8-colour terminal is not a cosmetic loss: a
|
|
6
|
+
* terminal that does not understand it either ignores the sequence or renders
|
|
7
|
+
* the row in the wrong colour, and the diff rows are the ones that suffer. The
|
|
8
|
+
* palette is downgraded here instead, by hue family rather than by nearest RGB
|
|
9
|
+
* distance — the diff colours are deliberately muted, and nearest-RGB maps them
|
|
10
|
+
* to grey, which throws away the only signal they carry. Status is never colour
|
|
11
|
+
* alone in this TUI (a `+`/`-`/`●`/`⚠`/`✖` is always present), so a downgraded
|
|
12
|
+
* palette stays readable.
|
|
13
|
+
* @module dsh-ssh-tui/color-depth
|
|
14
|
+
*/
|
|
15
|
+
/** What the terminal advertised, or what the user forced. */
|
|
16
|
+
export type ColorDepth = 'truecolor' | '256' | '8' | 'none';
|
|
17
|
+
/**
|
|
18
|
+
* Decide the palette from the environment.
|
|
19
|
+
*
|
|
20
|
+
* `DSH_TUI_COLOR_DEPTH` wins outright so a user can correct a wrong guess.
|
|
21
|
+
* Otherwise: no colour at all when `NO_COLOR` is set or `TERM` names a
|
|
22
|
+
* monochrome terminal; truecolor when `COLORTERM` says so; 256 for a
|
|
23
|
+
* `*256color` terminal and for tmux/screen (their default palette is 256 and
|
|
24
|
+
* they translate truecolor poorly); 8 for everything else.
|
|
25
|
+
* @param env - the environment to read (tests pass their own).
|
|
26
|
+
* @returns the palette to paint with.
|
|
27
|
+
*/
|
|
28
|
+
export declare function colorDepth(env?: NodeJS.ProcessEnv): ColorDepth;
|
|
29
|
+
/** The xterm 256-colour cube index back to RGB. */
|
|
30
|
+
export declare function rgbFrom256(index: number): [number, number, number];
|
|
31
|
+
/**
|
|
32
|
+
* The nearest standard colour, by hue family.
|
|
33
|
+
*
|
|
34
|
+
* Brightness picks the bright half of the palette; a colour whose channels are
|
|
35
|
+
* nearly equal becomes a grey. Everything else keeps its hue, so a muted green
|
|
36
|
+
* stays green instead of collapsing to grey the way nearest-RGB would leave it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function basicSgrFor(r: number, g: number, b: number): number;
|
|
39
|
+
/**
|
|
40
|
+
* Rewrite one SGR parameter list for the palette in use.
|
|
41
|
+
*
|
|
42
|
+
* Attributes (bold, dim, italic, underline, reverse) always survive: they are
|
|
43
|
+
* what carries meaning when colour cannot. Colour parameters are dropped at
|
|
44
|
+
* `none`, mapped to the standard 16 at `8`, and mapped into the 256-cube at
|
|
45
|
+
* `256`.
|
|
46
|
+
* @param code - the parameter list, e.g. `1;38;2;77;107;253`.
|
|
47
|
+
* @param depth - the palette to rewrite for.
|
|
48
|
+
* @returns the parameter list to emit, possibly empty.
|
|
49
|
+
*/
|
|
50
|
+
export declare function downgradeSgr(code: string, depth: ColorDepth): string;
|
package/lib/types/commands.d.ts
CHANGED
|
@@ -75,6 +75,15 @@ export declare const LOCAL_COMMANDS: readonly [{
|
|
|
75
75
|
readonly name: "lang";
|
|
76
76
|
readonly key: "cmd.language";
|
|
77
77
|
readonly aliasOf: "language";
|
|
78
|
+
}, {
|
|
79
|
+
readonly name: "doctor";
|
|
80
|
+
readonly key: "cmd.doctor";
|
|
81
|
+
}, {
|
|
82
|
+
readonly name: "fix";
|
|
83
|
+
readonly key: "cmd.fix";
|
|
84
|
+
}, {
|
|
85
|
+
readonly name: "preset";
|
|
86
|
+
readonly key: "cmd.preset";
|
|
78
87
|
}, {
|
|
79
88
|
readonly name: "dialog-test";
|
|
80
89
|
readonly key: "cmd.dialog-test";
|
package/lib/types/dialogs.d.ts
CHANGED
|
@@ -22,6 +22,21 @@ export interface QuestionDialog {
|
|
|
22
22
|
total: number;
|
|
23
23
|
selected: Set<number>;
|
|
24
24
|
cursor: number;
|
|
25
|
+
/**
|
|
26
|
+
* The typed filter, once the user pressed `/`. Letters and digits are the
|
|
27
|
+
* list's hotkeys, so filtering is entered explicitly instead of hijacking
|
|
28
|
+
* every keystroke — the two would otherwise fight over the same keys.
|
|
29
|
+
*/
|
|
30
|
+
filter?: string;
|
|
31
|
+
/** True while the filter is being typed; Enter applies it, Esc clears it. */
|
|
32
|
+
filtering?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Machine names for the options, indexed like `question.options`. The option
|
|
35
|
+
* shape belongs to the questions package and carries only display text, so a
|
|
36
|
+
* preset id would otherwise be unmatchable — and `routing-suite` is exactly
|
|
37
|
+
* what a user types to find it.
|
|
38
|
+
*/
|
|
39
|
+
matchKeys?: readonly string[];
|
|
25
40
|
resolve(selection: {
|
|
26
41
|
selected: string[];
|
|
27
42
|
custom?: string;
|
|
@@ -51,6 +66,21 @@ export declare function optionsLength(dialog: Dialog): number;
|
|
|
51
66
|
* the painted keys or names no option.
|
|
52
67
|
*/
|
|
53
68
|
export declare function questionOptionIndex(key: string, optionCount: number): number | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* The option indexes the current filter leaves visible, in list order.
|
|
71
|
+
*
|
|
72
|
+
* An empty or absent filter keeps everything. The match is the same shape the
|
|
73
|
+
* pickers use: the label or the description, case-insensitively.
|
|
74
|
+
*/
|
|
75
|
+
export declare function visibleQuestionIndexes(dialog: QuestionDialog): number[];
|
|
76
|
+
/** Append a typed character to the filter (filter mode only). */
|
|
77
|
+
export declare function typeQuestionFilter(dialog: QuestionDialog, text: string): boolean;
|
|
78
|
+
/** Remove the last character of the filter; the caller exits when it is empty. */
|
|
79
|
+
export declare function backspaceQuestionFilter(dialog: QuestionDialog): boolean;
|
|
80
|
+
/** Leave filter mode, keeping whatever was typed (Enter applies it). */
|
|
81
|
+
export declare function applyQuestionFilter(dialog: QuestionDialog): void;
|
|
82
|
+
/** Leave filter mode and show the whole list again (Esc clears it). */
|
|
83
|
+
export declare function clearQuestionFilter(dialog: QuestionDialog): void;
|
|
54
84
|
/** Move the question highlight, clamped to its options. Returns false if not a question dialog. */
|
|
55
85
|
export declare function moveQuestionCursor(dialog: QuestionDialog, delta: number): boolean;
|
|
56
86
|
/** Select option `index`: toggles in a multi-select list, replaces otherwise. */
|
|
@@ -67,11 +97,12 @@ export type QuestionSubmit = {
|
|
|
67
97
|
kind: 'none';
|
|
68
98
|
};
|
|
69
99
|
/**
|
|
70
|
-
* What Enter means for a question dialog.
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
100
|
+
* What Enter means for a question dialog. Enter never answers with nothing: the
|
|
101
|
+
* list opens on the first option, and that highlight is the answer for a
|
|
102
|
+
* single-select list and a multi-select one alike — pressing Enter without
|
|
103
|
+
* touching anything returns the option under the cursor, and Esc stays the
|
|
104
|
+
* explicit cancel. Space (or a digit/letter) still builds a multi-select set,
|
|
105
|
+
* and a list with no options at all answers with the typed text.
|
|
75
106
|
*/
|
|
76
107
|
export declare function questionSubmit(dialog: QuestionDialog, input: string): QuestionSubmit;
|
|
77
108
|
/** y/n/ctrl-c/esc answers for a confirm prompt. */
|
|
@@ -115,6 +115,12 @@ export interface HostExitWatch {
|
|
|
115
115
|
/** Stop watching; call once the channel is confirmed up. */
|
|
116
116
|
dispose(): void;
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* How long a dead-pid report waits for the child's `exit` event before giving
|
|
120
|
+
* up on its code. The event is normally delivered within a tick; the bound only
|
|
121
|
+
* exists so a host that never reports still fails fast.
|
|
122
|
+
*/
|
|
123
|
+
export declare const HOST_EXIT_GRACE_MS = 250;
|
|
118
124
|
export declare function waitForDisplaySock(path: string, timeoutMs?: number, pid?: number, errFile?: string, exitWatch?: HostExitWatch): Promise<void>;
|
|
119
125
|
export declare function hostArgvForSession(sessionId: string, argv?: string[], execArgv?: string[]): string[];
|
|
120
126
|
export interface SpawnedHost {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/doctor`: one pass over the deployment's composition, dependencies, and
|
|
3
|
+
* compatibility, each verdict carrying the row, line, or path behind it.
|
|
4
|
+
*
|
|
5
|
+
* `/diag` answers "why can't I get in?" about one session's channel, lock, and
|
|
6
|
+
* Host. `/doctor` answers the other half — "is this install composed the way
|
|
7
|
+
* the plugin needs?" — which is where 0.6.3's regression lived: 0.5.1 dropped
|
|
8
|
+
* the roster rows to pass a store check, and the only symptom was `/mode`
|
|
9
|
+
* reporting a missing service months later. Every check therefore reports the
|
|
10
|
+
* concrete row (with its line) or path, plus the command that acts on it.
|
|
11
|
+
*
|
|
12
|
+
* The verdicts are a pure function of collected facts, so they are testable
|
|
13
|
+
* without a Host, a profile, or a filesystem; collection lives in
|
|
14
|
+
* `collectDoctor` and every probe is best-effort. Nothing leaves the machine.
|
|
15
|
+
* @module dsh-ssh-tui/doctor
|
|
16
|
+
*/
|
|
17
|
+
import { type PatchAnalysis, type PatchRowRef, type RosterRow } from './preset-rows.js';
|
|
18
|
+
export type DoctorStatus = 'ok' | 'warn' | 'fail';
|
|
19
|
+
export interface DoctorCheck {
|
|
20
|
+
id: string;
|
|
21
|
+
status: DoctorStatus;
|
|
22
|
+
/** One line: the conclusion. */
|
|
23
|
+
summary: string;
|
|
24
|
+
/** The rows, lines, and paths behind the conclusion. */
|
|
25
|
+
details: string[];
|
|
26
|
+
/** Whether `/doctor --fix` (or `/fix <row>`) can act on this check. */
|
|
27
|
+
fixable?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** One route's model list, for the routing self-consistency check. */
|
|
30
|
+
export interface DoctorRouting {
|
|
31
|
+
provider?: string;
|
|
32
|
+
model?: string;
|
|
33
|
+
/** Every provider id the llm-pi-ai section configures. */
|
|
34
|
+
routes: readonly string[];
|
|
35
|
+
/** Model ids the selected provider's route lists. */
|
|
36
|
+
routeModels: readonly string[];
|
|
37
|
+
subProvider?: string;
|
|
38
|
+
subModel?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface DoctorFacts {
|
|
41
|
+
pluginVersion: string;
|
|
42
|
+
hostVersion: string;
|
|
43
|
+
nodeVersion: string;
|
|
44
|
+
profile: string;
|
|
45
|
+
dshHome: string;
|
|
46
|
+
patchPath: string;
|
|
47
|
+
/** Services the running composition actually registered. */
|
|
48
|
+
services: {
|
|
49
|
+
/** `agentPresets` — the roster `/mode` lists. */
|
|
50
|
+
roster: boolean;
|
|
51
|
+
/** `codeRuntime` — what the PTC preset mounts. */
|
|
52
|
+
codeRuntime: boolean;
|
|
53
|
+
};
|
|
54
|
+
/** The profile patch. `readable` is false when it does not exist yet. */
|
|
55
|
+
patch: {
|
|
56
|
+
readable: boolean;
|
|
57
|
+
text: string;
|
|
58
|
+
analysis: PatchAnalysis;
|
|
59
|
+
};
|
|
60
|
+
/** Rows the plugin's own bundle patch mounts. */
|
|
61
|
+
bundleRows: readonly PatchRowRef[];
|
|
62
|
+
/** The manifest's compatibility declaration, when the package is readable. */
|
|
63
|
+
compatibility?: {
|
|
64
|
+
range?: string;
|
|
65
|
+
releases: Record<string, string>;
|
|
66
|
+
};
|
|
67
|
+
/** Distinct installs of `@deepseek-ai/dsh-scope` found from the anchors. */
|
|
68
|
+
scopeCopies: readonly string[];
|
|
69
|
+
/** Absent when routing could not be read (no settings service). */
|
|
70
|
+
routing?: DoctorRouting;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Every check, in report order. Pure: same facts in, same verdicts out.
|
|
74
|
+
* @param facts - one collected snapshot.
|
|
75
|
+
* @returns the checks, each with its conclusion and evidence.
|
|
76
|
+
*/
|
|
77
|
+
export declare function doctorChecks(facts: DoctorFacts): DoctorCheck[];
|
|
78
|
+
/** The whole report as transcript lines. Pure. */
|
|
79
|
+
export declare function formatDoctorReport(facts: DoctorFacts, checks: readonly DoctorCheck[]): string[];
|
|
80
|
+
/**
|
|
81
|
+
* Distinct installs of `@deepseek-ai/dsh-scope` reachable from the anchors.
|
|
82
|
+
*
|
|
83
|
+
* Anchors are the running CLI entry and the plugin's module base, the same two
|
|
84
|
+
* the provider catalog walks: a nested npm install puts a second copy under one
|
|
85
|
+
* of them, and preset composition then fails with "refusing to compose an
|
|
86
|
+
* unscoped context".
|
|
87
|
+
* @param anchors - file or directory paths that plausibly sit inside the install.
|
|
88
|
+
* @returns real paths, deduplicated; empty when none was found.
|
|
89
|
+
*/
|
|
90
|
+
export declare function findScopeCopies(anchors: ReadonlyArray<string | undefined>): string[];
|
|
91
|
+
/**
|
|
92
|
+
* Gather one snapshot. Every probe is best-effort: a failure is reported by the
|
|
93
|
+
* check that needed it, never thrown.
|
|
94
|
+
* @param options - identity, services, and the already-read routing facts.
|
|
95
|
+
* @returns the facts {@link doctorChecks} turns into verdicts.
|
|
96
|
+
*/
|
|
97
|
+
export declare function collectDoctor(options: {
|
|
98
|
+
profile: string;
|
|
99
|
+
dshHome: string;
|
|
100
|
+
hostVersion: string;
|
|
101
|
+
services: DoctorFacts['services'];
|
|
102
|
+
anchors: ReadonlyArray<string | undefined>;
|
|
103
|
+
routing?: DoctorRouting;
|
|
104
|
+
}): Promise<DoctorFacts>;
|
|
105
|
+
/** Rows `/doctor --fix` would mount, given the services the composition registered. */
|
|
106
|
+
export declare function rowsToRepair(facts: DoctorFacts): RosterRow[];
|
|
107
|
+
/**
|
|
108
|
+
* What to tell a user whose boot died on the module-copy trap.
|
|
109
|
+
*
|
|
110
|
+
* `/doctor` cannot answer this one: the composition fails before the TUI exists,
|
|
111
|
+
* so the launcher's own failure report is the only place the hint can land. The
|
|
112
|
+
* signature is `agent-presets` refusing an unscoped context, which happens when
|
|
113
|
+
* the plugin and the host resolve different `@deepseek-ai/dsh-scope` instances
|
|
114
|
+
* (a profile that links the plugin into a checkout is the usual shape).
|
|
115
|
+
* @param detail - the host failure text the launcher is about to print.
|
|
116
|
+
* @returns the hint, or `undefined` for every other failure.
|
|
117
|
+
*/
|
|
118
|
+
export declare function copyTrapHint(detail: string): string | undefined;
|
package/lib/types/footer.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export declare function contextPressureView(sample: ContextPressureSample): Cont
|
|
|
44
44
|
*/
|
|
45
45
|
export declare const CONTEXT_RING_EMPTY = "\u28C0";
|
|
46
46
|
export declare const CONTEXT_RING_SEGMENTS: readonly ["⣀", "⠉", "⠋", "⠛", "⠞", "⠟", "⠿", "⡿", "⣿"];
|
|
47
|
+
/** The full cell of the same Braille family, for bars built like the ring. */
|
|
48
|
+
export declare const CONTEXT_RING_FULL = "\u28FF";
|
|
47
49
|
export declare function formatContextPressureRing(percent: number): string;
|
|
48
50
|
export declare function contextPressureRingColor(level: ContextPressureView['level']): string;
|
|
49
51
|
export declare function formatContextPressureChip(view: ContextPressureView, color?: boolean): string;
|
|
@@ -70,6 +72,39 @@ export interface FooterStatsInput {
|
|
|
70
72
|
}
|
|
71
73
|
/** Stats groups in drop order (last is dropped first when the row is too wide). */
|
|
72
74
|
export declare function footerStatsGroups(stats: FooterStatsInput): string[];
|
|
75
|
+
/**
|
|
76
|
+
* One group in the status strip, with the graphic-only form a narrow terminal
|
|
77
|
+
* keeps. `priority` orders the losses: 0 is the last thing to go.
|
|
78
|
+
*/
|
|
79
|
+
export interface FooterChip {
|
|
80
|
+
id: string;
|
|
81
|
+
/** Glyph plus text: what a wide terminal shows. */
|
|
82
|
+
long: string;
|
|
83
|
+
/** Glyph only, or `''` for a group that is pure text. */
|
|
84
|
+
short: string;
|
|
85
|
+
priority: number;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Fit an ordered strip into `width` cells, losing text before graphics.
|
|
89
|
+
*
|
|
90
|
+
* The old fitter dropped whole groups from the end, so a narrow terminal lost
|
|
91
|
+
* the ⚠ and the context ring — the two signals that say something is wrong —
|
|
92
|
+
* while keeping long numeric groups. Two passes now: every chip trades its text
|
|
93
|
+
* for its glyph in reverse priority order, and only then do whole groups go,
|
|
94
|
+
* again lowest priority first. The result never exceeds the width.
|
|
95
|
+
*
|
|
96
|
+
* Chips may carry SGR accents (the link pips, the ⚠) and the caller may pass a
|
|
97
|
+
* styled separator, so both the measurement and the final cut are ANSI-aware:
|
|
98
|
+
* feeding styled text to a plain-text truncator strips the accent's `ESC` and
|
|
99
|
+
* leaves its `[32m` body on the grid.
|
|
100
|
+
*/
|
|
101
|
+
export declare function fitFooterChips(chips: readonly FooterChip[], width: number, separator?: string): string;
|
|
102
|
+
/**
|
|
103
|
+
* The roster's health as a chip: absent means `/mode` cannot switch presets and
|
|
104
|
+
* the preset-owned tools are missing. It leads the strip and keeps its glyph
|
|
105
|
+
* longest, because it is the one group that reports a broken install.
|
|
106
|
+
*/
|
|
107
|
+
export declare function footerHealthChip(missing: boolean, color?: boolean): FooterChip | undefined;
|
|
73
108
|
export declare function fitFooterStatsLine(chip: string, groups: readonly string[], width: number): string;
|
|
74
109
|
export type FooterActivityKind = 'plan-review' | 'waiting' | 'compacting' | 'retry' | 'subagents' | 'tools' | 'plan-open' | 'plan-pending' | 'goal' | 'waiting-llm' | 'idle';
|
|
75
110
|
export interface FooterStatusInput {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rebindable keys: a small set of high-frequency actions, and the rules for
|
|
3
|
+
* applying a user's overrides without breaking the defaults.
|
|
4
|
+
*
|
|
5
|
+
* The TUI dispatches on raw byte sequences (`\x1b[5~` for PageUp, `\x1b` for
|
|
6
|
+
* Esc), which is fine until someone's terminal sends something else for a key,
|
|
7
|
+
* or their hands want the action elsewhere. Only actions are rebindable — never
|
|
8
|
+
* arbitrary bytes — so a typo cannot invent a key that does nothing, and a key
|
|
9
|
+
* that two actions claim is refused rather than silently winning by order.
|
|
10
|
+
* @module dsh-ssh-tui/keymap
|
|
11
|
+
*/
|
|
12
|
+
/** The actions a user may move. */
|
|
13
|
+
export type KeyAction = 'pageUp' | 'pageDown' | 'toggleCard' | 'copy' | 'cancel';
|
|
14
|
+
/** Every action, in the order reports list them. */
|
|
15
|
+
export declare const KEY_ACTIONS: readonly KeyAction[];
|
|
16
|
+
/** The key each action answers to out of the box. */
|
|
17
|
+
export declare const DEFAULT_KEYMAP: Readonly<Record<KeyAction, string>>;
|
|
18
|
+
/**
|
|
19
|
+
* The bytes one key name stands for, or undefined when the name means nothing.
|
|
20
|
+
*
|
|
21
|
+
* Accepts the named keys above, `ctrl+<letter>`, and a single character.
|
|
22
|
+
* @param name - the name from the config or the default table.
|
|
23
|
+
* @returns the sequence, or undefined for an unknown name.
|
|
24
|
+
*/
|
|
25
|
+
export declare function keySequence(name: string): string | undefined;
|
|
26
|
+
/** What a config's `keys` section asked for, and what was wrong with it. */
|
|
27
|
+
export interface ResolvedKeymap {
|
|
28
|
+
/** Sequence to action, for the keys that survived. */
|
|
29
|
+
sequences: Map<string, KeyAction>;
|
|
30
|
+
/** Names the config used that no action has. */
|
|
31
|
+
unknownActions: string[];
|
|
32
|
+
/** Keys the config asked for that are not keys this TUI knows. */
|
|
33
|
+
unknownKeys: string[];
|
|
34
|
+
/** Keys two actions claim; the first (default order) keeps it. */
|
|
35
|
+
conflicts: Array<{
|
|
36
|
+
key: string;
|
|
37
|
+
actions: KeyAction[];
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Actions the config actually moved. Only these are dispatched through the
|
|
41
|
+
* keymap, so a key the user did not touch keeps the handling it always had —
|
|
42
|
+
* Enter in the input box still submits, Esc still closes a dialog.
|
|
43
|
+
*/
|
|
44
|
+
overridden: Set<KeyAction>;
|
|
45
|
+
/**
|
|
46
|
+
* Sequences that used to be a default and are now unclaimed. Pressing one
|
|
47
|
+
* does nothing instead of falling through to the old hard-coded branch.
|
|
48
|
+
*/
|
|
49
|
+
suppressed: Set<string>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Apply a user's overrides to the defaults.
|
|
53
|
+
*
|
|
54
|
+
* Order matters and is deliberate: the defaults are laid down first, then the
|
|
55
|
+
* overrides in {@link KEY_ACTIONS} order. A key that ends up claimed twice is
|
|
56
|
+
* removed from the map and reported, so neither action surprises the other —
|
|
57
|
+
* the user is told, and pressing that key does nothing rather than the wrong
|
|
58
|
+
* thing. An action whose override is unusable keeps its default.
|
|
59
|
+
* @param overrides - the config's `keys` section, action to key name.
|
|
60
|
+
* @returns the keymap, plus what to tell the user about it.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveKeymap(overrides?: Readonly<Record<string, unknown>>): ResolvedKeymap;
|
|
63
|
+
/**
|
|
64
|
+
* One line describing what was wrong with a config's keys, or undefined when
|
|
65
|
+
* there is nothing to say. Silent acceptance is how a user ends up pressing a
|
|
66
|
+
* key that does nothing and blaming the TUI.
|
|
67
|
+
*/
|
|
68
|
+
export declare function keymapReport(resolved: ResolvedKeymap): string | undefined;
|