@agent-native/core 0.131.7 → 0.132.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 +2 -2
- package/corpus/core/CHANGELOG.md +123 -0
- package/corpus/core/docs/content/durable-background-runs.mdx +15 -6
- package/corpus/core/docs/design/durable-agent-runs.md +15 -11
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/activity.ts +127 -10
- package/corpus/core/src/a2a/index.ts +3 -0
- package/corpus/core/src/agent/durable-background.ts +38 -28
- package/corpus/core/src/agent/harness/background.ts +12 -2
- package/corpus/core/src/agent/production-agent.ts +18 -15
- package/corpus/core/src/agent/run-store.ts +45 -3
- package/corpus/core/src/application-state/handlers.ts +45 -0
- package/corpus/core/src/application-state/index.ts +3 -0
- package/corpus/core/src/application-state/store.ts +38 -34
- package/corpus/core/src/audit/redact.ts +104 -18
- package/corpus/core/src/cli/create.ts +115 -16
- package/corpus/core/src/client/AgentPanel.tsx +2 -7
- package/corpus/core/src/client/AssistantChat.tsx +10 -2
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +3 -0
- package/corpus/core/src/client/RunStuckBanner.tsx +29 -6
- package/corpus/core/src/client/agent-chat.ts +4 -9
- package/corpus/core/src/client/application-state.ts +115 -12
- package/corpus/core/src/client/dynamic-suggestions.ts +4 -12
- package/corpus/core/src/client/frame-protocol.ts +5 -1
- package/corpus/core/src/client/guided-questions.tsx +12 -23
- package/corpus/core/src/db/client.ts +148 -28
- package/corpus/core/src/db/create-get-db.ts +39 -6
- package/corpus/core/src/db/runtime-diagnostics.ts +41 -6
- package/corpus/core/src/deploy/build.ts +26 -33
- package/corpus/core/src/feature-flags/store.ts +10 -5
- package/corpus/core/src/integrations/a2a-continuations-store.ts +11 -0
- package/corpus/core/src/integrations/config-store.ts +13 -0
- package/corpus/core/src/integrations/google-docs-poller.ts +26 -2
- package/corpus/core/src/mcp-client/routes.ts +33 -2
- package/corpus/core/src/onboarding/plugin.ts +31 -25
- package/corpus/core/src/org/accept-pending.ts +2 -0
- package/corpus/core/src/org/auto-join-domain.ts +2 -0
- package/corpus/core/src/org/context.ts +35 -10
- package/corpus/core/src/org/handlers.ts +6 -0
- package/corpus/core/src/org/request-org-cache.ts +71 -0
- package/corpus/core/src/resources/store.ts +10 -9
- package/corpus/core/src/scripts/runner.ts +6 -3
- package/corpus/core/src/secrets/storage.ts +97 -5
- package/corpus/core/src/server/action-change.ts +33 -0
- package/corpus/core/src/server/action-routes.ts +11 -9
- package/corpus/core/src/server/agent-teams.ts +10 -2
- package/corpus/core/src/server/better-auth-instance.ts +39 -12
- package/corpus/core/src/server/core-routes-plugin.ts +19 -2
- package/corpus/core/src/server/credential-provider.ts +57 -14
- package/corpus/core/src/server/http-response-telemetry.ts +1 -0
- package/corpus/core/src/server/poll.ts +95 -57
- package/corpus/core/src/server/voice-providers-status.ts +19 -4
- package/corpus/core/src/workspace-files/tool.ts +18 -11
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +38 -0
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1 -1
- package/corpus/templates/analytics/actions/ensure-native-v2-dashboards.ts +299 -0
- package/corpus/templates/analytics/actions/list-native-v2-dashboard-templates.ts +34 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +82 -0
- package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +2 -2
- package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +40 -34
- package/corpus/templates/analytics/app/hooks/use-user-pref.ts +3 -3
- package/corpus/templates/analytics/app/i18n-data.ts +40 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +18 -10
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +14 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +1 -0
- package/corpus/templates/analytics/app/routes/chart.tsx +3 -0
- package/corpus/templates/analytics/changelog/2026-07-30-analytics-dashboards-now-expose-native-funnel-heatmap-callou.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-30-analytics-now-ships-reusable-native-v2-dashboard-manifests-w.md +6 -0
- package/corpus/templates/analytics/server/lib/dashboard-report-render.ts +81 -1
- package/corpus/templates/analytics/server/lib/native-v2-dashboards.ts +457 -0
- package/corpus/templates/analytics/server/lib/report-chart-svg.ts +73 -2
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/analytics/shared/dashboard-funnel.ts +83 -0
- package/corpus/templates/clips/actions/generate-workflow.ts +6 -4
- package/corpus/templates/clips/actions/reconcile-workflow-generation.ts +257 -0
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +166 -4
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -1
- package/corpus/templates/clips/changelog/2026-07-27-stopped-agent-runs-no-longer-leave-generated-workflow-cards-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-30-workflow-generation-now-stops-retrying-after-repeated-failur.md +6 -0
- package/corpus/templates/clips/shared/workflow.ts +5 -0
- package/corpus/templates/content/changelog/2026-07-30-long-running-delegated-requests-now-continue-reliably.md +6 -0
- package/corpus/templates/content/netlify.toml +3 -0
- package/corpus/templates/content/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +2 -1
- package/corpus/templates/slides/actions/patch-deck.ts +25 -1
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +17 -4
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +9 -1
- package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +11 -3
- package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +39 -11
- package/corpus/templates/slides/app/pages/Index.tsx +2 -2
- package/corpus/templates/slides/changelog/2026-07-30-slide-previews-keep-their-aspect-ratio-isolate-embedded-styl.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-30-slides-no-longer-lose-decks-while-organization-access-is-loa.md +6 -0
- package/corpus/toolkit/CHANGELOG.md +8 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/composer/runtime-adapters.tsx +16 -11
- package/dist/a2a/activity.d.ts +22 -4
- package/dist/a2a/activity.d.ts.map +1 -1
- package/dist/a2a/activity.js +97 -11
- package/dist/a2a/activity.js.map +1 -1
- package/dist/a2a/index.d.ts +1 -1
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js +1 -1
- package/dist/a2a/index.js.map +1 -1
- package/dist/agent/durable-background.d.ts +13 -8
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +36 -28
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/harness/background.d.ts.map +1 -1
- package/dist/agent/harness/background.js +12 -2
- package/dist/agent/harness/background.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +18 -15
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-store.d.ts +2 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +44 -3
- package/dist/agent/run-store.js.map +1 -1
- package/dist/application-state/handlers.d.ts +15 -0
- package/dist/application-state/handlers.d.ts.map +1 -1
- package/dist/application-state/handlers.js +42 -2
- package/dist/application-state/handlers.js.map +1 -1
- package/dist/application-state/index.d.ts +2 -2
- package/dist/application-state/index.d.ts.map +1 -1
- package/dist/application-state/index.js +2 -2
- package/dist/application-state/index.js.map +1 -1
- package/dist/application-state/store.d.ts +11 -0
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +37 -40
- package/dist/application-state/store.js.map +1 -1
- package/dist/audit/redact.d.ts +25 -2
- package/dist/audit/redact.d.ts.map +1 -1
- package/dist/audit/redact.js +85 -21
- package/dist/audit/redact.js.map +1 -1
- package/dist/cli/create.d.ts +8 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +98 -16
- package/dist/cli/create.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -7
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +10 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +1 -1
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts +11 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +20 -7
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +4 -6
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/application-state.d.ts +11 -0
- package/dist/client/application-state.d.ts.map +1 -1
- package/dist/client/application-state.js +87 -9
- package/dist/client/application-state.js.map +1 -1
- package/dist/client/dynamic-suggestions.d.ts.map +1 -1
- package/dist/client/dynamic-suggestions.js +4 -13
- package/dist/client/dynamic-suggestions.js.map +1 -1
- package/dist/client/frame-protocol.d.ts +1 -0
- package/dist/client/frame-protocol.d.ts.map +1 -1
- package/dist/client/frame-protocol.js.map +1 -1
- package/dist/client/guided-questions.d.ts.map +1 -1
- package/dist/client/guided-questions.js +6 -22
- package/dist/client/guided-questions.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/db/client.d.ts +57 -10
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +122 -31
- package/dist/db/client.js.map +1 -1
- package/dist/db/create-get-db.d.ts.map +1 -1
- package/dist/db/create-get-db.js +31 -7
- package/dist/db/create-get-db.js.map +1 -1
- package/dist/db/runtime-diagnostics.d.ts +1 -0
- package/dist/db/runtime-diagnostics.d.ts.map +1 -1
- package/dist/db/runtime-diagnostics.js +31 -6
- package/dist/db/runtime-diagnostics.js.map +1 -1
- package/dist/deploy/build.d.ts +5 -20
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +18 -30
- package/dist/deploy/build.js.map +1 -1
- package/dist/feature-flags/store.d.ts.map +1 -1
- package/dist/feature-flags/store.js +10 -5
- package/dist/feature-flags/store.js.map +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +12 -0
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/config-store.d.ts +1 -0
- package/dist/integrations/config-store.d.ts.map +1 -1
- package/dist/integrations/config-store.js +11 -0
- package/dist/integrations/config-store.js.map +1 -1
- package/dist/integrations/google-docs-poller.d.ts.map +1 -1
- package/dist/integrations/google-docs-poller.js +19 -2
- package/dist/integrations/google-docs-poller.js.map +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
- package/dist/mcp-client/routes.d.ts.map +1 -1
- package/dist/mcp-client/routes.js +30 -2
- package/dist/mcp-client/routes.js.map +1 -1
- package/dist/notifications/routes.d.ts +6 -6
- package/dist/onboarding/plugin.d.ts.map +1 -1
- package/dist/onboarding/plugin.js +12 -8
- package/dist/onboarding/plugin.js.map +1 -1
- package/dist/org/accept-pending.d.ts.map +1 -1
- package/dist/org/accept-pending.js +2 -0
- package/dist/org/accept-pending.js.map +1 -1
- package/dist/org/auto-join-domain.d.ts.map +1 -1
- package/dist/org/auto-join-domain.js +2 -0
- package/dist/org/auto-join-domain.js.map +1 -1
- package/dist/org/context.d.ts.map +1 -1
- package/dist/org/context.js +29 -9
- package/dist/org/context.js.map +1 -1
- package/dist/org/handlers.d.ts.map +1 -1
- package/dist/org/handlers.js +5 -0
- package/dist/org/handlers.js.map +1 -1
- package/dist/org/request-org-cache.d.ts +16 -0
- package/dist/org/request-org-cache.d.ts.map +1 -0
- package/dist/org/request-org-cache.js +62 -0
- package/dist/org/request-org-cache.js.map +1 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/resources/store.d.ts.map +1 -1
- package/dist/resources/store.js +11 -9
- package/dist/resources/store.js.map +1 -1
- package/dist/scripts/runner.d.ts.map +1 -1
- package/dist/scripts/runner.js +3 -3
- package/dist/scripts/runner.js.map +1 -1
- package/dist/secrets/storage.d.ts.map +1 -1
- package/dist/secrets/storage.js +82 -5
- package/dist/secrets/storage.js.map +1 -1
- package/dist/server/action-change.d.ts +20 -0
- package/dist/server/action-change.d.ts.map +1 -1
- package/dist/server/action-change.js +29 -0
- package/dist/server/action-change.js.map +1 -1
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +7 -8
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/agent-teams.d.ts.map +1 -1
- package/dist/server/agent-teams.js +10 -2
- package/dist/server/agent-teams.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +31 -14
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +18 -4
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/credential-provider.d.ts +16 -0
- package/dist/server/credential-provider.d.ts.map +1 -1
- package/dist/server/credential-provider.js +49 -13
- package/dist/server/credential-provider.js.map +1 -1
- package/dist/server/http-response-telemetry.d.ts.map +1 -1
- package/dist/server/http-response-telemetry.js +1 -0
- package/dist/server/http-response-telemetry.js.map +1 -1
- package/dist/server/poll.d.ts +6 -0
- package/dist/server/poll.d.ts.map +1 -1
- package/dist/server/poll.js +84 -47
- package/dist/server/poll.js.map +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/voice-providers-status.d.ts.map +1 -1
- package/dist/server/voice-providers-status.js +12 -3
- package/dist/server/voice-providers-status.js.map +1 -1
- package/dist/workspace-files/tool.d.ts.map +1 -1
- package/dist/workspace-files/tool.js +18 -11
- package/dist/workspace-files/tool.js.map +1 -1
- package/docs/content/durable-background-runs.mdx +15 -6
- package/docs/design/durable-agent-runs.md +15 -11
- package/package.json +3 -3
- package/src/a2a/activity.ts +127 -10
- package/src/a2a/index.ts +3 -0
- package/src/agent/durable-background.ts +38 -28
- package/src/agent/harness/background.ts +12 -2
- package/src/agent/production-agent.ts +18 -15
- package/src/agent/run-store.ts +45 -3
- package/src/application-state/handlers.ts +45 -0
- package/src/application-state/index.ts +3 -0
- package/src/application-state/store.ts +38 -34
- package/src/audit/redact.ts +104 -18
- package/src/cli/create.ts +115 -16
- package/src/client/AgentPanel.tsx +2 -7
- package/src/client/AssistantChat.tsx +10 -2
- package/src/client/MultiTabAssistantChat.tsx +3 -0
- package/src/client/RunStuckBanner.tsx +29 -6
- package/src/client/agent-chat.ts +4 -9
- package/src/client/application-state.ts +115 -12
- package/src/client/dynamic-suggestions.ts +4 -12
- package/src/client/frame-protocol.ts +5 -1
- package/src/client/guided-questions.tsx +12 -23
- package/src/db/client.ts +148 -28
- package/src/db/create-get-db.ts +39 -6
- package/src/db/runtime-diagnostics.ts +41 -6
- package/src/deploy/build.ts +26 -33
- package/src/feature-flags/store.ts +10 -5
- package/src/integrations/a2a-continuations-store.ts +11 -0
- package/src/integrations/config-store.ts +13 -0
- package/src/integrations/google-docs-poller.ts +26 -2
- package/src/mcp-client/routes.ts +33 -2
- package/src/onboarding/plugin.ts +31 -25
- package/src/org/accept-pending.ts +2 -0
- package/src/org/auto-join-domain.ts +2 -0
- package/src/org/context.ts +35 -10
- package/src/org/handlers.ts +6 -0
- package/src/org/request-org-cache.ts +71 -0
- package/src/resources/store.ts +10 -9
- package/src/scripts/runner.ts +6 -3
- package/src/secrets/storage.ts +97 -5
- package/src/server/action-change.ts +33 -0
- package/src/server/action-routes.ts +11 -9
- package/src/server/agent-teams.ts +10 -2
- package/src/server/better-auth-instance.ts +39 -12
- package/src/server/core-routes-plugin.ts +19 -2
- package/src/server/credential-provider.ts +57 -14
- package/src/server/http-response-telemetry.ts +1 -0
- package/src/server/poll.ts +95 -57
- package/src/server/voice-providers-status.ts +19 -4
- package/src/workspace-files/tool.ts +18 -11
|
@@ -50,6 +50,18 @@ const FIRST_PARTY_TARBALL_SYMLINK_EXCLUDES = [
|
|
|
50
50
|
"*/.claude/skills",
|
|
51
51
|
];
|
|
52
52
|
const localPackageTarballs = new Map<string, string>();
|
|
53
|
+
/** VCS/editor files that don't count as "not empty" for an in-place scaffold. */
|
|
54
|
+
const IN_PLACE_ALLOWLIST = new Set([
|
|
55
|
+
".git",
|
|
56
|
+
".gitignore",
|
|
57
|
+
".gitattributes",
|
|
58
|
+
".DS_Store",
|
|
59
|
+
".idea",
|
|
60
|
+
".vscode",
|
|
61
|
+
"LICENSE",
|
|
62
|
+
"README.md",
|
|
63
|
+
"Thumbs.db",
|
|
64
|
+
]);
|
|
53
65
|
|
|
54
66
|
/**
|
|
55
67
|
* Tagged error for input that fails CLI-level validation (repo names, app
|
|
@@ -110,6 +122,11 @@ export interface CreateAppOptions {
|
|
|
110
122
|
* unconditional workspace scaffold.
|
|
111
123
|
*/
|
|
112
124
|
forceWorkspace?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Internal: scaffold into the current directory instead of a new subfolder.
|
|
127
|
+
* Set when the name argument is `.`/`./` (see `createApp`).
|
|
128
|
+
*/
|
|
129
|
+
inPlace?: boolean;
|
|
113
130
|
}
|
|
114
131
|
|
|
115
132
|
/**
|
|
@@ -127,6 +144,13 @@ export async function createApp(
|
|
|
127
144
|
): Promise<void> {
|
|
128
145
|
const clack = await import("@clack/prompts");
|
|
129
146
|
|
|
147
|
+
// `create .` (or `./`) means "scaffold into the current folder" — derive the
|
|
148
|
+
// project name from the folder's basename, like create-react-app / npm init.
|
|
149
|
+
if (name === "." || name === "./") {
|
|
150
|
+
name = path.basename(process.cwd());
|
|
151
|
+
opts = { ...opts, inPlace: true };
|
|
152
|
+
}
|
|
153
|
+
|
|
130
154
|
// Reject an invalid provided name before any interactive prompt so bad input
|
|
131
155
|
// fails fast instead of blocking on the start-shape picker below.
|
|
132
156
|
if (name !== undefined) {
|
|
@@ -335,6 +359,44 @@ function assertValidProjectName(
|
|
|
335
359
|
process.exit(1);
|
|
336
360
|
}
|
|
337
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Resolve where a scaffold writes and guard the target. A named project writes
|
|
364
|
+
* to a new sibling subfolder that must not already exist; `create .` writes
|
|
365
|
+
* into the current directory, which must be empty apart from benign VCS/editor
|
|
366
|
+
* files (a pre-existing repo is allowed).
|
|
367
|
+
*
|
|
368
|
+
* For an in-place scaffold we do NOT return the current directory: we build
|
|
369
|
+
* into a private staging directory and `finalizeScaffold` copies the result
|
|
370
|
+
* in afterward. Staging keeps the whole scaffold atomic — a mid-scaffold
|
|
371
|
+
* failure's cleanup can only ever delete the staging dir, never the user's
|
|
372
|
+
* current directory (including its `.git`).
|
|
373
|
+
*/
|
|
374
|
+
function resolveScaffoldTarget(
|
|
375
|
+
name: string,
|
|
376
|
+
inPlace: boolean | undefined,
|
|
377
|
+
clack: typeof import("@clack/prompts"),
|
|
378
|
+
): string {
|
|
379
|
+
if (inPlace) {
|
|
380
|
+
const conflicting = fs
|
|
381
|
+
.readdirSync(process.cwd())
|
|
382
|
+
.filter((entry) => !IN_PLACE_ALLOWLIST.has(entry));
|
|
383
|
+
if (conflicting.length > 0) {
|
|
384
|
+
const shown = conflicting.slice(0, 3).join(", ");
|
|
385
|
+
const more = conflicting.length > 3 ? ", …" : "";
|
|
386
|
+
clack.cancel(
|
|
387
|
+
`Current directory is not empty (${shown}${more}). Scaffold into an empty folder, or run \`create <name>\` to make a new one.`,
|
|
388
|
+
);
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "agent-native-create-"));
|
|
392
|
+
}
|
|
393
|
+
const targetDir = path.resolve(process.cwd(), name);
|
|
394
|
+
if (fs.existsSync(targetDir)) {
|
|
395
|
+
clack.cancel(`Directory "${name}" already exists.`);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
return targetDir;
|
|
399
|
+
}
|
|
338
400
|
|
|
339
401
|
/* ─────────────────────────────────────────────────────────────────────────
|
|
340
402
|
* Workspace creation (new default)
|
|
@@ -378,11 +440,7 @@ async function createWorkspaceInteractive(
|
|
|
378
440
|
});
|
|
379
441
|
const templates = ["dispatch", ...optionalPicks];
|
|
380
442
|
|
|
381
|
-
const targetDir =
|
|
382
|
-
if (fs.existsSync(targetDir)) {
|
|
383
|
-
clack.cancel(`Directory "${name}" already exists.`);
|
|
384
|
-
process.exit(1);
|
|
385
|
-
}
|
|
443
|
+
const targetDir = resolveScaffoldTarget(name, opts?.inPlace, clack);
|
|
386
444
|
|
|
387
445
|
const s = clack.spinner();
|
|
388
446
|
for (const template of templates) {
|
|
@@ -482,13 +540,15 @@ async function createWorkspaceInteractive(
|
|
|
482
540
|
} catch (err: any) {
|
|
483
541
|
s.stop("Failed to scaffold workspace.");
|
|
484
542
|
// Remove the partially-scaffolded workspace so a retry of `agent-native
|
|
485
|
-
// create <name>` doesn't trip the "Directory already exists" guard.
|
|
543
|
+
// create <name>` doesn't trip the "Directory already exists" guard. For an
|
|
544
|
+
// in-place scaffold `targetDir` is the private staging dir, so this never
|
|
545
|
+
// touches the user's current directory.
|
|
486
546
|
cleanupOnFailure(targetDir);
|
|
487
547
|
clack.cancel(err?.message ?? String(err));
|
|
488
548
|
process.exit(1);
|
|
489
549
|
}
|
|
490
550
|
|
|
491
|
-
|
|
551
|
+
finalizeScaffold(targetDir, opts?.inPlace);
|
|
492
552
|
|
|
493
553
|
// Show the user the tree we just built so the workspace/app distinction is
|
|
494
554
|
// visible, not just described. First-time users routinely expect their
|
|
@@ -845,11 +905,7 @@ async function createStandaloneApp(
|
|
|
845
905
|
|
|
846
906
|
name = await promptNameIfMissing(name, clack, "app", "my-app");
|
|
847
907
|
|
|
848
|
-
const targetDir =
|
|
849
|
-
if (fs.existsSync(targetDir)) {
|
|
850
|
-
clack.cancel(`Directory "${name}" already exists.`);
|
|
851
|
-
process.exit(1);
|
|
852
|
-
}
|
|
908
|
+
const targetDir = resolveScaffoldTarget(name, opts?.inPlace, clack);
|
|
853
909
|
|
|
854
910
|
// Standalone is single-select — pick one template.
|
|
855
911
|
let template =
|
|
@@ -889,12 +945,14 @@ async function createStandaloneApp(
|
|
|
889
945
|
s.stop("App created!");
|
|
890
946
|
} catch (err: any) {
|
|
891
947
|
s.stop("Failed to create app.");
|
|
948
|
+
// `targetDir` is the private staging dir for an in-place scaffold, so this
|
|
949
|
+
// only ever removes the staging copy, never the user's current directory.
|
|
892
950
|
cleanupOnFailure(targetDir);
|
|
893
951
|
clack.cancel(err?.message ?? String(err));
|
|
894
952
|
process.exit(1);
|
|
895
953
|
}
|
|
896
954
|
|
|
897
|
-
|
|
955
|
+
finalizeScaffold(targetDir, opts?.inPlace);
|
|
898
956
|
|
|
899
957
|
if (template === "headless") {
|
|
900
958
|
clack.outro(
|
|
@@ -954,6 +1012,35 @@ function cleanupOnFailure(targetDir: string): void {
|
|
|
954
1012
|
}
|
|
955
1013
|
}
|
|
956
1014
|
|
|
1015
|
+
/**
|
|
1016
|
+
* Land a finished scaffold in its final home and initialize git. A named
|
|
1017
|
+
* scaffold is already in place, so this only inits git. An in-place scaffold
|
|
1018
|
+
* (`create .`) was built in `scaffoldDir` (a staging dir); copy only the files
|
|
1019
|
+
* that don't already exist into the current directory so pre-existing files
|
|
1020
|
+
* (`.git`, `README.md`, `.gitignore`, editor configs) are preserved, then drop
|
|
1021
|
+
* the staging dir. Git init/commit is skipped when the current directory is
|
|
1022
|
+
* already a repo so we never write an unexpected commit into the user's
|
|
1023
|
+
* history.
|
|
1024
|
+
*/
|
|
1025
|
+
function finalizeScaffold(scaffoldDir: string, inPlace?: boolean): void {
|
|
1026
|
+
if (!inPlace) {
|
|
1027
|
+
tryGitInitUnlessRepo(scaffoldDir);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
const dest = process.cwd();
|
|
1031
|
+
try {
|
|
1032
|
+
copyDir(scaffoldDir, dest, undefined, { skipExisting: true });
|
|
1033
|
+
} finally {
|
|
1034
|
+
cleanupOnFailure(scaffoldDir);
|
|
1035
|
+
}
|
|
1036
|
+
tryGitInitUnlessRepo(dest);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function tryGitInitUnlessRepo(dir: string): void {
|
|
1040
|
+
if (fs.existsSync(path.join(dir, ".git"))) return;
|
|
1041
|
+
tryGitInit(dir);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
957
1044
|
/* ─────────────────────────────────────────────────────────────────────────
|
|
958
1045
|
* Shared scaffolding helpers
|
|
959
1046
|
* ───────────────────────────────────────────────────────────────────────── */
|
|
@@ -3399,13 +3486,25 @@ function replacePlaceholders(
|
|
|
3399
3486
|
}
|
|
3400
3487
|
}
|
|
3401
3488
|
|
|
3402
|
-
function copyDir(
|
|
3489
|
+
function copyDir(
|
|
3490
|
+
src: string,
|
|
3491
|
+
dest: string,
|
|
3492
|
+
root?: string,
|
|
3493
|
+
opts?: { skipExisting?: boolean },
|
|
3494
|
+
): void {
|
|
3403
3495
|
const resolvedRoot = root ?? path.resolve(src);
|
|
3496
|
+
const skipExisting = opts?.skipExisting ?? false;
|
|
3404
3497
|
fs.mkdirSync(dest, { recursive: true });
|
|
3405
3498
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
3406
3499
|
const srcPath = path.join(src, entry.name);
|
|
3407
3500
|
if (shouldSkipScaffoldEntry(entry.name, srcPath)) continue;
|
|
3408
3501
|
const destPath = path.join(dest, entry.name);
|
|
3502
|
+
// Preserve anything already at the destination (in-place scaffold merges
|
|
3503
|
+
// into a directory the user may already own). Directories still recurse so
|
|
3504
|
+
// new files land inside a pre-existing folder.
|
|
3505
|
+
if (skipExisting && !entry.isDirectory() && fs.existsSync(destPath)) {
|
|
3506
|
+
continue;
|
|
3507
|
+
}
|
|
3409
3508
|
if (entry.isSymbolicLink()) {
|
|
3410
3509
|
const target = fs.readlinkSync(srcPath);
|
|
3411
3510
|
const resolvedTarget = path.resolve(path.dirname(srcPath), target);
|
|
@@ -3415,7 +3514,7 @@ function copyDir(src: string, dest: string, root?: string): void {
|
|
|
3415
3514
|
try {
|
|
3416
3515
|
const stat = fs.statSync(srcPath);
|
|
3417
3516
|
if (stat.isDirectory()) {
|
|
3418
|
-
copyDir(srcPath, destPath, resolvedRoot);
|
|
3517
|
+
copyDir(srcPath, destPath, resolvedRoot, opts);
|
|
3419
3518
|
} else {
|
|
3420
3519
|
fs.copyFileSync(srcPath, destPath);
|
|
3421
3520
|
}
|
|
@@ -3424,7 +3523,7 @@ function copyDir(src: string, dest: string, root?: string): void {
|
|
|
3424
3523
|
}
|
|
3425
3524
|
}
|
|
3426
3525
|
} else if (entry.isDirectory()) {
|
|
3427
|
-
copyDir(srcPath, destPath, resolvedRoot);
|
|
3526
|
+
copyDir(srcPath, destPath, resolvedRoot, opts);
|
|
3428
3527
|
} else {
|
|
3429
3528
|
fs.copyFileSync(srcPath, destPath);
|
|
3430
3529
|
}
|
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
} from "./agent-sidebar-state.js";
|
|
89
89
|
import { trackEvent } from "./analytics.js";
|
|
90
90
|
import { agentNativePath, appPath } from "./api-path.js";
|
|
91
|
+
import { readClientAppState } from "./application-state.js";
|
|
91
92
|
import { assistantUiRecoverableRenderErrorKind } from "./assistant-ui-recovery.js";
|
|
92
93
|
import type { AssistantChatProps } from "./AssistantChat.js";
|
|
93
94
|
import { shouldParentFrameOwnAgentPanel } from "./builder-frame.js";
|
|
@@ -2356,13 +2357,7 @@ function URLSync({ browserTabId }: { browserTabId?: string }) {
|
|
|
2356
2357
|
queryKey: setUrlQueryKey,
|
|
2357
2358
|
queryFn: async () => {
|
|
2358
2359
|
const read = async (key: string) => {
|
|
2359
|
-
const
|
|
2360
|
-
agentNativePath(`/_agent-native/application-state/${key}`),
|
|
2361
|
-
);
|
|
2362
|
-
if (!res.ok || res.status === 204) return null;
|
|
2363
|
-
const text = await res.text();
|
|
2364
|
-
if (!text) return null;
|
|
2365
|
-
const data = JSON.parse(text);
|
|
2360
|
+
const data = await readClientAppState<Record<string, unknown>>(key);
|
|
2366
2361
|
return data ? { key, command: data } : null;
|
|
2367
2362
|
};
|
|
2368
2363
|
try {
|
|
@@ -3421,7 +3421,11 @@ const AssistantChatInner = forwardRef<
|
|
|
3421
3421
|
}
|
|
3422
3422
|
window.dispatchEvent(
|
|
3423
3423
|
new CustomEvent("agentNative.chatRunning", {
|
|
3424
|
-
detail: {
|
|
3424
|
+
detail: {
|
|
3425
|
+
isRunning: false,
|
|
3426
|
+
tabId: tabId || threadId,
|
|
3427
|
+
reason: "failed",
|
|
3428
|
+
},
|
|
3425
3429
|
}),
|
|
3426
3430
|
);
|
|
3427
3431
|
return;
|
|
@@ -4510,7 +4514,11 @@ const AssistantChatInner = forwardRef<
|
|
|
4510
4514
|
if (typeof window !== "undefined") {
|
|
4511
4515
|
window.dispatchEvent(
|
|
4512
4516
|
new CustomEvent("agentNative.chatRunning", {
|
|
4513
|
-
detail: {
|
|
4517
|
+
detail: {
|
|
4518
|
+
isRunning: false,
|
|
4519
|
+
tabId: tabId || threadId,
|
|
4520
|
+
reason: "stopped",
|
|
4521
|
+
},
|
|
4514
4522
|
}),
|
|
4515
4523
|
);
|
|
4516
4524
|
}
|
|
@@ -2610,6 +2610,9 @@ export function MultiTabAssistantChat({
|
|
|
2610
2610
|
hasInFlightWork={() =>
|
|
2611
2611
|
chatRefs.current.get(tabId)?.hasInFlightWork() ?? false
|
|
2612
2612
|
}
|
|
2613
|
+
isAwaitingResponse={() =>
|
|
2614
|
+
chatRefs.current.get(tabId)?.isRunning() ?? false
|
|
2615
|
+
}
|
|
2613
2616
|
onRetry={() => {
|
|
2614
2617
|
const handle = chatRefs.current.get(tabId);
|
|
2615
2618
|
handle?.sendRecoveryMessage(
|
|
@@ -50,6 +50,16 @@ export interface RunStuckBannerProps {
|
|
|
50
50
|
* render; omit to keep the unconditional Retry/Cancel behavior.
|
|
51
51
|
*/
|
|
52
52
|
hasInFlightWork?: () => boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Returns true when this chat is actually waiting on a reply - typically
|
|
55
|
+
* backed by `chatHandle.isRunning()`. "Stuck" is a claim about THIS client,
|
|
56
|
+
* but the run row is the server's; a turn that finished normally can leave a
|
|
57
|
+
* row in `running` (terminal marking is best-effort, and the stale-run reaper
|
|
58
|
+
* runs later). Without this gate an idle, completed chat shows the warning
|
|
59
|
+
* and auto-retry re-prompts a thread the user considers done. Checked fresh
|
|
60
|
+
* on every render; omit to keep the unconditional behavior.
|
|
61
|
+
*/
|
|
62
|
+
isAwaitingResponse?: () => boolean;
|
|
53
63
|
/**
|
|
54
64
|
* Called whenever the stuck state transitions. Useful for surfacing
|
|
55
65
|
* observability events (Sentry, PostHog) at the call site.
|
|
@@ -170,6 +180,7 @@ export function RunStuckBanner({
|
|
|
170
180
|
autoRetry = false,
|
|
171
181
|
autoRetryOwnerId,
|
|
172
182
|
hasInFlightWork,
|
|
183
|
+
isAwaitingResponse,
|
|
173
184
|
className,
|
|
174
185
|
}: RunStuckBannerProps) {
|
|
175
186
|
const state = useRunStuckDetection({
|
|
@@ -180,6 +191,11 @@ export function RunStuckBanner({
|
|
|
180
191
|
});
|
|
181
192
|
const abortRun = useAbortRun(apiUrl);
|
|
182
193
|
const [busy, setBusy] = useState<BusyState>({ type: "none" });
|
|
194
|
+
// "An automatic attempt was made for this run" is a different fact from
|
|
195
|
+
// "the controls are locked while a request is in flight". Sharing one flag
|
|
196
|
+
// for both would force the buttons to stay disabled just to keep the
|
|
197
|
+
// message on screen.
|
|
198
|
+
const [autoRetriedRunId, setAutoRetriedRunId] = useState<string | null>(null);
|
|
183
199
|
const autoRetriedRunIdsRef = useRef<Set<string>>(new Set());
|
|
184
200
|
const generatedOwnerIdRef = useRef<string | null>(null);
|
|
185
201
|
if (!generatedOwnerIdRef.current) {
|
|
@@ -204,6 +220,7 @@ export function RunStuckBanner({
|
|
|
204
220
|
// either signal says so.
|
|
205
221
|
const inFlightWork =
|
|
206
222
|
state.hasInFlightWork === true || (hasInFlightWork?.() ?? false);
|
|
223
|
+
const awaitingResponse = isAwaitingResponse?.() ?? true;
|
|
207
224
|
// Server-continued runs are recovered by the SERVER (chained continuation
|
|
208
225
|
// chunks + lost-handoff sweep); an automatic client abort would kill a live
|
|
209
226
|
// server-chained run. Auto-retry is therefore disabled unconditionally for
|
|
@@ -261,6 +278,9 @@ export function RunStuckBanner({
|
|
|
261
278
|
// A live tool/A2A call in flight — never auto-abort real work (see
|
|
262
279
|
// `inFlightWork` comment above).
|
|
263
280
|
inFlightWork ||
|
|
281
|
+
// Nothing is waiting on this run here; re-prompting would restart a
|
|
282
|
+
// thread the user already considers finished.
|
|
283
|
+
!awaitingResponse ||
|
|
264
284
|
!state.isStuck ||
|
|
265
285
|
!state.runId ||
|
|
266
286
|
busy.type !== "none" ||
|
|
@@ -274,21 +294,22 @@ export function RunStuckBanner({
|
|
|
274
294
|
autoRetriedRunIdsRef.current.add(runId);
|
|
275
295
|
if (!claimed) return;
|
|
276
296
|
setBusy({ type: "retry", runId });
|
|
297
|
+
setAutoRetriedRunId(runId);
|
|
277
298
|
trackEvent("agent_chat_stuck_auto_retry", {
|
|
278
299
|
runId,
|
|
279
300
|
threadId: threadId ?? null,
|
|
280
301
|
stuckSinceMs: state.stuckSinceMs ?? null,
|
|
281
302
|
});
|
|
282
303
|
void abortRun(runId, "auto_stuck_retry").then((aborted) => {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
304
|
+
// Release the controls on both outcomes. If the abort does not move
|
|
305
|
+
// the row off `running`, polling may never observe a new run id. The
|
|
306
|
+
// automatic-attempt set still prevents re-entry.
|
|
287
307
|
setBusy((current) =>
|
|
288
308
|
current.type !== "none" && current.runId === runId
|
|
289
309
|
? { type: "none" }
|
|
290
310
|
: current,
|
|
291
311
|
);
|
|
312
|
+
if (aborted) onRetry?.(aborted);
|
|
292
313
|
});
|
|
293
314
|
});
|
|
294
315
|
}, [
|
|
@@ -304,6 +325,7 @@ export function RunStuckBanner({
|
|
|
304
325
|
state.runId,
|
|
305
326
|
state.stuckSinceMs,
|
|
306
327
|
threadId,
|
|
328
|
+
awaitingResponse,
|
|
307
329
|
]);
|
|
308
330
|
|
|
309
331
|
// A stale progress timestamp is not actionable while the server is still
|
|
@@ -314,7 +336,8 @@ export function RunStuckBanner({
|
|
|
314
336
|
!state.isStuck ||
|
|
315
337
|
!state.runId ||
|
|
316
338
|
backgroundWorkerStillAlive ||
|
|
317
|
-
inFlightWork
|
|
339
|
+
inFlightWork ||
|
|
340
|
+
!awaitingResponse
|
|
318
341
|
) {
|
|
319
342
|
return null;
|
|
320
343
|
}
|
|
@@ -380,7 +403,7 @@ export function RunStuckBanner({
|
|
|
380
403
|
No progress
|
|
381
404
|
{stuckSeconds != null ? ` for ${stuckSeconds}s` : ""}. The agent may
|
|
382
405
|
have hit a server timeout or lost its connection.
|
|
383
|
-
{autoRetry &&
|
|
406
|
+
{autoRetry && autoRetriedRunId === state.runId
|
|
384
407
|
? " Retrying automatically now."
|
|
385
408
|
: ""}
|
|
386
409
|
</span>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { ReasoningEffort } from "../shared/reasoning-effort.js";
|
|
11
11
|
import { agentNativePath } from "./api-path.js";
|
|
12
|
+
import { readClientAppState } from "./application-state.js";
|
|
12
13
|
import {
|
|
13
14
|
isInBuilderFrame,
|
|
14
15
|
isTrustedBuilderMessage,
|
|
@@ -598,15 +599,9 @@ export async function refreshAgentChatContext(): Promise<AgentChatContextState>
|
|
|
598
599
|
return agentChatContextState;
|
|
599
600
|
}
|
|
600
601
|
try {
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
),
|
|
605
|
-
);
|
|
606
|
-
if (!res.ok || res.status === 204) return agentChatContextState;
|
|
607
|
-
const text = await res.text();
|
|
608
|
-
if (!text) return agentChatContextState;
|
|
609
|
-
const state = normalizeAgentChatContextState(JSON.parse(text));
|
|
602
|
+
const raw = await readClientAppState(AGENT_CHAT_CONTEXT_STATE_KEY);
|
|
603
|
+
if (raw === null) return agentChatContextState;
|
|
604
|
+
const state = normalizeAgentChatContextState(raw);
|
|
610
605
|
if (!state) return agentChatContextState;
|
|
611
606
|
return publishAgentChatContextItems(state.items, {
|
|
612
607
|
persist: false,
|
|
@@ -93,19 +93,117 @@ function jsonBody(value: unknown): string {
|
|
|
93
93
|
return body;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Result of a batched read. `values` holds only the keys the server has a row
|
|
98
|
+
* for; every other requested key is in `missing`. A key stored with a `null`
|
|
99
|
+
* value lands in `values` with `null` and NOT in `missing` — that is how
|
|
100
|
+
* "never written" stays distinguishable from "written as null/empty".
|
|
101
|
+
*/
|
|
102
|
+
export interface ClientAppStateBatch {
|
|
103
|
+
values: Record<string, unknown>;
|
|
104
|
+
missing: string[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Server caps a batch at 100 keys; stay under it when splitting. */
|
|
108
|
+
const MAX_BATCH_KEYS = 100;
|
|
109
|
+
|
|
110
|
+
export async function readClientAppStateMany(
|
|
111
|
+
keys: readonly string[],
|
|
112
|
+
options: ClientAppStateReadOptions = {},
|
|
113
|
+
): Promise<ClientAppStateBatch> {
|
|
114
|
+
const unique = [...new Set(keys)];
|
|
115
|
+
for (const key of unique) appStateUrl(key); // validates the key shape
|
|
116
|
+
if (unique.length === 0) return { values: {}, missing: [] };
|
|
117
|
+
|
|
118
|
+
const merged: ClientAppStateBatch = { values: {}, missing: [] };
|
|
119
|
+
for (let i = 0; i < unique.length; i += MAX_BATCH_KEYS) {
|
|
120
|
+
const chunk = unique.slice(i, i + MAX_BATCH_KEYS);
|
|
121
|
+
const url = `${agentNativePath("/_agent-native/application-state")}?keys=${chunk
|
|
122
|
+
.map(encodeURIComponent)
|
|
123
|
+
.join(",")}`;
|
|
124
|
+
const response = await fetch(url, {
|
|
125
|
+
method: "GET",
|
|
126
|
+
cache: "no-store",
|
|
127
|
+
signal: options.signal,
|
|
128
|
+
});
|
|
129
|
+
const batch = await parseAppStateResponse<ClientAppStateBatch | null>(
|
|
130
|
+
response,
|
|
131
|
+
`Read application state [${chunk.join(", ")}]`,
|
|
132
|
+
);
|
|
133
|
+
if (!batch || typeof batch !== "object" || !batch.values) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Read application state [${chunk.join(", ")}] returned an unexpected payload.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
Object.assign(merged.values, batch.values);
|
|
139
|
+
merged.missing.push(...(batch.missing ?? []));
|
|
140
|
+
}
|
|
141
|
+
return merged;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Reads issued in the same tick are coalesced into one batched request. Every
|
|
145
|
+
// mounted composer, question card and suggestion hook reads its own key on
|
|
146
|
+
// mount, which used to be one HTTP request (and one full identity resolution)
|
|
147
|
+
// each — ~55 on an analytics dashboard load.
|
|
148
|
+
let pendingBatch:
|
|
149
|
+
| { keys: Set<string>; promise: Promise<ClientAppStateBatch> }
|
|
150
|
+
| undefined;
|
|
151
|
+
|
|
152
|
+
function scheduleBatchedRead(key: string): Promise<ClientAppStateBatch> {
|
|
153
|
+
if (pendingBatch) {
|
|
154
|
+
pendingBatch.keys.add(key);
|
|
155
|
+
return pendingBatch.promise;
|
|
156
|
+
}
|
|
157
|
+
const keys = new Set([key]);
|
|
158
|
+
const promise = new Promise<ClientAppStateBatch>((resolve, reject) => {
|
|
159
|
+
setTimeout(() => {
|
|
160
|
+
pendingBatch = undefined;
|
|
161
|
+
readClientAppStateMany([...keys]).then(resolve, reject);
|
|
162
|
+
}, 0);
|
|
163
|
+
});
|
|
164
|
+
pendingBatch = { keys, promise };
|
|
165
|
+
return promise;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function awaitWithAbort<T>(
|
|
169
|
+
promise: Promise<T>,
|
|
170
|
+
signal?: AbortSignal,
|
|
171
|
+
): Promise<T> {
|
|
172
|
+
if (!signal) return promise;
|
|
173
|
+
const abortReason = () =>
|
|
174
|
+
signal.reason ?? new Error("The operation was aborted");
|
|
175
|
+
if (signal.aborted) return Promise.reject(abortReason());
|
|
176
|
+
return new Promise<T>((resolve, reject) => {
|
|
177
|
+
const onAbort = () => {
|
|
178
|
+
cleanup();
|
|
179
|
+
reject(abortReason());
|
|
180
|
+
};
|
|
181
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
182
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
183
|
+
promise.then(
|
|
184
|
+
(value) => {
|
|
185
|
+
cleanup();
|
|
186
|
+
if (signal.aborted) reject(abortReason());
|
|
187
|
+
else resolve(value);
|
|
188
|
+
},
|
|
189
|
+
(error) => {
|
|
190
|
+
cleanup();
|
|
191
|
+
reject(error);
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
96
197
|
export async function readClientAppState<T = unknown>(
|
|
97
198
|
key: string,
|
|
98
199
|
options: ClientAppStateReadOptions = {},
|
|
99
200
|
): Promise<T | null> {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return
|
|
106
|
-
response,
|
|
107
|
-
`Read application state "${key}"`,
|
|
108
|
-
);
|
|
201
|
+
appStateUrl(key); // validates the key shape before it joins a batch
|
|
202
|
+
if (options.signal?.aborted) {
|
|
203
|
+
throw options.signal.reason ?? new Error("Aborted");
|
|
204
|
+
}
|
|
205
|
+
const batch = await awaitWithAbort(scheduleBatchedRead(key), options.signal);
|
|
206
|
+
return (batch.values[key] ?? null) as T | null;
|
|
109
207
|
}
|
|
110
208
|
|
|
111
209
|
export async function writeClientAppState<T = unknown>(
|
|
@@ -129,9 +227,14 @@ export async function deleteClientAppState(
|
|
|
129
227
|
): Promise<void> {
|
|
130
228
|
const response = await fetch(appStateUrl(key), {
|
|
131
229
|
method: "DELETE",
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
230
|
+
// DELETE carries no JSON body, so this custom header is the only
|
|
231
|
+
// same-origin marker the CSRF check can see from an embedded frame.
|
|
232
|
+
headers: {
|
|
233
|
+
"X-Agent-Native-CSRF": "1",
|
|
234
|
+
...(options.requestSource
|
|
235
|
+
? { "X-Request-Source": options.requestSource }
|
|
236
|
+
: {}),
|
|
237
|
+
},
|
|
135
238
|
keepalive: options.keepalive,
|
|
136
239
|
signal: options.signal,
|
|
137
240
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { readClientAppState } from "./application-state.js";
|
|
4
4
|
import { useChangeVersions } from "./use-change-version.js";
|
|
5
5
|
import type { ChatThreadScope } from "./use-chat-threads.js";
|
|
6
6
|
|
|
@@ -89,17 +89,9 @@ function appStateKeyForBrowserTab(key: string, browserTabId?: string): string {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
async function readAppState(key: string): Promise<unknown> {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
);
|
|
95
|
-
if (!res.ok || res.status === 204) return null;
|
|
96
|
-
const text = await res.text();
|
|
97
|
-
if (!text) return null;
|
|
98
|
-
try {
|
|
99
|
-
return JSON.parse(text);
|
|
100
|
-
} catch {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
92
|
+
// Reads issued in the same tick coalesce into one batched request, so the
|
|
93
|
+
// four keys below cost one round trip rather than four.
|
|
94
|
+
return readClientAppState(key).catch(() => null);
|
|
103
95
|
}
|
|
104
96
|
|
|
105
97
|
async function readScopedAppState(
|
|
@@ -115,7 +115,11 @@ export interface FrameOriginMessage {
|
|
|
115
115
|
|
|
116
116
|
export interface ChatRunningMessage {
|
|
117
117
|
type: "agentNative.chatRunning";
|
|
118
|
-
detail: {
|
|
118
|
+
detail: {
|
|
119
|
+
isRunning: boolean;
|
|
120
|
+
tabId?: string;
|
|
121
|
+
reason?: "stopped" | "failed";
|
|
122
|
+
};
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
export interface UserInfoMessage {
|
|
@@ -12,8 +12,11 @@ import {
|
|
|
12
12
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
13
13
|
|
|
14
14
|
import { sendToAgentChat } from "./agent-chat.js";
|
|
15
|
-
import {
|
|
16
|
-
|
|
15
|
+
import {
|
|
16
|
+
deleteClientAppState,
|
|
17
|
+
readClientAppState,
|
|
18
|
+
setClientAppState,
|
|
19
|
+
} from "./application-state.js";
|
|
17
20
|
import { useChangeVersions } from "./use-change-version.js";
|
|
18
21
|
import { cn } from "./utils.js";
|
|
19
22
|
|
|
@@ -998,10 +1001,6 @@ export function useGuidedQuestionFlow({
|
|
|
998
1001
|
() => normalizeBrowserTabId(browserTabId),
|
|
999
1002
|
[browserTabId],
|
|
1000
1003
|
);
|
|
1001
|
-
const endpointFor = useCallback(
|
|
1002
|
-
(key: string) => agentNativePath(`/_agent-native/application-state/${key}`),
|
|
1003
|
-
[],
|
|
1004
|
-
);
|
|
1005
1004
|
const scopedKey = normalizedBrowserTabId
|
|
1006
1005
|
? `${stateKey}:${normalizedBrowserTabId}`
|
|
1007
1006
|
: stateKey;
|
|
@@ -1036,17 +1035,11 @@ export function useGuidedQuestionFlow({
|
|
|
1036
1035
|
enabled,
|
|
1037
1036
|
queryFn: async () => {
|
|
1038
1037
|
const read = async (key: string) => {
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
if (
|
|
1043
|
-
|
|
1044
|
-
const parsed = JSON.parse(text);
|
|
1045
|
-
if (Array.isArray(parsed?.questions) && parsed.questions.length > 0) {
|
|
1046
|
-
return parsed as GuidedQuestionPayload;
|
|
1047
|
-
}
|
|
1048
|
-
} catch {
|
|
1049
|
-
return null;
|
|
1038
|
+
const parsed = await readClientAppState<GuidedQuestionPayload>(
|
|
1039
|
+
key,
|
|
1040
|
+
).catch(() => null);
|
|
1041
|
+
if (Array.isArray(parsed?.questions) && parsed.questions.length > 0) {
|
|
1042
|
+
return parsed;
|
|
1050
1043
|
}
|
|
1051
1044
|
return null;
|
|
1052
1045
|
};
|
|
@@ -1086,16 +1079,12 @@ export function useGuidedQuestionFlow({
|
|
|
1086
1079
|
const clear = useCallback(() => {
|
|
1087
1080
|
setPayload(null);
|
|
1088
1081
|
queryClient.setQueryData(resolvedQueryKey, null);
|
|
1089
|
-
const del = (key: string) =>
|
|
1090
|
-
fetch(endpointFor(key), {
|
|
1091
|
-
method: "DELETE",
|
|
1092
|
-
headers: { "X-Agent-Native-CSRF": "1" },
|
|
1093
|
-
}).catch(() => {});
|
|
1082
|
+
const del = (key: string) => deleteClientAppState(key).catch(() => {});
|
|
1094
1083
|
// Clear whichever key actually held the payload (scoped or bare) so the
|
|
1095
1084
|
// card doesn't reappear on the next poll.
|
|
1096
1085
|
del(scopedKey);
|
|
1097
1086
|
if (scopedKey !== stateKey) del(stateKey);
|
|
1098
|
-
}, [
|
|
1087
|
+
}, [queryClient, resolvedQueryKey, scopedKey, stateKey]);
|
|
1099
1088
|
|
|
1100
1089
|
const handleSubmit = useCallback(
|
|
1101
1090
|
(answers: GuidedQuestionAnswers) => {
|