@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,218 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+
3
+ export default function useRepoBranchesModels({
4
+ apiFetch,
5
+ attachmentSessionId,
6
+ llmProvider,
7
+ loadRepoLastCommit,
8
+ processing,
9
+ setBranchMenuOpen,
10
+ socketRef,
11
+ t,
12
+ }) {
13
+ const [models, setModels] = useState([]);
14
+ const [selectedModel, setSelectedModel] = useState("");
15
+ const [selectedReasoningEffort, setSelectedReasoningEffort] = useState("");
16
+ const [modelLoading, setModelLoading] = useState(false);
17
+ const [modelError, setModelError] = useState("");
18
+ const [providerModelState, setProviderModelState] = useState({});
19
+ const [branches, setBranches] = useState([]);
20
+ const [currentBranch, setCurrentBranch] = useState("");
21
+ const [defaultBranch, setDefaultBranch] = useState("");
22
+ const [branchLoading, setBranchLoading] = useState(false);
23
+ const [branchError, setBranchError] = useState("");
24
+ const initialBranchRef = useRef("");
25
+
26
+ const loadBranches = useCallback(async () => {
27
+ if (!attachmentSessionId) {
28
+ return;
29
+ }
30
+ setBranchLoading(true);
31
+ setBranchError("");
32
+ try {
33
+ const response = await apiFetch(
34
+ `/api/v1/sessions/${encodeURIComponent(
35
+ attachmentSessionId
36
+ )}/branches`
37
+ );
38
+ const payload = await response.json().catch(() => ({}));
39
+ if (!response.ok) {
40
+ throw new Error(payload.error || t("Unable to load branches."));
41
+ }
42
+ setBranches(Array.isArray(payload.branches) ? payload.branches : []);
43
+ setCurrentBranch(payload.current || "");
44
+ if (!initialBranchRef.current && payload.current) {
45
+ initialBranchRef.current = payload.current;
46
+ setDefaultBranch(payload.current);
47
+ }
48
+ } catch (error) {
49
+ setBranchError(error.message || t("Unable to load branches."));
50
+ } finally {
51
+ setBranchLoading(false);
52
+ }
53
+ }, [attachmentSessionId, apiFetch, t]);
54
+
55
+ const loadProviderModels = useCallback(
56
+ async (provider) => {
57
+ if (!attachmentSessionId || !provider) {
58
+ return;
59
+ }
60
+ setProviderModelState((current) => ({
61
+ ...current,
62
+ [provider]: {
63
+ ...(current?.[provider] || {}),
64
+ loading: true,
65
+ error: "",
66
+ },
67
+ }));
68
+ try {
69
+ const response = await apiFetch(
70
+ `/api/v1/sessions/${encodeURIComponent(
71
+ attachmentSessionId
72
+ )}/models?provider=${encodeURIComponent(provider)}`
73
+ );
74
+ const payload = await response.json().catch(() => ({}));
75
+ if (!response.ok) {
76
+ throw new Error(payload.error || t("Unable to load models."));
77
+ }
78
+ setProviderModelState((current) => ({
79
+ ...current,
80
+ [provider]: {
81
+ models: Array.isArray(payload.models) ? payload.models : [],
82
+ loading: false,
83
+ error: "",
84
+ },
85
+ }));
86
+ } catch (error) {
87
+ setProviderModelState((current) => ({
88
+ ...current,
89
+ [provider]: {
90
+ ...(current?.[provider] || {}),
91
+ loading: false,
92
+ error: error.message || t("Unable to load models."),
93
+ },
94
+ }));
95
+ }
96
+ },
97
+ [attachmentSessionId, apiFetch, t]
98
+ );
99
+
100
+ useEffect(() => {
101
+ if (!attachmentSessionId) {
102
+ setBranches([]);
103
+ setCurrentBranch("");
104
+ setDefaultBranch("");
105
+ setBranchError("");
106
+ initialBranchRef.current = "";
107
+ setProviderModelState({});
108
+ return;
109
+ }
110
+ initialBranchRef.current = "";
111
+ setDefaultBranch("");
112
+ setProviderModelState({});
113
+ loadBranches();
114
+ }, [attachmentSessionId, loadBranches]);
115
+
116
+ useEffect(() => {
117
+ if (!attachmentSessionId) {
118
+ return;
119
+ }
120
+ void loadRepoLastCommit?.();
121
+ }, [attachmentSessionId, currentBranch, loadRepoLastCommit]);
122
+
123
+ const requestModelList = useCallback(() => {
124
+ if (!socketRef.current || llmProvider !== "codex") {
125
+ return;
126
+ }
127
+ setModelLoading(true);
128
+ setModelError("");
129
+ socketRef.current.send(JSON.stringify({ type: "model_list" }));
130
+ }, [llmProvider, socketRef]);
131
+
132
+ const handleModelChange = useCallback(
133
+ (event) => {
134
+ const value = event.target.value;
135
+ setSelectedModel(value);
136
+ if (!socketRef.current) {
137
+ return;
138
+ }
139
+ setModelLoading(true);
140
+ setModelError("");
141
+ socketRef.current.send(
142
+ JSON.stringify({
143
+ type: "model_set",
144
+ model: value,
145
+ reasoningEffort: selectedReasoningEffort || null,
146
+ })
147
+ );
148
+ },
149
+ [selectedReasoningEffort, socketRef]
150
+ );
151
+
152
+ const handleReasoningEffortChange = useCallback(
153
+ (event) => {
154
+ const value = event.target.value;
155
+ setSelectedReasoningEffort(value);
156
+ if (!socketRef.current) {
157
+ return;
158
+ }
159
+ setModelLoading(true);
160
+ setModelError("");
161
+ socketRef.current.send(
162
+ JSON.stringify({
163
+ type: "model_set",
164
+ model: selectedModel || null,
165
+ reasoningEffort: value || null,
166
+ })
167
+ );
168
+ },
169
+ [selectedModel, socketRef]
170
+ );
171
+
172
+ const handleBranchSelect = useCallback(
173
+ async (branch) => {
174
+ if (!attachmentSessionId || processing) {
175
+ return;
176
+ }
177
+ setBranchError(t("Branch switching is no longer supported."));
178
+ if (setBranchMenuOpen) {
179
+ setBranchMenuOpen(false);
180
+ }
181
+ },
182
+ [
183
+ attachmentSessionId,
184
+ processing,
185
+ setBranchMenuOpen,
186
+ t,
187
+ ]
188
+ );
189
+
190
+ return {
191
+ branches,
192
+ branchError,
193
+ branchLoading,
194
+ currentBranch,
195
+ defaultBranch,
196
+ handleBranchSelect,
197
+ handleModelChange,
198
+ handleReasoningEffortChange,
199
+ loadBranches,
200
+ loadProviderModels,
201
+ modelError,
202
+ modelLoading,
203
+ models,
204
+ providerModelState,
205
+ requestModelList,
206
+ selectedModel,
207
+ selectedReasoningEffort,
208
+ setBranches,
209
+ setCurrentBranch,
210
+ setDefaultBranch,
211
+ setModelError,
212
+ setModelLoading,
213
+ setModels,
214
+ setProviderModelState,
215
+ setSelectedModel,
216
+ setSelectedReasoningEffort,
217
+ };
218
+ }
@@ -0,0 +1,350 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+
3
+ const MAX_UNTRACKED_FILE_PANELS = 100;
4
+
5
+ const parseStatusLine = (line) => {
6
+ const raw = String(line || "").trim();
7
+ if (!raw) return null;
8
+ const code = raw.slice(0, 2);
9
+ let path = raw.slice(3).trim();
10
+ if (!code || !path) return null;
11
+ if ((code.startsWith("R") || code.startsWith("C")) && path.includes(" -> ")) {
12
+ path = path.split(" -> ").pop()?.trim() || path;
13
+ }
14
+ if (path.startsWith("\"") && path.endsWith("\"")) {
15
+ try {
16
+ path = JSON.parse(path);
17
+ } catch {
18
+ // Keep raw quoted path if parsing fails.
19
+ }
20
+ }
21
+ return { code, path };
22
+ };
23
+
24
+ export default function useRepoStatus({
25
+ apiFetch,
26
+ attachmentSessionId,
27
+ currentBranch,
28
+ activeWorktreeId,
29
+ parseDiff,
30
+ setWorktrees,
31
+ worktrees,
32
+ t,
33
+ }) {
34
+ const [repoDiff, setRepoDiff] = useState({ status: "", diff: "" });
35
+ const [repoLastCommit, setRepoLastCommit] = useState(null);
36
+ const [untrackedFilePanels, setUntrackedFilePanels] = useState([]);
37
+ const [untrackedLoading, setUntrackedLoading] = useState(false);
38
+ const [worktreeLastCommitById, setWorktreeLastCommitById] = useState(
39
+ new Map()
40
+ );
41
+
42
+ const currentDiff = useMemo(() => {
43
+ if (activeWorktreeId && activeWorktreeId !== "main") {
44
+ const wt = worktrees.get(activeWorktreeId);
45
+ return wt?.diff || { status: "", diff: "" };
46
+ }
47
+ return repoDiff;
48
+ }, [activeWorktreeId, worktrees, repoDiff]);
49
+
50
+ const diffFiles = useMemo(() => {
51
+ if (!currentDiff.diff) {
52
+ return [];
53
+ }
54
+ try {
55
+ return parseDiff(currentDiff.diff);
56
+ } catch {
57
+ return [];
58
+ }
59
+ }, [currentDiff.diff, parseDiff]);
60
+
61
+ const diffStatusLines = useMemo(
62
+ () =>
63
+ (currentDiff.status || "")
64
+ .split(/\r?\n/)
65
+ .map((line) => line.trim())
66
+ .filter(Boolean),
67
+ [currentDiff.status]
68
+ );
69
+ const parsedStatusEntries = useMemo(
70
+ () => diffStatusLines.map(parseStatusLine).filter(Boolean),
71
+ [diffStatusLines]
72
+ );
73
+ const untrackedRoots = useMemo(() => {
74
+ const seen = new Set();
75
+ const result = [];
76
+ parsedStatusEntries.forEach((entry) => {
77
+ if (entry.code !== "??") return;
78
+ const normalized = String(entry.path || "").trim().replace(/\\/g, "/");
79
+ if (!normalized || seen.has(normalized)) return;
80
+ seen.add(normalized);
81
+ result.push(normalized);
82
+ });
83
+ return result;
84
+ }, [parsedStatusEntries]);
85
+
86
+ const hasCurrentChanges = useMemo(
87
+ () => diffStatusLines.length > 0 || Boolean((currentDiff.diff || "").trim()),
88
+ [diffStatusLines.length, currentDiff.diff]
89
+ );
90
+
91
+ useEffect(() => {
92
+ if (!attachmentSessionId) {
93
+ setUntrackedFilePanels([]);
94
+ setUntrackedLoading(false);
95
+ return;
96
+ }
97
+ if (!untrackedRoots.length) {
98
+ setUntrackedFilePanels([]);
99
+ setUntrackedLoading(false);
100
+ return;
101
+ }
102
+
103
+ let cancelled = false;
104
+ const worktreeId =
105
+ activeWorktreeId && activeWorktreeId !== "main" ? activeWorktreeId : "main";
106
+
107
+ const load = async () => {
108
+ setUntrackedLoading(true);
109
+ const queue = [...untrackedRoots];
110
+ const visited = new Set();
111
+ const panels = [];
112
+
113
+ while (
114
+ queue.length > 0 &&
115
+ panels.length < MAX_UNTRACKED_FILE_PANELS &&
116
+ !cancelled
117
+ ) {
118
+ const currentPath = String(queue.shift() || "").trim();
119
+ if (!currentPath || visited.has(currentPath)) {
120
+ continue;
121
+ }
122
+ visited.add(currentPath);
123
+
124
+ const shouldBrowseFirst = currentPath.endsWith("/");
125
+
126
+ if (!shouldBrowseFirst) {
127
+ try {
128
+ const fileResponse = await apiFetch(
129
+ `/api/v1/sessions/${encodeURIComponent(
130
+ attachmentSessionId
131
+ )}/worktrees/${encodeURIComponent(worktreeId)}/file?path=${encodeURIComponent(
132
+ currentPath
133
+ )}`
134
+ );
135
+ if (fileResponse.ok) {
136
+ const payload = await fileResponse.json().catch(() => ({}));
137
+ panels.push({
138
+ path: currentPath,
139
+ binary: Boolean(payload?.binary),
140
+ content: payload?.content || "",
141
+ truncated: Boolean(payload?.truncated),
142
+ error: false,
143
+ });
144
+ continue;
145
+ }
146
+ } catch {
147
+ // Fall through to directory probe.
148
+ }
149
+ }
150
+
151
+ try {
152
+ const browseResponse = await apiFetch(
153
+ `/api/v1/sessions/${encodeURIComponent(
154
+ attachmentSessionId
155
+ )}/worktrees/${encodeURIComponent(worktreeId)}/browse?path=${encodeURIComponent(
156
+ currentPath
157
+ )}`
158
+ );
159
+ if (!browseResponse.ok) {
160
+ panels.push({
161
+ path: currentPath,
162
+ binary: false,
163
+ content: "",
164
+ truncated: false,
165
+ error: true,
166
+ });
167
+ continue;
168
+ }
169
+ const payload = await browseResponse.json().catch(() => ({}));
170
+ const entries = Array.isArray(payload?.entries) ? payload.entries : [];
171
+ entries.forEach((entry) => {
172
+ if (!entry?.path) return;
173
+ queue.push(entry.path);
174
+ });
175
+ } catch {
176
+ panels.push({
177
+ path: currentPath,
178
+ binary: false,
179
+ content: "",
180
+ truncated: false,
181
+ error: true,
182
+ });
183
+ }
184
+ }
185
+
186
+ if (cancelled) return;
187
+ setUntrackedFilePanels(panels);
188
+ setUntrackedLoading(false);
189
+ };
190
+
191
+ void load();
192
+ return () => {
193
+ cancelled = true;
194
+ };
195
+ }, [activeWorktreeId, apiFetch, attachmentSessionId, untrackedRoots]);
196
+
197
+ const loadRepoLastCommit = useCallback(async () => {
198
+ if (!attachmentSessionId) {
199
+ return;
200
+ }
201
+ try {
202
+ const response = await apiFetch(
203
+ `/api/v1/sessions/${encodeURIComponent(attachmentSessionId)}/last-commit`
204
+ );
205
+ const payload = await response.json().catch(() => ({}));
206
+ if (!response.ok) {
207
+ throw new Error(payload.error || t("Unable to load the latest commit."));
208
+ }
209
+ const commit = payload.commit || {};
210
+ setRepoLastCommit({
211
+ branch: payload.branch || "",
212
+ sha: commit.sha || "",
213
+ message: commit.message || "",
214
+ });
215
+ } catch (error) {
216
+ setRepoLastCommit(null);
217
+ }
218
+ }, [attachmentSessionId, apiFetch, t]);
219
+
220
+ const loadWorktreeLastCommit = useCallback(
221
+ async (worktreeId) => {
222
+ if (!attachmentSessionId || !worktreeId) {
223
+ return;
224
+ }
225
+ try {
226
+ const response = await apiFetch(
227
+ `/api/v1/sessions/${encodeURIComponent(
228
+ attachmentSessionId
229
+ )}/worktrees/${encodeURIComponent(worktreeId)}/commits?limit=1`
230
+ );
231
+ const payload = await response.json().catch(() => ({}));
232
+ if (!response.ok) {
233
+ throw new Error(payload.error || t("Unable to load the commit."));
234
+ }
235
+ const commit = Array.isArray(payload.commits)
236
+ ? payload.commits[0]
237
+ : null;
238
+ if (!commit?.sha) {
239
+ return;
240
+ }
241
+ setWorktreeLastCommitById((current) => {
242
+ const next = new Map(current);
243
+ next.set(worktreeId, { sha: commit.sha, message: commit.message || "" });
244
+ return next;
245
+ });
246
+ } catch {
247
+ // Ignore worktree commit errors.
248
+ }
249
+ },
250
+ [attachmentSessionId, apiFetch, t]
251
+ );
252
+
253
+ const requestWorktreeDiff = useCallback(
254
+ async (worktreeId) => {
255
+ if (!attachmentSessionId || !worktreeId) {
256
+ return;
257
+ }
258
+ try {
259
+ const response = await apiFetch(
260
+ `/api/v1/sessions/${encodeURIComponent(
261
+ attachmentSessionId
262
+ )}/worktrees/${encodeURIComponent(worktreeId)}/diff`
263
+ );
264
+ if (!response.ok) {
265
+ return;
266
+ }
267
+ const payload = await response.json();
268
+ if (!payload) {
269
+ return;
270
+ }
271
+ setWorktrees((current) => {
272
+ const next = new Map(current);
273
+ const wt = next.get(worktreeId);
274
+ if (wt) {
275
+ next.set(worktreeId, {
276
+ ...wt,
277
+ diff: {
278
+ status: payload.status || "",
279
+ diff: payload.diff || "",
280
+ },
281
+ });
282
+ }
283
+ return next;
284
+ });
285
+ } catch {
286
+ // Ignore diff refresh failures.
287
+ }
288
+ },
289
+ [attachmentSessionId, apiFetch, setWorktrees]
290
+ );
291
+
292
+ const requestRepoDiff = useCallback(async () => {
293
+ if (!attachmentSessionId) {
294
+ return;
295
+ }
296
+ try {
297
+ const response = await apiFetch(
298
+ `/api/v1/sessions/${encodeURIComponent(attachmentSessionId)}/diff`
299
+ );
300
+ if (!response.ok) {
301
+ return;
302
+ }
303
+ const payload = await response.json();
304
+ if (!payload) {
305
+ return;
306
+ }
307
+ setRepoDiff({
308
+ status: payload.status || "",
309
+ diff: payload.diff || "",
310
+ });
311
+ } catch {
312
+ // Ignore diff refresh failures.
313
+ }
314
+ }, [attachmentSessionId, apiFetch]);
315
+
316
+ useEffect(() => {
317
+ if (!attachmentSessionId) {
318
+ setRepoLastCommit(null);
319
+ setWorktreeLastCommitById(new Map());
320
+ return;
321
+ }
322
+ loadRepoLastCommit();
323
+ }, [attachmentSessionId, loadRepoLastCommit]);
324
+
325
+ useEffect(() => {
326
+ if (!attachmentSessionId) {
327
+ return;
328
+ }
329
+ void loadRepoLastCommit();
330
+ }, [attachmentSessionId, currentBranch, loadRepoLastCommit]);
331
+
332
+ return {
333
+ currentDiff,
334
+ diffFiles,
335
+ diffStatusLines,
336
+ hasCurrentChanges,
337
+ untrackedFilePanels,
338
+ untrackedLoading,
339
+ loadRepoLastCommit,
340
+ loadWorktreeLastCommit,
341
+ repoDiff,
342
+ repoLastCommit,
343
+ requestRepoDiff,
344
+ requestWorktreeDiff,
345
+ setRepoDiff,
346
+ setRepoLastCommit,
347
+ setWorktreeLastCommitById,
348
+ worktreeLastCommitById,
349
+ };
350
+ }
@@ -0,0 +1,19 @@
1
+ import { useCallback } from "react";
2
+
3
+ export default function useRpcLogActions({
4
+ activeWorktreeId,
5
+ setRpcLogs,
6
+ }) {
7
+ const handleClearRpcLogs = useCallback(() => {
8
+ setRpcLogs((current) => {
9
+ if (activeWorktreeId && activeWorktreeId !== "main") {
10
+ return current.filter(
11
+ (entry) => entry?.worktreeId !== activeWorktreeId
12
+ );
13
+ }
14
+ return current.filter((entry) => Boolean(entry?.worktreeId));
15
+ });
16
+ }, [activeWorktreeId, setRpcLogs]);
17
+
18
+ return { handleClearRpcLogs };
19
+ }
@@ -0,0 +1,58 @@
1
+ import { useCallback, useMemo } from "react";
2
+
3
+ export default function useRpcLogView({
4
+ rpcLogs,
5
+ activeWorktreeId,
6
+ locale,
7
+ logFilterByTab,
8
+ setLogFilterByTab,
9
+ }) {
10
+ const logFilter = logFilterByTab[activeWorktreeId] || "all";
11
+ const setLogFilter = useCallback(
12
+ (value) => {
13
+ const key = activeWorktreeId || "main";
14
+ setLogFilterByTab((current) => ({
15
+ ...current,
16
+ [key]: value,
17
+ }));
18
+ },
19
+ [activeWorktreeId]
20
+ );
21
+ const scopedRpcLogs = useMemo(() => {
22
+ if (activeWorktreeId && activeWorktreeId !== "main") {
23
+ return (rpcLogs || []).filter(
24
+ (entry) => entry?.worktreeId === activeWorktreeId
25
+ );
26
+ }
27
+ return (rpcLogs || []).filter((entry) => !entry?.worktreeId);
28
+ }, [rpcLogs, activeWorktreeId]);
29
+ const formattedRpcLogs = useMemo(
30
+ () =>
31
+ scopedRpcLogs.map((entry) => ({
32
+ ...entry,
33
+ timeLabel: entry?.timestamp
34
+ ? new Date(entry.timestamp).toLocaleTimeString(locale)
35
+ : "",
36
+ })),
37
+ [scopedRpcLogs, locale]
38
+ );
39
+ const filteredRpcLogs = useMemo(() => {
40
+ if (logFilter === "stdin") {
41
+ return formattedRpcLogs.filter((entry) => entry.direction === "stdin");
42
+ }
43
+ if (logFilter === "stdout") {
44
+ return formattedRpcLogs.filter((entry) => entry.direction === "stdout");
45
+ }
46
+ return formattedRpcLogs;
47
+ }, [formattedRpcLogs, logFilter]);
48
+
49
+ return {
50
+ logFilterByTab,
51
+ setLogFilterByTab,
52
+ logFilter,
53
+ setLogFilter,
54
+ scopedRpcLogs,
55
+ formattedRpcLogs,
56
+ filteredRpcLogs,
57
+ };
58
+ }