moqi-tui 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +782 -0
- package/bin/moqi.mjs +40 -0
- package/cordis.patch.yml +41 -0
- package/lib/cross-find.js +217 -0
- package/lib/file-index.js +121 -0
- package/lib/fleet-sources.js +114 -0
- package/lib/index.js +3999 -0
- package/lib/persist.js +194 -0
- package/lib/plugins.js +371 -0
- package/lib/presence.js +144 -0
- package/lib/rename.js +35 -0
- package/lib/rewind.js +94 -0
- package/lib/sessions-store.js +134 -0
- package/lib/startup.js +92 -0
- package/lib/tui/atfile.js +154 -0
- package/lib/tui/export.js +48 -0
- package/lib/tui/fleet.js +346 -0
- package/lib/tui/i18n.js +201 -0
- package/lib/tui/jobs.js +65 -0
- package/lib/tui/keys.js +205 -0
- package/lib/tui/markdown.js +368 -0
- package/lib/tui/mcp.js +95 -0
- package/lib/tui/panels.js +231 -0
- package/lib/tui/screen.js +156 -0
- package/lib/tui/state.js +502 -0
- package/lib/tui/stream.js +109 -0
- package/lib/tui/text.js +173 -0
- package/lib/tui/theme.js +183 -0
- package/lib/tui/themes.js +153 -0
- package/lib/tui/tooldetail.js +140 -0
- package/lib/tui/view.js +830 -0
- package/lib/tui/vim.js +222 -0
- package/lib/tui-host-core.js +141 -0
- package/lib/tui-host.js +48 -0
- package/lib/types/cross-find.d.ts +66 -0
- package/lib/types/file-index.d.ts +34 -0
- package/lib/types/fleet-sources.d.ts +34 -0
- package/lib/types/index.d.ts +51 -0
- package/lib/types/persist.d.ts +116 -0
- package/lib/types/plugins.d.ts +218 -0
- package/lib/types/presence.d.ts +48 -0
- package/lib/types/rename.d.ts +32 -0
- package/lib/types/rewind.d.ts +75 -0
- package/lib/types/sessions-store.d.ts +46 -0
- package/lib/types/startup.d.ts +45 -0
- package/lib/types/tui/atfile.d.ts +90 -0
- package/lib/types/tui/export.d.ts +18 -0
- package/lib/types/tui/fleet.d.ts +209 -0
- package/lib/types/tui/i18n.d.ts +34 -0
- package/lib/types/tui/jobs.d.ts +28 -0
- package/lib/types/tui/keys.d.ts +52 -0
- package/lib/types/tui/markdown.d.ts +14 -0
- package/lib/types/tui/mcp.d.ts +34 -0
- package/lib/types/tui/panels.d.ts +125 -0
- package/lib/types/tui/screen.d.ts +79 -0
- package/lib/types/tui/state.d.ts +323 -0
- package/lib/types/tui/stream.d.ts +78 -0
- package/lib/types/tui/text.d.ts +28 -0
- package/lib/types/tui/theme.d.ts +87 -0
- package/lib/types/tui/themes.d.ts +70 -0
- package/lib/types/tui/tooldetail.d.ts +45 -0
- package/lib/types/tui/view.d.ts +163 -0
- package/lib/types/tui/vim.d.ts +64 -0
- package/lib/types/tui-host-core.d.ts +62 -0
- package/lib/types/tui-host.d.ts +42 -0
- package/lib/types/version.d.ts +8 -0
- package/lib/types/voice.d.ts +227 -0
- package/lib/version.js +32 -0
- package/lib/voice.js +405 -0
- package/package.json +119 -0
- package/scripts/harness-root.mjs +88 -0
- package/scripts/install-profile.mjs +133 -0
package/lib/tui/vim.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vim modal editing for the composer.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately a small subset with exact semantics: motions (`h l 0 ^ $ w b`),
|
|
5
|
+
* edits (`x X dd d$ d0 dw`), insert entries (`i I a A o O`), and `u` for undo.
|
|
6
|
+
* Anything else is ignored rather than guessed at, so an unexpected key can
|
|
7
|
+
* never mangle a draft.
|
|
8
|
+
*
|
|
9
|
+
* The composer is the only thing mutated; the app decides when to route keys
|
|
10
|
+
* here, which keeps this testable as plain state.
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
/** How many edits `u` can walk back. */
|
|
14
|
+
export const VIM_UNDO_LIMIT = 100;
|
|
15
|
+
/**
|
|
16
|
+
* Modal vim state over one composer.
|
|
17
|
+
*
|
|
18
|
+
* Insert mode is the default: enabling vim must not change what typing does.
|
|
19
|
+
* An operator key (`d`) waits for its motion, and any key that is not a valid
|
|
20
|
+
* continuation cancels it — vim's own rule, and the safe one for a draft.
|
|
21
|
+
*/
|
|
22
|
+
export class Vim {
|
|
23
|
+
enabled = false;
|
|
24
|
+
mode = 'insert';
|
|
25
|
+
operator;
|
|
26
|
+
undoStack = [];
|
|
27
|
+
/** Turn modal editing on or off; disabling returns to insert mode. */
|
|
28
|
+
setEnabled(enabled) {
|
|
29
|
+
this.enabled = enabled;
|
|
30
|
+
this.mode = 'insert';
|
|
31
|
+
this.operator = undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Whether the next key should be interpreted as a command. */
|
|
34
|
+
get normal() {
|
|
35
|
+
return this.enabled && this.mode === 'normal';
|
|
36
|
+
}
|
|
37
|
+
/** Remember the draft before an edit, so `u` can restore it. */
|
|
38
|
+
snapshot(composer) {
|
|
39
|
+
this.undoStack.push({ text: composer.value(), cursor: composer.position() });
|
|
40
|
+
if (this.undoStack.length > VIM_UNDO_LIMIT)
|
|
41
|
+
this.undoStack.shift();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Vim's `w`: the start of the next word.
|
|
45
|
+
*
|
|
46
|
+
* Readline's word motion (which ctrl+right uses) stops at the END of a word,
|
|
47
|
+
* so vim mode cannot borrow it without disagreeing with every vim user's
|
|
48
|
+
* muscle memory.
|
|
49
|
+
*/
|
|
50
|
+
wordForward(composer) {
|
|
51
|
+
const value = composer.value();
|
|
52
|
+
composer.wordRight();
|
|
53
|
+
while (composer.position() < value.length && /\s/.test(value[composer.position()] ?? '')) {
|
|
54
|
+
composer.right();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Undo the last vim edit. */
|
|
58
|
+
undo(composer) {
|
|
59
|
+
const previous = this.undoStack.pop();
|
|
60
|
+
if (previous === undefined)
|
|
61
|
+
return;
|
|
62
|
+
composer.adopt(previous.text, previous.cursor);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Handle one key.
|
|
66
|
+
*
|
|
67
|
+
* @param name - decoded key name (`h`, `esc`, `ctrl+j`, …).
|
|
68
|
+
* @param text - printable text the key carries, if any.
|
|
69
|
+
* @param composer - the buffer being edited.
|
|
70
|
+
*/
|
|
71
|
+
handle(name, text, composer) {
|
|
72
|
+
if (!this.enabled)
|
|
73
|
+
return 'pass';
|
|
74
|
+
if (this.mode === 'insert') {
|
|
75
|
+
// Escape is the one key insert mode claims; everything else types.
|
|
76
|
+
if (name === 'esc') {
|
|
77
|
+
this.mode = 'normal';
|
|
78
|
+
this.operator = undefined;
|
|
79
|
+
// Leaving insert parks the cursor where vim would: one column left.
|
|
80
|
+
composer.left();
|
|
81
|
+
return 'mode';
|
|
82
|
+
}
|
|
83
|
+
return 'pass';
|
|
84
|
+
}
|
|
85
|
+
// Normal mode: an operator waits for its motion.
|
|
86
|
+
if (this.operator !== undefined) {
|
|
87
|
+
const pending = this.operator;
|
|
88
|
+
this.operator = undefined;
|
|
89
|
+
switch (pending) {
|
|
90
|
+
case 'd':
|
|
91
|
+
return this.deleteMotion(name, composer);
|
|
92
|
+
default:
|
|
93
|
+
return 'handled';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
switch (name) {
|
|
97
|
+
case 'esc':
|
|
98
|
+
return 'handled';
|
|
99
|
+
case 'h':
|
|
100
|
+
case 'left':
|
|
101
|
+
composer.left();
|
|
102
|
+
return 'handled';
|
|
103
|
+
case 'l':
|
|
104
|
+
case 'right':
|
|
105
|
+
composer.right();
|
|
106
|
+
return 'handled';
|
|
107
|
+
case '0':
|
|
108
|
+
case 'home':
|
|
109
|
+
composer.home();
|
|
110
|
+
return 'handled';
|
|
111
|
+
case '^': {
|
|
112
|
+
composer.home();
|
|
113
|
+
// First non-blank of the line.
|
|
114
|
+
const value = composer.value();
|
|
115
|
+
let at = composer.position();
|
|
116
|
+
while (at < value.length && (value[at] === ' ' || value[at] === '\t')) {
|
|
117
|
+
composer.right();
|
|
118
|
+
at += 1;
|
|
119
|
+
}
|
|
120
|
+
return 'handled';
|
|
121
|
+
}
|
|
122
|
+
case '$':
|
|
123
|
+
case 'end':
|
|
124
|
+
composer.end();
|
|
125
|
+
return 'handled';
|
|
126
|
+
case 'w':
|
|
127
|
+
this.wordForward(composer);
|
|
128
|
+
return 'handled';
|
|
129
|
+
case 'b':
|
|
130
|
+
composer.wordLeft();
|
|
131
|
+
return 'handled';
|
|
132
|
+
case 'i':
|
|
133
|
+
this.mode = 'insert';
|
|
134
|
+
return 'mode';
|
|
135
|
+
case 'I': {
|
|
136
|
+
composer.home();
|
|
137
|
+
this.mode = 'insert';
|
|
138
|
+
return 'mode';
|
|
139
|
+
}
|
|
140
|
+
case 'a': {
|
|
141
|
+
composer.right();
|
|
142
|
+
this.mode = 'insert';
|
|
143
|
+
return 'mode';
|
|
144
|
+
}
|
|
145
|
+
case 'A': {
|
|
146
|
+
composer.end();
|
|
147
|
+
this.mode = 'insert';
|
|
148
|
+
return 'mode';
|
|
149
|
+
}
|
|
150
|
+
case 'o': {
|
|
151
|
+
this.snapshot(composer);
|
|
152
|
+
composer.end();
|
|
153
|
+
composer.insert('\n');
|
|
154
|
+
this.mode = 'insert';
|
|
155
|
+
return 'mode';
|
|
156
|
+
}
|
|
157
|
+
case 'O': {
|
|
158
|
+
this.snapshot(composer);
|
|
159
|
+
composer.home();
|
|
160
|
+
composer.insert('\n');
|
|
161
|
+
composer.left();
|
|
162
|
+
this.mode = 'insert';
|
|
163
|
+
return 'mode';
|
|
164
|
+
}
|
|
165
|
+
case 'x': {
|
|
166
|
+
this.snapshot(composer);
|
|
167
|
+
composer.deleteForward();
|
|
168
|
+
return 'handled';
|
|
169
|
+
}
|
|
170
|
+
case 'X': {
|
|
171
|
+
this.snapshot(composer);
|
|
172
|
+
composer.backspace();
|
|
173
|
+
return 'handled';
|
|
174
|
+
}
|
|
175
|
+
case 'u':
|
|
176
|
+
this.undo(composer);
|
|
177
|
+
return 'handled';
|
|
178
|
+
case 'd':
|
|
179
|
+
this.operator = 'd';
|
|
180
|
+
return 'handled';
|
|
181
|
+
default:
|
|
182
|
+
// Unbound keys are swallowed, as in vim: a stray `j` must not type.
|
|
183
|
+
return text === '' ? 'handled' : 'handled';
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** `d` plus a motion, in terms of absolute buffers offsets. */
|
|
187
|
+
deleteMotion(motion, composer) {
|
|
188
|
+
switch (motion) {
|
|
189
|
+
case 'd': {
|
|
190
|
+
this.snapshot(composer);
|
|
191
|
+
const start = composer.lineStartIndex();
|
|
192
|
+
const end = composer.lineEndIndex();
|
|
193
|
+
// A line delete takes its newline with it, as vim's `dd` does; the
|
|
194
|
+
// last line has none, so it simply shortens.
|
|
195
|
+
const trailing = composer.value()[end] === '\n' ? 1 : 0;
|
|
196
|
+
composer.deleteRange(start, end + trailing);
|
|
197
|
+
return 'handled';
|
|
198
|
+
}
|
|
199
|
+
case '$': {
|
|
200
|
+
this.snapshot(composer);
|
|
201
|
+
composer.deleteRange(composer.position(), composer.lineEndIndex());
|
|
202
|
+
return 'handled';
|
|
203
|
+
}
|
|
204
|
+
case '0':
|
|
205
|
+
case '^': {
|
|
206
|
+
this.snapshot(composer);
|
|
207
|
+
composer.deleteRange(composer.lineStartIndex(), composer.position());
|
|
208
|
+
return 'handled';
|
|
209
|
+
}
|
|
210
|
+
case 'w': {
|
|
211
|
+
this.snapshot(composer);
|
|
212
|
+
const before = composer.position();
|
|
213
|
+
this.wordForward(composer);
|
|
214
|
+
composer.deleteRange(before, composer.position());
|
|
215
|
+
return 'handled';
|
|
216
|
+
}
|
|
217
|
+
default:
|
|
218
|
+
// `d` followed by something unbound is cancelled, not guessed at.
|
|
219
|
+
return 'handled';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `tuiHost` seam's framework-free half: what a shortcut claim and the
|
|
3
|
+
* status slot mean, with no Cordis import.
|
|
4
|
+
*
|
|
5
|
+
* It lives apart from `./tui-host.ts` so `npm test` can exercise it without a
|
|
6
|
+
* Harness on disk — the suites run from a bare `npm ci`, where `@deepseek-ai/*`
|
|
7
|
+
* does not resolve at all.
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
/** The name the service registers under (`ctx.tuiHost`). */
|
|
11
|
+
export const TUI_HOST_NAME = 'tuiHost';
|
|
12
|
+
/**
|
|
13
|
+
* Combinations the app itself owns.
|
|
14
|
+
*
|
|
15
|
+
* Built-ins win outright: a plugin that tries to take `ctrl+c` would be able to
|
|
16
|
+
* swallow the quit confirmation, so the registration is refused rather than
|
|
17
|
+
* silently ordered.
|
|
18
|
+
*/
|
|
19
|
+
export const RESERVED_COMBOS = new Set([
|
|
20
|
+
'ctrl+c',
|
|
21
|
+
'ctrl+d',
|
|
22
|
+
'ctrl+j',
|
|
23
|
+
'ctrl+n',
|
|
24
|
+
'ctrl+p',
|
|
25
|
+
'ctrl+r',
|
|
26
|
+
'ctrl+t',
|
|
27
|
+
'ctrl+u',
|
|
28
|
+
'ctrl+v',
|
|
29
|
+
'ctrl+w',
|
|
30
|
+
'ctrl+x',
|
|
31
|
+
'ctrl+y',
|
|
32
|
+
'ctrl+f',
|
|
33
|
+
'ctrl+g',
|
|
34
|
+
'ctrl+b',
|
|
35
|
+
'ctrl+o',
|
|
36
|
+
'ctrl+k',
|
|
37
|
+
'ctrl+left',
|
|
38
|
+
'ctrl+right',
|
|
39
|
+
'ctrl+up',
|
|
40
|
+
'ctrl+down',
|
|
41
|
+
'ctrl+enter',
|
|
42
|
+
'esc',
|
|
43
|
+
'enter',
|
|
44
|
+
'tab',
|
|
45
|
+
'up',
|
|
46
|
+
'down',
|
|
47
|
+
'left',
|
|
48
|
+
'right',
|
|
49
|
+
'pageup',
|
|
50
|
+
'pagedown',
|
|
51
|
+
'home',
|
|
52
|
+
'end',
|
|
53
|
+
'backspace',
|
|
54
|
+
'delete',
|
|
55
|
+
'shift+up',
|
|
56
|
+
'shift+down',
|
|
57
|
+
'alt+n',
|
|
58
|
+
'alt+p',
|
|
59
|
+
'alt+e',
|
|
60
|
+
'alt+c',
|
|
61
|
+
'alt+up',
|
|
62
|
+
'alt+down',
|
|
63
|
+
'wheelup',
|
|
64
|
+
'wheeldown',
|
|
65
|
+
]);
|
|
66
|
+
/** Why a registration was refused, or `undefined` when it was accepted. */
|
|
67
|
+
export function shortcutProblem(combo, taken) {
|
|
68
|
+
if (combo.trim() === '')
|
|
69
|
+
return 'the combination is empty';
|
|
70
|
+
if (!combo.startsWith('ctrl+') && !combo.startsWith('alt+')) {
|
|
71
|
+
return 'plugin shortcuts must carry ctrl or alt';
|
|
72
|
+
}
|
|
73
|
+
if (RESERVED_COMBOS.has(combo))
|
|
74
|
+
return `${combo} is a built-in binding`;
|
|
75
|
+
if (taken.has(combo))
|
|
76
|
+
return `${combo} is already registered`;
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
/** Shortcut claims, without any Cordis dependency — the testable half. */
|
|
80
|
+
export class ShortcutRegistry {
|
|
81
|
+
shortcuts = new Map();
|
|
82
|
+
/**
|
|
83
|
+
* Claim a key combination.
|
|
84
|
+
*
|
|
85
|
+
* @returns a disposer that releases it, or `undefined` when the combination
|
|
86
|
+
* is reserved, malformed, or already registered.
|
|
87
|
+
*/
|
|
88
|
+
register(shortcut) {
|
|
89
|
+
const problem = shortcutProblem(shortcut.combo, new Set(this.shortcuts.keys()));
|
|
90
|
+
if (problem !== undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
this.shortcuts.set(shortcut.combo, shortcut);
|
|
93
|
+
return () => {
|
|
94
|
+
// Only release it if this exact registration still owns the combo.
|
|
95
|
+
if (this.shortcuts.get(shortcut.combo) === shortcut)
|
|
96
|
+
this.shortcuts.delete(shortcut.combo);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Every registered combination, in registration order. */
|
|
100
|
+
registered() {
|
|
101
|
+
return [...this.shortcuts.values()];
|
|
102
|
+
}
|
|
103
|
+
/** The label for one combination, when it is claimed. */
|
|
104
|
+
labelOf(combo) {
|
|
105
|
+
return this.shortcuts.get(combo)?.label;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Run the handler for one key, if a plugin owns it.
|
|
109
|
+
*
|
|
110
|
+
* @returns whether the key was claimed, so the app can stop before it treats
|
|
111
|
+
* the key as text.
|
|
112
|
+
*/
|
|
113
|
+
dispatch(combo) {
|
|
114
|
+
const shortcut = this.shortcuts.get(combo);
|
|
115
|
+
if (shortcut === undefined)
|
|
116
|
+
return false;
|
|
117
|
+
shortcut.handler();
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** The one-line status slot, also independent of Cordis. */
|
|
122
|
+
export class StatusLine {
|
|
123
|
+
line;
|
|
124
|
+
/**
|
|
125
|
+
* Contribute the line. The slot is replaced, not stacked: a terminal has one
|
|
126
|
+
* line to give, and last registration wins — the same rule a status bar has.
|
|
127
|
+
*/
|
|
128
|
+
set(text) {
|
|
129
|
+
const previous = this.line;
|
|
130
|
+
const applied = text === '' ? undefined : text;
|
|
131
|
+
this.line = applied;
|
|
132
|
+
return () => {
|
|
133
|
+
// Only restore if nothing newer has taken the line since.
|
|
134
|
+
if (this.line === applied)
|
|
135
|
+
this.line = previous;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
get() {
|
|
139
|
+
return this.line;
|
|
140
|
+
}
|
|
141
|
+
}
|
package/lib/tui-host.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `tuiHost` service: the seam other plugins extend this terminal with.
|
|
3
|
+
*
|
|
4
|
+
* A plugin can own a key combination (Ctrl/Alt only, built-ins always win) and
|
|
5
|
+
* can contribute one line above the composer. Everything is disposer-scoped,
|
|
6
|
+
* so an unloaded plugin leaves nothing behind — and nothing here can take the
|
|
7
|
+
* keyboard away from the app's own bindings.
|
|
8
|
+
*
|
|
9
|
+
* This module is the Cordis-bound half; the rules it delegates to live in
|
|
10
|
+
* `./tui-host-core.ts` and are re-exported here, so `moqi-tui/tui-host`
|
|
11
|
+
* remains the one import a plugin needs.
|
|
12
|
+
* @module moqi-tui/tui-host
|
|
13
|
+
*/
|
|
14
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
15
|
+
import { ShortcutRegistry, StatusLine, TUI_HOST_NAME } from "./tui-host-core.js";
|
|
16
|
+
export * from "./tui-host-core.js";
|
|
17
|
+
/**
|
|
18
|
+
* The extension seam (`ctx.tuiHost`). Plugins register shortcuts and a status
|
|
19
|
+
* line; the app dispatches keys and draws the line, and owns nothing else.
|
|
20
|
+
*/
|
|
21
|
+
export class TuiHost extends Service {
|
|
22
|
+
shortcuts = new ShortcutRegistry();
|
|
23
|
+
line = new StatusLine();
|
|
24
|
+
constructor(ctx) {
|
|
25
|
+
super(ctx, TUI_HOST_NAME);
|
|
26
|
+
}
|
|
27
|
+
/** Claim a key combination; see {@link ShortcutRegistry.register}. */
|
|
28
|
+
registerShortcut(shortcut) {
|
|
29
|
+
return this.shortcuts.register(shortcut);
|
|
30
|
+
}
|
|
31
|
+
/** Every registered combination, for help output and tests. */
|
|
32
|
+
registered() {
|
|
33
|
+
return this.shortcuts.registered();
|
|
34
|
+
}
|
|
35
|
+
/** Run a key's plugin handler, if one is registered. */
|
|
36
|
+
dispatch(combo) {
|
|
37
|
+
return this.shortcuts.dispatch(combo);
|
|
38
|
+
}
|
|
39
|
+
/** Contribute the one-line status above the composer. */
|
|
40
|
+
setStatusLine(text) {
|
|
41
|
+
return this.line.set(text);
|
|
42
|
+
}
|
|
43
|
+
/** The status line a plugin contributed, if any. */
|
|
44
|
+
statusLine() {
|
|
45
|
+
return this.line.get();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export default TuiHost;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-session search: `/find --sessions`.
|
|
3
|
+
*
|
|
4
|
+
* Stored sessions live under `$DSH_HOME/sessions/<projectKey>/<sessionId>/` as
|
|
5
|
+
* either a plain `.jsonl` log or a zstd-compressed one. This module walks them,
|
|
6
|
+
* reads whichever form it finds, and returns the matching lines with enough
|
|
7
|
+
* context to recognize the conversation — no index, no cache, and nothing
|
|
8
|
+
* written, so it can never corrupt a log another process is appending to.
|
|
9
|
+
*
|
|
10
|
+
* The zstd decoder is feature-detected: on a Node without it, compressed logs
|
|
11
|
+
* are skipped and reported as such rather than silently ignored.
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
/** One matching line in one stored session. */
|
|
15
|
+
export interface SessionHit {
|
|
16
|
+
sessionId: string;
|
|
17
|
+
/** The project directory key, for orientation. */
|
|
18
|
+
project: string;
|
|
19
|
+
/** The matched line, trimmed and truncated for a picker row. */
|
|
20
|
+
line: string;
|
|
21
|
+
/** Whether the line came from the human or the model, when determinable. */
|
|
22
|
+
role?: 'user' | 'assistant';
|
|
23
|
+
/** Absolute path of the log the match came from. */
|
|
24
|
+
path: string;
|
|
25
|
+
}
|
|
26
|
+
/** Limits that keep a search from walking an unbounded store. */
|
|
27
|
+
export interface SearchLimits {
|
|
28
|
+
/** Sessions scanned, newest-first by directory mtime. */
|
|
29
|
+
maxSessions: number;
|
|
30
|
+
/** Hits returned overall. */
|
|
31
|
+
maxHits: number;
|
|
32
|
+
/** Bytes of an uncompressed log read per session. */
|
|
33
|
+
maxBytes: number;
|
|
34
|
+
}
|
|
35
|
+
export declare const DEFAULT_LIMITS: SearchLimits;
|
|
36
|
+
/** Whether this build of Node can decompress a zstd log. */
|
|
37
|
+
export declare function zstdAvailable(): boolean;
|
|
38
|
+
/** One readable message from a stored session log. */
|
|
39
|
+
export interface LogMessage {
|
|
40
|
+
role: 'user' | 'assistant';
|
|
41
|
+
text: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse a whole JSONL session body into its visible messages.
|
|
45
|
+
*
|
|
46
|
+
* Shared by cross-session search and the fleet preview, so both read a log the
|
|
47
|
+
* same way — including a foreign device's log, which may be an older format
|
|
48
|
+
* whose unknown records are simply skipped.
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseLogMessages(body: string): LogMessage[];
|
|
51
|
+
/** Whether a buffer carries the zstd frame magic. */
|
|
52
|
+
export declare function isZstdFrame(bytes: Uint8Array): boolean;
|
|
53
|
+
/** Decode raw log bytes, whichever form they arrived in. */
|
|
54
|
+
export declare function decodeLogBytes(bytes: Uint8Array): string | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Search every stored session for a case-insensitive substring.
|
|
57
|
+
*
|
|
58
|
+
* @returns matching lines in session order (newest session first), capped by
|
|
59
|
+
* the limits. Skipped compressed logs on a Node without zstd mean fewer
|
|
60
|
+
* results, never wrong ones.
|
|
61
|
+
*/
|
|
62
|
+
export declare function searchSessions(root: string, query: string, limits?: SearchLimits): {
|
|
63
|
+
hits: SessionHit[];
|
|
64
|
+
scanned: number;
|
|
65
|
+
skippedCompressed: number;
|
|
66
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bounded, cached walk of the workspace for `@` file completion.
|
|
3
|
+
*
|
|
4
|
+
* The listing is refreshable rather than watched: completion is an
|
|
5
|
+
* interactive nicety, so a few seconds of staleness after a checkout or a
|
|
6
|
+
* build costs nothing, while a filesystem watcher would cost a handle and a
|
|
7
|
+
* failure mode.
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
export interface DirectoryListing {
|
|
11
|
+
files: {
|
|
12
|
+
path: string;
|
|
13
|
+
directory: boolean;
|
|
14
|
+
}[];
|
|
15
|
+
}
|
|
16
|
+
/** One workspace's file list, recomputed lazily on demand. */
|
|
17
|
+
export declare class FileIndex {
|
|
18
|
+
private readonly root;
|
|
19
|
+
private entries;
|
|
20
|
+
private readAt;
|
|
21
|
+
private reading;
|
|
22
|
+
constructor(root: string);
|
|
23
|
+
/** Every workspace-relative file path, oldest acceptable snapshot or fresh. */
|
|
24
|
+
list(): string[];
|
|
25
|
+
/** Synchronously rebuild the listing. Errors leave the previous snapshot. */
|
|
26
|
+
refresh(): void;
|
|
27
|
+
private walk;
|
|
28
|
+
/**
|
|
29
|
+
* List one directory for a path-shaped query, relative to the workspace
|
|
30
|
+
* root. `.` and `..` are offered alongside the entries so navigation works
|
|
31
|
+
* the way a shell expects.
|
|
32
|
+
*/
|
|
33
|
+
listDir(relativePath: string): DirectoryListing;
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collecting presence records from this device and its peers.
|
|
3
|
+
*
|
|
4
|
+
* The transport is SSH, for one reason: it is the only channel in this setup
|
|
5
|
+
* that is already authenticated, already encrypted, and already working. The
|
|
6
|
+
* Harness's own web server offers no TLS, no authentication and no origin
|
|
7
|
+
* policy, and binding it off loopback would publish every route on the
|
|
8
|
+
* network — so the overview never does that.
|
|
9
|
+
*
|
|
10
|
+
* Every remote read is one short, non-interactive command. Nothing is
|
|
11
|
+
* installed on the peer beyond dsh itself, and nothing listens anywhere.
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { type FleetSource } from './tui/fleet.ts';
|
|
15
|
+
/** A peer device the overview should include. */
|
|
16
|
+
export interface PeerConfig {
|
|
17
|
+
/** Anything `ssh` accepts: a host, an alias, or user@host. */
|
|
18
|
+
host: string;
|
|
19
|
+
/** `$DSH_HOME` on that device; defaults to `~/.dsh` there. */
|
|
20
|
+
dshHome?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Resolve this device's Harness home the way the Harness itself does. */
|
|
23
|
+
export declare function localDshHome(env?: NodeJS.ProcessEnv): string;
|
|
24
|
+
/** Read this device's records. */
|
|
25
|
+
export declare function collectLocal(dshHome?: string): FleetSource;
|
|
26
|
+
/**
|
|
27
|
+
* Read one peer's records over SSH.
|
|
28
|
+
*
|
|
29
|
+
* `BatchMode=yes` matters: a peer whose key is missing must fail fast with an
|
|
30
|
+
* error in the overview rather than blocking the UI on a password prompt.
|
|
31
|
+
*/
|
|
32
|
+
export declare function collectPeer(peer: PeerConfig): Promise<FleetSource>;
|
|
33
|
+
/** Read every device in parallel; one slow peer must not hold up the rest. */
|
|
34
|
+
export declare function collectFleet(peers: readonly PeerConfig[], dshHome?: string): Promise<FleetSource[]>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* moqi — an interactive terminal app for DeepSeek Harness.
|
|
3
|
+
*
|
|
4
|
+
* The bundle patch rides over `dsh-base` without a Host, HTTP server, or
|
|
5
|
+
* browser plugin: the terminal is the only surface. This module owns the
|
|
6
|
+
* Harness wiring — creating or resuming an Agent, projecting its assistant
|
|
7
|
+
* stream into the transcript, and dispatching slash commands through
|
|
8
|
+
* `ctx.commands` — while `./tui/*` owns everything drawn on screen.
|
|
9
|
+
*
|
|
10
|
+
* @module moqi
|
|
11
|
+
*/
|
|
12
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
13
|
+
import z from '@deepseek-ai/schemastery';
|
|
14
|
+
/** Stable Cordis plugin name. */
|
|
15
|
+
export declare const name = "moqi";
|
|
16
|
+
/** Core services required before the terminal can open. */
|
|
17
|
+
export declare const inject: string[];
|
|
18
|
+
/** Plugin config, resolved from this app's startup provider. */
|
|
19
|
+
export interface Config {
|
|
20
|
+
resumeSessionId?: string;
|
|
21
|
+
model?: string;
|
|
22
|
+
thinking?: boolean;
|
|
23
|
+
contextLimit?: number;
|
|
24
|
+
/** Report mouse events so the wheel scrolls; off by default. */
|
|
25
|
+
mouse?: boolean;
|
|
26
|
+
/** Ring the terminal bell when a session's turn finishes; on by default. */
|
|
27
|
+
bell?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Devices to include in the fleet overview, as anything `ssh` accepts.
|
|
30
|
+
* Empty means the overview shows only this machine.
|
|
31
|
+
*/
|
|
32
|
+
peers?: string[];
|
|
33
|
+
/** Reopen the sessions that were open at the last exit; on by default. */
|
|
34
|
+
restore?: boolean;
|
|
35
|
+
/** Whisper weights for push-to-talk; absent falls back to the default search. */
|
|
36
|
+
voiceModel?: string;
|
|
37
|
+
/** Whisper executable for push-to-talk; absent looks for the known names. */
|
|
38
|
+
voiceBin?: string;
|
|
39
|
+
/**
|
|
40
|
+
* The profile `/dispatch` boots on a peer. The shipped `headless` profile is
|
|
41
|
+
* the one that answers a single task and exits.
|
|
42
|
+
*/
|
|
43
|
+
dispatchProfile?: string;
|
|
44
|
+
}
|
|
45
|
+
export declare const Config: z<Config>;
|
|
46
|
+
/**
|
|
47
|
+
* Mount the terminal app.
|
|
48
|
+
* @param ctx - plugin context carrying the core services and launcher exit.
|
|
49
|
+
* @param config - validated startup options.
|
|
50
|
+
*/
|
|
51
|
+
export declare function apply(ctx: Context, config: Config): void;
|