@pugi/cli 0.1.0-alpha.7 → 0.1.0-alpha.8
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/dist/core/repl/session.js +150 -1
- package/dist/core/repl/slash-commands.js +126 -23
- package/dist/runtime/cli.js +1 -1
- package/dist/tui/input-box.js +1 -1
- package/dist/tui/repl.js +31 -2
- package/package.json +2 -2
|
@@ -32,6 +32,9 @@ import { evaluateCap, describeVerdict } from './cap-warning.js';
|
|
|
32
32
|
import { parseSlashCommand } from './slash-commands.js';
|
|
33
33
|
import { webFetchTool } from '../../tools/web-fetch.js';
|
|
34
34
|
import { loadSettings } from '../settings.js';
|
|
35
|
+
import { getJobRegistry } from '../jobs/registry.js';
|
|
36
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
37
|
+
import { resolve as resolvePath } from 'node:path';
|
|
35
38
|
const MAX_TRANSCRIPT_ROWS = 500;
|
|
36
39
|
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
37
40
|
const RECONNECT_BASE_MS = 250;
|
|
@@ -45,6 +48,18 @@ export class ReplSession {
|
|
|
45
48
|
reconnectAttempt = 0;
|
|
46
49
|
reconnectTimer;
|
|
47
50
|
closed = false;
|
|
51
|
+
/**
|
|
52
|
+
* Last non-trivial step.detail recorded per taskId. The server streams
|
|
53
|
+
* the persona reply incrementally via `agent.step` events whose
|
|
54
|
+
* `detail` field carries the cumulative model output. `agent.completed`
|
|
55
|
+
* arrives last and previously overwrote the visible detail to the
|
|
56
|
+
* literal string `'shipped'` while the transcript line said only
|
|
57
|
+
* `shipped.` — the actual reply text was lost. By caching the last
|
|
58
|
+
* non-trivial detail here, we can flush it into the transcript when
|
|
59
|
+
* the agent completes so the operator sees what the persona actually
|
|
60
|
+
* said. CEO wave-2 fix 2026-05-25.
|
|
61
|
+
*/
|
|
62
|
+
lastStepDetail = new Map();
|
|
48
63
|
constructor(options) {
|
|
49
64
|
this.options = options;
|
|
50
65
|
this.state = {
|
|
@@ -139,8 +154,112 @@ export class ReplSession {
|
|
|
139
154
|
await this.dispatchWebFetch(verdict.url);
|
|
140
155
|
return verdict;
|
|
141
156
|
}
|
|
157
|
+
case 'clear': {
|
|
158
|
+
this.clearTranscript();
|
|
159
|
+
return verdict;
|
|
160
|
+
}
|
|
161
|
+
case 'version': {
|
|
162
|
+
this.appendSystemLine(`pugi ${this.options.cliVersion}`);
|
|
163
|
+
return verdict;
|
|
164
|
+
}
|
|
165
|
+
case 'jobs': {
|
|
166
|
+
await this.dispatchJobs();
|
|
167
|
+
return verdict;
|
|
168
|
+
}
|
|
169
|
+
case 'diff': {
|
|
170
|
+
this.dispatchDiff();
|
|
171
|
+
return verdict;
|
|
172
|
+
}
|
|
173
|
+
case 'cost': {
|
|
174
|
+
this.dispatchCost();
|
|
175
|
+
return verdict;
|
|
176
|
+
}
|
|
177
|
+
case 'status': {
|
|
178
|
+
this.dispatchStatus();
|
|
179
|
+
return verdict;
|
|
180
|
+
}
|
|
181
|
+
case 'stub': {
|
|
182
|
+
this.appendSystemLine(verdict.message);
|
|
183
|
+
return verdict;
|
|
184
|
+
}
|
|
142
185
|
}
|
|
143
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Reset the conversation transcript. The agent registry stays intact
|
|
189
|
+
* so the operator can `/clear` to declutter the chat pane without
|
|
190
|
+
* losing visibility into running dispatches.
|
|
191
|
+
*/
|
|
192
|
+
clearTranscript() {
|
|
193
|
+
this.patch({ transcript: [] });
|
|
194
|
+
}
|
|
195
|
+
/* ------------- Tier 1 / Tier 2 wired handlers -------------- */
|
|
196
|
+
async dispatchJobs() {
|
|
197
|
+
try {
|
|
198
|
+
const registry = getJobRegistry();
|
|
199
|
+
const entries = await registry.list();
|
|
200
|
+
if (entries.length === 0) {
|
|
201
|
+
this.appendSystemLine('No background jobs tracked.');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
this.appendSystemLine(`Background jobs (${entries.length}):`);
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
const id = entry.id.replace(/^pj-/, '').slice(0, 8);
|
|
207
|
+
const status = entry.status;
|
|
208
|
+
const cmd = entry.command.length > 48 ? `${entry.command.slice(0, 47)}…` : entry.command;
|
|
209
|
+
this.appendSystemLine(` ${id} ${status.padEnd(10)} ${cmd}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
this.appendSystemLine(`/jobs failed: ${this.errorMessage(error)}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
dispatchDiff() {
|
|
217
|
+
try {
|
|
218
|
+
const artifactsRoot = resolvePath(process.cwd(), '.pugi', 'artifacts');
|
|
219
|
+
if (!existsSync(artifactsRoot)) {
|
|
220
|
+
this.appendSystemLine('No pending diffs (.pugi/artifacts/ not found).');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const subdirs = readdirSync(artifactsRoot, { withFileTypes: true })
|
|
224
|
+
.filter((d) => d.isDirectory())
|
|
225
|
+
.map((d) => d.name);
|
|
226
|
+
const diffs = [];
|
|
227
|
+
for (const name of subdirs) {
|
|
228
|
+
const candidate = resolvePath(artifactsRoot, name, 'diff.patch');
|
|
229
|
+
if (existsSync(candidate)) {
|
|
230
|
+
const size = statSync(candidate).size;
|
|
231
|
+
diffs.push(` ${name}/diff.patch (${size} bytes)`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (diffs.length === 0) {
|
|
235
|
+
this.appendSystemLine('No pending diffs.');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.appendSystemLine(`Pending diffs (${diffs.length}):`);
|
|
239
|
+
for (const line of diffs)
|
|
240
|
+
this.appendSystemLine(line);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
this.appendSystemLine(`/diff failed: ${this.errorMessage(error)}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
dispatchCost() {
|
|
247
|
+
const { tokensDownstreamTotal, agents } = this.state;
|
|
248
|
+
const active = agents.filter((a) => a.status === 'queued' || a.status === 'thinking').length;
|
|
249
|
+
const lineTokens = `Tokens this session: ${tokensDownstreamTotal.toLocaleString()} (in+out).`;
|
|
250
|
+
const lineAgents = `Active dispatches: ${active} of cap.`;
|
|
251
|
+
this.appendSystemLine(lineTokens);
|
|
252
|
+
this.appendSystemLine(lineAgents);
|
|
253
|
+
this.appendSystemLine('Full per-persona budget breakdown lands in α6.5.');
|
|
254
|
+
}
|
|
255
|
+
dispatchStatus() {
|
|
256
|
+
const sessionId = this.state.sessionId ?? '(unbound)';
|
|
257
|
+
const reach = this.state.connection;
|
|
258
|
+
this.appendSystemLine(`Backend: ${this.options.apiUrl} (${reach}).`);
|
|
259
|
+
this.appendSystemLine(`Session: ${sessionId}.`);
|
|
260
|
+
this.appendSystemLine(`Workspace: ${this.state.workspaceLabel}.`);
|
|
261
|
+
this.appendSystemLine(`CLI: pugi ${this.state.cliVersion}.`);
|
|
262
|
+
}
|
|
144
263
|
/**
|
|
145
264
|
* Fetch one URL via the web_fetch tool and inject the resulting
|
|
146
265
|
* Markdown into the transcript as an operator-attributed brief. The
|
|
@@ -307,6 +426,12 @@ export class ReplSession {
|
|
|
307
426
|
return;
|
|
308
427
|
}
|
|
309
428
|
case 'agent.step': {
|
|
429
|
+
// Cache the running detail per task so we can surface the
|
|
430
|
+
// model's actual reply when agent.completed lands (otherwise
|
|
431
|
+
// the reply disappears under the literal 'shipped' patch).
|
|
432
|
+
if (event.detail && event.detail.trim().length > 0) {
|
|
433
|
+
this.lastStepDetail.set(event.taskId, event.detail);
|
|
434
|
+
}
|
|
310
435
|
this.patch({
|
|
311
436
|
agents: this.state.agents.map((a) => a.taskId === event.taskId
|
|
312
437
|
? { ...a, status: 'thinking', detail: event.detail }
|
|
@@ -330,18 +455,41 @@ export class ReplSession {
|
|
|
330
455
|
}
|
|
331
456
|
case 'agent.completed': {
|
|
332
457
|
const target = this.state.agents.find((a) => a.taskId === event.taskId);
|
|
458
|
+
const finalDetail = this.lastStepDetail.get(event.taskId);
|
|
459
|
+
this.lastStepDetail.delete(event.taskId);
|
|
333
460
|
this.patch({
|
|
334
461
|
agents: this.state.agents.map((a) => a.taskId === event.taskId
|
|
335
462
|
? { ...a, status: 'shipped', detail: 'shipped' }
|
|
336
463
|
: a),
|
|
337
464
|
});
|
|
338
465
|
if (target) {
|
|
339
|
-
|
|
466
|
+
// If the persona actually produced a reply via incremental
|
|
467
|
+
// agent.step events, render that reply in the transcript so
|
|
468
|
+
// the operator sees the full answer. The threshold (>4 chars
|
|
469
|
+
// + not the queued placeholder) filters out the no-op
|
|
470
|
+
// "shipped"/"queued for dispatch" placeholders the dispatcher
|
|
471
|
+
// emits before the model speaks. Multi-line replies are
|
|
472
|
+
// emitted line by line so the conversation pane wraps each
|
|
473
|
+
// sentence on its own row.
|
|
474
|
+
if (finalDetail
|
|
475
|
+
&& finalDetail !== 'queued for dispatch'
|
|
476
|
+
&& finalDetail.trim().length > 4) {
|
|
477
|
+
for (const line of finalDetail.split('\n')) {
|
|
478
|
+
const trimmed = line.trim();
|
|
479
|
+
if (trimmed.length > 0) {
|
|
480
|
+
this.appendPersonaLine(target.personaSlug, trimmed);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
this.appendPersonaLine(target.personaSlug, 'shipped.');
|
|
486
|
+
}
|
|
340
487
|
}
|
|
341
488
|
return;
|
|
342
489
|
}
|
|
343
490
|
case 'agent.blocked': {
|
|
344
491
|
const target = this.state.agents.find((a) => a.taskId === event.taskId);
|
|
492
|
+
this.lastStepDetail.delete(event.taskId);
|
|
345
493
|
this.patch({
|
|
346
494
|
agents: this.state.agents.map((a) => a.taskId === event.taskId
|
|
347
495
|
? { ...a, status: 'blocked', detail: event.detail }
|
|
@@ -354,6 +502,7 @@ export class ReplSession {
|
|
|
354
502
|
}
|
|
355
503
|
case 'agent.failed': {
|
|
356
504
|
const target = this.state.agents.find((a) => a.taskId === event.taskId);
|
|
505
|
+
this.lastStepDetail.delete(event.taskId);
|
|
357
506
|
this.patch({
|
|
358
507
|
agents: this.state.agents.map((a) => a.taskId === event.taskId
|
|
359
508
|
? { ...a, status: 'failed', detail: event.error }
|
|
@@ -1,32 +1,99 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* REPL slash command registry
|
|
2
|
+
* REPL slash command registry — Sprint α5.7, expanded α6.14 wave 2.
|
|
3
3
|
*
|
|
4
|
-
* The REPL input box surfaces a
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* overlay
|
|
8
|
-
* complementary surfaces (`/sync`, `/handoff`, `/budget`) ship as proper
|
|
9
|
-
* subcommands in α5.8+ and stay reachable from a non-REPL `pugi
|
|
10
|
-
* <command>` invocation.
|
|
4
|
+
* The REPL input box surfaces a palette of slash commands the operator
|
|
5
|
+
* can run from inside a persistent session. The wave-2 expansion (CEO
|
|
6
|
+
* 2026-05-25) grows the surface from 6 to 20 commands so the `/help`
|
|
7
|
+
* overlay matches the breadth Claude Code / Codex CLI operators expect.
|
|
11
8
|
*
|
|
12
|
-
* The registry is pure: each
|
|
13
|
-
* describing what the REPL session should do next.
|
|
14
|
-
* owns the side effects (network calls, dispatcher
|
|
15
|
-
* Keeping the surface pure lets
|
|
16
|
-
* without standing up an Ink runtime
|
|
9
|
+
* The registry is pure: each `parseSlashCommand` call returns a
|
|
10
|
+
* `SlashCommandResult` describing what the REPL session should do next.
|
|
11
|
+
* The session module owns the side effects (network calls, dispatcher
|
|
12
|
+
* invocation, exit, transcript clear). Keeping the surface pure lets
|
|
13
|
+
* the unit test exercise every shape without standing up an Ink runtime
|
|
14
|
+
* or an Anvil endpoint.
|
|
15
|
+
*
|
|
16
|
+
* Tiering (per CEO wave-2 spec):
|
|
17
|
+
*
|
|
18
|
+
* Tier 1 — wired against real state (3 + existing 6 = 9 wired):
|
|
19
|
+
* brief, agents, stop, help, quit, web, clear, version, jobs.
|
|
20
|
+
*
|
|
21
|
+
* Tier 2 — best-effort wiring against existing surfaces (3):
|
|
22
|
+
* diff, cost, status.
|
|
23
|
+
*
|
|
24
|
+
* Tier 3 — deterministic stubs ("coming in αX.Y") (8):
|
|
25
|
+
* compact, resume, memory, config, privacy, budget, mcp, undo.
|
|
17
26
|
*
|
|
18
27
|
* Brand voice (brandbook §08): power words `brief / dispatch / stop /
|
|
19
|
-
* agents / quit`. Tagline `Brief it. It ships.` reserved for
|
|
20
|
-
* confirmation and `/help` footer
|
|
28
|
+
* agents / quit / shipped`. Tagline `Brief it. It ships.` reserved for
|
|
29
|
+
* `/quit` confirmation and `/help` footer — never inline.
|
|
21
30
|
*/
|
|
22
31
|
import { listRoles } from '../agents/registry.js';
|
|
32
|
+
/**
|
|
33
|
+
* Deterministic stub copy returned by the Tier 3 commands. Spec'd
|
|
34
|
+
* inline so the unit test can pin the exact text without poking at
|
|
35
|
+
* the help overlay. The version tag at the end maps to the sprint we
|
|
36
|
+
* intend to land the real wiring in.
|
|
37
|
+
*/
|
|
38
|
+
export const SLASH_STUB_MESSAGES = Object.freeze({
|
|
39
|
+
brief: '',
|
|
40
|
+
agents: '',
|
|
41
|
+
stop: '',
|
|
42
|
+
help: '',
|
|
43
|
+
quit: '',
|
|
44
|
+
web: '',
|
|
45
|
+
clear: '',
|
|
46
|
+
version: '',
|
|
47
|
+
jobs: '',
|
|
48
|
+
diff: '',
|
|
49
|
+
cost: '',
|
|
50
|
+
status: '',
|
|
51
|
+
compact: 'Manual context compaction lands in α6.5.',
|
|
52
|
+
resume: 'Resume last session lands in α6.4 once SQLite session.db lands.',
|
|
53
|
+
memory: 'Session memory editor lands in α6.5.',
|
|
54
|
+
config: 'Run `pugi config list` from a fresh shell for the full surface; in-REPL editor lands in α6.5.',
|
|
55
|
+
privacy: 'Run `pugi privacy show` from a fresh shell; in-REPL toggle lands in α6.5.',
|
|
56
|
+
budget: 'Run `pugi budget` from a fresh shell; in-REPL summary lands in α6.5.',
|
|
57
|
+
mcp: 'Run `pugi config mcp list` from a fresh shell; in-REPL palette lands in α6.5.',
|
|
58
|
+
undo: 'Run `pugi undo` from a fresh shell; in-REPL undo lands in α6.5.',
|
|
59
|
+
});
|
|
23
60
|
export const SLASH_COMMAND_HELP = Object.freeze([
|
|
24
|
-
|
|
25
|
-
{ name: '
|
|
26
|
-
{ name: '
|
|
27
|
-
{ name: '
|
|
28
|
-
{ name: '
|
|
29
|
-
|
|
61
|
+
// Workforce dispatch
|
|
62
|
+
{ name: 'brief', args: '<text>', gloss: 'Dispatch a brief to the workforce', group: 'Workforce dispatch' },
|
|
63
|
+
{ name: 'agents', args: '', gloss: 'List the on-watch agent roster', group: 'Workforce dispatch' },
|
|
64
|
+
{ name: 'stop', args: '<persona>', gloss: 'Stop one agent by persona slug', group: 'Workforce dispatch' },
|
|
65
|
+
{ name: 'jobs', args: '', gloss: 'List background jobs', group: 'Workforce dispatch' },
|
|
66
|
+
// Session
|
|
67
|
+
{ name: 'clear', args: '', gloss: 'Clear conversation pane', group: 'Session' },
|
|
68
|
+
{ name: 'resume', args: '', gloss: 'Resume last session (α6.4)', group: 'Session', stub: true },
|
|
69
|
+
{ name: 'compact', args: '', gloss: 'Manual context compaction (α6.5)', group: 'Session', stub: true },
|
|
70
|
+
{ name: 'memory', args: '', gloss: 'Session memory editor (α6.5)', group: 'Session', stub: true },
|
|
71
|
+
// Pugi tools
|
|
72
|
+
{ name: 'web', args: '<url>', gloss: 'Fetch a URL into context', group: 'Pugi tools' },
|
|
73
|
+
{ name: 'diff', args: '', gloss: 'Show pending diff', group: 'Pugi tools' },
|
|
74
|
+
{ name: 'cost', args: '', gloss: 'Token usage + budget', group: 'Pugi tools' },
|
|
75
|
+
{ name: 'status', args: '', gloss: 'Backend + tenant status', group: 'Pugi tools' },
|
|
76
|
+
// Settings
|
|
77
|
+
{ name: 'config', args: '', gloss: 'Show config', group: 'Settings', stub: true },
|
|
78
|
+
{ name: 'privacy', args: '', gloss: 'Show privacy mode', group: 'Settings', stub: true },
|
|
79
|
+
{ name: 'budget', args: '', gloss: 'Show usage budget', group: 'Settings', stub: true },
|
|
80
|
+
{ name: 'mcp', args: '', gloss: 'List MCP servers', group: 'Settings', stub: true },
|
|
81
|
+
{ name: 'undo', args: '', gloss: 'Undo last write', group: 'Settings', stub: true },
|
|
82
|
+
// Meta
|
|
83
|
+
{ name: 'help', args: '', gloss: 'Show this help overlay', group: 'Meta' },
|
|
84
|
+
{ name: 'version', args: '', gloss: 'Show CLI version', group: 'Meta' },
|
|
85
|
+
{ name: 'quit', args: '', gloss: 'Exit the REPL', group: 'Meta' },
|
|
86
|
+
]);
|
|
87
|
+
/**
|
|
88
|
+
* Ordered list of groups. Drives the `/help` overlay sectioning so the
|
|
89
|
+
* operator reads commands by intent (dispatch first, meta last).
|
|
90
|
+
*/
|
|
91
|
+
export const SLASH_COMMAND_GROUPS = Object.freeze([
|
|
92
|
+
'Workforce dispatch',
|
|
93
|
+
'Session',
|
|
94
|
+
'Pugi tools',
|
|
95
|
+
'Settings',
|
|
96
|
+
'Meta',
|
|
30
97
|
]);
|
|
31
98
|
/**
|
|
32
99
|
* Parse one line of input from the REPL. The contract:
|
|
@@ -34,13 +101,15 @@ export const SLASH_COMMAND_HELP = Object.freeze([
|
|
|
34
101
|
* - Empty / whitespace-only input returns `noop` with the original
|
|
35
102
|
* text so the REPL can ignore it without printing anything.
|
|
36
103
|
* - Input that does not start with `/` is treated as an implicit
|
|
37
|
-
* `/brief <text>`
|
|
104
|
+
* `/brief <text>` — the most-common operator action.
|
|
38
105
|
* - `/<name> [args]` resolves the name against the registry; unknown
|
|
39
106
|
* names return `error` so the REPL can render a one-line tip
|
|
40
107
|
* instead of silently dropping the input.
|
|
108
|
+
* - Tier 3 stubs return `{ kind: 'stub', name, message }` so the REPL
|
|
109
|
+
* can render the deterministic "coming in αX.Y" copy uniformly.
|
|
41
110
|
*
|
|
42
111
|
* The function never throws. Bad input maps to a structured result the
|
|
43
|
-
* REPL can render
|
|
112
|
+
* REPL can render — the alternative (throwing from a keystroke handler)
|
|
44
113
|
* would unmount Ink mid-frame.
|
|
45
114
|
*/
|
|
46
115
|
export function parseSlashCommand(input) {
|
|
@@ -98,6 +167,40 @@ export function parseSlashCommand(input) {
|
|
|
98
167
|
}
|
|
99
168
|
return { kind: 'web', url: tail };
|
|
100
169
|
}
|
|
170
|
+
case 'clear':
|
|
171
|
+
case 'cls': {
|
|
172
|
+
return { kind: 'clear' };
|
|
173
|
+
}
|
|
174
|
+
case 'version':
|
|
175
|
+
case 'v': {
|
|
176
|
+
return { kind: 'version' };
|
|
177
|
+
}
|
|
178
|
+
case 'jobs': {
|
|
179
|
+
return { kind: 'jobs' };
|
|
180
|
+
}
|
|
181
|
+
case 'diff': {
|
|
182
|
+
return { kind: 'diff' };
|
|
183
|
+
}
|
|
184
|
+
case 'cost': {
|
|
185
|
+
return { kind: 'cost' };
|
|
186
|
+
}
|
|
187
|
+
case 'status': {
|
|
188
|
+
return { kind: 'status' };
|
|
189
|
+
}
|
|
190
|
+
case 'compact':
|
|
191
|
+
case 'resume':
|
|
192
|
+
case 'memory':
|
|
193
|
+
case 'config':
|
|
194
|
+
case 'privacy':
|
|
195
|
+
case 'budget':
|
|
196
|
+
case 'mcp':
|
|
197
|
+
case 'undo': {
|
|
198
|
+
return {
|
|
199
|
+
kind: 'stub',
|
|
200
|
+
name: name,
|
|
201
|
+
message: SLASH_STUB_MESSAGES[name],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
101
204
|
default: {
|
|
102
205
|
return {
|
|
103
206
|
kind: 'error',
|
package/dist/runtime/cli.js
CHANGED
|
@@ -35,7 +35,7 @@ import { runBudgetCommand } from './commands/budget.js';
|
|
|
35
35
|
* packages/pugi-sdk/package.json); the publish workflow validates the
|
|
36
36
|
* three are in lockstep.
|
|
37
37
|
*/
|
|
38
|
-
const PUGI_CLI_VERSION = '0.1.0-alpha.
|
|
38
|
+
const PUGI_CLI_VERSION = '0.1.0-alpha.8';
|
|
39
39
|
const handlers = {
|
|
40
40
|
accounts,
|
|
41
41
|
build: runEngineTask('build_task'),
|
package/dist/tui/input-box.js
CHANGED
|
@@ -456,7 +456,7 @@ export function InputBox(props) {
|
|
|
456
456
|
: Math.min(paletteIndex, paletteView.rows.length - 1);
|
|
457
457
|
const divider = '─'.repeat(innerWidth);
|
|
458
458
|
const focusedMatch = search ? search.matches[search.focusedIndex] : undefined;
|
|
459
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", dimColor: true, children: divider }), _jsx(Box, {
|
|
459
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", dimColor: true, children: divider }), _jsx(Box, { paddingX: 1, flexDirection: "column", children: search ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '(reverse-i-search) ' }), _jsx(Text, { children: `\`${search.query}\`: ` }), _jsx(Text, { color: "yellow", children: focusedMatch ? focusedMatch.brief : '(no match)' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `Ctrl+R next · Ctrl+S prev · Enter accept · Esc cancel · ${search.matches.length} match${search.matches.length === 1 ? '' : 'es'}` }) })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '› ' }), _jsx(Text, { children: renderLineWithCursor(line, cursor, cursorVisible) })] })) }), _jsx(Text, { color: "cyan", dimColor: true, children: divider }), line.length > innerWidth - 4 ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: '┊ ' }), _jsx(Text, { dimColor: true, children: 'line wraps - Enter still submits' })] })) : null, _jsx(SlashPalette, { rows: paletteView.rows, focusedIndex: clampedPaletteIndex, totalBeforeLimit: paletteView.totalBeforeLimit }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: '↑/↓ history · Ctrl+R search · / commands · Enter brief · Esc cancel · Ctrl+C ×2 exit' }) })] }));
|
|
460
460
|
}
|
|
461
461
|
/**
|
|
462
462
|
* Render the line with the cursor glyph inserted at `cursor`. The cursor
|
package/dist/tui/repl.js
CHANGED
|
@@ -26,7 +26,7 @@ import { InputBox } from './input-box.js';
|
|
|
26
26
|
import { StatusBar } from './status-bar.js';
|
|
27
27
|
import { UpdateBanner } from './update-banner.js';
|
|
28
28
|
import { slugForCwd } from '../core/repl/history.js';
|
|
29
|
-
import { SLASH_COMMAND_HELP } from '../core/repl/slash-commands.js';
|
|
29
|
+
import { SLASH_COMMAND_HELP, SLASH_COMMAND_GROUPS } from '../core/repl/slash-commands.js';
|
|
30
30
|
const TICK_INTERVAL_MS = 200;
|
|
31
31
|
const PULSE_INTERVAL_MS = 700;
|
|
32
32
|
export function Repl(props) {
|
|
@@ -114,7 +114,27 @@ function MainArea({ state, personaNames, nowEpochMs, }) {
|
|
|
114
114
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ConversationPane, { rows: conversationSlice, personaNames: personaNames }), _jsx(Box, { marginTop: 1, children: _jsx(AgentTree, { agents: state.agents, nowEpochMs: nowEpochMs }) })] }));
|
|
115
115
|
}
|
|
116
116
|
function HelpOverlay() {
|
|
117
|
-
|
|
117
|
+
// Group commands by their `group` field so the operator scans the
|
|
118
|
+
// palette by intent (dispatch → session → tools → settings → meta).
|
|
119
|
+
// The α6.14 wave-2 expansion grew the surface from 6 to 20 commands;
|
|
120
|
+
// a flat list would force the operator to read 20 rows top-to-bottom
|
|
121
|
+
// every time. Grouping cuts perceived complexity dramatically.
|
|
122
|
+
const grouped = new Map();
|
|
123
|
+
for (const row of SLASH_COMMAND_HELP) {
|
|
124
|
+
const list = grouped.get(row.group);
|
|
125
|
+
if (list) {
|
|
126
|
+
grouped.set(row.group, [...list, row]);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
grouped.set(row.group, [row]);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Pugi REPL help" }), SLASH_COMMAND_GROUPS.map((group) => {
|
|
133
|
+
const rows = grouped.get(group);
|
|
134
|
+
if (!rows || rows.length === 0)
|
|
135
|
+
return null;
|
|
136
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: ` -- ${group} --` }), rows.map((row) => (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: ` /${row.name} ${row.args}`.padEnd(22, ' ') }), _jsx(Text, { dimColor: true, children: row.gloss })] }, row.name)))] }, group));
|
|
137
|
+
}), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: `${PUGI_TAGLINE}` }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: 'Press any key to dismiss.' }) })] }));
|
|
118
138
|
}
|
|
119
139
|
function RosterOverlay() {
|
|
120
140
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "On-watch roster" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: THE_TEN.map((persona) => (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: ` ${persona.name.padEnd(10, ' ')}` }), _jsx(Text, { dimColor: true, children: `${persona.role.padEnd(20, ' ')}` }), _jsx(Text, { children: persona.oneLiner })] }, persona.slug))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: 'Press any key to dismiss.' }) })] }));
|
|
@@ -138,6 +158,15 @@ function applyVerdictSideEffects(verdict, handlers) {
|
|
|
138
158
|
case 'web':
|
|
139
159
|
case 'error':
|
|
140
160
|
case 'noop':
|
|
161
|
+
case 'clear':
|
|
162
|
+
case 'version':
|
|
163
|
+
case 'jobs':
|
|
164
|
+
case 'diff':
|
|
165
|
+
case 'cost':
|
|
166
|
+
case 'status':
|
|
167
|
+
case 'stub':
|
|
168
|
+
// All non-overlay verdicts: the session module already appended
|
|
169
|
+
// any operator-visible system lines. No further UI side effect.
|
|
141
170
|
return;
|
|
142
171
|
}
|
|
143
172
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pugi/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.8",
|
|
4
4
|
"description": "Pugi CLI — terminal-native software execution system",
|
|
5
5
|
"homepage": "https://pugi.io",
|
|
6
6
|
"repository": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"undici": "^8.3.0",
|
|
48
48
|
"zod": "^3.23.0",
|
|
49
49
|
"@pugi/personas": "0.1.0",
|
|
50
|
-
"@pugi/sdk": "0.1.0-alpha.
|
|
50
|
+
"@pugi/sdk": "0.1.0-alpha.8"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/node": "^22.0.0",
|