@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,97 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ export default function useSessionHandoff({ t, apiFetch, attachmentSessionId }) {
3
+ const [handoffOpen, setHandoffOpen] = useState(false);
4
+ const [handoffQrDataUrl, setHandoffQrDataUrl] = useState("");
5
+ const [handoffExpiresAt, setHandoffExpiresAt] = useState(null);
6
+ const [handoffLoading, setHandoffLoading] = useState(false);
7
+ const [handoffError, setHandoffError] = useState("");
8
+ const [handoffRemaining, setHandoffRemaining] = useState(null);
9
+
10
+ const buildHandoffPayload = (token, expiresAt) =>
11
+ JSON.stringify({
12
+ type: "vibe80_handoff",
13
+ handoffToken: token,
14
+ baseUrl: window.location.origin,
15
+ expiresAt,
16
+ });
17
+
18
+ const requestHandoffQr = useCallback(async () => {
19
+ if (!attachmentSessionId) {
20
+ return;
21
+ }
22
+ setHandoffLoading(true);
23
+ setHandoffError("");
24
+ try {
25
+ const response = await apiFetch("/api/v1/sessions/handoff", {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify({ sessionId: attachmentSessionId }),
29
+ });
30
+ if (!response.ok) {
31
+ const payload = await response.json().catch(() => ({}));
32
+ throw new Error(payload?.error || t("Unable to generate the QR code."));
33
+ }
34
+ const data = await response.json();
35
+ const token = data?.handoffToken;
36
+ if (!token) {
37
+ throw new Error(t("Invalid resume token."));
38
+ }
39
+ const expiresAt = data?.expiresAt ?? null;
40
+ const payload = buildHandoffPayload(token, expiresAt);
41
+ const { default: QRCode } = await import("qrcode");
42
+ const qrDataUrl = await QRCode.toDataURL(payload, {
43
+ width: 260,
44
+ margin: 1,
45
+ });
46
+ setHandoffQrDataUrl(qrDataUrl);
47
+ setHandoffExpiresAt(expiresAt);
48
+ setHandoffOpen(true);
49
+ } catch (error) {
50
+ setHandoffError(error?.message || t("Error during generation."));
51
+ } finally {
52
+ setHandoffLoading(false);
53
+ }
54
+ }, [attachmentSessionId, apiFetch, t]);
55
+
56
+ const closeHandoffQr = useCallback(() => {
57
+ setHandoffOpen(false);
58
+ setHandoffError("");
59
+ setHandoffQrDataUrl("");
60
+ setHandoffExpiresAt(null);
61
+ setHandoffRemaining(null);
62
+ }, []);
63
+
64
+ useEffect(() => {
65
+ if (!handoffOpen || !handoffExpiresAt) {
66
+ setHandoffRemaining(null);
67
+ return;
68
+ }
69
+ const expiresAtMs =
70
+ typeof handoffExpiresAt === "number"
71
+ ? handoffExpiresAt
72
+ : new Date(handoffExpiresAt).getTime();
73
+ if (!Number.isFinite(expiresAtMs)) {
74
+ setHandoffRemaining(null);
75
+ return;
76
+ }
77
+ const tick = () => {
78
+ const remainingMs = expiresAtMs - Date.now();
79
+ const remainingSeconds = Math.max(0, Math.ceil(remainingMs / 1000));
80
+ setHandoffRemaining(remainingSeconds);
81
+ };
82
+ tick();
83
+ const intervalId = setInterval(tick, 1000);
84
+ return () => clearInterval(intervalId);
85
+ }, [handoffOpen, handoffExpiresAt]);
86
+
87
+ return {
88
+ handoffOpen,
89
+ handoffQrDataUrl,
90
+ handoffExpiresAt,
91
+ handoffLoading,
92
+ handoffError,
93
+ handoffRemaining,
94
+ requestHandoffQr,
95
+ closeHandoffQr,
96
+ };
97
+ }
@@ -0,0 +1,287 @@
1
+ import { useCallback, useEffect } from "react";
2
+
3
+ const getSessionIdFromUrl = () =>
4
+ new URLSearchParams(window.location.search).get("session");
5
+
6
+ export default function useSessionLifecycle({
7
+ t,
8
+ apiFetch,
9
+ workspaceToken,
10
+ handleLeaveWorkspace,
11
+ repoUrl,
12
+ setRepoUrl,
13
+ repoInput,
14
+ sessionNameInput,
15
+ repoAuth,
16
+ setRepoAuth,
17
+ authMode,
18
+ sshKeyInput,
19
+ httpUsername,
20
+ httpPassword,
21
+ sessionMode,
22
+ sessionRequested,
23
+ setSessionRequested,
24
+ defaultInternetAccess,
25
+ defaultDenyGitCredentialsAccess,
26
+ attachmentSession,
27
+ setAttachmentSession,
28
+ setAttachmentsLoading,
29
+ setAttachmentsError,
30
+ setWorkspaceToken,
31
+ setWorkspaceMode,
32
+ setWorkspaceError,
33
+ setOpenAiLoginPending,
34
+ setOpenAiLoginRequest,
35
+ }) {
36
+ useEffect(() => {
37
+ const sessionId = getSessionIdFromUrl();
38
+ if (!sessionId || !workspaceToken || attachmentSession?.sessionId) {
39
+ return;
40
+ }
41
+ const resumeSession = async () => {
42
+ try {
43
+ setSessionRequested(true);
44
+ setAttachmentsError("");
45
+ const response = await apiFetch(
46
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}`
47
+ );
48
+ if (!response.ok) {
49
+ throw new Error(t("Session not found."));
50
+ }
51
+ const data = await response.json();
52
+ setAttachmentSession(data);
53
+ } catch (error) {
54
+ setAttachmentsError(
55
+ error.message || t("Unable to resume the session.")
56
+ );
57
+ setSessionRequested(false);
58
+ }
59
+ };
60
+
61
+ resumeSession();
62
+ }, [
63
+ workspaceToken,
64
+ attachmentSession?.sessionId,
65
+ apiFetch,
66
+ handleLeaveWorkspace,
67
+ setAttachmentSession,
68
+ setAttachmentsError,
69
+ setSessionRequested,
70
+ t,
71
+ ]);
72
+
73
+ useEffect(() => {
74
+ if (!repoUrl || attachmentSession?.sessionId || sessionMode !== "new") {
75
+ return;
76
+ }
77
+ const createAttachmentSession = async () => {
78
+ try {
79
+ setAttachmentsLoading(true);
80
+ setAttachmentsError("");
81
+ const payload = {
82
+ repoUrl,
83
+ defaultInternetAccess,
84
+ defaultDenyGitCredentialsAccess,
85
+ };
86
+ const trimmedName = sessionNameInput.trim();
87
+ if (trimmedName) {
88
+ payload.name = trimmedName;
89
+ }
90
+ if (repoAuth) {
91
+ payload.auth = repoAuth;
92
+ }
93
+ const response = await apiFetch("/api/v1/sessions", {
94
+ method: "POST",
95
+ headers: { "Content-Type": "application/json" },
96
+ body: JSON.stringify(payload),
97
+ });
98
+ if (!response.ok) {
99
+ let details = "";
100
+ let errorType = "";
101
+ try {
102
+ const errorPayload = await response.json();
103
+ if (typeof errorPayload?.error === "string") {
104
+ details = errorPayload.error;
105
+ } else if (typeof errorPayload?.message === "string") {
106
+ details = errorPayload.message;
107
+ } else if (typeof errorPayload === "string") {
108
+ details = errorPayload;
109
+ }
110
+ if (typeof errorPayload?.error_type === "string") {
111
+ errorType = errorPayload.error_type;
112
+ }
113
+ } catch (parseError) {
114
+ try {
115
+ details = await response.text();
116
+ } catch (readError) {
117
+ details = "";
118
+ }
119
+ }
120
+ const isInvalidToken =
121
+ response.status === 401 &&
122
+ (errorType === "WORKSPACE_TOKEN_INVALID" ||
123
+ (typeof details === "string" &&
124
+ details.toLowerCase().includes("invalid workspace token")));
125
+ if (isInvalidToken) {
126
+ setWorkspaceToken("");
127
+ setWorkspaceMode("existing");
128
+ setWorkspaceError(
129
+ t("Invalid workspace token. Please sign in again.")
130
+ );
131
+ setAttachmentsError("");
132
+ return;
133
+ }
134
+ const suffix = details ? `: ${details}` : "";
135
+ if (response.status === 401 || response.status === 403) {
136
+ throw new Error(
137
+ t("Git authentication failed{{suffix}}.", { suffix })
138
+ );
139
+ }
140
+ if (response.status === 404) {
141
+ throw new Error(
142
+ t("Git repository not found{{suffix}}.", { suffix })
143
+ );
144
+ }
145
+ throw new Error(
146
+ t(
147
+ "Impossible de creer la session de pieces jointes (HTTP {{status}}{{statusText}}){{suffix}}.",
148
+ {
149
+ status: response.status,
150
+ statusText: response.statusText ? ` ${response.statusText}` : "",
151
+ suffix,
152
+ }
153
+ )
154
+ );
155
+ }
156
+ const data = await response.json();
157
+ setAttachmentSession(data);
158
+ } catch (error) {
159
+ setAttachmentsError(
160
+ error.message || t("Unable to create the attachment session.")
161
+ );
162
+ setOpenAiLoginPending(false);
163
+ setOpenAiLoginRequest(null);
164
+ } finally {
165
+ setAttachmentsLoading(false);
166
+ setSessionRequested(false);
167
+ }
168
+ };
169
+
170
+ createAttachmentSession();
171
+ }, [
172
+ repoUrl,
173
+ repoAuth,
174
+ attachmentSession?.sessionId,
175
+ apiFetch,
176
+ sessionMode,
177
+ defaultInternetAccess,
178
+ defaultDenyGitCredentialsAccess,
179
+ sessionNameInput,
180
+ setAttachmentSession,
181
+ setAttachmentsError,
182
+ setAttachmentsLoading,
183
+ setOpenAiLoginPending,
184
+ setOpenAiLoginRequest,
185
+ setSessionRequested,
186
+ setWorkspaceError,
187
+ setWorkspaceMode,
188
+ setWorkspaceToken,
189
+ t,
190
+ ]);
191
+
192
+ useEffect(() => {
193
+ if (!attachmentSession?.sessionId) {
194
+ return;
195
+ }
196
+ const url = new URL(window.location.href);
197
+ url.searchParams.set("session", attachmentSession.sessionId);
198
+ window.history.replaceState({}, "", url);
199
+ }, [attachmentSession?.sessionId]);
200
+
201
+ const handleResumeSession = useCallback(
202
+ async (sessionId) => {
203
+ if (!sessionId) {
204
+ return;
205
+ }
206
+ try {
207
+ setSessionRequested(true);
208
+ setAttachmentsError("");
209
+ const response = await apiFetch(
210
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}`
211
+ );
212
+ if (!response.ok) {
213
+ throw new Error(t("Session not found."));
214
+ }
215
+ const data = await response.json();
216
+ setAttachmentSession(data);
217
+ } catch (error) {
218
+ setAttachmentsError(
219
+ error.message || t("Unable to resume the session.")
220
+ );
221
+ setSessionRequested(false);
222
+ }
223
+ },
224
+ [
225
+ apiFetch,
226
+ setAttachmentSession,
227
+ setAttachmentsError,
228
+ setSessionRequested,
229
+ t,
230
+ ]
231
+ );
232
+
233
+ const onRepoSubmit = useCallback(
234
+ (event) => {
235
+ event.preventDefault();
236
+ const hasSession = Boolean(attachmentSession?.sessionId);
237
+ const trimmed = repoInput.trim();
238
+ if (!hasSession && !trimmed) {
239
+ setAttachmentsError("URL de depot git requise pour demarrer.");
240
+ return;
241
+ }
242
+ let auth = null;
243
+ if (!hasSession) {
244
+ if (authMode === "ssh") {
245
+ const trimmedKey = sshKeyInput.trim();
246
+ if (!trimmedKey) {
247
+ setAttachmentsError("Cle SSH privee requise pour demarrer.");
248
+ return;
249
+ }
250
+ auth = { type: "ssh", privateKey: trimmedKey };
251
+ }
252
+ if (authMode === "http") {
253
+ const user = httpUsername.trim();
254
+ if (!user || !httpPassword) {
255
+ setAttachmentsError(t("Username and password required."));
256
+ return;
257
+ }
258
+ auth = { type: "http", username: user, password: httpPassword };
259
+ }
260
+ }
261
+ setAttachmentsError("");
262
+ if (!hasSession) {
263
+ setSessionRequested(true);
264
+ setRepoAuth(auth);
265
+ setRepoUrl(trimmed);
266
+ }
267
+ },
268
+ [
269
+ attachmentSession?.sessionId,
270
+ authMode,
271
+ httpPassword,
272
+ httpUsername,
273
+ repoInput,
274
+ setAttachmentsError,
275
+ setRepoAuth,
276
+ setRepoUrl,
277
+ setSessionRequested,
278
+ sshKeyInput,
279
+ t,
280
+ ]
281
+ );
282
+
283
+ return {
284
+ handleResumeSession,
285
+ onRepoSubmit,
286
+ };
287
+ }
@@ -0,0 +1,63 @@
1
+ import { useCallback } from "react";
2
+
3
+ export default function useSessionReset({
4
+ setAttachmentSession,
5
+ setRepoUrl,
6
+ setRepoInput,
7
+ setRepoAuth,
8
+ setSessionRequested,
9
+ setAttachmentsError,
10
+ setAttachmentsLoading,
11
+ setMessages,
12
+ setRepoDiff,
13
+ setRpcLogs,
14
+ setRpcLogsEnabled,
15
+ setRepoLastCommit,
16
+ setWorktreeLastCommitById,
17
+ setCurrentTurnId,
18
+ setActivity,
19
+ setDefaultDenyGitCredentialsAccess,
20
+ }) {
21
+ const handleLeaveSession = useCallback(() => {
22
+ setAttachmentSession(null);
23
+ setRepoUrl("");
24
+ setRepoInput("");
25
+ setRepoAuth(null);
26
+ setSessionRequested(false);
27
+ setAttachmentsError("");
28
+ setAttachmentsLoading(false);
29
+ setMessages([]);
30
+ setRepoDiff({ status: "", diff: "" });
31
+ setRpcLogs([]);
32
+ setRpcLogsEnabled(true);
33
+ setRepoLastCommit(null);
34
+ setWorktreeLastCommitById(new Map());
35
+ setCurrentTurnId(null);
36
+ setActivity("");
37
+ if (typeof setDefaultDenyGitCredentialsAccess === "function") {
38
+ setDefaultDenyGitCredentialsAccess(false);
39
+ }
40
+ const url = new URL(window.location.href);
41
+ url.searchParams.delete("session");
42
+ window.history.replaceState({}, "", url);
43
+ }, [
44
+ setActivity,
45
+ setAttachmentSession,
46
+ setAttachmentsError,
47
+ setAttachmentsLoading,
48
+ setCurrentTurnId,
49
+ setMessages,
50
+ setRepoAuth,
51
+ setRepoDiff,
52
+ setRepoInput,
53
+ setRepoLastCommit,
54
+ setRepoUrl,
55
+ setRpcLogs,
56
+ setRpcLogsEnabled,
57
+ setSessionRequested,
58
+ setDefaultDenyGitCredentialsAccess,
59
+ setWorktreeLastCommitById,
60
+ ]);
61
+
62
+ return { handleLeaveSession };
63
+ }
@@ -0,0 +1,77 @@
1
+ import { useCallback } from "react";
2
+
3
+ export default function useSessionResync({
4
+ attachmentSessionId,
5
+ apiFetch,
6
+ llmProvider,
7
+ setLlmProvider,
8
+ setSelectedProviders,
9
+ setOpenAiReady,
10
+ setClaudeReady,
11
+ setRepoDiff,
12
+ setRpcLogsEnabled,
13
+ setRpcLogs,
14
+ setTerminalEnabled,
15
+ loadMainWorktreeSnapshot,
16
+ }) {
17
+ const resyncSession = useCallback(async () => {
18
+ if (!attachmentSessionId) {
19
+ return;
20
+ }
21
+ try {
22
+ const response = await apiFetch(
23
+ `/api/v1/sessions/${encodeURIComponent(attachmentSessionId)}`
24
+ );
25
+ if (!response.ok) {
26
+ return;
27
+ }
28
+ const data = await response.json();
29
+ if (data?.defaultProvider && data.defaultProvider !== llmProvider) {
30
+ setLlmProvider(data.defaultProvider);
31
+ }
32
+ if (Array.isArray(data?.providers) && data.providers.length) {
33
+ const filtered = data.providers.filter(
34
+ (entry) => entry === "codex" || entry === "claude"
35
+ );
36
+ if (filtered.length) {
37
+ setSelectedProviders(filtered);
38
+ setOpenAiReady(filtered.includes("codex"));
39
+ setClaudeReady(filtered.includes("claude"));
40
+ }
41
+ }
42
+ if (data?.repoDiff) {
43
+ setRepoDiff(data.repoDiff);
44
+ }
45
+ if (typeof data?.rpcLogsEnabled === "boolean") {
46
+ setRpcLogsEnabled(data.rpcLogsEnabled);
47
+ if (!data.rpcLogsEnabled) {
48
+ setRpcLogs([]);
49
+ }
50
+ }
51
+ if (Array.isArray(data?.rpcLogs) && data?.rpcLogsEnabled !== false) {
52
+ setRpcLogs(data.rpcLogs);
53
+ }
54
+ if (typeof data?.terminalEnabled === "boolean") {
55
+ setTerminalEnabled(data.terminalEnabled);
56
+ }
57
+ void loadMainWorktreeSnapshot();
58
+ } catch (error) {
59
+ // Ignore resync failures; reconnect loop will retry.
60
+ }
61
+ }, [
62
+ attachmentSessionId,
63
+ apiFetch,
64
+ llmProvider,
65
+ loadMainWorktreeSnapshot,
66
+ setClaudeReady,
67
+ setLlmProvider,
68
+ setOpenAiReady,
69
+ setRepoDiff,
70
+ setRpcLogs,
71
+ setRpcLogsEnabled,
72
+ setSelectedProviders,
73
+ setTerminalEnabled,
74
+ ]);
75
+
76
+ return { resyncSession };
77
+ }