cvc-tui 0.1.0 → 0.4.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.
Files changed (130) hide show
  1. package/dist/app/completion.js +4 -0
  2. package/dist/app/createGatewayEventHandler.js +508 -0
  3. package/dist/app/createSlashHandler.js +101 -0
  4. package/dist/app/delegationStore.js +51 -0
  5. package/dist/app/gatewayContext.js +17 -0
  6. package/dist/app/historyStore.js +4 -0
  7. package/dist/app/inputBuffer.js +4 -0
  8. package/dist/app/inputSelectionStore.js +8 -0
  9. package/dist/app/inputStore.js +4 -0
  10. package/dist/app/interfaces.js +6 -0
  11. package/dist/app/overlayStore.js +40 -0
  12. package/dist/app/promptStore.js +4 -0
  13. package/dist/app/queueStore.js +4 -0
  14. package/dist/app/scroll.js +44 -0
  15. package/dist/app/setupHandoff.js +28 -0
  16. package/dist/app/slash/commands/core.js +421 -234
  17. package/dist/app/slash/commands/debug.js +39 -6
  18. package/dist/app/slash/commands/ops.js +471 -136
  19. package/dist/app/slash/commands/session.js +405 -65
  20. package/dist/app/slash/commands/setup.js +16 -43
  21. package/dist/app/slash/commands/toggles.js +4 -0
  22. package/dist/app/slash/registry.js +7 -68
  23. package/dist/app/slash/types.js +1 -16
  24. package/dist/app/spawnHistoryStore.js +105 -0
  25. package/dist/app/turnController.js +650 -0
  26. package/dist/app/turnStore.js +47 -59
  27. package/dist/app/uiStore.js +34 -29
  28. package/dist/app/useComposerState.js +265 -0
  29. package/dist/app/useConfigSync.js +144 -0
  30. package/dist/app/useInputHandlers.js +403 -0
  31. package/dist/app/useLongRunToolCharms.js +50 -0
  32. package/dist/app/useMainApp.js +629 -0
  33. package/dist/app/useSessionLifecycle.js +175 -0
  34. package/dist/app/useSubmission.js +287 -0
  35. package/dist/app.js +13 -217
  36. package/dist/banner.js +40 -3
  37. package/dist/components/agentsOverlay.js +474 -0
  38. package/dist/components/appChrome.js +252 -0
  39. package/dist/components/appLayout.js +121 -22
  40. package/dist/components/appOverlays.js +65 -0
  41. package/dist/components/branding.js +97 -6
  42. package/dist/components/fpsOverlay.js +22 -0
  43. package/dist/components/helpHint.js +21 -0
  44. package/dist/components/markdown.js +501 -0
  45. package/dist/components/maskedPrompt.js +12 -0
  46. package/dist/components/messageLine.js +82 -0
  47. package/dist/components/modelPicker.js +254 -0
  48. package/dist/components/overlayControls.js +30 -0
  49. package/dist/components/overlays/helpOverlay.js +1 -0
  50. package/dist/components/overlays/historySearch.js +1 -0
  51. package/dist/components/overlays/modelPicker.js +2 -1
  52. package/dist/components/overlays/overlayUtils.js +2 -1
  53. package/dist/components/overlays/secretPrompt.js +1 -0
  54. package/dist/components/overlays/sessionPicker.js +1 -0
  55. package/dist/components/overlays/skillsHub.js +1 -0
  56. package/dist/components/prompts.js +95 -0
  57. package/dist/components/queuedMessages.js +24 -0
  58. package/dist/components/sessionPicker.js +130 -0
  59. package/dist/components/skillsHub.js +165 -0
  60. package/dist/components/streamingAssistant.js +35 -0
  61. package/dist/components/streamingMarkdown.js +110 -186
  62. package/dist/components/textInput.js +748 -218
  63. package/dist/components/themed.js +12 -0
  64. package/dist/components/thinking.js +493 -36
  65. package/dist/components/todoPanel.js +40 -0
  66. package/dist/config/env.js +18 -0
  67. package/dist/config/limits.js +22 -0
  68. package/dist/config/timing.js +4 -0
  69. package/dist/content/charms.js +5 -0
  70. package/dist/content/faces.js +21 -0
  71. package/dist/content/fortunes.js +29 -0
  72. package/dist/content/hotkeys.js +38 -0
  73. package/dist/content/placeholders.js +15 -0
  74. package/dist/content/setup.js +14 -0
  75. package/dist/content/verbs.js +41 -0
  76. package/dist/domain/details.js +53 -0
  77. package/dist/domain/messages.js +63 -0
  78. package/dist/domain/paths.js +16 -0
  79. package/dist/domain/providers.js +11 -0
  80. package/dist/domain/roles.js +6 -0
  81. package/dist/domain/slash.js +11 -0
  82. package/dist/domain/usage.js +1 -0
  83. package/dist/domain/viewport.js +33 -0
  84. package/dist/entry.js +65 -40
  85. package/dist/gatewayClient.js +574 -0
  86. package/dist/gatewayTypes.js +1 -0
  87. package/dist/hooks/useCompletion.js +86 -0
  88. package/dist/hooks/useGitBranch.js +58 -0
  89. package/dist/hooks/useInputHistory.js +12 -0
  90. package/dist/hooks/useQueue.js +57 -0
  91. package/dist/hooks/useVirtualHistory.js +401 -0
  92. package/dist/lib/circularBuffer.js +43 -0
  93. package/dist/lib/clipboard.js +126 -0
  94. package/dist/lib/editor.js +41 -0
  95. package/dist/lib/editor.test.js +58 -0
  96. package/dist/lib/emoji.js +49 -0
  97. package/dist/lib/externalCli.js +11 -0
  98. package/dist/lib/forceTruecolor.js +26 -0
  99. package/dist/lib/fpsStore.js +36 -0
  100. package/dist/lib/gracefulExit.js +29 -0
  101. package/dist/lib/history.js +69 -0
  102. package/dist/lib/inputMetrics.js +143 -0
  103. package/dist/lib/liveProgress.js +51 -0
  104. package/dist/lib/liveProgress.test.js +89 -0
  105. package/dist/lib/mathUnicode.js +685 -0
  106. package/dist/lib/memory.js +123 -0
  107. package/dist/lib/memoryMonitor.js +76 -0
  108. package/dist/lib/messages.js +3 -0
  109. package/dist/lib/messages.test.js +25 -0
  110. package/dist/lib/osc52.js +53 -0
  111. package/dist/lib/perfPane.js +94 -0
  112. package/dist/lib/platform.js +312 -0
  113. package/dist/lib/precisionWheel.js +25 -0
  114. package/dist/lib/reasoning.js +39 -0
  115. package/dist/lib/rpc.js +26 -0
  116. package/dist/lib/subagentTree.js +287 -0
  117. package/dist/lib/syntax.js +89 -0
  118. package/dist/lib/terminalModes.js +46 -0
  119. package/dist/lib/terminalParity.js +48 -0
  120. package/dist/lib/terminalSetup.js +321 -0
  121. package/dist/lib/text.js +203 -0
  122. package/dist/lib/text.test.js +18 -0
  123. package/dist/lib/todo.js +2 -0
  124. package/dist/lib/todo.test.js +22 -0
  125. package/dist/lib/viewportStore.js +82 -0
  126. package/dist/lib/virtualHeights.js +61 -0
  127. package/dist/lib/wheelAccel.js +143 -0
  128. package/dist/theme.js +398 -0
  129. package/dist/types.js +1 -7
  130. package/package.json +2 -1
@@ -1,163 +1,498 @@
1
+ import { applyDelegationStatus, getDelegationState } from '../../delegationStore.js';
2
+ import { patchOverlayState } from '../../overlayStore.js';
3
+ import { getSpawnHistory, pushDiskSnapshot, setDiffPair } from '../../spawnHistoryStore.js';
1
4
  export const opsCommands = [
2
5
  {
3
- name: 'model',
4
- category: 'ops',
5
- help: 'switch model (arrow-key picker if blank)',
6
- run: (arg, ctx) => {
7
- const name = arg.trim();
8
- if (!name)
9
- return ctx.emit({ kind: 'overlay', overlay: 'modelPicker' });
10
- ctx.emit({ kind: 'rpc', method: 'config.set', params: { key: 'model', value: name } });
11
- ctx.emit({ kind: 'sys', text: `model: ${name}` });
12
- },
6
+ help: 'stop background processes',
7
+ name: 'stop',
8
+ run: (_arg, ctx) => {
9
+ ctx.gateway
10
+ .rpc('process.stop', {})
11
+ .then(ctx.guarded(r => {
12
+ const killed = Number(r.killed ?? 0);
13
+ const noun = killed === 1 ? 'process' : 'processes';
14
+ ctx.transcript.sys(`stopped ${killed} background ${noun}`);
15
+ }))
16
+ .catch(ctx.guardedErr);
17
+ }
13
18
  },
14
19
  {
15
- name: 'provider',
16
- category: 'ops',
17
- help: 'switch provider',
20
+ aliases: ['reload_mcp'],
21
+ help: 'reload MCP servers in the live session (warns about prompt cache invalidation)',
22
+ name: 'reload-mcp',
18
23
  run: (arg, ctx) => {
19
- const name = arg.trim();
20
- if (!name)
21
- return ctx.emit({ kind: 'rpc', method: 'config.get', params: { key: 'provider' } });
22
- ctx.emit({ kind: 'rpc', method: 'config.set', params: { key: 'provider', value: name } });
23
- ctx.emit({ kind: 'sys', text: `provider: ${name}` });
24
- },
25
- },
26
- {
27
- name: 'skills',
28
- category: 'ops',
29
- help: 'browse the skills hub',
30
- run: (_a, ctx) => ctx.emit({ kind: 'overlay', overlay: 'skillsHub' }),
31
- },
32
- {
33
- name: 'reload-skills',
34
- category: 'ops',
35
- help: 'rescan skill directory',
36
- run: (_a, ctx) => {
37
- ctx.emit({ kind: 'rpc', method: 'skills.reload' });
38
- ctx.emit({ kind: 'sys', text: 'skills reloaded' });
39
- },
24
+ // Parse arg: `now` / `always` skip the confirmation gate.
25
+ // `always` additionally persists approvals.mcp_reload_confirm=false.
26
+ const a = (arg || '').trim().toLowerCase();
27
+ const params = {
28
+ session_id: ctx.sid
29
+ };
30
+ if (a === 'now' || a === 'approve' || a === 'once' || a === 'yes') {
31
+ params.confirm = true;
32
+ }
33
+ else if (a === 'always') {
34
+ params.confirm = true;
35
+ params.always = true;
36
+ }
37
+ ctx.gateway
38
+ .rpc('reload.mcp', params)
39
+ .then(ctx.guarded(r => {
40
+ if (r.status === 'confirm_required') {
41
+ ctx.transcript.sys(r.message || '/reload-mcp requires confirmation');
42
+ return;
43
+ }
44
+ if (r.status === 'reloaded') {
45
+ ctx.transcript.sys(params.always
46
+ ? 'MCP servers reloaded · future /reload-mcp will run without confirmation'
47
+ : 'MCP servers reloaded');
48
+ return;
49
+ }
50
+ ctx.transcript.sys('reload complete');
51
+ }))
52
+ .catch(ctx.guardedErr);
53
+ }
40
54
  },
41
55
  {
42
- name: 'agents',
43
- category: 'ops',
44
- help: 'list registered agents',
45
- run: (_a, ctx) => ctx.emit({ kind: 'overlay', overlay: 'agentsList' }),
56
+ help: 're-read ~/.hermes/.env into the running gateway (CLI parity)',
57
+ name: 'reload',
58
+ run: (_arg, ctx) => {
59
+ ctx.gateway
60
+ .rpc('reload.env', {})
61
+ .then(ctx.guarded(r => {
62
+ const n = Number(r.updated ?? 0);
63
+ const noun = n === 1 ? 'var' : 'vars';
64
+ ctx.transcript.sys(`reloaded .env (${n} ${noun} updated)`);
65
+ }))
66
+ .catch(ctx.guardedErr);
67
+ }
46
68
  },
47
69
  {
48
- name: 'memory',
49
- aliases: ['mem'],
50
- category: 'ops',
51
- help: 'open the memory editor',
70
+ help: 'manage browser CDP connection [connect|disconnect|status]',
71
+ name: 'browser',
52
72
  run: (arg, ctx) => {
53
- const m = arg.trim().toLowerCase();
54
- if (m === 'clear') {
55
- ctx.emit({ kind: 'rpc', method: 'memory.clear' });
56
- return ctx.emit({ kind: 'sys', text: 'memory cleared' });
57
- }
58
- if (m === 'show')
59
- return ctx.emit({ kind: 'rpc', method: 'memory.show' });
60
- ctx.emit({ kind: 'overlay', overlay: 'memoryEditor' });
61
- },
62
- },
63
- {
64
- name: 'tools',
65
- category: 'ops',
66
- help: 'list available tools',
67
- run: (_a, ctx) => ctx.emit({ kind: 'rpc', method: 'tools.list' }),
68
- },
69
- {
70
- name: 'reload',
71
- category: 'ops',
72
- help: 'reload CVC config',
73
- run: (_a, ctx) => {
74
- ctx.emit({ kind: 'rpc', method: 'config.reload' });
75
- ctx.emit({ kind: 'sys', text: 'config reloaded' });
76
- },
73
+ const [rawAction = 'status', ...rest] = arg.trim().split(/\s+/).filter(Boolean);
74
+ const action = rawAction.toLowerCase();
75
+ if (!['connect', 'disconnect', 'status'].includes(action)) {
76
+ return ctx.transcript.sys('usage: /browser [connect|disconnect|status] [url] · persistent: set browser.cdp_url in config.yaml');
77
+ }
78
+ const sid = ctx.sid ?? null;
79
+ const url = action === 'connect' ? rest.join(' ').trim() || 'http://127.0.0.1:9222' : undefined;
80
+ if (url) {
81
+ ctx.transcript.sys(`checking Chrome remote debugging at ${url}...`);
82
+ }
83
+ ctx.gateway
84
+ .rpc('browser.manage', { action, session_id: sid, ...(url && { url }) })
85
+ .then(ctx.guarded(r => {
86
+ // Without a session we can't subscribe to streamed
87
+ // browser.progress events, so flush the bundled list.
88
+ if (!sid) {
89
+ r.messages?.forEach(message => ctx.transcript.sys(message));
90
+ }
91
+ if (action === 'status') {
92
+ return ctx.transcript.sys(r.connected
93
+ ? `browser connected: ${r.url || '(url unavailable)'}`
94
+ : 'browser not connected (try /browser connect <url> or set browser.cdp_url in config.yaml)');
95
+ }
96
+ if (action === 'disconnect') {
97
+ return ctx.transcript.sys('browser disconnected');
98
+ }
99
+ if (r.connected) {
100
+ ctx.transcript.sys('Browser connected to live Chrome via CDP');
101
+ ctx.transcript.sys(`Endpoint: ${r.url || '(url unavailable)'}`);
102
+ ctx.transcript.sys('next browser tool call will use this CDP endpoint');
103
+ }
104
+ }))
105
+ .catch(ctx.guardedErr);
106
+ }
77
107
  },
78
108
  {
79
- name: 'reload-mcp',
80
- category: 'ops',
81
- help: 'reload MCP servers',
82
- run: (_a, ctx) => {
83
- ctx.emit({ kind: 'rpc', method: 'mcp.reload' });
84
- ctx.emit({ kind: 'sys', text: 'mcp servers reloading' });
85
- },
109
+ help: 'list, diff, or restore checkpoints',
110
+ name: 'rollback',
111
+ run: (arg, ctx) => {
112
+ if (!ctx.sid) {
113
+ return ctx.transcript.sys('no active session nothing to rollback');
114
+ }
115
+ const trimmed = arg.trim();
116
+ const [first = '', ...rest] = trimmed.split(/\s+/).filter(Boolean);
117
+ const lower = first.toLowerCase();
118
+ if (!trimmed || lower === 'list' || lower === 'ls') {
119
+ return ctx.gateway
120
+ .rpc('rollback.list', { session_id: ctx.sid })
121
+ .then(ctx.guarded(r => {
122
+ if (!r.enabled) {
123
+ return ctx.transcript.sys('checkpoints are not enabled');
124
+ }
125
+ const checkpoints = r.checkpoints ?? [];
126
+ if (!checkpoints.length) {
127
+ return ctx.transcript.sys('no checkpoints found');
128
+ }
129
+ ctx.transcript.panel('Rollback checkpoints', [
130
+ {
131
+ rows: checkpoints.map((c, idx) => [
132
+ `${idx + 1}. ${c.hash.slice(0, 10)}`,
133
+ [c.timestamp, c.message].filter(Boolean).join(' · ') || '(no metadata)'
134
+ ])
135
+ }
136
+ ]);
137
+ }))
138
+ .catch(ctx.guardedErr);
139
+ }
140
+ if (lower === 'diff') {
141
+ const hash = rest[0];
142
+ if (!hash) {
143
+ return ctx.transcript.sys('usage: /rollback diff <checkpoint>');
144
+ }
145
+ return ctx.gateway
146
+ .rpc('rollback.diff', { hash, session_id: ctx.sid })
147
+ .then(ctx.guarded(r => {
148
+ const body = (r.rendered || r.diff || '').trim();
149
+ if (!body && !r.stat) {
150
+ return ctx.transcript.sys('no changes since this checkpoint');
151
+ }
152
+ const text = [r.stat || '', body].filter(Boolean).join('\n\n');
153
+ ctx.transcript.page(text, 'Rollback diff');
154
+ }))
155
+ .catch(ctx.guardedErr);
156
+ }
157
+ const hash = first;
158
+ const filePath = rest.join(' ').trim();
159
+ return ctx.gateway
160
+ .rpc('rollback.restore', {
161
+ ...(filePath ? { file_path: filePath } : {}),
162
+ hash,
163
+ session_id: ctx.sid
164
+ })
165
+ .then(ctx.guarded(r => {
166
+ if (!r.success) {
167
+ return ctx.transcript.sys(`rollback failed: ${r.error || r.message || 'unknown error'}`);
168
+ }
169
+ const target = filePath || 'workspace';
170
+ const detail = r.reason || r.message || r.restored_to || 'restored';
171
+ ctx.transcript.sys(`rollback restored ${target}: ${detail}`);
172
+ if ((r.history_removed ?? 0) > 0) {
173
+ ctx.transcript.setHistoryItems(prev => ctx.transcript.trimLastExchange(prev));
174
+ }
175
+ }))
176
+ .catch(ctx.guardedErr);
177
+ }
86
178
  },
87
179
  {
88
- name: 'personality',
89
- category: 'ops',
90
- help: 'switch personality',
180
+ aliases: ['tasks'],
181
+ help: 'open the spawn-tree dashboard (live audit + kill/pause controls)',
182
+ name: 'agents',
91
183
  run: (arg, ctx) => {
92
- const name = arg.trim();
93
- if (!name)
94
- return ctx.emit({ kind: 'rpc', method: 'config.get', params: { key: 'personality' } });
95
- ctx.emit({ kind: 'rpc', method: 'config.set', params: { key: 'personality', value: name } });
96
- ctx.emit({ kind: 'sys', text: `personality: ${name}` });
97
- },
184
+ const sub = arg.trim().toLowerCase();
185
+ // Stay compatible with the gateway `/agents [pause|resume|status]` CLI —
186
+ // explicit subcommands skip the overlay and act directly so scripts and
187
+ // multi-step flows can drive it without entering interactive mode.
188
+ if (sub === 'pause' || sub === 'resume' || sub === 'unpause') {
189
+ const paused = sub === 'pause';
190
+ ctx.gateway.gw
191
+ .request('delegation.pause', { paused })
192
+ .then(r => {
193
+ applyDelegationStatus({ paused: r?.paused });
194
+ ctx.transcript.sys(`delegation · ${r?.paused ? 'paused' : 'resumed'}`);
195
+ })
196
+ .catch(ctx.guardedErr);
197
+ return;
198
+ }
199
+ if (sub === 'status') {
200
+ const d = getDelegationState();
201
+ ctx.transcript.sys(`delegation · ${d.paused ? 'paused' : 'active'} · caps d${d.maxSpawnDepth ?? '?'}/${d.maxConcurrentChildren ?? '?'}`);
202
+ return;
203
+ }
204
+ patchOverlayState({ agents: true, agentsInitialHistoryIndex: 0 });
205
+ }
98
206
  },
99
207
  {
100
- name: 'skin',
101
- category: 'ops',
102
- help: 'switch CLI skin',
208
+ help: 'replay a completed spawn tree · `/replay [N|last|list|load <path>]`',
209
+ name: 'replay',
103
210
  run: (arg, ctx) => {
104
- const name = arg.trim();
105
- if (!name)
106
- return ctx.emit({ kind: 'rpc', method: 'config.get', params: { key: 'skin' } });
107
- ctx.emit({ kind: 'rpc', method: 'config.set', params: { key: 'skin', value: name } });
108
- ctx.emit({ kind: 'sys', text: `skin: ${name}` });
109
- },
211
+ const history = getSpawnHistory();
212
+ const raw = arg.trim();
213
+ const lower = raw.toLowerCase();
214
+ // ── Disk-backed listing ─────────────────────────────────────
215
+ if (lower === 'list' || lower === 'ls') {
216
+ ctx.gateway
217
+ .rpc('spawn_tree.list', {
218
+ limit: 30,
219
+ session_id: ctx.sid ?? 'default'
220
+ })
221
+ .then(ctx.guarded(r => {
222
+ const entries = r.entries ?? [];
223
+ if (!entries.length) {
224
+ return ctx.transcript.sys('no archived spawn trees on disk for this session');
225
+ }
226
+ const rows = entries.map(e => {
227
+ const ts = e.finished_at ? new Date(e.finished_at * 1000).toLocaleString() : '?';
228
+ const label = e.label || `${e.count} subagents`;
229
+ return [`${ts} · ${e.count}×`, `${label}\n ${e.path}`];
230
+ });
231
+ ctx.transcript.panel('Archived spawn trees', [{ rows }]);
232
+ }))
233
+ .catch(ctx.guardedErr);
234
+ return;
235
+ }
236
+ // ── Disk-backed load by path ─────────────────────────────────
237
+ if (lower.startsWith('load ')) {
238
+ const path = raw.slice(5).trim();
239
+ if (!path) {
240
+ return ctx.transcript.sys('usage: /replay load <path>');
241
+ }
242
+ ctx.gateway
243
+ .rpc('spawn_tree.load', { path })
244
+ .then(ctx.guarded(r => {
245
+ if (!r.subagents?.length) {
246
+ return ctx.transcript.sys('snapshot empty or unreadable');
247
+ }
248
+ // Push onto the in-memory history so the overlay picks it up
249
+ // by index 1 just like any other snapshot.
250
+ pushDiskSnapshot(r, path);
251
+ patchOverlayState({ agents: true, agentsInitialHistoryIndex: 1 });
252
+ }))
253
+ .catch(ctx.guardedErr);
254
+ return;
255
+ }
256
+ // ── In-memory nav (same-session) ─────────────────────────────
257
+ if (!history.length) {
258
+ return ctx.transcript.sys('no completed spawn trees this session · try /replay list');
259
+ }
260
+ let index = 1;
261
+ if (raw && lower !== 'last') {
262
+ const parsed = parseInt(raw, 10);
263
+ if (Number.isNaN(parsed) || parsed < 1 || parsed > history.length) {
264
+ return ctx.transcript.sys(`replay: index out of range 1..${history.length} · use /replay list for disk`);
265
+ }
266
+ index = parsed;
267
+ }
268
+ patchOverlayState({ agents: true, agentsInitialHistoryIndex: index });
269
+ }
110
270
  },
111
271
  {
112
- name: 'statusbar',
113
- aliases: ['sb'],
114
- category: 'ops',
115
- help: 'status bar position (on|off|top|bottom)',
272
+ help: 'diff two completed spawn trees · `/replay-diff <baseline> <candidate>` (indexes from /replay list or history N)',
273
+ name: 'replay-diff',
116
274
  run: (arg, ctx) => {
117
- const m = arg.trim().toLowerCase();
118
- const cur = ctx.ui.statusBar ?? 'top';
119
- const next = !m || m === 'toggle'
120
- ? (cur === 'off' ? 'top' : 'off')
121
- : m === 'on' || m === 'top'
122
- ? 'top'
123
- : (m === 'off' || m === 'bottom') ? m : null;
124
- if (!next)
125
- return ctx.emit({ kind: 'sys', text: 'usage: /statusbar [on|off|top|bottom|toggle]' });
126
- ctx.emit({ kind: 'patchUi', ui: { statusBar: next } });
127
- ctx.emit({ kind: 'rpc', method: 'config.set', params: { key: 'statusbar', value: next } });
128
- ctx.emit({ kind: 'sys', text: `statusbar: ${next}` });
129
- },
275
+ const parts = arg.trim().split(/\s+/).filter(Boolean);
276
+ if (parts.length !== 2) {
277
+ return ctx.transcript.sys('usage: /replay-diff <a> <b> (e.g. /replay-diff 1 2 for last two)');
278
+ }
279
+ const [a, b] = parts;
280
+ const history = getSpawnHistory();
281
+ const resolve = (token) => {
282
+ const n = parseInt(token, 10);
283
+ if (Number.isFinite(n) && n >= 1 && n <= history.length) {
284
+ return history[n - 1] ?? null;
285
+ }
286
+ return null;
287
+ };
288
+ const baseline = resolve(a);
289
+ const candidate = resolve(b);
290
+ if (!baseline || !candidate) {
291
+ return ctx.transcript.sys(`replay-diff: could not resolve indices · history has ${history.length} entries`);
292
+ }
293
+ setDiffPair({ baseline, candidate });
294
+ patchOverlayState({ agents: true, agentsInitialHistoryIndex: 0 });
295
+ }
130
296
  },
131
297
  {
132
- name: 'indicator',
133
- category: 'ops',
134
- help: 'busy-indicator style [dots|braille|kawaii|off]',
135
- run: (arg, ctx) => {
136
- const m = arg.trim().toLowerCase();
137
- const allowed = ['dots', 'braille', 'kawaii', 'off'];
138
- if (!m)
139
- return ctx.emit({ kind: 'sys', text: `indicator: ${ctx.ui.indicator ?? 'kawaii'}` });
140
- if (!allowed.includes(m))
141
- return ctx.emit({ kind: 'sys', text: `usage: /indicator [${allowed.join('|')}]` });
142
- ctx.emit({ kind: 'patchUi', ui: { indicator: m } });
143
- ctx.emit({ kind: 'rpc', method: 'config.set', params: { key: 'indicator', value: m } });
144
- ctx.emit({ kind: 'sys', text: `indicator: ${m}` });
145
- },
298
+ aliases: ['reload_skills'],
299
+ help: 're-scan installed skills in the live TUI gateway',
300
+ name: 'reload-skills',
301
+ run: (_arg, ctx) => {
302
+ ctx.gateway
303
+ .rpc('skills.reload', {})
304
+ .then(ctx.guarded(r => {
305
+ ctx.transcript.page(r.output || 'skills reloaded', 'Reload Skills');
306
+ ctx.gateway
307
+ .rpc('commands.catalog', {})
308
+ .then(ctx.guarded(catalog => {
309
+ if (!catalog?.pairs) {
310
+ return;
311
+ }
312
+ ctx.local.setCatalog({
313
+ canon: (catalog.canon ?? {}),
314
+ categories: catalog.categories ?? [],
315
+ pairs: catalog.pairs,
316
+ skillCount: (catalog.skill_count ?? 0),
317
+ sub: (catalog.sub ?? {})
318
+ });
319
+ }))
320
+ .catch(() => { });
321
+ }))
322
+ .catch(ctx.guardedErr);
323
+ }
146
324
  },
147
325
  {
148
- name: 'mouse',
149
- aliases: ['scroll'],
150
- category: 'ops',
151
- help: 'toggle mouse/wheel tracking',
152
- run: (arg, ctx) => {
153
- const cur = Boolean(ctx.ui.mouseTracking);
154
- const m = arg.trim().toLowerCase();
155
- const next = !m || m === 'toggle' ? !cur : m === 'on' ? true : m === 'off' ? false : null;
156
- if (next === null)
157
- return ctx.emit({ kind: 'sys', text: 'usage: /mouse [on|off|toggle]' });
158
- ctx.emit({ kind: 'patchUi', ui: { mouseTracking: next } });
159
- ctx.emit({ kind: 'rpc', method: 'config.set', params: { key: 'mouse', value: next ? 'on' : 'off' } });
160
- ctx.emit({ kind: 'sys', text: `mouse tracking ${next ? 'on' : 'off'}` });
161
- },
326
+ help: 'browse, inspect, install skills',
327
+ name: 'skills',
328
+ run: (arg, ctx, cmd) => {
329
+ const text = arg.trim();
330
+ if (!text) {
331
+ return patchOverlayState({ skillsHub: true });
332
+ }
333
+ const [sub, ...rest] = text.split(/\s+/);
334
+ const query = rest.join(' ').trim();
335
+ const { rpc } = ctx.gateway;
336
+ const { panel, sys } = ctx.transcript;
337
+ const runViaSlashWorker = () => {
338
+ ctx.gateway.gw
339
+ .request('slash.exec', { command: cmd.slice(1), session_id: ctx.sid })
340
+ .then(r => {
341
+ if (ctx.stale()) {
342
+ return;
343
+ }
344
+ const body = r?.output || '/skills: no output';
345
+ const formatted = r?.warning ? `warning: ${r.warning}\n${body}` : body;
346
+ const long = formatted.length > 180 || formatted.split('\n').filter(Boolean).length > 2;
347
+ long ? ctx.transcript.page(formatted, 'Skills') : ctx.transcript.sys(formatted);
348
+ })
349
+ .catch(ctx.guardedErr);
350
+ };
351
+ if (sub === 'list') {
352
+ rpc('skills.manage', { action: 'list' })
353
+ .then(ctx.guarded(r => {
354
+ const cats = Object.entries(r.skills ?? {}).sort();
355
+ if (!cats.length) {
356
+ return sys('no skills available');
357
+ }
358
+ panel('Skills', cats.map(([title, items]) => ({ items, title })));
359
+ }))
360
+ .catch(ctx.guardedErr);
361
+ return;
362
+ }
363
+ if (sub === 'inspect') {
364
+ if (!query) {
365
+ return sys('usage: /skills inspect <name>');
366
+ }
367
+ rpc('skills.manage', { action: 'inspect', query })
368
+ .then(ctx.guarded(r => {
369
+ const info = r.info ?? {};
370
+ if (!info.name) {
371
+ return sys(`unknown skill: ${query}`);
372
+ }
373
+ const rows = [
374
+ ['Name', String(info.name)],
375
+ ['Category', String(info.category ?? '')],
376
+ ['Path', String(info.path ?? '')]
377
+ ];
378
+ const sections = [{ rows }];
379
+ if (info.description) {
380
+ sections.push({ text: String(info.description) });
381
+ }
382
+ panel('Skill', sections);
383
+ }))
384
+ .catch(ctx.guardedErr);
385
+ return;
386
+ }
387
+ if (sub === 'search') {
388
+ if (!query) {
389
+ return sys('usage: /skills search <query>');
390
+ }
391
+ rpc('skills.manage', { action: 'search', query })
392
+ .then(ctx.guarded(r => {
393
+ const results = r.results ?? [];
394
+ if (!results.length) {
395
+ return sys(`no results for: ${query}`);
396
+ }
397
+ panel(`Search: ${query}`, [{ rows: results.map(s => [s.name, s.description ?? '']) }]);
398
+ }))
399
+ .catch(ctx.guardedErr);
400
+ return;
401
+ }
402
+ if (sub === 'install') {
403
+ if (!query) {
404
+ return sys('usage: /skills install <name or url>');
405
+ }
406
+ sys(`installing ${query}…`);
407
+ rpc('skills.manage', { action: 'install', query })
408
+ .then(ctx.guarded(r => sys(r.installed ? `installed ${r.name ?? query}` : 'install failed')))
409
+ .catch(ctx.guardedErr);
410
+ return;
411
+ }
412
+ if (sub === 'browse') {
413
+ const pageNum = query ? parseInt(query, 10) : 1;
414
+ if (Number.isNaN(pageNum) || pageNum < 1) {
415
+ return sys('usage: /skills browse [page] (page must be a positive number)');
416
+ }
417
+ sys('fetching community skills (scans 6 sources, may take ~15s)…');
418
+ rpc('skills.manage', { action: 'browse', page: pageNum })
419
+ .then(ctx.guarded(r => {
420
+ const items = r.items ?? [];
421
+ if (!items.length) {
422
+ return sys(`no skills on page ${pageNum}${r.total ? ` (total ${r.total})` : ''}`);
423
+ }
424
+ const rows = items.map(s => [
425
+ s.trust ? `${s.name} · ${s.trust}` : s.name,
426
+ String(s.description ?? '').slice(0, 160)
427
+ ]);
428
+ const footer = [];
429
+ if (r.page && r.total_pages) {
430
+ footer.push(`page ${r.page} of ${r.total_pages}`);
431
+ }
432
+ if (r.total) {
433
+ footer.push(`${r.total} skills total`);
434
+ }
435
+ if (r.page && r.total_pages && r.page < r.total_pages) {
436
+ footer.push(`/skills browse ${r.page + 1} for more`);
437
+ }
438
+ panel(`Browse Skills${pageNum > 1 ? ` — p${pageNum}` : ''}`, [
439
+ { rows },
440
+ ...(footer.length ? [{ text: footer.join(' · ') }] : [])
441
+ ]);
442
+ }))
443
+ .catch(ctx.guardedErr);
444
+ return;
445
+ }
446
+ runViaSlashWorker();
447
+ }
162
448
  },
449
+ {
450
+ help: 'enable or disable tools (client-side history reset on change)',
451
+ name: 'tools',
452
+ run: (arg, ctx, cmd) => {
453
+ const [subcommand, ...names] = arg.trim().split(/\s+/).filter(Boolean);
454
+ if (subcommand !== 'disable' && subcommand !== 'enable') {
455
+ ctx.gateway.gw
456
+ .request('slash.exec', { command: cmd.slice(1), session_id: ctx.sid })
457
+ .then(r => {
458
+ if (ctx.stale()) {
459
+ return;
460
+ }
461
+ const body = r?.output || '/tools: no output';
462
+ const text = r?.warning ? `warning: ${r.warning}\n${body}` : body;
463
+ const long = text.length > 180 || text.split('\n').filter(Boolean).length > 2;
464
+ long ? ctx.transcript.page(text, 'Tools') : ctx.transcript.sys(text);
465
+ })
466
+ .catch(ctx.guardedErr);
467
+ return;
468
+ }
469
+ if (!names.length) {
470
+ ctx.transcript.sys(`usage: /tools ${subcommand} <name> [name ...]`);
471
+ ctx.transcript.sys(`built-in toolset: /tools ${subcommand} web`);
472
+ ctx.transcript.sys(`MCP tool: /tools ${subcommand} github:create_issue`);
473
+ return;
474
+ }
475
+ ctx.gateway
476
+ .rpc('tools.configure', { action: subcommand, names, session_id: ctx.sid })
477
+ .then(ctx.guarded(r => {
478
+ if (r.info) {
479
+ ctx.session.setSessionStartedAt(Date.now());
480
+ ctx.session.resetVisibleHistory(r.info);
481
+ }
482
+ if (r.changed?.length) {
483
+ ctx.transcript.sys(`${subcommand === 'disable' ? 'disabled' : 'enabled'}: ${r.changed.join(', ')}`);
484
+ }
485
+ if (r.unknown?.length) {
486
+ ctx.transcript.sys(`unknown toolsets: ${r.unknown.join(', ')}`);
487
+ }
488
+ if (r.missing_servers?.length) {
489
+ ctx.transcript.sys(`missing MCP servers: ${r.missing_servers.join(', ')}`);
490
+ }
491
+ if (r.reset) {
492
+ ctx.transcript.sys('session reset. new tool configuration is active.');
493
+ }
494
+ }))
495
+ .catch(ctx.guardedErr);
496
+ }
497
+ }
163
498
  ];