@pugi/cli 0.1.0-beta.2 → 0.1.0-beta.20
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/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-mascot.ansi +15 -40
- package/bin/run.js +33 -1
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +15 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +196 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/diff-capture.js +73 -0
- package/dist/core/context/index.js +7 -0
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +86 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +442 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/edits/dispatch.js +218 -2
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +111 -18
- package/dist/core/engine/anvil-client.js +115 -5
- package/dist/core/engine/budgets.js +89 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +744 -210
- package/dist/core/engine/prompts.js +61 -6
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +818 -31
- package/dist/core/file-cache.js +113 -1
- package/dist/core/init/scaffold.js +195 -0
- package/dist/core/lsp/client.js +174 -29
- package/dist/core/mcp/client.js +75 -6
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +24 -2
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/permissions/gate.js +187 -0
- package/dist/core/permissions/index.js +18 -0
- package/dist/core/permissions/mode.js +102 -0
- package/dist/core/permissions/state.js +160 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/repl/codebase-survey.js +308 -0
- package/dist/core/repl/history.js +11 -1
- package/dist/core/repl/init-interview.js +457 -0
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/onboarding-state.js +297 -0
- package/dist/core/repl/session.js +719 -29
- package/dist/core/repl/slash-commands.js +133 -9
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/settings.js +71 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +113 -24
- package/dist/core/subagents/index.js +18 -5
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +1588 -266
- package/dist/runtime/commands/compact.js +296 -0
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +289 -0
- package/dist/runtime/commands/doctor.js +369 -0
- package/dist/runtime/commands/lsp.js +187 -5
- package/dist/runtime/commands/mcp.js +824 -0
- package/dist/runtime/commands/patch.js +17 -0
- package/dist/runtime/commands/permissions.js +87 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/review-consensus.js +17 -2
- package/dist/runtime/commands/roster.js +117 -0
- package/dist/runtime/commands/status.js +178 -0
- package/dist/runtime/commands/worktree.js +50 -6
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/runtime/version.js +65 -0
- package/dist/tools/agent-tool.js +206 -0
- package/dist/tools/apply-patch.js +281 -39
- package/dist/tools/ask-user-question.js +213 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/file-tools.js +85 -14
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/registry.js +22 -2
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/web-fetch.js +147 -2
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +10 -0
- package/dist/tui/ask-modal.js +2 -2
- package/dist/tui/ask-user-question-prompt.js +192 -0
- package/dist/tui/compact-banner.js +54 -0
- package/dist/tui/conversation-pane.js +69 -8
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/doctor-table.js +31 -0
- package/dist/tui/input-box.js +1 -1
- package/dist/tui/markdown-render.js +4 -4
- package/dist/tui/repl-render.js +276 -37
- package/dist/tui/repl-splash.js +2 -2
- package/dist/tui/repl.js +25 -6
- package/dist/tui/splash.js +1 -1
- package/dist/tui/status-bar.js +94 -16
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/tool-stream-pane.js +7 -0
- package/dist/tui/update-banner.js +20 -2
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +9 -6
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* AskUserQuestionPrompt — Ink modal for the structured tool grammar
|
|
4
|
+
* (leak L5, openclaude pattern).
|
|
5
|
+
*
|
|
6
|
+
* Renders the four required elements of an openclaude-style clarifier:
|
|
7
|
+
* 1. A short "header" chip at the top (e.g. "Auth method"). Max 12
|
|
8
|
+
* chars by schema, fits one line at the standard 80-col REPL width.
|
|
9
|
+
* 2. The full question prose (must end "?"). Word-wrapped by Ink.
|
|
10
|
+
* 3. The 2-4 options as a selectable list with j/k navigation. Each
|
|
11
|
+
* option shows the `label` (bright) + `description` (dim).
|
|
12
|
+
* 4. An auto-appended "Other" row that the operator can pick to type
|
|
13
|
+
* a custom answer. The model NEVER emits this — the UI owns it.
|
|
14
|
+
*
|
|
15
|
+
* Multi-select mode: when `multiSelect=true`, space toggles the
|
|
16
|
+
* current row, Enter submits the toggled set. Selected rows are
|
|
17
|
+
* marked with a leading checkbox glyph. Single-select mode: Enter
|
|
18
|
+
* commits the highlighted row immediately.
|
|
19
|
+
*
|
|
20
|
+
* Resolver contract: `onResolve` receives either an `answers: string[]`
|
|
21
|
+
* (one or more picked labels), a `customInput: string` (Other path),
|
|
22
|
+
* or `cancelled: true` (Esc). Mirrors AskModal so the REPL wiring
|
|
23
|
+
* stays uniform.
|
|
24
|
+
*
|
|
25
|
+
* Brand voice gate: ASCII glyphs only. No em-dashes, no banned brand
|
|
26
|
+
* words. The copy is power-word neutral so a localised variant lands
|
|
27
|
+
* cleanly later.
|
|
28
|
+
*/
|
|
29
|
+
import { useState } from 'react';
|
|
30
|
+
import { Box, Text, useInput } from 'ink';
|
|
31
|
+
export function AskUserQuestionPrompt(props) {
|
|
32
|
+
const multiSelect = props.multiSelect === true;
|
|
33
|
+
const [mode, setMode] = useState('pick');
|
|
34
|
+
const [cursor, setCursor] = useState(0);
|
|
35
|
+
// Used in multi-select mode: indices in `options` that the operator
|
|
36
|
+
// has toggled. Order preserved so the resolved answer list reflects
|
|
37
|
+
// selection order (the model's downstream reasoning often weights
|
|
38
|
+
// earlier picks higher — preserving order is cheap).
|
|
39
|
+
const [picked, setPicked] = useState([]);
|
|
40
|
+
const [buffer, setBuffer] = useState('');
|
|
41
|
+
const otherIndex = props.options.length; // 0-indexed slot for "Other"
|
|
42
|
+
const totalRows = props.options.length + 1; // options + Other
|
|
43
|
+
useInput((input, key) => {
|
|
44
|
+
// Esc cancels the modal in either mode.
|
|
45
|
+
if (key.escape) {
|
|
46
|
+
props.onResolve({ cancelled: true });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (mode === 'pick') {
|
|
50
|
+
// Numeric hotkeys: 1..N selects the matching option directly
|
|
51
|
+
// (single-select), or toggles it (multi-select). Convenience
|
|
52
|
+
// shortcut so a keyboard-only user does not need to j/k walk
|
|
53
|
+
// through 4 rows. Out-of-range keys fall through.
|
|
54
|
+
const numeric = Number.parseInt(input, 10);
|
|
55
|
+
if (!Number.isNaN(numeric) && numeric >= 1 && numeric <= totalRows) {
|
|
56
|
+
const row = numeric - 1;
|
|
57
|
+
if (row === otherIndex) {
|
|
58
|
+
setMode('custom');
|
|
59
|
+
setBuffer('');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (multiSelect) {
|
|
63
|
+
togglePick(row);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
commitSinglePick(row);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Vim-style navigation: j down, k up. Arrow keys also work.
|
|
70
|
+
if (input === 'j' || key.downArrow) {
|
|
71
|
+
setCursor((c) => (c + 1) % totalRows);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (input === 'k' || key.upArrow) {
|
|
75
|
+
setCursor((c) => (c - 1 + totalRows) % totalRows);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// 'o' hotkey: jump straight to Other (mirrors AskModal).
|
|
79
|
+
if (input === 'o' || input === 'O') {
|
|
80
|
+
setMode('custom');
|
|
81
|
+
setBuffer('');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (key.return) {
|
|
85
|
+
if (cursor === otherIndex) {
|
|
86
|
+
setMode('custom');
|
|
87
|
+
setBuffer('');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (multiSelect) {
|
|
91
|
+
// Enter in multi-select mode COMMITS the toggled set. If the
|
|
92
|
+
// current row is not yet toggled, fold it in first so the
|
|
93
|
+
// operator does not have to press space+enter for a single pick.
|
|
94
|
+
const finalPicks = picked.includes(cursor) ? picked : [...picked, cursor];
|
|
95
|
+
if (finalPicks.length === 0) {
|
|
96
|
+
// No picks + Enter = ignore; the footer hint nudges them.
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const answers = finalPicks.map((i) => props.options[i].label);
|
|
100
|
+
props.onResolve({ answers, cancelled: false });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
commitSinglePick(cursor);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Space toggles the current row in multi-select mode. Ignored in
|
|
107
|
+
// single-select (a single space could otherwise leak into a buffer).
|
|
108
|
+
if (multiSelect && input === ' ') {
|
|
109
|
+
togglePick(cursor);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Custom-input mode: line editor.
|
|
115
|
+
if (key.return) {
|
|
116
|
+
if (buffer.trim().length === 0) {
|
|
117
|
+
// Empty buffer + Enter = bounce back to pick mode (mirrors AskModal).
|
|
118
|
+
setMode('pick');
|
|
119
|
+
setBuffer('');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
props.onResolve({
|
|
123
|
+
customInput: buffer.trim(),
|
|
124
|
+
cancelled: false,
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (key.backspace || key.delete) {
|
|
129
|
+
setBuffer((prev) => prev.slice(0, -1));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (input && !key.meta && !key.ctrl) {
|
|
133
|
+
setBuffer((prev) => prev + input);
|
|
134
|
+
}
|
|
135
|
+
}, { isActive: props.inert !== true });
|
|
136
|
+
function togglePick(row) {
|
|
137
|
+
setPicked((prev) => prev.includes(row) ? prev.filter((i) => i !== row) : [...prev, row]);
|
|
138
|
+
}
|
|
139
|
+
function commitSinglePick(row) {
|
|
140
|
+
if (row < 0 || row >= props.options.length)
|
|
141
|
+
return;
|
|
142
|
+
const opt = props.options[row];
|
|
143
|
+
if (!opt)
|
|
144
|
+
return;
|
|
145
|
+
props.onResolve({ answers: [opt.label], cancelled: false });
|
|
146
|
+
}
|
|
147
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "yellow", children: '? ' }), _jsx(Text, { inverse: true, bold: true, color: "yellow", children: ` ${props.header} ` }), _jsx(Text, { bold: true, children: ' Need your call before I continue' })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: props.question }) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [props.options.map((opt, idx) => {
|
|
148
|
+
const isCursor = mode === 'pick' && cursor === idx;
|
|
149
|
+
const isPicked = multiSelect && picked.includes(idx);
|
|
150
|
+
const marker = multiSelect ? (isPicked ? '[x]' : '[ ]') : `${idx + 1}.`;
|
|
151
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isCursor ? 'cyan' : '#3da9fc', bold: true, children: isCursor ? '> ' : ' ' }), _jsx(Text, { color: "#3da9fc", bold: true, children: `${marker} ` }), _jsx(Text, { bold: isCursor, children: opt.label })] }), _jsx(Box, { marginLeft: multiSelect ? 7 : 5, children: _jsx(Text, { dimColor: true, children: opt.description }) })] }, `${idx}-${opt.label}`));
|
|
152
|
+
}), _jsxs(Box, { children: [_jsx(Text, { color: mode === 'pick' && cursor === otherIndex ? 'cyan' : '#3da9fc', bold: true, children: mode === 'pick' && cursor === otherIndex ? '> ' : ' ' }), _jsx(Text, { color: "#3da9fc", bold: true, children: `${multiSelect ? '[*]' : `${totalRows}.`} ` }), _jsx(Text, { dimColor: true, children: 'Other (type a custom answer)' })] })] }), mode === 'pick' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: multiSelect
|
|
153
|
+
? `j/k navigate. Space toggles. Enter commits. Esc cancels.`
|
|
154
|
+
: `1-${totalRows} or j/k navigate. Enter commits. Esc cancels.` }) })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: '> ' }), _jsx(Text, { children: buffer }), _jsx(Text, { inverse: true, children: ' ' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: 'Type your custom answer. Enter submits. Esc cancels.' }) })] }))] }));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Encode the prompt verdict into the literal turn-injection string. The
|
|
158
|
+
* persona prompt teaches the model to recognise this prefix; mirrors
|
|
159
|
+
* AskModal's `encodeAskVerdict` for consistency.
|
|
160
|
+
*
|
|
161
|
+
* Examples:
|
|
162
|
+
* { answers: ['Vercel'] } → "[ASK-USER-QUESTION:answered] Vercel"
|
|
163
|
+
* { answers: ['a', 'b'] } → "[ASK-USER-QUESTION:answered] a, b"
|
|
164
|
+
* { customInput: 'gcp' } → "[ASK-USER-QUESTION:other] gcp"
|
|
165
|
+
* { cancelled: true } → "[ASK-USER-QUESTION:cancelled]"
|
|
166
|
+
*/
|
|
167
|
+
export function encodeAskUserQuestionVerdict(verdict) {
|
|
168
|
+
if (verdict.cancelled)
|
|
169
|
+
return '[ASK-USER-QUESTION:cancelled]';
|
|
170
|
+
if (verdict.answers && verdict.answers.length > 0) {
|
|
171
|
+
return `[ASK-USER-QUESTION:answered] ${verdict.answers.join(', ')}`;
|
|
172
|
+
}
|
|
173
|
+
if (verdict.customInput && verdict.customInput.trim().length > 0) {
|
|
174
|
+
// Strip any forged verdict header (mirrors AskModal sanitiser).
|
|
175
|
+
const cleaned = sanitiseVerdictText(verdict.customInput);
|
|
176
|
+
if (cleaned.length > 0) {
|
|
177
|
+
return `[ASK-USER-QUESTION:other] ${cleaned}`;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return '[ASK-USER-QUESTION:cancelled]';
|
|
181
|
+
}
|
|
182
|
+
function sanitiseVerdictText(raw) {
|
|
183
|
+
let cleaned = raw;
|
|
184
|
+
for (let i = 0; i < raw.length + 4; i += 1) {
|
|
185
|
+
const stripped = cleaned.replace(/^\s*\[(?:ASK-RESPONSE|PLAN-VERDICT|ASK-USER-QUESTION):[^\]]*\]\s*/u, '');
|
|
186
|
+
if (stripped === cleaned)
|
|
187
|
+
break;
|
|
188
|
+
cleaned = stripped;
|
|
189
|
+
}
|
|
190
|
+
return cleaned.trim();
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=ask-user-question-prompt.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Visible boundary marker for the conversation pane.
|
|
4
|
+
*
|
|
5
|
+
* Rendered inline in the transcript when the replay walker hits a
|
|
6
|
+
* `compaction` event. Conveys three facts at a glance:
|
|
7
|
+
*
|
|
8
|
+
* 1. A compaction happened (separator line).
|
|
9
|
+
* 2. How many turns were folded into the summary.
|
|
10
|
+
* 3. Whether it was manual or auto-triggered.
|
|
11
|
+
*
|
|
12
|
+
* The separator line uses U+2500 (BOX DRAWINGS LIGHT HORIZONTAL) so
|
|
13
|
+
* the visual weight matches the rest of the Ink chrome. Dim ink-color
|
|
14
|
+
* `gray` keeps the marker subdued — it is a navigation aid, not the
|
|
15
|
+
* conversation itself.
|
|
16
|
+
*
|
|
17
|
+
* Width-aware: when stdout columns are known we render the dashes to
|
|
18
|
+
* fill the line minus the centred label; on unknown width we fall
|
|
19
|
+
* back to a fixed 64-dash pad. The fallback is wide enough for any
|
|
20
|
+
* realistic terminal and narrow enough to not wrap on small ones.
|
|
21
|
+
*/
|
|
22
|
+
import { Box, Text } from 'ink';
|
|
23
|
+
const FALLBACK_COLUMNS = 80;
|
|
24
|
+
/**
|
|
25
|
+
* Render the boundary line. The wrapping `<Box>` keeps the marker on
|
|
26
|
+
* its own row even when the surrounding flex container packs siblings.
|
|
27
|
+
*/
|
|
28
|
+
export function CompactBanner(props) {
|
|
29
|
+
const columns = props.columns && props.columns > 20 ? props.columns : FALLBACK_COLUMNS;
|
|
30
|
+
const label = buildLabel(props);
|
|
31
|
+
const dashesEach = Math.max(3, Math.floor((columns - label.length - 2) / 2));
|
|
32
|
+
const dashes = '─'.repeat(dashesEach);
|
|
33
|
+
return (_jsx(Box, { children: _jsx(Text, { color: "gray", children: `${dashes} ${label} ${dashes}` }) }));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build the centred label. Examples:
|
|
37
|
+
* "context compacted (12 turns → 1 summary, auto)"
|
|
38
|
+
* "context compacted (4 turns → 1 summary, manual · ~3.2k tokens)"
|
|
39
|
+
*/
|
|
40
|
+
export function buildLabel(props) {
|
|
41
|
+
const trigger = props.trigger === 'auto' ? 'auto' : 'manual';
|
|
42
|
+
const turns = `${props.turnsBefore} ${props.turnsBefore === 1 ? 'turn' : 'turns'}`;
|
|
43
|
+
const tokens = props.summaryTokenCount && props.summaryTokenCount > 0
|
|
44
|
+
? ` · ~${formatTokens(props.summaryTokenCount)} tokens`
|
|
45
|
+
: '';
|
|
46
|
+
return `context compacted (${turns} → 1 summary, ${trigger}${tokens})`;
|
|
47
|
+
}
|
|
48
|
+
/** Format token counts like 1234 → 1.2k, 950 → 950. */
|
|
49
|
+
function formatTokens(n) {
|
|
50
|
+
if (n < 1000)
|
|
51
|
+
return `${n}`;
|
|
52
|
+
return `${(n / 1000).toFixed(1)}k`;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=compact-banner.js.map
|
|
@@ -36,23 +36,84 @@ function PaneHeader({ count }) {
|
|
|
36
36
|
function ConversationRow({ row, personaNames, }) {
|
|
37
37
|
switch (row.source) {
|
|
38
38
|
case 'operator':
|
|
39
|
-
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "
|
|
39
|
+
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "#3da9fc", children: '› ' }), _jsx(Text, { children: row.text })] }));
|
|
40
40
|
case 'system':
|
|
41
41
|
return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: '· ' }), _jsx(Text, { dimColor: true, children: row.text })] }));
|
|
42
42
|
case 'persona': {
|
|
43
43
|
const slug = row.personaSlug ?? '';
|
|
44
44
|
const color = HUE_COLOR_BY_SLUG[slug] ?? 'white';
|
|
45
45
|
const displayName = personaNames?.get(slug) ?? slug;
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
// CEO live dogfood 2026-05-27: a body that started with a
|
|
47
|
+
// Cyrillic letter (e.g. "Принял.") rendered as "PugiПринял." on
|
|
48
|
+
// the same line because the persona label sat at zero margin and
|
|
49
|
+
// the body fell flush behind it. The label is bold-coloured chrome,
|
|
50
|
+
// not text the operator is meant to read as a sentence stem.
|
|
51
|
+
// Split body to its own row and indent two columns so the
|
|
52
|
+
// transcript reads:
|
|
53
|
+
//
|
|
54
|
+
// ▸ Pugi
|
|
55
|
+
// Принял.
|
|
56
|
+
//
|
|
57
|
+
// matching the Claude Code / Codex / Gemini baseline visually +
|
|
58
|
+
// preventing the "PugiПринял" glue regardless of the body's
|
|
59
|
+
// leading character.
|
|
60
|
+
//
|
|
61
|
+
// The render also strips a leading identity-intro phrase as a
|
|
62
|
+
// defense-in-depth complement to the backend output gate — when
|
|
63
|
+
// turnIndex > 1, the operator must NOT see "Я Pugi - твой
|
|
64
|
+
// инженерный напарник." opening every reply. This is belt-and-
|
|
65
|
+
// braces; the prompt + output-gate already block it upstream.
|
|
66
|
+
const stripped = stripLeadingIdentityIntro(row.text);
|
|
67
|
+
const containsMarkdown = looksLikeMarkdown(stripped);
|
|
68
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsx(Text, { color: color, bold: true, children: `▸ ${displayName}` }) }), containsMarkdown ? (_jsx(Box, { marginLeft: 2, children: _jsx(MarkdownRender, { source: stripped }) })) : (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { children: stripped }) }))] }));
|
|
53
69
|
}
|
|
54
70
|
}
|
|
55
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Strip a leading identity-intro line ("I'm Pugi — your engineering
|
|
74
|
+
* copilot..." / "Я Pugi — твой инженерный напарник...") from the
|
|
75
|
+
* transcript body as a defense-in-depth complement to the backend
|
|
76
|
+
* output gate. The prompt + gate already block this upstream; the
|
|
77
|
+
* render guards against the rare case where a legacy model release
|
|
78
|
+
* sneaks the intro past both layers.
|
|
79
|
+
*
|
|
80
|
+
* The strip is intentionally conservative — we only remove the
|
|
81
|
+
* canonical phrasings the prompt teaches Mira to emit. Any other
|
|
82
|
+
* prose containing the word "Pugi" passes through unchanged.
|
|
83
|
+
*
|
|
84
|
+
* Exported for spec.
|
|
85
|
+
*/
|
|
86
|
+
export function stripLeadingIdentityIntro(text) {
|
|
87
|
+
if (!text || text.length === 0)
|
|
88
|
+
return text;
|
|
89
|
+
// Try each canonical intro shape in turn (RU + EN). We strip only
|
|
90
|
+
// the LEADING occurrence; a mid-body mention is intentional citation
|
|
91
|
+
// (e.g. "the Pugi codename..." in an explainer turn).
|
|
92
|
+
const introPatterns = [
|
|
93
|
+
// RU canonical — both masculine ("напарник/напарника/напарнику") and
|
|
94
|
+
// feminine ("напарница/напарницей/напарнице") declensions land on this
|
|
95
|
+
// strip. The prior pattern `напарни[ккк][аеу]?` was a character-class
|
|
96
|
+
// typo (three `к`s collapse to one) and accepted no feminine form, so
|
|
97
|
+
// a Mira reply opening "Я Pugi — твоя инженерная напарница…" leaked
|
|
98
|
+
// past the strip. P1 reviewer fix PR #540 (2026-05-27).
|
|
99
|
+
/^(?:Я\s+Pugi\s*[—–-]\s*тв(?:ой|ё|оя)\s+инженерн[аыяий][хйеомя]*\s+напарни(?:к[аеу]?|ц(?:ей|а|ы|е|у))[.,!]?\s*)/u,
|
|
100
|
+
/^(?:Я\s+Pugi\s*[—–-]\s*координатор[.,!]?\s*)/u,
|
|
101
|
+
// EN canonical
|
|
102
|
+
/^(?:I'?m\s+Pugi\s*[—–-]\s*your\s+engineering\s+copilot[.,!]?\s*)/iu,
|
|
103
|
+
/^(?:Pugi\s+here[.,!]?\s*)/iu,
|
|
104
|
+
/^(?:This\s+is\s+Pugi[.,!]?\s*)/iu,
|
|
105
|
+
];
|
|
106
|
+
let out = text;
|
|
107
|
+
for (const re of introPatterns) {
|
|
108
|
+
out = out.replace(re, '');
|
|
109
|
+
}
|
|
110
|
+
// If the body had ONLY the intro phrase, return the original — never
|
|
111
|
+
// hand the operator an empty bubble. The output gate would have
|
|
112
|
+
// logged the verbosity hit; the operator sees the unmodified text.
|
|
113
|
+
if (out.trim().length === 0)
|
|
114
|
+
return text;
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
56
117
|
/**
|
|
57
118
|
* Cheap heuristic for "this transcript row will benefit from Markdown
|
|
58
119
|
* rendering". We only pay the parser cost when the row plausibly
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { estimateUsd, formatTokensWithCommas, formatUsd, rateFor } from '../core/cost/rate-card.js';
|
|
4
|
+
/**
|
|
5
|
+
* Column widths chosen to match the L19 spec output. The model column is
|
|
6
|
+
* the widest because slug strings like `qwen3-coder-480b-instruct-fp8`
|
|
7
|
+
* run 31 chars. We pad/truncate inside the renderer so a TUI on a 80-col
|
|
8
|
+
* terminal does not wrap mid-table.
|
|
9
|
+
*/
|
|
10
|
+
const COL_MODEL = 34;
|
|
11
|
+
const COL_IN = 8;
|
|
12
|
+
const COL_OUT = 9;
|
|
13
|
+
const COL_USD = 10;
|
|
14
|
+
/**
|
|
15
|
+
* Render one cost report. Stateless — re-rendering with the same view
|
|
16
|
+
* produces the same output by construction. Tests assert against
|
|
17
|
+
* `lastFrame()` from `ink-testing-library`.
|
|
18
|
+
*/
|
|
19
|
+
export function CostTable({ view }) {
|
|
20
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: view.heading }), _jsx(Text, { children: "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [pad('MODEL', COL_MODEL), padLeft('IN_TOK', COL_IN), padLeft('OUT_TOK', COL_OUT), padLeft('$ EST', COL_USD)] }), view.rows.length === 0 ? (_jsx(Text, { dimColor: true, children: "No calls recorded yet \u2014 brief a persona to charge the meter." })) : (view.rows.map((row) => (_jsxs(Text, { children: [pad(row.model, COL_MODEL), padLeft(formatTokensWithCommas(row.inputTokens), COL_IN), padLeft(formatTokensWithCommas(row.outputTokens), COL_OUT), padLeft(formatUsd(row.usd), COL_USD), row.note ? ` (${row.note})` : ''] }, row.model)))), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Total tokens: ", formatTokensWithCommas(view.totalInputTokens + view.totalOutputTokens), ' (in: ', formatTokensWithCommas(view.totalInputTokens), ', out: ', formatTokensWithCommas(view.totalOutputTokens), ')'] }), _jsxs(Text, { children: ["Total dollar estimate: ", formatUsd(view.totalUsd)] }), view.tier ? _jsxs(Text, { children: ["Tier: ", view.tier.tier, view.tier.quotaLine ? ` (${view.tier.quotaLine})` : ''] }) : null] }));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build a `CostView` from a `SessionAggregate`. The function lives here
|
|
24
|
+
* (next to the renderer) so a future caller — the REPL slash, the CLI
|
|
25
|
+
* command, a JSON-only path — uses the same view-model shape and the
|
|
26
|
+
* same row-sort rule.
|
|
27
|
+
*/
|
|
28
|
+
export function buildCostView(input) {
|
|
29
|
+
const rows = [];
|
|
30
|
+
for (const [model, entry] of Object.entries(input.aggregate.models)) {
|
|
31
|
+
const usd = estimateUsd(model, entry.input, entry.output);
|
|
32
|
+
const rate = rateFor(model);
|
|
33
|
+
rows.push({
|
|
34
|
+
model,
|
|
35
|
+
inputTokens: entry.input,
|
|
36
|
+
outputTokens: entry.output,
|
|
37
|
+
usd,
|
|
38
|
+
note: rate.note,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// Sort by USD descending first (most-expensive-first, matches the L19
|
|
42
|
+
// sample output where the Claude row leads). Ties break by total
|
|
43
|
+
// tokens so two free open-weight rows order deterministically.
|
|
44
|
+
rows.sort((a, b) => {
|
|
45
|
+
if (b.usd !== a.usd)
|
|
46
|
+
return b.usd - a.usd;
|
|
47
|
+
return (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens);
|
|
48
|
+
});
|
|
49
|
+
let totalIn = 0;
|
|
50
|
+
let totalOut = 0;
|
|
51
|
+
let totalUsd = 0;
|
|
52
|
+
for (const row of rows) {
|
|
53
|
+
totalIn += row.inputTokens;
|
|
54
|
+
totalOut += row.outputTokens;
|
|
55
|
+
totalUsd += row.usd;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
heading: input.heading,
|
|
59
|
+
rows,
|
|
60
|
+
totalInputTokens: totalIn,
|
|
61
|
+
totalOutputTokens: totalOut,
|
|
62
|
+
totalUsd,
|
|
63
|
+
tier: input.tier,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Plain-string renderer for non-TTY / `--json` callers. Produces the
|
|
68
|
+
* same table the Ink component would render — no ANSI / no color so it
|
|
69
|
+
* pipes cleanly into `less` or a JSON tool.
|
|
70
|
+
*/
|
|
71
|
+
export function renderCostTableText(view) {
|
|
72
|
+
const lines = [];
|
|
73
|
+
lines.push(view.heading);
|
|
74
|
+
lines.push('════════════════════════════════════════════════');
|
|
75
|
+
lines.push('');
|
|
76
|
+
lines.push(pad('MODEL', COL_MODEL) +
|
|
77
|
+
padLeft('IN_TOK', COL_IN) +
|
|
78
|
+
padLeft('OUT_TOK', COL_OUT) +
|
|
79
|
+
padLeft('$ EST', COL_USD));
|
|
80
|
+
if (view.rows.length === 0) {
|
|
81
|
+
lines.push('No calls recorded yet — brief a persona to charge the meter.');
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
for (const row of view.rows) {
|
|
85
|
+
const noteSuffix = row.note ? ` (${row.note})` : '';
|
|
86
|
+
lines.push(pad(row.model, COL_MODEL) +
|
|
87
|
+
padLeft(formatTokensWithCommas(row.inputTokens), COL_IN) +
|
|
88
|
+
padLeft(formatTokensWithCommas(row.outputTokens), COL_OUT) +
|
|
89
|
+
padLeft(formatUsd(row.usd), COL_USD) +
|
|
90
|
+
noteSuffix);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
lines.push('');
|
|
94
|
+
lines.push(`Total tokens: ${formatTokensWithCommas(view.totalInputTokens + view.totalOutputTokens)} (in: ${formatTokensWithCommas(view.totalInputTokens)}, out: ${formatTokensWithCommas(view.totalOutputTokens)})`);
|
|
95
|
+
lines.push(`Total dollar estimate: ${formatUsd(view.totalUsd)}`);
|
|
96
|
+
if (view.tier) {
|
|
97
|
+
lines.push(`Tier: ${view.tier.tier}${view.tier.quotaLine ? ` (${view.tier.quotaLine})` : ''}`);
|
|
98
|
+
}
|
|
99
|
+
return lines.join('\n');
|
|
100
|
+
}
|
|
101
|
+
function pad(value, width) {
|
|
102
|
+
if (value.length >= width)
|
|
103
|
+
return value.slice(0, Math.max(0, width - 1)) + ' ';
|
|
104
|
+
return value + ' '.repeat(width - value.length);
|
|
105
|
+
}
|
|
106
|
+
function padLeft(value, width) {
|
|
107
|
+
if (value.length >= width)
|
|
108
|
+
return value.slice(0, width);
|
|
109
|
+
return ' '.repeat(width - value.length) + value;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=cost-table.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
/** Brand-voice palette for status cells. Mirrors the table chrome
|
|
4
|
+
* used by `<AgentProgressCard>` so the operator's eye trains on a
|
|
5
|
+
* consistent OK/WARN/ERROR/SKIPPED colour grammar. */
|
|
6
|
+
const STATUS_COLOR = {
|
|
7
|
+
ok: 'green',
|
|
8
|
+
warn: 'yellow',
|
|
9
|
+
error: 'red',
|
|
10
|
+
skipped: 'gray',
|
|
11
|
+
};
|
|
12
|
+
const OVERALL_COLOR = {
|
|
13
|
+
healthy: 'green',
|
|
14
|
+
warning: 'yellow',
|
|
15
|
+
error: 'red',
|
|
16
|
+
};
|
|
17
|
+
export function DoctorTable({ envelope }) {
|
|
18
|
+
const nameWidth = Math.max('NAME'.length, ...envelope.probes.map((row) => row.name.length));
|
|
19
|
+
const statusWidth = Math.max('STATUS'.length, ...envelope.probes.map((row) => row.status.length));
|
|
20
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Pugi Doctor" }) }), envelope.probes.map((row) => (_jsx(DoctorRow, { row: row, nameWidth: nameWidth, statusWidth: statusWidth }, row.name))), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [envelope.counts.error, " error(s), ", envelope.counts.warn, " warning(s), ", envelope.counts.ok, " ok,", ' ', envelope.counts.skipped, " skipped. Overall:", ' ', _jsx(Text, { color: OVERALL_COLOR[envelope.overall], bold: true, children: envelope.overall.toUpperCase() })] }) }), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: ["CLI ", envelope.meta.cliVersion, " Node ", envelope.meta.nodeVersion, " cwd ", envelope.meta.cwd] }) })] }));
|
|
21
|
+
}
|
|
22
|
+
function DoctorRow({ row, nameWidth, statusWidth }) {
|
|
23
|
+
const namePart = row.name.padEnd(nameWidth, ' ');
|
|
24
|
+
const statusPart = row.status.toUpperCase().padEnd(statusWidth, ' ');
|
|
25
|
+
const latencyPart = typeof row.latencyMs === 'number' ? ` (${row.latencyMs}ms)` : '';
|
|
26
|
+
const showRemediation = typeof row.remediation === 'string' &&
|
|
27
|
+
row.remediation.length > 0 &&
|
|
28
|
+
(row.status === 'warn' || row.status === 'error');
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { children: [namePart, " "] }), _jsx(Text, { color: STATUS_COLOR[row.status], bold: true, children: statusPart }), _jsxs(Text, { children: [' ', row.detail, latencyPart] })] }), showRemediation && (_jsx(Box, { marginLeft: nameWidth + statusWidth + 4, children: _jsxs(Text, { dimColor: true, children: ["\u2192 ", row.remediation] }) }))] }));
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=doctor-table.js.map
|
package/dist/tui/input-box.js
CHANGED
|
@@ -499,7 +499,7 @@ export function InputBox(props) {
|
|
|
499
499
|
: Math.min(paletteIndex, paletteView.rows.length - 1);
|
|
500
500
|
const divider = '─'.repeat(innerWidth);
|
|
501
501
|
const focusedMatch = search ? search.matches[search.focusedIndex] : undefined;
|
|
502
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "
|
|
502
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#3da9fc", dimColor: true, children: divider }), _jsx(Box, { paddingX: 1, flexDirection: "column", children: search ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", 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: "#3da9fc", children: '› ' }), _jsx(Text, { children: renderLineWithCursor(line, cursor, cursorVisible) })] })) }), _jsx(Text, { color: "#3da9fc", 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 abort / ×2 exit' }) })] }));
|
|
503
503
|
}
|
|
504
504
|
/**
|
|
505
505
|
* Render the line with the cursor glyph inserted at `cursor`. The cursor
|
|
@@ -107,9 +107,9 @@ function renderBlock(block, key) {
|
|
|
107
107
|
case 'paragraph':
|
|
108
108
|
return (_jsx(Text, { children: renderInline(block.text) }, key));
|
|
109
109
|
case 'bullet':
|
|
110
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: "
|
|
110
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: '• ' }), _jsx(Text, { children: renderInline(block.text) })] }, key));
|
|
111
111
|
case 'ordered':
|
|
112
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: "
|
|
112
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: `${block.index}. ` }), _jsx(Text, { children: renderInline(block.text) })] }, key));
|
|
113
113
|
case 'code':
|
|
114
114
|
return renderCodeBlock(block.lang, block.body, key);
|
|
115
115
|
case 'blank':
|
|
@@ -148,7 +148,7 @@ function renderCodeLine(line, keywords) {
|
|
|
148
148
|
spans.push(_jsx(Text, { color: "green", children: tok }, key));
|
|
149
149
|
}
|
|
150
150
|
else if (keywords.includes(tok)) {
|
|
151
|
-
spans.push(_jsx(Text, { color: "
|
|
151
|
+
spans.push(_jsx(Text, { color: "#3da9fc", bold: true, children: tok }, key));
|
|
152
152
|
}
|
|
153
153
|
else {
|
|
154
154
|
spans.push(_jsx(Text, { children: tok }, key));
|
|
@@ -260,7 +260,7 @@ function renderSpan(span, key) {
|
|
|
260
260
|
case 'code':
|
|
261
261
|
return _jsx(Text, { color: "green", children: span.text }, key);
|
|
262
262
|
case 'link':
|
|
263
|
-
return (_jsxs(Text, { children: [_jsx(Text, { color: "
|
|
263
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: "#3da9fc", underline: true, children: span.text }), _jsx(Text, { dimColor: true, children: ` (${span.url})` })] }, key));
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
//# sourceMappingURL=markdown-render.js.map
|