@chrysb/alphaclaw 0.5.2 → 0.5.4-beta.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 (35) hide show
  1. package/lib/public/js/components/agent-send-modal.js +11 -25
  2. package/lib/public/js/components/doctor/index.js +6 -5
  3. package/lib/public/js/components/file-tree.js +26 -1
  4. package/lib/public/js/components/file-viewer/constants.js +2 -0
  5. package/lib/public/js/components/file-viewer/index.js +1 -0
  6. package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -1
  7. package/lib/public/js/components/file-viewer/use-file-viewer.js +24 -6
  8. package/lib/public/js/components/file-viewer/utils.js +19 -0
  9. package/lib/public/js/components/google/gmail-setup-wizard.js +117 -50
  10. package/lib/public/js/components/google/index.js +45 -44
  11. package/lib/public/js/components/google/use-gmail-watch.js +2 -2
  12. package/lib/public/js/components/icons.js +13 -0
  13. package/lib/public/js/components/models-tab/index.js +5 -3
  14. package/lib/public/js/components/models-tab/provider-auth-card.js +1 -1
  15. package/lib/public/js/components/onboarding/welcome-form-step.js +9 -1
  16. package/lib/public/js/components/session-select-field.js +72 -0
  17. package/lib/public/js/components/webhooks.js +114 -44
  18. package/lib/public/js/components/welcome/use-welcome.js +41 -20
  19. package/lib/public/js/hooks/use-destination-session-selection.js +85 -0
  20. package/lib/public/js/hooks/useAgentSessions.js +14 -0
  21. package/lib/public/js/lib/api.js +10 -4
  22. package/lib/public/js/lib/clipboard.js +40 -0
  23. package/lib/public/js/lib/model-config.js +2 -2
  24. package/lib/server/auth-profiles.js +3 -0
  25. package/lib/server/constants.js +2 -2
  26. package/lib/server/db/usage/pricing.js +1 -1
  27. package/lib/server/doctor/prompt.js +32 -10
  28. package/lib/server/gmail-watch.js +49 -22
  29. package/lib/server/onboarding/github.js +1 -1
  30. package/lib/server/routes/gmail.js +5 -1
  31. package/lib/server/routes/models.js +84 -1
  32. package/lib/server/routes/system.js +28 -26
  33. package/lib/server/routes/webhooks.js +2 -2
  34. package/lib/server/webhooks.js +32 -4
  35. package/package.json +2 -2
@@ -5,6 +5,7 @@ import { ModalShell } from "./modal-shell.js";
5
5
  import { ActionButton } from "./action-button.js";
6
6
  import { PageHeader } from "./page-header.js";
7
7
  import { CloseIcon } from "./icons.js";
8
+ import { SessionSelectField } from "./session-select-field.js";
8
9
  import { useAgentSessions } from "../hooks/useAgentSessions.js";
9
10
 
10
11
  const html = htm.bind(h);
@@ -83,31 +84,16 @@ export const AgentSendModal = ({
83
84
  </button>
84
85
  `}
85
86
  />
86
- <div class="space-y-2">
87
- <label class="text-xs text-gray-500">Send to session</label>
88
- <select
89
- value=${selectedSessionKey}
90
- onChange=${(event) =>
91
- setSelectedSessionKey(String(event.currentTarget?.value || ""))}
92
- disabled=${loadingSessions || sending}
93
- class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 focus:border-gray-500"
94
- >
95
- ${sessions.length === 0
96
- ? html`<option value="">No sessions available</option>`
97
- : null}
98
- ${sessions.map(
99
- (sessionRow) => html`
100
- <option value=${String(sessionRow?.key || "")}>
101
- ${String(sessionRow?.label || sessionRow?.key || "Session")}
102
- </option>
103
- `,
104
- )}
105
- </select>
106
- ${loadingSessions
107
- ? html`<div class="text-xs text-gray-500">Loading sessions...</div>`
108
- : null}
109
- ${loadError ? html`<div class="text-xs text-red-400">${loadError}</div>` : null}
110
- </div>
87
+ <${SessionSelectField}
88
+ label="Send to session"
89
+ sessions=${sessions}
90
+ selectedSessionKey=${selectedSessionKey}
91
+ onChangeSessionKey=${setSelectedSessionKey}
92
+ disabled=${loadingSessions || sending}
93
+ loading=${loadingSessions}
94
+ error=${loadError}
95
+ emptyOptionLabel="No sessions available"
96
+ />
111
97
  <div class="space-y-2">
112
98
  <label class="text-xs text-gray-500">${messageLabel}</label>
113
99
  <textarea
@@ -523,11 +523,12 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
523
523
  ${selectedRunIsInProgress
524
524
  ? html`
525
525
  <div class="ac-surface-inset rounded-xl p-4">
526
- <div class="text-xs leading-5 text-gray-400">
527
- <span
528
- >Run in progress. Findings will appear when analysis
529
- completes.</span
530
- >
526
+ <div class="flex items-center gap-2 text-xs leading-5 text-gray-400">
527
+ <${LoadingSpinner} className="h-3.5 w-3.5" />
528
+ <span>
529
+ Run in progress. Findings will appear when analysis
530
+ completes.
531
+ </span>
531
532
  </div>
532
533
  </div>
533
534
  `
@@ -42,10 +42,12 @@ import {
42
42
  FolderAddLineIcon,
43
43
  DeleteBinLineIcon,
44
44
  DownloadLineIcon,
45
+ FileCopyLineIcon,
45
46
  } from "./icons.js";
46
47
  import { LoadingSpinner } from "./loading-spinner.js";
47
48
  import { ConfirmDialog } from "./confirm-dialog.js";
48
49
  import { showToast } from "./toast.js";
50
+ import { copyTextToClipboard } from "../lib/clipboard.js";
49
51
 
50
52
  const html = htm.bind(h);
51
53
  const kTreeIndentPx = 9;
@@ -226,6 +228,7 @@ const TreeContextMenu = ({
226
228
  isLocked,
227
229
  onNewFile,
228
230
  onNewFolder,
231
+ onCopyPath,
229
232
  onDownload,
230
233
  onDelete,
231
234
  onClose,
@@ -255,6 +258,7 @@ const TreeContextMenu = ({
255
258
  const isRoot = targetType === "root";
256
259
  const contextFolder = isFolder ? targetPath : "";
257
260
  const canCreate = !isLocked && (isFolder || isRoot);
261
+ const canCopyPath = Boolean((isFolder || isFile) && targetPath);
258
262
  const canDownload = isFile && targetPath;
259
263
  const canDelete = !isLocked && (isFolder || isFile) && targetPath;
260
264
 
@@ -282,11 +286,22 @@ const TreeContextMenu = ({
282
286
  </button>
283
287
  `
284
288
  : null}
285
- ${canDownload || canDelete
289
+ ${canCopyPath || canDownload || canDelete
286
290
  ? html`
287
291
  ${canCreate
288
292
  ? html`<div class="tree-context-menu-sep"></div>`
289
293
  : null}
294
+ ${canCopyPath
295
+ ? html`
296
+ <button
297
+ class="tree-context-menu-item"
298
+ onclick=${() => { onCopyPath(targetPath); onClose(); }}
299
+ >
300
+ <${FileCopyLineIcon} className="tree-context-menu-icon" />
301
+ <span>Copy Path</span>
302
+ </button>
303
+ `
304
+ : null}
290
305
  ${canDownload
291
306
  ? html`
292
307
  <button
@@ -970,6 +985,15 @@ export const FileTree = ({
970
985
  }
971
986
  };
972
987
 
988
+ const copyPath = async (targetPath) => {
989
+ const copied = await copyTextToClipboard(targetPath);
990
+ if (copied) {
991
+ showToast("Path copied", "success");
992
+ return;
993
+ }
994
+ showToast("Could not copy path", "error");
995
+ };
996
+
973
997
  const handleDragDrop = async (action, sourcePath, targetFolder) => {
974
998
  if (action === "start") {
975
999
  setDragSourcePath(sourcePath);
@@ -1236,6 +1260,7 @@ export const FileTree = ({
1236
1260
  isLocked=${!!contextMenu.isLocked}
1237
1261
  onNewFile=${(folder) => requestCreate(folder, "file")}
1238
1262
  onNewFolder=${(folder) => requestCreate(folder, "folder")}
1263
+ onCopyPath=${copyPath}
1239
1264
  onDownload=${requestDownload}
1240
1265
  onDelete=${requestDelete}
1241
1266
  onClose=${closeContextMenu}
@@ -5,3 +5,5 @@ export {
5
5
  export const kLoadingIndicatorDelayMs = 1000;
6
6
  export const kFileRefreshIntervalMs = 5000;
7
7
  export const kSqlitePageSize = 50;
8
+ export const kLargeFileSimpleEditorCharThreshold = 250000;
9
+ export const kLargeFileSimpleEditorLineThreshold = 5000;
@@ -146,6 +146,7 @@ export const FileViewer = ({
146
146
  editorLineNumbers=${derived.editorLineNumbers}
147
147
  editorLineNumbersRef=${refs.editorLineNumbersRef}
148
148
  editorLineNumberRowRefs=${refs.editorLineNumberRowRefs}
149
+ shouldUseHighlightedEditor=${derived.shouldUseHighlightedEditor}
149
150
  highlightedEditorLines=${derived.highlightedEditorLines}
150
151
  editorHighlightRef=${refs.editorHighlightRef}
151
152
  editorHighlightLineRefs=${refs.editorHighlightLineRefs}
@@ -12,6 +12,7 @@ export const MarkdownSplitView = ({
12
12
  editorLineNumbers,
13
13
  editorLineNumbersRef,
14
14
  editorLineNumberRowRefs,
15
+ shouldUseHighlightedEditor,
15
16
  highlightedEditorLines,
16
17
  editorHighlightRef,
17
18
  editorHighlightLineRefs,
@@ -37,7 +38,7 @@ export const MarkdownSplitView = ({
37
38
  editorLineNumbers=${editorLineNumbers}
38
39
  editorLineNumbersRef=${editorLineNumbersRef}
39
40
  editorLineNumberRowRefs=${editorLineNumberRowRefs}
40
- shouldUseHighlightedEditor=${true}
41
+ shouldUseHighlightedEditor=${shouldUseHighlightedEditor}
41
42
  highlightedEditorLines=${highlightedEditorLines}
42
43
  editorHighlightRef=${editorHighlightRef}
43
44
  editorHighlightLineRefs=${editorHighlightLineRefs}
@@ -18,9 +18,14 @@ import {
18
18
  normalizeBrowsePolicyPath,
19
19
  } from "../../lib/browse-file-policies.js";
20
20
  import { showToast } from "../toast.js";
21
- import { kFileViewerModeStorageKey, kLoadingIndicatorDelayMs } from "./constants.js";
21
+ import {
22
+ kFileViewerModeStorageKey,
23
+ kLargeFileSimpleEditorCharThreshold,
24
+ kLargeFileSimpleEditorLineThreshold,
25
+ kLoadingIndicatorDelayMs,
26
+ } from "./constants.js";
22
27
  import { readStoredFileViewerMode, writeStoredEditorSelection } from "./storage.js";
23
- import { parsePathSegments } from "./utils.js";
28
+ import { countTextLines, parsePathSegments, shouldUseSimpleEditorMode } from "./utils.js";
24
29
  import { useScrollSync } from "./scroll-sync.js";
25
30
  import { useFileLoader } from "./use-file-loader.js";
26
31
  import { useFileDiff } from "./use-file-diff.js";
@@ -149,7 +154,19 @@ export const useFileViewer = ({
149
154
  !isDeleteBlocked;
150
155
  const syntaxKind = useMemo(() => getFileSyntaxKind(normalizedPath), [normalizedPath]);
151
156
  const isMarkdownFile = syntaxKind === "markdown";
152
- const shouldUseHighlightedEditor = syntaxKind !== "plain";
157
+ const editorLineCount = useMemo(() => countTextLines(renderContent), [renderContent]);
158
+ const useSimpleEditor = useMemo(
159
+ () =>
160
+ shouldUseSimpleEditorMode({
161
+ contentLength: renderContent.length,
162
+ lineCount: editorLineCount,
163
+ charThreshold: kLargeFileSimpleEditorCharThreshold,
164
+ lineThreshold: kLargeFileSimpleEditorLineThreshold,
165
+ }),
166
+ [renderContent, editorLineCount],
167
+ );
168
+ const shouldUseHighlightedEditor = syntaxKind !== "plain" && !useSimpleEditor;
169
+ const shouldRenderLineNumbers = !useSimpleEditor;
153
170
  const parsedFrontmatter = useMemo(
154
171
  () => (isMarkdownFile ? parseFrontmatter(renderContent) : { entries: [], body: renderContent }),
155
172
  [renderContent, isMarkdownFile],
@@ -159,9 +176,9 @@ export const useFileViewer = ({
159
176
  [renderContent, shouldUseHighlightedEditor, syntaxKind],
160
177
  );
161
178
  const editorLineNumbers = useMemo(() => {
162
- const lineCount = String(renderContent || "").split("\n").length;
163
- return Array.from({ length: lineCount }, (_, index) => index + 1);
164
- }, [renderContent]);
179
+ if (!shouldRenderLineNumbers) return [];
180
+ return Array.from({ length: editorLineCount }, (_, index) => index + 1);
181
+ }, [editorLineCount, shouldRenderLineNumbers]);
165
182
  const previewHtml = useMemo(
166
183
  () =>
167
184
  isMarkdownFile
@@ -475,6 +492,7 @@ export const useFileViewer = ({
475
492
  isProtectedFile,
476
493
  isProtectedLocked,
477
494
  shouldUseHighlightedEditor,
495
+ shouldRenderLineNumbers,
478
496
  parsedFrontmatter,
479
497
  highlightedEditorLines,
480
498
  editorLineNumbers,
@@ -9,3 +9,22 @@ export const clampSelectionIndex = (value, maxValue) => {
9
9
  if (!Number.isFinite(numericValue)) return 0;
10
10
  return Math.max(0, Math.min(maxValue, numericValue));
11
11
  };
12
+
13
+ export const countTextLines = (content) => {
14
+ const text = String(content || "");
15
+ if (!text) return 1;
16
+ let lineCount = 1;
17
+ for (let index = 0; index < text.length; index += 1) {
18
+ if (text.charCodeAt(index) === 10) lineCount += 1;
19
+ }
20
+ return lineCount;
21
+ };
22
+
23
+ export const shouldUseSimpleEditorMode = ({
24
+ contentLength = 0,
25
+ lineCount = 1,
26
+ charThreshold = 250000,
27
+ lineThreshold = 5000,
28
+ }) =>
29
+ Number(contentLength) > Number(charThreshold) ||
30
+ Number(lineCount) > Number(lineThreshold);
@@ -1,13 +1,24 @@
1
1
  import { h } from "https://esm.sh/preact";
2
- import { useCallback, useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useState,
7
+ } from "https://esm.sh/preact/hooks";
3
8
  import htm from "https://esm.sh/htm";
4
9
  import { ModalShell } from "../modal-shell.js";
5
10
  import { PageHeader } from "../page-header.js";
6
11
  import { CloseIcon } from "../icons.js";
7
12
  import { ActionButton } from "../action-button.js";
13
+ import { SessionSelectField } from "../session-select-field.js";
8
14
  import { sendAgentMessage } from "../../lib/api.js";
9
15
  import { showToast } from "../toast.js";
10
16
  import { useAgentSessions } from "../../hooks/useAgentSessions.js";
17
+ import {
18
+ kDestinationSessionFilter,
19
+ kNoDestinationSessionValue,
20
+ useDestinationSessionSelection,
21
+ } from "../../hooks/use-destination-session-selection.js";
11
22
 
12
23
  const html = htm.bind(h);
13
24
 
@@ -44,12 +55,7 @@ const kStepTitles = [
44
55
  "Build with your Agent",
45
56
  ];
46
57
  const kTotalSteps = kStepTitles.length;
47
- const kNoSessionSelectedValue = "__none__";
48
-
49
- const kDirectOrGroupFilter = (sessionRow) => {
50
- const key = String(sessionRow?.key || "").toLowerCase();
51
- return key.includes(":direct:") || key.includes(":group:");
52
- };
58
+ const kNoSessionSelectedValue = kNoDestinationSessionValue;
53
59
 
54
60
  const renderCommandBlock = (command = "", onCopy = () => {}) => html`
55
61
  <div class="rounded-lg border border-border bg-black/30 p-3">
@@ -89,12 +95,23 @@ export const GmailSetupWizard = ({
89
95
  const [agentMessageSent, setAgentMessageSent] = useState(false);
90
96
 
91
97
  const {
92
- sessions: selectableAgentSessions,
93
98
  selectedSessionKey,
94
99
  setSelectedSessionKey,
95
100
  loading: loadingAgentSessions,
96
101
  error: agentSessionsError,
97
- } = useAgentSessions({ enabled: visible, filter: kDirectOrGroupFilter });
102
+ } = useAgentSessions({
103
+ enabled: visible,
104
+ filter: kDestinationSessionFilter,
105
+ });
106
+ const {
107
+ sessions: selectableAgentSessions,
108
+ destinationSessionKey,
109
+ setDestinationSessionKey,
110
+ selectedDestination,
111
+ } = useDestinationSessionSelection({
112
+ enabled: visible,
113
+ resetKey: String(account?.id || ""),
114
+ });
98
115
 
99
116
  useEffect(() => {
100
117
  if (!visible) return;
@@ -118,6 +135,9 @@ export const GmailSetupWizard = ({
118
135
  String(projectIdInput || "").trim() ||
119
136
  String(clientConfig?.projectId || "").trim() ||
120
137
  "<project-id>";
138
+ const hasExistingWebhookSetup = Boolean(
139
+ clientConfig?.configured && clientConfig?.transformExists,
140
+ );
121
141
  const client =
122
142
  String(account?.client || clientConfig?.client || "default").trim() ||
123
143
  "default";
@@ -151,6 +171,7 @@ export const GmailSetupWizard = ({
151
171
  await onFinish({
152
172
  client,
153
173
  projectId: String(projectIdInput || "").trim(),
174
+ destination: selectedDestination,
154
175
  });
155
176
  setWatchEnabled(true);
156
177
  setStep((prev) => Math.min(prev + 1, kTotalSteps - 1));
@@ -184,7 +205,8 @@ export const GmailSetupWizard = ({
184
205
  if (sendingToAgent || agentMessageSent) return;
185
206
  try {
186
207
  setSendingToAgent(true);
187
- const accountEmail = String(account?.email || "this account").trim() || "this account";
208
+ const accountEmail =
209
+ String(account?.email || "this account").trim() || "this account";
188
210
  const message =
189
211
  `I just enabled Gmail watch for "${accountEmail}", set up the webhook, ` +
190
212
  `and created the transform file. Help me set up what I want to do ` +
@@ -242,7 +264,9 @@ export const GmailSetupWizard = ({
242
264
  class="rounded-lg border border-border bg-black/20 p-3 space-y-2"
243
265
  >
244
266
  <div class="text-sm">
245
- ${editingProjectId ? "Change project ID" : "Project ID required"}
267
+ ${editingProjectId
268
+ ? "Change project ID"
269
+ : "Project ID required"}
246
270
  </div>
247
271
  <div class="text-xs text-gray-500">
248
272
  Find it in the${" "}
@@ -320,9 +344,37 @@ export const GmailSetupWizard = ({
320
344
  }
321
345
  ${
322
346
  !needsProjectId && step === 3
323
- ? renderCommandBlock(commands?.createSubscription || "", () =>
324
- handleCopy(commands?.createSubscription || ""),
325
- )
347
+ ? html`
348
+ ${renderCommandBlock(commands?.createSubscription || "", () =>
349
+ handleCopy(commands?.createSubscription || ""),
350
+ )}
351
+ <div
352
+ class="rounded-lg border border-border bg-black/20 p-3 space-y-2"
353
+ >
354
+ <${SessionSelectField}
355
+ label="Deliver to"
356
+ sessions=${selectableAgentSessions}
357
+ selectedSessionKey=${destinationSessionKey}
358
+ onChangeSessionKey=${setDestinationSessionKey}
359
+ disabled=${hasExistingWebhookSetup ||
360
+ loadingAgentSessions ||
361
+ saving}
362
+ loading=${loadingAgentSessions}
363
+ error=${agentSessionsError}
364
+ allowNone=${true}
365
+ noneValue=${kNoSessionSelectedValue}
366
+ noneLabel="Default"
367
+ loadingLabel="Loading sessions..."
368
+ helperText=${hasExistingWebhookSetup
369
+ ? "This Gmail webhook has already been created. To edit delivery routing, ask your agent."
370
+ : null}
371
+ selectClassName="w-full bg-black/30 border border-border rounded-lg px-2.5 py-2 text-xs font-mono focus:border-gray-500 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
372
+ helperClassName="text-xs text-gray-500"
373
+ statusClassName="text-[11px] text-gray-500"
374
+ errorClassName="text-[11px] text-red-400"
375
+ />
376
+ </div>
377
+ `
326
378
  : null
327
379
  }
328
380
  ${
@@ -338,21 +390,29 @@ export const GmailSetupWizard = ({
338
390
  incoming email to continue the setup.
339
391
  </div>
340
392
  <div class="pt-2 space-y-2">
341
- <div class="text-[11px] text-gray-500">Send this to session</div>
393
+ <div class="text-[11px] text-gray-500">
394
+ Send this to session
395
+ </div>
342
396
  <div class="flex items-center gap-2">
343
397
  <select
344
398
  value=${selectedSessionKey || kNoSessionSelectedValue}
345
399
  oninput=${(event) => {
346
400
  const nextValue = String(event.target.value || "");
347
401
  setSelectedSessionKey(
348
- nextValue === kNoSessionSelectedValue ? "" : nextValue,
402
+ nextValue === kNoSessionSelectedValue
403
+ ? ""
404
+ : nextValue,
349
405
  );
350
406
  }}
351
- disabled=${loadingAgentSessions || sendingToAgent || agentMessageSent}
407
+ disabled=${loadingAgentSessions ||
408
+ sendingToAgent ||
409
+ agentMessageSent}
352
410
  class="flex-1 min-w-0 bg-black/30 border border-border rounded-lg px-2.5 py-2 text-xs font-mono focus:border-gray-500 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
353
411
  >
354
412
  ${!selectedSessionKey
355
- ? html`<option value=${kNoSessionSelectedValue}>Select a session...</option>`
413
+ ? html`<option value=${kNoSessionSelectedValue}>
414
+ Select a session...
415
+ </option>`
356
416
  : null}
357
417
  ${selectableAgentSessions.map(
358
418
  (sessionRow) => html`
@@ -366,7 +426,6 @@ export const GmailSetupWizard = ({
366
426
  onClick=${handleSendToAgent}
367
427
  disabled=${!selectedSessionKey || agentMessageSent}
368
428
  loading=${sendingToAgent}
369
- loadingMode="inline"
370
429
  idleLabel=${agentMessageSent ? "Sent" : "Send to Agent"}
371
430
  loadingLabel="Sending..."
372
431
  tone="primary"
@@ -375,10 +434,14 @@ export const GmailSetupWizard = ({
375
434
  />
376
435
  </div>
377
436
  ${loadingAgentSessions
378
- ? html`<div class="text-[11px] text-gray-500">Loading sessions...</div>`
437
+ ? html`<div class="text-[11px] text-gray-500">
438
+ Loading sessions...
439
+ </div>`
379
440
  : null}
380
441
  ${agentSessionsError
381
- ? html`<div class="text-[11px] text-red-400">${agentSessionsError}</div>`
442
+ ? html`<div class="text-[11px] text-red-400">
443
+ ${agentSessionsError}
444
+ </div>`
382
445
  : null}
383
446
  </div>
384
447
  </div>
@@ -387,24 +450,26 @@ export const GmailSetupWizard = ({
387
450
  : null
388
451
  }
389
452
  <div class="grid grid-cols-2 gap-2 pt-2">
390
- ${step === 0
391
- ? html`${!needsProjectId
392
- ? html`<button
393
- type="button"
394
- onclick=${handleChangeProjectId}
395
- class="justify-self-start text-xs px-2 py-1 rounded-lg ac-btn-ghost"
396
- >
397
- Change project ID
398
- </button>`
399
- : html`<div></div>`}`
400
- : html`<${ActionButton}
401
- onClick=${() => setStep((prev) => Math.max(prev - 1, 0))}
402
- disabled=${saving}
403
- idleLabel="Back"
404
- tone="secondary"
405
- size="md"
406
- className="w-full justify-center"
407
- />`}
453
+ ${
454
+ step === 0
455
+ ? html`${!needsProjectId
456
+ ? html`<button
457
+ type="button"
458
+ onclick=${handleChangeProjectId}
459
+ class="justify-self-start text-xs px-2 py-1 rounded-lg ac-btn-ghost"
460
+ >
461
+ Change project ID
462
+ </button>`
463
+ : html`<div></div>`}`
464
+ : html`<${ActionButton}
465
+ onClick=${() => setStep((prev) => Math.max(prev - 1, 0))}
466
+ disabled=${saving}
467
+ idleLabel="Back"
468
+ tone="secondary"
469
+ size="md"
470
+ className="w-full justify-center"
471
+ />`
472
+ }
408
473
  ${
409
474
  step < kTotalSteps - 2
410
475
  ? html`<${ActionButton}
@@ -417,23 +482,25 @@ export const GmailSetupWizard = ({
417
482
  />`
418
483
  : step === kTotalSteps - 2
419
484
  ? html`<${ActionButton}
420
- onClick=${handleFinish}
485
+ onClick=${hasExistingWebhookSetup ? handleNext : handleFinish}
421
486
  disabled=${false}
422
487
  loading=${saving}
423
- idleLabel="Enable watch"
424
- loadingLabel="Enabling..."
488
+ idleLabel=${hasExistingWebhookSetup ? "Next" : "Enable watch"}
489
+ loadingLabel=${hasExistingWebhookSetup
490
+ ? "Loading..."
491
+ : "Enabling..."}
425
492
  tone="primary"
426
493
  size="md"
427
494
  className="w-full justify-center"
428
495
  />`
429
- : html`<${ActionButton}
430
- onClick=${onClose}
431
- disabled=${saving || sendingToAgent}
432
- idleLabel="Done"
433
- tone="secondary"
434
- size="md"
435
- className="w-full justify-center"
436
- />`
496
+ : html`<${ActionButton}
497
+ onClick=${onClose}
498
+ disabled=${saving || sendingToAgent}
499
+ idleLabel="Done"
500
+ tone="secondary"
501
+ size="md"
502
+ className="w-full justify-center"
503
+ />`
437
504
  }
438
505
  </div>
439
506
  </${ModalShell}>
@@ -16,7 +16,6 @@ import { getDefaultScopes, toggleScopeLogic } from "../scope-picker.js";
16
16
  import { CredentialsModal } from "../credentials-modal.js";
17
17
  import { ConfirmDialog } from "../confirm-dialog.js";
18
18
  import { showToast } from "../toast.js";
19
- import { PageHeader } from "../page-header.js";
20
19
  import { ActionButton } from "../action-button.js";
21
20
  import { GoogleAccountRow } from "./account-row.js";
22
21
  import { AddGoogleAccountModal } from "./add-account-modal.js";
@@ -397,7 +396,11 @@ export const Google = ({
397
396
  }
398
397
  };
399
398
 
400
- const handleFinishGmailSetupWizard = async ({ client, projectId }) => {
399
+ const handleFinishGmailSetupWizard = async ({
400
+ client,
401
+ projectId,
402
+ destination = null,
403
+ }) => {
401
404
  const accountId = String(gmailWizardState.accountId || "").trim();
402
405
  if (!accountId) return;
403
406
  await saveClientSetup({
@@ -405,7 +408,7 @@ export const Google = ({
405
408
  projectId,
406
409
  regeneratePushToken: false,
407
410
  });
408
- await startWatchForAccount(accountId);
411
+ await startWatchForAccount(accountId, { destination });
409
412
  showToast("Gmail setup complete and watch enabled", "success");
410
413
  };
411
414
 
@@ -447,48 +450,46 @@ export const Google = ({
447
450
 
448
451
  return html`
449
452
  <div class="bg-surface border border-border rounded-xl p-4">
450
- <${PageHeader}
451
- title="Google Accounts"
452
- actions=${html`
453
- ${accounts.length
454
- ? html`
455
- <div class="relative">
456
- <button
457
- type="button"
458
- onclick=${() => setAddMenuOpen((prev) => !prev)}
459
- class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary"
460
- >
461
- + Add Account
462
- </button>
463
- ${addMenuOpen
464
- ? html`
465
- <div
466
- class="absolute right-0 top-full mt-2 min-w-[210px] rounded-lg border border-border bg-modal p-1 z-20"
453
+ <div class="flex items-center justify-between gap-3">
454
+ <h2 class="card-label">Google Accounts</h2>
455
+ ${accounts.length
456
+ ? html`
457
+ <div class="relative">
458
+ <button
459
+ type="button"
460
+ onclick=${() => setAddMenuOpen((prev) => !prev)}
461
+ class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary"
462
+ >
463
+ + Add Account
464
+ </button>
465
+ ${addMenuOpen
466
+ ? html`
467
+ <div
468
+ class="absolute right-0 top-full mt-2 min-w-[210px] rounded-lg border border-border bg-modal p-1 z-20"
469
+ >
470
+ <button
471
+ type="button"
472
+ onclick=${handleAddCompanyClick}
473
+ class="w-full text-left px-2.5 py-1.5 text-xs rounded-md hover:bg-black/30"
467
474
  >
468
- <button
469
- type="button"
470
- onclick=${handleAddCompanyClick}
471
- class="w-full text-left px-2.5 py-1.5 text-xs rounded-md hover:bg-black/30"
472
- >
473
- Company account
474
- </button>
475
- ${!hasPersonalAccount
476
- ? html`<button
477
- type="button"
478
- onclick=${handleAddPersonalClick}
479
- class="w-full text-left px-2.5 py-1.5 text-xs rounded-md hover:bg-black/30"
480
- >
481
- Personal account
482
- </button>`
483
- : null}
484
- </div>
485
- `
486
- : null}
487
- </div>
488
- `
489
- : null}
490
- `}
491
- />
475
+ Company account
476
+ </button>
477
+ ${!hasPersonalAccount
478
+ ? html`<button
479
+ type="button"
480
+ onclick=${handleAddPersonalClick}
481
+ class="w-full text-left px-2.5 py-1.5 text-xs rounded-md hover:bg-black/30"
482
+ >
483
+ Personal account
484
+ </button>`
485
+ : null}
486
+ </div>
487
+ `
488
+ : null}
489
+ </div>
490
+ `
491
+ : null}
492
+ </div>
492
493
  ${loading
493
494
  ? html`<div class="text-gray-500 text-sm text-center py-2">
494
495
  Loading...