@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,449 @@
1
+ import React, { useEffect, useMemo, useState } from "react";
2
+ import Editor from "@monaco-editor/react";
3
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4
+ import {
5
+ faArrowsRotate,
6
+ faFileCirclePlus,
7
+ faFolderPlus,
8
+ faPenToSquare,
9
+ faTrashCan,
10
+ } from "@fortawesome/free-solid-svg-icons";
11
+
12
+ export default function ExplorerPanel({
13
+ t,
14
+ activePane,
15
+ repoName,
16
+ activeWorktree,
17
+ isInWorktree,
18
+ activeWorktreeId,
19
+ attachmentSession,
20
+ requestExplorerTree,
21
+ requestExplorerStatus,
22
+ activeExplorer,
23
+ renderExplorerNodes,
24
+ explorerStatusByPath,
25
+ explorerDirStatus,
26
+ saveExplorerFile,
27
+ updateExplorerDraft,
28
+ setActiveExplorerFile,
29
+ closeExplorerFile,
30
+ startExplorerRename,
31
+ createExplorerFile,
32
+ createExplorerFolder,
33
+ deleteExplorerSelection,
34
+ getLanguageForPath,
35
+ themeMode,
36
+ }) {
37
+ const tabId = activeWorktreeId || "main";
38
+ const openTabPaths = Array.isArray(activeExplorer.openTabPaths)
39
+ ? activeExplorer.openTabPaths
40
+ : [];
41
+ const activeFilePath = activeExplorer.activeFilePath || "";
42
+ const activeFile = activeFilePath
43
+ ? activeExplorer.filesByPath?.[activeFilePath]
44
+ : null;
45
+ const [newFileDialogOpen, setNewFileDialogOpen] = useState(false);
46
+ const [newFileName, setNewFileName] = useState("");
47
+ const [newFileSubmitting, setNewFileSubmitting] = useState(false);
48
+ const [newFolderDialogOpen, setNewFolderDialogOpen] = useState(false);
49
+ const [newFolderName, setNewFolderName] = useState("");
50
+ const [newFolderSubmitting, setNewFolderSubmitting] = useState(false);
51
+
52
+ const selectedPath = activeExplorer.selectedPath || "";
53
+ const canRename = Boolean(selectedPath);
54
+ const canDelete = Boolean(selectedPath);
55
+
56
+ useEffect(() => {
57
+ if (activePane !== "explorer") {
58
+ return undefined;
59
+ }
60
+ const onKeyDown = (event) => {
61
+ if (!(event.ctrlKey || event.metaKey)) {
62
+ return;
63
+ }
64
+ if (event.key.toLowerCase() !== "s") {
65
+ return;
66
+ }
67
+ if (!activeFilePath || activeFile?.binary || activeFile?.saving) {
68
+ return;
69
+ }
70
+ event.preventDefault();
71
+ saveExplorerFile(tabId, activeFilePath);
72
+ };
73
+ window.addEventListener("keydown", onKeyDown);
74
+ return () => {
75
+ window.removeEventListener("keydown", onKeyDown);
76
+ };
77
+ }, [
78
+ activePane,
79
+ activeFilePath,
80
+ activeFile?.binary,
81
+ activeFile?.saving,
82
+ saveExplorerFile,
83
+ tabId,
84
+ ]);
85
+
86
+ const getFileLabel = (path) => {
87
+ if (!path) {
88
+ return "";
89
+ }
90
+ const parts = path.split("/");
91
+ return parts[parts.length - 1] || path;
92
+ };
93
+
94
+ const subtitle = useMemo(() => {
95
+ if (isInWorktree) {
96
+ return activeWorktree?.branchName || activeWorktree?.name || "";
97
+ }
98
+ return repoName || "";
99
+ }, [isInWorktree, activeWorktree?.branchName, activeWorktree?.name, repoName]);
100
+
101
+ return (
102
+ <div
103
+ className={`explorer-panel ${activePane === "explorer" ? "" : "is-hidden"}`}
104
+ >
105
+ <div className="explorer-header">
106
+ <div>
107
+ <div className="explorer-title">{t("Explorer")}</div>
108
+ {subtitle ? <div className="explorer-subtitle">{subtitle}</div> : null}
109
+ </div>
110
+ </div>
111
+ <div className="explorer-body">
112
+ <div className="explorer-tree-wrap">
113
+ <div className="explorer-tree-header">
114
+ <button
115
+ type="button"
116
+ className="explorer-tree-icon-btn"
117
+ title={t("New file")}
118
+ aria-label={t("New file")}
119
+ onClick={() => {
120
+ setNewFileName("");
121
+ setNewFileDialogOpen(true);
122
+ }}
123
+ disabled={!attachmentSession?.sessionId}
124
+ >
125
+ <FontAwesomeIcon icon={faFileCirclePlus} />
126
+ </button>
127
+ <button
128
+ type="button"
129
+ className="explorer-tree-icon-btn"
130
+ title={t("New folder")}
131
+ aria-label={t("New folder")}
132
+ onClick={() => {
133
+ setNewFolderName("");
134
+ setNewFolderDialogOpen(true);
135
+ }}
136
+ disabled={!attachmentSession?.sessionId}
137
+ >
138
+ <FontAwesomeIcon icon={faFolderPlus} />
139
+ </button>
140
+ <button
141
+ type="button"
142
+ className="explorer-tree-icon-btn"
143
+ title={t("Rename")}
144
+ aria-label={t("Rename")}
145
+ onClick={() => startExplorerRename(tabId)}
146
+ disabled={!attachmentSession?.sessionId || !canRename}
147
+ >
148
+ <FontAwesomeIcon icon={faPenToSquare} />
149
+ </button>
150
+ <button
151
+ type="button"
152
+ className="explorer-tree-icon-btn"
153
+ title={t("Delete")}
154
+ aria-label={t("Delete")}
155
+ onClick={() => {
156
+ void deleteExplorerSelection(tabId);
157
+ }}
158
+ disabled={!attachmentSession?.sessionId || !canDelete || Boolean(activeExplorer.deletingPath)}
159
+ >
160
+ <FontAwesomeIcon icon={faTrashCan} />
161
+ </button>
162
+ <button
163
+ type="button"
164
+ className="explorer-tree-icon-btn"
165
+ title={t("Refresh")}
166
+ aria-label={t("Refresh")}
167
+ onClick={() => {
168
+ requestExplorerTree(tabId, true);
169
+ requestExplorerStatus(tabId, true);
170
+ }}
171
+ disabled={!attachmentSession?.sessionId}
172
+ >
173
+ <FontAwesomeIcon icon={faArrowsRotate} />
174
+ </button>
175
+ </div>
176
+
177
+ <div className="explorer-tree">
178
+ {activeExplorer.loading ? (
179
+ <div className="explorer-empty">{t("Loading...")}</div>
180
+ ) : activeExplorer.error ? (
181
+ <div className="explorer-empty">{activeExplorer.error}</div>
182
+ ) : Array.isArray(activeExplorer.tree) &&
183
+ activeExplorer.tree.length > 0 ? (
184
+ <>
185
+ {renderExplorerNodes(
186
+ activeExplorer.tree,
187
+ tabId,
188
+ new Set(activeExplorer.expandedPaths || []),
189
+ activeExplorer.selectedPath || activeFilePath,
190
+ activeExplorer.selectedType || null,
191
+ activeExplorer.renamingPath || null,
192
+ activeExplorer.renameDraft || "",
193
+ explorerStatusByPath,
194
+ explorerDirStatus
195
+ )}
196
+ {activeExplorer.treeTruncated && (
197
+ <div className="explorer-truncated">
198
+ {t("List truncated after {{count}} entries.", {
199
+ count: activeExplorer.treeTotal,
200
+ })}
201
+ </div>
202
+ )}
203
+ </>
204
+ ) : (
205
+ <div className="explorer-empty">{t("No file found.")}</div>
206
+ )}
207
+ </div>
208
+ </div>
209
+
210
+ <div className="explorer-editor">
211
+ <div className="explorer-editor-tabs">
212
+ <div className="explorer-editor-tabs-list">
213
+ {openTabPaths.map((path) => {
214
+ const file = activeExplorer.filesByPath?.[path];
215
+ const isActive = path === activeFilePath;
216
+ return (
217
+ <div
218
+ key={path}
219
+ className={`explorer-editor-tab ${isActive ? "is-active" : ""}`}
220
+ >
221
+ <button
222
+ type="button"
223
+ className="explorer-editor-tab-open"
224
+ onClick={() => setActiveExplorerFile(tabId, path)}
225
+ >
226
+ {getFileLabel(path)}
227
+ {file?.isDirty ? " *" : ""}
228
+ </button>
229
+ <button
230
+ type="button"
231
+ className="explorer-editor-tab-close"
232
+ onClick={(event) => {
233
+ event.stopPropagation();
234
+ closeExplorerFile(tabId, path);
235
+ }}
236
+ aria-label={t("Close")}
237
+ >
238
+ ×
239
+ </button>
240
+ </div>
241
+ );
242
+ })}
243
+ </div>
244
+ <div className="explorer-editor-tabs-actions">
245
+ {activeFilePath && !activeFile?.binary && (
246
+ <button
247
+ type="button"
248
+ className="explorer-action primary"
249
+ onClick={() => saveExplorerFile(tabId, activeFilePath)}
250
+ disabled={activeFile?.saving || !activeFile?.isDirty}
251
+ >
252
+ {activeFile?.saving ? t("Saving...") : t("Save")}
253
+ </button>
254
+ )}
255
+ </div>
256
+ </div>
257
+ {activeFile?.loading ? (
258
+ <div className="explorer-editor-empty">{t("Loading...")}</div>
259
+ ) : activeFile?.error ? (
260
+ <div className="explorer-editor-empty">{activeFile.error}</div>
261
+ ) : activeFile?.binary ? (
262
+ <div className="explorer-editor-empty">
263
+ {t("Binary file not displayed.")}
264
+ </div>
265
+ ) : activeFilePath ? (
266
+ <>
267
+ <div className="explorer-editor-input">
268
+ <Editor
269
+ path={activeFilePath}
270
+ defaultValue={activeFile?.draftContent || ""}
271
+ onChange={(value) =>
272
+ updateExplorerDraft(tabId, activeFilePath, value || "")
273
+ }
274
+ language={getLanguageForPath(activeFilePath)}
275
+ theme={themeMode === "dark" ? "vs-dark" : "light"}
276
+ options={{
277
+ minimap: { enabled: false },
278
+ fontSize: 12,
279
+ lineHeight: 18,
280
+ fontFamily:
281
+ '"Space Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
282
+ scrollBeyondLastLine: false,
283
+ automaticLayout: true,
284
+ wordWrap: "off",
285
+ readOnly: false,
286
+ }}
287
+ />
288
+ </div>
289
+ {activeFile?.saveError && (
290
+ <div className="explorer-truncated">{activeFile.saveError}</div>
291
+ )}
292
+ {activeFile?.truncated && (
293
+ <div className="explorer-truncated">
294
+ {t("File truncated for display.")}
295
+ </div>
296
+ )}
297
+ </>
298
+ ) : (
299
+ <div className="explorer-editor-empty">
300
+ {t("Select a file in the tree.")}
301
+ </div>
302
+ )}
303
+ </div>
304
+ </div>
305
+
306
+ {newFileDialogOpen && (
307
+ <div
308
+ className="explorer-file-dialog-overlay"
309
+ onClick={() => {
310
+ if (!newFileSubmitting) {
311
+ setNewFileDialogOpen(false);
312
+ }
313
+ }}
314
+ >
315
+ <div
316
+ className="explorer-file-dialog"
317
+ onClick={(event) => event.stopPropagation()}
318
+ >
319
+ <h3>{t("New file")}</h3>
320
+ <div className="explorer-file-dialog-field">
321
+ <label htmlFor="explorer-new-file-input">{t("File path")}</label>
322
+ <input
323
+ id="explorer-new-file-input"
324
+ type="text"
325
+ value={newFileName}
326
+ onChange={(event) => setNewFileName(event.target.value)}
327
+ placeholder={t("e.g. src/new-file.ts")}
328
+ autoFocus
329
+ onKeyDown={(event) => {
330
+ if (event.key === "Enter" && !newFileSubmitting) {
331
+ event.preventDefault();
332
+ setNewFileSubmitting(true);
333
+ createExplorerFile(tabId, newFileName)
334
+ .then((ok) => {
335
+ if (ok) {
336
+ setNewFileDialogOpen(false);
337
+ setNewFileName("");
338
+ }
339
+ })
340
+ .finally(() => setNewFileSubmitting(false));
341
+ }
342
+ }}
343
+ />
344
+ </div>
345
+ <div className="explorer-file-dialog-actions">
346
+ <button
347
+ type="button"
348
+ className="session-button secondary"
349
+ onClick={() => setNewFileDialogOpen(false)}
350
+ disabled={newFileSubmitting}
351
+ >
352
+ {t("Cancel")}
353
+ </button>
354
+ <button
355
+ type="button"
356
+ className="session-button primary"
357
+ disabled={newFileSubmitting || !newFileName.trim()}
358
+ onClick={() => {
359
+ setNewFileSubmitting(true);
360
+ createExplorerFile(tabId, newFileName)
361
+ .then((ok) => {
362
+ if (ok) {
363
+ setNewFileDialogOpen(false);
364
+ setNewFileName("");
365
+ }
366
+ })
367
+ .finally(() => setNewFileSubmitting(false));
368
+ }}
369
+ >
370
+ {newFileSubmitting ? t("Creating") : t("Create")}
371
+ </button>
372
+ </div>
373
+ </div>
374
+ </div>
375
+ )}
376
+
377
+ {newFolderDialogOpen && (
378
+ <div
379
+ className="explorer-file-dialog-overlay"
380
+ onClick={() => {
381
+ if (!newFolderSubmitting) {
382
+ setNewFolderDialogOpen(false);
383
+ }
384
+ }}
385
+ >
386
+ <div
387
+ className="explorer-file-dialog"
388
+ onClick={(event) => event.stopPropagation()}
389
+ >
390
+ <h3>{t("New folder")}</h3>
391
+ <div className="explorer-file-dialog-field">
392
+ <label htmlFor="explorer-new-folder-input">{t("Folder path")}</label>
393
+ <input
394
+ id="explorer-new-folder-input"
395
+ type="text"
396
+ value={newFolderName}
397
+ onChange={(event) => setNewFolderName(event.target.value)}
398
+ placeholder={t("e.g. src/new-folder")}
399
+ autoFocus
400
+ onKeyDown={(event) => {
401
+ if (event.key === "Enter" && !newFolderSubmitting) {
402
+ event.preventDefault();
403
+ setNewFolderSubmitting(true);
404
+ createExplorerFolder(tabId, newFolderName)
405
+ .then((ok) => {
406
+ if (ok) {
407
+ setNewFolderDialogOpen(false);
408
+ setNewFolderName("");
409
+ }
410
+ })
411
+ .finally(() => setNewFolderSubmitting(false));
412
+ }
413
+ }}
414
+ />
415
+ </div>
416
+ <div className="explorer-file-dialog-actions">
417
+ <button
418
+ type="button"
419
+ className="session-button secondary"
420
+ onClick={() => setNewFolderDialogOpen(false)}
421
+ disabled={newFolderSubmitting}
422
+ >
423
+ {t("Cancel")}
424
+ </button>
425
+ <button
426
+ type="button"
427
+ className="session-button primary"
428
+ disabled={newFolderSubmitting || !newFolderName.trim()}
429
+ onClick={() => {
430
+ setNewFolderSubmitting(true);
431
+ createExplorerFolder(tabId, newFolderName)
432
+ .then((ok) => {
433
+ if (ok) {
434
+ setNewFolderDialogOpen(false);
435
+ setNewFolderName("");
436
+ }
437
+ })
438
+ .finally(() => setNewFolderSubmitting(false));
439
+ }}
440
+ >
441
+ {newFolderSubmitting ? t("Creating") : t("Create")}
442
+ </button>
443
+ </div>
444
+ </div>
445
+ </div>
446
+ )}
447
+ </div>
448
+ );
449
+ }
@@ -0,0 +1,80 @@
1
+ import React from "react";
2
+
3
+ export default function LogsPanel({
4
+ t,
5
+ activePane,
6
+ filteredRpcLogs,
7
+ logFilter,
8
+ setLogFilter,
9
+ scopedRpcLogs,
10
+ handleClearRpcLogs,
11
+ }) {
12
+ return (
13
+ <div className={`logs-panel ${activePane === "logs" ? "" : "is-hidden"}`}>
14
+ <div className="logs-header">
15
+ <div className="logs-title">{t("JSON-RPC")}</div>
16
+ <div className="logs-controls">
17
+ <div className="logs-count">
18
+ {t("{{count}} item(s)", { count: filteredRpcLogs.length })}
19
+ </div>
20
+ <div className="logs-filters">
21
+ <button
22
+ type="button"
23
+ className={`logs-filter ${logFilter === "all" ? "is-active" : ""}`}
24
+ onClick={() => setLogFilter("all")}
25
+ >
26
+ {t("All")}
27
+ </button>
28
+ <button
29
+ type="button"
30
+ className={`logs-filter ${logFilter === "stdin" ? "is-active" : ""}`}
31
+ onClick={() => setLogFilter("stdin")}
32
+ >
33
+ {t("Stdin")}
34
+ </button>
35
+ <button
36
+ type="button"
37
+ className={`logs-filter ${logFilter === "stdout" ? "is-active" : ""}`}
38
+ onClick={() => setLogFilter("stdout")}
39
+ >
40
+ {t("Stdout")}
41
+ </button>
42
+ </div>
43
+ <button
44
+ type="button"
45
+ className="logs-clear"
46
+ onClick={handleClearRpcLogs}
47
+ disabled={scopedRpcLogs.length === 0}
48
+ >
49
+ {t("Clear")}
50
+ </button>
51
+ </div>
52
+ </div>
53
+ {filteredRpcLogs.length === 0 ? (
54
+ <div className="logs-empty">{t("No logs yet.")}</div>
55
+ ) : (
56
+ <div className="logs-list">
57
+ {filteredRpcLogs.map((entry, index) => (
58
+ <div
59
+ key={`${entry.timestamp}-${index}`}
60
+ className={`logs-item logs-${entry.direction}`}
61
+ >
62
+ <div className="logs-meta">
63
+ <span className="logs-direction">
64
+ {entry.direction === "stdin" ? t("stdin") : t("stdout")}
65
+ </span>
66
+ <span className="logs-time">{entry.timeLabel}</span>
67
+ {entry.payload?.method && (
68
+ <span className="logs-method">{entry.payload.method}</span>
69
+ )}
70
+ </div>
71
+ <pre className="logs-payload">
72
+ {JSON.stringify(entry.payload, null, 2)}
73
+ </pre>
74
+ </div>
75
+ ))}
76
+ </div>
77
+ )}
78
+ </div>
79
+ );
80
+ }