@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,294 @@
1
+ import { useCallback, useEffect } from "react";
2
+
3
+ export default function useChatCommands({
4
+ activeProvider,
5
+ activeWorktreeId,
6
+ addToBacklog,
7
+ apiFetch,
8
+ attachmentSessionId,
9
+ captureScreenshot,
10
+ connected,
11
+ handleSendMessageRef,
12
+ handleViewSelect,
13
+ input,
14
+ isWorktreeStopped,
15
+ isCodexReady,
16
+ isInWorktree,
17
+ openPathInExplorer,
18
+ requestRepoDiff,
19
+ requestWorktreeDiff,
20
+ sendMessage,
21
+ sendWorktreeMessage,
22
+ buildAnnotatedMessage,
23
+ clearScopedAnnotations,
24
+ setAnnotationMode,
25
+ setCommandMenuOpen,
26
+ setDraftAttachments,
27
+ setInput,
28
+ setMessages,
29
+ setWorktrees,
30
+ socketRef,
31
+ t,
32
+ showToast,
33
+ }) {
34
+ const handleSendMessage = useCallback(
35
+ (textOverride, attachmentsOverride) => {
36
+ const rawText = (textOverride ?? input).trim();
37
+ if (!rawText) {
38
+ return;
39
+ }
40
+ const annotationMatch = rawText.match(/^\/annotation\s+(on|off)\s*$/i);
41
+ if (annotationMatch) {
42
+ const nextValue = annotationMatch[1].toLowerCase() === "on";
43
+ setAnnotationMode(nextValue);
44
+ showToast(
45
+ nextValue
46
+ ? t("Annotation mode enabled.")
47
+ : t("Annotation mode disabled."),
48
+ "info"
49
+ );
50
+ setInput("");
51
+ setDraftAttachments([]);
52
+ setCommandMenuOpen(false);
53
+ return;
54
+ }
55
+ if (rawText.startsWith("/annotation")) {
56
+ showToast(t("Usage: /annotation on|off"), "info");
57
+ return;
58
+ }
59
+ if (isWorktreeStopped) {
60
+ showToast(t("Worktree is stopped. Wake it up before sending a message."), "info");
61
+ return;
62
+ }
63
+ if (activeProvider === "codex" && !isCodexReady) {
64
+ showToast(t("Codex is starting. Please wait."), "info");
65
+ return;
66
+ }
67
+ if (rawText === "/diff" || rawText.startsWith("/diff ")) {
68
+ handleViewSelect("diff");
69
+ if (activeWorktreeId && activeWorktreeId !== "main") {
70
+ requestWorktreeDiff(activeWorktreeId);
71
+ } else {
72
+ requestRepoDiff();
73
+ }
74
+ setInput("");
75
+ setDraftAttachments([]);
76
+ setCommandMenuOpen(false);
77
+ return;
78
+ }
79
+ if (rawText.startsWith("/backlog")) {
80
+ if (!attachmentSessionId) {
81
+ showToast(t("Session not found."), "error");
82
+ return;
83
+ }
84
+ void (async () => {
85
+ try {
86
+ const response = await apiFetch(
87
+ `/api/v1/sessions/${encodeURIComponent(attachmentSessionId)}/backlog-items`
88
+ );
89
+ if (!response.ok) {
90
+ const payload = await response.json().catch(() => null);
91
+ throw new Error(payload?.error || t("Unable to load backlog."));
92
+ }
93
+ const payload = await response.json().catch(() => ({}));
94
+ const items = Array.isArray(payload?.items) ? payload.items : [];
95
+ const messageId = `backlog-${Date.now()}-${Math.random()
96
+ .toString(16)
97
+ .slice(2, 8)}`;
98
+ const backlogMessage = {
99
+ id: messageId,
100
+ role: "assistant",
101
+ type: "backlog_view",
102
+ text: "Backlog",
103
+ backlog: {
104
+ items,
105
+ page: 0,
106
+ },
107
+ };
108
+ if (isInWorktree && activeWorktreeId) {
109
+ setWorktrees((current) => {
110
+ const next = new Map(current);
111
+ const wt = next.get(activeWorktreeId);
112
+ if (wt) {
113
+ next.set(activeWorktreeId, {
114
+ ...wt,
115
+ messages: [...(wt.messages || []), backlogMessage],
116
+ });
117
+ }
118
+ return next;
119
+ });
120
+ } else {
121
+ setMessages((current) => [...current, backlogMessage]);
122
+ }
123
+ } catch (error) {
124
+ showToast(error.message || t("Unable to load backlog."), "error");
125
+ }
126
+ })();
127
+ setInput("");
128
+ setDraftAttachments([]);
129
+ setCommandMenuOpen(false);
130
+ return;
131
+ }
132
+ if (rawText.startsWith("/open")) {
133
+ const targetPath = rawText.replace(/^\/open\s*/i, "").trim();
134
+ if (!targetPath) {
135
+ showToast(t("Path required."), "error");
136
+ return;
137
+ }
138
+ openPathInExplorer(targetPath)
139
+ .then(() => {
140
+ setInput("");
141
+ setDraftAttachments([]);
142
+ setCommandMenuOpen(false);
143
+ })
144
+ .catch(() => null);
145
+ return;
146
+ }
147
+ if (rawText.startsWith("/todo")) {
148
+ const action = rawText.replace(/^\/todo\s*/i, "").trim();
149
+ if (!action) {
150
+ showToast(t("Todo text required."), "error");
151
+ return;
152
+ }
153
+ if (!attachmentSessionId) {
154
+ showToast(t("Session not found."), "error");
155
+ return;
156
+ }
157
+ void (async () => {
158
+ try {
159
+ const response = await apiFetch(
160
+ `/api/v1/sessions/${encodeURIComponent(attachmentSessionId)}/backlog-items`,
161
+ {
162
+ method: "POST",
163
+ headers: { "Content-Type": "application/json" },
164
+ body: JSON.stringify({ text: action }),
165
+ }
166
+ );
167
+ if (!response.ok) {
168
+ const payload = await response.json().catch(() => null);
169
+ throw new Error(payload?.error || t("Unable to update backlog."));
170
+ }
171
+ showToast(t("Added to backlog."));
172
+ } catch (error) {
173
+ showToast(
174
+ error.message || t("Unable to update backlog."),
175
+ "error"
176
+ );
177
+ }
178
+ })();
179
+ setInput("");
180
+ setDraftAttachments([]);
181
+ setCommandMenuOpen(false);
182
+ return;
183
+ }
184
+ if (rawText.startsWith("/run")) {
185
+ const command = rawText.replace(/^\/run\s*/i, "").trim();
186
+ if (!command) {
187
+ showToast(t("Command required."), "error");
188
+ return;
189
+ }
190
+ if (!socketRef.current || !connected) {
191
+ showToast(t("Disconnected"), "error");
192
+ return;
193
+ }
194
+ const targetWorktreeId =
195
+ isInWorktree && activeWorktreeId ? activeWorktreeId : null;
196
+ socketRef.current.send(
197
+ JSON.stringify({
198
+ type: "action_request",
199
+ request: "run",
200
+ arg: command,
201
+ worktreeId: targetWorktreeId || undefined,
202
+ })
203
+ );
204
+ setInput("");
205
+ setDraftAttachments([]);
206
+ setCommandMenuOpen(false);
207
+ return;
208
+ }
209
+ if (rawText.startsWith("/screenshot")) {
210
+ captureScreenshot()
211
+ .then(() => {
212
+ setInput("");
213
+ setCommandMenuOpen(false);
214
+ })
215
+ .catch(() => null);
216
+ return;
217
+ }
218
+ if (rawText.startsWith("/git")) {
219
+ const command = rawText.replace(/^\/git\s*/i, "").trim();
220
+ if (!command) {
221
+ showToast(t("Git command required."), "error");
222
+ return;
223
+ }
224
+ if (!socketRef.current || !connected) {
225
+ showToast(t("Disconnected"), "error");
226
+ return;
227
+ }
228
+ const targetWorktreeId =
229
+ isInWorktree && activeWorktreeId ? activeWorktreeId : null;
230
+ socketRef.current.send(
231
+ JSON.stringify({
232
+ type: "action_request",
233
+ request: "git",
234
+ arg: command,
235
+ worktreeId: targetWorktreeId || undefined,
236
+ })
237
+ );
238
+ setInput("");
239
+ setDraftAttachments([]);
240
+ setCommandMenuOpen(false);
241
+ return;
242
+ }
243
+ const transportText = buildAnnotatedMessage
244
+ ? buildAnnotatedMessage(rawText)
245
+ : rawText;
246
+ if (isInWorktree && activeWorktreeId) {
247
+ sendWorktreeMessage(
248
+ activeWorktreeId,
249
+ rawText,
250
+ attachmentsOverride,
251
+ transportText
252
+ );
253
+ } else {
254
+ sendMessage(rawText, attachmentsOverride, transportText);
255
+ }
256
+ clearScopedAnnotations?.();
257
+ },
258
+ [
259
+ activeProvider,
260
+ activeWorktreeId,
261
+ apiFetch,
262
+ attachmentSessionId,
263
+ buildAnnotatedMessage,
264
+ captureScreenshot,
265
+ clearScopedAnnotations,
266
+ connected,
267
+ handleViewSelect,
268
+ input,
269
+ isWorktreeStopped,
270
+ isCodexReady,
271
+ isInWorktree,
272
+ openPathInExplorer,
273
+ requestRepoDiff,
274
+ requestWorktreeDiff,
275
+ sendMessage,
276
+ sendWorktreeMessage,
277
+ setAnnotationMode,
278
+ setCommandMenuOpen,
279
+ setDraftAttachments,
280
+ setInput,
281
+ showToast,
282
+ socketRef,
283
+ t,
284
+ ]
285
+ );
286
+
287
+ useEffect(() => {
288
+ handleSendMessageRef.current = handleSendMessage;
289
+ }, [handleSendMessage, handleSendMessageRef]);
290
+
291
+ return {
292
+ handleSendMessage,
293
+ };
294
+ }
@@ -0,0 +1,144 @@
1
+ import { useCallback } from "react";
2
+
3
+ export default function useChatExport({
4
+ currentMessages,
5
+ attachmentRepoUrl,
6
+ repoUrl,
7
+ isInWorktree,
8
+ activeWorktree,
9
+ t,
10
+ normalizeAttachments,
11
+ downloadTextFile,
12
+ formatExportName,
13
+ extractRepoName,
14
+ setToolbarExportOpen,
15
+ }) {
16
+ const handleExportChat = useCallback(
17
+ (format) => {
18
+ setToolbarExportOpen(false);
19
+ const exportMessages = Array.isArray(currentMessages)
20
+ ? currentMessages
21
+ : [];
22
+ if (!exportMessages.length) {
23
+ return;
24
+ }
25
+ setToolbarExportOpen(false);
26
+ const baseName = extractRepoName(attachmentRepoUrl || repoUrl || "");
27
+ const tabLabel =
28
+ isInWorktree && activeWorktree
29
+ ? `${activeWorktree.name || t("Worktree")} (${activeWorktree.branchName || activeWorktree.id})`
30
+ : t("Main");
31
+ if (format === "markdown") {
32
+ const lines = [
33
+ `# ${t("Chat history")}`,
34
+ "",
35
+ `${t("Export")}: ${new Date().toISOString()}`,
36
+ `${t("Tab")}: ${tabLabel}`,
37
+ "",
38
+ ];
39
+ exportMessages.forEach((message) => {
40
+ if (message.role === "commandExecution") {
41
+ lines.push(`## ${t("Command")}`);
42
+ lines.push(`\`${message.command || t("Command")}\``);
43
+ if (message.output) {
44
+ lines.push("```");
45
+ lines.push(message.output);
46
+ lines.push("```");
47
+ }
48
+ lines.push("");
49
+ return;
50
+ }
51
+ if (message.role === "tool_result") {
52
+ const toolName =
53
+ message.toolResult?.name || message.toolResult?.tool || t("Tool");
54
+ const toolOutput = message.toolResult?.output || message.text || "";
55
+ lines.push(`## ${t("Tool result")}`);
56
+ lines.push(`\`${toolName}\``);
57
+ if (toolOutput) {
58
+ lines.push("```");
59
+ lines.push(toolOutput);
60
+ lines.push("```");
61
+ }
62
+ lines.push("");
63
+ return;
64
+ }
65
+ const roleLabel =
66
+ message.role === "user" ? t("Username") : t("Assistant");
67
+ lines.push(`## ${roleLabel}`);
68
+ lines.push(message.text || "");
69
+ const attachmentNames = normalizeAttachments(
70
+ message.attachments || []
71
+ )
72
+ .map((item) => item?.name || item?.path)
73
+ .filter(Boolean);
74
+ if (attachmentNames.length) {
75
+ lines.push(`${t("Attachments")}: ${attachmentNames.join(", ")}`);
76
+ }
77
+ lines.push("");
78
+ });
79
+ const content = lines.join("\n").trim() + "\n";
80
+ downloadTextFile(
81
+ formatExportName(baseName, "md"),
82
+ content,
83
+ "text/markdown"
84
+ );
85
+ return;
86
+ }
87
+ const payload = {
88
+ exportedAt: new Date().toISOString(),
89
+ repoUrl: attachmentRepoUrl || repoUrl || "",
90
+ tab: {
91
+ type: isInWorktree ? "worktree" : "main",
92
+ worktreeId: activeWorktree?.id || null,
93
+ worktreeName: activeWorktree?.name || null,
94
+ branchName: activeWorktree?.branchName || null,
95
+ },
96
+ messages: exportMessages.map((message) => {
97
+ if (message.role === "commandExecution") {
98
+ return {
99
+ id: message.id,
100
+ role: message.role,
101
+ command: message.command || "",
102
+ output: message.output || "",
103
+ status: message.status || "",
104
+ };
105
+ }
106
+ if (message.role === "tool_result") {
107
+ return {
108
+ id: message.id,
109
+ role: message.role,
110
+ text: message.text || "",
111
+ toolResult: message.toolResult || null,
112
+ };
113
+ }
114
+ return {
115
+ id: message.id,
116
+ role: message.role,
117
+ text: message.text || "",
118
+ attachments: normalizeAttachments(message.attachments || []),
119
+ };
120
+ }),
121
+ };
122
+ downloadTextFile(
123
+ formatExportName(baseName, "json"),
124
+ JSON.stringify(payload, null, 2),
125
+ "application/json"
126
+ );
127
+ },
128
+ [
129
+ activeWorktree,
130
+ attachmentRepoUrl,
131
+ currentMessages,
132
+ downloadTextFile,
133
+ extractRepoName,
134
+ formatExportName,
135
+ isInWorktree,
136
+ normalizeAttachments,
137
+ repoUrl,
138
+ setToolbarExportOpen,
139
+ t,
140
+ ]
141
+ );
142
+
143
+ return { handleExportChat };
144
+ }
@@ -0,0 +1,69 @@
1
+ import { useCallback } from "react";
2
+
3
+ export default function useChatMessagesState({
4
+ normalizeAttachments,
5
+ messageIndex,
6
+ commandIndex,
7
+ messagesRef,
8
+ setMessages,
9
+ setCommandPanelOpen,
10
+ setToolResultPanelOpen,
11
+ }) {
12
+ const applyMessages = useCallback(
13
+ (items = []) => {
14
+ const normalized = items.map((item, index) => ({
15
+ ...item,
16
+ id: item.id || `history-${index}`,
17
+ role: item.role,
18
+ text: item.text,
19
+ toolResult: item.toolResult,
20
+ attachments: normalizeAttachments(item.attachments || []),
21
+ }));
22
+ messageIndex.clear();
23
+ commandIndex.clear();
24
+ normalized.forEach((item, index) => {
25
+ if (item.role === "assistant") {
26
+ messageIndex.set(item.id, index);
27
+ }
28
+ });
29
+ setMessages(normalized);
30
+ setCommandPanelOpen({});
31
+ setToolResultPanelOpen({});
32
+ },
33
+ [
34
+ commandIndex,
35
+ messageIndex,
36
+ normalizeAttachments,
37
+ setCommandPanelOpen,
38
+ setMessages,
39
+ setToolResultPanelOpen,
40
+ ]
41
+ );
42
+
43
+ const mergeAndApplyMessages = useCallback(
44
+ (incoming = []) => {
45
+ if (!Array.isArray(incoming) || incoming.length === 0) {
46
+ return;
47
+ }
48
+ const current = Array.isArray(messagesRef.current)
49
+ ? messagesRef.current
50
+ : [];
51
+ const seen = new Set(current.map((item) => item?.id).filter(Boolean));
52
+ const merged = [...current];
53
+ for (const item of incoming) {
54
+ const id = item?.id;
55
+ if (id && seen.has(id)) {
56
+ continue;
57
+ }
58
+ if (id) {
59
+ seen.add(id);
60
+ }
61
+ merged.push(item);
62
+ }
63
+ applyMessages(merged);
64
+ },
65
+ [applyMessages, messagesRef]
66
+ );
67
+
68
+ return { applyMessages, mergeAndApplyMessages };
69
+ }
@@ -0,0 +1,158 @@
1
+ import { useCallback } from "react";
2
+
3
+ export default function useChatSend({
4
+ input,
5
+ setInput,
6
+ setMessages,
7
+ setDraftAttachments,
8
+ socketRef,
9
+ connected,
10
+ normalizeAttachments,
11
+ draftAttachments,
12
+ setWorktrees,
13
+ setProcessing,
14
+ setActivity,
15
+ processingLabel,
16
+ handleSendMessageRef,
17
+ ensureNotificationPermission,
18
+ }) {
19
+ const sendMessage = useCallback(
20
+ (textOverride, attachmentsOverride, transportTextOverride) => {
21
+ const displayText = (textOverride ?? input).trim();
22
+ if (!displayText || !socketRef.current || !connected) {
23
+ return;
24
+ }
25
+ const rawTransportText = (transportTextOverride ?? displayText).trim();
26
+ if (!rawTransportText) {
27
+ return;
28
+ }
29
+
30
+ void ensureNotificationPermission?.();
31
+ const resolvedAttachments = normalizeAttachments(
32
+ attachmentsOverride ?? draftAttachments
33
+ );
34
+ const selectedPaths = resolvedAttachments
35
+ .map((item) => item?.path)
36
+ .filter(Boolean);
37
+ const suffix =
38
+ selectedPaths.length > 0
39
+ ? `;; attachments: ${JSON.stringify(selectedPaths)}`
40
+ : "";
41
+ const text = `${rawTransportText}${suffix}`;
42
+ setMessages((current) => [
43
+ ...current,
44
+ {
45
+ id: `user-${Date.now()}`,
46
+ role: "user",
47
+ text: displayText,
48
+ attachments: resolvedAttachments,
49
+ },
50
+ ]);
51
+ setProcessing?.(true);
52
+ setActivity?.(processingLabel || "Processing...");
53
+ socketRef.current.send(
54
+ JSON.stringify({
55
+ type: "worktree_send_message",
56
+ worktreeId: "main",
57
+ text,
58
+ displayText,
59
+ attachments: resolvedAttachments,
60
+ })
61
+ );
62
+ setInput("");
63
+ setDraftAttachments([]);
64
+ },
65
+ [
66
+ connected,
67
+ draftAttachments,
68
+ input,
69
+ normalizeAttachments,
70
+ setDraftAttachments,
71
+ setInput,
72
+ setMessages,
73
+ socketRef,
74
+ ]
75
+ );
76
+
77
+ const sendCommitMessage = useCallback(
78
+ (text) => {
79
+ if (!handleSendMessageRef.current) {
80
+ return;
81
+ }
82
+ handleSendMessageRef.current(text, []);
83
+ },
84
+ [handleSendMessageRef]
85
+ );
86
+
87
+ const sendWorktreeMessage = useCallback(
88
+ (worktreeId, textOverride, attachmentsOverride, transportTextOverride) => {
89
+ const displayText = (textOverride ?? input).trim();
90
+ if (!displayText || !socketRef.current || !connected || !worktreeId) return;
91
+ const rawTransportText = (transportTextOverride ?? displayText).trim();
92
+ if (!rawTransportText) {
93
+ return;
94
+ }
95
+
96
+ const resolvedAttachments = normalizeAttachments(
97
+ attachmentsOverride ?? draftAttachments
98
+ );
99
+ const selectedPaths = resolvedAttachments
100
+ .map((item) => item?.path)
101
+ .filter(Boolean);
102
+ const suffix =
103
+ selectedPaths.length > 0
104
+ ? `;; attachments: ${JSON.stringify(selectedPaths)}`
105
+ : "";
106
+ const text = `${rawTransportText}${suffix}`;
107
+
108
+ setWorktrees((current) => {
109
+ const next = new Map(current);
110
+ if (worktreeId === "main") {
111
+ return next;
112
+ }
113
+ const wt = next.get(worktreeId);
114
+ if (wt) {
115
+ const messages = [
116
+ ...wt.messages,
117
+ {
118
+ id: `user-${Date.now()}`,
119
+ role: "user",
120
+ text: displayText,
121
+ attachments: resolvedAttachments,
122
+ },
123
+ ];
124
+ next.set(worktreeId, { ...wt, messages, status: "processing" });
125
+ }
126
+ return next;
127
+ });
128
+ if (worktreeId === "main") {
129
+ setProcessing?.(true);
130
+ setActivity?.(processingLabel || "Processing...");
131
+ }
132
+
133
+ socketRef.current.send(
134
+ JSON.stringify({
135
+ type: "worktree_send_message",
136
+ worktreeId,
137
+ text,
138
+ displayText,
139
+ attachments: resolvedAttachments,
140
+ })
141
+ );
142
+ setInput("");
143
+ setDraftAttachments([]);
144
+ },
145
+ [
146
+ connected,
147
+ draftAttachments,
148
+ input,
149
+ normalizeAttachments,
150
+ setDraftAttachments,
151
+ setInput,
152
+ setWorktrees,
153
+ socketRef,
154
+ ]
155
+ );
156
+
157
+ return { sendMessage, sendCommitMessage, sendWorktreeMessage };
158
+ }