@deepseek-harness-tui/dsh-tui 0.7.0 → 0.7.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.md +9 -6
- package/bin/dsh-tui.js +67 -14
- package/cordis.patch.yml +10 -0
- package/lib/types/commands.js +1 -1
- package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
- package/lib/types/components/LoadedContextPanel.js +1 -1
- package/lib/types/components/design-system/ThemedText.d.ts +2 -2
- package/lib/types/components/design-system/ThemedText.d.ts.map +1 -1
- package/lib/types/components/design-system/ThemedText.js +1 -3
- package/lib/types/components/sessions/SessionListRow.d.ts +26 -0
- package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -0
- package/lib/types/components/sessions/SessionListRow.js +35 -0
- package/lib/types/components/sessions/SessionPreview.d.ts +25 -0
- package/lib/types/components/sessions/SessionPreview.d.ts.map +1 -0
- package/lib/types/components/sessions/SessionPreview.js +60 -0
- package/lib/types/dsh-adapter/channel.d.ts +23 -4
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +190 -75
- package/lib/types/dsh-adapter/compat/index.d.ts +1 -1
- package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/index.js +1 -1
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts +8 -0
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/sessionLog.js +1 -1
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +10 -1
- package/lib/types/dsh-adapter/packaged-presets.d.ts +22 -0
- package/lib/types/dsh-adapter/packaged-presets.d.ts.map +1 -0
- package/lib/types/dsh-adapter/packaged-presets.js +90 -0
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +60 -4
- package/lib/types/dsh-adapter/presets.d.ts +16 -0
- package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
- package/lib/types/dsh-adapter/presets.js +23 -0
- package/lib/types/dsh-adapter/sessions/digest.d.ts +29 -0
- package/lib/types/dsh-adapter/sessions/digest.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessions/digest.js +224 -0
- package/lib/types/dsh-adapter/sessions/frames.d.ts +100 -0
- package/lib/types/dsh-adapter/sessions/frames.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessions/frames.js +246 -0
- package/lib/types/dsh-adapter/sessions/header.d.ts +50 -0
- package/lib/types/dsh-adapter/sessions/header.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessions/header.js +62 -0
- package/lib/types/dsh-adapter/sessions/index.d.ts +17 -0
- package/lib/types/dsh-adapter/sessions/index.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessions/index.js +15 -0
- package/lib/types/dsh-adapter/sessions/list.d.ts +41 -0
- package/lib/types/dsh-adapter/sessions/list.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessions/list.js +213 -0
- package/lib/types/dsh-adapter/sessions/store.d.ts +48 -0
- package/lib/types/dsh-adapter/sessions/store.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessions/store.js +164 -0
- package/lib/types/dsh-adapter/sessions/types.d.ts +142 -0
- package/lib/types/dsh-adapter/sessions/types.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessions/types.js +14 -0
- package/lib/types/i18n.d.ts +122 -14
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +37 -6
- package/lib/types/screens/Chat.d.ts +3 -2
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +28 -144
- package/lib/types/screens/SessionBrowser.d.ts +37 -0
- package/lib/types/screens/SessionBrowser.d.ts.map +1 -0
- package/lib/types/screens/SessionBrowser.js +437 -0
- package/lib/types/sessionHistory.d.ts +0 -8
- package/lib/types/sessionHistory.d.ts.map +1 -1
- package/lib/types/sessions/format.d.ts +117 -0
- package/lib/types/sessions/format.d.ts.map +1 -0
- package/lib/types/sessions/format.js +244 -0
- package/lib/types/sessions/view.d.ts +125 -0
- package/lib/types/sessions/view.d.ts.map +1 -0
- package/lib/types/sessions/view.js +222 -0
- package/package.json +12 -5
- package/presets/liangshen/.dsh-tui-managed.json +5 -0
- package/presets/liangshen/agent.cordis.yml +389 -0
- package/presets/liangshen/compaction-epoch.mjs +81 -0
- package/presets/liangshen/custom-bash.mjs +126 -0
- package/presets/liangshen/instruction-hint.mjs +181 -0
- package/presets/liangshen/preset.yml +3 -0
- package/presets/liangshen/skill-search.mjs +142 -0
- package/presets/liangshen/tool-bootstrap.mjs +300 -0
- package/lib/invariant.js +0 -28
- package/lib/types/components/ResumePicker.d.ts +0 -26
- package/lib/types/components/ResumePicker.d.ts.map +0 -1
- package/lib/types/components/ResumePicker.js +0 -50
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session browser formatting.
|
|
3
|
+
*
|
|
4
|
+
* Presentation only, and deliberately outside the adapter: these are choices
|
|
5
|
+
* about a terminal's width and a reader's eye, not about what a session log
|
|
6
|
+
* means.
|
|
7
|
+
*
|
|
8
|
+
* @module @deepseek-harness-tui/dsh-tui/sessions/format
|
|
9
|
+
*/
|
|
10
|
+
import { t } from '../i18n.js';
|
|
11
|
+
import { stringWidth } from '../ink/stringWidth.js';
|
|
12
|
+
/**
|
|
13
|
+
* Truncate to a terminal DISPLAY width, CJK-aware (a wide character costs two
|
|
14
|
+
* columns).
|
|
15
|
+
*
|
|
16
|
+
* Used wherever the cut has to be exact: Ink's own `wrap="truncate"` appends
|
|
17
|
+
* its ellipsis as soon as content is as wide as its box rather than wider,
|
|
18
|
+
* which silently eats the last character of a row laid out at its natural
|
|
19
|
+
* width — and a row that then reflows pushes every row below it down.
|
|
20
|
+
*
|
|
21
|
+
* @param text - Plain text, no ANSI.
|
|
22
|
+
* @param maxWidth - Column budget, ellipsis included.
|
|
23
|
+
* @returns `text` when it fits, otherwise a cut ending in `…`.
|
|
24
|
+
*/
|
|
25
|
+
export function truncateWidth(text, maxWidth) {
|
|
26
|
+
if (maxWidth <= 0)
|
|
27
|
+
return '';
|
|
28
|
+
if (stringWidth(text) <= maxWidth)
|
|
29
|
+
return text;
|
|
30
|
+
let width = 0;
|
|
31
|
+
let out = '';
|
|
32
|
+
for (const char of text) {
|
|
33
|
+
const charWidth = stringWidth(char);
|
|
34
|
+
if (width + charWidth > maxWidth - 1)
|
|
35
|
+
break;
|
|
36
|
+
width += charWidth;
|
|
37
|
+
out += char;
|
|
38
|
+
}
|
|
39
|
+
return `${out}…`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Lay out one row with its two ends pushed apart.
|
|
43
|
+
*
|
|
44
|
+
* Measured in COLUMNS, which is the whole reason this is a function rather
|
|
45
|
+
* than three expressions at the call site. Every string it receives is
|
|
46
|
+
* localized, and in Chinese a character is two columns wide — arithmetic on
|
|
47
|
+
* `.length` overstates the gap by the width of the text itself, so the row
|
|
48
|
+
* overflows and the terminal either wraps it (shifting every region below it
|
|
49
|
+
* down a line) or clips it (silently eating the right-hand segment). Both
|
|
50
|
+
* failures are invisible to an English-only test.
|
|
51
|
+
*
|
|
52
|
+
* The invariant, which the regression checks exhaustively: the assembled row
|
|
53
|
+
* is never wider than `columns`.
|
|
54
|
+
*
|
|
55
|
+
* @param left - Text pinned to the start of the row.
|
|
56
|
+
* @param right - Text pinned to the end, truncated when it will not fit.
|
|
57
|
+
* @param columns - Total width available.
|
|
58
|
+
*/
|
|
59
|
+
export function spreadRow(left, right, columns) {
|
|
60
|
+
if (columns <= 0)
|
|
61
|
+
return { left: '', gap: 0, right: '' };
|
|
62
|
+
// One column of separation is always reserved, so the two segments can never
|
|
63
|
+
// run together into an unreadable single word — which means the left segment
|
|
64
|
+
// yields too when the row is narrower than it is.
|
|
65
|
+
const fittedLeft = truncateWidth(left, Math.max(0, columns - 1));
|
|
66
|
+
const leftWidth = stringWidth(fittedLeft);
|
|
67
|
+
const fittedRight = truncateWidth(right, Math.max(0, columns - leftWidth - 1));
|
|
68
|
+
return {
|
|
69
|
+
left: fittedLeft,
|
|
70
|
+
gap: Math.max(1, columns - leftWidth - stringWidth(fittedRight)),
|
|
71
|
+
right: fittedRight,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Keep the END of a string within a display width, CJK-aware.
|
|
76
|
+
*
|
|
77
|
+
* What a single-line editor does: the caret lives at the end, so that is the
|
|
78
|
+
* part worth showing. Used where text the user is typing has to stay on ONE
|
|
79
|
+
* row — a wrapped input row steals a line from the list and can push the row
|
|
80
|
+
* below it off the bottom of the screen.
|
|
81
|
+
*
|
|
82
|
+
* @param text - Plain text, no ANSI.
|
|
83
|
+
* @param maxWidth - Column budget, leading ellipsis included.
|
|
84
|
+
* @returns `text` when it fits, otherwise `…` followed by its tail.
|
|
85
|
+
*/
|
|
86
|
+
export function tailWidth(text, maxWidth) {
|
|
87
|
+
if (maxWidth <= 0)
|
|
88
|
+
return '';
|
|
89
|
+
if (stringWidth(text) <= maxWidth)
|
|
90
|
+
return text;
|
|
91
|
+
const characters = [...text];
|
|
92
|
+
let width = 0;
|
|
93
|
+
let out = '';
|
|
94
|
+
for (let at = characters.length - 1; at >= 0; at--) {
|
|
95
|
+
const character = characters[at];
|
|
96
|
+
const characterWidth = stringWidth(character);
|
|
97
|
+
if (width + characterWidth > maxWidth - 1)
|
|
98
|
+
break;
|
|
99
|
+
width += characterWidth;
|
|
100
|
+
out = character + out;
|
|
101
|
+
}
|
|
102
|
+
return `…${out}`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Elapsed time as a person would say it: `just now`, `9 hours ago`, and an
|
|
106
|
+
* absolute date once "ago" stops being useful.
|
|
107
|
+
*
|
|
108
|
+
* The cut to an absolute date is at a week rather than a month because that is
|
|
109
|
+
* roughly where a relative offset stops locating anything — "23 days ago" is a
|
|
110
|
+
* number to be decoded, `Mar 3` is a memory.
|
|
111
|
+
*
|
|
112
|
+
* @param at - Epoch ms of the moment being described.
|
|
113
|
+
* @param now - Epoch ms of the present, injectable so the formatter is a pure
|
|
114
|
+
* function and its regression does not depend on a clock.
|
|
115
|
+
*/
|
|
116
|
+
export function formatWhen(at, now) {
|
|
117
|
+
const seconds = Math.max(0, Math.round((now - at) / 1000));
|
|
118
|
+
if (seconds < 45)
|
|
119
|
+
return t('session-when-now');
|
|
120
|
+
const minutes = Math.round(seconds / 60);
|
|
121
|
+
if (minutes < 60)
|
|
122
|
+
return t('session-when-minutes', { n: minutes });
|
|
123
|
+
const hours = Math.round(minutes / 60);
|
|
124
|
+
if (hours < 24)
|
|
125
|
+
return t('session-when-hours', { n: hours });
|
|
126
|
+
const days = Math.round(hours / 24);
|
|
127
|
+
if (days <= 7)
|
|
128
|
+
return t('session-when-days', { n: days });
|
|
129
|
+
const date = new Date(at);
|
|
130
|
+
return t('session-when-date', {
|
|
131
|
+
month: String(date.getMonth() + 1),
|
|
132
|
+
day: String(date.getDate()),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Byte size at the precision the number is worth: `812 B`, `142.9 KB`,
|
|
137
|
+
* `4.2 MB`. One decimal from kilobytes up, because the digit distinguishes a
|
|
138
|
+
* short exchange from a long one and a second would not.
|
|
139
|
+
*/
|
|
140
|
+
export function formatBytes(bytes) {
|
|
141
|
+
if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0)
|
|
142
|
+
return undefined;
|
|
143
|
+
if (bytes < 1024)
|
|
144
|
+
return `${bytes} B`;
|
|
145
|
+
if (bytes < 1024 * 1024)
|
|
146
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
147
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Marker for a row's kind, or undefined for an ordinary conversation.
|
|
151
|
+
*
|
|
152
|
+
* Only the exceptional kinds are marked. A badge on every row would cost a
|
|
153
|
+
* column of width and teach nothing — what a reader needs to see at a glance
|
|
154
|
+
* is which rows are *not* the thing they came for.
|
|
155
|
+
*/
|
|
156
|
+
export function kindMark(kind) {
|
|
157
|
+
if (kind.kind === 'subagent')
|
|
158
|
+
return { glyph: '⑂', color: 'autoAccept' };
|
|
159
|
+
if (kind.kind === 'fork')
|
|
160
|
+
return { glyph: '⑃', color: 'planMode' };
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Wrap to a display width, CJK-aware.
|
|
165
|
+
*
|
|
166
|
+
* Greedy, breaking on a space when one is available in the line just filled
|
|
167
|
+
* and mid-character when it is not — which is the correct behaviour for CJK,
|
|
168
|
+
* where there are no spaces to break on and a word-only wrapper would emit one
|
|
169
|
+
* enormous unbreakable line. Newlines in the input are honoured.
|
|
170
|
+
*
|
|
171
|
+
* @param text - Plain text, no ANSI.
|
|
172
|
+
* @param width - Column budget per line.
|
|
173
|
+
* @returns The wrapped lines, never empty for non-empty input.
|
|
174
|
+
*/
|
|
175
|
+
export function wrapWidth(text, width) {
|
|
176
|
+
if (width <= 0)
|
|
177
|
+
return [];
|
|
178
|
+
const lines = [];
|
|
179
|
+
for (const paragraph of text.split('\n')) {
|
|
180
|
+
let line = '';
|
|
181
|
+
let used = 0;
|
|
182
|
+
for (const char of paragraph) {
|
|
183
|
+
const charWidth = stringWidth(char);
|
|
184
|
+
if (used + charWidth > width) {
|
|
185
|
+
// Prefer a word boundary, but only when it does not throw away most
|
|
186
|
+
// of the line — a single long token must still make progress.
|
|
187
|
+
const breakAt = line.lastIndexOf(' ');
|
|
188
|
+
if (breakAt > width / 2) {
|
|
189
|
+
lines.push(line.slice(0, breakAt));
|
|
190
|
+
line = line.slice(breakAt + 1);
|
|
191
|
+
used = stringWidth(line);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
lines.push(line);
|
|
195
|
+
line = '';
|
|
196
|
+
used = 0;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
line += char;
|
|
200
|
+
used += charWidth;
|
|
201
|
+
}
|
|
202
|
+
lines.push(line);
|
|
203
|
+
}
|
|
204
|
+
return lines;
|
|
205
|
+
}
|
|
206
|
+
/** Human name of a session kind, for the preview pane's header. */
|
|
207
|
+
export function kindLabel(kind) {
|
|
208
|
+
if (kind.kind === 'subagent')
|
|
209
|
+
return t('session-kind-subagent');
|
|
210
|
+
if (kind.kind === 'fork')
|
|
211
|
+
return t('session-kind-fork');
|
|
212
|
+
return t('session-kind-root');
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Colour for a title, by how much is actually known about it.
|
|
216
|
+
*
|
|
217
|
+
* A title the user chose is stated plainly; one a model generated reads the
|
|
218
|
+
* same, because it is a real title; a first-prompt excerpt is dimmer, because
|
|
219
|
+
* it is the session speaking rather than a name; a directory basename is
|
|
220
|
+
* dimmest of all, because it says only "nothing here was readable".
|
|
221
|
+
*/
|
|
222
|
+
export function titleColor(source, focused) {
|
|
223
|
+
if (focused)
|
|
224
|
+
return 'suggestion';
|
|
225
|
+
if (source === 'fallback')
|
|
226
|
+
return 'subtle';
|
|
227
|
+
if (source === 'prompt')
|
|
228
|
+
return 'inactive';
|
|
229
|
+
return 'text';
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Project label for a group header: the working directory, with `$HOME`
|
|
233
|
+
* collapsed to `~` so the eye lands on the part that differs.
|
|
234
|
+
*/
|
|
235
|
+
export function formatProject(cwd, home) {
|
|
236
|
+
if (cwd.length === 0)
|
|
237
|
+
return t('session-project-unknown');
|
|
238
|
+
const normalized = cwd.replace(/\\/g, '/');
|
|
239
|
+
const base = home.replace(/\\/g, '/').replace(/\/$/, '');
|
|
240
|
+
if (base.length > 0 && (normalized === base || normalized.startsWith(`${base}/`))) {
|
|
241
|
+
return `~${normalized.slice(base.length)}`;
|
|
242
|
+
}
|
|
243
|
+
return normalized;
|
|
244
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session browser's view model.
|
|
3
|
+
*
|
|
4
|
+
* Every decision about *which* sessions are on screen and in what order lives
|
|
5
|
+
* here, as one pure function from the full list plus the current filters to a
|
|
6
|
+
* flat list of rows. Two reasons it is shaped that way:
|
|
7
|
+
*
|
|
8
|
+
* - Filtering is where this feature's bugs were. Keeping it pure means the
|
|
9
|
+
* truth table — a sub-agent run is hidden, a rewind fork is not, a boot
|
|
10
|
+
* artifact is hidden but counted — is checkable without rendering anything.
|
|
11
|
+
* - Rows are flattened rather than nested. A windowed list of variable-height
|
|
12
|
+
* groups has to guess how much room each group needs; a flat list of
|
|
13
|
+
* single-line rows is windowed by arithmetic, and the group headers simply
|
|
14
|
+
* travel with their sessions.
|
|
15
|
+
*
|
|
16
|
+
* @module @deepseek-harness-tui/dsh-tui/sessions/view
|
|
17
|
+
*/
|
|
18
|
+
import type { SessionSummary } from '../dsh-adapter/sessions/index.js';
|
|
19
|
+
/** What the browser is currently showing. */
|
|
20
|
+
export interface BrowserFilters {
|
|
21
|
+
/** Free-text query; empty means no text filter. */
|
|
22
|
+
readonly query: string;
|
|
23
|
+
/** Show every project, not just the session's own working directory. */
|
|
24
|
+
readonly allProjects: boolean;
|
|
25
|
+
/** Show only sessions last used on the current git branch. */
|
|
26
|
+
readonly branchOnly: boolean;
|
|
27
|
+
/** Reveal delegated sub-agent runs, indented under their parents. */
|
|
28
|
+
readonly showSubagents: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** The browser's default view: this project, conversations only. */
|
|
31
|
+
export declare const DEFAULT_FILTERS: BrowserFilters;
|
|
32
|
+
/** One line in the browser's list. */
|
|
33
|
+
export type BrowserRow = {
|
|
34
|
+
readonly kind: 'project';
|
|
35
|
+
readonly project: string;
|
|
36
|
+
readonly count: number;
|
|
37
|
+
} | {
|
|
38
|
+
readonly kind: 'session';
|
|
39
|
+
readonly session: SessionSummary;
|
|
40
|
+
readonly depth: number;
|
|
41
|
+
};
|
|
42
|
+
/** The rendered list plus what it left out and why. */
|
|
43
|
+
export interface BrowserView {
|
|
44
|
+
readonly rows: readonly BrowserRow[];
|
|
45
|
+
/** Sessions shown, excluding group headers. */
|
|
46
|
+
readonly shown: number;
|
|
47
|
+
/** Delegated runs folded away by the current filters. */
|
|
48
|
+
readonly hiddenSubagents: number;
|
|
49
|
+
/** Sessions with no conversation in them, never listed but worth counting. */
|
|
50
|
+
readonly emptyCount: number;
|
|
51
|
+
/** Ids of those empty sessions, for the cleanup action. */
|
|
52
|
+
readonly emptyIds: readonly string[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Build the browser's rows.
|
|
56
|
+
*
|
|
57
|
+
* @param sessions - Every stored session, newest first, as the adapter listed
|
|
58
|
+
* them. Nothing is pre-filtered.
|
|
59
|
+
* @param filters - The current view.
|
|
60
|
+
* @param context - The session doing the browsing: its working directory
|
|
61
|
+
* anchors the default project filter, its branch the branch filter, and its
|
|
62
|
+
* own id is never offered (a live session cannot be resumed into itself).
|
|
63
|
+
* @returns The flat row list and the counts behind what it hides.
|
|
64
|
+
*/
|
|
65
|
+
export declare function buildView(sessions: readonly SessionSummary[], filters: BrowserFilters, context: {
|
|
66
|
+
cwd: string;
|
|
67
|
+
branch: string | undefined;
|
|
68
|
+
currentId: string;
|
|
69
|
+
sameProject: (a: string, b: string) => boolean;
|
|
70
|
+
}): BrowserView;
|
|
71
|
+
/**
|
|
72
|
+
* Index of the first selectable row at or after `from`.
|
|
73
|
+
*
|
|
74
|
+
* Group headers are rows but not targets, so every movement resolves through
|
|
75
|
+
* here rather than each caller re-deriving "is this one selectable".
|
|
76
|
+
*
|
|
77
|
+
* @param rows - The view's rows.
|
|
78
|
+
* @param from - Where to start looking.
|
|
79
|
+
* @param step - +1 to search forward, -1 backward.
|
|
80
|
+
* @returns The index, or -1 when no selectable row lies that way.
|
|
81
|
+
*/
|
|
82
|
+
export declare function seekSelectable(rows: readonly BrowserRow[], from: number, step: 1 | -1): number;
|
|
83
|
+
/**
|
|
84
|
+
* Move the selection by one selectable row, wrapping at both ends.
|
|
85
|
+
*
|
|
86
|
+
* @param rows - The view's rows.
|
|
87
|
+
* @param current - Current index.
|
|
88
|
+
* @param step - +1 for down, -1 for up.
|
|
89
|
+
* @returns The new index, or the current one when nothing is selectable.
|
|
90
|
+
*/
|
|
91
|
+
export declare function moveSelection(rows: readonly BrowserRow[], current: number, step: 1 | -1): number;
|
|
92
|
+
/** The session under the cursor, when the cursor is on one. */
|
|
93
|
+
export declare function sessionAt(rows: readonly BrowserRow[], index: number): SessionSummary | undefined;
|
|
94
|
+
/** Lines one row occupies: a session shows a title and a metadata line. */
|
|
95
|
+
export declare function rowHeight(row: BrowserRow): number;
|
|
96
|
+
/**
|
|
97
|
+
* Where the visible window should start.
|
|
98
|
+
*
|
|
99
|
+
* Rows have different heights, so the window cannot be `focus ± n`: a slice
|
|
100
|
+
* that looks right by index can overflow the box by lines, and a fixed-height
|
|
101
|
+
* box whose content overflows renders its rows on top of each other. This
|
|
102
|
+
* resolves the window in LINES, which is the unit the box is measured in.
|
|
103
|
+
*
|
|
104
|
+
* The window is anchored rather than centred: it moves only as far as it must
|
|
105
|
+
* to keep the focused row fully visible, so scrolling a long list does not
|
|
106
|
+
* re-shuffle everything on screen under every keystroke.
|
|
107
|
+
*
|
|
108
|
+
* @param rows - The view's rows.
|
|
109
|
+
* @param focus - Index of the focused row.
|
|
110
|
+
* @param budget - Lines available to the list.
|
|
111
|
+
* @param previous - The previous window start, so a stationary focus keeps a
|
|
112
|
+
* stationary window.
|
|
113
|
+
* @returns The new window start index.
|
|
114
|
+
*/
|
|
115
|
+
export declare function anchorTop(rows: readonly BrowserRow[], focus: number, budget: number, previous: number): number;
|
|
116
|
+
/**
|
|
117
|
+
* How many rows starting at `top` fit in `budget` lines.
|
|
118
|
+
*
|
|
119
|
+
* @param rows - The view's rows.
|
|
120
|
+
* @param top - First visible row.
|
|
121
|
+
* @param budget - Lines available.
|
|
122
|
+
* @returns Exclusive end index of the visible slice.
|
|
123
|
+
*/
|
|
124
|
+
export declare function windowEnd(rows: readonly BrowserRow[], top: number, budget: number): number;
|
|
125
|
+
//# sourceMappingURL=view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../../src/sessions/view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAA;AAEtE,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,wEAAwE;IACxE,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAA;IAC5B,qEAAqE;IACrE,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAA;CAChC;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,EAAE,cAK7B,CAAA;AAED,sCAAsC;AACtC,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC9E;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAE1F,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,CAAA;IACpC,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,yDAAyD;IACzD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CACrC;AAgBD;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,SAAS,cAAc,EAAE,EACnC,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAA;CAAE,GACtH,WAAW,CAuFb;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAK9F;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAKhG;AAED,+DAA+D;AAC/D,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAGhG;AAED,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAEjD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,SAAS,UAAU,EAAE,EAC3B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,MAAM,CAoBR;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAU1F"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/** The browser's default view: this project, conversations only. */
|
|
2
|
+
export const DEFAULT_FILTERS = {
|
|
3
|
+
query: '',
|
|
4
|
+
allProjects: false,
|
|
5
|
+
branchOnly: false,
|
|
6
|
+
showSubagents: false,
|
|
7
|
+
};
|
|
8
|
+
/** Case-insensitive substring test over the fields a reader would search by. */
|
|
9
|
+
function matches(session, needle) {
|
|
10
|
+
if (needle.length === 0)
|
|
11
|
+
return true;
|
|
12
|
+
const haystack = [
|
|
13
|
+
session.title.text,
|
|
14
|
+
session.cwd,
|
|
15
|
+
session.branch ?? '',
|
|
16
|
+
session.model ?? '',
|
|
17
|
+
session.label ?? '',
|
|
18
|
+
session.agentPreset ?? '',
|
|
19
|
+
];
|
|
20
|
+
return haystack.some(field => field.toLowerCase().includes(needle));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build the browser's rows.
|
|
24
|
+
*
|
|
25
|
+
* @param sessions - Every stored session, newest first, as the adapter listed
|
|
26
|
+
* them. Nothing is pre-filtered.
|
|
27
|
+
* @param filters - The current view.
|
|
28
|
+
* @param context - The session doing the browsing: its working directory
|
|
29
|
+
* anchors the default project filter, its branch the branch filter, and its
|
|
30
|
+
* own id is never offered (a live session cannot be resumed into itself).
|
|
31
|
+
* @returns The flat row list and the counts behind what it hides.
|
|
32
|
+
*/
|
|
33
|
+
export function buildView(sessions, filters, context) {
|
|
34
|
+
const needle = filters.query.trim().toLowerCase();
|
|
35
|
+
const inScope = (session) => session.id !== context.currentId &&
|
|
36
|
+
(filters.allProjects || context.sameProject(context.cwd, session.cwd));
|
|
37
|
+
// Empty sessions never reach a view, but they are counted — and the count
|
|
38
|
+
// feeds a DESTRUCTIVE action, so it is scoped exactly as the list is.
|
|
39
|
+
// Counting them across every project while showing one project's rows would
|
|
40
|
+
// put "clean up 15 sessions" on screen next to six, and deleting another
|
|
41
|
+
// project's history from a view that never mentioned it is not a surprise
|
|
42
|
+
// anyone should get. The search query deliberately does NOT narrow it:
|
|
43
|
+
// typing a filter is about finding one session, not about redefining what
|
|
44
|
+
// "empty" means.
|
|
45
|
+
const emptyIds = [];
|
|
46
|
+
for (const session of sessions) {
|
|
47
|
+
if (!session.hasPrompt && inScope(session))
|
|
48
|
+
emptyIds.push(session.id);
|
|
49
|
+
}
|
|
50
|
+
const empty = new Set(emptyIds);
|
|
51
|
+
const eligible = sessions.filter(session => inScope(session) &&
|
|
52
|
+
!empty.has(session.id) &&
|
|
53
|
+
(!filters.branchOnly || (context.branch !== undefined && session.branch === context.branch)));
|
|
54
|
+
const conversations = eligible.filter(session => session.kind.kind !== 'subagent');
|
|
55
|
+
const runs = eligible.filter(session => session.kind.kind === 'subagent');
|
|
56
|
+
// A run is shown under its parent; one whose parent is not in this view
|
|
57
|
+
// (filtered out, or never listed) would otherwise be unreachable, so it is
|
|
58
|
+
// offered at the top level instead of silently dropped.
|
|
59
|
+
const byParent = new Map();
|
|
60
|
+
const orphans = [];
|
|
61
|
+
const visibleIds = new Set(conversations.map(session => session.id));
|
|
62
|
+
for (const run of runs) {
|
|
63
|
+
const parent = run.kind.kind === 'subagent' ? run.kind.parent : undefined;
|
|
64
|
+
if (parent !== undefined && visibleIds.has(parent)) {
|
|
65
|
+
const siblings = byParent.get(parent);
|
|
66
|
+
if (siblings === undefined)
|
|
67
|
+
byParent.set(parent, [run]);
|
|
68
|
+
else
|
|
69
|
+
siblings.push(run);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
orphans.push(run);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const top = filters.showSubagents ? [...conversations, ...orphans] : conversations;
|
|
76
|
+
const visible = top.filter(session => {
|
|
77
|
+
if (matches(session, needle))
|
|
78
|
+
return true;
|
|
79
|
+
// A parent whose own text does not match is still shown when one of its
|
|
80
|
+
// runs does — hiding it would strand the match under a row that is gone.
|
|
81
|
+
if (!filters.showSubagents)
|
|
82
|
+
return false;
|
|
83
|
+
return (byParent.get(session.id) ?? []).some(run => matches(run, needle));
|
|
84
|
+
});
|
|
85
|
+
const rows = [];
|
|
86
|
+
let shown = 0;
|
|
87
|
+
let lastProject;
|
|
88
|
+
for (const session of visible.sort((left, right) => right.updatedAt - left.updatedAt)) {
|
|
89
|
+
// Group headers only earn their line when more than one project is in
|
|
90
|
+
// play; inside a single project they would repeat the same path forever.
|
|
91
|
+
if (filters.allProjects && session.cwd !== lastProject) {
|
|
92
|
+
lastProject = session.cwd;
|
|
93
|
+
rows.push({
|
|
94
|
+
kind: 'project',
|
|
95
|
+
project: session.cwd,
|
|
96
|
+
count: visible.filter(other => other.cwd === session.cwd).length,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
rows.push({ kind: 'session', session, depth: 0 });
|
|
100
|
+
shown += 1;
|
|
101
|
+
if (!filters.showSubagents)
|
|
102
|
+
continue;
|
|
103
|
+
for (const run of (byParent.get(session.id) ?? []).filter(child => matches(child, needle) || matches(session, needle))) {
|
|
104
|
+
rows.push({ kind: 'session', session: run, depth: 1 });
|
|
105
|
+
shown += 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
rows,
|
|
110
|
+
shown,
|
|
111
|
+
hiddenSubagents: filters.showSubagents ? 0 : runs.length,
|
|
112
|
+
emptyCount: emptyIds.length,
|
|
113
|
+
emptyIds,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Index of the first selectable row at or after `from`.
|
|
118
|
+
*
|
|
119
|
+
* Group headers are rows but not targets, so every movement resolves through
|
|
120
|
+
* here rather than each caller re-deriving "is this one selectable".
|
|
121
|
+
*
|
|
122
|
+
* @param rows - The view's rows.
|
|
123
|
+
* @param from - Where to start looking.
|
|
124
|
+
* @param step - +1 to search forward, -1 backward.
|
|
125
|
+
* @returns The index, or -1 when no selectable row lies that way.
|
|
126
|
+
*/
|
|
127
|
+
export function seekSelectable(rows, from, step) {
|
|
128
|
+
for (let at = from; at >= 0 && at < rows.length; at += step) {
|
|
129
|
+
if (rows[at]?.kind === 'session')
|
|
130
|
+
return at;
|
|
131
|
+
}
|
|
132
|
+
return -1;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Move the selection by one selectable row, wrapping at both ends.
|
|
136
|
+
*
|
|
137
|
+
* @param rows - The view's rows.
|
|
138
|
+
* @param current - Current index.
|
|
139
|
+
* @param step - +1 for down, -1 for up.
|
|
140
|
+
* @returns The new index, or the current one when nothing is selectable.
|
|
141
|
+
*/
|
|
142
|
+
export function moveSelection(rows, current, step) {
|
|
143
|
+
const next = seekSelectable(rows, current + step, step);
|
|
144
|
+
if (next >= 0)
|
|
145
|
+
return next;
|
|
146
|
+
const wrapped = seekSelectable(rows, step === 1 ? 0 : rows.length - 1, step);
|
|
147
|
+
return wrapped >= 0 ? wrapped : current;
|
|
148
|
+
}
|
|
149
|
+
/** The session under the cursor, when the cursor is on one. */
|
|
150
|
+
export function sessionAt(rows, index) {
|
|
151
|
+
const row = rows[index];
|
|
152
|
+
return row?.kind === 'session' ? row.session : undefined;
|
|
153
|
+
}
|
|
154
|
+
/** Lines one row occupies: a session shows a title and a metadata line. */
|
|
155
|
+
export function rowHeight(row) {
|
|
156
|
+
return row.kind === 'session' ? 2 : 1;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Where the visible window should start.
|
|
160
|
+
*
|
|
161
|
+
* Rows have different heights, so the window cannot be `focus ± n`: a slice
|
|
162
|
+
* that looks right by index can overflow the box by lines, and a fixed-height
|
|
163
|
+
* box whose content overflows renders its rows on top of each other. This
|
|
164
|
+
* resolves the window in LINES, which is the unit the box is measured in.
|
|
165
|
+
*
|
|
166
|
+
* The window is anchored rather than centred: it moves only as far as it must
|
|
167
|
+
* to keep the focused row fully visible, so scrolling a long list does not
|
|
168
|
+
* re-shuffle everything on screen under every keystroke.
|
|
169
|
+
*
|
|
170
|
+
* @param rows - The view's rows.
|
|
171
|
+
* @param focus - Index of the focused row.
|
|
172
|
+
* @param budget - Lines available to the list.
|
|
173
|
+
* @param previous - The previous window start, so a stationary focus keeps a
|
|
174
|
+
* stationary window.
|
|
175
|
+
* @returns The new window start index.
|
|
176
|
+
*/
|
|
177
|
+
export function anchorTop(rows, focus, budget, previous) {
|
|
178
|
+
if (rows.length === 0 || budget <= 0)
|
|
179
|
+
return 0;
|
|
180
|
+
let top = Math.min(Math.max(0, previous), Math.max(0, rows.length - 1));
|
|
181
|
+
if (focus < top)
|
|
182
|
+
top = focus;
|
|
183
|
+
// Scroll down only until the focused row's last line fits.
|
|
184
|
+
for (;;) {
|
|
185
|
+
let used = 0;
|
|
186
|
+
for (let at = top; at <= focus; at++)
|
|
187
|
+
used += rowHeight(rows[at]);
|
|
188
|
+
if (used <= budget || top >= focus)
|
|
189
|
+
break;
|
|
190
|
+
top += 1;
|
|
191
|
+
}
|
|
192
|
+
// A window that has slack below the last row wastes it; pull the start back
|
|
193
|
+
// up so the final screenful is full rather than ragged.
|
|
194
|
+
let total = 0;
|
|
195
|
+
for (let at = top; at < rows.length; at++)
|
|
196
|
+
total += rowHeight(rows[at]);
|
|
197
|
+
while (top > 0 && total + rowHeight(rows[top - 1]) <= budget) {
|
|
198
|
+
top -= 1;
|
|
199
|
+
total += rowHeight(rows[top]);
|
|
200
|
+
}
|
|
201
|
+
return top;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* How many rows starting at `top` fit in `budget` lines.
|
|
205
|
+
*
|
|
206
|
+
* @param rows - The view's rows.
|
|
207
|
+
* @param top - First visible row.
|
|
208
|
+
* @param budget - Lines available.
|
|
209
|
+
* @returns Exclusive end index of the visible slice.
|
|
210
|
+
*/
|
|
211
|
+
export function windowEnd(rows, top, budget) {
|
|
212
|
+
let used = 0;
|
|
213
|
+
let at = top;
|
|
214
|
+
while (at < rows.length) {
|
|
215
|
+
const next = used + rowHeight(rows[at]);
|
|
216
|
+
if (next > budget)
|
|
217
|
+
break;
|
|
218
|
+
used = next;
|
|
219
|
+
at += 1;
|
|
220
|
+
}
|
|
221
|
+
return at;
|
|
222
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-harness-tui/dsh-tui",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Claude Code style interactive TUI front door for DeepSeek Harness agents, built on the ported Ink core.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,17 +39,17 @@
|
|
|
39
39
|
},
|
|
40
40
|
"./invariant": {
|
|
41
41
|
"types": "./lib/types/dsh-adapter/invariant.d.ts",
|
|
42
|
-
"default": "./lib/invariant.js"
|
|
42
|
+
"default": "./lib/types/dsh-adapter/invariant.js"
|
|
43
43
|
},
|
|
44
44
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
45
|
-
"./package.json": "./package.json"
|
|
46
|
-
"./src/*": "./src/*"
|
|
45
|
+
"./package.json": "./package.json"
|
|
47
46
|
},
|
|
48
47
|
"files": [
|
|
49
48
|
"bin",
|
|
50
49
|
"lib",
|
|
51
50
|
"cordis.patch.yml",
|
|
52
51
|
"cordis.yml",
|
|
52
|
+
"presets",
|
|
53
53
|
"skills"
|
|
54
54
|
],
|
|
55
55
|
"engines": {
|
|
@@ -149,12 +149,19 @@
|
|
|
149
149
|
}
|
|
150
150
|
},
|
|
151
151
|
"scripts": {
|
|
152
|
-
"
|
|
152
|
+
"clean": "node scripts/clean-lib.mjs",
|
|
153
|
+
"compile": "npm run clean && tsc -p tsconfig.json",
|
|
154
|
+
"build": "npm run compile && npm run verify:build",
|
|
153
155
|
"tui": "node --import tsx/esm scripts/run.ts",
|
|
154
156
|
"smoke": "node --import tsx/esm scripts/smoke.tsx",
|
|
157
|
+
"verify:build": "npm run verify:boundary && npm run verify:contract && npm run verify:patch-surface && npm run verify:packaged-presets && npm run verify:minimal-preset-tools && npm run verify:liangshen-bootstrap",
|
|
155
158
|
"verify:boundary": "node --import tsx/esm scripts/verify-adapter-boundary.ts",
|
|
156
159
|
"verify:contract": "node --import tsx/esm scripts/verify-upstream-contract.ts",
|
|
157
160
|
"verify:patch-surface": "node --import tsx/esm scripts/verify-patch-surface.ts",
|
|
161
|
+
"verify:packaged-presets": "node scripts/verify-packaged-presets.mjs",
|
|
162
|
+
"verify:minimal-preset-tools": "node scripts/verify-minimal-preset-tools.mjs",
|
|
163
|
+
"verify:liangshen-bootstrap": "node scripts/verify-liangshen-bootstrap.mjs",
|
|
164
|
+
"verify:package": "npm pack --dry-run --json --ignore-scripts | node scripts/verify-package.mjs",
|
|
158
165
|
"verify:clipboard-image": "node --import tsx/esm scripts/verify-clipboard-image.ts"
|
|
159
166
|
}
|
|
160
167
|
}
|