@agent-native/core 0.137.6 → 0.137.8

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 (37) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +65 -18
  3. package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
  4. package/corpus/templates/clips/desktop/package.json +1 -0
  5. package/corpus/templates/clips/desktop/src/app.tsx +70 -6
  6. package/corpus/templates/clips/desktop/src/components/AlertDialog.tsx +131 -0
  7. package/corpus/templates/clips/desktop/src/styles.css +129 -0
  8. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +2 -0
  9. package/corpus/templates/design/actions/generate-design.ts +3 -2
  10. package/corpus/templates/design/actions/update-design.ts +35 -14
  11. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +7 -2
  12. package/corpus/templates/design/app/components/design/FigmaHydrationDialog.tsx +11 -2
  13. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +8 -1
  14. package/corpus/templates/design/app/i18n-data.ts +53 -22
  15. package/corpus/templates/design/app/lib/design-file-upload.ts +2 -3
  16. package/corpus/templates/design/app/lib/upload-limits.ts +4 -0
  17. package/corpus/templates/design/changelog/2026-08-05-chat-attachments-now-warn-before-upload-when-they-exceed-the.md +6 -0
  18. package/corpus/templates/design/changelog/2026-08-05-figma-fig-uploads-now-show-a-clear-4-mb-limit-up-front-inste.md +6 -0
  19. package/corpus/templates/design/server/handlers/import-design-file.ts +9 -5
  20. package/corpus/templates/design/server/handlers/uploads.ts +13 -9
  21. package/corpus/templates/design/server/lib/fig-file-limits.ts +2 -1
  22. package/corpus/templates/design/server/lib/figma-image-hydration.ts +1 -1
  23. package/corpus/templates/design/server/lib/request-body-limits.ts +11 -0
  24. package/corpus/templates/design/shared/canvas-frames.ts +81 -0
  25. package/dist/client/AssistantChat.js +1 -0
  26. package/dist/client/chat/message-components.d.ts +10 -1
  27. package/dist/client/chat/message-components.js +13 -4
  28. package/dist/client/chat/runtime.js +20 -9
  29. package/dist/client/chat/tool-call-display.d.ts +7 -0
  30. package/dist/client/chat/tool-call-display.js +9 -1
  31. package/dist/client/require-session.js +13 -4
  32. package/dist/client/sse-event-processor.d.ts +2 -0
  33. package/dist/client/sse-event-processor.js +39 -1
  34. package/dist/client/use-session.d.ts +11 -0
  35. package/dist/client/use-session.js +34 -6
  36. package/dist/server/realtime-token.d.ts +1 -1
  37. package/package.json +1 -1
@@ -19,7 +19,8 @@ import { cn } from "../utils.js";
19
19
  import { MarkdownText, renderMarkdownToClipboardHtml, } from "./markdown-renderer.js";
20
20
  import { getAssistantRunDurationMs } from "./repo-helpers.js";
21
21
  import { getRunErrorMetadata, runErrorHeadline, runErrorKey, } from "./run-recovery.js";
22
- import { ToolCallFallback, ToolActivityPresentation, FilesChangedSummary, ASSISTANT_VISIBLE_TOOL_CALL_LIMIT, ChatRunningContext, ChatRunDurationContext, formatWorkedDuration, RanToolsSummary, ReasoningCell, WorkedForSummary, } from "./tool-call-display.js";
22
+ import { ToolCallFallback, ToolActivityPresentation, FilesChangedSummary, ASSISTANT_VISIBLE_TOOL_CALL_LIMIT, ChatRunningContext, ChatRunDurationContext, formatWorkedDuration, RanToolsSummary, ReasoningCell, WorkedForSummary, toolCallHasPendingApproval, } from "./tool-call-display.js";
23
+ export { toolCallHasPendingApproval };
23
24
  // ─── Pending selection context key ───────────────────────────────────────────
24
25
  // Mirrored from AssistantChat to avoid a cross-import on a private constant.
25
26
  const PENDING_SELECTION_KEY = "pending-selection-context";
@@ -506,7 +507,9 @@ export function assistantMessageHasCompletedCustomUi(content) {
506
507
  }
507
508
  hasCompletedTool = true;
508
509
  lastCompletedToolIsCustomUi =
509
- record.chatUI !== undefined || record.mcpApp !== undefined;
510
+ record.chatUI !== undefined ||
511
+ record.mcpApp !== undefined ||
512
+ toolCallHasPendingApproval(record);
510
513
  }
511
514
  return hasCompletedTool && lastCompletedToolIsCustomUi;
512
515
  }
@@ -518,7 +521,9 @@ export function assistantMessageHasCustomUi(content) {
518
521
  return false;
519
522
  const record = part;
520
523
  return (record.type === "tool-call" &&
521
- (record.chatUI !== undefined || record.mcpApp !== undefined));
524
+ (record.chatUI !== undefined ||
525
+ record.mcpApp !== undefined ||
526
+ toolCallHasPendingApproval(record)));
522
527
  });
523
528
  }
524
529
  // Only the last assistant message may shimmer as "the currently running
@@ -628,7 +633,11 @@ export function isCollapsibleAssistantWorkPart(part) {
628
633
  return (part.type === "tool-call" &&
629
634
  !ALWAYS_VISIBLE_ASSISTANT_TOOLS.has(part.toolName ?? "") &&
630
635
  part.chatUI === undefined &&
631
- part.mcpApp === undefined);
636
+ part.mcpApp === undefined &&
637
+ // Keep the Approve/Deny affordance outside "Worked for…" — needsApproval
638
+ // tools finish with a result string, so without this they collapse and the
639
+ // human gate disappears from the viewport.
640
+ !toolCallHasPendingApproval(part));
632
641
  }
633
642
  export function getAssistantToolSummaryInfo(parts) {
634
643
  const toolCallIndices = parts.reduce((indices, part, index) => {
@@ -622,7 +622,10 @@ function mapAgentNativeEvent(raw, input) {
622
622
  type: "approval-request",
623
623
  ...base,
624
624
  approvalId: ev.approvalKey ?? ev.id ?? createRuntimeId("approval"),
625
- toolCallId: ev.id,
625
+ // `approval_required` carries the model-side call id as `toolCallId`,
626
+ // not `id`. Without this the request falls back to matching by tool
627
+ // name, which picks the wrong call when two are pending at once.
628
+ toolCallId: ev.toolCallId ?? ev.id,
626
629
  toolName: ev.tool,
627
630
  message: ev.label ?? "Approve this tool call?",
628
631
  input: ev.input,
@@ -849,18 +852,26 @@ function applyRuntimeEventToContent(event, projection) {
849
852
  return { content: [...content] };
850
853
  }
851
854
  if (typed.type === "approval-request") {
852
- const part = [...content]
853
- .reverse()
854
- .find((candidate) => candidate.type === "tool-call" &&
855
- (candidate.toolCallId === typed.toolCallId ||
856
- candidate.toolName === typed.toolName));
857
- if (part) {
855
+ const reversed = [...content].reverse();
856
+ const isToolCall = (candidate) => candidate.type === "tool-call";
857
+ // Match on the exact call id whenever the server supplied one. Falling back
858
+ // to "newest call with this name" would hand this call's approvalKey to a
859
+ // different parallel call of the same action, so name matching is reserved
860
+ // for events that carry no id at all.
861
+ const part = typed.toolCallId
862
+ ? reversed.find((candidate) => isToolCall(candidate) && candidate.toolCallId === typed.toolCallId)
863
+ : reversed.find((candidate) => isToolCall(candidate) && candidate.toolName === typed.toolName);
864
+ if (part && part.type === "tool-call") {
858
865
  part.approval = { approvalKey: typed.approvalId };
859
866
  }
860
- else {
867
+ else if (!typed.toolCallId) {
868
+ // Only runtimes that never announced the call (no id) get a synthesized
869
+ // card. An id that matches nothing means the call was never observed or
870
+ // is already resolved, and inventing an Approve/Deny card for it would
871
+ // gate something the user cannot see.
861
872
  content.push({
862
873
  type: "tool-call",
863
- toolCallId: typed.toolCallId ?? typed.approvalId,
874
+ toolCallId: typed.approvalId,
864
875
  toolName: typed.toolName ?? "approval",
865
876
  argsText: typed.input ? JSON.stringify(typed.input) : "",
866
877
  args: toContentPartInput(typed.input),
@@ -30,6 +30,13 @@ export type ApprovalContextValue = {
30
30
  onAlwaysAllow?: (approvalKey: string) => void;
31
31
  };
32
32
  export declare const ApprovalContext: React.Context<ApprovalContextValue | null>;
33
+ /** Pending human-in-the-loop gate still waiting for Approve/Deny. */
34
+ export declare function toolCallHasPendingApproval(part: {
35
+ approval?: {
36
+ approvalKey?: string;
37
+ dismissed?: boolean;
38
+ } | null;
39
+ }): boolean;
33
40
  export declare const TOOL_LONG_RUNNING_HINT_DELAY_MS = 45000;
34
41
  export declare function ToolActivityPresentation({ toolName, isRunning, isActiveTail, suppressLongRunningHint, children, }: {
35
42
  toolName: string;
@@ -20,6 +20,13 @@ export const ChatRunningContext = React.createContext(false);
20
20
  export const ChatRunDurationContext = React.createContext(null);
21
21
  export const ASSISTANT_VISIBLE_TOOL_CALL_LIMIT = 3;
22
22
  export const ApprovalContext = React.createContext(null);
23
+ /** Pending human-in-the-loop gate still waiting for Approve/Deny. */
24
+ export function toolCallHasPendingApproval(part) {
25
+ const approval = part.approval;
26
+ return (typeof approval?.approvalKey === "string" &&
27
+ approval.approvalKey.length > 0 &&
28
+ approval.dismissed !== true);
29
+ }
23
30
  export const TOOL_LONG_RUNNING_HINT_DELAY_MS = 45_000;
24
31
  export function ToolActivityPresentation({ toolName, isRunning, isActiveTail, suppressLongRunningHint = false, children, }) {
25
32
  const [showLongRunningHint, setShowLongRunningHint] = useState(false);
@@ -514,7 +521,8 @@ function isReconnectSummarizablePart(part) {
514
521
  (part.type === "tool-call" &&
515
522
  part.toolName !== "connect-builder" &&
516
523
  part.chatUI === undefined &&
517
- part.mcpApp === undefined));
524
+ part.mcpApp === undefined &&
525
+ !toolCallHasPendingApproval(part)));
518
526
  }
519
527
  function isReconnectToolSummaryPart(content, index, startIndex) {
520
528
  if (startIndex < 0 || index >= startIndex)
@@ -1,4 +1,4 @@
1
- import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * Client-side session gate for an authenticated app shell.
4
4
  *
@@ -70,12 +70,12 @@ export function RequireSession({ children, fallback, redirect = true, signedOut,
70
70
  return (_jsx(ResolvedSessionGate, { fallback: fallback, redirect: redirect, signedOut: signedOut, children: children }));
71
71
  }
72
72
  function ResolvedSessionGate({ children, fallback, redirect = true, signedOut, }) {
73
- const { session, isLoading } = useSession();
73
+ const { session, status, retry } = useSession();
74
74
  // Guard against firing the redirect more than once (effect re-runs, React
75
75
  // StrictMode double-invoke) — a second navigation while the first is in
76
76
  // flight is harmless but noisy.
77
77
  const redirectedRef = useRef(false);
78
- const mustRedirect = !isLoading && !session && redirect;
78
+ const mustRedirect = status === "unauthenticated" && redirect;
79
79
  useEffect(() => {
80
80
  if (!mustRedirect)
81
81
  return;
@@ -97,8 +97,14 @@ function ResolvedSessionGate({ children, fallback, redirect = true, signedOut, }
97
97
  }, [mustRedirect]);
98
98
  // Still resolving, or redirect already in flight: show the loading fallback
99
99
  // rather than flashing app chrome the visitor can't use.
100
- if (isLoading)
100
+ if (status === "loading")
101
101
  return _jsx(_Fragment, { children: fallback ?? _jsx(DefaultSpinner, {}) });
102
+ // Unreadable is not signed-out. Redirecting here would bounce a signed-in
103
+ // user to the sign-in page over a transient 5xx, and rendering the spinner
104
+ // would strand them on a screen that never resolves.
105
+ if (status === "unavailable") {
106
+ return _jsx(SessionUnavailableNotice, { retry: retry });
107
+ }
102
108
  if (!session) {
103
109
  if (redirect)
104
110
  return _jsx(_Fragment, { children: fallback ?? _jsx(DefaultSpinner, {}) });
@@ -106,4 +112,7 @@ function ResolvedSessionGate({ children, fallback, redirect = true, signedOut, }
106
112
  }
107
113
  return _jsx(_Fragment, { children: children });
108
114
  }
115
+ function SessionUnavailableNotice({ retry }) {
116
+ return (_jsxs("div", { className: "flex h-screen w-full flex-col items-center justify-center gap-4 px-6 text-center", children: [_jsx("p", { className: "max-w-md text-sm text-muted-foreground", children: "We couldn't reach the server to confirm you're signed in. This is usually temporary." }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { type: "button", onClick: retry, className: "rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground", children: "Try again" }), _jsx("button", { type: "button", onClick: () => window.location.reload(), className: "rounded-md border border-border px-3 py-1.5 text-sm font-medium", children: "Reload page" })] })] }));
117
+ }
109
118
  //# sourceMappingURL=require-session.js.map
@@ -68,6 +68,8 @@ export interface SSEEvent {
68
68
  /** Stable key the client echoes back in `approvedToolCalls` to approve a
69
69
  * paused `needsApproval` tool call. Present on `approval_required` events. */
70
70
  approvalKey?: string;
71
+ /** Model-side tool-call id for `approval_required` (mirrors AgentChatEvent). */
72
+ toolCallId?: string;
71
73
  error?: string;
72
74
  seq?: number;
73
75
  agent?: string;
@@ -201,6 +201,39 @@ function findPendingToolCallIndexById(content, toolCallId) {
201
201
  }
202
202
  return -1;
203
203
  }
204
+ /**
205
+ * Locate the tool call an `approval_required` event refers to.
206
+ *
207
+ * Stricter than `findPendingToolCallIndex`: when the server supplies a call id
208
+ * we never fall back to "newest pending call with this name". A paused
209
+ * `tool_done` resolves the call right after the gate fires, so a replayed or
210
+ * reordered approval would otherwise miss on id and silently attach this call's
211
+ * approvalKey to a *different* in-flight call of the same action — putting the
212
+ * wrong key behind a visible Approve button.
213
+ *
214
+ * The only tolerated fallback mirrors `findPendingActivityToolCallIndex`: a
215
+ * single unambiguous reader-local (`tc_N`) placeholder, which is how a call
216
+ * looks when an older server omitted the id on `tool_start`.
217
+ */
218
+ function findApprovalToolCallIndex(content, toolName, toolCallId) {
219
+ if (!toolCallId) {
220
+ return findPendingToolCallIndex(content, toolName);
221
+ }
222
+ const exactIndex = findPendingToolCallIndexById(content, toolCallId);
223
+ if (exactIndex >= 0)
224
+ return exactIndex;
225
+ const readerLocalCandidates = [];
226
+ for (let i = 0; i < content.length; i += 1) {
227
+ const part = content[i];
228
+ if (part.type === "tool-call" &&
229
+ part.toolName === toolName &&
230
+ part.result === undefined &&
231
+ /^tc_\d+$/.test(part.toolCallId)) {
232
+ readerLocalCandidates.push(i);
233
+ }
234
+ }
235
+ return readerLocalCandidates.length === 1 ? readerLocalCandidates[0] : -1;
236
+ }
204
237
  function findOldestPendingActivityToolCallIndex(content, toolName) {
205
238
  for (let i = 0; i < content.length; i += 1) {
206
239
  const part = content[i];
@@ -656,6 +689,8 @@ function coalesceJournalRecoveredTool(content, completedIndex) {
656
689
  prior.mcpApp = current.mcpApp;
657
690
  if (current.chatUI)
658
691
  prior.chatUI = current.chatUI;
692
+ if (current.approval)
693
+ prior.approval = { ...current.approval };
659
694
  }
660
695
  content.splice(completedIndex, 1);
661
696
  return true;
@@ -1044,7 +1079,10 @@ export function processEvent(ev, content, toolCallCounter, tabId, state) {
1044
1079
  const approvalTool = ev.tool ?? "unknown";
1045
1080
  const approvalKey = ev.approvalKey;
1046
1081
  if (approvalKey) {
1047
- const idx = findPendingToolCallIndex(content, approvalTool, ev.id);
1082
+ // `toolCallId` is the model-side id in the `approval_required` contract;
1083
+ // `id` is only carried by older frames. Same precedence as the runtime
1084
+ // path so both processors resolve an event to the same call.
1085
+ const idx = findApprovalToolCallIndex(content, approvalTool, ev.toolCallId ?? ev.id);
1048
1086
  if (idx >= 0) {
1049
1087
  const part = content[idx];
1050
1088
  if (part.type === "tool-call") {
@@ -1,8 +1,19 @@
1
1
  import type { AuthSession } from "../server/auth.js";
2
2
  export type { AuthSession };
3
+ /**
4
+ * `"unavailable"` is the session endpoint being unreadable — a 5xx, a network
5
+ * failure, or a timeout. It is NOT the visitor being signed out, and a caller
6
+ * that collapses the two either strands the user on a spinner forever or
7
+ * bounces a signed-in user to the sign-in page over a transient blip.
8
+ */
9
+ export type SessionStatus = "loading" | "authenticated" | "unauthenticated" | "unavailable";
3
10
  interface UseSessionResult {
4
11
  session: AuthSession | null;
5
12
  isLoading: boolean;
13
+ status: SessionStatus;
14
+ error: Error | null;
15
+ /** Restart the resolve loop, e.g. from a "Try again" control. */
16
+ retry: () => void;
6
17
  }
7
18
  /**
8
19
  * Client-side hook to get the current auth session.
@@ -1,8 +1,9 @@
1
- import { useEffect, useState } from "react";
1
+ import { useCallback, useEffect, useState } from "react";
2
2
  import { setSentryUser, trackSessionStatus } from "./analytics.js";
3
3
  import { fetchAuthSessionStatus } from "./client-status-requests.js";
4
4
  const SESSION_CACHE_TTL_MS = 30_000;
5
5
  const SESSION_RETRY_DELAY_MS = 1_000;
6
+ const SESSION_MAX_ATTEMPTS = 4;
6
7
  let cachedSession;
7
8
  let cachedSessionAt = 0;
8
9
  let sessionRequest;
@@ -65,22 +66,41 @@ function fetchSharedSession() {
65
66
  export function useSession() {
66
67
  const cached = hasFreshSessionCache() ? (cachedSession ?? null) : null;
67
68
  const [session, setSession] = useState(cached);
68
- const [isLoading, setIsLoading] = useState(!hasFreshSessionCache());
69
+ const [status, setStatus] = useState(() => {
70
+ if (!hasFreshSessionCache())
71
+ return "loading";
72
+ return cached ? "authenticated" : "unauthenticated";
73
+ });
74
+ const [error, setError] = useState(null);
75
+ const [retryToken, setRetryToken] = useState(0);
76
+ const retry = useCallback(() => {
77
+ setError(null);
78
+ setStatus("loading");
79
+ setRetryToken((token) => token + 1);
80
+ }, []);
69
81
  useEffect(() => {
70
82
  let cancelled = false;
71
83
  let retryTimer;
84
+ let attempts = 0;
72
85
  const resolveSession = async () => {
73
86
  const resolved = await fetchSharedSession();
74
87
  if (cancelled)
75
88
  return;
76
89
  if (resolved === undefined) {
90
+ attempts += 1;
91
+ if (attempts >= SESSION_MAX_ATTEMPTS) {
92
+ setError(new Error(`Could not read the session after ${attempts} attempts.`));
93
+ setStatus("unavailable");
94
+ return;
95
+ }
77
96
  retryTimer = setTimeout(() => {
78
97
  void resolveSession();
79
- }, SESSION_RETRY_DELAY_MS);
98
+ }, SESSION_RETRY_DELAY_MS * attempts);
80
99
  return;
81
100
  }
82
101
  setSession(resolved);
83
- setIsLoading(false);
102
+ setError(null);
103
+ setStatus(resolved ? "authenticated" : "unauthenticated");
84
104
  };
85
105
  void resolveSession();
86
106
  return () => {
@@ -88,7 +108,15 @@ export function useSession() {
88
108
  if (retryTimer)
89
109
  clearTimeout(retryTimer);
90
110
  };
91
- }, []);
92
- return { session, isLoading };
111
+ }, [retryToken]);
112
+ // Callers that only read `isLoading`/`session` (most of the codebase, not
113
+ // yet migrated to `status`) must not see "unavailable" as "signed out" —
114
+ // that bounces an authenticated user through sign-in-only UI over a
115
+ // transient blip. Keeping `isLoading` true here reproduces this hook's
116
+ // pre-existing behavior for those callers (an indefinite "still resolving"
117
+ // instead of a wrong answer); only `status`-aware callers get the distinct
118
+ // "unavailable" treatment with a retry affordance.
119
+ const isLoading = status === "loading" || status === "unavailable";
120
+ return { session, isLoading, status, error, retry };
93
121
  }
94
122
  //# sourceMappingURL=use-session.js.map
@@ -26,9 +26,9 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
26
26
  expiresAt?: undefined;
27
27
  ttlSeconds?: undefined;
28
28
  } | {
29
+ error?: undefined;
29
30
  token: string;
30
31
  expiresAt: string;
31
32
  ttlSeconds: number;
32
- error?: undefined;
33
33
  }>>;
34
34
  //# sourceMappingURL=realtime-token.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.137.6",
3
+ "version": "0.137.8",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {