@chrysb/alphaclaw 0.9.0-beta.7 → 0.9.0

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 (66) hide show
  1. package/bin/alphaclaw.js +25 -25
  2. package/lib/cli/git-runtime.js +97 -0
  3. package/lib/public/css/chat.css +0 -12
  4. package/lib/public/css/explorer.css +48 -0
  5. package/lib/public/css/shell.css +149 -0
  6. package/lib/public/css/tailwind.generated.css +1 -1
  7. package/lib/public/css/theme.css +265 -0
  8. package/lib/public/dist/app.bundle.js +2770 -2762
  9. package/lib/public/js/app.js +26 -14
  10. package/lib/public/js/components/agents-tab/create-channel-modal.js +259 -59
  11. package/lib/public/js/components/gateway.js +0 -286
  12. package/lib/public/js/components/general/index.js +0 -7
  13. package/lib/public/js/components/icons.js +26 -25
  14. package/lib/public/js/components/modal-shell.js +1 -1
  15. package/lib/public/js/components/models-tab/provider-auth-card.js +60 -49
  16. package/lib/public/js/components/models-tab/use-models.js +74 -9
  17. package/lib/public/js/components/models.js +52 -37
  18. package/lib/public/js/components/onboarding/use-welcome-codex.js +34 -24
  19. package/lib/public/js/components/onboarding/welcome-config.js +76 -10
  20. package/lib/public/js/components/onboarding/welcome-form-step.js +2 -7
  21. package/lib/public/js/components/onboarding/welcome-header.js +12 -14
  22. package/lib/public/js/components/onboarding/welcome-setup-step.js +3 -3
  23. package/lib/public/js/components/providers.js +53 -42
  24. package/lib/public/js/components/routes/chat-route.js +2 -9
  25. package/lib/public/js/components/routes/general-route.js +0 -6
  26. package/lib/public/js/components/routes/index.js +0 -1
  27. package/lib/public/js/components/routes/watchdog-route.js +0 -6
  28. package/lib/public/js/components/sidebar.js +21 -7
  29. package/lib/public/js/components/theme-toggle.js +113 -0
  30. package/lib/public/js/components/update-modal.js +174 -51
  31. package/lib/public/js/components/watchdog-tab/index.js +0 -6
  32. package/lib/public/js/components/welcome/index.js +0 -2
  33. package/lib/public/js/components/welcome/use-welcome.js +101 -36
  34. package/lib/public/js/hooks/use-app-shell-controller.js +16 -33
  35. package/lib/public/js/lib/api.js +0 -28
  36. package/lib/public/js/lib/app-navigation.js +0 -2
  37. package/lib/public/js/lib/channel-provider-availability.js +1 -2
  38. package/lib/public/js/lib/codex-oauth-window.js +22 -0
  39. package/lib/public/js/lib/model-catalog.js +20 -0
  40. package/lib/public/js/lib/storage-keys.js +1 -1
  41. package/lib/public/login.html +8 -4
  42. package/lib/public/setup.html +9 -0
  43. package/lib/scripts/git +47 -1
  44. package/lib/server/agents/channels.js +1 -4
  45. package/lib/server/alphaclaw-version.js +590 -132
  46. package/lib/server/constants.js +5 -0
  47. package/lib/server/db/webhooks/index.js +48 -8
  48. package/lib/server/exec-defaults-config.js +163 -0
  49. package/lib/server/init/register-server-routes.js +0 -8
  50. package/lib/server/init/server-lifecycle.js +2 -0
  51. package/lib/server/model-catalog-cache.js +251 -0
  52. package/lib/server/onboarding/index.js +5 -0
  53. package/lib/server/routes/models.js +14 -23
  54. package/lib/server/routes/nodes.js +9 -23
  55. package/lib/server/routes/system.js +3 -16
  56. package/lib/server/routes/webhooks.js +12 -1
  57. package/lib/server/startup.js +8 -0
  58. package/lib/server/watchdog-notify.js +172 -55
  59. package/lib/server.js +17 -2
  60. package/package.json +2 -2
  61. package/patches/openclaw+2026.4.9.patch +13 -0
  62. package/lib/public/js/components/mcp-tab/index.js +0 -237
  63. package/lib/public/js/components/routes/mcp-route.js +0 -7
  64. package/lib/server/mcp-bridge.js +0 -158
  65. package/lib/server/routes/mcp.js +0 -292
  66. package/patches/openclaw+2026.3.28.patch +0 -13
@@ -1,10 +1,7 @@
1
1
  import { h } from "preact";
2
2
  import { useEffect, useMemo, useState } from "preact/hooks";
3
3
  import htm from "htm";
4
- import { fetchOpenclawVersion, updateOpenclaw } from "../lib/api.js";
5
4
  import { UpdateActionButton } from "./update-action-button.js";
6
- import { ConfirmDialog } from "./confirm-dialog.js";
7
- import { showToast } from "./toast.js";
8
5
  const html = htm.bind(h);
9
6
 
10
7
  const formatDuration = (ms) => {
@@ -21,287 +18,14 @@ const formatDuration = (ms) => {
21
18
  return `${seconds}s`;
22
19
  };
23
20
 
24
- const VersionRow = ({
25
- label,
26
- currentVersion,
27
- fetchVersion,
28
- applyUpdate,
29
- updateInProgress = false,
30
- onActionComplete = () => {},
31
- }) => {
32
- const [checking, setChecking] = useState(false);
33
- const [version, setVersion] = useState(currentVersion || null);
34
- const [latestVersion, setLatestVersion] = useState(null);
35
- const [hasUpdate, setHasUpdate] = useState(false);
36
- const [error, setError] = useState("");
37
- const [hasViewedChangelog, setHasViewedChangelog] = useState(false);
38
- const [confirmWithoutChangelogOpen, setConfirmWithoutChangelogOpen] =
39
- useState(false);
40
- const simulateUpdate = (() => {
41
- try {
42
- const params = new URLSearchParams(window.location.search);
43
- return params.get("simulateUpdate") === "1";
44
- } catch {
45
- return false;
46
- }
47
- })();
48
- const simulatedVersion = (() => {
49
- if (!simulateUpdate) return null;
50
- try {
51
- const params = new URLSearchParams(window.location.search);
52
- return params.get("simulateVersion") || "v0.0.0-preview";
53
- } catch {
54
- return "v0.0.0-preview";
55
- }
56
- })();
57
- const effectiveHasUpdate = simulateUpdate || hasUpdate;
58
- const effectiveLatestVersion = simulatedVersion || latestVersion;
59
- const isUpdateActionActive = updateInProgress || effectiveHasUpdate;
60
- const updateIdleLabel = effectiveLatestVersion
61
- ? `Update to ${effectiveLatestVersion}`
62
- : "Update";
63
- const changelogUrl = "https://github.com/openclaw/openclaw/tags";
64
- const showMobileUpdateRow = effectiveHasUpdate && effectiveLatestVersion;
65
-
66
- useEffect(() => {
67
- setVersion(currentVersion || null);
68
- }, [currentVersion]);
69
-
70
- useEffect(() => {
71
- let active = true;
72
- const load = async (refresh = false) => {
73
- try {
74
- const data = await fetchVersion(refresh);
75
- if (!active) return;
76
- setVersion(data.currentVersion || currentVersion || null);
77
- setLatestVersion(data.latestVersion || null);
78
- setHasUpdate(!!data.hasUpdate);
79
- setError(data.ok ? "" : data.error || "");
80
- } catch (err) {
81
- if (!active) return;
82
- setError(err.message || "Could not check updates");
83
- }
84
- };
85
- load(false);
86
- return () => {
87
- active = false;
88
- };
89
- }, [currentVersion, fetchVersion]);
90
-
91
- useEffect(() => {
92
- if (updateInProgress) return () => {};
93
- let active = true;
94
- const timeoutId = setTimeout(async () => {
95
- try {
96
- const data = await fetchVersion(true);
97
- if (!active) return;
98
- setVersion(data.currentVersion || currentVersion || null);
99
- setLatestVersion(data.latestVersion || null);
100
- setHasUpdate(!!data.hasUpdate);
101
- setError(data.ok ? "" : data.error || "");
102
- } catch {}
103
- }, 1200);
104
- return () => {
105
- active = false;
106
- clearTimeout(timeoutId);
107
- };
108
- }, [updateInProgress, currentVersion, fetchVersion]);
109
-
110
- useEffect(() => {
111
- if (!effectiveHasUpdate || !effectiveLatestVersion) {
112
- setHasViewedChangelog(false);
113
- return;
114
- }
115
- setHasViewedChangelog(false);
116
- }, [effectiveHasUpdate, effectiveLatestVersion]);
117
-
118
- const runAction = async () => {
119
- const isUpdateAction = !!effectiveHasUpdate;
120
- const busy = isUpdateActionActive ? checking || updateInProgress : checking;
121
- if (busy) return;
122
- setChecking(true);
123
- setError("");
124
- try {
125
- const data = isUpdateAction
126
- ? await applyUpdate()
127
- : await fetchVersion(true);
128
- setVersion(data.currentVersion || version);
129
- setLatestVersion(data.latestVersion || null);
130
- setHasUpdate(!!data.hasUpdate);
131
- setError(data.ok ? "" : data.error || "");
132
- if (isUpdateAction) {
133
- if (!data.ok) {
134
- showToast(data.error || `${label} update failed`, "error");
135
- } else if (data.updated || data.restarting) {
136
- showToast(
137
- data.restarting
138
- ? `${label} updated — restarting...`
139
- : `Updated ${label} to ${data.currentVersion}`,
140
- "success",
141
- );
142
- } else {
143
- showToast(`Already at latest ${label} version`, "success");
144
- }
145
- } else if (data.hasUpdate && data.latestVersion) {
146
- showToast(
147
- `${label} update available: ${data.latestVersion}`,
148
- "warning",
149
- );
150
- } else {
151
- showToast(`${label} is up to date`, "success");
152
- }
153
- await onActionComplete({
154
- type: isUpdateAction ? "update" : "check",
155
- ok: !!data?.ok,
156
- result: data,
157
- });
158
- } catch (err) {
159
- setError(
160
- err.message ||
161
- (isUpdateAction
162
- ? `Could not update ${label}`
163
- : "Could not check updates"),
164
- );
165
- showToast(
166
- isUpdateAction
167
- ? `Could not update ${label}`
168
- : "Could not check updates",
169
- "error",
170
- );
171
- await onActionComplete({
172
- type: isUpdateAction ? "update" : "check",
173
- ok: false,
174
- error: err,
175
- });
176
- } finally {
177
- setChecking(false);
178
- }
179
- };
180
-
181
- const handleAction = () => {
182
- const busy = isUpdateActionActive ? checking || updateInProgress : checking;
183
- if (busy) return;
184
- if (effectiveHasUpdate && effectiveLatestVersion && !hasViewedChangelog) {
185
- setConfirmWithoutChangelogOpen(true);
186
- return;
187
- }
188
- runAction();
189
- };
190
-
191
- const handleConfirmWithoutChangelog = () => {
192
- setConfirmWithoutChangelogOpen(false);
193
- runAction();
194
- };
195
-
196
- const updateButtonLoading = isUpdateActionActive
197
- ? checking || updateInProgress
198
- : checking;
199
-
200
- return html`
201
- <div class="flex items-center justify-between gap-3">
202
- <div class="min-w-0">
203
- <p class="text-xs text-body truncate">
204
- <span class="text-fg-muted">${label}</span>${" "}${version
205
- ? `${version}`
206
- : "..."}
207
- </p>
208
- ${error &&
209
- effectiveHasUpdate &&
210
- html`<div
211
- class="mt-1 text-xs text-status-error bg-status-error-bg border border-status-error-border rounded-lg px-2 py-1"
212
- >
213
- ${error}
214
- </div>`}
215
- </div>
216
- <div class="flex items-center gap-3 shrink-0">
217
- ${effectiveHasUpdate &&
218
- effectiveLatestVersion &&
219
- html`
220
- <a
221
- href=${changelogUrl}
222
- target="_blank"
223
- rel="noreferrer"
224
- onclick=${() => setHasViewedChangelog(true)}
225
- class="hidden md:inline text-xs text-fg-muted hover:text-body transition-colors"
226
- >View changelog</a
227
- >
228
- `}
229
- ${showMobileUpdateRow
230
- ? html`
231
- <${UpdateActionButton}
232
- onClick=${handleAction}
233
- loading=${updateButtonLoading}
234
- warning=${isUpdateActionActive}
235
- idleLabel=${isUpdateActionActive
236
- ? updateIdleLabel
237
- : "Check updates"}
238
- loadingLabel=${isUpdateActionActive
239
- ? "Updating..."
240
- : "Checking..."}
241
- className="hidden md:inline-flex"
242
- />
243
- `
244
- : html`
245
- <${UpdateActionButton}
246
- onClick=${handleAction}
247
- loading=${updateButtonLoading}
248
- warning=${isUpdateActionActive}
249
- idleLabel=${isUpdateActionActive
250
- ? updateIdleLabel
251
- : "Check updates"}
252
- loadingLabel=${isUpdateActionActive
253
- ? "Updating..."
254
- : "Checking..."}
255
- />
256
- `}
257
- </div>
258
- </div>
259
- ${showMobileUpdateRow &&
260
- html`
261
- <div class="mt-2 md:hidden flex items-center gap-2">
262
- <a
263
- href=${changelogUrl}
264
- target="_blank"
265
- rel="noreferrer"
266
- onclick=${() => setHasViewedChangelog(true)}
267
- class="inline-flex items-center justify-center flex-1 h-9 text-xs rounded-lg border border-border text-fg-muted hover:text-body hover:border-fg-muted transition-colors"
268
- >View changelog</a
269
- >
270
- <${UpdateActionButton}
271
- onClick=${handleAction}
272
- loading=${updateButtonLoading}
273
- warning=${isUpdateActionActive}
274
- idleLabel=${updateIdleLabel}
275
- loadingLabel="Updating..."
276
- className="flex-1 h-9 px-3"
277
- />
278
- </div>
279
- `}
280
- <${ConfirmDialog}
281
- visible=${confirmWithoutChangelogOpen}
282
- title="Update without changelog?"
283
- message="Are you sure you want to update without viewing the changelog?"
284
- confirmLabel=${`Update to ${effectiveLatestVersion || "latest"}`}
285
- cancelLabel="Cancel"
286
- confirmTone="warning"
287
- onCancel=${() => setConfirmWithoutChangelogOpen(false)}
288
- onConfirm=${handleConfirmWithoutChangelog}
289
- />
290
- `;
291
- };
292
-
293
21
  export const Gateway = ({
294
22
  status,
295
- openclawVersion,
296
23
  restarting = false,
297
24
  onRestart,
298
25
  watchdogStatus = null,
299
26
  onOpenWatchdog,
300
27
  onRepair,
301
28
  repairing = false,
302
- openclawUpdateInProgress = false,
303
- onOpenclawVersionActionComplete = () => {},
304
- onOpenclawUpdate = updateOpenclaw,
305
29
  }) => {
306
30
  const [nowMs, setNowMs] = useState(() => Date.now());
307
31
  const isRunning = status === "running" && !restarting;
@@ -436,15 +160,5 @@ export const Gateway = ({
436
160
  : null}
437
161
  </div>
438
162
  </div>
439
- <div class="mt-3">
440
- <${VersionRow}
441
- label="OpenClaw"
442
- currentVersion=${openclawVersion}
443
- fetchVersion=${fetchOpenclawVersion}
444
- applyUpdate=${onOpenclawUpdate}
445
- updateInProgress=${openclawUpdateInProgress}
446
- onActionComplete=${onOpenclawVersionActionComplete}
447
- />
448
- </div>
449
163
  </div>`;
450
164
  };
@@ -28,9 +28,6 @@ export const GeneralTab = ({
28
28
  restartingGateway = false,
29
29
  onRestartGateway = () => {},
30
30
  restartSignal = 0,
31
- openclawUpdateInProgress = false,
32
- onOpenclawVersionActionComplete = () => {},
33
- onOpenclawUpdate = () => {},
34
31
  onRestartRequired = () => {},
35
32
  onDismissDoctorWarning = () => {},
36
33
  }) => {
@@ -47,16 +44,12 @@ export const GeneralTab = ({
47
44
  <div class="space-y-4">
48
45
  <${Gateway}
49
46
  status=${state.gatewayStatus}
50
- openclawVersion=${state.openclawVersion}
51
47
  restarting=${restartingGateway}
52
48
  onRestart=${onRestartGateway}
53
49
  watchdogStatus=${state.watchdogStatus}
54
50
  onOpenWatchdog=${() => onSwitchTab("watchdog")}
55
51
  onRepair=${actions.handleWatchdogRepair}
56
52
  repairing=${state.repairingWatchdog}
57
- openclawUpdateInProgress=${openclawUpdateInProgress}
58
- onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
59
- onOpenclawUpdate=${onOpenclawUpdate}
60
53
  />
61
54
  <${GeneralDoctorWarning}
62
55
  doctorStatus=${state.doctorStatus}
@@ -321,19 +321,6 @@ export const SignalTowerLineIcon = ({ className = "" }) => html`
321
321
  </svg>
322
322
  `;
323
323
 
324
- export const WebhookLineIcon = ({ className = "" }) => html`
325
- <svg
326
- class=${className}
327
- viewBox="0 0 24 24"
328
- fill="currentColor"
329
- aria-hidden="true"
330
- >
331
- <path
332
- d="M8.86874 14.1392C8.6556 14.4912 8.55014 14.7778 8.72043 15.2253C9.1905 16.4613 8.52737 17.664 7.28097 17.9905C6.10556 18.2985 4.96035 17.526 4.72713 16.2676C4.52048 15.1537 5.38488 14.0617 6.61294 13.8877C6.67963 13.8781 6.74717 13.874 6.83351 13.8688C6.88044 13.866 6.93293 13.8628 6.99384 13.8582L8.86194 10.7257C7.687 9.55742 6.98767 8.19164 7.14246 6.49936C7.25188 5.30308 7.72226 4.26933 8.58208 3.42201C10.2288 1.79945 12.7411 1.53667 14.68 2.78212C16.5423 3.97841 17.3951 6.30867 16.6681 8.30311L14.9611 7.84C15.1895 6.73115 15.0206 5.73536 14.2727 4.88234C13.7786 4.31914 13.1446 4.02394 12.4236 3.91516C10.9783 3.69681 9.55922 4.6254 9.13816 6.04399C8.66019 7.65406 9.38355 8.96924 11.3603 9.96029C10.5311 11.3541 9.70859 12.7518 8.86874 14.1392ZM13.7838 8.27337C14.3816 9.32798 14.9886 10.3986 15.5902 11.4593C18.631 10.5186 20.9237 12.2018 21.7462 14.004C22.7396 16.1809 22.0605 18.7593 20.1094 20.1023C18.1067 21.481 15.5741 21.2454 13.7997 19.4744L15.1919 18.3094C16.9444 19.4445 18.4772 19.3911 19.6151 18.047C20.5855 16.9003 20.5644 15.1906 19.5659 14.068C18.4136 12.7726 16.8701 12.7331 15.0044 13.9767C14.2305 12.6037 13.443 11.2413 12.6936 9.85845C12.4409 9.39233 12.1618 9.12196 11.5923 9.0233C10.6411 8.85839 10.027 8.04157 9.99016 7.12642C9.95395 6.22138 10.4871 5.4033 11.3205 5.08455C12.146 4.7688 13.1148 5.02367 13.6701 5.72554C14.1239 6.29901 14.2681 6.94443 14.0293 7.65167C13.9843 7.7852 13.9304 7.91584 13.8713 8.05885C13.8431 8.12694 13.8138 8.19801 13.7838 8.27337ZM11.552 16.895H15.2126C15.2636 16.963 15.3113 17.0303 15.3579 17.0959C15.4551 17.233 15.5474 17.3632 15.6551 17.4788C16.4304 18.3077 17.7395 18.3489 18.5682 17.5795C19.4271 16.7821 19.466 15.4426 18.6544 14.6101C17.8602 13.7955 16.5029 13.7177 15.7655 14.5802C15.3176 15.1044 14.8586 15.166 14.2641 15.1567C12.7414 15.1332 11.2177 15.149 9.69524 15.149C9.79406 17.2909 8.98436 18.6255 7.37841 18.9424C5.80582 19.2528 4.3575 18.4504 3.84759 16.9864C3.26842 15.3229 3.98467 13.9925 6.05421 12.9366C5.89847 12.3725 5.74115 11.8016 5.58541 11.236C3.32977 11.7276 1.63749 13.916 1.8122 16.378C1.96652 18.5514 3.71968 20.4815 5.86369 20.8273C7.02819 21.0153 8.12233 20.82 9.13741 20.2442C10.4433 19.5032 11.2011 18.3381 11.552 16.895Z"
333
- />
334
- </svg>
335
- `;
336
-
337
324
  export const GitBranchLineIcon = ({ className = "" }) => html`
338
325
  <svg
339
326
  class=${className}
@@ -521,48 +508,62 @@ export const EyeLineIcon = ({ className = "" }) => html`
521
508
  </svg>
522
509
  `;
523
510
 
524
- export const FullscreenLineIcon = ({ className = "" }) => html`
511
+ export const SunIcon = ({ className = "" }) => html`
525
512
  <svg
526
513
  class=${className}
527
514
  viewBox="0 0 24 24"
528
- fill="currentColor"
515
+ fill="none"
516
+ stroke="currentColor"
517
+ stroke-width="2"
518
+ stroke-linecap="round"
519
+ stroke-linejoin="round"
529
520
  aria-hidden="true"
530
521
  >
531
- <path d="M8 3V5H4V9H2V3H8ZM2 21V15H4V19H8V21H2ZM22 21H16V19H20V15H22V21ZM22 9H20V5H16V3H22V9Z" />
522
+ <circle cx="12" cy="12" r="5" />
523
+ <line x1="12" y1="1" x2="12" y2="3" />
524
+ <line x1="12" y1="21" x2="12" y2="23" />
525
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
526
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
527
+ <line x1="1" y1="12" x2="3" y2="12" />
528
+ <line x1="21" y1="12" x2="23" y2="12" />
529
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
530
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
532
531
  </svg>
533
532
  `;
534
533
 
535
- export const ComputerLineIcon = ({ className = "" }) => html`
534
+ export const MoonIcon = ({ className = "" }) => html`
536
535
  <svg
537
536
  class=${className}
538
537
  viewBox="0 0 24 24"
539
- fill="currentColor"
538
+ fill="none"
539
+ stroke="currentColor"
540
+ stroke-width="2"
541
+ stroke-linecap="round"
542
+ stroke-linejoin="round"
540
543
  aria-hidden="true"
541
544
  >
542
- <path d="M4 16H20V5H4V16ZM13 18V20H17V22H7V20H11V18H2.9918C2.44405 18 2 17.5511 2 16.9925V4.00748C2 3.45107 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44892 22 4.00748V16.9925C22 17.5489 21.5447 18 21.0082 18H13Z" />
545
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
543
546
  </svg>
544
547
  `;
545
548
 
546
- export const PlugLineIcon = ({ className = "" }) => html`
549
+ export const FullscreenLineIcon = ({ className = "" }) => html`
547
550
  <svg
548
551
  class=${className}
549
552
  viewBox="0 0 24 24"
550
553
  fill="currentColor"
551
554
  aria-hidden="true"
552
555
  >
553
- <path d="M13 18V20H17V22H7V20H11V18H6C5.44772 18 5 17.5523 5 17V10.0001H3V8.00005H5V6C5 5.44772 5.44772 5 6 5H9V2H11V5H13V2H15V5H18C18.5523 5 19 5.44772 19 6V8.00005H21V10.0001H19V17C19 17.5523 18.5523 18 18 18H13ZM7 7V16H17V7H7Z" />
556
+ <path d="M8 3V5H4V9H2V3H8ZM2 21V15H4V19H8V21H2ZM22 21H16V19H20V15H22V21ZM22 9H20V5H16V3H22V9Z" />
554
557
  </svg>
555
558
  `;
556
559
 
557
- export const LinksLineIcon = ({ className = "" }) => html`
560
+ export const ComputerLineIcon = ({ className = "" }) => html`
558
561
  <svg
559
562
  class=${className}
560
563
  viewBox="0 0 24 24"
561
564
  fill="currentColor"
562
565
  aria-hidden="true"
563
566
  >
564
- <path
565
- d="M13.0607 8.11097L14.4749 9.52518C17.2086 12.2589 17.2086 16.691 14.4749 19.4247L14.1214 19.7782C11.3877 22.5119 6.95555 22.5119 4.22188 19.7782C1.48821 17.0446 1.48821 12.6124 4.22188 9.87874L5.6361 11.293C3.68348 13.2456 3.68348 16.4114 5.6361 18.364C7.58872 20.3166 10.7545 20.3166 12.7072 18.364L13.0607 18.0105C15.0133 16.0578 15.0133 12.892 13.0607 10.9394L11.6465 9.52518L13.0607 8.11097ZM19.7782 14.1214L18.364 12.7072C20.3166 10.7545 20.3166 7.58872 18.364 5.6361C16.4114 3.68348 13.2456 3.68348 11.293 5.6361L10.9394 5.98965C8.98678 7.94227 8.98678 11.1081 10.9394 13.0607L12.3536 14.4749L10.9394 15.8891L9.52518 14.4749C6.79151 11.7413 6.79151 7.30911 9.52518 4.57544L9.87874 4.22188C12.6124 1.48821 17.0446 1.48821 19.7782 4.22188C22.5119 6.95555 22.5119 11.3877 19.7782 14.1214Z"
566
- />
567
+ <path d="M4 16H20V5H4V16ZM13 18V20H17V22H7V20H11V18H2.9918C2.44405 18 2 17.5511 2 16.9925V4.00748C2 3.45107 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44892 22 4.00748V16.9925C22 17.5489 21.5447 18 21.0082 18H13Z" />
567
568
  </svg>
568
569
  `;
@@ -29,7 +29,7 @@ export const ModalShell = ({
29
29
  return createPortal(
30
30
  html`
31
31
  <div
32
- class="fixed inset-0 bg-overlay flex items-center justify-center p-4 z-50"
32
+ class="fixed inset-0 bg-overlay flex items-start justify-center overflow-y-auto p-4 sm:items-center z-50"
33
33
  onclick=${(event) => {
34
34
  if (closeOnOverlayClick && event.target === event.currentTarget) onClose?.();
35
35
  }}
@@ -5,6 +5,10 @@ import { Badge } from "../badge.js";
5
5
  import { SecretInput } from "../secret-input.js";
6
6
  import { ActionButton } from "../action-button.js";
7
7
  import { exchangeCodexOAuth, disconnectCodex } from "../../lib/api.js";
8
+ import {
9
+ isCodexAuthCallbackMessage,
10
+ openCodexAuthWindow,
11
+ } from "../../lib/codex-oauth-window.js";
8
12
  import { showToast } from "../toast.js";
9
13
  import {
10
14
  kProviderAuthFields,
@@ -108,6 +112,7 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
108
112
  const [authWaiting, setAuthWaiting] = useState(false);
109
113
  const [manualInput, setManualInput] = useState("");
110
114
  const [exchanging, setExchanging] = useState(false);
115
+ const exchangeInFlightRef = useRef(false);
111
116
  const popupPollRef = useRef(null);
112
117
 
113
118
  useEffect(
@@ -117,6 +122,30 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
117
122
  [],
118
123
  );
119
124
 
125
+ const submitAuthInput = async (input) => {
126
+ const normalizedInput = String(input || "").trim();
127
+ if (!normalizedInput || exchangeInFlightRef.current) return;
128
+ exchangeInFlightRef.current = true;
129
+ setManualInput(normalizedInput);
130
+ setExchanging(true);
131
+ try {
132
+ const result = await exchangeCodexOAuth(normalizedInput);
133
+ if (!result.ok)
134
+ throw new Error(result.error || "Codex OAuth exchange failed");
135
+ setManualInput("");
136
+ showToast("Codex connected", "success");
137
+ setAuthStarted(false);
138
+ setAuthWaiting(false);
139
+ await onRefreshCodex();
140
+ } catch (err) {
141
+ setAuthWaiting(false);
142
+ showToast(err.message || "Codex OAuth exchange failed", "error");
143
+ } finally {
144
+ exchangeInFlightRef.current = false;
145
+ setExchanging(false);
146
+ }
147
+ };
148
+
120
149
  useEffect(() => {
121
150
  const onMessage = async (e) => {
122
151
  if (e.data?.codex === "success") {
@@ -124,6 +153,8 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
124
153
  setAuthStarted(false);
125
154
  setAuthWaiting(false);
126
155
  await onRefreshCodex();
156
+ } else if (isCodexAuthCallbackMessage(e.data)) {
157
+ await submitAuthInput(e.data.input);
127
158
  } else if (e.data?.codex === "error") {
128
159
  showToast(
129
160
  `Codex auth failed: ${e.data.message || "unknown error"}`,
@@ -133,19 +164,14 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
133
164
  };
134
165
  window.addEventListener("message", onMessage);
135
166
  return () => window.removeEventListener("message", onMessage);
136
- }, [onRefreshCodex]);
167
+ }, [onRefreshCodex, submitAuthInput]);
137
168
 
138
169
  const startAuth = () => {
139
170
  setAuthStarted(true);
140
171
  setAuthWaiting(true);
141
- const popup = window.open(
142
- "/auth/codex/start",
143
- "codex-auth",
144
- "popup=yes,width=640,height=780",
145
- );
172
+ const popup = openCodexAuthWindow();
146
173
  if (!popup || popup.closed) {
147
174
  setAuthWaiting(false);
148
- window.location.href = "/auth/codex/start";
149
175
  return;
150
176
  }
151
177
  if (popupPollRef.current) clearInterval(popupPollRef.current);
@@ -159,22 +185,7 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
159
185
  };
160
186
 
161
187
  const completeAuth = async () => {
162
- if (!manualInput.trim() || exchanging) return;
163
- setExchanging(true);
164
- try {
165
- const result = await exchangeCodexOAuth(manualInput.trim());
166
- if (!result.ok)
167
- throw new Error(result.error || "Codex OAuth exchange failed");
168
- setManualInput("");
169
- showToast("Codex connected", "success");
170
- setAuthStarted(false);
171
- setAuthWaiting(false);
172
- await onRefreshCodex();
173
- } catch (err) {
174
- showToast(err.message || "Codex OAuth exchange failed", "error");
175
- } finally {
176
- setExchanging(false);
177
- }
188
+ await submitAuthInput(manualInput);
178
189
  };
179
190
 
180
191
  const handleDisconnect = async () => {
@@ -198,7 +209,23 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
198
209
  ? html`<${Badge} tone="success">Connected</${Badge}>`
199
210
  : html`<${Badge} tone="warning">Not connected</${Badge}>`}
200
211
  </div>
201
- ${codexStatus.connected
212
+ ${authStarted
213
+ ? html`
214
+ <div class="flex items-center justify-between gap-2">
215
+ <p class="text-xs text-fg-muted">
216
+ ${authWaiting
217
+ ? "Complete login in the popup. AlphaClaw should finish automatically, but you can paste the redirect URL below if it doesn't."
218
+ : "Paste the redirect URL from your browser to finish connecting."}
219
+ </p>
220
+ <button
221
+ onclick=${startAuth}
222
+ class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary shrink-0"
223
+ >
224
+ Restart
225
+ </button>
226
+ </div>
227
+ `
228
+ : codexStatus.connected
202
229
  ? html`
203
230
  <div class="flex gap-2">
204
231
  <button
@@ -215,32 +242,16 @@ const CodexOAuthSection = ({ codexStatus, onRefreshCodex }) => {
215
242
  </button>
216
243
  </div>
217
244
  `
218
- : !authStarted
245
+ : html`
246
+ <button
247
+ onclick=${startAuth}
248
+ class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-cyan"
249
+ >
250
+ Connect Codex OAuth
251
+ </button>
252
+ `}
253
+ ${authStarted
219
254
  ? html`
220
- <button
221
- onclick=${startAuth}
222
- class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-cyan"
223
- >
224
- Connect Codex OAuth
225
- </button>
226
- `
227
- : html`
228
- <div class="flex items-center justify-between gap-2">
229
- <p class="text-xs text-fg-muted">
230
- ${authWaiting
231
- ? "Complete login in the popup, then paste the redirect URL."
232
- : "Paste the redirect URL from your browser to finish connecting."}
233
- </p>
234
- <button
235
- onclick=${startAuth}
236
- class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary shrink-0"
237
- >
238
- Restart
239
- </button>
240
- </div>
241
- `}
242
- ${!codexStatus.connected && authStarted
243
- ? html`
244
255
  <p class="text-xs text-fg-muted">
245
256
  After login, copy the full redirect URL (starts with
246
257
  <code class="text-xs bg-field px-1 rounded"