@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,811 @@
1
+ import React from "react";
2
+ import ReactMarkdown from "react-markdown";
3
+ import remarkGfm from "remark-gfm";
4
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5
+ import {
6
+ faCode,
7
+ faCodeBranch,
8
+ faCommentDots,
9
+ faDice,
10
+ faTowerBroadcast,
11
+ faKey,
12
+ faTriangleExclamation,
13
+ faCopy,
14
+ } from "@fortawesome/free-solid-svg-icons";
15
+
16
+ const getAnnotatableLines = (text) => {
17
+ const lines = String(text || "").split(/\r?\n/);
18
+ const results = [];
19
+ let inCodeFence = false;
20
+ for (let index = 0; index < lines.length; index += 1) {
21
+ const rawLine = lines[index];
22
+ const trimmed = rawLine.trim();
23
+ if (trimmed.startsWith("```")) {
24
+ inCodeFence = !inCodeFence;
25
+ continue;
26
+ }
27
+ if (inCodeFence || !trimmed) {
28
+ continue;
29
+ }
30
+ const isTableSeparator = /^[:\-\s|]+$/.test(trimmed) && trimmed.includes("|");
31
+ const isTableRowLike =
32
+ (trimmed.startsWith("|") && trimmed.endsWith("|")) || isTableSeparator;
33
+ if (isTableRowLike) {
34
+ continue;
35
+ }
36
+ results.push({
37
+ lineIndex: index,
38
+ lineText: rawLine,
39
+ });
40
+ }
41
+ return results;
42
+ };
43
+
44
+ export default function ChatMessages({
45
+ t,
46
+ activePane,
47
+ listRef,
48
+ showChatInfoPanel,
49
+ repoTitle,
50
+ activeBranchLabel,
51
+ shortSha,
52
+ activeCommit,
53
+ showProviderMeta,
54
+ activeProviderLabel,
55
+ activeModelLabel,
56
+ showInternetAccess,
57
+ showGitCredentialsShared,
58
+ activeTaskLabel,
59
+ currentMessages,
60
+ chatHistoryWindow,
61
+ activeChatKey,
62
+ setShowOlderMessagesByTab,
63
+ showChatCommands,
64
+ showToolResults,
65
+ commandPanelOpen,
66
+ setCommandPanelOpen,
67
+ toolResultPanelOpen,
68
+ setToolResultPanelOpen,
69
+ renderMessageAttachments,
70
+ currentProcessing,
71
+ currentInteractionBlocked,
72
+ currentActivity,
73
+ extractVibe80Blocks,
74
+ handleChoiceClick,
75
+ choiceSelections,
76
+ openVibe80Form,
77
+ copyTextToClipboard,
78
+ openFileInExplorer,
79
+ setInput,
80
+ inputRef,
81
+ markBacklogItemDone,
82
+ setBacklogMessagePage,
83
+ activeWorktreeId,
84
+ BACKLOG_PAGE_SIZE,
85
+ MAX_USER_DISPLAY_LENGTH,
86
+ getTruncatedText,
87
+ annotationMode,
88
+ scopedAnnotations,
89
+ setAnnotationDraft,
90
+ removeAnnotation,
91
+ addOrFocusAnnotation,
92
+ }) {
93
+ return (
94
+ <main className={`chat ${activePane === "chat" ? "" : "is-hidden"}`}>
95
+ <div className="chat-scroll" ref={listRef}>
96
+ <div
97
+ className={`chat-scroll-inner ${showChatInfoPanel ? "has-meta" : ""}`}
98
+ >
99
+ <div
100
+ className={`chat-history-grid ${showChatInfoPanel ? "has-meta" : ""} ${
101
+ annotationMode ? "has-annotations" : ""
102
+ }`}
103
+ >
104
+ {showChatInfoPanel && (
105
+ <div className="chat-meta-rail">
106
+ <div className="chat-meta-card">
107
+ <div className="chat-meta-section chat-meta-repo">
108
+ <div className="chat-meta-repo-title">
109
+ <span className="chat-meta-repo-name">{repoTitle}</span>
110
+ </div>
111
+ <div className="chat-meta-repo-branch-line">
112
+ <span className="chat-meta-repo-icon" aria-hidden="true">
113
+ <FontAwesomeIcon icon={faCodeBranch} />
114
+ </span>
115
+ <span className="chat-meta-repo-branch">
116
+ {activeBranchLabel}
117
+ </span>
118
+ </div>
119
+ <div className="chat-meta-repo-commit">
120
+ <span className="chat-meta-hash">{shortSha}</span>
121
+ <span className="chat-meta-message">
122
+ {activeCommit?.message || ""}
123
+ </span>
124
+ </div>
125
+ </div>
126
+
127
+ {showProviderMeta && (
128
+ <div className="chat-meta-section chat-meta-provider">
129
+ <span className="chat-meta-provider-icon" aria-hidden="true">
130
+ <FontAwesomeIcon icon={faDice} />
131
+ </span>
132
+ <span className="chat-meta-provider-label">
133
+ {activeProviderLabel}
134
+ </span>
135
+ <span className="chat-meta-provider-sep">•</span>
136
+ <span className="chat-meta-provider-model">
137
+ {activeModelLabel}
138
+ </span>
139
+ </div>
140
+ )}
141
+
142
+ {(showInternetAccess ||
143
+ showGitCredentialsShared ||
144
+ activeTaskLabel) && (
145
+ <div className="chat-meta-section chat-meta-permissions">
146
+ {showInternetAccess && (
147
+ <div className="chat-meta-permission">
148
+ <span
149
+ className="chat-meta-permission-icon is-internet"
150
+ aria-hidden="true"
151
+ >
152
+ <FontAwesomeIcon icon={faTowerBroadcast} />
153
+ </span>
154
+ <span>{t("Internet access enabled")}</span>
155
+ </div>
156
+ )}
157
+ {showGitCredentialsShared && (
158
+ <div className="chat-meta-permission">
159
+ <span
160
+ className="chat-meta-permission-icon is-credentials"
161
+ aria-hidden="true"
162
+ >
163
+ <FontAwesomeIcon icon={faKey} />
164
+ </span>
165
+ <span>{t("Git credentials shared")}</span>
166
+ </div>
167
+ )}
168
+ {activeTaskLabel && (
169
+ <span className="chat-meta-task">
170
+ <span
171
+ className="chat-meta-task-loader"
172
+ aria-hidden="true"
173
+ />
174
+ <ReactMarkdown
175
+ className="chat-meta-task-text"
176
+ remarkPlugins={[remarkGfm]}
177
+ components={{
178
+ p: ({ children }) => <span>{children}</span>,
179
+ }}
180
+ >
181
+ {activeTaskLabel}
182
+ </ReactMarkdown>
183
+ </span>
184
+ )}
185
+ </div>
186
+ )}
187
+ </div>
188
+ </div>
189
+ )}
190
+ <div className="chat-history">
191
+ {currentMessages.length === 0 && (
192
+ <div className="empty">
193
+ <p>{t("Send a message to start a session.")}</p>
194
+ </div>
195
+ )}
196
+ {chatHistoryWindow.hiddenCount > 0 && (
197
+ <button
198
+ type="button"
199
+ className="chat-history-reveal"
200
+ onClick={() =>
201
+ setShowOlderMessagesByTab((current) => ({
202
+ ...current,
203
+ [activeChatKey]: true,
204
+ }))
205
+ }
206
+ >
207
+ {t("View previous messages ({{count}})", {
208
+ count: chatHistoryWindow.hiddenCount,
209
+ })}
210
+ </button>
211
+ )}
212
+ {chatHistoryWindow.visibleMessages.map((message) => {
213
+ if (message?.groupType === "commandExecution") {
214
+ return (
215
+ <div key={message.id} className="bubble command-execution">
216
+ {message.items.map((item) => {
217
+ const commandTitle = t("Command: {{command}}", {
218
+ command: item.command || t("Command"),
219
+ });
220
+ const showLoader = item.status !== "completed";
221
+ const isExpandable =
222
+ item.isExpandable || Boolean(item.output);
223
+ const summaryContent = (
224
+ <>
225
+ {showLoader && (
226
+ <span
227
+ className="loader command-execution-loader"
228
+ title={t("Execution in progress")}
229
+ >
230
+ <span className="dot" />
231
+ <span className="dot" />
232
+ <span className="dot" />
233
+ </span>
234
+ )}
235
+ <span className="command-execution-title">
236
+ {commandTitle}
237
+ </span>
238
+ </>
239
+ );
240
+ return (
241
+ <div key={item.id}>
242
+ {isExpandable ? (
243
+ <details
244
+ className="command-execution-panel"
245
+ open={Boolean(commandPanelOpen[item.id])}
246
+ onToggle={(event) => {
247
+ const isOpen = event.currentTarget.open;
248
+ setCommandPanelOpen((prev) => ({
249
+ ...prev,
250
+ [item.id]: isOpen,
251
+ }));
252
+ }}
253
+ >
254
+ <summary className="command-execution-summary">
255
+ {summaryContent}
256
+ </summary>
257
+ <pre className="command-execution-output">
258
+ {item.output || ""}
259
+ </pre>
260
+ </details>
261
+ ) : (
262
+ <div className="command-execution-summary is-static">
263
+ {summaryContent}
264
+ </div>
265
+ )}
266
+ </div>
267
+ );
268
+ })}
269
+ </div>
270
+ );
271
+ }
272
+ if (message?.groupType === "toolResult") {
273
+ return (
274
+ <div key={message.id} className="bubble command-execution">
275
+ {message.items.map((item) => {
276
+ const isActionResult = item?.type === "action_result";
277
+ const actionRequest = item?.action?.request || "";
278
+ const actionArg = item?.action?.arg || "";
279
+ const commandLabel = `${actionRequest}${actionArg ? ` ${actionArg}` : ""}`.trim();
280
+ const toolTitle = isActionResult
281
+ ? t("User command : {{command}}", {
282
+ command: commandLabel || t("Command"),
283
+ })
284
+ : t("Tool: {{tool}}", {
285
+ tool:
286
+ item.toolResult?.name ||
287
+ item.toolResult?.tool ||
288
+ "Tool",
289
+ });
290
+ const output = isActionResult
291
+ ? item?.action?.output || item.text || ""
292
+ : item.toolResult?.output || item.text || "";
293
+ const isExpandable = Boolean(output);
294
+ const summaryContent = (
295
+ <span className="command-execution-title">
296
+ <span
297
+ className="command-execution-tool-icon"
298
+ aria-hidden="true"
299
+ >
300
+ <FontAwesomeIcon icon={faCode} />
301
+ </span>
302
+ <span>{toolTitle}</span>
303
+ </span>
304
+ );
305
+ const panelKey = `tool-${item.id}`;
306
+ const isPanelOpen = isActionResult
307
+ ? toolResultPanelOpen[panelKey] !== false
308
+ : Boolean(toolResultPanelOpen[panelKey]);
309
+ return (
310
+ <div key={item.id}>
311
+ {isExpandable ? (
312
+ <details
313
+ className="command-execution-panel"
314
+ open={isPanelOpen}
315
+ onToggle={(event) => {
316
+ const isOpen = event.currentTarget.open;
317
+ setToolResultPanelOpen((prev) => ({
318
+ ...prev,
319
+ [panelKey]: isOpen,
320
+ }));
321
+ }}
322
+ >
323
+ <summary className="command-execution-summary">
324
+ {summaryContent}
325
+ </summary>
326
+ <pre className="command-execution-output">
327
+ {output}
328
+ </pre>
329
+ </details>
330
+ ) : (
331
+ <div className="command-execution-summary is-static">
332
+ {summaryContent}
333
+ </div>
334
+ )}
335
+ </div>
336
+ );
337
+ })}
338
+ </div>
339
+ );
340
+ }
341
+ if (message?.type === "backlog_view") {
342
+ const backlogItems = Array.isArray(message.backlog?.items)
343
+ ? message.backlog.items
344
+ : [];
345
+ const pendingItems = backlogItems.filter((item) => !item?.done);
346
+ const totalPages = Math.max(
347
+ 1,
348
+ Math.ceil(pendingItems.length / BACKLOG_PAGE_SIZE)
349
+ );
350
+ const requestedPage = Number.isFinite(message.backlog?.page)
351
+ ? message.backlog.page
352
+ : 0;
353
+ const currentPage = Math.min(
354
+ Math.max(0, requestedPage),
355
+ totalPages - 1
356
+ );
357
+ const startIndex = currentPage * BACKLOG_PAGE_SIZE;
358
+ const pageItems = pendingItems.slice(
359
+ startIndex,
360
+ startIndex + BACKLOG_PAGE_SIZE
361
+ );
362
+ const backlogScopeId =
363
+ activeWorktreeId && activeWorktreeId !== "main"
364
+ ? activeWorktreeId
365
+ : "main";
366
+ return (
367
+ <div key={message.id} className="bubble backlog">
368
+ <details
369
+ className="command-execution-panel backlog-panel"
370
+ open
371
+ >
372
+ <summary className="command-execution-summary">
373
+ <span className="command-execution-title">
374
+ {t("Backlog")}
375
+ </span>
376
+ </summary>
377
+ <div className="backlog-view">
378
+ {pageItems.length === 0 ? (
379
+ <div className="backlog-empty">
380
+ {t("No pending tasks at the moment.")}
381
+ </div>
382
+ ) : (
383
+ <div className="backlog-list">
384
+ {pageItems.map((item) => (
385
+ <div key={item.id} className="backlog-row">
386
+ <input
387
+ type="checkbox"
388
+ className="backlog-checkbox"
389
+ onChange={() => markBacklogItemDone(item.id)}
390
+ />
391
+ <button
392
+ type="button"
393
+ className="backlog-text"
394
+ title={item.text}
395
+ onClick={() => {
396
+ setInput(item.text || "");
397
+ inputRef.current?.focus();
398
+ }}
399
+ >
400
+ {item.text}
401
+ </button>
402
+ </div>
403
+ ))}
404
+ </div>
405
+ )}
406
+ {totalPages > 1 ? (
407
+ <div className="backlog-pagination">
408
+ <button
409
+ type="button"
410
+ className="backlog-page-button"
411
+ disabled={currentPage === 0}
412
+ onClick={() =>
413
+ setBacklogMessagePage(
414
+ backlogScopeId,
415
+ message.id,
416
+ Math.max(0, currentPage - 1)
417
+ )
418
+ }
419
+ >
420
+ {t("Previous")}
421
+ </button>
422
+ <span className="backlog-page-status">
423
+ {currentPage + 1} / {totalPages}
424
+ </span>
425
+ <button
426
+ type="button"
427
+ className="backlog-page-button"
428
+ disabled={currentPage >= totalPages - 1}
429
+ onClick={() =>
430
+ setBacklogMessagePage(
431
+ backlogScopeId,
432
+ message.id,
433
+ Math.min(totalPages - 1, currentPage + 1)
434
+ )
435
+ }
436
+ >
437
+ {t("Next")}
438
+ </button>
439
+ </div>
440
+ ) : null}
441
+ </div>
442
+ </details>
443
+ </div>
444
+ );
445
+ }
446
+ const isLongUserMessage =
447
+ message.role === "user" &&
448
+ (message.text || "").length > MAX_USER_DISPLAY_LENGTH;
449
+ if (isLongUserMessage) {
450
+ const truncatedText = getTruncatedText(
451
+ message.text,
452
+ MAX_USER_DISPLAY_LENGTH
453
+ );
454
+ return (
455
+ <div key={message.id} className={`bubble ${message.role}`}>
456
+ <div className="plain-text">{truncatedText}</div>
457
+ {renderMessageAttachments(message.attachments)}
458
+ </div>
459
+ );
460
+ }
461
+
462
+ return (
463
+ <div key={message.id} className={`bubble ${message.role}`}>
464
+ {(() => {
465
+ const rawText = message.text || "";
466
+ const isWarning = rawText.startsWith("⚠️");
467
+ const warningText = rawText.replace(/^⚠️\s*/, "");
468
+ const { cleanedText, blocks, filerefs } =
469
+ extractVibe80Blocks(
470
+ isWarning ? warningText : rawText,
471
+ t
472
+ );
473
+ const showAnnotationSource =
474
+ annotationMode && message.role === "assistant";
475
+ const content = (
476
+ <ReactMarkdown
477
+ remarkPlugins={[remarkGfm]}
478
+ components={{
479
+ a: ({ node, ...props }) => (
480
+ <a {...props} target="_blank" rel="noopener noreferrer" />
481
+ ),
482
+ code: ({
483
+ node,
484
+ inline,
485
+ className,
486
+ children,
487
+ ...props
488
+ }) => {
489
+ const raw = Array.isArray(children)
490
+ ? children.join("")
491
+ : String(children);
492
+ const text = raw.replace(/\n$/, "");
493
+ if (!inline) {
494
+ return (
495
+ <code className={className} {...props}>
496
+ {children}
497
+ </code>
498
+ );
499
+ }
500
+ const trimmed = text.trim();
501
+ const isRelativePath =
502
+ Boolean(trimmed) &&
503
+ !trimmed.startsWith("/") &&
504
+ !trimmed.startsWith("~") &&
505
+ !/^[a-zA-Z]+:\/\//.test(trimmed) &&
506
+ !trimmed.includes("\\") &&
507
+ !trimmed.includes(" ") &&
508
+ (trimmed.startsWith("./") ||
509
+ trimmed.startsWith("../") ||
510
+ trimmed.includes("/") ||
511
+ /^[\w.-]+$/.test(trimmed));
512
+ return (
513
+ <span
514
+ className={`inline-code${
515
+ isRelativePath ? " inline-code--link" : ""
516
+ }`}
517
+ >
518
+ {isRelativePath ? (
519
+ <button
520
+ type="button"
521
+ className="inline-code-link"
522
+ onClick={(event) => {
523
+ event.preventDefault();
524
+ event.stopPropagation();
525
+ setInput(`/open ${trimmed}`);
526
+ inputRef.current?.focus();
527
+ }}
528
+ >
529
+ <code className={className} {...props}>
530
+ {text}
531
+ </code>
532
+ </button>
533
+ ) : (
534
+ <code className={className} {...props}>
535
+ {text}
536
+ </code>
537
+ )}
538
+ <button
539
+ type="button"
540
+ className="code-copy"
541
+ aria-label={t("Copy code")}
542
+ title={t("Copy")}
543
+ onClick={(event) => {
544
+ event.preventDefault();
545
+ event.stopPropagation();
546
+ copyTextToClipboard(text);
547
+ }}
548
+ >
549
+ <FontAwesomeIcon icon={faCopy} />
550
+ </button>
551
+ </span>
552
+ );
553
+ },
554
+ }}
555
+ >
556
+ {cleanedText}
557
+ </ReactMarkdown>
558
+ );
559
+ return (
560
+ <>
561
+ {showAnnotationSource ? (
562
+ <div className="annotation-line-source-list">
563
+ {getAnnotatableLines(cleanedText).map((entry) => (
564
+ <div
565
+ key={`${message.id}-${entry.lineIndex}`}
566
+ className="annotation-line-source-row"
567
+ >
568
+ <div className="annotation-line-source-text">
569
+ <ReactMarkdown
570
+ remarkPlugins={[remarkGfm]}
571
+ components={{
572
+ p: ({ children }) => <span>{children}</span>,
573
+ a: ({ node, ...props }) => (
574
+ <a
575
+ {...props}
576
+ target="_blank"
577
+ rel="noopener noreferrer"
578
+ />
579
+ ),
580
+ }}
581
+ >
582
+ {entry.lineText}
583
+ </ReactMarkdown>
584
+ </div>
585
+ <button
586
+ type="button"
587
+ className="annotation-line-source-button"
588
+ aria-label={t("Annotate line")}
589
+ title={t("Annotate line")}
590
+ onClick={() =>
591
+ addOrFocusAnnotation({
592
+ messageId: message.id,
593
+ lineIndex: entry.lineIndex,
594
+ lineText: entry.lineText,
595
+ })
596
+ }
597
+ >
598
+ <FontAwesomeIcon icon={faCommentDots} />
599
+ </button>
600
+ </div>
601
+ ))}
602
+ </div>
603
+ ) : isWarning ? (
604
+ <div className="warning-message">
605
+ <span className="warning-icon" aria-hidden="true">
606
+ <FontAwesomeIcon icon={faTriangleExclamation} />
607
+ </span>
608
+ <div className="warning-body">{content}</div>
609
+ </div>
610
+ ) : (
611
+ content
612
+ )}
613
+ {filerefs.length ? (
614
+ <ul className="fileref-list">
615
+ {filerefs.map((pathRef) => (
616
+ <li
617
+ key={`${message.id}-fileref-${pathRef}`}
618
+ className="fileref-item"
619
+ >
620
+ <button
621
+ type="button"
622
+ className="fileref-link"
623
+ onClick={(event) => {
624
+ event.preventDefault();
625
+ event.stopPropagation();
626
+ openFileInExplorer(pathRef);
627
+ }}
628
+ >
629
+ {pathRef}
630
+ </button>
631
+ </li>
632
+ ))}
633
+ </ul>
634
+ ) : null}
635
+ {blocks.map((block, index) => {
636
+ const blockKey = `${message.id}-${index}`;
637
+ if (block.type === "form") {
638
+ return (
639
+ <div className="vibe80-form" key={blockKey}>
640
+ <button
641
+ type="button"
642
+ className="vibe80-form-button"
643
+ onClick={() => openVibe80Form(block, blockKey)}
644
+ >
645
+ {block.question || t("Open form")}
646
+ </button>
647
+ </div>
648
+ );
649
+ }
650
+
651
+ const selectedIndex = choiceSelections[blockKey];
652
+ const choicesWithIndex = block.choices.map(
653
+ (choice, choiceIndex) => ({
654
+ choice,
655
+ choiceIndex,
656
+ })
657
+ );
658
+ const orderedChoices =
659
+ selectedIndex === undefined
660
+ ? choicesWithIndex
661
+ : [
662
+ choicesWithIndex.find(
663
+ ({ choiceIndex }) =>
664
+ choiceIndex === selectedIndex
665
+ ),
666
+ ...choicesWithIndex.filter(
667
+ ({ choiceIndex }) =>
668
+ choiceIndex !== selectedIndex
669
+ ),
670
+ ].filter(Boolean);
671
+
672
+ const isInline = block.type === "yesno";
673
+ return (
674
+ <div
675
+ className={`choices ${isInline ? "is-inline" : ""}`}
676
+ key={blockKey}
677
+ >
678
+ {block.question && (
679
+ <div className="choices-question">
680
+ {block.question}
681
+ </div>
682
+ )}
683
+ <div
684
+ className={`choices-list ${
685
+ selectedIndex !== undefined ? "is-selected" : ""
686
+ } ${isInline ? "is-inline" : ""}`}
687
+ >
688
+ {orderedChoices.map(
689
+ ({ choice, choiceIndex }) => {
690
+ const isSelected =
691
+ selectedIndex === choiceIndex;
692
+ return (
693
+ <button
694
+ type="button"
695
+ key={`${blockKey}-${choiceIndex}`}
696
+ onClick={() =>
697
+ handleChoiceClick(
698
+ choice,
699
+ blockKey,
700
+ choiceIndex
701
+ )
702
+ }
703
+ disabled={currentInteractionBlocked}
704
+ className={`choice-button ${
705
+ isSelected
706
+ ? "is-selected"
707
+ : selectedIndex !== undefined
708
+ ? "is-muted"
709
+ : ""
710
+ }`}
711
+ >
712
+ <ReactMarkdown
713
+ remarkPlugins={[remarkGfm]}
714
+ components={{
715
+ p: ({ node, ...props }) => (
716
+ <span {...props} />
717
+ ),
718
+ a: ({ node, ...props }) => (
719
+ <a
720
+ {...props}
721
+ target="_blank"
722
+ rel="noopener noreferrer"
723
+ />
724
+ ),
725
+ }}
726
+ >
727
+ {choice}
728
+ </ReactMarkdown>
729
+ </button>
730
+ );
731
+ }
732
+ )}
733
+ </div>
734
+ </div>
735
+ );
736
+ })}
737
+ {renderMessageAttachments(message.attachments)}
738
+ </>
739
+ );
740
+ })()}
741
+ </div>
742
+ );
743
+ })}
744
+ </div>
745
+ {annotationMode ? (
746
+ <div className="chat-annotation-rail">
747
+ <div className="chat-annotation-card">
748
+ <div className="chat-annotation-title">{t("Annotations")}</div>
749
+ <div className="chat-annotation-subtitle">
750
+ {t("Only sent with the next message.")}
751
+ </div>
752
+ {scopedAnnotations.length === 0 ? (
753
+ <div className="chat-annotation-empty">
754
+ {t("No annotations yet.")}
755
+ </div>
756
+ ) : (
757
+ <div className="chat-annotation-list">
758
+ {scopedAnnotations.map((annotation) => (
759
+ <div
760
+ className="chat-annotation-item"
761
+ key={annotation.annotationKey}
762
+ >
763
+ <div className="chat-annotation-quote">
764
+ &gt; {annotation.lineText}
765
+ </div>
766
+ <textarea
767
+ className="chat-annotation-input"
768
+ value={annotation.annotationText}
769
+ placeholder={t("Write annotation...")}
770
+ rows={3}
771
+ onChange={(event) =>
772
+ setAnnotationDraft(
773
+ annotation.annotationKey,
774
+ event.target.value
775
+ )
776
+ }
777
+ />
778
+ <button
779
+ type="button"
780
+ className="chat-annotation-remove"
781
+ onClick={() => removeAnnotation(annotation.annotationKey)}
782
+ >
783
+ {t("Delete")}
784
+ </button>
785
+ </div>
786
+ ))}
787
+ </div>
788
+ )}
789
+ </div>
790
+ </div>
791
+ ) : null}
792
+ </div>
793
+ </div>
794
+ </div>
795
+ {currentProcessing && (
796
+ <div className="bubble assistant typing">
797
+ <div className="typing-indicator">
798
+ <div className="loader" title={currentActivity || t("Processing...")}>
799
+ <span className="dot" />
800
+ <span className="dot" />
801
+ <span className="dot" />
802
+ </div>
803
+ <span className="typing-text">
804
+ {currentActivity || t("Processing...")}
805
+ </span>
806
+ </div>
807
+ </div>
808
+ )}
809
+ </main>
810
+ );
811
+ }