pi-crew 0.2.2 → 0.2.4

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 (354) hide show
  1. package/AGENTS.md +57 -32
  2. package/CHANGELOG.md +466 -413
  3. package/LICENSE +21 -21
  4. package/NOTICE.md +16 -16
  5. package/README.md +323 -323
  6. package/docs/FEATURE_INTAKE.md +126 -0
  7. package/docs/HARNESS.md +86 -0
  8. package/docs/HARNESS_BACKLOG.md +41 -0
  9. package/docs/TEST_MATRIX.md +49 -0
  10. package/docs/actions-reference.md +595 -595
  11. package/docs/architecture.md +180 -180
  12. package/docs/code-review-2026-05-11.md +592 -0
  13. package/docs/commands-reference.md +347 -347
  14. package/docs/comparison-pi-subagents-vs-pi-crew.md +303 -0
  15. package/docs/decisions/0001-durable-state.md +41 -0
  16. package/docs/decisions/0002-child-process-for-async.md +42 -0
  17. package/docs/decisions/0003-depth-guard.md +36 -0
  18. package/docs/decisions/0004-execfile-over-exec.md +34 -0
  19. package/docs/decisions/0005-no-parameter-properties.md +49 -0
  20. package/docs/decisions/0006-publish-bundled-esm.md +63 -0
  21. package/docs/decisions/0007-active-run-binary-index.md +54 -0
  22. package/docs/decisions/0008-child-pi-warm-pool.md +61 -0
  23. package/docs/decisions/README.md +23 -0
  24. package/docs/followup-plan-2026-05-12.md +463 -0
  25. package/docs/followup-review-2026-05-12.md +297 -0
  26. package/docs/followup-review-round3-2026-05-12.md +342 -0
  27. package/docs/followup-review-round4-2026-05-13.md +107 -0
  28. package/docs/implementation-plan-top3.md +333 -0
  29. package/docs/live-mailbox-runtime.md +36 -36
  30. package/docs/next-upgrade-roadmap.md +808 -808
  31. package/docs/oh-my-pi-research.md +509 -0
  32. package/docs/perf/baseline-2026-05.md +113 -0
  33. package/docs/perf/final-report-2026-05.md +206 -0
  34. package/docs/perf/sprint-1-report.md +71 -0
  35. package/docs/perf/sprint-2-report.md +81 -0
  36. package/docs/perf/sprint-2.5-report.md +53 -0
  37. package/docs/perf/sprint-3-report.md +36 -0
  38. package/docs/perf/sprint-4-report.md +47 -0
  39. package/docs/perf/sprint-5-report.md +51 -0
  40. package/docs/perf/sprint-6-report.md +94 -0
  41. package/docs/perf/sprint-7-report.md +74 -0
  42. package/docs/perf/upgrade-plan-2026-05.md +147 -0
  43. package/docs/pi-subagents3-deep-analysis.md +508 -0
  44. package/docs/product/README.md +31 -0
  45. package/docs/product/platform.md +27 -0
  46. package/docs/product/runtime-safety.md +37 -0
  47. package/docs/product/team-run.md +39 -0
  48. package/docs/product/team-tool.md +37 -0
  49. package/docs/publishing.md +65 -65
  50. package/docs/resource-formats.md +134 -134
  51. package/docs/runtime-analysis-child-vs-live.md +171 -0
  52. package/docs/runtime-flow.md +148 -148
  53. package/docs/runtime-migration-in-process-analysis.md +250 -0
  54. package/docs/stories/README.md +30 -0
  55. package/docs/stories/backlog.md +36 -0
  56. package/docs/templates/decision.md +27 -0
  57. package/docs/templates/story.md +44 -0
  58. package/docs/templates/validation-report.md +32 -0
  59. package/docs/usage.md +238 -238
  60. package/index.ts +7 -6
  61. package/install.mjs +65 -65
  62. package/package.json +107 -99
  63. package/schema.json +222 -222
  64. package/skills/child-pi-spawning/SKILL.md +213 -0
  65. package/skills/context-artifact-hygiene/SKILL.md +32 -0
  66. package/skills/event-log-tracing/SKILL.md +299 -0
  67. package/skills/git-master/SKILL.md +225 -24
  68. package/skills/live-agent-lifecycle/SKILL.md +192 -0
  69. package/skills/mailbox-interactive/SKILL.md +300 -19
  70. package/skills/model-routing-context/SKILL.md +94 -0
  71. package/skills/multi-perspective-review/SKILL.md +88 -0
  72. package/skills/read-only-explorer/SKILL.md +250 -26
  73. package/skills/safe-bash/SKILL.md +307 -21
  74. package/skills/verification-before-done/SKILL.md +11 -2
  75. package/skills/widget-rendering/SKILL.md +258 -0
  76. package/skills/workspace-isolation/SKILL.md +202 -0
  77. package/skills/worktree-isolation/SKILL.md +202 -18
  78. package/src/adapters/claude-adapter.ts +25 -25
  79. package/src/adapters/codex-adapter.ts +21 -21
  80. package/src/adapters/cursor-adapter.ts +17 -17
  81. package/src/adapters/export-util.ts +137 -137
  82. package/src/adapters/index.ts +15 -15
  83. package/src/adapters/registry.ts +18 -18
  84. package/src/adapters/types.ts +23 -23
  85. package/src/agents/agent-config.ts +38 -38
  86. package/src/agents/agent-serializer.ts +38 -38
  87. package/src/agents/discover-agents.ts +121 -118
  88. package/src/config/config.ts +740 -858
  89. package/src/config/defaults.ts +96 -96
  90. package/src/config/drift-detector.ts +211 -211
  91. package/src/config/markers.ts +327 -327
  92. package/src/config/resilient-parser.ts +109 -108
  93. package/src/config/suggestions.ts +74 -74
  94. package/src/config/types.ts +199 -0
  95. package/src/extension/async-notifier.ts +123 -89
  96. package/src/extension/autonomous-policy.ts +169 -169
  97. package/src/extension/cross-extension-rpc.ts +104 -103
  98. package/src/extension/help.ts +47 -47
  99. package/src/extension/import-index.ts +69 -69
  100. package/src/extension/management.ts +395 -382
  101. package/src/extension/notification-router.ts +116 -116
  102. package/src/extension/notification-sink.ts +51 -51
  103. package/src/extension/project-init.ts +168 -168
  104. package/src/extension/register.ts +859 -668
  105. package/src/extension/registration/artifact-cleanup.ts +15 -15
  106. package/src/extension/registration/command-utils.ts +54 -54
  107. package/src/extension/registration/commands.ts +559 -452
  108. package/src/extension/registration/compaction-guard.ts +125 -125
  109. package/src/extension/registration/subagent-helpers.ts +102 -102
  110. package/src/extension/registration/subagent-tools.ts +220 -158
  111. package/src/extension/registration/team-tool.ts +159 -98
  112. package/src/extension/registration/viewers.ts +29 -0
  113. package/src/extension/result-watcher.ts +128 -128
  114. package/src/extension/run-bundle-schema.ts +89 -89
  115. package/src/extension/run-export.ts +73 -73
  116. package/src/extension/run-import.ts +84 -84
  117. package/src/extension/run-index.ts +94 -94
  118. package/src/extension/run-maintenance.ts +142 -142
  119. package/src/extension/session-summary.ts +8 -8
  120. package/src/extension/team-manager-command.ts +96 -95
  121. package/src/extension/team-recommendation.ts +188 -188
  122. package/src/extension/team-tool/api.ts +5 -2
  123. package/src/extension/team-tool/cancel.ts +224 -209
  124. package/src/extension/team-tool/config-patch.ts +36 -36
  125. package/src/extension/team-tool/context.ts +60 -60
  126. package/src/extension/team-tool/doctor.ts +242 -242
  127. package/src/extension/team-tool/handle-settings.ts +421 -195
  128. package/src/extension/team-tool/inspect.ts +41 -41
  129. package/src/extension/team-tool/lifecycle-actions.ts +139 -139
  130. package/src/extension/team-tool/parallel-dispatch.ts +156 -156
  131. package/src/extension/team-tool/plan.ts +19 -19
  132. package/src/extension/team-tool/respond.ts +112 -111
  133. package/src/extension/team-tool/run.ts +246 -228
  134. package/src/extension/team-tool/status.ts +110 -110
  135. package/src/extension/team-tool-types.ts +13 -13
  136. package/src/extension/team-tool.ts +16 -4
  137. package/src/extension/tool-result.ts +16 -16
  138. package/src/extension/validate-resources.ts +77 -77
  139. package/src/hooks/registry.ts +61 -61
  140. package/src/hooks/types.ts +40 -40
  141. package/src/i18n.ts +184 -184
  142. package/src/observability/correlation.ts +35 -35
  143. package/src/observability/event-to-metric.ts +68 -68
  144. package/src/observability/exporters/adapter.ts +30 -30
  145. package/src/observability/exporters/otlp-exporter.ts +106 -92
  146. package/src/observability/exporters/prometheus-exporter.ts +54 -54
  147. package/src/observability/metric-registry.ts +87 -87
  148. package/src/observability/metric-retention.ts +54 -54
  149. package/src/observability/metric-sink.ts +81 -56
  150. package/src/observability/metrics-primitives.ts +167 -167
  151. package/src/prompt/prompt-runtime.ts +72 -72
  152. package/src/runtime/adaptive-plan.ts +338 -0
  153. package/src/runtime/agent-control.ts +169 -169
  154. package/src/runtime/agent-memory.ts +72 -72
  155. package/src/runtime/agent-observability.ts +114 -114
  156. package/src/runtime/async-marker.ts +26 -26
  157. package/src/runtime/async-runner.ts +153 -79
  158. package/src/runtime/attention-events.ts +28 -28
  159. package/src/runtime/auto-resume.ts +100 -100
  160. package/src/runtime/background-runner.ts +122 -88
  161. package/src/runtime/cancellation.ts +61 -61
  162. package/src/runtime/capability-inventory.ts +116 -116
  163. package/src/runtime/child-pi-pool.ts +68 -0
  164. package/src/runtime/child-pi.ts +541 -463
  165. package/src/runtime/code-summary.ts +247 -247
  166. package/src/runtime/compaction-summary.ts +271 -271
  167. package/src/runtime/concurrency.ts +58 -58
  168. package/src/runtime/crash-recovery.ts +317 -301
  169. package/src/runtime/crew-agent-records.ts +379 -281
  170. package/src/runtime/crew-agent-runtime.ts +60 -60
  171. package/src/runtime/cross-extension-rpc.ts +72 -0
  172. package/src/runtime/custom-tools/irc-tool.ts +201 -201
  173. package/src/runtime/custom-tools/submit-result-tool.ts +90 -90
  174. package/src/runtime/deadletter.ts +47 -47
  175. package/src/runtime/delivery-coordinator.ts +176 -176
  176. package/src/runtime/delta-conflict.ts +360 -360
  177. package/src/runtime/diagnostic-export.ts +102 -102
  178. package/src/runtime/direct-run.ts +35 -35
  179. package/src/runtime/effectiveness.ts +82 -81
  180. package/src/runtime/errors/crew-errors.ts +166 -0
  181. package/src/runtime/event-stream-bridge.ts +92 -92
  182. package/src/runtime/foreground-control.ts +82 -82
  183. package/src/runtime/green-contract.ts +46 -46
  184. package/src/runtime/group-join.ts +234 -106
  185. package/src/runtime/heartbeat-watcher.ts +145 -124
  186. package/src/runtime/iteration-hooks.ts +267 -264
  187. package/src/runtime/live-agent-control.ts +88 -88
  188. package/src/runtime/live-agent-manager.ts +377 -179
  189. package/src/runtime/live-control-realtime.ts +36 -36
  190. package/src/runtime/live-session-runtime.ts +676 -599
  191. package/src/runtime/loop-gates.ts +129 -129
  192. package/src/runtime/manifest-cache.ts +263 -263
  193. package/src/runtime/mcp-proxy.ts +113 -113
  194. package/src/runtime/metric-parser.ts +40 -40
  195. package/src/runtime/model-fallback.ts +282 -274
  196. package/src/runtime/model-resolver.ts +118 -0
  197. package/src/runtime/output-validator.ts +187 -187
  198. package/src/runtime/overflow-recovery.ts +175 -175
  199. package/src/runtime/parallel-research.ts +44 -44
  200. package/src/runtime/parallel-utils.ts +156 -156
  201. package/src/runtime/parent-guard.ts +80 -80
  202. package/src/runtime/phase-progress.ts +217 -217
  203. package/src/runtime/pi-args.ts +165 -165
  204. package/src/runtime/pi-json-output.ts +111 -111
  205. package/src/runtime/pi-spawn.ts +167 -167
  206. package/src/runtime/policy-engine.ts +79 -79
  207. package/src/runtime/post-checks.ts +125 -122
  208. package/src/runtime/post-exit-stdio-guard.ts +86 -86
  209. package/src/runtime/process-status.ts +97 -73
  210. package/src/runtime/progress-event-coalescer.ts +43 -43
  211. package/src/runtime/recovery-recipes.ts +74 -74
  212. package/src/runtime/retry-executor.ts +81 -81
  213. package/src/runtime/role-permission.ts +39 -39
  214. package/src/runtime/run-tracker.ts +99 -0
  215. package/src/runtime/runtime-policy.ts +21 -0
  216. package/src/runtime/runtime-resolver.ts +94 -90
  217. package/src/runtime/scheduler.ts +294 -0
  218. package/src/runtime/semaphore.ts +131 -131
  219. package/src/runtime/sensitive-paths.ts +92 -92
  220. package/src/runtime/session-usage.ts +79 -79
  221. package/src/runtime/settings-store.ts +103 -0
  222. package/src/runtime/sidechain-output.ts +29 -29
  223. package/src/runtime/skill-instructions.ts +222 -222
  224. package/src/runtime/stale-reconciler.ts +198 -189
  225. package/src/runtime/streaming-output.ts +47 -0
  226. package/src/runtime/subagent-manager.ts +404 -395
  227. package/src/runtime/subprocess-tool-registry.ts +67 -67
  228. package/src/runtime/task-display.ts +38 -38
  229. package/src/runtime/task-graph-scheduler.ts +122 -122
  230. package/src/runtime/task-graph.ts +207 -207
  231. package/src/runtime/task-output-context.ts +177 -177
  232. package/src/runtime/task-packet.ts +93 -93
  233. package/src/runtime/task-quality.ts +207 -207
  234. package/src/runtime/task-runner/capabilities.ts +78 -78
  235. package/src/runtime/task-runner/live-executor.ts +131 -113
  236. package/src/runtime/task-runner/progress.ts +119 -119
  237. package/src/runtime/task-runner/prompt-builder.ts +139 -139
  238. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  239. package/src/runtime/task-runner/result-utils.ts +14 -14
  240. package/src/runtime/task-runner/run-projection.ts +103 -103
  241. package/src/runtime/task-runner/state-helpers.ts +22 -22
  242. package/src/runtime/task-runner.ts +469 -458
  243. package/src/runtime/team-runner.ts +693 -945
  244. package/src/runtime/usage-tracker.ts +71 -0
  245. package/src/runtime/worker-heartbeat.ts +21 -21
  246. package/src/runtime/worker-startup.ts +57 -57
  247. package/src/runtime/workflow-state.ts +187 -187
  248. package/src/runtime/yield-handler.ts +190 -189
  249. package/src/schema/config-schema.ts +172 -168
  250. package/src/schema/team-tool-schema.ts +126 -125
  251. package/src/schema/validation-types.ts +151 -148
  252. package/src/skills/discover-skills.ts +67 -67
  253. package/src/skills/skill-templates.ts +374 -374
  254. package/src/state/active-run-registry.ts +227 -191
  255. package/src/state/artifact-store.ts +130 -129
  256. package/src/state/atomic-write.ts +262 -178
  257. package/src/state/blob-store.ts +116 -116
  258. package/src/state/contracts.ts +111 -111
  259. package/src/state/event-log-rotation.ts +161 -158
  260. package/src/state/event-log.ts +383 -240
  261. package/src/state/event-reconstructor.ts +217 -217
  262. package/src/state/jsonl-writer.ts +82 -82
  263. package/src/state/locks.ts +146 -148
  264. package/src/state/mailbox.ts +446 -405
  265. package/src/state/state-store.ts +364 -351
  266. package/src/state/task-claims.ts +44 -44
  267. package/src/state/types.ts +285 -285
  268. package/src/state/usage.ts +29 -29
  269. package/src/subagents/async-entry.ts +1 -1
  270. package/src/subagents/index.ts +3 -3
  271. package/src/subagents/live/control.ts +1 -1
  272. package/src/subagents/live/manager.ts +1 -1
  273. package/src/subagents/live/realtime.ts +1 -1
  274. package/src/subagents/live/session-runtime.ts +1 -1
  275. package/src/subagents/manager.ts +1 -1
  276. package/src/subagents/spawn.ts +1 -1
  277. package/src/teams/discover-teams.ts +116 -116
  278. package/src/teams/team-config.ts +27 -27
  279. package/src/teams/team-serializer.ts +38 -38
  280. package/src/types/diff.d.ts +18 -18
  281. package/src/ui/agent-management-overlay.ts +144 -144
  282. package/src/ui/crew-widget.ts +487 -370
  283. package/src/ui/dashboard-panes/agents-pane.ts +109 -28
  284. package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
  285. package/src/ui/dashboard-panes/capability-pane.ts +59 -59
  286. package/src/ui/dashboard-panes/health-pane.ts +30 -30
  287. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
  288. package/src/ui/dashboard-panes/progress-pane.ts +30 -30
  289. package/src/ui/dashboard-panes/transcript-pane.ts +10 -10
  290. package/src/ui/heartbeat-aggregator.ts +63 -63
  291. package/src/ui/keybinding-map.ts +97 -94
  292. package/src/ui/live-conversation-overlay.ts +152 -0
  293. package/src/ui/live-run-sidebar.ts +180 -180
  294. package/src/ui/mascot.ts +442 -442
  295. package/src/ui/overlays/agent-picker-overlay.ts +57 -57
  296. package/src/ui/overlays/confirm-overlay.ts +58 -58
  297. package/src/ui/overlays/mailbox-compose-overlay.ts +144 -144
  298. package/src/ui/overlays/mailbox-compose-preview.ts +63 -63
  299. package/src/ui/overlays/mailbox-detail-overlay.ts +122 -122
  300. package/src/ui/pi-ui-compat.ts +57 -57
  301. package/src/ui/powerbar-publisher.ts +221 -197
  302. package/src/ui/render-scheduler.ts +216 -143
  303. package/src/ui/run-action-dispatcher.ts +118 -117
  304. package/src/ui/run-dashboard.ts +526 -464
  305. package/src/ui/run-event-bus.ts +208 -208
  306. package/src/ui/run-snapshot-cache.ts +826 -777
  307. package/src/ui/settings-overlay.ts +721 -0
  308. package/src/ui/snapshot-types.ts +86 -70
  309. package/src/ui/theme-adapter.ts +190 -190
  310. package/src/ui/tool-progress-formatter.ts +89 -0
  311. package/src/ui/transcript-cache.ts +94 -94
  312. package/src/ui/transcript-viewer.ts +335 -335
  313. package/src/utils/conflict-detect.ts +662 -0
  314. package/src/utils/env-filter.ts +30 -0
  315. package/src/utils/file-coalescer.ts +86 -86
  316. package/src/utils/frontmatter.ts +68 -68
  317. package/src/utils/fs-watch.ts +88 -31
  318. package/src/utils/gh-protocol.ts +479 -0
  319. package/src/utils/ids.ts +17 -17
  320. package/src/utils/incremental-reader.ts +104 -104
  321. package/src/utils/internal-error.ts +6 -6
  322. package/src/utils/names.ts +27 -27
  323. package/src/utils/paths.ts +102 -63
  324. package/src/utils/redaction.ts +44 -44
  325. package/src/utils/resolve-shell.ts +34 -0
  326. package/src/utils/safe-paths.ts +47 -47
  327. package/src/utils/scan-cache.ts +136 -136
  328. package/src/utils/sleep.ts +2 -1
  329. package/src/utils/sse-parser.ts +134 -134
  330. package/src/utils/task-name-generator.ts +337 -337
  331. package/src/utils/timings.ts +33 -33
  332. package/src/utils/visual.ts +243 -198
  333. package/src/workflows/discover-workflows.ts +139 -139
  334. package/src/workflows/validate-workflow.ts +40 -40
  335. package/src/workflows/workflow-config.ts +26 -26
  336. package/src/workflows/workflow-serializer.ts +32 -32
  337. package/src/worktree/branch-freshness.ts +45 -45
  338. package/src/worktree/cleanup.ts +75 -72
  339. package/src/worktree/worktree-manager.ts +188 -146
  340. package/teams/default.team.md +12 -12
  341. package/teams/fast-fix.team.md +11 -11
  342. package/teams/implementation.team.md +18 -18
  343. package/teams/parallel-research.team.md +14 -14
  344. package/teams/research.team.md +11 -11
  345. package/teams/review.team.md +12 -12
  346. package/tsconfig.json +19 -19
  347. package/workflows/default.workflow.md +30 -30
  348. package/workflows/fast-fix.workflow.md +23 -23
  349. package/workflows/implementation.workflow.md +43 -43
  350. package/workflows/parallel-research.workflow.md +46 -46
  351. package/workflows/research.workflow.md +22 -22
  352. package/workflows/review.workflow.md +30 -30
  353. package/skills/task-packet/SKILL.md +0 -28
  354. package/skills/verify-evidence/SKILL.md +0 -27
@@ -0,0 +1,721 @@
1
+ /**
2
+ * Interactive TUI Settings Overlay for pi-crew.
3
+ * Mirrors Pi's built-in /settings selector: tab bar, settings list with
4
+ * label/value alignment, inline toggle, select submenu, and text input.
5
+ */
6
+ import type { CrewTheme } from "./theme-adapter.ts";
7
+ import { DynamicCrewBorder } from "./dynamic-border.ts";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Types
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export type SettingType = "boolean" | "enum" | "number" | "string" | "agent";
14
+
15
+ export interface SettingDef {
16
+ id: string;
17
+ label: string;
18
+ description?: string;
19
+ type: SettingType;
20
+ /** For enum: list of allowed values */
21
+ values?: string[];
22
+ /** Tab grouping */
23
+ tab: string;
24
+ }
25
+
26
+ export interface SettingsOverlayCallbacks {
27
+ onChange: (id: string, value: unknown) => void;
28
+ onClose: () => void;
29
+ }
30
+
31
+ interface TabDef {
32
+ id: string;
33
+ label: string;
34
+ icon: string;
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Setting Definitions — mirrors config schema
39
+ // ---------------------------------------------------------------------------
40
+
41
+ const TABS: TabDef[] = [
42
+ { id: "runtime", label: "Runtime", icon: "⚙" },
43
+ { id: "limits", label: "Limits", icon: "📐" },
44
+ { id: "agents", label: "Agents", icon: "🤖" },
45
+ { id: "ui", label: "UI", icon: "🖥" },
46
+ { id: "autonomous", label: "Auto", icon: "🚀" },
47
+ { id: "advanced", label: "Advanced", icon: "🔧" },
48
+ ];
49
+
50
+ const SETTINGS: SettingDef[] = [
51
+ // Runtime
52
+ { id: "runtime.mode", label: "Runtime Mode", type: "enum", values: ["auto", "scaffold", "child-process", "live-session"], tab: "runtime", description: "How workers execute. 'auto' picks best available. 'scaffold' = dry-run." },
53
+ { id: "runtime.maxTurns", label: "Max Turns", type: "number", tab: "runtime", description: "Maximum agent turns per task." },
54
+ { id: "runtime.graceTurns", label: "Grace Turns", type: "number", tab: "runtime", description: "Extra turns allowed after completion." },
55
+ { id: "runtime.inheritContext", label: "Inherit Context", type: "boolean", tab: "runtime", description: "Pass parent conversation context to workers." },
56
+ { id: "runtime.promptMode", label: "Prompt Mode", type: "enum", values: ["compact", "full", "minimal"], tab: "runtime", description: "How much prompt detail to send to workers." },
57
+ { id: "runtime.completionMutationGuard", label: "Mutation Guard", type: "enum", values: ["off", "warn", "block"], tab: "runtime", description: "Guard against tasks completing without file mutations." },
58
+ { id: "runtime.isolationPolicy", label: "Isolation Policy", type: "enum", values: ["workspace", "none"], tab: "runtime", description: "Workspace isolation between agents." },
59
+ // Limits
60
+ { id: "limits.maxConcurrentWorkers", label: "Max Concurrent", type: "number", tab: "limits", description: "Max number of workers running simultaneously." },
61
+ { id: "limits.maxTaskDepth", label: "Max Task Depth", type: "number", tab: "limits", description: "Maximum depth of nested task spawning." },
62
+ { id: "limits.maxRunMinutes", label: "Max Run Minutes", type: "number", tab: "limits", description: "Maximum total run time in minutes." },
63
+ { id: "limits.maxRetriesPerTask", label: "Max Retries", type: "number", tab: "limits", description: "Max retry attempts per failed task." },
64
+ { id: "limits.maxTasksPerRun", label: "Max Tasks", type: "number", tab: "limits", description: "Maximum number of tasks per run." },
65
+ { id: "limits.heartbeatStaleMs", label: "Heartbeat Stale", type: "number", tab: "limits", description: "Milliseconds before a worker is considered stale." },
66
+ // Agents
67
+ { id: "agents.overrides", label: "Agent Model Overrides", type: "agent", tab: "agents", description: "Model and thinking overrides per agent role." },
68
+ { id: "agents.disableBuiltins", label: "Disable Builtins", type: "boolean", tab: "agents", description: "Disable built-in agent definitions." },
69
+ // UI
70
+ { id: "ui.showModel", label: "Show Model", type: "boolean", tab: "ui", description: "Show model name in widget/dashboard." },
71
+ { id: "ui.showTokens", label: "Show Tokens", type: "boolean", tab: "ui", description: "Show token counts in dashboard." },
72
+ { id: "ui.showTools", label: "Show Tools", type: "boolean", tab: "ui", description: "Show tool usage in dashboard." },
73
+ { id: "ui.dashboardPlacement", label: "Dashboard Placement", type: "enum", values: ["center", "right"], tab: "ui", description: "Where to place the dashboard overlay." },
74
+ { id: "ui.dashboardWidth", label: "Dashboard Width", type: "number", tab: "ui", description: "Dashboard width as percentage or pixels." },
75
+ { id: "ui.autoOpenDashboard", label: "Auto Open Dashboard", type: "boolean", tab: "ui", description: "Auto-open dashboard when a run starts." },
76
+ { id: "ui.widgetPlacement", label: "Widget Placement", type: "enum", values: ["bottom", "hidden"], tab: "ui", description: "Where to place the crew widget." },
77
+ // Autonomous
78
+ { id: "autonomous.enabled", label: "Enabled", type: "boolean", tab: "autonomous", description: "Enable autonomous pi-crew delegation." },
79
+ { id: "autonomous.injectPolicy", label: "Inject Policy", type: "boolean", tab: "autonomous", description: "Inject delegation policy into agent context." },
80
+ { id: "autonomous.preferAsyncForLongTasks", label: "Prefer Async", type: "boolean", tab: "autonomous", description: "Prefer async execution for long tasks." },
81
+ { id: "autonomous.allowWorktreeSuggestion", label: "Allow Worktree", type: "boolean", tab: "autonomous", description: "Allow suggesting worktree isolation." },
82
+ // Advanced
83
+ { id: "executeWorkers", label: "Execute Workers", type: "boolean", tab: "advanced", description: "Allow real child Pi workers. false = scaffold only." },
84
+ { id: "asyncByDefault", label: "Async By Default", type: "boolean", tab: "advanced", description: "Run teams asynchronously by default." },
85
+ { id: "notifierIntervalMs", label: "Notifier Interval", type: "number", tab: "advanced", description: "Async run notifier check interval in ms." },
86
+ { id: "reliability.autoRetry", label: "Auto Retry", type: "boolean", tab: "advanced", description: "Automatically retry failed tasks." },
87
+ { id: "reliability.autoRecover", label: "Auto Recover", type: "boolean", tab: "advanced", description: "Automatically recover from crashes." },
88
+ { id: "telemetry.enabled", label: "Telemetry", type: "boolean", tab: "advanced", description: "Enable telemetry collection." },
89
+ { id: "notifications.enabled", label: "Notifications", type: "boolean", tab: "advanced", description: "Enable run notifications." },
90
+ ];
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Effective defaults — values used when config key is not set
94
+ // ---------------------------------------------------------------------------
95
+
96
+ const EFFECTIVE_DEFAULTS: Record<string, unknown> = {
97
+ "runtime.mode": "auto",
98
+ "runtime.maxTurns": 10000,
99
+ "runtime.graceTurns": 5,
100
+ "runtime.inheritContext": false,
101
+ "runtime.promptMode": "replace",
102
+ "runtime.completionMutationGuard": "warn",
103
+ "runtime.isolationPolicy": undefined,
104
+ "limits.maxConcurrentWorkers": 1024,
105
+ "limits.maxTaskDepth": 100,
106
+ "limits.maxRunMinutes": 1440,
107
+ "limits.maxRetriesPerTask": 100,
108
+ "limits.maxTasksPerRun": 10000,
109
+ "limits.heartbeatStaleMs": 86400000,
110
+ "agents.disableBuiltins": false,
111
+ "ui.showModel": true,
112
+ "ui.showTokens": true,
113
+ "ui.showTools": true,
114
+ "ui.dashboardPlacement": "center",
115
+ "ui.dashboardWidth": 72,
116
+ "ui.autoOpenDashboard": false,
117
+ "ui.widgetPlacement": "aboveEditor",
118
+ "autonomous.enabled": true,
119
+ "autonomous.injectPolicy": true,
120
+ "autonomous.preferAsyncForLongTasks": false,
121
+ "autonomous.allowWorktreeSuggestion": true,
122
+ "executeWorkers": true,
123
+ "asyncByDefault": false,
124
+ "notifierIntervalMs": 5000,
125
+ "reliability.autoRetry": false,
126
+ "reliability.autoRecover": false,
127
+ "telemetry.enabled": false,
128
+ "notifications.enabled": false,
129
+ };
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Helpers
133
+ // ---------------------------------------------------------------------------
134
+
135
+ /** Visible character width (ignores ANSI escapes). */
136
+ function visibleWidth(text: string): number {
137
+ // eslint-disable-next-line no-control-regex
138
+ let w = 0;
139
+ let inEscape = false;
140
+ for (const ch of text) {
141
+ if (ch === "\x1b") { inEscape = true; continue; }
142
+ if (inEscape) { if (/[a-zA-Z]/.test(ch)) inEscape = false; continue; }
143
+ w++;
144
+ }
145
+ return w;
146
+ }
147
+
148
+ /** Truncate string to fit within maxVis visible characters. */
149
+ function truncateToWidth(text: string, maxVis: number): string {
150
+ // eslint-disable-next-line no-control-regex
151
+ let w = 0;
152
+ let result = "";
153
+ let inEscape = false;
154
+ for (const ch of text) {
155
+ if (ch === "\x1b") { inEscape = true; result += ch; continue; }
156
+ if (inEscape) { result += ch; if (/[a-zA-Z]/.test(ch)) inEscape = false; continue; }
157
+ w++;
158
+ if (w > maxVis) return result + "…";
159
+ result += ch;
160
+ }
161
+ return result;
162
+ }
163
+
164
+ /** Pad string to exactly maxVis visible width. */
165
+ function padToWidth(text: string, maxVis: number, padChar = " "): string {
166
+ const vw = visibleWidth(text);
167
+ if (vw >= maxVis) return truncateToWidth(text, maxVis);
168
+ return text + padChar.repeat(maxVis - vw);
169
+ }
170
+
171
+ function formatValue(value: unknown, id: string): string {
172
+ if (value === undefined || value === null) {
173
+ const def = EFFECTIVE_DEFAULTS[id];
174
+ if (def !== undefined) return `${String(def)}`;
175
+ return "<not set>";
176
+ }
177
+ if (typeof value === "boolean") return value ? "true" : "false";
178
+ if (typeof value === "number") return String(value);
179
+ if (typeof value === "object") return JSON.stringify(value);
180
+ return String(value);
181
+ }
182
+
183
+ function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
184
+ const keys = path.split(".");
185
+ let current: unknown = obj;
186
+ for (const key of keys) {
187
+ if (!current || typeof current !== "object") return undefined;
188
+ current = (current as Record<string, unknown>)[key];
189
+ }
190
+ return current;
191
+ }
192
+
193
+ function isExplicitlySet(config: Record<string, unknown>, id: string): boolean {
194
+ return getNestedValue(config, id) !== undefined;
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Submenu: Select from list (enum picker)
199
+ // ---------------------------------------------------------------------------
200
+
201
+ class SelectSubmenu {
202
+ private selectedIndex = 0;
203
+ private readonly items: string[];
204
+ private readonly theme: CrewTheme;
205
+ private readonly onSelect: (value: string) => void;
206
+ private readonly onCancel: () => void;
207
+ private readonly title: string;
208
+ private readonly description: string;
209
+
210
+ constructor(title: string, description: string, options: string[], current: string, theme: CrewTheme, onSelect: (value: string) => void, onCancel: () => void) {
211
+ this.title = title;
212
+ this.description = description;
213
+ this.items = options;
214
+ this.theme = theme;
215
+ this.onSelect = onSelect;
216
+ this.onCancel = onCancel;
217
+ this.selectedIndex = Math.max(0, options.indexOf(current));
218
+ }
219
+
220
+ invalidate(): void {}
221
+
222
+ render(width: number): string[] {
223
+ const lines: string[] = [];
224
+ lines.push(this.theme.bold(this.theme.fg("accent", this.title)));
225
+ if (this.description) {
226
+ lines.push(this.theme.fg("muted", this.description));
227
+ }
228
+ lines.push("");
229
+ for (const [i, item] of this.items.entries()) {
230
+ const isSelected = i === this.selectedIndex;
231
+ const prefix = isSelected ? " → " : " ";
232
+ const line = `${prefix}${item}`;
233
+ lines.push(isSelected ? (this.theme.inverse?.(line) ?? line) : line);
234
+ }
235
+ lines.push("");
236
+ lines.push(this.theme.fg("dim", "Enter to select · Esc to go back"));
237
+ return lines;
238
+ }
239
+
240
+ handleInput(data: string): void {
241
+ if (data === "\x1b[A" || data === "k") {
242
+ this.selectedIndex = (this.selectedIndex - 1 + this.items.length) % this.items.length;
243
+ return;
244
+ }
245
+ if (data === "\x1b[B" || data === "j") {
246
+ this.selectedIndex = (this.selectedIndex + 1) % this.items.length;
247
+ return;
248
+ }
249
+ if (data === "\r" || data === "\n") {
250
+ this.onSelect(this.items[this.selectedIndex]!);
251
+ return;
252
+ }
253
+ if (data === "\x1b" || data === "q") {
254
+ this.onCancel();
255
+ return;
256
+ }
257
+ }
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // Submenu: Text input (number / string)
262
+ // ---------------------------------------------------------------------------
263
+
264
+ class TextinputSubmenu {
265
+ private buffer = "";
266
+ private readonly title: string;
267
+ private readonly description: string;
268
+ private readonly theme: CrewTheme;
269
+ private readonly onSubmit: (value: string) => void;
270
+ private readonly onCancel: () => void;
271
+
272
+ constructor(title: string, description: string, initialValue: string, theme: CrewTheme, onSubmit: (value: string) => void, onCancel: () => void) {
273
+ this.title = title;
274
+ this.description = description;
275
+ this.buffer = initialValue;
276
+ this.theme = theme;
277
+ this.onSubmit = onSubmit;
278
+ this.onCancel = onCancel;
279
+ }
280
+
281
+ invalidate(): void {}
282
+
283
+ render(width: number): string[] {
284
+ const lines: string[] = [];
285
+ lines.push(this.theme.bold(this.theme.fg("accent", this.title)));
286
+ if (this.description) {
287
+ lines.push(this.theme.fg("muted", this.description));
288
+ }
289
+ lines.push("");
290
+ lines.push(` ${this.buffer}█`);
291
+ lines.push("");
292
+ lines.push(this.theme.fg("dim", "Enter to save · Esc to cancel · Clear to unset"));
293
+ return lines;
294
+ }
295
+
296
+ handleInput(data: string): void {
297
+ if (data === "\r" || data === "\n") {
298
+ this.onSubmit(this.buffer);
299
+ return;
300
+ }
301
+ if (data === "\x1b" || data === "q") {
302
+ this.onCancel();
303
+ return;
304
+ }
305
+ // Backspace
306
+ if (data === "\x7f" || data === "\b") {
307
+ this.buffer = this.buffer.slice(0, -1);
308
+ return;
309
+ }
310
+ // Printable character
311
+ if (data.length === 1 && data >= " " && data <= "~") {
312
+ this.buffer += data;
313
+ return;
314
+ }
315
+ }
316
+ }
317
+
318
+ // ---------------------------------------------------------------------------
319
+ // Submenu: Agent overrides editor
320
+ // ---------------------------------------------------------------------------
321
+
322
+ class AgentOverridesSubmenu {
323
+ private readonly overrides: Record<string, { model?: string; thinking?: string }>;
324
+ private readonly theme: CrewTheme;
325
+ private readonly agents: string[];
326
+ private selectedIndex = 0;
327
+ private editField: "model" | "thinking" | null = null;
328
+ private editBuffer = "";
329
+ private readonly onApply: (overrides: Record<string, unknown>) => void;
330
+ private readonly onCancel: () => void;
331
+
332
+ constructor(config: Record<string, unknown>, theme: CrewTheme, onApply: (overrides: Record<string, unknown>) => void, onCancel: () => void) {
333
+ this.theme = theme;
334
+ this.onApply = onApply;
335
+ this.onCancel = onCancel;
336
+ const existing = (config.agents as Record<string, unknown>)?.overrides as Record<string, { model?: string; thinking?: string }> | undefined;
337
+ this.overrides = existing ? structuredClone(existing) : {};
338
+ this.agents = ["explorer", "planner", "analyst", "critic", "executor", "reviewer", "security-reviewer", "test-engineer", "verifier", "writer"];
339
+ }
340
+
341
+ invalidate(): void {}
342
+
343
+ render(width: number): string[] {
344
+ if (this.editField) return this.renderEdit(width);
345
+
346
+ const lines: string[] = [];
347
+ lines.push(this.theme.bold(this.theme.fg("accent", "Agent Model Overrides")));
348
+ lines.push("");
349
+ const labelWidth = 22;
350
+ for (const [i, agent] of this.agents.entries()) {
351
+ const isSelected = i === this.selectedIndex;
352
+ const ov = this.overrides[agent];
353
+ const model = ov?.model ?? "";
354
+ const thinking = ov?.thinking ?? "";
355
+ const label = padToWidth(agent, labelWidth);
356
+ const modelPart = model ? `model=${model}` : "";
357
+ const thinkingPart = thinking ? `thinking=${thinking}` : "";
358
+ const valueParts = [modelPart, thinkingPart].filter(Boolean).join(", ");
359
+ const valueText = valueParts || this.theme.fg("dim", "(default)");
360
+ const prefix = isSelected ? " → " : " ";
361
+ const line = `${prefix}${label} ${valueText}`;
362
+ lines.push(isSelected ? (this.theme.inverse?.(truncateToWidth(line, width - 2)) ?? truncateToWidth(line, width - 2)) : truncateToWidth(line, width - 2));
363
+ }
364
+ lines.push("");
365
+ lines.push(this.theme.fg("dim", "Enter to edit model · e to edit thinking · Esc to go back"));
366
+ return lines;
367
+ }
368
+
369
+ private renderEdit(width: number): string[] {
370
+ const agent = this.agents[this.selectedIndex];
371
+ const field = this.editField === "model" ? "model" : "thinking";
372
+ const lines: string[] = [];
373
+ lines.push(this.theme.bold(this.theme.fg("accent", `Edit ${agent} ${field}`)));
374
+ lines.push("");
375
+ lines.push(` ${this.editBuffer}█`);
376
+ lines.push("");
377
+ lines.push(this.theme.fg("dim", "Enter to save · Esc to cancel · Clear to unset"));
378
+ return lines;
379
+ }
380
+
381
+ handleInput(data: string): void {
382
+ if (this.editField) return this.handleEditInput(data);
383
+
384
+ if (data === "\x1b[A" || data === "k") { this.selectedIndex = (this.selectedIndex - 1 + this.agents.length) % this.agents.length; return; }
385
+ if (data === "\x1b[B" || data === "j") { this.selectedIndex = (this.selectedIndex + 1) % this.agents.length; return; }
386
+ if (data === "\r" || data === "\n") {
387
+ const agent = this.agents[this.selectedIndex]!;
388
+ this.editField = "model";
389
+ this.editBuffer = this.overrides[agent]?.model ?? "";
390
+ return;
391
+ }
392
+ if (data === "e") {
393
+ const agent = this.agents[this.selectedIndex]!;
394
+ this.editField = "thinking";
395
+ this.editBuffer = this.overrides[agent]?.thinking ?? "";
396
+ return;
397
+ }
398
+ if (data === "\x1b") { this.onCancel(); return; }
399
+ }
400
+
401
+ private handleEditInput(data: string): void {
402
+ if (data === "\r" || data === "\n") {
403
+ const agent = this.agents[this.selectedIndex]!;
404
+ if (!this.overrides[agent]) this.overrides[agent] = {};
405
+ if (this.editField === "model") {
406
+ this.overrides[agent]!.model = this.editBuffer || undefined;
407
+ } else {
408
+ this.overrides[agent]!.thinking = this.editBuffer || undefined;
409
+ }
410
+ // Clean up empty overrides
411
+ if (!this.overrides[agent]!.model && !this.overrides[agent]!.thinking) {
412
+ delete this.overrides[agent];
413
+ }
414
+ this.editField = null;
415
+ return;
416
+ }
417
+ if (data === "\x1b") { this.editField = null; return; }
418
+ if (data === "\x7f" || data === "\b") { this.editBuffer = this.editBuffer.slice(0, -1); return; }
419
+ if (data.length === 1 && data >= " " && data <= "~") { this.editBuffer += data; }
420
+ }
421
+ }
422
+
423
+ // ---------------------------------------------------------------------------
424
+ // Main Overlay
425
+ // ---------------------------------------------------------------------------
426
+
427
+ class SettingsOverlay {
428
+ private config: Record<string, unknown>;
429
+ private theme: CrewTheme;
430
+ private callbacks: SettingsOverlayCallbacks;
431
+ private currentTabIndex = 0;
432
+ private selectedIndex = 0;
433
+ private scrollOffset = 0;
434
+ private maxVisible = 10;
435
+ private submenu: SelectSubmenu | TextinputSubmenu | AgentOverridesSubmenu | null = null;
436
+ private submenuSettingId: string | null = null;
437
+ private changedValues = new Map<string, unknown>();
438
+
439
+ constructor(config: Record<string, unknown>, theme: CrewTheme, callbacks: SettingsOverlayCallbacks) {
440
+ this.config = config;
441
+ this.theme = theme;
442
+ this.callbacks = callbacks;
443
+ }
444
+
445
+ invalidate(): void {
446
+ this.submenu?.invalidate();
447
+ }
448
+
449
+ render(width: number): string[] {
450
+ // Border wrapper — same style as RunDashboard
451
+ const innerWidth = Math.max(30, width - 4);
452
+ const borderWidth = Math.min(innerWidth, Math.max(0, width - 2));
453
+ const fg = (color: Parameters<CrewTheme["fg"]>[0], text: string) => this.theme.fg(color, text);
454
+ const borderFill = (count: number) => new DynamicCrewBorder(this.theme).render(Math.max(0, count))[0];
455
+ const border = (left: string, right: string) => `${fg("border", left)}${borderFill(borderWidth)}${fg("border", right)}`;
456
+ const row = (text: string) => `│ ${padToWidth(truncateToWidth(text, innerWidth - 1), innerWidth - 1)}│`;
457
+
458
+ const lines: string[] = [];
459
+
460
+ // ── Title bar ──
461
+ lines.push(border("╭", "╮"));
462
+ lines.push(row(`${fg("accent", "▐")} ${this.theme.bold("pi-crew Settings")}`));
463
+
464
+ // ── Tab bar ──
465
+ const tabLine = this.renderTabBarContent(innerWidth - 2);
466
+ lines.push(row(tabLine));
467
+ lines.push(border("├", "┤"));
468
+
469
+ // ── Content ──
470
+ const content = this.submenu
471
+ ? this.renderSubmenuContent(innerWidth - 4)
472
+ : this.renderSettingsContent(innerWidth - 4);
473
+ for (const line of content) {
474
+ lines.push(row(` ${truncateToWidth(line, innerWidth - 2)}`));
475
+ }
476
+
477
+ // ── Bottom border ──
478
+ lines.push(border("╰", "╯"));
479
+
480
+ return lines;
481
+ }
482
+
483
+ private renderTabBarContent(innerWidth: number): string {
484
+ const parts: string[] = [];
485
+ for (const [i, tab] of TABS.entries()) {
486
+ const isActive = i === this.currentTabIndex;
487
+ const text = `${tab.icon} ${tab.label}`;
488
+ parts.push(isActive
489
+ ? this.theme.bold(this.theme.fg("accent", text))
490
+ : this.theme.fg("dim", text),
491
+ );
492
+ }
493
+ return parts.join(" " + this.theme.fg("border", "│") + " ");
494
+ }
495
+
496
+ private renderSettingsContent(innerWidth: number): string[] {
497
+ const tabId = TABS[this.currentTabIndex]?.id ?? "runtime";
498
+ const settings = SETTINGS.filter(s => s.tab === tabId);
499
+ const lines: string[] = [];
500
+
501
+ // Calculate max label width for alignment
502
+ const maxLabelWidth = Math.min(28, Math.max(...settings.map(s => visibleWidth(s.label))));
503
+
504
+ // Render visible items
505
+ const startIdx = this.scrollOffset;
506
+ const endIdx = Math.min(startIdx + this.maxVisible, settings.length);
507
+
508
+ for (let i = startIdx; i < endIdx; i++) {
509
+ const def = settings[i];
510
+ if (!def) continue;
511
+ const isSelected = i === this.selectedIndex;
512
+
513
+ const effective = this.changedValues.has(def.id)
514
+ ? this.changedValues.get(def.id)
515
+ : getNestedValue(this.config, def.id);
516
+ const isDefault = !this.changedValues.has(def.id) && !isExplicitlySet(this.config, def.id);
517
+ const valueStr = formatValue(effective, def.id);
518
+ const suffix = isDefault && (effective !== undefined || EFFECTIVE_DEFAULTS[def.id] !== undefined) ? " (default)" : "";
519
+
520
+ const prefix = isSelected ? " → " : " ";
521
+ const labelPad = padToWidth(def.label, maxLabelWidth);
522
+ const valueMax = innerWidth - maxLabelWidth - 6 - prefix.length - suffix.length;
523
+ const valueText = truncateToWidth(valueStr, Math.max(10, valueMax));
524
+ const line = `${prefix}${labelPad} ${this.theme.fg(isSelected ? "accent" : "muted", valueText)}${suffix ? this.theme.fg("dim", suffix) : ""}`;
525
+
526
+ if (isSelected) {
527
+ lines.push(this.theme.inverse?.(truncateToWidth(line, innerWidth)) ?? truncateToWidth(line, innerWidth));
528
+ } else {
529
+ lines.push(truncateToWidth(line, innerWidth));
530
+ }
531
+ }
532
+
533
+ // Scroll indicator
534
+ if (startIdx > 0 || endIdx < settings.length) {
535
+ const remaining = settings.length - endIdx;
536
+ const count = startIdx > 0 ? `↑${startIdx}` : "";
537
+ const below = remaining > 0 ? `↓${remaining}` : "";
538
+ const parts = [count, below].filter(Boolean);
539
+ if (parts.length > 0) {
540
+ lines.push(this.theme.fg("dim", ` (${this.selectedIndex + 1}/${settings.length}) ${parts.join(" ")}`));
541
+ }
542
+ }
543
+
544
+ // Description
545
+ const selectedDef = settings[this.selectedIndex];
546
+ if (selectedDef?.description) {
547
+ lines.push("");
548
+ lines.push(this.theme.fg("muted", ` ${selectedDef.description}`));
549
+ }
550
+
551
+ // Hints
552
+ lines.push("");
553
+ lines.push(this.theme.fg("dim", " ↑↓ Navigate · Enter/Space change · Tab switch · Esc close"));
554
+
555
+ return lines;
556
+ }
557
+
558
+ private renderSubmenuContent(innerWidth: number): string[] {
559
+ if (!this.submenu) return [];
560
+ return this.submenu.render(innerWidth);
561
+ }
562
+
563
+ handleInput(data: string): void {
564
+ // Submenu takes priority
565
+ if (this.submenu) {
566
+ this.submenu.handleInput(data);
567
+ return;
568
+ }
569
+
570
+ // Escape closes overlay
571
+ if (data === "\x1b" || data === "q") {
572
+ this.callbacks.onClose();
573
+ return;
574
+ }
575
+
576
+ // Tab navigation
577
+ if (data === "\t" || data === "\x1b[C") {
578
+ this.currentTabIndex = (this.currentTabIndex + 1) % TABS.length;
579
+ this.selectedIndex = 0;
580
+ this.scrollOffset = 0;
581
+ return;
582
+ }
583
+ if (data === "Z" || data === "\x1b[D") {
584
+ this.currentTabIndex = (this.currentTabIndex - 1 + TABS.length) % TABS.length;
585
+ this.selectedIndex = 0;
586
+ this.scrollOffset = 0;
587
+ return;
588
+ }
589
+
590
+ // Item navigation
591
+ const tabId = TABS[this.currentTabIndex]?.id ?? "runtime";
592
+ const settings = SETTINGS.filter(s => s.tab === tabId);
593
+
594
+ if (data === "\x1b[A" || data === "k") {
595
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
596
+ this.ensureVisible(settings.length);
597
+ return;
598
+ }
599
+ if (data === "\x1b[B" || data === "j") {
600
+ this.selectedIndex = Math.min(settings.length - 1, this.selectedIndex + 1);
601
+ this.ensureVisible(settings.length);
602
+ return;
603
+ }
604
+
605
+ // Activate item
606
+ if (data === "\r" || data === "\n" || data === " ") {
607
+ this.activateItem(settings);
608
+ }
609
+ }
610
+
611
+ private activateItem(settings: SettingDef[]): void {
612
+ const def = settings[this.selectedIndex];
613
+ if (!def) return;
614
+
615
+ const current = this.changedValues.has(def.id) ? this.changedValues.get(def.id) : getNestedValue(this.config, def.id);
616
+
617
+ switch (def.type) {
618
+ case "boolean": {
619
+ const newVal = current !== true;
620
+ this.changedValues.set(def.id, newVal);
621
+ this.callbacks.onChange(def.id, newVal);
622
+ break;
623
+ }
624
+ case "enum": {
625
+ if (!def.values?.length) return;
626
+ this.submenuSettingId = def.id;
627
+ this.submenu = new SelectSubmenu(
628
+ def.label,
629
+ def.description ?? "",
630
+ def.values,
631
+ typeof current === "string" ? current : def.values[0]!,
632
+ this.theme,
633
+ (value: string) => {
634
+ this.changedValues.set(def.id, value);
635
+ this.callbacks.onChange(def.id, value);
636
+ this.submenu = null;
637
+ this.submenuSettingId = null;
638
+ },
639
+ () => { this.submenu = null; this.submenuSettingId = null; },
640
+ );
641
+ break;
642
+ }
643
+ case "number": {
644
+ this.submenuSettingId = def.id;
645
+ this.submenu = new TextinputSubmenu(
646
+ def.label,
647
+ def.description ?? "",
648
+ typeof current === "number" ? String(current) : "",
649
+ this.theme,
650
+ (value: string) => {
651
+ const num = value === "" ? undefined : Number(value);
652
+ if (num !== undefined && !Number.isNaN(num)) {
653
+ this.changedValues.set(def.id, num);
654
+ this.callbacks.onChange(def.id, num);
655
+ } else if (value === "") {
656
+ this.changedValues.set(def.id, undefined);
657
+ this.callbacks.onChange(def.id, undefined);
658
+ }
659
+ this.submenu = null;
660
+ this.submenuSettingId = null;
661
+ },
662
+ () => { this.submenu = null; this.submenuSettingId = null; },
663
+ );
664
+ break;
665
+ }
666
+ case "string": {
667
+ this.submenuSettingId = def.id;
668
+ this.submenu = new TextinputSubmenu(
669
+ def.label,
670
+ def.description ?? "",
671
+ typeof current === "string" ? current : "",
672
+ this.theme,
673
+ (value: string) => {
674
+ this.changedValues.set(def.id, value || undefined);
675
+ this.callbacks.onChange(def.id, value || undefined);
676
+ this.submenu = null;
677
+ this.submenuSettingId = null;
678
+ },
679
+ () => { this.submenu = null; this.submenuSettingId = null; },
680
+ );
681
+ break;
682
+ }
683
+ case "agent": {
684
+ this.submenu = new AgentOverridesSubmenu(
685
+ this.config,
686
+ this.theme,
687
+ (overrides: Record<string, unknown>) => {
688
+ this.changedValues.set("agents.overrides", overrides);
689
+ this.callbacks.onChange("agents.overrides", overrides);
690
+ this.submenu = null;
691
+ this.submenuSettingId = null;
692
+ },
693
+ () => { this.submenu = null; this.submenuSettingId = null; },
694
+ );
695
+ break;
696
+ }
697
+ }
698
+ }
699
+
700
+ private ensureVisible(count: number): void {
701
+ if (this.selectedIndex < this.scrollOffset) {
702
+ this.scrollOffset = this.selectedIndex;
703
+ } else if (this.selectedIndex >= this.scrollOffset + this.maxVisible) {
704
+ this.scrollOffset = Math.max(0, this.selectedIndex - this.maxVisible + 1);
705
+ }
706
+ }
707
+ }
708
+
709
+ // ---------------------------------------------------------------------------
710
+ // Public factory
711
+ // ---------------------------------------------------------------------------
712
+
713
+ export function createSettingsOverlay(
714
+ config: Record<string, unknown>,
715
+ theme: CrewTheme,
716
+ onChange: (id: string, value: unknown) => void,
717
+ done: () => void,
718
+ ) {
719
+ const overlay = new SettingsOverlay(config, theme, { onChange, onClose: done });
720
+ return { overlay, component: overlay };
721
+ }