@agent-native/core 0.157.0 → 0.157.2

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 (114) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +19 -4
  3. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +7 -2
  4. package/corpus/templates/assets/app/global.css +4 -0
  5. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1 -2
  6. package/corpus/templates/brain/app/routes/sources.tsx +1 -2
  7. package/corpus/templates/calendar/app/hooks/use-bookings.ts +12 -2
  8. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +64 -2
  9. package/corpus/templates/calendar/server/handlers/bookings.ts +224 -93
  10. package/corpus/templates/calendar/server/plugins/auth.ts +0 -2
  11. package/corpus/templates/clips/actions/create-folder.ts +7 -1
  12. package/corpus/templates/clips/actions/create-meeting.ts +6 -2
  13. package/corpus/templates/clips/actions/create-recording.ts +5 -3
  14. package/corpus/templates/clips/actions/finalize-recording.ts +97 -65
  15. package/corpus/templates/clips/actions/import-loom-recording.ts +5 -3
  16. package/corpus/templates/clips/actions/request-recording-access.ts +305 -0
  17. package/corpus/templates/clips/actions/stitch-recordings.ts +5 -3
  18. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +38 -63
  19. package/corpus/templates/clips/app/components/player/share-dialog.tsx +75 -75
  20. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +3 -21
  21. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +13 -30
  22. package/corpus/templates/clips/app/i18n/en-US.ts +10 -0
  23. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -5
  24. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +9 -16
  25. package/corpus/templates/clips/app/routes/share.$shareId.tsx +135 -18
  26. package/corpus/templates/clips/changelog/2026-08-14-long-resumable-uploads-now-abort-cleanly-and-show-errors.md +6 -0
  27. package/corpus/templates/clips/changelog/2026-08-14-standardized-share-popovers.md +6 -0
  28. package/corpus/templates/clips/chrome-extension/public/manifest.json +11 -3
  29. package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +37 -8
  30. package/corpus/templates/clips/chrome-extension/src/github-preview.ts +3 -3
  31. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +8 -25
  32. package/corpus/templates/clips/desktop/src/app.tsx +24 -0
  33. package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +42 -13
  34. package/corpus/templates/clips/desktop/src/lib/recorder.ts +11 -18
  35. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +1 -0
  36. package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
  37. package/corpus/templates/clips/desktop/src/styles.css +24 -9
  38. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -0
  39. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +8 -0
  40. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +154 -176
  41. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +115 -31
  42. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +7 -0
  43. package/corpus/templates/clips/server/db/index.ts +1 -0
  44. package/corpus/templates/clips/server/db/schema.ts +1 -0
  45. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -1
  46. package/corpus/templates/clips/server/lib/emails.ts +23 -0
  47. package/corpus/templates/clips/server/lib/public-agent-context.ts +55 -0
  48. package/corpus/templates/clips/server/lib/recordings.ts +2 -1
  49. package/corpus/templates/clips/server/lib/resumable-upload-cleanup.ts +49 -0
  50. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +34 -7
  51. package/corpus/templates/clips/server/lib/upload-interruption.ts +15 -1
  52. package/corpus/templates/clips/server/lib/upload-lease.ts +94 -9
  53. package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +82 -7
  54. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +1 -0
  55. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +71 -12
  56. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +4 -3
  57. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +133 -4
  58. package/corpus/templates/clips/server/routes/api/uploads/reap.post.ts +8 -1
  59. package/corpus/templates/clips/shared/recording-link.ts +4 -0
  60. package/corpus/templates/clips/shared/share-loader-response.ts +2 -1
  61. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +7 -16
  62. package/corpus/templates/factory/AGENTS.md +5 -1
  63. package/corpus/templates/factory/README.md +6 -2
  64. package/corpus/templates/factory/actions/view-screen.ts +15 -0
  65. package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +58 -189
  66. package/corpus/templates/factory/app/components/layout/Header.tsx +1 -0
  67. package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
  68. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +7 -0
  69. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -0
  70. package/corpus/templates/factory/app/i18n/en-US.ts +3 -1
  71. package/corpus/templates/factory/app/routes/agents.tsx +9 -0
  72. package/corpus/templates/mail/server/plugins/auth.ts +0 -2
  73. package/corpus/templates/plan/app/components/plan/LocalCodebasePicker.tsx +50 -14
  74. package/corpus/templates/plan/app/pages/PlansPage.tsx +9 -19
  75. package/corpus/templates/slides/app/components/editor/HistoryPanel.tsx +7 -1
  76. package/corpus/templates/slides/app/context/DeckContext.tsx +71 -22
  77. package/corpus/templates/slides/app/lib/editor-shortcuts.ts +46 -0
  78. package/corpus/templates/slides/app/pages/DeckEditor.tsx +13 -0
  79. package/dist/client/AgentPanel.d.ts +13 -1
  80. package/dist/client/AgentPanel.js +12 -10
  81. package/dist/client/AssistantChat.js +25 -5
  82. package/dist/client/HighlightedCodeBlock.js +12 -2
  83. package/dist/client/agent-chat/index.d.ts +1 -0
  84. package/dist/client/agent-chat/index.js +1 -0
  85. package/dist/client/chat/markdown-renderer.js +16 -3
  86. package/dist/client/chat-first/apps-rail.js +1 -1
  87. package/dist/client/components/ui/message-scroller.js +1 -1
  88. package/dist/client/settings/AccountSettingsCard.js +48 -19
  89. package/dist/client/sharing/ShareButton.d.ts +1 -2
  90. package/dist/client/sharing/ShareButton.js +5 -41
  91. package/dist/client/sharing/ShareDialog.js +5 -4
  92. package/dist/client/use-chat-threads.js +10 -14
  93. package/dist/collab/awareness.d.ts +2 -2
  94. package/dist/collab/routes.d.ts +1 -1
  95. package/dist/collab/struct-routes.d.ts +1 -1
  96. package/dist/localization/default-messages.d.ts +1 -0
  97. package/dist/localization/default-messages.js +1 -0
  98. package/dist/notifications/routes.d.ts +3 -3
  99. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  100. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  101. package/dist/server/auth-marketing.d.ts +1 -0
  102. package/dist/server/auth-marketing.js +0 -2
  103. package/dist/server/auth.d.ts +1 -0
  104. package/dist/server/auth.js +18 -0
  105. package/dist/server/onboarding-html.d.ts +1 -0
  106. package/dist/server/onboarding-html.js +2 -88
  107. package/dist/server/realtime-token.d.ts +1 -1
  108. package/dist/shared/markdown-block-split.d.ts +16 -0
  109. package/dist/shared/markdown-block-split.js +81 -5
  110. package/dist/styles/agent-native.css +13 -0
  111. package/dist/voice/voice-cleanup-prompt.js +1 -0
  112. package/docs/content/cloneable-saas.mdx +24 -26
  113. package/package.json +3 -3
  114. package/corpus/templates/clips/shared/mic-audio-cleanup.ts +0 -350
@@ -59,10 +59,17 @@ export default defineEventHandler(async (event: H3Event) => {
59
59
  const result = await reapExpiredUploads({
60
60
  dryRun: query.dryRun === "1" || query.dryRun === "true",
61
61
  });
62
- if (result.failed > 0 || result.scratchKeysDeleted > 0) {
62
+ if (
63
+ result.failed > 0 ||
64
+ result.scratchKeysDeleted > 0 ||
65
+ result.resumableSessionsAborted > 0 ||
66
+ result.resumableCleanupFailed > 0
67
+ ) {
63
68
  console.log("[uploads] reaped expired uploads", {
64
69
  failed: result.failed,
65
70
  scratchKeysDeleted: result.scratchKeysDeleted,
71
+ resumableSessionsAborted: result.resumableSessionsAborted,
72
+ resumableCleanupFailed: result.resumableCleanupFailed,
66
73
  });
67
74
  }
68
75
  return { ok: true, ...result };
@@ -12,6 +12,10 @@
12
12
  */
13
13
  import { withShareAttribution } from "./share-attribution";
14
14
 
15
+ /** Short-lived capability used by a signed-in private share viewer to request access. */
16
+ export const CLIPS_ACCESS_REQUEST_TOKEN_PREFIX = "clips-access-request";
17
+ export const CLIPS_ACCESS_REQUEST_TOKEN_TTL_SECONDS = 10 * 60;
18
+
15
19
  /** Public share path for a recording, relative to the app base path. */
16
20
  export function recordingSharePath(recordingId: string): string {
17
21
  return `/share/${encodeURIComponent(recordingId)}`;
@@ -5,8 +5,9 @@ export const PRIVATE_SHARE_RESPONSE_HEADERS = {
5
5
  "Referrer-Policy": "no-referrer",
6
6
  };
7
7
 
8
- export function privateShareLoaderData<T>(payload: T) {
8
+ export function privateShareLoaderData<T>(payload: T, status = 200) {
9
9
  return data(payload, {
10
+ status,
10
11
  headers: PRIVATE_SHARE_RESPONSE_HEADERS,
11
12
  });
12
13
  }
@@ -6,6 +6,7 @@ import { useT } from "@agent-native/core/client/i18n";
6
6
  import { ShareButton } from "@agent-native/core/client/sharing";
7
7
  import { CreativeContextShareTab } from "@agent-native/creative-context/client";
8
8
  import { PresenceBar } from "@agent-native/toolkit/collab-ui";
9
+ import { ShareTrigger } from "@agent-native/toolkit/sharing";
9
10
  import type { DocumentSourceInfo } from "@shared/api";
10
11
  import {
11
12
  IconArrowBarDown,
@@ -31,7 +32,6 @@ import {
31
32
  IconLink,
32
33
  IconMessageCircle,
33
34
  IconRefresh,
34
- IconShare3,
35
35
  IconTrash,
36
36
  } from "@tabler/icons-react";
37
37
  import { useQueryClient } from "@tanstack/react-query";
@@ -891,22 +891,13 @@ export function DocumentToolbar({
891
891
  className="mr-1"
892
892
  />
893
893
  {isLocalFileDocument ? (
894
- <Button
895
- size="sm"
896
- variant="outline"
897
- className="h-9 gap-1.5 rounded-lg px-3"
894
+ <ShareTrigger
895
+ className="h-9 rounded-lg px-3"
896
+ pending={shareLocalFile.isPending}
898
897
  disabled={shareLocalFile.isPending}
899
- onClick={() => void handleShareLocalFile()}
900
- >
901
- {shareLocalFile.isPending ? (
902
- <IconLoader2 className="h-4 w-4 animate-spin" />
903
- ) : (
904
- <IconShare3 className="h-4 w-4" />
905
- )}
906
- <span className="hidden sm:inline">
907
- {t("editor.toolbar.share")}
908
- </span>
909
- </Button>
898
+ label={t("editor.toolbar.share")}
899
+ onPress={() => void handleShareLocalFile()}
900
+ />
910
901
  ) : (
911
902
  <>
912
903
  <ShareButton
@@ -35,7 +35,8 @@ decisions, feedback, agent runs, and provider audit records.
35
35
 
36
36
  ## Application state
37
37
 
38
- - `navigation.view`: `factory` when the workspace is open.
38
+ - `navigation.view`: `factory` for the Factory workspace or `agents` for the
39
+ shared Agents sidebar surface.
39
40
  - `navigation.factoryId`: selected Factory id when present.
40
41
  - `navigation.factoryTab`: `map` | `inbox` | `rules` | `automations` | `agents` | `audit` | `settings`.
41
42
  - `navigation.factoryAuditRunId`: selected automation run in the audit view when present.
@@ -43,6 +44,9 @@ decisions, feedback, agent runs, and provider audit records.
43
44
  - A selected graph node or edge is part of `navigation` context. Read
44
45
  `view-screen` before answering why a route exists or changing the selected
45
46
  Factory.
47
+ - The `/agents` surface shows mounted agentic apps and reusable agent packs from
48
+ the same Dispatch SQL/action registry. Use `view-screen` there before
49
+ answering questions about the visible agent inventory.
46
50
 
47
51
  ## Action contract
48
52
 
@@ -76,8 +76,8 @@ approves a Factory item.
76
76
 
77
77
  ## Agents and agentic apps
78
78
 
79
- Factory's top-level Agents tab is a shared workspace surface, not a second
80
- registry. It reads mounted agentic apps and their editable Dispatch metadata,
79
+ Factory's top-level **Agents** sidebar tab is a shared workspace surface, not a
80
+ second registry. It reads mounted agentic apps and their editable Dispatch metadata,
81
81
  then embeds Dispatch's reusable-agent manager for create, chat, import, and
82
82
  folder-backed pack editing. A simple agent's profile lives at
83
83
  `agents/<slug>.md`; its optional context, references, and private skills live
@@ -86,6 +86,10 @@ Cowork-style folder, or another text-based agent setup. The importer strips
86
86
  credentials, hooks, shell commands, and local environment settings, and All-app
87
87
  imports remain subject to Dispatch approval policy.
88
88
 
89
+ The page uses the same two-column library treatment for mounted apps and
90
+ reusable agents. Empty states keep creation actions in the content area, and
91
+ agent-specific secondary actions live behind each row's overflow menu.
92
+
89
93
  When editing a factory flow, an agent step can bind to either a shared reusable
90
94
  agent or a ready mounted agentic app from the same workspace database. The map
91
95
  stores the selected target type and id, while the graph remains a reviewable
@@ -82,6 +82,21 @@ export default defineAction({
82
82
  }
83
83
  }
84
84
 
85
+ if (
86
+ navigation &&
87
+ typeof navigation === "object" &&
88
+ "view" in navigation &&
89
+ navigation.view === "agents"
90
+ ) {
91
+ const [apps, agents] = await Promise.all([
92
+ runDispatchAction("list-workspace-apps", {
93
+ includeAgentCards: false,
94
+ }),
95
+ runDispatchAction("list-workspace-resources", { kind: "agent" }),
96
+ ]);
97
+ screen.agents = { apps, agents };
98
+ }
99
+
85
100
  if (Object.keys(screen).length === 0) {
86
101
  return "No application state found. Is the app running?";
87
102
  }
@@ -1,33 +1,14 @@
1
- import {
2
- useActionMutation,
3
- useActionQuery,
4
- } from "@agent-native/core/client/hooks";
1
+ import { useActionQuery } from "@agent-native/core/client/hooks";
5
2
  import { useT } from "@agent-native/core/client/i18n";
6
- import { SimpleAgentsPanel } from "@agent-native/dispatch/components";
7
- import { ActionQueryError } from "@agent-native/dispatch/components";
8
- import { Badge } from "@agent-native/dispatch/components/ui/badge";
9
- import {
10
- Dialog,
11
- DialogContent,
12
- DialogDescription,
13
- DialogFooter,
14
- DialogHeader,
15
- DialogTitle,
16
- DialogTrigger,
17
- } from "@agent-native/dispatch/components/ui/dialog";
18
3
  import {
19
- IconEdit,
20
- IconExternalLink,
21
- IconLayoutGrid,
22
- } from "@tabler/icons-react";
23
- import { useEffect, useState } from "react";
24
- import { toast } from "sonner";
4
+ ActionQueryError,
5
+ CreateAppPopover,
6
+ SimpleAgentsPanel,
7
+ WorkspaceAppCard,
8
+ } from "@agent-native/dispatch/components";
9
+ import { IconPlus } from "@tabler/icons-react";
25
10
 
26
11
  import { Button } from "@/components/ui/button";
27
- import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
28
- import { Input } from "@/components/ui/input";
29
- import { Label } from "@/components/ui/label";
30
- import { Textarea } from "@/components/ui/textarea";
31
12
 
32
13
  interface WorkspaceAppSummary {
33
14
  id: string;
@@ -40,88 +21,12 @@ interface WorkspaceAppSummary {
40
21
  statusLabel?: string;
41
22
  }
42
23
 
43
- function EditAppDialog({
44
- app,
45
- onSaved,
46
- t,
47
- }: {
48
- app: WorkspaceAppSummary;
49
- onSaved: () => void;
50
- t: ReturnType<typeof useT>;
51
- }) {
52
- const [open, setOpen] = useState(false);
53
- const [name, setName] = useState(app.name);
54
- const [description, setDescription] = useState(app.description ?? "");
55
- const update = useActionMutation("update-workspace-app-metadata", {
56
- onSuccess: () => {
57
- toast.success(t("factoryRoute.appDetailsUpdated"));
58
- setOpen(false);
59
- onSaved();
60
- },
61
- onError: (error) => toast.error(error.message),
62
- });
63
-
64
- useEffect(() => {
65
- if (!open) return;
66
- setName(app.name);
67
- setDescription(app.description ?? "");
68
- }, [app, open]);
69
-
24
+ function CreateAppButton({ label }: { label: string }) {
70
25
  return (
71
- <Dialog open={open} onOpenChange={setOpen}>
72
- <DialogTrigger asChild>
73
- <Button variant="ghost" size="icon" aria-label={`Edit ${app.name}`}>
74
- <IconEdit size={16} />
75
- </Button>
76
- </DialogTrigger>
77
- <DialogContent>
78
- <DialogHeader>
79
- <DialogTitle>{t("factoryRoute.editAppDetails")}</DialogTitle>
80
- <DialogDescription className="sr-only">
81
- {t("factoryRoute.editAppDetailsDescription")}
82
- </DialogDescription>
83
- </DialogHeader>
84
- <div className="space-y-4 py-2">
85
- <div className="space-y-2">
86
- <Label htmlFor={`factory-app-name-${app.id}`}>
87
- {t("factoryRoute.appName")}
88
- </Label>
89
- <Input
90
- id={`factory-app-name-${app.id}`}
91
- value={name}
92
- onChange={(event) => setName(event.target.value)}
93
- />
94
- </div>
95
- <div className="space-y-2">
96
- <Label htmlFor={`factory-app-description-${app.id}`}>
97
- {t("factoryRoute.appDescription")}
98
- </Label>
99
- <Textarea
100
- id={`factory-app-description-${app.id}`}
101
- value={description}
102
- onChange={(event) => setDescription(event.target.value)}
103
- rows={4}
104
- />
105
- </div>
106
- </div>
107
- <DialogFooter>
108
- <Button
109
- onClick={() =>
110
- update.mutate({
111
- appId: app.id,
112
- name: name.trim(),
113
- description: description.trim(),
114
- })
115
- }
116
- disabled={!name.trim() || update.isPending}
117
- >
118
- {update.isPending
119
- ? t("factoryRoute.savingChanges")
120
- : t("factoryRoute.saveChanges")}
121
- </Button>
122
- </DialogFooter>
123
- </DialogContent>
124
- </Dialog>
26
+ <Button size="sm">
27
+ <IconPlus size={15} />
28
+ {label}
29
+ </Button>
125
30
  );
126
31
  }
127
32
 
@@ -130,6 +35,7 @@ function AgenticAppsSection({ t }: { t: ReturnType<typeof useT> }) {
130
35
  includeAgentCards: false,
131
36
  });
132
37
  const apps = (query.data ?? []).filter((app) => !app.isDispatch);
38
+ const createAppLabel = t("factoryRoute.createApp");
133
39
 
134
40
  if (query.isError) {
135
41
  return (
@@ -141,80 +47,52 @@ function AgenticAppsSection({ t }: { t: ReturnType<typeof useT> }) {
141
47
  }
142
48
 
143
49
  return (
144
- <Card>
145
- <CardHeader>
146
- <CardTitle className="flex items-center gap-2 text-base">
147
- <IconLayoutGrid className="size-4 text-muted-foreground" />
50
+ <section className="flex flex-col gap-4">
51
+ <div className="flex flex-wrap items-center justify-between gap-3">
52
+ <h2 className="text-base font-medium text-foreground">
148
53
  {t("factoryRoute.agenticAppsTitle")}
149
- </CardTitle>
150
- </CardHeader>
151
- <CardContent>
152
- {query.isLoading && apps.length === 0 ? (
153
- <div className="space-y-3">
154
- {[0, 1].map((item) => (
155
- <div
156
- key={item}
157
- className="h-16 animate-pulse rounded-xl bg-muted"
158
- />
159
- ))}
160
- </div>
161
- ) : apps.length > 0 ? (
162
- <div className="space-y-2">
163
- {apps.map((app) => (
164
- <div
165
- key={app.id}
166
- className="flex items-start gap-3 rounded-xl border px-4 py-3"
167
- >
168
- <div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
169
- <IconLayoutGrid size={18} />
170
- </div>
171
- <div className="min-w-0 flex-1">
172
- <div className="flex flex-wrap items-center gap-2">
173
- <span className="text-sm font-medium">{app.name}</span>
174
- {app.status ? (
175
- <Badge variant="outline">
176
- {app.statusLabel || app.status}
177
- </Badge>
178
- ) : null}
179
- </div>
180
- {app.description ? (
181
- <div className="mt-1 text-sm text-muted-foreground">
182
- {app.description}
183
- </div>
184
- ) : null}
185
- <div className="mt-1 font-mono text-[11px] text-muted-foreground/70">
186
- {app.path}
187
- </div>
188
- </div>
189
- <div className="flex shrink-0 items-center gap-1">
190
- {app.url ? (
191
- <Button variant="ghost" size="icon" asChild>
192
- <a
193
- href={app.url}
194
- target="_blank"
195
- rel="noreferrer"
196
- aria-label={t("factoryRoute.openApp")}
197
- >
198
- <IconExternalLink size={16} />
199
- </a>
200
- </Button>
201
- ) : null}
202
- <EditAppDialog
203
- app={app}
204
- t={t}
205
- onSaved={() => void query.refetch()}
206
- />
207
- </div>
208
- </div>
209
- ))}
210
- </div>
211
- ) : (
212
- <div className="rounded-xl border border-dashed px-6 py-10 text-center text-sm text-muted-foreground">
54
+ </h2>
55
+ {apps.length > 0 ? (
56
+ <CreateAppPopover
57
+ align="end"
58
+ trigger={<CreateAppButton label={createAppLabel} />}
59
+ onCreated={() => void query.refetch()}
60
+ />
61
+ ) : null}
62
+ </div>
63
+ {query.isLoading && apps.length === 0 ? (
64
+ <div className="grid gap-3 md:grid-cols-2">
65
+ {[0, 1, 2, 3].map((item) => (
66
+ <div
67
+ key={item}
68
+ className="h-24 animate-pulse rounded-2xl border bg-muted"
69
+ />
70
+ ))}
71
+ </div>
72
+ ) : apps.length > 0 ? (
73
+ <div className="grid gap-3 md:grid-cols-2">
74
+ {apps.map((app) => (
75
+ <WorkspaceAppCard
76
+ key={app.id}
77
+ app={app}
78
+ className="rounded-2xl border bg-card px-4 py-4"
79
+ />
80
+ ))}
81
+ </div>
82
+ ) : (
83
+ <div className="rounded-2xl border border-dashed bg-card px-4 py-12 text-center">
84
+ <div className="text-sm font-medium text-foreground">
213
85
  {t("factoryRoute.agenticAppsEmpty")}
214
86
  </div>
215
- )}
216
- </CardContent>
217
- </Card>
87
+ <div className="mt-4 flex justify-center">
88
+ <CreateAppPopover
89
+ trigger={<CreateAppButton label={createAppLabel} />}
90
+ onCreated={() => void query.refetch()}
91
+ />
92
+ </div>
93
+ </div>
94
+ )}
95
+ </section>
218
96
  );
219
97
  }
220
98
 
@@ -222,18 +100,9 @@ export function FactoryAgentsView() {
222
100
  const t = useT();
223
101
 
224
102
  return (
225
- <div className="space-y-6 p-4 lg:p-6">
103
+ <div className="flex flex-col gap-8 p-6 lg:p-8">
226
104
  <AgenticAppsSection t={t} />
227
- <Card>
228
- <CardHeader>
229
- <CardTitle className="text-base">
230
- {t("factoryRoute.agentsTitle")}
231
- </CardTitle>
232
- </CardHeader>
233
- <CardContent>
234
- <SimpleAgentsPanel />
235
- </CardContent>
236
- </Card>
105
+ <SimpleAgentsPanel title={t("factoryRoute.agentsTitle")} />
237
106
  </div>
238
107
  );
239
108
  }
@@ -12,6 +12,7 @@ import { APP_TITLE } from "@/lib/app-config";
12
12
  const pageTitleKeys: Record<string, string> = {
13
13
  "/": "navigation.chat",
14
14
  "/chat": "navigation.chat",
15
+ "/agents": "navigation.agents",
15
16
  "/observability": "navigation.observability",
16
17
  "/agent": "settings.agentTitle",
17
18
  "/settings": "navigation.settings",
@@ -52,7 +52,7 @@ export function Layout({ children }: LayoutProps) {
52
52
  const navigate = useNavigate();
53
53
  const t = useT();
54
54
  const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
55
- const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
55
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
56
56
  const isChatRoute =
57
57
  location.pathname === "/" ||
58
58
  location.pathname === "/chat" ||
@@ -17,6 +17,7 @@ import {
17
17
  IconLayoutSidebarLeftCollapse,
18
18
  IconLayoutSidebarLeftExpand,
19
19
  IconGitPullRequest,
20
+ IconHierarchy2,
20
21
  IconMessageCircle,
21
22
  IconSearch,
22
23
  IconSettings,
@@ -46,6 +47,12 @@ const navItems = [
46
47
  href: "/factory",
47
48
  view: "factory",
48
49
  },
50
+ {
51
+ icon: IconHierarchy2,
52
+ labelKey: "navigation.agents",
53
+ href: "/agents",
54
+ view: "agents",
55
+ },
49
56
  ];
50
57
 
51
58
  const bottomNavItems = [
@@ -72,6 +72,7 @@ function viewForPath(pathname: string): string {
72
72
  if (pathname.startsWith("/database")) return "database";
73
73
  if (pathname.startsWith("/extensions")) return "extensions";
74
74
  if (pathname.startsWith("/observability")) return "observability";
75
+ if (pathname.startsWith("/agents")) return "agents";
75
76
  if (pathname.startsWith("/factory")) return "factory";
76
77
  if (pathname.startsWith("/settings/agent") || pathname.startsWith("/agent")) {
77
78
  return "agent";
@@ -95,6 +96,8 @@ function pathForView(view?: string): string {
95
96
  return "/observability";
96
97
  case "factory":
97
98
  return "/factory";
99
+ case "agents":
100
+ return "/agents";
98
101
  case "agent":
99
102
  return "/settings/agent";
100
103
  case "settings":
@@ -40,6 +40,7 @@ const messages = {
40
40
  unpinChat: "Unpin Chat",
41
41
  },
42
42
  navigation: {
43
+ agents: "Agents",
43
44
  chat: "Chat",
44
45
  collapseSidebar: "Collapse Sidebar",
45
46
  database: "Database",
@@ -51,7 +52,7 @@ const messages = {
51
52
  openNavigation: "Open navigation",
52
53
  settings: "Settings",
53
54
  team: "Team",
54
- triage: "Factory",
55
+ triage: "Factories",
55
56
  },
56
57
  triage: {
57
58
  title: "Factory",
@@ -165,6 +166,7 @@ const messages = {
165
166
  agentsTab: "Agents",
166
167
  agenticAppsTitle: "Agentic apps",
167
168
  agenticAppsEmpty: "No agentic apps in this workspace yet.",
169
+ createApp: "Create app",
168
170
  editAppDetails: "Edit app details",
169
171
  editAppDetailsDescription:
170
172
  "Update the shared display name and description for this workspace app.",
@@ -0,0 +1,9 @@
1
+ import { FactoryAgentsView } from "@/components/factory/FactoryAgentsView";
2
+
3
+ export default function AgentsRoute() {
4
+ return (
5
+ <div className="h-full min-h-0 overflow-y-auto bg-background">
6
+ <FactoryAgentsView />
7
+ </div>
8
+ );
9
+ }
@@ -33,8 +33,6 @@ export default createAuthPlugin({
33
33
  "Multi-account Gmail in a single unified inbox",
34
34
  "Autonomous triage, archiving, and follow-ups",
35
35
  ],
36
- runLocalCommand:
37
- "npx @agent-native/core@latest create my-mail-app --template mail",
38
36
  },
39
37
  // Gmail Pub/Sub push notifications POST here from Google's servers — no
40
38
  // user session. The handler itself verifies the OIDC token when
@@ -108,8 +108,17 @@ export function LocalCodebasePicker() {
108
108
  if (removedCount > 0) {
109
109
  await queryClient.invalidateQueries({ queryKey: ["resources"] });
110
110
  }
111
+
112
+ const failedResult = results.find(
113
+ (result) => result.status === "rejected",
114
+ );
115
+ if (failedResult?.status === "rejected") {
116
+ throw failedResult.reason instanceof Error
117
+ ? failedResult.reason
118
+ : new Error(t("raw.localCodebase.codebaseSyncFailed"));
119
+ }
111
120
  },
112
- [queryClient],
121
+ [queryClient, t],
113
122
  );
114
123
 
115
124
  useEffect(() => {
@@ -187,11 +196,12 @@ export function LocalCodebasePicker() {
187
196
  handle: chosen.handle,
188
197
  latest: null,
189
198
  };
190
- if (active && active.id !== selection.id) {
191
- await cleanupLocalResources(active);
192
- }
193
- setActive(selection);
199
+ setSyncState({ kind: "syncing" });
194
200
  try {
201
+ if (active && active.id !== selection.id) {
202
+ await cleanupLocalResources(active);
203
+ }
204
+ setActive(selection);
195
205
  await syncSelection(selection);
196
206
  } catch (err) {
197
207
  const message =
@@ -207,16 +217,28 @@ export function LocalCodebasePicker() {
207
217
 
208
218
  const clearSelection = useCallback(async () => {
209
219
  const previous = active;
210
- await clearLocalCodebaseSelection();
211
- setActive(null);
212
- setSummary(null);
213
- setSyncState({ kind: "idle" });
214
- await syncAppState(null);
215
- if (previous) {
216
- await cleanupLocalResources(previous);
220
+ setSyncState({ kind: "syncing" });
221
+ try {
222
+ if (previous) {
223
+ await cleanupLocalResources(previous);
224
+ }
225
+ await clearLocalCodebaseSelection();
226
+ setActive(null);
227
+ setSummary(null);
228
+ await syncAppState(null);
229
+ setSyncState({ kind: "idle" });
230
+ toast(t("raw.localCodebase.codebaseUnlinked"));
231
+ } catch (err) {
232
+ const message =
233
+ err instanceof Error
234
+ ? err.message
235
+ : t("raw.localCodebase.codebaseSyncFailed");
236
+ setSyncState({ kind: "error", message });
237
+ toast.error(t("raw.localCodebase.codebaseSyncFailed"), {
238
+ description: message,
239
+ });
217
240
  }
218
- toast(t("raw.localCodebase.codebaseUnlinked"));
219
- }, [active, cleanupLocalResources, syncAppState]);
241
+ }, [active, cleanupLocalResources, syncAppState, t]);
220
242
 
221
243
  const resync = useCallback(async () => {
222
244
  if (!active) return;
@@ -338,6 +360,20 @@ export function LocalCodebasePicker() {
338
360
  </>
339
361
  )}
340
362
 
363
+ {syncState.kind === "syncing" && (
364
+ <span
365
+ className={cn(
366
+ "inline-flex min-h-8 max-w-[280px] items-center gap-1.5 truncate text-xs",
367
+ statusClasses(syncState.kind),
368
+ )}
369
+ >
370
+ <IconRefresh className="size-3.5 shrink-0 animate-spin" />
371
+ <span className="truncate">
372
+ {t("raw.localCodebase.clearCodebase")}
373
+ </span>
374
+ </span>
375
+ )}
376
+
341
377
  {syncState.kind === "error" && (
342
378
  <span
343
379
  className={cn(