pi-crew 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +57 -32
- package/CHANGELOG.md +466 -448
- package/LICENSE +21 -21
- package/NOTICE.md +16 -16
- package/README.md +323 -323
- package/docs/FEATURE_INTAKE.md +126 -0
- package/docs/HARNESS.md +86 -0
- package/docs/HARNESS_BACKLOG.md +41 -0
- package/docs/TEST_MATRIX.md +49 -0
- package/docs/actions-reference.md +595 -595
- package/docs/architecture.md +180 -180
- package/docs/code-review-2026-05-11.md +592 -592
- package/docs/commands-reference.md +347 -347
- package/docs/comparison-pi-subagents-vs-pi-crew.md +303 -0
- package/docs/decisions/0001-durable-state.md +41 -0
- package/docs/decisions/0002-child-process-for-async.md +42 -0
- package/docs/decisions/0003-depth-guard.md +36 -0
- package/docs/decisions/0004-execfile-over-exec.md +34 -0
- package/docs/decisions/0005-no-parameter-properties.md +49 -0
- package/docs/decisions/0006-publish-bundled-esm.md +63 -0
- package/docs/decisions/0007-active-run-binary-index.md +54 -0
- package/docs/decisions/0008-child-pi-warm-pool.md +61 -0
- package/docs/decisions/README.md +23 -0
- package/docs/followup-review-round4-2026-05-13.md +107 -0
- package/docs/implementation-plan-top3.md +333 -0
- package/docs/live-mailbox-runtime.md +36 -36
- package/docs/next-upgrade-roadmap.md +808 -808
- package/docs/oh-my-pi-research.md +509 -0
- package/docs/perf/baseline-2026-05.md +113 -0
- package/docs/perf/final-report-2026-05.md +206 -0
- package/docs/perf/sprint-1-report.md +71 -0
- package/docs/perf/sprint-2-report.md +81 -0
- package/docs/perf/sprint-2.5-report.md +53 -0
- package/docs/perf/sprint-3-report.md +36 -0
- package/docs/perf/sprint-4-report.md +47 -0
- package/docs/perf/sprint-5-report.md +51 -0
- package/docs/perf/sprint-6-report.md +94 -0
- package/docs/perf/sprint-7-report.md +74 -0
- package/docs/perf/upgrade-plan-2026-05.md +147 -0
- package/docs/pi-subagents3-deep-analysis.md +508 -0
- package/docs/product/README.md +31 -0
- package/docs/product/platform.md +27 -0
- package/docs/product/runtime-safety.md +37 -0
- package/docs/product/team-run.md +39 -0
- package/docs/product/team-tool.md +37 -0
- package/docs/publishing.md +65 -65
- package/docs/resource-formats.md +134 -134
- package/docs/runtime-analysis-child-vs-live.md +171 -0
- package/docs/runtime-flow.md +148 -148
- package/docs/runtime-migration-in-process-analysis.md +250 -0
- package/docs/stories/README.md +30 -0
- package/docs/stories/backlog.md +36 -0
- package/docs/templates/decision.md +27 -0
- package/docs/templates/story.md +44 -0
- package/docs/templates/validation-report.md +32 -0
- package/docs/usage.md +238 -238
- package/index.ts +7 -6
- package/install.mjs +65 -65
- package/package.json +107 -100
- package/schema.json +222 -222
- package/skills/child-pi-spawning/SKILL.md +213 -0
- package/skills/context-artifact-hygiene/SKILL.md +32 -0
- package/skills/event-log-tracing/SKILL.md +299 -0
- package/skills/git-master/SKILL.md +225 -24
- package/skills/live-agent-lifecycle/SKILL.md +192 -0
- package/skills/mailbox-interactive/SKILL.md +300 -19
- package/skills/model-routing-context/SKILL.md +94 -0
- package/skills/multi-perspective-review/SKILL.md +88 -0
- package/skills/read-only-explorer/SKILL.md +250 -26
- package/skills/safe-bash/SKILL.md +307 -21
- package/skills/verification-before-done/SKILL.md +11 -2
- package/skills/widget-rendering/SKILL.md +258 -0
- package/skills/workspace-isolation/SKILL.md +202 -0
- package/skills/worktree-isolation/SKILL.md +202 -18
- package/src/adapters/claude-adapter.ts +25 -25
- package/src/adapters/codex-adapter.ts +21 -21
- package/src/adapters/cursor-adapter.ts +17 -17
- package/src/adapters/export-util.ts +137 -137
- package/src/adapters/index.ts +15 -15
- package/src/adapters/registry.ts +18 -18
- package/src/adapters/types.ts +23 -23
- package/src/agents/agent-config.ts +38 -38
- package/src/agents/agent-serializer.ts +38 -38
- package/src/agents/discover-agents.ts +121 -118
- package/src/config/config.ts +740 -858
- package/src/config/defaults.ts +96 -96
- package/src/config/drift-detector.ts +211 -211
- package/src/config/markers.ts +327 -327
- package/src/config/resilient-parser.ts +109 -108
- package/src/config/suggestions.ts +74 -74
- package/src/config/types.ts +199 -0
- package/src/extension/async-notifier.ts +123 -89
- package/src/extension/autonomous-policy.ts +169 -169
- package/src/extension/cross-extension-rpc.ts +104 -104
- package/src/extension/help.ts +47 -47
- package/src/extension/import-index.ts +69 -69
- package/src/extension/management.ts +395 -382
- package/src/extension/notification-router.ts +116 -116
- package/src/extension/notification-sink.ts +51 -51
- package/src/extension/project-init.ts +168 -168
- package/src/extension/register.ts +859 -668
- package/src/extension/registration/artifact-cleanup.ts +15 -15
- package/src/extension/registration/command-utils.ts +54 -54
- package/src/extension/registration/commands.ts +559 -452
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +102 -102
- package/src/extension/registration/subagent-tools.ts +220 -159
- package/src/extension/registration/team-tool.ts +159 -99
- package/src/extension/registration/viewers.ts +29 -0
- package/src/extension/result-watcher.ts +128 -128
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-export.ts +73 -73
- package/src/extension/run-import.ts +84 -84
- package/src/extension/run-index.ts +94 -94
- package/src/extension/run-maintenance.ts +142 -142
- package/src/extension/session-summary.ts +8 -8
- package/src/extension/team-manager-command.ts +96 -96
- package/src/extension/team-recommendation.ts +188 -188
- package/src/extension/team-tool/api.ts +5 -2
- package/src/extension/team-tool/cancel.ts +224 -209
- package/src/extension/team-tool/config-patch.ts +36 -36
- package/src/extension/team-tool/context.ts +60 -60
- package/src/extension/team-tool/doctor.ts +242 -242
- package/src/extension/team-tool/handle-settings.ts +421 -195
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +139 -139
- package/src/extension/team-tool/parallel-dispatch.ts +156 -156
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +112 -111
- package/src/extension/team-tool/run.ts +246 -229
- package/src/extension/team-tool/status.ts +110 -110
- package/src/extension/team-tool-types.ts +13 -13
- package/src/extension/team-tool.ts +344 -344
- package/src/extension/tool-result.ts +16 -16
- package/src/extension/validate-resources.ts +77 -77
- package/src/hooks/registry.ts +61 -61
- package/src/hooks/types.ts +40 -40
- package/src/i18n.ts +184 -184
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +68 -68
- package/src/observability/exporters/adapter.ts +30 -30
- package/src/observability/exporters/otlp-exporter.ts +106 -92
- package/src/observability/exporters/prometheus-exporter.ts +54 -54
- package/src/observability/metric-registry.ts +87 -87
- package/src/observability/metric-retention.ts +54 -54
- package/src/observability/metric-sink.ts +81 -56
- package/src/observability/metrics-primitives.ts +167 -167
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/adaptive-plan.ts +338 -0
- package/src/runtime/agent-control.ts +169 -169
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/async-runner.ts +153 -153
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/auto-resume.ts +100 -100
- package/src/runtime/background-runner.ts +122 -89
- package/src/runtime/cancellation.ts +61 -61
- package/src/runtime/capability-inventory.ts +116 -116
- package/src/runtime/child-pi-pool.ts +68 -0
- package/src/runtime/child-pi.ts +541 -461
- package/src/runtime/code-summary.ts +247 -247
- package/src/runtime/compaction-summary.ts +271 -271
- package/src/runtime/concurrency.ts +58 -58
- package/src/runtime/crash-recovery.ts +317 -301
- package/src/runtime/crew-agent-records.ts +379 -281
- package/src/runtime/crew-agent-runtime.ts +60 -60
- package/src/runtime/cross-extension-rpc.ts +72 -0
- package/src/runtime/custom-tools/irc-tool.ts +201 -201
- package/src/runtime/custom-tools/submit-result-tool.ts +90 -90
- package/src/runtime/deadletter.ts +47 -47
- package/src/runtime/delivery-coordinator.ts +176 -176
- package/src/runtime/delta-conflict.ts +360 -360
- package/src/runtime/diagnostic-export.ts +102 -102
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/effectiveness.ts +82 -81
- package/src/runtime/errors/crew-errors.ts +166 -0
- package/src/runtime/event-stream-bridge.ts +92 -92
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +234 -106
- package/src/runtime/heartbeat-watcher.ts +145 -124
- package/src/runtime/iteration-hooks.ts +267 -267
- package/src/runtime/live-agent-control.ts +88 -88
- package/src/runtime/live-agent-manager.ts +377 -179
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +676 -600
- package/src/runtime/loop-gates.ts +129 -129
- package/src/runtime/manifest-cache.ts +263 -263
- package/src/runtime/mcp-proxy.ts +113 -113
- package/src/runtime/metric-parser.ts +40 -40
- package/src/runtime/model-fallback.ts +282 -274
- package/src/runtime/model-resolver.ts +118 -0
- package/src/runtime/output-validator.ts +187 -187
- package/src/runtime/overflow-recovery.ts +175 -175
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +156 -156
- package/src/runtime/parent-guard.ts +80 -80
- package/src/runtime/phase-progress.ts +217 -217
- package/src/runtime/pi-args.ts +165 -165
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +167 -167
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/post-checks.ts +125 -125
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +97 -73
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +81 -81
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/run-tracker.ts +99 -0
- package/src/runtime/runtime-policy.ts +21 -0
- package/src/runtime/runtime-resolver.ts +94 -91
- package/src/runtime/scheduler.ts +294 -0
- package/src/runtime/semaphore.ts +131 -131
- package/src/runtime/sensitive-paths.ts +92 -92
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/settings-store.ts +103 -0
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/skill-instructions.ts +222 -222
- package/src/runtime/stale-reconciler.ts +198 -189
- package/src/runtime/streaming-output.ts +47 -0
- package/src/runtime/subagent-manager.ts +404 -400
- package/src/runtime/subprocess-tool-registry.ts +67 -67
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-graph-scheduler.ts +122 -122
- package/src/runtime/task-graph.ts +207 -207
- package/src/runtime/task-output-context.ts +177 -177
- package/src/runtime/task-packet.ts +93 -93
- package/src/runtime/task-quality.ts +207 -207
- package/src/runtime/task-runner/capabilities.ts +78 -78
- package/src/runtime/task-runner/live-executor.ts +131 -113
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +139 -139
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/run-projection.ts +103 -103
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +469 -459
- package/src/runtime/team-runner.ts +693 -945
- package/src/runtime/usage-tracker.ts +71 -0
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/runtime/workflow-state.ts +187 -187
- package/src/runtime/yield-handler.ts +190 -190
- package/src/schema/config-schema.ts +172 -168
- package/src/schema/team-tool-schema.ts +126 -126
- package/src/schema/validation-types.ts +151 -148
- package/src/skills/discover-skills.ts +67 -67
- package/src/skills/skill-templates.ts +374 -374
- package/src/state/active-run-registry.ts +227 -191
- package/src/state/artifact-store.ts +130 -129
- package/src/state/atomic-write.ts +262 -195
- package/src/state/blob-store.ts +116 -116
- package/src/state/contracts.ts +111 -111
- package/src/state/event-log-rotation.ts +161 -158
- package/src/state/event-log.ts +383 -303
- package/src/state/event-reconstructor.ts +217 -217
- package/src/state/jsonl-writer.ts +82 -82
- package/src/state/locks.ts +146 -146
- package/src/state/mailbox.ts +446 -405
- package/src/state/state-store.ts +364 -351
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +285 -285
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +116 -116
- package/src/teams/team-config.ts +27 -27
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/agent-management-overlay.ts +144 -144
- package/src/ui/crew-widget.ts +487 -370
- package/src/ui/dashboard-panes/agents-pane.ts +109 -28
- package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
- package/src/ui/dashboard-panes/capability-pane.ts +59 -59
- package/src/ui/dashboard-panes/health-pane.ts +30 -30
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
- package/src/ui/dashboard-panes/progress-pane.ts +30 -30
- package/src/ui/dashboard-panes/transcript-pane.ts +10 -10
- package/src/ui/heartbeat-aggregator.ts +63 -63
- package/src/ui/keybinding-map.ts +97 -94
- package/src/ui/live-conversation-overlay.ts +152 -0
- package/src/ui/live-run-sidebar.ts +180 -180
- package/src/ui/mascot.ts +442 -442
- package/src/ui/overlays/agent-picker-overlay.ts +57 -57
- package/src/ui/overlays/confirm-overlay.ts +58 -58
- package/src/ui/overlays/mailbox-compose-overlay.ts +144 -144
- package/src/ui/overlays/mailbox-compose-preview.ts +63 -63
- package/src/ui/overlays/mailbox-detail-overlay.ts +122 -122
- package/src/ui/pi-ui-compat.ts +57 -57
- package/src/ui/powerbar-publisher.ts +221 -197
- package/src/ui/render-scheduler.ts +216 -143
- package/src/ui/run-action-dispatcher.ts +118 -118
- package/src/ui/run-dashboard.ts +526 -464
- package/src/ui/run-event-bus.ts +208 -208
- package/src/ui/run-snapshot-cache.ts +826 -777
- package/src/ui/settings-overlay.ts +721 -0
- package/src/ui/snapshot-types.ts +86 -70
- package/src/ui/theme-adapter.ts +190 -190
- package/src/ui/tool-progress-formatter.ts +89 -0
- package/src/ui/transcript-cache.ts +94 -94
- package/src/ui/transcript-viewer.ts +335 -335
- package/src/utils/conflict-detect.ts +662 -0
- package/src/utils/file-coalescer.ts +86 -86
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/fs-watch.ts +88 -31
- package/src/utils/gh-protocol.ts +479 -0
- package/src/utils/ids.ts +17 -17
- package/src/utils/incremental-reader.ts +104 -104
- package/src/utils/internal-error.ts +6 -6
- package/src/utils/names.ts +27 -27
- package/src/utils/paths.ts +102 -63
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/scan-cache.ts +136 -136
- package/src/utils/sse-parser.ts +134 -134
- package/src/utils/task-name-generator.ts +337 -337
- package/src/utils/timings.ts +33 -33
- package/src/utils/visual.ts +243 -198
- package/src/workflows/discover-workflows.ts +139 -139
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/workflows/workflow-config.ts +26 -26
- package/src/workflows/workflow-serializer.ts +32 -32
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/cleanup.ts +75 -75
- package/src/worktree/worktree-manager.ts +188 -188
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/tsconfig.json +19 -19
- package/workflows/default.workflow.md +30 -30
- package/workflows/fast-fix.workflow.md +23 -23
- package/workflows/implementation.workflow.md +43 -43
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
- package/skills/task-packet/SKILL.md +0 -28
- 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
|
+
}
|