mstro-app 0.5.1 → 0.5.6
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/PRIVACY.md +9 -9
- package/README.md +71 -28
- package/bin/commands/config.js +1 -1
- package/bin/mstro.js +55 -4
- package/dist/server/cli/eta-estimator.d.ts +55 -0
- package/dist/server/cli/eta-estimator.d.ts.map +1 -0
- package/dist/server/cli/eta-estimator.js +222 -0
- package/dist/server/cli/eta-estimator.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-process.js +9 -1
- package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
- package/dist/server/cli/headless/mcp-config.d.ts +22 -5
- package/dist/server/cli/headless/mcp-config.d.ts.map +1 -1
- package/dist/server/cli/headless/mcp-config.js +7 -5
- package/dist/server/cli/headless/mcp-config.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +19 -0
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts +50 -0
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +64 -9
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +21 -0
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +19 -12
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts +16 -1
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-history-store.d.ts.map +1 -1
- package/dist/server/cli/improvisation-history-store.js +5 -1
- package/dist/server/cli/improvisation-history-store.js.map +1 -1
- package/dist/server/cli/improvisation-output-queue.d.ts +5 -1
- package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -1
- package/dist/server/cli/improvisation-output-queue.js +30 -7
- package/dist/server/cli/improvisation-output-queue.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +35 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +58 -1
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts +9 -0
- package/dist/server/cli/improvisation-types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-types.js.map +1 -1
- package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -1
- package/dist/server/cli/retry/retry-runner-factory.js +1 -0
- package/dist/server/cli/retry/retry-runner-factory.js.map +1 -1
- package/dist/server/engines/EngineEvent.d.ts +126 -0
- package/dist/server/engines/EngineEvent.d.ts.map +1 -0
- package/dist/server/engines/EngineEvent.js +11 -0
- package/dist/server/engines/EngineEvent.js.map +1 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.d.ts +47 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.d.ts.map +1 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.js +338 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.js.map +1 -0
- package/dist/server/engines/factory.d.ts +21 -0
- package/dist/server/engines/factory.d.ts.map +1 -0
- package/dist/server/engines/factory.js +152 -0
- package/dist/server/engines/factory.js.map +1 -0
- package/dist/server/engines/opencode/OpenCodeEngine.d.ts +148 -0
- package/dist/server/engines/opencode/OpenCodeEngine.d.ts.map +1 -0
- package/dist/server/engines/opencode/OpenCodeEngine.js +630 -0
- package/dist/server/engines/opencode/OpenCodeEngine.js.map +1 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.d.ts +172 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.d.ts.map +1 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.js +390 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.js.map +1 -0
- package/dist/server/engines/opencode/model-catalog.d.ts +94 -0
- package/dist/server/engines/opencode/model-catalog.d.ts.map +1 -0
- package/dist/server/engines/opencode/model-catalog.js +141 -0
- package/dist/server/engines/opencode/model-catalog.js.map +1 -0
- package/dist/server/engines/types.d.ts +146 -0
- package/dist/server/engines/types.d.ts.map +1 -0
- package/dist/server/engines/types.js +4 -0
- package/dist/server/engines/types.js.map +1 -0
- package/dist/server/index.js +9 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.d.ts +17 -4
- package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-haiku.js +8 -124
- package/dist/server/mcp/bouncer-haiku.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts +45 -0
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +69 -5
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/classifier/BouncerClassifier.d.ts +34 -0
- package/dist/server/mcp/classifier/BouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/BouncerClassifier.js +4 -0
- package/dist/server/mcp/classifier/BouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts +17 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +142 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts +68 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js +182 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/factory.d.ts +70 -0
- package/dist/server/mcp/classifier/factory.d.ts.map +1 -0
- package/dist/server/mcp/classifier/factory.js +155 -0
- package/dist/server/mcp/classifier/factory.js.map +1 -0
- package/dist/server/mcp/server.js +52 -0
- package/dist/server/mcp/server.js.map +1 -1
- package/dist/server/routes/index.d.ts +1 -0
- package/dist/server/routes/index.d.ts.map +1 -1
- package/dist/server/routes/index.js +1 -0
- package/dist/server/routes/index.js.map +1 -1
- package/dist/server/routes/internal.d.ts +16 -0
- package/dist/server/routes/internal.d.ts.map +1 -0
- package/dist/server/routes/internal.js +94 -0
- package/dist/server/routes/internal.js.map +1 -0
- package/dist/server/services/plan/agent-resolver.d.ts +26 -0
- package/dist/server/services/plan/agent-resolver.d.ts.map +1 -0
- package/dist/server/services/plan/agent-resolver.js +102 -0
- package/dist/server/services/plan/agent-resolver.js.map +1 -0
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +59 -11
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +3 -1
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.js +33 -1
- package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
- package/dist/server/services/plan/parser-core.d.ts.map +1 -1
- package/dist/server/services/plan/parser-core.js +1 -0
- package/dist/server/services/plan/parser-core.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +1 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/runtime-info.d.ts +3 -0
- package/dist/server/services/runtime-info.d.ts.map +1 -0
- package/dist/server/services/runtime-info.js +21 -0
- package/dist/server/services/runtime-info.js.map +1 -0
- package/dist/server/services/settings.d.ts +76 -2
- package/dist/server/services/settings.d.ts.map +1 -1
- package/dist/server/services/settings.js +127 -4
- package/dist/server/services/settings.js.map +1 -1
- package/dist/server/services/websocket/ask-user-question-bridge.d.ts +32 -0
- package/dist/server/services/websocket/ask-user-question-bridge.d.ts.map +1 -0
- package/dist/server/services/websocket/ask-user-question-bridge.js +115 -0
- package/dist/server/services/websocket/ask-user-question-bridge.js.map +1 -0
- package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.js +19 -6
- package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +25 -1
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +84 -2
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-complexity.js +78 -26
- package/dist/server/services/websocket/quality-complexity.js.map +1 -1
- package/dist/server/services/websocket/quality-eta.d.ts +47 -0
- package/dist/server/services/websocket/quality-eta.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-eta.js +110 -0
- package/dist/server/services/websocket/quality-eta.js.map +1 -0
- package/dist/server/services/websocket/quality-grading.d.ts +27 -4
- package/dist/server/services/websocket/quality-grading.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-grading.js +369 -201
- package/dist/server/services/websocket/quality-grading.js.map +1 -1
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +145 -7
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-operations.d.ts +34 -0
- package/dist/server/services/websocket/quality-operations.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-operations.js +47 -0
- package/dist/server/services/websocket/quality-operations.js.map +1 -0
- package/dist/server/services/websocket/quality-persistence.d.ts +9 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-persistence.js +10 -0
- package/dist/server/services/websocket/quality-persistence.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +105 -56
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-service.d.ts +9 -1
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +334 -14
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts +21 -0
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-tools.js +49 -0
- package/dist/server/services/websocket/quality-tools.js.map +1 -1
- package/dist/server/services/websocket/quality-types.d.ts +35 -2
- package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-types.js +1 -1
- package/dist/server/services/websocket/quality-types.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts +3 -1
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +60 -9
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-history.js +3 -0
- package/dist/server/services/websocket/session-history.js.map +1 -1
- package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
- package/dist/server/services/websocket/session-initialization.js +158 -42
- package/dist/server/services/websocket/session-initialization.js.map +1 -1
- package/dist/server/services/websocket/session-registry.d.ts +25 -0
- package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
- package/dist/server/services/websocket/session-registry.js +19 -0
- package/dist/server/services/websocket/session-registry.js.map +1 -1
- package/dist/server/services/websocket/settings-handlers.d.ts +1 -1
- package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/settings-handlers.js +35 -4
- package/dist/server/services/websocket/settings-handlers.js.map +1 -1
- package/dist/server/services/websocket/tab-broadcast.d.ts +7 -2
- package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-broadcast.js +10 -2
- package/dist/server/services/websocket/tab-broadcast.js.map +1 -1
- package/dist/server/services/websocket/tab-event-buffer.d.ts +97 -8
- package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-event-buffer.js +138 -12
- package/dist/server/services/websocket/tab-event-buffer.js.map +1 -1
- package/dist/server/services/websocket/tab-event-replay.d.ts +29 -13
- package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-event-replay.js +55 -2
- package/dist/server/services/websocket/tab-event-replay.js.map +1 -1
- package/dist/server/services/websocket/tab-handlers.d.ts +9 -1
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-handlers.js +47 -2
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +67 -7
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +12 -6
- package/dist/server/services/websocket/types.js.map +1 -1
- package/package.json +5 -3
- package/server/cli/eta-estimator.ts +249 -0
- package/server/cli/headless/claude-invoker-process.ts +9 -1
- package/server/cli/headless/mcp-config.ts +30 -5
- package/server/cli/headless/runner.ts +21 -0
- package/server/cli/headless/stall-assessor.ts +93 -0
- package/server/cli/headless/tool-watchdog.ts +21 -0
- package/server/cli/headless/types.ts +16 -1
- package/server/cli/improvisation-history-store.ts +4 -1
- package/server/cli/improvisation-output-queue.ts +29 -7
- package/server/cli/improvisation-session-manager.ts +63 -1
- package/server/cli/improvisation-types.ts +9 -0
- package/server/cli/retry/retry-runner-factory.ts +1 -0
- package/server/engines/EngineEvent.ts +156 -0
- package/server/engines/claude/ClaudeCodeEngine.ts +404 -0
- package/server/engines/factory.ts +176 -0
- package/server/engines/opencode/OpenCodeEngine.ts +786 -0
- package/server/engines/opencode/OpenCodeServerManager.ts +577 -0
- package/server/engines/opencode/model-catalog.ts +217 -0
- package/server/engines/types.ts +173 -0
- package/server/index.ts +9 -1
- package/server/mcp/bouncer-haiku.ts +21 -145
- package/server/mcp/bouncer-integration.ts +107 -5
- package/server/mcp/classifier/BouncerClassifier.ts +40 -0
- package/server/mcp/classifier/ClaudeBouncerClassifier.ts +189 -0
- package/server/mcp/classifier/OpenCodeBouncerClassifier.ts +305 -0
- package/server/mcp/classifier/factory.ts +195 -0
- package/server/mcp/server.ts +57 -0
- package/server/routes/index.ts +1 -0
- package/server/routes/internal.ts +112 -0
- package/server/services/plan/agent-resolver.ts +115 -0
- package/server/services/plan/agents/code-review.md +38 -8
- package/server/services/plan/composer.ts +63 -11
- package/server/services/plan/executor.ts +3 -1
- package/server/services/plan/issue-prompt-builder.ts +39 -1
- package/server/services/plan/parser-core.ts +1 -0
- package/server/services/plan/types.ts +4 -0
- package/server/services/runtime-info.ts +24 -0
- package/server/services/settings.ts +161 -4
- package/server/services/websocket/ask-user-question-bridge.ts +148 -0
- package/server/services/websocket/git-branch-handlers.ts +20 -6
- package/server/services/websocket/handler.ts +89 -2
- package/server/services/websocket/quality-complexity.ts +80 -26
- package/server/services/websocket/quality-eta.ts +155 -0
- package/server/services/websocket/quality-grading.ts +445 -222
- package/server/services/websocket/quality-handlers.ts +153 -7
- package/server/services/websocket/quality-operations.ts +72 -0
- package/server/services/websocket/quality-persistence.ts +17 -0
- package/server/services/websocket/quality-review-agent.ts +154 -64
- package/server/services/websocket/quality-service.ts +361 -13
- package/server/services/websocket/quality-tools.ts +51 -0
- package/server/services/websocket/quality-types.ts +41 -2
- package/server/services/websocket/session-handlers.ts +67 -10
- package/server/services/websocket/session-history.ts +3 -0
- package/server/services/websocket/session-initialization.ts +189 -46
- package/server/services/websocket/session-registry.ts +37 -0
- package/server/services/websocket/settings-handlers.ts +41 -4
- package/server/services/websocket/tab-broadcast.ts +10 -2
- package/server/services/websocket/tab-event-buffer.ts +143 -11
- package/server/services/websocket/tab-event-replay.ts +70 -3
- package/server/services/websocket/tab-handlers.ts +53 -5
- package/server/services/websocket/types.ts +85 -7
|
@@ -7,7 +7,42 @@ import { runQualityScan } from './quality-service.js';
|
|
|
7
7
|
import type { SessionRegistry } from './session-registry.js';
|
|
8
8
|
import { resolveSkillPrompt } from './skill-handlers.js';
|
|
9
9
|
import { broadcastTabEvent } from './tab-broadcast.js';
|
|
10
|
-
import type
|
|
10
|
+
import { type EngineId, normalizeEngineId, type WebSocketMessage, type WSContext } from './types.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Apply per-prompt engine/model/effortLevel overrides from the `execute`
|
|
14
|
+
* WebSocket payload onto the session. `session.options` is mutated in place
|
|
15
|
+
* so the next `executePrompt` call (which reads options.model / effortLevel
|
|
16
|
+
* inside the retry loop) picks up the new values without needing a new
|
|
17
|
+
* session. Pass-through for undefined fields — global defaults already live
|
|
18
|
+
* on the session from initialization.
|
|
19
|
+
*/
|
|
20
|
+
function applyExecuteOverrides(
|
|
21
|
+
session: ImprovisationSessionManager,
|
|
22
|
+
data: { engine?: unknown; model?: unknown; effortLevel?: unknown },
|
|
23
|
+
): void {
|
|
24
|
+
const options = (session as unknown as { options: { model?: string; effortLevel?: string } }).options;
|
|
25
|
+
if (typeof data.model === 'string' && data.model.length > 0) {
|
|
26
|
+
options.model = data.model;
|
|
27
|
+
}
|
|
28
|
+
if (typeof data.effortLevel === 'string' && data.effortLevel.length > 0) {
|
|
29
|
+
options.effortLevel = data.effortLevel;
|
|
30
|
+
}
|
|
31
|
+
if (data.engine === 'claude-code' || data.engine === 'opencode') {
|
|
32
|
+
// Record the engine on the session history so `resolveEngineForSession`
|
|
33
|
+
// returns the user-chosen value on subsequent movementStart /
|
|
34
|
+
// movementComplete broadcasts. The actual engine factory swap happens
|
|
35
|
+
// at session-start (later-epic work); per-prompt engine flips flow
|
|
36
|
+
// through here so model/effort stay in sync.
|
|
37
|
+
const history = (session as unknown as { history: { engine: string } }).history;
|
|
38
|
+
history.engine = data.engine;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Resolve the engine for a session, defaulting to 'claude-code' for pre-engine sessions. */
|
|
43
|
+
export function resolveEngineForSession(session: ImprovisationSessionManager | undefined): EngineId {
|
|
44
|
+
return normalizeEngineId(session?.engine);
|
|
45
|
+
}
|
|
11
46
|
|
|
12
47
|
// Re-export from extracted modules for backward compatibility
|
|
13
48
|
export { handleHistoryMessage } from './session-history.js';
|
|
@@ -213,6 +248,10 @@ export function buildOutputHistory(session: ImprovisationSessionManager): Array<
|
|
|
213
248
|
*/
|
|
214
249
|
export function setupSessionListeners(ctx: HandlerContext, session: ImprovisationSessionManager, _ws: WSContext, tabId: string): void {
|
|
215
250
|
session.removeAllListeners();
|
|
251
|
+
// Bind tabId before listeners — the headless runner reads it at executePrompt
|
|
252
|
+
// time to wire AskUserQuestion routing back to this tab's web clients.
|
|
253
|
+
session.setTabId(tabId);
|
|
254
|
+
const engine = resolveEngineForSession(session);
|
|
216
255
|
|
|
217
256
|
session.on('onHistoryPersisted', () => {
|
|
218
257
|
const registry = ctx.getRegistry('');
|
|
@@ -227,19 +266,19 @@ export function setupSessionListeners(ctx: HandlerContext, session: Improvisatio
|
|
|
227
266
|
broadcastTabEvent(ctx, tabId, 'thinking', { text });
|
|
228
267
|
});
|
|
229
268
|
|
|
230
|
-
session.on('onMovementStart', (sequenceNumber: number, prompt: string, isAutoContinue?: boolean) => {
|
|
231
|
-
broadcastTabEvent(ctx, tabId, 'movementStart', { sequenceNumber, prompt, timestamp: Date.now(), executionStartTimestamp: session.executionStartTimestamp, isAutoContinue });
|
|
232
|
-
ctx.broadcastToAll({ type: 'tabStateChanged', data: { tabId, isExecuting: true, executionStartTimestamp: session.executionStartTimestamp } });
|
|
269
|
+
session.on('onMovementStart', (sequenceNumber: number, prompt: string, isAutoContinue?: boolean, etaProfile?: import('../../cli/eta-estimator.js').EtaProfile | null) => {
|
|
270
|
+
broadcastTabEvent(ctx, tabId, 'movementStart', { sequenceNumber, prompt, timestamp: Date.now(), executionStartTimestamp: session.executionStartTimestamp, isAutoContinue, etaProfile: etaProfile ?? undefined }, engine);
|
|
271
|
+
ctx.broadcastToAll({ type: 'tabStateChanged', engine, data: { tabId, isExecuting: true, executionStartTimestamp: session.executionStartTimestamp } });
|
|
233
272
|
});
|
|
234
273
|
|
|
235
274
|
session.on('onMovementComplete', (movement: Record<string, unknown>) => {
|
|
236
|
-
broadcastTabEvent(ctx, tabId, 'movementComplete', movement);
|
|
275
|
+
broadcastTabEvent(ctx, tabId, 'movementComplete', movement, engine);
|
|
237
276
|
|
|
238
277
|
const registry = ctx.getRegistry('');
|
|
239
278
|
// Use a try/catch since getRegistry may not have been initialized with the right workingDir
|
|
240
279
|
try { registry.markTabUnviewed(tabId); } catch { /* ignore */ }
|
|
241
280
|
|
|
242
|
-
ctx.broadcastToAll({ type: 'tabStateChanged', data: { tabId, isExecuting: false, hasUnviewedCompletion: true } });
|
|
281
|
+
ctx.broadcastToAll({ type: 'tabStateChanged', engine, data: { tabId, isExecuting: false, hasUnviewedCompletion: true } });
|
|
243
282
|
|
|
244
283
|
if (ctx.usageReporter && movement.tokensUsed) {
|
|
245
284
|
ctx.usageReporter({
|
|
@@ -257,12 +296,12 @@ export function setupSessionListeners(ctx: HandlerContext, session: Improvisatio
|
|
|
257
296
|
});
|
|
258
297
|
|
|
259
298
|
session.on('onMovementError', (error: Error) => {
|
|
260
|
-
broadcastTabEvent(ctx, tabId, 'movementError', { message: error.message });
|
|
261
|
-
ctx.broadcastToAll({ type: 'tabStateChanged', data: { tabId, isExecuting: false } });
|
|
299
|
+
broadcastTabEvent(ctx, tabId, 'movementError', { message: error.message }, engine);
|
|
300
|
+
ctx.broadcastToAll({ type: 'tabStateChanged', engine, data: { tabId, isExecuting: false } });
|
|
262
301
|
});
|
|
263
302
|
|
|
264
303
|
session.on('onSessionUpdate', (history: Record<string, unknown>) => {
|
|
265
|
-
broadcastTabEvent(ctx, tabId, 'sessionUpdate', history);
|
|
304
|
+
broadcastTabEvent(ctx, tabId, 'sessionUpdate', history, engine);
|
|
266
305
|
});
|
|
267
306
|
|
|
268
307
|
session.on('onPlanNeedsConfirmation', (plan: Record<string, unknown>) => {
|
|
@@ -336,6 +375,13 @@ function handleExecuteMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocket
|
|
|
336
375
|
console.log(`[session] execute accepted msgId=${msgId} tabId=${tabId} sessionId=${sessionId}`);
|
|
337
376
|
}
|
|
338
377
|
|
|
378
|
+
// Apply per-prompt engine/model/effort overrides from the payload. The
|
|
379
|
+
// web client populates these from the tab's effective EnginePicker state
|
|
380
|
+
// (override > global). Missing fields fall through to whatever the session
|
|
381
|
+
// already has — typically the machine-level defaults from `settings.json`
|
|
382
|
+
// applied at session init.
|
|
383
|
+
applyExecuteOverrides(session, msg.data);
|
|
384
|
+
|
|
339
385
|
const worktreeDir = ctx.gitDirectories.get(tabId);
|
|
340
386
|
const attachments = mergePreUploadedAttachments(ctx, tabId, msg.data.attachments);
|
|
341
387
|
|
|
@@ -346,6 +392,17 @@ function handleExecuteMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocket
|
|
|
346
392
|
const effectiveDir = worktreeDir || session.getSessionInfo().workingDir;
|
|
347
393
|
const resolved = resolveSkillPrompt(rawPrompt, effectiveDir);
|
|
348
394
|
|
|
395
|
+
// Authoritative prompt-input clear for all connected devices. The submitter
|
|
396
|
+
// already cleared locally; this guarantees other devices clear even if the
|
|
397
|
+
// submitter's debounced syncPromptText never fires (e.g. mobile tab
|
|
398
|
+
// suspended after Send). Clients suppress this via locallyEditingTabs if
|
|
399
|
+
// the user is actively typing a new prompt.
|
|
400
|
+
ctx.broadcastToAll({
|
|
401
|
+
type: 'promptTextSync',
|
|
402
|
+
tabId,
|
|
403
|
+
data: { tabId, text: '' },
|
|
404
|
+
});
|
|
405
|
+
|
|
349
406
|
session.executePrompt(
|
|
350
407
|
resolved ? resolved.prompt : rawPrompt,
|
|
351
408
|
attachments,
|
|
@@ -372,7 +429,7 @@ function handleNewSessionMessage(ctx: HandlerContext, ws: WSContext, tabId: stri
|
|
|
372
429
|
if (tabMap) tabMap.set(tabId, newSessionId);
|
|
373
430
|
const registry = ctx.getRegistry('');
|
|
374
431
|
try { registry.updateTabSession(tabId, newSessionId); } catch { /* ignore */ }
|
|
375
|
-
ctx.send(ws, { type: 'newSession', tabId, data: newSession.getSessionInfo() });
|
|
432
|
+
ctx.send(ws, { type: 'newSession', tabId, engine: resolveEngineForSession(newSession), data: newSession.getSessionInfo() });
|
|
376
433
|
}
|
|
377
434
|
|
|
378
435
|
export function handleSessionMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, permission?: 'view'): void {
|
|
@@ -91,6 +91,7 @@ function getSessionById(workingDir: string, sessionId: string): Record<string, u
|
|
|
91
91
|
startedAt: historyData.startedAt,
|
|
92
92
|
lastActivityAt: historyData.lastActivityAt,
|
|
93
93
|
totalTokens: historyData.totalTokens,
|
|
94
|
+
engine: historyData.engine || 'claude-code',
|
|
94
95
|
movementCount: historyData.movements?.length || 0,
|
|
95
96
|
title: firstPrompt.slice(0, 80) + (firstPrompt.length > 80 ? '...' : ''),
|
|
96
97
|
movements: historyData.movements || [],
|
|
@@ -171,11 +172,13 @@ function buildSessionSummary(historyData: Record<string, unknown>): Record<strin
|
|
|
171
172
|
const movementPreviews = (movements || []).slice(0, 3).map((m: Record<string, unknown>) => ({
|
|
172
173
|
userPrompt: (typeof m.userPrompt === 'string' ? m.userPrompt : '').slice(0, 100) || ''
|
|
173
174
|
}));
|
|
175
|
+
const engine = typeof historyData.engine === 'string' && historyData.engine ? historyData.engine : 'claude-code';
|
|
174
176
|
return {
|
|
175
177
|
sessionId: historyData.sessionId,
|
|
176
178
|
startedAt: historyData.startedAt,
|
|
177
179
|
lastActivityAt: historyData.lastActivityAt,
|
|
178
180
|
totalTokens: historyData.totalTokens,
|
|
181
|
+
engine,
|
|
179
182
|
movementCount: movements?.length || 0,
|
|
180
183
|
title: firstPrompt.slice(0, 80) + (firstPrompt.length > 80 ? '...' : ''),
|
|
181
184
|
movements: movementPreviews
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
2
|
|
|
3
|
+
import type { EtaProfile } from '../../cli/eta-estimator.js';
|
|
3
4
|
import { ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
|
|
4
5
|
import { getEffortLevel, getModel } from '../settings.js';
|
|
5
6
|
import type { HandlerContext } from './handler-context.js';
|
|
6
|
-
import { buildOutputHistory, setupSessionListeners } from './session-handlers.js';
|
|
7
|
-
import type { SessionRegistry } from './session-registry.js';
|
|
8
|
-
import { replayTabEventsSince } from './tab-event-replay.js';
|
|
7
|
+
import { buildOutputHistory, resolveEngineForSession, setupSessionListeners } from './session-handlers.js';
|
|
8
|
+
import type { SessionRegistry, TabEngineOverride } from './session-registry.js';
|
|
9
|
+
import { type ReplayResult, replayTabEventsSince } from './tab-event-replay.js';
|
|
9
10
|
import type { WSContext } from './types.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -22,6 +23,138 @@ function extractLastSeenSeq(data: unknown): number | undefined {
|
|
|
22
23
|
return typeof candidate === 'number' && Number.isFinite(candidate) ? candidate : undefined;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* When the session is mid-execution, expose the cached eta profile so the
|
|
28
|
+
* web's ComposingIndicator can render an ETA immediately on reconnect
|
|
29
|
+
* instead of waiting for the next movementStart (which won't fire until
|
|
30
|
+
* the user submits a fresh prompt).
|
|
31
|
+
*/
|
|
32
|
+
function inflightEtaPayload(session: ImprovisationSessionManager): { etaProfile?: EtaProfile } {
|
|
33
|
+
if (session.isExecuting && session.etaProfile) return { etaProfile: session.etaProfile };
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build the full-snapshot data payload for a `tabInitialized` message.
|
|
39
|
+
*
|
|
40
|
+
* Used in three situations:
|
|
41
|
+
* 1. Cold init (no `lastSeenSeq`) — web has no prior state to merge with.
|
|
42
|
+
* 2. Cold reattach (existing session, no prior seq) — same shape.
|
|
43
|
+
* 3. Replay-gap recovery — `replayTabEventsSince` returned `hadGap`, so
|
|
44
|
+
* the web's incremental state is provably stale; we replace it.
|
|
45
|
+
*
|
|
46
|
+
* `replayGap` flags the recovery case so the web can branch: drop any
|
|
47
|
+
* already-rendered tab output and rebuild from `outputHistory` +
|
|
48
|
+
* `executionEvents` instead of merging on top of stale incremental state.
|
|
49
|
+
* Old web clients that don't know the flag still get the full snapshot and
|
|
50
|
+
* render correctly — `replayGap` is purely additive telemetry.
|
|
51
|
+
*/
|
|
52
|
+
function buildFullSnapshotData(
|
|
53
|
+
session: ImprovisationSessionManager,
|
|
54
|
+
options: {
|
|
55
|
+
worktreePath?: string;
|
|
56
|
+
worktreeBranch?: string;
|
|
57
|
+
engineOverride?: TabEngineOverride;
|
|
58
|
+
replayGap?: boolean;
|
|
59
|
+
extra?: Record<string, unknown>;
|
|
60
|
+
} = {},
|
|
61
|
+
): Record<string, unknown> {
|
|
62
|
+
const isExecuting = session.isExecuting;
|
|
63
|
+
return {
|
|
64
|
+
...session.getSessionInfo(),
|
|
65
|
+
engine: resolveEngineForSession(session),
|
|
66
|
+
outputHistory: buildOutputHistory(session),
|
|
67
|
+
isExecuting,
|
|
68
|
+
...(isExecuting ? { executionEvents: session.getExecutionEventLog() } : {}),
|
|
69
|
+
...(isExecuting && session.executionStartTimestamp
|
|
70
|
+
? { executionStartTimestamp: session.executionStartTimestamp }
|
|
71
|
+
: {}),
|
|
72
|
+
...inflightEtaPayload(session),
|
|
73
|
+
...(options.worktreePath
|
|
74
|
+
? { worktreePath: options.worktreePath, worktreeBranch: options.worktreeBranch }
|
|
75
|
+
: {}),
|
|
76
|
+
...(options.engineOverride ? { engineOverride: options.engineOverride } : {}),
|
|
77
|
+
...(options.replayGap ? { replayGap: true } : {}),
|
|
78
|
+
...(options.extra ?? {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Snapshot vs incremental decision based on the replay outcome.
|
|
84
|
+
*
|
|
85
|
+
* - `incremental`: the web should keep its current state and append the
|
|
86
|
+
* replayed events that already arrived (handled by `replayTabEventsSince`
|
|
87
|
+
* itself when there's no gap). We send `tabInitialized` with
|
|
88
|
+
* `resumedFromSeq: true`.
|
|
89
|
+
* - `snapshot`: the web should discard tab output and rebuild from a full
|
|
90
|
+
* snapshot. Triggered either by `lastSeenSeq === undefined` (cold start)
|
|
91
|
+
* or by `result.hadGap` (replay would silently skip events).
|
|
92
|
+
*/
|
|
93
|
+
function decideRecoveryMode(
|
|
94
|
+
result: ReplayResult,
|
|
95
|
+
lastSeenSeq: number | undefined,
|
|
96
|
+
): 'incremental' | 'snapshot' {
|
|
97
|
+
if (lastSeenSeq === undefined) return 'snapshot';
|
|
98
|
+
if (result.hadGap) return 'snapshot';
|
|
99
|
+
return 'incremental';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Send `tabInitialized` for a resume path (`tryResumeFromDisk` /
|
|
104
|
+
* `resumeHistoricalSession`). Picks the snapshot or incremental envelope
|
|
105
|
+
* shape based on `mode` and threads any extra fields the caller needs to
|
|
106
|
+
* carry (e.g. `resumeFailed`, worktree state, engine override).
|
|
107
|
+
*
|
|
108
|
+
* Extracted to keep the resume call sites flat — without this helper, each
|
|
109
|
+
* caller pushes the function over the project's cognitive-complexity gate.
|
|
110
|
+
*/
|
|
111
|
+
function sendResumedTabInitialized(
|
|
112
|
+
ctx: HandlerContext,
|
|
113
|
+
ws: WSContext,
|
|
114
|
+
tabId: string,
|
|
115
|
+
session: ImprovisationSessionManager,
|
|
116
|
+
mode: 'incremental' | 'snapshot',
|
|
117
|
+
replay: ReplayResult,
|
|
118
|
+
options: {
|
|
119
|
+
worktreePath?: string;
|
|
120
|
+
worktreeBranch?: string;
|
|
121
|
+
engineOverride?: TabEngineOverride;
|
|
122
|
+
extra?: Record<string, unknown>;
|
|
123
|
+
} = {},
|
|
124
|
+
): void {
|
|
125
|
+
const engine = resolveEngineForSession(session);
|
|
126
|
+
if (mode === 'snapshot') {
|
|
127
|
+
ctx.send(ws, {
|
|
128
|
+
type: 'tabInitialized',
|
|
129
|
+
tabId,
|
|
130
|
+
engine,
|
|
131
|
+
data: buildFullSnapshotData(session, {
|
|
132
|
+
worktreePath: options.worktreePath,
|
|
133
|
+
worktreeBranch: options.worktreeBranch,
|
|
134
|
+
engineOverride: options.engineOverride,
|
|
135
|
+
replayGap: replay.hadGap,
|
|
136
|
+
extra: options.extra,
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
ctx.send(ws, {
|
|
142
|
+
type: 'tabInitialized',
|
|
143
|
+
tabId,
|
|
144
|
+
engine,
|
|
145
|
+
data: {
|
|
146
|
+
...session.getSessionInfo(),
|
|
147
|
+
engine,
|
|
148
|
+
resumedFromSeq: true,
|
|
149
|
+
...(options.worktreePath
|
|
150
|
+
? { worktreePath: options.worktreePath, worktreeBranch: options.worktreeBranch }
|
|
151
|
+
: {}),
|
|
152
|
+
...(options.engineOverride ? { engineOverride: options.engineOverride } : {}),
|
|
153
|
+
...(options.extra ?? {}),
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
25
158
|
function tryResumeFromDisk(
|
|
26
159
|
ctx: HandlerContext,
|
|
27
160
|
ws: WSContext,
|
|
@@ -54,16 +187,17 @@ function tryResumeFromDisk(
|
|
|
54
187
|
// BEFORE tabInitialized so they arrive in the right order. Web-side
|
|
55
188
|
// handlers append; `tabInitialized` does NOT reset when `resumedFromSeq`
|
|
56
189
|
// is set, preserving the replayed additions.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
190
|
+
//
|
|
191
|
+
// If `replayTabEventsSince` reports `hadGap`, no events were emitted and
|
|
192
|
+
// we fall back to a full-snapshot `tabInitialized` so the web replaces
|
|
193
|
+
// its (now-known-stale) incremental state instead of merging on top.
|
|
194
|
+
const replay = replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
|
|
195
|
+
const mode = decideRecoveryMode(replay, lastSeenSeq);
|
|
196
|
+
|
|
197
|
+
sendResumedTabInitialized(ctx, ws, tabId, diskSession, mode, replay, {
|
|
198
|
+
worktreePath,
|
|
199
|
+
worktreeBranch,
|
|
200
|
+
engineOverride: regTab?.engineOverride,
|
|
67
201
|
});
|
|
68
202
|
return true;
|
|
69
203
|
} catch {
|
|
@@ -121,21 +255,32 @@ export async function initializeTab(ctx: HandlerContext, ws: WSContext, tabId: s
|
|
|
121
255
|
|
|
122
256
|
registry.registerTab(tabId, sessionId, tabName || existingTab?.tabName);
|
|
123
257
|
const registeredTab = registry.getTab(tabId);
|
|
124
|
-
|
|
258
|
+
const engine = resolveEngineForSession(session);
|
|
259
|
+
// Mirror terminal-handlers.ts: broadcastToOthers, not broadcastToAll. The
|
|
260
|
+
// requesting client already drove this initTab and will receive
|
|
261
|
+
// `tabInitialized` below — echoing `tabCreated` back risks racing the
|
|
262
|
+
// discovery handler during a flicker and producing a phantom tab.
|
|
263
|
+
ctx.broadcastToOthers(ws, {
|
|
125
264
|
type: 'tabCreated',
|
|
126
|
-
|
|
265
|
+
engine,
|
|
266
|
+
data: { tabId, tabName: registeredTab?.tabName || 'Chat', createdAt: registeredTab?.createdAt, order: registeredTab?.order, engine, sessionInfo: session.getSessionInfo() }
|
|
127
267
|
});
|
|
128
268
|
|
|
129
269
|
// Fresh session (no disk/memory predecessor) has nothing to replay,
|
|
130
270
|
// but we still pass lastSeenSeq through so the web flag is consistent.
|
|
131
|
-
|
|
271
|
+
// hadGap is impossible here (buffer is empty for a brand-new tab), but
|
|
272
|
+
// route through `decideRecoveryMode` for uniformity with the resume paths.
|
|
273
|
+
const replay = replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
|
|
274
|
+
const mode = decideRecoveryMode(replay, lastSeenSeq);
|
|
132
275
|
|
|
133
276
|
ctx.send(ws, {
|
|
134
277
|
type: 'tabInitialized',
|
|
135
278
|
tabId,
|
|
279
|
+
engine,
|
|
136
280
|
data: {
|
|
137
281
|
...session.getSessionInfo(),
|
|
138
|
-
...(
|
|
282
|
+
...(mode === 'incremental' ? { resumedFromSeq: true } : {}),
|
|
283
|
+
...(replay.hadGap ? { replayGap: true } : {}),
|
|
139
284
|
}
|
|
140
285
|
});
|
|
141
286
|
}
|
|
@@ -192,17 +337,14 @@ export async function resumeHistoricalSession(
|
|
|
192
337
|
|
|
193
338
|
registry.registerTab(tabId, sessionId);
|
|
194
339
|
|
|
195
|
-
replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
|
|
340
|
+
const replay = replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
|
|
341
|
+
const mode = decideRecoveryMode(replay, lastSeenSeq);
|
|
196
342
|
|
|
197
|
-
ctx
|
|
198
|
-
|
|
199
|
-
tabId,
|
|
200
|
-
data: {
|
|
201
|
-
...session.getSessionInfo(),
|
|
202
|
-
...(lastSeenSeq === undefined ? { outputHistory: buildOutputHistory(session) } : { resumedFromSeq: true }),
|
|
343
|
+
sendResumedTabInitialized(ctx, ws, tabId, session, mode, replay, {
|
|
344
|
+
extra: {
|
|
203
345
|
resumeFailed: isNewSession,
|
|
204
|
-
originalSessionId: isNewSession ? historicalSessionId : undefined
|
|
205
|
-
}
|
|
346
|
+
originalSessionId: isNewSession ? historicalSessionId : undefined,
|
|
347
|
+
},
|
|
206
348
|
});
|
|
207
349
|
}
|
|
208
350
|
|
|
@@ -231,11 +373,14 @@ function reattachSession(
|
|
|
231
373
|
const worktreePath = ctx.gitDirectories.get(tabId);
|
|
232
374
|
const worktreeBranch = ctx.gitBranches.get(tabId);
|
|
233
375
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
376
|
+
const inflightEta = inflightEtaPayload(session);
|
|
377
|
+
const replay = replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
|
|
378
|
+
const mode = decideRecoveryMode(replay, lastSeenSeq);
|
|
379
|
+
|
|
380
|
+
// Fast path: the web already has local state (via Zustand) AND the replay
|
|
381
|
+
// covered the gap cleanly — just replay the buffered events and tell the
|
|
382
|
+
// client to skip the destructive reset in its tabInitialized handler.
|
|
383
|
+
if (mode === 'incremental') {
|
|
239
384
|
ctx.send(ws, {
|
|
240
385
|
type: 'tabInitialized',
|
|
241
386
|
tabId,
|
|
@@ -244,29 +389,27 @@ function reattachSession(
|
|
|
244
389
|
resumedFromSeq: true,
|
|
245
390
|
isExecuting: session.isExecuting,
|
|
246
391
|
...(session.isExecuting && session.executionStartTimestamp ? { executionStartTimestamp: session.executionStartTimestamp } : {}),
|
|
392
|
+
...inflightEta,
|
|
247
393
|
...(worktreePath ? { worktreePath, worktreeBranch } : {}),
|
|
248
394
|
}
|
|
249
395
|
});
|
|
250
396
|
return;
|
|
251
397
|
}
|
|
252
398
|
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
: undefined;
|
|
259
|
-
|
|
399
|
+
// Snapshot path: either cold-start reattach (no prior seq) or replay-gap
|
|
400
|
+
// recovery (`hadGap=true`, no events sent). Both want a full snapshot so
|
|
401
|
+
// the web rebuilds from `outputHistory` + `executionEvents`. The
|
|
402
|
+
// `replayGap` flag distinguishes the two for telemetry — the wire
|
|
403
|
+
// payload shape is otherwise identical.
|
|
260
404
|
ctx.send(ws, {
|
|
261
405
|
type: 'tabInitialized',
|
|
262
406
|
tabId,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
407
|
+
engine: resolveEngineForSession(session),
|
|
408
|
+
data: buildFullSnapshotData(session, {
|
|
409
|
+
worktreePath,
|
|
410
|
+
worktreeBranch,
|
|
411
|
+
engineOverride: regTab?.engineOverride,
|
|
412
|
+
replayGap: replay.hadGap,
|
|
413
|
+
}),
|
|
271
414
|
});
|
|
272
415
|
}
|
|
@@ -12,6 +12,19 @@
|
|
|
12
12
|
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
14
14
|
import { join } from 'node:path'
|
|
15
|
+
import type { EngineId } from './types.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Per-tab engine override persisted alongside the tab record. Survives
|
|
19
|
+
* WebSocket disconnects + process restarts — the web client re-reads it on
|
|
20
|
+
* `tabInitialized` / `activeTabs` to restore the picker state after a
|
|
21
|
+
* reconnect.
|
|
22
|
+
*/
|
|
23
|
+
export interface TabEngineOverride {
|
|
24
|
+
engine: EngineId
|
|
25
|
+
model: string
|
|
26
|
+
effortLevel: string
|
|
27
|
+
}
|
|
15
28
|
|
|
16
29
|
export interface RegisteredTab {
|
|
17
30
|
sessionId: string
|
|
@@ -29,6 +42,12 @@ export interface RegisteredTab {
|
|
|
29
42
|
hasPersistedHistory?: boolean
|
|
30
43
|
worktreePath?: string
|
|
31
44
|
worktreeBranch?: string
|
|
45
|
+
/**
|
|
46
|
+
* Per-tab engine override. Absent when the tab uses the global defaults —
|
|
47
|
+
* authored from the web via `setTabEngine`, cleared by passing `null` to
|
|
48
|
+
* {@link SessionRegistry.updateTabEngineOverride}.
|
|
49
|
+
*/
|
|
50
|
+
engineOverride?: TabEngineOverride
|
|
32
51
|
}
|
|
33
52
|
|
|
34
53
|
interface RegistryData {
|
|
@@ -241,6 +260,24 @@ export class SessionRegistry {
|
|
|
241
260
|
}
|
|
242
261
|
}
|
|
243
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Update or clear the per-tab engine override. Pass `null` to remove the
|
|
265
|
+
* override and route future prompts through the global defaults. Writes
|
|
266
|
+
* through to disk so the override survives WS disconnects — that's the
|
|
267
|
+
* core persistence guarantee of IS-019.
|
|
268
|
+
*/
|
|
269
|
+
updateTabEngineOverride(tabId: string, override: TabEngineOverride | null): void {
|
|
270
|
+
const tab = this.data.tabs[tabId]
|
|
271
|
+
if (!tab) return
|
|
272
|
+
if (override === null) {
|
|
273
|
+
delete tab.engineOverride
|
|
274
|
+
} else {
|
|
275
|
+
tab.engineOverride = { ...override }
|
|
276
|
+
}
|
|
277
|
+
tab.lastActivityAt = new Date().toISOString()
|
|
278
|
+
this.save()
|
|
279
|
+
}
|
|
280
|
+
|
|
244
281
|
/**
|
|
245
282
|
* Reorder tabs. Accepts an ordered array of tabIds and reassigns order values.
|
|
246
283
|
*/
|
|
@@ -3,22 +3,59 @@
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getSettings,
|
|
8
|
+
isEngineSwapEnabled,
|
|
9
|
+
setBouncerClassifier,
|
|
10
|
+
setEffortLevel,
|
|
11
|
+
setModel,
|
|
12
|
+
} from '../settings.js';
|
|
7
13
|
import type { HandlerContext } from './handler-context.js';
|
|
8
14
|
import type { WebSocketMessage, WSContext } from './types.js';
|
|
9
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Return the stored settings with the resolved `engineSwap` boolean patched
|
|
18
|
+
* in, so web clients always see the effective flag value (env-var override,
|
|
19
|
+
* NODE_ENV default, etc.) rather than the raw — possibly `undefined` —
|
|
20
|
+
* stored field.
|
|
21
|
+
*/
|
|
22
|
+
function getSettingsWithResolvedFlags() {
|
|
23
|
+
return { ...getSettings(), engineSwap: isEngineSwapEnabled() };
|
|
24
|
+
}
|
|
25
|
+
|
|
10
26
|
export function handleGetSettings(ctx: HandlerContext, ws: WSContext): void {
|
|
11
|
-
ctx.send(ws, { type: 'settings', data:
|
|
27
|
+
ctx.send(ws, { type: 'settings', data: getSettingsWithResolvedFlags() });
|
|
12
28
|
}
|
|
13
29
|
|
|
14
|
-
export function handleUpdateSettings(ctx: HandlerContext,
|
|
30
|
+
export function handleUpdateSettings(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage): void {
|
|
15
31
|
if (msg.data?.model !== undefined) {
|
|
16
32
|
setModel(msg.data.model);
|
|
17
33
|
}
|
|
18
34
|
if (msg.data?.effortLevel !== undefined) {
|
|
19
35
|
setEffortLevel(msg.data.effortLevel);
|
|
20
36
|
}
|
|
21
|
-
|
|
37
|
+
if (msg.data?.bouncerClassifier !== undefined) {
|
|
38
|
+
try {
|
|
39
|
+
setBouncerClassifier(msg.data.bouncerClassifier);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
// Reject crafted payloads (non-eligible model, bad engine) — surface
|
|
42
|
+
// the reason to the requester and skip the broadcast so other clients
|
|
43
|
+
// keep showing the previous valid config.
|
|
44
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
45
|
+
ctx.send(ws, {
|
|
46
|
+
type: 'error',
|
|
47
|
+
data: {
|
|
48
|
+
scope: 'bouncerClassifier',
|
|
49
|
+
message,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
// Still echo the current settings back to the requester so the UI can
|
|
53
|
+
// revert its optimistic update.
|
|
54
|
+
ctx.send(ws, { type: 'settings', data: getSettingsWithResolvedFlags() });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
ctx.broadcastToAll({ type: 'settingsUpdated', data: getSettingsWithResolvedFlags() });
|
|
22
59
|
}
|
|
23
60
|
|
|
24
61
|
export async function generateNotificationSummary(
|
|
@@ -16,22 +16,30 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { HandlerContext } from './handler-context.js'
|
|
19
|
-
import type { WebSocketResponse } from './types.js'
|
|
19
|
+
import type { EngineId, WebSocketResponse } from './types.js'
|
|
20
20
|
|
|
21
21
|
type TabScopedEventType = WebSocketResponse['type']
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Record + broadcast a tab-scoped event in one call. Returns the assigned
|
|
25
25
|
* sequence number purely for logging/tests — callers rarely need it.
|
|
26
|
+
*
|
|
27
|
+
* `engine` is optional: when supplied (typically by session-driven movement
|
|
28
|
+
* events), it rides on the wire envelope so the web client can render
|
|
29
|
+
* engine-specific affordances. Buffer replay preserves it because the engine
|
|
30
|
+
* is part of the envelope produced for each broadcast.
|
|
26
31
|
*/
|
|
27
32
|
export function broadcastTabEvent(
|
|
28
33
|
ctx: HandlerContext,
|
|
29
34
|
tabId: string,
|
|
30
35
|
type: TabScopedEventType,
|
|
31
36
|
data: unknown,
|
|
37
|
+
engine?: EngineId,
|
|
32
38
|
): number {
|
|
33
39
|
const buffer = ctx.tabEventBuffers.getOrCreate(tabId)
|
|
34
40
|
const seq = buffer.record(type, data)
|
|
35
|
-
|
|
41
|
+
const envelope: WebSocketResponse = { type, tabId, data, seq }
|
|
42
|
+
if (engine) envelope.engine = engine
|
|
43
|
+
ctx.broadcastToAll(envelope)
|
|
36
44
|
return seq
|
|
37
45
|
}
|