@agent-native/core 0.84.1 → 0.84.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +7 -0
  3. package/corpus/core/docs/design/durable-agent-runs.md +34 -35
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/agent/engine/builder-engine.ts +81 -9
  6. package/corpus/core/src/agent/run-manager.ts +8 -7
  7. package/corpus/core/src/cli/design-connect.ts +175 -0
  8. package/corpus/core/src/cli/skills.ts +5 -7
  9. package/corpus/core/src/client/AgentPanel.tsx +8 -0
  10. package/corpus/core/src/client/MultiTabAssistantChat.tsx +4 -1
  11. package/corpus/core/src/client/agent-chat-adapter.ts +45 -15
  12. package/corpus/core/src/client/sse-event-processor.ts +31 -4
  13. package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +15 -17
  14. package/corpus/templates/design/actions/apply-motion-edit.ts +50 -18
  15. package/corpus/templates/design/actions/get-motion-timeline.ts +4 -14
  16. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +156 -12
  17. package/corpus/templates/design/app/components/design/MotionDock.tsx +2 -3
  18. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +217 -75
  19. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +39 -39
  20. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +558 -37
  21. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +75 -3
  22. package/corpus/templates/design/app/components/design/types.ts +13 -0
  23. package/corpus/templates/design/app/components/layout/Layout.tsx +1 -0
  24. package/corpus/templates/design/app/i18n/zh-TW.ts +17 -0
  25. package/corpus/templates/design/app/i18n-data.ts +245 -0
  26. package/corpus/templates/design/app/pages/DesignEditor.tsx +762 -153
  27. package/corpus/templates/design/changelog/2026-06-30-copying-or-dragging-screen-elements-onto-the-infinite-canvas.md +6 -0
  28. package/corpus/templates/design/changelog/2026-06-30-design-questions-now-use-tighter-editor-typography-and-contr.md +6 -0
  29. package/corpus/templates/design/changelog/2026-06-30-element-drags-can-be-cancelled-with-escape-before-they-commi.md +6 -0
  30. package/corpus/templates/design/changelog/2026-06-30-pending-visual-style-edits-now-warn-before-you-leave-the-edi.md +6 -0
  31. package/corpus/templates/design/changelog/2026-06-30-visual-style-drags-stay-live-while-pending-edits-can-be-appl.md +6 -0
  32. package/corpus/templates/design/changelog/2026-07-01-design-chat-no-longer-shows-a-redundant-context-tab-above-the-composer.md +6 -0
  33. package/corpus/templates/design/changelog/2026-07-01-motion-track-creation-no-longer-fails-in-local-editors.md +6 -0
  34. package/corpus/templates/design/server/plugins/db.ts +10 -0
  35. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  36. package/dist/agent/engine/builder-engine.js +61 -10
  37. package/dist/agent/engine/builder-engine.js.map +1 -1
  38. package/dist/agent/run-manager.d.ts +8 -7
  39. package/dist/agent/run-manager.d.ts.map +1 -1
  40. package/dist/agent/run-manager.js +8 -7
  41. package/dist/agent/run-manager.js.map +1 -1
  42. package/dist/cli/design-connect.d.ts +2 -0
  43. package/dist/cli/design-connect.d.ts.map +1 -1
  44. package/dist/cli/design-connect.js +140 -0
  45. package/dist/cli/design-connect.js.map +1 -1
  46. package/dist/cli/skills.d.ts.map +1 -1
  47. package/dist/cli/skills.js +5 -7
  48. package/dist/cli/skills.js.map +1 -1
  49. package/dist/client/AgentPanel.d.ts +5 -1
  50. package/dist/client/AgentPanel.d.ts.map +1 -1
  51. package/dist/client/AgentPanel.js +4 -4
  52. package/dist/client/AgentPanel.js.map +1 -1
  53. package/dist/client/MultiTabAssistantChat.d.ts +3 -1
  54. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  55. package/dist/client/MultiTabAssistantChat.js +2 -2
  56. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  57. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  58. package/dist/client/agent-chat-adapter.js +37 -14
  59. package/dist/client/agent-chat-adapter.js.map +1 -1
  60. package/dist/client/sse-event-processor.d.ts +14 -1
  61. package/dist/client/sse-event-processor.d.ts.map +1 -1
  62. package/dist/client/sse-event-processor.js +21 -5
  63. package/dist/client/sse-event-processor.js.map +1 -1
  64. package/dist/collab/awareness.d.ts +2 -2
  65. package/dist/collab/awareness.d.ts.map +1 -1
  66. package/dist/collab/routes.d.ts +2 -2
  67. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  68. package/dist/notifications/routes.d.ts +3 -3
  69. package/dist/observability/routes.d.ts +2 -2
  70. package/dist/resources/handlers.d.ts +2 -2
  71. package/dist/server/transcribe-voice.d.ts +1 -1
  72. package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +15 -17
  73. package/docs/design/durable-agent-runs.md +34 -35
  74. package/package.json +1 -1
  75. package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +15 -17
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2043
31
- - template files: 4895
31
+ - template files: 4902
@@ -1,5 +1,12 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 3ff4f55: Extend Builder gateway timeouts for local and background agent-chat runs, and surface recoverable gateway timeout failures instead of silently retrying activity-only tool preparation loops.
8
+ - 3ff4f55: Allow scoped chat surfaces to hide the composer scope badge while keeping thread context attached.
9
+
3
10
  ## 0.84.1
4
11
 
5
12
  ### Patch Changes
@@ -83,10 +83,11 @@ terminate with an untruthful "done" state.
83
83
  not buy more time — it just converts a graceful hand-off into a hard kill. The
84
84
  walls, in order:
85
85
 
86
- 1. **Builder model gateway hard cap — ~45s.**
87
- `MAX_BUILDER_GATEWAY_TIMEOUT_MS = 45_000` in
88
- `packages/core/src/agent/engine/builder-engine.ts`. A single model call is
89
- killed at the gateway after 45s. **Not raisable** by the framework.
86
+ 1. **Builder model gateway foreground cap — ~45s.**
87
+ Hosted foreground calls keep a 45s cap in
88
+ `packages/core/src/agent/engine/builder-engine.ts`. Local/non-hosted and
89
+ proven background-function calls use the longer background-style cap because
90
+ they are not constrained by the synchronous function wall.
90
91
  2. **Serverless function kill — ~60–65s.** The hosting function is terminated
91
92
  shortly after; the heartbeat then reaps the run row as `stale_run`.
92
93
 
@@ -366,8 +367,8 @@ The whole `auto_continue` / 40s soft-timeout dance exists to stay under the
366
367
  **serverless function wall** (~60–65s synchronous on Netlify), not under any
367
368
  model limit. Evidence:
368
369
 
369
- - `builder-engine.ts:62` `MAX_LOCAL_BUILDER_GATEWAY_TIMEOUT_MS = 180_000`
370
- (3 min) is allowed locally; the run loop has no inherent reason to stop at 40s.
370
+ - `builder-engine.ts` allows the long background gateway cap in local/non-hosted
371
+ runs; the run loop has no inherent reason to stop at 40s.
371
372
  - `run-manager.ts:58,68` `DEFAULT_HOSTED_RUN_SOFT_TIMEOUT_MS` /
372
373
  `HOSTED_SOFT_TIMEOUT_CEILING_MS = 40_000` are pinned just under the function
373
374
  wall, and `templates/brain/netlify.toml` sets `[functions."*"] timeout = 75`.
@@ -379,10 +380,9 @@ function there is no ~60s wall, so:
379
380
 
380
381
  - The agent loop can run for minutes in a single invocation with **few or no
381
382
  `auto_continue` continuations**.
382
- - The per-model-call gateway cap (`MAX_BUILDER_GATEWAY_TIMEOUT_MS = 45_000`,
383
- `builder-engine.ts:60`) still applies per call — see
384
- [Per-model-call gateway cap](#per-model-call-gateway-cap) but the _run_ is no
385
- longer chopped into 40s chunks.
383
+ - The foreground hosted per-model-call gateway cap still applies per call — see
384
+ [Per-model-call gateway cap](#per-model-call-gateway-cap)but the _run_ is
385
+ no longer chopped into 40s chunks.
386
386
 
387
387
  This is exactly the Layer 1 worker with a concrete long-lived host: Netlify is
388
388
  the durable worker, reached through the existing self-dispatch primitive — the
@@ -527,29 +527,27 @@ Minimal client change, because the reconnect machinery already exists:
527
527
 
528
528
  ## Per-model-call gateway cap
529
529
 
530
- Even in a 15-min function, a _single_ model call is still capped by the Builder
531
- gateway: `MAX_BUILDER_GATEWAY_TIMEOUT_MS = 45_000` (`builder-engine.ts:60`),
532
- resolved by `getBuilderGatewayTimeoutMs()` (`:770`) /
533
- `resolveMaxBuilderGatewayTimeoutMs()` (`:758`). Locally the cap is already
534
- `180_000` (`:62`) when the gateway base URL is localhost.
530
+ Builder gateway calls now use a runtime-aware cap. Hosted foreground calls keep
531
+ the 45s cap so the synchronous function can still checkpoint before its hard
532
+ wall. Local/non-hosted runs use a longer local cap, and proven Netlify
533
+ background-function runs may use a background cap below the 15-minute function
534
+ wall. The run-manager's 13-minute background soft-timeout should fire before
535
+ the gateway cap in normal durable background operation.
535
536
 
536
537
  Options, in scope-order:
537
538
 
538
- 1. **In scope / safe now:** keep the 45s per-call cap. A 15-min function makes
539
- _many_ 45s calls; the cap bounds one call, not the run. This alone delivers
540
- the goal (long multi-step runs finish) with **zero gateway changes**.
541
- 2. **Flag, out of scope until confirmed:** raise the per-call cap toward 180s in
542
- the background context. `getBuilderGatewayTimeoutMs()` already honors
543
- `AGENT_NATIVE_BUILDER_GATEWAY_TIMEOUT_MS`, but it is hard-clamped to
544
- `resolveMaxBuilderGatewayTimeoutMs()` = 45s for non-localhost gateways. Raising
545
- it requires the **hosted Builder gateway to actually allow >45s upstream** —
546
- unverified, and not the framework's call. Until confirmed, do not raise it.
547
- 3. **Escape hatch:** a direct Anthropic engine (no Builder gateway) has no 45s
548
- cap, so a single very long call could use it. Out of scope for this change;
549
- note it as the lever if a single model call truly needs >45s.
550
-
551
- **Recommendation:** ship with option 1. The win is removing the _run-level_
552
- ceiling; the per-call cap is orthogonal and rarely the binding constraint.
539
+ 1. **Foreground hosted:** keep the 45s cap. The synchronous function wall is the
540
+ constraint, and a larger cap would turn graceful checkpointing into a hard
541
+ platform kill.
542
+ 2. **Local/non-hosted:** allow longer calls by default. Local development should
543
+ not inherit a serverless wall it does not have.
544
+ 3. **Durable background:** allow longer calls only when the runtime proves it is
545
+ inside the emitted background function. Keep the cap below 15 minutes and
546
+ above the 13-minute run soft-timeout so background checkpointing still owns
547
+ logical-turn continuation.
548
+
549
+ **Recommendation:** preserve this split. Do not raise the foreground cap; use
550
+ durable background for long-running tool-input generation.
553
551
 
554
552
  ## Idempotency / dedup
555
553
 
@@ -626,7 +624,8 @@ only): foreground handler inserts the run row, dispatches, returns the SSE
626
624
  stream from `subscribeToRun`. The background `_process-run` claims the run and
627
625
  calls the same `startRun` + `runAgentLoop` the inline path uses, with
628
626
  `softTimeoutMs ≈ 13min` (new `backgroundFunction` option in
629
- `resolveRunSoftTimeoutMs`). Per-call gateway cap stays at 45s (option 1).
627
+ `resolveRunSoftTimeoutMs`). Builder gateway calls use the background cap only
628
+ when the worker proves it is inside the emitted background function.
630
629
  **Done when:** a long multi-step turn that thrashes today completes in one
631
630
  background invocation with zero `auto_continue`, events streaming to the client
632
631
  via the SQL-poll path, terminal `done` persisted.
@@ -645,10 +644,10 @@ dispatches, and background→background `auto_continue` chaining for the rare
645
644
  > 13-min turn (mirror `agent-teams.ts:1886`). Internal checkpointing (Option A)
646
645
  > for monotonic progress across any continuation.
647
646
 
648
- **Slice 4 (optional, separate decision) raise the per-call gateway cap.**
649
- Only after confirming the hosted Builder gateway accepts >45s upstream; wire
650
- `AGENT_NATIVE_BUILDER_GATEWAY_TIMEOUT_MS` through with a background-context max.
651
- Out of scope until that confirmation exists.
647
+ **Slice 4 — foreground remains capped.**
648
+ Do not raise the hosted foreground per-call gateway cap. Any future tuning
649
+ should stay in the local/background timeout regime unless the synchronous
650
+ platform wall changes.
652
651
 
653
652
  ## Open risks / unknowns
654
653
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.1",
3
+ "version": "0.84.2",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -24,6 +24,7 @@ import {
24
24
  normalizeReasoningEffortForModel,
25
25
  type ReasoningEffort,
26
26
  } from "../../shared/reasoning-effort.js";
27
+ import { isInBackgroundFunctionRuntime } from "../durable-background.js";
27
28
  import { BUILDER_MODEL_CONFIG } from "../model-config.js";
28
29
  import { getBuilderGatewayRequestHeaders } from "./builder-gateway-headers.js";
29
30
  import {
@@ -53,13 +54,20 @@ export const BUILDER_CAPABILITIES: EngineCapabilities = {
53
54
 
54
55
  export const BUILDER_SUPPORTED_MODELS = BUILDER_MODEL_CONFIG.supportedModels;
55
56
 
56
- // Keep the gateway timeout below the hosted run soft timeout so the agent loop
57
- // can append a continuation and persist terminal state before serverless hosts
58
- // (Netlify synchronous Functions are 60s) hard-kill the invocation.
57
+ // Keep the foreground hosted gateway timeout below the synchronous serverless
58
+ // wall so the agent loop can append a continuation and persist terminal state
59
+ // before the host hard-kills the invocation.
59
60
  const DEFAULT_BUILDER_GATEWAY_TIMEOUT_MS = 45_000;
60
- const MAX_BUILDER_GATEWAY_TIMEOUT_MS = 45_000;
61
- /** Local ai-services has no serverless wall; allow longer streams in dev. */
62
- const MAX_LOCAL_BUILDER_GATEWAY_TIMEOUT_MS = 180_000;
61
+ const MAX_HOSTED_FOREGROUND_BUILDER_GATEWAY_TIMEOUT_MS = 45_000;
62
+ /**
63
+ * Netlify background functions have a 15-minute wall. Keep the Builder gateway
64
+ * ceiling below that, but above the run-manager's 13-minute soft-timeout so
65
+ * durable background runs checkpoint before this timer wins.
66
+ */
67
+ const MAX_BACKGROUND_BUILDER_GATEWAY_TIMEOUT_MS = 14 * 60_000;
68
+ /** Local and non-hosted runtimes have no synchronous serverless wall. */
69
+ const MAX_LOCAL_BUILDER_GATEWAY_TIMEOUT_MS =
70
+ MAX_BACKGROUND_BUILDER_GATEWAY_TIMEOUT_MS;
63
71
  const BUILDER_GATEWAY_NETWORK_ERROR_CODE = "builder_gateway_network_error";
64
72
 
65
73
  export const BUILDER_DEFAULT_MODEL = BUILDER_MODEL_CONFIG.defaultModel;
@@ -315,6 +323,7 @@ class BuilderEngine implements AgentEngine {
315
323
  }
316
324
 
317
325
  yield* parseJsonlStream(reader, opts.model, {
326
+ abortSignal: gatewayAbort.signal,
318
327
  didGatewayTimeout: gatewayAbort.didTimeout,
319
328
  gatewayTimeoutMs,
320
329
  gatewayUrl,
@@ -430,11 +439,12 @@ async function* emitHttpError(response: Response): AsyncIterable<EngineEvent> {
430
439
  // a complete line and the client must still process it.
431
440
  async function* readJsonlLines(
432
441
  reader: ReadableStreamDefaultReader<Uint8Array>,
442
+ abortSignal?: AbortSignal,
433
443
  ): AsyncIterable<string> {
434
444
  const decoder = new TextDecoder();
435
445
  let buffer = "";
436
446
  while (true) {
437
- const { done, value } = await reader.read();
447
+ const { done, value } = await readStreamChunk(reader, abortSignal);
438
448
  if (done) break;
439
449
  buffer += decoder.decode(value, { stream: true });
440
450
  let newlineIdx = buffer.indexOf("\n");
@@ -457,6 +467,7 @@ async function* parseJsonlStream(
457
467
  reader: ReadableStreamDefaultReader<Uint8Array>,
458
468
  model: string,
459
469
  captureContext: {
470
+ abortSignal?: AbortSignal;
460
471
  didGatewayTimeout?: () => boolean;
461
472
  gatewayTimeoutMs?: number;
462
473
  gatewayUrl?: URL;
@@ -487,7 +498,10 @@ async function* parseJsonlStream(
487
498
  };
488
499
 
489
500
  try {
490
- for await (const line of readJsonlLines(reader)) {
501
+ for await (const line of readJsonlLines(
502
+ reader,
503
+ captureContext.abortSignal,
504
+ )) {
491
505
  let event: any;
492
506
  try {
493
507
  event = JSON.parse(line);
@@ -722,6 +736,32 @@ async function* parseJsonlStream(
722
736
  }
723
737
  }
724
738
 
739
+ function readStreamChunk(
740
+ reader: ReadableStreamDefaultReader<Uint8Array>,
741
+ abortSignal?: AbortSignal,
742
+ ): Promise<ReadableStreamReadResult<Uint8Array>> {
743
+ if (!abortSignal) return reader.read();
744
+ if (abortSignal.aborted) {
745
+ return Promise.reject(abortSignal.reason ?? new Error("Stream aborted"));
746
+ }
747
+ return new Promise((resolve, reject) => {
748
+ const onAbort = () => {
749
+ reject(abortSignal.reason ?? new Error("Stream aborted"));
750
+ };
751
+ abortSignal.addEventListener("abort", onAbort, { once: true });
752
+ reader.read().then(
753
+ (value) => {
754
+ abortSignal.removeEventListener("abort", onAbort);
755
+ resolve(value);
756
+ },
757
+ (err) => {
758
+ abortSignal.removeEventListener("abort", onAbort);
759
+ reject(err);
760
+ },
761
+ );
762
+ });
763
+ }
764
+
725
765
  function normalizeGatewayErrorText(raw: string, status: number): string {
726
766
  const text = raw.trim();
727
767
  const looksHtml = /<html[\s>]|<body[\s>]|<head[\s>]/i.test(text);
@@ -761,6 +801,14 @@ export function createBuilderEngine(
761
801
  }
762
802
 
763
803
  function resolveMaxBuilderGatewayTimeoutMs(): number {
804
+ if (isInBackgroundFunctionRuntime()) {
805
+ return MAX_BACKGROUND_BUILDER_GATEWAY_TIMEOUT_MS;
806
+ }
807
+
808
+ if (!isHostedBuilderRuntime()) {
809
+ return MAX_LOCAL_BUILDER_GATEWAY_TIMEOUT_MS;
810
+ }
811
+
764
812
  try {
765
813
  const base = getBuilderGatewayBaseUrl();
766
814
  if (/^https?:\/\/(localhost|127\.0\.0\.1)([:/]|$)/i.test(base)) {
@@ -769,7 +817,31 @@ function resolveMaxBuilderGatewayTimeoutMs(): number {
769
817
  } catch {
770
818
  // ignore malformed override
771
819
  }
772
- return MAX_BUILDER_GATEWAY_TIMEOUT_MS;
820
+ return MAX_HOSTED_FOREGROUND_BUILDER_GATEWAY_TIMEOUT_MS;
821
+ }
822
+
823
+ function isHostedBuilderRuntime(): boolean {
824
+ if (
825
+ process.env.NETLIFY &&
826
+ process.env.NETLIFY !== "false" &&
827
+ process.env.NETLIFY_LOCAL !== "true"
828
+ ) {
829
+ return true;
830
+ }
831
+ if (
832
+ process.env.AWS_LAMBDA_FUNCTION_NAME &&
833
+ process.env.NETLIFY_LOCAL !== "true"
834
+ ) {
835
+ return true;
836
+ }
837
+ return Boolean(
838
+ process.env.CF_PAGES ||
839
+ process.env.VERCEL ||
840
+ process.env.VERCEL_ENV ||
841
+ process.env.RENDER ||
842
+ process.env.FLY_APP_NAME ||
843
+ process.env.K_SERVICE,
844
+ );
773
845
  }
774
846
 
775
847
  function getBuilderGatewayTimeoutMs(): number {
@@ -46,8 +46,8 @@ const CLEANUP_DELAY_MS = 5 * 60 * 1000;
46
46
  *
47
47
  * This MUST fire before the two upstream hard walls that otherwise kill a run
48
48
  * mid-turn with no chance to hand off:
49
- * 1. The Builder model gateway hard-caps a single model call at 45s
50
- * (builder-engine.ts MAX_BUILDER_GATEWAY_TIMEOUT_MS) not raisable.
49
+ * 1. The Builder model gateway keeps a 45s cap only for hosted foreground
50
+ * runs; local and proven background-function runs use longer caps.
51
51
  * 2. Serverless functions are hard-killed around 60-65s (the heartbeat then
52
52
  * reaps the row as a stale_run).
53
53
  * Production data showed every cutoff landing in the 44-70s window with ZERO
@@ -61,11 +61,12 @@ export const DEFAULT_HOSTED_RUN_SOFT_TIMEOUT_MS = 40_000;
61
61
 
62
62
  /**
63
63
  * Hard ceiling for the hosted soft timeout. On a hosted runtime the
64
- * auto_continue soft timeout can never usefully exceed this — the gateway
65
- * (45s) / function (~60s) walls kill the run first, so a larger configured or
66
- * env value just guarantees the cutoff is a hard error instead of a graceful
67
- * hand-off. Any resolved value above this is clamped down when hosted. Local
68
- * dev (non-hosted) is left alone so long-running local turns aren't chunked.
64
+ * foreground auto_continue soft timeout can never usefully exceed this — the
65
+ * synchronous function (~60s) wall kills the run first, so a larger configured
66
+ * or env value just guarantees the cutoff is a hard error instead of a graceful
67
+ * hand-off. Any resolved value above this is clamped down for hosted foreground
68
+ * runs. Local dev (non-hosted) is left alone so long-running local turns aren't
69
+ * chunked.
69
70
  *
70
71
  * IMPORTANT: this clamp is for the INTERACTIVE / foreground path and must NOT
71
72
  * be raised. The foreground POST still rides a synchronous serverless function
@@ -1,3 +1,4 @@
1
+ import { spawn } from "node:child_process";
1
2
  import crypto from "node:crypto";
2
3
  import fsSync from "node:fs";
3
4
  import fs from "node:fs/promises";
@@ -44,6 +45,7 @@ export interface DesignConnectArgs {
44
45
  json: boolean;
45
46
  once: boolean;
46
47
  dryRun: boolean;
48
+ daemon: boolean;
47
49
  help: boolean;
48
50
  }
49
51
 
@@ -115,6 +117,7 @@ export function parseDesignConnectArgs(argv: string[]): DesignConnectArgs {
115
117
  json: false,
116
118
  once: false,
117
119
  dryRun: false,
120
+ daemon: false,
118
121
  help: false,
119
122
  };
120
123
 
@@ -155,6 +158,8 @@ export function parseDesignConnectArgs(argv: string[]): DesignConnectArgs {
155
158
  } else if (arg === "--dry-run") {
156
159
  parsed.dryRun = true;
157
160
  parsed.once = true;
161
+ } else if (arg === "--daemon") {
162
+ parsed.daemon = true;
158
163
  } else if (arg.startsWith("-")) {
159
164
  throw new Error(`Unknown option: ${arg}`);
160
165
  } else {
@@ -165,6 +170,11 @@ export function parseDesignConnectArgs(argv: string[]): DesignConnectArgs {
165
170
  if (!Number.isInteger(parsed.port) || parsed.port <= 0) {
166
171
  throw new Error("--port must be a positive integer");
167
172
  }
173
+ if (parsed.daemon && (parsed.json || parsed.once || parsed.dryRun)) {
174
+ throw new Error(
175
+ "--daemon cannot be combined with --json, --once, or --dry-run",
176
+ );
177
+ }
168
178
  parsed.root = path.resolve(parsed.root);
169
179
  parsed.url = parsed.url ? normalizeHttpUrl(parsed.url) : undefined;
170
180
  return parsed;
@@ -298,6 +308,86 @@ async function probeDevServer(url: string): Promise<boolean> {
298
308
  }
299
309
  }
300
310
 
311
+ async function waitForBridgeHealth(
312
+ bridgeUrl: string,
313
+ timeoutMs = 5_000,
314
+ ): Promise<boolean> {
315
+ const healthUrl = new URL("/health", bridgeUrl).toString();
316
+ const deadline = Date.now() + timeoutMs;
317
+ while (Date.now() < deadline) {
318
+ const controller = new AbortController();
319
+ const timeout = setTimeout(() => controller.abort(), 400);
320
+ try {
321
+ const response = await fetch(healthUrl, {
322
+ method: "GET",
323
+ signal: controller.signal,
324
+ });
325
+ if (response.ok) return true;
326
+ } catch {
327
+ // Keep polling until the detached process finishes binding the port.
328
+ } finally {
329
+ clearTimeout(timeout);
330
+ }
331
+ await new Promise((resolve) => setTimeout(resolve, 150));
332
+ }
333
+ return false;
334
+ }
335
+
336
+ function isDesignConnectManifest(
337
+ value: unknown,
338
+ ): value is DesignConnectManifest {
339
+ if (!value || typeof value !== "object") return false;
340
+ const manifest = value as Partial<DesignConnectManifest>;
341
+ return (
342
+ manifest.version === 1 &&
343
+ manifest.source === "agent-native-design-connect" &&
344
+ manifest.sourceType === "localhost" &&
345
+ manifest.localOnly === true &&
346
+ typeof manifest.devServerUrl === "string" &&
347
+ typeof manifest.bridgeUrl === "string" &&
348
+ typeof manifest.rootPath === "string"
349
+ );
350
+ }
351
+
352
+ async function fetchRunningBridgeManifest(
353
+ bridgeUrl: string,
354
+ ): Promise<DesignConnectManifest | null> {
355
+ const manifestUrl = new URL("/manifest.json", bridgeUrl).toString();
356
+ const controller = new AbortController();
357
+ const timeout = setTimeout(() => controller.abort(), 800);
358
+ try {
359
+ const response = await fetch(manifestUrl, {
360
+ method: "GET",
361
+ signal: controller.signal,
362
+ });
363
+ if (!response.ok) return null;
364
+ const body = (await response.json()) as unknown;
365
+ return isDesignConnectManifest(body) ? body : null;
366
+ } catch {
367
+ return null;
368
+ } finally {
369
+ clearTimeout(timeout);
370
+ }
371
+ }
372
+
373
+ export function designConnectManifestsTargetSameApp(
374
+ running: Pick<DesignConnectManifest, "devServerUrl" | "rootPath">,
375
+ requested: Pick<DesignConnectManifest, "devServerUrl" | "rootPath">,
376
+ ): boolean {
377
+ let runningUrl = running.devServerUrl;
378
+ let requestedUrl = requested.devServerUrl;
379
+ try {
380
+ runningUrl = normalizeHttpUrl(runningUrl);
381
+ requestedUrl = normalizeHttpUrl(requestedUrl);
382
+ } catch {
383
+ // Fall through to direct string comparison for malformed legacy manifests.
384
+ }
385
+ return (
386
+ runningUrl === requestedUrl &&
387
+ path.resolve(running.rootPath) === path.resolve(requested.rootPath)
388
+ );
389
+ }
390
+
301
391
  async function resolveDevServerUrl(url?: string): Promise<string> {
302
392
  if (url) return normalizeHttpUrl(url);
303
393
  for (const candidate of DEFAULT_DEV_SERVER_CANDIDATES) {
@@ -942,6 +1032,7 @@ Options:
942
1032
  --route-manifest <path> Non-destructive route manifest output path
943
1033
  --app-url <url> Deployed design app URL for self-registration
944
1034
  (also reads AGENT_NATIVE_URL / DESIGN_APP_URL env)
1035
+ --daemon Start the bridge detached, wait for /health, then exit
945
1036
  --json Print the manifest JSON and exit
946
1037
  --once Prepare/scaffold the manifest and exit
947
1038
  --dry-run Print what would be exposed without writing files
@@ -966,6 +1057,87 @@ Element provenance (resolveNodeToFile):
966
1057
  Cross-origin localhost iframes cannot be read regardless of attributes (CSP).`);
967
1058
  }
968
1059
 
1060
+ function removeDaemonFlag(argv: string[]): string[] {
1061
+ return argv.filter((arg) => arg !== "--daemon");
1062
+ }
1063
+
1064
+ function resolveCurrentCliInvocation(argv: string[]): {
1065
+ command: string;
1066
+ args: string[];
1067
+ } {
1068
+ const suffixLength = argv.length + 1; // leading "design" command + runDesign argv
1069
+ const prefixEnd = Math.max(1, process.argv.length - suffixLength);
1070
+ const cliPrefix = process.argv.slice(1, prefixEnd);
1071
+ const entry = cliPrefix[0] ?? process.argv[1];
1072
+ if (!entry) {
1073
+ throw new Error("Could not resolve current CLI entrypoint for --daemon");
1074
+ }
1075
+ if (entry.endsWith(".ts") || entry.endsWith(".tsx")) {
1076
+ return {
1077
+ command: "tsx",
1078
+ args: [...cliPrefix, "design", ...removeDaemonFlag(argv)],
1079
+ };
1080
+ }
1081
+ return {
1082
+ command: process.execPath,
1083
+ args: [...cliPrefix, "design", ...removeDaemonFlag(argv)],
1084
+ };
1085
+ }
1086
+
1087
+ async function startDetachedDesignBridge(
1088
+ argv: string[],
1089
+ manifest: DesignConnectManifest,
1090
+ ): Promise<number> {
1091
+ if (await waitForBridgeHealth(manifest.bridgeUrl, 800)) {
1092
+ const runningManifest = await fetchRunningBridgeManifest(
1093
+ manifest.bridgeUrl,
1094
+ );
1095
+ if (
1096
+ runningManifest &&
1097
+ designConnectManifestsTargetSameApp(runningManifest, manifest)
1098
+ ) {
1099
+ console.error(
1100
+ `Design localhost bridge already running at ${manifest.bridgeUrl}`,
1101
+ );
1102
+ console.log(JSON.stringify(runningManifest, null, 2));
1103
+ return 0;
1104
+ }
1105
+
1106
+ console.error(
1107
+ [
1108
+ `Design localhost bridge already running at ${manifest.bridgeUrl} for a different app.`,
1109
+ runningManifest
1110
+ ? `Running app: ${runningManifest.devServerUrl} (${runningManifest.rootPath})`
1111
+ : "Running bridge did not expose a compatible manifest.",
1112
+ `Requested app: ${manifest.devServerUrl} (${manifest.rootPath})`,
1113
+ "Stop the existing bridge or choose a different --port.",
1114
+ ].join("\n"),
1115
+ );
1116
+ return 1;
1117
+ }
1118
+
1119
+ const invocation = resolveCurrentCliInvocation(argv);
1120
+ const child = spawn(invocation.command, invocation.args, {
1121
+ cwd: process.cwd(),
1122
+ detached: true,
1123
+ env: process.env,
1124
+ stdio: "ignore",
1125
+ shell: process.platform === "win32",
1126
+ });
1127
+ child.unref();
1128
+
1129
+ if (await waitForBridgeHealth(manifest.bridgeUrl)) {
1130
+ console.error(`Design localhost bridge running at ${manifest.bridgeUrl}`);
1131
+ console.log(JSON.stringify(manifest, null, 2));
1132
+ return 0;
1133
+ }
1134
+
1135
+ console.error(
1136
+ `Timed out waiting for detached Design bridge at ${manifest.bridgeUrl}`,
1137
+ );
1138
+ return 1;
1139
+ }
1140
+
969
1141
  export async function runDesign(argv: string[]) {
970
1142
  const subcommand = argv[0];
971
1143
  if (subcommand !== "connect") {
@@ -988,6 +1160,9 @@ export async function runDesign(argv: string[]) {
988
1160
  }
989
1161
 
990
1162
  const manifest = await prepareDesignConnectManifest(parsed);
1163
+ if (parsed.daemon) {
1164
+ return startDetachedDesignBridge(argv, manifest);
1165
+ }
991
1166
  if (parsed.json || parsed.once || parsed.dryRun) {
992
1167
  console.log(JSON.stringify(manifest, null, 2));
993
1168
  return 0;
@@ -464,18 +464,16 @@ iframe-backed screens on the infinite canvas.
464
464
  From the target app repo, make sure its dev server is running, then run:
465
465
 
466
466
  \`\`\`bash
467
- npx @agent-native/core@latest design connect --url http://localhost:5173 --root .
467
+ npx @agent-native/core@latest design connect --url http://localhost:5173 --root . --daemon
468
468
  \`\`\`
469
469
 
470
- Use the app's real port. The command starts a local bridge on
471
- \`http://127.0.0.1:7331\` by default and exposes \`/manifest.json\`,
472
- \`/routes.json\`, and \`/health\`.
470
+ Use the app's real port. The command starts a detached local bridge on
471
+ \`http://127.0.0.1:7331\` by default, waits for \`/health\`, prints the
472
+ manifest JSON, and keeps the bridge alive after the agent command exits.
473
473
 
474
- For one-shot agent setup, ask for JSON and keep the long-running bridge open in
475
- a second terminal if the user needs live updates:
474
+ For a manual health/manifest check:
476
475
 
477
476
  \`\`\`bash
478
- npx @agent-native/core@latest design connect --url http://localhost:5173 --root .
479
477
  curl http://127.0.0.1:7331/manifest.json
480
478
  \`\`\`
481
479
 
@@ -548,6 +548,8 @@ export interface AgentPanelProps extends Omit<
548
548
  * see the `Layout` files for each template.
549
549
  */
550
550
  scope?: import("./use-chat-threads.js").ChatThreadScope | null;
551
+ /** Show the compact scope chip above the composer. Default: true. */
552
+ showScopeBadge?: MultiTabAssistantChatProps["showScopeBadge"];
551
553
  /** Stable browser tab id used for tab-scoped app-state context. */
552
554
  browserTabId?: string;
553
555
  /** Keep chat thread selection in URL state. */
@@ -673,6 +675,7 @@ function AgentPanelInner({
673
675
  storageKey,
674
676
  restoreActiveThread = true,
675
677
  scope,
678
+ showScopeBadge,
676
679
  browserTabId,
677
680
  threadUrlSync,
678
681
  chatNotice,
@@ -1710,6 +1713,7 @@ function AgentPanelInner({
1710
1713
  storageKey={storageKey}
1711
1714
  restoreActiveThread={restoreActiveThread}
1712
1715
  scope={scope}
1716
+ showScopeBadge={showScopeBadge}
1713
1717
  browserTabId={browserTabId}
1714
1718
  threadUrlSync={threadUrlSync}
1715
1719
  />
@@ -2393,6 +2397,8 @@ export interface AgentSidebarProps {
2393
2397
  * Templates compute this from the active route (see template layouts).
2394
2398
  */
2395
2399
  scope?: import("./use-chat-threads.js").ChatThreadScope | null;
2400
+ /** Show the compact scope chip above the composer. Default: true. */
2401
+ showScopeBadge?: MultiTabAssistantChatProps["showScopeBadge"];
2396
2402
  /** Stable browser tab id used for tab-scoped app-state context. */
2397
2403
  browserTabId?: string;
2398
2404
  /** Keep chat thread selection in URL state. */
@@ -2422,6 +2428,7 @@ export function AgentSidebar({
2422
2428
  openOnChatRunning = false,
2423
2429
  onFullscreenRequest,
2424
2430
  scope,
2431
+ showScopeBadge,
2425
2432
  browserTabId,
2426
2433
  threadUrlSync,
2427
2434
  }: AgentSidebarProps) {
@@ -2955,6 +2962,7 @@ export function AgentSidebar({
2955
2962
  }
2956
2963
  storageKey={storageKey}
2957
2964
  scope={scope}
2965
+ showScopeBadge={showScopeBadge}
2958
2966
  browserTabId={browserTabId}
2959
2967
  threadUrlSync={threadUrlSync}
2960
2968
  />
@@ -929,6 +929,8 @@ export type MultiTabAssistantChatProps = Omit<
929
929
  * the composer.
930
930
  */
931
931
  scope?: ChatThreadScope | null;
932
+ /** Show the compact scope chip above the composer. Default: true. */
933
+ showScopeBadge?: boolean;
932
934
  };
933
935
 
934
936
  export function MultiTabAssistantChat({
@@ -942,6 +944,7 @@ export function MultiTabAssistantChat({
942
944
  browserTabId,
943
945
  threadUrlSync = false,
944
946
  scope = null,
947
+ showScopeBadge = true,
945
948
  ...props
946
949
  }: MultiTabAssistantChatProps) {
947
950
  const {
@@ -2625,7 +2628,7 @@ export function MultiTabAssistantChat({
2625
2628
  ? props.dynamicSuggestions
2626
2629
  : false;
2627
2630
  const scopeComposerSlot =
2628
- tabId === activeThreadId && !contentHidden ? (
2631
+ showScopeBadge && tabId === activeThreadId && !contentHidden ? (
2629
2632
  tabScope && activeThreadId ? (
2630
2633
  <ScopeBadge
2631
2634
  scope={tabScope}