@agent-native/core 0.120.0 → 0.120.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 (55) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +7 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/MultiTabAssistantChat.tsx +45 -0
  5. package/corpus/core/src/client/use-chat-models.ts +45 -0
  6. package/corpus/core/src/server/analytics.ts +15 -0
  7. package/corpus/core/src/server/auth.ts +17 -13
  8. package/corpus/core/src/server/better-auth-instance.ts +13 -1
  9. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +1 -1
  10. package/corpus/templates/assets/changelog/2026-07-23-new-chat-aligns-with-sidebar-controls.md +6 -0
  11. package/corpus/templates/forms/.agents/skills/form-publishing/SKILL.md +9 -3
  12. package/corpus/templates/forms/AGENTS.md +11 -1
  13. package/corpus/templates/forms/README.md +24 -1
  14. package/corpus/templates/forms/actions/update-form.ts +12 -5
  15. package/corpus/templates/forms/app/components/builder/FieldPropertiesPanel.tsx +199 -1
  16. package/corpus/templates/forms/app/i18n/en-US.ts +13 -1
  17. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +35 -4
  18. package/corpus/templates/forms/app/pages/FormFillPage.tsx +7 -16
  19. package/corpus/templates/forms/changelog/2026-07-23-forms-can-branch-into-follow-up-questions-and-route-response.md +6 -0
  20. package/corpus/templates/forms/server/handlers/submissions.ts +22 -31
  21. package/corpus/templates/forms/server/lib/integrations.ts +11 -2
  22. package/corpus/templates/forms/server/lib/public-form-ssr.ts +26 -5
  23. package/corpus/templates/forms/server/lib/validate-fields.ts +48 -11
  24. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -0
  25. package/corpus/templates/forms/shared/conditional.ts +67 -0
  26. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  27. package/dist/client/MultiTabAssistantChat.js +35 -0
  28. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  29. package/dist/client/use-chat-models.d.ts +1 -0
  30. package/dist/client/use-chat-models.d.ts.map +1 -1
  31. package/dist/client/use-chat-models.js +33 -0
  32. package/dist/client/use-chat-models.js.map +1 -1
  33. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  34. package/dist/observability/routes.d.ts +2 -2
  35. package/dist/progress/routes.d.ts +1 -1
  36. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  37. package/dist/provider-api/actions/provider-api.d.ts +9 -9
  38. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  39. package/dist/server/analytics.d.ts +8 -0
  40. package/dist/server/analytics.d.ts.map +1 -1
  41. package/dist/server/analytics.js +16 -0
  42. package/dist/server/analytics.js.map +1 -1
  43. package/dist/server/auth.d.ts.map +1 -1
  44. package/dist/server/auth.js +5 -4
  45. package/dist/server/auth.js.map +1 -1
  46. package/dist/server/better-auth-instance.d.ts +8 -0
  47. package/dist/server/better-auth-instance.d.ts.map +1 -1
  48. package/dist/server/better-auth-instance.js +10 -1
  49. package/dist/server/better-auth-instance.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/client/MultiTabAssistantChat.tsx +45 -0
  52. package/src/client/use-chat-models.ts +45 -0
  53. package/src/server/analytics.ts +15 -0
  54. package/src/server/auth.ts +17 -13
  55. package/src/server/better-auth-instance.ts +13 -1
@@ -295,6 +295,18 @@ export function FormBuilderPage() {
295
295
  [updateForm],
296
296
  );
297
297
 
298
+ const saveImmediately = useCallback(
299
+ async (data: Parameters<typeof updateForm.mutate>[0]) => {
300
+ clearTimeout(saveTimeout.current);
301
+ try {
302
+ await updateForm.mutateAsync(data);
303
+ } finally {
304
+ fieldsDirty.current = false;
305
+ }
306
+ },
307
+ [updateForm],
308
+ );
309
+
298
310
  // Debounced field-op save — uses patch-form-fields (server-side merge) so
299
311
  // concurrent edits to different fields both survive.
300
312
  const fieldOpTimeout = useRef<ReturnType<typeof setTimeout>>(undefined);
@@ -853,8 +865,17 @@ export function FormBuilderPage() {
853
865
  key={JSON.stringify(form.settings)}
854
866
  form={form}
855
867
  onSave={(settings) => {
856
- save({ id: form.id, settings });
857
- toast.success(t("builder.settingsSaved"));
868
+ void saveImmediately({ id: form.id, settings })
869
+ .then(() => toast.success(t("builder.settingsSaved")))
870
+ .catch((error: unknown) => {
871
+ toast.error(
872
+ error instanceof Error && error.message
873
+ ? error.message
874
+ : t("builder.saveFailed", {
875
+ defaultValue: "Failed to save changes",
876
+ }),
877
+ );
878
+ });
858
879
  }}
859
880
  />
860
881
  </div>
@@ -868,8 +889,17 @@ export function FormBuilderPage() {
868
889
  key={JSON.stringify(form.settings?.integrations)}
869
890
  form={form}
870
891
  onSave={(settings) => {
871
- save({ id: form.id, settings });
872
- toast.success(t("builder.integrationsSaved"));
892
+ void saveImmediately({ id: form.id, settings })
893
+ .then(() => toast.success(t("builder.integrationsSaved")))
894
+ .catch((error: unknown) => {
895
+ toast.error(
896
+ error instanceof Error && error.message
897
+ ? error.message
898
+ : t("builder.saveFailed", {
899
+ defaultValue: "Failed to save changes",
900
+ }),
901
+ );
902
+ });
873
903
  }}
874
904
  />
875
905
  </div>
@@ -1054,6 +1084,7 @@ function BuilderContent({
1054
1084
  >
1055
1085
  <FieldPropertiesPanel
1056
1086
  field={field}
1087
+ fields={fields}
1057
1088
  onChange={onUpdateField}
1058
1089
  onDelete={() => onDeleteField(field.id)}
1059
1090
  />
@@ -1,5 +1,6 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
2
  import { Turnstile, PoweredByBadge } from "@agent-native/core/client/ui";
3
+ import { isConditionalFieldVisible } from "@shared/conditional";
3
4
  import type { FormField, FormSettings } from "@shared/types";
4
5
  import { IconCircleCheck, IconRefresh } from "@tabler/icons-react";
5
6
  import { useState, useMemo, useEffect } from "react";
@@ -88,21 +89,7 @@ export function FormFillPage() {
88
89
 
89
90
  // Evaluate conditional visibility
90
91
  const visibleFields = useMemo(() => {
91
- return fields.filter((field) => {
92
- if (!field.conditional) return true;
93
- const { fieldId, operator, value: condValue } = field.conditional;
94
- const fieldVal = String(values[fieldId] ?? "");
95
- switch (operator) {
96
- case "equals":
97
- return fieldVal === condValue;
98
- case "not_equals":
99
- return fieldVal !== condValue;
100
- case "contains":
101
- return fieldVal.includes(condValue);
102
- default:
103
- return true;
104
- }
105
- });
92
+ return fields.filter((field) => isConditionalFieldVisible(field, values));
106
93
  }, [fields, values]);
107
94
 
108
95
  function handleChange(fieldId: string, value: unknown) {
@@ -166,7 +153,11 @@ export function FormFillPage() {
166
153
  submitForm.mutate(
167
154
  {
168
155
  formId: form.id,
169
- data: values,
156
+ data: Object.fromEntries(
157
+ visibleFields
158
+ .filter((field) => values[field.id] !== undefined)
159
+ .map((field) => [field.id, values[field.id]]),
160
+ ),
170
161
  captchaToken,
171
162
  _hp: honeypot,
172
163
  _t: pageLoadTime,
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-23
4
+ ---
5
+
6
+ Forms can branch into follow-up questions and route responses to configured destinations.
@@ -17,6 +17,10 @@ import {
17
17
  } from "h3";
18
18
  import { nanoid } from "nanoid";
19
19
 
20
+ import {
21
+ isConditionalFieldVisible,
22
+ sanitizeConditionalValues as sanitizeVisibleValues,
23
+ } from "../../shared/conditional.js";
20
24
  import {
21
25
  cleanSubmitterEmail,
22
26
  publicSubmitterEmail,
@@ -158,43 +162,26 @@ export const submitForm = defineEventHandler(async (event: H3Event) => {
158
162
  // Parse form fields and build whitelist of valid field IDs
159
163
  const fields: FormField[] = JSON.parse(form.fields);
160
164
  const fieldMap = new Map(fields.map((f) => [f.id, f]));
161
- const rawData =
165
+ const submittedData =
162
166
  body.data && typeof body.data === "object" && !Array.isArray(body.data)
163
167
  ? (body.data as Record<string, unknown>)
164
168
  : {};
165
169
 
166
170
  // Whitelist: only accept keys matching form field IDs
167
- const data: Record<string, unknown> = {};
168
- for (const [key, value] of Object.entries(rawData)) {
171
+ const whitelistedData: Record<string, unknown> = {};
172
+ for (const [key, value] of Object.entries(submittedData)) {
169
173
  const field = fieldMap.get(key);
170
174
  if (!field) continue; // Strip unknown fields
171
- data[key] = value;
175
+ whitelistedData[key] = value;
172
176
  }
173
177
 
178
+ const data = sanitizeVisibleValues(fields, whitelistedData);
179
+
174
180
  // Validate required fields and field-specific constraints. Recompute
175
181
  // conditional visibility on the server so direct POSTs cannot submit hidden
176
182
  // field values or bypass client-side validation.
177
- function isFieldVisible(field: FormField): boolean {
178
- if (!field.conditional) return true;
179
- const { fieldId, operator, value: condValue } = field.conditional;
180
- const fieldVal = String(data[fieldId] ?? "");
181
- switch (operator) {
182
- case "equals":
183
- return fieldVal === condValue;
184
- case "not_equals":
185
- return fieldVal !== condValue;
186
- case "contains":
187
- return fieldVal.includes(condValue);
188
- default:
189
- return true;
190
- }
191
- }
192
-
193
183
  for (const field of fields) {
194
- if (!isFieldVisible(field)) {
195
- delete data[field.id];
196
- continue;
197
- }
184
+ if (field.conditional && !isConditionalFieldVisible(field, data)) continue;
198
185
 
199
186
  const val = data[field.id];
200
187
  if (field.required && isEmptySubmissionValue(val)) {
@@ -268,13 +255,17 @@ export const submitForm = defineEventHandler(async (event: H3Event) => {
268
255
 
269
256
  if (settings.emailOnNewResponses === true && form.ownerEmail) {
270
257
  try {
271
- await sendNewResponseEmail({
272
- to: form.ownerEmail,
273
- formTitle: form.title,
274
- fields,
275
- data,
276
- submittedAt: now,
277
- });
258
+ await runWithRequestContext(
259
+ { userEmail: form.ownerEmail, orgId: form.orgId ?? undefined },
260
+ () =>
261
+ sendNewResponseEmail({
262
+ to: form.ownerEmail!,
263
+ formTitle: form.title,
264
+ fields,
265
+ data,
266
+ submittedAt: now,
267
+ }),
268
+ );
278
269
  } catch (error) {
279
270
  // Email is best-effort — a provider outage must not reject a public
280
271
  // submission that was already persisted successfully.
@@ -118,9 +118,14 @@ function formatFields(
118
118
  data: Record<string, unknown>,
119
119
  ): Record<string, unknown> {
120
120
  const out: Record<string, unknown> = {};
121
+ const usedLabels = new Set<string>();
121
122
  for (const field of fields) {
122
123
  if (data[field.id] !== undefined) {
123
- out[field.label] = data[field.id];
124
+ const label = field.label.trim() || field.id;
125
+ let key = label;
126
+ if (usedLabels.has(key)) key = `${label} (${field.id})`;
127
+ usedLabels.add(key);
128
+ out[key] = data[field.id];
124
129
  }
125
130
  }
126
131
  return out;
@@ -258,9 +263,13 @@ function buildDiscordPayload(submission: SubmissionPayload) {
258
263
  }
259
264
 
260
265
  /** Google Sheets (Apps Script web app) — flat key/value pairs */
261
- function buildGoogleSheetsPayload(submission: SubmissionPayload) {
266
+ export function buildGoogleSheetsPayload(submission: SubmissionPayload) {
262
267
  return {
268
+ event: "form_submission",
269
+ eventVersion: 1,
270
+ formId: submission.formId,
263
271
  formTitle: submission.formTitle,
272
+ responseId: submission.responseId,
264
273
  submittedAt: submission.submittedAt,
265
274
  submitterEmail: publicSubmitterEmail(submission.submitterEmail) ?? "",
266
275
  chatSessionIds: (submission.chatSessionIds ?? []).join(", "),
@@ -510,19 +510,40 @@ function renderFormPage(
510
510
  var condVal = el.dataset.condVal;
511
511
  var depVal = getFieldValue(depId);
512
512
  var show = true;
513
- if (op === "equals") show = depVal === condVal;
513
+ if (Array.isArray(depVal)) {
514
+ if (op === "equals") show = depVal.length === 1 && depVal[0] === condVal;
515
+ else if (op === "not_equals") show = depVal.indexOf(condVal) < 0;
516
+ else if (op === "contains") show = depVal.indexOf(condVal) >= 0;
517
+ } else if (op === "equals") show = depVal === condVal;
514
518
  else if (op === "not_equals") show = depVal !== condVal;
515
519
  else if (op === "contains") show = depVal.indexOf(condVal) >= 0;
516
520
  el.style.display = show ? "" : "none";
517
521
  el.dataset.hidden = show ? "" : "1";
522
+ el.querySelectorAll("input, textarea, select, button").forEach(function(control) {
523
+ control.disabled = !show;
524
+ });
518
525
  });
519
526
  }
520
527
 
521
528
  function getFieldValue(id) {
522
- var el = document.querySelector('[name="' + id + '"]');
523
- if (!el) return "";
524
- if (el.type === "checkbox" && !el.closest(".ms-group")) return el.checked ? "true" : "";
525
- return el.value || "";
529
+ var controls = document.getElementsByName(id);
530
+ if (!controls.length) return "";
531
+ var first = controls[0];
532
+ if (first.type === "checkbox") {
533
+ if (controls.length > 1) {
534
+ return Array.prototype.map.call(controls, function(control) {
535
+ return control.checked ? control.value : "";
536
+ }).filter(Boolean);
537
+ }
538
+ return first.checked ? "true" : "false";
539
+ }
540
+ if (first.type === "radio") {
541
+ for (var i = 0; i < controls.length; i++) {
542
+ if (controls[i].checked) return controls[i].value || "";
543
+ }
544
+ return "";
545
+ }
546
+ return first.value || "";
526
547
  }
527
548
 
528
549
  document.getElementById("mainForm").addEventListener("input", updateVisibility);
@@ -4,6 +4,7 @@
4
4
  // runtime — an unrestricted id like `x" onfocus="alert(1)` would otherwise
5
5
  // stored-XSS every anonymous submitter of a published form.
6
6
  export const FIELD_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
7
+ const CONDITIONAL_OPERATORS = new Set(["equals", "not_equals", "contains"]);
7
8
 
8
9
  export function assertValidFields(fields: unknown): void {
9
10
  if (!Array.isArray(fields)) {
@@ -28,17 +29,30 @@ export function assertValidFields(fields: unknown): void {
28
29
  seenIds.add(id);
29
30
 
30
31
  const cond = f.conditional;
31
- if (cond && typeof cond === "object") {
32
- const condFieldId = (cond as Record<string, unknown>).fieldId;
33
- if (condFieldId !== undefined) {
34
- if (
35
- typeof condFieldId !== "string" ||
36
- !FIELD_ID_PATTERN.test(condFieldId)
37
- ) {
38
- throw new Error(
39
- `field #${idx + 1} conditional.fieldId ${JSON.stringify(condFieldId)} is invalid — must match ${FIELD_ID_PATTERN.source}`,
40
- );
41
- }
32
+ if (cond !== undefined) {
33
+ if (cond == null || typeof cond !== "object") {
34
+ throw new Error(`field #${idx + 1} conditional must be an object`);
35
+ }
36
+ const condition = cond as Record<string, unknown>;
37
+ const condFieldId = condition.fieldId;
38
+ if (
39
+ typeof condFieldId !== "string" ||
40
+ !FIELD_ID_PATTERN.test(condFieldId)
41
+ ) {
42
+ throw new Error(
43
+ `field #${idx + 1} conditional.fieldId ${JSON.stringify(condFieldId)} is invalid — must match ${FIELD_ID_PATTERN.source}`,
44
+ );
45
+ }
46
+ if (
47
+ typeof condition.operator !== "string" ||
48
+ !CONDITIONAL_OPERATORS.has(condition.operator)
49
+ ) {
50
+ throw new Error(
51
+ `field #${idx + 1} conditional.operator must be equals, not_equals, or contains`,
52
+ );
53
+ }
54
+ if (typeof condition.value !== "string") {
55
+ throw new Error(`field #${idx + 1} conditional.value must be a string`);
42
56
  }
43
57
  }
44
58
 
@@ -69,4 +83,27 @@ export function assertValidFields(fields: unknown): void {
69
83
  }
70
84
  }
71
85
  }
86
+
87
+ const fieldIndexes = new Map(
88
+ fields.map((field, index) => [
89
+ (field as Record<string, unknown>).id,
90
+ index,
91
+ ]),
92
+ );
93
+ for (const [idx, field] of fields.entries()) {
94
+ const condition = (field as Record<string, unknown>).conditional;
95
+ if (!condition || typeof condition !== "object") continue;
96
+ const condFieldId = (condition as Record<string, unknown>).fieldId;
97
+ const sourceIndex = fieldIndexes.get(condFieldId);
98
+ if (sourceIndex === undefined) {
99
+ throw new Error(
100
+ `field #${idx + 1} conditional.fieldId ${JSON.stringify(condFieldId)} does not reference a field in this form`,
101
+ );
102
+ }
103
+ if (sourceIndex >= idx) {
104
+ throw new Error(
105
+ `field #${idx + 1} conditional.fieldId must reference an earlier field`,
106
+ );
107
+ }
108
+ }
72
109
  }
@@ -18,6 +18,7 @@ Core rules:
18
18
  - When the user asks for an anonymous feedback form or survey, create the complete form atomically with \`status: "published"\`, then return the exact public response URL from the successful action result. Do not return only the private editor link, and do not claim it is live without verifying the saved status and fields.
19
19
  - Use \`preview-form\` when the user asks about a form's setup, fields, configuration, publish state, or response count. It renders a native chat summary.
20
20
  - When the user wants email notifications for new responses, set \`settings.emailOnNewResponses: true\` with \`create-form\` or \`update-form\`; notifications go to the form owner's account email.
21
+ - When the user wants Slack, Discord, Google Sheets, or webhook notifications, explain that Forms already supports them from the form builder's Integrations tab and navigate there with \`tab: "integrations"\`. Slack uses an Incoming Webhook URL; Google Sheets uses a deployed Apps Script \`/exec\` URL that parses \`e.postData.contents\`. Do not redirect users to the managed Messaging integration for these form destinations.
21
22
  - Use \`response-insights\` for response analytics. Do not invent SQL or fake chart data. Pass \`displayMode: "chart"\` for chart-only requests, \`displayMode: "table"\` only when the user asks for a table/rows, and \`displayMode: "insights"\` only for combined dashboards or reports.
22
23
  - Use \`list-responses\` and \`export-responses\` for response data review and export. When the user asks to see, open, or view all responses for a form, call \`navigate\` with \`view: "responses"\` instead of listing rows in chat.
23
24
  - Use \`navigate\` to open focused workspace views such as the forms list, builder, published preview, responses, response insights, extensions, or team/settings views. For builder sub-tabs, pass \`view=form\`, the form ID, and \`tab=edit|responses|settings|integrations\`.
@@ -0,0 +1,67 @@
1
+ import type { FormField } from "./types.js";
2
+
3
+ export function conditionalValue(value: unknown): string {
4
+ if (Array.isArray(value)) return value.map(conditionalValue).join(",");
5
+ if (value === true) return "true";
6
+ if (value === false) return "false";
7
+ if (value == null) return "";
8
+ return String(value);
9
+ }
10
+
11
+ export function isConditionalFieldVisible(
12
+ field: Pick<FormField, "conditional">,
13
+ values: Record<string, unknown>,
14
+ ): boolean {
15
+ const condition = field.conditional;
16
+ if (!condition) return true;
17
+
18
+ if (
19
+ typeof condition.fieldId !== "string" ||
20
+ typeof condition.value !== "string"
21
+ ) {
22
+ return false;
23
+ }
24
+
25
+ const rawValue = values[condition.fieldId];
26
+ if (Array.isArray(rawValue)) {
27
+ const selectedValues = rawValue.map(conditionalValue);
28
+ switch (condition.operator) {
29
+ case "equals":
30
+ return (
31
+ selectedValues.length === 1 && selectedValues[0] === condition.value
32
+ );
33
+ case "not_equals":
34
+ return !selectedValues.includes(condition.value);
35
+ case "contains":
36
+ return selectedValues.includes(condition.value);
37
+ default:
38
+ return false;
39
+ }
40
+ }
41
+
42
+ const currentValue = conditionalValue(rawValue);
43
+ switch (condition.operator) {
44
+ case "equals":
45
+ return currentValue === condition.value;
46
+ case "not_equals":
47
+ return currentValue !== condition.value;
48
+ case "contains":
49
+ return currentValue.includes(condition.value);
50
+ default:
51
+ return false;
52
+ }
53
+ }
54
+
55
+ export function sanitizeConditionalValues(
56
+ fields: FormField[],
57
+ values: Record<string, unknown>,
58
+ ): Record<string, unknown> {
59
+ const visibleValues: Record<string, unknown> = {};
60
+ for (const field of fields) {
61
+ if (!isConditionalFieldVisible(field, visibleValues)) continue;
62
+ if (Object.prototype.hasOwnProperty.call(values, field.id)) {
63
+ visibleValues[field.id] = values[field.id];
64
+ }
65
+ }
66
+ return visibleValues;
67
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"MultiTabAssistantChat.d.ts","sourceRoot":"","sources":["../../src/client/MultiTabAssistantChat.tsx"],"names":[],"mappings":"AACA,OAAO,KAAmD,MAAM,OAAO,CAAC;AA4BxE,OAAO,EAEL,KAAK,kBAAkB,EAExB,MAAM,oBAAoB,CAAC;AAyB5B,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,uBAAuB,CAAC;AAoa/B,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,CAAC;IACzC,uDAAuD;IACvD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAsGD,MAAM,WAAW,wBAAwB;IACvC,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,8EAA8E;IAC9E,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,CAAC;IAC9C,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;CACpE;AAyED,MAAM,WAAW,gCAAgC;IAC/C,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,+BAA+B;IAC/B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAID,MAAM,MAAM,0BAA0B,GAAG,IAAI,CAC3C,kBAAkB,EAClB,OAAO,GAAG,UAAU,CACrB,GAAG;IACF,sCAAsC;IACtC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,iDAAiD;IACjD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,KAAK,CAAC,SAAS,CAAC;IAC5E,uEAAuE;IACvE,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,KAAK,CAAC,SAAS,CAAC;IAC7E,sGAAsG;IACtG,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,GAAG,wBAAwB,CAAC;IACnD,2DAA2D;IAC3D,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAC/B,iEAAiE;IACjE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,EACpC,UAAiB,EACjB,YAAY,EACZ,aAAa,EACb,aAAqB,EACrB,MAAqD,EACrD,UAAU,EACV,mBAA0B,EAC1B,YAAY,EACZ,aAAqB,EACrB,KAAY,EACZ,GAAG,KAAK,EACT,EAAE,0BAA0B,qBA2yD5B"}
1
+ {"version":3,"file":"MultiTabAssistantChat.d.ts","sourceRoot":"","sources":["../../src/client/MultiTabAssistantChat.tsx"],"names":[],"mappings":"AACA,OAAO,KAAmD,MAAM,OAAO,CAAC;AA4BxE,OAAO,EAEL,KAAK,kBAAkB,EAExB,MAAM,oBAAoB,CAAC;AA0B5B,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,uBAAuB,CAAC;AA2a/B,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,CAAC;IACzC,uDAAuD;IACvD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAsGD,MAAM,WAAW,wBAAwB;IACvC,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,8EAA8E;IAC9E,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,CAAC;IAC9C,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;CACpE;AAyED,MAAM,WAAW,gCAAgC;IAC/C,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,+BAA+B;IAC/B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAID,MAAM,MAAM,0BAA0B,GAAG,IAAI,CAC3C,kBAAkB,EAClB,OAAO,GAAG,UAAU,CACrB,GAAG;IACF,sCAAsC;IACtC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,iDAAiD;IACjD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,KAAK,CAAC,SAAS,CAAC;IAC5E,uEAAuE;IACvE,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,KAAK,CAAC,SAAS,CAAC;IAC7E,sGAAsG;IACtG,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,GAAG,wBAAwB,CAAC;IACnD,2DAA2D;IAC3D,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAC/B,iEAAiE;IACjE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,EACpC,UAAiB,EACjB,YAAY,EACZ,aAAa,EACb,aAAqB,EACrB,MAAqD,EACrD,UAAU,EACV,mBAA0B,EAC1B,YAAY,EACZ,aAAqB,EACrB,KAAY,EACZ,GAAG,KAAK,EACT,EAAE,0BAA0B,qBAg1D5B"}
@@ -14,6 +14,7 @@ import { isTrustedFrameMessage } from "./frame.js";
14
14
  import { RunStuckBanner } from "./RunStuckBanner.js";
15
15
  import { callAction } from "./use-action.js";
16
16
  import { useChangeVersion } from "./use-change-version.js";
17
+ import { CHAT_MODEL_SELECTION_CHANGED_EVENT } from "./use-chat-models.js";
17
18
  import { useChatThreads, } from "./use-chat-threads.js";
18
19
  import { cn } from "./utils.js";
19
20
  /** The single path that hands a queued send to a mounted chat ref. */
@@ -66,6 +67,11 @@ function writeStoredModelSelection(key, selection) {
66
67
  return;
67
68
  try {
68
69
  window.localStorage.setItem(key, JSON.stringify(selection));
70
+ queueMicrotask(() => {
71
+ window.dispatchEvent(new CustomEvent(CHAT_MODEL_SELECTION_CHANGED_EVENT, {
72
+ detail: { key },
73
+ }));
74
+ });
69
75
  }
70
76
  catch { }
71
77
  }
@@ -520,6 +526,35 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
520
526
  const bumpModelSelectionVersion = useCallback(() => {
521
527
  setModelSelectionVersion((version) => version + 1);
522
528
  }, []);
529
+ useEffect(() => {
530
+ if (typeof window === "undefined")
531
+ return;
532
+ const syncPersistedSelection = (event) => {
533
+ const detail = event
534
+ ?.detail;
535
+ if (detail?.key && detail.key !== modelSelectionKey)
536
+ return;
537
+ const next = readStoredModelSelection(modelSelectionKey);
538
+ if (!next)
539
+ return;
540
+ const activeThreadId = activeThreadIdRef.current;
541
+ if (activeThreadId) {
542
+ threadModelRef.current.set(activeThreadId, next);
543
+ }
544
+ setPersistedModelSelection(next);
545
+ bumpModelSelectionVersion();
546
+ };
547
+ const handleStorage = (event) => {
548
+ if (event.key === modelSelectionKey)
549
+ syncPersistedSelection();
550
+ };
551
+ window.addEventListener(CHAT_MODEL_SELECTION_CHANGED_EVENT, syncPersistedSelection);
552
+ window.addEventListener("storage", handleStorage);
553
+ return () => {
554
+ window.removeEventListener(CHAT_MODEL_SELECTION_CHANGED_EVENT, syncPersistedSelection);
555
+ window.removeEventListener("storage", handleStorage);
556
+ };
557
+ }, [bumpModelSelectionVersion, modelSelectionKey]);
523
558
  const postMessageSubmissionsDisabled = props.composerDisabled === true;
524
559
  const setContextInTab = useCallback((threadId, item, options) => {
525
560
  const ref = chatRefs.current.get(threadId);