spec-cat 0.1.21 → 0.1.23

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 (139) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/{qt0TxNaY.js → -EMqkm_u.js} +1 -1
  3. package/.output/public/_nuxt/{N3ZxlKm3.js → BDd_kh9e.js} +1 -1
  4. package/.output/public/_nuxt/BJKzOiTU.js +1 -0
  5. package/.output/public/_nuxt/{DqNujSig.js → Bk4uZfKR.js} +1 -1
  6. package/.output/public/_nuxt/C9Qk2Hno.js +1 -0
  7. package/.output/public/_nuxt/CDLI67Cr.js +1 -0
  8. package/.output/public/_nuxt/CLPz4Oer.js +1 -0
  9. package/.output/public/_nuxt/CYVeOpC3.js +1 -0
  10. package/.output/public/_nuxt/CZbP5QXb.js +1 -0
  11. package/.output/public/_nuxt/DQtVbA-s.js +150 -0
  12. package/.output/public/_nuxt/DXWMFGmi.js +1 -0
  13. package/.output/public/_nuxt/DgiJut-o.js +1 -0
  14. package/.output/public/_nuxt/{DUodkQcr.js → FbKhJXKu.js} +3 -3
  15. package/.output/public/_nuxt/builds/latest.json +1 -1
  16. package/.output/public/_nuxt/builds/meta/53b5c409-86a8-4624-8a0f-5bf31ab82deb.json +1 -0
  17. package/.output/public/_nuxt/default.eSO6fRPf.css +1 -0
  18. package/.output/public/_nuxt/entry.BtencOYX.css +1 -0
  19. package/.output/public/_nuxt/useTheme.Dp77PlfC.css +1 -0
  20. package/.output/server/chunks/_/conversationStore.mjs +13 -1
  21. package/.output/server/chunks/_/conversationStore.mjs.map +1 -1
  22. package/.output/server/chunks/_/git.mjs +7 -6
  23. package/.output/server/chunks/_/git.mjs.map +1 -1
  24. package/.output/server/chunks/_/git2.mjs.map +1 -1
  25. package/.output/server/chunks/_/gitApiHelpers.mjs +43 -0
  26. package/.output/server/chunks/_/gitApiHelpers.mjs.map +1 -0
  27. package/.output/server/chunks/_/jobPersister.mjs +326 -0
  28. package/.output/server/chunks/_/jobPersister.mjs.map +1 -0
  29. package/.output/server/chunks/_/jobQueue.mjs +714 -0
  30. package/.output/server/chunks/_/jobQueue.mjs.map +1 -0
  31. package/.output/server/chunks/build/client.precomputed.mjs +1 -1
  32. package/.output/server/chunks/build/client.precomputed.mjs.map +1 -1
  33. package/.output/server/chunks/nitro/nitro.mjs +693 -657
  34. package/.output/server/chunks/routes/_ws.mjs +133 -527
  35. package/.output/server/chunks/routes/_ws.mjs.map +1 -1
  36. package/.output/server/chunks/routes/api/git/branch-rename.post.mjs +5 -18
  37. package/.output/server/chunks/routes/api/git/branch-rename.post.mjs.map +1 -1
  38. package/.output/server/chunks/routes/api/git/branch.delete.mjs +5 -18
  39. package/.output/server/chunks/routes/api/git/branch.delete.mjs.map +1 -1
  40. package/.output/server/chunks/routes/api/git/branches.get.mjs +5 -18
  41. package/.output/server/chunks/routes/api/git/branches.get.mjs.map +1 -1
  42. package/.output/server/chunks/routes/api/git/checkout.post.mjs +5 -19
  43. package/.output/server/chunks/routes/api/git/checkout.post.mjs.map +1 -1
  44. package/.output/server/chunks/routes/api/git/cherry-pick.post.mjs +5 -18
  45. package/.output/server/chunks/routes/api/git/cherry-pick.post.mjs.map +1 -1
  46. package/.output/server/chunks/routes/api/git/clean.post.mjs +5 -19
  47. package/.output/server/chunks/routes/api/git/clean.post.mjs.map +1 -1
  48. package/.output/server/chunks/routes/api/git/commit/_id_.get.mjs +23 -26
  49. package/.output/server/chunks/routes/api/git/commit/_id_.get.mjs.map +1 -1
  50. package/.output/server/chunks/routes/api/git/commit.post.mjs +5 -19
  51. package/.output/server/chunks/routes/api/git/commit.post.mjs.map +1 -1
  52. package/.output/server/chunks/routes/api/git/diff.get.mjs +5 -24
  53. package/.output/server/chunks/routes/api/git/diff.get.mjs.map +1 -1
  54. package/.output/server/chunks/routes/api/git/fetch.post.mjs +5 -18
  55. package/.output/server/chunks/routes/api/git/fetch.post.mjs.map +1 -1
  56. package/.output/server/chunks/routes/api/git/file-diff.get.mjs +5 -24
  57. package/.output/server/chunks/routes/api/git/file-diff.get.mjs.map +1 -1
  58. package/.output/server/chunks/routes/api/git/graph.get.mjs +9 -20
  59. package/.output/server/chunks/routes/api/git/graph.get.mjs.map +1 -1
  60. package/.output/server/chunks/routes/api/git/log.get.mjs +5 -24
  61. package/.output/server/chunks/routes/api/git/log.get.mjs.map +1 -1
  62. package/.output/server/chunks/routes/api/git/merge-base.get.mjs +5 -24
  63. package/.output/server/chunks/routes/api/git/merge-base.get.mjs.map +1 -1
  64. package/.output/server/chunks/routes/api/git/merge.post.mjs +5 -18
  65. package/.output/server/chunks/routes/api/git/merge.post.mjs.map +1 -1
  66. package/.output/server/chunks/routes/api/git/pull.post.mjs +14 -26
  67. package/.output/server/chunks/routes/api/git/pull.post.mjs.map +1 -1
  68. package/.output/server/chunks/routes/api/git/push.post.mjs +15 -27
  69. package/.output/server/chunks/routes/api/git/push.post.mjs.map +1 -1
  70. package/.output/server/chunks/routes/api/git/rebase.post.mjs +10 -22
  71. package/.output/server/chunks/routes/api/git/rebase.post.mjs.map +1 -1
  72. package/.output/server/chunks/routes/api/git/remote.delete.mjs +5 -18
  73. package/.output/server/chunks/routes/api/git/remote.delete.mjs.map +1 -1
  74. package/.output/server/chunks/routes/api/git/remote.post.mjs +10 -22
  75. package/.output/server/chunks/routes/api/git/remote.post.mjs.map +1 -1
  76. package/.output/server/chunks/routes/api/git/remote.put.mjs +5 -18
  77. package/.output/server/chunks/routes/api/git/remote.put.mjs.map +1 -1
  78. package/.output/server/chunks/routes/api/git/remotes.get.mjs +5 -18
  79. package/.output/server/chunks/routes/api/git/remotes.get.mjs.map +1 -1
  80. package/.output/server/chunks/routes/api/git/reset.post.mjs +12 -24
  81. package/.output/server/chunks/routes/api/git/reset.post.mjs.map +1 -1
  82. package/.output/server/chunks/routes/api/git/revert.post.mjs +5 -18
  83. package/.output/server/chunks/routes/api/git/revert.post.mjs.map +1 -1
  84. package/.output/server/chunks/routes/api/git/show.get.mjs +5 -24
  85. package/.output/server/chunks/routes/api/git/show.get.mjs.map +1 -1
  86. package/.output/server/chunks/routes/api/git/stage.post.mjs +9 -22
  87. package/.output/server/chunks/routes/api/git/stage.post.mjs.map +1 -1
  88. package/.output/server/chunks/routes/api/git/stash-apply.post.mjs +5 -19
  89. package/.output/server/chunks/routes/api/git/stash-apply.post.mjs.map +1 -1
  90. package/.output/server/chunks/routes/api/git/stash-branch.post.mjs +5 -19
  91. package/.output/server/chunks/routes/api/git/stash-branch.post.mjs.map +1 -1
  92. package/.output/server/chunks/routes/api/git/stash-drop.post.mjs +5 -19
  93. package/.output/server/chunks/routes/api/git/stash-drop.post.mjs.map +1 -1
  94. package/.output/server/chunks/routes/api/git/stash-pop.post.mjs +5 -19
  95. package/.output/server/chunks/routes/api/git/stash-pop.post.mjs.map +1 -1
  96. package/.output/server/chunks/routes/api/git/stash.get.mjs +5 -25
  97. package/.output/server/chunks/routes/api/git/stash.get.mjs.map +1 -1
  98. package/.output/server/chunks/routes/api/git/stash.post.mjs +5 -19
  99. package/.output/server/chunks/routes/api/git/stash.post.mjs.map +1 -1
  100. package/.output/server/chunks/routes/api/git/state.get.mjs +5 -25
  101. package/.output/server/chunks/routes/api/git/state.get.mjs.map +1 -1
  102. package/.output/server/chunks/routes/api/git/status.get.mjs +5 -25
  103. package/.output/server/chunks/routes/api/git/status.get.mjs.map +1 -1
  104. package/.output/server/chunks/routes/api/git/tag/_name_.get.mjs +5 -18
  105. package/.output/server/chunks/routes/api/git/tag/_name_.get.mjs.map +1 -1
  106. package/.output/server/chunks/routes/api/git/tag-push.post.mjs +10 -22
  107. package/.output/server/chunks/routes/api/git/tag-push.post.mjs.map +1 -1
  108. package/.output/server/chunks/routes/api/git/tag.delete.mjs +5 -18
  109. package/.output/server/chunks/routes/api/git/tag.delete.mjs.map +1 -1
  110. package/.output/server/chunks/routes/api/git/tag.post.mjs +17 -29
  111. package/.output/server/chunks/routes/api/git/tag.post.mjs.map +1 -1
  112. package/.output/server/chunks/routes/api/git/unstage.post.mjs +5 -19
  113. package/.output/server/chunks/routes/api/git/unstage.post.mjs.map +1 -1
  114. package/.output/server/chunks/routes/api/index.get.mjs +25 -14
  115. package/.output/server/chunks/routes/api/index.get.mjs.map +1 -1
  116. package/.output/server/chunks/routes/api/index.get2.mjs +14 -141
  117. package/.output/server/chunks/routes/api/index.get2.mjs.map +1 -1
  118. package/.output/server/chunks/routes/api/index.get3.mjs +167 -0
  119. package/.output/server/chunks/routes/api/index.get3.mjs.map +1 -0
  120. package/.output/server/chunks/routes/api/index.post.mjs +77 -116
  121. package/.output/server/chunks/routes/api/index.post.mjs.map +1 -1
  122. package/.output/server/chunks/routes/api/index.post2.mjs +149 -0
  123. package/.output/server/chunks/routes/api/index.post2.mjs.map +1 -0
  124. package/.output/server/chunks/routes/api/jobs/_id/cancel.post.mjs +48 -0
  125. package/.output/server/chunks/routes/api/jobs/_id/cancel.post.mjs.map +1 -0
  126. package/.output/server/chunks/routes/api/jobs/_id_.get.mjs +55 -0
  127. package/.output/server/chunks/routes/api/jobs/_id_.get.mjs.map +1 -0
  128. package/.output/server/chunks/routes/api/repository/status.get.mjs +1 -1
  129. package/.output/server/package.json +1 -1
  130. package/package.json +1 -1
  131. package/.output/public/_nuxt/BIw1AQHU.js +0 -150
  132. package/.output/public/_nuxt/DBtLi_wJ.js +0 -1
  133. package/.output/public/_nuxt/DSDWvT5-.js +0 -1
  134. package/.output/public/_nuxt/K5rMM4le.js +0 -1
  135. package/.output/public/_nuxt/builds/meta/c0768eef-2dd5-410c-8648-edbd9e6c218c.json +0 -1
  136. package/.output/public/_nuxt/ddKcAgQK.js +0 -1
  137. package/.output/public/_nuxt/default.CZoNL3P_.css +0 -1
  138. package/.output/public/_nuxt/entry.DLBgeD7S.css +0 -1
  139. package/.output/public/_nuxt/sxXWehCn.js +0 -1
@@ -1,12 +1,6 @@
1
- import { n as defineWebSocketHandler, e as getProjectDir } from '../nitro/nitro.mjs';
2
- import { existsSync } from 'node:fs';
3
- import { exec } from 'node:child_process';
4
- import { promisify } from 'node:util';
5
- import { join } from 'node:path';
6
- import { r as resolveServerProviderSelection, a as guardProviderCapability } from '../_/aiProviderSelection.mjs';
7
- import { s as streamChatWithProvider, h as hasCodexPermissionError, a as hasCodexMissingRolloutPathError, b as summarizeProviderProcessError } from '../_/providerProcessError.mjs';
8
- import { g as getProvider } from '../_/aiProviderRegistry.mjs';
9
- import { n as normalizeToolName, e as extractPermissionRequestFromProcessOutput, a as normalizeTools, c as checkForPermissionRequest, i as isRenderableEvent } from '../_/uiAdapter.mjs';
1
+ import { n as defineWebSocketHandler } from '../nitro/nitro.mjs';
2
+ import { e as eventBus, G as GLOBAL_CHANNEL, n as normalizeImageAttachments, j as jobQueue } from '../_/jobQueue.mjs';
3
+ import { s as startPersisting } from '../_/jobPersister.mjs';
10
4
  import 'node:http';
11
5
  import 'node:https';
12
6
  import 'node:crypto';
@@ -22,212 +16,69 @@ import 'tls';
22
16
  import 'url';
23
17
  import 'node:events';
24
18
  import 'node:buffer';
19
+ import 'node:fs';
20
+ import 'node:path';
25
21
  import 'node:os';
26
22
  import 'node:module';
27
23
  import 'node:fs/promises';
28
24
  import 'node:url';
25
+ import 'node:child_process';
26
+ import 'node:util';
27
+ import '../_/aiProviderSelection.mjs';
28
+ import '../_/aiProviderRegistry.mjs';
29
+ import '../_/providerProcessError.mjs';
30
+ import '../_/uiAdapter.mjs';
31
+ import '../_/conversationStore.mjs';
29
32
 
30
- const execAsync = promisify(exec);
31
- const WORKTREE_PREFIX = "/tmp/sc-";
32
- async function ensureChatWorktree(projectDir, worktreePath, knownBranch) {
33
- if (!worktreePath.startsWith(WORKTREE_PREFIX)) {
34
- return { recovered: false };
35
- }
36
- if (existsSync(worktreePath)) {
37
- return { recovered: false };
33
+ const peerConnections = /* @__PURE__ */ new Map();
34
+ function getPeerConnection(peerId) {
35
+ let conn = peerConnections.get(peerId);
36
+ if (!conn) {
37
+ conn = { conversationId: null, unsubscribe: null, unsubscribeGlobal: null };
38
+ peerConnections.set(peerId, conn);
39
+ }
40
+ return conn;
41
+ }
42
+ function subscribePeerToConversation(peer, conversationId) {
43
+ const conn = getPeerConnection(peer.id);
44
+ if (conn.conversationId === conversationId && conn.unsubscribe) {
45
+ return;
38
46
  }
39
- let branchName;
40
- if (knownBranch) {
41
- branchName = knownBranch;
42
- } else {
43
- const convId = worktreePath.slice(WORKTREE_PREFIX.length);
44
- if (!convId) {
45
- return { recovered: false, error: `Cannot derive branch name from worktree path: ${worktreePath}` };
46
- }
47
- branchName = `sc/${convId}`;
47
+ if (conn.unsubscribe) {
48
+ conn.unsubscribe();
48
49
  }
49
- try {
50
- await execAsync("git worktree prune", { cwd: projectDir });
50
+ conn.conversationId = conversationId;
51
+ conn.unsubscribe = eventBus.subscribe(conversationId, (event) => {
51
52
  try {
52
- await execAsync(`git rev-parse --verify "${branchName}"`, { cwd: projectDir });
53
- } catch {
54
- return { recovered: false, error: `Branch "${branchName}" no longer exists` };
55
- }
56
- await execAsync(`git worktree add "${worktreePath}" "${branchName}"`, { cwd: projectDir });
57
- console.log(`[ensureChatWorktree] Recovered worktree: ${worktreePath} \u2192 ${branchName}`);
58
- return { recovered: true };
59
- } catch (err) {
60
- const message = err instanceof Error ? err.message : String(err);
61
- console.error(`[ensureChatWorktree] Recovery failed for ${worktreePath}:`, message);
62
- return { recovered: false, error: `Worktree recovery failed: ${message}` };
63
- }
64
- }
65
-
66
- async function loadSpecContext(projectDir, featureId) {
67
- const featurePath = join(projectDir, "specs", featureId);
68
- const specPath = join(featurePath, "spec.md");
69
- if (!existsSync(specPath)) {
70
- return null;
71
- }
72
- const sections = [];
73
- sections.push(`# Feature Context: ${featureId}`);
74
- sections.push("");
75
- sections.push("You are working on the feature described in the following specification files.");
76
- sections.push("You MUST read these files first to understand the feature requirements:");
77
- sections.push("");
78
- sections.push(`- specs/${featureId}/spec.md`);
79
- const planPath = join(featurePath, "plan.md");
80
- if (existsSync(planPath)) {
81
- sections.push(`- specs/${featureId}/plan.md`);
82
- }
83
- const tasksPath = join(featurePath, "tasks.md");
84
- if (existsSync(tasksPath)) {
85
- sections.push(`- specs/${featureId}/tasks.md`);
86
- }
87
- sections.push("");
88
- sections.push("## Spec-Driven Workflow (MANDATORY)");
89
- sections.push("");
90
- sections.push("This chat is linked to a feature spec. You MUST follow the spec-driven workflow:");
91
- sections.push("");
92
- sections.push("1. **Read the spec files above** before doing anything else.");
93
- sections.push("2. **When the user requests changes or new functionality:**");
94
- sections.push(" - First, update the relevant spec file(s) (spec.md, plan.md, tasks.md) to reflect the new requirements.");
95
- sections.push(" - Then, implement the code changes according to the updated spec.");
96
- sections.push(" - Never skip the spec update step. The spec is the source of truth.");
97
- sections.push("3. **When the user requests a bug fix:**");
98
- sections.push(" - Check if the bug contradicts the spec. If so, fix the code to match the spec.");
99
- sections.push(" - If the spec itself is wrong, update the spec first, then fix the code.");
100
- sections.push("4. **FR Traceability:** Every change must be traceable from spec \u2192 plan \u2192 task \u2192 implementation.");
101
- return sections.join("\n");
102
- }
103
-
104
- function isApprovalMode(mode) {
105
- return mode === "ask" || mode === "plan";
106
- }
107
- function deriveApprovalRequestFromEvent(event, approvedTools, providerId, mode) {
108
- if (!isApprovalMode(mode)) return null;
109
- return checkForPermissionRequest(event, approvedTools, providerId);
110
- }
111
- function deriveApprovalRequestFromProcessOutput(nonJsonOutput, mode) {
112
- if (!isApprovalMode(mode)) return null;
113
- const inferred = extractPermissionRequestFromProcessOutput(nonJsonOutput);
114
- if (!inferred) return null;
115
- const tools = normalizeTools(inferred.tools);
116
- return {
117
- type: "permission_request",
118
- tool: tools[0] || "Permission",
119
- tools,
120
- description: inferred.description
121
- };
122
- }
123
- function approveTools(approvedTools, tools) {
124
- for (const tool of tools) {
125
- const normalized = normalizeToolName(tool);
126
- if (normalized) {
127
- approvedTools.add(normalized);
128
- }
129
- }
130
- }
131
-
132
- const peerStates = /* @__PURE__ */ new Map();
133
- const MAX_ATTACHMENT_COUNT = 4;
134
- const MAX_ATTACHMENT_SIZE_BYTES = 5 * 1024 * 1024;
135
- const SPECKIT_AUTONOMY_DIRECTIVE = [
136
- "Speckit Execution Mode (MANDATORY):",
137
- "- Do not ask the user for confirmation, follow-up, or permission to proceed.",
138
- '- Do not end with questions like "Would you like me to..." or "Shall I...".',
139
- "- For remediation and traceability gaps, directly edit the relevant spec files now (spec.md, plan.md, tasks.md) when writable.",
140
- "- Prefer concrete file edits over recommendations; provide a brief change summary after edits are complete.",
141
- "- Only stop without edits if blocked by a hard constraint (missing files, permission failure), and report the blocker explicitly."
142
- ].join("\n");
143
- function getPeerState(peerId) {
144
- let state = peerStates.get(peerId);
145
- if (!state) {
146
- state = {
147
- proc: null,
148
- procGeneration: 0,
149
- pendingMessage: null,
150
- approvedTools: /* @__PURE__ */ new Set(),
151
- pendingTools: [],
152
- providerSessionId: null
153
- };
154
- peerStates.set(peerId, state);
155
- }
156
- return state;
157
- }
158
- function isSpeckitCommand(message) {
159
- return message.trim().startsWith("/speckit.");
160
- }
161
- function killProc(proc) {
162
- try {
163
- proc.kill();
164
- } catch {
165
- }
166
- }
167
- function normalizeImageAttachments(attachments) {
168
- if (!Array.isArray(attachments)) return [];
169
- return attachments.slice(0, MAX_ATTACHMENT_COUNT).map((item) => {
170
- if (!item || typeof item !== "object") return null;
171
- const record = item;
172
- const id = typeof record.id === "string" ? record.id : "";
173
- const name = typeof record.name === "string" ? record.name : "image";
174
- const mimeType = typeof record.mimeType === "string" ? record.mimeType : "";
175
- const size = typeof record.size === "number" ? record.size : 0;
176
- const dataUrl = typeof record.dataUrl === "string" ? record.dataUrl : "";
177
- if (!id || !mimeType.startsWith("image/") || size <= 0 || size > MAX_ATTACHMENT_SIZE_BYTES || !dataUrl.startsWith("data:image/")) {
178
- return null;
53
+ peer.send(JSON.stringify(event));
54
+ } catch (err) {
55
+ console.error("[WS] Failed to send event to peer:", err);
179
56
  }
180
- return { id, name, mimeType, size, dataUrl };
181
- }).filter((entry) => entry !== null);
182
- }
183
- function buildProviderMessage(baseMessage, attachments) {
184
- if (attachments.length === 0) return baseMessage;
185
- const lines = [];
186
- if (baseMessage.trim().length > 0) {
187
- lines.push(baseMessage);
188
- lines.push("");
189
- } else {
190
- lines.push("User sent image attachments without additional text.");
191
- lines.push("");
192
- }
193
- lines.push("Attached images (data URLs):");
194
- attachments.forEach((attachment, index) => {
195
- lines.push(`${index + 1}. ${attachment.name} (${attachment.mimeType}, ${attachment.size} bytes)`);
196
- lines.push(attachment.dataUrl);
197
- lines.push("");
198
57
  });
199
- lines.push("Use the attached images as part of your answer.");
200
- return lines.join("\n");
201
- }
202
- function sendAssistantText(peer, text, sessionId) {
203
- peer.send(JSON.stringify({
204
- type: "ui_event",
205
- event: {
206
- type: "block_start",
207
- sessionId: sessionId || void 0,
208
- blockId: `blk-${Date.now()}`,
209
- blockType: "text",
210
- text
211
- }
212
- }));
213
- peer.send(JSON.stringify({
214
- type: "ui_event",
215
- event: {
216
- type: "block_end",
217
- sessionId: sessionId || void 0,
218
- blockId: ""
219
- }
220
- }));
221
58
  }
222
59
  const _ws = defineWebSocketHandler({
223
- open(_peer) {
60
+ open(peer) {
61
+ console.log("[WS] Peer connected:", peer.id);
62
+ const conn = getPeerConnection(peer.id);
63
+ conn.unsubscribeGlobal = eventBus.subscribe(GLOBAL_CHANNEL, (event) => {
64
+ try {
65
+ const payload = JSON.stringify(event);
66
+ if (event.type === "notification") {
67
+ console.log("[WS] Forwarding global notification to peer", peer.id, ":", event.notificationEvent);
68
+ }
69
+ peer.send(payload);
70
+ } catch (err) {
71
+ console.error("[WS] Failed to send global event to peer:", err);
72
+ }
73
+ });
224
74
  },
225
75
  close(peer) {
226
- const state = peerStates.get(peer.id);
227
- if (state == null ? void 0 : state.proc) {
228
- killProc(state.proc);
76
+ const conn = peerConnections.get(peer.id);
77
+ if (conn) {
78
+ if (conn.unsubscribe) conn.unsubscribe();
79
+ if (conn.unsubscribeGlobal) conn.unsubscribeGlobal();
229
80
  }
230
- peerStates.delete(peer.id);
81
+ peerConnections.delete(peer.id);
231
82
  },
232
83
  error(peer, error) {
233
84
  console.error("[WS] Error for peer", peer.id, ":", error);
@@ -260,67 +111,13 @@ const _ws = defineWebSocketHandler({
260
111
  handleResetContext(peer);
261
112
  return;
262
113
  }
114
+ if (msg.type === "subscribe") {
115
+ handleSubscribe(peer, msg);
116
+ return;
117
+ }
263
118
  }
264
119
  });
265
- function handlePermissionResponse(peer, msg) {
266
- var _a, _b;
267
- const state = getPeerState(peer.id);
268
- console.log("[WS] Permission response:", {
269
- allow: msg.allow,
270
- pendingTools: state.pendingTools,
271
- approvedTools: Array.from(state.approvedTools),
272
- sessionId: state.providerSessionId,
273
- providerId: (_a = state.pendingMessage) == null ? void 0 : _a.providerId,
274
- providerModelKey: (_b = state.pendingMessage) == null ? void 0 : _b.providerModelKey
275
- });
276
- if (msg.allow && state.pendingMessage) {
277
- approveTools(state.approvedTools, state.pendingTools);
278
- console.log("[WS] Tools approved:", state.pendingTools, "- Total approved:", Array.from(state.approvedTools));
279
- state.pendingTools = [];
280
- const resumeMode = state.pendingMessage.permissionMode || "ask";
281
- runProvider(peer, state, {
282
- ...state.pendingMessage,
283
- permissionMode: resumeMode
284
- });
285
- } else {
286
- state.pendingTools = [];
287
- state.pendingMessage = null;
288
- peer.send(JSON.stringify({ type: "done", requestId: "denied", denied: true }));
289
- }
290
- }
291
- function handleAbort(peer) {
292
- const state = getPeerState(peer.id);
293
- console.log("[WS] Abort requested for peer:", peer.id);
294
- if (state.proc) {
295
- state.procGeneration++;
296
- killProc(state.proc);
297
- state.proc = null;
298
- }
299
- state.pendingMessage = null;
300
- state.pendingTools = [];
301
- peer.send(JSON.stringify({ type: "aborted" }));
302
- console.log("[WS] Abort completed for peer:", peer.id);
303
- }
304
- function clearProviderSession(state) {
305
- if (state.proc) {
306
- state.procGeneration++;
307
- killProc(state.proc);
308
- state.proc = null;
309
- }
310
- state.providerSessionId = null;
311
- state.approvedTools.clear();
312
- state.pendingMessage = null;
313
- state.pendingTools = [];
314
- }
315
- function handleResetContext(peer) {
316
- const state = getPeerState(peer.id);
317
- console.log("[WS] Reset context requested for peer:", peer.id);
318
- clearProviderSession(state);
319
- peer.send(JSON.stringify({ type: "context_reset" }));
320
- console.log("[WS] Context reset completed for peer:", peer.id);
321
- }
322
- async function handleChatMessage(peer, msg) {
323
- const state = getPeerState(peer.id);
120
+ function handleChatMessage(peer, msg) {
324
121
  const attachments = normalizeImageAttachments(msg.attachments);
325
122
  if (typeof msg.message !== "string") {
326
123
  console.error("[WS] Invalid chat message - missing or invalid message property:", msg);
@@ -339,291 +136,100 @@ async function handleChatMessage(peer, msg) {
339
136
  }));
340
137
  return;
341
138
  }
342
- const speckitCommand = isSpeckitCommand(msg.message);
343
- if (speckitCommand) {
344
- console.log("[WS] Speckit command detected - auto-resetting context for peer:", peer.id);
345
- clearProviderSession(state);
346
- } else {
347
- if (state.proc) {
348
- state.procGeneration++;
349
- killProc(state.proc);
350
- state.proc = null;
351
- }
352
- }
353
- state.pendingMessage = msg;
354
- state.pendingTools = [];
355
- if (!speckitCommand && msg.sessionId) {
356
- state.providerSessionId = msg.sessionId;
357
- } else if (!speckitCommand) {
358
- state.approvedTools.clear();
359
- state.providerSessionId = null;
360
- }
361
- console.log("[WS] Chat message received:", {
362
- hasSessionId: !!msg.sessionId,
363
- sessionId: state.providerSessionId,
364
- approvedTools: Array.from(state.approvedTools),
365
- attachmentCount: attachments.length,
366
- providerId: msg.providerId,
367
- providerModelKey: msg.providerModelKey,
368
- isSpeckitCommand: speckitCommand
369
- });
370
- runProvider(peer, state, msg);
371
- }
372
- async function runProvider(peer, state, msg, isRetry = false, forceEphemeral = false) {
373
- const requestedSelection = msg.providerId ? { providerId: msg.providerId, modelKey: msg.providerModelKey || "" } : { providerId: "claude", modelKey: msg.providerModelKey || "" };
374
- const selection = await resolveServerProviderSelection(requestedSelection);
375
- const provider = getProvider(selection.providerId);
376
- if (!provider) {
139
+ if (!msg.conversationId) {
377
140
  peer.send(JSON.stringify({
378
141
  type: "error",
379
- error: `Provider "${selection.providerId}" is not registered`,
142
+ error: "Invalid message: conversationId is required",
380
143
  requestId: msg.requestId
381
144
  }));
382
145
  return;
383
146
  }
384
- const providerGuard = await guardProviderCapability(
385
- selection,
386
- "streaming",
387
- "Choose a provider with streaming capability in Settings."
388
- );
389
- if ("failure" in providerGuard) {
390
- peer.send(JSON.stringify({
391
- type: "error",
392
- error: providerGuard.failure.error,
393
- requestId: msg.requestId
394
- }));
147
+ subscribePeerToConversation(peer, msg.conversationId);
148
+ const jobMessage = {
149
+ message: msg.message,
150
+ conversationId: msg.conversationId,
151
+ attachments: attachments.length > 0 ? attachments : void 0,
152
+ requestId: msg.requestId,
153
+ sessionId: msg.sessionId,
154
+ permissionMode: msg.permissionMode,
155
+ cwd: msg.cwd,
156
+ worktreeBranch: msg.worktreeBranch,
157
+ featureId: msg.featureId,
158
+ providerId: msg.providerId,
159
+ providerModelKey: msg.providerModelKey
160
+ };
161
+ startPersisting(msg.conversationId, msg.message);
162
+ jobQueue.submit(jobMessage);
163
+ }
164
+ function handlePermissionResponse(peer, msg) {
165
+ const conn = getPeerConnection(peer.id);
166
+ if (!conn.conversationId) return;
167
+ console.log("[WS] Permission response:", { allow: msg.allow, conversationId: conn.conversationId });
168
+ jobQueue.respondToPermission(conn.conversationId, msg.allow);
169
+ }
170
+ function handleAbort(peer) {
171
+ const conn = getPeerConnection(peer.id);
172
+ console.log("[WS] Abort requested for peer:", peer.id);
173
+ if (conn.conversationId) {
174
+ jobQueue.abort(conn.conversationId);
175
+ }
176
+ peer.send(JSON.stringify({ type: "aborted" }));
177
+ console.log("[WS] Abort completed for peer:", peer.id);
178
+ }
179
+ function handleSubscribe(peer, msg) {
180
+ if (!msg.conversationId) {
181
+ peer.send(JSON.stringify({ type: "error", error: "conversationId is required for subscribe" }));
395
182
  return;
396
183
  }
397
- const projectDir = getProjectDir();
398
- const workingDirectory = msg.cwd || projectDir;
399
- const mode = msg.permissionMode || "ask";
400
- if (mode === "ask" || mode === "plan") {
401
- const permissionGuard = await guardProviderCapability(
402
- selection,
403
- "permissions",
404
- "Use auto/bypass permission mode or choose a provider that supports permission prompts."
405
- );
406
- if ("failure" in permissionGuard) {
184
+ subscribePeerToConversation(peer, msg.conversationId);
185
+ const activeJob = jobQueue.getActiveJob(msg.conversationId);
186
+ if (activeJob) {
187
+ const cursor = typeof msg.cursor === "number" && msg.cursor >= 0 ? msg.cursor : 0;
188
+ const bufferedEvents = activeJob.events.slice(cursor);
189
+ if (bufferedEvents.length > 0) {
407
190
  peer.send(JSON.stringify({
408
- type: "error",
409
- error: permissionGuard.failure.error,
410
- requestId: msg.requestId
191
+ type: "replay_start",
192
+ jobId: activeJob.id,
193
+ jobStatus: activeJob.status,
194
+ eventCount: bufferedEvents.length,
195
+ cursor
411
196
  }));
412
- return;
413
- }
414
- }
415
- if (workingDirectory.startsWith("/tmp/sc-") && !existsSync(workingDirectory)) {
416
- const result = await ensureChatWorktree(projectDir, workingDirectory, msg.worktreeBranch);
417
- if (result.recovered) {
418
- peer.send(JSON.stringify({ type: "worktree_recovered" }));
419
- } else if (result.error) {
197
+ for (const event of bufferedEvents) {
198
+ try {
199
+ peer.send(JSON.stringify(event));
200
+ } catch {
201
+ break;
202
+ }
203
+ }
420
204
  peer.send(JSON.stringify({
421
- type: "error",
422
- error: `Worktree recovery failed: ${result.error}`,
423
- requestId: msg.requestId
205
+ type: "replay_end",
206
+ jobId: activeJob.id,
207
+ nextCursor: activeJob.events.length
208
+ }));
209
+ } else {
210
+ peer.send(JSON.stringify({
211
+ type: "subscribed",
212
+ conversationId: msg.conversationId,
213
+ jobId: activeJob.id,
214
+ jobStatus: activeJob.status
424
215
  }));
425
- return;
426
- }
427
- }
428
- const usedResumeFlag = !isRetry && !!state.providerSessionId;
429
- const resumeSessionId = usedResumeFlag ? state.providerSessionId : void 0;
430
- let systemPrompt;
431
- const speckitCommand = isSpeckitCommand(msg.message);
432
- if (msg.featureId && !usedResumeFlag) {
433
- try {
434
- const specContext = await loadSpecContext(projectDir, msg.featureId);
435
- if (specContext) {
436
- systemPrompt = specContext;
437
- }
438
- } catch (error) {
439
- console.error("[WS] Failed to load spec context:", error);
440
216
  }
441
- }
442
- if (speckitCommand && !usedResumeFlag) {
443
- systemPrompt = systemPrompt ? `${systemPrompt}
444
-
445
- ${SPECKIT_AUTONOMY_DIRECTIVE}` : SPECKIT_AUTONOMY_DIRECTIVE;
446
- }
447
- console.log("[WS] Running provider stream:", selection.providerId, selection.modelKey, isRetry ? "(retry)" : "");
448
- const generation = state.procGeneration;
449
- let permissionRequested = false;
450
- let emittedRenderableContent = false;
451
- let emittedTerminalErrorEvent = false;
452
- const attachments = normalizeImageAttachments(msg.attachments);
453
- const providerMessage = buildProviderMessage(msg.message, attachments);
454
- try {
455
- state.proc = await streamChatWithProvider(
456
- {
457
- message: providerMessage,
458
- selection,
459
- cwd: workingDirectory,
460
- permissionMode: mode,
461
- approvedTools: Array.from(state.approvedTools),
462
- resumeSessionId,
463
- systemPrompt,
464
- ephemeral: forceEphemeral && selection.providerId === "codex"
465
- },
466
- {
467
- onProviderJson(parsed) {
468
- var _a;
469
- const events = provider.toCanonicalEvents(parsed);
470
- for (const event of events) {
471
- if (event.sessionId) {
472
- state.providerSessionId = event.sessionId;
473
- }
474
- if (isRenderableEvent(event)) {
475
- emittedRenderableContent = true;
476
- }
477
- if (event.type === "error" || event.type === "turn_result" && event.subtype !== "success") {
478
- emittedTerminalErrorEvent = true;
479
- }
480
- if ((mode === "ask" || mode === "plan") && !permissionRequested) {
481
- const permRequest = deriveApprovalRequestFromEvent(
482
- event,
483
- state.approvedTools,
484
- selection.providerId,
485
- mode
486
- );
487
- if (permRequest) {
488
- permissionRequested = true;
489
- state.pendingTools = permRequest.tools || [permRequest.tool];
490
- peer.send(JSON.stringify({
491
- type: "permission_request",
492
- tool: permRequest.tool,
493
- tools: state.pendingTools,
494
- description: permRequest.description || `Permission required: ${permRequest.tool}`
495
- }));
496
- (_a = state.proc) == null ? void 0 : _a.kill();
497
- return;
498
- }
499
- }
500
- peer.send(JSON.stringify({ type: "ui_event", event }));
501
- }
502
- },
503
- onClose({ exitCode, signal, nonJsonOutput }) {
504
- if (state.procGeneration !== generation) {
505
- return;
506
- }
507
- try {
508
- if (!permissionRequested) {
509
- if (exitCode !== 0 && exitCode !== null) {
510
- console.error("[WS] Provider process exited unexpectedly", {
511
- providerId: selection.providerId,
512
- modelKey: selection.modelKey,
513
- permissionMode: mode,
514
- requestId: msg.requestId,
515
- exitCode,
516
- signal,
517
- nonJsonOutput: nonJsonOutput.slice(-25)
518
- });
519
- const inferred = deriveApprovalRequestFromProcessOutput(nonJsonOutput, mode);
520
- if (inferred) {
521
- permissionRequested = true;
522
- state.pendingTools = inferred.tools || [inferred.tool];
523
- peer.send(JSON.stringify({
524
- type: "permission_request",
525
- tool: inferred.tool,
526
- tools: state.pendingTools,
527
- description: inferred.description
528
- }));
529
- state.proc = null;
530
- return;
531
- }
532
- const hasPermissionError = hasCodexPermissionError(nonJsonOutput);
533
- const missingRolloutPath = hasCodexMissingRolloutPathError(nonJsonOutput);
534
- if (missingRolloutPath && !hasPermissionError && !isRetry) {
535
- peer.send(JSON.stringify({
536
- type: "session_reset",
537
- reason: "Codex session state was missing rollout data. Retrying with a fresh ephemeral session."
538
- }));
539
- state.providerSessionId = null;
540
- state.proc = null;
541
- runProvider(peer, state, msg, true, true);
542
- return;
543
- }
544
- if (usedResumeFlag && !isRetry) {
545
- const retryWithEphemeral = selection.providerId === "codex";
546
- peer.send(JSON.stringify({
547
- type: "session_reset",
548
- reason: retryWithEphemeral ? `Session resume failed (exit code ${exitCode}). Retrying with a fresh ephemeral session.` : `Session resume failed (exit code ${exitCode}). Retrying with a fresh session.`
549
- }));
550
- state.providerSessionId = null;
551
- state.proc = null;
552
- runProvider(peer, state, msg, true, retryWithEphemeral);
553
- return;
554
- }
555
- const summary = summarizeProviderProcessError(nonJsonOutput, 700);
556
- if (!summary && emittedTerminalErrorEvent && emittedRenderableContent) {
557
- peer.send(JSON.stringify({ type: "done", requestId: msg.requestId }));
558
- state.pendingMessage = null;
559
- return;
560
- }
561
- const details = summary ? ` \u2014 ${summary}` : "";
562
- peer.send(JSON.stringify({
563
- type: "error",
564
- error: `Provider process exited unexpectedly (code: ${exitCode}${signal ? ", signal: " + signal : ""})${details}`,
565
- requestId: msg.requestId
566
- }));
567
- } else if (exitCode === null && signal) {
568
- const summary = summarizeProviderProcessError(nonJsonOutput, 700);
569
- const details = summary ? ` \u2014 ${summary}` : "";
570
- peer.send(JSON.stringify({
571
- type: "error",
572
- error: `Provider process was killed by signal ${signal}${details}`,
573
- requestId: msg.requestId
574
- }));
575
- console.error("[WS] Provider process killed by signal", {
576
- providerId: selection.providerId,
577
- modelKey: selection.modelKey,
578
- permissionMode: mode,
579
- requestId: msg.requestId,
580
- signal,
581
- nonJsonOutput: nonJsonOutput.slice(-25)
582
- });
583
- } else {
584
- if (!emittedRenderableContent) {
585
- const summary = summarizeProviderProcessError(nonJsonOutput, 700);
586
- const fallbackText = summary ? `Provider returned no structured response.
587
-
588
- Raw output:
589
- ${summary}` : "Provider completed without returning visible response content.";
590
- sendAssistantText(peer, fallbackText, state.providerSessionId);
591
- }
592
- peer.send(JSON.stringify({ type: "done", requestId: msg.requestId }));
593
- }
594
- state.pendingMessage = null;
595
- }
596
- } finally {
597
- state.proc = null;
598
- }
599
- },
600
- onError(error) {
601
- try {
602
- peer.send(JSON.stringify({
603
- type: "error",
604
- error: `Provider process error: ${error.message}`,
605
- requestId: msg.requestId
606
- }));
607
- } finally {
608
- state.pendingMessage = null;
609
- state.pendingTools = [];
610
- state.proc = null;
611
- }
612
- }
613
- }
614
- );
615
- } catch (error) {
616
- const errorMsg = error instanceof Error ? error.message : "Failed to start provider process";
217
+ } else {
617
218
  peer.send(JSON.stringify({
618
- type: "error",
619
- error: errorMsg,
620
- requestId: msg.requestId
219
+ type: "subscribed",
220
+ conversationId: msg.conversationId
621
221
  }));
622
- state.pendingMessage = null;
623
- state.pendingTools = [];
624
- state.proc = null;
625
222
  }
626
223
  }
224
+ function handleResetContext(peer) {
225
+ const conn = getPeerConnection(peer.id);
226
+ console.log("[WS] Reset context requested for peer:", peer.id);
227
+ if (conn.conversationId) {
228
+ jobQueue.resetContext(conn.conversationId);
229
+ }
230
+ peer.send(JSON.stringify({ type: "context_reset" }));
231
+ console.log("[WS] Context reset completed for peer:", peer.id);
232
+ }
627
233
 
628
234
  export { _ws as default };
629
235
  //# sourceMappingURL=_ws.mjs.map