@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,125 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+
3
+ export default function useAttachments({
4
+ attachmentSessionId,
5
+ workspaceToken,
6
+ normalizeAttachments,
7
+ isImageAttachment,
8
+ getAttachmentName,
9
+ attachmentIcon,
10
+ t,
11
+ }) {
12
+ const [draftAttachments, setDraftAttachments] = useState([]);
13
+ const [attachmentsLoading, setAttachmentsLoading] = useState(false);
14
+ const [attachmentsError, setAttachmentsError] = useState("");
15
+ const [attachmentPreview, setAttachmentPreview] = useState(null);
16
+
17
+ const getAttachmentUrl = useCallback(
18
+ (attachment) => {
19
+ if (!attachmentSessionId) {
20
+ return "";
21
+ }
22
+ const url = new URL(
23
+ `/api/v1/sessions/${encodeURIComponent(attachmentSessionId)}/attachments/file`,
24
+ window.location.origin
25
+ );
26
+ if (workspaceToken) {
27
+ url.searchParams.set("token", workspaceToken);
28
+ }
29
+ if (attachment?.path) {
30
+ url.searchParams.set("path", attachment.path);
31
+ } else if (attachment?.name) {
32
+ url.searchParams.set("name", attachment.name);
33
+ }
34
+ return url.toString();
35
+ },
36
+ [attachmentSessionId, workspaceToken]
37
+ );
38
+
39
+ const renderMessageAttachments = useCallback(
40
+ (attachments = []) => {
41
+ const normalized = normalizeAttachments(attachments);
42
+ if (!normalized.length) {
43
+ return null;
44
+ }
45
+ return (
46
+ <div className="bubble-attachments">
47
+ {normalized.map((attachment) => {
48
+ const name = getAttachmentName(attachment);
49
+ const url = getAttachmentUrl(attachment);
50
+ const key = attachment?.path || attachment?.name || name;
51
+ if (isImageAttachment(attachment)) {
52
+ return (
53
+ <button
54
+ type="button"
55
+ key={key}
56
+ className="attachment-card attachment-card--image"
57
+ onClick={() =>
58
+ url ? setAttachmentPreview({ url, name }) : null
59
+ }
60
+ disabled={!url}
61
+ >
62
+ {url ? (
63
+ <img
64
+ src={url}
65
+ alt={name || t("Attached image")}
66
+ className="attachment-thumb"
67
+ loading="lazy"
68
+ />
69
+ ) : (
70
+ <div className="attachment-thumb attachment-thumb--empty" />
71
+ )}
72
+ <span className="attachment-name">{name}</span>
73
+ </button>
74
+ );
75
+ }
76
+ if (url) {
77
+ return (
78
+ <a
79
+ key={key}
80
+ className="attachment-card"
81
+ href={url}
82
+ target="_blank"
83
+ rel="noopener noreferrer"
84
+ >
85
+ <span className="attachment-icon" aria-hidden="true">
86
+ {attachmentIcon}
87
+ </span>
88
+ <span className="attachment-name">{name}</span>
89
+ </a>
90
+ );
91
+ }
92
+ return (
93
+ <div key={key} className="attachment-card">
94
+ <span className="attachment-icon" aria-hidden="true">
95
+ {attachmentIcon}
96
+ </span>
97
+ <span className="attachment-name">{name}</span>
98
+ </div>
99
+ );
100
+ })}
101
+ </div>
102
+ );
103
+ },
104
+ [getAttachmentName, getAttachmentUrl, isImageAttachment, normalizeAttachments, t]
105
+ );
106
+
107
+ useEffect(() => {
108
+ if (!attachmentSessionId) {
109
+ setDraftAttachments([]);
110
+ }
111
+ }, [attachmentSessionId]);
112
+
113
+ return {
114
+ attachmentPreview,
115
+ attachmentsError,
116
+ attachmentsLoading,
117
+ draftAttachments,
118
+ getAttachmentUrl,
119
+ renderMessageAttachments,
120
+ setAttachmentPreview,
121
+ setAttachmentsError,
122
+ setAttachmentsLoading,
123
+ setDraftAttachments,
124
+ };
125
+ }
@@ -0,0 +1,254 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+
3
+ const toIsoDateTime = (value) => {
4
+ if (value == null || value === "") {
5
+ return null;
6
+ }
7
+ const parsed = new Date(value);
8
+ return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : null;
9
+ };
10
+
11
+ const normalizeBacklogItem = (item) => {
12
+ if (!item || typeof item !== "object") {
13
+ return item;
14
+ }
15
+ return {
16
+ ...item,
17
+ createdAt: toIsoDateTime(item.createdAt),
18
+ doneAt: toIsoDateTime(item.doneAt),
19
+ };
20
+ };
21
+
22
+ export default function useBacklog({
23
+ attachmentSessionId,
24
+ apiFetch,
25
+ normalizeAttachments,
26
+ sendMessage,
27
+ setInput,
28
+ setMessages,
29
+ setWorktrees,
30
+ setDraftAttachments,
31
+ input,
32
+ draftAttachments,
33
+ inputRef,
34
+ showToast,
35
+ t,
36
+ }) {
37
+ const [backlog, setBacklog] = useState([]);
38
+
39
+ const backlogKey = useMemo(
40
+ () =>
41
+ attachmentSessionId ? `backlog:${attachmentSessionId}` : null,
42
+ [attachmentSessionId]
43
+ );
44
+
45
+ useEffect(() => {
46
+ if (!backlogKey) {
47
+ return;
48
+ }
49
+ try {
50
+ const stored = JSON.parse(localStorage.getItem(backlogKey) || "[]");
51
+ setBacklog(
52
+ Array.isArray(stored)
53
+ ? stored.map(normalizeBacklogItem).filter(Boolean)
54
+ : []
55
+ );
56
+ } catch {
57
+ setBacklog([]);
58
+ }
59
+ }, [backlogKey]);
60
+
61
+ useEffect(() => {
62
+ if (!backlogKey) {
63
+ return;
64
+ }
65
+ localStorage.setItem(backlogKey, JSON.stringify(backlog));
66
+ }, [backlog, backlogKey]);
67
+
68
+ const updateBacklogMessages = useCallback(
69
+ (updateFn) => {
70
+ setMessages((current) =>
71
+ current.map((message) => {
72
+ if (message?.type !== "backlog_view") {
73
+ return message;
74
+ }
75
+ const items = Array.isArray(message.backlog?.items)
76
+ ? message.backlog.items
77
+ : [];
78
+ const updatedItems = updateFn(items);
79
+ if (updatedItems === items) {
80
+ return message;
81
+ }
82
+ return {
83
+ ...message,
84
+ backlog: {
85
+ ...(message.backlog || {}),
86
+ items: updatedItems,
87
+ },
88
+ };
89
+ })
90
+ );
91
+ setWorktrees((current) => {
92
+ const next = new Map(current);
93
+ next.forEach((wt, id) => {
94
+ if (!Array.isArray(wt?.messages)) {
95
+ return;
96
+ }
97
+ let changed = false;
98
+ const updatedMessages = wt.messages.map((message) => {
99
+ if (message?.type !== "backlog_view") {
100
+ return message;
101
+ }
102
+ const items = Array.isArray(message.backlog?.items)
103
+ ? message.backlog.items
104
+ : [];
105
+ const updatedItems = updateFn(items);
106
+ if (updatedItems === items) {
107
+ return message;
108
+ }
109
+ changed = true;
110
+ return {
111
+ ...message,
112
+ backlog: {
113
+ ...(message.backlog || {}),
114
+ items: updatedItems,
115
+ },
116
+ };
117
+ });
118
+ if (changed) {
119
+ next.set(id, { ...wt, messages: updatedMessages });
120
+ }
121
+ });
122
+ return next;
123
+ });
124
+ },
125
+ [setMessages, setWorktrees]
126
+ );
127
+
128
+ const setBacklogMessagePage = useCallback(
129
+ (targetWorktreeId, messageId, page) => {
130
+ if (targetWorktreeId && targetWorktreeId !== "main") {
131
+ setWorktrees((current) => {
132
+ const next = new Map(current);
133
+ const wt = next.get(targetWorktreeId);
134
+ if (!wt) {
135
+ return current;
136
+ }
137
+ const updatedMessages = wt.messages.map((message) =>
138
+ message?.id === messageId && message.type === "backlog_view"
139
+ ? {
140
+ ...message,
141
+ backlog: {
142
+ ...(message.backlog || {}),
143
+ page,
144
+ },
145
+ }
146
+ : message
147
+ );
148
+ next.set(targetWorktreeId, { ...wt, messages: updatedMessages });
149
+ return next;
150
+ });
151
+ return;
152
+ }
153
+ setMessages((current) =>
154
+ current.map((message) =>
155
+ message?.id === messageId && message.type === "backlog_view"
156
+ ? {
157
+ ...message,
158
+ backlog: {
159
+ ...(message.backlog || {}),
160
+ page,
161
+ },
162
+ }
163
+ : message
164
+ )
165
+ );
166
+ },
167
+ [setMessages, setWorktrees]
168
+ );
169
+
170
+ const markBacklogItemDone = useCallback(
171
+ async (itemId) => {
172
+ if (!attachmentSessionId) {
173
+ showToast?.(t("Session not found."), "error");
174
+ return;
175
+ }
176
+ try {
177
+ const response = await apiFetch(
178
+ `/api/v1/sessions/${encodeURIComponent(
179
+ attachmentSessionId
180
+ )}/backlog-items/${encodeURIComponent(itemId)}`,
181
+ {
182
+ method: "PATCH",
183
+ headers: { "Content-Type": "application/json" },
184
+ body: JSON.stringify({ done: true }),
185
+ }
186
+ );
187
+ if (!response.ok) {
188
+ const payload = await response.json().catch(() => ({}));
189
+ throw new Error(payload?.error || t("Unable to update backlog."));
190
+ }
191
+ const payload = await response.json().catch(() => ({}));
192
+ const updatedItem = normalizeBacklogItem(payload?.item);
193
+ updateBacklogMessages((items) =>
194
+ items.map((item) =>
195
+ item?.id === itemId
196
+ ? { ...item, ...updatedItem, done: true }
197
+ : item
198
+ )
199
+ );
200
+ } catch (error) {
201
+ showToast?.(error.message || t("Unable to update backlog."), "error");
202
+ }
203
+ },
204
+ [apiFetch, attachmentSessionId, showToast, t, updateBacklogMessages]
205
+ );
206
+
207
+ const addToBacklog = useCallback(() => {
208
+ const trimmed = input.trim();
209
+ if (!trimmed) {
210
+ return;
211
+ }
212
+ const entry = {
213
+ id: `backlog-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
214
+ text: trimmed,
215
+ createdAt: new Date().toISOString(),
216
+ attachments: draftAttachments,
217
+ };
218
+ setBacklog((current) => [entry, ...current]);
219
+ setInput("");
220
+ }, [draftAttachments, input, setInput]);
221
+
222
+ const removeFromBacklog = useCallback((id) => {
223
+ setBacklog((current) => current.filter((item) => item.id !== id));
224
+ }, []);
225
+
226
+ const editBacklogItem = useCallback(
227
+ (item) => {
228
+ setInput(item.text || "");
229
+ setDraftAttachments(normalizeAttachments(item.attachments || []));
230
+ inputRef?.current?.focus();
231
+ },
232
+ [inputRef, normalizeAttachments, setDraftAttachments, setInput]
233
+ );
234
+
235
+ const launchBacklogItem = useCallback(
236
+ (item) => {
237
+ sendMessage(item.text || "", item.attachments || []);
238
+ removeFromBacklog(item.id);
239
+ },
240
+ [removeFromBacklog, sendMessage]
241
+ );
242
+
243
+ return {
244
+ addToBacklog,
245
+ backlog,
246
+ markBacklogItemDone,
247
+ removeFromBacklog,
248
+ editBacklogItem,
249
+ launchBacklogItem,
250
+ setBacklog,
251
+ setBacklogMessagePage,
252
+ updateBacklogMessages,
253
+ };
254
+ }
@@ -0,0 +1,90 @@
1
+ import { useCallback } from "react";
2
+
3
+ export default function useChatClear({
4
+ activeWorktreeId,
5
+ setToolbarExportOpen,
6
+ setWorktrees,
7
+ lastNotifiedIdRef,
8
+ attachmentSessionId,
9
+ apiFetch,
10
+ setMessages,
11
+ messageIndex,
12
+ commandIndex,
13
+ setChoiceSelections,
14
+ choicesKey,
15
+ setCommandPanelOpen,
16
+ setToolResultPanelOpen,
17
+ llmProvider,
18
+ }) {
19
+ const handleClearChat = useCallback(async () => {
20
+ setToolbarExportOpen(false);
21
+ if (activeWorktreeId !== "main") {
22
+ setWorktrees((current) => {
23
+ const next = new Map(current);
24
+ const wt = next.get(activeWorktreeId);
25
+ if (wt) {
26
+ next.set(activeWorktreeId, { ...wt, messages: [] });
27
+ }
28
+ return next;
29
+ });
30
+ lastNotifiedIdRef.current = null;
31
+ if (attachmentSessionId) {
32
+ try {
33
+ await apiFetch(
34
+ `/api/v1/sessions/${encodeURIComponent(attachmentSessionId)}/clear`,
35
+ {
36
+ method: "POST",
37
+ headers: { "Content-Type": "application/json" },
38
+ body: JSON.stringify({ worktreeId: activeWorktreeId }),
39
+ }
40
+ );
41
+ } catch (error) {
42
+ // Ignore clear failures; next refresh will resync.
43
+ }
44
+ }
45
+ return;
46
+ }
47
+
48
+ setMessages([]);
49
+ messageIndex.clear();
50
+ commandIndex.clear();
51
+ setChoiceSelections({});
52
+ if (choicesKey) {
53
+ localStorage.removeItem(choicesKey);
54
+ }
55
+ setCommandPanelOpen({});
56
+ setToolResultPanelOpen({});
57
+ lastNotifiedIdRef.current = null;
58
+ if (attachmentSessionId) {
59
+ try {
60
+ await apiFetch(
61
+ `/api/v1/sessions/${encodeURIComponent(attachmentSessionId)}/clear`,
62
+ {
63
+ method: "POST",
64
+ headers: { "Content-Type": "application/json" },
65
+ body: JSON.stringify({ provider: llmProvider }),
66
+ }
67
+ );
68
+ } catch (error) {
69
+ // Ignore clear failures; next refresh will resync.
70
+ }
71
+ }
72
+ }, [
73
+ activeWorktreeId,
74
+ apiFetch,
75
+ attachmentSessionId,
76
+ choicesKey,
77
+ commandIndex,
78
+ llmProvider,
79
+ messageIndex,
80
+ setChoiceSelections,
81
+ setCommandPanelOpen,
82
+ setMessages,
83
+ setToolResultPanelOpen,
84
+ setToolbarExportOpen,
85
+ setWorktrees,
86
+ lastNotifiedIdRef,
87
+ ]);
88
+
89
+ return { handleClearChat };
90
+ }
@@ -0,0 +1,42 @@
1
+ import { useMemo, useState } from "react";
2
+
3
+ export default function useChatCollapse({
4
+ activeChatKey,
5
+ displayedGroupedMessages,
6
+ CHAT_COLLAPSE_THRESHOLD,
7
+ CHAT_COLLAPSE_VISIBLE,
8
+ }) {
9
+ const [showOlderMessagesByTab, setShowOlderMessagesByTab] = useState({});
10
+ const showOlderMessages = Boolean(showOlderMessagesByTab[activeChatKey]);
11
+ const collapsedMessages = useMemo(() => {
12
+ const total = displayedGroupedMessages.length;
13
+ const shouldCollapse = !showOlderMessages && total > CHAT_COLLAPSE_THRESHOLD;
14
+ if (!shouldCollapse) {
15
+ return {
16
+ visibleMessages: displayedGroupedMessages,
17
+ hiddenCount: 0,
18
+ isCollapsed: false,
19
+ };
20
+ }
21
+ const visibleMessages = displayedGroupedMessages.slice(
22
+ Math.max(0, total - CHAT_COLLAPSE_VISIBLE)
23
+ );
24
+ return {
25
+ visibleMessages,
26
+ hiddenCount: Math.max(0, total - visibleMessages.length),
27
+ isCollapsed: true,
28
+ };
29
+ }, [
30
+ displayedGroupedMessages,
31
+ showOlderMessages,
32
+ CHAT_COLLAPSE_THRESHOLD,
33
+ CHAT_COLLAPSE_VISIBLE,
34
+ ]);
35
+
36
+ return {
37
+ showOlderMessagesByTab,
38
+ setShowOlderMessagesByTab,
39
+ showOlderMessages,
40
+ collapsedMessages,
41
+ };
42
+ }