@agent-native/core 0.92.5 → 0.92.6

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 (147) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/docs/content/template-assets.mdx +139 -0
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/agent/production-agent.ts +96 -6
  6. package/corpus/core/src/client/AssistantChat.tsx +127 -15
  7. package/corpus/core/src/client/chat/tool-call-display.tsx +9 -3
  8. package/corpus/core/src/client/org/TeamPage.tsx +274 -177
  9. package/corpus/core/src/client/settings/DemoModeSection.tsx +6 -6
  10. package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
  11. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +49 -1
  12. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +2 -2
  13. package/corpus/core/src/demo/config.ts +3 -3
  14. package/corpus/core/src/demo/redact.ts +720 -0
  15. package/corpus/core/src/extensions/url-safety.ts +13 -1
  16. package/corpus/core/src/localization/default-messages.ts +7 -0
  17. package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +7 -0
  18. package/corpus/core/src/templates/default/app/i18n/de-DE.ts +7 -0
  19. package/corpus/core/src/templates/default/app/i18n/en-US.ts +7 -0
  20. package/corpus/core/src/templates/default/app/i18n/es-ES.ts +7 -0
  21. package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +7 -0
  22. package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +7 -0
  23. package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +7 -0
  24. package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +7 -0
  25. package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +7 -0
  26. package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +7 -0
  27. package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +7 -0
  28. package/corpus/templates/analytics/AGENTS.md +6 -3
  29. package/corpus/templates/analytics/changelog/2026-07-09-uptime-monitors-use-scheduled-workers-in-production-serverless.md +6 -0
  30. package/corpus/templates/analytics/docs/uptime-monitoring.md +5 -0
  31. package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +14 -2
  32. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +35 -0
  33. package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +19 -0
  34. package/corpus/templates/assets/AGENTS.md +13 -0
  35. package/corpus/templates/assets/actions/_generation-preset-settings.ts +73 -0
  36. package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +63 -0
  37. package/corpus/templates/assets/actions/create-generation-preset.ts +22 -8
  38. package/corpus/templates/assets/actions/duplicate-library.ts +49 -1
  39. package/corpus/templates/assets/actions/generate-image-batch.ts +14 -1
  40. package/corpus/templates/assets/actions/generate-image.ts +195 -26
  41. package/corpus/templates/assets/actions/import-asset-from-url.ts +323 -0
  42. package/corpus/templates/assets/actions/list-generation-presets.ts +1 -1
  43. package/corpus/templates/assets/actions/rerun-generation-run.ts +37 -0
  44. package/corpus/templates/assets/actions/update-generation-preset.ts +19 -1
  45. package/corpus/templates/assets/app/i18n/ar-SA.ts +1 -0
  46. package/corpus/templates/assets/app/i18n/de-DE.ts +1 -0
  47. package/corpus/templates/assets/app/i18n/es-ES.ts +1 -0
  48. package/corpus/templates/assets/app/i18n/fr-FR.ts +1 -0
  49. package/corpus/templates/assets/app/i18n/hi-IN.ts +1 -0
  50. package/corpus/templates/assets/app/i18n/ja-JP.ts +1 -0
  51. package/corpus/templates/assets/app/i18n/ko-KR.ts +1 -0
  52. package/corpus/templates/assets/app/i18n/pt-BR.ts +1 -0
  53. package/corpus/templates/assets/app/i18n/zh-CN.ts +1 -0
  54. package/corpus/templates/assets/app/i18n/zh-TW.ts +41 -0
  55. package/corpus/templates/assets/app/i18n-data.ts +93 -0
  56. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +66 -11
  57. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +959 -24
  58. package/corpus/templates/assets/changelog/2026-07-07-preset-skeleton-controls-fit-correctly-on-narrower-edit-page.md +6 -0
  59. package/corpus/templates/assets/changelog/2026-07-09-import-reference-images-from-url.md +6 -0
  60. package/corpus/templates/assets/changelog/2026-07-09-preset-reference-board.md +6 -0
  61. package/corpus/templates/assets/server/handlers/assets.ts +7 -27
  62. package/corpus/templates/assets/server/lib/generation.ts +83 -2
  63. package/corpus/templates/assets/server/lib/preset-chat-context.ts +17 -4
  64. package/corpus/templates/assets/server/lib/preset-references.ts +182 -0
  65. package/corpus/templates/assets/server/lib/upload-validation.ts +38 -0
  66. package/corpus/templates/assets/shared/api.ts +20 -0
  67. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +6 -0
  68. package/corpus/templates/calendar/actions/create-event.ts +8 -2
  69. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +22 -7
  70. package/corpus/templates/calendar/changelog/2026-07-09-calendar-events-with-guests-now-automatically-get-a-google-m.md +6 -0
  71. package/corpus/templates/calendar/changelog/2026-07-09-new-events-now-mark-the-creator-rsvp-as-yes.md +6 -0
  72. package/corpus/templates/calendar/server/handlers/events.ts +9 -2
  73. package/corpus/templates/calendar/server/lib/event-video-conferencing.ts +58 -0
  74. package/corpus/templates/calendar/server/lib/google-calendar.ts +1 -0
  75. package/corpus/templates/clips/changelog/2026-07-09-meeting-reminder-popovers-can-now-be-dismissed-with-a-top-le.md +6 -0
  76. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +3 -1
  77. package/corpus/templates/clips/desktop/src/styles.css +13 -9
  78. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +5 -2
  79. package/dist/agent/production-agent.d.ts +34 -1
  80. package/dist/agent/production-agent.d.ts.map +1 -1
  81. package/dist/agent/production-agent.js +58 -5
  82. package/dist/agent/production-agent.js.map +1 -1
  83. package/dist/client/AssistantChat.d.ts +3 -1
  84. package/dist/client/AssistantChat.d.ts.map +1 -1
  85. package/dist/client/AssistantChat.js +96 -15
  86. package/dist/client/AssistantChat.js.map +1 -1
  87. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  88. package/dist/client/chat/tool-call-display.js +5 -1
  89. package/dist/client/chat/tool-call-display.js.map +1 -1
  90. package/dist/client/org/TeamPage.d.ts.map +1 -1
  91. package/dist/client/org/TeamPage.js +32 -13
  92. package/dist/client/org/TeamPage.js.map +1 -1
  93. package/dist/client/settings/DemoModeSection.d.ts +3 -3
  94. package/dist/client/settings/DemoModeSection.js +4 -4
  95. package/dist/client/settings/DemoModeSection.js.map +1 -1
  96. package/dist/client/settings/SettingsPanel.js +1 -1
  97. package/dist/client/settings/SettingsPanel.js.map +1 -1
  98. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
  99. package/dist/client/settings/SettingsTabsPage.js +38 -2
  100. package/dist/client/settings/SettingsTabsPage.js.map +1 -1
  101. package/dist/collab/awareness.d.ts +2 -2
  102. package/dist/collab/awareness.d.ts.map +1 -1
  103. package/dist/demo/actions/toggle-demo-mode.js +2 -2
  104. package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
  105. package/dist/demo/config.js +3 -3
  106. package/dist/demo/config.js.map +1 -1
  107. package/dist/demo/redact.d.ts +1 -1
  108. package/dist/demo/redact.d.ts.map +1 -1
  109. package/dist/demo/redact.js +0 -0
  110. package/dist/demo/redact.js.map +1 -1
  111. package/dist/extensions/url-safety.d.ts +5 -0
  112. package/dist/extensions/url-safety.d.ts.map +1 -1
  113. package/dist/extensions/url-safety.js +10 -0
  114. package/dist/extensions/url-safety.js.map +1 -1
  115. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  116. package/dist/localization/default-messages.d.ts +7 -0
  117. package/dist/localization/default-messages.d.ts.map +1 -1
  118. package/dist/localization/default-messages.js +7 -0
  119. package/dist/localization/default-messages.js.map +1 -1
  120. package/dist/observability/routes.d.ts +5 -5
  121. package/dist/progress/routes.d.ts +1 -1
  122. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  123. package/dist/resources/handlers.d.ts +1 -1
  124. package/dist/templates/default/app/i18n/ar-SA.ts +7 -0
  125. package/dist/templates/default/app/i18n/de-DE.ts +7 -0
  126. package/dist/templates/default/app/i18n/en-US.ts +7 -0
  127. package/dist/templates/default/app/i18n/es-ES.ts +7 -0
  128. package/dist/templates/default/app/i18n/fr-FR.ts +7 -0
  129. package/dist/templates/default/app/i18n/hi-IN.ts +7 -0
  130. package/dist/templates/default/app/i18n/ja-JP.ts +7 -0
  131. package/dist/templates/default/app/i18n/ko-KR.ts +7 -0
  132. package/dist/templates/default/app/i18n/pt-BR.ts +7 -0
  133. package/dist/templates/default/app/i18n/zh-CN.ts +7 -0
  134. package/dist/templates/default/app/i18n/zh-TW.ts +7 -0
  135. package/docs/content/template-assets.mdx +139 -0
  136. package/package.json +1 -1
  137. package/src/templates/default/app/i18n/ar-SA.ts +7 -0
  138. package/src/templates/default/app/i18n/de-DE.ts +7 -0
  139. package/src/templates/default/app/i18n/en-US.ts +7 -0
  140. package/src/templates/default/app/i18n/es-ES.ts +7 -0
  141. package/src/templates/default/app/i18n/fr-FR.ts +7 -0
  142. package/src/templates/default/app/i18n/hi-IN.ts +7 -0
  143. package/src/templates/default/app/i18n/ja-JP.ts +7 -0
  144. package/src/templates/default/app/i18n/ko-KR.ts +7 -0
  145. package/src/templates/default/app/i18n/pt-BR.ts +7 -0
  146. package/src/templates/default/app/i18n/zh-CN.ts +7 -0
  147. package/src/templates/default/app/i18n/zh-TW.ts +7 -0
@@ -1,3 +1,11 @@
1
+ import {
2
+ Table,
3
+ TableBody,
4
+ TableCell,
5
+ TableHead,
6
+ TableHeader,
7
+ TableRow,
8
+ } from "@agent-native/toolkit/ui/table";
1
9
  import {
2
10
  IconUserPlus,
3
11
  IconTrash,
@@ -301,12 +309,22 @@ function OrgNameDisplay({ name, canEdit }: { name: string; canEdit: boolean }) {
301
309
  );
302
310
  }
303
311
 
312
+ interface MemberListItem {
313
+ email: string;
314
+ role: string;
315
+ }
316
+
317
+ interface PendingInviteListItem {
318
+ id: string;
319
+ email: string;
320
+ role: string;
321
+ }
322
+
304
323
  function MembersCard() {
305
324
  const t = useT();
306
325
  const { data: org } = useOrg();
307
326
  const { data: membersData, isLoading: isLoadingMembers } = useOrgMembers();
308
327
  const { data: invitationsData } = useOrgInvitations();
309
- const removeMember = useRemoveMember();
310
328
  const switchOrg = useSwitchOrg();
311
329
 
312
330
  const [showInviteForm, setShowInviteForm] = useState(false);
@@ -320,108 +338,62 @@ function MembersCard() {
320
338
  const hasMultipleOrgs = (org.orgs?.length ?? 0) > 1;
321
339
 
322
340
  return (
323
- <section className="rounded-lg border border-border bg-card p-4 space-y-4">
324
- <div className="flex items-center justify-between">
325
- <div className="flex items-center gap-3">
326
- <div className="flex h-9 w-9 items-center justify-center rounded-lg bg-blue-600/10">
327
- <IconUsersGroup className="h-5 w-5 text-blue-600" />
328
- </div>
329
- <div>
330
- <OrgNameDisplay name={org.orgName ?? ""} canEdit={isOwnerOrAdmin} />
331
- <div className="text-xs text-muted-foreground">
332
- {t("org.memberCount", { count: members.length })} ·{" "}
333
- {t("org.youAreRole", { role: org.role })}
334
- </div>
335
- </div>
336
- </div>
337
- {hasMultipleOrgs && (
338
- <select
339
- value={org.orgId ?? ""}
340
- onChange={(e) => switchOrg.mutate(e.target.value || null)}
341
- disabled={switchOrg.isPending}
342
- className="rounded-md border border-border bg-background px-2.5 py-1.5 text-xs"
343
- >
344
- {org.orgs.map((o) => (
345
- <option key={o.orgId} value={o.orgId}>
346
- {o.orgName}
347
- </option>
348
- ))}
349
- </select>
350
- )}
351
- </div>
352
-
353
- <div className="grid gap-5 border-t border-border pt-3 lg:grid-cols-[minmax(0,1fr)_minmax(18rem,22rem)]">
354
- <div className="min-w-0 space-y-4">
355
- <div className="space-y-1">
356
- <div className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
357
- {t("org.members")}
341
+ <div className="space-y-4">
342
+ <section className="rounded-lg border border-border bg-card p-4 space-y-4">
343
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
344
+ <div className="flex min-w-0 items-center gap-3">
345
+ <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-blue-600/10">
346
+ <IconUsersGroup className="h-5 w-5 text-blue-600" />
358
347
  </div>
359
- {isLoadingMembers && members.length === 0 && (
360
- <>
361
- {[0, 1, 2].map((i) => (
362
- <div
363
- key={i}
364
- className="flex items-center justify-between py-1.5 px-2"
365
- >
366
- <div className="flex items-center gap-2">
367
- <div
368
- className="h-3.5 rounded bg-muted animate-pulse"
369
- style={{ width: `${140 + i * 30}px` }}
370
- />
371
- <div className="h-3.5 w-3.5 rounded bg-muted animate-pulse" />
372
- </div>
373
- </div>
374
- ))}
375
- </>
376
- )}
377
- {members.map((m) => (
378
- <MemberRow
379
- key={m.email}
380
- email={m.email}
381
- role={m.role}
382
- isCurrentUser={m.email === org.email}
383
- currentUserRole={org.role ?? null}
348
+ <div className="min-w-0">
349
+ <OrgNameDisplay
350
+ name={org.orgName ?? ""}
351
+ canEdit={isOwnerOrAdmin}
384
352
  />
385
- ))}
386
- {pendingInvites.map((inv) => (
387
- <div
388
- key={inv.id}
389
- className="flex items-center justify-between gap-2 py-1.5 px-2 opacity-60"
390
- >
391
- <div className="flex min-w-0 items-center gap-2">
392
- <span className="truncate text-sm">{inv.email}</span>
393
- <RoleIcon role={inv.role} />
394
- <span className="rounded border border-border px-1.5 py-0.5 text-[10px] text-muted-foreground">
395
- Invited{inv.role === "admin" ? " · admin" : ""}
396
- </span>
397
- </div>
353
+ <div className="text-xs text-muted-foreground">
354
+ {t("org.memberCount", { count: members.length })} ·{" "}
355
+ {t("org.youAreRole", { role: org.role })}
398
356
  </div>
399
- ))}
400
- </div>
401
-
402
- {isOwnerOrAdmin && (
403
- <div className="border-t border-border pt-3">
404
- {!showInviteForm ? (
405
- <button
406
- type="button"
407
- onClick={() => setShowInviteForm(true)}
408
- className="flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1.5 text-xs font-medium hover:bg-accent/50"
409
- >
410
- <IconUserPlus className="h-3.5 w-3.5" />
411
- Invite members
412
- </button>
413
- ) : (
414
- <BulkInviteForm
415
- currentUserRole={org.role}
416
- onClose={() => setShowInviteForm(false)}
417
- />
418
- )}
419
357
  </div>
358
+ </div>
359
+ {hasMultipleOrgs && (
360
+ <select
361
+ value={org.orgId ?? ""}
362
+ onChange={(e) => switchOrg.mutate(e.target.value || null)}
363
+ disabled={switchOrg.isPending}
364
+ className="w-full rounded-md border border-border bg-background px-2.5 py-1.5 text-xs sm:w-auto"
365
+ >
366
+ {org.orgs.map((o) => (
367
+ <option key={o.orgId} value={o.orgId}>
368
+ {o.orgName}
369
+ </option>
370
+ ))}
371
+ </select>
420
372
  )}
421
373
  </div>
422
374
 
423
375
  {isOwnerOrAdmin && (
424
- <div className="min-w-0 space-y-3 border-t border-border pt-3 lg:border-s lg:border-t-0 lg:ps-5 lg:pt-0">
376
+ <div className="border-t border-border pt-4">
377
+ {!showInviteForm ? (
378
+ <button
379
+ type="button"
380
+ onClick={() => setShowInviteForm(true)}
381
+ className="inline-flex items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs font-medium text-background hover:opacity-90"
382
+ >
383
+ <IconUserPlus className="h-3.5 w-3.5" />
384
+ {t("org.inviteMembers")}
385
+ </button>
386
+ ) : (
387
+ <BulkInviteForm
388
+ currentUserRole={org.role}
389
+ onClose={() => setShowInviteForm(false)}
390
+ />
391
+ )}
392
+ </div>
393
+ )}
394
+
395
+ {isOwnerOrAdmin && (
396
+ <div className="grid gap-5 border-t border-border pt-4 lg:grid-cols-2">
425
397
  <DomainSettingsSection
426
398
  domain={org.allowedDomain}
427
399
  ownerEmail={org.email}
@@ -430,14 +402,134 @@ function MembersCard() {
430
402
  {isOwner && <A2ASecretSection secret={org.a2aSecret} />}
431
403
  </div>
432
404
  )}
433
- </div>
434
405
 
435
- <ErrorText error={removeMember.error} />
436
- <ErrorText error={switchOrg.error} />
406
+ <ErrorText error={switchOrg.error} />
407
+ </section>
408
+
409
+ <MembersTableCard
410
+ members={members}
411
+ pendingInvites={pendingInvites}
412
+ isLoadingMembers={isLoadingMembers}
413
+ currentUserEmail={org.email}
414
+ currentUserRole={org.role ?? null}
415
+ />
416
+ </div>
417
+ );
418
+ }
419
+
420
+ function MembersTableCard({
421
+ members,
422
+ pendingInvites,
423
+ isLoadingMembers,
424
+ currentUserEmail,
425
+ currentUserRole,
426
+ }: {
427
+ members: MemberListItem[];
428
+ pendingInvites: PendingInviteListItem[];
429
+ isLoadingMembers: boolean;
430
+ currentUserEmail: string;
431
+ currentUserRole: string | null;
432
+ }) {
433
+ const t = useT();
434
+
435
+ return (
436
+ <section className="rounded-lg border border-border bg-card">
437
+ <div className="flex flex-col gap-1 border-b border-border px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
438
+ <div>
439
+ <h3 className="text-sm font-medium">{t("org.members")}</h3>
440
+ <p className="text-xs text-muted-foreground">
441
+ {t("org.memberCount", { count: members.length })}
442
+ </p>
443
+ </div>
444
+ </div>
445
+ <Table>
446
+ <TableHeader>
447
+ <TableRow>
448
+ <TableHead>{t("org.member")}</TableHead>
449
+ <TableHead>{t("org.role")}</TableHead>
450
+ <TableHead>{t("org.status")}</TableHead>
451
+ <TableHead className="text-end">{t("org.actions")}</TableHead>
452
+ </TableRow>
453
+ </TableHeader>
454
+ <TableBody>
455
+ {isLoadingMembers && members.length === 0 ? (
456
+ [0, 1, 2].map((i) => (
457
+ <TableRow key={i}>
458
+ <TableCell colSpan={4}>
459
+ <div
460
+ className="h-3.5 rounded bg-muted animate-pulse"
461
+ style={{ width: `${180 + i * 48}px` }}
462
+ />
463
+ </TableCell>
464
+ </TableRow>
465
+ ))
466
+ ) : members.length === 0 && pendingInvites.length === 0 ? (
467
+ <TableRow>
468
+ <TableCell
469
+ colSpan={4}
470
+ className="py-8 text-center text-sm text-muted-foreground"
471
+ >
472
+ {t("org.noMembers")}
473
+ </TableCell>
474
+ </TableRow>
475
+ ) : (
476
+ <>
477
+ {members.map((m) => (
478
+ <MemberRow
479
+ key={m.email}
480
+ email={m.email}
481
+ role={m.role}
482
+ isCurrentUser={m.email === currentUserEmail}
483
+ currentUserRole={currentUserRole}
484
+ />
485
+ ))}
486
+ {pendingInvites.map((inv) => (
487
+ <PendingInviteRow key={inv.id} invite={inv} />
488
+ ))}
489
+ </>
490
+ )}
491
+ </TableBody>
492
+ </Table>
437
493
  </section>
438
494
  );
439
495
  }
440
496
 
497
+ function roleLabel(role: string, t: ReturnType<typeof useT>) {
498
+ if (role === "owner") return t("org.owner");
499
+ if (role === "admin") return t("org.admin");
500
+ return t("org.member");
501
+ }
502
+
503
+ function RoleBadge({ role }: { role: string }) {
504
+ const t = useT();
505
+ return (
506
+ <span className="inline-flex items-center gap-1.5 rounded border border-border px-2 py-1 text-xs text-muted-foreground">
507
+ <RoleIcon role={role} />
508
+ {roleLabel(role, t)}
509
+ </span>
510
+ );
511
+ }
512
+
513
+ function PendingInviteRow({ invite }: { invite: PendingInviteListItem }) {
514
+ const t = useT();
515
+ return (
516
+ <TableRow className="opacity-70">
517
+ <TableCell className="min-w-56">
518
+ <span className="truncate text-sm">{invite.email}</span>
519
+ </TableCell>
520
+ <TableCell>
521
+ <RoleBadge role={invite.role} />
522
+ </TableCell>
523
+ <TableCell>
524
+ <span className="rounded border border-border px-1.5 py-0.5 text-[10px] text-muted-foreground">
525
+ {t("org.invited")}
526
+ </span>
527
+ </TableCell>
528
+ <TableCell className="text-end text-muted-foreground">-</TableCell>
529
+ </TableRow>
530
+ );
531
+ }
532
+
441
533
  function MemberRow({
442
534
  email,
443
535
  role,
@@ -465,99 +557,104 @@ function MemberRow({
465
557
  (currentUserRole === "admin" && role === "member"));
466
558
 
467
559
  return (
468
- <div className="flex items-center justify-between gap-2 py-1.5 px-2 rounded hover:bg-accent/30">
469
- <div className="flex min-w-0 items-center gap-2">
560
+ <TableRow>
561
+ <TableCell className="min-w-56">
470
562
  <span className="truncate text-sm">{email}</span>
471
- <RoleIcon role={role} />
472
- {isCurrentUser && (
563
+ </TableCell>
564
+ <TableCell>
565
+ <RoleBadge role={role} />
566
+ </TableCell>
567
+ <TableCell>
568
+ {isCurrentUser ? (
473
569
  <span className="rounded border border-border px-1.5 py-0.5 text-[10px] text-muted-foreground">
474
570
  {t("org.you")}
475
571
  </span>
572
+ ) : (
573
+ <span className="text-muted-foreground">-</span>
476
574
  )}
477
- {role === "admin" && (
478
- <span className="rounded border border-border px-1.5 py-0.5 text-[10px] text-blue-600">
479
- {t("org.admin")}
480
- </span>
481
- )}
482
- </div>
483
- {canManage && (
484
- <div className="flex shrink-0 items-center gap-1">
485
- {editing ? (
486
- <select
487
- autoFocus
488
- value={role}
489
- onChange={(e) => {
490
- const next = e.target.value === "admin" ? "admin" : "member";
491
- if (next !== role) {
492
- changeRole.mutate(
493
- { email, role: next },
494
- { onSuccess: () => setEditing(false) },
495
- );
496
- } else {
497
- setEditing(false);
498
- }
499
- }}
500
- onBlur={() => setEditing(false)}
501
- disabled={changeRole.isPending}
502
- className="rounded-md border border-border bg-background px-1.5 py-0.5 text-[11px]"
503
- >
504
- <option value="member">{t("org.member")}</option>
505
- <option value="admin">{t("org.admin")}</option>
506
- </select>
507
- ) : (
508
- <Tooltip>
509
- <TooltipTrigger asChild>
575
+ </TableCell>
576
+ <TableCell>
577
+ {canManage ? (
578
+ <div className="flex shrink-0 items-center justify-end gap-1">
579
+ {editing ? (
580
+ <select
581
+ autoFocus
582
+ value={role}
583
+ onChange={(e) => {
584
+ const next = e.target.value === "admin" ? "admin" : "member";
585
+ if (next !== role) {
586
+ changeRole.mutate(
587
+ { email, role: next },
588
+ { onSuccess: () => setEditing(false) },
589
+ );
590
+ } else {
591
+ setEditing(false);
592
+ }
593
+ }}
594
+ onBlur={() => setEditing(false)}
595
+ disabled={changeRole.isPending}
596
+ className="rounded-md border border-border bg-background px-1.5 py-0.5 text-[11px]"
597
+ >
598
+ <option value="member">{t("org.member")}</option>
599
+ <option value="admin">{t("org.admin")}</option>
600
+ </select>
601
+ ) : (
602
+ <Tooltip>
603
+ <TooltipTrigger asChild>
604
+ <button
605
+ type="button"
606
+ onClick={() => setEditing(true)}
607
+ className="text-muted-foreground hover:text-foreground"
608
+ >
609
+ <IconPencil className="h-3.5 w-3.5" />
610
+ </button>
611
+ </TooltipTrigger>
612
+ <TooltipContent>{t("org.changeRole")}</TooltipContent>
613
+ </Tooltip>
614
+ )}
615
+ {confirmingRemove ? (
616
+ <div className="flex items-center gap-1">
510
617
  <button
511
618
  type="button"
512
- onClick={() => setEditing(true)}
513
- className="text-muted-foreground hover:text-foreground"
619
+ onClick={() => setConfirmingRemove(false)}
620
+ className="rounded px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground"
514
621
  >
515
- <IconPencil className="h-3.5 w-3.5" />
622
+ {t("org.cancel")}
516
623
  </button>
517
- </TooltipTrigger>
518
- <TooltipContent>{t("org.changeRole")}</TooltipContent>
519
- </Tooltip>
520
- )}
521
- {confirmingRemove ? (
522
- <div className="flex items-center gap-1">
523
- <button
524
- type="button"
525
- onClick={() => setConfirmingRemove(false)}
526
- className="rounded px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground"
527
- >
528
- {t("org.cancel")}
529
- </button>
530
- <button
531
- type="button"
532
- disabled={removeMember.isPending}
533
- onClick={() =>
534
- removeMember.mutate(email, {
535
- onSettled: () => setConfirmingRemove(false),
536
- })
537
- }
538
- className="rounded bg-red-500 px-1.5 py-0.5 text-[11px] text-white hover:bg-red-600 disabled:opacity-50"
539
- >
540
- {t("org.remove")}
541
- </button>
542
- </div>
543
- ) : (
544
- <Tooltip>
545
- <TooltipTrigger asChild>
546
624
  <button
547
625
  type="button"
548
626
  disabled={removeMember.isPending}
549
- onClick={() => setConfirmingRemove(true)}
550
- className="text-muted-foreground hover:text-red-500 disabled:opacity-50"
627
+ onClick={() =>
628
+ removeMember.mutate(email, {
629
+ onSettled: () => setConfirmingRemove(false),
630
+ })
631
+ }
632
+ className="rounded bg-red-500 px-1.5 py-0.5 text-[11px] text-white hover:bg-red-600 disabled:opacity-50"
551
633
  >
552
- <IconTrash className="h-3.5 w-3.5" />
634
+ {t("org.remove")}
553
635
  </button>
554
- </TooltipTrigger>
555
- <TooltipContent>{t("org.removeMember")}</TooltipContent>
556
- </Tooltip>
557
- )}
558
- </div>
559
- )}
560
- </div>
636
+ </div>
637
+ ) : (
638
+ <Tooltip>
639
+ <TooltipTrigger asChild>
640
+ <button
641
+ type="button"
642
+ disabled={removeMember.isPending}
643
+ onClick={() => setConfirmingRemove(true)}
644
+ className="text-muted-foreground hover:text-red-500 disabled:opacity-50"
645
+ >
646
+ <IconTrash className="h-3.5 w-3.5" />
647
+ </button>
648
+ </TooltipTrigger>
649
+ <TooltipContent>{t("org.removeMember")}</TooltipContent>
650
+ </Tooltip>
651
+ )}
652
+ </div>
653
+ ) : (
654
+ <div className="text-end text-muted-foreground">-</div>
655
+ )}
656
+ </TableCell>
657
+ </TableRow>
561
658
  );
562
659
  }
563
660
 
@@ -1,7 +1,7 @@
1
1
  /**
2
- * <DemoModeSection /> — toggle that replaces names, emails, and numbers with
3
- * realistic fake data everywhere (UI + what the agent sees) while preserving
4
- * IDs and structure so the app keeps working.
2
+ * <DemoModeSection /> — toggle that replaces contact/free-text names, emails,
3
+ * and numbers with realistic fake data everywhere (UI + what the agent sees)
4
+ * while preserving labels, IDs, and structure so the app keeps working.
5
5
  *
6
6
  * The toggle is stored in application_state under `demo-mode`
7
7
  * (`{ enabled: boolean }`) and written via `PUT
@@ -87,9 +87,9 @@ export function DemoModeSection() {
87
87
  Enable demo mode
88
88
  </div>
89
89
  <p className="text-[10px] text-muted-foreground mt-0.5">
90
- Replace names, emails, and numbers with realistic fake data everywhere
91
- — in the UI and what the agent sees. IDs and structure are preserved
92
- so the app keeps working.
90
+ Replace contact/free-text names, emails, and numbers with realistic
91
+ fake data everywhere — in the UI and what the agent sees. Labels, IDs,
92
+ and structure are preserved so the app keeps working.
93
93
  </p>
94
94
  {forced && (
95
95
  <p className="text-[10px] text-muted-foreground mt-0.5">
@@ -2891,7 +2891,7 @@ function SettingsPanelContent({
2891
2891
  id={settingsSectionDomId("demo-mode")}
2892
2892
  icon={<IconEyeOff size={14} />}
2893
2893
  title="Demo mode"
2894
- subtitle="Replace names, emails, and numbers with realistic fake data everywhere — in the UI and what the agent sees. IDs and structure are preserved so the app keeps working."
2894
+ subtitle="Replace contact/free-text names, emails, and numbers with realistic fake data everywhere — in the UI and what the agent sees. Labels, IDs, and structure are preserved so the app keeps working."
2895
2895
  open={openSection === "demo-mode"}
2896
2896
  onToggle={() => toggle("demo-mode")}
2897
2897
  >
@@ -9,6 +9,7 @@ import {
9
9
  useCallback,
10
10
  useEffect,
11
11
  useMemo,
12
+ useRef,
12
13
  useState,
13
14
  type ComponentType,
14
15
  type ReactNode,
@@ -151,6 +152,17 @@ function updateHashForTab(tabId: string) {
151
152
  window.history.pushState(null, "", `${pathname}${search}${hash}`);
152
153
  }
153
154
 
155
+ function isEditableElement(element: Element | null): boolean {
156
+ if (!(element instanceof HTMLElement)) return false;
157
+ const tagName = element.tagName.toLowerCase();
158
+ return (
159
+ tagName === "input" ||
160
+ tagName === "textarea" ||
161
+ tagName === "select" ||
162
+ element.isContentEditable
163
+ );
164
+ }
165
+
154
166
  export function SettingsTabsPage({
155
167
  general,
156
168
  team,
@@ -171,6 +183,9 @@ export function SettingsTabsPage({
171
183
  value,
172
184
  onValueChange,
173
185
  }: SettingsTabsPageProps) {
186
+ const rootRef = useRef<HTMLDivElement | null>(null);
187
+ const searchInputRef = useRef<HTMLInputElement | null>(null);
188
+ const autoFocusedSearchRef = useRef(false);
174
189
  const tabs = useMemo<SettingsTabItem[]>(() => {
175
190
  const hasOrganizationTab = extraTabs.some(
176
191
  (tab) => tab.id === "organization",
@@ -254,6 +269,37 @@ export function SettingsTabsPage({
254
269
  return () => window.removeEventListener("hashchange", handleHashChange);
255
270
  }, [isControlled, tabs]);
256
271
 
272
+ useEffect(() => {
273
+ if (!enableSearch || autoFocusedSearchRef.current) return;
274
+ if (
275
+ typeof window === "undefined" ||
276
+ typeof window.matchMedia !== "function"
277
+ ) {
278
+ return;
279
+ }
280
+ if (window.matchMedia("(max-width: 767px)").matches) return;
281
+
282
+ const frame = window.requestAnimationFrame(() => {
283
+ autoFocusedSearchRef.current = true;
284
+ const input = searchInputRef.current;
285
+ if (!input) return;
286
+
287
+ const activeElement = document.activeElement;
288
+ const activeInsideSettings =
289
+ !!activeElement && rootRef.current?.contains(activeElement);
290
+ if (
291
+ activeElement !== input &&
292
+ (isEditableElement(activeElement) || activeInsideSettings)
293
+ ) {
294
+ return;
295
+ }
296
+
297
+ input.focus({ preventScroll: true });
298
+ });
299
+
300
+ return () => window.cancelAnimationFrame(frame);
301
+ }, [enableSearch]);
302
+
257
303
  const selectedTab = tabs.find((tab) => tab.id === activeTab) ?? tabs[0];
258
304
 
259
305
  // Flatten tab + deep-link entries into one searchable index.
@@ -338,6 +384,7 @@ export function SettingsTabsPage({
338
384
 
339
385
  return (
340
386
  <div
387
+ ref={rootRef}
341
388
  className={cn(
342
389
  "flex h-full min-h-0 w-full flex-col overflow-hidden bg-background sm:flex-row",
343
390
  className,
@@ -345,7 +392,7 @@ export function SettingsTabsPage({
345
392
  >
346
393
  <div
347
394
  className={cn(
348
- "flex shrink-0 flex-col gap-2 border-b border-border bg-background p-2 sm:min-h-0 sm:w-56 sm:overflow-y-auto sm:border-b-0 sm:border-e sm:p-3",
395
+ "flex shrink-0 flex-col gap-2 bg-background p-2 sm:min-h-0 sm:w-56 sm:overflow-y-auto sm:p-3",
349
396
  navClassName,
350
397
  )}
351
398
  >
@@ -353,6 +400,7 @@ export function SettingsTabsPage({
353
400
  <div className="relative sm:mb-1">
354
401
  <IconSearch className="pointer-events-none absolute start-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
355
402
  <input
403
+ ref={searchInputRef}
356
404
  type="search"
357
405
  value={query}
358
406
  onChange={(event) => setQuery(event.target.value)}
@@ -5,7 +5,7 @@ import { writeAppState } from "../../application-state/script-helpers.js";
5
5
 
6
6
  export default defineAction({
7
7
  description:
8
- "Turn demo mode on or off. When demo mode is on, the app replaces real names, email addresses, and numbers with realistic fake data everywhere — in the UI and in what you (the agent) see — while keeping IDs, dates, and structure intact so everything still works. Use when the user asks to 'hide my data', 'turn on demo mode', 'anonymize this for a screen share / recording', or similar. This is the same toggle as the Demo mode switch in settings.",
8
+ "Turn demo mode on or off. When demo mode is on, the app replaces contact/free-text names, email addresses, and numbers with realistic fake data everywhere — in the UI and in what you (the agent) see — while keeping labels, IDs, dates, and structure intact so everything still works. Use when the user asks to 'hide my data', 'turn on demo mode', 'anonymize this for a screen share / recording', or similar. This is the same toggle as the Demo mode switch in settings.",
9
9
  schema: z.object({
10
10
  enabled: z
11
11
  .boolean()
@@ -16,7 +16,7 @@ export default defineAction({
16
16
  return {
17
17
  enabled,
18
18
  message: enabled
19
- ? "Demo mode is ON — real names, emails, and numbers are now replaced with deterministic fake data everywhere (UI and agent). IDs and structure are preserved so everything keeps working."
19
+ ? "Demo mode is ON — contact/free-text names, emails, and numbers are now replaced with deterministic fake data everywhere (UI and agent). Labels, IDs, and structure are preserved so everything keeps working."
20
20
  : "Demo mode is OFF — real data is shown again.",
21
21
  };
22
22
  },