@vibe80/vibe80 0.1.1

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 (123) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +52 -0
  3. package/bin/vibe80.js +176 -0
  4. package/client/dist/assets/DiffPanel-C_IGzKI5.js +1 -0
  5. package/client/dist/assets/ExplorerPanel-BtlyAT00.js +11 -0
  6. package/client/dist/assets/LogsPanel-BW79JWzR.js +1 -0
  7. package/client/dist/assets/SettingsPanel-b9B7ygP_.js +1 -0
  8. package/client/dist/assets/TerminalPanel-C3fc1HbK.js +1 -0
  9. package/client/dist/assets/browser-e3WgtMs-.js +8 -0
  10. package/client/dist/assets/index-CgqGyssr.css +32 -0
  11. package/client/dist/assets/index-DnwKjoj7.js +706 -0
  12. package/client/dist/assets/vibe80_dark-D7OVPKcU.svg +51 -0
  13. package/client/dist/assets/vibe80_light-BJK37ybI.svg +50 -0
  14. package/client/dist/favicon.ico +0 -0
  15. package/client/dist/favicon.png +0 -0
  16. package/client/dist/favicon.svg +35 -0
  17. package/client/dist/index.html +14 -0
  18. package/client/index.html +16 -0
  19. package/client/package.json +34 -0
  20. package/client/public/favicon.ico +0 -0
  21. package/client/public/favicon.png +0 -0
  22. package/client/public/favicon.svg +35 -0
  23. package/client/public/pwa-192x192.png +0 -0
  24. package/client/public/pwa-512x512.png +0 -0
  25. package/client/src/App.jsx +3131 -0
  26. package/client/src/assets/logo_small.png +0 -0
  27. package/client/src/assets/vibe80_dark.svg +51 -0
  28. package/client/src/assets/vibe80_light.svg +50 -0
  29. package/client/src/components/Chat/ChatComposer.jsx +228 -0
  30. package/client/src/components/Chat/ChatMessages.jsx +811 -0
  31. package/client/src/components/Chat/ChatToolbar.jsx +109 -0
  32. package/client/src/components/Chat/useChatComposer.js +462 -0
  33. package/client/src/components/Diff/DiffPanel.jsx +129 -0
  34. package/client/src/components/Explorer/ExplorerPanel.jsx +449 -0
  35. package/client/src/components/Logs/LogsPanel.jsx +80 -0
  36. package/client/src/components/SessionGate/SessionGate.jsx +874 -0
  37. package/client/src/components/Settings/SettingsPanel.jsx +212 -0
  38. package/client/src/components/Terminal/TerminalPanel.jsx +39 -0
  39. package/client/src/components/Topbar/Topbar.jsx +101 -0
  40. package/client/src/components/WorktreeTabs.css +419 -0
  41. package/client/src/components/WorktreeTabs.jsx +604 -0
  42. package/client/src/hooks/useAttachments.jsx +125 -0
  43. package/client/src/hooks/useBacklog.js +254 -0
  44. package/client/src/hooks/useChatClear.js +90 -0
  45. package/client/src/hooks/useChatCollapse.js +42 -0
  46. package/client/src/hooks/useChatCommands.js +294 -0
  47. package/client/src/hooks/useChatExport.js +144 -0
  48. package/client/src/hooks/useChatMessagesState.js +69 -0
  49. package/client/src/hooks/useChatSend.js +158 -0
  50. package/client/src/hooks/useChatSocket.js +1239 -0
  51. package/client/src/hooks/useDiffNavigation.js +19 -0
  52. package/client/src/hooks/useExplorerActions.js +1184 -0
  53. package/client/src/hooks/useGitIdentity.js +114 -0
  54. package/client/src/hooks/useLayoutMode.js +31 -0
  55. package/client/src/hooks/useLocalPreferences.js +131 -0
  56. package/client/src/hooks/useMessageSync.js +30 -0
  57. package/client/src/hooks/useNotifications.js +132 -0
  58. package/client/src/hooks/usePaneNavigation.js +67 -0
  59. package/client/src/hooks/usePanelState.js +13 -0
  60. package/client/src/hooks/useProviderSelection.js +70 -0
  61. package/client/src/hooks/useRepoBranchesModels.js +218 -0
  62. package/client/src/hooks/useRepoStatus.js +350 -0
  63. package/client/src/hooks/useRpcLogActions.js +19 -0
  64. package/client/src/hooks/useRpcLogView.js +58 -0
  65. package/client/src/hooks/useSessionHandoff.js +97 -0
  66. package/client/src/hooks/useSessionLifecycle.js +287 -0
  67. package/client/src/hooks/useSessionReset.js +63 -0
  68. package/client/src/hooks/useSessionResync.js +77 -0
  69. package/client/src/hooks/useTerminalSession.js +328 -0
  70. package/client/src/hooks/useToolbarExport.js +27 -0
  71. package/client/src/hooks/useTurnInterrupt.js +43 -0
  72. package/client/src/hooks/useVibe80Forms.js +128 -0
  73. package/client/src/hooks/useWorkspaceAuth.js +932 -0
  74. package/client/src/hooks/useWorktreeCloseConfirm.js +46 -0
  75. package/client/src/hooks/useWorktrees.js +396 -0
  76. package/client/src/i18n.jsx +87 -0
  77. package/client/src/index.css +5147 -0
  78. package/client/src/locales/en.json +37 -0
  79. package/client/src/locales/fr.json +321 -0
  80. package/client/src/main.jsx +16 -0
  81. package/client/vite.config.js +62 -0
  82. package/docs/api/asyncapi.json +1511 -0
  83. package/docs/api/openapi.json +3242 -0
  84. package/git_hooks/prepare-commit-msg +35 -0
  85. package/package.json +36 -0
  86. package/server/package.json +29 -0
  87. package/server/scripts/rotate-workspace-secret.js +101 -0
  88. package/server/src/claudeClient.js +454 -0
  89. package/server/src/clientEvents.js +594 -0
  90. package/server/src/clientFactory.js +164 -0
  91. package/server/src/codexClient.js +468 -0
  92. package/server/src/config.js +27 -0
  93. package/server/src/helpers.js +138 -0
  94. package/server/src/index.js +1641 -0
  95. package/server/src/middleware/auth.js +93 -0
  96. package/server/src/middleware/debug.js +89 -0
  97. package/server/src/middleware/errorTypes.js +60 -0
  98. package/server/src/providerLogger.js +60 -0
  99. package/server/src/routes/files.js +114 -0
  100. package/server/src/routes/git.js +183 -0
  101. package/server/src/routes/health.js +13 -0
  102. package/server/src/routes/sessions.js +407 -0
  103. package/server/src/routes/workspaces.js +296 -0
  104. package/server/src/routes/worktrees.js +993 -0
  105. package/server/src/runAs.js +458 -0
  106. package/server/src/runtimeStore.js +32 -0
  107. package/server/src/services/auth.js +157 -0
  108. package/server/src/services/claudeThreadDirectory.js +33 -0
  109. package/server/src/services/session.js +918 -0
  110. package/server/src/services/workspace.js +858 -0
  111. package/server/src/storage/index.js +17 -0
  112. package/server/src/storage/redis.js +412 -0
  113. package/server/src/storage/sqlite.js +649 -0
  114. package/server/src/worktreeManager.js +717 -0
  115. package/server/tests/README.md +13 -0
  116. package/server/tests/factories/workspaceFactory.js +13 -0
  117. package/server/tests/fixtures/workspaceCredentials.json +4 -0
  118. package/server/tests/integration/routes/workspaces-routes.test.js +626 -0
  119. package/server/tests/setup/env.js +9 -0
  120. package/server/tests/unit/helpers.test.js +95 -0
  121. package/server/tests/unit/services/auth.test.js +181 -0
  122. package/server/tests/unit/services/workspace.test.js +115 -0
  123. package/server/vitest.config.js +23 -0
@@ -0,0 +1,1239 @@
1
+ import { useEffect } from "react";
2
+
3
+ export default function useChatSocket({
4
+ attachmentSessionId,
5
+ workspaceToken,
6
+ socketRef,
7
+ reconnectTimerRef,
8
+ reconnectAttemptRef,
9
+ closingRef,
10
+ pingIntervalRef,
11
+ lastPongRef,
12
+ messageIndex,
13
+ commandIndex,
14
+ rpcLogsEnabledRef,
15
+ mergeAndApplyMessages,
16
+ requestMessageSync,
17
+ requestWorktreesList,
18
+ requestWorktreeMessages,
19
+ applyWorktreesList,
20
+ resyncSession,
21
+ t,
22
+ setStatus,
23
+ setConnected,
24
+ setAppServerReady,
25
+ setHasMainWorktreeStatus,
26
+ setMessages,
27
+ setProcessing,
28
+ setActivity,
29
+ setCurrentTurnId,
30
+ setMainTaskLabel,
31
+ setModelLoading,
32
+ setModelError,
33
+ setModels,
34
+ setProviderModelState,
35
+ setSelectedModel,
36
+ setSelectedReasoningEffort,
37
+ setRepoDiff,
38
+ setRpcLogs,
39
+ setWorktrees,
40
+ setPaneByTab,
41
+ setLogFilterByTab,
42
+ setActiveWorktreeId,
43
+ activeWorktreeIdRef,
44
+ extractVibe80Task,
45
+ extractFirstLine,
46
+ getItemActivityLabel,
47
+ maybeNotify,
48
+ normalizeAttachments,
49
+ loadRepoLastCommit,
50
+ loadBranches,
51
+ loadWorktreeLastCommit,
52
+ openAiLoginRequest,
53
+ setOpenAiLoginRequest,
54
+ connected,
55
+ }) {
56
+ useEffect(() => {
57
+ if (!attachmentSessionId || !workspaceToken) {
58
+ return;
59
+ }
60
+ let isMounted = true;
61
+ let wakeUpInterval = null;
62
+
63
+ const clearReconnectTimer = () => {
64
+ if (reconnectTimerRef.current) {
65
+ clearTimeout(reconnectTimerRef.current);
66
+ reconnectTimerRef.current = null;
67
+ }
68
+ };
69
+
70
+ const clearPingInterval = () => {
71
+ if (pingIntervalRef.current) {
72
+ clearInterval(pingIntervalRef.current);
73
+ pingIntervalRef.current = null;
74
+ }
75
+ };
76
+ const clearWakeUpInterval = () => {
77
+ if (wakeUpInterval) {
78
+ clearInterval(wakeUpInterval);
79
+ wakeUpInterval = null;
80
+ }
81
+ };
82
+
83
+ const startPingInterval = () => {
84
+ clearPingInterval();
85
+ lastPongRef.current = Date.now();
86
+ pingIntervalRef.current = setInterval(() => {
87
+ const socket = socketRef.current;
88
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
89
+ return;
90
+ }
91
+ if (document.hidden) {
92
+ lastPongRef.current = Date.now();
93
+ return;
94
+ }
95
+ const elapsed = Date.now() - lastPongRef.current;
96
+ if (elapsed > 10000 + 5000) {
97
+ socket.close();
98
+ return;
99
+ }
100
+ socket.send(JSON.stringify({ type: "ping" }));
101
+ }, 10000);
102
+ };
103
+
104
+ const startWakeUpInterval = () => {
105
+ clearWakeUpInterval();
106
+ const sendWakeUp = () => {
107
+ const socket = socketRef.current;
108
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
109
+ return;
110
+ }
111
+ const worktreeId = activeWorktreeIdRef?.current || "main";
112
+ socket.send(JSON.stringify({ type: "wake_up", worktreeId }));
113
+ };
114
+ sendWakeUp();
115
+ wakeUpInterval = setInterval(sendWakeUp, 60 * 1000);
116
+ };
117
+
118
+ const scheduleReconnect = () => {
119
+ if (!isMounted) {
120
+ return;
121
+ }
122
+ const attempt = Math.min(reconnectAttemptRef.current + 1, 6);
123
+ reconnectAttemptRef.current = attempt;
124
+ const baseDelay = 500;
125
+ const maxDelay = 10000;
126
+ const delay = Math.min(baseDelay * 2 ** (attempt - 1), maxDelay);
127
+ const jitter = Math.floor(Math.random() * 250);
128
+ clearReconnectTimer();
129
+ reconnectTimerRef.current = setTimeout(() => {
130
+ connect();
131
+ }, delay + jitter);
132
+ };
133
+
134
+ const connect = () => {
135
+ if (!isMounted) {
136
+ return;
137
+ }
138
+ setStatus(t("Connecting..."));
139
+ const socket = new WebSocket(
140
+ `${window.location.protocol === "https:" ? "wss" : "ws"}://${
141
+ window.location.host
142
+ }/ws?session=${encodeURIComponent(attachmentSessionId)}`
143
+ );
144
+ socketRef.current = socket;
145
+ let authenticated = false;
146
+
147
+ const isCurrent = () => socketRef.current === socket;
148
+
149
+ socket.addEventListener("open", () => {
150
+ if (!isCurrent()) {
151
+ return;
152
+ }
153
+ socket.send(JSON.stringify({ type: "auth", token: workspaceToken }));
154
+ });
155
+
156
+ socket.addEventListener("close", () => {
157
+ if (!isCurrent()) {
158
+ return;
159
+ }
160
+ setConnected(false);
161
+ setStatus(t("Disconnected"));
162
+ setAppServerReady(false);
163
+ clearPingInterval();
164
+ clearWakeUpInterval();
165
+ if (!closingRef.current) {
166
+ scheduleReconnect();
167
+ }
168
+ });
169
+
170
+ socket.addEventListener("error", () => {
171
+ if (!isCurrent()) {
172
+ return;
173
+ }
174
+ socket.close();
175
+ });
176
+
177
+ socket.addEventListener("message", (event) => {
178
+ if (!isCurrent()) {
179
+ return;
180
+ }
181
+ let payload;
182
+ try {
183
+ payload = JSON.parse(event.data);
184
+ } catch (error) {
185
+ return;
186
+ }
187
+
188
+ const scopedWorktreeId =
189
+ typeof payload.worktreeId === "string" ? payload.worktreeId : "";
190
+ const hasWorktreeScope = scopedWorktreeId.length > 0;
191
+ const isMainScope = scopedWorktreeId === "main";
192
+ const isWorktreeScoped = hasWorktreeScope && !isMainScope;
193
+ const isMainScopedOrLegacy = isMainScope || !hasWorktreeScope;
194
+
195
+ if (
196
+ isMainScopedOrLegacy &&
197
+ (payload.type === "assistant_delta" ||
198
+ payload.type === "command_execution_delta" ||
199
+ payload.type === "command_execution_completed" ||
200
+ payload.type === "item_started")
201
+ ) {
202
+ setProcessing((current) => (current ? current : true));
203
+ setActivity(t("Processing..."));
204
+ }
205
+
206
+ if (payload.type === "auth_ok") {
207
+ if (!authenticated) {
208
+ authenticated = true;
209
+ reconnectAttemptRef.current = 0;
210
+ clearReconnectTimer();
211
+ setConnected(true);
212
+ setStatus(t("Connected"));
213
+ startPingInterval();
214
+ startWakeUpInterval();
215
+ void resyncSession();
216
+ requestMessageSync();
217
+ requestWorktreesList();
218
+ const socket = socketRef.current;
219
+ if (socket && socket.readyState === WebSocket.OPEN) {
220
+ const worktreeId = activeWorktreeIdRef?.current || "main";
221
+ socket.send(JSON.stringify({ type: "wake_up", worktreeId }));
222
+ }
223
+ }
224
+ return;
225
+ }
226
+
227
+ if (!authenticated) {
228
+ return;
229
+ }
230
+
231
+ if (payload.type === "pong") {
232
+ lastPongRef.current = Date.now();
233
+ }
234
+
235
+ if (payload.type === "status") {
236
+ setStatus(payload.message);
237
+ if (payload.provider === "codex") {
238
+ setAppServerReady(false);
239
+ }
240
+ }
241
+
242
+ if (payload.type === "ready") {
243
+ setStatus(t("Ready"));
244
+ setAppServerReady(true);
245
+ }
246
+
247
+ if (payload.type === "provider_status") {
248
+ if (payload.provider === "codex") {
249
+ setAppServerReady(payload.status === "ready");
250
+ }
251
+ }
252
+
253
+ if (isMainScopedOrLegacy && payload.type === "assistant_delta") {
254
+ if (typeof payload.delta !== "string") {
255
+ return;
256
+ }
257
+ setMessages((current) => {
258
+ const next = [...current];
259
+ const existingIndex = next.findIndex(
260
+ (item) => item?.id === payload.itemId
261
+ );
262
+ if (existingIndex === -1) {
263
+ next.push({
264
+ id: payload.itemId,
265
+ role: "assistant",
266
+ text: payload.delta,
267
+ });
268
+ return next;
269
+ }
270
+
271
+ const updated = { ...next[existingIndex] };
272
+ updated.text += payload.delta;
273
+ next[existingIndex] = updated;
274
+ return next;
275
+ });
276
+ }
277
+
278
+ if (isMainScopedOrLegacy && payload.type === "assistant_message") {
279
+ if (typeof payload.text !== "string") {
280
+ return;
281
+ }
282
+ const taskLabel = extractVibe80Task(payload.text);
283
+ if (taskLabel) {
284
+ setMainTaskLabel(taskLabel);
285
+ }
286
+ maybeNotify({ id: payload.itemId, text: payload.text });
287
+ setMessages((current) => {
288
+ const next = [...current];
289
+ const existingIndex = next.findIndex(
290
+ (item) => item?.id === payload.itemId
291
+ );
292
+ if (existingIndex === -1) {
293
+ next.push({
294
+ id: payload.itemId,
295
+ role: "assistant",
296
+ text: payload.text,
297
+ });
298
+ return next;
299
+ }
300
+
301
+ next[existingIndex] = {
302
+ ...next[existingIndex],
303
+ text: payload.text,
304
+ };
305
+ return next;
306
+ });
307
+ }
308
+
309
+ if (isMainScopedOrLegacy && payload.type === "action_request") {
310
+ if (!payload.id) {
311
+ return;
312
+ }
313
+ setMessages((current) => {
314
+ const next = [...current];
315
+ const existingIndex = next.findIndex(
316
+ (item) => item?.id === payload.id
317
+ );
318
+ if (existingIndex === -1) {
319
+ next.push({
320
+ id: payload.id,
321
+ role: "user",
322
+ type: "action_request",
323
+ text:
324
+ payload.text ||
325
+ `/${payload.request || "run"} ${payload.arg || ""}`.trim(),
326
+ action: {
327
+ request: payload.request,
328
+ arg: payload.arg,
329
+ },
330
+ });
331
+ }
332
+ return next;
333
+ });
334
+ }
335
+
336
+ if (isMainScopedOrLegacy && payload.type === "action_result") {
337
+ if (!payload.id) {
338
+ return;
339
+ }
340
+ setMessages((current) => {
341
+ const next = [...current];
342
+ const existingIndex = next.findIndex(
343
+ (item) => item?.id === payload.id
344
+ );
345
+ const nextMessage = {
346
+ id: payload.id,
347
+ role: "assistant",
348
+ type: "action_result",
349
+ text: payload.text || "",
350
+ action: {
351
+ request: payload.request,
352
+ arg: payload.arg,
353
+ status: payload.status,
354
+ output: payload.output,
355
+ },
356
+ };
357
+ if (existingIndex === -1) {
358
+ next.push(nextMessage);
359
+ } else {
360
+ next[existingIndex] = {
361
+ ...next[existingIndex],
362
+ ...nextMessage,
363
+ };
364
+ }
365
+ return next;
366
+ });
367
+ if (payload.request === "run" || payload.request === "git") {
368
+ void loadRepoLastCommit();
369
+ if (typeof loadBranches === "function") {
370
+ void loadBranches();
371
+ }
372
+ }
373
+ }
374
+
375
+ if (isMainScopedOrLegacy && payload.type === "backlog_view") {
376
+ if (!payload.id) {
377
+ return;
378
+ }
379
+ setMessages((current) => {
380
+ const next = [...current];
381
+ const existingIndex = next.findIndex(
382
+ (item) => item?.id === payload.id
383
+ );
384
+ if (existingIndex === -1) {
385
+ next.push({
386
+ id: payload.id,
387
+ role: "assistant",
388
+ type: "backlog_view",
389
+ text: payload.text || "Backlog",
390
+ backlog: {
391
+ items: Array.isArray(payload.items) ? payload.items : [],
392
+ page: Number.isFinite(payload.page) ? payload.page : 0,
393
+ },
394
+ });
395
+ }
396
+ return next;
397
+ });
398
+ }
399
+
400
+ if (isMainScopedOrLegacy && payload.type === "command_execution_delta") {
401
+ if (typeof payload.delta !== "string") {
402
+ return;
403
+ }
404
+ setMessages((current) => {
405
+ const next = [...current];
406
+ const existingIndex = commandIndex.get(payload.itemId);
407
+ if (existingIndex === undefined) {
408
+ const entry = {
409
+ id: payload.itemId,
410
+ role: "commandExecution",
411
+ command: t("Command"),
412
+ output: payload.delta,
413
+ isExpandable: true,
414
+ status: "running",
415
+ };
416
+ commandIndex.set(payload.itemId, next.length);
417
+ next.push(entry);
418
+ return next;
419
+ }
420
+ const updated = { ...next[existingIndex] };
421
+ updated.output = `${updated.output || ""}${payload.delta}`;
422
+ updated.isExpandable = true;
423
+ next[existingIndex] = updated;
424
+ return next;
425
+ });
426
+ }
427
+
428
+ if (
429
+ isMainScopedOrLegacy &&
430
+ payload.type === "command_execution_completed"
431
+ ) {
432
+ const item = payload.item;
433
+ const itemId = payload.itemId || item?.id;
434
+ if (!itemId) {
435
+ return;
436
+ }
437
+ setMessages((current) => {
438
+ const next = [...current];
439
+ const existingIndex = commandIndex.get(itemId);
440
+ const command =
441
+ item?.commandActions?.command || item?.command || t("Command");
442
+ if (existingIndex === undefined) {
443
+ const entry = {
444
+ id: itemId,
445
+ role: "commandExecution",
446
+ command,
447
+ output: item?.aggregatedOutput || "",
448
+ isExpandable: true,
449
+ status: "completed",
450
+ };
451
+ commandIndex.set(itemId, next.length);
452
+ next.push(entry);
453
+ return next;
454
+ }
455
+ const updated = { ...next[existingIndex] };
456
+ updated.command = command;
457
+ updated.output = item?.aggregatedOutput || updated.output || "";
458
+ updated.isExpandable = true;
459
+ updated.status = "completed";
460
+ next[existingIndex] = updated;
461
+ return next;
462
+ });
463
+ }
464
+
465
+ if (isMainScopedOrLegacy && payload.type === "turn_error") {
466
+ setStatus(t("Error: {{message}}", { message: payload.message }));
467
+ setCurrentTurnId(null);
468
+ setMainTaskLabel("");
469
+ }
470
+
471
+ if (isMainScopedOrLegacy && payload.type === "agent_reasoning") {
472
+ const label = extractFirstLine(payload.text);
473
+ if (label) {
474
+ setMainTaskLabel(label);
475
+ }
476
+ }
477
+
478
+ if (isMainScopedOrLegacy && payload.type === "error") {
479
+ setStatus(payload.message || t("Unexpected error"));
480
+ setModelLoading(false);
481
+ setModelError(payload.message || t("Unexpected error"));
482
+ }
483
+
484
+ if (isWorktreeScoped && payload.type === "error") {
485
+ setWorktrees((current) => {
486
+ const next = new Map(current);
487
+ const wt = next.get(scopedWorktreeId);
488
+ if (!wt) {
489
+ return current;
490
+ }
491
+ next.set(scopedWorktreeId, {
492
+ ...wt,
493
+ modelLoading: false,
494
+ modelError: payload.message || t("Unexpected error"),
495
+ });
496
+ return next;
497
+ });
498
+ }
499
+
500
+ if (isMainScopedOrLegacy && payload.type === "turn_started") {
501
+ setCurrentTurnId(payload.turnId || null);
502
+ }
503
+
504
+ if (isMainScopedOrLegacy && payload.type === "turn_completed") {
505
+ const errorPayload = payload?.turn?.error || payload?.error || null;
506
+ const turnErrorInfo = errorPayload?.codexErrorInfo;
507
+ if (turnErrorInfo === "usageLimitExceeded") {
508
+ const warningId = `usage-limit-${
509
+ payload.turnId || payload.turn?.id || Date.now()
510
+ }`;
511
+ const warningText =
512
+ (typeof errorPayload === "string"
513
+ ? errorPayload
514
+ : errorPayload?.message) ||
515
+ t("Usage limit reached. Please try again later.");
516
+ setMessages((current) => {
517
+ if (current.some((message) => message.id === warningId)) {
518
+ return current;
519
+ }
520
+ return [
521
+ ...current,
522
+ {
523
+ id: warningId,
524
+ role: "assistant",
525
+ text: `⚠️ ${warningText}`,
526
+ },
527
+ ];
528
+ });
529
+ }
530
+ setCurrentTurnId(null);
531
+ setMainTaskLabel("");
532
+ void loadRepoLastCommit();
533
+ }
534
+
535
+ if (isMainScopedOrLegacy && payload.type === "repo_diff") {
536
+ setRepoDiff({
537
+ status: payload.status || "",
538
+ diff: payload.diff || "",
539
+ });
540
+ }
541
+
542
+ if (isMainScopedOrLegacy && payload.type === "model_list") {
543
+ const list = Array.isArray(payload.models) ? payload.models : [];
544
+ setModels(list);
545
+ if (payload.provider) {
546
+ setProviderModelState((current) => ({
547
+ ...current,
548
+ [payload.provider]: {
549
+ models: list,
550
+ loading: false,
551
+ error: "",
552
+ },
553
+ }));
554
+ }
555
+ const defaultModel = list.find((model) => model.isDefault);
556
+ if (defaultModel?.model) {
557
+ setSelectedModel(defaultModel.model);
558
+ }
559
+ if (defaultModel?.defaultReasoningEffort) {
560
+ setSelectedReasoningEffort(defaultModel.defaultReasoningEffort);
561
+ }
562
+ setModelLoading(false);
563
+ setModelError("");
564
+ }
565
+
566
+ if (isWorktreeScoped && payload.type === "model_list") {
567
+ const list = Array.isArray(payload.models) ? payload.models : [];
568
+ setWorktrees((current) => {
569
+ const next = new Map(current);
570
+ const wt = next.get(scopedWorktreeId);
571
+ if (!wt) {
572
+ return current;
573
+ }
574
+ const defaultModel = list.find((model) => model.isDefault);
575
+ const resolvedModel = wt.model || defaultModel?.model || null;
576
+ next.set(scopedWorktreeId, {
577
+ ...wt,
578
+ models: list,
579
+ model: resolvedModel,
580
+ modelLoading: false,
581
+ modelError: "",
582
+ });
583
+ return next;
584
+ });
585
+ }
586
+
587
+ if (isMainScopedOrLegacy && payload.type === "model_set") {
588
+ setSelectedModel(payload.model || "");
589
+ if (payload.reasoningEffort !== undefined) {
590
+ setSelectedReasoningEffort(payload.reasoningEffort || "");
591
+ }
592
+ setModelLoading(false);
593
+ setModelError("");
594
+ }
595
+
596
+ if (isWorktreeScoped && payload.type === "model_set") {
597
+ setWorktrees((current) => {
598
+ const next = new Map(current);
599
+ const wt = next.get(scopedWorktreeId);
600
+ if (!wt) {
601
+ return current;
602
+ }
603
+ next.set(scopedWorktreeId, {
604
+ ...wt,
605
+ model: payload.model || null,
606
+ reasoningEffort: payload.reasoningEffort ?? wt.reasoningEffort ?? null,
607
+ modelLoading: false,
608
+ modelError: "",
609
+ });
610
+ return next;
611
+ });
612
+ }
613
+
614
+ if (isMainScopedOrLegacy && payload.type === "rpc_log") {
615
+ if (!rpcLogsEnabledRef.current) {
616
+ return;
617
+ }
618
+ if (payload.entry) {
619
+ setRpcLogs((current) => [...current, payload.entry]);
620
+ }
621
+ }
622
+
623
+ if (isMainScopedOrLegacy && payload.type === "session_sync") {
624
+ if (!payload?.session) {
625
+ return;
626
+ }
627
+ const data = payload.session;
628
+ setMessages(
629
+ (data.messages || []).map((message, index) => ({
630
+ ...message,
631
+ id: message?.id || `history-${index}`,
632
+ attachments: normalizeAttachments(message?.attachments || []),
633
+ toolResult: message?.toolResult,
634
+ }))
635
+ );
636
+ setRepoDiff(data.repoDiff || { status: "", diff: "" });
637
+ if (data.provider) {
638
+ setProviderModelState((current) => ({
639
+ ...current,
640
+ [data.provider]: {
641
+ models: data.models || [],
642
+ loading: false,
643
+ error: "",
644
+ },
645
+ }));
646
+ }
647
+ }
648
+
649
+ if (isMainScopedOrLegacy && payload.type === "worktrees_list") {
650
+ applyWorktreesList(payload.worktrees || []);
651
+ }
652
+
653
+ if (isMainScopedOrLegacy && payload.type === "worktree_messages_sync") {
654
+ if (payload.worktreeId === "main") {
655
+ mergeAndApplyMessages(payload.messages || []);
656
+ return;
657
+ }
658
+ setWorktrees((current) => {
659
+ const next = new Map(current);
660
+ const wt = next.get(payload.worktreeId);
661
+ if (wt) {
662
+ const incoming = (payload.messages || []).map((message, index) => ({
663
+ ...message,
664
+ id: message?.id || `history-${index}`,
665
+ attachments: normalizeAttachments(message?.attachments || []),
666
+ toolResult: message?.toolResult,
667
+ }));
668
+ const seen = new Set(
669
+ wt.messages.map((message) => message?.id).filter(Boolean)
670
+ );
671
+ const merged = [...wt.messages];
672
+ incoming.forEach((message) => {
673
+ if (message?.id && seen.has(message.id)) {
674
+ return;
675
+ }
676
+ if (message?.id) {
677
+ seen.add(message.id);
678
+ }
679
+ merged.push(message);
680
+ });
681
+ next.set(payload.worktreeId, {
682
+ ...wt,
683
+ messages: merged,
684
+ status: payload.status ?? wt.status,
685
+ });
686
+ }
687
+ return next;
688
+ });
689
+ }
690
+
691
+ if (payload.type === "worktree_diff") {
692
+ setWorktrees((current) => {
693
+ const next = new Map(current);
694
+ const wt = next.get(payload.worktreeId);
695
+ if (wt) {
696
+ next.set(payload.worktreeId, {
697
+ ...wt,
698
+ diff: { status: payload.status, diff: payload.diff },
699
+ });
700
+ }
701
+ return next;
702
+ });
703
+ }
704
+
705
+ if (
706
+ isWorktreeScoped &&
707
+ (payload.type === "assistant_delta" ||
708
+ payload.type === "assistant_message" ||
709
+ payload.type === "action_request" ||
710
+ payload.type === "action_result" ||
711
+ payload.type === "backlog_view" ||
712
+ payload.type === "command_execution_delta" ||
713
+ payload.type === "command_execution_completed" ||
714
+ payload.type === "turn_started" ||
715
+ payload.type === "turn_completed" ||
716
+ payload.type === "turn_error")
717
+ ) {
718
+ const wtId = payload.worktreeId;
719
+
720
+ if (
721
+ payload.type === "assistant_delta" ||
722
+ payload.type === "command_execution_delta" ||
723
+ payload.type === "command_execution_completed" ||
724
+ payload.type === "item_started"
725
+ ) {
726
+ setWorktrees((current) => {
727
+ const next = new Map(current);
728
+ const wt = next.get(wtId);
729
+ if (wt && wt.status === "ready") {
730
+ next.set(wtId, { ...wt, status: "processing" });
731
+ }
732
+ return next;
733
+ });
734
+ }
735
+
736
+ if (payload.type === "turn_started") {
737
+ setWorktrees((current) => {
738
+ const next = new Map(current);
739
+ const wt = next.get(wtId);
740
+ if (wt) {
741
+ next.set(wtId, {
742
+ ...wt,
743
+ currentTurnId: payload.turnId,
744
+ activity: t("Processing..."),
745
+ });
746
+ }
747
+ return next;
748
+ });
749
+ }
750
+
751
+ if (payload.type === "action_request") {
752
+ if (!payload.id) {
753
+ return;
754
+ }
755
+ setWorktrees((current) => {
756
+ const next = new Map(current);
757
+ const wt = next.get(wtId);
758
+ if (wt) {
759
+ next.set(wtId, {
760
+ ...wt,
761
+ messages: [
762
+ ...wt.messages,
763
+ {
764
+ id: payload.id,
765
+ role: "user",
766
+ type: "action_request",
767
+ text:
768
+ payload.text ||
769
+ `/${payload.request || "run"} ${payload.arg || ""}`.trim(),
770
+ action: {
771
+ request: payload.request,
772
+ arg: payload.arg,
773
+ },
774
+ },
775
+ ],
776
+ });
777
+ }
778
+ return next;
779
+ });
780
+ }
781
+
782
+ if (payload.type === "action_result") {
783
+ if (!payload.id) {
784
+ return;
785
+ }
786
+ setWorktrees((current) => {
787
+ const next = new Map(current);
788
+ const wt = next.get(wtId) || {
789
+ id: wtId,
790
+ name: wtId,
791
+ branchName: "",
792
+ provider: "codex",
793
+ status: "processing",
794
+ messages: [],
795
+ models: [],
796
+ modelLoading: false,
797
+ modelError: "",
798
+ activity: "",
799
+ currentTurnId: null,
800
+ };
801
+ if (wt.messages.some((message) => message?.id === payload.id)) {
802
+ return current;
803
+ }
804
+ next.set(wtId, {
805
+ ...wt,
806
+ messages: [
807
+ ...wt.messages,
808
+ {
809
+ id: payload.id,
810
+ role: "assistant",
811
+ type: "action_result",
812
+ text: payload.text || "",
813
+ action: {
814
+ request: payload.request,
815
+ arg: payload.arg,
816
+ status: payload.status,
817
+ output: payload.output,
818
+ },
819
+ },
820
+ ],
821
+ });
822
+ return next;
823
+ });
824
+ if (payload.request === "run" || payload.request === "git") {
825
+ void loadWorktreeLastCommit(wtId);
826
+ if (typeof loadBranches === "function" && wtId === "main") {
827
+ void loadBranches();
828
+ }
829
+ }
830
+ }
831
+
832
+ if (payload.type === "backlog_view") {
833
+ if (!payload.id) {
834
+ return;
835
+ }
836
+ setWorktrees((current) => {
837
+ const next = new Map(current);
838
+ const wt = next.get(wtId);
839
+ if (wt) {
840
+ next.set(wtId, {
841
+ ...wt,
842
+ messages: [
843
+ ...wt.messages,
844
+ {
845
+ id: payload.id,
846
+ role: "assistant",
847
+ type: "backlog_view",
848
+ text: payload.text || "Backlog",
849
+ backlog: {
850
+ items: Array.isArray(payload.items) ? payload.items : [],
851
+ page: Number.isFinite(payload.page) ? payload.page : 0,
852
+ },
853
+ },
854
+ ],
855
+ });
856
+ }
857
+ return next;
858
+ });
859
+ }
860
+
861
+ if (payload.type === "turn_completed" || payload.type === "turn_error") {
862
+ setWorktrees((current) => {
863
+ const next = new Map(current);
864
+ const wt = next.get(wtId);
865
+ if (wt) {
866
+ next.set(wtId, {
867
+ ...wt,
868
+ currentTurnId: null,
869
+ activity: "",
870
+ taskLabel: "",
871
+ });
872
+ }
873
+ return next;
874
+ });
875
+ }
876
+
877
+ if (payload.type === "turn_completed") {
878
+ const errorPayload = payload?.turn?.error || payload?.error || null;
879
+ const turnErrorInfo = errorPayload?.codexErrorInfo;
880
+ if (turnErrorInfo === "usageLimitExceeded") {
881
+ const warningId = `usage-limit-${payload.turnId || Date.now()}`;
882
+ const warningText =
883
+ (typeof errorPayload === "string"
884
+ ? errorPayload
885
+ : errorPayload?.message) ||
886
+ t("Usage limit reached. Please try again later.");
887
+ setWorktrees((current) => {
888
+ const next = new Map(current);
889
+ const wt = next.get(wtId);
890
+ if (!wt) return current;
891
+ if (wt.messages.some((message) => message.id === warningId)) {
892
+ return current;
893
+ }
894
+ next.set(wtId, {
895
+ ...wt,
896
+ messages: [
897
+ ...wt.messages,
898
+ { id: warningId, role: "assistant", text: `⚠️ ${warningText}` },
899
+ ],
900
+ });
901
+ return next;
902
+ });
903
+ }
904
+ void loadWorktreeLastCommit(wtId);
905
+ }
906
+
907
+ if (
908
+ payload.type === "assistant_delta" ||
909
+ payload.type === "assistant_message"
910
+ ) {
911
+ setWorktrees((current) => {
912
+ const next = new Map(current);
913
+ const wt = next.get(wtId);
914
+ if (!wt) return current;
915
+
916
+ const messages = [...wt.messages];
917
+ const existingIdx = messages.findIndex(
918
+ (m) => m.id === payload.itemId
919
+ );
920
+
921
+ if (payload.type === "assistant_delta") {
922
+ if (existingIdx === -1) {
923
+ messages.push({
924
+ id: payload.itemId,
925
+ role: "assistant",
926
+ text: payload.delta || "",
927
+ });
928
+ } else {
929
+ messages[existingIdx] = {
930
+ ...messages[existingIdx],
931
+ text:
932
+ (messages[existingIdx].text || "") + (payload.delta || ""),
933
+ };
934
+ }
935
+ } else {
936
+ if (existingIdx === -1) {
937
+ messages.push({
938
+ id: payload.itemId,
939
+ role: "assistant",
940
+ text: payload.text || "",
941
+ });
942
+ } else {
943
+ messages[existingIdx] = {
944
+ ...messages[existingIdx],
945
+ text: payload.text || "",
946
+ };
947
+ }
948
+ }
949
+
950
+ next.set(wtId, { ...wt, messages });
951
+ return next;
952
+ });
953
+ if (payload.type === "assistant_message" && typeof payload.text === "string") {
954
+ const taskLabel = extractVibe80Task(payload.text);
955
+ if (taskLabel) {
956
+ setWorktrees((current) => {
957
+ const next = new Map(current);
958
+ const wt = next.get(wtId);
959
+ if (!wt) return current;
960
+ next.set(wtId, { ...wt, taskLabel });
961
+ return next;
962
+ });
963
+ }
964
+ }
965
+ }
966
+
967
+ if (
968
+ payload.type === "command_execution_delta" ||
969
+ payload.type === "command_execution_completed"
970
+ ) {
971
+ setWorktrees((current) => {
972
+ const next = new Map(current);
973
+ const wt = next.get(wtId);
974
+ if (!wt) return current;
975
+
976
+ const messages = [...wt.messages];
977
+ const itemId = payload.itemId || payload.item?.id;
978
+ const existingIdx = messages.findIndex((m) => m.id === itemId);
979
+
980
+ if (payload.type === "command_execution_delta") {
981
+ if (existingIdx === -1) {
982
+ messages.push({
983
+ id: itemId,
984
+ role: "commandExecution",
985
+ command: t("Command"),
986
+ output: payload.delta || "",
987
+ status: "running",
988
+ isExpandable: true,
989
+ });
990
+ } else {
991
+ messages[existingIdx] = {
992
+ ...messages[existingIdx],
993
+ output:
994
+ (messages[existingIdx].output || "") + (payload.delta || ""),
995
+ };
996
+ }
997
+ } else {
998
+ const item = payload.item;
999
+ const command =
1000
+ item?.commandActions?.command || item?.command || t("Command");
1001
+ if (existingIdx === -1) {
1002
+ messages.push({
1003
+ id: itemId,
1004
+ role: "commandExecution",
1005
+ command,
1006
+ output: item?.aggregatedOutput || "",
1007
+ status: "completed",
1008
+ isExpandable: true,
1009
+ });
1010
+ } else {
1011
+ messages[existingIdx] = {
1012
+ ...messages[existingIdx],
1013
+ command,
1014
+ output:
1015
+ item?.aggregatedOutput ||
1016
+ messages[existingIdx].output ||
1017
+ "",
1018
+ status: "completed",
1019
+ };
1020
+ }
1021
+ }
1022
+
1023
+ next.set(wtId, { ...wt, messages });
1024
+ return next;
1025
+ });
1026
+ }
1027
+ }
1028
+
1029
+ if (payload.type === "agent_reasoning" && isWorktreeScoped) {
1030
+ const label = extractFirstLine(payload.text);
1031
+ if (label) {
1032
+ setWorktrees((current) => {
1033
+ const next = new Map(current);
1034
+ const wt = next.get(payload.worktreeId);
1035
+ if (!wt) return current;
1036
+ next.set(payload.worktreeId, { ...wt, taskLabel: label });
1037
+ return next;
1038
+ });
1039
+ }
1040
+ }
1041
+
1042
+ if (payload.type === "item_started" && isWorktreeScoped) {
1043
+ const label = getItemActivityLabel(payload.item);
1044
+ if (!label) {
1045
+ return;
1046
+ }
1047
+ const wtId = payload.worktreeId;
1048
+ setWorktrees((current) => {
1049
+ const next = new Map(current);
1050
+ const wt = next.get(wtId);
1051
+ if (wt) {
1052
+ next.set(wtId, { ...wt, activity: label });
1053
+ }
1054
+ return next;
1055
+ });
1056
+ }
1057
+
1058
+ if (payload.type === "worktree_created") {
1059
+ setWorktrees((current) => {
1060
+ const next = new Map(current);
1061
+ next.set(payload.worktreeId, {
1062
+ id: payload.worktreeId,
1063
+ name: payload.name,
1064
+ branchName: payload.branchName,
1065
+ provider: payload.provider,
1066
+ model: payload.model || null,
1067
+ reasoningEffort: payload.reasoningEffort || null,
1068
+ internetAccess: Boolean(payload.internetAccess),
1069
+ denyGitCredentialsAccess:
1070
+ typeof payload.denyGitCredentialsAccess === "boolean"
1071
+ ? payload.denyGitCredentialsAccess
1072
+ : true,
1073
+ status: payload.status || "creating",
1074
+ color: payload.color,
1075
+ models: [],
1076
+ modelLoading: false,
1077
+ modelError: "",
1078
+ messages: [],
1079
+ activity: "",
1080
+ currentTurnId: null,
1081
+ });
1082
+ return next;
1083
+ });
1084
+ setPaneByTab((current) => ({
1085
+ ...current,
1086
+ [payload.worktreeId]: current[payload.worktreeId] || "chat",
1087
+ }));
1088
+ setLogFilterByTab((current) => ({
1089
+ ...current,
1090
+ [payload.worktreeId]: current[payload.worktreeId] || "all",
1091
+ }));
1092
+ setActiveWorktreeId(payload.worktreeId);
1093
+ }
1094
+
1095
+ if (payload.type === "worktree_ready") {
1096
+ setWorktrees((current) => {
1097
+ const next = new Map(current);
1098
+ const wt = next.get(payload.worktreeId);
1099
+ if (wt) {
1100
+ next.set(payload.worktreeId, { ...wt, status: "ready" });
1101
+ }
1102
+ return next;
1103
+ });
1104
+ }
1105
+
1106
+ if (payload.type === "worktree_status") {
1107
+ if (payload.worktreeId === "main") {
1108
+ if (!payload.status) {
1109
+ return;
1110
+ }
1111
+ setHasMainWorktreeStatus?.(true);
1112
+ const nextStatus = payload.status;
1113
+ setProcessing(nextStatus === "processing");
1114
+ setActivity(nextStatus === "processing" ? t("Processing...") : "");
1115
+ if (nextStatus !== "processing") {
1116
+ setMainTaskLabel("");
1117
+ }
1118
+ return;
1119
+ }
1120
+ if (!payload.status) {
1121
+ return;
1122
+ }
1123
+ setWorktrees((current) => {
1124
+ const next = new Map(current);
1125
+ const wt = next.get(payload.worktreeId);
1126
+ if (wt) {
1127
+ next.set(payload.worktreeId, {
1128
+ ...wt,
1129
+ status: payload.status,
1130
+ error: payload.error || null,
1131
+ ...(payload.status === "processing"
1132
+ ? {}
1133
+ : { activity: "", taskLabel: "", currentTurnId: null }),
1134
+ });
1135
+ }
1136
+ return next;
1137
+ });
1138
+ }
1139
+
1140
+ if (payload.type === "worktree_removed") {
1141
+ setWorktrees((current) => {
1142
+ const next = new Map(current);
1143
+ next.delete(payload.worktreeId);
1144
+ return next;
1145
+ });
1146
+ setPaneByTab((current) => {
1147
+ const next = { ...current };
1148
+ delete next[payload.worktreeId];
1149
+ return next;
1150
+ });
1151
+ setLogFilterByTab((current) => {
1152
+ const next = { ...current };
1153
+ delete next[payload.worktreeId];
1154
+ return next;
1155
+ });
1156
+ if (activeWorktreeIdRef.current === payload.worktreeId) {
1157
+ setActiveWorktreeId("main");
1158
+ }
1159
+ }
1160
+
1161
+ if (payload.type === "worktree_renamed") {
1162
+ setWorktrees((current) => {
1163
+ const next = new Map(current);
1164
+ const wt = next.get(payload.worktreeId);
1165
+ if (wt) {
1166
+ next.set(payload.worktreeId, { ...wt, name: payload.name });
1167
+ }
1168
+ return next;
1169
+ });
1170
+ }
1171
+ });
1172
+ };
1173
+
1174
+ connect();
1175
+
1176
+ const handleVisibilityChange = () => {
1177
+ if (document.hidden || !isMounted) {
1178
+ return;
1179
+ }
1180
+ lastPongRef.current = Date.now();
1181
+ const socket = socketRef.current;
1182
+ if (socket && socket.readyState === WebSocket.OPEN) {
1183
+ socket.send(JSON.stringify({ type: "ping" }));
1184
+ void resyncSession();
1185
+ requestMessageSync();
1186
+ const worktreeId = activeWorktreeIdRef?.current || "main";
1187
+ socket.send(JSON.stringify({ type: "wake_up", worktreeId }));
1188
+ } else {
1189
+ clearReconnectTimer();
1190
+ reconnectAttemptRef.current = 0;
1191
+ connect();
1192
+ }
1193
+ };
1194
+ document.addEventListener("visibilitychange", handleVisibilityChange);
1195
+
1196
+ return () => {
1197
+ isMounted = false;
1198
+ closingRef.current = true;
1199
+ clearReconnectTimer();
1200
+ clearPingInterval();
1201
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
1202
+ if (socketRef.current) {
1203
+ socketRef.current.close();
1204
+ }
1205
+ clearWakeUpInterval();
1206
+ closingRef.current = false;
1207
+ };
1208
+ }, [
1209
+ attachmentSessionId,
1210
+ workspaceToken,
1211
+ messageIndex,
1212
+ commandIndex,
1213
+ mergeAndApplyMessages,
1214
+ requestMessageSync,
1215
+ requestWorktreesList,
1216
+ requestWorktreeMessages,
1217
+ applyWorktreesList,
1218
+ resyncSession,
1219
+ t,
1220
+ ]);
1221
+
1222
+ useEffect(() => {
1223
+ if (!attachmentSessionId || !openAiLoginRequest || !connected) {
1224
+ return;
1225
+ }
1226
+ const socket = socketRef.current;
1227
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
1228
+ return;
1229
+ }
1230
+ socket.send(
1231
+ JSON.stringify({
1232
+ type: "account_login_start",
1233
+ provider: "codex",
1234
+ params: openAiLoginRequest,
1235
+ })
1236
+ );
1237
+ setOpenAiLoginRequest(null);
1238
+ }, [attachmentSessionId, connected, openAiLoginRequest, setOpenAiLoginRequest, socketRef]);
1239
+ }