@agent-native/core 0.168.13 → 0.169.1

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 (96) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/bigquery-table-info.ts +3 -2
  3. package/corpus/templates/analytics/actions/search-bigquery-schema.ts +130 -4
  4. package/corpus/templates/analytics/agent-native.config.ts +1 -0
  5. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +36 -23
  6. package/corpus/templates/analytics/app/lib/dashboard-visibility.ts +41 -0
  7. package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +34 -9
  8. package/corpus/templates/analytics/changelog/2026-08-21-dashboard-filters-keep-other-users-dashboards-out-of-mine.md +6 -0
  9. package/corpus/templates/analytics/server/lib/real-data-actions.ts +14 -3
  10. package/corpus/templates/analytics/server/plugins/agent-chat.ts +33 -3
  11. package/corpus/templates/brain/server/lib/brain.ts +27 -2
  12. package/corpus/templates/clips/actions/stop-meeting-recording.ts +87 -6
  13. package/corpus/templates/clips/app/components/library/search-bar.tsx +1 -1
  14. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +3 -4
  15. package/corpus/templates/design/app/components/design/EditPanel.tsx +15 -47
  16. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -3
  17. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +0 -7
  18. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +2 -4
  19. package/corpus/templates/design/app/i18n-data.ts +10 -10
  20. package/corpus/templates/design/app/pages/DesignEditor.tsx +22 -22
  21. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +39 -8
  22. package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
  23. package/corpus/templates/design/app/pages/Index.tsx +1 -1
  24. package/corpus/templates/design/app/pages/design-editor/preview-navigation.ts +25 -0
  25. package/corpus/templates/mail/app/hooks/use-emails.ts +13 -3
  26. package/dist/agent/engine/credential-errors.d.ts +1 -1
  27. package/dist/agent/engine/credential-errors.js +2 -2
  28. package/dist/agent/engine/first-event-timeout.d.ts +8 -0
  29. package/dist/agent/engine/first-event-timeout.js +8 -0
  30. package/dist/agent/production-agent.d.ts +0 -30
  31. package/dist/agent/production-agent.js +17 -38
  32. package/dist/agent/run-loop-with-resume.d.ts +38 -25
  33. package/dist/agent/run-loop-with-resume.js +140 -55
  34. package/dist/agent/run-manager.d.ts +83 -68
  35. package/dist/agent/run-manager.js +280 -94
  36. package/dist/agent/run-store.d.ts +31 -0
  37. package/dist/agent/run-store.js +42 -12
  38. package/dist/app-config/agent.d.ts +2 -0
  39. package/dist/app-config/agent.js +33 -0
  40. package/dist/app-config/run-lifecycle-invariants.d.ts +248 -0
  41. package/dist/app-config/run-lifecycle-invariants.js +342 -0
  42. package/dist/app-config/schema.d.ts +2 -0
  43. package/dist/app-config/store.js +9 -1
  44. package/dist/client/AssistantChat.js +47 -15
  45. package/dist/client/ConnectBuilderCard.d.ts +3 -1
  46. package/dist/client/ConnectBuilderCard.js +6 -3
  47. package/dist/client/agent-chat-adapter.d.ts +0 -2
  48. package/dist/client/agent-chat-adapter.js +7 -23
  49. package/dist/client/chat/message-components.d.ts +7 -0
  50. package/dist/client/chat/message-components.js +1 -1
  51. package/dist/client/chat/run-recovery.js +5 -2
  52. package/dist/client/chat/tool-call-display.js +9 -3
  53. package/dist/client/error-format.js +12 -0
  54. package/dist/client/navigation/index.d.ts +1 -0
  55. package/dist/client/navigation/index.js +1 -0
  56. package/dist/client/route-state.js +6 -0
  57. package/dist/client/workspace-app-navigation.d.ts +10 -0
  58. package/dist/client/workspace-app-navigation.js +38 -0
  59. package/dist/collab/struct-routes.d.ts +1 -1
  60. package/dist/deploy/build.d.ts +13 -0
  61. package/dist/deploy/build.js +51 -0
  62. package/dist/jobs/background-automation-runner.d.ts +25 -0
  63. package/dist/jobs/background-automation-runner.js +104 -21
  64. package/dist/jobs/run-history.d.ts +7 -1
  65. package/dist/jobs/run-history.js +57 -14
  66. package/dist/localization/core-messages/ar-SA.js +1 -1
  67. package/dist/localization/core-messages/de-DE.js +1 -1
  68. package/dist/localization/core-messages/en-US.d.ts +1 -1
  69. package/dist/localization/core-messages/en-US.js +1 -1
  70. package/dist/localization/core-messages/es-ES.js +1 -1
  71. package/dist/localization/core-messages/fr-FR.js +1 -1
  72. package/dist/localization/core-messages/hi-IN.js +1 -1
  73. package/dist/localization/core-messages/ja-JP.js +1 -1
  74. package/dist/localization/core-messages/ko-KR.js +1 -1
  75. package/dist/localization/core-messages/pt-BR.js +1 -1
  76. package/dist/localization/core-messages/zh-CN.js +1 -1
  77. package/dist/localization/core-messages/zh-TW.js +1 -1
  78. package/dist/localization/core-messages.d.ts +1 -1
  79. package/dist/notifications/routes.d.ts +1 -1
  80. package/dist/observability/routes.d.ts +3 -3
  81. package/dist/observability/traces.d.ts +13 -0
  82. package/dist/observability/traces.js +369 -317
  83. package/dist/progress/routes.d.ts +1 -1
  84. package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
  85. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  86. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  87. package/dist/resources/handlers.d.ts +1 -1
  88. package/dist/scripts/db/check-scoping.js +9 -6
  89. package/dist/secrets/routes.d.ts +6 -6
  90. package/dist/server/agent-chat-plugin.js +2 -4
  91. package/dist/server/builder-browser.d.ts +4 -0
  92. package/dist/server/builder-browser.js +23 -1
  93. package/dist/server/core-routes-plugin.js +13 -1
  94. package/dist/server/transcribe-voice.d.ts +1 -1
  95. package/package.json +3 -3
  96. /package/corpus/templates/dispatch/app/routes/{apps.$appId.tsx → apps.$appId.$.tsx} +0 -0
@@ -31,6 +31,7 @@ import {
31
31
  needsCorpusWorkflowForCoverageSensitiveRequest,
32
32
  needsSourceRecordBodyWorkflowForCoverageSensitiveRequest,
33
33
  registerGroundingActions,
34
+ stripInjectedAnalyticsGuardContext,
34
35
  } from "../lib/real-data-actions";
35
36
 
36
37
  // Which actions return real data-source evidence is a fact each action states
@@ -107,7 +108,10 @@ function hasPartialDashboardBuild(
107
108
  }
108
109
 
109
110
  export const BOUNDED_STRUCTURED_LOOKUP_GUIDANCE =
110
- "BOUNDED STRUCTURED LOOKUP FAST PATH — Treat existing analytics work like an engineer treats existing code: grep before writing. For an ordinary count, aggregate, grouped metric, trend, or record lookup, first call `search-analytics-query-catalog` once with focused metric/entity terms. It searches accessible dashboard names, chart titles/descriptions/saved queries, shipped dashboard patterns, and data-dictionary definitions together. Prefer the strongest approved dictionary or saved-chart match, preserve its source and business logic, adapt only the requested filters and explicit time window, then run one bounded query against that source. A user-named source wins, but still use a matching saved definition when it supplies the source's proven query shape. If there is no useful match, inspect only the most likely source schema or ask one clarification; do not fan out across providers. Do not separately list every dashboard, call data-source status, browse the whole dictionary, load provider catalogs/corpus tools, or query a second source after a strong match. Once the query succeeds, answer immediately with its source, time window, filters, row count, and only necessary caveats. Do not enrich, cross-check, retry, or add breakdowns unless the user requested them, the first query failed, or its result conflicts with the known definition. The words `all`, `total`, or `exact` in a structured aggregate do not by themselves make it a corpus investigation. Never repeat an identical invalid or failed tool call; correct its arguments once or surface the error. This does not waive the real-data requirement: never answer from a guess, stale value, or unverified result. ";
111
+ "BOUNDED STRUCTURED LOOKUP FAST PATH — Treat existing analytics work like an engineer treats existing code: grep before writing. For an ordinary count, aggregate, grouped metric, trend, or record lookup, first call `search-analytics-query-catalog` once with focused metric/entity terms. It searches accessible dashboard names, chart titles/descriptions/saved queries, shipped dashboard patterns, and data-dictionary definitions together. Prefer the strongest approved dictionary or saved-chart match, preserve its source and business logic, adapt only the requested filters and explicit time window, then run one bounded query against that source. A user-named source wins, but still use a matching saved definition when it supplies the source's proven query shape. If there is no useful match, inspect the most likely source schema before asking a clarification about business meaning; do not ask the user to provide internal dataset, table, column, or SQL identifiers. Do not fan out across providers. Do not separately list every dashboard, call data-source status, browse the whole dictionary, load provider catalogs/corpus tools, or query a second source after a strong match. Once the query succeeds, answer immediately with its source, time window, filters, row count, and only necessary caveats. Do not enrich, cross-check, retry, or add breakdowns unless the user requested them, the first query failed, or its result conflicts with the known definition. The words `all`, `total`, or `exact` in a structured aggregate do not by themselves make it a corpus investigation. Never repeat an identical invalid or failed tool call; correct its arguments once or surface the error. This does not waive the real-data requirement: never answer from a guess, stale value, or unverified result. ";
112
+
113
+ export const INTERNAL_PRODUCT_USAGE_GUIDANCE =
114
+ "INTERNAL PRODUCT USAGE / CROSS-SOURCE ROUTING — Requests for product usage, AI credits, credit consumption, allowances, quotas, branch creation, branch creators, or user-level adoption by month are live data requests even when they say pull, export, or prepare a report. For workspace-wide internal Builder.io usage, route to Dispatch with `call-agent` and `list-dispatch-usage-metrics`; use its `monthlyByUser` and `workspaceAppCreationsByUserMonth` result. Do not ask for a user export or BigQuery schema on that path. For a named customer or account such as OCBC, preserve the customer scope and do not substitute workspace metrics. Start with `search-analytics-query-catalog`; if it has no usable definition, call `list-data-dictionary` with focused metric terms and `search-bigquery-schema` with the same terms. `search-bigquery-schema` can search the configured project without a dataset, so do not ask the user to name the dataset, table, columns, or SQL. Use the exact discovered schema in one bounded `bigquery` query, and resolve the named account identity before attributing rows to it. A table that contains limits, plans, or changelog metadata is not proof of actual consumption or creator identity. If the configured actions prove that the source or metric is unavailable, report that exact gap and the inspected coverage instead of asking the user to identify internal tables. ";
111
115
 
112
116
  export const ANALYTICS_ACCOUNT_HEALTH_GUIDANCE =
113
117
  "ACCOUNT HEALTH / CUSTOMER SCOPE GUARD — Use this for a named customer, organization ID, account-health, QBR, renewal, contract-usage, risk, or adoption request. Treat a prompt-supplied org ID as a lookup key, not proof of row ownership: resolve it against canonical account data first, carry the resolved customer name plus organization/root-organization identifiers through every usage query, and stop if results contain a different customer, mixed IDs, or an unresolved identity. Use the account-health skill. Account health is incomplete until verified usage queries cover each requested product or feature dimension separately. Before warehouse SQL, use the catalog/dictionary and schema metadata. Do not run definitions marked deprecated or retired, or use a source whose freshness cannot be verified; use the current approved definition instead. Do not call a current partial-period snapshot a completed period without proving the period/as-of row and freshness. Distinguish contract metrics from similarly named platform metrics, total distinct contracted users from DAU/WAU, and actual usage from contracted capacity. Always surface utilization at or above 100%, report adoption window and coverage, and state gaps instead of inferring them. ";
@@ -123,6 +127,7 @@ export const ANALYTICS_OBSERVABILITY_INCIDENT_GUIDANCE =
123
127
 
124
128
  export const ANALYTICS_CROSS_APP_ROUTING_GUIDANCE =
125
129
  "WORKSPACE APP ROUTING — Analytics is the sibling app for first-party product usage, app/template events, agent-native signups, conversions, and other curated product metrics. When another app delegates one of these questions with `call-agent`, answer it here using the built-in first-party source and query catalog; do not send the user back to another app or ask the caller to invent SQL. " +
130
+ 'INTERNAL USAGE EXCEPTION — Builder.io or AI credit spend, LLM usage by workspace member or month, and workspace app or Builder branch creation history are Dispatch-owned internal metrics. For workspace-wide internal requests, call `call-agent` with agent `dispatch`, action `list-dispatch-usage-metrics`, and the exact read-only input `{ sinceDays, scope: "workspace" }` (add `userEmail` only when the user explicitly narrows the request). The result includes `monthlyByUser` and `workspaceAppCreationsByUserMonth` from the shared `token_usage` and Dispatch audit tables. Do not ask for a user export or BigQuery schema. A request scoped to a named customer or account, such as OCBC, is different: keep that customer scope and use the catalog, dictionary, configured warehouse schema, and one bounded query; do not substitute the current workspace metrics. ' +
126
131
  "WORKSPACE APP ROUTING — Brain is the sibling app that owns company knowledge and indexed Slack context. Brain is not an Analytics extension and will not appear in `list-extensions`. When the user asks about company knowledge, decisions, meeting context, or Slack messages/context such as a named channel or thread, use `call-agent` with agent `brain` and a narrow natural-language question. Use `describe-workspace-apps` only when you need to confirm the sibling capability. Do not use `list-extensions` to find Brain, and do not use `provider-api-request` to call Brain. If Brain reports an access or source error, preserve that exact error; do not infer that the Slack bot is absent from a channel or tell the user to re-invite it. Stay in Analytics for metrics and aggregates over a named provider. ";
127
132
 
128
133
  export const NON_ANALYTICS_REQUEST_GUIDANCE =
@@ -156,6 +161,7 @@ export function analyticsSourceGuidanceOpening(): string {
156
161
  // Measured in production: this ran in under 1% of data threads while the
157
162
  // equivalent instruction sat ~7000 words deep. Threads that did call it used
158
163
  // roughly a third the tool calls. Keep it first and keep it imperative.
164
+ "INTERNAL USAGE OVERRIDE — For workspace-wide Builder.io or AI credit spend, LLM usage by workspace member or month, or workspace app/Builder branch creation history, your FIRST tool call is `call-agent` for Dispatch with action `list-dispatch-usage-metrics`, scope `workspace`, and the requested `sinceDays`. Do not start with a user list, `search-bigquery-schema`, or a request for dataset/table/column names. If the request names a customer or account such as OCBC, preserve that customer scope and use the Analytics catalog/warehouse path instead of substituting workspace metrics. " +
159
165
  "START HERE — For an ordinary metric, cohort, list, count, or trend question, your FIRST tool call is `search-analytics-query-catalog`. For a request to replicate, clone, or adapt an existing dashboard, your FIRST tool call is `search-dashboard-references` instead; inspect the returned dashboard before choosing a source or writing anything. The catalog path is the analytics equivalent of grepping a codebase before writing new code: someone has very likely already built and saved the query you need, and its saved SQL tells you the exact source, table, and column names so you do not have to discover them. Adapt the closest saved query to the requested filters and time window, run it once, and stop. Only fall back to schema discovery or provider catalogs when the catalog search returns nothing usable — and never run more than one schema-discovery pass before querying. " +
160
166
  ANALYTICS_CROSS_APP_ROUTING_GUIDANCE +
161
167
  'ONE BOUNDED CALL — List, filter, count, and cohort questions ("which X, excluding Y") are a single query, not a loop. Express the include filter, the exclude filter, and the aggregation in one SQL statement or one `run-code` script that filters server-side. Never page through a cohort across separate tool calls and never fan out per item to apply a filter; that is what turns a ten-second answer into a twenty-minute one. ' +
@@ -163,6 +169,7 @@ export function analyticsSourceGuidanceOpening(): string {
163
169
  NON_ANALYTICS_REQUEST_GUIDANCE +
164
170
  DASHBOARD_REFERENCE_GUIDANCE +
165
171
  BOUNDED_STRUCTURED_LOOKUP_GUIDANCE +
172
+ INTERNAL_PRODUCT_USAGE_GUIDANCE +
166
173
  ANALYTICS_ACCOUNT_HEALTH_GUIDANCE +
167
174
  BUILT_IN_FIRST_PARTY_SOURCE_GUIDANCE +
168
175
  ANALYTICS_OBSERVABILITY_INCIDENT_GUIDANCE +
@@ -171,9 +178,18 @@ export function analyticsSourceGuidanceOpening(): string {
171
178
  );
172
179
  }
173
180
 
181
+ const SCHEMA_DETAILS_REQUEST_PATTERN =
182
+ /\b(?:could you|can you|would you|please\s+(?:provide|share|send|tell)|provide|share|send(?: me)?|tell me|i need(?: you to)?|what (?:is|are)|which)\b[\s\S]{0,260}\b(?:bigquery\s+)?(?:dataset(?: name)?s?|table(?: name)?s?|column(?: name)?s?|field(?: name)?s?|schema|sql query)\b/i;
183
+
184
+ function looksLikeSchemaDetailsRequest(text: string): boolean {
185
+ return SCHEMA_DETAILS_REQUEST_PATTERN.test(
186
+ stripInjectedAnalyticsGuardContext(String(text ?? "")),
187
+ );
188
+ }
189
+
174
190
  export function analyticsDataDictionaryRoutingContext(): string {
175
191
  return `<data-dictionary-routing>
176
- Data-dictionary definitions are available through \`search-analytics-query-catalog\`, which combines focused dictionary lookup with a search over existing dashboard/chart SQL. Use that combined catalog search as the normal preflight for a bounded metric lookup. Call \`list-data-dictionary\` separately only when the user asks to browse definitions or filter them by department. Treat approved entries as canonical, verify unreviewed human entries when stakes are high, and treat AI-generated unapproved entries as suggestions only. After the catalog identifies one source and query shape, query that source once and stop on success. If no matching definition or chart exists, inspect only the most likely source schema or ask the user instead of fanning out across providers.
192
+ Data-dictionary definitions are available through \`search-analytics-query-catalog\`, which combines focused dictionary lookup with a search over existing dashboard/chart SQL. Use that combined catalog search as the normal preflight for a bounded metric lookup. Call \`list-data-dictionary\` separately when the catalog has no usable match or when the user asks to browse definitions or filter them by department. Treat approved entries as canonical, verify unreviewed human entries when stakes are high, and treat AI-generated unapproved entries as suggestions only. After the catalog identifies one source and query shape, query that source once and stop on success. If no matching definition or chart exists, inspect the most likely source schema before asking a clarification about business meaning. Never ask the user to supply internal dataset, table, column, or SQL identifiers that the configured Analytics actions can discover.
177
193
  </data-dictionary-routing>`;
178
194
  }
179
195
 
@@ -782,8 +798,22 @@ export function realDataFinalGuard(
782
798
  };
783
799
  }
784
800
 
785
- if (dataQueryAttempted) return null;
786
801
  const failedQueryMessage = failedDataQueryAttemptMessage(context.toolResults);
802
+ if (looksLikeSchemaDetailsRequest(context.text)) {
803
+ const failedQueryRecovery = failedQueryMessage
804
+ ? ` ${failedQueryMessage}`
805
+ : "";
806
+ return {
807
+ retryMessage:
808
+ "The draft asks the user to supply internal dataset, table, column, or SQL details. Do not ask the user for warehouse schema identifiers. Use the configured Analytics tools now: call `search-analytics-query-catalog`, then `list-data-dictionary` and `search-bigquery-schema` with focused metric terms when the catalog has no usable definition, and run one authoritative `bigquery` query using the exact discovered references. `search-bigquery-schema` searches the configured project without a dataset. For a named customer, verify identity and distinguish actual consumption from limits or changelog metadata. If the tools prove the source or metric is unavailable, state that exact evidence gap instead of asking the user to name internal tables." +
809
+ failedQueryRecovery,
810
+ fallbackMessage:
811
+ "I couldn't complete that lookup from the configured Analytics sources yet. Please retry and I'll inspect the catalog and warehouse schema directly rather than asking you to provide internal table names.",
812
+ maxRetries: 2,
813
+ expandToolSurface: true,
814
+ };
815
+ }
816
+ if (dataQueryAttempted) return null;
787
817
  // Whether the built-in source is worth trying is decided by tool evidence,
788
818
  // not by how the draft is worded, so it is asked once for every draft shape.
789
819
  // A source that already failed this turn is not an untried source: steering
@@ -643,8 +643,33 @@ export async function ensureManualSource(title = "Manual imports") {
643
643
  }
644
644
 
645
645
  function isUniqueConflict(error: unknown): boolean {
646
- const message = error instanceof Error ? error.message : String(error);
647
- return /unique constraint|duplicate key|unique/i.test(message);
646
+ if (!error || typeof error !== "object") {
647
+ return /unique constraint|duplicate key|duplicate entry|unique/i.test(
648
+ String(error),
649
+ );
650
+ }
651
+
652
+ const candidate = error as {
653
+ code?: unknown;
654
+ message?: unknown;
655
+ cause?: unknown;
656
+ };
657
+ if (
658
+ candidate.code === "23505" ||
659
+ candidate.code === "SQLITE_CONSTRAINT_UNIQUE"
660
+ ) {
661
+ return true;
662
+ }
663
+ if (typeof candidate.message === "string") {
664
+ if (
665
+ /unique constraint|duplicate key|duplicate entry|unique violation|unique/i.test(
666
+ candidate.message,
667
+ )
668
+ ) {
669
+ return true;
670
+ }
671
+ }
672
+ return candidate.cause !== error && isUniqueConflict(candidate.cause);
648
673
  }
649
674
 
650
675
  const UPSTREAM_DELETED_POLICY_VERSION = "upstream-deleted-v1";
@@ -11,10 +11,57 @@
11
11
  import { defineAction } from "@agent-native/core";
12
12
  import { writeAppState } from "@agent-native/core/application-state";
13
13
  import { assertAccess } from "@agent-native/core/sharing";
14
+ import shareResource from "@agent-native/core/sharing/actions/share-resource";
14
15
  import { and, eq } from "drizzle-orm";
15
16
  import { z } from "zod";
16
17
 
17
18
  import { getDb, schema } from "../server/db/index.js";
19
+ import { normalizeOwnerEmail } from "../server/lib/recordings.js";
20
+
21
+ type ShareableResourceType = "meeting" | "recording";
22
+
23
+ async function sharePublicMeetingResourcesWithParticipants(args: {
24
+ meetingId: string;
25
+ recordingId: string | null;
26
+ recordingVisibility: string | null | undefined;
27
+ ownerEmail: string;
28
+ participants: Array<{ email: string }>;
29
+ }) {
30
+ const ownerEmail = normalizeOwnerEmail(args.ownerEmail);
31
+ const participantEmails = [
32
+ ...new Set(
33
+ args.participants
34
+ .map((participant) => normalizeOwnerEmail(participant.email))
35
+ .filter((email) => email && email !== ownerEmail),
36
+ ),
37
+ ];
38
+ if (!participantEmails.length) return;
39
+
40
+ const resources: Array<{
41
+ resourceType: ShareableResourceType;
42
+ resourceId: string;
43
+ }> = [
44
+ { resourceType: "meeting", resourceId: args.meetingId },
45
+ ...(args.recordingId && args.recordingVisibility === "public"
46
+ ? [{ resourceType: "recording" as const, resourceId: args.recordingId }]
47
+ : []),
48
+ ];
49
+
50
+ await Promise.all(
51
+ resources.flatMap(({ resourceType, resourceId }) =>
52
+ participantEmails.map((principalId) =>
53
+ shareResource.run({
54
+ resourceType,
55
+ resourceId,
56
+ principalType: "user",
57
+ principalId,
58
+ role: "viewer",
59
+ notify: false,
60
+ }),
61
+ ),
62
+ ),
63
+ );
64
+ }
18
65
 
19
66
  export default defineAction({
20
67
  description:
@@ -23,7 +70,7 @@ export default defineAction({
23
70
  meetingId: z.string().describe("Meeting id"),
24
71
  }),
25
72
  run: async (args) => {
26
- await assertAccess("meeting", args.meetingId, "editor");
73
+ const access = await assertAccess("meeting", args.meetingId, "editor");
27
74
  const db = getDb();
28
75
  const nowIso = new Date().toISOString();
29
76
 
@@ -39,12 +86,24 @@ export default defineAction({
39
86
  // no way for the UI to distinguish "notes coming" from "nothing was ever
40
87
  // captured". Match finalize-meeting's own empty-transcript handling.
41
88
  let hasTranscript = false;
89
+ let recordingVisibility: string | null | undefined;
42
90
  if (meeting.recordingId) {
43
- const [transcript] = await db
44
- .select({ fullText: schema.recordingTranscripts.fullText })
45
- .from(schema.recordingTranscripts)
46
- .where(eq(schema.recordingTranscripts.recordingId, meeting.recordingId))
47
- .limit(1);
91
+ const [transcriptRows, recordingRows] = await Promise.all([
92
+ db
93
+ .select({ fullText: schema.recordingTranscripts.fullText })
94
+ .from(schema.recordingTranscripts)
95
+ .where(
96
+ eq(schema.recordingTranscripts.recordingId, meeting.recordingId),
97
+ )
98
+ .limit(1),
99
+ db
100
+ .select({ visibility: schema.recordings.visibility })
101
+ .from(schema.recordings)
102
+ .where(eq(schema.recordings.id, meeting.recordingId))
103
+ .limit(1),
104
+ ]);
105
+ const transcript = transcriptRows[0];
106
+ recordingVisibility = recordingRows[0]?.visibility;
48
107
  hasTranscript = Boolean(transcript?.fullText?.trim());
49
108
  }
50
109
 
@@ -74,6 +133,28 @@ export default defineAction({
74
133
  requestedAt: nowIso,
75
134
  });
76
135
  }
136
+
137
+ // Public resources are intentionally link-only and therefore stay out of
138
+ // the signed-in Meetings/Shared lists. Calendar participants are the one
139
+ // known audience we can safely admit without making every public meeting
140
+ // discoverable, so grant them standard viewer access when the call ends.
141
+ if (
142
+ meeting.visibility === "public" &&
143
+ (access.role === "owner" || access.role === "admin")
144
+ ) {
145
+ const participants = await db
146
+ .select({ email: schema.meetingParticipants.email })
147
+ .from(schema.meetingParticipants)
148
+ .where(eq(schema.meetingParticipants.meetingId, args.meetingId));
149
+ await sharePublicMeetingResourcesWithParticipants({
150
+ meetingId: args.meetingId,
151
+ recordingId: meeting.recordingId ?? null,
152
+ recordingVisibility,
153
+ ownerEmail: meeting.ownerEmail,
154
+ participants,
155
+ });
156
+ }
157
+
77
158
  await writeAppState("refresh-signal", { ts: Date.now() });
78
159
 
79
160
  return { meetingId: args.meetingId, recordingId: meeting.recordingId };
@@ -160,7 +160,7 @@ export function SearchBar({ className, side = "right" }: SearchBarProps) {
160
160
  </button>
161
161
  ) : (
162
162
  <span className="absolute end-1.5 top-1/2 -translate-y-1/2 inline-flex items-center gap-0.5 rounded border border-border bg-muted px-1 py-0.5 text-[10px] font-medium text-muted-foreground">
163
- {shortcutLabel("cmd+k")}
163
+ {shortcutLabel("/")}
164
164
  </span>
165
165
  )}
166
166
  </div>
@@ -3954,9 +3954,8 @@ export const editorChromeBridgeScript: string = `"use strict";
3954
3954
  // Cmd from Ctrl the way isPlatformPrimaryModifier does host-side, but
3955
3955
  // forwarding is harmless when the host has no match for the combo.
3956
3956
  "f",
3957
- // Cmd/Ctrl+R rename / Cmd/Ctrl+Shift+R paste-to-replace (onRename /
3958
- // onPasteToReplace) both live under bare primary+r.
3959
- "r",
3957
+ // Cmd/Ctrl+Shift+R paste-to-replace. Bare primary+r stays native
3958
+ // so browser refresh keeps its expected meaning.
3960
3959
  // Cmd/Ctrl+K — open the host command menu even while the iframe has
3961
3960
  // focus. DesignEditor routes this chord to openCommandMenu().
3962
3961
  "k"
@@ -3965,7 +3964,7 @@ export const editorChromeBridgeScript: string = `"use strict";
3965
3964
  // Cmd+H / Cmd+L — common OS "Hide app" / browser "focus address bar"
3966
3965
  // shortcuts the host has no bare-primary binding for — are left
3967
3966
  // alone (see useDesignHotkeys.ts: both require event.shiftKey).
3968
- e.shiftKey && (normalized === "h" || normalized === "l") || // Cmd/Ctrl+Alt+B detach instance / Cmd/Ctrl+Alt+K create component
3967
+ e.shiftKey && (normalized === "h" || normalized === "l") || e.shiftKey && normalized === "r" || // Cmd/Ctrl+Alt+B detach instance / Cmd/Ctrl+Alt+K create component
3969
3968
  // (onDetachInstance / onCreateComponent). Gated on altKey so bare
3970
3969
  // Cmd+B is left alone — the host has no bare-primary binding for it.
3971
3970
  e.altKey && (normalized === "b" || normalized === "k") || // Ctrl+Alt+H / Ctrl+Alt+T — distribute horizontal / tidy up
@@ -1,6 +1,5 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
2
  import type { TweakDefinition } from "@shared/api";
3
- import { alphaToOpacity, parseCssColor } from "@shared/color-utils";
4
3
  import {
5
4
  listInteractionStates,
6
5
  readResolvedStateStyles,
@@ -66,7 +65,6 @@ import {
66
65
  type DocumentColorSourceFile,
67
66
  extractDocumentColorPalette,
68
67
  selectionColorValues,
69
- selectionDisplayHex,
70
68
  } from "./edit-panel/document-colors";
71
69
  import { EffectsProperties } from "./edit-panel/effects-properties";
72
70
  import {
@@ -1536,52 +1534,22 @@ function SelectionColorsProperties({
1536
1534
  {expanded ? (
1537
1535
  <div className="space-y-1.5">
1538
1536
  {colors.map((color, index) => {
1539
- const parsed = parseCssColor(color.value);
1540
- const opacity = parsed ? alphaToOpacity(parsed.a) : 100;
1541
1537
  return (
1542
- <Popover key={`${color.value}-${index}`}>
1543
- <PopoverTrigger asChild>
1544
- <button
1545
- type="button"
1546
- className="flex h-6 w-full items-center gap-1.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 !text-[11px] hover:bg-[var(--design-editor-panel-raised-bg)] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
1547
- aria-label={color.value}
1548
- >
1549
- <span
1550
- className="size-4 shrink-0 rounded-[3px] border border-border/60"
1551
- style={swatchStyle(color.value)}
1552
- />
1553
- <span className="min-w-0 flex-1 truncate text-left uppercase tabular-nums">
1554
- {selectionDisplayHex(color.value)}
1555
- </span>
1556
- <span className="shrink-0 tabular-nums text-muted-foreground">
1557
- {opacity}%
1558
- </span>
1559
- </button>
1560
- </PopoverTrigger>
1561
- <PopoverContent
1562
- side="left"
1563
- align="start"
1564
- sideOffset={8}
1565
- className="w-80 p-0"
1566
- >
1567
- <DesignColorPicker
1568
- value={cssColorOrFallback(color.value, "#000000")}
1569
- // PF12: per-tick drag preview vs. one authoritative
1570
- // commit on gesture-end — same split as ColorInput's
1571
- // setNext (see its PF12 comment above).
1572
- onChange={(value) =>
1573
- onStyleChange(color.property, value, {
1574
- phase: "preview",
1575
- })
1576
- }
1577
- onChangeComplete={(value) =>
1578
- onStyleChange(color.property, value, {
1579
- phase: "commit",
1580
- })
1581
- }
1582
- />
1583
- </PopoverContent>
1584
- </Popover>
1538
+ <DesignColorPicker
1539
+ key={`${color.value}-${index}`}
1540
+ // guard:allow-raw-color - neutral picker fallback for an invalid stored color
1541
+ value={cssColorOrFallback(color.value, "#000000")}
1542
+ className="w-full"
1543
+ // PF12: per-tick drag preview vs. one authoritative commit
1544
+ // on gesture-end — same split as ColorInput's setNext (see
1545
+ // its PF12 comment above).
1546
+ onChange={(value) =>
1547
+ onStyleChange(color.property, value, { phase: "preview" })
1548
+ }
1549
+ onChangeComplete={(value) =>
1550
+ onStyleChange(color.property, value, { phase: "commit" })
1551
+ }
1552
+ />
1585
1553
  );
1586
1554
  })}
1587
1555
  </div>
@@ -5289,9 +5289,8 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
5289
5289
  // Cmd from Ctrl the way isPlatformPrimaryModifier does host-side, but
5290
5290
  // forwarding is harmless when the host has no match for the combo.
5291
5291
  "f",
5292
- // Cmd/Ctrl+R rename / Cmd/Ctrl+Shift+R paste-to-replace (onRename /
5293
- // onPasteToReplace) both live under bare primary+r.
5294
- "r",
5292
+ // Cmd/Ctrl+Shift+R paste-to-replace. Bare primary+r stays native
5293
+ // so browser refresh keeps its expected meaning.
5295
5294
  // Cmd/Ctrl+K — open the host command menu even while the iframe has
5296
5295
  // focus. DesignEditor routes this chord to openCommandMenu().
5297
5296
  "k",
@@ -5306,6 +5305,7 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
5306
5305
  // shortcuts the host has no bare-primary binding for — are left
5307
5306
  // alone (see useDesignHotkeys.ts: both require event.shiftKey).
5308
5307
  (e.shiftKey && (normalized === "h" || normalized === "l")) ||
5308
+ (e.shiftKey && normalized === "r") ||
5309
5309
  // Cmd/Ctrl+Alt+B detach instance / Cmd/Ctrl+Alt+K create component
5310
5310
  // (onDetachInstance / onCreateComponent). Gated on altKey so bare
5311
5311
  // Cmd+B is left alone — the host has no bare-primary binding for it.
@@ -386,13 +386,6 @@ export const DESIGN_SHORTCUTS: readonly DesignShortcutDefinition[] = [
386
386
  labelKey: "designEditor.keyboardShortcuts.commands.delete",
387
387
  handler: "onDelete",
388
388
  }),
389
- shortcut({
390
- id: "rename",
391
- category: "edit",
392
- bindings: ["$mod+r"],
393
- labelKey: "designEditor.keyboardShortcuts.commands.rename",
394
- handler: "onRename",
395
- }),
396
389
  shortcut({
397
390
  id: "find",
398
391
  category: "edit",
@@ -582,13 +582,11 @@ export function handleDesignHotkey(
582
582
  if (primary && key === "d") {
583
583
  return runSharedCanvasCommand() || run(props.onDuplicate);
584
584
  }
585
- // Figma's Shift+Cmd+R "Paste to replace" must be checked BEFORE plain
586
- // Cmd+R (rename) so shift wins; onPasteToReplace absent falls through to
587
- // rename so existing behavior is unaffected until the handler is wired.
585
+ // Keep Shift+Cmd+R available for Figma's "Paste to replace" command. Bare
586
+ // Cmd/Ctrl+R stays native so browser refresh keeps its expected meaning.
588
587
  if (primary && event.shiftKey && key === "r") {
589
588
  return run(props.onPasteToReplace);
590
589
  }
591
- if (primary && key === "r") return run(props.onRename);
592
590
  // Cmd+Shift+H/L (hide/lock the current selection) must take precedence over
593
591
  // the unmodified/shift-only h/l transform and alignment families.
594
592
  if (primary && event.shiftKey && key === "h") {
@@ -119,7 +119,7 @@ const enUS = {
119
119
  color: "Color",
120
120
  align: "Align",
121
121
  lineHeight: "Line Height",
122
- tracking: "Tracking",
122
+ tracking: "Letter spacing",
123
123
  direction: "Direction",
124
124
  justify: "Justify",
125
125
  gap: "Gap",
@@ -1789,7 +1789,7 @@ const designLocaleOverrides = {
1789
1789
  color: "颜色",
1790
1790
  align: "对齐",
1791
1791
  lineHeight: "行高",
1792
- tracking: "追踪",
1792
+ tracking: "字间距",
1793
1793
  direction: "方向",
1794
1794
  justify: "证明合法",
1795
1795
  gap: "差距",
@@ -2212,7 +2212,7 @@ const designLocaleOverrides = {
2212
2212
  color: "Color",
2213
2213
  align: "Alinear",
2214
2214
  lineHeight: "Altura de línea",
2215
- tracking: "Seguimiento",
2215
+ tracking: "Espaciado entre letras",
2216
2216
  direction: "Dirección",
2217
2217
  justify: "Justificar",
2218
2218
  gap: "Brecha",
@@ -2659,7 +2659,7 @@ const designLocaleOverrides = {
2659
2659
  color: "Couleur",
2660
2660
  align: "Aligner",
2661
2661
  lineHeight: "Hauteur de ligne",
2662
- tracking: "Suivi",
2662
+ tracking: "Espacement des lettres",
2663
2663
  direction: "Direction",
2664
2664
  justify: "Justifier",
2665
2665
  gap: "Écart",
@@ -3111,7 +3111,7 @@ const designLocaleOverrides = {
3111
3111
  color: "Farbe",
3112
3112
  align: "Ausrichten",
3113
3113
  lineHeight: "Zeilenhöhe",
3114
- tracking: "Verfolgung",
3114
+ tracking: "Buchstabenabstand",
3115
3115
  direction: "Richtung",
3116
3116
  justify: "Rechtfertigen",
3117
3117
  gap: "Lücke",
@@ -3563,7 +3563,7 @@ const designLocaleOverrides = {
3563
3563
  color: "色",
3564
3564
  align: "整列",
3565
3565
  lineHeight: "線の高さ",
3566
- tracking: "トラッキング",
3566
+ tracking: "文字間隔",
3567
3567
  direction: "方向",
3568
3568
  justify: "正当化する",
3569
3569
  gap: "ギャップ",
@@ -4010,7 +4010,7 @@ const designLocaleOverrides = {
4010
4010
  color: "색상",
4011
4011
  align: "맞추다",
4012
4012
  lineHeight: "라인 높이",
4013
- tracking: "추적",
4013
+ tracking: "자간",
4014
4014
  direction: "방향",
4015
4015
  justify: "신이 옳다고 하다",
4016
4016
  gap: "갭",
@@ -4449,7 +4449,7 @@ const designLocaleOverrides = {
4449
4449
  color: "Cor",
4450
4450
  align: "Alinhar",
4451
4451
  lineHeight: "Altura da linha",
4452
- tracking: "Monitorando",
4452
+ tracking: "Espaçamento entre letras",
4453
4453
  direction: "Direção",
4454
4454
  justify: "Justificar",
4455
4455
  gap: "Brecha",
@@ -4896,7 +4896,7 @@ const designLocaleOverrides = {
4896
4896
  color: "रंग",
4897
4897
  align: "संरेखित",
4898
4898
  lineHeight: "ऊंची लाईन",
4899
- tracking: "ट्रैकिंग",
4899
+ tracking: "अक्षर रिक्ति",
4900
4900
  direction: "दिशा",
4901
4901
  justify: "औचित्य",
4902
4902
  gap: "अंतर",
@@ -5332,7 +5332,7 @@ const designLocaleOverrides = {
5332
5332
  color: "لون",
5333
5333
  align: "محاذاة",
5334
5334
  lineHeight: "ارتفاع الخط",
5335
- tracking: "تتبع",
5335
+ tracking: "تباعد الأحرف",
5336
5336
  direction: "اتجاه",
5337
5337
  justify: "يبرر",
5338
5338
  gap: "فجوة",
@@ -748,6 +748,7 @@ import {
748
748
  PngCaptureError,
749
749
  type PngCaptureScope,
750
750
  } from "./design-editor/png-export-render";
751
+ import { openPreviewUrl } from "./design-editor/preview-navigation";
751
752
  import {
752
753
  computeInteractZoomToFit,
753
754
  DEFAULT_INTERACT_DEVICE_PRESET,
@@ -13524,7 +13525,7 @@ function DesignEditor() {
13524
13525
  setOverviewSelectAllRequest((request) => request + 1);
13525
13526
  }, [activeFile, getFreshActiveContent, overviewScreens]);
13526
13527
 
13527
- // L12: shared by Cmd+R and the canvas context-menu Rename item — the single
13528
+ // Shared by the canvas context-menu Rename item — the single
13528
13529
  // currently-selected layer id eligible for the layers-panel inline rename,
13529
13530
  // or null when zero or more-than-one layers are selected (screen/file rows
13530
13531
  // are excluded; renaming those is a separate flow). `__`-prefixed and file
@@ -13620,14 +13621,9 @@ function DesignEditor() {
13620
13621
  handleDeleteOverviewSelection(selectedLayerIdsState);
13621
13622
  }
13622
13623
  : undefined,
13623
- // L12: Cmd+R (and the context-menu Rename item, both routed through
13624
- // useDesignHotkeys' onRename) previously always renamed the DESIGN
13625
- // TITLE, even while a layer was selected — surprising when the user's
13626
- // focus is clearly on a specific layer. Route to the layer's real inline
13627
- // rename editor (LayersPanel ref's beginRename) when exactly one
13628
- // selectable (non-file-row) layer is selected; only fall back to
13629
- // renaming the design title when nothing (or more than one layer) is
13630
- // selected.
13624
+ // The context-menu Rename item routes to the layer's real inline rename
13625
+ // editor (LayersPanel ref's beginRename) when exactly one selectable
13626
+ // (non-file-row) layer is selected.
13631
13627
  onRename: () => {
13632
13628
  if (!canEditDesign) return;
13633
13629
  const layerId = getSingleSelectedRenamableLayerId();
@@ -16017,19 +16013,24 @@ function DesignEditor() {
16017
16013
 
16018
16014
  // ── Preview, publish waitlist, gradient, interaction states ────────────────
16019
16015
  const handleOpenDesignPreview = useCallback(() => {
16020
- if (activeScreenPreviewUrl) {
16021
- window.open(activeScreenPreviewUrl, "_blank", "noopener,noreferrer");
16022
- return;
16016
+ let previewUrl = activeScreenPreviewUrl;
16017
+ let blobUrl: string | null = null;
16018
+ if (!previewUrl) {
16019
+ if (!activeContent.trim()) return;
16020
+ blobUrl = URL.createObjectURL(
16021
+ new Blob([fullPreviewHtml(activeContent)], { type: "text/html" }),
16022
+ );
16023
+ previewUrl = blobUrl;
16023
16024
  }
16024
16025
 
16025
- const content = activeContent.trim();
16026
- if (!content) return;
16027
-
16028
- const blobUrl = URL.createObjectURL(
16029
- new Blob([fullPreviewHtml(activeContent)], { type: "text/html" }),
16026
+ openPreviewUrl(
16027
+ previewUrl,
16028
+ (url, target) => window.open(url, target),
16029
+ (url) => window.location.assign(url),
16030
16030
  );
16031
- window.open(blobUrl, "_blank", "noopener,noreferrer");
16032
- window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
16031
+ if (blobUrl) {
16032
+ window.setTimeout(() => URL.revokeObjectURL(blobUrl!), 60_000);
16033
+ }
16033
16034
  }, [activeContent, activeScreenPreviewUrl]);
16034
16035
 
16035
16036
  const handleJoinPublishWaitlist = useCallback(async () => {
@@ -19458,9 +19459,8 @@ function DesignEditor() {
19458
19459
  (selectedScreenIds.length > 0 && files.length > 1)),
19459
19460
  )}
19460
19461
  canReorder={canEditDesign && Boolean(selectedElement)}
19461
- // L12: rename is only offered for a single selectable layer
19462
- // target (matching Cmd+R's routing) the design-title rename
19463
- // flow lives elsewhere (the title control), not this menu.
19462
+ // Rename is only offered for a single selectable layer target;
19463
+ // design-title rename lives in the title control, not this menu.
19464
19464
  canRename={
19465
19465
  canEditDesign && Boolean(getSingleSelectedRenamableLayerId())
19466
19466
  }