@agent-native/core 0.180.0-nightly-20260915184036 → 0.180.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +10 -0
  3. package/corpus/templates/calendar/CHANGELOG.md +39 -0
  4. package/corpus/templates/calendar/INTERACTION-MATRIX.md +19 -17
  5. package/corpus/templates/calendar/actions/update-calendar-visual-preferences.ts +21 -0
  6. package/corpus/templates/calendar/actions/view-screen.ts +10 -2
  7. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +11 -0
  8. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +174 -145
  9. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +20 -16
  10. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +1 -0
  11. package/corpus/templates/calendar/app/hooks/use-events.ts +16 -0
  12. package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +126 -14
  13. package/corpus/templates/calendar/app/i18n-data.ts +90 -0
  14. package/corpus/templates/calendar/app/lib/calendar-navigation.ts +29 -9
  15. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +2 -0
  16. package/corpus/templates/calendar/app/lib/event-time-range.ts +34 -0
  17. package/corpus/templates/calendar/app/pages/CalendarView.tsx +245 -35
  18. package/corpus/templates/calendar/changelog/2026-09-13-calendar-sidebar-overflow-is-easier-to-discover.md +6 -0
  19. package/corpus/templates/calendar/changelog/2026-09-15-calendar-mirrors-notion-calendar-s-customizable-day-ranges-a.md +6 -0
  20. package/corpus/templates/calendar/shared/calendar-view-preferences.ts +36 -0
  21. package/corpus/templates/calendar/shared/timezone.ts +9 -2
  22. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +2 -0
  23. package/corpus/templates/clips/CHANGELOG.md +19 -0
  24. package/corpus/templates/clips/actions/save-browser-diagnostics.ts +43 -4
  25. package/corpus/templates/clips/app/components/player/browser-diagnostics-panel.tsx +114 -1
  26. package/corpus/templates/clips/app/i18n/en-US.ts +7 -0
  27. package/corpus/templates/clips/app/lib/browser-diagnostics-capture.ts +108 -0
  28. package/corpus/templates/clips/app/routes/record.tsx +1 -0
  29. package/corpus/templates/clips/changelog/2026-09-15-recordings-include-a-structured-timeline-of-browser-interact.md +6 -0
  30. package/corpus/templates/clips/chrome-extension/public/manifest.json +7 -0
  31. package/corpus/templates/clips/chrome-extension/src/background.ts +235 -9
  32. package/corpus/templates/clips/chrome-extension/src/content-history-bridge.ts +80 -0
  33. package/corpus/templates/clips/chrome-extension/src/content-script.ts +166 -5
  34. package/corpus/templates/clips/chrome-extension/vite.config.ts +4 -0
  35. package/corpus/templates/clips/server/db/schema.ts +3 -0
  36. package/corpus/templates/clips/server/lib/public-agent-context.ts +40 -2
  37. package/corpus/templates/clips/server/plugins/db.ts +5 -0
  38. package/corpus/templates/clips/shared/browser-diagnostics.ts +171 -0
  39. package/corpus/templates/content/.agents/skills/content/SKILL.md +4 -2
  40. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +8 -1
  41. package/corpus/templates/content/AGENTS.md +5 -6
  42. package/corpus/templates/content/CHANGELOG.md +24 -0
  43. package/corpus/templates/content/actions/_content-database-row-migration.ts +67 -36
  44. package/corpus/templates/content/actions/_document-edit-mutation.ts +74 -23
  45. package/corpus/templates/content/actions/edit-document.ts +48 -8
  46. package/corpus/templates/content/actions/migrate-content-database-rows.ts +69 -28
  47. package/corpus/templates/content/server/plugins/agent-chat.ts +1 -1
  48. package/corpus/templates/design/CHANGELOG.md +22 -0
  49. package/corpus/templates/factory/CHANGELOG.md +11 -0
  50. package/corpus/templates/forms/CHANGELOG.md +6 -0
  51. package/corpus/templates/mail/.agents/skills/mail-interaction-parity/references/interaction-matrix.md +24 -4
  52. package/corpus/templates/mail/CHANGELOG.md +68 -0
  53. package/corpus/templates/mail/app/components/email/ComposeBubbleToolbar.tsx +8 -0
  54. package/corpus/templates/mail/app/components/email/ComposeModal.tsx +33 -13
  55. package/corpus/templates/mail/app/components/email/EmailListItem.tsx +23 -4
  56. package/corpus/templates/mail/app/components/email/EmailThread.tsx +56 -13
  57. package/corpus/templates/mail/app/components/email/InlineReplyComposer.tsx +23 -4
  58. package/corpus/templates/mail/app/components/email/RecipientInput.tsx +9 -0
  59. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +8 -1
  60. package/corpus/templates/mail/app/i18n/en-US.ts +31 -0
  61. package/corpus/templates/mail/changelog/2026-09-15-mail-labels-recipient-and-account-controls-for-keyboard-and-.md +6 -0
  62. package/corpus/templates/slides/CHANGELOG.md +32 -0
  63. package/dist/client/CommandMenu.js +14 -2
  64. package/dist/client/settings/AccountSettingsCard.js +3 -2
  65. package/dist/client/settings/SettingsTabsPage.js +9 -1
  66. package/dist/client/settings/agent-settings-search.js +5 -1
  67. package/dist/client/sign-out.d.ts +1 -0
  68. package/dist/client/sign-out.js +8 -0
  69. package/dist/collab/routes.d.ts +1 -1
  70. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  71. package/dist/observability/routes.d.ts +3 -3
  72. package/dist/org/handlers.d.ts +1 -1
  73. package/dist/resources/handlers.d.ts +1 -1
  74. package/dist/secrets/routes.d.ts +3 -3
  75. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  76. package/dist/server/transcribe-voice.d.ts +1 -1
  77. package/package.json +4 -4
@@ -22,6 +22,148 @@
22
22
  flags.__clipsOverlayHostReady = true;
23
23
  let recordingActive = false;
24
24
 
25
+ type InteractionKind = "navigation" | "click" | "input" | "scroll";
26
+ const HISTORY_NAVIGATION_DEDUPE_MS = 250;
27
+ const HISTORY_NAVIGATION_WINDOW_MS = 1000;
28
+ const MAX_HISTORY_NAVIGATION_MESSAGES_PER_WINDOW = 20;
29
+ const CLICK_INPUT_WINDOW_MS = 1000;
30
+ const MAX_CLICK_INPUT_MESSAGES_PER_WINDOW = 100;
31
+ let historyBridgeToken: string | null = null;
32
+ let historyNavigationWindowStartedAt = 0;
33
+ let historyNavigationCount = 0;
34
+ let clickInputWindowStartedAt = 0;
35
+ let clickInputCount = 0;
36
+ let lastHistoryNavigation: { url: string; sentAtMs: number } | null = null;
37
+ const OVERLAY_ROOT_ID = "clips-recorder-overlay-root";
38
+
39
+ function resetDiagnosticQuotas(): void {
40
+ historyNavigationWindowStartedAt = 0;
41
+ historyNavigationCount = 0;
42
+ clickInputWindowStartedAt = 0;
43
+ clickInputCount = 0;
44
+ lastHistoryNavigation = null;
45
+ }
46
+
47
+ function targetDescriptor(target: EventTarget | null): string | undefined {
48
+ if (!(target instanceof Element)) return undefined;
49
+ if (target.closest(`#${OVERLAY_ROOT_ID}`)) return undefined;
50
+ const element = target.closest(
51
+ "button,a,input,textarea,select,[role=button]",
52
+ ) as Element | null;
53
+ const candidate = element ?? target;
54
+ const tag = candidate.tagName.toLowerCase();
55
+ const id = candidate.id;
56
+ const testId = candidate.getAttribute("data-testid");
57
+ const name = candidate.getAttribute("name");
58
+ const part = [id, testId, name].find((value): value is string => {
59
+ if (!value || value.length > 80) return false;
60
+ return /^[A-Za-z0-9_.:-]+$/.test(value);
61
+ });
62
+ return `${tag}${part ? `#${part}` : ""}`.slice(0, 200);
63
+ }
64
+
65
+ function sendDiagnosticInteraction(
66
+ kind: InteractionKind,
67
+ target: EventTarget | null = null,
68
+ url?: string,
69
+ ): void {
70
+ if (!recordingActive) return;
71
+ if (kind === "click" || kind === "input") {
72
+ const now = Date.now();
73
+ if (now - clickInputWindowStartedAt >= CLICK_INPUT_WINDOW_MS) {
74
+ clickInputWindowStartedAt = now;
75
+ clickInputCount = 0;
76
+ }
77
+ if (clickInputCount >= MAX_CLICK_INPUT_MESSAGES_PER_WINDOW) return;
78
+ clickInputCount += 1;
79
+ }
80
+ try {
81
+ chrome.runtime.sendMessage(
82
+ {
83
+ type: "CLIPS_DIAGNOSTIC_INTERACTION",
84
+ kind,
85
+ target: targetDescriptor(target),
86
+ ...(url ? { url } : {}),
87
+ },
88
+ () => void chrome.runtime.lastError,
89
+ );
90
+ // coercion-ok: the extension context can disappear after page unload; capture is best-effort
91
+ } catch {
92
+ /* background unavailable */
93
+ }
94
+ }
95
+
96
+ function sendDiagnosticNavigation(url: string): void {
97
+ if (!recordingActive) return;
98
+ const now = Date.now();
99
+ if (
100
+ lastHistoryNavigation?.url === url &&
101
+ now - lastHistoryNavigation.sentAtMs < HISTORY_NAVIGATION_DEDUPE_MS
102
+ ) {
103
+ return;
104
+ }
105
+ if (
106
+ now - historyNavigationWindowStartedAt >=
107
+ HISTORY_NAVIGATION_WINDOW_MS
108
+ ) {
109
+ historyNavigationWindowStartedAt = now;
110
+ historyNavigationCount = 0;
111
+ }
112
+ if (historyNavigationCount >= MAX_HISTORY_NAVIGATION_MESSAGES_PER_WINDOW) {
113
+ return;
114
+ }
115
+ historyNavigationCount += 1;
116
+ lastHistoryNavigation = { url, sentAtMs: now };
117
+ sendDiagnosticInteraction("navigation", null, url);
118
+ }
119
+
120
+ window.addEventListener("message", (event) => {
121
+ if (event.source !== window) return;
122
+ const data = event.data as
123
+ | { source?: unknown; kind?: unknown; token?: unknown; url?: unknown }
124
+ | undefined;
125
+ if (
126
+ data?.source === "clips-diagnostic-history" &&
127
+ data.kind === "token" &&
128
+ typeof data.token === "string"
129
+ ) {
130
+ historyBridgeToken = data.token;
131
+ return;
132
+ }
133
+ if (!recordingActive) return;
134
+ if (
135
+ data?.source !== "clips-diagnostic-history" ||
136
+ data.kind !== "navigation" ||
137
+ data.token !== historyBridgeToken ||
138
+ typeof data.url !== "string"
139
+ ) {
140
+ return;
141
+ }
142
+ sendDiagnosticNavigation(data.url);
143
+ });
144
+ window.postMessage(
145
+ { source: "clips-diagnostic-history", kind: "request-token" },
146
+ "*",
147
+ );
148
+
149
+ let lastScrollAt = 0;
150
+ const onClick = (event: MouseEvent) =>
151
+ sendDiagnosticInteraction("click", event.target);
152
+ const onInput = (event: Event) =>
153
+ sendDiagnosticInteraction("input", event.target);
154
+ const onScroll = (event: Event) => {
155
+ const now = Date.now();
156
+ if (now - lastScrollAt < 250) return;
157
+ lastScrollAt = now;
158
+ sendDiagnosticInteraction("scroll", event.target);
159
+ };
160
+ const onNavigation = () => sendDiagnosticNavigation(window.location.href);
161
+ document.addEventListener("click", onClick, true);
162
+ document.addEventListener("input", onInput, true);
163
+ document.addEventListener("scroll", onScroll, true);
164
+ window.addEventListener("popstate", onNavigation);
165
+ window.addEventListener("hashchange", onNavigation);
166
+
25
167
  function errorPayload(error: unknown): {
26
168
  name: string;
27
169
  message: string;
@@ -82,7 +224,12 @@
82
224
  try {
83
225
  chrome.storage.onChanged.addListener((changes, areaName) => {
84
226
  if (areaName !== "local" || !changes.clipsRecordingActive) return;
85
- recordingActive = changes.clipsRecordingActive.newValue === true;
227
+ const nextRecordingActive =
228
+ changes.clipsRecordingActive.newValue === true;
229
+ if (nextRecordingActive && !recordingActive) {
230
+ resetDiagnosticQuotas();
231
+ }
232
+ recordingActive = nextRecordingActive;
86
233
  });
87
234
  } catch {
88
235
  /* storage unavailable */
@@ -354,8 +501,15 @@
354
501
  try {
355
502
  chrome.storage.local.get("clipsRecordingActive", (value) => {
356
503
  if (chrome.runtime.lastError) return;
357
- recordingActive = value?.clipsRecordingActive === true;
358
- if (recordingActive) requestState();
504
+ const nextRecordingActive = value?.clipsRecordingActive === true;
505
+ if (nextRecordingActive && !recordingActive) {
506
+ resetDiagnosticQuotas();
507
+ }
508
+ recordingActive = nextRecordingActive;
509
+ if (recordingActive) {
510
+ sendDiagnosticNavigation(window.location.href);
511
+ requestState();
512
+ }
359
513
  });
360
514
  } catch {
361
515
  /* ignore */
@@ -554,9 +708,12 @@
554
708
  });
555
709
  }
556
710
 
557
- function reconcile(parts: OverlayPart[]): void {
711
+ function reconcile(parts: OverlayPart[], resetQuotas = false): void {
558
712
  console.log("[clips-cs] reconcile parts:", parts, "on", location.href);
559
713
  const wanted = new Set(parts.filter((p) => ALL_PARTS.includes(p)));
714
+ const enteringRecording =
715
+ wanted.has("toolbar") && !lastWantedParts.has("toolbar");
716
+ if (resetQuotas || enteringRecording) resetDiagnosticQuotas();
560
717
  const enteringCameraCountdown =
561
718
  wanted.has("countdown") &&
562
719
  wanted.has("bubble") &&
@@ -614,7 +771,11 @@
614
771
  const type = (message as { type?: unknown }).type;
615
772
  if (type === "CLIPS_OVERLAY_MOUNT") {
616
773
  const parts = (message as { parts?: unknown }).parts;
617
- reconcile(Array.isArray(parts) ? (parts as OverlayPart[]) : []);
774
+ reconcile(
775
+ Array.isArray(parts) ? (parts as OverlayPart[]) : [],
776
+ (message as { resetDiagnosticQuotas?: unknown })
777
+ .resetDiagnosticQuotas === true,
778
+ );
618
779
  } else if (type === "CLIPS_OVERLAY_UNMOUNT") {
619
780
  reconcile([]);
620
781
  }
@@ -68,6 +68,10 @@ export default defineConfig({
68
68
  rollupOptions: {
69
69
  input: {
70
70
  background: resolve(root, "src/background.ts"),
71
+ "content-history-bridge": resolve(
72
+ root,
73
+ "src/content-history-bridge.ts",
74
+ ),
71
75
  "content-script": resolve(root, "src/content-script.ts"),
72
76
  "github-preview-content": resolve(
73
77
  root,
@@ -305,6 +305,9 @@ export const recordingBrowserDiagnostics = table(
305
305
  endedAt: text("ended_at").notNull(),
306
306
  consoleLogsJson: text("console_logs_json").notNull().default("[]"),
307
307
  networkRequestsJson: text("network_requests_json").notNull().default("[]"),
308
+ interactionEventsJson: text("interaction_events_json")
309
+ .notNull()
310
+ .default("[]"),
308
311
  redactionVersion: integer("redaction_version").notNull().default(1),
309
312
  createdAt: text("created_at").notNull().default(now()),
310
313
  updatedAt: text("updated_at").notNull().default(now()),
@@ -21,7 +21,9 @@ import {
21
21
  } from "../../shared/agent-context.js";
22
22
  import {
23
23
  parseBrowserDiagnosticsRow,
24
+ sanitizeBrowserDiagnosticNavigationUrl,
24
25
  type BrowserDiagnosticsData,
26
+ type BrowserDiagnosticTimelineEvent,
25
27
  } from "../../shared/browser-diagnostics.js";
26
28
  import {
27
29
  isLoomEmbedBackedRecording,
@@ -414,6 +416,7 @@ export async function loadAgentBugReport(
414
416
  // the true total counts.
415
417
  const MAX_PUBLIC_AGENT_CONSOLE_LOGS = 100;
416
418
  const MAX_PUBLIC_AGENT_NETWORK_REQUESTS = 100;
419
+ const MAX_PUBLIC_AGENT_TIMELINE_EVENTS = 100;
417
420
  // Curated warn/error highlights and failed-request highlights.
418
421
  const MAX_PUBLIC_AGENT_DIAGNOSTIC_ISSUES = 20;
419
422
 
@@ -443,6 +446,37 @@ function toPublicNetworkEntry(
443
446
  };
444
447
  }
445
448
 
449
+ function toPublicTimelineEntry(entry: BrowserDiagnosticTimelineEvent) {
450
+ const base = {
451
+ timestampMs: entry.elapsedMs,
452
+ kind: entry.kind,
453
+ };
454
+ if (entry.kind === "console") {
455
+ return {
456
+ ...base,
457
+ level: entry.level,
458
+ message: entry.message,
459
+ };
460
+ }
461
+ if (entry.kind === "network") {
462
+ return {
463
+ ...base,
464
+ phase: entry.phase,
465
+ type: entry.type,
466
+ method: entry.method,
467
+ url: entry.url,
468
+ status: entry.status ?? null,
469
+ error: entry.error ?? null,
470
+ durationMs: entry.durationMs ?? null,
471
+ };
472
+ }
473
+ return {
474
+ ...base,
475
+ target: entry.target ?? null,
476
+ url: entry.url ? sanitizeBrowserDiagnosticNavigationUrl(entry.url) : null,
477
+ };
478
+ }
479
+
446
480
  function isFailedNetworkRequest(
447
481
  entry: BrowserDiagnosticsData["networkRequests"][number],
448
482
  ): boolean {
@@ -474,13 +508,17 @@ function compactBrowserDiagnostics(diagnostics: BrowserDiagnosticsData | null) {
474
508
  .filter(isFailedNetworkRequest)
475
509
  .slice(-MAX_PUBLIC_AGENT_DIAGNOSTIC_ISSUES)
476
510
  .map(toPublicNetworkEntry);
511
+ const timeline = (diagnostics.timeline ?? [])
512
+ .slice(-MAX_PUBLIC_AGENT_TIMELINE_EVENTS)
513
+ .map(toPublicTimelineEntry);
477
514
  return {
478
515
  summary: diagnostics.summary,
516
+ timeline,
479
517
  consoleLogs,
480
518
  consoleIssues,
481
519
  networkRequests,
482
520
  failedNetworkRequests,
483
- note: "Console logs include all levels (debug/log/info/warn/error). Network requests include method, sanitized URL (path kept, query values redacted), status, and duration. Diagnostics are bounded; the recording's own page URL, request headers, request/response bodies, and cookies are omitted.",
521
+ note: "Timeline entries are relative to recording start and include redacted navigation, click/input targets, console events, and request/response markers. Diagnostics are bounded; the recording's own page URL, request headers, request/response bodies, and cookies are omitted.",
484
522
  };
485
523
  }
486
524
 
@@ -613,7 +651,7 @@ export function buildPublicAgentContext({
613
651
  : []),
614
652
  ...(browserDiagnostics
615
653
  ? [
616
- "Use browserDiagnostics.consoleLogs for the redacted console stream (all levels: debug/log/info/warn/error) and browserDiagnostics.networkRequests for the fetch/XHR requests (method, sanitized URL, status, duration) captured during the recording. browserDiagnostics.consoleIssues highlights just the warnings/errors, and browserDiagnostics.failedNetworkRequests highlights failed requests.",
654
+ "Use browserDiagnostics.timeline for the bounded, relative event sequence: navigation, click/input targets, console events, and request/response markers. Use browserDiagnostics.consoleLogs and browserDiagnostics.networkRequests for the full redacted streams; consoleIssues and failedNetworkRequests are curated failure highlights.",
617
655
  ]
618
656
  : []),
619
657
  ...(!clipIsReady
@@ -1245,6 +1245,11 @@ export const migrations = runMigrations(
1245
1245
  CREATE INDEX IF NOT EXISTS clips_intake_sessions_expires_idx
1246
1246
  ON clips_intake_sessions (status, expires_at)`,
1247
1247
  },
1248
+ {
1249
+ version: 73,
1250
+ name: "recording-browser-diagnostics-interaction-events",
1251
+ sql: `ALTER TABLE recording_browser_diagnostics ADD COLUMN IF NOT EXISTS interaction_events_json TEXT NOT NULL DEFAULT '[]'`,
1252
+ },
1248
1253
  ],
1249
1254
  { table: "clips_migrations" },
1250
1255
  );
@@ -1,6 +1,12 @@
1
1
  export const MAX_BROWSER_DIAGNOSTIC_CONSOLE_LOGS = 400;
2
2
  export const MAX_BROWSER_DIAGNOSTIC_NETWORK_REQUESTS = 400;
3
+ export const MAX_BROWSER_DIAGNOSTIC_INTERACTION_EVENTS = 800;
4
+ export const MAX_BROWSER_DIAGNOSTIC_TIMELINE_EVENTS =
5
+ MAX_BROWSER_DIAGNOSTIC_CONSOLE_LOGS +
6
+ MAX_BROWSER_DIAGNOSTIC_NETWORK_REQUESTS * 2 +
7
+ MAX_BROWSER_DIAGNOSTIC_INTERACTION_EVENTS;
3
8
  export const MAX_BROWSER_DIAGNOSTIC_MESSAGE_LENGTH = 2_000;
9
+ export const MAX_BROWSER_DIAGNOSTIC_TARGET_LENGTH = 200;
4
10
  export const MAX_BROWSER_DIAGNOSTIC_URL_LENGTH = 1_000;
5
11
 
6
12
  const SECRET_KEY_FRAGMENT =
@@ -40,6 +46,29 @@ export function redactBrowserDiagnosticString(
40
46
  : redacted;
41
47
  }
42
48
 
49
+ export function sanitizeBrowserDiagnosticNavigationUrl(raw: string): string {
50
+ const redacted = redactBrowserDiagnosticString(raw, {
51
+ redactQueryValues: true,
52
+ });
53
+ try {
54
+ const parsed = new URL(redacted, "https://clips.local");
55
+ parsed.username = "";
56
+ parsed.password = "";
57
+ parsed.hash = "";
58
+ const params = new URLSearchParams();
59
+ for (const key of parsed.searchParams.keys()) {
60
+ params.set(key, "<redacted>");
61
+ }
62
+ parsed.search = params.toString();
63
+ return `${parsed.pathname}${parsed.search}`.slice(
64
+ 0,
65
+ MAX_BROWSER_DIAGNOSTIC_URL_LENGTH,
66
+ );
67
+ } catch {
68
+ return "<redacted>";
69
+ }
70
+ }
71
+
43
72
  export type BrowserDiagnosticConsoleLevel =
44
73
  | "debug"
45
74
  | "log"
@@ -68,6 +97,50 @@ export interface BrowserDiagnosticNetworkRequest {
68
97
  error?: string;
69
98
  }
70
99
 
100
+ export type BrowserDiagnosticInteractionKind =
101
+ | "navigation"
102
+ | "click"
103
+ | "input"
104
+ | "scroll";
105
+
106
+ export interface BrowserDiagnosticInteractionEvent {
107
+ timestampMs: number;
108
+ elapsedMs: number;
109
+ kind: BrowserDiagnosticInteractionKind;
110
+ target?: string;
111
+ url?: string;
112
+ }
113
+
114
+ type BrowserDiagnosticTimelineBase = Pick<
115
+ BrowserDiagnosticInteractionEvent,
116
+ "timestampMs" | "elapsedMs"
117
+ >;
118
+
119
+ export type BrowserDiagnosticTimelineEvent =
120
+ | (BrowserDiagnosticTimelineBase & {
121
+ kind: BrowserDiagnosticInteractionKind;
122
+ target?: string;
123
+ url?: string;
124
+ })
125
+ | (BrowserDiagnosticTimelineBase & {
126
+ kind: "console";
127
+ level: BrowserDiagnosticConsoleLevel;
128
+ message: string;
129
+ stack?: string;
130
+ })
131
+ | (BrowserDiagnosticTimelineBase & {
132
+ kind: "network";
133
+ phase: "request" | "response";
134
+ type: BrowserDiagnosticNetworkRequest["type"];
135
+ method: string;
136
+ url: string;
137
+ status?: number;
138
+ statusText?: string;
139
+ ok?: boolean;
140
+ durationMs?: number;
141
+ error?: string;
142
+ });
143
+
71
144
  export interface BrowserDiagnosticsSnapshot {
72
145
  pageUrl: string | null;
73
146
  userAgent: string | null;
@@ -75,6 +148,7 @@ export interface BrowserDiagnosticsSnapshot {
75
148
  endedAt: string;
76
149
  consoleLogs: BrowserDiagnosticConsoleLog[];
77
150
  networkRequests: BrowserDiagnosticNetworkRequest[];
151
+ interactionEvents?: BrowserDiagnosticInteractionEvent[];
78
152
  }
79
153
 
80
154
  export interface BrowserDiagnosticsSummary {
@@ -88,6 +162,7 @@ export interface BrowserDiagnosticsSummary {
88
162
 
89
163
  export interface BrowserDiagnosticsData extends BrowserDiagnosticsSnapshot {
90
164
  summary: BrowserDiagnosticsSummary;
165
+ timeline?: BrowserDiagnosticTimelineEvent[];
91
166
  }
92
167
 
93
168
  function safeArray(value: string | null | undefined): unknown[] {
@@ -176,6 +251,93 @@ export function normalizeBrowserDiagnosticNetworkRequest(
176
251
  };
177
252
  }
178
253
 
254
+ export function normalizeBrowserDiagnosticInteractionEvent(
255
+ value: unknown,
256
+ ): BrowserDiagnosticInteractionEvent | null {
257
+ if (!value || typeof value !== "object") return null;
258
+ const entry = value as Record<string, unknown>;
259
+ const timestampMs = safeNumber(entry.timestampMs);
260
+ const elapsedMs = safeNumber(entry.elapsedMs);
261
+ const kind =
262
+ entry.kind === "navigation" ||
263
+ entry.kind === "click" ||
264
+ entry.kind === "input" ||
265
+ entry.kind === "scroll"
266
+ ? entry.kind
267
+ : null;
268
+ if (timestampMs === null || elapsedMs === null || !kind) return null;
269
+ const target = safeString(entry.target, MAX_BROWSER_DIAGNOSTIC_TARGET_LENGTH);
270
+ const rawUrl = safeString(entry.url, MAX_BROWSER_DIAGNOSTIC_URL_LENGTH);
271
+ const url = rawUrl ? sanitizeBrowserDiagnosticNavigationUrl(rawUrl) : null;
272
+ return {
273
+ timestampMs,
274
+ elapsedMs,
275
+ kind,
276
+ ...(target ? { target } : {}),
277
+ ...(url ? { url } : {}),
278
+ };
279
+ }
280
+
281
+ export function buildBrowserDiagnosticTimeline({
282
+ consoleLogs,
283
+ networkRequests,
284
+ interactionEvents = [],
285
+ }: Pick<
286
+ BrowserDiagnosticsSnapshot,
287
+ "consoleLogs" | "networkRequests" | "interactionEvents"
288
+ >): BrowserDiagnosticTimelineEvent[] {
289
+ const entries: Array<{
290
+ event: BrowserDiagnosticTimelineEvent;
291
+ order: number;
292
+ }> = [];
293
+ let order = 0;
294
+ const add = (event: BrowserDiagnosticTimelineEvent) => {
295
+ entries.push({ event, order: order++ });
296
+ };
297
+
298
+ for (const event of interactionEvents) add(event);
299
+ for (const entry of consoleLogs) {
300
+ add({
301
+ timestampMs: entry.timestampMs,
302
+ elapsedMs: entry.elapsedMs,
303
+ kind: "console",
304
+ level: entry.level,
305
+ message: entry.message,
306
+ ...(entry.stack ? { stack: entry.stack } : {}),
307
+ });
308
+ }
309
+ for (const entry of networkRequests) {
310
+ add({
311
+ timestampMs: entry.timestampMs,
312
+ elapsedMs: entry.elapsedMs,
313
+ kind: "network",
314
+ phase: "request",
315
+ type: entry.type,
316
+ method: entry.method,
317
+ url: entry.url,
318
+ });
319
+ add({
320
+ timestampMs: entry.timestampMs + entry.durationMs,
321
+ elapsedMs: entry.elapsedMs + entry.durationMs,
322
+ kind: "network",
323
+ phase: "response",
324
+ type: entry.type,
325
+ method: entry.method,
326
+ url: entry.url,
327
+ ...(typeof entry.status === "number" ? { status: entry.status } : {}),
328
+ ...(entry.statusText ? { statusText: entry.statusText } : {}),
329
+ ...(typeof entry.ok === "boolean" ? { ok: entry.ok } : {}),
330
+ durationMs: entry.durationMs,
331
+ ...(entry.error ? { error: entry.error } : {}),
332
+ });
333
+ }
334
+
335
+ return entries
336
+ .sort((a, b) => a.event.elapsedMs - b.event.elapsedMs || a.order - b.order)
337
+ .slice(0, MAX_BROWSER_DIAGNOSTIC_TIMELINE_EVENTS)
338
+ .map(({ event }) => event);
339
+ }
340
+
179
341
  export function summarizeBrowserDiagnostics(
180
342
  snapshot: Pick<
181
343
  BrowserDiagnosticsSnapshot,
@@ -209,6 +371,7 @@ export function parseBrowserDiagnosticsRow(
209
371
  endedAt?: string | null;
210
372
  consoleLogsJson?: string | null;
211
373
  networkRequestsJson?: string | null;
374
+ interactionEventsJson?: string | null;
212
375
  }
213
376
  | null
214
377
  | undefined,
@@ -222,6 +385,12 @@ export function parseBrowserDiagnosticsRow(
222
385
  .map(normalizeBrowserDiagnosticNetworkRequest)
223
386
  .filter((entry): entry is BrowserDiagnosticNetworkRequest => Boolean(entry))
224
387
  .slice(0, MAX_BROWSER_DIAGNOSTIC_NETWORK_REQUESTS);
388
+ const interactionEvents = safeArray(row.interactionEventsJson)
389
+ .map(normalizeBrowserDiagnosticInteractionEvent)
390
+ .filter((entry): entry is BrowserDiagnosticInteractionEvent =>
391
+ Boolean(entry),
392
+ )
393
+ .slice(0, MAX_BROWSER_DIAGNOSTIC_INTERACTION_EVENTS);
225
394
  const snapshot: BrowserDiagnosticsSnapshot = {
226
395
  pageUrl: row.pageUrl ?? null,
227
396
  userAgent: row.userAgent ?? null,
@@ -229,9 +398,11 @@ export function parseBrowserDiagnosticsRow(
229
398
  endedAt: row.endedAt ?? "",
230
399
  consoleLogs,
231
400
  networkRequests,
401
+ interactionEvents,
232
402
  };
233
403
  return {
234
404
  ...snapshot,
235
405
  summary: summarizeBrowserDiagnostics(snapshot),
406
+ timeline: buildBrowserDiagnosticTimeline(snapshot),
236
407
  };
237
408
  }
@@ -33,8 +33,10 @@ actions, and local folders that sync into the same collection model.
33
33
  `share-local-file-document`, `list-local-component-files`, and
34
34
  `write-local-component-file`.
35
35
  - Use `pull-document` or `get-document` before editing a page. Use
36
- `edit-document` for precise find/replace changes and `update-document` for
37
- full rewrites or new content.
36
+ `edit-document` for revision-guarded body changes. When `get-document`
37
+ returns a literally empty body, initialize it with `initializeContent`, the
38
+ returned `baseRevision`, and a stable `idempotencyKey`. Browser-owned full
39
+ rewrites and metadata changes use `update-document`.
38
40
  - Use `list-documents` or `search-documents` to find an existing collection by
39
41
  its exact title, then inspect it with `get-content-database` before creating
40
42
  or submitting anything. Do not create a second collection when the canonical
@@ -99,9 +99,16 @@ pnpm action edit-document --id abc123 --find "delete me" --replace ""
99
99
  pnpm action edit-document --id abc123 --edits '[{"find":"old","replace":"new"},{"find":"also old","replace":"also new"}]'
100
100
  ```
101
101
 
102
+ External MCP, WebMCP, tool, and A2A callers first read the document, then pass
103
+ its `baseRevision` and one stable `idempotencyKey`. When the returned body is
104
+ literally empty, pass non-whitespace `initializeContent` instead of `find` or `edits`.
105
+ Initialization rejects whitespace-only and all other nonempty bodies, preserves
106
+ the Markdown bytes exactly, and safely replays an identical retry.
107
+
102
108
  ### update-document
103
109
 
104
- Update an existing document. Use for **full rewrites or new content**, not for small changes (use `edit-document` instead).
110
+ Update an existing document's metadata or browser-owned content. External
111
+ callers use the revisioned `edit-document` protocol for every body change.
105
112
 
106
113
  ```bash
107
114
  pnpm action update-document --id abc123 --title "New Title"
@@ -25,10 +25,9 @@ Read the relevant skill before deeper work:
25
25
  and access checks are preserved. Never use `curl`, raw HTTP requests, or
26
26
  `db-exec` with raw SQL for document operations.
27
27
  - Call these actions directly; `ask_app` only delegates to Content's agent.
28
- - The editor uses live Yjs collaboration — raw SQL writes to `documents` won't
29
- appear in an open editor. Always use `edit-document` or `update-document`,
30
- and prefer `edit-document` for small changes (it sends only the changed text
31
- and syncs live via CRDT instead of regenerating the whole document).
28
+ - The live Yjs editor requires actions for body writes. External agents use
29
+ revisioned `edit-document`, with `initializeContent` only for an empty body.
30
+ Browser full rewrites use `update-document`.
32
31
  - Preserve user-authored content. Prefer targeted edits over wholesale rewrites
33
32
  unless requested.
34
33
  - `create-document`, `update-document`, and `delete-document` already signal
@@ -80,8 +79,8 @@ Read the relevant skill before deeper work:
80
79
  | `get-blocks-field-word-count` | Count one exact Blocks field; omit `propertyId` for the primary Content body |
81
80
  | `create-document` | Create a page, optionally under a parent |
82
81
  | `resolve-content-landing` | Restore the caller's last authorized page |
83
- | `edit-document` | Find/replace edit — preferred for small changes |
84
- | `update-document` | Full rewrite of title, content, or description |
82
+ | `edit-document` | Revisioned find/replace, or initialize an empty body |
83
+ | `update-document` | Metadata or browser-owned full rewrite |
85
84
  | `delete-document` | Move a page and its children to Trash |
86
85
  | `list-content-database-blocks` | List stable blocks and revisions in one exact collection row/property |
87
86
  | `mutate-content-database-block` | Insert, update, upsert, delete, or reorder one supported stable block |
@@ -5,14 +5,34 @@ time from the command menu (Cmd+K → "What's new").
5
5
 
6
6
  ## 2026-09-14
7
7
 
8
+ ### Improved
9
+
10
+ - Search is now available from the Content sidebar, with clearer date filters and keyboard-friendly calendar navigation.
11
+
8
12
  ### Fixed
9
13
 
14
+ - Pages created in agent conversations now appear in navigation and open when they are ready.
15
+ - Cmd+K now opens the command menu while writing in a page.
10
16
  - The block drag-handle menu now always opens beside the block instead of sometimes jumping to the corner of the window.
11
17
  - Pasting a checklist from Notion or GitHub now keeps its checkboxes instead of turning into plain bullets.
12
18
  - Asking the agent about Notion no longer reports "not connected" when the Notion MCP server is connected — it now names the account connection and the MCP connection separately.
13
19
 
20
+ ## 2026-09-13
21
+
22
+ ### Fixed
23
+
24
+ - Suggestion previews and highlights stay aligned when editing text near lists.
25
+
14
26
  ## 2026-09-12
15
27
 
28
+ ### Improved
29
+
30
+ - Search with quoted phrases, exclusions, OR, and intitle: operators, search all workspaces, pick a modified date from the calendar, and see snippets anchored at the matching sentence deep inside long pages.
31
+
32
+ ### Fixed
33
+
34
+ - Setting up Notion from a document now opens the Integrations settings, where Notion can actually be connected.
35
+
16
36
  ### Changed
17
37
 
18
38
  - Databases are now called **collections** across Content — same pages, views, and sharing, friendlier name.
@@ -29,6 +49,10 @@ time from the command menu (Cmd+K → "What's new").
29
49
 
30
50
  - Connected agents can set up ordinary databases, configure fields and table views, and recover them from Trash with verified results.
31
51
 
52
+ ### Improved
53
+
54
+ - Search pages and databases with scoped filters, result previews, and keyboard navigation across result pages.
55
+
32
56
  ### Fixed
33
57
 
34
58
  - Sidebar Search opens the command picker, icon controls have accessible names, and the sidebar can be resized with the keyboard.