@agent-native/core 0.172.9 → 0.173.0-nightly-20260825065619

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 (58) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/changelog/2026-08-24-chat-draft-preservation.md +1 -0
  3. package/corpus/templates/calendar/actions/view-screen.ts +0 -5
  4. package/corpus/templates/calendar/app/components/calendar/CommandPalette.tsx +1 -1
  5. package/corpus/templates/clips/app/components/player/video-player.tsx +3 -1
  6. package/corpus/templates/clips/changelog/2026-08-24-muted-microphone-start-guidance.md +1 -0
  7. package/corpus/templates/clips/changelog/2026-08-24-shared-clips-now-expose-agent-readable-transcript-and-frame-metadata-from-public-links.md +6 -0
  8. package/corpus/templates/clips/server/routes/[...page].get.ts +172 -3
  9. package/corpus/templates/plan/app/components/layout/Header.tsx +1 -1
  10. package/corpus/templates/plan/app/components/layout/Layout.tsx +19 -3
  11. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +11 -10
  12. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +10 -9
  13. package/corpus/templates/plan/app/pages/PlansPage.tsx +1 -1
  14. package/corpus/templates/plan/app/root.tsx +9 -8
  15. package/corpus/templates/plan/app/routes/_index.tsx +6 -30
  16. package/corpus/templates/plan/app/routes/chat.tsx +34 -0
  17. package/corpus/templates/plan/server/plugins/auth.ts +1 -0
  18. package/corpus/templates/slides/changelog/2026-08-24-deck-previews-and-sharing-live-in-the-overflow-menu.md +1 -0
  19. package/dist/agent/harness/migrations.js +1 -1
  20. package/dist/client/AgentPanel.d.ts +3 -1
  21. package/dist/client/AgentPanel.js +67 -46
  22. package/dist/client/AssistantChat.d.ts +8 -0
  23. package/dist/client/AssistantChat.js +2 -1
  24. package/dist/client/agent-chat/index.d.ts +2 -0
  25. package/dist/client/agent-chat/index.js +2 -0
  26. package/dist/client/chat/index.d.ts +2 -0
  27. package/dist/client/chat/index.js +2 -0
  28. package/dist/client/chat/message-components.d.ts +5 -14
  29. package/dist/client/chat/message-components.js +15 -8
  30. package/dist/client/chat/tool-call-display.js +24 -11
  31. package/dist/client/thinking-display.d.ts +23 -0
  32. package/dist/client/thinking-display.js +86 -0
  33. package/dist/collab/struct-routes.d.ts +1 -1
  34. package/dist/localization/core-messages/ar-SA.js +4 -0
  35. package/dist/localization/core-messages/de-DE.js +4 -0
  36. package/dist/localization/core-messages/en-US.d.ts +4 -0
  37. package/dist/localization/core-messages/en-US.js +4 -0
  38. package/dist/localization/core-messages/es-ES.js +4 -0
  39. package/dist/localization/core-messages/fr-FR.js +4 -0
  40. package/dist/localization/core-messages/hi-IN.js +4 -0
  41. package/dist/localization/core-messages/ja-JP.js +4 -0
  42. package/dist/localization/core-messages/ko-KR.js +4 -0
  43. package/dist/localization/core-messages/pt-BR.js +4 -0
  44. package/dist/localization/core-messages/zh-CN.js +4 -0
  45. package/dist/localization/core-messages/zh-TW.js +4 -0
  46. package/dist/localization/core-messages.d.ts +4 -0
  47. package/dist/observability/posthog-ai.d.ts +6 -0
  48. package/dist/observability/posthog-ai.js +6 -0
  49. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  50. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  51. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  52. package/dist/secrets/routes.d.ts +6 -6
  53. package/dist/server/realtime-token.d.ts +1 -1
  54. package/dist/shared/thinking-display.d.ts +23 -0
  55. package/dist/shared/thinking-display.js +31 -0
  56. package/dist/tracking/providers.js +22 -1
  57. package/docs/content/template-calendar-developers.mdx +54 -54
  58. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8596
34
+ - template files: 8598
@@ -2,4 +2,5 @@
2
2
  type: fixed
3
3
  date: 2026-08-24
4
4
  ---
5
+
5
6
  Analytics chat drafts now stay in place while a new chat finishes loading.
@@ -55,11 +55,6 @@ async function fetchEventsForRange(
55
55
  }
56
56
  }
57
57
 
58
- function dateKeyFromParts(date: Date): string {
59
- const pad = (value: number) => String(value).padStart(2, "0");
60
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
61
- }
62
-
63
58
  export default defineAction({
64
59
  description:
65
60
  "See what the user is currently looking at on screen. Returns the current view, date range, and visible events. Always call this first before taking any action.",
@@ -13,7 +13,7 @@ import {
13
13
  IconExternalLink,
14
14
  } from "@tabler/icons-react";
15
15
  import * as chrono from "chrono-node";
16
- import { format, parseISO, parse, isValid } from "date-fns";
16
+ import { format, parse, isValid } from "date-fns";
17
17
 
18
18
  import { cn } from "@/lib/utils";
19
19
 
@@ -1629,6 +1629,8 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
1629
1629
  const fullscreenMenuContainer = isFullscreen ? containerRef.current : null;
1630
1630
 
1631
1631
  const showThroughoutCta = cta && cta.placement === "throughout";
1632
+ const controlsVisible =
1633
+ showControls || !isPlaying || isPlayPending || isBuffering;
1632
1634
  // Mobile Safari may defer loadeddata/canplay until playback starts. Keep
1633
1635
  // the paused state actionable even when those readiness events have not
1634
1636
  // fired yet; once the user asks to play, the pending/buffering states give
@@ -2067,7 +2069,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
2067
2069
  <div
2068
2070
  className={cn(
2069
2071
  "absolute inset-x-0 bottom-0 z-20 transition-opacity duration-200",
2070
- showControls ? "opacity-100" : "opacity-0 pointer-events-none",
2072
+ controlsVisible ? "opacity-100" : "opacity-0 pointer-events-none",
2071
2073
  )}
2072
2074
  >
2073
2075
  <PlayerControls
@@ -2,4 +2,5 @@
2
2
  type: fixed
3
3
  date: 2026-08-24
4
4
  ---
5
+
5
6
  Muted microphone alerts now return you to recording setup before recording starts.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-24
4
+ ---
5
+
6
+ Shared Clips now expose agent-readable transcript and frame metadata from public links.
@@ -1,21 +1,190 @@
1
+ import { verifyScopedAgentAccessToken } from "@agent-native/core/server";
1
2
  import { createH3SSRHandler } from "@agent-native/core/server/ssr-handler";
2
- import { defineEventHandler, setResponseHeader } from "h3";
3
+ import {
4
+ injectDocumentMarkup,
5
+ safeJsonForHtml,
6
+ } from "@agent-native/core/shared";
7
+ import { eq } from "drizzle-orm";
8
+ import {
9
+ defineEventHandler,
10
+ getQuery,
11
+ getRequestURL,
12
+ setResponseHeader,
13
+ type H3Event,
14
+ } from "h3";
3
15
 
16
+ import {
17
+ buildAgentApiUrls,
18
+ buildAgentDiscoveryPayload,
19
+ CLIP_AGENT_ACCESS_TOKEN_PREFIX,
20
+ CLIPS_AGENT_ACCESS_PARAM,
21
+ } from "../../shared/agent-context.js";
22
+ import { getDb, schema } from "../db/index.js";
4
23
  import {
5
24
  MEDIA_CAPTURE_PERMISSIONS_POLICY,
6
25
  withMediaCapturePermissions,
7
26
  } from "../lib/media-permissions.js";
27
+ import {
28
+ getServerAppBasePath,
29
+ queryString,
30
+ } from "../lib/public-agent-context.js";
31
+ import { isRecordingExpired } from "../lib/recording-page-access.js";
8
32
 
9
33
  const ssrHandler = createH3SSRHandler(
10
34
  () => import("virtual:react-router/server-build"),
11
35
  );
12
36
 
13
- export default defineEventHandler(async (event) => {
14
- const response = (await ssrHandler(event)) as Response;
37
+ function stripAppBasePath(pathname: string): string {
38
+ const basePath = getServerAppBasePath();
39
+ if (!basePath) return pathname;
40
+ if (pathname === basePath) return "/";
41
+ return pathname.startsWith(`${basePath}/`)
42
+ ? pathname.slice(basePath.length) || "/"
43
+ : pathname;
44
+ }
45
+
46
+ function clipIdFromPath(pathname: string): string | null {
47
+ const match = stripAppBasePath(pathname).match(/^\/(?:share|r)\/([^/]+)\/?$/);
48
+ if (!match?.[1]) return null;
49
+ try {
50
+ return decodeURIComponent(match[1]);
51
+ } catch {
52
+ return match[1];
53
+ }
54
+ }
55
+
56
+ function escapeHtmlAttribute(value: string): string {
57
+ return value.replace(/[&<>\"']/g, (character) => {
58
+ switch (character) {
59
+ case "&":
60
+ return "&amp;";
61
+ case "<":
62
+ return "&lt;";
63
+ case ">":
64
+ return "&gt;";
65
+ case '\"':
66
+ return "&quot;";
67
+ case "'":
68
+ return "&#39;";
69
+ default:
70
+ return character;
71
+ }
72
+ });
73
+ }
74
+
75
+ async function buildClipAgentDiscovery(event: H3Event): Promise<{
76
+ markup: string;
77
+ noReferrer: boolean;
78
+ } | null> {
79
+ const requestUrl = getRequestURL(event);
80
+ const recordingId = clipIdFromPath(requestUrl.pathname);
81
+ if (!recordingId) return null;
82
+
83
+ const [recording] = await getDb()
84
+ .select({
85
+ id: schema.recordings.id,
86
+ title: schema.recordings.title,
87
+ status: schema.recordings.status,
88
+ visibility: schema.recordings.visibility,
89
+ password: schema.recordings.password,
90
+ expiresAt: schema.recordings.expiresAt,
91
+ archivedAt: schema.recordings.archivedAt,
92
+ trashedAt: schema.recordings.trashedAt,
93
+ })
94
+ .from(schema.recordings)
95
+ .where(eq(schema.recordings.id, recordingId))
96
+ .limit(1);
97
+
98
+ if (
99
+ !recording ||
100
+ recording.archivedAt ||
101
+ recording.trashedAt ||
102
+ isRecordingExpired(recording.expiresAt)
103
+ ) {
104
+ return null;
105
+ }
106
+
107
+ const query = getQuery(event);
108
+ const suppliedToken =
109
+ queryString(query[CLIPS_AGENT_ACCESS_PARAM]) || queryString(query.t);
110
+ const tokenGrantsAgentAccess = suppliedToken
111
+ ? verifyScopedAgentAccessToken(suppliedToken, {
112
+ resourceKind: CLIP_AGENT_ACCESS_TOKEN_PREFIX,
113
+ resourceId: recording.id,
114
+ }).ok
115
+ : false;
116
+ const anonymousAccess =
117
+ recording.visibility === "public" && !recording.password;
118
+ if (!anonymousAccess && !tokenGrantsAgentAccess) return null;
119
+
120
+ const token = tokenGrantsAgentAccess ? suppliedToken : undefined;
121
+ const agentContextUrl = buildAgentApiUrls(recording.id, {
122
+ origin: requestUrl.origin,
123
+ basePath: getServerAppBasePath(),
124
+ token,
125
+ }).contextUrl;
126
+ const discovery = buildAgentDiscoveryPayload({
127
+ recordingId: recording.id,
128
+ title: recording.title,
129
+ status: recording.status,
130
+ agentContextUrl,
131
+ });
132
+ const contextHref = escapeHtmlAttribute(agentContextUrl);
133
+
134
+ return {
135
+ markup: `<link rel="alternate" type="application/json" href="${contextHref}" title="Agent-readable clip context"><script type="application/agent-native+json" id="clips-agent-context">${safeJsonForHtml(discovery)}</script>`,
136
+ noReferrer: tokenGrantsAgentAccess,
137
+ };
138
+ }
139
+
140
+ function injectClipAgentDiscovery(html: string, markup: string): string {
141
+ const scriptId = 'id="clips-agent-context"';
142
+ const scriptMarkup = html.includes(scriptId)
143
+ ? ""
144
+ : markup.slice(markup.indexOf("<script"));
145
+ const linkMarkup = markup.slice(0, markup.indexOf("<script"));
146
+ const injection = `${html.includes(linkMarkup) ? "" : linkMarkup}${scriptMarkup}`;
147
+ return injectDocumentMarkup(html, injection, { target: "head" });
148
+ }
149
+
150
+ function withClipsResponseHeaders(
151
+ event: H3Event,
152
+ response: Response,
153
+ ): Response {
15
154
  setResponseHeader(
16
155
  event,
17
156
  "Permissions-Policy",
18
157
  MEDIA_CAPTURE_PERMISSIONS_POLICY,
19
158
  );
20
159
  return withMediaCapturePermissions(response);
160
+ }
161
+
162
+ export default defineEventHandler(async (event) => {
163
+ const response = (await ssrHandler(event)) as Response;
164
+ const discovery = await buildClipAgentDiscovery(event);
165
+ if (!discovery || !response.ok)
166
+ return withClipsResponseHeaders(event, response);
167
+
168
+ const contentType = response.headers.get("content-type") ?? "";
169
+ if (!contentType.includes("text/html")) {
170
+ return withClipsResponseHeaders(event, response);
171
+ }
172
+
173
+ const headers = new Headers(response.headers);
174
+ headers.delete("content-length");
175
+ if (discovery.noReferrer) {
176
+ headers.set("Referrer-Policy", "no-referrer");
177
+ setResponseHeader(event, "Referrer-Policy", "no-referrer");
178
+ }
179
+
180
+ const html = await response.text();
181
+ const discoveredResponse = new Response(
182
+ injectClipAgentDiscovery(html, discovery.markup),
183
+ {
184
+ status: response.status,
185
+ statusText: response.statusText,
186
+ headers,
187
+ },
188
+ );
189
+ return withClipsResponseHeaders(event, discoveredResponse);
21
190
  });
@@ -10,7 +10,7 @@ import { useLocation } from "react-router";
10
10
  import { APP_TITLE } from "@/lib/app-config";
11
11
 
12
12
  const pageTitleKeys: Record<string, string> = {
13
- "/": "header.plan",
13
+ "/chat": "header.plan",
14
14
  "/plans": "header.plan",
15
15
  "/agent": "settings.agentTitle",
16
16
  "/settings": "header.settings",
@@ -4,6 +4,7 @@ import {
4
4
  useAgentChatHomeHandoff,
5
5
  useAgentChatHomeHandoffLinks,
6
6
  } from "@agent-native/core/client/agent-chat";
7
+ import { useSession } from "@agent-native/core/client/hooks";
7
8
  import { useT } from "@agent-native/core/client/i18n";
8
9
  import { HeaderActionsProvider } from "@agent-native/toolkit/app-shell";
9
10
  import { IconMenu2 } from "@tabler/icons-react";
@@ -47,6 +48,7 @@ function isPlanDetailRoute(pathname: string): boolean {
47
48
 
48
49
  export function Layout({ children }: LayoutProps) {
49
50
  const location = useLocation();
51
+ const pathname = location.pathname.replace(/\/+$/, "") || "/";
50
52
  const t = useT();
51
53
  const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
52
54
  const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
@@ -87,19 +89,22 @@ export function Layout({ children }: LayoutProps) {
87
89
 
88
90
  const ownsToolbar = routeOwnsToolbar(location.pathname);
89
91
  const planDetailRoute = isPlanDetailRoute(location.pathname);
90
- const chatRoute = location.pathname === "/";
92
+ const chatRoute = pathname === "/chat";
93
+ const { session, isLoading: sessionLoading } = useSession();
91
94
  const chatHomeHandoffActive = useAgentChatHomeHandoff({
92
95
  storageKey: "plans",
93
- activePath: location.pathname,
96
+ activePath: pathname,
94
97
  enabled: !chatRoute,
95
98
  });
96
99
  const chatHomeHandoffPending = isAgentChatHomeHandoffActive("plans");
97
100
  useAgentChatHomeHandoffLinks({
98
101
  storageKey: "plans",
99
- chatPath: "/",
102
+ chatPath: "/chat",
103
+ isChatPath: (path) => (path.replace(/\/+$/, "") || "/") === "/chat",
100
104
  requireActiveHandoff: true,
101
105
  });
102
106
  const hideAppNavigation = planDetailRoute && planReaderImmersive;
107
+ const hideAppHeader = pathname === "/plans" && !sessionLoading && !session;
103
108
  const effectiveSidebarCollapsed = chatRoute
104
109
  ? chatSidebarCollapsed
105
110
  : sidebarCollapsed;
@@ -168,6 +173,17 @@ export function Layout({ children }: LayoutProps) {
168
173
  </button>
169
174
  </div>
170
175
  )
176
+ ) : hideAppHeader ? (
177
+ <div className="flex h-12 items-center border-b border-border px-4 md:hidden shrink-0">
178
+ <button
179
+ type="button"
180
+ onClick={() => setMobileSidebarOpen(true)}
181
+ aria-label={t("sidebar.openNavigation")}
182
+ className="flex h-9 w-9 cursor-pointer items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-accent"
183
+ >
184
+ <IconMenu2 className="h-4 w-4" />
185
+ </button>
186
+ </div>
171
187
  ) : (
172
188
  <Header onOpenMobileSidebar={() => setMobileSidebarOpen(true)} />
173
189
  )}
@@ -72,7 +72,7 @@ function buildBrandingCustomizationMessage(request: string) {
72
72
  }
73
73
 
74
74
  const navItems = [
75
- { icon: IconMessageCircle, labelKey: "navigation.ask", href: "/" },
75
+ { icon: IconMessageCircle, labelKey: "navigation.ask", href: "/chat" },
76
76
  { icon: IconClipboardCheck, labelKey: "navigation.plan", href: "/plans" },
77
77
  ];
78
78
 
@@ -216,7 +216,7 @@ function PlanChatsSection({
216
216
 
217
217
  function openThread(threadId: string, options?: { isNew?: boolean }) {
218
218
  switchThread(threadId);
219
- navigateWithAgentChatViewTransition(navigate, "/");
219
+ navigateWithAgentChatViewTransition(navigate, "/chat");
220
220
  window.requestAnimationFrame(() => {
221
221
  window.dispatchEvent(
222
222
  new CustomEvent("agent-chat:open-thread", {
@@ -536,6 +536,7 @@ export function Sidebar({
536
536
  onCollapsedChange,
537
537
  }: SidebarProps) {
538
538
  const location = useLocation();
539
+ const pathname = location.pathname.replace(/\/+$/, "") || "/";
539
540
  const { session, isLoading: sessionLoading } = useSession();
540
541
  const t = useT();
541
542
  const returnPath = planReturnPathFromLocation(location);
@@ -652,13 +653,13 @@ export function Sidebar({
652
653
  {navItems.map((item) => {
653
654
  const Icon = item.icon;
654
655
  const isActive =
655
- item.href === "/"
656
- ? location.pathname === "/"
656
+ item.href === "/chat"
657
+ ? pathname === "/chat"
657
658
  : item.href === "/plans"
658
- ? location.pathname.startsWith("/plans") ||
659
- location.pathname.startsWith("/recaps") ||
660
- location.pathname.startsWith("/local-plans")
661
- : location.pathname.startsWith(item.href);
659
+ ? pathname.startsWith("/plans") ||
660
+ pathname.startsWith("/recaps") ||
661
+ pathname.startsWith("/local-plans")
662
+ : pathname.startsWith(item.href);
662
663
  const link = (
663
664
  <Link
664
665
  to={item.href}
@@ -681,7 +682,7 @@ export function Sidebar({
681
682
  return (
682
683
  <div key={item.href}>
683
684
  {link}
684
- {item.href === "/" ? (
685
+ {item.href === "/chat" ? (
685
686
  <PlanChatsSection collapsed={collapsed} open={isActive} />
686
687
  ) : null}
687
688
  {item.href === "/plans" && isActive ? (
@@ -695,7 +696,7 @@ export function Sidebar({
695
696
  <nav className="grid shrink-0 gap-1 px-2 py-1">
696
697
  {bottomNavItems.map((item) => {
697
698
  const Icon = item.icon;
698
- const isActive = location.pathname.startsWith(item.href);
699
+ const isActive = pathname.startsWith(item.href);
699
700
  const link = (
700
701
  <Link
701
702
  to={item.href}
@@ -122,22 +122,23 @@ export function useNavigationState() {
122
122
  }
123
123
 
124
124
  function viewForPath(pathname: string): string {
125
+ const normalizedPathname = pathname.replace(/\/+$/, "") || "/";
125
126
  // Recaps are a kind of plan; both detail routes map to the "plan" view so the
126
127
  // agent's navigation/selection state is the same surface regardless of route.
127
128
  if (
128
- pathname.startsWith("/plans/") ||
129
- pathname.startsWith("/recaps/") ||
130
- pathname.startsWith("/local-plans/")
129
+ normalizedPathname.startsWith("/plans/") ||
130
+ normalizedPathname.startsWith("/recaps/") ||
131
+ normalizedPathname.startsWith("/local-plans/")
131
132
  ) {
132
133
  return "plan";
133
134
  }
134
- if (pathname === "/") {
135
+ if (normalizedPathname === "/chat") {
135
136
  return "chat";
136
137
  }
137
138
  if (
138
- pathname.startsWith("/plans") ||
139
- pathname.startsWith("/recaps") ||
140
- pathname.startsWith("/local-plans")
139
+ normalizedPathname.startsWith("/plans") ||
140
+ normalizedPathname.startsWith("/recaps") ||
141
+ normalizedPathname.startsWith("/local-plans")
141
142
  ) {
142
143
  return "plans";
143
144
  }
@@ -187,7 +188,7 @@ function localPathFromCommandPath(value: unknown): string | null {
187
188
  function pathForView(view?: string): string {
188
189
  switch (view) {
189
190
  case "chat":
190
- return "/";
191
+ return "/chat";
191
192
  case "plan":
192
193
  case "plans":
193
194
  return "/plans";
@@ -198,7 +199,7 @@ function pathForView(view?: string): string {
198
199
  case "team":
199
200
  return "/settings/organization";
200
201
  default:
201
- return "/";
202
+ return "/chat";
202
203
  }
203
204
  }
204
205
 
@@ -7448,7 +7448,7 @@ function LoggedOutEmptyPlan() {
7448
7448
  }, []);
7449
7449
 
7450
7450
  return (
7451
- <div className="flex min-h-full items-center justify-center overflow-auto p-4 sm:p-8">
7451
+ <div className="flex min-h-full -translate-y-[10vh] items-center justify-center overflow-auto p-4 sm:p-8">
7452
7452
  <div className="flex w-full max-w-4xl flex-col items-center gap-3 py-8 text-center">
7453
7453
  <h2 className="text-2xl font-semibold tracking-tight">
7454
7454
  {t("plansPage.loggedOut.title")}
@@ -166,7 +166,7 @@ function AppContent() {
166
166
  changelogKey="plan"
167
167
  >
168
168
  <CommandMenu.Group heading={t("root.commandActions")}>
169
- <CommandMenu.Item onSelect={() => go("/")}>
169
+ <CommandMenu.Item onSelect={() => go("/chat")}>
170
170
  {t("root.askPlan")}
171
171
  </CommandMenu.Item>
172
172
  <CommandMenu.Item onSelect={() => go("/plans")}>
@@ -223,14 +223,15 @@ function AppContent() {
223
223
  export default function Root() {
224
224
  const [queryClient] = useState(() => createAgentNativeQueryClient());
225
225
  const location = useLocation();
226
+ const pathname = location.pathname.replace(/\/+$/, "") || "/";
226
227
  const sessionBypass =
227
- location.pathname === "/" ||
228
- location.pathname === "/plans" ||
229
- location.pathname.startsWith("/plans/") ||
230
- location.pathname === "/recaps" ||
231
- location.pathname.startsWith("/recaps/") ||
232
- location.pathname === "/local-plans" ||
233
- location.pathname.startsWith("/local-plans/");
228
+ pathname === "/chat" ||
229
+ pathname === "/plans" ||
230
+ pathname.startsWith("/plans/") ||
231
+ pathname === "/recaps" ||
232
+ pathname.startsWith("/recaps/") ||
233
+ pathname === "/local-plans" ||
234
+ pathname.startsWith("/local-plans/");
234
235
  const localPlanPrivacyRoute = !shouldCapturePlanContent(location.pathname);
235
236
  return (
236
237
  // Pass the plan-specific styled Toaster via `toaster` so only one sonner
@@ -1,34 +1,10 @@
1
- import { Spinner } from "@/components/ui/spinner";
2
- import { APP_TITLE } from "@/lib/app-config";
3
- import { PlanChatPage } from "@/pages/PlanChatPage";
1
+ import { withSsrHtmlContentType } from "@agent-native/core/shared";
2
+ import { redirect } from "react-router";
4
3
 
5
- const SEO_TITLE = `${APP_TITLE} - Open Source visual planning and PR recaps for coding agents`;
6
- const SEO_DESCRIPTION =
7
- "Open Source planning workspace for coding agents with visual plans, PR recaps, diagrams, wireframes, API specs, and prototypes.";
8
-
9
- export function meta() {
10
- return [
11
- { title: SEO_TITLE },
12
- {
13
- name: "description",
14
- content: SEO_DESCRIPTION,
15
- },
16
- { property: "og:title", content: SEO_TITLE },
17
- { property: "og:description", content: SEO_DESCRIPTION },
18
- { name: "twitter:card", content: "summary" },
19
- { name: "twitter:title", content: SEO_TITLE },
20
- { name: "twitter:description", content: SEO_DESCRIPTION },
21
- ];
22
- }
23
-
24
- export function HydrateFallback() {
25
- return (
26
- <div className="flex items-center justify-center h-screen w-full">
27
- <Spinner className="size-8 text-foreground" />
28
- </div>
29
- );
4
+ export function loader() {
5
+ return withSsrHtmlContentType(redirect("/plans"));
30
6
  }
31
7
 
32
- export default function IndexPage() {
33
- return <PlanChatPage />;
8
+ export default function IndexRedirect() {
9
+ return null;
34
10
  }
@@ -0,0 +1,34 @@
1
+ import { Spinner } from "@/components/ui/spinner";
2
+ import { APP_TITLE } from "@/lib/app-config";
3
+ import { PlanChatPage } from "@/pages/PlanChatPage";
4
+
5
+ const SEO_TITLE = `${APP_TITLE} - Open Source visual planning and PR recaps for coding agents`;
6
+ const SEO_DESCRIPTION =
7
+ "Open Source planning workspace for coding agents with visual plans, PR recaps, diagrams, wireframes, API specs, and prototypes.";
8
+
9
+ export function meta() {
10
+ return [
11
+ { title: SEO_TITLE },
12
+ {
13
+ name: "description",
14
+ content: SEO_DESCRIPTION,
15
+ },
16
+ { property: "og:title", content: SEO_TITLE },
17
+ { property: "og:description", content: SEO_DESCRIPTION },
18
+ { name: "twitter:card", content: "summary" },
19
+ { name: "twitter:title", content: SEO_TITLE },
20
+ { name: "twitter:description", content: SEO_DESCRIPTION },
21
+ ];
22
+ }
23
+
24
+ export function HydrateFallback() {
25
+ return (
26
+ <div className="flex h-screen w-full items-center justify-center">
27
+ <Spinner className="size-8 text-foreground" />
28
+ </div>
29
+ );
30
+ }
31
+
32
+ export default function ChatRoute() {
33
+ return <PlanChatPage />;
34
+ }
@@ -48,6 +48,7 @@ export default createAuthPlugin({
48
48
  // auth so the UI does not create placeholder plans for signed-out visitors.
49
49
  workspaceAppPublicPaths: [
50
50
  "/",
51
+ "/chat",
51
52
  "/plans",
52
53
  "/plans/plan_",
53
54
  "/recaps",
@@ -2,4 +2,5 @@
2
2
  type: improved
3
3
  date: 2026-08-24
4
4
  ---
5
+
5
6
  Decks now show a first-slide preview and keep sharing controls in the overflow menu.
@@ -62,7 +62,7 @@ export const AGENT_HARNESS_SESSION_MIGRATIONS = [
62
62
  name: "agent-harness-sessions-generation",
63
63
  sql: {
64
64
  postgres: "ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS generation BIGINT NOT NULL DEFAULT 0",
65
- sqlite: "ALTER TABLE agent_harness_sessions ADD COLUMN generation INTEGER NOT NULL DEFAULT 0",
65
+ sqlite: "ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS generation INTEGER NOT NULL DEFAULT 0",
66
66
  },
67
67
  },
68
68
  ];
@@ -264,12 +264,14 @@ export interface AgentSidebarProps {
264
264
  agentPageHref?: string;
265
265
  /** Suppress first-run onboarding while a deep-linked resource is open. */
266
266
  suppressFirstRunOnboarding?: boolean;
267
+ /** Pin how much model reasoning the chat shows. Omit to let the reader choose. */
268
+ thinkingDisplay?: AssistantChatProps["thinkingDisplay"];
267
269
  }
268
270
  /**
269
271
  * Wraps app content with a toggleable agent sidebar.
270
272
  * Use AgentToggleButton in your header to open/close it.
271
273
  */
272
- export declare function AgentSidebar({ children, enabled, emptyStateText, suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, availableAgents, availableModels, modelListLoading, selectedAgent, onAgentChange, onConnectLocalRuntime, onConnectProvider, runtime, adapterReloadKey, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position, defaultOpen, animateMobile, animateDesktop, chatViewTransition, chatViewTransitionHandoff, storageKey, openStorageKey, restoreActiveThread, apiUrl, agentChatSurface, desktopIdentityUnauthenticated, desktopIdentityAuthenticated, showTabBar, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning, onFullscreenRequest, scope, toggleScopeId, isolateHistoryByScope, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding, }: AgentSidebarProps): React.JSX.Element;
274
+ export declare function AgentSidebar({ children, enabled, emptyStateText, suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, availableAgents, availableModels, modelListLoading, selectedAgent, onAgentChange, onConnectLocalRuntime, onConnectProvider, runtime, adapterReloadKey, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position, defaultOpen, animateMobile, animateDesktop, chatViewTransition, chatViewTransitionHandoff, storageKey, openStorageKey, restoreActiveThread, apiUrl, agentChatSurface, desktopIdentityUnauthenticated, desktopIdentityAuthenticated, showTabBar, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning, onFullscreenRequest, scope, toggleScopeId, isolateHistoryByScope, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding, thinkingDisplay, }: AgentSidebarProps): React.JSX.Element;
273
275
  /**
274
276
  * Focus the agent chat composer input.
275
277
  * Opens the sidebar if closed, then focuses the text input.