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,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-level diffing for the tool card, and the word-level emphasis inside a
|
|
3
|
+
* changed pair.
|
|
4
|
+
*
|
|
5
|
+
* The card used to print every old line as `-` and every new line as `+`: a
|
|
6
|
+
* one-line change in a hundred-line file produced two hundred rows, which is
|
|
7
|
+
* unreadable and hides the change it is trying to show. This module keeps the
|
|
8
|
+
* classic shape — common lines as context, changes as `-`/`+` pairs, unchanged
|
|
9
|
+
* stretches collapsed to a marker — and reports which characters inside a
|
|
10
|
+
* replaced pair actually changed so the renderer can emphasise them.
|
|
11
|
+
* @module dsh-ssh-tui/line-diff
|
|
12
|
+
*/
|
|
13
|
+
/** One line of the diff. */
|
|
14
|
+
export interface DiffLine {
|
|
15
|
+
kind: 'same' | 'add' | 'del';
|
|
16
|
+
text: string;
|
|
17
|
+
/** 0-based index in the old text, for `same` and `del`. */
|
|
18
|
+
oldIndex?: number;
|
|
19
|
+
/** 0-based index in the new text, for `same` and `add`. */
|
|
20
|
+
newIndex?: number;
|
|
21
|
+
}
|
|
22
|
+
/** A run of lines with its context, and how many unchanged lines were skipped. */
|
|
23
|
+
export interface DiffHunk {
|
|
24
|
+
/** Unchanged lines collapsed before this run; 0 for the first. */
|
|
25
|
+
omittedBefore: number;
|
|
26
|
+
lines: DiffLine[];
|
|
27
|
+
}
|
|
28
|
+
/** Two columns plus a gutter need this much room to be worth it. */
|
|
29
|
+
export declare const SIDE_BY_SIDE_MIN_WIDTH = 100;
|
|
30
|
+
/** The gutter between the two columns. */
|
|
31
|
+
export declare const SIDE_BY_SIDE_GUTTER = " \u2502 ";
|
|
32
|
+
/**
|
|
33
|
+
* Whether a diff should be shown side by side at this terminal width.
|
|
34
|
+
*
|
|
35
|
+
* Below the threshold the columns would be narrower than the code they hold,
|
|
36
|
+
* which turns every changed line into a wrapped mess — stacked reads better.
|
|
37
|
+
* @param width - the terminal width in cells.
|
|
38
|
+
* @returns the layout to use.
|
|
39
|
+
*/
|
|
40
|
+
export declare function diffLayout(width: number): 'side-by-side' | 'stacked';
|
|
41
|
+
/** One row of a side-by-side rendering. */
|
|
42
|
+
export interface SideBySideRow {
|
|
43
|
+
/** Marker plus text for the left column: `- old`, ` context`, or empty. */
|
|
44
|
+
left: string;
|
|
45
|
+
/** Marker plus text for the right column: `+ new`, ` context`, or empty. */
|
|
46
|
+
right: string;
|
|
47
|
+
/** Emphasis inside `left`, offsets into it. */
|
|
48
|
+
leftSpans: DiffSpan[];
|
|
49
|
+
/** Emphasis inside `right`, offsets into it. */
|
|
50
|
+
rightSpans: DiffSpan[];
|
|
51
|
+
/** Whether both columns carry the same line (context) rather than a change. */
|
|
52
|
+
context: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Pair a line diff into two columns.
|
|
56
|
+
*
|
|
57
|
+
* A removed line immediately followed by an added one is a replacement and
|
|
58
|
+
* shares a row, with the changed characters emphasised on each side; a lone
|
|
59
|
+
* removal or addition keeps its own row with the other column empty. Context
|
|
60
|
+
* rows carry the text on both sides so the eye can follow either column.
|
|
61
|
+
* @param lines - a diff, as {@link diffLines} returns it.
|
|
62
|
+
* @returns the rows, in order.
|
|
63
|
+
*/
|
|
64
|
+
export declare function sideBySideRows(lines: readonly DiffLine[]): SideBySideRow[];
|
|
65
|
+
/** One emphasized range inside a line, in UTF-16 offsets. */
|
|
66
|
+
export interface DiffSpan {
|
|
67
|
+
start: number;
|
|
68
|
+
end: number;
|
|
69
|
+
}
|
|
70
|
+
/** Split text into lines, treating an empty string as no lines. */
|
|
71
|
+
export declare function splitLines(text: string): string[];
|
|
72
|
+
/**
|
|
73
|
+
* Diff two texts line by line.
|
|
74
|
+
*
|
|
75
|
+
* Equal lines are `same`, lines only in the old text are `del`, only in the new
|
|
76
|
+
* are `add` — in the order a reader expects. A pair this module cannot afford
|
|
77
|
+
* (see {@link MAX_DIFF_LINES}) degrades to "all removed, then all added", which
|
|
78
|
+
* is what the card rendered before but is at least bounded.
|
|
79
|
+
* @param oldText - the previous content; empty for a new file.
|
|
80
|
+
* @param newText - the new content.
|
|
81
|
+
* @returns the diff, oldest first.
|
|
82
|
+
*/
|
|
83
|
+
export declare function diffLines(oldText: string, newText: string): DiffLine[];
|
|
84
|
+
/**
|
|
85
|
+
* Group a diff into runs of change with `context` unchanged lines around them,
|
|
86
|
+
* reporting how many unchanged lines were left out between runs.
|
|
87
|
+
* @param lines - a diff, as {@link diffLines} returns it.
|
|
88
|
+
* @param context - unchanged lines to keep on each side of a change.
|
|
89
|
+
* @returns the runs, in order; a diff with no changes yields no runs.
|
|
90
|
+
*/
|
|
91
|
+
export declare function diffHunks(lines: readonly DiffLine[], context?: number): DiffHunk[];
|
|
92
|
+
/**
|
|
93
|
+
* The characters that differ between two lines: the common prefix and suffix
|
|
94
|
+
* are trimmed away, and what is left is emphasised.
|
|
95
|
+
*
|
|
96
|
+
* Whole-line emphasis was the old behaviour and it reads as noise on a line that
|
|
97
|
+
* changed one identifier. Trimming is greedy and therefore approximate — it does
|
|
98
|
+
* not try to align moved words — but it is exact about the boundaries it does
|
|
99
|
+
* report: everything outside the spans really is identical.
|
|
100
|
+
* @param oldText - the removed line.
|
|
101
|
+
* @param newText - the added line.
|
|
102
|
+
* @returns one span list per side, possibly empty when they are equal.
|
|
103
|
+
*/
|
|
104
|
+
export declare function wordDiffSpans(oldText: string, newText: string): {
|
|
105
|
+
old: DiffSpan[];
|
|
106
|
+
new: DiffSpan[];
|
|
107
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line mode: one plain line per event, for screen readers, log capture, and
|
|
3
|
+
* terminals where a full-screen repaint is the wrong shape.
|
|
4
|
+
*
|
|
5
|
+
* The TUI normally paints a framed screen: absolute cursor addressing, an
|
|
6
|
+
* alternate screen buffer, only the rows that changed, and a spinner that
|
|
7
|
+
* redraws in place. None of that survives a pipe, a screen reader, or a
|
|
8
|
+
* recording — the same event can appear twice, out of order, or not at all.
|
|
9
|
+
* This module turns a transcript row into the lines a log would carry, and the
|
|
10
|
+
* caller appends them once, in order, and never repaints.
|
|
11
|
+
*
|
|
12
|
+
* Colour is deliberately absent: the caller may style a line, but the text has
|
|
13
|
+
* to stand on its own because that is the point of the mode.
|
|
14
|
+
* @module dsh-ssh-tui/line-mode
|
|
15
|
+
*/
|
|
16
|
+
import type { Row } from './transcript-types.js';
|
|
17
|
+
/** Whether the environment asks for line mode. `1/true/on/yes` all count. */
|
|
18
|
+
export declare function lineModeEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The lines one row contributes to the log.
|
|
21
|
+
*
|
|
22
|
+
* A row with embedded newlines contributes several lines — the alternative is
|
|
23
|
+
* joining them, which corrupts a report's structure for the reader this mode
|
|
24
|
+
* exists for. Rows that only make sense on a screen (the logo, a transient
|
|
25
|
+
* reasoning buffer) contribute nothing rather than noise.
|
|
26
|
+
* @param row - the transcript row that was just pushed.
|
|
27
|
+
* @returns the lines to append, in order; empty when the row is screen-only.
|
|
28
|
+
*/
|
|
29
|
+
export declare function lineModeLines(row: Row): string[];
|
|
30
|
+
/**
|
|
31
|
+
* What one row's lines look like once appended.
|
|
32
|
+
*
|
|
33
|
+
* A trailing newline per line, and nothing else: no carriage returns (they
|
|
34
|
+
* would overwrite the line just written on a terminal) and no escape sequences.
|
|
35
|
+
*/
|
|
36
|
+
export declare function appendRow(lines: readonly string[]): string;
|
package/lib/types/paint.d.ts
CHANGED
|
@@ -43,10 +43,34 @@ export type LinkQuality = 'local' | 'good' | 'ok' | 'slow' | 'poor' | 'unknown';
|
|
|
43
43
|
export declare function linkQualityOf(kind: PaintLinkKind, rttMs: number | undefined): LinkQuality;
|
|
44
44
|
/** How many filled signal pips: 4 local/fast, 3 ok, 2 slow, 1 poor, 0 unknown. */
|
|
45
45
|
export declare function linkSignalPips(quality: LinkQuality): number;
|
|
46
|
+
/**
|
|
47
|
+
* How many bytes one frame may carry before the rest waits for the next tick.
|
|
48
|
+
*
|
|
49
|
+
* A 100x30 full repaint costs about 3.7 kB, which on a 400ms link is a third of
|
|
50
|
+
* a second of serialization the user waits through before anything moves. The
|
|
51
|
+
* cadence already follows the measured RTT; the budget bounds what each of
|
|
52
|
+
* those ticks may spend, so a resize or a burst of streaming output degrades
|
|
53
|
+
* into a few ordered frames instead of one long freeze and a queue behind it.
|
|
54
|
+
*/
|
|
55
|
+
export declare const FRAME_BYTE_BUDGETS: Record<LinkQuality, number>;
|
|
56
|
+
/** The per-frame byte budget for a link quality. */
|
|
57
|
+
export declare function frameByteBudget(quality: LinkQuality): number;
|
|
58
|
+
/**
|
|
59
|
+
* How many body lines an expanded card may show before it points at the
|
|
60
|
+
* overlay, by link quality.
|
|
61
|
+
*
|
|
62
|
+
* A measured slow link is the only case that truncates: there, every body line
|
|
63
|
+
* is bytes the user waits for, and the overlay is one key away (`Enter`). Fast
|
|
64
|
+
* and unmeasured links keep the behavior 0.3.9 settled on — an expanded body
|
|
65
|
+
* shows in full, or opens in the overlay when it cannot fit the workspace — so
|
|
66
|
+
* nothing changes for a local terminal or a link whose RTT could not be probed.
|
|
67
|
+
*/
|
|
68
|
+
export declare const TOOL_BODY_LINES_BY_QUALITY: Record<LinkQuality, number>;
|
|
69
|
+
/** The body-line cap for a link quality; `Infinity` means "show it all". */
|
|
70
|
+
export declare function toolBodyLineLimit(quality: LinkQuality): number;
|
|
46
71
|
/** Compact footer chip: `SSH ●●●○ 90ms` — 1 pip red, 2 yellow, 3+ green. */
|
|
47
72
|
export declare function formatLinkQualityChip(kind: PaintLinkKind, intervalMs: number, rttMs: number | undefined, probed: boolean, color?: boolean): string;
|
|
48
|
-
|
|
49
|
-
export declare function composePaintOutput(options: {
|
|
73
|
+
export interface PaintOptions {
|
|
50
74
|
width: number;
|
|
51
75
|
height: number;
|
|
52
76
|
paintRows: readonly string[];
|
|
@@ -59,7 +83,60 @@ export declare function composePaintOutput(options: {
|
|
|
59
83
|
cursorColumn: number;
|
|
60
84
|
/** Keep the cursor hidden (session picker). Default shows it on the input. */
|
|
61
85
|
hideCursor?: boolean;
|
|
62
|
-
}
|
|
86
|
+
}
|
|
87
|
+
export interface PaintFrame {
|
|
88
|
+
/** The single stdout write for this frame. */
|
|
89
|
+
output: string;
|
|
90
|
+
/** Row indices actually written, ascending. Only these are now up to date. */
|
|
91
|
+
painted: number[];
|
|
92
|
+
/** Rows that needed a paint but did not fit the byte budget. */
|
|
93
|
+
deferred: number[];
|
|
94
|
+
/** Highest deferred row, to pass back as `from` on the next frame. */
|
|
95
|
+
resume: number | undefined;
|
|
96
|
+
/** Bytes this frame carried, for the ledger `/diag` and the tests read. */
|
|
97
|
+
bytes: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Which dirty rows to paint first when a byte budget applies.
|
|
101
|
+
*
|
|
102
|
+
* Rows at or below `from` were deferred by the previous frame and go first so
|
|
103
|
+
* they cannot starve; everything above is the tail the user is looking at,
|
|
104
|
+
* painted newest-row-first so the input area is current even when the budget
|
|
105
|
+
* runs out. Without a `from`, the order is simply tail-first.
|
|
106
|
+
* @param dirty - row indices that need a paint, ascending.
|
|
107
|
+
* @param from - highest row the previous frame deferred, if any.
|
|
108
|
+
* @returns indices in paint order.
|
|
109
|
+
*/
|
|
110
|
+
export declare function paintOrder(dirty: readonly number[], from?: number): number[];
|
|
111
|
+
/**
|
|
112
|
+
* Compose one incremental paint, optionally bounded by a byte budget.
|
|
113
|
+
*
|
|
114
|
+
* Every row is addressed absolutely, so the paint order is free: with a budget
|
|
115
|
+
* the frame spends it on the tail first and reports what it could not reach, so
|
|
116
|
+
* the caller keeps those rows dirty instead of forgetting them.
|
|
117
|
+
* @param options - the frame to paint, plus the budget and resume point.
|
|
118
|
+
* @returns the write, what it covered, and what is left for the next tick.
|
|
119
|
+
*/
|
|
120
|
+
export declare function composePaintFrame(options: PaintOptions & {
|
|
121
|
+
maxBytes?: number;
|
|
122
|
+
from?: number;
|
|
123
|
+
}): PaintFrame;
|
|
124
|
+
/**
|
|
125
|
+
* The snapshot for the next frame: only the rows a frame painted become clean.
|
|
126
|
+
*
|
|
127
|
+
* A deferred row has to keep its previous content in the snapshot, or the next
|
|
128
|
+
* frame would compare it against itself, call it unchanged, and never paint it:
|
|
129
|
+
* the row would simply stay wrong. The array also keeps its old length while
|
|
130
|
+
* anything is owed, so the "row count changed" clear keeps firing until the
|
|
131
|
+
* frame is complete.
|
|
132
|
+
* @param previous - the snapshot the frame painted against.
|
|
133
|
+
* @param current - the rows the frame wanted to show.
|
|
134
|
+
* @param painted - indices the frame actually wrote.
|
|
135
|
+
* @returns the snapshot to hand to the next frame.
|
|
136
|
+
*/
|
|
137
|
+
export declare function advancePaintedRows(previous: readonly string[], current: readonly string[], painted: readonly number[]): string[];
|
|
138
|
+
/** One incremental paint as a single stdout write (one SSH packet when corked). */
|
|
139
|
+
export declare function composePaintOutput(options: PaintOptions): string;
|
|
63
140
|
/** Whether `text` could still grow into a recognized escape sequence. */
|
|
64
141
|
export declare function isEscapePrefix(text: string): boolean;
|
|
65
142
|
/** Parse a Device Status Report cursor reply (`CSI row;col R`). */
|
|
@@ -92,5 +169,13 @@ export declare function findCursorPositionReply(text: string): {
|
|
|
92
169
|
export declare function probeTerminalRttMs(stdin?: NodeJS.ReadStream, stdout?: NodeJS.WriteStream, timeoutMs?: number, ssh?: boolean): Promise<number | undefined>;
|
|
93
170
|
/** Sliding window of `windowSize` items that keeps `cursor` visible. */
|
|
94
171
|
export declare function pickerWindowStart(cursor: number, total: number, windowSize?: number): number;
|
|
95
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* Immediate first-frame chrome so a 2–3s Host boot is not a blank TTY.
|
|
174
|
+
*
|
|
175
|
+
* Line mode gets nothing: a splash enters the alternate screen and paints over
|
|
176
|
+
* itself, which is exactly what that mode exists to avoid — the log would open
|
|
177
|
+
* with screen control instead of the session's first event.
|
|
178
|
+
* @param message - what the launcher is waiting for.
|
|
179
|
+
* @param color - whether colour is allowed.
|
|
180
|
+
*/
|
|
96
181
|
export declare function writeBootSplash(message: string, color?: boolean): void;
|
package/lib/types/plan.d.ts
CHANGED
|
@@ -62,6 +62,13 @@ export declare function planDockNote(plan: {
|
|
|
62
62
|
/** Compact per-status counts matching the web plan strip. */
|
|
63
63
|
export declare function todoProgressLabel(todos: readonly PlanTodoItem[]): string;
|
|
64
64
|
export declare function todoItemKind(status: PlanTodoItem['status']): DisplayKind;
|
|
65
|
+
/**
|
|
66
|
+
* A Braille bar for the plan's completion, in the same family as the context
|
|
67
|
+
* ring: `⣿` filled, `⣀` empty, and the eight partial cells between them.
|
|
68
|
+
* @param todos - the list as the model last wrote it.
|
|
69
|
+
* @param cells - bar width in cells.
|
|
70
|
+
*/
|
|
71
|
+
export declare function todoProgressBar(todos: readonly PlanTodoItem[], cells?: number): string;
|
|
65
72
|
export declare function planMarkdownFromArgs(value: unknown): string | undefined;
|
|
66
73
|
/** First markdown heading of an exit_plan_mode plan body. */
|
|
67
74
|
export declare function planTitleFromMarkdown(markdown: string): string | undefined;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/preset`: the terminal's authoring surface for agent presets.
|
|
3
|
+
*
|
|
4
|
+
* A preset is a directory — an id, a composition file, and optional display
|
|
5
|
+
* metadata — and the web client reaches the same four operations the plugin
|
|
6
|
+
* exposes to it: list, read, copy, delete. This module mirrors that surface and
|
|
7
|
+
* adds display-name/description edits, which are the one authoring write the
|
|
8
|
+
* upstream service does not offer; it stays pure so every refusal is testable
|
|
9
|
+
* without a host, and `tui.ts` performs the two calls that touch the disk.
|
|
10
|
+
*
|
|
11
|
+
* The rules are the upstream ones, re-stated here so a command refuses before
|
|
12
|
+
* it calls out:
|
|
13
|
+
*
|
|
14
|
+
* - only `user` presets are writable; a shipped preset is refused by the
|
|
15
|
+
* service as well (`agent-preset/read-only`);
|
|
16
|
+
* - a copy never overwrites: the destination id must be free;
|
|
17
|
+
* - ids match `/^[a-z0-9][a-z0-9-]*$/`;
|
|
18
|
+
* - deleting the preset the session is running is refused here, even though the
|
|
19
|
+
* service allows it — the running session would keep its composition while
|
|
20
|
+
* every later launch falls back to the default.
|
|
21
|
+
* @module dsh-ssh-tui/preset-authoring
|
|
22
|
+
*/
|
|
23
|
+
import { type AgentPreset, type PresetMetadata } from '@deepseek-ai/dsh-agent-presets';
|
|
24
|
+
/** The id shape discovery accepts; a directory that fails it is skipped. */
|
|
25
|
+
export declare const PRESET_ID_PATTERN: RegExp;
|
|
26
|
+
/** Whether an id would be discovered at all. */
|
|
27
|
+
export declare function validatePresetId(id: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* The directory a preset owns.
|
|
30
|
+
*
|
|
31
|
+
* `AgentPreset.path` is the composition file the preset publishes, not the
|
|
32
|
+
* directory, so anything that writes beside it (display metadata) starts here.
|
|
33
|
+
* @param preset - a discovered preset.
|
|
34
|
+
* @returns the absolute preset directory.
|
|
35
|
+
*/
|
|
36
|
+
export declare function presetDirectory(preset: AgentPreset): string;
|
|
37
|
+
export type PresetRefusalCode = 'invalid-id' | 'exists' | 'not-found' | 'read-only' | 'system' | 'running' | 'empty-metadata';
|
|
38
|
+
export interface PresetRefusal {
|
|
39
|
+
error: PresetRefusalCode;
|
|
40
|
+
/** The id the refusal is about, when it names one. */
|
|
41
|
+
id?: string;
|
|
42
|
+
/** The name that collided, for `exists`. */
|
|
43
|
+
name?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface PresetCopyPlan {
|
|
46
|
+
kind: 'copy';
|
|
47
|
+
from: string;
|
|
48
|
+
id: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface PresetMetadataPlan {
|
|
52
|
+
kind: 'metadata';
|
|
53
|
+
id: string;
|
|
54
|
+
directory: string;
|
|
55
|
+
/** The `preset.yml` document to write, already rendered by upstream. */
|
|
56
|
+
text: string;
|
|
57
|
+
}
|
|
58
|
+
export interface PresetDeletePlan {
|
|
59
|
+
kind: 'delete';
|
|
60
|
+
id: string;
|
|
61
|
+
}
|
|
62
|
+
export type PresetPlan = PresetCopyPlan | PresetMetadataPlan | PresetDeletePlan;
|
|
63
|
+
export declare function isRefusal(value: PresetPlan | PresetRefusal): value is PresetRefusal;
|
|
64
|
+
/** One composition row as `/preset show` renders it. */
|
|
65
|
+
export interface PresetRowView {
|
|
66
|
+
entryId: string | null;
|
|
67
|
+
moduleName: string;
|
|
68
|
+
enabled: boolean | 'conditional';
|
|
69
|
+
condition?: string;
|
|
70
|
+
}
|
|
71
|
+
/** One preset's composition as `/preset show` renders it. */
|
|
72
|
+
export interface PresetCompositionView {
|
|
73
|
+
id: string;
|
|
74
|
+
name?: string;
|
|
75
|
+
isDefault: boolean;
|
|
76
|
+
broken?: string;
|
|
77
|
+
rows: readonly PresetRowView[];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The authoring subset of the service, feature-detected rather than assumed:
|
|
81
|
+
* the plugin still supports 0.1.2-rc.1, whose `agentPresets` may predate
|
|
82
|
+
* `copy`, `remove`, `read`, and `compositionInventory`. A missing member turns
|
|
83
|
+
* into a clear message instead of a crash.
|
|
84
|
+
*/
|
|
85
|
+
export interface PresetAuthoringApi {
|
|
86
|
+
readonly authorable?: boolean;
|
|
87
|
+
copy?(from: string, id: string, name?: string): Promise<void>;
|
|
88
|
+
remove?(id: string): Promise<void>;
|
|
89
|
+
read?(id: string): Promise<string>;
|
|
90
|
+
compositionInventory?(): Promise<readonly PresetCompositionView[]>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Plan a copy of an existing preset under a new id.
|
|
94
|
+
*
|
|
95
|
+
* Copy is the only way to create one (upstream has no create or rename), and it
|
|
96
|
+
* carries the whole directory — composition, display metadata, and any scripts
|
|
97
|
+
* a preset ships beside them.
|
|
98
|
+
* @param input - the roster, whether authoring is possible, and the request.
|
|
99
|
+
* @returns the call to make, or why it must be refused.
|
|
100
|
+
*/
|
|
101
|
+
export declare function planCopy(input: {
|
|
102
|
+
presets: readonly AgentPreset[];
|
|
103
|
+
authorable: boolean;
|
|
104
|
+
from: string;
|
|
105
|
+
id: string;
|
|
106
|
+
name?: string | undefined;
|
|
107
|
+
}): PresetCopyPlan | PresetRefusal;
|
|
108
|
+
/**
|
|
109
|
+
* Plan a display-metadata edit (name, description, or both).
|
|
110
|
+
*
|
|
111
|
+
* The current document is merged, so editing one field never drops the other,
|
|
112
|
+
* and `order` survives. A shipped preset cannot be edited: its directory is
|
|
113
|
+
* read-only by definition.
|
|
114
|
+
* @param input - the preset, its current metadata, and the fields to change.
|
|
115
|
+
* @returns the file to write, or why it must be refused.
|
|
116
|
+
*/
|
|
117
|
+
export declare function planMetadata(input: {
|
|
118
|
+
preset: AgentPreset;
|
|
119
|
+
current: PresetMetadata;
|
|
120
|
+
patch: {
|
|
121
|
+
name?: string | undefined;
|
|
122
|
+
description?: string | undefined;
|
|
123
|
+
};
|
|
124
|
+
}): PresetMetadataPlan | PresetRefusal;
|
|
125
|
+
/**
|
|
126
|
+
* Plan a deletion.
|
|
127
|
+
* @param input - the preset, whether authoring is possible, and which preset
|
|
128
|
+
* this session is running.
|
|
129
|
+
* @returns the call to make, or why it must be refused.
|
|
130
|
+
*/
|
|
131
|
+
export declare function planDelete(input: {
|
|
132
|
+
preset: AgentPreset;
|
|
133
|
+
authorable: boolean;
|
|
134
|
+
current: boolean;
|
|
135
|
+
}): PresetDeletePlan | PresetRefusal;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/mode` picker's list: which presets to show, in what order, and what to
|
|
3
|
+
* say about each one.
|
|
4
|
+
*
|
|
5
|
+
* The dialog is a flat list of options, so the grouping a browser surface can
|
|
6
|
+
* draw is carried in three cheap ways: the groups are ordered (shipped first,
|
|
7
|
+
* then authored locally), every description names its group, and a preset that
|
|
8
|
+
* declares a position shows it. Nothing here touches the filesystem or the
|
|
9
|
+
* service — the caller passes what `list()` returned.
|
|
10
|
+
* @module dsh-ssh-tui/preset-picker
|
|
11
|
+
*/
|
|
12
|
+
import type { AgentPreset } from '@deepseek-ai/dsh-agent-presets';
|
|
13
|
+
/** One option as the question dialog wants it, plus the id it stands for. */
|
|
14
|
+
export interface PresetPickerOption {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
description: string;
|
|
18
|
+
/** Why this preset cannot run, carried so the dialog can refuse it on pick. */
|
|
19
|
+
broken?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Every name this preset answers to: the id, and the name it published even
|
|
22
|
+
* when the display label resolves elsewhere. `/mode <name>` matched a
|
|
23
|
+
* published name before the list was grouped, and dropping that would break
|
|
24
|
+
* a script that uses one.
|
|
25
|
+
*/
|
|
26
|
+
aliases: string[];
|
|
27
|
+
}
|
|
28
|
+
/** The shipped presets, then the locally authored ones. */
|
|
29
|
+
export interface PresetPickerGroup {
|
|
30
|
+
trust: 'system' | 'user';
|
|
31
|
+
title: string;
|
|
32
|
+
options: PresetPickerOption[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Within a group: a declared position first, in ascending order, then the rest
|
|
36
|
+
* by id. `order` is the preset's own statement about where it belongs, so a
|
|
37
|
+
* deployment's curated sequence survives discovery.
|
|
38
|
+
*/
|
|
39
|
+
export declare function comparePresets(left: AgentPreset, right: AgentPreset): number;
|
|
40
|
+
/**
|
|
41
|
+
* What one preset's line says underneath its name: where it sits, what it is
|
|
42
|
+
* for, and why it cannot run — in that order, because a broken preset's reason
|
|
43
|
+
* is the only thing the reader needs.
|
|
44
|
+
*/
|
|
45
|
+
export declare function presetOptionDescription(preset: AgentPreset, currentId: string): string;
|
|
46
|
+
/** One option's label: the name the roster publishes, never empty. */
|
|
47
|
+
export declare function presetOptionLabel(preset: AgentPreset): string;
|
|
48
|
+
/**
|
|
49
|
+
* Group and order the roster for the picker.
|
|
50
|
+
* @param presets - what the service listed.
|
|
51
|
+
* @param currentId - the preset in effect, marked in each description.
|
|
52
|
+
* @returns shipped presets first, then authored ones; empty groups are omitted.
|
|
53
|
+
*/
|
|
54
|
+
export declare function groupPresets(presets: readonly AgentPreset[], currentId: string): PresetPickerGroup[];
|
|
55
|
+
/** The same list, flattened the way the dialog takes it. */
|
|
56
|
+
export declare function flattenGroups(groups: readonly PresetPickerGroup[]): PresetPickerOption[];
|
|
57
|
+
/** Whether a preset answers to the name a caller typed. */
|
|
58
|
+
export declare function optionMatches(option: PresetPickerOption, query: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Narrow the roster to what a typed query matches: the id, the published name,
|
|
61
|
+
* and the description, case-insensitively. An empty query keeps everything, so
|
|
62
|
+
* the picker opens on the full roster.
|
|
63
|
+
*/
|
|
64
|
+
export declare function filterPresets(presets: readonly AgentPreset[], query: string): AgentPreset[];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The profile patch that mounts the agent-preset roster `/mode` needs
|
|
2
|
+
* The profile patch that mounts the agent-preset roster `/mode` needs, plus the
|
|
3
|
+
* repair planner `/doctor` uses to fix a profile patch.
|
|
3
4
|
*
|
|
4
5
|
* A terminal profile built on `dsh-base` composes no preset roster (only the
|
|
5
6
|
* Web bundle does), and this plugin's own bundle patch may not mount one: DSH
|
|
@@ -12,27 +13,116 @@
|
|
|
12
13
|
* The roster is not cosmetic: without it `/mode` cannot switch, and the tools
|
|
13
14
|
* the shipped presets own (`ask_user_question`, `present`, PTC's presentation)
|
|
14
15
|
* are absent from the agent's catalog.
|
|
16
|
+
*
|
|
17
|
+
* Everything in this module is either a pure text function or a file write that
|
|
18
|
+
* takes a backup first: `/doctor` may repair a user's profile patch, and that
|
|
19
|
+
* must never lose the previous content or touch a row the user wrote.
|
|
15
20
|
* @module dsh-ssh-tui/preset-rows
|
|
16
21
|
*/
|
|
22
|
+
/** One host row the roster block mounts. */
|
|
23
|
+
export interface RosterRow {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
/** Lines below `config:`, written one level deeper than `config:`. */
|
|
27
|
+
config?: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The rows the terminal profile has to mount itself: the roster `/mode` lists,
|
|
31
|
+
* the TypeScript runtime the PTC preset needs, and the host-owned subagent
|
|
32
|
+
* delegation setting.
|
|
33
|
+
*/
|
|
34
|
+
export declare const ROSTER_ROWS: readonly RosterRow[];
|
|
35
|
+
/** The comment header the block introduces itself with, in both writers. */
|
|
36
|
+
export declare const ROSTER_PATCH_HEADER = "# dsh-ssh-tui /mode: the agent-preset roster (standard / minimal / PTC /\n# cordis, plus every preset under $DSH_HOME/.agent-presets) and the two host\n# services the shipped presets need. dsh-base composes no roster in a terminal\n# profile, and a third-party bundle patch may not mount an @deepseek-ai row, so\n# the profile's user layer owns them.\n";
|
|
37
|
+
/** One top-level `- insert:` entry mounting exactly these rows. */
|
|
38
|
+
export declare function rosterInsertEntry(rows: readonly RosterRow[]): string;
|
|
17
39
|
/**
|
|
18
40
|
* The exact profile patch block that mounts the roster and the two host
|
|
19
41
|
* services the shipped presets need. `scripts/ensure-profile-rows.sh` carries
|
|
20
42
|
* the same text; a test compares the two so they cannot drift.
|
|
21
43
|
*/
|
|
22
|
-
export declare const ROSTER_PATCH_BLOCK
|
|
44
|
+
export declare const ROSTER_PATCH_BLOCK: string;
|
|
23
45
|
/** The profile patch file the roster block belongs in. */
|
|
24
46
|
export declare function rosterPatchPath(home: string, profile: string): string;
|
|
47
|
+
/** One row a patch file declares, with enough position to point at it. */
|
|
48
|
+
export interface PatchRowRef {
|
|
49
|
+
/** Insert id, or the target of an override/disable entry. */
|
|
50
|
+
id: string;
|
|
51
|
+
/** Module name for an insert that mounts one. */
|
|
52
|
+
name?: string;
|
|
53
|
+
kind: 'insert' | 'override' | 'disable' | 'other';
|
|
54
|
+
/** 0-based top-level entry index. */
|
|
55
|
+
entry: number;
|
|
56
|
+
/** 0-based index inside the entry's insert list (inserts only). */
|
|
57
|
+
item?: number;
|
|
58
|
+
/** 1-based line the id is written on, when the text can be scanned. */
|
|
59
|
+
line: number;
|
|
60
|
+
}
|
|
61
|
+
export interface PatchAnalysis {
|
|
62
|
+
/** The YAML parses into a top-level array of patch entries. */
|
|
63
|
+
parseable: boolean;
|
|
64
|
+
/** What the loader would reject, when it does not parse. */
|
|
65
|
+
error?: string;
|
|
66
|
+
rows: PatchRowRef[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read one patch file into the rows it declares.
|
|
70
|
+
*
|
|
71
|
+
* Unparseable YAML is reported instead of thrown: `/doctor` has to *describe*
|
|
72
|
+
* a broken patch, which is exactly when the loader would refuse to boot.
|
|
73
|
+
* @param text - the patch file's content.
|
|
74
|
+
* @returns the rows, and whether the text parsed at all.
|
|
75
|
+
*/
|
|
76
|
+
export declare function analyzePatch(text: string): PatchAnalysis;
|
|
77
|
+
/**
|
|
78
|
+
* Insert rows that appear more than once, keeping the first occurrence.
|
|
79
|
+
*
|
|
80
|
+
* A row is duplicated when an earlier insert declares the same id, or mounts
|
|
81
|
+
* the same module name — the two ways a second mount reaches the loader and
|
|
82
|
+
* fails with "service … has been registered". Overrides and disables are never
|
|
83
|
+
* duplicates of an insert: they target a row instead of mounting it.
|
|
84
|
+
* @param rows - every row the patch declares.
|
|
85
|
+
* @returns the later occurrences, in file order.
|
|
86
|
+
*/
|
|
87
|
+
export declare function duplicatePatchRows(rows: readonly PatchRowRef[]): PatchRowRef[];
|
|
88
|
+
/** Whether a patch already declares one roster row, by id or by module name. */
|
|
89
|
+
export declare function patchNamesRow(rows: readonly PatchRowRef[], row: RosterRow): boolean;
|
|
90
|
+
/** The roster rows a patch does not declare yet. */
|
|
91
|
+
export declare function missingRosterRows(rows: readonly PatchRowRef[]): RosterRow[];
|
|
92
|
+
export interface PatchRepair {
|
|
93
|
+
text: string;
|
|
94
|
+
/** Roster rows this repair adds, by id. */
|
|
95
|
+
added: string[];
|
|
96
|
+
/** Duplicate rows this repair removes, by id, with the line they sat on. */
|
|
97
|
+
removed: Array<{
|
|
98
|
+
id: string;
|
|
99
|
+
line: number;
|
|
100
|
+
}>;
|
|
101
|
+
}
|
|
25
102
|
/**
|
|
26
|
-
* The patch text with the roster
|
|
27
|
-
* already
|
|
103
|
+
* The patch text with the given roster rows mounted, or `undefined` when the
|
|
104
|
+
* file already declares every one of them.
|
|
28
105
|
*
|
|
29
|
-
* A
|
|
30
|
-
*
|
|
31
|
-
*
|
|
106
|
+
* A row is matched by id or by module name, so a profile that composes the
|
|
107
|
+
* roster through its own bundle is left alone. A file that is exactly `[]`
|
|
108
|
+
* (the profile template) is replaced; anything else keeps its content and gains
|
|
109
|
+
* a new `- insert:` entry after a blank line.
|
|
32
110
|
* @param existing - the patch file's current text.
|
|
111
|
+
* @param missing - the roster rows to mount; defaults to all of them.
|
|
33
112
|
* @returns the new text, or `undefined` when nothing has to change.
|
|
34
113
|
*/
|
|
35
|
-
export declare function
|
|
114
|
+
export declare function planRosterRepair(existing: string, missing?: readonly RosterRow[]): PatchRepair | undefined;
|
|
115
|
+
/**
|
|
116
|
+
* The patch text with every repeated insert removed, or `undefined` when the
|
|
117
|
+
* file has no duplicate.
|
|
118
|
+
*
|
|
119
|
+
* Only the later occurrence is cut, and only its own item when the enclosing
|
|
120
|
+
* `- insert:` entry mounts other rows too. Comments directly above a removed
|
|
121
|
+
* entry go with it; anything the user wrote around it stays.
|
|
122
|
+
* @param existing - the patch file's current text.
|
|
123
|
+
* @returns the new text and what was removed, or `undefined` when nothing is.
|
|
124
|
+
*/
|
|
125
|
+
export declare function planDuplicateRepair(existing: string): PatchRepair | undefined;
|
|
36
126
|
/**
|
|
37
127
|
* Mount the roster in one profile, unless it is already composed.
|
|
38
128
|
*
|
|
@@ -42,6 +132,20 @@ export declare function rosterPatchText(existing: string): string | undefined;
|
|
|
42
132
|
* half-written patch would only be seen by the next launch.
|
|
43
133
|
* @param home - the harness home carrying `profiles/`.
|
|
44
134
|
* @param profile - the profile to patch.
|
|
135
|
+
* @param missing - the rows to mount; defaults to the whole roster.
|
|
45
136
|
* @returns `present` when the row already exists, else `written`.
|
|
46
137
|
*/
|
|
47
|
-
export declare function ensureRosterRows(home: string, profile: string): Promise<'present' | 'written'>;
|
|
138
|
+
export declare function ensureRosterRows(home: string, profile: string, missing?: readonly RosterRow[]): Promise<'present' | 'written'>;
|
|
139
|
+
/**
|
|
140
|
+
* Write a repaired patch next to a copy of the previous content.
|
|
141
|
+
*
|
|
142
|
+
* `/doctor` changes a file the user owns, so the old text is kept beside it
|
|
143
|
+
* before anything is replaced. A file that does not exist yet needs no backup.
|
|
144
|
+
* @param path - the patch file to replace.
|
|
145
|
+
* @param text - the new content.
|
|
146
|
+
* @param stamp - filename-safe timestamp for the backup, injectable for tests.
|
|
147
|
+
* @returns the backup path, or `undefined` when there was nothing to back up.
|
|
148
|
+
*/
|
|
149
|
+
export declare function writePatchWithBackup(path: string, text: string, stamp?: string): Promise<string | undefined>;
|
|
150
|
+
/** The patch text the roster block would produce on its own, for callers that report it. */
|
|
151
|
+
export declare function rosterPatchText(existing: string): string | undefined;
|