@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.
Files changed (263) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/assets/pugi-mascot.ansi +15 -25
  3. package/assets/pugi-prozr2-mascot.ansi +9 -0
  4. package/bin/run.js +33 -1
  5. package/dist/commands/jobs-watch.js +201 -0
  6. package/dist/commands/jobs.js +15 -0
  7. package/dist/commands/smoke.js +133 -0
  8. package/dist/core/agent-progress/cleanup.js +134 -0
  9. package/dist/core/agent-progress/schema.js +144 -0
  10. package/dist/core/agent-progress/writer.js +101 -0
  11. package/dist/core/artifact-chain/dispatcher.js +148 -0
  12. package/dist/core/artifact-chain/exporter.js +164 -0
  13. package/dist/core/artifact-chain/state.js +243 -0
  14. package/dist/core/artifact-chain/steps.js +169 -0
  15. package/dist/core/auth/ensure-authenticated.js +129 -0
  16. package/dist/core/auth/env-provider.js +238 -0
  17. package/dist/core/auto-update/channels.js +122 -0
  18. package/dist/core/auto-update/checker.js +241 -0
  19. package/dist/core/auto-update/state.js +235 -0
  20. package/dist/core/bare-mode/index.js +107 -0
  21. package/dist/core/bash-classifier.js +400 -4
  22. package/dist/core/checkpoint/resumer.js +149 -0
  23. package/dist/core/checkpoint/rewinder.js +291 -0
  24. package/dist/core/codegraph/decision-store.js +248 -0
  25. package/dist/core/codegraph/detect-repo.js +459 -0
  26. package/dist/core/codegraph/install.js +134 -0
  27. package/dist/core/codegraph/offer-hook.js +220 -0
  28. package/dist/core/compact/auto-trigger.js +96 -0
  29. package/dist/core/compact/buffer-rewriter.js +115 -0
  30. package/dist/core/compact/summarizer.js +208 -0
  31. package/dist/core/compact/token-counter.js +108 -0
  32. package/dist/core/consensus/diff-capture.js +112 -3
  33. package/dist/core/context/index.js +7 -0
  34. package/dist/core/context/markdown-traverse.js +255 -0
  35. package/dist/core/cost/rate-card.js +129 -0
  36. package/dist/core/cost/tracker.js +221 -0
  37. package/dist/core/denial-tracking/index.js +8 -0
  38. package/dist/core/denial-tracking/state.js +264 -0
  39. package/dist/core/diagnostics/probe-runner.js +93 -0
  40. package/dist/core/diagnostics/probes/api.js +46 -0
  41. package/dist/core/diagnostics/probes/auth.js +86 -0
  42. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  43. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  44. package/dist/core/diagnostics/probes/config.js +72 -0
  45. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  46. package/dist/core/diagnostics/probes/disk.js +81 -0
  47. package/dist/core/diagnostics/probes/git.js +65 -0
  48. package/dist/core/diagnostics/probes/hooks.js +118 -0
  49. package/dist/core/diagnostics/probes/mcp.js +75 -0
  50. package/dist/core/diagnostics/probes/node.js +59 -0
  51. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  52. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  53. package/dist/core/diagnostics/probes/sandbox.js +40 -0
  54. package/dist/core/diagnostics/probes/session.js +74 -0
  55. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  56. package/dist/core/diagnostics/probes/workspace.js +63 -0
  57. package/dist/core/diagnostics/types.js +70 -0
  58. package/dist/core/dispatch/cache-cleanup.js +197 -0
  59. package/dist/core/dispatch/cache-handoff.js +295 -0
  60. package/dist/core/edits/dispatch.js +218 -2
  61. package/dist/core/edits/journal.js +199 -0
  62. package/dist/core/edits/layer-d-ast.js +557 -14
  63. package/dist/core/edits/verify-hook.js +273 -0
  64. package/dist/core/edits/worktree.js +322 -0
  65. package/dist/core/engine/anvil-client.js +115 -5
  66. package/dist/core/engine/budgets.js +98 -0
  67. package/dist/core/engine/context-prefix.js +155 -0
  68. package/dist/core/engine/intent.js +260 -0
  69. package/dist/core/engine/native-pugi.js +860 -211
  70. package/dist/core/engine/prompts.js +88 -2
  71. package/dist/core/engine/strip-internal-fields.js +124 -0
  72. package/dist/core/engine/tool-bridge.js +1045 -36
  73. package/dist/core/feedback/queue.js +177 -0
  74. package/dist/core/feedback/submitter.js +145 -0
  75. package/dist/core/file-cache.js +113 -1
  76. package/dist/core/hooks/events.js +44 -0
  77. package/dist/core/hooks/index.js +15 -0
  78. package/dist/core/hooks/registry.js +213 -0
  79. package/dist/core/hooks/runner.js +236 -0
  80. package/dist/core/hooks/v2/event-emitter.js +115 -0
  81. package/dist/core/hooks/v2/executor.js +282 -0
  82. package/dist/core/hooks/v2/index.js +25 -0
  83. package/dist/core/hooks/v2/lifecycle.js +104 -0
  84. package/dist/core/hooks/v2/loader.js +216 -0
  85. package/dist/core/hooks/v2/matcher.js +125 -0
  86. package/dist/core/hooks/v2/trust.js +143 -0
  87. package/dist/core/hooks/v2/types.js +86 -0
  88. package/dist/core/lsp/cache.js +105 -0
  89. package/dist/core/lsp/client.js +776 -0
  90. package/dist/core/lsp/language-detect.js +66 -0
  91. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  92. package/dist/core/mcp/client.js +75 -6
  93. package/dist/core/mcp/http-server.js +553 -0
  94. package/dist/core/mcp/orchestrator-tools.js +662 -0
  95. package/dist/core/mcp/permission.js +190 -0
  96. package/dist/core/mcp/registry.js +24 -2
  97. package/dist/core/mcp/server-tools.js +219 -0
  98. package/dist/core/mcp/server.js +397 -0
  99. package/dist/core/memory/dual-write.js +416 -0
  100. package/dist/core/memory/phase1-kinds.js +20 -0
  101. package/dist/core/memory-sync/queue.js +158 -0
  102. package/dist/core/onboarding/ensure-initialized.js +133 -0
  103. package/dist/core/onboarding/marker.js +111 -0
  104. package/dist/core/onboarding/telemetry-state.js +108 -0
  105. package/dist/core/output-style/presets.js +176 -0
  106. package/dist/core/output-style/state.js +185 -0
  107. package/dist/core/path-security.js +284 -2
  108. package/dist/core/permissions/auto-classifier.js +124 -0
  109. package/dist/core/permissions/circuit-breaker.js +83 -0
  110. package/dist/core/permissions/gate.js +278 -0
  111. package/dist/core/permissions/index.js +20 -0
  112. package/dist/core/permissions/mode.js +174 -0
  113. package/dist/core/permissions/state.js +241 -0
  114. package/dist/core/permissions/tool-class.js +93 -0
  115. package/dist/core/prd-check/parser.js +215 -0
  116. package/dist/core/prd-check/reporter.js +127 -0
  117. package/dist/core/prd-check/session-review.js +557 -0
  118. package/dist/core/prd-check/verifiers.js +223 -0
  119. package/dist/core/pugi-md/context-injector.js +76 -0
  120. package/dist/core/pugi-md/walk-up.js +207 -0
  121. package/dist/core/release-notes/parser.js +241 -0
  122. package/dist/core/release-notes/state.js +116 -0
  123. package/dist/core/repl/history.js +11 -1
  124. package/dist/core/repl/model-pricing.js +135 -0
  125. package/dist/core/repl/session.js +1897 -37
  126. package/dist/core/repl/slash-commands.js +430 -15
  127. package/dist/core/repl/store/session-store.js +31 -2
  128. package/dist/core/repl/workspace-context.js +22 -0
  129. package/dist/core/repo-map/build.js +125 -0
  130. package/dist/core/repo-map/cache.js +185 -0
  131. package/dist/core/repo-map/extractor.js +254 -0
  132. package/dist/core/repo-map/formatter.js +145 -0
  133. package/dist/core/repo-map/scanner.js +211 -0
  134. package/dist/core/retry-budget/budget.js +284 -0
  135. package/dist/core/retry-budget/index.js +5 -0
  136. package/dist/core/session.js +92 -0
  137. package/dist/core/settings.js +80 -0
  138. package/dist/core/share/formatter.js +271 -0
  139. package/dist/core/share/redactor.js +221 -0
  140. package/dist/core/share/uploader.js +267 -0
  141. package/dist/core/skills/defaults.js +457 -0
  142. package/dist/core/smoke/headless-driver.js +174 -0
  143. package/dist/core/smoke/orchestrator.js +194 -0
  144. package/dist/core/smoke/runner.js +238 -0
  145. package/dist/core/smoke/scenario-parser.js +316 -0
  146. package/dist/core/subagents/dispatcher-real.js +600 -0
  147. package/dist/core/subagents/dispatcher.js +113 -24
  148. package/dist/core/subagents/index.js +18 -5
  149. package/dist/core/subagents/isolation-matrix.js +213 -0
  150. package/dist/core/subagents/spawn.js +19 -4
  151. package/dist/core/telemetry/emitter.js +229 -0
  152. package/dist/core/telemetry/queue.js +251 -0
  153. package/dist/core/theme/context.js +91 -0
  154. package/dist/core/theme/presets.js +228 -0
  155. package/dist/core/theme/state.js +181 -0
  156. package/dist/core/todos/invariant.js +10 -0
  157. package/dist/core/todos/state.js +177 -0
  158. package/dist/core/transport/version-interceptor.js +166 -0
  159. package/dist/core/vim/keymap.js +288 -0
  160. package/dist/core/vim/state.js +92 -0
  161. package/dist/core/worktree-manager/cleanup.js +123 -0
  162. package/dist/core/worktree-manager/manager.js +303 -0
  163. package/dist/index.js +28 -0
  164. package/dist/runtime/bootstrap.js +190 -0
  165. package/dist/runtime/cli.js +3241 -343
  166. package/dist/runtime/commands/cancel.js +231 -0
  167. package/dist/runtime/commands/chain.js +489 -0
  168. package/dist/runtime/commands/codegraph-status.js +227 -0
  169. package/dist/runtime/commands/compact.js +297 -0
  170. package/dist/runtime/commands/cost.js +199 -0
  171. package/dist/runtime/commands/delegate.js +242 -11
  172. package/dist/runtime/commands/dispatch.js +126 -0
  173. package/dist/runtime/commands/doctor.js +412 -0
  174. package/dist/runtime/commands/feedback.js +184 -0
  175. package/dist/runtime/commands/hooks.js +184 -0
  176. package/dist/runtime/commands/lsp.js +368 -0
  177. package/dist/runtime/commands/mcp.js +879 -0
  178. package/dist/runtime/commands/memory.js +508 -0
  179. package/dist/runtime/commands/model.js +237 -0
  180. package/dist/runtime/commands/onboarding.js +275 -0
  181. package/dist/runtime/commands/patch.js +128 -0
  182. package/dist/runtime/commands/permissions.js +112 -0
  183. package/dist/runtime/commands/plan.js +143 -0
  184. package/dist/runtime/commands/prd-check.js +285 -0
  185. package/dist/runtime/commands/redo-blob-store.js +92 -0
  186. package/dist/runtime/commands/redo.js +361 -0
  187. package/dist/runtime/commands/release-notes.js +229 -0
  188. package/dist/runtime/commands/repo-map.js +95 -0
  189. package/dist/runtime/commands/report.js +299 -0
  190. package/dist/runtime/commands/resume.js +118 -0
  191. package/dist/runtime/commands/review-consensus.js +17 -2
  192. package/dist/runtime/commands/rewind.js +333 -0
  193. package/dist/runtime/commands/sessions.js +163 -0
  194. package/dist/runtime/commands/share.js +316 -0
  195. package/dist/runtime/commands/status.js +186 -0
  196. package/dist/runtime/commands/stickers.js +82 -0
  197. package/dist/runtime/commands/style.js +194 -0
  198. package/dist/runtime/commands/theme.js +196 -0
  199. package/dist/runtime/commands/undo.js +32 -0
  200. package/dist/runtime/commands/update.js +289 -0
  201. package/dist/runtime/commands/vim.js +140 -0
  202. package/dist/runtime/commands/worktree.js +177 -0
  203. package/dist/runtime/commands/worktrees.js +155 -0
  204. package/dist/runtime/headless-repl.js +195 -0
  205. package/dist/runtime/headless.js +543 -0
  206. package/dist/runtime/load-hooks-or-exit.js +71 -0
  207. package/dist/runtime/plan-decompose.js +531 -0
  208. package/dist/runtime/version.js +65 -0
  209. package/dist/tools/agent-tool.js +229 -0
  210. package/dist/tools/apply-patch.js +556 -0
  211. package/dist/tools/ask-user-question.js +213 -0
  212. package/dist/tools/ask-user.js +115 -0
  213. package/dist/tools/bash.js +203 -4
  214. package/dist/tools/file-tools.js +85 -14
  215. package/dist/tools/lsp-tools.js +189 -0
  216. package/dist/tools/mcp-tool.js +260 -0
  217. package/dist/tools/multi-edit.js +361 -0
  218. package/dist/tools/powershell.js +268 -0
  219. package/dist/tools/registry.js +51 -0
  220. package/dist/tools/skill-tool.js +96 -0
  221. package/dist/tools/tasks.js +208 -0
  222. package/dist/tools/todo-write.js +184 -0
  223. package/dist/tools/web-fetch.js +147 -2
  224. package/dist/tools/web-search.js +458 -0
  225. package/dist/tui/agent-progress-card.js +111 -0
  226. package/dist/tui/agent-tree.js +10 -0
  227. package/dist/tui/ask-modal.js +2 -2
  228. package/dist/tui/ask-user-question-prompt.js +192 -0
  229. package/dist/tui/compact-banner.js +81 -0
  230. package/dist/tui/conversation-pane.js +82 -8
  231. package/dist/tui/cost-table.js +111 -0
  232. package/dist/tui/doctor-table.js +46 -0
  233. package/dist/tui/feedback-prompt.js +156 -0
  234. package/dist/tui/input-box.js +218 -3
  235. package/dist/tui/markdown-render.js +4 -4
  236. package/dist/tui/onboarding-wizard.js +240 -0
  237. package/dist/tui/permissions-picker.js +86 -0
  238. package/dist/tui/render.js +35 -0
  239. package/dist/tui/repl-render.js +313 -35
  240. package/dist/tui/repl-splash-art.js +1 -1
  241. package/dist/tui/repl-splash-mascot.js +32 -8
  242. package/dist/tui/repl-splash.js +2 -2
  243. package/dist/tui/repl.js +85 -5
  244. package/dist/tui/splash.js +1 -1
  245. package/dist/tui/status-bar.js +94 -16
  246. package/dist/tui/status-table.js +7 -0
  247. package/dist/tui/stickers-art.js +136 -0
  248. package/dist/tui/style-table.js +28 -0
  249. package/dist/tui/theme-table.js +29 -0
  250. package/dist/tui/thinking-spinner.js +123 -0
  251. package/dist/tui/tool-stream-pane.js +52 -3
  252. package/dist/tui/update-banner.js +27 -2
  253. package/dist/tui/vim-input.js +267 -0
  254. package/dist/tui/welcome-banner.js +107 -0
  255. package/dist/tui/welcome-data.js +293 -0
  256. package/docs/examples/codegraph.mcp.json +10 -0
  257. package/package.json +12 -6
  258. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  259. package/test/scenarios/compact-force.scenario.txt +11 -0
  260. package/test/scenarios/identity.scenario.txt +11 -0
  261. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  262. package/test/scenarios/walkback.scenario.txt +12 -0
  263. package/dist/core/engine/compaction-hook.js +0 -154
@@ -0,0 +1,156 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * `/feedback` interactive prompt — Leak L21 (2026-05-27).
4
+ *
5
+ * Five-step wizard mounted from both the top-level `pugi feedback`
6
+ * shell handler and the in-REPL `/feedback` slash. Steps:
7
+ *
8
+ * 1. category — bug / feature / general / praise (numeric pick 1-4)
9
+ * 2. rating — 1-5 stars (numeric pick 1-5)
10
+ * 3. comment — free-text, multi-line, Ctrl-D submits, Esc cancels
11
+ * 4. context — y/n include last 5 turns (redacted)? default no
12
+ * 5. confirm — y/n submit?
13
+ *
14
+ * Brand voice gate: ASCII glyphs only, no em-dashes, no banned brand
15
+ * words. Copy is intentionally power-word neutral so the future i18n
16
+ * landing localises cleanly.
17
+ *
18
+ * The component is PURE in the Ink sense — props in, one terminal
19
+ * `onResolve` event out. The caller owns the submit + queue logic.
20
+ *
21
+ * The verdict the operator submits is:
22
+ * - `{ cancelled: true }` when the operator presses Esc at any step
23
+ * - `{ cancelled: false, draft }` when the wizard completes
24
+ *
25
+ * `draft` is the assembled `FeedbackDraft` — NOT yet a full envelope
26
+ * (the caller still injects `ts` + `cliVersion` + maybe `tier`).
27
+ */
28
+ import { useState } from 'react';
29
+ import { Box, Text, render, useApp, useInput } from 'ink';
30
+ const CATEGORIES = [
31
+ { value: 'bug', label: 'bug', gloss: 'something broke or behaves unexpectedly' },
32
+ { value: 'feature', label: 'feature', gloss: 'request a new capability' },
33
+ { value: 'general', label: 'general', gloss: 'observation, idea, comment' },
34
+ { value: 'praise', label: 'praise', gloss: 'positive note for the team' },
35
+ ];
36
+ export function FeedbackPrompt(props) {
37
+ const [step, setStep] = useState('category');
38
+ const [category, setCategory] = useState(null);
39
+ const [rating, setRating] = useState(null);
40
+ const [commentBuffer, setCommentBuffer] = useState('');
41
+ const [includeContext, setIncludeContext] = useState(false);
42
+ useInput((input, key) => {
43
+ if (key.escape) {
44
+ props.onResolve({ cancelled: true });
45
+ return;
46
+ }
47
+ if (step === 'category') {
48
+ const n = Number.parseInt(input, 10);
49
+ if (!Number.isNaN(n) && n >= 1 && n <= CATEGORIES.length) {
50
+ const picked = CATEGORIES[n - 1];
51
+ if (picked) {
52
+ setCategory(picked.value);
53
+ setStep('rating');
54
+ }
55
+ }
56
+ return;
57
+ }
58
+ if (step === 'rating') {
59
+ const n = Number.parseInt(input, 10);
60
+ if (!Number.isNaN(n) && n >= 1 && n <= 5) {
61
+ setRating(n);
62
+ setStep('comment');
63
+ }
64
+ return;
65
+ }
66
+ if (step === 'comment') {
67
+ // Ctrl-D submits the comment (even when empty — rating-only
68
+ // feedback is allowed). Enter inserts a newline so the
69
+ // operator can write multi-line bug repros without the wizard
70
+ // ending the buffer prematurely.
71
+ if (key.ctrl && (input === 'd' || input === '')) {
72
+ setStep('context');
73
+ return;
74
+ }
75
+ if (key.return) {
76
+ setCommentBuffer((prev) => prev + '\n');
77
+ return;
78
+ }
79
+ if (key.backspace || key.delete) {
80
+ setCommentBuffer((prev) => prev.slice(0, -1));
81
+ return;
82
+ }
83
+ // Printable characters land in the buffer. We accept anything
84
+ // non-empty + non-control. Ink's useInput sometimes delivers
85
+ // multi-char paste bursts as one `input` — we append the whole
86
+ // burst so paste lands intact.
87
+ if (input && !key.ctrl && !key.meta) {
88
+ setCommentBuffer((prev) => prev + input);
89
+ }
90
+ return;
91
+ }
92
+ if (step === 'context') {
93
+ if (input === 'y' || input === 'Y') {
94
+ setIncludeContext(true);
95
+ setStep('confirm');
96
+ return;
97
+ }
98
+ if (input === 'n' || input === 'N' || key.return) {
99
+ // Default no — Enter at the context prompt picks "no" so
100
+ // the operator can blast through with all defaults.
101
+ setIncludeContext(false);
102
+ setStep('confirm');
103
+ return;
104
+ }
105
+ return;
106
+ }
107
+ if (step === 'confirm') {
108
+ if (input === 'y' || input === 'Y' || key.return) {
109
+ if (category != null && rating != null) {
110
+ props.onResolve({
111
+ cancelled: false,
112
+ draft: {
113
+ category,
114
+ rating,
115
+ comment: commentBuffer,
116
+ includeSessionContext: includeContext,
117
+ },
118
+ });
119
+ }
120
+ return;
121
+ }
122
+ if (input === 'n' || input === 'N') {
123
+ props.onResolve({ cancelled: true });
124
+ return;
125
+ }
126
+ return;
127
+ }
128
+ }, { isActive: !props.inert });
129
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Pugi /feedback" }), _jsx(Text, { children: " \u2014 share what you saw. Esc cancels at any step." })] }), step === 'category' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "1. Category" }), CATEGORIES.map((c, idx) => (_jsx(Text, { children: ` ${idx + 1}. ${c.label.padEnd(8)} ${c.gloss}` }, c.value))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Pick 1-4." }) })] })), step === 'rating' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "2. Rating" }), _jsx(Text, { children: ` 1=poor, 5=excellent. Picked category: ${category ?? '?'}` }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Pick 1-5." }) })] })), step === 'comment' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "3. Comment" }), _jsx(Text, { dimColor: true, children: "Multi-line. Enter inserts a newline. Ctrl-D submits." }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: commentBuffer.length === 0 ? (_jsx(Text, { dimColor: true, children: "(empty \u2014 Ctrl-D submits without a comment)" })) : (commentBuffer.split('\n').map((line, idx) => (_jsx(Text, { children: `> ${line}` }, idx)))) })] })), step === 'context' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "4. Include session context?" }), _jsx(Text, { children: ' Last 5 turns, redacted (tokens / *_KEY=* values stripped).' }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "y / n \u2014 default n (Enter)." }) })] })), step === 'confirm' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "5. Submit?" }), _jsx(Text, { children: ` category=${category ?? '?'} rating=${rating ?? '?'} context=${includeContext ? 'yes' : 'no'}` }), _jsx(Text, { children: ` comment=${commentBuffer.length} chars` }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "y / n \u2014 default y (Enter)." }) })] }))] }));
130
+ }
131
+ export async function renderFeedbackPrompt() {
132
+ let resolveOuter;
133
+ const outerPromise = new Promise((resolve) => {
134
+ resolveOuter = resolve;
135
+ });
136
+ function App() {
137
+ const { exit } = useApp();
138
+ return (_jsx(FeedbackPrompt, { onResolve: (verdict) => {
139
+ resolveOuter(verdict);
140
+ // Mirror renderAskCli: tiny delay so Ink flushes unmount
141
+ // before the caller prints the toast line.
142
+ setTimeout(() => exit(), 16);
143
+ } }));
144
+ }
145
+ const instance = render(_jsx(App, {}));
146
+ const verdict = await outerPromise;
147
+ try {
148
+ await instance.waitUntilExit();
149
+ }
150
+ catch {
151
+ // Ink can throw when exit() races with a re-render — the verdict
152
+ // is already captured so we ignore.
153
+ }
154
+ return verdict;
155
+ }
156
+ //# sourceMappingURL=feedback-prompt.js.map
@@ -33,6 +33,14 @@ import { SlashPalette, completePalette, filterPalette, } from './slash-palette.j
33
33
  import { EMPTY_KILL_RING, killToLineEnd, killToLineStart, killWordBackward, yankAtCursor, } from '../core/repl/kill-ring.js';
34
34
  import { readClipboard } from '../core/repl/clipboard-read.js';
35
35
  const CTRL_C_DOUBLE_TAP_MS = 1_000;
36
+ /**
37
+ * Wave 6 BT 8 (Claude Code parity): Esc-Esc walks the conversation back
38
+ * one turn. 500ms is tight enough that an operator clearing the buffer +
39
+ * later changing their mind does NOT accidentally pop a turn, while
40
+ * still feeling like one motion. Matches Claude Code's documented
41
+ * double-Esc window.
42
+ */
43
+ const ESCAPE_DOUBLE_TAP_MS = 500;
36
44
  /** Width subtracted from the terminal width so the border + padding fit. */
37
45
  const FRAME_OVERHEAD_COLUMNS = 4;
38
46
  /** Fallback width when ink cannot read stdout (e.g. test harness). */
@@ -75,6 +83,18 @@ export function InputBox(props) {
75
83
  const [history, setHistory] = useState(seededHistory);
76
84
  const [historyIndex, setHistoryIndex] = useState(-1);
77
85
  const [lastCtrlCAt, setLastCtrlCAt] = useState(undefined);
86
+ // CEO P0 #2 (2026-05-29): Claude Code parity — surface а visible
87
+ // "Press Ctrl+C again to exit" toast on the first Ctrl+C press so
88
+ // the operator knows the second press will terminate the REPL.
89
+ // Auto-clears after CTRL_C_DOUBLE_TAP_MS so it never lingers past
90
+ // the double-tap window.
91
+ const [ctrlCToast, setCtrlCToast] = useState(null);
92
+ const ctrlCToastTimerRef = useRef(null);
93
+ // Wave 6 BT 8: Esc-Esc walkback double-tap window. Tracks the epoch
94
+ // ms of the most recent Esc press so the next Esc within
95
+ // ESCAPE_DOUBLE_TAP_MS triggers the walkback handler instead of
96
+ // re-clearing the buffer.
97
+ const [lastEscapeAt, setLastEscapeAt] = useState(undefined);
78
98
  const [cursorVisible, setCursorVisible] = useState(true);
79
99
  // Ctrl+R / Ctrl+S reverse-search mode. Undefined when idle, a
80
100
  // HistorySearchState while the operator is searching.
@@ -94,6 +114,11 @@ export function InputBox(props) {
94
114
  // panes when Ctrl+L wipes the terminal (the parent React tree is
95
115
  // otherwise stable and would not redraw on a stdout.write alone).
96
116
  const [, setRedrawTick] = useState(0);
117
+ // Wave 7 Shift+Tab toast — flashed for 2s after a mode cycle so the
118
+ // operator sees `Mode → acceptEdits` под the input divider. Cleared
119
+ // by a setTimeout so a quick second Shift+Tab refreshes the toast.
120
+ const [modeCycleToast, setModeCycleToast] = useState(null);
121
+ const modeCycleTimerRef = useRef(null);
97
122
  const now = props.now ?? Date.now;
98
123
  const { stdout } = useStdout();
99
124
  const columns = stdout?.columns ?? FALLBACK_COLUMNS;
@@ -173,6 +198,28 @@ export function InputBox(props) {
173
198
  return;
174
199
  }
175
200
  setLastCtrlCAt(t);
201
+ // CEO P0 #2 (2026-05-29): surface the "Press Ctrl+C again to
202
+ // exit" toast on the first press so the operator sees the
203
+ // double-tap semantics in the UI, not just в the bottom hint
204
+ // line. Mirrors Claude Code's exit affordance verbatim. The
205
+ // toast string varies by which branch fired (cancel vs idle
206
+ // clear) so the operator learns what the press just did:
207
+ //
208
+ // - cancelResult === true → "Aborted. Press Ctrl+C again to exit."
209
+ // - cancelResult === false → "Press Ctrl+C again to exit."
210
+ //
211
+ // (The undefined branch already returned above — а modal owns
212
+ // input и the toast is suppressed.)
213
+ const toastCopy = cancelResult === true
214
+ ? 'Aborted. Press Ctrl+C again to exit.'
215
+ : 'Press Ctrl+C again to exit.';
216
+ setCtrlCToast(toastCopy);
217
+ if (ctrlCToastTimerRef.current)
218
+ clearTimeout(ctrlCToastTimerRef.current);
219
+ ctrlCToastTimerRef.current = setTimeout(() => {
220
+ setCtrlCToast(null);
221
+ ctrlCToastTimerRef.current = null;
222
+ }, CTRL_C_DOUBLE_TAP_MS);
176
223
  // Legacy behaviour: on idle (or no onCancel wired), clear the
177
224
  // buffer + reset search so the operator's screen is calm before
178
225
  // they confirm exit. When we DID cancel a live dispatch, keep
@@ -184,6 +231,24 @@ export function InputBox(props) {
184
231
  }
185
232
  return;
186
233
  }
234
+ // Wave 7 — Claude Code parity: Shift+Tab cycles permission mode.
235
+ // The host owns the cycle logic + persistence; we just intercept
236
+ // the chord and surface a one-line toast on success. Place this
237
+ // BEFORE the search-mode and palette branches so a Shift+Tab fires
238
+ // even while reverse-search is active (operator habit-driven).
239
+ if (key.shift && key.tab && props.onCyclePermissionMode) {
240
+ const nextMode = props.onCyclePermissionMode();
241
+ if (nextMode) {
242
+ setModeCycleToast(`Mode → ${nextMode}`);
243
+ if (modeCycleTimerRef.current)
244
+ clearTimeout(modeCycleTimerRef.current);
245
+ modeCycleTimerRef.current = setTimeout(() => {
246
+ setModeCycleToast(null);
247
+ modeCycleTimerRef.current = null;
248
+ }, 2_000);
249
+ }
250
+ return;
251
+ }
187
252
  // Search-mode key handling. Ctrl+R / Ctrl+S cycle, Enter accepts,
188
253
  // Esc cancels (restoring the pre-search draft), backspace shortens
189
254
  // the query, typed characters extend it.
@@ -202,7 +267,9 @@ export function InputBox(props) {
202
267
  setCursor(draftBeforeSearch.length);
203
268
  return;
204
269
  }
205
- if (key.return) {
270
+ // Bare LF accepts the focused match, same as CR (`key.return`).
271
+ // See the post-search block below for the rationale.
272
+ if (key.return || (input === '\n' && !key.meta && !key.ctrl && !key.shift)) {
206
273
  const picked = currentBrief(search);
207
274
  setSearch(undefined);
208
275
  if (picked !== null) {
@@ -221,6 +288,11 @@ export function InputBox(props) {
221
288
  return;
222
289
  }
223
290
  if (input && !key.meta && !key.ctrl) {
291
+ // Drop a bare LF from the search query — the Enter-accept
292
+ // branch above already handled it; falling through here would
293
+ // splice a newline into the search string.
294
+ if (input === '\n')
295
+ return;
224
296
  const nextQuery = search.query + input;
225
297
  setSearch(applyQuery(search, nextQuery, history));
226
298
  return;
@@ -243,6 +315,118 @@ export function InputBox(props) {
243
315
  setSearch(initialSearchState(history));
244
316
  return;
245
317
  }
318
+ // P0 fix (CEO 2026-05-29 dogfood, second iteration): bare LF (`\n`)
319
+ // MUST submit the brief, same as bare CR (`\r`). Ink's parseKeypress
320
+ // maps `\r` to `key.return` and `\n` to `key.name === 'enter'`
321
+ // WITHOUT setting `key.return`. Most real terminals deliver CR for
322
+ // Enter (ICRNL on by default), so the `key.return` branch below
323
+ // catches them. But when stdin is a PTY whose parent writes raw
324
+ // `\n` (Python's `pty.fork` + `os.write(fd, b"\n")`, automation
325
+ // harnesses, certain SSH multiplexers), the LF arrives as a
326
+ // printable char.
327
+ //
328
+ // PR #697 (beta.45) fixed the case where `input === '\n'` exactly.
329
+ // CEO PTY smoke 2026-05-29 surfaced the REAL shape: when the parent
330
+ // writes the brief AND the Enter as separate `os.write` calls (or
331
+ // even when it doesn't), Node's stdin buffer COALESCES them into
332
+ // ONE chunk before Ink delivers the `useInput` event. The repro
333
+ // confirmed via stderr instrumentation: typing `hi\n` arrives in
334
+ // input-box as `bytes=[68 69 0a] len=3 flags=-` — a SINGLE 3-char
335
+ // chunk "hi\n" with no key flags. The PR #697 branch (`input ===
336
+ // '\n'`) does not match, so `hi\n` falls through to the printable-
337
+ // char branch and the literal newline lands in the buffer as
338
+ // `› hi\n █` (multi-line composer, brief never dispatches, status
339
+ // stays `idle` forever).
340
+ //
341
+ // Fix: detect a TRAILING `\n` in a printable chunk with no
342
+ // modifiers — type the prefix into the buffer, then submit. The
343
+ // discriminator that keeps multi-line paste working: the chunk
344
+ // must contain EXACTLY ONE `\n` (the trailing one) and no other
345
+ // newlines. Multi-line pastes have ≥2 `\n` characters (or arrive
346
+ // wrapped in bracketed-paste markers handled below), so they
347
+ // still preserve interior newlines via the printable-char branch.
348
+ //
349
+ // Detection contract:
350
+ // - `input` ends with `\n`
351
+ // - no Ctrl / Meta / Shift modifiers
352
+ // - exactly ONE `\n` in the chunk (the trailing one)
353
+ // - chunk is not bracketed-paste wrapped (markers stripped below)
354
+ //
355
+ // Edge cases covered by `test/input-box-lf-submit.spec.tsx`:
356
+ // - bare `\n` → submit empty (no-op on empty buf)
357
+ // - `hi\n` → splice `hi` + submit
358
+ // - `hi\nthere\n` (multi-line) → printable branch, preserves \n
359
+ // - `\r` (CR) → key.return branch unchanged
360
+ // - `hi\r\n` (CRLF) → key.return branch (CR wins first)
361
+ const endsWithLf = input.length > 0 && input.charCodeAt(input.length - 1) === 0x0a;
362
+ const newlineCount = (input.match(/\n/g) || []).length;
363
+ if (endsWithLf
364
+ && newlineCount === 1
365
+ && !key.meta
366
+ && !key.ctrl
367
+ && !key.shift) {
368
+ // Splice the prefix (everything before the trailing `\n`) into
369
+ // the buffer at the cursor, then run the canonical submit path.
370
+ // Refs (cursorRef / lineRef) hold the latest committed values so
371
+ // the splice runs against the operator's most recent edits even
372
+ // if a previous async paste / setState is still mid-flight.
373
+ const prefix = input.slice(0, -1);
374
+ let mergedLine = lineRef.current;
375
+ let mergedCursor = cursorRef.current;
376
+ if (prefix.length > 0) {
377
+ // Same sanitisation as the printable-char branch below — strip
378
+ // bracketed-paste markers so a stray escape sequence never
379
+ // lands in the submitted brief.
380
+ const stripped = prefix
381
+ .replace(/\x1b\[200~/g, '')
382
+ .replace(/\x1b\[201~/g, '')
383
+ .replace(/\[200~/g, '')
384
+ .replace(/\[201~/g, '');
385
+ if (stripped.length > 0) {
386
+ mergedLine =
387
+ mergedLine.slice(0, mergedCursor) + stripped + mergedLine.slice(mergedCursor);
388
+ mergedCursor = mergedCursor + stripped.length;
389
+ }
390
+ }
391
+ // Synthesise the same payload-shape the `key.return` branch
392
+ // below uses so palette completion + history dedup + onSubmit
393
+ // dispatch all run identically.
394
+ const paletteHere = !paletteSuppressed
395
+ ? filterPalette(mergedLine)
396
+ : { rows: [], totalBeforeLimit: 0 };
397
+ const paletteOpenHere = paletteHere.rows.length > 0;
398
+ const paletteFocusedIndexHere = paletteHere.rows.length === 0
399
+ ? 0
400
+ : Math.min(paletteIndex, paletteHere.rows.length - 1);
401
+ let payload = mergedLine;
402
+ if (paletteOpenHere) {
403
+ const completed = completePalette(mergedLine, paletteHere.rows, paletteFocusedIndexHere);
404
+ if (completed !== null)
405
+ payload = completed;
406
+ }
407
+ const trimmed = payload.trim();
408
+ if (trimmed.length > 0) {
409
+ setHistory((prev) => {
410
+ if (prev[prev.length - 1] === trimmed)
411
+ return prev;
412
+ return [...prev, trimmed];
413
+ });
414
+ setHistoryIndex(-1);
415
+ if (props.workspaceSlug) {
416
+ appendHistory({
417
+ home: props.historyHome,
418
+ workspaceSlug: props.workspaceSlug,
419
+ brief: trimmed,
420
+ });
421
+ }
422
+ props.onSubmit(trimmed);
423
+ }
424
+ setLine('');
425
+ setCursor(0);
426
+ setPaletteSuppressed(false);
427
+ setPaletteIndex(0);
428
+ return;
429
+ }
246
430
  // Readline-style kill ring shortcuts. All four kills push the
247
431
  // removed slice onto the ring; Ctrl+Y yanks the most recent.
248
432
  if (key.ctrl && input === 'u') {
@@ -375,10 +559,41 @@ export function InputBox(props) {
375
559
  if (key.escape) {
376
560
  if (paletteOpen) {
377
561
  // Close the palette without clearing the buffer so the operator
378
- // can still send `/help` as plain text if they want.
562
+ // can still send `/help` as plain text if they want. Palette
563
+ // takes precedence over walkback because the operator's mental
564
+ // model is "Esc closes the visible overlay first".
379
565
  setPaletteSuppressed(true);
566
+ setLastEscapeAt(undefined);
567
+ return;
568
+ }
569
+ // Wave 6 BT 8: Esc-Esc walkback. Two presses within
570
+ // ESCAPE_DOUBLE_TAP_MS step the conversation back by one turn.
571
+ // First press still clears the buffer (legacy behaviour for the
572
+ // single-Esc cancel UX); the second press calls the host's
573
+ // walkback handler. Buffer-clear on the first press is what makes
574
+ // the double-tap feel "free" - the operator did not have to
575
+ // memorise a new chord; they just have to keep pressing.
576
+ const tEsc = now();
577
+ const withinEscapeWindow = typeof lastEscapeAt === 'number'
578
+ && tEsc - lastEscapeAt <= ESCAPE_DOUBLE_TAP_MS;
579
+ if (withinEscapeWindow && props.onWalkback) {
580
+ // Second tap inside the window. Buffer was already cleared on
581
+ // the first press, so the host sees a clean input box AND the
582
+ // walkback result. We clear the window so a third tap restarts
583
+ // the cycle (no run-on walkbacks from a stuck Esc key).
584
+ const verdict = props.onWalkback();
585
+ setLastEscapeAt(undefined);
586
+ if (verdict !== 'walked-back') {
587
+ // Host refused (dispatch in flight, no turns to pop). The
588
+ // host owns the refusal copy via its own writeOutput path;
589
+ // we do not double-message here.
590
+ return;
591
+ }
380
592
  return;
381
593
  }
594
+ // First Esc (or no walkback wired). Arm the window + clear the
595
+ // buffer per the long-standing single-Esc cancel contract.
596
+ setLastEscapeAt(tEsc);
382
597
  setLine('');
383
598
  setCursor(0);
384
599
  setHistoryIndex(-1);
@@ -499,7 +714,7 @@ export function InputBox(props) {
499
714
  : Math.min(paletteIndex, paletteView.rows.length - 1);
500
715
  const divider = '─'.repeat(innerWidth);
501
716
  const focusedMatch = search ? search.matches[search.focusedIndex] : undefined;
502
- 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 abort / ×2 exit' }) })] }));
717
+ 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 }), modeCycleToast ? (_jsx(Box, { children: _jsx(Text, { color: "#3da9fc", bold: true, children: ` ${modeCycleToast}` }) })) : null, ctrlCToast ? (_jsx(Box, { children: _jsx(Text, { color: "yellow", bold: true, children: ` ${ctrlCToast}` }) })) : null, 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 · Shift+Tab mode · Enter brief · Esc cancel · Ctrl+C abort / ×2 exit' }) })] }));
503
718
  }
504
719
  /**
505
720
  * 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: "cyan", children: '• ' }), _jsx(Text, { children: renderInline(block.text) })] }, key));
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: "cyan", children: `${block.index}. ` }), _jsx(Text, { children: renderInline(block.text) })] }, key));
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: "cyan", bold: true, children: tok }, key));
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: "cyan", underline: true, children: span.text }), _jsx(Text, { dimColor: true, children: ` (${span.url})` })] }, key));
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