@pellux/goodvibes-tui 1.0.0 → 1.7.0

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 (209) hide show
  1. package/CHANGELOG.md +130 -127
  2. package/README.md +32 -14
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
  6. package/src/cli/entrypoint.ts +2 -0
  7. package/src/core/alert-gating.ts +67 -0
  8. package/src/core/approval-alert.ts +72 -0
  9. package/src/core/budget-breach-notifier.ts +106 -0
  10. package/src/core/conversation-rendering.ts +19 -0
  11. package/src/core/conversation.ts +58 -0
  12. package/src/core/focus-tracker.ts +41 -0
  13. package/src/core/long-task-notifier.ts +33 -6
  14. package/src/core/stream-event-wiring.ts +104 -2
  15. package/src/core/stream-stall-watchdog.ts +41 -17
  16. package/src/core/system-message-router.ts +37 -51
  17. package/src/core/turn-cancellation.ts +20 -0
  18. package/src/core/turn-event-wiring.ts +64 -3
  19. package/src/export/cost-utils.ts +126 -8
  20. package/src/input/command-registry.ts +54 -1
  21. package/src/input/commands/checkpoint-runtime.ts +280 -0
  22. package/src/input/commands/codebase-runtime.ts +192 -0
  23. package/src/input/commands/diff-runtime.ts +61 -30
  24. package/src/input/commands/eval.ts +1 -1
  25. package/src/input/commands/health-runtime.ts +1 -1
  26. package/src/input/commands/image-runtime.ts +112 -0
  27. package/src/input/commands/intelligence-runtime.ts +11 -1
  28. package/src/input/commands/local-auth-runtime.ts +4 -1
  29. package/src/input/commands/local-runtime.ts +9 -3
  30. package/src/input/commands/marketplace-runtime.ts +2 -2
  31. package/src/input/commands/memory.ts +6 -1
  32. package/src/input/commands/operator-panel-runtime.ts +40 -24
  33. package/src/input/commands/planning-runtime.ts +26 -3
  34. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  35. package/src/input/commands/plugin-runtime.ts +2 -2
  36. package/src/input/commands/policy-dispatch.ts +21 -0
  37. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  38. package/src/input/commands/qrcode-runtime.ts +3 -3
  39. package/src/input/commands/recall-review.ts +35 -0
  40. package/src/input/commands/remote-runtime.ts +3 -3
  41. package/src/input/commands/runtime-services.ts +54 -13
  42. package/src/input/commands/services-runtime.ts +1 -1
  43. package/src/input/commands/session-content.ts +12 -0
  44. package/src/input/commands/session-workflow.ts +19 -1
  45. package/src/input/commands/settings-sync-runtime.ts +1 -1
  46. package/src/input/commands/share-runtime.ts +9 -2
  47. package/src/input/commands/shell-core.ts +15 -7
  48. package/src/input/commands/skills-runtime.ts +2 -8
  49. package/src/input/commands/subscription-runtime.ts +2 -2
  50. package/src/input/commands/test-runtime.ts +277 -0
  51. package/src/input/commands/websearch-runtime.ts +101 -0
  52. package/src/input/commands/work-plan-runtime.ts +36 -10
  53. package/src/input/commands/workstream-runtime.ts +338 -0
  54. package/src/input/commands.ts +12 -0
  55. package/src/input/config-modal-types.ts +161 -0
  56. package/src/input/config-modal.ts +377 -0
  57. package/src/input/feed-context-factory.ts +7 -8
  58. package/src/input/handler-command-route.ts +27 -17
  59. package/src/input/handler-content-actions.ts +22 -8
  60. package/src/input/handler-feed-routes.ts +107 -7
  61. package/src/input/handler-feed.ts +49 -13
  62. package/src/input/handler-interactions.ts +3 -3
  63. package/src/input/handler-modal-routes.ts +65 -0
  64. package/src/input/handler-modal-stack.ts +3 -5
  65. package/src/input/handler-modal-token-routes.ts +16 -49
  66. package/src/input/handler-picker-routes.ts +11 -94
  67. package/src/input/handler-shortcuts.ts +24 -14
  68. package/src/input/handler-types.ts +3 -6
  69. package/src/input/handler-ui-state.ts +10 -22
  70. package/src/input/handler.ts +10 -28
  71. package/src/input/keybindings.ts +22 -8
  72. package/src/input/model-picker-types.ts +24 -6
  73. package/src/input/model-picker.ts +58 -0
  74. package/src/input/panel-integration-actions.ts +9 -170
  75. package/src/input/settings-modal-data.ts +95 -0
  76. package/src/input/settings-modal-mutations.ts +6 -4
  77. package/src/main.ts +41 -44
  78. package/src/panels/agent-inspector-shared.ts +35 -11
  79. package/src/panels/base-panel.ts +22 -1
  80. package/src/panels/builtin/agent.ts +21 -107
  81. package/src/panels/builtin/development.ts +16 -62
  82. package/src/panels/builtin/knowledge.ts +8 -32
  83. package/src/panels/builtin/operations.ts +84 -428
  84. package/src/panels/builtin/session.ts +21 -112
  85. package/src/panels/builtin/shared.ts +9 -43
  86. package/src/panels/builtin-modals.ts +218 -0
  87. package/src/panels/builtin-panels.ts +5 -0
  88. package/src/panels/cost-tracker-panel.ts +63 -14
  89. package/src/panels/diff-panel.ts +123 -60
  90. package/src/panels/eval-registry.ts +60 -0
  91. package/src/panels/fleet-panel.ts +800 -0
  92. package/src/panels/fleet-read-model.ts +477 -0
  93. package/src/panels/fleet-steer.ts +92 -0
  94. package/src/panels/fleet-stop.ts +125 -0
  95. package/src/panels/fleet-tabs.ts +230 -0
  96. package/src/panels/fleet-transcript.ts +356 -0
  97. package/src/panels/git-panel.ts +9 -9
  98. package/src/panels/index.ts +7 -31
  99. package/src/panels/local-auth-panel.ts +10 -0
  100. package/src/panels/modals/hooks-modal.ts +187 -0
  101. package/src/panels/modals/keybindings-modal.ts +151 -0
  102. package/src/panels/modals/knowledge-modal.ts +158 -0
  103. package/src/panels/modals/local-auth-modal.ts +132 -0
  104. package/src/panels/modals/marketplace-modal.ts +218 -0
  105. package/src/panels/modals/memory-modal.ts +180 -0
  106. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  107. package/src/panels/modals/modal-theme.ts +36 -0
  108. package/src/panels/modals/pairing-modal.ts +144 -0
  109. package/src/panels/modals/planning-modal.ts +282 -0
  110. package/src/panels/modals/plugins-modal.ts +174 -0
  111. package/src/panels/modals/policy-modal.ts +256 -0
  112. package/src/panels/modals/provider-health-modal.ts +136 -0
  113. package/src/panels/modals/remote-modal.ts +115 -0
  114. package/src/panels/modals/sandbox-modal.ts +150 -0
  115. package/src/panels/modals/security-modal.ts +217 -0
  116. package/src/panels/modals/services-modal.ts +179 -0
  117. package/src/panels/modals/settings-sync-modal.ts +142 -0
  118. package/src/panels/modals/skills-modal.ts +189 -0
  119. package/src/panels/modals/subscription-modal.ts +172 -0
  120. package/src/panels/modals/work-plan-modal.ts +149 -0
  121. package/src/panels/panel-confirm-overlay.ts +72 -0
  122. package/src/panels/panel-manager.ts +108 -5
  123. package/src/panels/project-planning-answer-actions.ts +4 -2
  124. package/src/panels/scrollable-list-panel.ts +9 -0
  125. package/src/panels/skills-panel.ts +7 -51
  126. package/src/panels/token-budget-panel.ts +5 -3
  127. package/src/panels/types.ts +26 -0
  128. package/src/permissions/hunk-selection.ts +179 -0
  129. package/src/permissions/prompt.ts +60 -4
  130. package/src/renderer/compaction-history-modal.ts +19 -3
  131. package/src/renderer/compaction-preview.ts +13 -2
  132. package/src/renderer/compaction-quality.ts +100 -0
  133. package/src/renderer/config-modal.ts +81 -0
  134. package/src/renderer/conversation-overlays.ts +10 -17
  135. package/src/renderer/fleet-tab-strip.ts +56 -0
  136. package/src/renderer/footer-tips.ts +10 -2
  137. package/src/renderer/git-status.ts +40 -0
  138. package/src/renderer/help-overlay.ts +2 -2
  139. package/src/renderer/model-workspace.ts +44 -1
  140. package/src/renderer/panel-workspace-bar.ts +8 -2
  141. package/src/renderer/shell-surface.ts +8 -1
  142. package/src/renderer/turn-injection.ts +114 -0
  143. package/src/renderer/ui-factory.ts +91 -7
  144. package/src/runtime/bootstrap-command-context.ts +24 -1
  145. package/src/runtime/bootstrap-command-parts.ts +36 -7
  146. package/src/runtime/bootstrap-core.ts +19 -4
  147. package/src/runtime/bootstrap-hook-bridge.ts +5 -0
  148. package/src/runtime/bootstrap-shell.ts +41 -17
  149. package/src/runtime/bootstrap.ts +11 -17
  150. package/src/runtime/code-index-services.ts +112 -0
  151. package/src/runtime/orchestrator-core-services.ts +49 -0
  152. package/src/runtime/process-lifecycle.ts +3 -1
  153. package/src/runtime/services.ts +91 -43
  154. package/src/runtime/ui-services.ts +8 -0
  155. package/src/runtime/workstream-services.ts +195 -0
  156. package/src/shell/blocking-input.ts +22 -1
  157. package/src/shell/ui-openers.ts +129 -17
  158. package/src/utils/format-duration.ts +8 -18
  159. package/src/utils/splash-lines.ts +1 -1
  160. package/src/version.ts +1 -1
  161. package/src/panels/agent-inspector-panel.ts +0 -786
  162. package/src/panels/approval-panel.ts +0 -252
  163. package/src/panels/automation-control-panel.ts +0 -479
  164. package/src/panels/cockpit-panel.ts +0 -481
  165. package/src/panels/cockpit-read-model.ts +0 -233
  166. package/src/panels/communication-panel.ts +0 -256
  167. package/src/panels/control-plane-panel.ts +0 -470
  168. package/src/panels/debug-panel.ts +0 -609
  169. package/src/panels/docs-panel.ts +0 -375
  170. package/src/panels/eval-panel.ts +0 -627
  171. package/src/panels/file-explorer-panel.ts +0 -664
  172. package/src/panels/file-preview-panel.ts +0 -517
  173. package/src/panels/hooks-panel.ts +0 -401
  174. package/src/panels/incident-review-panel.ts +0 -406
  175. package/src/panels/intelligence-panel.ts +0 -383
  176. package/src/panels/knowledge-graph-panel.ts +0 -506
  177. package/src/panels/marketplace-panel.ts +0 -399
  178. package/src/panels/memory-panel.ts +0 -558
  179. package/src/panels/ops-control-panel.ts +0 -329
  180. package/src/panels/ops-strategy-panel.ts +0 -321
  181. package/src/panels/orchestration-panel.ts +0 -465
  182. package/src/panels/panel-list-panel.ts +0 -557
  183. package/src/panels/plan-dashboard-panel.ts +0 -707
  184. package/src/panels/policy-panel.ts +0 -517
  185. package/src/panels/project-planning-panel.ts +0 -721
  186. package/src/panels/provider-health-panel.ts +0 -678
  187. package/src/panels/provider-health-tracker.ts +0 -306
  188. package/src/panels/provider-health-views.ts +0 -560
  189. package/src/panels/qr-panel.ts +0 -280
  190. package/src/panels/remote-panel.ts +0 -534
  191. package/src/panels/routes-panel.ts +0 -241
  192. package/src/panels/sandbox-panel.ts +0 -456
  193. package/src/panels/security-panel.ts +0 -447
  194. package/src/panels/services-panel.ts +0 -329
  195. package/src/panels/session-browser-panel.ts +0 -487
  196. package/src/panels/settings-sync-panel.ts +0 -398
  197. package/src/panels/subscription-panel.ts +0 -342
  198. package/src/panels/symbol-outline-panel.ts +0 -619
  199. package/src/panels/system-messages-panel.ts +0 -364
  200. package/src/panels/tasks-panel.ts +0 -608
  201. package/src/panels/thinking-panel.ts +0 -333
  202. package/src/panels/tool-inspector-panel.ts +0 -537
  203. package/src/panels/work-plan-panel.ts +0 -530
  204. package/src/panels/worktree-panel.ts +0 -360
  205. package/src/panels/wrfc-panel.ts +0 -790
  206. package/src/renderer/agent-detail-modal.ts +0 -465
  207. package/src/renderer/live-tail-modal.ts +0 -156
  208. package/src/renderer/process-modal.ts +0 -656
  209. package/src/renderer/qr-renderer.ts +0 -120
@@ -55,6 +55,7 @@ export interface UiPlatformServices {
55
55
  readonly tokenAuditor: RuntimeServices['tokenAuditor'];
56
56
  readonly replayEngine: RuntimeServices['replayEngine'];
57
57
  readonly webhookNotifier: RuntimeServices['webhookNotifier'];
58
+ readonly focusTracker: RuntimeServices['focusTracker'];
58
59
  readonly policyRuntimeState: RuntimeServices['policyRuntimeState'];
59
60
  readonly externalServices?: {
60
61
  inspect(): {
@@ -101,6 +102,10 @@ export interface UiRuntimeSharedServices {
101
102
  readonly distributedRuntime: RuntimeServices['distributedRuntime'];
102
103
  readonly remoteRunnerRegistry: RuntimeServices['remoteRunnerRegistry'] & RemoteRunnerRegistry;
103
104
  readonly remoteSupervisor: RuntimeServices['remoteSupervisor'] & RemoteSupervisor;
105
+ /** W2.2: the shared live process registry backing the Fleet panel. */
106
+ readonly processRegistry: RuntimeServices['processRegistry'];
107
+ /** Wave-3 (W3.2): the shared runtime event bus — the Fleet panel subscribes to its 'communication' domain for the honest steer-consumed signal. */
108
+ readonly runtimeBus: RuntimeServices['runtimeBus'];
104
109
  };
105
110
  }
106
111
 
@@ -167,6 +172,7 @@ export function createUiRuntimeServices(
167
172
  tokenAuditor: runtimeServices.tokenAuditor,
168
173
  replayEngine: runtimeServices.replayEngine,
169
174
  webhookNotifier: runtimeServices.webhookNotifier,
175
+ focusTracker: runtimeServices.focusTracker,
170
176
  policyRuntimeState: runtimeServices.policyRuntimeState,
171
177
  },
172
178
  planning: {
@@ -183,6 +189,8 @@ export function createUiRuntimeServices(
183
189
  distributedRuntime: runtimeServices.distributedRuntime,
184
190
  remoteRunnerRegistry: runtimeServices.remoteRunnerRegistry,
185
191
  remoteSupervisor: runtimeServices.remoteSupervisor,
192
+ processRegistry: runtimeServices.processRegistry,
193
+ runtimeBus: runtimeServices.runtimeBus,
186
194
  },
187
195
  events: createUiRuntimeEvents(runtimeServices.runtimeBus),
188
196
  readModels: createUiReadModels(runtimeServices, options),
@@ -0,0 +1,195 @@
1
+ // ---------------------------------------------------------------------------
2
+ // workstream-services.ts — Wave 4 (wo703)
3
+ //
4
+ // Constructs the TUI's OrchestrationEngine instance (@pellux/goodvibes-sdk/
5
+ // platform/orchestration, landed on SDK main as wo701/W4.1) and a thin
6
+ // command-facing facade around it. Extracted into its own module rather than
7
+ // built inline in services.ts: services.ts sits at the architecture check's
8
+ // 800-line cap (scripts/check-architecture.ts), so any new service gets its
9
+ // own construction module and a single wiring call there (mirrors
10
+ // createWorkflowServices's one-function-bundle shape).
11
+ //
12
+ // Why a facade, not the bare engine: createOrchestrationEngine() is a pure
13
+ // construction call with no auto-start and NO concept of a not-yet-launched
14
+ // "proposal" a human can review/edit before anything is spent — its only
15
+ // creation entry point, createWorkstream(), immediately materializes a real,
16
+ // ticking-eligible Workstream. The /workstream command module
17
+ // (input/commands/workstream-runtime.ts) needs a create -> propose -> approve
18
+ // -> launch flow (Pillar-3 doctrine: render the plan in the transcript before
19
+ // spending anything, mirroring /plan's approve step). REALITY-WINS DIVERGENCE
20
+ // from the wo703 design brief: the brief recommended an "engine-owned/
21
+ // journal-backed" draft so `/workstream status` could re-render a pending
22
+ // proposal after a restart; the real engine (verified against the linked SDK
23
+ // build) exposes no pre-creation draft concept at all, so WorkstreamDraft
24
+ // below is process-lifetime-only state, owned by this module's facade
25
+ // instance (constructed once, threaded onto CommandContext) — never a
26
+ // module-level ambient global. A draft that existed only in memory is lost on
27
+ // restart, same as an unsent chat draft; that is a real, stated limitation,
28
+ // not a bug.
29
+ // ---------------------------------------------------------------------------
30
+
31
+ import {
32
+ createOrchestrationEngine,
33
+ fromChainSpec,
34
+ type CreateWorkstreamInput,
35
+ type OrchestrationEngine,
36
+ } from '@pellux/goodvibes-sdk/platform/orchestration';
37
+ export type { OrchestrationEngine } from '@pellux/goodvibes-sdk/platform/orchestration';
38
+ import { AdaptivePlanner, type DecompositionGate, type PlannerInputs } from '@pellux/goodvibes-sdk/platform/core';
39
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
40
+ import type { AgentManager } from '@pellux/goodvibes-sdk/platform/tools';
41
+ import type { RuntimeEventBus } from '@/runtime/index.ts';
42
+ import { calcSessionCost, isModelPriced } from '../export/cost-utils.ts';
43
+
44
+ export interface WorkstreamServicesDeps {
45
+ readonly agentManager: Pick<AgentManager, 'spawn' | 'getStatus' | 'cancel' | 'registerCancellationSignal' | 'releaseCancellationSignal'>;
46
+ readonly configManager: Pick<ConfigManager, 'get' | 'getCategory'>;
47
+ readonly adaptivePlanner: AdaptivePlanner;
48
+ readonly runtimeBus: RuntimeEventBus;
49
+ readonly projectRoot: string;
50
+ }
51
+
52
+ /** A not-yet-launched /workstream proposal. See this file's header doc for why it lives here rather than on the engine. */
53
+ export interface WorkstreamDraft {
54
+ readonly id: string;
55
+ task: string;
56
+ spec: CreateWorkstreamInput;
57
+ readonly gate: DecompositionGate;
58
+ approved: boolean;
59
+ readonly createdAt: number;
60
+ }
61
+
62
+ /** `ctx.session.workstreamEngine`'s real shape: the live engine plus the draft-proposal bookkeeping the engine itself has no concept of. */
63
+ export interface WorkstreamCommandService {
64
+ readonly engine: OrchestrationEngine;
65
+ proposeDraft(task: string): WorkstreamDraft;
66
+ getDraft(id: string): WorkstreamDraft | undefined;
67
+ listDrafts(): WorkstreamDraft[];
68
+ /** Re-derive a held draft's spec from a new task string. Clears any prior approval — an edit must be re-approved. */
69
+ editDraft(id: string, task: string): WorkstreamDraft | undefined;
70
+ approveDraft(id: string): WorkstreamDraft | undefined;
71
+ removeDraft(id: string): boolean;
72
+ /** Materialize an approved draft into a real, running Workstream (engine.createWorkstream + start), then drop the draft. Null when the draft is missing or not approved. */
73
+ launchDraft(id: string): { workstreamId: string } | null;
74
+ }
75
+
76
+ export interface WorkstreamServices {
77
+ readonly orchestrationEngine: OrchestrationEngine;
78
+ readonly workstreamCommands: WorkstreamCommandService;
79
+ }
80
+
81
+ /**
82
+ * Placeholder planner inputs until a richer risk/latency signal is wired in.
83
+ * `/workstream create` is inherently a multi-step authoring surface, so
84
+ * isMultiStep is always true; the rest are neutral defaults that let
85
+ * AdaptivePlanner's real scoring run rather than short-circuiting it.
86
+ */
87
+ function buildPlannerInputs(task: string): PlannerInputs {
88
+ return {
89
+ riskScore: 0.3,
90
+ latencyBudgetMs: Number.POSITIVE_INFINITY,
91
+ isMultiStep: true,
92
+ remoteAvailable: false,
93
+ backgroundEligible: false,
94
+ taskDescription: task,
95
+ };
96
+ }
97
+
98
+ function createWorkstreamCommandService(
99
+ engine: OrchestrationEngine,
100
+ adaptivePlanner: AdaptivePlanner,
101
+ configManager: Pick<ConfigManager, 'get' | 'getCategory'>,
102
+ ): WorkstreamCommandService {
103
+ const drafts = new Map<string, WorkstreamDraft>();
104
+
105
+ /**
106
+ * fromChainSpec is the SDK's own bridge from "a task string" to a real,
107
+ * launchable engineer -> review CreateWorkstreamInput — the same two-phase
108
+ * shape WrfcController.createChain would otherwise start (see
109
+ * controller-compat.ts). AdaptivePlanner.proposeWorkstream's own
110
+ * PlanProposal is deliberately NOT used for the rendered shape: it always
111
+ * degrades to a fictional single-phase fallback in this wave (nothing yet
112
+ * supplies it a `raw` LLM decomposition — no planning-agent-spawn pipeline
113
+ * exists in the shipped SDK surface this item compiles against), which
114
+ * would silently disagree with what launchDraft actually creates. Showing
115
+ * the real fromChainSpec shape keeps the proposal and the launch
116
+ * byte-for-byte the same plan. proposeWorkstream is still called (below)
117
+ * for its `gate` — the real strategy/reason-code rationale — which stays
118
+ * meaningful even without a raw decomposition.
119
+ */
120
+ function buildSpec(task: string): CreateWorkstreamInput {
121
+ return fromChainSpec({ id: `item-${crypto.randomUUID().slice(0, 8)}`, task }, configManager);
122
+ }
123
+
124
+ return {
125
+ engine,
126
+ proposeDraft(task: string): WorkstreamDraft {
127
+ const { gate } = adaptivePlanner.proposeWorkstream(buildPlannerInputs(task));
128
+ const draft: WorkstreamDraft = {
129
+ id: `wsd_${crypto.randomUUID().slice(0, 8)}`,
130
+ task,
131
+ spec: buildSpec(task),
132
+ gate,
133
+ approved: false,
134
+ createdAt: Date.now(),
135
+ };
136
+ drafts.set(draft.id, draft);
137
+ return draft;
138
+ },
139
+ getDraft: (id) => drafts.get(id),
140
+ listDrafts: () => Array.from(drafts.values()).sort((a, b) => a.createdAt - b.createdAt),
141
+ editDraft(id, task) {
142
+ const draft = drafts.get(id);
143
+ if (!draft) return undefined;
144
+ draft.task = task;
145
+ draft.spec = buildSpec(task);
146
+ draft.approved = false;
147
+ return draft;
148
+ },
149
+ approveDraft(id) {
150
+ const draft = drafts.get(id);
151
+ if (!draft) return undefined;
152
+ draft.approved = true;
153
+ return draft;
154
+ },
155
+ removeDraft: (id) => drafts.delete(id),
156
+ launchDraft(id) {
157
+ const draft = drafts.get(id);
158
+ if (!draft || !draft.approved) return null;
159
+ const workstream = engine.createWorkstream(draft.spec);
160
+ engine.start(workstream.id);
161
+ drafts.delete(id);
162
+ return { workstreamId: workstream.id };
163
+ },
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Constructs the TUI's OrchestrationEngine instance and its command-facing
169
+ * facade. `persist` and `createWorktree` are left at the engine's own
170
+ * defaults: journal-backed snapshots under .goodvibes/orchestration/ (so
171
+ * resumeAllFromDisk below has something to resume) and a plain
172
+ * AgentWorktree(projectRoot) — the same "shared working directory, no real
173
+ * per-item isolation" behavior WrfcController itself uses today (see
174
+ * phase-runner.ts's own REALITY-WINS divergence doc in the SDK).
175
+ */
176
+ export function createWorkstreamServices(deps: WorkstreamServicesDeps): WorkstreamServices {
177
+ const orchestrationEngine = createOrchestrationEngine({
178
+ agentManager: deps.agentManager,
179
+ configManager: deps.configManager,
180
+ runtimeBus: deps.runtimeBus,
181
+ projectRoot: deps.projectRoot,
182
+ priceUsage: (model, usage) => {
183
+ const modelId = model ?? 'unknown';
184
+ if (!isModelPriced(modelId)) return null;
185
+ return calcSessionCost(usage.inputTokens, usage.outputTokens, usage.cacheReadTokens, usage.cacheWriteTokens, modelId);
186
+ },
187
+ });
188
+ // Honest resume: a prior process's still-in-flight workstreams pick back up
189
+ // (and reappear in the fleet tree) instead of silently vanishing on
190
+ // restart. Never throws — persistence.ts guards every read/parse and
191
+ // quarantines an unrecognized snapshot rather than propagating.
192
+ orchestrationEngine.resumeAllFromDisk();
193
+ const workstreamCommands = createWorkstreamCommandService(orchestrationEngine, deps.adaptivePlanner, deps.configManager);
194
+ return { orchestrationEngine, workstreamCommands };
195
+ }
@@ -4,9 +4,12 @@ import type { SessionSnapshot } from '@/runtime/index.ts';
4
4
  import type { SystemMessageRouter } from '../core/system-message-router.ts';
5
5
  import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
6
6
  import { replayJournalForSession } from '../core/session-recovery.ts';
7
+ import { applyHunkKey, buildModifiedEditArgs, type HunkSelectionState } from '../permissions/hunk-selection.ts';
7
8
 
8
9
  export type PendingPermissionState = PermissionRequest & {
9
- resolve: (approved: boolean, remember?: boolean) => void;
10
+ resolve: (approved: boolean, remember?: boolean, modifiedArgs?: Record<string, unknown>) => void;
11
+ /** Present only when isHunkSelectable(request) was true when the prompt was opened. */
12
+ hunkState?: HunkSelectionState;
10
13
  };
11
14
 
12
15
  export type BlockingInputHandlerOptions = {
@@ -73,6 +76,24 @@ export function handleBlockingShellInput(
73
76
 
74
77
  if (pendingPermission) {
75
78
  const req = pendingPermission;
79
+
80
+ if (req.hunkState) {
81
+ const { state, commit } = applyHunkKey(req.hunkState, data);
82
+ if (commit === 'apply') {
83
+ req.resolve(true, false, buildModifiedEditArgs(req, state));
84
+ render();
85
+ return { handled: true, pendingPermission: null, recoveryPending };
86
+ }
87
+ if (commit === 'cancel') {
88
+ req.resolve(false, false);
89
+ abortTurn();
90
+ render();
91
+ return { handled: true, pendingPermission: null, recoveryPending };
92
+ }
93
+ render();
94
+ return { handled: true, pendingPermission: { ...req, hunkState: state }, recoveryPending };
95
+ }
96
+
76
97
  const key = data.toLowerCase().trim();
77
98
 
78
99
  if (key === 'y') {
@@ -10,8 +10,10 @@ import type { FeatureFlagManager } from '@/runtime/index.ts';
10
10
  import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
11
11
  import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
12
12
  import type { SecretsManager } from '@pellux/goodvibes-sdk/platform/config';
13
+ import type { MemoryEmbeddingProviderRegistry } from '@pellux/goodvibes-sdk/platform/state';
13
14
  import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
14
- import type { ModelPickerTargetInfo } from '../input/model-picker.ts';
15
+ import type { EmbeddingProviderPickerEntry, ModelPickerTargetInfo } from '../input/model-picker.ts';
16
+ import type { SelectionItem } from '../input/selection-modal.ts';
15
17
  import { syncServiceSettingToPlatform } from './service-settings-sync.ts';
16
18
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
17
19
 
@@ -28,6 +30,8 @@ type WireShellUiOpenersOptions = {
28
30
  subscriptionManager: SubscriptionManager;
29
31
  secretsManager?: Pick<SecretsManager, 'delete' | 'get' | 'set'>;
30
32
  serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'>;
33
+ /** B29: backs the model picker's 'embeddings' target and its own item-list mode. */
34
+ memoryEmbeddingRegistry: Pick<MemoryEmbeddingProviderRegistry, 'getDefaultProviderId' | 'setDefaultProvider' | 'status'>;
31
35
  workingDirectory: string;
32
36
  homeDirectory: string;
33
37
  getConfiguredProviderIds: () => string[];
@@ -93,6 +97,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
93
97
  subscriptionManager,
94
98
  secretsManager,
95
99
  serviceRegistry,
100
+ memoryEmbeddingRegistry,
96
101
  workingDirectory,
97
102
  homeDirectory,
98
103
  getConfiguredProviderIds,
@@ -125,6 +130,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
125
130
  if (target === 'helper') return String(configManager.get('helper.globalModel') || runtime.model);
126
131
  if (target === 'tool') return String(configManager.get('tools.llmModel') || runtime.model);
127
132
  if (target === 'tts') return String(configManager.get('tts.llmModel') || runtime.model);
133
+ if (target === 'embeddings') return memoryEmbeddingRegistry.getDefaultProviderId();
128
134
  return runtime.model;
129
135
  };
130
136
 
@@ -134,10 +140,29 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
134
140
  if (target === 'helper') return String(configManager.get('helper.globalProvider') || runtime.provider);
135
141
  if (target === 'tool') return String(configManager.get('tools.llmProvider') || runtime.provider);
136
142
  if (target === 'tts') return String(configManager.get('tts.llmProvider') || runtime.provider);
143
+ if (target === 'embeddings') return memoryEmbeddingRegistry.getDefaultProviderId();
137
144
  return runtime.provider;
138
145
  };
139
146
 
140
- const buildModelPickerTargets = (): ModelPickerTargetInfo[] => {
147
+ /**
148
+ * Fetch the embedding-provider status list once per picker-open and shape
149
+ * it into the picker's own tiny item type. Uses the registry's async
150
+ * `.status()` (not the sync `.list()`) specifically because `.list()` does
151
+ * not carry `configured` — and unconfigured providers must be shown
152
+ * honestly, not hidden.
153
+ */
154
+ async function resolveEmbeddingProviderEntries(): Promise<EmbeddingProviderPickerEntry[]> {
155
+ const statuses = await memoryEmbeddingRegistry.status();
156
+ return statuses.map((status) => ({
157
+ id: status.id,
158
+ label: status.label,
159
+ dimensions: status.dimensions,
160
+ configured: status.configured,
161
+ ...(status.detail ? { detail: status.detail } : {}),
162
+ }));
163
+ }
164
+
165
+ const buildModelPickerTargets = (embeddingEntries: EmbeddingProviderPickerEntry[]): ModelPickerTargetInfo[] => {
141
166
  const mainProvider = getProviderIdFromModel(configManager.get('provider.model') || runtime.provider).trim();
142
167
  const mainModel = String(configManager.get('provider.model') || runtime.model || '').trim();
143
168
  const helperProvider = String(configManager.get('helper.globalProvider') ?? '').trim();
@@ -146,6 +171,11 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
146
171
  const toolModel = String(configManager.get('tools.llmModel') ?? '').trim();
147
172
  const ttsProvider = String(configManager.get('tts.llmProvider') ?? '').trim();
148
173
  const ttsModel = String(configManager.get('tts.llmModel') ?? '').trim();
174
+ const embeddingProviderId = memoryEmbeddingRegistry.getDefaultProviderId();
175
+ const embeddingEntry = embeddingEntries.find((entry) => entry.id === embeddingProviderId);
176
+ const embeddingNote = embeddingEntry
177
+ ? `${embeddingEntry.id} · ${embeddingEntry.dimensions}d${embeddingEntry.configured ? '' : ' · unconfigured'}`
178
+ : `${embeddingProviderId} · unregistered`;
149
179
 
150
180
  return [
151
181
  {
@@ -184,6 +214,16 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
184
214
  enabled: true,
185
215
  inherited: ttsProvider.length === 0 && ttsModel.length === 0,
186
216
  },
217
+ {
218
+ target: 'embeddings',
219
+ label: 'Embeddings',
220
+ description: 'Embedding provider used for memory search and the code index. Not an LLM route — no model concept.',
221
+ provider: embeddingProviderId,
222
+ model: '',
223
+ enabled: true,
224
+ inherited: false,
225
+ configuredNote: embeddingNote,
226
+ },
187
227
  ];
188
228
  };
189
229
 
@@ -199,8 +239,10 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
199
239
  input.modelPicker.pinnedIds = new Set(pinned);
200
240
  });
201
241
  void input.modelPicker.loadRecentModels().catch(() => {}); // best-effort: prefetch for UI, failure is non-visible
242
+ const embeddingEntries = await resolveEmbeddingProviderEntries();
243
+ input.modelPicker.embeddingProviders = embeddingEntries;
202
244
  input.modalOpened('modelPicker');
203
- input.modelPicker.setTargetInfos(buildModelPickerTargets());
245
+ input.modelPicker.setTargetInfos(buildModelPickerTargets(embeddingEntries));
204
246
  input.modelPicker.openAllModels(models, getCurrentModelForPickerTarget());
205
247
  render();
206
248
  })().catch((error: unknown) => {
@@ -219,8 +261,10 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
219
261
  input.modelPicker.configuredProviders = configuredIds;
220
262
  const secretProviderIds = await resolveSecretProviderIds();
221
263
  input.modelPicker.configuredViaMap = buildConfiguredViaMap(providers, configuredIds, subscriptionManager, secretProviderIds);
264
+ const embeddingEntries = await resolveEmbeddingProviderEntries();
265
+ input.modelPicker.embeddingProviders = embeddingEntries;
222
266
  input.modalOpened('modelPicker');
223
- input.modelPicker.setTargetInfos(buildModelPickerTargets());
267
+ input.modelPicker.setTargetInfos(buildModelPickerTargets(embeddingEntries));
224
268
  input.modelPicker.openProviders(providers, getCurrentProviderForPickerTarget());
225
269
  render();
226
270
  })().catch((error: unknown) => {
@@ -229,6 +273,15 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
229
273
  });
230
274
  };
231
275
 
276
+ commandContext.completeEmbeddingProviderSelection = (providerId: string) => {
277
+ try {
278
+ memoryEmbeddingRegistry.setDefaultProvider(providerId);
279
+ } catch (error: unknown) {
280
+ commandContext.print?.(`Failed to set embedding provider: ${summarizeError(error)}`);
281
+ }
282
+ render();
283
+ };
284
+
232
285
  commandContext.openSelection = (title, items, opts, callback) => {
233
286
  input.openSelection(title, items, opts, callback);
234
287
  };
@@ -281,6 +334,47 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
281
334
  render();
282
335
  };
283
336
 
337
+ // W6.1 (the purge): open a MIGRATE-TO-MODAL surface by name. Fed from both
338
+ // ctx.openModal(name) (migrated command front-doors) and PanelManager's
339
+ // open()-time modal-redirect callback (a legacy panel id resolving to a modal
340
+ // name). Surfaces are registered in builtin-modals.ts; a name with no
341
+ // registered surface degrades honestly to a print rather than a blank modal.
342
+ // The stack name is the stable 'config' slot (one config modal at a time —
343
+ // opening another swaps the surface), so Esc close/return and the modal-stack
344
+ // machinery need only the single 'config' case (handler-ui-state.ts).
345
+ // Some panel-id redirects (and migrated front-doors) resolve to a NATIVE
346
+ // modal that is not a ConfigModalSurface — e.g. `sessions` -> `sessionPicker`,
347
+ // where 'sessionPicker' is the real session-picker modal opened by
348
+ // commandContext.openSessionPicker below, NOT a registered config surface.
349
+ // getModalSurface can never find these, so consult this small name->opener
350
+ // dispatch FIRST. Each entry reuses the same opener the front-door command
351
+ // uses (no duplicated open logic); resolved lazily so wiring order does not
352
+ // matter. Any name that is neither a native modal nor a registered surface
353
+ // still falls through to the honest "not available" print below.
354
+ const nativeModalOpeners: Record<string, () => void> = {
355
+ // openSessionPicker is wired below in this same function; call it lazily
356
+ // (optional-chained for the type, always present at invoke time).
357
+ sessionPicker: () => commandContext.openSessionPicker?.(),
358
+ };
359
+
360
+ commandContext.openModal = (name: string) => {
361
+ const openNative = nativeModalOpeners[name];
362
+ if (openNative) {
363
+ openNative();
364
+ return;
365
+ }
366
+ const surface = panelManager.getModalSurface(name);
367
+ if (!surface) {
368
+ commandContext.print(`'${name}' is not available yet in this build.`);
369
+ render();
370
+ return;
371
+ }
372
+ input.modalOpened('config');
373
+ input.configModal.open(surface, render);
374
+ render();
375
+ };
376
+ panelManager.setOpenModalCallback(commandContext.openModal);
377
+
284
378
  commandContext.openMcpWorkspace = () => {
285
379
  input.openMcpWorkspace(commandContext);
286
380
  render();
@@ -296,25 +390,43 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
296
390
  // Focus ownership lives in PanelManager (focusTarget); read it there rather
297
391
  // than tracking a parallel input.panelFocused flag. Toggle semantics: if the
298
392
  // workspace is visible AND already focused, hide it; otherwise reveal and
299
- // focus it (opening the panel list when nothing is open yet).
393
+ // focus it (opening a picker when nothing is open yet).
300
394
  if (panelManager.isVisible() && panelManager.getFocusTarget() === 'panel') {
301
395
  panelManager.hide();
302
396
  panelManager.focusPrompt();
303
397
  conversation.setSplashSuppressed(false);
304
398
  conversation.rebuildHistory();
305
- } else {
306
- if (panelManager.getAllOpen().length === 0) {
307
- try {
308
- panelManager.open('panel-list');
309
- } catch {
310
- // non-fatal
311
- }
312
- }
313
- panelManager.show();
314
- panelManager.focusPanels();
315
- conversation.setSplashSuppressed(true);
316
- conversation.rebuildHistory();
399
+ render();
400
+ return;
401
+ }
402
+ if (panelManager.getAllOpen().length === 0) {
403
+ // W6.1 (the purge): 'panel-list' (a browse-all-panels picker PANEL)
404
+ // was DELETE-disposition — a picker over a handful of panels is dead
405
+ // weight now. Its replacement is this selection MODAL, built from the
406
+ // live registry (PanelManager.getRegisteredTypes()) rather than a
407
+ // hardcoded list, so it can never list a retired/deleted id.
408
+ const items: SelectionItem[] = panelManager.getRegisteredTypes().map((entry) => ({
409
+ id: entry.id,
410
+ label: `${entry.icon} ${entry.name}`,
411
+ detail: entry.description,
412
+ category: entry.category,
413
+ primaryAction: 'select',
414
+ }));
415
+ commandContext.openSelection?.('Open Panel', items, { allowSearch: true }, (result) => {
416
+ if (!result) return;
417
+ panelManager.open(result.item.id);
418
+ panelManager.show();
419
+ panelManager.focusPanels();
420
+ conversation.setSplashSuppressed(true);
421
+ conversation.rebuildHistory();
422
+ render();
423
+ });
424
+ return;
317
425
  }
426
+ panelManager.show();
427
+ panelManager.focusPanels();
428
+ conversation.setSplashSuppressed(true);
429
+ conversation.rebuildHistory();
318
430
  render();
319
431
  };
320
432
 
@@ -1,16 +1,21 @@
1
1
  /**
2
2
  * Shared ms-duration formatters for the panel layer.
3
3
  *
4
- * Three families are exported, each matching a distinct behavior cluster.
4
+ * Two families are exported, each matching a distinct behavior cluster.
5
5
  * DO NOT replace these with formatElapsed from utils/format-elapsed.ts —
6
6
  * formatElapsed floors to integer seconds and lacks the null/'?ms' guards
7
7
  * that the latency panels require.
8
+ *
9
+ * W6.1 (the purge): formatShortDuration (used only by incident-review-panel
10
+ * and eval-panel, both removed — RETIRE-INTO-FLEET and DELETE respectively)
11
+ * was removed as a genuinely orphaned export — no remaining caller anywhere
12
+ * in src/.
8
13
  */
9
14
 
10
15
  /**
11
16
  * Format a latency value in milliseconds with sub-second precision.
12
17
  *
13
- * Used by: debug-panel, provider-health-panel
18
+ * Used by: provider-health-panel
14
19
  *
15
20
  * ms <= 0 → 'n/a'
16
21
  * ms >= 10000 → '12.3s' (one decimal)
@@ -27,7 +32,7 @@ export function formatLatencyMs(ms: number): string {
27
32
  /**
28
33
  * Format a task/agent duration with minute-rolling notation.
29
34
  *
30
- * Used by: tool-inspector-panel, agent-inspector-shared
35
+ * Used by: agent-inspector-shared
31
36
  *
32
37
  * ms < 1000 → '500ms'
33
38
  * ms < 60000 → '3.5s'
@@ -38,18 +43,3 @@ export function formatDuration(ms: number): string {
38
43
  if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
39
44
  return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
40
45
  }
41
-
42
- /**
43
- * Format a short eval/forensics duration; treats undefined as '?ms'.
44
- *
45
- * Used by: incident-review-panel, eval-panel
46
- *
47
- * undefined → '?ms'
48
- * ms < 1000 → '500ms'
49
- * else → '1.5s'
50
- */
51
- export function formatShortDuration(ms: number | undefined): string {
52
- if (ms === undefined) return '?ms';
53
- if (ms < 1000) return `${ms}ms`;
54
- return `${(ms / 1000).toFixed(1)}s`;
55
- }
@@ -24,7 +24,7 @@ const versionLine = (version: string) =>
24
24
  ` ✦ v${version} █ terminal AI assistant █ 自動コード  ✦`;
25
25
 
26
26
  /** Fixed hint line — the three primary shell entry points. */
27
- const HINT_LINE = 'Ctrl+P panels / ? help / F2 processes';
27
+ const HINT_LINE = 'Ctrl+P panels / ? help / F2 fleet';
28
28
 
29
29
  export interface SplashOptions {
30
30
  workingDir?: string;
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '1.0.0';
9
+ let _version = '1.7.0';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;