@agent-native/core 0.161.23 → 0.162.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +18 -2
- package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/calendar/server/handlers/google-auth.ts +1 -8
- package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +88 -35
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +36 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -3
- package/corpus/templates/clips/desktop/src/app.tsx +30 -40
- package/corpus/templates/clips/desktop/src/components/MicOffConfirmation.tsx +47 -0
- package/corpus/templates/clips/desktop/src/styles.css +110 -0
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +114 -7
- package/corpus/templates/clips/shared/recording-capture.ts +18 -13
- package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -5
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +0 -3
- package/corpus/templates/content/app/components/editor/NotionConflictBanner.tsx +0 -4
- package/corpus/templates/content/app/components/editor/NotionSyncBar.tsx +4 -1
- package/corpus/templates/content/app/hooks/use-notion.ts +17 -15
- package/corpus/templates/crm/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/design/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/dispatch/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/factory/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/macros/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +18 -3
- package/corpus/templates/mail/server/handlers/google-auth.ts +1 -8
- package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +4 -0
- package/corpus/templates/slides/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/slides/actions/_await-fit-check.ts +30 -5
- package/corpus/templates/slides/actions/add-slide.ts +6 -2
- package/corpus/templates/slides/actions/create-deck.ts +2 -1
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +34 -1
- package/corpus/templates/slides/app/routes/p.$id.tsx +3 -1
- package/corpus/templates/slides/changelog/2026-08-18-generated-slides-preserve-speaker-notes.md +6 -0
- package/corpus/templates/slides/server/handlers/assets.ts +23 -12
- package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +15 -6
- package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +15 -6
- package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +34 -19
- package/corpus/templates/slides/server/handlers/share.ts +1 -1
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/slides/server/routes/api/deck-agent-context.json.get.ts +1 -1
- package/corpus/templates/slides/server/routes/api/exports/[filename].get.ts +9 -3
- package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +12 -5
- package/corpus/templates/slides/server/routes/api/exports/html.post.ts +9 -7
- package/corpus/templates/slides/server/routes/api/exports/pptx.post.ts +9 -7
- package/corpus/templates/slides/shared/api.ts +7 -1
- package/corpus/templates/tasks/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/agent/engine/translate-anthropic.js +27 -2
- package/dist/agent/production-agent.d.ts +2 -2
- package/dist/agent/production-agent.js +16 -8
- package/dist/agent/thread-data-builder.d.ts +1 -0
- package/dist/agent/thread-data-builder.js +6 -4
- package/dist/agent/tool-approval-store.d.ts +10 -1
- package/dist/agent/tool-approval-store.js +19 -2
- package/dist/agent/types.d.ts +9 -0
- package/dist/client/AssistantChat.js +14 -6
- package/dist/client/EnvironmentBadge.d.ts +15 -0
- package/dist/client/EnvironmentBadge.js +79 -0
- package/dist/client/MultiTabAssistantChat.js +73 -17
- package/dist/client/agent-chat-adapter.js +11 -1
- package/dist/client/api-path.js +14 -1
- package/dist/client/app-providers.js +2 -1
- package/dist/client/chat/tool-call-display.d.ts +12 -2
- package/dist/client/chat/tool-call-display.js +17 -7
- package/dist/client/chat/widgets/WorkspaceFileWidget.js +1 -1
- package/dist/client/chat/widgets/builtin-tool-renderers.d.ts +6 -0
- package/dist/client/chat/widgets/builtin-tool-renderers.js +19 -0
- package/dist/client/onboarding/FirstRunOnboarding.js +46 -32
- package/dist/client/onboarding/use-onboarding.d.ts +8 -1
- package/dist/client/onboarding/use-onboarding.js +22 -7
- package/dist/client/session-replay.js +58 -15
- package/dist/client/settings/SettingsTabsPage.js +19 -6
- package/dist/client/sse-event-processor.d.ts +7 -0
- package/dist/client/sse-event-processor.js +4 -1
- package/dist/client/ui/index.d.ts +1 -0
- package/dist/client/ui/index.js +1 -0
- package/dist/client/use-action.js +26 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/config.d.ts +15 -0
- package/dist/config.js +53 -0
- package/dist/extensions/fetch-tool.js +8 -2
- package/dist/mcp/embed-app.js +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/provider-api/index.d.ts +10 -0
- package/dist/provider-api/index.js +45 -9
- package/dist/server/auth.js +10 -3
- package/dist/server/poll.js +7 -3
- package/dist/server/sentry.d.ts +1 -1
- package/dist/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/vite/client.d.ts +5 -0
- package/dist/vite/client.js +64 -2
- package/dist/workspace-files/actions.d.ts +1 -8
- package/dist/workspace-files/actions.js +5 -21
- package/dist/workspace-files/index.d.ts +1 -1
- package/dist/workspace-files/index.js +1 -1
- package/dist/workspace-files/store.d.ts +18 -0
- package/dist/workspace-files/store.js +24 -3
- package/package.json +3 -3
- package/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
|
@@ -16,7 +16,7 @@ import { cn } from "../utils.js";
|
|
|
16
16
|
import { ActionChatUiSurface } from "./action-chat-ui-surface.js";
|
|
17
17
|
import { SmoothMarkdownText, HighlightedCodeBlock, } from "./markdown-renderer.js";
|
|
18
18
|
import { resolveToolRenderer } from "./tool-render-registry.js";
|
|
19
|
-
import { isBuiltinDataWidgetActionRenderer, resolveBuiltinActionChatRenderer, resolveBuiltinFallbackToolRenderer, } from "./widgets/builtin-tool-renderers.js";
|
|
19
|
+
import { isBuiltinDataWidgetActionRenderer, isBuiltinWorkspaceFileResult, resolveBuiltinActionChatRenderer, resolveBuiltinFallbackToolRenderer, } from "./widgets/builtin-tool-renderers.js";
|
|
20
20
|
// Exported so AssistantChatInner can provide a context value.
|
|
21
21
|
export const ChatRunningContext = React.createContext(false);
|
|
22
22
|
export const ChatRunningRunIdContext = React.createContext(null);
|
|
@@ -249,7 +249,7 @@ function ApprovalAffordance({ toolName, toolCallId, approval, }) {
|
|
|
249
249
|
const t = useT();
|
|
250
250
|
const ctx = React.useContext(ApprovalContext);
|
|
251
251
|
const [localResolution, setLocalResolution] = useState(null);
|
|
252
|
-
const retainedResolution = ctx?.getApprovalResolution?.(approval.approvalKey, toolCallId) ?? null;
|
|
252
|
+
const retainedResolution = ctx?.getApprovalResolution?.(approval.approvalKey, toolCallId, approval.askId) ?? null;
|
|
253
253
|
const resolution = retainedResolution ??
|
|
254
254
|
localResolution ??
|
|
255
255
|
(approval.dismissed === true ? "denied" : null);
|
|
@@ -266,15 +266,15 @@ function ApprovalAffordance({ toolName, toolCallId, approval, }) {
|
|
|
266
266
|
}
|
|
267
267
|
return (_jsxs("div", { className: "mt-1.5 flex min-w-0 flex-wrap items-center gap-2 rounded-md border border-border bg-muted/40 px-2.5 py-1.5", children: [_jsx(IconShieldCheck, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }), _jsx("span", { className: "min-w-0 flex-1 text-xs text-muted-foreground", children: t("agentChat.approval.question", { tool: toolName }) }), ctx && (_jsxs("button", { type: "button", onClick: () => {
|
|
268
268
|
setLocalResolution("approved");
|
|
269
|
-
ctx.onApprovalResolved?.(approval.approvalKey, "approved", toolCallId);
|
|
269
|
+
ctx.onApprovalResolved?.(approval.approvalKey, "approved", toolCallId, approval.askId);
|
|
270
270
|
ctx.onApprove(approval.approvalKey);
|
|
271
271
|
}, className: cn("inline-flex shrink-0 items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium transition-colors", "bg-foreground text-background hover:bg-foreground/90"), children: [_jsx(IconCheck, { className: "h-3.5 w-3.5" }), t("agentChat.approval.approve")] })), ctx?.onAlwaysAllow && (_jsxs("button", { type: "button", onClick: () => {
|
|
272
272
|
setLocalResolution("approved");
|
|
273
|
-
ctx.onApprovalResolved?.(approval.approvalKey, "approved", toolCallId);
|
|
273
|
+
ctx.onApprovalResolved?.(approval.approvalKey, "approved", toolCallId, approval.askId);
|
|
274
274
|
ctx.onAlwaysAllow?.(approval.approvalKey);
|
|
275
275
|
}, title: t("agentChat.approval.alwaysAllowHint"), className: cn("inline-flex shrink-0 items-center gap-1 rounded-md border border-border px-2.5 py-1 text-xs font-medium transition-colors", "text-foreground hover:bg-muted"), children: [_jsx(IconShieldCheck, { className: "h-3.5 w-3.5" }), t("agentChat.approval.alwaysAllow")] })), _jsxs("button", { type: "button", onClick: () => {
|
|
276
276
|
setLocalResolution("denied");
|
|
277
|
-
ctx?.onApprovalResolved?.(approval.approvalKey, "denied", toolCallId);
|
|
277
|
+
ctx?.onApprovalResolved?.(approval.approvalKey, "denied", toolCallId, approval.askId);
|
|
278
278
|
ctx?.onDeny?.(approval.approvalKey);
|
|
279
279
|
}, className: cn("inline-flex shrink-0 items-center gap-1 rounded-md border border-border px-2.5 py-1 text-xs font-medium transition-colors", "text-foreground hover:bg-muted"), children: [_jsx(IconX, { className: "h-3.5 w-3.5" }), t("agentChat.approval.deny")] })] }));
|
|
280
280
|
}
|
|
@@ -403,7 +403,8 @@ function ToolCallDisplayGeneric({ toolName, toolCallId, argsText, args, result,
|
|
|
403
403
|
(skipRegistryRenderer ? null : resolveToolRenderer(nativeToolContext)) ??
|
|
404
404
|
resolveBuiltinFallbackToolRenderer(nativeToolContext));
|
|
405
405
|
if (NativeToolRenderer) {
|
|
406
|
-
return (_jsx(ActionChatUiSurface, { context: nativeToolContext, isBuiltinDataWidget: isBuiltinDataWidgetActionRenderer(nativeToolContext)
|
|
406
|
+
return (_jsx(ActionChatUiSurface, { context: nativeToolContext, isBuiltinDataWidget: isBuiltinDataWidgetActionRenderer(nativeToolContext) ||
|
|
407
|
+
isBuiltinWorkspaceFileResult(nativeToolContext), children: _jsx(NativeToolRenderer, { context: nativeToolContext }) }));
|
|
407
408
|
}
|
|
408
409
|
const inputPayload = hasArgs
|
|
409
410
|
? toolInputPayload(toolName, args, {
|
|
@@ -432,7 +433,16 @@ function ToolCallDisplayGeneric({ toolName, toolCallId, argsText, args, result,
|
|
|
432
433
|
}
|
|
433
434
|
return (_jsxs("div", { className: "group/tool my-0.5 w-full overflow-hidden", children: [mcpApp && !(suppressInlineOpenApp && toolName === "open_app") && (_jsx(McpAppRenderer, { app: mcpApp, className: "mb-1.5" })), _jsxs("button", { type: "button", onClick: () => canExpand && setExpanded(!isExpanded), "aria-expanded": canExpand ? isExpanded : undefined, className: cn("flex w-full items-center gap-1.5 rounded-md py-0.5 text-left text-[13px] text-muted-foreground transition-colors", canExpand && "hover:text-foreground", isRunning && "text-muted-foreground"), children: [_jsx("span", { className: "relative flex size-4 shrink-0 items-center justify-center", children: isRunning ? (_jsx(IconLoader2, { className: "size-3.5 animate-spin" })) : isAgentError ? (_jsx(IconCircleX, { className: "size-3.5 text-destructive" })) : isUnknownOutcome ? (_jsx(IconAlertTriangle, { className: "size-3.5 text-muted-foreground" })) : (_jsxs(_Fragment, { children: [_jsx(ToolIcon, { className: cn("size-3.5 transition-opacity", canExpand && "group-hover/tool:opacity-0") }), canExpand && (_jsx(IconChevronRight, { className: cn("absolute size-3.5 opacity-0 transition-[opacity,transform] group-hover/tool:opacity-100", isExpanded && "rotate-90") }))] })) }), _jsx("span", { className: cn("min-w-0 truncate font-normal", isActiveTail && "agent-running-shimmer"), children: displayName }), repeatCount && repeatCount > 1 && (_jsxs("span", { className: "shrink-0 rounded border border-border/60 px-1.5 py-0.5 text-[10px] leading-none text-muted-foreground", title: t("agentChat.tool.repeated", { count: repeatCount }), children: [repeatCount, "x"] }))] }), _jsx(AnimatedCollapse, { open: isExpanded && !isAgentCall && (hasArgs || result !== undefined), children: _jsxs("div", { className: "mt-1 space-y-2 pl-5", children: [inputPayload && (_jsx(SimpleCodeViewer, { text: inputPayload.text, lang: inputPayload.lang })), resultPayload && (_jsx(ToolOutputPopover, { open: outputOpen, onOpenChange: setOutputOpen, title: outputTitle, payload: resultPayload, children: _jsx("button", { type: "button", "aria-label": t("agentChat.tool.viewOutput", {
|
|
434
435
|
tool: toolName,
|
|
435
|
-
}), className: "inline-flex size-6 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-accent hover:text-foreground", children: _jsx(IconCode, { className: "size-3.5" }) }) }))] }) }), isUnknownOutcome && (_jsx("p", { role: "status", className: "ps-5 text-xs text-muted-foreground", children: t("agentChat.tool.interrupted") })), approval && (_jsx(ApprovalAffordance
|
|
436
|
+
}), className: "inline-flex size-6 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-accent hover:text-foreground", children: _jsx(IconCode, { className: "size-3.5" }) }) }))] }) }), isUnknownOutcome && (_jsx("p", { role: "status", className: "ps-5 text-xs text-muted-foreground", children: t("agentChat.tool.interrupted") })), approval && (_jsx(ApprovalAffordance
|
|
437
|
+
// A changed `askId` means the server re-emitted approval_required
|
|
438
|
+
// for this same call (e.g. a failed resume never consumed the
|
|
439
|
+
// prior grant) rather than the same ask re-rendering. Keying on it
|
|
440
|
+
// forces a fresh mount so a stale local "approved" state from the
|
|
441
|
+
// earlier ask can't linger and hide Approve/Deny with no way to
|
|
442
|
+
// retry. Falls back to approvalKey when askId is absent (older
|
|
443
|
+
// events, non-production-agent approval sources) to keep the
|
|
444
|
+
// existing remount-safe behavior unchanged there.
|
|
445
|
+
, { toolName: toolName, toolCallId: toolCallId, approval: approval }, approval.askId ?? approval.approvalKey))] }));
|
|
436
446
|
}
|
|
437
447
|
function AgentCallCell({ agentName, activity, progress, responseText, isRunning, isError, durationMs, }) {
|
|
438
448
|
const t = useT();
|
|
@@ -29,5 +29,5 @@ export function WorkspaceFileWidget({ result, }) {
|
|
|
29
29
|
const { file } = result;
|
|
30
30
|
const FileIcon = fileIcon(file.contentType);
|
|
31
31
|
const href = resourceDownloadUrl(file.resourceId);
|
|
32
|
-
return (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex size-10 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: _jsx(FileIcon, { className: "size-5", "aria-hidden": "true" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate text-sm font-medium", children: file.name }), _jsxs("div", { className: "truncate text-xs text-muted-foreground", children: [formatFileSize(file.sizeBytes, formatNumber), " \u00B7 ", file.path] })] }), _jsxs("a", { href: href, download: file.name, className: "inline-flex shrink-0 items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground no-underline transition-colors hover:bg-primary/90 hover:no-underline", children: [_jsx(IconDownload, { className: "size-3.5", "aria-hidden": "true" }), t("workspaceFile.download")] })] }));
|
|
32
|
+
return (_jsxs("div", { className: "flex items-center gap-3 rounded-lg border border-border bg-card p-3 text-card-foreground shadow-sm", children: [_jsx("div", { className: "flex size-10 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: _jsx(FileIcon, { className: "size-5", "aria-hidden": "true" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate text-sm font-medium", children: file.name }), _jsxs("div", { className: "truncate text-xs text-muted-foreground", children: [formatFileSize(file.sizeBytes, formatNumber), " \u00B7 ", file.path] })] }), _jsxs("a", { href: href, download: file.name, className: "inline-flex shrink-0 items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground no-underline transition-colors hover:bg-primary/90 hover:no-underline", children: [_jsx(IconDownload, { className: "size-3.5", "aria-hidden": "true" }), t("workspaceFile.download")] })] }));
|
|
33
33
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { type ToolRendererContext, type ToolRendererComponent } from "../tool-render-registry.js";
|
|
2
2
|
export declare function isBuiltinDataWidgetActionRenderer(context: ToolRendererContext): boolean;
|
|
3
|
+
/** True whenever a result renders as a workspace-file card — with or
|
|
4
|
+
* without a static chatUI on the action that produced it (see the
|
|
5
|
+
* shape-based fallback registered below). The widget frames itself, so
|
|
6
|
+
* callers use this the same way they use `isBuiltinDataWidgetActionRenderer`
|
|
7
|
+
* to skip the chatUI-gated outer border and avoid a double frame. */
|
|
8
|
+
export declare function isBuiltinWorkspaceFileResult(context: ToolRendererContext): boolean;
|
|
3
9
|
export declare function resolveBuiltinActionChatRenderer(context: ToolRendererContext): ToolRendererComponent | null;
|
|
4
10
|
export declare function resolveBuiltinFallbackToolRenderer(context: ToolRendererContext): ToolRendererComponent | null;
|
|
@@ -99,6 +99,14 @@ export function isBuiltinDataWidgetActionRenderer(context) {
|
|
|
99
99
|
renderer === ACTION_CHAT_UI_DATA_INSIGHTS_RENDERER ||
|
|
100
100
|
renderer === ACTION_CHAT_UI_DATA_WIDGET_RENDERER);
|
|
101
101
|
}
|
|
102
|
+
/** True whenever a result renders as a workspace-file card — with or
|
|
103
|
+
* without a static chatUI on the action that produced it (see the
|
|
104
|
+
* shape-based fallback registered below). The widget frames itself, so
|
|
105
|
+
* callers use this the same way they use `isBuiltinDataWidgetActionRenderer`
|
|
106
|
+
* to skip the chatUI-gated outer border and avoid a double frame. */
|
|
107
|
+
export function isBuiltinWorkspaceFileResult(context) {
|
|
108
|
+
return normalizeWorkspaceFileResult(context.resultJson) !== null;
|
|
109
|
+
}
|
|
102
110
|
export function resolveBuiltinActionChatRenderer(context) {
|
|
103
111
|
if (context.chatUI?.renderer === ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER &&
|
|
104
112
|
normalizeInlineExtensionToolResult(context)) {
|
|
@@ -143,3 +151,14 @@ registerReservedFallbackToolRenderer({
|
|
|
143
151
|
match: (context) => normalizeActionDataWidgetResult(context) !== null,
|
|
144
152
|
Component: BuiltinDataWidgetRenderer,
|
|
145
153
|
});
|
|
154
|
+
// Shape-based, not chatUI-based: any tool result — from show-workspace-file,
|
|
155
|
+
// or any other action that spreads `{ file: toWorkspaceFileCard(meta) }` into
|
|
156
|
+
// its own result — renders a download card without a second discretionary
|
|
157
|
+
// call to show-workspace-file. Matching by shape (instead of statically
|
|
158
|
+
// tagging every such action with `chatUI`) also keeps read/list-style calls
|
|
159
|
+
// on the same multi-purpose action collapsible when they don't produce a file.
|
|
160
|
+
registerReservedFallbackToolRenderer({
|
|
161
|
+
id: "core.workspace-file",
|
|
162
|
+
match: (context) => normalizeWorkspaceFileResult(context.resultJson) !== null,
|
|
163
|
+
Component: BuiltinWorkspaceFileRenderer,
|
|
164
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Skeleton } from "@agent-native/toolkit/ui/skeleton";
|
|
3
3
|
import { IconArrowRight, IconBrandGithub, IconCheck, IconExternalLink, IconInfoCircle, IconKey, IconLoader2, IconSearch, } from "@tabler/icons-react";
|
|
4
|
-
import React, { useMemo, useState } from "react";
|
|
4
|
+
import React, { useCallback, useMemo, useState } from "react";
|
|
5
5
|
import { docsUrl } from "../../shared/docs-url.js";
|
|
6
6
|
import { appPath } from "../api-path.js";
|
|
7
7
|
import { Tooltip, TooltipContent, TooltipTrigger, } from "../components/ui/tooltip.js";
|
|
@@ -28,7 +28,14 @@ const BUILDER_MORE_SERVICES = [
|
|
|
28
28
|
];
|
|
29
29
|
export function FirstRunOnboarding({ skipIntegrations = shouldSkipFirstRunIntegrations(), initialFirstRun = false, } = {}) {
|
|
30
30
|
const previewMode = useOnboardingPreviewMode();
|
|
31
|
-
const { firstRun, loading, error, profile, completeFirstRun } = useOnboarding({ preview: previewMode, initialFirstRun });
|
|
31
|
+
const { firstRun, loading, error, profile, completeFirstRun, completeFirstRunError, } = useOnboarding({ preview: previewMode, initialFirstRun });
|
|
32
|
+
// completeFirstRun() rejects on failure — swallow it here so a Skip/
|
|
33
|
+
// Continue click never becomes an unhandled rejection; completeFirstRunError
|
|
34
|
+
// (rendered below) is the real signal, and the user stays on this screen
|
|
35
|
+
// to retry instead of being bounced to an unrelated error screen.
|
|
36
|
+
const finishOnboarding = useCallback(() => {
|
|
37
|
+
completeFirstRun().catch(() => { });
|
|
38
|
+
}, [completeFirstRun]);
|
|
32
39
|
const [screen, setScreen] = useState("intro");
|
|
33
40
|
const [extensionIndex, setExtensionIndex] = useState(0);
|
|
34
41
|
const [integrationQuery, setIntegrationQuery] = useState("");
|
|
@@ -86,15 +93,15 @@ export function FirstRunOnboarding({ skipIntegrations = shouldSkipFirstRunIntegr
|
|
|
86
93
|
trackingFlow: "connect_llm",
|
|
87
94
|
});
|
|
88
95
|
};
|
|
89
|
-
const handleOpenSettings =
|
|
96
|
+
const handleOpenSettings = () => {
|
|
90
97
|
window.dispatchEvent(new CustomEvent("agent-panel:open-settings", {
|
|
91
98
|
detail: { section: "integrations" },
|
|
92
99
|
}));
|
|
93
|
-
|
|
100
|
+
finishOnboarding();
|
|
94
101
|
};
|
|
95
102
|
const handleFinish = () => {
|
|
96
103
|
if (extensions.length === 0) {
|
|
97
|
-
|
|
104
|
+
finishOnboarding();
|
|
98
105
|
return;
|
|
99
106
|
}
|
|
100
107
|
setExtensionIndex(0);
|
|
@@ -155,7 +162,7 @@ export function FirstRunOnboarding({ skipIntegrations = shouldSkipFirstRunIntegr
|
|
|
155
162
|
if (screen === "extension") {
|
|
156
163
|
const extension = extensions[extensionIndex];
|
|
157
164
|
if (!extension) {
|
|
158
|
-
|
|
165
|
+
finishOnboarding();
|
|
159
166
|
return null;
|
|
160
167
|
}
|
|
161
168
|
const Extension = extension.component;
|
|
@@ -164,9 +171,9 @@ export function FirstRunOnboarding({ skipIntegrations = shouldSkipFirstRunIntegr
|
|
|
164
171
|
setExtensionIndex((current) => current + 1);
|
|
165
172
|
return;
|
|
166
173
|
}
|
|
167
|
-
|
|
174
|
+
finishOnboarding();
|
|
168
175
|
};
|
|
169
|
-
return (_jsx(Extension, { onComplete: advanceExtension, onSkip:
|
|
176
|
+
return (_jsxs(_Fragment, { children: [_jsx(Extension, { onComplete: advanceExtension, onSkip: finishOnboarding }), completeFirstRunError && (_jsx(FirstRunCompletionError, { message: completeFirstRunError, onRetry: finishOnboarding }))] }));
|
|
170
177
|
}
|
|
171
178
|
if (screen === "intro") {
|
|
172
179
|
return (_jsx(OnboardingShell, { profile: profile, screen: "intro", children: _jsxs("div", { className: "mx-auto flex w-full max-w-lg flex-col items-center text-center", children: [_jsxs("h1", { className: "text-3xl font-semibold tracking-[-0.05em] sm:text-4xl", children: ["Free forever.", _jsx("br", {}), _jsx("span", { className: "text-primary", children: "Open source for life." })] }), _jsxs("div", { className: "mt-7 grid w-full gap-2 text-left sm:grid-cols-3", children: [_jsxs("div", { className: "rounded-lg bg-muted/35 px-3 py-3", children: [_jsx("p", { className: "text-xs font-medium", children: "Fully customizable" }), _jsx("p", { className: "mt-1 text-[11px] leading-4 text-muted-foreground", children: "Change the UI, code, and behavior." })] }), _jsxs("div", { className: "rounded-lg bg-muted/35 px-3 py-3", children: [_jsx("p", { className: "text-xs font-medium", children: "Bring your own keys" }), _jsx("p", { className: "mt-1 text-[11px] leading-4 text-muted-foreground", children: "Use your own providers and accounts." })] }), _jsxs("div", { className: "rounded-lg bg-muted/35 px-3 py-3", children: [_jsx("p", { className: "text-xs font-medium", children: "Build your own" }), _jsx("p", { className: "mt-1 text-[11px] leading-4 text-muted-foreground", children: "Mix and match toolkit pieces in your own apps." })] })] }), _jsxs("button", { type: "button", className: cn(primaryButtonClass, "mt-6"), onClick: () => setScreen("choice"), children: ["Continue", _jsx(IconArrowRight, { size: 15 })] }), _jsxs("a", { href: "https://github.com/builderio/agent-native", target: "_blank", rel: "noreferrer", className: "mt-3 inline-flex items-center gap-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [_jsx(IconBrandGithub, { size: 14 }), _jsx("span", { children: "View source" }), _jsx(IconExternalLink, { size: 13 })] })] }) }));
|
|
@@ -204,29 +211,29 @@ export function FirstRunOnboarding({ skipIntegrations = shouldSkipFirstRunIntegr
|
|
|
204
211
|
if (screen === "connecting") {
|
|
205
212
|
return (_jsx(OnboardingShell, { profile: profile, screen: "choice", children: _jsxs("div", { className: "mx-auto flex w-full max-w-md flex-col items-center text-center", children: [_jsx("div", { className: "flex size-10 items-center justify-center rounded-full bg-primary/10 text-primary", children: _jsx(IconLoader2, { className: "animate-spin", size: 19 }) }), _jsx("h1", { className: "mt-5 text-xl font-semibold tracking-[-0.04em]", children: "Connecting Builder.io free credits" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: "Finish the one-click connection in the new window." }), _jsxs("div", { className: "mt-7 w-full rounded-xl bg-muted/35 p-4 text-left", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx(Skeleton, { className: "h-3 w-28" }), _jsx(Skeleton, { className: "h-5 w-16 rounded-full" })] }), _jsx(Skeleton, { className: "mt-4 h-8 w-full" }), _jsxs("div", { className: "mt-3 grid grid-cols-3 gap-2", children: [_jsx(Skeleton, { className: "h-7 w-full" }), _jsx(Skeleton, { className: "h-7 w-full" }), _jsx(Skeleton, { className: "h-7 w-full" })] })] }), connectFlow.error && (_jsxs("div", { className: "mt-4 flex flex-col items-center gap-2", children: [_jsx("p", { className: "text-xs text-destructive", children: connectFlow.error }), _jsx("button", { type: "button", className: secondaryButtonClass, onClick: () => setScreen("choice"), children: "Try again" })] }))] }) }));
|
|
206
213
|
}
|
|
207
|
-
return (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
214
|
+
return (_jsxs(OnboardingShell, { profile: profile, screen: "ready", children: [_jsxs("div", { className: "mx-auto flex w-full max-w-2xl flex-col items-center text-center", children: [_jsx("div", { className: "flex size-12 items-center justify-center rounded-2xl bg-primary/10 text-primary", children: _jsx(IconCheck, { size: 20 }) }), _jsx("h1", { className: "mt-5 text-xl font-semibold tracking-[-0.04em]", children: "Your agent is ready." }), _jsx("p", { className: "mt-2 max-w-md text-sm leading-6 text-muted-foreground", children: "Start with a chat, then connect more tools whenever you need them." }), _jsx("div", { className: "mt-7 grid w-full gap-2 text-left sm:grid-cols-3", children: (skipIntegrations
|
|
215
|
+
? [
|
|
216
|
+
[
|
|
217
|
+
"Workflow actions",
|
|
218
|
+
"Use the app's buttons and sidebar to run the workflow.",
|
|
219
|
+
],
|
|
220
|
+
[
|
|
221
|
+
"AI sidebar",
|
|
222
|
+
"Ask the agent to review or refine a step in context.",
|
|
223
|
+
],
|
|
224
|
+
[
|
|
225
|
+
"Flexible providers",
|
|
226
|
+
"Use Builder.io free credits or your own keys.",
|
|
227
|
+
],
|
|
228
|
+
]
|
|
229
|
+
: [
|
|
230
|
+
["Chat + actions", "Ask your agent to work across the app."],
|
|
231
|
+
["Agent integrations", "Connect tools from Settings anytime."],
|
|
232
|
+
[
|
|
233
|
+
"Flexible providers",
|
|
234
|
+
"Use Builder.io free credits or your own keys.",
|
|
235
|
+
],
|
|
236
|
+
]).map(([title, description]) => (_jsxs("div", { className: "rounded-xl bg-muted/35 px-4 py-4", children: [_jsx("span", { className: "flex size-7 items-center justify-center rounded-full bg-primary/10 text-primary", children: _jsx(IconCheck, { size: 14 }) }), _jsx("p", { className: "mt-3 text-sm font-medium", children: title }), _jsx("p", { className: "mt-1 text-xs leading-5 text-muted-foreground", children: description })] }, title))) }), _jsxs("button", { type: "button", "data-testid": "first-run-open-app", className: cn(primaryButtonClass, "mt-7"), onClick: handleFinish, children: ["Open app", _jsx(IconArrowRight, { size: 15 })] })] }), completeFirstRunError && (_jsx(FirstRunCompletionError, { message: completeFirstRunError, onRetry: finishOnboarding }))] }));
|
|
230
237
|
}
|
|
231
238
|
function OnboardingShell({ profile, screen, footer, children, }) {
|
|
232
239
|
return (_jsxs("div", { className: "fixed inset-0 z-[100] flex h-full min-h-0 flex-col bg-background text-foreground", "data-onboarding-screen": screen, role: "dialog", "aria-modal": "true", "aria-label": `${profile?.appName ?? "Your app"} setup`, children: [_jsx("div", { className: "h-0.5 shrink-0 bg-muted", "data-testid": "onboarding-progress", children: _jsx("div", { className: "h-full bg-primary transition-[width] duration-200", style: {
|
|
@@ -266,6 +273,13 @@ function CapabilityInfoButton({ capability, ariaLabel, }) {
|
|
|
266
273
|
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": ariaLabel, className: "inline-flex size-4 items-center justify-center rounded-full text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: _jsx(IconInfoCircle, { size: 13 }) }) }), _jsx(TooltipContent, { side: "top", className: "max-w-xs text-xs", children: capability.why })] }));
|
|
267
274
|
}
|
|
268
275
|
const primaryButtonClass = "inline-flex min-h-9 items-center justify-center gap-2 rounded-lg bg-primary px-4 text-xs font-medium text-primary-foreground shadow-sm transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
|
|
276
|
+
/** Inline failure signal for a failed completeFirstRun() call — keeps the
|
|
277
|
+
* user on their current screen with a way forward, instead of swapping to
|
|
278
|
+
* an unrelated full-screen error or leaving Skip/Continue looking like it
|
|
279
|
+
* did nothing. */
|
|
280
|
+
function FirstRunCompletionError({ message, onRetry, }) {
|
|
281
|
+
return (_jsxs("div", { className: "fixed inset-x-0 bottom-4 z-50 mx-auto flex w-fit max-w-[90vw] items-center gap-3 rounded-lg border border-destructive/30 bg-background px-4 py-2 text-xs shadow-lg", children: [_jsx("span", { className: "text-destructive", children: message }), _jsx("button", { type: "button", className: "font-medium underline underline-offset-2 hover:no-underline", onClick: onRetry, children: "Try again" })] }));
|
|
282
|
+
}
|
|
269
283
|
const secondaryButtonClass = "inline-flex min-h-9 items-center justify-center gap-2 rounded-lg border border-border bg-background px-4 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
|
|
270
284
|
function compareUrl(value) {
|
|
271
285
|
try {
|
|
@@ -30,8 +30,15 @@ export interface UseOnboardingResult {
|
|
|
30
30
|
reopen: () => Promise<void>;
|
|
31
31
|
/** True until the post-signup full-screen flow is completed. */
|
|
32
32
|
firstRun: boolean;
|
|
33
|
-
/** Clear the post-signup full-screen flow marker.
|
|
33
|
+
/** Clear the post-signup full-screen flow marker. Rejects instead of
|
|
34
|
+
* resolving silently when the server call fails — see
|
|
35
|
+
* `completeFirstRunError` for the message to show the user. */
|
|
34
36
|
completeFirstRun: () => Promise<void>;
|
|
37
|
+
/** Set when the last `completeFirstRun()` call failed. Cleared on the next
|
|
38
|
+
* attempt (success or failure). Distinct from `error` (the initial steps
|
|
39
|
+
* load failure) so a failed Skip/Continue doesn't swap the whole screen
|
|
40
|
+
* for an unrelated "could not load" message. */
|
|
41
|
+
completeFirstRunError: string | null;
|
|
35
42
|
}
|
|
36
43
|
export declare function useOnboarding(options?: {
|
|
37
44
|
preview?: boolean;
|
|
@@ -10,6 +10,7 @@ export function useOnboarding(options = {}) {
|
|
|
10
10
|
const [error, setError] = useState(null);
|
|
11
11
|
const [dismissed, setDismissed] = useState(false);
|
|
12
12
|
const [firstRun, setFirstRun] = useState(preview || initialFirstRun);
|
|
13
|
+
const [completeFirstRunError, setCompleteFirstRunError] = useState(null);
|
|
13
14
|
const mountedRef = useRef(true);
|
|
14
15
|
useEffect(() => {
|
|
15
16
|
setFirstRun(preview || initialFirstRun);
|
|
@@ -116,14 +117,27 @@ export function useOnboarding(options = {}) {
|
|
|
116
117
|
}
|
|
117
118
|
return;
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
setCompleteFirstRunError(null);
|
|
121
|
+
// Both a rejected fetch (offline, dropped connection) and a non-ok
|
|
122
|
+
// response are real failures — neither may look like success to the
|
|
123
|
+
// caller, so both throw instead of returning as if the step advanced.
|
|
124
|
+
let response;
|
|
125
|
+
try {
|
|
126
|
+
response = await fetch(agentNativePath("/_agent-native/onboarding/first-run/complete"), {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "Content-Type": "application/json" },
|
|
129
|
+
body: "{}",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
const message = e instanceof Error ? e.message : "first-run completion request failed";
|
|
134
|
+
setCompleteFirstRunError(message);
|
|
135
|
+
throw e instanceof Error ? e : new Error(message);
|
|
136
|
+
}
|
|
124
137
|
if (!response.ok) {
|
|
125
|
-
|
|
126
|
-
|
|
138
|
+
const message = `first-run completion failed: ${response.status}`;
|
|
139
|
+
setCompleteFirstRunError(message);
|
|
140
|
+
throw new Error(message);
|
|
127
141
|
}
|
|
128
142
|
setFirstRun(false);
|
|
129
143
|
if (typeof window !== "undefined") {
|
|
@@ -153,5 +167,6 @@ export function useOnboarding(options = {}) {
|
|
|
153
167
|
reopen,
|
|
154
168
|
firstRun,
|
|
155
169
|
completeFirstRun,
|
|
170
|
+
completeFirstRunError,
|
|
156
171
|
};
|
|
157
172
|
}
|
|
@@ -1040,6 +1040,31 @@ function isDefinitiveReplayUploadClientError(status) {
|
|
|
1040
1040
|
return status === 400 || status === 409 || status === 413 || status === 422;
|
|
1041
1041
|
}
|
|
1042
1042
|
const MAX_TRANSIENT_REPLAY_CLIENT_FAILURES = 3;
|
|
1043
|
+
// A hung upload never settles, so `state.flushing` stays set, every later flush
|
|
1044
|
+
// early-returns while still queueing rrweb events, and the queue grows until the
|
|
1045
|
+
// session ends. Time the request out so the failure is observable and the lock
|
|
1046
|
+
// is released.
|
|
1047
|
+
const REPLAY_UPLOAD_TIMEOUT_MS = 15_000;
|
|
1048
|
+
function startReplayUploadTimeout() {
|
|
1049
|
+
// Use an explicit controller/timer pair instead of relying only on
|
|
1050
|
+
// AbortSignal.timeout: the controller is available in older supported
|
|
1051
|
+
// browsers, and the timer can always be cleared when a normal upload
|
|
1052
|
+
// settles so completed uploads do not leave timeout work behind.
|
|
1053
|
+
const controller = typeof AbortController === "undefined" ? undefined : new AbortController();
|
|
1054
|
+
let rejectTimeout;
|
|
1055
|
+
const promise = new Promise((_, reject) => {
|
|
1056
|
+
rejectTimeout = reject;
|
|
1057
|
+
});
|
|
1058
|
+
const timer = setTimeout(() => {
|
|
1059
|
+
controller?.abort();
|
|
1060
|
+
rejectTimeout(new Error("Session replay upload timed out"));
|
|
1061
|
+
}, REPLAY_UPLOAD_TIMEOUT_MS);
|
|
1062
|
+
return {
|
|
1063
|
+
signal: controller?.signal,
|
|
1064
|
+
promise,
|
|
1065
|
+
done: () => clearTimeout(timer),
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1043
1068
|
function isTransientReplayUploadClientError(status) {
|
|
1044
1069
|
return status === 401 || status === 403 || status === 404;
|
|
1045
1070
|
}
|
|
@@ -1048,12 +1073,21 @@ async function sendReplayUpload(options, body, callbacks = {}) {
|
|
|
1048
1073
|
const canUseKeepalive = canUseReplayKeepalive(body);
|
|
1049
1074
|
if (canUseKeepalive)
|
|
1050
1075
|
callbacks.beforeKeepaliveUpload?.();
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1076
|
+
const timeout = startReplayUploadTimeout();
|
|
1077
|
+
let response;
|
|
1078
|
+
try {
|
|
1079
|
+
const request = fetch(options.endpoint, {
|
|
1080
|
+
method: "POST",
|
|
1081
|
+
body,
|
|
1082
|
+
keepalive: canUseKeepalive,
|
|
1083
|
+
headers: { "Content-Type": "text/plain;charset=UTF-8" },
|
|
1084
|
+
signal: timeout.signal,
|
|
1085
|
+
});
|
|
1086
|
+
response = await Promise.race([request, timeout.promise]);
|
|
1087
|
+
}
|
|
1088
|
+
finally {
|
|
1089
|
+
timeout.done();
|
|
1090
|
+
}
|
|
1057
1091
|
if (!response.ok) {
|
|
1058
1092
|
throw new ReplayUploadHttpError(response.status);
|
|
1059
1093
|
}
|
|
@@ -1063,15 +1097,24 @@ async function sendReplayUpload(options, body, callbacks = {}) {
|
|
|
1063
1097
|
const canUseKeepalive = canUseReplayKeepalive(upload.body);
|
|
1064
1098
|
if (canUseKeepalive)
|
|
1065
1099
|
callbacks.beforeKeepaliveUpload?.();
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1100
|
+
const timeout = startReplayUploadTimeout();
|
|
1101
|
+
let response;
|
|
1102
|
+
try {
|
|
1103
|
+
const request = fetch(options.endpoint, {
|
|
1104
|
+
method: "POST",
|
|
1105
|
+
body: upload.body,
|
|
1106
|
+
keepalive: canUseKeepalive,
|
|
1107
|
+
headers: {
|
|
1108
|
+
...upload.headers,
|
|
1109
|
+
"X-Agent-Native-Analytics-Key": options.publicKey,
|
|
1110
|
+
},
|
|
1111
|
+
signal: timeout.signal,
|
|
1112
|
+
});
|
|
1113
|
+
response = await Promise.race([request, timeout.promise]);
|
|
1114
|
+
}
|
|
1115
|
+
finally {
|
|
1116
|
+
timeout.done();
|
|
1117
|
+
}
|
|
1075
1118
|
if (!response.ok) {
|
|
1076
1119
|
throw new ReplayUploadHttpError(response.status);
|
|
1077
1120
|
}
|
|
@@ -3,6 +3,7 @@ import { Tabs, useDesignSystem } from "@agent-native/toolkit/design-system";
|
|
|
3
3
|
import { IconArrowUpRight, IconHistory, IconSearch, IconSettings, IconUserCircle, IconUsers, IconX, } from "@tabler/icons-react";
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
5
5
|
import { Link } from "react-router";
|
|
6
|
+
import { appBasePath, appPath } from "../../client/api-path.js";
|
|
6
7
|
import { buildSettingsRoute } from "../../navigation/index.js";
|
|
7
8
|
import { cn } from "../utils.js";
|
|
8
9
|
function normalizeTabId(value) {
|
|
@@ -70,12 +71,13 @@ function resolveTabId(tabs, value) {
|
|
|
70
71
|
function activeTabFromLocation(tabs, defaultTab) {
|
|
71
72
|
if (typeof window === "undefined")
|
|
72
73
|
return defaultTab;
|
|
74
|
+
const pathname = appLocalPathname();
|
|
73
75
|
const settingsPrefix = "/settings";
|
|
74
|
-
if (
|
|
76
|
+
if (pathname === settingsPrefix) {
|
|
75
77
|
return resolveTabId(tabs, window.location.hash) ?? defaultTab;
|
|
76
78
|
}
|
|
77
|
-
if (
|
|
78
|
-
const segments =
|
|
79
|
+
if (pathname.startsWith(`${settingsPrefix}/`)) {
|
|
80
|
+
const segments = pathname
|
|
79
81
|
.slice(`${settingsPrefix}/`.length)
|
|
80
82
|
.split("/")
|
|
81
83
|
.filter(Boolean)
|
|
@@ -95,6 +97,17 @@ function activeTabFromLocation(tabs, defaultTab) {
|
|
|
95
97
|
}
|
|
96
98
|
return resolveTabId(tabs, window.location.hash) ?? defaultTab;
|
|
97
99
|
}
|
|
100
|
+
function appLocalPathname() {
|
|
101
|
+
if (typeof window === "undefined")
|
|
102
|
+
return "/";
|
|
103
|
+
const pathname = window.location.pathname;
|
|
104
|
+
const basePath = appBasePath();
|
|
105
|
+
if (basePath &&
|
|
106
|
+
(pathname === basePath || pathname.startsWith(`${basePath}/`))) {
|
|
107
|
+
return pathname.slice(basePath.length) || "/";
|
|
108
|
+
}
|
|
109
|
+
return pathname;
|
|
110
|
+
}
|
|
98
111
|
function buildSettingsEntryRoute(tabId, section) {
|
|
99
112
|
const normalizedSection = section?.replace(/^#/, "").trim();
|
|
100
113
|
if (!normalizedSection || normalizedSection === tabId) {
|
|
@@ -112,7 +125,7 @@ function updateRouteForTab(tabId, section) {
|
|
|
112
125
|
if (typeof window === "undefined")
|
|
113
126
|
return;
|
|
114
127
|
const route = buildSettingsEntryRoute(tabId, section);
|
|
115
|
-
window.history.pushState(null, "", `${route}${window.location.search}`);
|
|
128
|
+
window.history.pushState(null, "", `${appPath(route)}${window.location.search}`);
|
|
116
129
|
}
|
|
117
130
|
function isEditableElement(element) {
|
|
118
131
|
if (!(element instanceof HTMLElement))
|
|
@@ -233,7 +246,7 @@ export function SettingsTabsPage({ general, account, team, whatsNew, extraTabs =
|
|
|
233
246
|
const fromPath = activeTabFromLocation(tabs, fallbackTab);
|
|
234
247
|
if (fromPath)
|
|
235
248
|
setInternalTab(fromPath);
|
|
236
|
-
if (
|
|
249
|
+
if (appLocalPathname().startsWith("/settings/"))
|
|
237
250
|
return;
|
|
238
251
|
const hashValue = window.location.hash.replace(/^#/, "");
|
|
239
252
|
const fromHash = resolveTabId(tabs, hashValue);
|
|
@@ -254,7 +267,7 @@ export function SettingsTabsPage({ general, account, team, whatsNew, extraTabs =
|
|
|
254
267
|
if (!isControlled)
|
|
255
268
|
return;
|
|
256
269
|
const syncControlledLocation = () => {
|
|
257
|
-
const fromPath =
|
|
270
|
+
const fromPath = appLocalPathname().startsWith("/settings/")
|
|
258
271
|
? activeTabFromLocation(tabs, defaultTab)
|
|
259
272
|
: null;
|
|
260
273
|
const hashValue = window.location.hash.replace(/^#/, "");
|
|
@@ -39,10 +39,15 @@ export type ContentPart = {
|
|
|
39
39
|
* call (opt-in `needsApproval` actions). The action did NOT run; the UI
|
|
40
40
|
* renders an Approve/Deny affordance. `approvalKey` is echoed back in
|
|
41
41
|
* `approvedToolCalls` to approve, `dismissed` records a local Deny.
|
|
42
|
+
* `askId` identifies THIS gate hit; it changes when a failed resume
|
|
43
|
+
* re-emits `approval_required` for the same call, which is how the UI
|
|
44
|
+
* tells that apart from the same ask simply re-rendering (see
|
|
45
|
+
* `ApprovalAffordance` in chat/tool-call-display.tsx).
|
|
42
46
|
*/
|
|
43
47
|
approval?: {
|
|
44
48
|
approvalKey: string;
|
|
45
49
|
dismissed?: boolean;
|
|
50
|
+
askId?: string;
|
|
46
51
|
};
|
|
47
52
|
/**
|
|
48
53
|
* Structured metadata from the coding-tools executor side-channel.
|
|
@@ -70,6 +75,8 @@ export interface SSEEvent {
|
|
|
70
75
|
approvalKey?: string;
|
|
71
76
|
/** Model-side tool-call id for `approval_required` (mirrors AgentChatEvent). */
|
|
72
77
|
toolCallId?: string;
|
|
78
|
+
/** Identifies this `approval_required` gate hit (mirrors AgentChatEvent). */
|
|
79
|
+
askId?: string;
|
|
73
80
|
error?: string;
|
|
74
81
|
seq?: number;
|
|
75
82
|
agent?: string;
|
|
@@ -1182,7 +1182,10 @@ export function processEvent(ev, content, toolCallCounter, tabId, state) {
|
|
|
1182
1182
|
if (idx >= 0) {
|
|
1183
1183
|
const part = content[idx];
|
|
1184
1184
|
if (part.type === "tool-call") {
|
|
1185
|
-
part.approval = {
|
|
1185
|
+
part.approval = {
|
|
1186
|
+
approvalKey,
|
|
1187
|
+
...(ev.askId ? { askId: ev.askId } : {}),
|
|
1188
|
+
};
|
|
1186
1189
|
}
|
|
1187
1190
|
}
|
|
1188
1191
|
}
|
|
@@ -12,6 +12,7 @@ export { ErrorBoundary } from "../ErrorBoundary.js";
|
|
|
12
12
|
export { ClientOnly } from "../ClientOnly.js";
|
|
13
13
|
export { DefaultSpinner } from "../DefaultSpinner.js";
|
|
14
14
|
export { RuntimeConfigNotice } from "../RuntimeConfigNotice.js";
|
|
15
|
+
export { EnvironmentBadge, buildEnvironmentUrl, isBuilderIoEmployee, resolveEnvironmentChannel, resolveEnvironmentTargets, type EnvironmentBadgeTargets, } from "../EnvironmentBadge.js";
|
|
15
16
|
export { RouteTransitionIndicator, ROUTE_TRANSITION_INDICATOR_DELAY_MS, } from "../RouteTransitionIndicator.js";
|
|
16
17
|
export { applyEmbeddedThemeUpdate, buildEmbeddedThemeUpdate, EMBEDDED_THEME_CHANGE_EVENT, EMBEDDED_THEME_UPDATE_MESSAGE, getThemeInitScript, parseEmbeddedThemeUpdate, themeInitScript, type EmbeddedThemeUpdate, type NormalizedEmbeddedThemeUpdate, type ResolvedTheme, type ThemePreference, } from "../theme.js";
|
|
17
18
|
export { APPEARANCE_PRESETS, applyAppearance, getStoredAppearance, useAppearance, useAppearanceSync, type AppearancePresetId, } from "../appearance.js";
|
package/dist/client/ui/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { ErrorBoundary } from "../ErrorBoundary.js";
|
|
|
12
12
|
export { ClientOnly } from "../ClientOnly.js";
|
|
13
13
|
export { DefaultSpinner } from "../DefaultSpinner.js";
|
|
14
14
|
export { RuntimeConfigNotice } from "../RuntimeConfigNotice.js";
|
|
15
|
+
export { EnvironmentBadge, buildEnvironmentUrl, isBuilderIoEmployee, resolveEnvironmentChannel, resolveEnvironmentTargets, } from "../EnvironmentBadge.js";
|
|
15
16
|
export { RouteTransitionIndicator, ROUTE_TRANSITION_INDICATOR_DELAY_MS, } from "../RouteTransitionIndicator.js";
|
|
16
17
|
export { applyEmbeddedThemeUpdate, buildEmbeddedThemeUpdate, EMBEDDED_THEME_CHANGE_EVENT, EMBEDDED_THEME_UPDATE_MESSAGE, getThemeInitScript, parseEmbeddedThemeUpdate, themeInitScript, } from "../theme.js";
|
|
17
18
|
export { APPEARANCE_PRESETS, applyAppearance, getStoredAppearance, useAppearance, useAppearanceSync, } from "../appearance.js";
|
|
@@ -52,6 +52,11 @@ function isActionTimeout(error) {
|
|
|
52
52
|
typeof error === "object" &&
|
|
53
53
|
error.timedOut === true);
|
|
54
54
|
}
|
|
55
|
+
function isActionMethodMismatch(error) {
|
|
56
|
+
return (!!error &&
|
|
57
|
+
typeof error === "object" &&
|
|
58
|
+
error.code === "action_method_mismatch");
|
|
59
|
+
}
|
|
55
60
|
/** @internal exported for tests */
|
|
56
61
|
export function defaultActionQueryRetry(failureCount, error) {
|
|
57
62
|
if (isAuthFailure(error))
|
|
@@ -60,6 +65,9 @@ export function defaultActionQueryRetry(failureCount, error) {
|
|
|
60
65
|
// silently retrying would multiply that wait. Surface it instead.
|
|
61
66
|
if (isActionTimeout(error))
|
|
62
67
|
return false;
|
|
68
|
+
// Wrong verb is deterministic — retrying sends the same wrong verb again.
|
|
69
|
+
if (isActionMethodMismatch(error))
|
|
70
|
+
return false;
|
|
63
71
|
if (isBrowserResourceExhaustion(error))
|
|
64
72
|
return false;
|
|
65
73
|
// Network-level failures never carry an HTTP `status` (actionFetch only
|
|
@@ -338,6 +346,24 @@ async function performActionFetch(name, method, params, options) {
|
|
|
338
346
|
(raw && raw.slice(0, 200)) ||
|
|
339
347
|
res.statusText ||
|
|
340
348
|
`HTTP ${res.status}`;
|
|
349
|
+
// mountActionRoutes only ever returns 405 for one reason: the verb this
|
|
350
|
+
// call sent doesn't match the action's declared `http.method`. The client
|
|
351
|
+
// has no way to look that method up ahead of time (defineAction lives in
|
|
352
|
+
// server-only modules), so callers routinely default to POST and hit this
|
|
353
|
+
// blind. Name the action and the required method instead of leaving a
|
|
354
|
+
// bare 405 for the caller to reverse-engineer from a "Use X" string.
|
|
355
|
+
if (res.status === 405) {
|
|
356
|
+
const requiredMethod = /\bUse (GET|POST|PUT|DELETE)\b/.exec(message)?.[1];
|
|
357
|
+
const error = new Error(`Action ${name} was called with ${method}, but it declares ` +
|
|
358
|
+
`http: { method: "${requiredMethod ?? "?"}" }. Pass { method: "${requiredMethod ?? "..."}" } ` +
|
|
359
|
+
`to this call (or use the hook that defaults to it) to match the action's declared method.`);
|
|
360
|
+
error.status = 405;
|
|
361
|
+
error.code = "action_method_mismatch";
|
|
362
|
+
error.sentMethod = method;
|
|
363
|
+
if (requiredMethod)
|
|
364
|
+
error.requiredMethod = requiredMethod;
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
341
367
|
const error = new Error(`Action ${name} failed: ${message}`);
|
|
342
368
|
error.status = res.status;
|
|
343
369
|
throw error;
|