jettypod 4.4.118 → 4.4.121
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/.env +4 -3
- package/Cargo.lock +6450 -0
- package/Cargo.toml +35 -0
- package/README.md +5 -1
- package/TAURI-MIGRATION-PLAN.md +840 -0
- package/apps/dashboard/app/connect-claude/page.tsx +5 -6
- package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
- package/apps/dashboard/app/demo/gates/page.tsx +43 -45
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +80 -4
- package/apps/dashboard/app/install-claude/page.tsx +4 -6
- package/apps/dashboard/app/login/page.tsx +72 -54
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +61 -13
- package/apps/dashboard/app/signup/page.tsx +242 -0
- package/apps/dashboard/app/subscribe/page.tsx +0 -2
- package/apps/dashboard/app/tests/page.tsx +37 -4
- package/apps/dashboard/app/welcome/page.tsx +13 -16
- package/apps/dashboard/app/work/[id]/page.tsx +117 -118
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +92 -85
- package/apps/dashboard/components/CardMenu.tsx +45 -12
- package/apps/dashboard/components/ClaudePanel.tsx +771 -850
- package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
- package/apps/dashboard/components/CopyableId.tsx +3 -4
- package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
- package/apps/dashboard/components/DragContext.tsx +134 -63
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +6 -7
- package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +26 -7
- package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
- package/apps/dashboard/components/EpicGroup.tsx +359 -0
- package/apps/dashboard/components/GateCard.tsx +79 -17
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
- package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
- package/apps/dashboard/components/JettyLoader.tsx +37 -0
- package/apps/dashboard/components/KanbanBoard.tsx +368 -958
- package/apps/dashboard/components/KanbanCard.tsx +740 -0
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
- package/apps/dashboard/components/MainNav.tsx +38 -73
- package/apps/dashboard/components/MessageBlock.tsx +468 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -16
- package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
- package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
- package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
- package/apps/dashboard/components/ReviewFooter.tsx +139 -0
- package/apps/dashboard/components/SessionList.tsx +19 -19
- package/apps/dashboard/components/SubscribeContent.tsx +91 -47
- package/apps/dashboard/components/TestTree.tsx +16 -16
- package/apps/dashboard/components/TipCard.tsx +16 -17
- package/apps/dashboard/components/Toast.tsx +5 -6
- package/apps/dashboard/components/TypeIcon.tsx +55 -0
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
- package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
- package/apps/dashboard/components/WorkItemTree.tsx +11 -32
- package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
- package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
- package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
- package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
- package/apps/dashboard/contexts/UsageContext.tsx +87 -32
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1265
- package/apps/dashboard/lib/environment-config.ts +173 -0
- package/apps/dashboard/lib/environment-verification.ts +119 -0
- package/apps/dashboard/lib/kanban-utils.ts +270 -0
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/service-recovery.ts +326 -0
- package/apps/dashboard/lib/session-state-machine.ts +1 -0
- package/apps/dashboard/lib/session-state-utils.ts +0 -164
- package/apps/dashboard/lib/session-stream-manager.ts +308 -134
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
- package/apps/dashboard/lib/tauri-bridge.ts +102 -0
- package/apps/dashboard/lib/tauri.ts +106 -0
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -32
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.png +0 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.png +0 -0
- package/apps/dashboard/public/star-icon.png +0 -0
- package/apps/dashboard/public/wrench-icon.png +0 -0
- package/apps/dashboard/scripts/tauri-build.js +228 -0
- package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/src/main.tsx +12 -0
- package/apps/dashboard/src/router.tsx +107 -0
- package/apps/dashboard/src/vite-env.d.ts +1 -0
- package/apps/dashboard/tsconfig.json +7 -12
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
- package/apps/dashboard/vite.config.ts +33 -0
- package/apps/update-server/src/index.ts +228 -80
- package/claude-hooks/global-guardrails.js +14 -13
- package/crates/jettypod-cli/Cargo.toml +19 -0
- package/crates/jettypod-cli/src/commands.rs +1249 -0
- package/crates/jettypod-cli/src/main.rs +595 -0
- package/crates/jettypod-core/Cargo.toml +26 -0
- package/crates/jettypod-core/build.rs +98 -0
- package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
- package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
- package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
- package/crates/jettypod-core/src/auth.rs +294 -0
- package/crates/jettypod-core/src/config.rs +397 -0
- package/crates/jettypod-core/src/db/mod.rs +507 -0
- package/crates/jettypod-core/src/db/recovery.rs +114 -0
- package/crates/jettypod-core/src/db/startup.rs +101 -0
- package/crates/jettypod-core/src/db/validate.rs +149 -0
- package/crates/jettypod-core/src/error.rs +76 -0
- package/crates/jettypod-core/src/git.rs +458 -0
- package/crates/jettypod-core/src/lib.rs +20 -0
- package/crates/jettypod-core/src/sessions.rs +625 -0
- package/crates/jettypod-core/src/skills.rs +556 -0
- package/crates/jettypod-core/src/work.rs +1086 -0
- package/crates/jettypod-core/src/worktree.rs +628 -0
- package/crates/jettypod-core/src/ws.rs +767 -0
- package/cucumber-test.cjs +6 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +145 -116
- package/lib/bdd-preflight.js +96 -0
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/027-plan-at-creation-column.js +3 -1
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +10 -5
- package/lib/seed-onboarding.js +1 -1
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +144 -19
- package/lib/work-tracking/index.js +148 -27
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +79 -20
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +171 -69
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +82 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +173 -74
- package/skills-templates/production-mode/SKILL.md +69 -49
- package/skills-templates/request-routing/SKILL.md +4 -4
- package/skills-templates/simple-improvement/SKILL.md +74 -29
- package/skills-templates/speed-mode/SKILL.md +217 -141
- package/skills-templates/stable-mode/SKILL.md +148 -89
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
- package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
- package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
- package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
- package/apps/dashboard/app/api/kanban/route.ts +0 -15
- package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
- package/apps/dashboard/app/api/settings/general/route.ts +0 -21
- package/apps/dashboard/app/api/tests/route.ts +0 -9
- package/apps/dashboard/app/api/tests/run/route.ts +0 -82
- package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
- package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
- package/apps/dashboard/app/api/usage/route.ts +0 -17
- package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -43
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
- package/apps/dashboard/electron/ipc-handlers.js +0 -1028
- package/apps/dashboard/electron/main.js +0 -2124
- package/apps/dashboard/electron/preload.js +0 -123
- package/apps/dashboard/electron/session-manager.js +0 -141
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/claude-process-manager.ts +0 -492
- package/apps/dashboard/lib/db-bridge.ts +0 -282
- package/apps/dashboard/lib/prototypes.ts +0 -202
- package/apps/dashboard/lib/test-results-db.ts +0 -307
- package/apps/dashboard/lib/tests.ts +0 -282
- package/apps/dashboard/next.config.js +0 -50
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
- package/docs/bdd-guidance.md +0 -390
|
@@ -3,9 +3,31 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extracted from useClaudeStream hook to enable each session to have its own
|
|
5
5
|
* independent stream state. Multiple instances can run concurrently.
|
|
6
|
+
*
|
|
7
|
+
* Uses Tauri IPC (invoke) to spawn Claude processes and Tauri events to receive
|
|
8
|
+
* streaming output, replacing the previous fetch-based SSE approach.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import type { AttachedImage } from '../components/ClaudePanelInput';
|
|
12
|
+
import { invoke, listen } from './tauri';
|
|
13
|
+
|
|
14
|
+
// Tauri event listener type
|
|
15
|
+
type UnlistenFn = () => void;
|
|
16
|
+
|
|
17
|
+
interface ProcessOutputBatchEvent {
|
|
18
|
+
pid: number;
|
|
19
|
+
stream: string; // "stdout" | "stderr"
|
|
20
|
+
lines: string[];
|
|
21
|
+
label: string;
|
|
22
|
+
kind: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ProcessExitEvent {
|
|
26
|
+
pid: number;
|
|
27
|
+
code: number | null;
|
|
28
|
+
label: string;
|
|
29
|
+
kind: string;
|
|
30
|
+
}
|
|
9
31
|
|
|
10
32
|
// ============================================================================
|
|
11
33
|
// Types
|
|
@@ -46,12 +68,14 @@ export interface StreamState {
|
|
|
46
68
|
isReconnecting: boolean;
|
|
47
69
|
reconnectAttempt: number;
|
|
48
70
|
narratedMode: boolean;
|
|
71
|
+
fullReadoutMode: boolean;
|
|
72
|
+
rawEvents: unknown[];
|
|
49
73
|
queuedMessage: QueuedMessage | null;
|
|
50
74
|
}
|
|
51
75
|
|
|
52
76
|
export interface StreamManagerCallbacks {
|
|
53
77
|
onStateChange?: (state: StreamState) => void;
|
|
54
|
-
onWorkItemCreated?: (workItemId: number, title: string) => void;
|
|
78
|
+
onWorkItemCreated?: (workItemId: number, title: string, sourceSessionId: string) => void;
|
|
55
79
|
onGate?: (gate: ClaudeMessage) => void;
|
|
56
80
|
onQuestion?: (gate: ClaudeMessage) => void;
|
|
57
81
|
}
|
|
@@ -80,10 +104,12 @@ const GATE_PATTERN = /\[GATE:([\w-]+)\](.*?)\[\/GATE\]/;
|
|
|
80
104
|
// ============================================================================
|
|
81
105
|
|
|
82
106
|
/**
|
|
83
|
-
* Transform Claude's stream-json format into our ClaudeMessage format
|
|
107
|
+
* Transform Claude's stream-json format into our ClaudeMessage format.
|
|
108
|
+
* Returns an array when a single event contains multiple content blocks
|
|
109
|
+
* (e.g., assistant message with both text and tool_use).
|
|
84
110
|
*/
|
|
85
111
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
-
function transformClaudeMessage(parsed: any): ClaudeMessage | null {
|
|
112
|
+
function transformClaudeMessage(parsed: any): ClaudeMessage | ClaudeMessage[] | null {
|
|
87
113
|
const timestamp = Date.now();
|
|
88
114
|
|
|
89
115
|
switch (parsed.type) {
|
|
@@ -94,24 +120,32 @@ function transformClaudeMessage(parsed: any): ClaudeMessage | null {
|
|
|
94
120
|
case 'assistant': {
|
|
95
121
|
const messageContent = parsed.message?.content;
|
|
96
122
|
if (Array.isArray(messageContent)) {
|
|
123
|
+
const messages: ClaudeMessage[] = [];
|
|
124
|
+
|
|
97
125
|
const textParts = messageContent
|
|
98
126
|
.filter((part: { type: string }) => part.type === 'text')
|
|
99
127
|
.map((part: { text: string }) => part.text)
|
|
100
128
|
.join('');
|
|
101
129
|
|
|
102
130
|
if (textParts) {
|
|
103
|
-
|
|
131
|
+
messages.push({ type: 'assistant', content: textParts, timestamp });
|
|
104
132
|
}
|
|
105
133
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
134
|
+
// Emit ALL tool_use blocks (Claude can call multiple tools in parallel)
|
|
135
|
+
for (const part of messageContent) {
|
|
136
|
+
if (part.type === 'tool_use') {
|
|
137
|
+
messages.push({
|
|
138
|
+
type: 'tool_use',
|
|
139
|
+
tool_name: part.name,
|
|
140
|
+
tool_input: part.input,
|
|
141
|
+
timestamp,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
114
144
|
}
|
|
145
|
+
|
|
146
|
+
if (messages.length === 0) return null;
|
|
147
|
+
if (messages.length === 1) return messages[0];
|
|
148
|
+
return messages;
|
|
115
149
|
}
|
|
116
150
|
return null;
|
|
117
151
|
}
|
|
@@ -195,6 +229,23 @@ function transformClaudeMessage(parsed: any): ClaudeMessage | null {
|
|
|
195
229
|
timestamp,
|
|
196
230
|
};
|
|
197
231
|
|
|
232
|
+
// Claude CLI v2.1.49+ may emit streaming content block deltas
|
|
233
|
+
case 'content_block_delta': {
|
|
234
|
+
const delta = parsed.delta as { type?: string; text?: string } | undefined;
|
|
235
|
+
if (delta?.type === 'text_delta' && delta.text) {
|
|
236
|
+
return { type: 'assistant', content: delta.text, timestamp };
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case 'content_block_start':
|
|
242
|
+
case 'content_block_stop':
|
|
243
|
+
case 'message_start':
|
|
244
|
+
case 'message_delta':
|
|
245
|
+
case 'message_stop':
|
|
246
|
+
// Streaming lifecycle events — no user-visible content
|
|
247
|
+
return null;
|
|
248
|
+
|
|
198
249
|
default:
|
|
199
250
|
return null;
|
|
200
251
|
}
|
|
@@ -213,14 +264,13 @@ function extractCreatedWorkItem(content: string | undefined): { id: number; titl
|
|
|
213
264
|
* Check any message for work item creation
|
|
214
265
|
*/
|
|
215
266
|
function checkMessageForWorkItemCreation(message: ClaudeMessage): { id: number; title: string } | null {
|
|
216
|
-
|
|
267
|
+
// Only check tool_result messages, not assistant content.
|
|
268
|
+
// Checking message.content would false-positive when Claude mentions
|
|
269
|
+
// patterns like "Created feature #123: My Feature" in its text output.
|
|
270
|
+
if (message.type === 'tool_result' && message.result) {
|
|
217
271
|
const created = extractCreatedWorkItem(message.result);
|
|
218
272
|
if (created) return created;
|
|
219
273
|
}
|
|
220
|
-
if (message.content) {
|
|
221
|
-
const created = extractCreatedWorkItem(message.content);
|
|
222
|
-
if (created) return created;
|
|
223
|
-
}
|
|
224
274
|
return null;
|
|
225
275
|
}
|
|
226
276
|
|
|
@@ -273,6 +323,9 @@ export class SessionStreamManager {
|
|
|
273
323
|
private _isReconnecting: boolean = false;
|
|
274
324
|
private _reconnectAttempt: number = 0;
|
|
275
325
|
private _narratedMode: boolean = true;
|
|
326
|
+
private _userToggledNarratedMode: boolean = false;
|
|
327
|
+
private _fullReadoutMode: boolean = false;
|
|
328
|
+
private _rawEvents: unknown[] = [];
|
|
276
329
|
private _pendingQuestion: ClaudeMessage | null = null;
|
|
277
330
|
private _queuedMessage: QueuedMessage | null = null;
|
|
278
331
|
private _isFirstMessage: boolean = true;
|
|
@@ -280,15 +333,29 @@ export class SessionStreamManager {
|
|
|
280
333
|
// Notification batching — coalesces rapid state changes into one callback per frame
|
|
281
334
|
private _notifyPending: boolean = false;
|
|
282
335
|
|
|
336
|
+
// Tauri process tracking
|
|
337
|
+
private activePid: number | null = null;
|
|
338
|
+
private unlistenOutput: UnlistenFn | null = null;
|
|
339
|
+
private unlistenExit: UnlistenFn | null = null;
|
|
340
|
+
private _isContinue: boolean = false; // tracks if this is a follow-up message
|
|
341
|
+
|
|
283
342
|
// Control
|
|
284
|
-
private abortController: AbortController | null = null;
|
|
285
343
|
private creatingStatusTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
286
344
|
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
287
345
|
private stopped: boolean = false;
|
|
288
346
|
|
|
347
|
+
// Last tool_use tracking (to correlate tool_result with the tool that produced it)
|
|
348
|
+
private _lastToolUse: { name: string; command: string } | null = null;
|
|
349
|
+
|
|
350
|
+
// stderr buffer — collected per-stream to surface in error messages
|
|
351
|
+
private _stderrLines: string[] = [];
|
|
352
|
+
|
|
289
353
|
// Last request (for retry/reconnect)
|
|
290
354
|
private lastMessage: string | null = null;
|
|
291
355
|
private lastImages: AttachedImage[] | undefined = undefined;
|
|
356
|
+
private _lastWasHidden: boolean = false;
|
|
357
|
+
// Index of the user message that triggered the current request (for precise retry removal)
|
|
358
|
+
private _lastUserMessageIndex: number = -1;
|
|
292
359
|
|
|
293
360
|
// Callbacks
|
|
294
361
|
private callbacks: StreamManagerCallbacks;
|
|
@@ -334,6 +401,14 @@ export class SessionStreamManager {
|
|
|
334
401
|
return this._narratedMode;
|
|
335
402
|
}
|
|
336
403
|
|
|
404
|
+
get fullReadoutMode(): boolean {
|
|
405
|
+
return this._fullReadoutMode;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
get rawEvents(): unknown[] {
|
|
409
|
+
return this._rawEvents;
|
|
410
|
+
}
|
|
411
|
+
|
|
337
412
|
get pendingQuestion(): ClaudeMessage | null {
|
|
338
413
|
return this._pendingQuestion;
|
|
339
414
|
}
|
|
@@ -361,9 +436,29 @@ export class SessionStreamManager {
|
|
|
361
436
|
|
|
362
437
|
setNarratedMode(enabled: boolean): void {
|
|
363
438
|
this._narratedMode = enabled;
|
|
439
|
+
this._userToggledNarratedMode = true;
|
|
440
|
+
this.notifyStateChange();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Update narrated mode without firing notifyStateChange.
|
|
445
|
+
* Used when React state is the source of truth (e.g., user toggle)
|
|
446
|
+
* to keep the stream manager in sync without overwriting React state.
|
|
447
|
+
*/
|
|
448
|
+
setNarratedModeQuiet(enabled: boolean): void {
|
|
449
|
+
this._narratedMode = enabled;
|
|
450
|
+
this._userToggledNarratedMode = true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
setFullReadoutMode(enabled: boolean): void {
|
|
454
|
+
this._fullReadoutMode = enabled;
|
|
364
455
|
this.notifyStateChange();
|
|
365
456
|
}
|
|
366
457
|
|
|
458
|
+
setFullReadoutModeQuiet(enabled: boolean): void {
|
|
459
|
+
this._fullReadoutMode = enabled;
|
|
460
|
+
}
|
|
461
|
+
|
|
367
462
|
/**
|
|
368
463
|
* Answer a pending question gate by clearing it and sending the selection as a message
|
|
369
464
|
*/
|
|
@@ -383,6 +478,8 @@ export class SessionStreamManager {
|
|
|
383
478
|
isReconnecting: this._isReconnecting,
|
|
384
479
|
reconnectAttempt: this._reconnectAttempt,
|
|
385
480
|
narratedMode: this._narratedMode,
|
|
481
|
+
fullReadoutMode: this._fullReadoutMode,
|
|
482
|
+
rawEvents: this._rawEvents,
|
|
386
483
|
queuedMessage: this._queuedMessage,
|
|
387
484
|
};
|
|
388
485
|
}
|
|
@@ -454,7 +551,9 @@ export class SessionStreamManager {
|
|
|
454
551
|
}
|
|
455
552
|
// Add the gate message instead of (or in addition to) the raw message
|
|
456
553
|
this._messages = [...this._messages, gate];
|
|
457
|
-
this.
|
|
554
|
+
if (!this._userToggledNarratedMode) {
|
|
555
|
+
this._narratedMode = true; // Auto-enable narrated mode on first gate
|
|
556
|
+
}
|
|
458
557
|
this.callbacks.onGate?.(gate);
|
|
459
558
|
|
|
460
559
|
// Question gates pause the workflow for user input
|
|
@@ -493,11 +592,29 @@ export class SessionStreamManager {
|
|
|
493
592
|
return false;
|
|
494
593
|
}
|
|
495
594
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
595
|
+
/**
|
|
596
|
+
* Clean up Tauri event listeners
|
|
597
|
+
*/
|
|
598
|
+
private cleanupEventListeners(): void {
|
|
599
|
+
if (this.unlistenOutput) {
|
|
600
|
+
this.unlistenOutput();
|
|
601
|
+
this.unlistenOutput = null;
|
|
602
|
+
}
|
|
603
|
+
if (this.unlistenExit) {
|
|
604
|
+
this.unlistenExit();
|
|
605
|
+
this.unlistenExit = null;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Process any queued message after the current stream completes
|
|
611
|
+
*/
|
|
612
|
+
private processQueuedMessage(): void {
|
|
613
|
+
if (this._queuedMessage && !this.stopped) {
|
|
614
|
+
const { message, images } = this._queuedMessage;
|
|
615
|
+
this._queuedMessage = null;
|
|
616
|
+
this.sendMessage(message, images);
|
|
617
|
+
}
|
|
501
618
|
}
|
|
502
619
|
|
|
503
620
|
/**
|
|
@@ -522,18 +639,16 @@ export class SessionStreamManager {
|
|
|
522
639
|
this.reconnectTimeout = setTimeout(() => {
|
|
523
640
|
if (this.stopped || !this.lastMessage) return;
|
|
524
641
|
|
|
525
|
-
// Remove
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (lastUserIndex >= 0) {
|
|
529
|
-
this._messages = this._messages.slice(0, lastUserIndex);
|
|
642
|
+
// Remove messages from the failed request (will be re-added on retry)
|
|
643
|
+
if (this._lastUserMessageIndex >= 0) {
|
|
644
|
+
this._messages = this._messages.slice(0, this._lastUserMessageIndex);
|
|
530
645
|
}
|
|
531
646
|
|
|
532
647
|
this._isReconnecting = false;
|
|
533
648
|
this.notifyStateChange();
|
|
534
649
|
|
|
535
650
|
// Retry the request
|
|
536
|
-
this.sendMessage(this.lastMessage, this.lastImages);
|
|
651
|
+
this.sendMessage(this.lastMessage, this.lastImages, { hidden: this._lastWasHidden });
|
|
537
652
|
}, delay);
|
|
538
653
|
}
|
|
539
654
|
|
|
@@ -542,22 +657,32 @@ export class SessionStreamManager {
|
|
|
542
657
|
// -------------------------------------------------------------------------
|
|
543
658
|
|
|
544
659
|
/**
|
|
545
|
-
* Send a message to Claude and stream the response
|
|
660
|
+
* Send a message to Claude and stream the response.
|
|
661
|
+
* When hidden is true, the user message is not added to visible messages —
|
|
662
|
+
* used for conversational sessions where Claude should speak first.
|
|
546
663
|
*/
|
|
547
|
-
async sendMessage(message: string, images?: AttachedImage[]): Promise<void> {
|
|
664
|
+
async sendMessage(message: string, images?: AttachedImage[], options?: { hidden?: boolean }): Promise<void> {
|
|
665
|
+
const hidden = options?.hidden ?? false;
|
|
548
666
|
this.stopped = false;
|
|
667
|
+
this._stderrLines = [];
|
|
549
668
|
|
|
550
669
|
// Store for potential retry/reconnect
|
|
551
670
|
this.lastMessage = message;
|
|
552
671
|
this.lastImages = images;
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
672
|
+
this._lastWasHidden = hidden;
|
|
673
|
+
|
|
674
|
+
// Add user message immediately (unless hidden — conversational system instructions)
|
|
675
|
+
if (!hidden) {
|
|
676
|
+
const userMessage: ClaudeMessage = {
|
|
677
|
+
type: 'user',
|
|
678
|
+
content: message,
|
|
679
|
+
timestamp: Date.now(),
|
|
680
|
+
};
|
|
681
|
+
this._lastUserMessageIndex = this._messages.length;
|
|
682
|
+
this.addMessage(userMessage);
|
|
683
|
+
} else {
|
|
684
|
+
this._lastUserMessageIndex = this._messages.length;
|
|
685
|
+
}
|
|
561
686
|
|
|
562
687
|
// For first message in a new session, show "creating" status
|
|
563
688
|
// Conversational sessions skip the 5s delay and go straight to streaming
|
|
@@ -584,119 +709,152 @@ export class SessionStreamManager {
|
|
|
584
709
|
this.notifyStateChange();
|
|
585
710
|
}
|
|
586
711
|
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
712
|
+
// Clean up any previous event listeners
|
|
713
|
+
this.cleanupEventListeners();
|
|
714
|
+
|
|
715
|
+
// Set up event listeners BEFORE spawning so we never miss events.
|
|
716
|
+
// Use a mutable ref for PID filtering — events arriving before the PID is set
|
|
717
|
+
// are from other processes and get filtered out. Once the spawn returns and
|
|
718
|
+
// the PID is assigned, all subsequent events from our process are captured.
|
|
719
|
+
const pidRef = { current: 0 };
|
|
592
720
|
|
|
593
721
|
try {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
722
|
+
await this.listenForProcessEvents(pidRef);
|
|
723
|
+
|
|
724
|
+
// Spawn Claude process via Tauri IPC
|
|
725
|
+
const pid = await invoke<number>('claude_start_stream', {
|
|
726
|
+
args: {
|
|
727
|
+
session_id: this.sessionContext.workItemId,
|
|
600
728
|
message,
|
|
729
|
+
work_item_id: this.sessionContext.standalone ? null : Number(this.sessionContext.workItemId) || null,
|
|
601
730
|
images: images?.map(img => ({
|
|
602
731
|
type: img.type,
|
|
603
732
|
data: img.dataUrl,
|
|
604
|
-
})),
|
|
605
|
-
|
|
606
|
-
|
|
733
|
+
})) || null,
|
|
734
|
+
is_continue: this._isContinue,
|
|
735
|
+
narrated_mode: this._narratedMode,
|
|
736
|
+
full_readout: this._fullReadoutMode,
|
|
737
|
+
},
|
|
607
738
|
});
|
|
608
739
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (!response.body) {
|
|
614
|
-
throw new Error('Response body is null');
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
await this.processStream(response.body);
|
|
740
|
+
// Set PID — events from our process will now be accepted by the listeners
|
|
741
|
+
pidRef.current = pid;
|
|
742
|
+
this.activePid = pid;
|
|
743
|
+
this._isContinue = true; // subsequent messages use --continue
|
|
618
744
|
|
|
619
|
-
//
|
|
745
|
+
// Reset reconnect state on successful spawn
|
|
620
746
|
this._reconnectAttempt = 0;
|
|
621
747
|
this._isReconnecting = false;
|
|
622
748
|
} catch (err) {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
// Non-network error or max attempts exhausted
|
|
631
|
-
this._status = 'error';
|
|
632
|
-
this._error = err.message;
|
|
633
|
-
this._canRetry = true; // Allow manual retry
|
|
634
|
-
this.notifyStateChange();
|
|
635
|
-
}
|
|
636
|
-
}
|
|
749
|
+
// Clean up listeners on failure
|
|
750
|
+
this.cleanupEventListeners();
|
|
751
|
+
// Tauri invoke rejects with strings (not Error objects), so handle both.
|
|
752
|
+
this._status = 'error';
|
|
753
|
+
this._error = err instanceof Error ? err.message : String(err);
|
|
754
|
+
this._canRetry = true;
|
|
755
|
+
this.notifyStateChange();
|
|
637
756
|
}
|
|
638
757
|
}
|
|
639
758
|
|
|
640
759
|
/**
|
|
641
|
-
*
|
|
760
|
+
* Listen for Tauri process events and process Claude output.
|
|
761
|
+
* Replaces the old processStream() method that consumed SSE from fetch.
|
|
642
762
|
*/
|
|
643
|
-
private async
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
const lines = buffer.split('\n');
|
|
661
|
-
buffer = lines.pop() || '';
|
|
662
|
-
|
|
663
|
-
for (const line of lines) {
|
|
664
|
-
if (line.startsWith('data: ')) {
|
|
665
|
-
const data = line.slice(6);
|
|
666
|
-
try {
|
|
667
|
-
const parsed = JSON.parse(data);
|
|
668
|
-
const claudeMessage = transformClaudeMessage(parsed);
|
|
669
|
-
|
|
670
|
-
if (claudeMessage && !this.stopped) {
|
|
763
|
+
private async listenForProcessEvents(pidRef: { current: number }): Promise<void> {
|
|
764
|
+
// Listen for stdout/stderr line batches (backend sends batches of lines every ~50ms)
|
|
765
|
+
this.unlistenOutput = await listen<ProcessOutputBatchEvent>('process-output-batch', (event) => {
|
|
766
|
+
const { payload } = event;
|
|
767
|
+
if (pidRef.current === 0 || payload.pid !== pidRef.current) return; // Filter to our process
|
|
768
|
+
|
|
769
|
+
if (payload.stream === 'stdout') {
|
|
770
|
+
for (const line of payload.lines) {
|
|
771
|
+
// Each stdout line from `claude --output-format stream-json` is a JSON object
|
|
772
|
+
try {
|
|
773
|
+
const parsed = JSON.parse(line);
|
|
774
|
+
this._rawEvents.push(parsed);
|
|
775
|
+
const result = transformClaudeMessage(parsed);
|
|
776
|
+
const claudeMessages = result === null ? [] : Array.isArray(result) ? result : [result];
|
|
777
|
+
|
|
778
|
+
for (const claudeMessage of claudeMessages) {
|
|
779
|
+
if (!this.stopped) {
|
|
671
780
|
this.addMessage(claudeMessage);
|
|
672
781
|
|
|
782
|
+
// Track last tool_use to correlate with tool_result
|
|
783
|
+
if (claudeMessage.type === 'tool_use') {
|
|
784
|
+
this._lastToolUse = {
|
|
785
|
+
name: claudeMessage.tool_name || '',
|
|
786
|
+
command: (claudeMessage.tool_input as Record<string, string>)?.command || '',
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
673
790
|
// Check for work item creation
|
|
674
791
|
if (this.sessionContext.standalone && this.callbacks.onWorkItemCreated) {
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
792
|
+
const wasBashJettypod = this._lastToolUse?.name === 'Bash'
|
|
793
|
+
&& this._lastToolUse.command.includes('jettypod');
|
|
794
|
+
if (wasBashJettypod) {
|
|
795
|
+
const created = checkMessageForWorkItemCreation(claudeMessage);
|
|
796
|
+
if (created) {
|
|
797
|
+
this.callbacks.onWorkItemCreated(created.id, created.title, this.sessionContext.workItemId);
|
|
798
|
+
}
|
|
678
799
|
}
|
|
679
800
|
}
|
|
680
801
|
}
|
|
802
|
+
}
|
|
681
803
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
692
|
-
|
|
804
|
+
// Handle completion states from the JSON
|
|
805
|
+
if (parsed.type === 'result' || parsed.type === 'done') {
|
|
806
|
+
this._status = 'done';
|
|
807
|
+
this.notifyStateChange();
|
|
808
|
+
this.cleanupEventListeners();
|
|
809
|
+
// Defer queued message to next microtask to avoid setting up
|
|
810
|
+
// new listeners while old ones are being cleaned up
|
|
811
|
+
queueMicrotask(() => this.processQueuedMessage());
|
|
812
|
+
return; // Stop processing batch on completion
|
|
813
|
+
} else if (parsed.type === 'error') {
|
|
814
|
+
this._status = 'error';
|
|
815
|
+
this._error = parsed.content || 'Unknown error';
|
|
816
|
+
this.notifyStateChange();
|
|
817
|
+
this.cleanupEventListeners();
|
|
818
|
+
return; // Stop processing batch on error
|
|
693
819
|
}
|
|
820
|
+
} catch {
|
|
821
|
+
// Non-JSON line from stdout, skip (e.g., progress indicators)
|
|
694
822
|
}
|
|
695
823
|
}
|
|
696
824
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
825
|
+
// Collect stderr lines so we can surface them if the process fails
|
|
826
|
+
if (payload.stream === 'stderr') {
|
|
827
|
+
this._stderrLines.push(...payload.lines);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// Listen for process exit
|
|
832
|
+
this.unlistenExit = await listen<ProcessExitEvent>('process-exit', (event) => {
|
|
833
|
+
const { payload } = event;
|
|
834
|
+
if (pidRef.current === 0 || payload.pid !== pidRef.current) return;
|
|
835
|
+
|
|
836
|
+
this.activePid = null;
|
|
837
|
+
|
|
838
|
+
// If status wasn't already set to 'done' by a result/done JSON message,
|
|
839
|
+
// set it based on exit code
|
|
840
|
+
if (this._status === 'streaming' || this._status === 'creating' || this._status === 'connecting') {
|
|
841
|
+
if (payload.code === 0) {
|
|
842
|
+
this._status = 'done';
|
|
843
|
+
} else {
|
|
844
|
+
this._status = 'error';
|
|
845
|
+
// Include stderr output so users can see why the process failed
|
|
846
|
+
const stderrSummary = this._stderrLines.join('\n').trim();
|
|
847
|
+
this._error = stderrSummary
|
|
848
|
+
? `${stderrSummary}`
|
|
849
|
+
: `Process exited with code ${payload.code}`;
|
|
850
|
+
this._canRetry = true;
|
|
851
|
+
}
|
|
852
|
+
this.notifyStateChange();
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
this.cleanupEventListeners();
|
|
856
|
+
queueMicrotask(() => this.processQueuedMessage());
|
|
857
|
+
});
|
|
700
858
|
}
|
|
701
859
|
|
|
702
860
|
/**
|
|
@@ -705,11 +863,17 @@ export class SessionStreamManager {
|
|
|
705
863
|
stop(): void {
|
|
706
864
|
this.stopped = true;
|
|
707
865
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
this.
|
|
866
|
+
// Kill the active Claude process
|
|
867
|
+
if (this.activePid !== null) {
|
|
868
|
+
invoke('claude_stop_stream', { sessionId: this.sessionContext.workItemId }).catch(err => {
|
|
869
|
+
console.error('Failed to stop stream:', this.sessionContext.workItemId, err);
|
|
870
|
+
});
|
|
871
|
+
this.activePid = null;
|
|
711
872
|
}
|
|
712
873
|
|
|
874
|
+
// Clean up event listeners
|
|
875
|
+
this.cleanupEventListeners();
|
|
876
|
+
|
|
713
877
|
if (this.reconnectTimeout) {
|
|
714
878
|
clearTimeout(this.reconnectTimeout);
|
|
715
879
|
this.reconnectTimeout = null;
|
|
@@ -723,7 +887,7 @@ export class SessionStreamManager {
|
|
|
723
887
|
this._status = 'idle';
|
|
724
888
|
this._isReconnecting = false;
|
|
725
889
|
this._reconnectAttempt = 0;
|
|
726
|
-
this._queuedMessage = null;
|
|
890
|
+
this._queuedMessage = null;
|
|
727
891
|
this.notifyStateChange();
|
|
728
892
|
}
|
|
729
893
|
|
|
@@ -731,6 +895,8 @@ export class SessionStreamManager {
|
|
|
731
895
|
* Clear all messages and reset state
|
|
732
896
|
*/
|
|
733
897
|
clear(): void {
|
|
898
|
+
this.cleanupEventListeners();
|
|
899
|
+
this._isContinue = false; // Reset continue flag
|
|
734
900
|
this._messages = [];
|
|
735
901
|
this._status = 'idle';
|
|
736
902
|
this._error = null;
|
|
@@ -739,6 +905,8 @@ export class SessionStreamManager {
|
|
|
739
905
|
this._isReconnecting = false;
|
|
740
906
|
this._reconnectAttempt = 0;
|
|
741
907
|
this._queuedMessage = null;
|
|
908
|
+
this._rawEvents = [];
|
|
909
|
+
this._stderrLines = [];
|
|
742
910
|
this.lastMessage = null;
|
|
743
911
|
this.lastImages = undefined;
|
|
744
912
|
this.notifyStateChange();
|
|
@@ -758,16 +926,15 @@ export class SessionStreamManager {
|
|
|
758
926
|
this._error = null;
|
|
759
927
|
this._canRetry = false;
|
|
760
928
|
|
|
761
|
-
// Remove the failed
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
this._messages = this._messages.slice(0, lastUserIndex);
|
|
929
|
+
// Remove messages from the failed request (will be re-added on retry)
|
|
930
|
+
if (this._lastUserMessageIndex >= 0) {
|
|
931
|
+
this._messages = this._messages.slice(0, this._lastUserMessageIndex);
|
|
765
932
|
}
|
|
766
933
|
|
|
767
934
|
this.notifyStateChange();
|
|
768
935
|
|
|
769
936
|
// Resend the last message
|
|
770
|
-
this.sendMessage(this.lastMessage, this.lastImages);
|
|
937
|
+
this.sendMessage(this.lastMessage, this.lastImages, { hidden: this._lastWasHidden });
|
|
771
938
|
}
|
|
772
939
|
|
|
773
940
|
/**
|
|
@@ -787,7 +954,9 @@ export class SessionStreamManager {
|
|
|
787
954
|
timestamp: Date.now(),
|
|
788
955
|
};
|
|
789
956
|
this._messages = [...this._messages, gate];
|
|
790
|
-
this.
|
|
957
|
+
if (!this._userToggledNarratedMode) {
|
|
958
|
+
this._narratedMode = true;
|
|
959
|
+
}
|
|
791
960
|
this.callbacks.onGate?.(gate);
|
|
792
961
|
this.notifyStateChange();
|
|
793
962
|
}
|
|
@@ -804,7 +973,12 @@ export class SessionStreamManager {
|
|
|
804
973
|
*/
|
|
805
974
|
destroy(): void {
|
|
806
975
|
this.stop();
|
|
976
|
+
// Remove session from Rust tracker
|
|
977
|
+
invoke('claude_remove_stream_session', { sessionId: this.sessionContext.workItemId }).catch(err => {
|
|
978
|
+
console.error('Failed to remove stream session:', this.sessionContext.workItemId, err);
|
|
979
|
+
});
|
|
807
980
|
this._messages = [];
|
|
981
|
+
this._rawEvents = [];
|
|
808
982
|
}
|
|
809
983
|
}
|
|
810
984
|
|