@bahulam/code 0.1.1

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 (278) hide show
  1. package/README.md +93 -0
  2. package/package.json +56 -0
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/benchmark/route.ts +113 -0
  6. package/pulse/app/api/benchmarks/route.ts +195 -0
  7. package/pulse/app/api/costs/route.ts +88 -0
  8. package/pulse/app/api/export/route.ts +77 -0
  9. package/pulse/app/api/history/route.ts +11 -0
  10. package/pulse/app/api/import/route.ts +31 -0
  11. package/pulse/app/api/memory/route.ts +50 -0
  12. package/pulse/app/api/plans/route.ts +9 -0
  13. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  14. package/pulse/app/api/projects/route.ts +121 -0
  15. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  16. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  17. package/pulse/app/api/sessions/route.ts +112 -0
  18. package/pulse/app/api/settings/route.ts +14 -0
  19. package/pulse/app/api/stats/route.ts +143 -0
  20. package/pulse/app/api/todos/route.ts +9 -0
  21. package/pulse/app/api/tools/route.ts +160 -0
  22. package/pulse/app/benchmarks/page.tsx +224 -0
  23. package/pulse/app/costs/page.tsx +179 -0
  24. package/pulse/app/export/page.tsx +465 -0
  25. package/pulse/app/favicon.ico +0 -0
  26. package/pulse/app/globals.css +263 -0
  27. package/pulse/app/help/page.tsx +143 -0
  28. package/pulse/app/history/page.tsx +157 -0
  29. package/pulse/app/layout.tsx +46 -0
  30. package/pulse/app/memory/page.tsx +365 -0
  31. package/pulse/app/overview-client.tsx +393 -0
  32. package/pulse/app/page.tsx +14 -0
  33. package/pulse/app/plans/page.tsx +308 -0
  34. package/pulse/app/projects/[slug]/page.tsx +390 -0
  35. package/pulse/app/projects/page.tsx +110 -0
  36. package/pulse/app/sessions/[id]/page.tsx +243 -0
  37. package/pulse/app/sessions/page.tsx +39 -0
  38. package/pulse/app/settings/page.tsx +188 -0
  39. package/pulse/app/todos/page.tsx +211 -0
  40. package/pulse/app/tools/page.tsx +249 -0
  41. package/pulse/cli.js +164 -0
  42. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  43. package/pulse/components/activity/streak-card.tsx +36 -0
  44. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  45. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  46. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  47. package/pulse/components/costs/model-token-table.tsx +60 -0
  48. package/pulse/components/global-search.tsx +193 -0
  49. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  50. package/pulse/components/layout/bottom-nav.tsx +53 -0
  51. package/pulse/components/layout/client-layout.tsx +31 -0
  52. package/pulse/components/layout/sidebar-context.tsx +50 -0
  53. package/pulse/components/layout/sidebar.tsx +183 -0
  54. package/pulse/components/layout/top-bar.tsx +121 -0
  55. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  56. package/pulse/components/overview/conversation-table.tsx +148 -0
  57. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  58. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  59. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  60. package/pulse/components/overview/stat-card.tsx +102 -0
  61. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  62. package/pulse/components/projects/project-card.tsx +175 -0
  63. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  64. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  65. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  66. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  67. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  68. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  69. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  70. package/pulse/components/sessions/session-badges.tsx +49 -0
  71. package/pulse/components/sessions/session-table.tsx +299 -0
  72. package/pulse/components/theme-provider.tsx +44 -0
  73. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  74. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  75. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  76. package/pulse/components/tools/version-history-table.tsx +32 -0
  77. package/pulse/components/ui/alert.tsx +66 -0
  78. package/pulse/components/ui/badge.tsx +48 -0
  79. package/pulse/components/ui/breadcrumb.tsx +109 -0
  80. package/pulse/components/ui/button.tsx +64 -0
  81. package/pulse/components/ui/calendar.tsx +220 -0
  82. package/pulse/components/ui/card.tsx +92 -0
  83. package/pulse/components/ui/command.tsx +158 -0
  84. package/pulse/components/ui/dialog.tsx +158 -0
  85. package/pulse/components/ui/input.tsx +21 -0
  86. package/pulse/components/ui/popover.tsx +89 -0
  87. package/pulse/components/ui/progress.tsx +31 -0
  88. package/pulse/components/ui/select.tsx +190 -0
  89. package/pulse/components/ui/separator.tsx +28 -0
  90. package/pulse/components/ui/sheet.tsx +143 -0
  91. package/pulse/components/ui/skeleton.tsx +13 -0
  92. package/pulse/components/ui/table.tsx +116 -0
  93. package/pulse/components/ui/tabs.tsx +91 -0
  94. package/pulse/components/ui/tooltip.tsx +57 -0
  95. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  96. package/pulse/components.json +23 -0
  97. package/pulse/eslint.config.mjs +18 -0
  98. package/pulse/lib/bahulam-paths.ts +23 -0
  99. package/pulse/lib/claude-reader.ts +592 -0
  100. package/pulse/lib/decode.ts +129 -0
  101. package/pulse/lib/pricing.ts +102 -0
  102. package/pulse/lib/replay-parser.ts +165 -0
  103. package/pulse/lib/tool-categories.ts +127 -0
  104. package/pulse/lib/utils.ts +6 -0
  105. package/pulse/next-env.d.ts +6 -0
  106. package/pulse/next.config.ts +16 -0
  107. package/pulse/package.json +45 -0
  108. package/pulse/postcss.config.mjs +7 -0
  109. package/pulse/public/activity.png +0 -0
  110. package/pulse/public/cc-lens.png +0 -0
  111. package/pulse/public/command-k.png +0 -0
  112. package/pulse/public/costs.png +0 -0
  113. package/pulse/public/dashboard-dark.png +0 -0
  114. package/pulse/public/dashboard-white.png +0 -0
  115. package/pulse/public/export.png +0 -0
  116. package/pulse/public/file.svg +1 -0
  117. package/pulse/public/globe.svg +1 -0
  118. package/pulse/public/next.svg +1 -0
  119. package/pulse/public/projects.png +0 -0
  120. package/pulse/public/session-chat.png +0 -0
  121. package/pulse/public/todos.png +0 -0
  122. package/pulse/public/tools.png +0 -0
  123. package/pulse/public/vercel.svg +1 -0
  124. package/pulse/public/window.svg +1 -0
  125. package/pulse/tsconfig.json +34 -0
  126. package/pulse/types/claude.ts +294 -0
  127. package/src/agents/loader.mjs +94 -0
  128. package/src/agents/multi_workflow_loader.mjs +330 -0
  129. package/src/agents/parser.mjs +205 -0
  130. package/src/agents/scaffold.mjs +223 -0
  131. package/src/agents/teams.mjs +123 -0
  132. package/src/agents/workflow_loader.mjs +122 -0
  133. package/src/agents/workflow_scaffold.mjs +249 -0
  134. package/src/auth/oauth.mjs +220 -0
  135. package/src/auth/tarang-auth.mjs +312 -0
  136. package/src/commands/agent.mjs +221 -0
  137. package/src/commands/workflow.mjs +581 -0
  138. package/src/config/cli-args.mjs +202 -0
  139. package/src/config/env.mjs +263 -0
  140. package/src/config/hook-runner.mjs +100 -0
  141. package/src/config/memory-loader.mjs +32 -0
  142. package/src/config/model-catalog.mjs +57 -0
  143. package/src/config/settings-loader.mjs +45 -0
  144. package/src/config/settings.mjs +132 -0
  145. package/src/context/ast-parser.mjs +298 -0
  146. package/src/context/bm25.mjs +85 -0
  147. package/src/context/prose-chunker.mjs +255 -0
  148. package/src/context/retriever.mjs +425 -0
  149. package/src/context/skeleton.mjs +134 -0
  150. package/src/context/symbol-indexer.mjs +375 -0
  151. package/src/core/agent-history.mjs +111 -0
  152. package/src/core/agent-loop.mjs +486 -0
  153. package/src/core/approval-log.mjs +145 -0
  154. package/src/core/approval.mjs +700 -0
  155. package/src/core/attachments.mjs +666 -0
  156. package/src/core/backend-url.mjs +68 -0
  157. package/src/core/bundled-runtime.mjs +418 -0
  158. package/src/core/cache-control.mjs +92 -0
  159. package/src/core/cache.mjs +105 -0
  160. package/src/core/callback-client.mjs +180 -0
  161. package/src/core/checkpoints.mjs +142 -0
  162. package/src/core/compact-history.mjs +127 -0
  163. package/src/core/context-envelope.mjs +54 -0
  164. package/src/core/context-manager.mjs +198 -0
  165. package/src/core/error-guidance.mjs +331 -0
  166. package/src/core/file-diff.mjs +217 -0
  167. package/src/core/headless.mjs +460 -0
  168. package/src/core/hooks-manager.mjs +87 -0
  169. package/src/core/jsonl-writer.mjs +449 -0
  170. package/src/core/local-agent.mjs +538 -0
  171. package/src/core/local-store.mjs +836 -0
  172. package/src/core/mode-selector.mjs +51 -0
  173. package/src/core/output-filter.mjs +177 -0
  174. package/src/core/paths.mjs +190 -0
  175. package/src/core/policy-resolver.mjs +156 -0
  176. package/src/core/pricing.mjs +336 -0
  177. package/src/core/project-artifacts.mjs +39 -0
  178. package/src/core/project-context-loader.mjs +139 -0
  179. package/src/core/providers.mjs +219 -0
  180. package/src/core/rate-limit-display.mjs +121 -0
  181. package/src/core/rate-limiter.mjs +119 -0
  182. package/src/core/resume-mode.mjs +192 -0
  183. package/src/core/risk-tier.mjs +388 -0
  184. package/src/core/safety.mjs +260 -0
  185. package/src/core/scheduler.mjs +173 -0
  186. package/src/core/session-manager.mjs +360 -0
  187. package/src/core/session.mjs +143 -0
  188. package/src/core/settings-sync.mjs +85 -0
  189. package/src/core/stagnation.mjs +57 -0
  190. package/src/core/stream-client.mjs +957 -0
  191. package/src/core/streaming.mjs +182 -0
  192. package/src/core/system-prompt.mjs +140 -0
  193. package/src/core/tasks.mjs +196 -0
  194. package/src/core/tool-executor.mjs +2231 -0
  195. package/src/core/trust.mjs +160 -0
  196. package/src/core/work-scope.mjs +248 -0
  197. package/src/hooks/engine.mjs +162 -0
  198. package/src/mcp/client.mjs +253 -0
  199. package/src/mcp/transport-shttp.mjs +130 -0
  200. package/src/mcp/transport-sse.mjs +131 -0
  201. package/src/mcp/transport-ws.mjs +134 -0
  202. package/src/onboarding/preflight.mjs +374 -0
  203. package/src/permissions/checker.mjs +57 -0
  204. package/src/permissions/command-classifier.mjs +700 -0
  205. package/src/permissions/injection-check.mjs +60 -0
  206. package/src/permissions/path-check.mjs +102 -0
  207. package/src/permissions/prompt.mjs +73 -0
  208. package/src/permissions/sandbox.mjs +112 -0
  209. package/src/plugins/loader.mjs +138 -0
  210. package/src/skills/installer.mjs +188 -0
  211. package/src/skills/loader.mjs +252 -0
  212. package/src/skills/runner.mjs +55 -0
  213. package/src/state/orbit.mjs +263 -0
  214. package/src/state/verbosity.mjs +99 -0
  215. package/src/telemetry/index.mjs +122 -0
  216. package/src/terminal/agents.mjs +353 -0
  217. package/src/terminal/analytics.mjs +292 -0
  218. package/src/terminal/ansi.mjs +695 -0
  219. package/src/terminal/init.mjs +145 -0
  220. package/src/terminal/main.mjs +310 -0
  221. package/src/terminal/repl-ask-form.mjs +120 -0
  222. package/src/terminal/repl-explore.mjs +44 -0
  223. package/src/terminal/repl-format.mjs +317 -0
  224. package/src/terminal/repl-model-form.mjs +132 -0
  225. package/src/terminal/repl-render.mjs +833 -0
  226. package/src/terminal/repl-resume.mjs +640 -0
  227. package/src/terminal/repl-state.mjs +120 -0
  228. package/src/terminal/repl-utils.mjs +34 -0
  229. package/src/terminal/repl.mjs +5032 -0
  230. package/src/terminal/skills.mjs +54 -0
  231. package/src/terminal/tool-display.mjs +392 -0
  232. package/src/tools/agent.mjs +137 -0
  233. package/src/tools/ask-user.mjs +61 -0
  234. package/src/tools/bash.mjs +231 -0
  235. package/src/tools/cron-create.mjs +120 -0
  236. package/src/tools/cron-delete.mjs +49 -0
  237. package/src/tools/cron-list.mjs +37 -0
  238. package/src/tools/edit.mjs +82 -0
  239. package/src/tools/enter-worktree.mjs +69 -0
  240. package/src/tools/exit-worktree.mjs +57 -0
  241. package/src/tools/glob.mjs +117 -0
  242. package/src/tools/grep.mjs +129 -0
  243. package/src/tools/lint.mjs +71 -0
  244. package/src/tools/ls.mjs +58 -0
  245. package/src/tools/lsp.mjs +115 -0
  246. package/src/tools/multi-edit.mjs +94 -0
  247. package/src/tools/notebook-edit.mjs +96 -0
  248. package/src/tools/project-overview.mjs +703 -0
  249. package/src/tools/read-mcp-resource.mjs +57 -0
  250. package/src/tools/read.mjs +138 -0
  251. package/src/tools/registry.mjs +116 -0
  252. package/src/tools/remote-trigger.mjs +84 -0
  253. package/src/tools/send-message.mjs +64 -0
  254. package/src/tools/skill.mjs +52 -0
  255. package/src/tools/test-runner.mjs +49 -0
  256. package/src/tools/todo-write.mjs +68 -0
  257. package/src/tools/tool-search.mjs +77 -0
  258. package/src/tools/web-fetch.mjs +65 -0
  259. package/src/tools/web-search.mjs +89 -0
  260. package/src/tools/write.mjs +55 -0
  261. package/src/ui/approval.mjs +510 -0
  262. package/src/ui/banner.mjs +232 -0
  263. package/src/ui/commands.mjs +537 -0
  264. package/src/ui/formatter.mjs +409 -0
  265. package/src/ui/icons.mjs +170 -0
  266. package/src/ui/input-dock.mjs +772 -0
  267. package/src/ui/markdown.mjs +278 -0
  268. package/src/ui/mission-report.mjs +296 -0
  269. package/src/ui/palette.mjs +189 -0
  270. package/src/ui/render-queue.mjs +500 -0
  271. package/src/ui/slash-commands.mjs +257 -0
  272. package/src/ui/spinner.mjs +116 -0
  273. package/src/ui/sub-agent.mjs +167 -0
  274. package/src/ui/term.mjs +174 -0
  275. package/src/ui/text-layout.mjs +127 -0
  276. package/src/ui/tool-card.mjs +740 -0
  277. package/src/ui/tool-details.mjs +504 -0
  278. package/src/ui/transcript-block.mjs +20 -0
@@ -0,0 +1,510 @@
1
+ /**
2
+ * Approval prompt UI — Mission Control (PRD-055 §8.2).
3
+ *
4
+ * ── ⚠ APPROVAL · SHELL-MEDIUM · shell ───────────────────────────────
5
+ * ⚙️ Running git add package.json && git status --short
6
+ * Decision
7
+ * ▸ [y] approve once run this call
8
+ * [t] always allow auto-approve future calls to this tool
9
+ * [n] cancel do not run
10
+ * ↑↓ move · Enter pick · letter shortcut · Esc cancel
11
+ *
12
+ * The rule colour is `brand.accent` (magenta) for explicit-approval
13
+ * tiers; safe-default prompts use `brand.data` so they read as advisory.
14
+ *
15
+ * Pure — caller writes the returned string to stderr.
16
+ */
17
+
18
+ import { paint, width as visibleWidth } from './palette.mjs';
19
+ import { icon } from './icons.mjs';
20
+ import { shellCommandDisplay, shellCommandProfile, toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
21
+ import { label as tierLabel, requiresExplicitApproval, TIERS } from '../core/risk-tier.mjs';
22
+ import { isSensitiveConfigPath } from '../core/safety.mjs';
23
+
24
+ /**
25
+ * Default option set per tier. Caller can override via `opts.options`.
26
+ *
27
+ * Each option is `{ key, label, value, hint? }`:
28
+ * key — single-letter shortcut (case-insensitive)
29
+ * label — text shown in the menu
30
+ * value — return value from the menu loop
31
+ * hint — secondary description shown to the right of the label
32
+ */
33
+ export function defaultOptions(tier, { tool = '', args = {} } = {}) {
34
+ const approve = { key: 'y', label: 'approve once', value: 'approve', hint: 'run this call' };
35
+ const cancel = { key: 'n', label: 'cancel', value: 'reject', hint: 'do not run' };
36
+ if (requiresExplicitApproval(tier)) {
37
+ return [approve, cancel];
38
+ }
39
+ if (tool === 'shell') {
40
+ return [
41
+ approve,
42
+ { key: 't', label: 'allow similar', value: 'allow-session', hint: `auto-approve ${shellTrustHint(args)} this session` },
43
+ cancel,
44
+ ];
45
+ }
46
+ return [
47
+ approve,
48
+ { key: 't', label: 'always allow', value: 'allow-type', hint: 'auto-approve future calls to this tool' },
49
+ cancel,
50
+ ];
51
+ }
52
+
53
+ /**
54
+ * Render the compact horizontal prompt with arrow-navigable options.
55
+ *
56
+ * ── ⚠ APPROVAL · SHELL-MEDIUM · shell ───────────────────────────────
57
+ * ⚙️ Running rm -rf node_modules && npm install
58
+ * Decision
59
+ * ▸ [y] approve once
60
+ * [t] always allow
61
+ * [n] cancel
62
+ * ↑↓ move · Enter pick · letter shortcut · Esc cancel
63
+ * ────────────────────────────────────────────────────────────────────
64
+ *
65
+ * @param {object} opts
66
+ * @param {string} opts.tool
67
+ * @param {object} opts.args
68
+ * @param {string} opts.tier
69
+ * @param {string} [opts.why]
70
+ * @param {number} [opts.width]
71
+ * @param {Array} [opts.options] — override default option set
72
+ * @param {number} [opts.selected] — index of the highlighted option
73
+ */
74
+ export function renderApprovalPrompt({
75
+ tool, args = {}, tier, why = '', width,
76
+ options, selected = 0, showDetails = false,
77
+ } = {}) {
78
+ const cols = Math.max(60, Math.min(width || process.stderr.columns || 96, 120));
79
+ const explicit = requiresExplicitApproval(tier);
80
+ const accent = explicit ? paint.brand.accent : paint.brand.data;
81
+ const opts = options || defaultOptions(tier, { tool, args });
82
+ const title = `⚠ ${approvalTitle(tier)} · ${tierLabel(tier)} · ${tool || 'tool'}`;
83
+
84
+ const lines = [
85
+ blockHeader(title, accent),
86
+ ...subjectRows(tool, args, cols, accent),
87
+ ...detailRows(tool, args, cols, accent, showDetails),
88
+ ...riskRows(tool, args, tier, accent),
89
+ ...reasonRows(tool, args, why, cols, accent),
90
+ blockLine(accent),
91
+ ...decisionRows(opts, selected, accent),
92
+ blockLine(accent, paint.text.dim(approvalFooter(tool, showDetails))),
93
+ ];
94
+
95
+ return '\n' + lines.join('\n');
96
+ }
97
+
98
+ export function renderApprovalDockPrompt({
99
+ tool, args = {}, tier, why = '', width,
100
+ options, selected = 0, showDetails = false,
101
+ } = {}) {
102
+ const cols = Math.max(60, Math.min(width || process.stderr.columns || 96, 120));
103
+ const opts = options || defaultOptions(tier, { tool, args });
104
+ // Multi-line shell/python scripts auto-expand: the user cannot approve
105
+ // what they cannot see. "shell script · 5 lines · 309 B" as the only
106
+ // subject made blind approval the default. After execution the script
107
+ // collapses back to the one-line tool card (details stay on /last).
108
+ const isScriptCommand = tool === 'shell'
109
+ && /\n/.test(String(args.command || args.cmd || ''));
110
+ const detailView = showDetails || isScriptCommand;
111
+ const subject = approvalDockSubject(tool, args, cols, detailView);
112
+ const risks = riskTerms(tool, args, tier);
113
+ const reason = compactReason(tool, args, why);
114
+ const lines = [
115
+ ...approvalDockSubjectRows(subject),
116
+ ...(risks.length ? [`${paint.text.dim('risk ')}${paint.state.warn(risks.join(', '))}`] : []),
117
+ ...(reason ? [`${paint.text.dim('reason ')}${paint.text.primary(truncate(reason, 120))}`] : []),
118
+ paint.text.dim('Decision'),
119
+ ...opts.map((option, index) => optionToken(option, index === selected, explicitAccent(tier))),
120
+ ];
121
+
122
+ // Show the WHOLE script when it fits — a partial script (…lines cut at
123
+ // the top) makes blind approval the default. Cap at half the terminal
124
+ // so the dock never eats the whole screen. Non-detail approvals keep
125
+ // the tight 8-row cap.
126
+ const termRows = Math.max(12, Number(process.stderr.rows) || 24);
127
+ const detailCap = Math.max(12, Math.floor(termRows / 2));
128
+ const maxRows = detailView ? Math.min(detailCap, Math.max(12, lines.length + 1)) : 8;
129
+
130
+ return {
131
+ prefix: '? approve › ',
132
+ value: truncateForDock(subject, detailView ? 1200 : 220),
133
+ context: `${approvalTitle(tier)} · ${tierLabel(tier)} · ${tool || 'tool'}`,
134
+ meta: '',
135
+ tips: approvalFooter(tool, detailView),
136
+ lines,
137
+ maxRows,
138
+ };
139
+ }
140
+
141
+ function shellTrustHint(args = {}) {
142
+ const display = shellCommandDisplay(args.command || args.cmd || '');
143
+ const parts = String(display.command || '').trim().split(/\s+/).filter(Boolean);
144
+ const shape = parts.slice(0, 2).join(' ');
145
+ return shape ? `${shape}*` : 'similar shell commands';
146
+ }
147
+
148
+ export function renderTrustedApproval({ tool, args = {}, scope = 'session', ruleId = '', delaySeconds = 0 } = {}) {
149
+ const summary = approvalSubjectSummary(tool, args);
150
+ const subject = `${tool || 'tool'}${summary ? ` "${truncate(summary, 80)}"` : ''}`;
151
+ const rule = ruleId ? ` · rule ${ruleId}` : '';
152
+ const delay = delaySeconds > 0 ? ` · auto-approved after ${delaySeconds}s` : '';
153
+ return ` ${paint.state.success('✓')} ${paint.text.primary(subject)} ${paint.text.dim(`· pre-approved (${String(scope || 'session').toLowerCase()}${rule})${delay}`)}\n`;
154
+ }
155
+
156
+ function truncate(text, n) {
157
+ const s = String(text || '');
158
+ if (s.length <= n) return s;
159
+ return s.slice(0, Math.max(0, n - 1)) + '…';
160
+ }
161
+
162
+ // ── Compatibility wrapper ──────────────────────────────────────────────
163
+
164
+ /**
165
+ * Render the unified approval prompt. Kept as a named export for older call
166
+ * sites/tests that imported the previous inline renderer.
167
+ */
168
+ export function renderInlinePrompt({ tool, args = {}, tier, why = '' } = {}) {
169
+ return renderApprovalPrompt({ tool, args, tier, why });
170
+ }
171
+
172
+ export { TIERS };
173
+
174
+ function tierTitle(tier) {
175
+ switch (tier) {
176
+ case TIERS.SENSITIVE_READ: return 'SENSITIVE';
177
+ case TIERS.PROTECTED_EDIT: return 'PROTECTED';
178
+ case TIERS.SHELL_DANGEROUS: return 'DANGEROUS';
179
+ case TIERS.DESTRUCTIVE: return 'DESTRUCTIVE';
180
+ case TIERS.SHELL_MEDIUM: return 'MEDIUM';
181
+ case TIERS.NETWORK: return 'NETWORK';
182
+ case TIERS.LOCAL_EDIT: return 'EDIT';
183
+ case TIERS.SHELL_SAFE: return 'SAFE';
184
+ case TIERS.READ: return 'READ';
185
+ default: return tierLabel(tier);
186
+ }
187
+ }
188
+
189
+ function approvalTitle(tier) {
190
+ switch (tier) {
191
+ case TIERS.SENSITIVE_READ:
192
+ case TIERS.PROTECTED_EDIT:
193
+ case TIERS.SHELL_DANGEROUS:
194
+ case TIERS.DESTRUCTIVE:
195
+ return tierTitle(tier);
196
+ default:
197
+ return 'APPROVAL';
198
+ }
199
+ }
200
+
201
+ function blockHeader(title, accent) {
202
+ return ` ${paint.bold(accent(title))}`;
203
+ }
204
+
205
+ function blockLine(accent, text = '') {
206
+ return text ? ` ${text}` : ' ';
207
+ }
208
+
209
+ function explicitAccent(tier) {
210
+ return requiresExplicitApproval(tier) ? paint.brand.accent : paint.brand.data;
211
+ }
212
+
213
+ function subjectRows(tool, args, cols, accent) {
214
+ const rows = [];
215
+ const available = Math.max(24, cols - 5);
216
+ const summary = toolDisplaySummary(tool, args, {});
217
+ const label = subjectLabel(tool);
218
+ const details = subjectDetails(tool, args, summary, Math.max(24, available - visibleWidth(label) - 1));
219
+
220
+ if (details.length === 1 && visibleWidth(`${label} ${details[0]}`) <= available) {
221
+ rows.push(blockLine(accent, `${label} ${paint.text.primary(details[0])}`));
222
+ return rows;
223
+ }
224
+
225
+ rows.push(blockLine(accent, label));
226
+ for (const line of subjectDetails(tool, args, summary, Math.max(24, available - 2))) {
227
+ rows.push(blockLine(accent, `${paint.text.dim(' ')}${paint.text.primary(line)}`));
228
+ }
229
+ return rows;
230
+ }
231
+
232
+ function subjectLabel(tool) {
233
+ const label = toolDisplayLabel(tool);
234
+ if (tool === 'shell') {
235
+ return `${paint.text.dim('• shell ·')} ${paint.text.primary(label)}`;
236
+ }
237
+ return `${icon(tool)} ${paint.text.primary(label)}`;
238
+ }
239
+
240
+ function decisionRows(opts, selected, accent) {
241
+ const rows = [blockLine(accent, paint.text.dim('Decision'))];
242
+ for (let i = 0; i < opts.length; i++) {
243
+ rows.push(blockLine(accent, optionToken(opts[i], i === selected, accent)));
244
+ }
245
+ return rows;
246
+ }
247
+
248
+ function reasonRows(tool, args, why, cols, accent) {
249
+ const reason = compactReason(tool, args, why);
250
+ if (!reason) return [];
251
+ const label = paint.text.dim('reason ');
252
+ const firstWidth = Math.max(20, cols - 5 - visibleWidth(label));
253
+ const restWidth = Math.max(20, cols - 5 - 7);
254
+ const wrapped = wrapText(reason, firstWidth);
255
+ const rows = [];
256
+ wrapped.forEach((line, index) => {
257
+ if (index === 0) {
258
+ rows.push(blockLine(accent, `${label}${paint.text.primary(line)}`));
259
+ } else {
260
+ for (const rest of wrapText(line, restWidth)) {
261
+ rows.push(blockLine(accent, `${paint.text.dim(' ')}${paint.text.primary(rest)}`));
262
+ }
263
+ }
264
+ });
265
+ return rows;
266
+ }
267
+
268
+ function compactReason(tool, args = {}, why = '') {
269
+ const reason = String(why || '').replace(/\s+/g, ' ').trim();
270
+ if (!reason || tool !== 'shell') return reason;
271
+
272
+ const redundantShellPrefix = reason.match(/^Shell command requires approval:\s*(.+)$/i);
273
+ if (!redundantShellPrefix) return reason;
274
+
275
+ const command = String(args.command || args.cmd || '').replace(/\s+/g, ' ').trim();
276
+ const repeated = redundantShellPrefix[1].replace(/\s+/g, ' ').trim();
277
+ if (!command || repeated === command || command.startsWith(repeated) || repeated.startsWith(command)) {
278
+ return 'Shell command requires approval.';
279
+ }
280
+ return reason;
281
+ }
282
+
283
+ function riskRows(tool, args = {}, tier, accent) {
284
+ const terms = riskTerms(tool, args, tier);
285
+ if (!terms.length) return [];
286
+ return [blockLine(accent, `${paint.text.dim('risk ')}${paint.state.warn(terms.join(', '))}`)];
287
+ }
288
+
289
+ function riskTerms(tool, args = {}, tier) {
290
+ const terms = [];
291
+ if (tool === 'shell') {
292
+ const command = String(args.command || args.cmd || '');
293
+ const patterns = [
294
+ [/rm\s+-[^\n]*r[^\n]*f|rm\s+-[^\n]*f[^\n]*r/i, 'rm -rf'],
295
+ [/\bsudo\b/i, 'sudo'],
296
+ [/\bgit\s+push\b[^\n]*(?:--force|-f)\b/i, 'force push'],
297
+ [/\bnpm\s+publish\b/i, 'publish'],
298
+ [/\b(?:kill|pkill)\b/i, 'process kill'],
299
+ [/\bchmod\s+777\b/i, 'chmod 777'],
300
+ [/\bcurl\b|\bwget\b/i, 'network download'],
301
+ ];
302
+ for (const [re, label] of patterns) {
303
+ if (re.test(command)) terms.push(label);
304
+ }
305
+ }
306
+ if (tier === TIERS.SENSITIVE_READ) terms.push('sensitive read');
307
+ if (tier === TIERS.PROTECTED_EDIT) terms.push('protected edit');
308
+ if (tier === TIERS.DESTRUCTIVE) terms.push('destructive');
309
+ return [...new Set(terms)].slice(0, 3);
310
+ }
311
+
312
+ function optionToken(option, selected, accent) {
313
+ const cursor = selected ? accent('▸ ') : paint.text.dim(' ');
314
+ const keyTag = paint.text.dim('[') + (selected ? accent(option.key) : paint.brand.data(option.key)) + paint.text.dim('] ');
315
+ const label = selected ? paint.bold(accent(option.label)) : paint.text.primary(option.label);
316
+ const hint = option.hint ? ` ${paint.text.muted(option.hint)}` : '';
317
+ return `${cursor}${keyTag}${label}${hint}`;
318
+ }
319
+
320
+ function subjectDetails(tool, args = {}, summary = '', available = 72) {
321
+ if (tool === 'shell') {
322
+ const command = args.command || args.cmd || summary || '';
323
+ const profile = shellCommandProfile(command);
324
+ if (profile.compact) {
325
+ const lines = [`$ ${profile.summary}`];
326
+ if (profile.cwdLabel) lines.push(`in ${profile.cwdLabel}`);
327
+ return lines;
328
+ }
329
+ const display = shellCommandDisplay(command);
330
+ const lines = wrapText(display.command, Math.max(20, available - 2))
331
+ .map((line, index) => `${index === 0 ? '$' : '>'} ${line}`);
332
+ if (display.cwdLabel) lines.push(`in ${display.cwdLabel}`);
333
+ return lines.length ? lines : ['(empty command)'];
334
+ }
335
+ if (tool === 'write_file') {
336
+ const file = args.file_path || args.path || summary || '';
337
+ if (isSensitiveConfigPath(file)) return [`${file} · content redacted`];
338
+ const lineCount = typeof args.content === 'string' ? args.content.split('\n').length : null;
339
+ return [`${file}${lineCount ? ` · ${lineCount} lines` : ''}`];
340
+ }
341
+ if (tool === 'edit_file') {
342
+ const file = args.file_path || args.path || '';
343
+ if (isSensitiveConfigPath(file)) {
344
+ const details = [`${file || summary}`];
345
+ if (args.search || args.old_string) details.push('match: [redacted]');
346
+ if (args.replace || args.new_string) details.push('replace: [redacted]');
347
+ return details;
348
+ }
349
+ const search = String(args.search || args.old_string || '').trim();
350
+ const replacement = String(args.replace || args.new_string || '').trim();
351
+ const details = [`${file || summary}`];
352
+ if (search) details.push(`match: ${truncate(search, available - 7)}`);
353
+ if (replacement) details.push(`replace: ${truncate(replacement, available - 9)}`);
354
+ return details.slice(0, 4);
355
+ }
356
+ if (tool === 'delete_file') {
357
+ return [args.file_path || args.path || summary || ''];
358
+ }
359
+ if (tool === 'WebFetch' || tool === 'fetch_url') {
360
+ const url = args.url || summary || '';
361
+ return [`Target ${hostFromUrl(url) || url}`];
362
+ }
363
+ return wrapText(summary || JSON.stringify(args || {}), available).slice(0, 4);
364
+ }
365
+
366
+ function detailRows(tool, args = {}, cols, accent, showDetails) {
367
+ if (!showDetails || tool !== 'shell') return [];
368
+ const profile = shellCommandProfile(args.command || args.cmd || '');
369
+ const rows = [];
370
+ const labelWidth = 9;
371
+ const textWidth = Math.max(28, cols - 5 - labelWidth);
372
+
373
+ if (profile.cwdLabel) {
374
+ rows.push(blockLine(accent, `${paint.text.dim('cwd ')} ${paint.brand.data(profile.cwdLabel)}`));
375
+ }
376
+
377
+ rows.push(blockLine(accent, paint.text.dim('details')));
378
+ if (profile.script?.body) {
379
+ const invocation = profile.script.invocation || profile.command.split('\n')[0] || profile.command;
380
+ for (const line of wrapText(invocation, textWidth)) {
381
+ rows.push(blockLine(accent, `${paint.text.dim('cmd ')} ${paint.text.primary(line)}`));
382
+ }
383
+ rows.push(blockLine(accent, paint.text.dim('script ')));
384
+ rows.push(...numberedRows(profile.script.body, cols, accent, 120));
385
+ } else {
386
+ rows.push(...wrappedLabeledRows('cmd ', profile.command, cols, accent, 120));
387
+ }
388
+
389
+ return rows;
390
+ }
391
+
392
+ function approvalFooter(tool, showDetails) {
393
+ const details = tool === 'shell'
394
+ ? ` · d ${showDetails ? 'hide details' : 'details'}`
395
+ : '';
396
+ return `↑↓ move · Enter pick · letter shortcut${details} · Esc cancel`;
397
+ }
398
+
399
+ function approvalDockDetails(tool, args = {}, cols = 96) {
400
+ if (tool !== 'shell') return subjectDetails(tool, args, toolDisplaySummary(tool, args, {}), cols).join(' · ');
401
+ const profile = shellCommandProfile(args.command || args.cmd || '');
402
+ if (profile.script?.body) {
403
+ const invocation = profile.script.invocation || profile.command.split('\n')[0] || profile.command;
404
+ return [
405
+ `$ ${invocation}`,
406
+ ...profile.script.body.split(/\r?\n/).slice(0, 12).map((line, index) => `${index + 1} ${line}`),
407
+ ...(profile.script.body.split(/\r?\n/).length > 12 ? ['...'] : []),
408
+ ].join('\n');
409
+ }
410
+ return profile.command;
411
+ }
412
+
413
+ function approvalDockSubject(tool, args = {}, cols = 96, showDetails = false) {
414
+ if (showDetails && tool === 'shell') return approvalDockDetails(tool, args, cols);
415
+ return subjectDetails(
416
+ tool,
417
+ args,
418
+ toolDisplaySummary(tool, args, {}),
419
+ Math.max(24, cols - 20),
420
+ ).join(' · ');
421
+ }
422
+
423
+ function approvalDockSubjectRows(subject) {
424
+ const lines = String(subject || '').split('\n');
425
+ const first = lines.shift() || '';
426
+ // 12 continuation rows matches approvalDockDetails' script cap — a
427
+ // 12-line script renders fully in the approval prompt.
428
+ return [
429
+ `${paint.text.dim('? approve ›')} ${paint.text.primary(truncate(first, 160))}`,
430
+ ...lines.slice(0, 12).map(line => `${paint.text.dim(' ')}${paint.text.primary(truncate(line, 160))}`),
431
+ ];
432
+ }
433
+
434
+ function approvalSubjectSummary(tool, args = {}) {
435
+ const summary = toolDisplaySummary(tool, args, {});
436
+ if (tool !== 'shell') return summary;
437
+ const display = shellCommandDisplay(args.command || args.cmd || summary || '');
438
+ return display.cwdLabel ? `${display.command} in ${display.cwdLabel}` : display.command;
439
+ }
440
+
441
+ function hostFromUrl(url) {
442
+ try {
443
+ return new URL(String(url)).host;
444
+ } catch {
445
+ return '';
446
+ }
447
+ }
448
+
449
+ function wrappedLabeledRows(label, text, cols, accent, maxLines = 120) {
450
+ const rows = [];
451
+ const labelText = paint.text.dim(label);
452
+ const firstWidth = Math.max(28, cols - 5 - visibleWidth(labelText) - 1);
453
+ const restWidth = Math.max(28, cols - 5 - visibleWidth(labelText) - 1);
454
+ const sourceLines = String(text || '').replace(/\r\n?/g, '\n').split('\n');
455
+ let emitted = 0;
456
+ for (const source of sourceLines) {
457
+ const wrapped = wrapText(source || ' ', emitted === 0 ? firstWidth : restWidth);
458
+ for (const line of wrapped) {
459
+ if (emitted >= maxLines) {
460
+ rows.push(blockLine(accent, `${paint.text.dim(' ')}${paint.text.dim('... detail truncated')}`));
461
+ return rows;
462
+ }
463
+ rows.push(blockLine(accent, `${emitted === 0 ? labelText : paint.text.dim(' ')} ${paint.text.primary(line)}`));
464
+ emitted++;
465
+ }
466
+ }
467
+ return rows;
468
+ }
469
+
470
+ function numberedRows(text, cols, accent, maxLines = 120) {
471
+ const rows = [];
472
+ const lines = String(text || '').replace(/\r\n?/g, '\n').split('\n');
473
+ const width = String(Math.min(lines.length, maxLines)).length;
474
+ const textWidth = Math.max(28, cols - 5 - width - 2);
475
+ for (let i = 0; i < Math.min(lines.length, maxLines); i++) {
476
+ const n = String(i + 1).padStart(width);
477
+ rows.push(blockLine(accent, `${paint.text.dim(`${n} `)}${paint.text.primary(truncate(lines[i], textWidth))}`));
478
+ }
479
+ if (lines.length > maxLines) {
480
+ rows.push(blockLine(accent, `${paint.text.dim(`... ${lines.length - maxLines} more line(s)`)}`));
481
+ }
482
+ return rows;
483
+ }
484
+
485
+ function truncateForDock(text, maxChars) {
486
+ const value = String(text || '').trim();
487
+ if (value.length <= maxChars) return value;
488
+ return `${value.slice(0, Math.max(0, maxChars - 1))}…`;
489
+ }
490
+
491
+ function wrapText(text, width) {
492
+ const words = String(text || '').split(/\s+/).filter(Boolean);
493
+ if (!words.length) return [''];
494
+ const lines = [];
495
+ let line = '';
496
+ for (const word of words) {
497
+ if (!line) {
498
+ line = word;
499
+ continue;
500
+ }
501
+ if ((line + ' ' + word).length <= width) {
502
+ line += ' ' + word;
503
+ } else {
504
+ lines.push(line);
505
+ line = word;
506
+ }
507
+ }
508
+ if (line) lines.push(line);
509
+ return lines;
510
+ }