@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,874 @@
1
+ import React from "react";
2
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3
+ import { faCheck, faCopy, faPlus, faRightFromBracket, faUser } from "@fortawesome/free-solid-svg-icons";
4
+
5
+ export default function SessionGate({
6
+ t,
7
+ brandLogo,
8
+ showStep1,
9
+ showStep2,
10
+ showStep3,
11
+ showStep4,
12
+ showMonoLoginRequired,
13
+ headerHint,
14
+ workspaceMode,
15
+ setWorkspaceMode,
16
+ formDisabled,
17
+ handleWorkspaceSubmit,
18
+ workspaceIdInput,
19
+ setWorkspaceIdInput,
20
+ workspaceSecretInput,
21
+ setWorkspaceSecretInput,
22
+ workspaceError,
23
+ handleWorkspaceProvidersSubmit,
24
+ workspaceProvider,
25
+ workspaceAuthExpanded,
26
+ setWorkspaceAuthExpanded,
27
+ setWorkspaceProviders,
28
+ providerAuthOptions,
29
+ getProviderAuthType,
30
+ workspaceAuthFiles,
31
+ setWorkspaceAuthFiles,
32
+ sessionMode,
33
+ setSessionMode,
34
+ setSessionRequested,
35
+ setAttachmentsError,
36
+ loadWorkspaceSessions,
37
+ deploymentMode,
38
+ handleLeaveWorkspace,
39
+ workspaceSessionsLoading,
40
+ workspaceSessions,
41
+ workspaceSessionsError,
42
+ workspaceSessionDeletingId,
43
+ handleResumeSession,
44
+ handleDeleteSession,
45
+ locale,
46
+ extractRepoName,
47
+ getTruncatedText,
48
+ isCloning,
49
+ repoDisplay,
50
+ onRepoSubmit,
51
+ sessionNameInput,
52
+ setSessionNameInput,
53
+ repoInput,
54
+ setRepoInput,
55
+ repoHistory,
56
+ authMode,
57
+ setAuthMode,
58
+ sshKeyInput,
59
+ setSshKeyInput,
60
+ httpUsername,
61
+ setHttpUsername,
62
+ httpPassword,
63
+ setHttpPassword,
64
+ defaultInternetAccess,
65
+ setDefaultInternetAccess,
66
+ defaultDenyGitCredentialsAccess,
67
+ setDefaultDenyGitCredentialsAccess,
68
+ attachmentsError,
69
+ sessionRequested,
70
+ workspaceBusy,
71
+ workspaceProvidersEditing,
72
+ providersBackStep,
73
+ setWorkspaceStep,
74
+ setWorkspaceProvidersEditing,
75
+ setWorkspaceError,
76
+ setProvidersBackStep,
77
+ loadWorkspaceProviders,
78
+ workspaceCreated,
79
+ workspaceId,
80
+ workspaceCopied,
81
+ handleWorkspaceCopy,
82
+ infoContent,
83
+ toast,
84
+ }) {
85
+ return (
86
+ <div className="session-gate session-fullscreen">
87
+ <div className="session-layout session-layout--fullscreen">
88
+ <div className="session-shell">
89
+ <div className="session-header">
90
+ <img className="brand-logo" src={brandLogo} alt="vibe80" />
91
+ <h1>
92
+ {showMonoLoginRequired
93
+ ? t("Login required")
94
+ : showStep4
95
+ ? t("Start a session")
96
+ : showStep3
97
+ ? t("Workspace created")
98
+ : showStep2
99
+ ? t("Configure AI providers")
100
+ : t("Configure the workspace")}
101
+ </h1>
102
+ {headerHint ? <p className="session-hint">{headerHint}</p> : null}
103
+ </div>
104
+ <div className="session-body">
105
+ {showStep1 && (
106
+ <>
107
+ <form
108
+ id="workspace-form"
109
+ className="session-form"
110
+ onSubmit={handleWorkspaceSubmit}
111
+ >
112
+ <div className="session-workspace-options">
113
+ <button
114
+ type="button"
115
+ className={`session-workspace-option ${
116
+ workspaceMode === "existing" ? "is-selected" : ""
117
+ }`}
118
+ onClick={() => setWorkspaceMode("existing")}
119
+ disabled={formDisabled}
120
+ aria-pressed={workspaceMode === "existing"}
121
+ >
122
+ <span
123
+ className="session-workspace-icon is-join"
124
+ aria-hidden="true"
125
+ >
126
+ <FontAwesomeIcon icon={faUser} />
127
+ </span>
128
+ <span className="session-workspace-option-text">
129
+ <span className="session-workspace-option-title">
130
+ {t("Join a workspace")}
131
+ </span>
132
+ <span className="session-workspace-option-subtitle">
133
+ {t("Access an existing space with your credentials")}
134
+ </span>
135
+ </span>
136
+ </button>
137
+ <button
138
+ type="button"
139
+ className={`session-workspace-option ${
140
+ workspaceMode === "new" ? "is-selected" : ""
141
+ }`}
142
+ onClick={() => setWorkspaceMode("new")}
143
+ disabled={formDisabled}
144
+ aria-pressed={workspaceMode === "new"}
145
+ >
146
+ <span
147
+ className="session-workspace-icon is-create"
148
+ aria-hidden="true"
149
+ >
150
+ <FontAwesomeIcon icon={faPlus} />
151
+ </span>
152
+ <span className="session-workspace-option-text">
153
+ <span className="session-workspace-option-title">
154
+ {t("Create a workspace")}
155
+ </span>
156
+ <span className="session-workspace-option-subtitle">
157
+ {t("Create a new space for you or your team")}
158
+ </span>
159
+ </span>
160
+ </button>
161
+ </div>
162
+ <div
163
+ className={`session-panel ${
164
+ workspaceMode === "existing" ? "is-visible" : "is-hidden"
165
+ }`}
166
+ aria-hidden={workspaceMode !== "existing"}
167
+ >
168
+ <div className="session-workspace-form">
169
+ <div className="session-workspace-form-labels">
170
+ <span>{t("Workspace ID")}</span>
171
+ <span>{t("Secret")}</span>
172
+ </div>
173
+ <div className="session-workspace-form-grid">
174
+ <input
175
+ type="text"
176
+ placeholder={t("workspaceId (e.g. w...)")}
177
+ value={workspaceIdInput}
178
+ onChange={(event) =>
179
+ setWorkspaceIdInput(event.target.value)
180
+ }
181
+ disabled={formDisabled}
182
+ spellCheck={false}
183
+ />
184
+ <input
185
+ type="password"
186
+ placeholder={t("workspaceSecret")}
187
+ value={workspaceSecretInput}
188
+ onChange={(event) =>
189
+ setWorkspaceSecretInput(event.target.value)
190
+ }
191
+ disabled={formDisabled}
192
+ autoComplete="off"
193
+ />
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </form>
198
+ {workspaceError && (
199
+ <div className="attachments-error">{workspaceError}</div>
200
+ )}
201
+ </>
202
+ )}
203
+
204
+ {showStep2 && (
205
+ <>
206
+ <form
207
+ id="providers-form"
208
+ className="session-form"
209
+ onSubmit={handleWorkspaceProvidersSubmit}
210
+ >
211
+ <div className="session-auth-options session-auth-accordion">
212
+ {["codex", "claude"].map((provider) => {
213
+ const config = workspaceProvider(provider);
214
+ const label =
215
+ provider === "codex" ? t("Codex") : t("Claude");
216
+ const expanded = Boolean(workspaceAuthExpanded[provider]);
217
+ const isEnabled = Boolean(config.enabled);
218
+ const providerInUse = workspaceProvidersEditing
219
+ && Array.isArray(workspaceSessions)
220
+ && workspaceSessions.some((session) => {
221
+ const providers = Array.isArray(session.providers) && session.providers.length
222
+ ? session.providers
223
+ : session.activeProvider
224
+ ? [session.activeProvider]
225
+ : [];
226
+ return providers.includes(provider);
227
+ });
228
+ const disableToggle = formDisabled
229
+ || (providerInUse && isEnabled);
230
+ return (
231
+ <div key={provider} className="session-auth-card">
232
+ <div className="session-auth-header">
233
+ <label className="session-auth-option">
234
+ <input
235
+ type="checkbox"
236
+ checked={isEnabled}
237
+ onChange={() => {
238
+ if (providerInUse && isEnabled) {
239
+ return;
240
+ }
241
+ const nextEnabled = !isEnabled;
242
+ setWorkspaceAuthExpanded((current) => ({
243
+ ...current,
244
+ [provider]: nextEnabled,
245
+ }));
246
+ setWorkspaceProviders((current) => ({
247
+ ...current,
248
+ [provider]: {
249
+ ...current[provider],
250
+ enabled: nextEnabled,
251
+ },
252
+ }));
253
+ }}
254
+ disabled={disableToggle}
255
+ title={
256
+ providerInUse && isEnabled
257
+ ? t("Provider cannot be disabled: active sessions use it.")
258
+ : undefined
259
+ }
260
+ />
261
+ {label}
262
+ </label>
263
+ </div>
264
+ {isEnabled && expanded ? (
265
+ <div className="session-auth-grid">
266
+ <select
267
+ value={getProviderAuthType(provider, config)}
268
+ onChange={(event) =>
269
+ setWorkspaceProviders((current) => ({
270
+ ...current,
271
+ [provider]: {
272
+ ...current[provider],
273
+ authType: event.target.value,
274
+ authValue: "",
275
+ },
276
+ }))
277
+ }
278
+ disabled={formDisabled}
279
+ >
280
+ {(providerAuthOptions[provider] || []).map(
281
+ (authType) => (
282
+ <option key={authType} value={authType}>
283
+ {t(authType)}
284
+ </option>
285
+ )
286
+ )}
287
+ </select>
288
+ {getProviderAuthType(provider, config) ===
289
+ "auth_json_b64" ? (
290
+ <div className="session-auth-file">
291
+ <input
292
+ type="file"
293
+ accept="application/json,.json"
294
+ onChange={async (event) => {
295
+ const file = event.target.files?.[0];
296
+ if (!file) {
297
+ return;
298
+ }
299
+ const content = await file.text();
300
+ setWorkspaceProviders((current) => ({
301
+ ...current,
302
+ [provider]: {
303
+ ...current[provider],
304
+ authValue: content,
305
+ },
306
+ }));
307
+ setWorkspaceAuthFiles((current) => ({
308
+ ...current,
309
+ [provider]: file.name,
310
+ }));
311
+ event.target.value = "";
312
+ }}
313
+ disabled={formDisabled}
314
+ />
315
+ {workspaceAuthFiles[provider] ? (
316
+ <span className="session-auth-file-name">
317
+ {workspaceAuthFiles[provider]}
318
+ </span>
319
+ ) : null}
320
+ </div>
321
+ ) : (
322
+ <input
323
+ type="password"
324
+ placeholder={t("Key or token")}
325
+ value={config.authValue}
326
+ onChange={(event) =>
327
+ setWorkspaceProviders((current) => ({
328
+ ...current,
329
+ [provider]: {
330
+ ...current[provider],
331
+ authValue: event.target.value,
332
+ },
333
+ }))
334
+ }
335
+ disabled={formDisabled}
336
+ autoComplete="off"
337
+ />
338
+ )}
339
+ </div>
340
+ ) : null}
341
+ </div>
342
+ );
343
+ })}
344
+ </div>
345
+ </form>
346
+ {workspaceError && (
347
+ <div className="attachments-error">{workspaceError}</div>
348
+ )}
349
+ </>
350
+ )}
351
+
352
+ {showStep3 && (
353
+ <>
354
+ <div className="workspace-created-card">
355
+ <div className="workspace-created-row">
356
+ <span className="workspace-created-label">
357
+ {t("Workspace ID")}
358
+ </span>
359
+ <span className="workspace-created-value">
360
+ {workspaceCreated?.workspaceId || workspaceId}
361
+ </span>
362
+ <button
363
+ type="button"
364
+ className="workspace-created-copy"
365
+ onClick={() =>
366
+ handleWorkspaceCopy(
367
+ "id",
368
+ workspaceCreated?.workspaceId || workspaceId || ""
369
+ )
370
+ }
371
+ aria-label={t("Copy workspace ID")}
372
+ >
373
+ <FontAwesomeIcon
374
+ icon={workspaceCopied.id ? faCheck : faCopy}
375
+ />
376
+ </button>
377
+ </div>
378
+ <div className="workspace-created-row">
379
+ <span className="workspace-created-label">
380
+ {t("Workspace Secret")}
381
+ </span>
382
+ <span className="workspace-created-value">
383
+ {workspaceCreated?.workspaceSecret || ""}
384
+ </span>
385
+ <button
386
+ type="button"
387
+ className="workspace-created-copy"
388
+ onClick={() =>
389
+ handleWorkspaceCopy(
390
+ "secret",
391
+ workspaceCreated?.workspaceSecret || ""
392
+ )
393
+ }
394
+ aria-label={t("Copy workspace secret")}
395
+ >
396
+ <FontAwesomeIcon
397
+ icon={workspaceCopied.secret ? faCheck : faCopy}
398
+ />
399
+ </button>
400
+ </div>
401
+ </div>
402
+ </>
403
+ )}
404
+
405
+ {showStep4 && (
406
+ <div className="session-step">
407
+ <div className="session-workspace-toggle">
408
+ <button
409
+ type="button"
410
+ className={`session-workspace-option is-compact ${
411
+ sessionMode === "new" ? "is-selected" : ""
412
+ }`}
413
+ onClick={() => {
414
+ setSessionMode("new");
415
+ setSessionRequested(false);
416
+ setAttachmentsError("");
417
+ }}
418
+ disabled={formDisabled}
419
+ aria-pressed={sessionMode === "new"}
420
+ >
421
+ <span
422
+ className="session-workspace-icon is-create"
423
+ aria-hidden="true"
424
+ >
425
+ <FontAwesomeIcon icon={faPlus} />
426
+ </span>
427
+ <span className="session-workspace-option-title">
428
+ {t("New session")}
429
+ </span>
430
+ </button>
431
+ <button
432
+ type="button"
433
+ className={`session-workspace-option is-compact ${
434
+ sessionMode === "existing" ? "is-selected" : ""
435
+ }`}
436
+ onClick={() => {
437
+ setSessionMode("existing");
438
+ setSessionRequested(false);
439
+ setAttachmentsError("");
440
+ loadWorkspaceSessions();
441
+ }}
442
+ disabled={formDisabled}
443
+ aria-pressed={sessionMode === "existing"}
444
+ >
445
+ <span
446
+ className="session-workspace-icon is-join"
447
+ aria-hidden="true"
448
+ >
449
+ <FontAwesomeIcon icon={faUser} />
450
+ </span>
451
+ <span className="session-workspace-option-title">
452
+ {t("Resume an existing session")}
453
+ </span>
454
+ </button>
455
+ {deploymentMode !== "mono_user" && (
456
+ <button
457
+ type="button"
458
+ className="session-workspace-option is-compact"
459
+ onClick={handleLeaveWorkspace}
460
+ >
461
+ <span
462
+ className="session-workspace-icon is-leave"
463
+ aria-hidden="true"
464
+ >
465
+ <FontAwesomeIcon icon={faRightFromBracket} />
466
+ </span>
467
+ <span className="session-workspace-option-title">
468
+ {t("Leave workspace")}
469
+ </span>
470
+ </button>
471
+ )}
472
+ </div>
473
+ <div
474
+ className={`session-panel ${
475
+ sessionMode === "existing" ? "is-visible" : "is-hidden"
476
+ }`}
477
+ aria-hidden={sessionMode !== "existing"}
478
+ >
479
+ <div className="session-auth">
480
+ <div className="session-auth-title">
481
+ {t("Existing sessions")}
482
+ </div>
483
+ {workspaceSessionsLoading ? (
484
+ <div className="session-auth-hint">
485
+ {t("Loading sessions...")}
486
+ </div>
487
+ ) : workspaceSessions.length === 0 ? (
488
+ <div className="session-auth-hint">
489
+ {t("No sessions available.")}
490
+ </div>
491
+ ) : (
492
+ <ul className="session-list">
493
+ {workspaceSessions.map((session) => {
494
+ const repoName = extractRepoName(session.repoUrl);
495
+ const title =
496
+ session.name || repoName || session.sessionId;
497
+ const subtitle = session.repoUrl
498
+ ? getTruncatedText(session.repoUrl, 72)
499
+ : session.sessionId;
500
+ const lastSeen = session.lastActivityAt
501
+ ? new Date(session.lastActivityAt).toLocaleString(
502
+ locale
503
+ )
504
+ : session.createdAt
505
+ ? new Date(
506
+ session.createdAt
507
+ ).toLocaleString(locale)
508
+ : "";
509
+ const isDeleting =
510
+ workspaceSessionDeletingId === session.sessionId;
511
+ return (
512
+ <li key={session.sessionId} className="session-item">
513
+ <div className="session-item-meta">
514
+ <div className="session-item-title">{title}</div>
515
+ <div className="session-item-sub">
516
+ {subtitle}
517
+ </div>
518
+ {lastSeen && (
519
+ <div className="session-item-sub">
520
+ {t("Last activity: {{date}}", {
521
+ date: lastSeen,
522
+ })}
523
+ </div>
524
+ )}
525
+ </div>
526
+ <div className="session-item-actions">
527
+ <button
528
+ type="button"
529
+ className="session-list-button"
530
+ onClick={() =>
531
+ handleResumeSession(session.sessionId)
532
+ }
533
+ disabled={formDisabled || isDeleting}
534
+ >
535
+ {t("Resume")}
536
+ </button>
537
+ <button
538
+ type="button"
539
+ className="session-list-button is-danger"
540
+ onClick={() => handleDeleteSession(session)}
541
+ disabled={formDisabled || isDeleting}
542
+ >
543
+ {isDeleting ? t("Deleting...") : t("Delete")}
544
+ </button>
545
+ </div>
546
+ </li>
547
+ );
548
+ })}
549
+ </ul>
550
+ )}
551
+ {workspaceSessionsError && (
552
+ <div className="attachments-error">
553
+ {workspaceSessionsError}
554
+ </div>
555
+ )}
556
+ </div>
557
+ </div>
558
+ <div
559
+ className={`session-panel ${
560
+ sessionMode === "new" ? "is-visible" : "is-hidden"
561
+ }`}
562
+ aria-hidden={sessionMode !== "new"}
563
+ >
564
+ {isCloning ? (
565
+ <div className="session-hint">
566
+ {t("Cloning repository...")}
567
+ {repoDisplay && (
568
+ <div className="session-meta">{repoDisplay}</div>
569
+ )}
570
+ </div>
571
+ ) : (
572
+ <form
573
+ id="repo-form"
574
+ className="session-form session-form--compact"
575
+ onSubmit={onRepoSubmit}
576
+ >
577
+ <div className="session-form-row is-compact-grid">
578
+ <input
579
+ type="text"
580
+ placeholder={t("Session name (optional)")}
581
+ value={sessionNameInput}
582
+ onChange={(event) =>
583
+ setSessionNameInput(event.target.value)
584
+ }
585
+ disabled={formDisabled}
586
+ />
587
+ <div className="session-repo-field">
588
+ <input
589
+ type="text"
590
+ placeholder={t(
591
+ "git@gitea.devops:my-org/my-repo.git"
592
+ )}
593
+ value={repoInput}
594
+ onChange={(event) => {
595
+ setRepoInput(event.target.value);
596
+ }}
597
+ disabled={formDisabled}
598
+ required
599
+ list={
600
+ repoHistory.length > 0
601
+ ? "repo-history"
602
+ : undefined
603
+ }
604
+ />
605
+ {repoHistory.length > 0 && (
606
+ <datalist id="repo-history">
607
+ {repoHistory.map((url) => (
608
+ <option key={url} value={url}>
609
+ {getTruncatedText(url, 72)}
610
+ </option>
611
+ ))}
612
+ </datalist>
613
+ )}
614
+ </div>
615
+ </div>
616
+ <div className="session-auth">
617
+ <div className="session-auth-title">
618
+ {t("Repository authentication (optional)")}
619
+ </div>
620
+ <div className="session-auth-options">
621
+ <select
622
+ value={authMode}
623
+ onChange={(event) =>
624
+ setAuthMode(event.target.value)
625
+ }
626
+ disabled={formDisabled}
627
+ >
628
+ <option value="none">{t("None")}</option>
629
+ <option value="ssh">
630
+ {t("Private SSH key (not recommended)")}
631
+ </option>
632
+ <option value="http">
633
+ {t("Username + password")}
634
+ </option>
635
+ </select>
636
+ </div>
637
+ {authMode === "ssh" && (
638
+ <>
639
+ <textarea
640
+ className="session-auth-textarea"
641
+ placeholder={t(
642
+ "-----BEGIN OPENSSH PRIVATE KEY-----"
643
+ )}
644
+ value={sshKeyInput}
645
+ onChange={(event) =>
646
+ setSshKeyInput(event.target.value)
647
+ }
648
+ disabled={formDisabled}
649
+ rows={6}
650
+ spellCheck={false}
651
+ />
652
+ </>
653
+ )}
654
+ {authMode === "http" && (
655
+ <>
656
+ <div className="session-auth-grid">
657
+ <input
658
+ type="text"
659
+ placeholder={t("Username")}
660
+ value={httpUsername}
661
+ onChange={(event) =>
662
+ setHttpUsername(event.target.value)
663
+ }
664
+ disabled={formDisabled}
665
+ autoComplete="username"
666
+ />
667
+ <input
668
+ type="password"
669
+ placeholder={t("Password or PAT")}
670
+ value={httpPassword}
671
+ onChange={(event) =>
672
+ setHttpPassword(event.target.value)
673
+ }
674
+ disabled={formDisabled}
675
+ autoComplete="current-password"
676
+ />
677
+ </div>
678
+ </>
679
+ )}
680
+ </div>
681
+ <div className="session-auth session-auth-compact">
682
+ <div className="session-auth-title">
683
+ {t("Permissions")}
684
+ </div>
685
+ <div className="session-auth-options session-auth-options--compact">
686
+ <label className="session-auth-option">
687
+ <input
688
+ type="checkbox"
689
+ checked={defaultInternetAccess}
690
+ onChange={(event) => {
691
+ const checked = event.target.checked;
692
+ setDefaultInternetAccess(checked);
693
+ if (!checked) {
694
+ setDefaultDenyGitCredentialsAccess(false);
695
+ }
696
+ }}
697
+ disabled={formDisabled}
698
+ />
699
+ {t("Internet access")}
700
+ </label>
701
+ {defaultInternetAccess && deploymentMode !== "mono_user" && (
702
+ <label className="session-auth-option">
703
+ <input
704
+ type="checkbox"
705
+ checked={defaultDenyGitCredentialsAccess}
706
+ onChange={(event) =>
707
+ setDefaultDenyGitCredentialsAccess(
708
+ event.target.checked
709
+ )
710
+ }
711
+ disabled={formDisabled}
712
+ />
713
+ {t("Deny git credentials access")}
714
+ </label>
715
+ )}
716
+ </div>
717
+ </div>
718
+ </form>
719
+ )}
720
+ </div>
721
+ {attachmentsError && (
722
+ <div className="attachments-error">{attachmentsError}</div>
723
+ )}
724
+ </div>
725
+ )}
726
+ {showMonoLoginRequired && (
727
+ <div className="session-step">
728
+ <div className="session-auth">
729
+ <div className="session-auth-title">{t("Login required")}</div>
730
+ <div className="session-auth-hint">
731
+ {t("Please use the console generated URL to login")}
732
+ </div>
733
+ </div>
734
+ {workspaceError && (
735
+ <div className="attachments-error">{workspaceError}</div>
736
+ )}
737
+ </div>
738
+ )}
739
+ </div>
740
+ <div className="session-footer">
741
+ {showMonoLoginRequired ? null : showStep1 ? (
742
+ <button
743
+ type="submit"
744
+ form="workspace-form"
745
+ className="session-button primary"
746
+ disabled={formDisabled}
747
+ >
748
+ {workspaceBusy ? t("Validating...") : t("Continue")}
749
+ </button>
750
+ ) : showStep2 ? (
751
+ <>
752
+ <button
753
+ type="button"
754
+ className="session-button secondary"
755
+ onClick={() => {
756
+ if (providersBackStep === 4) {
757
+ setWorkspaceProvidersEditing(false);
758
+ setWorkspaceStep(4);
759
+ return;
760
+ }
761
+ setWorkspaceStep(1);
762
+ }}
763
+ disabled={formDisabled}
764
+ >
765
+ {t("Back")}
766
+ </button>
767
+ <button
768
+ type="submit"
769
+ form="providers-form"
770
+ className="session-button primary"
771
+ disabled={formDisabled}
772
+ >
773
+ {workspaceBusy
774
+ ? t("Validating...")
775
+ : workspaceProvidersEditing
776
+ ? t("Save")
777
+ : t("Continue")}
778
+ </button>
779
+ </>
780
+ ) : showStep3 ? (
781
+ <button
782
+ type="button"
783
+ className="session-button primary"
784
+ onClick={() => setWorkspaceStep(4)}
785
+ disabled={formDisabled}
786
+ >
787
+ {t("Continue")}
788
+ </button>
789
+ ) : showStep4 ? (
790
+ sessionMode === "existing" ? (
791
+ <button
792
+ type="button"
793
+ className="session-button secondary session-footer-full"
794
+ disabled={formDisabled}
795
+ onClick={() => {
796
+ setWorkspaceProvidersEditing(true);
797
+ setWorkspaceError("");
798
+ setProvidersBackStep(4);
799
+ loadWorkspaceProviders();
800
+ loadWorkspaceSessions();
801
+ setWorkspaceStep(2);
802
+ }}
803
+ >
804
+ {t("AI providers")}
805
+ </button>
806
+ ) : (
807
+ <>
808
+ <button
809
+ type="button"
810
+ className="session-button secondary"
811
+ disabled={formDisabled}
812
+ onClick={() => {
813
+ setWorkspaceProvidersEditing(true);
814
+ setWorkspaceError("");
815
+ setProvidersBackStep(4);
816
+ loadWorkspaceProviders();
817
+ loadWorkspaceSessions();
818
+ setWorkspaceStep(2);
819
+ }}
820
+ >
821
+ {t("AI providers")}
822
+ </button>
823
+ <button
824
+ type="submit"
825
+ form="repo-form"
826
+ className="session-button primary"
827
+ disabled={formDisabled}
828
+ >
829
+ {sessionRequested ? t("Loading...") : t("Clone")}
830
+ </button>
831
+ </>
832
+ )
833
+ ) : null}
834
+ </div>
835
+ </div>
836
+ <aside className="session-info">
837
+ <div className="session-info-card">
838
+ <div className="session-info-title">
839
+ <span className="session-info-icon" aria-hidden="true">
840
+ ℹ️
841
+ </span>
842
+ {infoContent.title}
843
+ </div>
844
+ {infoContent.paragraphs?.map((paragraph) => (
845
+ <p key={paragraph}>{paragraph}</p>
846
+ ))}
847
+ {infoContent.securityLink ? (
848
+ <p>
849
+ {t(
850
+ "Vibe80 strictly controls access to resources (Git credentials and internet) using sandboxing. "
851
+ )}
852
+ <a
853
+ className="session-info-link"
854
+ href="https://vibe80.ai/security"
855
+ target="_blank"
856
+ rel="noreferrer"
857
+ >
858
+ {t("Click here to learn more.")}
859
+ </a>
860
+ </p>
861
+ ) : null}
862
+ </div>
863
+ </aside>
864
+ </div>
865
+ {toast && (
866
+ <div className="toast-container">
867
+ <div className={`toast is-${toast.type || "success"}`}>
868
+ {toast.message}
869
+ </div>
870
+ </div>
871
+ )}
872
+ </div>
873
+ );
874
+ }