@pugi/cli 0.1.0-beta.5 → 0.1.0-beta.50
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 -25
- package/assets/pugi-prozr2-mascot.ansi +9 -0
- package/bin/run.js +33 -1
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +15 -0
- package/dist/commands/smoke.js +133 -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/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash-classifier.js +400 -4
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -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 +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/diff-capture.js +112 -3
- 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/bare-mode.js +42 -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/hooks.js +118 -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/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/sandbox.js +40 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -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 +322 -0
- package/dist/core/engine/anvil-client.js +115 -5
- package/dist/core/engine/budgets.js +98 -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 +860 -211
- package/dist/core/engine/prompts.js +88 -2
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1045 -36
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +776 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/mcp/client.js +75 -6
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -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/memory/dual-write.js +416 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory-sync/queue.js +158 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/path-security.js +284 -2
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/history.js +11 -1
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/session.js +1897 -37
- package/dist/core/repl/slash-commands.js +430 -15
- package/dist/core/repl/store/session-store.js +31 -2
- package/dist/core/repl/workspace-context.js +22 -0
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +80 -0
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -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/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/core/worktree-manager/cleanup.js +123 -0
- package/dist/core/worktree-manager/manager.js +303 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +3241 -343
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +242 -11
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +412 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/lsp.js +368 -0
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +508 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +17 -2
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +32 -0
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/commands/worktrees.js +155 -0
- package/dist/runtime/headless-repl.js +195 -0
- 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 +229 -0
- package/dist/tools/apply-patch.js +556 -0
- package/dist/tools/ask-user-question.js +213 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +203 -4
- package/dist/tools/file-tools.js +85 -14
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +268 -0
- package/dist/tools/registry.js +51 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -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 +81 -0
- package/dist/tui/conversation-pane.js +82 -8
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +218 -3
- package/dist/tui/markdown-render.js +4 -4
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/repl-render.js +313 -35
- package/dist/tui/repl-splash-art.js +1 -1
- package/dist/tui/repl-splash-mascot.js +32 -8
- package/dist/tui/repl-splash.js +2 -2
- package/dist/tui/repl.js +85 -5
- 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/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/thinking-spinner.js +123 -0
- package/dist/tui/tool-stream-pane.js +52 -3
- package/dist/tui/update-banner.js +27 -2
- package/dist/tui/vim-input.js +267 -0
- package/dist/tui/welcome-banner.js +107 -0
- package/dist/tui/welcome-data.js +293 -0
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +12 -6
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
package/dist/tui/repl.js
CHANGED
|
@@ -26,9 +26,12 @@ import { ConversationPane } from './conversation-pane.js';
|
|
|
26
26
|
import { InputBox } from './input-box.js';
|
|
27
27
|
import { ReplSplash } from './repl-splash.js';
|
|
28
28
|
import { StatusBar } from './status-bar.js';
|
|
29
|
+
import { ThinkingSpinner } from './thinking-spinner.js';
|
|
29
30
|
import { ToolStreamPane } from './tool-stream-pane.js';
|
|
30
31
|
import { UpdateBanner } from './update-banner.js';
|
|
32
|
+
import { WelcomeBanner } from './welcome-banner.js';
|
|
31
33
|
import { collectWorkspaceContext } from './workspace-context.js';
|
|
34
|
+
import { useTheme } from '../core/theme/context.js';
|
|
32
35
|
import { slugForCwd } from '../core/repl/history.js';
|
|
33
36
|
import { SLASH_COMMAND_HELP, SLASH_COMMAND_GROUPS } from '../core/repl/slash-commands.js';
|
|
34
37
|
const TICK_INTERVAL_MS = 200;
|
|
@@ -56,6 +59,12 @@ export function Repl(props) {
|
|
|
56
59
|
// Tenant block crowding the top.
|
|
57
60
|
const [splashVisible, setSplashVisible] = useState(false);
|
|
58
61
|
const dismissSplash = useCallback(() => setSplashVisible(false), []);
|
|
62
|
+
// CEO P0 #2 (2026-05-29): CC-style welcome banner. Visible from boot
|
|
63
|
+
// until the operator submits the first brief OR the session emits
|
|
64
|
+
// its first agent event. The host owns dismissal lifecycle (kept
|
|
65
|
+
// symmetric with the splash) so the welcome card never lingers
|
|
66
|
+
// behind а live transcript.
|
|
67
|
+
const [welcomeVisible, setWelcomeVisible] = useState(Boolean(props.welcomeData));
|
|
59
68
|
// α6.14 wave 3: workspace context snapshot for the status bar. We
|
|
60
69
|
// read once at mount and freeze; a brand-new PUGI.md or skill is
|
|
61
70
|
// surfaced on the next REPL boot rather than via a watcher.
|
|
@@ -99,6 +108,20 @@ export function Repl(props) {
|
|
|
99
108
|
setSplashVisible(false);
|
|
100
109
|
}
|
|
101
110
|
}, [splashVisible, state.agents.length, state.transcript.length]);
|
|
111
|
+
// CEO P0 #2 (2026-05-29) v2: welcome banner stays until the operator
|
|
112
|
+
// actively engages the loop — first agent spawn. Boot-time auto-init
|
|
113
|
+
// emits system rows into `state.transcript` (skip-trust hints, dirty
|
|
114
|
+
// tree warnings) which used к dismiss the banner within ~2s, hiding
|
|
115
|
+
// the brand mascot before the operator could read it. Drop the
|
|
116
|
+
// `transcript.length` trigger; agent spawn (= real dispatch) remains
|
|
117
|
+
// the sole signal that the operator stopped reading the banner.
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (!welcomeVisible)
|
|
120
|
+
return;
|
|
121
|
+
if (state.agents.length > 0) {
|
|
122
|
+
setWelcomeVisible(false);
|
|
123
|
+
}
|
|
124
|
+
}, [welcomeVisible, state.agents.length]);
|
|
102
125
|
const personaNames = useMemo(() => buildPersonaNameMap(), []);
|
|
103
126
|
const { exit } = useApp();
|
|
104
127
|
const handleSubmit = useCallback((line) => {
|
|
@@ -106,6 +129,10 @@ export function Repl(props) {
|
|
|
106
129
|
// `setSplashVisible(false)` is a no-op once the state already
|
|
107
130
|
// settled to false (timer fired or `agent.spawned` arrived).
|
|
108
131
|
setSplashVisible(false);
|
|
132
|
+
// CEO P0 #2 (2026-05-29): same dismissal for the welcome banner
|
|
133
|
+
// — the operator engaging the input box is the cleanest signal
|
|
134
|
+
// they have finished reading the boot card.
|
|
135
|
+
setWelcomeVisible(false);
|
|
109
136
|
// Run async without awaiting - the session module owns the
|
|
110
137
|
// network call, errors land in the transcript automatically.
|
|
111
138
|
void props.session.handleInput(line).then((verdict) => {
|
|
@@ -183,6 +210,46 @@ export function Repl(props) {
|
|
|
183
210
|
return undefined;
|
|
184
211
|
return props.session.cancel();
|
|
185
212
|
}, [props.session, modalActive]);
|
|
213
|
+
// Wave 6 BT 8 (Claude Code parity): Esc-Esc walkback. Forwards to
|
|
214
|
+
// ReplSession.walkbackLastTurn which trims the trailing operator
|
|
215
|
+
// turn + its persona response from the in-memory transcript. Returns
|
|
216
|
+
// `'walked-back'` so the input box knows the host did the work;
|
|
217
|
+
// `'nothing'` covers both the empty-transcript and dispatch-in-flight
|
|
218
|
+
// refusals (the session module owns the refusal copy in both cases).
|
|
219
|
+
const handleWalkback = useCallback(() => {
|
|
220
|
+
if (modalActive)
|
|
221
|
+
return 'nothing';
|
|
222
|
+
const verdict = props.session.walkbackLastTurn();
|
|
223
|
+
return verdict === 'walked-back' ? 'walked-back' : 'nothing';
|
|
224
|
+
}, [props.session, modalActive]);
|
|
225
|
+
// Wave 7 — Shift+Tab cycles the 6 canonical permission modes (CC
|
|
226
|
+
// parity). Refuses while a modal is active so the operator does not
|
|
227
|
+
// accidentally flip mode mid-prompt; otherwise resolves the current
|
|
228
|
+
// mode through the workspace > global > default merge, advances via
|
|
229
|
+
// `nextPermissionMode`, и persists к .pugi/session.json. Returns the
|
|
230
|
+
// new mode string so the InputBox can flash a one-line toast.
|
|
231
|
+
const handleCyclePermissionMode = useCallback(() => {
|
|
232
|
+
if (modalActive)
|
|
233
|
+
return null;
|
|
234
|
+
try {
|
|
235
|
+
// Lazy-require так this code path doesn't drag the permissions
|
|
236
|
+
// module into the splash + boot stages where it isn't needed.
|
|
237
|
+
// The require is sync but the inner work is pure JSON IO.
|
|
238
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
239
|
+
const perm = require('../core/permissions/index.js');
|
|
240
|
+
const workspaceRoot = process.cwd();
|
|
241
|
+
const current = perm.resolveMode({ workspaceRoot });
|
|
242
|
+
const next = perm.nextPermissionMode(current);
|
|
243
|
+
perm.setCurrentMode(workspaceRoot, next);
|
|
244
|
+
return next;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// Persistence is best-effort — if .pugi/session.json is read-only
|
|
248
|
+
// или ENOENT-on-parent the toast is suppressed so we don't lie
|
|
249
|
+
// about the flip к the operator.
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}, [modalActive]);
|
|
186
253
|
// α6.14.5 CEO dogfood 2026-05-25 (parity with Claude Code): input
|
|
187
254
|
// box pinned to alt-screen BOTTOM, conversation grows above it.
|
|
188
255
|
// Beta.3's height={rows} fix broke keystroke focus - raw echo at
|
|
@@ -191,14 +258,25 @@ export function Repl(props) {
|
|
|
191
258
|
// input, and the input stays the sole focusable surface adjacent
|
|
192
259
|
// to the cursor row, so all keystrokes route through it.
|
|
193
260
|
const altScreenRows = process.stdout.rows ?? 24;
|
|
194
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, minHeight: altScreenRows, children: [props.
|
|
261
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, minHeight: altScreenRows, children: [welcomeVisible && props.welcomeData ? (_jsx(WelcomeBanner, { data: props.welcomeData, mascotPrePrinted: props.mascotPrePrinted === true, autoInitStatus: props.autoInitStatus ?? null })) : null, splashVisible ? (_jsx(ReplSplash, { cliVersion: state.cliVersion, workspaceLabel: state.workspaceLabel, plan: props.splashPlan, model: props.splashModel, tenant: props.splashTenant, onDismiss: dismissSplash, mascotPrePrinted: props.mascotPrePrinted === true })) : null, _jsx(Header, { state: state }), _jsx(Box, { flexDirection: "column", marginTop: 1, flexGrow: 1, justifyContent: "flex-end", children: overlay === 'help' ? (_jsx(HelpOverlay, {})) : overlay === 'roster' ? (_jsx(RosterOverlay, {})) : overlay === 'farewell' ? (_jsx(FarewellOverlay, {})) : (_jsx(MainArea, { state: state, personaNames: personaNames, nowEpochMs: tickNow, hideToolStream: props.hideToolStream === true, toolStreamCollapsed: toolStreamCollapsed })) }), state.pendingAsk ? (_jsx(Box, { marginTop: 1, children: _jsx(AskModal, { tag: state.pendingAsk, onResolve: handleAskResolve }) })) : null, state.pendingPlanReview ? (_jsx(Box, { marginTop: 1, children: _jsx(PlanReviewModal, { tag: state.pendingPlanReview, onResolve: handlePlanReviewResolve }) })) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [overlay === 'farewell' || modalActive ? null : (_jsx(InputBox, { onSubmit: handleSubmit, onExit: handleExit, onCancel: handleCancel, onWalkback: handleWalkback, onCyclePermissionMode: handleCyclePermissionMode, now: props.now,
|
|
195
262
|
// Slug from process.cwd() (full path) so two workspaces with
|
|
196
263
|
// the same basename do not share history. state.workspaceLabel
|
|
197
264
|
// is the basename only. Codex review P2.
|
|
198
|
-
workspaceSlug: slugForCwd(process.cwd()) })), _jsx(StatusBar, { connection: state.connection, activeAgentCount: countActive(state), tokensDownstreamTotal: state.tokensDownstreamTotal, briefStartedAtEpochMs: state.briefStartedAtEpochMs, nowEpochMs: tickNow, pulsePhase: pulsePhase, pugiMdCount: workspaceContext.pugiMdCount, mcpServerCount: workspaceContext.mcpServerCount, skillCount: workspaceContext.skillCount, quotaPct: props.quotaPct, dispatchState: state.dispatchState, dispatchToolLabel: state.dispatchToolLabel
|
|
265
|
+
workspaceSlug: slugForCwd(process.cwd()) })), _jsx(ThinkingSpinner, { dispatchState: state.dispatchState, dispatchToolLabel: state.dispatchToolLabel }), _jsx(StatusBar, { connection: state.connection, activeAgentCount: countActive(state), tokensDownstreamTotal: state.tokensDownstreamTotal, briefStartedAtEpochMs: state.briefStartedAtEpochMs, nowEpochMs: tickNow, pulsePhase: pulsePhase, pugiMdCount: workspaceContext.pugiMdCount, mcpServerCount: workspaceContext.mcpServerCount, skillCount: workspaceContext.skillCount, quotaPct: props.quotaPct, dispatchState: state.dispatchState, dispatchToolLabel: state.dispatchToolLabel, lastCompletedOutcome: state.lastCompletedOutcome,
|
|
266
|
+
// α7 cost-meter sprint — surface accumulated session totals
|
|
267
|
+
// + per-turn delta flash on the status bar's top row. The
|
|
268
|
+
// session module owns accumulation; the bar is a pure render.
|
|
269
|
+
sessionTokensIn: state.sessionTokensIn, sessionTokensOut: state.sessionTokensOut, sessionCostUsd: state.sessionCostUsd, sessionStartedAtEpochMs: state.sessionStartedAtEpochMs, lastTurnDelta: state.lastTurnDelta }), props.updateBanner ? _jsx(UpdateBanner, { result: props.updateBanner }) : null] })] }));
|
|
199
270
|
}
|
|
200
271
|
function Header({ state }) {
|
|
201
|
-
|
|
272
|
+
// Leak L30 (2026-05-27): the header `.io` brand accent + connection
|
|
273
|
+
// pill route through `useTheme()` so the operator's `/theme` flip
|
|
274
|
+
// (default / dark / light / colorblind) re-tints the chrome on
|
|
275
|
+
// re-mount. The `useTheme` hook returns the `default` preset's
|
|
276
|
+
// colors when no provider is mounted, preserving the previous
|
|
277
|
+
// `#3da9fc` constants for tests that import `<Repl />` standalone.
|
|
278
|
+
const theme = useTheme();
|
|
279
|
+
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Pugi" }), _jsx(Text, { bold: true, color: theme.accent, children: ".io" }), _jsx(Text, { dimColor: true, children: ` · workspace: ${state.workspaceLabel} · v${state.cliVersion} · ` }), _jsx(Text, { color: theme.accent, children: state.connection === 'on_watch' ? 'on watch' : state.connection.replace('_', ' ') })] }));
|
|
202
280
|
}
|
|
203
281
|
function MainArea({ state, personaNames, nowEpochMs, hideToolStream, toolStreamCollapsed, }) {
|
|
204
282
|
// α6.12: three vertical panes stacked above the input box.
|
|
@@ -238,14 +316,14 @@ function HelpOverlay() {
|
|
|
238
316
|
const rows = grouped.get(group);
|
|
239
317
|
if (!rows || rows.length === 0)
|
|
240
318
|
return null;
|
|
241
|
-
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: "
|
|
319
|
+
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: "#3da9fc", children: ` /${row.name} ${row.args}`.padEnd(22, ' ') }), _jsx(Text, { dimColor: true, children: row.gloss })] }, row.name)))] }, group));
|
|
242
320
|
}), _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.' }) })] }));
|
|
243
321
|
}
|
|
244
322
|
function RosterOverlay() {
|
|
245
323
|
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.' }) })] }));
|
|
246
324
|
}
|
|
247
325
|
function FarewellOverlay() {
|
|
248
|
-
return (_jsx(Box, { flexDirection: "column", alignItems: "center", paddingY: 2, children: _jsx(Text, { bold: true, color: "
|
|
326
|
+
return (_jsx(Box, { flexDirection: "column", alignItems: "center", paddingY: 2, children: _jsx(Text, { bold: true, color: "#3da9fc", children: PUGI_TAGLINE }) }));
|
|
249
327
|
}
|
|
250
328
|
function applyVerdictSideEffects(verdict, handlers) {
|
|
251
329
|
switch (verdict.kind) {
|
|
@@ -270,8 +348,10 @@ function applyVerdictSideEffects(verdict, handlers) {
|
|
|
270
348
|
case 'consensus':
|
|
271
349
|
case 'diff':
|
|
272
350
|
case 'cost':
|
|
351
|
+
case 'quota':
|
|
273
352
|
case 'status':
|
|
274
353
|
case 'resume':
|
|
354
|
+
case 'mcp':
|
|
275
355
|
case 'stub':
|
|
276
356
|
// All non-overlay verdicts: the session module already appended
|
|
277
357
|
// any operator-visible system lines (and, for `ask`, set
|
package/dist/tui/splash.js
CHANGED
|
@@ -21,7 +21,7 @@ export function Splash({ data }) {
|
|
|
21
21
|
cmd: 'pugi login',
|
|
22
22
|
gloss: 'Connect this terminal to your Pugi account',
|
|
23
23
|
};
|
|
24
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: "
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: "#3da9fc", children: "pugi.io" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `v${data.cliVersion} · ${data.apiUrl}` }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Account: " }), _jsx(Text, { children: accountLine })] }), data.isAuthenticated && data.plan ? (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Plan: " }), _jsx(Text, { children: data.plan })] })) : null] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Quick start:" }), _jsx(HintRow, { command: primaryHint.cmd, gloss: primaryHint.gloss }), data.isAuthenticated ? (_jsx(HintRow, { command: 'pugi login', gloss: 'Re-authenticate or switch accounts' })) : (_jsx(HintRow, { command: 'pugi code "fix the bug"', gloss: 'Run a one-shot coding task' })), _jsx(HintRow, { command: 'pugi review --triple', gloss: 'Run the Anvil triple-review gate' }), _jsx(HintRow, { command: 'pugi help', gloss: 'Full command reference' })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Docs: https://pugi.dev \u00B7 Status: https://pugi.io/status" }) })] }));
|
|
25
25
|
}
|
|
26
26
|
function HintRow({ command, gloss }) {
|
|
27
27
|
// Pad command names so the gloss column lines up across rows.
|
package/dist/tui/status-bar.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { formatCostUsd, formatTokens } from '../core/repl/model-pricing.js';
|
|
4
|
+
/**
|
|
5
|
+
* Window during which the per-turn delta flash stays visible on the
|
|
6
|
+
* cost-meter row. CEO spec: ~2 seconds after completion. Past that, the
|
|
7
|
+
* flash dimms out and the row shows session totals only.
|
|
8
|
+
*/
|
|
9
|
+
const TURN_DELTA_FLASH_MS = 2_000;
|
|
3
10
|
/**
|
|
4
11
|
* Cyan dot glyphs across the pulse cycle. Three steps keep the motion
|
|
5
12
|
* subtle - a true gradient would force an Ink rerender on every
|
|
@@ -17,8 +24,73 @@ export function StatusBar(props) {
|
|
|
17
24
|
// first. When the connection is healthy (`on_watch` / `connecting`),
|
|
18
25
|
// the FSM dispatch state takes over to show the dispatch lifecycle
|
|
19
26
|
// (`dispatching` / `tool: read` / `aborting` / etc.).
|
|
20
|
-
const status = composeStatusLabel(props.connection, props.dispatchState, props.dispatchToolLabel);
|
|
21
|
-
|
|
27
|
+
const status = composeStatusLabel(props.connection, props.dispatchState, props.dispatchToolLabel, props.lastCompletedOutcome);
|
|
28
|
+
// α7 cost-meter sprint — the cost row anchors above the legacy
|
|
29
|
+
// dispatch-state line so the operator's eye lands on the meter first
|
|
30
|
+
// (matches Claude Code TUI footer rhythm). The session-elapsed slot
|
|
31
|
+
// uses sessionStartedAtEpochMs (REPL boot), distinct from the
|
|
32
|
+
// per-brief `elapsedLabel` on the row below.
|
|
33
|
+
const costRow = renderCostMeterRow(props.sessionTokensIn ?? 0, props.sessionTokensOut ?? 0, props.sessionCostUsd ?? 0, props.sessionStartedAtEpochMs, now);
|
|
34
|
+
const deltaFlash = renderTurnDeltaFlash(props.lastTurnDelta, now);
|
|
35
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: `↑ ${costRow.tokensInLabel}` }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { color: "cyan", children: `↓ ${costRow.tokensOutLabel}` }), _jsx(Text, { dimColor: true, children: ` · ` }), _jsx(Text, { bold: true, children: costRow.costLabel }), _jsx(Text, { dimColor: true, children: ` · ${costRow.elapsedLabel}` }), deltaFlash ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { color: "green", children: deltaFlash })] })) : null] }), _jsxs(Box, { children: [_jsx(Text, { color: status.color, children: `${glyph ?? '●'} ${status.label}` }), _jsx(Text, { dimColor: true, children: ` · ${props.activeAgentCount} agents · ` }), _jsx(Text, { children: `↓ ${tokenLabel} tokens` }), _jsx(Text, { dimColor: true, children: ` · ${elapsedLabel}` }), typeof props.externalDispatchCount === 'number' && props.externalDispatchCount > 0 ? (_jsx(Text, { color: "yellow", children: ` · ${props.externalDispatchCount} dispatch${props.externalDispatchCount === 1 ? '' : 'es'} active. /cancel к manage.` })) : null] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `${formatCount(props.pugiMdCount)} PUGI.md · ${formatCount(props.mcpServerCount)} MCP · ${formatCount(props.skillCount)} skills · ${formatQuota(props.quotaPct)} quota` }) })] }));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* α7 cost-meter sprint — assemble the cost-meter row labels. Pure helper
|
|
39
|
+
* so the snapshot tests assert the formatted shape without standing up
|
|
40
|
+
* an Ink renderer.
|
|
41
|
+
*/
|
|
42
|
+
export function renderCostMeterRow(tokensIn, tokensOut, costUsd, sessionStartedAtEpochMs, nowEpochMs) {
|
|
43
|
+
const elapsedMs = typeof sessionStartedAtEpochMs === 'number'
|
|
44
|
+
? Math.max(0, nowEpochMs - sessionStartedAtEpochMs)
|
|
45
|
+
: 0;
|
|
46
|
+
return {
|
|
47
|
+
tokensInLabel: formatTokens(tokensIn),
|
|
48
|
+
tokensOutLabel: formatTokens(tokensOut),
|
|
49
|
+
costLabel: formatCostUsd(costUsd),
|
|
50
|
+
elapsedLabel: formatElapsedShort(elapsedMs),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* α7 cost-meter sprint — render the per-turn delta flash when the most
|
|
55
|
+
* recent turn completed within the flash window. Returns null when no
|
|
56
|
+
* turn has completed yet OR the flash has expired (the elapsed slot
|
|
57
|
+
* gets the slot back). The flash format mirrors the spec:
|
|
58
|
+
*
|
|
59
|
+
* `+200/+1.1k +$0.01`
|
|
60
|
+
*
|
|
61
|
+
* Exported for snapshot tests.
|
|
62
|
+
*/
|
|
63
|
+
export function renderTurnDeltaFlash(delta, nowEpochMs) {
|
|
64
|
+
if (!delta)
|
|
65
|
+
return null;
|
|
66
|
+
const elapsedSinceMs = nowEpochMs - delta.completedAtEpochMs;
|
|
67
|
+
if (elapsedSinceMs < 0 || elapsedSinceMs > TURN_DELTA_FLASH_MS)
|
|
68
|
+
return null;
|
|
69
|
+
const inLabel = formatTokens(delta.tokensIn);
|
|
70
|
+
const outLabel = formatTokens(delta.tokensOut);
|
|
71
|
+
const costLabel = delta.costUsd > 0 ? ` +${formatCostUsd(delta.costUsd)}` : '';
|
|
72
|
+
return `+${inLabel}/+${outLabel}${costLabel}`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* α7 cost-meter sprint — local copy of the session elapsed formatter.
|
|
76
|
+
* Mirrors the helper in `core/repl/session.ts` so the status bar stays
|
|
77
|
+
* a pure leaf component without a circular import on session.ts (the
|
|
78
|
+
* model-pricing module's `formatDuration` is similar but ships an
|
|
79
|
+
* `XhYm` ceiling that does not match the CEO spec's `2m44s` shape).
|
|
80
|
+
*/
|
|
81
|
+
function formatElapsedShort(elapsedMs) {
|
|
82
|
+
if (!Number.isFinite(elapsedMs) || elapsedMs <= 0)
|
|
83
|
+
return '0s';
|
|
84
|
+
const totalSec = Math.floor(elapsedMs / 1000);
|
|
85
|
+
if (totalSec < 60)
|
|
86
|
+
return `${totalSec}s`;
|
|
87
|
+
const min = Math.floor(totalSec / 60);
|
|
88
|
+
const sec = totalSec % 60;
|
|
89
|
+
if (min < 60)
|
|
90
|
+
return `${min}m${sec.toString().padStart(2, '0')}s`;
|
|
91
|
+
const hr = Math.floor(min / 60);
|
|
92
|
+
const restMin = min % 60;
|
|
93
|
+
return `${hr}h${restMin.toString().padStart(2, '0')}m`;
|
|
22
94
|
}
|
|
23
95
|
/**
|
|
24
96
|
* Render a count badge — number if defined, `—` placeholder otherwise.
|
|
@@ -75,7 +147,7 @@ export function connectionLabel(connection) {
|
|
|
75
147
|
* `tool: <kind>` upstream so we just concatenate; null falls through
|
|
76
148
|
* to the bare `tool` placeholder.
|
|
77
149
|
*/
|
|
78
|
-
function composeStatusLabel(connection, dispatchState, toolLabel) {
|
|
150
|
+
function composeStatusLabel(connection, dispatchState, toolLabel, lastCompletedOutcome) {
|
|
79
151
|
// Transport health wins.
|
|
80
152
|
if (connection === 'offline' || connection === 'reconnecting') {
|
|
81
153
|
return connectionLabel(connection);
|
|
@@ -93,6 +165,16 @@ function composeStatusLabel(connection, dispatchState, toolLabel) {
|
|
|
93
165
|
case 'awaiting_response':
|
|
94
166
|
return { label: 'dispatching', color: 'cyan' };
|
|
95
167
|
case 'completed':
|
|
168
|
+
// Branch on the work-done outcome so the bottom-bar tells the
|
|
169
|
+
// same truth as the agent-tree (2026-05-26 — memory
|
|
170
|
+
// feedback_no_fake_dispatch_promises). `'replied'` = text-only
|
|
171
|
+
// turn, render with the same neutral gray + arrow used in the
|
|
172
|
+
// agent-tree. `'shipped'` = real side-effect (or older server
|
|
173
|
+
// that omits the outcome field). Defaults to `'shipped'` so
|
|
174
|
+
// older callers without the prop wired stay back-compat.
|
|
175
|
+
if (lastCompletedOutcome === 'replied') {
|
|
176
|
+
return { label: 'replied', color: 'gray' };
|
|
177
|
+
}
|
|
96
178
|
return { label: 'shipped', color: 'green' };
|
|
97
179
|
case 'idle':
|
|
98
180
|
case undefined:
|
|
@@ -111,18 +193,14 @@ function formatElapsed(startedAt, now) {
|
|
|
111
193
|
const seconds = Math.floor((ms % 60_000) / 1000);
|
|
112
194
|
return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
|
|
113
195
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (total < 1_000_000)
|
|
123
|
-
return `${(total / 1_000).toFixed(1)}k`;
|
|
124
|
-
return `${(total / 1_000_000).toFixed(1)}m`;
|
|
125
|
-
}
|
|
196
|
+
// `formatTokens` for the downstream-throughput slot is imported from
|
|
197
|
+
// `core/repl/model-pricing.ts` — single source of truth for token
|
|
198
|
+
// formatting across the cost-meter row, `/cost` slash, and the legacy
|
|
199
|
+
// downstream-tokens slot. The shape is identical to the prior local
|
|
200
|
+
// helper (`<1000` raw, `<1m` one-decimal k, `≥1m` one-decimal m); the
|
|
201
|
+
// only semantic difference is non-finite / negative inputs render as
|
|
202
|
+
// `0` instead of throwing, matching the cost-meter row's defensive
|
|
203
|
+
// posture.
|
|
126
204
|
function clampPhase(phase) {
|
|
127
205
|
if (typeof phase !== 'number' || Number.isNaN(phase))
|
|
128
206
|
return 0;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export function StatusTable({ snapshot }) {
|
|
4
|
+
const labelWidth = Math.max('Label'.length, ...snapshot.fields.map((f) => f.label.length));
|
|
5
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Pugi status" }) }), snapshot.fields.map((field) => (_jsxs(Box, { children: [_jsxs(Text, { children: [field.label.padEnd(labelWidth, ' '), " "] }), field.available ? (_jsx(Text, { children: field.value })) : (_jsx(Text, { dimColor: true, children: field.value }))] }, field.key))), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["CLI ", snapshot.meta.cliVersion, " Node ", snapshot.meta.nodeVersion, " cwd ", snapshot.meta.cwd] }) })] }));
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=status-table.js.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
/**
|
|
4
|
+
* Curated ASCII pug corpus. Five variants — wide enough that repeat
|
|
5
|
+
* invocations look fresh, narrow enough that every entry stays
|
|
6
|
+
* hand-vetted (no procedural slop). Each art block intentionally fits
|
|
7
|
+
* inside an 80-column terminal so the surrounding box border does not
|
|
8
|
+
* wrap on narrow shells.
|
|
9
|
+
*
|
|
10
|
+
* The trailing newline at the end of each `art` string is intentional —
|
|
11
|
+
* keeps the renderer's join logic uniform between the boxed and the
|
|
12
|
+
* `--ascii-only` paths.
|
|
13
|
+
*/
|
|
14
|
+
export const PUG_STICKERS = Object.freeze([
|
|
15
|
+
{
|
|
16
|
+
id: 'classic-face',
|
|
17
|
+
caption: 'classic pug face',
|
|
18
|
+
art: [
|
|
19
|
+
' _._ _,-\'""`-._',
|
|
20
|
+
' (,-.`._,\'( |\\`-/|',
|
|
21
|
+
' `-.-\' \\ )-`( , o o)',
|
|
22
|
+
' `- \\`_`"\'-',
|
|
23
|
+
].join('\n'),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'sit-pose',
|
|
27
|
+
caption: 'sit, stay, ship',
|
|
28
|
+
art: [
|
|
29
|
+
' /\\___/\\',
|
|
30
|
+
' ( o o )',
|
|
31
|
+
' ( =^= )',
|
|
32
|
+
' (______)',
|
|
33
|
+
].join('\n'),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'peek',
|
|
37
|
+
caption: 'peek-a-pug',
|
|
38
|
+
art: [
|
|
39
|
+
' __',
|
|
40
|
+
' ___/ \\___',
|
|
41
|
+
' / o o \\',
|
|
42
|
+
' | > ^ < |',
|
|
43
|
+
' \\__________/',
|
|
44
|
+
].join('\n'),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'sleepy',
|
|
48
|
+
caption: 'sleepy pug, no Zzz today',
|
|
49
|
+
art: [
|
|
50
|
+
' .--.',
|
|
51
|
+
' / - -\\',
|
|
52
|
+
' ( ^ ^ )',
|
|
53
|
+
' \\ ^^ /',
|
|
54
|
+
' `----\'',
|
|
55
|
+
].join('\n'),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'shipping',
|
|
59
|
+
caption: 'shipping pug',
|
|
60
|
+
art: [
|
|
61
|
+
' .---. .---.',
|
|
62
|
+
' |o_o| |o_o|',
|
|
63
|
+
' \\_^_/ \\_^_/',
|
|
64
|
+
' /| |\\ /| |\\',
|
|
65
|
+
' shipped • shipped',
|
|
66
|
+
].join('\n'),
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
/**
|
|
70
|
+
* Curated rotating-quote pool. Brand voice gate (brandbook §08):
|
|
71
|
+
* `brief / dispatch / stop / agents / quit / shipped` are the power
|
|
72
|
+
* words; quotes lean on those and на the operator-mode register.
|
|
73
|
+
* Adding lines: keep each ≤ 64 chars so the boxed renderer never wraps,
|
|
74
|
+
* stay в the operator's voice, no AI attribution, no hype.
|
|
75
|
+
*/
|
|
76
|
+
export const PUG_QUOTES = Object.freeze([
|
|
77
|
+
'Pugi: your engineering co-pilot.',
|
|
78
|
+
'Brief it. It ships.',
|
|
79
|
+
'Built for operators, not for benchmarks.',
|
|
80
|
+
'Pugi: твой инженерный напарник.',
|
|
81
|
+
'Dispatch agents, not promises.',
|
|
82
|
+
'Small CLI. Loud workforce.',
|
|
83
|
+
'Engineering at the speed of brief.',
|
|
84
|
+
'Pugi: shipping is the default mode.',
|
|
85
|
+
]);
|
|
86
|
+
/**
|
|
87
|
+
* Clamp a raw rng draw to a safe array index. Handles every hostile
|
|
88
|
+
* shape the spec exercises:
|
|
89
|
+
* - rng returns NaN → fall back to 0
|
|
90
|
+
* - rng returns 1.0 → clamp to length-1 (Math.floor would land at n)
|
|
91
|
+
* - rng returns -ε → clamp to 0
|
|
92
|
+
* The caller hands в the corpus length; the helper never touches the
|
|
93
|
+
* corpus itself so it stays trivially testable.
|
|
94
|
+
*/
|
|
95
|
+
function safeIndex(raw, length) {
|
|
96
|
+
if (!Number.isFinite(raw))
|
|
97
|
+
return 0;
|
|
98
|
+
const floored = Math.floor(raw);
|
|
99
|
+
if (floored < 0)
|
|
100
|
+
return 0;
|
|
101
|
+
if (floored >= length)
|
|
102
|
+
return length - 1;
|
|
103
|
+
return floored;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Pick one art variant. Defaults to `Math.random` but the caller can
|
|
107
|
+
* inject a deterministic source — the spec uses a sequence-driven
|
|
108
|
+
* stub to assert the picker hits each entry в the corpus.
|
|
109
|
+
*/
|
|
110
|
+
export function pickArtVariant(rng = Math.random) {
|
|
111
|
+
const raw = rng() * PUG_STICKERS.length;
|
|
112
|
+
return PUG_STICKERS[safeIndex(raw, PUG_STICKERS.length)];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Pick one rotating brand quote. Same contract as `pickArtVariant` —
|
|
116
|
+
* test-injectable rng so the spec can pin the chosen index.
|
|
117
|
+
*/
|
|
118
|
+
export function pickQuote(rng = Math.random) {
|
|
119
|
+
const raw = rng() * PUG_QUOTES.length;
|
|
120
|
+
return PUG_QUOTES[safeIndex(raw, PUG_QUOTES.length)];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Plain-text renderer for the `--ascii-only` flag and the non-TTY shell
|
|
124
|
+
* path. Emits the art verbatim, then a blank line, then the quote.
|
|
125
|
+
* No box border — scripting use-case (`pugi stickers --ascii-only`
|
|
126
|
+
* piped to `figlet`, `lolcat`, or a regression-fixture file) gets a
|
|
127
|
+
* stable contract free of decorative ANSI noise.
|
|
128
|
+
*/
|
|
129
|
+
export function renderPugStickersText(art, quote) {
|
|
130
|
+
return `${art.art}\n\n${quote}`;
|
|
131
|
+
}
|
|
132
|
+
export function PugStickersArt({ art, quote }) {
|
|
133
|
+
const lines = art.art.split('\n');
|
|
134
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Pugi stickers" }), _jsxs(Text, { dimColor: true, children: [" \u2014 ", art.caption] })] }), _jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { children: line }, `art-${i}`))) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["\"", quote, "\""] }) })] }));
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=stickers-art.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { OUTPUT_STYLES, OUTPUT_STYLE_SLUGS, } from '../core/output-style/presets.js';
|
|
4
|
+
import { useTheme } from '../core/theme/context.js';
|
|
5
|
+
/**
|
|
6
|
+
* Banner above the table. Plain text (not bold) so the prefix `*`
|
|
7
|
+
* remains the dominant active-row cue.
|
|
8
|
+
*/
|
|
9
|
+
function buildBanner(active, source) {
|
|
10
|
+
return `Active style: ${active} (${source})`;
|
|
11
|
+
}
|
|
12
|
+
export function StyleTable({ active, source }) {
|
|
13
|
+
// Leak L30 (2026-05-27): the active-row marker color flows through
|
|
14
|
+
// the theme so `colorblind` operators see cyan instead of green
|
|
15
|
+
// (which their palette re-maps to `success`). Falls back to the
|
|
16
|
+
// default theme's `success` token when no provider is mounted.
|
|
17
|
+
const theme = useTheme();
|
|
18
|
+
const slugWidth = Math.max('NAME'.length, ...OUTPUT_STYLE_SLUGS.map((slug) => slug.length));
|
|
19
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Pugi output styles" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: ` ${'NAME'.padEnd(slugWidth)} GLOSS` }) }), OUTPUT_STYLE_SLUGS.map((slug) => (_jsx(StyleRow, { slug: slug, active: active, slugWidth: slugWidth, activeColor: theme.success }, slug))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: buildBanner(active, source) }) })] }));
|
|
20
|
+
}
|
|
21
|
+
function StyleRow({ slug, active, slugWidth, activeColor }) {
|
|
22
|
+
const isActive = slug === active;
|
|
23
|
+
const marker = isActive ? '*' : ' ';
|
|
24
|
+
const slugPart = slug.padEnd(slugWidth, ' ');
|
|
25
|
+
const gloss = OUTPUT_STYLES[slug].gloss;
|
|
26
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isActive ? activeColor : undefined, bold: isActive, children: `${marker} ${slugPart}` }), _jsx(Text, { children: ` ${gloss}` })] }));
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=style-table.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { compileSampleRow, THEMES, THEME_SLUGS, } from '../core/theme/presets.js';
|
|
4
|
+
/**
|
|
5
|
+
* Banner above the table. Plain text (not bold) so the prefix `*`
|
|
6
|
+
* remains the dominant active-row cue. Mirrors `<StyleTable>` so the
|
|
7
|
+
* Settings-group surfaces read identically.
|
|
8
|
+
*/
|
|
9
|
+
function buildBanner(active, source) {
|
|
10
|
+
return `Active theme: ${active} (${source})`;
|
|
11
|
+
}
|
|
12
|
+
export function ThemeTable({ active, source }) {
|
|
13
|
+
const slugWidth = Math.max('NAME'.length, ...THEME_SLUGS.map((slug) => slug.length));
|
|
14
|
+
// The gloss column gets sized to the widest gloss + 2 padding so
|
|
15
|
+
// the sample column lines up. Computed once per render so the
|
|
16
|
+
// layout stays stable when the catalogue grows.
|
|
17
|
+
const glossWidth = Math.max('GLOSS'.length, ...THEME_SLUGS.map((slug) => THEMES[slug].gloss.length));
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Pugi themes" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: ` ${'NAME'.padEnd(slugWidth)} ${'GLOSS'.padEnd(glossWidth)} SAMPLE` }) }), THEME_SLUGS.map((slug) => (_jsx(ThemeRow, { slug: slug, active: active, slugWidth: slugWidth, glossWidth: glossWidth }, slug))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: buildBanner(active, source) }) })] }));
|
|
19
|
+
}
|
|
20
|
+
function ThemeRow({ slug, active, slugWidth, glossWidth }) {
|
|
21
|
+
const isActive = slug === active;
|
|
22
|
+
const marker = isActive ? '*' : ' ';
|
|
23
|
+
const slugPart = slug.padEnd(slugWidth, ' ');
|
|
24
|
+
const preset = THEMES[slug];
|
|
25
|
+
const gloss = preset.gloss.padEnd(glossWidth, ' ');
|
|
26
|
+
const sample = compileSampleRow(slug);
|
|
27
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isActive ? preset.colors.accent : undefined, bold: isActive, children: `${marker} ${slugPart}` }), _jsx(Text, { children: ` ${gloss} ` }), _jsx(Text, { color: preset.colors.foreground, children: sample.foreground }), _jsx(Text, { children: ' ' }), _jsx(Text, { color: preset.colors.accent, children: sample.accent }), _jsx(Text, { children: ' ' }), _jsx(Text, { color: preset.colors.success, children: sample.success }), _jsx(Text, { children: ' ' }), _jsx(Text, { color: preset.colors.warning, children: sample.warning }), _jsx(Text, { children: ' ' }), _jsx(Text, { color: preset.colors.error, children: sample.error })] }));
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=theme-table.js.map
|