@agent-native/core 0.131.2 → 0.131.4

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 (100) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +34 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/types.ts +6 -0
  5. package/corpus/core/src/action.ts +13 -0
  6. package/corpus/core/src/client/AgentPanel.tsx +58 -34
  7. package/corpus/core/src/localization/default-messages.ts +6 -0
  8. package/corpus/core/src/server/agent-capabilities.ts +43 -8
  9. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +63 -1
  10. package/corpus/templates/analytics/AGENTS.md +5 -0
  11. package/corpus/templates/analytics/actions/hubspot-deals.ts +1 -0
  12. package/corpus/templates/analytics/actions/hubspot-records.ts +1 -0
  13. package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +2 -0
  14. package/corpus/templates/calendar/actions/get-event.ts +1 -0
  15. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +1 -0
  16. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +7 -3
  17. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +14 -19
  18. package/corpus/templates/calendar/app/hooks/use-events.ts +7 -0
  19. package/corpus/templates/calendar/app/lib/event-form-utils.ts +13 -0
  20. package/corpus/templates/calendar/app/pages/CalendarView.tsx +21 -14
  21. package/corpus/templates/calendar/changelog/2026-07-29-unnamed-events-now-use-a-title-placeholder.md +6 -0
  22. package/corpus/templates/calendar/server/lib/google-calendar.ts +2 -0
  23. package/corpus/templates/calendar/server/lib/ical-fetcher.ts +1 -0
  24. package/corpus/templates/calendar/shared/api.ts +2 -0
  25. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +3 -1
  26. package/corpus/templates/design/actions/get-design.ts +11 -3
  27. package/corpus/templates/design/actions/present-design-variants.ts +62 -5
  28. package/corpus/templates/design/app/components/design/BreakpointBar.tsx +64 -1
  29. package/corpus/templates/design/app/components/design/EditPanel.tsx +199 -25
  30. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +160 -46
  31. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +7 -1
  32. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +59 -12
  33. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +1 -1
  34. package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +34 -29
  35. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +2 -2
  36. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +28 -10
  37. package/corpus/templates/design/app/components/design/inspector/design-icons.tsx +15 -28
  38. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +4 -3
  39. package/corpus/templates/design/app/components/design/multi-screen/chrome-transitions.ts +12 -8
  40. package/corpus/templates/design/app/components/design/multi-screen/culling.ts +34 -9
  41. package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +13 -27
  42. package/corpus/templates/design/app/components/design/multi-screen/types.ts +8 -1
  43. package/corpus/templates/design/app/components/design/multi-screen/zoom-gesture.ts +95 -0
  44. package/corpus/templates/design/app/i18n-data.ts +121 -10
  45. package/corpus/templates/design/app/pages/design-editor/data-operations.ts +45 -0
  46. package/corpus/templates/design/app/pages/design-editor/layout-operations.ts +59 -0
  47. package/corpus/templates/design/app/pages/design-editor/text-edit-utils.ts +115 -40
  48. package/corpus/templates/design/changelog/2026-07-28-generated-design-variants-no-longer-overlap-each-other-on-th.md +6 -0
  49. package/corpus/templates/design/changelog/2026-07-29-auto-layout-direction-icons-show-an-arrow.md +6 -0
  50. package/corpus/templates/design/changelog/2026-07-29-breakpoints-no-longer-overlap-neighbouring-screens.md +6 -0
  51. package/corpus/templates/design/changelog/2026-07-29-canvas-is-more-responsive-when-selecting-and-adding-frames.md +6 -0
  52. package/corpus/templates/design/changelog/2026-07-29-canvas-zoom-is-smoother-and-labels-hold-their-size.md +6 -0
  53. package/corpus/templates/design/changelog/2026-07-29-double-click-a-frame-now-drills-into-its-layers.md +6 -0
  54. package/corpus/templates/design/changelog/2026-07-29-export-preview-shows-the-real-render.md +6 -0
  55. package/corpus/templates/design/changelog/2026-07-29-figma-parity-shortcuts.md +6 -0
  56. package/corpus/templates/design/changelog/2026-07-29-more-breakpoint-width-presets.md +6 -0
  57. package/corpus/templates/design/changelog/2026-07-29-resize-a-selected-frame-to-a-device-preset.md +6 -0
  58. package/corpus/templates/design/changelog/2026-07-29-text-tool-reaches-the-right-surface.md +6 -0
  59. package/corpus/templates/design/shared/canvas-frames.ts +23 -0
  60. package/corpus/templates/design/shared/code-layer.ts +54 -2
  61. package/corpus/templates/design/shared/responsive-frame-layout.ts +55 -0
  62. package/corpus/templates/slides/.agents/skills/analytics-data-for-decks/SKILL.md +48 -0
  63. package/corpus/templates/slides/AGENTS.md +3 -0
  64. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +72 -8
  65. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  66. package/corpus/templates/slides/changelog/2026-07-29-google-slides-exports-can-connect-your-google-account-direct.md +6 -0
  67. package/corpus/templates/slides/server/handlers/google-docs-auth.ts +4 -2
  68. package/dist/a2a/types.d.ts +6 -0
  69. package/dist/a2a/types.d.ts.map +1 -1
  70. package/dist/a2a/types.js.map +1 -1
  71. package/dist/action.d.ts +13 -0
  72. package/dist/action.d.ts.map +1 -1
  73. package/dist/action.js.map +1 -1
  74. package/dist/client/AgentPanel.d.ts.map +1 -1
  75. package/dist/client/AgentPanel.js +16 -5
  76. package/dist/client/AgentPanel.js.map +1 -1
  77. package/dist/collab/awareness.d.ts +2 -2
  78. package/dist/collab/awareness.d.ts.map +1 -1
  79. package/dist/collab/routes.d.ts +1 -1
  80. package/dist/localization/default-messages.d.ts +6 -0
  81. package/dist/localization/default-messages.d.ts.map +1 -1
  82. package/dist/localization/default-messages.js +6 -0
  83. package/dist/localization/default-messages.js.map +1 -1
  84. package/dist/notifications/routes.d.ts +2 -2
  85. package/dist/resources/handlers.d.ts +1 -1
  86. package/dist/server/agent-capabilities.d.ts.map +1 -1
  87. package/dist/server/agent-capabilities.js +32 -3
  88. package/dist/server/agent-capabilities.js.map +1 -1
  89. package/dist/server/agent-chat/action-filters-a2a.d.ts +17 -1
  90. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  91. package/dist/server/agent-chat/action-filters-a2a.js +50 -1
  92. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  93. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  94. package/package.json +1 -1
  95. package/src/a2a/types.ts +6 -0
  96. package/src/action.ts +13 -0
  97. package/src/client/AgentPanel.tsx +58 -34
  98. package/src/localization/default-messages.ts +6 -0
  99. package/src/server/agent-capabilities.ts +43 -8
  100. package/src/server/agent-chat/action-filters-a2a.ts +63 -1
package/corpus/README.md CHANGED
@@ -30,4 +30,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
30
30
 
31
31
  - core files: 1642
32
32
  - toolkit files: 160
33
- - template files: 7176
33
+ - template files: 7192
@@ -1,5 +1,39 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.131.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 13f7e6b: Simplify the agent panel's error-recovery copy and move it into the message catalog, so it no longer shows untranslated English or explains the failure as an "internal UI error".
8
+
9
+ ## 0.131.3
10
+
11
+ ### Patch Changes
12
+
13
+ - 5b67dea: Advertise A2A action input schemas in capability details.
14
+ - 5b67dea: Stop apps from handing each other raw queries to execute. An action whose input
15
+ is a program the receiver runs — `sql`, `code`, `script`, `expression` — is no
16
+ longer invocable by a sibling app over A2A.
17
+
18
+ The app that owns the data owns its schema, data dictionary, reference
19
+ dashboards, and dialect quirks. A calling app has none of that, so passing SQL
20
+ across apps makes every caller reimplement the owner's schema knowledge from
21
+ guesses, and each copy silently rots the next time the owner's shape changes.
22
+ Callers ask the owning app a question, or call a shaped action that takes
23
+ semantic parameters; the owner forms the query. `publicAgent.allowRawQueryInput`
24
+ opts a specific action out when that is genuinely the right call.
25
+
26
+ `query` is deliberately not treated as a raw query field: across the templates
27
+ it is natural-language search text (Brain's `search-everything`,
28
+ `search-knowledge`), and blocking it would break the ask-don't-instruct calls
29
+ this rule exists to encourage. MCP and in-app tool surfaces are unchanged — this
30
+ narrows cross-app invocation only.
31
+
32
+ Agent cards also now publish each advertised skill's `inputSchema`, and
33
+ `describe-workspace-apps` renders it as `input: { field*: type }`. Advertising an
34
+ action without its parameters is what led callers to invoke it with `{}` and fail
35
+ on a required field.
36
+
3
37
  ## 0.131.2
4
38
 
5
39
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.131.2",
3
+ "version": "0.131.4",
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": {
@@ -85,6 +85,12 @@ export interface AgentSkill {
85
85
  requiresAuth?: boolean;
86
86
  isConsequential?: boolean;
87
87
  publicAgent?: PublicAgentActionConfig;
88
+ /**
89
+ * JSON Schema for the action's `input`. Advertising a skill without it tells a
90
+ * caller the action exists but not how to call it, so callers invoke with `{}`
91
+ * and get a required-property error back.
92
+ */
93
+ inputSchema?: Record<string, unknown>;
88
94
  }
89
95
 
90
96
  export interface AgentCapabilities {
@@ -200,6 +200,19 @@ export interface PublicAgentActionConfig {
200
200
  isConsequential?: boolean;
201
201
  title?: string;
202
202
  description?: string;
203
+ /**
204
+ * Allow a sibling app to invoke this action over A2A even though its input is
205
+ * a free-form query or program (`sql`, `query`, `code`, `script`,
206
+ * `expression`).
207
+ *
208
+ * Off by default and rarely correct. The app that owns the data owns its
209
+ * schema, data dictionary, and reference queries; a caller has none of that,
210
+ * so passing raw SQL across apps makes every caller reimplement the owner's
211
+ * schema knowledge and silently break when it changes. Prefer a semantic
212
+ * action the owner implements, or let the caller ask in natural language and
213
+ * form the query yourself.
214
+ */
215
+ allowRawQueryInput?: boolean;
203
216
  }
204
217
 
205
218
  export type ActionPlanModeEffect = "read" | "write" | "unknown";
@@ -2585,47 +2585,71 @@ class AgentPanelErrorBoundary extends React.Component<
2585
2585
  assistantUiRecoverableRenderErrorKind(this.state.error) &&
2586
2586
  this.state.staleIndexRecoveryCount < 2
2587
2587
  ) {
2588
- return (
2589
- <div className="flex h-full items-center justify-center p-6 text-center text-xs text-muted-foreground">
2590
- Reloading chat UI...
2591
- </div>
2592
- );
2588
+ return <AgentPanelReloadingNotice />;
2593
2589
  }
2594
2590
 
2595
2591
  return (
2596
- <div className="flex h-full flex-col items-center justify-center gap-3 p-6 text-center">
2597
- <div className="max-w-[260px] space-y-1">
2598
- <p className="text-sm font-medium text-foreground">
2599
- Agent panel hit an internal UI error.
2600
- </p>
2601
- <p className="text-xs leading-relaxed text-muted-foreground">
2602
- The app is still usable. Reset the panel to reload the chat UI.
2603
- </p>
2604
- </div>
2605
- <button
2606
- type="button"
2607
- className="rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground hover:bg-accent"
2608
- onClick={() => {
2609
- this.setState({ error: null, staleIndexRecoveryCount: 0 });
2610
- this.props.onReset();
2611
- }}
2612
- >
2613
- Reset agent panel
2614
- </button>
2615
- <ErrorReportActions
2616
- appName="Agent panel"
2617
- title="Agent panel UI error"
2618
- details={this.state.error.message}
2619
- issueTitle="Agent panel UI error"
2620
- className="max-w-[260px]"
2621
- feedbackClassName="h-7"
2622
- githubClassName="h-7"
2623
- />
2624
- </div>
2592
+ <AgentPanelErrorFallback
2593
+ details={this.state.error.message}
2594
+ onReset={() => {
2595
+ this.setState({ error: null, staleIndexRecoveryCount: 0 });
2596
+ this.props.onReset();
2597
+ }}
2598
+ />
2625
2599
  );
2626
2600
  }
2627
2601
  }
2628
2602
 
2603
+ // The boundary must stay a class (componentDidCatch), but its copy still has to
2604
+ // come from the catalog like every other string in this file — so the fallback
2605
+ // UI lives in function components that can call useT.
2606
+ function AgentPanelReloadingNotice() {
2607
+ const t = useT();
2608
+ return (
2609
+ <div className="flex h-full items-center justify-center p-6 text-center text-xs text-muted-foreground">
2610
+ {t("agentPanel.uiError.reloading")}
2611
+ </div>
2612
+ );
2613
+ }
2614
+
2615
+ function AgentPanelErrorFallback({
2616
+ details,
2617
+ onReset,
2618
+ }: {
2619
+ details: string;
2620
+ onReset: () => void;
2621
+ }) {
2622
+ const t = useT();
2623
+ return (
2624
+ <div className="flex h-full flex-col items-center justify-center gap-3 p-6 text-center">
2625
+ <div className="max-w-[260px] space-y-1">
2626
+ <p className="text-sm font-medium text-foreground">
2627
+ {t("agentPanel.uiError.title")}
2628
+ </p>
2629
+ <p className="text-xs leading-relaxed text-muted-foreground">
2630
+ {t("agentPanel.uiError.description")}
2631
+ </p>
2632
+ </div>
2633
+ <button
2634
+ type="button"
2635
+ className="rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground hover:bg-accent"
2636
+ onClick={onReset}
2637
+ >
2638
+ {t("agentPanel.uiError.reset")}
2639
+ </button>
2640
+ <ErrorReportActions
2641
+ appName="Agent panel"
2642
+ title={t("agentPanel.uiError.title")}
2643
+ details={details}
2644
+ issueTitle="Agent panel UI error"
2645
+ className="max-w-[260px]"
2646
+ feedbackClassName="h-7"
2647
+ githubClassName="h-7"
2648
+ />
2649
+ </div>
2650
+ );
2651
+ }
2652
+
2629
2653
  export function AgentPanel(props: AgentPanelProps) {
2630
2654
  const [resetKey, setResetKey] = useState(0);
2631
2655
  const resetPanel = useCallback(() => {
@@ -336,6 +336,12 @@ const messages = {
336
336
  },
337
337
  },
338
338
  agentPanel: {
339
+ uiError: {
340
+ title: "Agent panel hit a glitch",
341
+ description: "The app's still usable. Reset to reload the chat.",
342
+ reset: "Reset agent panel",
343
+ reloading: "Reloading chat…",
344
+ },
339
345
  useBuilder: "Use Builder",
340
346
  openDesktopToEditCode: "Open Desktop to edit code",
341
347
  codeUnavailableDescription:
@@ -136,6 +136,38 @@ export function formatCapabilitySummary(
136
136
  .join("\n");
137
137
  }
138
138
 
139
+ /**
140
+ * Render a skill's `input` contract as `input: { field*: type, other?: type }`,
141
+ * marking required fields with `*`. Returns undefined when the skill publishes
142
+ * no schema, so callers fall back to the description alone.
143
+ */
144
+ function formatSkillInput(skill: AgentSkill): string | undefined {
145
+ const schema = skill.inputSchema as
146
+ | {
147
+ properties?: Record<string, { type?: unknown }>;
148
+ required?: unknown;
149
+ }
150
+ | undefined;
151
+ const properties = schema?.properties;
152
+ if (!properties || typeof properties !== "object") return undefined;
153
+ const names = Object.keys(properties);
154
+ if (names.length === 0) return " — input: {} (no fields)";
155
+
156
+ const required = new Set(
157
+ Array.isArray(schema?.required)
158
+ ? schema.required.filter(
159
+ (name): name is string => typeof name === "string",
160
+ )
161
+ : [],
162
+ );
163
+ const fields = names.map((name) => {
164
+ const type = properties[name]?.type;
165
+ const rendered = typeof type === "string" ? type : "any";
166
+ return `${name}${required.has(name) ? "*" : "?"}: ${rendered}`;
167
+ });
168
+ return ` — input: { ${fields.join(", ")} } (* = required)`;
169
+ }
170
+
139
171
  export function formatCapabilityDetail(
140
172
  peer: PeerCapabilities,
141
173
  callHint: (appId: string) => string,
@@ -151,14 +183,17 @@ export function formatCapabilityDetail(
151
183
 
152
184
  const shown = peer.skills.slice(0, MAX_SKILLS_IN_DETAIL);
153
185
  const rest = peer.skills.length - shown.length;
154
- const lines = shown.map(
155
- (skill) =>
156
- `- ${skill.id}${skill.readOnly ? "" : " (mutating)"}: ${
157
- skill.description
158
- ? truncate(skill.description, MAX_DESCRIPTION_CHARS)
159
- : skill.name || "(no description)"
160
- }`,
161
- );
186
+ const lines = shown.map((skill) => {
187
+ const summary = skill.description
188
+ ? truncate(skill.description, MAX_DESCRIPTION_CHARS)
189
+ : skill.name || "(no description)";
190
+ // Naming an action without its parameters is what makes a caller invoke it
191
+ // with `{}` and fail on a required field, which reads as the sibling being
192
+ // broken rather than as a malformed call.
193
+ return `- ${skill.id}${skill.readOnly ? "" : " (mutating)"}: ${summary}${
194
+ formatSkillInput(skill) ?? ""
195
+ }`;
196
+ });
162
197
 
163
198
  return [
164
199
  header,
@@ -65,10 +65,49 @@ export function filterPublicAgentActions(
65
65
  );
66
66
  }
67
67
 
68
+ /**
69
+ * Input fields that unambiguously carry a program the receiver executes.
70
+ *
71
+ * Deliberately excludes `query`: across the templates that field is nearly
72
+ * always natural-language search text (Brain's `search-everything`,
73
+ * `search-knowledge`), and blocking it would break exactly the ask-don't-instruct
74
+ * calls this rule exists to encourage.
75
+ */
76
+ const RAW_QUERY_INPUT_FIELDS = new Set(["sql", "code", "script", "expression"]);
77
+
78
+ /**
79
+ * True when an action takes a free-form query or program as input — raw SQL, a
80
+ * code body, an arbitrary expression.
81
+ *
82
+ * Such an action must not be sibling-invocable over A2A. The app that owns the
83
+ * data owns its schema, data dictionary, skills, and reference queries; a
84
+ * calling app has none of that, so letting it pass SQL means every caller
85
+ * reimplements the owner's schema knowledge badly and silently rots when the
86
+ * owner's shape changes. Callers ask the owning app a question; the owner
87
+ * forms the query. Set `publicAgent.allowRawQueryInput` to opt a specific
88
+ * action out of this rule.
89
+ */
90
+ export function hasRawQueryInput(entry: ActionEntry): boolean {
91
+ const parameters = entry.tool?.parameters as
92
+ | { properties?: Record<string, { type?: unknown }> }
93
+ | undefined;
94
+ const properties = parameters?.properties;
95
+ if (!properties || typeof properties !== "object") return false;
96
+ return Object.entries(properties).some(([field, schema]) => {
97
+ const type = schema?.type;
98
+ const acceptsString =
99
+ type === "string" ||
100
+ type === undefined ||
101
+ (Array.isArray(type) && type.includes("string"));
102
+ return RAW_QUERY_INPUT_FIELDS.has(field.toLowerCase()) && acceptsString;
103
+ });
104
+ }
105
+
68
106
  /**
69
107
  * Direct A2A action calls share the authenticated external-agent policy with
70
108
  * MCP, but remain stricter: only explicitly exposed, authenticated read-only
71
- * actions can skip the receiver's model loop.
109
+ * actions can skip the receiver's model loop, and never one that takes a raw
110
+ * query the caller wrote.
72
111
  */
73
112
  export function filterDirectA2AActions(
74
113
  actions: Record<string, ActionEntry>,
@@ -86,8 +125,11 @@ export function filterDirectA2AActions(
86
125
  (autoReads &&
87
126
  isAuthenticatedReadAction(entry) &&
88
127
  !isAutoReadExcludedActionName(name));
128
+ const rawQueryAllowed =
129
+ !hasRawQueryInput(entry) || exposure?.allowRawQueryInput === true;
89
130
  return (
90
131
  selected &&
132
+ rawQueryAllowed &&
91
133
  !denied.has(name) &&
92
134
  entry.agentTool !== false &&
93
135
  entry.readOnly === true &&
@@ -108,6 +150,7 @@ export function buildPublicAgentA2ASkills(
108
150
  name: string;
109
151
  description: string;
110
152
  publicAgent: ActionEntry["publicAgent"];
153
+ inputSchema?: Record<string, unknown>;
111
154
  }> {
112
155
  return Object.entries(filterPublicAgentActions(actions)).map(
113
156
  ([name, entry]) => ({
@@ -115,6 +158,14 @@ export function buildPublicAgentA2ASkills(
115
158
  name,
116
159
  description: entry.tool.description,
117
160
  publicAgent: entry.publicAgent,
161
+ ...(entry.tool.parameters
162
+ ? {
163
+ inputSchema: entry.tool.parameters as unknown as Record<
164
+ string,
165
+ unknown
166
+ >,
167
+ }
168
+ : {}),
118
169
  }),
119
170
  );
120
171
  }
@@ -135,6 +186,7 @@ export function buildAuthenticatedAgentA2ASkills(
135
186
  name: string;
136
187
  description: string;
137
188
  publicAgent: ActionEntry["publicAgent"];
189
+ inputSchema?: Record<string, unknown>;
138
190
  }> {
139
191
  return Object.entries(filterDirectA2AActions(actions, options)).map(
140
192
  ([name, entry]) => ({
@@ -142,6 +194,16 @@ export function buildAuthenticatedAgentA2ASkills(
142
194
  name,
143
195
  description: entry.tool.description,
144
196
  publicAgent: entry.publicAgent,
197
+ // Naming an action without its parameters is what makes a caller invoke
198
+ // it with `{}` and fail on a required field.
199
+ ...(entry.tool.parameters
200
+ ? {
201
+ inputSchema: entry.tool.parameters as unknown as Record<
202
+ string,
203
+ unknown
204
+ >,
205
+ }
206
+ : {}),
145
207
  }),
146
208
  );
147
209
  }
@@ -42,6 +42,11 @@ change the numbers.
42
42
 
43
43
  ## Core Rules
44
44
 
45
+ - A sibling app asking over A2A sends a question or shaped input, never SQL. It
46
+ lacks this app's schema, data dictionary, dashboards, and dialect knowledge,
47
+ so raw-query actions (`sql`, `code`, `script`, `expression`) are not
48
+ sibling-invocable. For recurring numbers, add a shaped read action with
49
+ semantic parameters and let this app own the query.
45
50
  - Data integrity first. Never invent numbers, dimensions, filters, or source
46
51
  semantics; only present values you actually retrieved, and state uncertainty.
47
52
  - Every analytical answer carries audit context: source(s), time window,
@@ -493,6 +493,7 @@ export default defineAction({
493
493
  .describe("Optional HubSpot pagination cursor for query results."),
494
494
  }),
495
495
  http: { method: "GET" },
496
+ publicAgent: { expose: true, readOnly: true, requiresAuth: true },
496
497
  run: async ({
497
498
  properties,
498
499
  owner,
@@ -48,6 +48,7 @@ export default defineAction({
48
48
  .describe("Optional HubSpot pagination cursor from a previous result."),
49
49
  }),
50
50
  http: { method: "GET" },
51
+ publicAgent: { expose: true, readOnly: true, requiresAuth: true },
51
52
  run: async ({ objectType, query, properties, limit, after }) => {
52
53
  const result = await searchHubSpotObjects({
53
54
  objectType,
@@ -9,6 +9,8 @@ export const ANALYTICS_CONNECTOR_CATALOG = [
9
9
  "account-deep-dive",
10
10
  "gong-calls",
11
11
  "gong-native-insights",
12
+ "hubspot-deals",
13
+ "hubspot-records",
12
14
  "list-session-recordings",
13
15
  "get-session-replay-summary",
14
16
  "get-session-replay-timeline",
@@ -66,6 +66,7 @@ export default defineAction({
66
66
  const calEvent: CalendarEvent = {
67
67
  id: `google-${evt.id}`,
68
68
  title: evt.summary || "Untitled",
69
+ titleIsGenerated: !evt.summary,
69
70
  description: evt.description || "",
70
71
  start: evt.start?.dateTime || evt.start?.date || "",
71
72
  end: evt.end?.dateTime || evt.end?.date || "",
@@ -757,6 +757,7 @@ export function CreateEventPopover({
757
757
 
758
758
  const payload: Parameters<typeof createEvent.mutate>[0] = {
759
759
  title: title.trim() || undefined,
760
+ titleIsGenerated: !title.trim(),
760
761
  description: isOutOfOffice ? "" : description,
761
762
  start: startValue,
762
763
  end: endValue,
@@ -34,6 +34,7 @@ import {
34
34
  } from "@/components/ui/tooltip";
35
35
  import { useUpdateEvent } from "@/hooks/use-events";
36
36
  import { useViewPreferences } from "@/hooks/use-view-preferences";
37
+ import { getEditableEventTitle } from "@/lib/event-form-utils";
37
38
  import { isOutOfOfficeEvent } from "@/lib/out-of-office";
38
39
  import { cn } from "@/lib/utils";
39
40
  import {
@@ -338,7 +339,10 @@ export function EventDetailPanel({
338
339
  if (e.key === "Enter") {
339
340
  e.preventDefault();
340
341
  const trimmed = editingTitle.trim();
341
- if (trimmed && trimmed !== event.title) {
342
+ if (
343
+ trimmed &&
344
+ trimmed !== getEditableEventTitle(event)
345
+ ) {
342
346
  onTitleSave?.(event.id, trimmed, event.accountEmail);
343
347
  }
344
348
  setIsEditingTitle(false);
@@ -350,7 +354,7 @@ export function EventDetailPanel({
350
354
  }}
351
355
  onBlur={() => {
352
356
  const trimmed = editingTitle.trim();
353
- if (trimmed && trimmed !== event.title) {
357
+ if (trimmed && trimmed !== getEditableEventTitle(event)) {
354
358
  onTitleSave?.(event.id, trimmed, event.accountEmail);
355
359
  }
356
360
  setIsEditingTitle(false);
@@ -366,7 +370,7 @@ export function EventDetailPanel({
366
370
  )}
367
371
  onClick={() => {
368
372
  if (isWorkingLocation) return;
369
- setEditingTitle(event.title);
373
+ setEditingTitle(getEditableEventTitle(event));
370
374
  setIsEditingTitle(true);
371
375
  }}
372
376
  >
@@ -83,6 +83,7 @@ import {
83
83
  buildReminderPayload,
84
84
  dateTimeInTimezoneToIso,
85
85
  formatReminderText,
86
+ getEditableEventTitle,
86
87
  getEventEndValidationMessage,
87
88
  getLocalTimezone,
88
89
  getRecurrencePreset,
@@ -536,7 +537,7 @@ export function EventDetailPopover({
536
537
  const isMobile = useIsMobile();
537
538
  const [open, setOpen] = useState(defaultOpen);
538
539
  const [editingTitle, setEditingTitle] = useState(
539
- defaultOpen ? event.title : "",
540
+ defaultOpen ? getEditableEventTitle(event) : "",
540
541
  );
541
542
  const [isEditingTitle, setIsEditingTitle] = useState(defaultOpen);
542
543
  const isNewEventRef = useRef(defaultOpen);
@@ -675,12 +676,12 @@ export function EventDetailPopover({
675
676
  setIsEditingTitle(true);
676
677
  isNewEventRef.current = true;
677
678
  setEditingTitle((current) => {
678
- const hasDraft = current.trim().length > 0 && current !== "(No title)";
679
- if (hasDraft && current !== event.title) return current;
680
- return event.title === "(No title)" ? "" : event.title;
679
+ const eventTitle = getEditableEventTitle(event);
680
+ if (current.trim() && current !== eventTitle) return current;
681
+ return eventTitle;
681
682
  });
682
683
  }
683
- }, [defaultOpen, event.title]);
684
+ }, [defaultOpen, event.title, event.titleIsGenerated]);
684
685
 
685
686
  // Focus title input when editing starts
686
687
  useEffect(() => {
@@ -1326,10 +1327,8 @@ export function EventDetailPopover({
1326
1327
 
1327
1328
  const handleCreateDraft = useCallback(() => {
1328
1329
  if (!onDraftCreate) return;
1329
- const updates =
1330
- isEditingTitle && editingTitle.trim()
1331
- ? { title: editingTitle.trim() }
1332
- : undefined;
1330
+ const title = editingTitle.trim();
1331
+ const updates = isEditingTitle && title ? { title } : undefined;
1333
1332
  if (updates) {
1334
1333
  onTitleSave?.(event.id, updates.title, event.accountEmail);
1335
1334
  setIsEditingTitle(false);
@@ -1370,7 +1369,7 @@ export function EventDetailPopover({
1370
1369
  let savedPendingChange = false;
1371
1370
  // Popover is closing — handle saves
1372
1371
  if (isEditingTitle) {
1373
- if (trimmedTitle && trimmedTitle !== "(No title)") {
1372
+ if (trimmedTitle) {
1374
1373
  onTitleSave?.(event.id, trimmedTitle, event.accountEmail);
1375
1374
  isNewEventRef.current = false;
1376
1375
  savedPendingChange = true;
@@ -1393,7 +1392,7 @@ export function EventDetailPopover({
1393
1392
  if (
1394
1393
  isNewEventRef.current &&
1395
1394
  !savedPendingChange &&
1396
- (!trimmedTitle || trimmedTitle === "(No title)") &&
1395
+ !trimmedTitle &&
1397
1396
  onDismissNew
1398
1397
  ) {
1399
1398
  onDismissNew(event.id, event.accountEmail);
@@ -1538,7 +1537,7 @@ export function EventDetailPopover({
1538
1537
  if (e.key === "Enter") {
1539
1538
  e.preventDefault();
1540
1539
  const trimmed = editingTitle.trim();
1541
- if (trimmed && trimmed !== "(No title)") {
1540
+ if (trimmed) {
1542
1541
  onTitleSave?.(event.id, trimmed, event.accountEmail);
1543
1542
  isNewEventRef.current = false;
1544
1543
  }
@@ -1548,7 +1547,7 @@ export function EventDetailPopover({
1548
1547
  if (isNewEventRef.current && onDismissNew) {
1549
1548
  handleOpenChange(false);
1550
1549
  } else {
1551
- setEditingTitle(event.title);
1550
+ setEditingTitle(getEditableEventTitle(event));
1552
1551
  setIsEditingTitle(false);
1553
1552
  }
1554
1553
  } else if (
@@ -1564,11 +1563,7 @@ export function EventDetailPopover({
1564
1563
  }}
1565
1564
  onBlur={() => {
1566
1565
  const trimmed = editingTitle.trim();
1567
- if (
1568
- trimmed &&
1569
- trimmed !== "(No title)" &&
1570
- trimmed !== event.title
1571
- ) {
1566
+ if (trimmed && trimmed !== getEditableEventTitle(event)) {
1572
1567
  onTitleSave?.(event.id, trimmed, event.accountEmail);
1573
1568
  isNewEventRef.current = false;
1574
1569
  }
@@ -1582,7 +1577,7 @@ export function EventDetailPopover({
1582
1577
  className={`mb-4 -mx-0.5 rounded px-0.5 text-lg font-semibold leading-tight text-foreground ${!isOverlay && !isWorkingLocation ? "cursor-text hover:bg-muted/50" : ""}`}
1583
1578
  onClick={() => {
1584
1579
  if (isOverlay || isWorkingLocation) return;
1585
- setEditingTitle(event.title);
1580
+ setEditingTitle(getEditableEventTitle(event));
1586
1581
  setIsEditingTitle(true);
1587
1582
  }}
1588
1583
  >
@@ -103,6 +103,12 @@ function getEventQueryKey(id: string) {
103
103
  return ["action", "get-event", { id }] as const;
104
104
  }
105
105
 
106
+ export function getOptimisticTitleIsGenerated(
107
+ input: Pick<CreateEventInput, "title" | "titleIsGenerated">,
108
+ ): boolean {
109
+ return input.titleIsGenerated ?? !input.title?.trim();
110
+ }
111
+
106
112
  function buildOptimisticCalendarEvent(
107
113
  newData: CreateEventInput,
108
114
  optimisticId: string,
@@ -130,6 +136,7 @@ function buildOptimisticCalendarEvent(
130
136
  title:
131
137
  newData.title?.trim() ||
132
138
  (newData.eventType === "outOfOffice" ? "Out of office" : ""),
139
+ titleIsGenerated: getOptimisticTitleIsGenerated(newData),
133
140
  start,
134
141
  end,
135
142
  startTimeZone: newData.startTimeZone,
@@ -34,6 +34,19 @@ export const REMINDER_PRESETS = [
34
34
  ] as const;
35
35
 
36
36
  export const MAX_EVENT_ATTACHMENTS = 25;
37
+ export const UNNAMED_EVENT_TITLE = "(No title)";
38
+
39
+ export function getEditableEventTitle(
40
+ event: Pick<CalendarEvent, "title" | "titleIsGenerated">,
41
+ ): string {
42
+ return event.titleIsGenerated ? "" : event.title;
43
+ }
44
+
45
+ export function buildEventTitleUpdate(
46
+ title: string,
47
+ ): Pick<CalendarEvent, "title" | "titleIsGenerated"> {
48
+ return { title: title.trim(), titleIsGenerated: false };
49
+ }
37
50
 
38
51
  const DAY_CODES = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"] as const;
39
52
  const DAY_CODE_BY_LABEL: Record<string, (typeof DAY_CODES)[number]> = {