@agent-native/core 0.114.8 → 0.114.10

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 (128) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +21 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +31 -3
  5. package/corpus/core/src/agent/engine/anthropic-engine.ts +20 -3
  6. package/corpus/core/src/agent/engine/builder-engine.ts +56 -9
  7. package/corpus/core/src/agent/engine/first-event-timeout.ts +64 -0
  8. package/corpus/core/src/client/AssistantChat.tsx +7 -3
  9. package/corpus/core/src/client/resources/ResourcesPanel.tsx +1 -1
  10. package/corpus/core/src/integrations/computer-supervision-store.ts +4 -4
  11. package/corpus/core/src/integrations/pending-tasks-store.ts +1 -1
  12. package/corpus/core/src/integrations/remote-commands-store.ts +5 -5
  13. package/corpus/core/src/integrations/remote-devices-store.ts +3 -3
  14. package/corpus/core/src/integrations/remote-push-store.ts +3 -3
  15. package/corpus/core/src/observability/traces.ts +38 -1
  16. package/corpus/core/src/server/action-routes.ts +23 -1
  17. package/corpus/core/src/server/http-response-telemetry.ts +6 -1
  18. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  19. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  20. package/corpus/templates/analytics/actions/account-deep-dive.ts +41 -6
  21. package/corpus/templates/analytics/changelog/2026-07-20-account-deep-dives-finish-with-partial-data-when-hubspot-lo.md +6 -0
  22. package/corpus/templates/analytics/changelog/2026-07-20-dashboard-email-reports-now-include-an-image-when-a-single-p.md +6 -0
  23. package/corpus/templates/analytics/server/lib/dashboard-report.ts +11 -3
  24. package/corpus/templates/calendar/actions/create-event.ts +5 -0
  25. package/corpus/templates/calendar/app/components/ThemeToggle.tsx +4 -1
  26. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +0 -1
  27. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +309 -421
  28. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +349 -0
  29. package/corpus/templates/calendar/app/i18n-data.ts +110 -0
  30. package/corpus/templates/calendar/app/lib/event-form-utils.ts +6 -0
  31. package/corpus/templates/calendar/app/pages/CalendarView.tsx +3 -0
  32. package/corpus/templates/calendar/app/root.tsx +4 -2
  33. package/corpus/templates/calendar/changelog/2026-07-20-events-can-be-updated-inline-with-date-time-timezone-and-rep.md +6 -0
  34. package/corpus/templates/calendar/changelog/2026-07-20-public-booking-pages-no-longer-hydrate-with-a-theme-mismatch.md +6 -0
  35. package/corpus/templates/calendar/shared/api.ts +1 -0
  36. package/corpus/templates/clips/AGENTS.md +7 -0
  37. package/corpus/templates/clips/actions/create-recording.ts +51 -10
  38. package/corpus/templates/clips/actions/get-recording-player-data.ts +34 -5
  39. package/corpus/templates/clips/actions/lib/transcript-preview.ts +48 -0
  40. package/corpus/templates/clips/actions/list-recordings.ts +15 -4
  41. package/corpus/templates/clips/actions/regenerate-summary.ts +32 -0
  42. package/corpus/templates/clips/actions/search-recordings.ts +28 -6
  43. package/corpus/templates/clips/actions/view-screen.ts +54 -47
  44. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -5
  45. package/corpus/templates/clips/app/components/library/recording-card.tsx +29 -32
  46. package/corpus/templates/clips/app/components/meetings/quick-ask-sidebar.tsx +13 -0
  47. package/corpus/templates/clips/changelog/2026-07-20-agent-summaries-use-bounded-transcript-context.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-20-clip-summaries-now-finish-without-sending-a-long-transcript-.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-20-clips-agents-only-discover-recordings-you-own-or-have-alread.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-20-hosted-recordings-no-longer-start-with-an-impossible-buffered-upload.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-20-meeting-recordings-now-offer-a-cleaner-transcript-view-with-.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-20-shared-clips-can-be-shared-with-limited-access-and-cards-sho.md +6 -0
  53. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +12 -8
  54. package/corpus/templates/clips/desktop/src/overlays/pill-logo.tsx +2 -15
  55. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +107 -340
  56. package/corpus/templates/clips/desktop/src/styles.css +27 -145
  57. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +6 -7
  58. package/corpus/templates/clips/server/lib/agent-recording-access.ts +73 -0
  59. package/corpus/templates/clips/server/plugins/agent-chat.ts +5 -2
  60. package/corpus/templates/plan/app/lib/plan-local-bridge.ts +18 -0
  61. package/corpus/templates/plan/app/pages/PlansPage.tsx +10 -11
  62. package/corpus/templates/plan/changelog/2026-07-20-bridged-local-plans-now-load-comments-from-the-local-workspace.md +6 -0
  63. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  64. package/dist/agent/engine/ai-sdk-engine.js +27 -4
  65. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  66. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  67. package/dist/agent/engine/anthropic-engine.js +19 -4
  68. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  69. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  70. package/dist/agent/engine/builder-engine.js +40 -7
  71. package/dist/agent/engine/builder-engine.js.map +1 -1
  72. package/dist/agent/engine/first-event-timeout.d.ts +25 -0
  73. package/dist/agent/engine/first-event-timeout.d.ts.map +1 -0
  74. package/dist/agent/engine/first-event-timeout.js +49 -0
  75. package/dist/agent/engine/first-event-timeout.js.map +1 -0
  76. package/dist/client/AssistantChat.d.ts.map +1 -1
  77. package/dist/client/AssistantChat.js +7 -3
  78. package/dist/client/AssistantChat.js.map +1 -1
  79. package/dist/client/resources/ResourcesPanel.js +1 -1
  80. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  81. package/dist/collab/routes.d.ts +1 -1
  82. package/dist/collab/struct-routes.d.ts +1 -1
  83. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  84. package/dist/integrations/computer-supervision-store.js +4 -4
  85. package/dist/integrations/computer-supervision-store.js.map +1 -1
  86. package/dist/integrations/pending-tasks-store.js +1 -1
  87. package/dist/integrations/pending-tasks-store.js.map +1 -1
  88. package/dist/integrations/remote-commands-store.js +5 -5
  89. package/dist/integrations/remote-commands-store.js.map +1 -1
  90. package/dist/integrations/remote-devices-store.js +3 -3
  91. package/dist/integrations/remote-devices-store.js.map +1 -1
  92. package/dist/integrations/remote-push-store.js +3 -3
  93. package/dist/integrations/remote-push-store.js.map +1 -1
  94. package/dist/notifications/routes.d.ts +1 -1
  95. package/dist/observability/routes.d.ts +3 -3
  96. package/dist/observability/traces.d.ts.map +1 -1
  97. package/dist/observability/traces.js +23 -1
  98. package/dist/observability/traces.js.map +1 -1
  99. package/dist/progress/routes.d.ts +1 -1
  100. package/dist/resources/handlers.d.ts +1 -1
  101. package/dist/server/action-routes.d.ts.map +1 -1
  102. package/dist/server/action-routes.js +23 -1
  103. package/dist/server/action-routes.js.map +1 -1
  104. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  105. package/dist/server/http-response-telemetry.d.ts +2 -0
  106. package/dist/server/http-response-telemetry.d.ts.map +1 -1
  107. package/dist/server/http-response-telemetry.js +5 -1
  108. package/dist/server/http-response-telemetry.js.map +1 -1
  109. package/dist/server/transcribe-voice.d.ts +1 -1
  110. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  111. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  112. package/package.json +2 -2
  113. package/src/agent/engine/ai-sdk-engine.ts +31 -3
  114. package/src/agent/engine/anthropic-engine.ts +20 -3
  115. package/src/agent/engine/builder-engine.ts +56 -9
  116. package/src/agent/engine/first-event-timeout.ts +64 -0
  117. package/src/client/AssistantChat.tsx +7 -3
  118. package/src/client/resources/ResourcesPanel.tsx +1 -1
  119. package/src/integrations/computer-supervision-store.ts +4 -4
  120. package/src/integrations/pending-tasks-store.ts +1 -1
  121. package/src/integrations/remote-commands-store.ts +5 -5
  122. package/src/integrations/remote-devices-store.ts +3 -3
  123. package/src/integrations/remote-push-store.ts +3 -3
  124. package/src/observability/traces.ts +38 -1
  125. package/src/server/action-routes.ts +23 -1
  126. package/src/server/http-response-telemetry.ts +6 -1
  127. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  128. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
@@ -5321,16 +5321,32 @@ body[data-clips-route="recording-pill"] #root {
5321
5321
  background: rgba(26, 26, 26, 0.95);
5322
5322
  color: white;
5323
5323
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);
5324
+ cursor: grab;
5324
5325
  backdrop-filter: blur(12px);
5325
5326
  -webkit-backdrop-filter: blur(12px);
5326
5327
  }
5327
5328
 
5328
- /* Collapsed (not expanded). Shrinks to dot + waveform; controls revealed via
5329
- CSS :hover which works at the WebKit level even when the app is unfocused.
5330
- Top-anchored (not centered) inside the headroom-sized window so the hover
5331
- reveal of the drag grip grows the capsule strictly DOWNWARD — centering it
5332
- would split the growth and raise the top edge, sliding buttons up under the
5333
- cursor and causing misclicks. */
5329
+ .pill-inner:active {
5330
+ cursor: grabbing;
5331
+ }
5332
+
5333
+ .pill-inner:not(.pill-inner-compact) {
5334
+ animation: pill-transcript-in 180ms ease-out;
5335
+ }
5336
+
5337
+ @keyframes pill-transcript-in {
5338
+ from {
5339
+ opacity: 0;
5340
+ transform: translateY(4px) scale(0.985);
5341
+ }
5342
+ to {
5343
+ opacity: 1;
5344
+ transform: translateY(0) scale(1);
5345
+ }
5346
+ }
5347
+
5348
+ /* Collapsed (not expanded). Shrinks to logo + waveform; controls revealed via
5349
+ CSS :hover which works at the WebKit level even when the app is unfocused. */
5334
5350
  .pill-inner-compact {
5335
5351
  width: fit-content;
5336
5352
  height: fit-content;
@@ -5393,9 +5409,7 @@ body[data-clips-route="recording-pill"] #root {
5393
5409
  height: 12px;
5394
5410
  }
5395
5411
 
5396
- /* Collapsed = vertical capsule: clips logo on top, waveform below, whole
5397
- pill clickable to expand. Detached mode keeps its own horizontal layout, so
5398
- this only applies to the non-detached collapsed pill. */
5412
+ /* Collapsed = vertical capsule: logo on top, waveform below. */
5399
5413
  .pill-inner-compact .pill-header.pill-vertical {
5400
5414
  flex-direction: column;
5401
5415
  gap: 0;
@@ -5416,20 +5430,11 @@ body[data-clips-route="recording-pill"] #root {
5416
5430
  }
5417
5431
 
5418
5432
  .pill-vertical .pill-wave-canvas {
5419
- height: 18px;
5420
- width: 18px;
5421
- }
5422
-
5423
- /* Dual-stream (mic + system audio) stacked in the collapsed capsule. */
5424
- .pill-vertical .pill-wave-dual {
5425
- height: 26px;
5426
- width: 18px;
5427
- gap: 1px;
5433
+ height: 22px;
5434
+ width: 22px;
5428
5435
  }
5429
5436
 
5430
- /* Wrapper around logo + waveform. Transparent in row layouts (expanded /
5431
- detached); a real, clickable box only in the collapsed vertical capsule —
5432
- clicking it expands the pill, and hovering tints it. */
5437
+ /* Wrapper around logo + waveform. */
5433
5438
  .pill-media {
5434
5439
  display: contents;
5435
5440
  }
@@ -5440,9 +5445,7 @@ body[data-clips-route="recording-pill"] #root {
5440
5445
  align-items: center;
5441
5446
  align-self: stretch;
5442
5447
  gap: 5px;
5443
- /* Padding + equal negative margin: the hover highlight covers the whole
5444
- upper region (to the top + side edges of the capsule) while leaving the
5445
- logo/waveform in their original positions. */
5448
+ /* The compact media area is the click-to-expand target. */
5446
5449
  padding: 11px 7px 4px;
5447
5450
  margin: -11px -7px 0;
5448
5451
  border-radius: 18px 18px 0 0;
@@ -5455,62 +5458,12 @@ body[data-clips-route="recording-pill"] #root {
5455
5458
  background: rgba(255, 255, 255, 0.08);
5456
5459
  }
5457
5460
 
5458
- /* Drag grip revealed at the bottom of the collapsed capsule on hover. The
5459
- pill grows downward to fit it (window is sized with headroom for this).
5460
- Dragging the pill is wired on the whole window; this is the visible
5461
- affordance and gets its own hover tint. */
5462
- .pill-vertical .pill-vgrip {
5463
- display: flex;
5464
- align-items: center;
5465
- justify-content: center;
5466
- height: 0;
5467
- margin-top: 0;
5468
- overflow: hidden;
5469
- opacity: 0;
5470
- border-radius: 8px;
5471
- color: rgba(255, 255, 255, 0.45);
5472
- cursor: grab;
5473
- transition:
5474
- height 140ms ease,
5475
- margin-top 140ms ease,
5476
- background 120ms ease,
5477
- opacity 120ms ease;
5478
- }
5479
-
5480
- .pill-inner-compact:hover .pill-vertical .pill-vgrip,
5481
- .pill-inner-compact.pill-hovered .pill-vertical .pill-vgrip {
5482
- height: 14px;
5483
- margin-top: 3px;
5484
- opacity: 1;
5485
- }
5486
-
5487
- .pill-vertical .pill-vgrip:active {
5488
- cursor: grabbing;
5489
- }
5490
-
5491
5461
  .pill-timer {
5492
5462
  font-size: 13px;
5493
5463
  font-weight: 500;
5494
5464
  font-variant-numeric: tabular-nums;
5495
5465
  }
5496
5466
 
5497
- .pill-wave-dual {
5498
- display: flex;
5499
- height: 26px;
5500
- width: 48px;
5501
- flex-shrink: 0;
5502
- flex-direction: column;
5503
- align-items: stretch;
5504
- gap: 2px;
5505
- }
5506
-
5507
- .pill-wave-canvas-half {
5508
- width: 100%;
5509
- flex: 1;
5510
- min-height: 0;
5511
- display: block;
5512
- }
5513
-
5514
5467
  .pill-wave-canvas {
5515
5468
  height: 26px;
5516
5469
  width: 48px;
@@ -5564,24 +5517,6 @@ body[data-clips-route="recording-pill"] #root {
5564
5517
  background: rgba(255, 255, 255, 0.1);
5565
5518
  }
5566
5519
 
5567
- .pill-drag-handle {
5568
- position: absolute;
5569
- bottom: 4px;
5570
- left: 50%;
5571
- transform: translateX(-50%);
5572
- height: 4px;
5573
- width: 40px;
5574
- cursor: pointer;
5575
- border-radius: 999px;
5576
- background: rgba(255, 255, 255, 0.3);
5577
- border: none;
5578
- transition: background 120ms;
5579
- }
5580
-
5581
- .pill-drag-handle:hover {
5582
- background: rgba(255, 255, 255, 0.5);
5583
- }
5584
-
5585
5520
  /* Whisper model status row in Settings */
5586
5521
 
5587
5522
  .whisper-status {
@@ -5707,29 +5642,6 @@ body[data-clips-route="recording-pill"] #root {
5707
5642
  overflow: hidden;
5708
5643
  }
5709
5644
 
5710
- /* Meeting mode: transcript + notes side by side. */
5711
- .pill-split {
5712
- display: flex;
5713
- flex: 1;
5714
- min-height: 0;
5715
- overflow: hidden;
5716
- }
5717
-
5718
- .pill-split-pane {
5719
- display: flex;
5720
- flex-direction: column;
5721
- flex: 1 1 50%;
5722
- min-width: 0;
5723
- min-height: 0;
5724
- }
5725
-
5726
- .pill-split-divider {
5727
- width: 1px;
5728
- flex-shrink: 0;
5729
- margin: 8px 0;
5730
- background: rgba(255, 255, 255, 0.1);
5731
- }
5732
-
5733
5645
  .pill-pane-label {
5734
5646
  flex-shrink: 0;
5735
5647
  padding: 8px 12px 2px;
@@ -5772,36 +5684,6 @@ body[data-clips-route="recording-pill"] #root {
5772
5684
  cursor: default;
5773
5685
  }
5774
5686
 
5775
- /* In the split, the transcript area should fill its pane below the label. */
5776
- .pill-split-pane .pill-transcript-area {
5777
- flex: 1;
5778
- min-height: 0;
5779
- }
5780
-
5781
- .pill-notes-area {
5782
- min-height: 0;
5783
- flex: 1;
5784
- display: flex;
5785
- overflow: hidden;
5786
- padding: 4px 12px 8px;
5787
- }
5788
-
5789
- .pill-notes-textarea {
5790
- flex: 1;
5791
- background: transparent;
5792
- border: none;
5793
- outline: none;
5794
- resize: none;
5795
- color: #f5f5f5;
5796
- font-size: 12px;
5797
- line-height: 1.5;
5798
- font-family: inherit;
5799
- }
5800
-
5801
- .pill-notes-textarea::placeholder {
5802
- color: #71717a;
5803
- }
5804
-
5805
5687
  .pill-saved-bar {
5806
5688
  flex-shrink: 0;
5807
5689
  display: flex;
@@ -9,7 +9,7 @@
9
9
  //!
10
10
  //! Two visual modes (driven entirely from the React side via the URL hash):
11
11
  //!
12
- //! - `meeting` — meeting-aware pill with mic + speaker waveforms.
12
+ //! - `meeting` — meeting-aware pill with a combined audio meter.
13
13
  //! - `clip` — solid-mic pill for non-meeting recording sessions.
14
14
  //!
15
15
  //! The pill is used by meeting-aware recordings and Wispr-style voice
@@ -68,9 +68,9 @@ static PILL_HOVER_TRACKING: AtomicBool = AtomicBool::new(false);
68
68
  /// expanded form stretches horizontally to fit the live-transcript area.
69
69
  const PILL_W_LOGICAL: u32 = 38;
70
70
  const PILL_W_EXPANDED_LOGICAL: u32 = 480;
71
- /// Meeting mode expands wider so the live transcript and the notes editor sit
72
- /// side by side without either column feeling cramped.
73
- const PILL_W_EXPANDED_MEETING_LOGICAL: u32 = 720;
71
+ /// Meeting mode uses the same focused transcript width as other recordings;
72
+ /// live notes are intentionally kept out of this compact overlay.
73
+ const PILL_W_EXPANDED_MEETING_LOGICAL: u32 = 480;
74
74
  const PILL_H_LOGICAL: u32 = 92;
75
75
  const PILL_H_EXPANDED_LOGICAL: u32 = 340;
76
76
  /// Bottom margin from the screen edge, logical px. Granola uses ~24.
@@ -240,9 +240,8 @@ fn pill_content_size_physical(app: &AppHandle, expanded: bool) -> (u32, u32) {
240
240
  let (w_log, h_log) = if detached {
241
241
  (PILL_DETACHED_W_LOGICAL, PILL_DETACHED_H_LOGICAL)
242
242
  } else if expanded {
243
- // Meeting mode (right-side anchor) expands wide enough for the
244
- // transcript + notes split; plain clip recordings keep the narrower
245
- // transcript-only width.
243
+ // Meeting mode (right-side anchor) keeps a focused transcript-only
244
+ // panel; plain clip recordings use the same width today.
246
245
  let w = if PILL_RIGHT_SIDE.load(Ordering::Relaxed) {
247
246
  PILL_W_EXPANDED_MEETING_LOGICAL
248
247
  } else {
@@ -0,0 +1,73 @@
1
+ import { getRequestUserEmail } from "@agent-native/core/server/request-context";
2
+ import { accessFilter } from "@agent-native/core/sharing";
3
+ import { and, eq, or, sql, type SQL } from "drizzle-orm";
4
+
5
+ type AgentRecordingAccessOptions = {
6
+ agentOnly?: boolean;
7
+ userEmail?: string;
8
+ };
9
+
10
+ export function isAgentRecordingCaller(caller: string | undefined): boolean {
11
+ return caller === "tool" || caller === "mcp" || caller === "a2a";
12
+ }
13
+
14
+ function normalizeEmail(email: string | undefined): string | null {
15
+ const normalized = email?.trim().toLowerCase();
16
+ return normalized || null;
17
+ }
18
+
19
+ /**
20
+ * Scope agent recording reads to normal access plus public recordings the
21
+ * current signed-in user has already opened. Public visibility alone never
22
+ * makes a recording discoverable, but explicit user and organization shares
23
+ * remain discoverable.
24
+ */
25
+ export function agentRecordingAccessFilter(
26
+ resourceTable: any,
27
+ sharesTable: any,
28
+ viewersTable: any,
29
+ options: AgentRecordingAccessOptions = {},
30
+ ): SQL {
31
+ const normalAccess = accessFilter(resourceTable, sharesTable);
32
+ if (!options.agentOnly) return normalAccess;
33
+
34
+ const normalizedEmail = normalizeEmail(
35
+ options.userEmail ?? getRequestUserEmail(),
36
+ );
37
+ if (!normalizedEmail) return sql`1 = 0`;
38
+
39
+ const viewed = sql`exists (select 1 from ${viewersTable}
40
+ where ${viewersTable.recordingId} = ${resourceTable.id}
41
+ and lower(${viewersTable.viewerEmail}) = ${normalizedEmail})`;
42
+ const owner = sql`lower(${resourceTable.ownerEmail}) = ${normalizedEmail}`;
43
+
44
+ return (
45
+ or(
46
+ owner,
47
+ normalAccess,
48
+ and(eq(resourceTable.visibility, "public"), viewed),
49
+ ) ?? normalAccess
50
+ );
51
+ }
52
+
53
+ export async function hasViewedPublicRecording(
54
+ db: any,
55
+ viewersTable: any,
56
+ recordingId: string,
57
+ userEmail = getRequestUserEmail(),
58
+ ): Promise<boolean> {
59
+ const normalizedEmail = normalizeEmail(userEmail);
60
+ if (!normalizedEmail) return false;
61
+
62
+ const [viewer] = await db
63
+ .select({ id: viewersTable.id })
64
+ .from(viewersTable)
65
+ .where(
66
+ and(
67
+ eq(viewersTable.recordingId, recordingId),
68
+ sql`lower(${viewersTable.viewerEmail}) = ${normalizedEmail}`,
69
+ ),
70
+ )
71
+ .limit(1);
72
+ return Boolean(viewer);
73
+ }
@@ -42,7 +42,10 @@ export default createAgentChatPlugin({
42
42
  initialToolNames: INITIAL_TOOL_NAMES,
43
43
  extraContext: async () =>
44
44
  `<clips-transcript-guidance>
45
- The transcript in view-screen is a bounded preview. When previewTruncated is true, its text is expected to end mid-sentence and is never evidence that transcription stopped early. Call get-recording-player-data before judging transcript completeness or quoting the full transcript. For a failed or pending transcript, use request-transcript with force=true. For an explicit fresh retry of an existing ready transcript, also pass regenerate=true. Agent-triggered retries are queued for the durable worker and return pending while processing.
46
- </clips-transcript-guidance>`,
45
+ The transcript in view-screen and get-recording-player-data are bounded previews when called by the agent. When previewTruncated is true, the text is expected to end mid-sentence and is never evidence that transcription stopped early. Use the bounded payload for a concise summary and do not request or reconstruct the omitted transcript by searching other recordings. For a failed or pending transcript, use request-transcript with force=true. For an explicit fresh retry of an existing ready transcript, also pass regenerate=true. Agent-triggered retries are queued for the durable worker and return pending while processing.
46
+ </clips-transcript-guidance>
47
+ <clips-recording-discovery>
48
+ Clips public recordings are unlisted-by-link, not a searchable public catalog. Treat a recording as known only when it is owned by the current user, already appears in the current screen, or the current user has already viewed it. Do not use list-recordings or search-recordings to discover someone else's clips, recover from a failed direct lookup, answer a date/time question, or search by a title that was not supplied by the user. A question such as when this clip was created refers to the clip already in context; answer it from current-screen or get-recording-player-data. If a direct recording lookup fails, report the failure and stop instead of broadening the search. Only inspect another clip when the user provides its share URL or recording id in the request.
49
+ </clips-recording-discovery>`,
47
50
  resolveOrgId: async (event) => (await getOrgContext(event)).orgId,
48
51
  });
@@ -480,6 +480,24 @@ export async function fetchLocalPlanBridgeBundle(
480
480
  return localPlanBridgePayloadToBundle(payload, fallbackSlug);
481
481
  }
482
482
 
483
+ export async function fetchLocalPlanBridgeComments(
484
+ bridgeUrl: string,
485
+ ): Promise<LocalPlanBundle["comments"]> {
486
+ const response = await fetch(localPlanBridgeCommentsUrl(bridgeUrl), {
487
+ cache: "no-store",
488
+ });
489
+ const payload = (await response
490
+ .json()
491
+ .catch(() => null)) as LocalPlanBridgePayload | null;
492
+ if (!response.ok || !payload?.ok) {
493
+ throw new Error(
494
+ payload?.error ||
495
+ `Local plan bridge returned ${response.status || "an error"}.`,
496
+ );
497
+ }
498
+ return (payload.comments ?? []).filter((comment) => !comment.deletedAt);
499
+ }
500
+
483
501
  export async function updateLocalPlanBridgeComments(
484
502
  bridgeUrl: string,
485
503
  fallbackSlug: string,
@@ -270,6 +270,7 @@ import {
270
270
  } from "@/lib/plan-comment-editor-helpers";
271
271
  import { planDocumentTitle } from "@/lib/plan-document-title";
272
272
  import {
273
+ fetchLocalPlanBridgeComments,
273
274
  fetchLocalPlanBridgeBundle,
274
275
  localNetworkAccessPermissionState,
275
276
  localPlanBridgeUrlFromLocation,
@@ -2082,26 +2083,24 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
2082
2083
  );
2083
2084
  // Bridge bundles carry no comments; load comments.json from the colocated
2084
2085
  // folder so they render and survive refresh in bridge mode too.
2085
- const localPlanBridgeCommentsQuery = useActionQuery<LocalPlanBundle>(
2086
- "get-local-plan-folder",
2087
- localPlanBundleQueryParams(localPlanSlug ?? "", localPlanRepoPath),
2088
- {
2089
- enabled: localPlanMode && Boolean(localPlanSlug && localPlanBridgeUrl),
2090
- refetchInterval: false,
2091
- retry: false,
2092
- },
2093
- );
2086
+ const localPlanBridgeCommentsQuery = useQuery<LocalPlanBundle["comments"]>({
2087
+ queryKey: ["local-plan-bridge-comments", localPlanBridgeUrl],
2088
+ enabled: localPlanMode && Boolean(localPlanSlug && localPlanBridgeUrl),
2089
+ refetchInterval: false,
2090
+ retry: false,
2091
+ queryFn: () => fetchLocalPlanBridgeComments(localPlanBridgeUrl ?? ""),
2092
+ });
2094
2093
  const localPlanData = useMemo(
2095
2094
  () =>
2096
2095
  localPlanBridgeUrl
2097
2096
  ? mergeLocalBridgeComments(
2098
2097
  localPlanBridgeQuery.data,
2099
- localPlanBridgeCommentsQuery.data?.comments,
2098
+ localPlanBridgeCommentsQuery.data,
2100
2099
  )
2101
2100
  : localPlanQuery.data,
2102
2101
  [
2103
2102
  localPlanBridgeQuery.data,
2104
- localPlanBridgeCommentsQuery.data?.comments,
2103
+ localPlanBridgeCommentsQuery.data,
2105
2104
  localPlanBridgeUrl,
2106
2105
  localPlanQuery.data,
2107
2106
  ],
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-20
4
+ ---
5
+
6
+ Bridged local plans now load comments from the local workspace.
@@ -1 +1 @@
1
- {"version":3,"file":"ai-sdk-engine.d.ts","sourceRoot":"","sources":["../../../src/agent/engine/ai-sdk-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAYH,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAW7E,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAInB,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAMxD,QAAA,MAAM,qBAAqB,EAAE,MAAM,CAAC,aAAa,EAAE,kBAAkB,CAyDpE,CAAC;AAMF,QAAA,MAAM,uBAAuB,EAKxB,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;AAEnC,QAAA,MAAM,yBAAyB,EAKf,MAAM,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;AAEzD,QAAA,MAAM,iBAAiB,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,CAStD,CAAC;AAEF,QAAA,MAAM,iBAAiB,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CASpD,CAAC;AA4CF,kDAAkD;AAClD,MAAM,WAAW,iBAAiB;IAChC,sFAAsF;IACtF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0GAA0G;IAC1G,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,yFAAyF;IACzF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA+SD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,aAAa,EACvB,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,WAAW,CAEb;AA8CD,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,yBAAyB,EACzB,iBAAiB,EACjB,iBAAiB,GAClB,CAAC"}
1
+ {"version":3,"file":"ai-sdk-engine.d.ts","sourceRoot":"","sources":["../../../src/agent/engine/ai-sdk-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAYH,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAe7E,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAInB,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAMxD,QAAA,MAAM,qBAAqB,EAAE,MAAM,CAAC,aAAa,EAAE,kBAAkB,CAyDpE,CAAC;AAMF,QAAA,MAAM,uBAAuB,EAKxB,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;AAEnC,QAAA,MAAM,yBAAyB,EAKf,MAAM,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;AAEzD,QAAA,MAAM,iBAAiB,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,CAStD,CAAC;AAEF,QAAA,MAAM,iBAAiB,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CASpD,CAAC;AA4CF,kDAAkD;AAClD,MAAM,WAAW,iBAAiB;IAChC,sFAAsF;IACtF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0GAA0G;IAC1G,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,yFAAyF;IACzF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAuUD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,aAAa,EACvB,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,WAAW,CAEb;AA8CD,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,yBAAyB,EACzB,iBAAiB,EACjB,iBAAiB,GAClB,CAAC"}
@@ -14,6 +14,7 @@
14
14
  import { clearProviderCredentialAuthFailure, readDeployCredentialEnv, recordProviderCredentialAuthFailure, } from "../../server/credential-provider.js";
15
15
  import { anthropicManualThinkingBudget, normalizeReasoningEffortForModel, supportsClaudeAdaptiveThinking, } from "../../shared/reasoning-effort.js";
16
16
  import { AI_SDK_MODEL_CONFIG } from "../model-config.js";
17
+ import { createFirstEventAbortController, FIRST_STREAM_EVENT_TIMEOUT_MS, } from "./first-event-timeout.js";
17
18
  import { clampThinkingBudgetTokens, resolveMaxOutputTokensForEngine, } from "./output-tokens.js";
18
19
  import { engineToolsToAISDK, engineMessagesToAISDK, aiSdkPartToEngineEvents, aiSdkStepToAssistantContent, } from "./translate-ai-sdk.js";
19
20
  // ---------------------------------------------------------------------------
@@ -299,6 +300,7 @@ class AISDKEngine {
299
300
  }
300
301
  }
301
302
  let assistantContent = [];
303
+ const firstEventAbort = createFirstEventAbortController(opts.abortSignal);
302
304
  try {
303
305
  const result = streamText({
304
306
  model: providerModel,
@@ -309,7 +311,7 @@ class AISDKEngine {
309
311
  ...(opts.temperature !== undefined
310
312
  ? { temperature: opts.temperature }
311
313
  : {}),
312
- abortSignal: opts.abortSignal,
314
+ abortSignal: firstEventAbort.signal,
313
315
  onStepFinish: (step) => {
314
316
  assistantContent = aiSdkStepToAssistantContent(step);
315
317
  },
@@ -320,7 +322,17 @@ class AISDKEngine {
320
322
  // Buffer the terminal stop so assistant-content can be emitted just
321
323
  // before it, regardless of where `finish` arrives in the stream.
322
324
  let bufferedStop;
325
+ let sawFirstEvent = false;
323
326
  for await (const part of result.fullStream) {
327
+ // "start" is a synthetic lifecycle marker the AI SDK enqueues
328
+ // synchronously when the stream begins — before any provider bytes
329
+ // arrive — so it does not count as real progress. Every other part
330
+ // (including "start-step", only enqueued on the step's first real
331
+ // chunk) proves the provider is actually responding.
332
+ if (!sawFirstEvent && part?.type !== "start") {
333
+ sawFirstEvent = true;
334
+ firstEventAbort.markFirstEvent();
335
+ }
324
336
  for (const event of aiSdkPartToEngineEvents(part)) {
325
337
  if (event.type === "stop") {
326
338
  bufferedStop = event;
@@ -330,6 +342,12 @@ class AISDKEngine {
330
342
  }
331
343
  }
332
344
  }
345
+ // AI SDK surfaces an aborted stream as a graceful `{type: "abort"}`
346
+ // part rather than a thrown error, so a first-event timeout would
347
+ // otherwise fall through to the normal end_turn completion below.
348
+ if (!sawFirstEvent && firstEventAbort.didTimeout()) {
349
+ throw new Error(`Model request produced no stream events within ${FIRST_STREAM_EVENT_TIMEOUT_MS / 1000}s; the connection appears wedged.`);
350
+ }
333
351
  yield { type: "assistant-content", parts: assistantContent };
334
352
  await clearProviderCredentialAuthFailure({
335
353
  key: PROVIDER_ENV_VARS[this.provider][0],
@@ -338,16 +356,18 @@ class AISDKEngine {
338
356
  yield bufferedStop ?? { type: "stop", reason: "end_turn" };
339
357
  }
340
358
  catch (err) {
359
+ const timedOut = firstEventAbort.didTimeout();
341
360
  // Surface structured fields from AI SDK's APICallError so
342
361
  // isRetryableError can check statusCode/providerRetryable directly
343
362
  // rather than keyword-matching the message string.
344
363
  const statusCode = typeof err?.statusCode === "number" ? err.statusCode : undefined;
345
364
  const errorMessage = err?.message ?? String(err);
346
- const isConnectionError = statusCode === undefined &&
365
+ const isConnectionError = !timedOut &&
366
+ statusCode === undefined &&
347
367
  String(errorMessage).trim().toLowerCase() === "connection error.";
348
368
  const providerRetryable = typeof err?.isRetryable === "boolean"
349
369
  ? err.isRetryable
350
- : isConnectionError
370
+ : isConnectionError || timedOut
351
371
  ? true
352
372
  : undefined;
353
373
  if (statusCode === 401) {
@@ -370,13 +390,16 @@ class AISDKEngine {
370
390
  // auto-resume too — matching the Builder gateway path.
371
391
  ...(statusCode !== undefined
372
392
  ? { errorCode: `http_${statusCode}`, statusCode }
373
- : isConnectionError
393
+ : isConnectionError || timedOut
374
394
  ? { errorCode: "provider_network_error" }
375
395
  : {}),
376
396
  ...(providerRetryable !== undefined ? { providerRetryable } : {}),
377
397
  };
378
398
  throw err;
379
399
  }
400
+ finally {
401
+ firstEventAbort.cleanup();
402
+ }
380
403
  }
381
404
  async createProviderModel(model) {
382
405
  const pkg = PROVIDER_PACKAGES[this.provider];