@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,604 @@
1
+ import { useState, useRef, useEffect, useMemo } from "react";
2
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3
+ import {
4
+ faCheck,
5
+ faCircle,
6
+ faCircleHalfStroke,
7
+ faCircleNotch,
8
+ faPlus,
9
+ faXmark,
10
+ } from "@fortawesome/free-solid-svg-icons";
11
+ import { useI18n } from "../i18n.jsx";
12
+ import "./WorktreeTabs.css";
13
+
14
+ const STATUS_ICONS = {
15
+ creating: faCircleNotch,
16
+ ready: faCircle,
17
+ processing: faCircleHalfStroke,
18
+ stopped: faCircle,
19
+ completed: faCheck,
20
+ error: faXmark,
21
+ };
22
+
23
+ const STATUS_COLORS = {
24
+ creating: "#9ca3af",
25
+ ready: "#10b981",
26
+ processing: "#f59e0b",
27
+ stopped: "#ef4444",
28
+ completed: "#3b82f6",
29
+ error: "#ef4444",
30
+ };
31
+
32
+ export default function WorktreeTabs({
33
+ worktrees,
34
+ activeWorktreeId,
35
+ onSelect,
36
+ onCreate,
37
+ onClose,
38
+ onRename,
39
+ provider,
40
+ providers,
41
+ branches,
42
+ defaultBranch,
43
+ branchLoading,
44
+ branchError,
45
+ defaultInternetAccess,
46
+ defaultDenyGitCredentialsAccess,
47
+ deploymentMode,
48
+ onRefreshBranches,
49
+ providerModelState,
50
+ onRequestProviderModels,
51
+ disabled,
52
+ isMobile,
53
+ }) {
54
+ const { t } = useI18n();
55
+ const [createDialogOpen, setCreateDialogOpen] = useState(false);
56
+ const [editingId, setEditingId] = useState(null);
57
+ const [editingName, setEditingName] = useState("");
58
+ const [newName, setNewName] = useState("");
59
+ const [startingBranch, setStartingBranch] = useState("");
60
+ const providerOptions = useMemo(
61
+ () =>
62
+ Array.isArray(providers) && providers.length
63
+ ? providers
64
+ : [provider || "codex"],
65
+ [providers, provider]
66
+ );
67
+ const [newProvider, setNewProvider] = useState(providerOptions[0]);
68
+ const [newContext, setNewContext] = useState("new");
69
+ const [newSourceWorktree, setNewSourceWorktree] = useState("main");
70
+ const [newModel, setNewModel] = useState("");
71
+ const [newReasoningEffort, setNewReasoningEffort] = useState("");
72
+ const [newInternetAccess, setNewInternetAccess] = useState(
73
+ Boolean(defaultInternetAccess)
74
+ );
75
+ const [newDenyGitCredentialsAccess, setNewDenyGitCredentialsAccess] = useState(
76
+ typeof defaultDenyGitCredentialsAccess === "boolean"
77
+ ? defaultDenyGitCredentialsAccess
78
+ : true
79
+ );
80
+ const statusLabels = useMemo(
81
+ () => ({
82
+ creating: t("Creating"),
83
+ ready: t("Ready"),
84
+ processing: t("In progress"),
85
+ stopped: t("Stopped"),
86
+ completed: t("Completed"),
87
+ error: t("Error"),
88
+ }),
89
+ [t]
90
+ );
91
+ const editInputRef = useRef(null);
92
+ const createInputRef = useRef(null);
93
+
94
+ useEffect(() => {
95
+ if (editingId && editInputRef.current) {
96
+ editInputRef.current.focus();
97
+ editInputRef.current.select();
98
+ }
99
+ }, [editingId]);
100
+
101
+ useEffect(() => {
102
+ if (createDialogOpen && createInputRef.current) {
103
+ createInputRef.current.focus();
104
+ }
105
+ }, [createDialogOpen]);
106
+
107
+ useEffect(() => {
108
+ if (!createDialogOpen) return;
109
+ setNewInternetAccess(Boolean(defaultInternetAccess));
110
+ setNewDenyGitCredentialsAccess(
111
+ typeof defaultDenyGitCredentialsAccess === "boolean"
112
+ ? defaultDenyGitCredentialsAccess
113
+ : true
114
+ );
115
+ if (!startingBranch) {
116
+ setStartingBranch(defaultBranch || "");
117
+ }
118
+ if (!branches?.length && onRefreshBranches && !branchLoading) {
119
+ onRefreshBranches();
120
+ }
121
+ if (newContext === "new" && onRequestProviderModels) {
122
+ const providerState = providerModelState?.[newProvider] || {};
123
+ if (!providerState.loading && !(providerState.models || []).length) {
124
+ onRequestProviderModels(newProvider);
125
+ }
126
+ }
127
+ }, [
128
+ createDialogOpen,
129
+ startingBranch,
130
+ defaultBranch,
131
+ branches,
132
+ defaultInternetAccess,
133
+ defaultDenyGitCredentialsAccess,
134
+ onRefreshBranches,
135
+ branchLoading,
136
+ newContext,
137
+ newProvider,
138
+ onRequestProviderModels,
139
+ ]);
140
+
141
+ useEffect(() => {
142
+ if (!providerOptions.includes(newProvider)) {
143
+ setNewProvider(providerOptions[0]);
144
+ }
145
+ }, [providerOptions, newProvider]);
146
+
147
+ useEffect(() => {
148
+ if (newContext === "new" && onRequestProviderModels) {
149
+ const providerState = providerModelState?.[newProvider] || {};
150
+ if (!providerState.loading && !(providerState.models || []).length) {
151
+ onRequestProviderModels(newProvider);
152
+ }
153
+ }
154
+ if (newContext === "new") {
155
+ setNewModel("");
156
+ setNewReasoningEffort("");
157
+ }
158
+ }, [newContext, newProvider, onRequestProviderModels]);
159
+
160
+ const providerState = providerModelState?.[newProvider] || {};
161
+ const availableModels = Array.isArray(providerState.models) ? providerState.models : [];
162
+ const branchOptions = useMemo(
163
+ () =>
164
+ Array.isArray(branches)
165
+ ? branches.map((branch) => branch.trim()).filter(Boolean)
166
+ : [],
167
+ [branches]
168
+ );
169
+ const effectiveBranch = (startingBranch || defaultBranch || "").trim();
170
+ const isBranchValid =
171
+ Boolean(effectiveBranch) && branchOptions.includes(effectiveBranch);
172
+ const defaultModel = useMemo(
173
+ () => availableModels.find((model) => model.isDefault) || null,
174
+ [availableModels]
175
+ );
176
+ const selectedModelDetails = useMemo(
177
+ () => availableModels.find((model) => model.model === newModel) || null,
178
+ [availableModels, newModel]
179
+ );
180
+ const showReasoningField =
181
+ newContext === "new" &&
182
+ newProvider === "codex" &&
183
+ (selectedModelDetails?.supportedReasoningEfforts?.length || 0) > 0;
184
+
185
+ useEffect(() => {
186
+ if (newContext !== "new") {
187
+ return;
188
+ }
189
+ if (!newModel && defaultModel?.model) {
190
+ setNewModel(defaultModel.model);
191
+ }
192
+ if (newProvider === "codex" && !newReasoningEffort && defaultModel?.defaultReasoningEffort) {
193
+ setNewReasoningEffort(defaultModel.defaultReasoningEffort);
194
+ }
195
+ if (newProvider !== "codex" && newReasoningEffort) {
196
+ setNewReasoningEffort("");
197
+ }
198
+ }, [newContext, newProvider, newModel, newReasoningEffort, defaultModel]);
199
+
200
+ useEffect(() => {
201
+ if (newContext !== "new" || newProvider !== "codex") {
202
+ if (newReasoningEffort) {
203
+ setNewReasoningEffort("");
204
+ }
205
+ return;
206
+ }
207
+ if (!selectedModelDetails?.supportedReasoningEfforts?.length) {
208
+ if (newReasoningEffort) {
209
+ setNewReasoningEffort("");
210
+ }
211
+ return;
212
+ }
213
+ const valid = selectedModelDetails.supportedReasoningEfforts.some(
214
+ (effort) => effort.reasoningEffort === newReasoningEffort
215
+ );
216
+ if (!valid && newReasoningEffort) {
217
+ setNewReasoningEffort("");
218
+ }
219
+ }, [newContext, newProvider, selectedModelDetails, newReasoningEffort]);
220
+
221
+ const handleCreate = () => {
222
+ if (onCreate) {
223
+ onCreate({
224
+ context: newContext,
225
+ name: newName.trim() || null,
226
+ provider: newProvider,
227
+ sourceWorktree: newContext === "fork" ? newSourceWorktree : null,
228
+ startingBranch: effectiveBranch || null,
229
+ model: newContext === "new" ? newModel || null : null,
230
+ reasoningEffort: newContext === "new" ? newReasoningEffort || null : null,
231
+ internetAccess: newInternetAccess,
232
+ denyGitCredentialsAccess: newDenyGitCredentialsAccess,
233
+ });
234
+ }
235
+ setNewName("");
236
+ setNewContext("new");
237
+ setNewProvider(providerOptions[0]);
238
+ setNewSourceWorktree("main");
239
+ setStartingBranch(defaultBranch || "");
240
+ setNewModel("");
241
+ setNewReasoningEffort("");
242
+ setNewInternetAccess(Boolean(defaultInternetAccess));
243
+ setNewDenyGitCredentialsAccess(
244
+ typeof defaultDenyGitCredentialsAccess === "boolean"
245
+ ? defaultDenyGitCredentialsAccess
246
+ : true
247
+ );
248
+ setCreateDialogOpen(false);
249
+ };
250
+
251
+ const handleStartEdit = (wt) => {
252
+ setEditingId(wt.id);
253
+ setEditingName(wt.name);
254
+ };
255
+
256
+ const handleFinishEdit = () => {
257
+ if (editingId && editingName.trim() && onRename) {
258
+ onRename(editingId, editingName.trim());
259
+ }
260
+ setEditingId(null);
261
+ setEditingName("");
262
+ };
263
+
264
+ const handleKeyDownEdit = (e) => {
265
+ if (e.key === "Enter") {
266
+ handleFinishEdit();
267
+ } else if (e.key === "Escape") {
268
+ setEditingId(null);
269
+ setEditingName("");
270
+ }
271
+ };
272
+
273
+ const handleKeyDownCreate = (e) => {
274
+ if (e.key === "Enter") {
275
+ handleCreate();
276
+ } else if (e.key === "Escape") {
277
+ setCreateDialogOpen(false);
278
+ }
279
+ };
280
+
281
+ const worktreeList = (Array.isArray(worktrees)
282
+ ? worktrees
283
+ : Array.from(worktrees?.values?.() || [])
284
+ ).slice().sort((a, b) => {
285
+ if (a?.id === "main" && b?.id !== "main") return -1;
286
+ if (b?.id === "main" && a?.id !== "main") return 1;
287
+ const aTime = a?.createdAt ? new Date(a.createdAt).getTime() : 0;
288
+ const bTime = b?.createdAt ? new Date(b.createdAt).getTime() : 0;
289
+ return aTime - bTime;
290
+ });
291
+ const sourceWorktreeOptions = worktreeList.map((wt) => ({
292
+ id: wt.id,
293
+ label: wt.id === "main" ? "main" : (wt.name || wt.branchName || wt.id),
294
+ provider: wt.provider || (wt.id === "main" ? provider || null : null),
295
+ }));
296
+ const selectedSourceWorktree = sourceWorktreeOptions.find(
297
+ (wt) => wt.id === newSourceWorktree
298
+ );
299
+ const showClaudeForkWarning =
300
+ newContext === "fork" && selectedSourceWorktree?.provider === "claude";
301
+
302
+ useEffect(() => {
303
+ if (!sourceWorktreeOptions.length) {
304
+ return;
305
+ }
306
+ const exists = sourceWorktreeOptions.some((wt) => wt.id === newSourceWorktree);
307
+ if (!exists) {
308
+ const fallback = sourceWorktreeOptions.find((wt) => wt.id === "main")?.id
309
+ || sourceWorktreeOptions[0].id;
310
+ setNewSourceWorktree(fallback);
311
+ }
312
+ }, [newSourceWorktree, sourceWorktreeOptions]);
313
+
314
+ return (
315
+ <div className="worktree-tabs-container">
316
+ <div className="worktree-tabs">
317
+ {isMobile ? (
318
+ <div className="worktree-tabs-select">
319
+ <select
320
+ className="worktree-select"
321
+ value={activeWorktreeId}
322
+ onChange={(event) => !disabled && onSelect?.(event.target.value)}
323
+ aria-label={t("Select a branch")}
324
+ disabled={disabled}
325
+ >
326
+ {worktreeList.map((wt) => (
327
+ <option key={wt.id} value={wt.id}>
328
+ {wt.name}
329
+ </option>
330
+ ))}
331
+ </select>
332
+ <button
333
+ className="worktree-tab-add"
334
+ onClick={() => setCreateDialogOpen(true)}
335
+ disabled={disabled || worktreeList.length >= 10}
336
+ title={t("New worktree")}
337
+ aria-label={t("New worktree")}
338
+ >
339
+ <FontAwesomeIcon icon={faPlus} />
340
+ </button>
341
+ </div>
342
+ ) : (
343
+ <>
344
+ {worktreeList.map((wt) => (
345
+ <div
346
+ key={wt.id}
347
+ className={`worktree-tab ${activeWorktreeId === wt.id ? "active" : ""}`}
348
+ onClick={() => !disabled && onSelect?.(wt.id)}
349
+ style={{
350
+ "--tab-accent": wt.color || "#3b82f6",
351
+ }}
352
+ >
353
+ <span
354
+ className={`worktree-status ${wt.status === "processing" ? "pulse" : ""}`}
355
+ style={{ color: STATUS_COLORS[wt.status] || STATUS_COLORS.ready }}
356
+ title={statusLabels[wt.status] || wt.status}
357
+ >
358
+ <FontAwesomeIcon icon={STATUS_ICONS[wt.status] || STATUS_ICONS.ready} />
359
+ </span>
360
+
361
+ {editingId === wt.id && wt.id !== "main" ? (
362
+ <input
363
+ ref={editInputRef}
364
+ className="worktree-tab-edit"
365
+ value={editingName}
366
+ onChange={(e) => setEditingName(e.target.value)}
367
+ onBlur={handleFinishEdit}
368
+ onKeyDown={handleKeyDownEdit}
369
+ onClick={(e) => e.stopPropagation()}
370
+ />
371
+ ) : (
372
+ <span
373
+ className="worktree-tab-name"
374
+ onDoubleClick={(e) => {
375
+ e.stopPropagation();
376
+ // Don't allow renaming "main" tab
377
+ if (wt.id !== "main") {
378
+ handleStartEdit(wt);
379
+ }
380
+ }}
381
+ title={
382
+ wt.id === "main"
383
+ ? wt.name
384
+ : `${wt.name || wt.branchName} (${wt.branchName || "main"})`
385
+ }
386
+ >
387
+ {wt.id === "main" ? wt.name : (wt.name || wt.branchName)}
388
+ </span>
389
+ )}
390
+ {/* Don't show close button for "main" tab */}
391
+ {wt.id !== "main" && (
392
+ <button
393
+ className="worktree-tab-close"
394
+ onClick={(e) => {
395
+ e.stopPropagation();
396
+ onClose?.(wt.id);
397
+ }}
398
+ title={t("Close")}
399
+ >
400
+ <FontAwesomeIcon icon={faXmark} />
401
+ </button>
402
+ )}
403
+ </div>
404
+ ))}
405
+
406
+ <button
407
+ className="worktree-tab-add"
408
+ onClick={() => setCreateDialogOpen(true)}
409
+ disabled={disabled || worktreeList.length >= 10}
410
+ title={t("New worktree")}
411
+ >
412
+ <FontAwesomeIcon icon={faPlus} />
413
+ </button>
414
+ </>
415
+ )}
416
+ </div>
417
+
418
+ {createDialogOpen && (
419
+ <div className="worktree-create-dialog-overlay" onClick={() => setCreateDialogOpen(false)}>
420
+ <div className="worktree-create-dialog" onClick={(e) => e.stopPropagation()}>
421
+ <h3>{t("New worktree")}</h3>
422
+ <div className="worktree-create-grid">
423
+ <div className="worktree-create-field">
424
+ <label>{t("Name (optional)")}</label>
425
+ <input
426
+ ref={createInputRef}
427
+ type="text"
428
+ placeholder={t("e.g. refactor-auth")}
429
+ value={newName}
430
+ onChange={(e) => setNewName(e.target.value)}
431
+ onKeyDown={handleKeyDownCreate}
432
+ />
433
+ </div>
434
+ <div className="worktree-create-field">
435
+ <label>{t("Source branch")}</label>
436
+ <input
437
+ type="text"
438
+ list="worktree-branch-options"
439
+ placeholder={defaultBranch || "main"}
440
+ value={startingBranch}
441
+ onChange={(e) => setStartingBranch(e.target.value)}
442
+ onKeyDown={handleKeyDownCreate}
443
+ />
444
+ <datalist id="worktree-branch-options">
445
+ {branchOptions.map((branch) => (
446
+ <option key={branch} value={branch} />
447
+ ))}
448
+ </datalist>
449
+ {!isBranchValid && (
450
+ <div className="worktree-field-error">
451
+ {t("Select a valid remote branch.")}
452
+ </div>
453
+ )}
454
+ {branchError && <div className="worktree-field-error">{branchError}</div>}
455
+ </div>
456
+ <div className="worktree-create-field">
457
+ <label>{t("Context")}</label>
458
+ <select
459
+ value={newContext}
460
+ onChange={(e) => setNewContext(e.target.value === "fork" ? "fork" : "new")}
461
+ >
462
+ <option value="new">{t("New")}</option>
463
+ <option value="fork">{t("Fork")}</option>
464
+ </select>
465
+ </div>
466
+ {newContext === "new" ? (
467
+ <div className="worktree-create-field">
468
+ <label>{t("Provider")}</label>
469
+ <select
470
+ value={newProvider}
471
+ onChange={(e) => setNewProvider(e.target.value)}
472
+ disabled={providerOptions.length <= 1}
473
+ >
474
+ {providerOptions.includes("codex") && (
475
+ <option value="codex">{t("Codex (OpenAI)")}</option>
476
+ )}
477
+ {providerOptions.includes("claude") && (
478
+ <option value="claude">{t("Claude")}</option>
479
+ )}
480
+ </select>
481
+ </div>
482
+ ) : (
483
+ <div className="worktree-create-field">
484
+ <label>{t("Source worktree")}</label>
485
+ <select
486
+ value={newSourceWorktree}
487
+ onChange={(e) => setNewSourceWorktree(e.target.value || "main")}
488
+ >
489
+ {sourceWorktreeOptions.map((wt) => (
490
+ <option key={wt.id} value={wt.id}>
491
+ {wt.label}
492
+ </option>
493
+ ))}
494
+ </select>
495
+ </div>
496
+ )}
497
+ {showClaudeForkWarning && (
498
+ <div className="worktree-create-field is-full">
499
+ <div className="worktree-warning-bubble">
500
+ {t(
501
+ "Claude Code does not natively support forked sessions with directory changes. This feature is experimental."
502
+ )}
503
+ </div>
504
+ </div>
505
+ )}
506
+ {newContext === "new" && (newProvider === "codex" || newProvider === "claude") && (
507
+ <>
508
+ <div
509
+ className={`worktree-create-field ${
510
+ showReasoningField ? "" : "is-full"
511
+ }`}
512
+ >
513
+ <label>{t("Model")}</label>
514
+ <select
515
+ value={newModel}
516
+ onChange={(e) => setNewModel(e.target.value)}
517
+ disabled={providerState.loading || availableModels.length === 0}
518
+ >
519
+ <option value="">{t("Default model")}</option>
520
+ {availableModels.map((model) => (
521
+ <option key={model.id} value={model.model}>
522
+ {model.displayName || model.model}
523
+ </option>
524
+ ))}
525
+ </select>
526
+ {providerState.error && (
527
+ <div className="worktree-field-error">{providerState.error}</div>
528
+ )}
529
+ </div>
530
+ {showReasoningField && (
531
+ <div className="worktree-create-field">
532
+ <label>{t("Reasoning")}</label>
533
+ <select
534
+ value={newReasoningEffort}
535
+ onChange={(e) => setNewReasoningEffort(e.target.value)}
536
+ disabled={providerState.loading || !selectedModelDetails}
537
+ >
538
+ <option value="">{t("Default reasoning")}</option>
539
+ {(selectedModelDetails?.supportedReasoningEfforts || []).map(
540
+ (effort) => (
541
+ <option
542
+ key={effort.reasoningEffort}
543
+ value={effort.reasoningEffort}
544
+ >
545
+ {effort.reasoningEffort}
546
+ </option>
547
+ )
548
+ )}
549
+ </select>
550
+ </div>
551
+ )}
552
+ </>
553
+ )}
554
+ <div className="worktree-create-field worktree-toggle-field">
555
+ <label className="worktree-toggle">
556
+ <input
557
+ type="checkbox"
558
+ checked={newInternetAccess}
559
+ onChange={(e) => {
560
+ const checked = e.target.checked;
561
+ setNewInternetAccess(checked);
562
+ if (!checked) {
563
+ setNewDenyGitCredentialsAccess(false);
564
+ }
565
+ }}
566
+ />
567
+ <span>{t("Internet access")}</span>
568
+ </label>
569
+ </div>
570
+ {newInternetAccess && deploymentMode !== "mono_user" && (
571
+ <div className="worktree-create-field worktree-toggle-field">
572
+ <label className="worktree-toggle">
573
+ <input
574
+ type="checkbox"
575
+ checked={newDenyGitCredentialsAccess}
576
+ onChange={(e) => setNewDenyGitCredentialsAccess(e.target.checked)}
577
+ />
578
+ <span>{t("Deny git credentials access")}</span>
579
+ </label>
580
+ </div>
581
+ )}
582
+ </div>
583
+ <div className="worktree-create-actions">
584
+ <button
585
+ className="worktree-btn-cancel"
586
+ onClick={() => setCreateDialogOpen(false)}
587
+ >
588
+ {t("Cancel")}
589
+ </button>
590
+ <button
591
+ className="worktree-btn-create"
592
+ onClick={handleCreate}
593
+ disabled={!isBranchValid || (newContext === "fork" && !newSourceWorktree)}
594
+ >
595
+ {t("Create")}
596
+ </button>
597
+ </div>
598
+ </div>
599
+ </div>
600
+ )}
601
+
602
+ </div>
603
+ );
604
+ }