@copilotkit/web-inspector 1.69.0 → 1.69.2

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.
package/dist/index.cjs CHANGED
@@ -34,11 +34,215 @@ const WHATS_NEW_VIEW_LABEL = "What's new";
34
34
  /** Menu key of the What's new leaf — the news signal's destination. */
35
35
  const WHATS_NEW_MENU_KEY = "whats-new";
36
36
  const NEWS_SIGNAL_ID = "whats-new";
37
- const NEWS_SIGNAL_TONE = "news";
38
- const NEWS_SIGNAL_COLOR = "#A78BFA";
39
- const NEWS_SIGNAL_CADENCE_MS = 2100;
40
- const NEWS_SIGNAL_DESTINATION = WHATS_NEW_MENU_KEY;
37
+ const LAUNCHER_SIGNAL_COLORS = {
38
+ news: "#A78BFA",
39
+ error: "#F87171"
40
+ };
41
+ /**
42
+ * The failure gesture, four phases in series:
43
+ *
44
+ * ```
45
+ * beat 400ms → open 250ms → hold 2500ms → close 250ms (3400ms total)
46
+ * ```
47
+ *
48
+ * Sequential rather than simultaneous, deliberately: the beat says *here*, the
49
+ * pill says *this*. The beat is short so the words arrive quickly.
50
+ *
51
+ * **All four durations live here and nowhere else.** The stylesheet reads the
52
+ * two animated phases as custom properties injected from these numbers rather
53
+ * than restating them, because they are taste and will be tuned by eye after
54
+ * the first live look — tuning the feel must be a number, not a refactor.
55
+ */
56
+ const ERROR_GESTURE_MS = {
57
+ beat: 400,
58
+ open: 250,
59
+ hold: 2500,
60
+ close: 250
61
+ };
62
+ /**
63
+ * The words the pill carries, which are the words the panel already carries.
64
+ *
65
+ * `Runtime error` is the System Health runtime tile's own label, and
66
+ * `Failed to load threads` is the Threads view's own heading. The outside and
67
+ * the inside must agree, so these are shared rather than paraphrased.
68
+ */
69
+ const RUNTIME_ERROR_LABEL = "Runtime error";
70
+ const THREADS_LOAD_ERROR_LABEL = "Failed to load threads";
71
+ const AGENT_RUN_FAILED_LABEL = "Agent run failed";
72
+ const TOOL_ERROR_LABEL = "Tool error";
73
+ const MEMORY_LOAD_ERROR_LABEL = "Failed to load learning data";
74
+ /**
75
+ * Copy on the landing view. Titles match the pill.
76
+ *
77
+ * The two fields differ in what they can promise. `advice` is about the
78
+ * reader's next move and is always true. `highlight` is a claim about *this
79
+ * view* — that the failed item is visible below — and the error carries no
80
+ * guarantee of that: a code mapped to `run` can arrive with no run in the
81
+ * buffer at all, and a tool error can arrive without the call id the
82
+ * highlight needs. So it is rendered only once the item is actually there.
83
+ * A card pointing at something the reader cannot find is worse than a card
84
+ * that stays quiet, because it sends them looking.
85
+ */
86
+ const EVENT_ERROR_GUIDANCE = {
87
+ run: {
88
+ title: AGENT_RUN_FAILED_LABEL,
89
+ highlight: "The failed run event is highlighted below."
90
+ },
91
+ tool: {
92
+ title: TOOL_ERROR_LABEL,
93
+ highlight: "The failed tool call is highlighted below."
94
+ },
95
+ memory: {
96
+ title: MEMORY_LOAD_ERROR_LABEL,
97
+ advice: "Confirm CopilotKit Intelligence is connected, then retry Learning."
98
+ }
99
+ };
100
+ /**
101
+ * The pill's second line, shared by every subject that carries a pill.
102
+ *
103
+ * **This is the one string in the feature that exists nowhere else in the
104
+ * product.** Every other word the launcher shows is word-identical to the
105
+ * panel, which is the standing rule; this line is a deliberate, owner-approved
106
+ * exception to it, because the pill became clickable and an invitation that
107
+ * says nothing is not an invitation.
108
+ *
109
+ * It is shown, never spoken: the polite live region carries the failure class
110
+ * alone. A screen-reader user cannot act on an instruction delivered through
111
+ * an announcement, and it would double the spoken length.
112
+ */
113
+ const PILL_SUBLINE_LABEL = "Open Inspector for details";
114
+ const HUD_OPEN_INSPECTOR_LABEL = "Open Inspector";
115
+ const HUD_THREADS_OFF_LABEL = "Turn on Threads";
116
+ const HUD_THREADS_ON_LABEL = "Threads on";
117
+ const HUD_INTELLIGENCE_OFF_LABEL = "Turn on Intelligence";
118
+ const HUD_INTELLIGENCE_ON_LABEL = "Intelligence connected";
119
+ const HUD_THREADS_OFF_DETAIL = "Inspect conversations from this app.";
120
+ const HUD_THREADS_ON_DETAIL = "Threads is on. Opens the Threads view.";
121
+ const HUD_INTELLIGENCE_OFF_DETAIL = "Connect Intelligence to use Threads and Learning.";
122
+ const HUD_INTELLIGENCE_ON_DETAIL = "Intelligence is connected. Opens Home.";
123
+ const HUD_LEARNING_OFF_LABEL = "Turn on Learning";
124
+ const HUD_LEARNING_ON_LABEL = "Learning on";
125
+ const HUD_LEARNING_OFF_DETAIL = "Connect Intelligence to use Learning.";
126
+ const HUD_LEARNING_ON_DETAIL = "Learning is on. Opens the Learning view.";
127
+ const HUD_OPEN_INSPECTOR_DETAIL = "Same as clicking the circle. Opens the full Inspector.";
128
+ const LAUNCHER_SIGNALS = {
129
+ "whats-new": {
130
+ tone: "news",
131
+ markerTarget: WHATS_NEW_MENU_KEY,
132
+ landingTarget: "home",
133
+ cadence: 2100,
134
+ priority: 0,
135
+ accessibleLabel: "new content"
136
+ },
137
+ connection: {
138
+ tone: "error",
139
+ markerTarget: "home",
140
+ landingTarget: "home",
141
+ cadence: ERROR_GESTURE_MS.beat,
142
+ priority: 5,
143
+ accessibleLabel: "runtime error",
144
+ pillLabel: RUNTIME_ERROR_LABEL
145
+ },
146
+ threads: {
147
+ tone: "error",
148
+ markerTarget: "threads",
149
+ landingTarget: "threads",
150
+ cadence: ERROR_GESTURE_MS.beat,
151
+ priority: 4,
152
+ accessibleLabel: "thread loading error",
153
+ pillLabel: THREADS_LOAD_ERROR_LABEL
154
+ },
155
+ run: {
156
+ tone: "error",
157
+ markerTarget: "ag-ui-events",
158
+ landingTarget: "ag-ui-events",
159
+ cadence: ERROR_GESTURE_MS.beat,
160
+ priority: 3,
161
+ accessibleLabel: "agent run failed",
162
+ pillLabel: AGENT_RUN_FAILED_LABEL
163
+ },
164
+ tool: {
165
+ tone: "error",
166
+ markerTarget: "agents",
167
+ landingTarget: "agents",
168
+ cadence: ERROR_GESTURE_MS.beat,
169
+ priority: 2,
170
+ accessibleLabel: "tool error",
171
+ pillLabel: TOOL_ERROR_LABEL
172
+ },
173
+ memory: {
174
+ tone: "error",
175
+ markerTarget: "memories",
176
+ landingTarget: "memories",
177
+ cadence: ERROR_GESTURE_MS.beat,
178
+ priority: 1,
179
+ accessibleLabel: "learning error",
180
+ pillLabel: MEMORY_LOAD_ERROR_LABEL
181
+ }
182
+ };
183
+ /** Highest priority first, so the winner of the single dot is the head. */
184
+ const LAUNCHER_SIGNAL_PRIORITY_ORDER = Object.keys(LAUNCHER_SIGNALS).sort((a, b) => LAUNCHER_SIGNALS[b].priority - LAUNCHER_SIGNALS[a].priority);
185
+ /** Wiring *state* — red until the problem heals. */
186
+ const WIRING_ERROR_KEYS = ["connection", "threads"];
187
+ /** Unread *events* — red until the landing view is read. */
188
+ const EVENT_ERROR_KEYS = [
189
+ "run",
190
+ "tool",
191
+ "memory"
192
+ ];
193
+ function isWiringErrorKey(key) {
194
+ return WIRING_ERROR_KEYS.includes(key);
195
+ }
196
+ /**
197
+ * Takes a plain string rather than a `LauncherSignalKey`, because one caller
198
+ * reads the subject back out of a `data-` attribute, where the DOM can only
199
+ * offer `string | undefined`. Narrowing untrusted input is what a guard is
200
+ * for; `LauncherSignalKey` still satisfies the parameter, so the callers that
201
+ * already hold one are unaffected.
202
+ */
203
+ function isEventErrorKey(key) {
204
+ return EVENT_ERROR_KEYS.includes(key);
205
+ }
206
+ /** Narrows a signal key to an error source, excluding the announcement. */
207
+ function isErrorSignalKey(key) {
208
+ return isWiringErrorKey(key) || isEventErrorKey(key);
209
+ }
210
+ /**
211
+ * The control range is the point, not an oversight: an attribute selector has
212
+ * to escape those characters too, and CSS.escape — which the fallback below
213
+ * stands in for — escapes them as well. Hoisted so the suppression can sit on
214
+ * the pattern rather than three lines above it.
215
+ */
216
+ const SELECTOR_ESCAPE_PATTERN = /[\0-\x1f\x7f!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g;
217
+ /** Attribute selector value. jsdom does not implement CSS.escape. */
218
+ function escapeSelectorValue(value) {
219
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(value);
220
+ return value.replace(SELECTOR_ESCAPE_PATTERN, "\\$&");
221
+ }
222
+ function eventErrorKeyForCode(code) {
223
+ switch (code) {
224
+ case _copilotkit_core.CopilotKitCoreErrorCode.TOOL_NOT_FOUND:
225
+ case _copilotkit_core.CopilotKitCoreErrorCode.TOOL_HANDLER_FAILED:
226
+ case _copilotkit_core.CopilotKitCoreErrorCode.TOOL_ARGUMENT_PARSE_FAILED:
227
+ case _copilotkit_core.CopilotKitCoreErrorCode.AGENT_NOT_FOUND: return "tool";
228
+ case _copilotkit_core.CopilotKitCoreErrorCode.AGENT_CONNECT_FAILED:
229
+ case _copilotkit_core.CopilotKitCoreErrorCode.AGENT_RUN_FAILED:
230
+ case _copilotkit_core.CopilotKitCoreErrorCode.AGENT_RUN_FAILED_EVENT:
231
+ case _copilotkit_core.CopilotKitCoreErrorCode.AGENT_RUN_ERROR_EVENT: return "run";
232
+ default: return null;
233
+ }
234
+ }
235
+ /**
236
+ * Coalesce inspector-owned GET /threads sends. The first refresh goes out
237
+ * at once. Further calls in this window share one trailing request, so a
238
+ * flaky network does not fire a burst of list fetches and error cards.
239
+ */
240
+ const THREAD_LIST_DEBOUNCE_MS = 300;
241
+ /** The launcher's accessible name with nothing wrong. */
242
+ const LAUNCHER_BASE_LABEL = "Web Inspector";
41
243
  const EDGE_MARGIN = 16;
244
+ /** HUD card plus the hover bridge. Used to pick left vs right. */
245
+ const LAUNCHER_HUD_WIDTH = 248;
42
246
  const DRAG_THRESHOLD = 6;
43
247
  const MIN_WINDOW_WIDTH = 880;
44
248
  const MIN_WINDOW_WIDTH_DOCKED_LEFT = 640;
@@ -514,6 +718,7 @@ var CpkThreadList = class extends PortableLitElement {
514
718
  super(..._args);
515
719
  this.threads = [];
516
720
  this.selectedThreadId = null;
721
+ this.inAppThreadId = null;
517
722
  this.errorMessage = null;
518
723
  this.suppressEmptyState = false;
519
724
  this._query = "";
@@ -525,6 +730,7 @@ var CpkThreadList = class extends PortableLitElement {
525
730
  this.properties = {
526
731
  threads: { attribute: false },
527
732
  selectedThreadId: { attribute: false },
733
+ inAppThreadId: { attribute: false },
528
734
  errorMessage: { attribute: false },
529
735
  suppressEmptyState: { attribute: false },
530
736
  _query: { state: true }
@@ -675,6 +881,11 @@ var CpkThreadList = class extends PortableLitElement {
675
881
  color: #087653;
676
882
  }
677
883
 
884
+ .cpk-tl__pill--in-app {
885
+ background: #bec2ff;
886
+ color: #010507;
887
+ }
888
+
678
889
  /* ── Empty state ── */
679
890
  .cpk-tl__empty {
680
891
  padding: 32px 16px;
@@ -798,6 +1009,9 @@ var CpkThreadList = class extends PortableLitElement {
798
1009
  ${thread.isExample ? lit.html`
799
1010
  <span class="cpk-tl__pill cpk-tl__pill--example">Example</span>
800
1011
  ` : lit.nothing}
1012
+ ${this.inAppThreadId === thread.id ? lit.html`
1013
+ <span class="cpk-tl__pill cpk-tl__pill--in-app">In app</span>
1014
+ ` : lit.nothing}
801
1015
  </span>
802
1016
  </button>
803
1017
  `)}
@@ -864,6 +1078,8 @@ var CpkThreadInspector = class CpkThreadInspector extends PortableLitElement {
864
1078
  this.agentStateInput = null;
865
1079
  this.agentEventsInput = [];
866
1080
  this.liveMessageVersion = 0;
1081
+ this.viewInAppMode = "hidden";
1082
+ this.viewInAppError = null;
867
1083
  this.focusMessageId = null;
868
1084
  this.focusRequestId = 0;
869
1085
  this._tab = "timeline";
@@ -937,6 +1153,8 @@ var CpkThreadInspector = class CpkThreadInspector extends PortableLitElement {
937
1153
  agentStateInput: { attribute: false },
938
1154
  agentEventsInput: { attribute: false },
939
1155
  liveMessageVersion: { attribute: false },
1156
+ viewInAppMode: { attribute: false },
1157
+ viewInAppError: { attribute: false },
940
1158
  focusMessageId: { attribute: false },
941
1159
  focusRequestId: { attribute: false },
942
1160
  _tab: { state: true },
@@ -1259,6 +1477,37 @@ var CpkThreadInspector = class CpkThreadInspector extends PortableLitElement {
1259
1477
  overflow-wrap: anywhere;
1260
1478
  }
1261
1479
 
1480
+ .cpk-td__view-in-app {
1481
+ appearance: none;
1482
+ flex-shrink: 0;
1483
+ margin: 0;
1484
+ border: 1px solid #5558b2;
1485
+ border-radius: 6px;
1486
+ background: #5558b2;
1487
+ color: #ffffff;
1488
+ font-family: "Plus Jakarta Sans", sans-serif;
1489
+ font-size: 11px;
1490
+ font-weight: 600;
1491
+ padding: 5px 10px;
1492
+ cursor: pointer;
1493
+ }
1494
+
1495
+ .cpk-td__view-in-app:focus-visible {
1496
+ outline: 2px solid #010507;
1497
+ outline-offset: 2px;
1498
+ }
1499
+
1500
+ .cpk-td__view-in-app--stop {
1501
+ background: #ffffff;
1502
+ color: #5558b2;
1503
+ }
1504
+
1505
+ .cpk-td__view-in-app-error {
1506
+ flex-basis: 100%;
1507
+ color: #c0333a;
1508
+ font-size: 11px;
1509
+ }
1510
+
1262
1511
  /*
1263
1512
  * Each tab's content is wrapped in this panel so the keep-mounted
1264
1513
  * inactive panels can be hidden via display:none without disturbing
@@ -2851,8 +3100,32 @@ var CpkThreadInspector = class CpkThreadInspector extends PortableLitElement {
2851
3100
  </span>
2852
3101
  `)}
2853
3102
  </div>
3103
+ ${this.renderViewInAppAction()}
2854
3104
  ${bulkControls}
2855
3105
  </div>
3106
+ `;
3107
+ }
3108
+ renderViewInAppAction() {
3109
+ if (this.viewInAppMode === "hidden") return lit.nothing;
3110
+ const isStop = this.viewInAppMode === "stop";
3111
+ return lit.html`
3112
+ <button
3113
+ type="button"
3114
+ class="cpk-td__view-in-app ${isStop ? "cpk-td__view-in-app--stop" : ""}"
3115
+ data-testid="cpk-inspector-view-in-app"
3116
+ aria-label=${isStop ? "Stop viewing this thread in the app" : "View this thread in your app"}
3117
+ @click=${() => {
3118
+ this.dispatchEvent(new CustomEvent(isStop ? "stopViewing" : "viewInApp", {
3119
+ bubbles: true,
3120
+ composed: true
3121
+ }));
3122
+ }}
3123
+ >
3124
+ ${isStop ? "Stop viewing" : "View in your app"}
3125
+ </button>
3126
+ ${this.viewInAppError ? lit.html`<span class="cpk-td__view-in-app-error" role="alert"
3127
+ >${this.viewInAppError}</span
3128
+ >` : lit.nothing}
2856
3129
  `;
2857
3130
  }
2858
3131
  renderActiveBulkControls() {
@@ -4133,6 +4406,12 @@ var WebInspectorElement = class WebInspectorElement extends lit.LitElement {
4133
4406
  };
4134
4407
  this.lastScrolledAgentNavigationLayout = null;
4135
4408
  this.selectedThreadId = null;
4409
+ this.inAppThreadId = null;
4410
+ this.inAppAgentId = null;
4411
+ this.inAppSource = null;
4412
+ this.activeViewInAppRequestId = null;
4413
+ this.viewInAppError = null;
4414
+ this.inspectorBridgeUnsubscribers = [];
4136
4415
  this.selectedRealThreadIsExplicit = false;
4137
4416
  this.selectedLocalExampleThreadId = null;
4138
4417
  this.requestedThreadId = null;
@@ -4196,9 +4475,39 @@ var WebInspectorElement = class WebInspectorElement extends lit.LitElement {
4196
4475
  this.announcementLoaded = false;
4197
4476
  this.announcementPromise = null;
4198
4477
  this.newsSignalArmed = false;
4199
- this.newsSignalPulsing = false;
4200
- this.newsSignalPulsePending = false;
4478
+ this.pulsingSignal = null;
4479
+ this.pendingPulseSignal = null;
4201
4480
  this.pulseTimeoutId = null;
4481
+ this.errorSignalArmed = {
4482
+ connection: false,
4483
+ threads: false
4484
+ };
4485
+ this.eventErrorArmed = {
4486
+ run: false,
4487
+ tool: false,
4488
+ memory: false
4489
+ };
4490
+ this.eventErrorDetails = {
4491
+ run: null,
4492
+ tool: null,
4493
+ memory: null
4494
+ };
4495
+ this.pendingScrollToEventId = null;
4496
+ this.pendingScrollToToolCallId = null;
4497
+ this.threadRefreshLastSentAt = /* @__PURE__ */ new Map();
4498
+ this.threadRefreshTrailingTimers = /* @__PURE__ */ new Map();
4499
+ this.errorBeatSpent = false;
4500
+ this.errorSignalViewedSources = /* @__PURE__ */ new Set();
4501
+ this.gestureSignal = null;
4502
+ this.pillPhase = null;
4503
+ this.pillDirection = null;
4504
+ this.pillTimeoutId = null;
4505
+ this.launcherHudOpen = false;
4506
+ this.launcherHudSide = "left";
4507
+ this.launcherHudHelp = null;
4508
+ this.launcherHudCloseTimer = null;
4509
+ this.hudLandingMenu = null;
4510
+ this.pillOutcome = null;
4202
4511
  this.viewedNewsSignalIds = /* @__PURE__ */ new Set();
4203
4512
  this.pendingNewsSignalViewed = null;
4204
4513
  this.viewedBannerSurfaces = /* @__PURE__ */ new Set();
@@ -4272,9 +4581,59 @@ var WebInspectorElement = class WebInspectorElement extends lit.LitElement {
4272
4581
  this.customTabIcons = { threads: `<svg class="h-3.5 w-3.5" width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.04167 15C8.29167 15 7.65972 14.7431 7.14583 14.2292C6.63194 13.7153 6.375 13.0972 6.375 12.375C6.375 11.3194 6.80208 10.3646 7.65625 9.51042C8.51042 8.65625 9.57639 8.125 10.8542 7.91667C10.8125 7.41667 10.6875 7.03819 10.4792 6.78125C10.2708 6.52431 9.98611 6.39583 9.625 6.39583C9.20833 6.39583 8.75694 6.56944 8.27083 6.91667C7.78472 7.26389 7.20833 7.83333 6.54167 8.625C5.45833 9.91667 4.66319 10.7569 4.15625 11.1458C3.64931 11.5347 3.10417 11.7292 2.52083 11.7292C1.8125 11.7292 1.21528 11.4653 0.729167 10.9375C0.243056 10.4097 0 9.77083 0 9.02083C0 8.27083 0.163194 7.50347 0.489583 6.71875C0.815972 5.93403 1.36806 4.99306 2.14583 3.89583C2.40972 3.53472 2.60417 3.22917 2.72917 2.97917C2.85417 2.72917 2.91667 2.52778 2.91667 2.375C2.91667 2.27778 2.89931 2.20486 2.86458 2.15625C2.82986 2.10764 2.77778 2.08333 2.70833 2.08333C2.56944 2.08333 2.39583 2.17014 2.1875 2.34375C1.97917 2.51736 1.73611 2.78472 1.45833 3.14583L0 1.66667C0.444444 1.125 0.895833 0.711806 1.35417 0.427083C1.8125 0.142361 2.26389 0 2.70833 0C3.34722 0 3.88889 0.222222 4.33333 0.666667C4.77778 1.11111 5 1.66667 5 2.33333C5 2.73611 4.89583 3.18056 4.6875 3.66667C4.47917 4.15278 4.13194 4.73611 3.64583 5.41667C3.11806 6.16667 2.72569 6.82639 2.46875 7.39583C2.21181 7.96528 2.08333 8.46528 2.08333 8.89583C2.08333 9.13194 2.12153 9.31597 2.19792 9.44792C2.27431 9.57986 2.38194 9.64583 2.52083 9.64583C2.65972 9.64583 2.78125 9.60764 2.88542 9.53125C2.98958 9.45486 3.18056 9.27083 3.45833 8.97917C3.63889 8.78472 3.85417 8.54514 4.10417 8.26042C4.35417 7.97569 4.65972 7.625 5.02083 7.20833C5.89583 6.16667 6.6875 5.42361 7.39583 4.97917C8.10417 4.53472 8.84722 4.3125 9.625 4.3125C10.5556 4.3125 11.3194 4.625 11.9167 5.25C12.5139 5.875 12.8542 6.72917 12.9375 7.8125H15V9.89583H12.9375C12.8264 11.4514 12.4201 12.691 11.7188 13.6146C11.0174 14.5382 10.125 15 9.04167 15ZM9.08333 12.9167C9.52778 12.9167 9.90278 12.6632 10.2083 12.1562C10.5139 11.6493 10.7222 10.9444 10.8333 10.0417C10.1944 10.1944 9.63889 10.4965 9.16667 10.9479C8.69444 11.3993 8.45833 11.8472 8.45833 12.2917C8.45833 12.4861 8.51389 12.6389 8.625 12.75C8.73611 12.8611 8.88889 12.9167 9.08333 12.9167Z" fill="currentColor"/></svg>` };
4273
4582
  this.handleDocumentVisibilityChange = () => {
4274
4583
  this.accountCtaMotionPaused = document.visibilityState !== "visible";
4275
- if (document.visibilityState === "visible" && this.newsSignalPulsePending && !this.isOpen) this.startNewsSignalPulse();
4584
+ if (document.visibilityState === "visible" && !this.isOpen) this.flushPendingSignalPulse();
4585
+ this.requestUpdate();
4586
+ };
4587
+ this.handlePillClick = (event) => {
4588
+ event.preventDefault();
4589
+ event.stopPropagation();
4590
+ this.openInspector("floating_button");
4591
+ };
4592
+ this.handleLauncherHudEnter = () => {
4593
+ this.openLauncherHud();
4594
+ };
4595
+ this.handleLauncherHudLeave = () => {
4596
+ if (this.launcherHudCloseTimer !== null) clearTimeout(this.launcherHudCloseTimer);
4597
+ this.launcherHudCloseTimer = setTimeout(() => {
4598
+ this.launcherHudCloseTimer = null;
4599
+ this.closeLauncherHud();
4600
+ }, 160);
4601
+ };
4602
+ this.handleLauncherHudFocusIn = () => {
4603
+ this.openLauncherHud();
4604
+ };
4605
+ this.handleLauncherHudFocusOut = (event) => {
4606
+ const next = event.relatedTarget;
4607
+ const wrapper = event.currentTarget;
4608
+ if (next instanceof Node && wrapper instanceof Node && wrapper.contains(next)) return;
4609
+ this.closeLauncherHud();
4610
+ };
4611
+ this.handleLauncherHudKeydown = (event) => {
4612
+ if (event.key !== "Escape") return;
4613
+ if (!this.launcherHudOpen) return;
4614
+ event.preventDefault();
4615
+ event.stopPropagation();
4616
+ this.closeLauncherHud();
4617
+ this.activeRoot.querySelector(".console-button")?.focus();
4618
+ };
4619
+ this.handleHudActionClick = (event, row) => {
4620
+ event.preventDefault();
4621
+ event.stopPropagation();
4622
+ this.hudLandingMenu = row === "inspector" ? null : row === "threads" ? "threads" : row === "learning" ? "memories" : "home";
4623
+ this.closeLauncherHud();
4624
+ this.openInspector("floating_button");
4625
+ };
4626
+ this.handleHudHelpClick = (event, row) => {
4627
+ event.preventDefault();
4628
+ event.stopPropagation();
4629
+ this.launcherHudHelp = this.launcherHudHelp === row ? null : row;
4276
4630
  this.requestUpdate();
4277
4631
  };
4632
+ this.handleHudRowClick = (event, row) => {
4633
+ const target = event.target;
4634
+ if (target instanceof Element && target.closest(".cpk-launcher-hud__help, [data-cpk-hud-action]")) return;
4635
+ this.handleHudActionClick(event, row);
4636
+ };
4278
4637
  this.handleSidebarToggle = () => {
4279
4638
  this.sidebarRailTooltip = null;
4280
4639
  this.sidebarCollapsed = !this.sidebarCollapsed;
@@ -4306,6 +4665,17 @@ var WebInspectorElement = class WebInspectorElement extends lit.LitElement {
4306
4665
  this.persistState();
4307
4666
  this.requestUpdate();
4308
4667
  };
4668
+ this.handleEventErrorBannerKeydown = (event) => {
4669
+ if (event.key !== "Enter" && event.key !== " ") return;
4670
+ event.preventDefault();
4671
+ this.refocusEventErrorLanding(event);
4672
+ };
4673
+ this.refocusEventErrorLanding = (event) => {
4674
+ const key = event.currentTarget?.dataset.cpkEventError;
4675
+ if (!key || !isEventErrorKey(key)) return;
4676
+ this.applyEventErrorLanding(key);
4677
+ this.requestUpdate();
4678
+ };
4309
4679
  this.handleSystemColorSchemeChange = (event) => {
4310
4680
  if (this.hasExplicitColorScheme) return;
4311
4681
  this.colorScheme = event.matches ? "dark" : "light";
@@ -4682,6 +5052,40 @@ var WebInspectorElement = class WebInspectorElement extends lit.LitElement {
4682
5052
  if (target.hasPointerCapture(this.threadDividerPointerId)) target.releasePointerCapture(this.threadDividerPointerId);
4683
5053
  this.threadDividerResizing = false;
4684
5054
  };
5055
+ this.handleViewInApp = () => {
5056
+ const thread = this.getSelectedRealThread();
5057
+ if (!thread) return;
5058
+ if (this.activeViewInAppRequestId && this.inAppAgentId) (0, _copilotkit_core.emitInspectorStopViewing)({
5059
+ requestId: this.activeViewInAppRequestId,
5060
+ agentId: this.inAppAgentId
5061
+ });
5062
+ this.viewInAppError = null;
5063
+ const requestId = (0, _copilotkit_core.createInspectorThreadRequestId)();
5064
+ this.activeViewInAppRequestId = requestId;
5065
+ if (!(0, _copilotkit_core.emitInspectorViewThread)({
5066
+ requestId,
5067
+ threadId: thread.id,
5068
+ agentId: thread.agentId
5069
+ })) {
5070
+ this.activeViewInAppRequestId = null;
5071
+ this.inAppThreadId = null;
5072
+ this.inAppAgentId = null;
5073
+ this.inAppSource = null;
5074
+ this.viewInAppError = "No official chat for this agent is on the page.";
5075
+ }
5076
+ this.requestUpdate();
5077
+ };
5078
+ this.handleStopViewing = () => {
5079
+ const requestId = this.activeViewInAppRequestId;
5080
+ const agentId = this.inAppAgentId;
5081
+ if (!requestId || !agentId) return;
5082
+ this.viewInAppError = null;
5083
+ (0, _copilotkit_core.emitInspectorStopViewing)({
5084
+ requestId,
5085
+ agentId
5086
+ });
5087
+ this.requestUpdate();
5088
+ };
4685
5089
  this.handleThreadsExampleOverviewVideoControl = () => {
4686
5090
  const video = this.threadsExampleOverviewVideoElement;
4687
5091
  if (!video) return;
@@ -5102,7 +5506,7 @@ var WebInspectorElement = class WebInspectorElement extends lit.LitElement {
5102
5506
  const statusSub = store.select(createThreadStoreStatusSelector()).subscribe(({ error, isLoading }) => {
5103
5507
  if (capabilityGeneration !== this.threadCapabilityGeneration || !this.areThreadEndpointsAvailable()) return;
5104
5508
  if (error) this._threadsErrorByAgent.set(agentId, error);
5105
- else this._threadsErrorByAgent.delete(agentId);
5509
+ else if (!isLoading) this._threadsErrorByAgent.delete(agentId);
5106
5510
  this._threadsLoadingByAgent.set(agentId, isLoading);
5107
5511
  this.requestUpdate();
5108
5512
  });
@@ -5116,7 +5520,7 @@ var WebInspectorElement = class WebInspectorElement extends lit.LitElement {
5116
5520
  this._threadsLoadingByAgent.set(agentId, (0, _copilotkit_core.ɵselectThreadsIsLoading)(initialState));
5117
5521
  const initialError = (0, _copilotkit_core.ɵselectThreadsError)(initialState);
5118
5522
  if (initialError) this._threadsErrorByAgent.set(agentId, initialError);
5119
- else this._threadsErrorByAgent.delete(agentId);
5523
+ else if (!(0, _copilotkit_core.ɵselectThreadsIsLoading)(initialState)) this._threadsErrorByAgent.delete(agentId);
5120
5524
  this.rebuildFlattenedThreads();
5121
5525
  this.autoSelectLatestThread();
5122
5526
  }
@@ -5197,8 +5601,28 @@ var WebInspectorElement = class WebInspectorElement extends lit.LitElement {
5197
5601
  if (!this.areThreadEndpointsAvailable()) return;
5198
5602
  const store = this._ownedThreadStores.get(agentId);
5199
5603
  if (!store) return;
5604
+ const now = Date.now();
5605
+ const waitMs = THREAD_LIST_DEBOUNCE_MS - (now - (this.threadRefreshLastSentAt.get(agentId) ?? 0));
5606
+ if (waitMs <= 0) {
5607
+ this.sendOwnedThreadRefresh(agentId, store, now);
5608
+ return;
5609
+ }
5610
+ if (this.threadRefreshTrailingTimers.has(agentId)) return;
5611
+ this.threadRefreshTrailingTimers.set(agentId, setTimeout(() => {
5612
+ this.threadRefreshTrailingTimers.delete(agentId);
5613
+ const current = this._ownedThreadStores.get(agentId);
5614
+ if (!current) return;
5615
+ this.sendOwnedThreadRefresh(agentId, current, Date.now());
5616
+ }, waitMs));
5617
+ }
5618
+ sendOwnedThreadRefresh(agentId, store, sentAt) {
5619
+ this.threadRefreshLastSentAt.set(agentId, sentAt);
5200
5620
  store.refresh();
5201
5621
  }
5622
+ cancelThreadRefreshDebounce() {
5623
+ for (const timer of this.threadRefreshTrailingTimers.values()) clearTimeout(timer);
5624
+ this.threadRefreshTrailingTimers.clear();
5625
+ }
5202
5626
  updateOwnedThreadStoreHeaders(headers) {
5203
5627
  if (!this.areThreadEndpointsAvailable()) return;
5204
5628
  const core = this.core;
@@ -5254,6 +5678,7 @@ var WebInspectorElement = class WebInspectorElement extends lit.LitElement {
5254
5678
  this.runtimeStatus = core.runtimeConnectionStatus;
5255
5679
  this.coreProperties = core.properties;
5256
5680
  this.lastCoreError = null;
5681
+ this.clearAllEventErrors();
5257
5682
  const supportsInspectorMetadata = this.coreSupportsInspectorMetadata(core);
5258
5683
  this.updateInspectorMetadataProjection(this.readCoreInspectorMetadata(core));
5259
5684
  this.coreSubscriber = {
@@ -5289,11 +5714,12 @@ var WebInspectorElement = class WebInspectorElement extends lit.LitElement {
5289
5714
  this.updateInspectorMetadataProjection(inspectorMetadata);
5290
5715
  this.requestUpdate();
5291
5716
  } } : {},
5292
- onError: ({ code, error }) => {
5717
+ onError: ({ code, error, context }) => {
5293
5718
  this.lastCoreError = {
5294
5719
  code,
5295
5720
  message: error.message
5296
5721
  };
5722
+ this.armEventErrorFromCode(code, error.message, context);
5297
5723
  this.requestUpdate();
5298
5724
  },
5299
5725
  onAgentsChanged: ({ agents }) => {
@@ -5386,6 +5812,7 @@ var WebInspectorElement = class WebInspectorElement extends lit.LitElement {
5386
5812
  }),
5387
5813
  memoryStore.select(_copilotkit_core.ɵselectMemoriesError).subscribe((v) => {
5388
5814
  this._memoriesError = v;
5815
+ if (v) this.armEventError("memory", v.message);
5389
5816
  this.requestUpdate();
5390
5817
  }),
5391
5818
  memoryStore.select(_copilotkit_core.ɵselectMemoriesAvailable).subscribe((v) => {
@@ -5472,10 +5899,12 @@ var WebInspectorElement = class WebInspectorElement extends lit.LitElement {
5472
5899
  this._recallQuery = "";
5473
5900
  this.coreSubscriber = null;
5474
5901
  this.runtimeStatus = null;
5902
+ this.cancelThreadRefreshDebounce();
5475
5903
  this.inspectorMetadataValue = void 0;
5476
5904
  this.inspectorMetadataProjection = require_inspector_metadata.projectInspectorMetadata(void 0, void 0);
5477
5905
  this.metadataTelemetryFingerprints.clear();
5478
5906
  this.lastCoreError = null;
5907
+ this.clearAllEventErrors();
5479
5908
  this.coreProperties = {};
5480
5909
  this.cachedTools = [];
5481
5910
  this.toolSignature = "";
@@ -5834,22 +6263,28 @@ var WebInspectorElement = class WebInspectorElement extends lit.LitElement {
5834
6263
  }
5835
6264
  renderToolCallDetails(toolCalls) {
5836
6265
  if (!Array.isArray(toolCalls) || toolCalls.length === 0) return lit.nothing;
6266
+ const toolError = this.eventErrorDetails.tool;
5837
6267
  return lit.html`
5838
6268
  <div class="mt-2 space-y-2">
5839
6269
  ${toolCalls.map((call, index) => {
5840
6270
  const functionName = call.function?.name ?? call.toolName ?? "Unknown function";
5841
6271
  const callId = typeof call?.id === "string" ? call.id : `tool-call-${index + 1}`;
5842
6272
  const argsString = this.formatToolCallArguments(call.function?.arguments);
6273
+ const isFailedCall = toolError?.toolCallId !== void 0 && toolError.toolCallId === callId;
5843
6274
  return lit.html`
5844
6275
  <div
5845
- class="rounded-md border border-gray-200 bg-gray-50 p-3 text-xs text-gray-700"
6276
+ class=${isFailedCall ? "rounded-md border border-rose-300 bg-rose-50 p-3 text-xs text-gray-900" : "rounded-md border border-gray-200 bg-gray-50 p-3 text-xs text-gray-700"}
6277
+ data-cpk-failed-tool-call=${isFailedCall ? callId : void 0}
5846
6278
  >
5847
6279
  <div
5848
6280
  class="flex flex-wrap items-center justify-between gap-1 font-medium text-gray-900"
5849
6281
  >
5850
- <span>${functionName}</span>
5851
- <span class="text-[10px] text-gray-500">ID: ${callId}</span>
6282
+ <span>${functionName}${isFailedCall ? " failed" : ""}</span>
6283
+ <span class="text-[10px] text-gray-600">ID: ${callId}</span>
5852
6284
  </div>
6285
+ ${isFailedCall && toolError?.message ? lit.html`<p class="mt-2 break-words leading-relaxed text-gray-800">
6286
+ ${toolError.message}
6287
+ </p>` : lit.nothing}
5853
6288
  ${argsString ? lit.html`<pre
5854
6289
  class="mt-2 overflow-auto rounded bg-white p-2 text-[11px] leading-relaxed text-gray-800"
5855
6290
  >
@@ -6001,18 +6436,28 @@ ${argsString}</pre
6001
6436
  .console-button-wrapper {
6002
6437
  position: relative;
6003
6438
  display: inline-flex;
6004
- }
6005
-
6006
- .console-button {
6439
+ /* The launcher's surface and edge, shared by the button and the pill so
6440
+ the two cannot drift apart. A dark grey rather than near-black: the
6441
+ launcher sits on a customer's page, and 1,5,7 against white is a
6442
+ harder edge than this surface needs. */
6443
+ --cpk-launcher-face: rgba(24, 28, 31, 0.95);
6444
+ --cpk-launcher-face-solid: rgb(24, 28, 31);
6445
+ --cpk-launcher-edge: rgba(190, 194, 255, 0.25);
6007
6446
  /* The launcher's own size, exposed so the signal dot can be placed
6008
6447
  against the OUTER rim with a length rather than a percentage.
6009
6448
  Percentages resolve against the padding box, which the 1px border
6010
- insets, and the dot would land inside the rim. */
6449
+ insets, and the dot would land inside the rim.
6450
+
6451
+ Declared on the wrapper rather than on the button so the pill, which
6452
+ is the button's sibling, can clear the mark by the same length. */
6011
6453
  --cpk-launcher-size: clamp(
6012
6454
  ${LAUNCHER_MIN_SIZE}px,
6013
6455
  7vw,
6014
6456
  ${LAUNCHER_MAX_SIZE}px
6015
6457
  );
6458
+ }
6459
+
6460
+ .console-button {
6016
6461
  width: var(--cpk-launcher-size);
6017
6462
  height: var(--cpk-launcher-size);
6018
6463
  /* Keep the 1px border inside the declared outer size. */
@@ -6414,14 +6859,29 @@ ${argsString}</pre
6414
6859
 
6415
6860
  /* ── Floating button ─────────────────────────────────────────── */
6416
6861
  .console-button {
6417
- background-color: rgba(1, 5, 7, 0.95) !important;
6418
- border-color: rgba(190, 194, 255, 0.25) !important;
6862
+ background-color: var(--cpk-launcher-face) !important;
6863
+ border-color: var(--cpk-launcher-edge) !important;
6864
+ /* One hairline, not two. The border above is it; a second ring used to
6865
+ sit 1px outside as a box-shadow and hardcoded the lilac instead of
6866
+ reading the --cpk-launcher-edge token, so it could not follow it.
6867
+ What replaces it is a one-pixel light edge along the top, which is
6868
+ what keeps the face from reading flat without drawing a frame.
6869
+
6870
+ The border is not decoration: the face is #181C1F, which against a
6871
+ dark host page (GitHub dark 1.10:1, Tailwind slate-900 1.04:1) is
6872
+ indistinguishable from the page. It is the only thing that gives the
6873
+ launcher an outline there, so it stays. */
6419
6874
  box-shadow:
6420
- 0 0 0 1px rgba(190, 194, 255, 0.15),
6875
+ inset 0 1px 0 rgba(255, 255, 255, 0.07),
6421
6876
  0 4px 14px rgba(1, 5, 7, 0.28) !important;
6877
+ /* Promotes the launcher to its own compositing layer, which the
6878
+ backdrop-filter this replaces used to do as a side effect. Without
6879
+ a layer the hover scale re-rasterises the mark every frame and it
6880
+ visibly jitters; with one, the compositor scales it as a texture. */
6881
+ will-change: transform;
6422
6882
  }
6423
6883
  .console-button:hover {
6424
- background-color: rgba(1, 5, 7, 1) !important;
6884
+ background-color: var(--cpk-launcher-face-solid) !important;
6425
6885
  border-color: rgba(190, 194, 255, 0.45) !important;
6426
6886
  }
6427
6887
  .console-button:focus-visible {
@@ -6439,7 +6899,6 @@ ${argsString}</pre
6439
6899
  * that forces a repaint every frame is not acceptable.
6440
6900
  */
6441
6901
  .console-button[data-cpk-signal] {
6442
- --cpk-launcher-face: rgba(1, 5, 7, 0.95);
6443
6902
  isolation: isolate;
6444
6903
  }
6445
6904
 
@@ -6545,8 +7004,197 @@ ${argsString}</pre
6545
7004
  width: 19%;
6546
7005
  height: 19%;
6547
7006
  border-radius: 50%;
6548
- background: var(--cpk-launcher-signal);
6549
- box-shadow: 0 0 0 1.5px var(--cpk-launcher-face);
7007
+ /* Lit from the upper left and shaded at the lower right, so the dot
7008
+ reads as a lens rather than a flat disc. Both stops are derived from
7009
+ the signal colour, so a new tone needs no new values. */
7010
+ background: radial-gradient(
7011
+ circle at 32% 28%,
7012
+ color-mix(in srgb, var(--cpk-launcher-signal), white 40%) 0%,
7013
+ var(--cpk-launcher-signal) 60%,
7014
+ color-mix(in srgb, var(--cpk-launcher-signal), black 20%) 100%
7015
+ );
7016
+ /* Replaces an opaque 1.5px collar in the launcher's own face. That
7017
+ collar was 21% of the dot's footprint, and because the dot's centre
7018
+ sits *on* the rim, its outer half painted a hard dark crescent onto
7019
+ the host page rather than onto the launcher. A hairline plus a soft
7020
+ drop does the same separating job without the hard edge. */
7021
+ box-shadow:
7022
+ 0 0 0 0.5px rgba(1, 5, 7, 0.4),
7023
+ 0 1px 2.5px rgba(1, 5, 7, 0.5);
7024
+ }
7025
+
7026
+ /* ── Launcher pill: the launcher opens sideways and says what ─── */
7027
+ /*
7028
+ * The pill is laid out at its FULL width from the first frame and
7029
+ * revealed by animating a rectangular clip. Nothing is scaled and
7030
+ * nothing is resized.
7031
+ *
7032
+ * That is not a stylistic choice. This component is permanently mounted
7033
+ * on top of a customer's application, so no property that forces a
7034
+ * layout on every frame is acceptable — and animating "width" does
7035
+ * exactly that, sixty times a second, on someone else's page. A clip
7036
+ * leaves the element's geometry constant and changes only the visible
7037
+ * region, which the compositor handles. Animating a horizontal scale
7038
+ * was the other candidate and squashes the mark itself, not merely the
7039
+ * rounded end, so the logo would need counter-scaling and the dot and
7040
+ * halo would become ellipses.
7041
+ *
7042
+ * The launcher's own face and border are repeated here so the two form
7043
+ * one capsule: the button paints last and therefore on top, with no
7044
+ * z-index needed. The mark's own ring and shadow are deliberately left
7045
+ * alone for the whole gesture — the circle's outline staying visible
7046
+ * inside the open pill was looked at against the alternative and kept.
7047
+ *
7048
+ * A column, not a row: the pill carries a heading and a subline stacked,
7049
+ * centred against a height that does not change. "justify-content"
7050
+ * centres the pair vertically and "align-items" keeps both lines flush
7051
+ * left, so the pill never grows taller than the launcher it opens from.
7052
+ */
7053
+ .cpk-launcher-pill {
7054
+ position: absolute;
7055
+ top: 50%;
7056
+ margin-top: calc(var(--cpk-launcher-size) / -2);
7057
+ height: var(--cpk-launcher-size);
7058
+ display: inline-flex;
7059
+ flex-direction: column;
7060
+ justify-content: center;
7061
+ align-items: flex-start;
7062
+ gap: 1px;
7063
+ box-sizing: border-box;
7064
+ border-radius: 999px;
7065
+ border: 1px solid var(--cpk-launcher-edge);
7066
+ background: var(--cpk-launcher-face);
7067
+ color: #ffffff;
7068
+ white-space: nowrap;
7069
+ pointer-events: none;
7070
+ opacity: 0;
7071
+ }
7072
+
7073
+ /* The failure class, word-identical to the panel's own wording. */
7074
+ .cpk-launcher-pill__heading {
7075
+ font-size: 12px;
7076
+ font-weight: 600;
7077
+ line-height: 1.2;
7078
+ }
7079
+
7080
+ /*
7081
+ * The one line of copy in this feature that exists nowhere else in the
7082
+ * product. The heading above is word-identical to the panel, which is
7083
+ * the standing rule; this line is a deliberate, owner-approved exception
7084
+ * to it, because the pill is now clickable and has to say so.
7085
+ *
7086
+ * It is NOT spoken. A screen-reader user cannot act on an instruction
7087
+ * delivered through an announcement, and it would double the spoken
7088
+ * length — so the live region carries the failure class alone.
7089
+ */
7090
+ .cpk-launcher-pill__subline {
7091
+ font-size: 10.5px;
7092
+ font-weight: 500;
7093
+ line-height: 1.2;
7094
+ opacity: 0.72;
7095
+ }
7096
+
7097
+ /*
7098
+ * Two directions, one animation with the inset on the other side. The
7099
+ * padding on the launcher's side clears the mark, so the words never sit
7100
+ * under it. The text-side padding is derived from the capsule's radius
7101
+ * (half the launcher size), NOT a bare literal: padding is measured from
7102
+ * the bounding box, but the first half-height of that side is the rounded
7103
+ * cap. Half the size lands the text exactly where the cap ends and the
7104
+ * straight edge begins. A literal 14px put it 16px inside the curve at the
7105
+ * production launcher size, which is itself a clamp on the viewport.
7106
+ */
7107
+ .cpk-launcher-pill[data-cpk-pill-direction="left"] {
7108
+ right: 0;
7109
+ padding: 0 calc(var(--cpk-launcher-size) + 12px) 0
7110
+ calc(var(--cpk-launcher-size) / 2);
7111
+ clip-path: inset(0 0 0 calc(100% - var(--cpk-launcher-size)));
7112
+ }
7113
+ .cpk-launcher-pill[data-cpk-pill-direction="right"] {
7114
+ left: 0;
7115
+ padding: 0 calc(var(--cpk-launcher-size) / 2) 0
7116
+ calc(var(--cpk-launcher-size) + 12px);
7117
+ clip-path: inset(0 calc(100% - var(--cpk-launcher-size)) 0 0);
7118
+ }
7119
+
7120
+ /*
7121
+ * "round" on both stops, so the revealing edge is the capsule's own
7122
+ * rounded end travelling sideways rather than a straight vertical line
7123
+ * wiping across it. An unrounded inset reads as a wipe; this reads as an
7124
+ * opening. It adds no animated property: the clip is still the clip.
7125
+ */
7126
+ @keyframes cpk-launcher-pill-left {
7127
+ 0% {
7128
+ opacity: 0;
7129
+ clip-path: inset(
7130
+ 0 0 0 calc(100% - var(--cpk-launcher-size)) round 999px
7131
+ );
7132
+ }
7133
+ 100% {
7134
+ opacity: 1;
7135
+ clip-path: inset(0 0 0 0 round 999px);
7136
+ }
7137
+ }
7138
+
7139
+ @keyframes cpk-launcher-pill-right {
7140
+ 0% {
7141
+ opacity: 0;
7142
+ clip-path: inset(
7143
+ 0 calc(100% - var(--cpk-launcher-size)) 0 0 round 999px
7144
+ );
7145
+ }
7146
+ 100% {
7147
+ opacity: 1;
7148
+ clip-path: inset(0 0 0 0 round 999px);
7149
+ }
7150
+ }
7151
+
7152
+ /*
7153
+ * The pill takes the pointer exactly while it is on screen, so the
7154
+ * instruction it now carries is honest: a click on it opens the
7155
+ * Inspector, the same action as pressing the mark. During the beat the
7156
+ * clip covers only the mark itself, and a click target nobody can see
7157
+ * over someone else's page is not something to ship — so the base rule
7158
+ * keeps "pointer-events: none" and only the three visible phases take it
7159
+ * back.
7160
+ * The button paints last and therefore wins the pointer where the two
7161
+ * overlap, so dragging the launcher is unaffected throughout.
7162
+ */
7163
+ .cpk-launcher-pill[data-cpk-pill-phase="opening"],
7164
+ .cpk-launcher-pill[data-cpk-pill-phase="holding"],
7165
+ .cpk-launcher-pill[data-cpk-pill-phase="closing"] {
7166
+ pointer-events: auto;
7167
+ cursor: pointer;
7168
+ }
7169
+
7170
+ /* Closing is the same animation played backwards, so the two phases can
7171
+ never drift apart. */
7172
+ .cpk-launcher-pill[data-cpk-pill-phase="opening"],
7173
+ .cpk-launcher-pill[data-cpk-pill-phase="closing"] {
7174
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
7175
+ animation-iteration-count: 1;
7176
+ animation-fill-mode: forwards;
7177
+ }
7178
+ .cpk-launcher-pill[data-cpk-pill-phase="opening"] {
7179
+ animation-duration: var(--cpk-launcher-pill-open);
7180
+ }
7181
+ .cpk-launcher-pill[data-cpk-pill-phase="closing"] {
7182
+ animation-duration: var(--cpk-launcher-pill-close);
7183
+ animation-direction: reverse;
7184
+ }
7185
+ .cpk-launcher-pill[data-cpk-pill-phase="opening"][data-cpk-pill-direction="left"],
7186
+ .cpk-launcher-pill[data-cpk-pill-phase="closing"][data-cpk-pill-direction="left"] {
7187
+ animation-name: cpk-launcher-pill-left;
7188
+ }
7189
+ .cpk-launcher-pill[data-cpk-pill-phase="opening"][data-cpk-pill-direction="right"],
7190
+ .cpk-launcher-pill[data-cpk-pill-phase="closing"][data-cpk-pill-direction="right"] {
7191
+ animation-name: cpk-launcher-pill-right;
7192
+ }
7193
+
7194
+ /* The hold is the end state of the reveal, held. */
7195
+ .cpk-launcher-pill[data-cpk-pill-phase="holding"] {
7196
+ opacity: 1;
7197
+ clip-path: inset(0 0 0 0);
6550
7198
  }
6551
7199
 
6552
7200
  /*
@@ -6554,6 +7202,18 @@ ${argsString}</pre
6554
7202
  * the information arrives without the movement.
6555
7203
  */
6556
7204
  @media (prefers-reduced-motion: reduce) {
7205
+ /*
7206
+ * The pill is shown by opacity alone, with no clip animation and the
7207
+ * same hold. The instruction is to reduce motion, not to withhold
7208
+ * information, and this reader needs the label as much as anyone.
7209
+ */
7210
+ .cpk-launcher-pill[data-cpk-pill-phase="opening"],
7211
+ .cpk-launcher-pill[data-cpk-pill-phase="holding"],
7212
+ .cpk-launcher-pill[data-cpk-pill-phase="closing"] {
7213
+ animation: none !important;
7214
+ opacity: 1;
7215
+ clip-path: inset(0 0 0 0);
7216
+ }
6557
7217
  .cpk-launcher-signal-wash {
6558
7218
  opacity: 0.85;
6559
7219
  }
@@ -6573,70 +7233,287 @@ ${argsString}</pre
6573
7233
  }
6574
7234
  }
6575
7235
 
6576
- /*
6577
- * Unread marker on the navigation entry, which is what keeps the signal
6578
- * alive once the panel is open and the launcher is hidden. Static by
6579
- * design: the pulse belongs to the launcher, and movement here would
6580
- * compete with the live event stream a developer is actually watching.
6581
- */
6582
- .inspector-nav-signal-dot {
6583
- display: inline-block;
6584
- flex: none;
6585
- width: 6px;
6586
- height: 6px;
6587
- border-radius: 50%;
6588
- background: ${(0, lit.unsafeCSS)(NEWS_SIGNAL_COLOR)};
7236
+ /* ── Launcher HUD: hover menu, quieter than the error island ── */
7237
+ .console-button-wrapper[data-cpk-hud="open"] .cpk-launcher-hud {
7238
+ pointer-events: auto;
7239
+ opacity: 1;
7240
+ transform: none;
7241
+ visibility: visible;
6589
7242
  }
6590
7243
 
6591
- /* ── Inspector window ────────────────────────────────────────── */
6592
- .inspector-window {
6593
- border: 1px solid #d8d8e8 !important;
6594
- border-radius: 5px !important;
6595
- box-shadow: none !important;
7244
+ .cpk-launcher-hud {
7245
+ position: absolute;
7246
+ top: 0;
7247
+ z-index: 4;
7248
+ padding-right: 14px;
7249
+ pointer-events: none;
7250
+ opacity: 0;
7251
+ visibility: hidden;
7252
+ transform: translateX(8px);
7253
+ transition:
7254
+ opacity 160ms ease,
7255
+ transform 200ms cubic-bezier(0.16, 1, 0.3, 1);
6596
7256
  }
6597
7257
 
6598
- /* ── Header drag area ────────────────────────────────────────── */
6599
- .drag-handle {
6600
- border-bottom-color: #d8d8e8 !important;
6601
- background-color: #f7f6fd !important;
7258
+ .cpk-launcher-hud[data-cpk-hud-side="left"] {
7259
+ right: 100%;
7260
+ padding-right: 14px;
7261
+ padding-left: 0;
6602
7262
  }
6603
7263
 
6604
- .inspector-account-strip {
6605
- background: linear-gradient(
6606
- 90deg,
6607
- #ffffff 0%,
6608
- #f3f1ff 58%,
6609
- #eefbf7 100%
6610
- ) !important;
6611
- color: #010507 !important;
7264
+ .cpk-launcher-hud[data-cpk-hud-side="right"] {
7265
+ left: 100%;
7266
+ right: auto;
7267
+ padding-right: 0;
7268
+ padding-left: 14px;
7269
+ transform: translateX(-8px);
6612
7270
  }
6613
7271
 
6614
- /* ── Tab buttons ─────────────────────────────────────────────── */
6615
- /*
6616
- * Named classes owned by this component — no Tailwind conflict.
6617
- * Active: brand surface/surfaceContainerActive (lilac tint) +
6618
- * border/borderActionEnabled underline.
6619
- * Dark fill is for primary action buttons only, not nav tabs.
6620
- */
6621
- .cpk-tab-active {
6622
- background-color: rgba(190, 194, 255, 0.18);
6623
- color: #010507;
6624
- font-weight: 600;
7272
+ .console-button-wrapper[data-cpk-hud="open"]
7273
+ .cpk-launcher-hud[data-cpk-hud-side="right"] {
7274
+ transform: none;
6625
7275
  }
6626
- .cpk-tab-icon {
6627
- display: inline-flex;
6628
- flex-shrink: 0;
6629
- align-items: center;
7276
+
7277
+ .cpk-launcher-hud__card {
7278
+ --hud-fill: rgb(28 31 36 / 0.88);
7279
+ --hud-line: rgb(190 194 255 / 0.5);
7280
+ position: relative;
7281
+ width: 228px;
7282
+ padding: 4px;
7283
+ border: 1px dotted var(--hud-line);
7284
+ border-radius: 10px;
7285
+ background: var(--hud-fill);
7286
+ color: #fff;
7287
+ backdrop-filter: blur(12px) saturate(1.2);
7288
+ box-shadow: 0 8px 20px rgb(1 5 7 / 0.18);
6630
7289
  }
6631
- .cpk-tab-active .cpk-tab-icon {
6632
- color: #5558b2;
7290
+
7291
+ .cpk-launcher-hud__arrow {
7292
+ position: absolute;
7293
+ top: calc(var(--cpk-launcher-size) / 2);
7294
+ width: 10px;
7295
+ height: 10px;
7296
+ background: var(--hud-fill);
7297
+ transform: translateY(-50%) rotate(45deg);
6633
7298
  }
6634
- .cpk-tab-inactive {
6635
- background-color: transparent;
6636
- color: #2b2b2b;
7299
+
7300
+ .cpk-launcher-hud[data-cpk-hud-side="left"] .cpk-launcher-hud__arrow {
7301
+ right: -5px;
7302
+ border-top: 1px solid var(--hud-line);
7303
+ border-right: 1px solid var(--hud-line);
6637
7304
  }
6638
- .cpk-tab-inactive .cpk-tab-icon {
6639
- color: #68686e;
7305
+
7306
+ .cpk-launcher-hud[data-cpk-hud-side="right"] .cpk-launcher-hud__arrow {
7307
+ left: -5px;
7308
+ border-bottom: 1px solid var(--hud-line);
7309
+ border-left: 1px solid var(--hud-line);
7310
+ }
7311
+
7312
+ .cpk-launcher-hud__list {
7313
+ margin: 0;
7314
+ padding: 0;
7315
+ list-style: none;
7316
+ }
7317
+
7318
+ .cpk-launcher-hud__list + .cpk-launcher-hud__list {
7319
+ margin-top: 4px;
7320
+ padding-top: 4px;
7321
+ border-top: 1px dotted var(--hud-line);
7322
+ }
7323
+
7324
+ .cpk-launcher-hud__row {
7325
+ position: relative;
7326
+ display: grid;
7327
+ grid-template-columns: 1fr 28px;
7328
+ align-items: start;
7329
+ border-radius: 7px;
7330
+ cursor: pointer;
7331
+ }
7332
+
7333
+ .cpk-launcher-hud__row + .cpk-launcher-hud__row {
7334
+ margin-top: 1px;
7335
+ }
7336
+
7337
+ .cpk-launcher-hud__row:hover,
7338
+ .cpk-launcher-hud__row:focus-within,
7339
+ .cpk-launcher-hud__row[data-cpk-hud-help="open"] {
7340
+ background: rgb(255 255 255 / 0.06);
7341
+ }
7342
+
7343
+ .cpk-launcher-hud__action {
7344
+ display: flex;
7345
+ gap: 8px;
7346
+ min-height: 32px;
7347
+ align-items: center;
7348
+ padding: 6px 8px;
7349
+ border: 0;
7350
+ border-radius: 7px;
7351
+ background: transparent;
7352
+ color: #fff;
7353
+ font-family: inherit;
7354
+ font-size: 12px;
7355
+ font-weight: 600;
7356
+ text-align: start;
7357
+ cursor: pointer;
7358
+ }
7359
+
7360
+ /* Stretch the row action over the whole tab, including the detail
7361
+ copy. The help mark sits above this layer. */
7362
+ .cpk-launcher-hud__action::after {
7363
+ content: "";
7364
+ position: absolute;
7365
+ inset: 0;
7366
+ }
7367
+
7368
+ .cpk-launcher-hud__check {
7369
+ flex: none;
7370
+ width: 14px;
7371
+ height: 14px;
7372
+ color: #34d399;
7373
+ }
7374
+
7375
+ .cpk-launcher-hud__help {
7376
+ position: relative;
7377
+ z-index: 1;
7378
+ display: inline-flex;
7379
+ width: 28px;
7380
+ height: 32px;
7381
+ align-items: center;
7382
+ justify-content: center;
7383
+ padding: 0;
7384
+ border: 0;
7385
+ background: transparent;
7386
+ color: rgb(255 255 255 / 0.78);
7387
+ font-size: 12px;
7388
+ font-weight: 600;
7389
+ cursor: pointer;
7390
+ }
7391
+
7392
+ .cpk-launcher-hud__help span {
7393
+ display: inline-flex;
7394
+ width: 16px;
7395
+ height: 16px;
7396
+ align-items: center;
7397
+ justify-content: center;
7398
+ border: 1px dotted rgb(190 194 255 / 0.55);
7399
+ border-radius: 50%;
7400
+ line-height: 1;
7401
+ }
7402
+
7403
+ .cpk-launcher-hud__help:focus-visible,
7404
+ .cpk-launcher-hud__action:focus-visible {
7405
+ outline: 2px solid #bec2ff;
7406
+ outline-offset: 1px;
7407
+ }
7408
+
7409
+ .cpk-launcher-hud__detail {
7410
+ grid-column: 1 / -1;
7411
+ max-height: 0;
7412
+ margin: 0;
7413
+ padding: 0 8px;
7414
+ overflow: hidden;
7415
+ color: rgb(255 255 255 / 0.78);
7416
+ font-size: 11px;
7417
+ font-weight: 400;
7418
+ line-height: 1.4;
7419
+ opacity: 0;
7420
+ pointer-events: none;
7421
+ transform: translateY(-6px);
7422
+ transition:
7423
+ max-height 200ms cubic-bezier(0.16, 1, 0.3, 1),
7424
+ opacity 150ms ease-out,
7425
+ transform 200ms cubic-bezier(0.16, 1, 0.3, 1),
7426
+ padding-bottom 200ms cubic-bezier(0.16, 1, 0.3, 1);
7427
+ }
7428
+
7429
+ .cpk-launcher-hud__row:hover .cpk-launcher-hud__detail,
7430
+ .cpk-launcher-hud__row:focus-within .cpk-launcher-hud__detail,
7431
+ .cpk-launcher-hud__row[data-cpk-hud-help="open"] .cpk-launcher-hud__detail {
7432
+ max-height: 72px;
7433
+ padding: 0 8px 7px;
7434
+ opacity: 1;
7435
+ transform: none;
7436
+ }
7437
+
7438
+ @media (prefers-reduced-motion: reduce) {
7439
+ .cpk-launcher-hud,
7440
+ .cpk-launcher-hud__detail {
7441
+ transition: none;
7442
+ }
7443
+ }
7444
+
7445
+ /*
7446
+ * Marker on the navigation entry, which is what keeps a signal alive
7447
+ * once the panel is open and the launcher is hidden. Static by design:
7448
+ * the beat belongs to the launcher, and movement here would compete with
7449
+ * the live event stream a developer is actually watching.
7450
+ *
7451
+ * Tone-selected rather than tone-agnostic, because the marker has to
7452
+ * agree with the dot that sent the reader here. Same shape, same
7453
+ * placement, one declaration different — as on the launcher, where the
7454
+ * treatment is shared and only the injected colour changes.
7455
+ */
7456
+ .inspector-nav-signal-dot {
7457
+ display: inline-block;
7458
+ flex: none;
7459
+ width: 6px;
7460
+ height: 6px;
7461
+ border-radius: 50%;
7462
+ background: ${(0, lit.unsafeCSS)(LAUNCHER_SIGNAL_COLORS.news)};
7463
+ }
7464
+ .inspector-nav-signal-dot[data-cpk-signal-tone="error"] {
7465
+ background: ${(0, lit.unsafeCSS)(LAUNCHER_SIGNAL_COLORS.error)};
7466
+ }
7467
+
7468
+ /* ── Inspector window ────────────────────────────────────────── */
7469
+ .inspector-window {
7470
+ border: 1px solid #d8d8e8 !important;
7471
+ border-radius: 5px !important;
7472
+ box-shadow: none !important;
7473
+ }
7474
+
7475
+ /* ── Header drag area ────────────────────────────────────────── */
7476
+ .drag-handle {
7477
+ border-bottom-color: #d8d8e8 !important;
7478
+ background-color: #f7f6fd !important;
7479
+ }
7480
+
7481
+ .inspector-account-strip {
7482
+ background: linear-gradient(
7483
+ 90deg,
7484
+ #ffffff 0%,
7485
+ #f3f1ff 58%,
7486
+ #eefbf7 100%
7487
+ ) !important;
7488
+ color: #010507 !important;
7489
+ }
7490
+
7491
+ /* ── Tab buttons ─────────────────────────────────────────────── */
7492
+ /*
7493
+ * Named classes owned by this component — no Tailwind conflict.
7494
+ * Active: brand surface/surfaceContainerActive (lilac tint) +
7495
+ * border/borderActionEnabled underline.
7496
+ * Dark fill is for primary action buttons only, not nav tabs.
7497
+ */
7498
+ .cpk-tab-active {
7499
+ background-color: rgba(190, 194, 255, 0.18);
7500
+ color: #010507;
7501
+ font-weight: 600;
7502
+ }
7503
+ .cpk-tab-icon {
7504
+ display: inline-flex;
7505
+ flex-shrink: 0;
7506
+ align-items: center;
7507
+ }
7508
+ .cpk-tab-active .cpk-tab-icon {
7509
+ color: #5558b2;
7510
+ }
7511
+ .cpk-tab-inactive {
7512
+ background-color: transparent;
7513
+ color: #2b2b2b;
7514
+ }
7515
+ .cpk-tab-inactive .cpk-tab-icon {
7516
+ color: #68686e;
6640
7517
  }
6641
7518
  .cpk-tab-inactive:hover {
6642
7519
  background-color: rgba(190, 194, 255, 0.08);
@@ -6917,6 +7794,7 @@ ${argsString}</pre
6917
7794
  require_persistence.clearLegacyAnnouncementReadState();
6918
7795
  this.tryAutoAttachCore();
6919
7796
  this.ensureAnnouncementLoading();
7797
+ this.subscribeToInspectorThreadBridge();
6920
7798
  }
6921
7799
  this.requestUpdate();
6922
7800
  }
@@ -6939,13 +7817,16 @@ ${argsString}</pre
6939
7817
  clearTimeout(this.transitionTimeoutId);
6940
7818
  this.transitionTimeoutId = null;
6941
7819
  }
7820
+ this.unsubscribeFromInspectorThreadBridge();
6942
7821
  this.threadsSetupPromptCopyGeneration += 1;
6943
7822
  if (this.threadsSetupPromptCopyResetTimeoutId !== null) {
6944
7823
  window.clearTimeout(this.threadsSetupPromptCopyResetTimeoutId);
6945
7824
  this.threadsSetupPromptCopyResetTimeoutId = null;
6946
7825
  }
6947
7826
  this.threadsSetupPromptCopyState = "idle";
6948
- this.stopNewsSignalPulse();
7827
+ this.stopSignalPulse();
7828
+ this.cancelGestureTail();
7829
+ this.cancelThreadRefreshDebounce();
6949
7830
  this.clearInspectorUsageRefresh();
6950
7831
  this.cleanupThreadsExampleOverviewVideo();
6951
7832
  this.removeDockStyles(true);
@@ -6988,6 +7869,7 @@ ${argsString}</pre
6988
7869
  ` : this.renderButton();
6989
7870
  }
6990
7871
  willUpdate() {
7872
+ this.evaluateErrorSignals();
6991
7873
  this.reconcileSelectedMenuVisibility();
6992
7874
  if (this.isOpen && this.dockMode === "docked-left") this.setAttribute("data-docked", "true");
6993
7875
  else this.removeAttribute("data-docked");
@@ -6997,7 +7879,11 @@ ${argsString}</pre
6997
7879
  this.syncThreadsExampleOverviewVideo();
6998
7880
  this.maybeTrackInspectorMetadataViews();
6999
7881
  this.maybeTrackNewsSignalViewed();
7882
+ this.resolvePillDirection();
7883
+ this.maybeTrackErrorSignalViewed();
7000
7884
  this.maybeCompleteWhatsNewView();
7885
+ this.maybeCompleteEventErrorView();
7886
+ this.flushErrorLandingScroll();
7001
7887
  this.maybeTrackHomeViewed();
7002
7888
  if (!this.isOpen) {
7003
7889
  this.lastScrolledAgentNavigationLayout = null;
@@ -7035,17 +7921,10 @@ ${argsString}</pre
7035
7921
  "justify-center",
7036
7922
  "rounded-full",
7037
7923
  "border",
7038
- "border-white/20",
7039
- "bg-slate-950/95",
7040
7924
  "text-xs",
7041
7925
  "font-medium",
7042
7926
  "text-white",
7043
- "ring-1",
7044
- "ring-white/10",
7045
- "backdrop-blur-md",
7046
7927
  "transition",
7047
- "hover:border-white/30",
7048
- "hover:bg-slate-900/95",
7049
7928
  "hover:scale-105",
7050
7929
  "focus-visible:outline",
7051
7930
  "focus-visible:outline-2",
@@ -7055,20 +7934,33 @@ ${argsString}</pre
7055
7934
  "select-none",
7056
7935
  this.isDragging ? "cursor-grabbing" : "cursor-pointer"
7057
7936
  ].join(" ");
7058
- const signalStyles = this.newsSignalArmed ? {
7059
- "--cpk-launcher-signal": NEWS_SIGNAL_COLOR,
7060
- "--cpk-launcher-cadence": `${NEWS_SIGNAL_CADENCE_MS}ms`
7937
+ const activeSignal = this.getActiveLauncherSignal();
7938
+ const signal = activeSignal ? LAUNCHER_SIGNALS[activeSignal] : null;
7939
+ const signalStyles = signal ? {
7940
+ "--cpk-launcher-signal": LAUNCHER_SIGNAL_COLORS[signal.tone],
7941
+ "--cpk-launcher-cadence": `${signal.cadence}ms`
7061
7942
  } : {};
7062
7943
  return lit.html`
7063
- <div class="console-button-wrapper">
7944
+ <div
7945
+ class="console-button-wrapper"
7946
+ data-cpk-hud=${this.launcherHudOpen ? "open" : "closed"}
7947
+ @pointerenter=${this.handleLauncherHudEnter}
7948
+ @pointerleave=${this.handleLauncherHudLeave}
7949
+ @focusin=${this.handleLauncherHudFocusIn}
7950
+ @focusout=${this.handleLauncherHudFocusOut}
7951
+ @keydown=${this.handleLauncherHudKeydown}
7952
+ >
7953
+ ${this.renderLauncherPill()}
7064
7954
  <button
7065
7955
  class=${buttonClasses}
7066
7956
  type="button"
7067
- aria-label="Web Inspector"
7068
- title=${this.newsSignalArmed ? `${WHATS_NEW_VIEW_LABEL} — unread` : lit.nothing}
7957
+ aria-expanded=${this.launcherHudOpen ? "true" : "false"}
7958
+ aria-controls=${this.launcherHudOpen ? "cpk-launcher-hud" : lit.nothing}
7959
+ aria-label=${signal && signal.tone === "error" ? `${LAUNCHER_BASE_LABEL}, ${signal.accessibleLabel}` : LAUNCHER_BASE_LABEL}
7960
+ title=${activeSignal === NEWS_SIGNAL_ID ? `${WHATS_NEW_VIEW_LABEL} — unread` : lit.nothing}
7069
7961
  data-drag-context="button"
7070
- data-cpk-signal=${this.newsSignalArmed ? NEWS_SIGNAL_TONE : lit.nothing}
7071
- data-cpk-signal-pulsing=${this.newsSignalArmed && this.newsSignalPulsing ? "true" : lit.nothing}
7962
+ data-cpk-signal=${signal ? signal.tone : lit.nothing}
7963
+ data-cpk-signal-pulsing=${activeSignal !== null && this.pulsingSignal === activeSignal ? "true" : lit.nothing}
7072
7964
  style=${(0, lit_directives_style_map_js.styleMap)(signalStyles)}
7073
7965
  data-dragging=${this.isDragging && this.pointerContext === "button" ? "true" : "false"}
7074
7966
  @pointerdown=${this.handlePointerDown}
@@ -7083,16 +7975,210 @@ ${argsString}</pre
7083
7975
  class="cpk-launcher-mark h-6 w-auto"
7084
7976
  loading="lazy"
7085
7977
  />
7086
- ${this.newsSignalArmed ? lit.html`<span
7978
+ ${activeSignal !== null ? lit.html`<span
7087
7979
  class="cpk-launcher-signal-wash"
7088
7980
  aria-hidden="true"
7089
7981
  ></span>
7090
7982
  <span
7091
7983
  class="cpk-launcher-signal-dot"
7092
- data-cpk-signal-dot=${NEWS_SIGNAL_ID}
7984
+ data-cpk-signal-dot=${activeSignal}
7093
7985
  aria-hidden="true"
7094
7986
  ></span>` : lit.nothing}
7095
7987
  </button>
7988
+ ${lit.html`<span
7989
+ class="sr-only"
7990
+ data-cpk-launcher-announcement
7991
+ role="status"
7992
+ aria-live="polite"
7993
+ >${this.getGestureLabel() ?? ""}</span
7994
+ >`}
7995
+ ${this.renderLauncherHud()}
7996
+ </div>
7997
+ `;
7998
+ }
7999
+ /**
8000
+ * The words the running gesture carries, or null when the launcher is quiet.
8001
+ *
8002
+ * Read from the signal's own description rather than from a condition on the
8003
+ * tone, so a third signal can carry a pill by declaring a label.
8004
+ */
8005
+ getGestureLabel() {
8006
+ if (this.gestureSignal === null) return null;
8007
+ return LAUNCHER_SIGNALS[this.gestureSignal].pillLabel ?? null;
8008
+ }
8009
+ /**
8010
+ * The pill, laid out at its full width and clipped, for the whole gesture.
8011
+ *
8012
+ * It renders from the first frame of the beat — clipped to nothing, so it
8013
+ * shows nothing — because the room it needs cannot be measured until it has
8014
+ * been laid out, and the direction is decided at gesture start.
8015
+ */
8016
+ renderLauncherPill() {
8017
+ const key = this.gestureSignal;
8018
+ if (key === null || this.pillPhase === null) return lit.nothing;
8019
+ const signal = LAUNCHER_SIGNALS[key];
8020
+ const label = signal.pillLabel;
8021
+ if (label === void 0) return lit.nothing;
8022
+ return lit.html`
8023
+ <span
8024
+ class="cpk-launcher-pill"
8025
+ data-cpk-launcher-pill=${key}
8026
+ data-cpk-pill-phase=${this.pillPhase}
8027
+ data-cpk-pill-direction=${this.pillDirection ?? "left"}
8028
+ style=${(0, lit_directives_style_map_js.styleMap)({
8029
+ "--cpk-launcher-signal": LAUNCHER_SIGNAL_COLORS[signal.tone],
8030
+ "--cpk-launcher-pill-open": `${ERROR_GESTURE_MS.open}ms`,
8031
+ "--cpk-launcher-pill-close": `${ERROR_GESTURE_MS.close}ms`
8032
+ })}
8033
+ aria-hidden="true"
8034
+ @click=${this.handlePillClick}
8035
+ >
8036
+ <span class="cpk-launcher-pill__heading" data-cpk-pill-heading
8037
+ >${label}</span
8038
+ >
8039
+ <span class="cpk-launcher-pill__subline" data-cpk-pill-subline
8040
+ >${PILL_SUBLINE_LABEL}</span
8041
+ >
8042
+ </span>
8043
+ `;
8044
+ }
8045
+ isLauncherHudBlocked() {
8046
+ return this.gestureSignal !== null;
8047
+ }
8048
+ resolveLauncherHudSide() {
8049
+ if (typeof window === "undefined") {
8050
+ this.launcherHudSide = "left";
8051
+ return;
8052
+ }
8053
+ const button = this.activeRoot.querySelector(".console-button");
8054
+ if (!button) {
8055
+ this.launcherHudSide = "left";
8056
+ return;
8057
+ }
8058
+ if (button.getBoundingClientRect().left - LAUNCHER_HUD_WIDTH >= EDGE_MARGIN) {
8059
+ this.launcherHudSide = "left";
8060
+ return;
8061
+ }
8062
+ this.launcherHudSide = "right";
8063
+ }
8064
+ openLauncherHud() {
8065
+ if (this.isLauncherHudBlocked() || this.isOpen) return;
8066
+ this.resolveLauncherHudSide();
8067
+ if (this.launcherHudCloseTimer !== null) {
8068
+ clearTimeout(this.launcherHudCloseTimer);
8069
+ this.launcherHudCloseTimer = null;
8070
+ }
8071
+ if (this.launcherHudOpen) return;
8072
+ this.launcherHudOpen = true;
8073
+ this.requestUpdate();
8074
+ }
8075
+ closeLauncherHud() {
8076
+ if (this.launcherHudCloseTimer !== null) {
8077
+ clearTimeout(this.launcherHudCloseTimer);
8078
+ this.launcherHudCloseTimer = null;
8079
+ }
8080
+ if (!this.launcherHudOpen && this.launcherHudHelp === null) return;
8081
+ this.launcherHudOpen = false;
8082
+ this.launcherHudHelp = null;
8083
+ this.requestUpdate();
8084
+ }
8085
+ renderHudCheck() {
8086
+ return lit.html`
8087
+ <svg
8088
+ class="cpk-launcher-hud__check"
8089
+ viewBox="0 0 16 16"
8090
+ aria-hidden="true"
8091
+ focusable="false"
8092
+ data-cpk-hud-check
8093
+ >
8094
+ <path
8095
+ fill="none"
8096
+ stroke="currentColor"
8097
+ stroke-width="2"
8098
+ stroke-linecap="round"
8099
+ stroke-linejoin="round"
8100
+ d="M3 8.5 6.5 12 13 4.5"
8101
+ />
8102
+ </svg>
8103
+ `;
8104
+ }
8105
+ renderHudRow(args) {
8106
+ const helpOpen = this.launcherHudHelp === args.id;
8107
+ const detailId = `cpk-hud-detail-${args.id}`;
8108
+ return lit.html`
8109
+ <li
8110
+ class="cpk-launcher-hud__row"
8111
+ data-cpk-hud-row=${args.id}
8112
+ data-cpk-hud-help=${helpOpen ? "open" : lit.nothing}
8113
+ @click=${(event) => this.handleHudRowClick(event, args.id)}
8114
+ >
8115
+ <button
8116
+ type="button"
8117
+ class="cpk-launcher-hud__action"
8118
+ data-cpk-hud-action
8119
+ aria-describedby=${detailId}
8120
+ @click=${(event) => this.handleHudActionClick(event, args.id)}
8121
+ @pointerdown=${(event) => event.stopPropagation()}
8122
+ >
8123
+ ${args.connected ? this.renderHudCheck() : lit.nothing}${args.label}
8124
+ </button>
8125
+ <button
8126
+ type="button"
8127
+ class="cpk-launcher-hud__help"
8128
+ aria-expanded=${helpOpen ? "true" : "false"}
8129
+ aria-controls=${detailId}
8130
+ aria-label=${`About ${args.label}`}
8131
+ @click=${(event) => this.handleHudHelpClick(event, args.id)}
8132
+ @pointerdown=${(event) => event.stopPropagation()}
8133
+ >
8134
+ <span aria-hidden="true">?</span>
8135
+ </button>
8136
+ <p class="cpk-launcher-hud__detail" id=${detailId}>${args.detail}</p>
8137
+ </li>
8138
+ `;
8139
+ }
8140
+ renderLauncherHud() {
8141
+ if (!this.launcherHudOpen) return lit.nothing;
8142
+ const threadsOn = this.areThreadEndpointsAvailable();
8143
+ const intelligenceOn = Boolean(this._core?.intelligence);
8144
+ const learningOn = intelligenceOn && this._memoriesAvailable;
8145
+ return lit.html`
8146
+ <div
8147
+ class="cpk-launcher-hud"
8148
+ id="cpk-launcher-hud"
8149
+ data-cpk-launcher-hud
8150
+ data-cpk-hud-side=${this.launcherHudSide}
8151
+ >
8152
+ <div class="cpk-launcher-hud__card">
8153
+ <span class="cpk-launcher-hud__arrow" aria-hidden="true"></span>
8154
+ <ul class="cpk-launcher-hud__list" role="list">
8155
+ ${this.renderHudRow({
8156
+ id: "inspector",
8157
+ label: HUD_OPEN_INSPECTOR_LABEL,
8158
+ detail: HUD_OPEN_INSPECTOR_DETAIL
8159
+ })}
8160
+ </ul>
8161
+ <ul class="cpk-launcher-hud__list" role="list">
8162
+ ${this.renderHudRow({
8163
+ id: "threads",
8164
+ label: threadsOn ? HUD_THREADS_ON_LABEL : HUD_THREADS_OFF_LABEL,
8165
+ detail: threadsOn ? HUD_THREADS_ON_DETAIL : HUD_THREADS_OFF_DETAIL,
8166
+ connected: threadsOn
8167
+ })}
8168
+ ${this.renderHudRow({
8169
+ id: "intelligence",
8170
+ label: intelligenceOn ? HUD_INTELLIGENCE_ON_LABEL : HUD_INTELLIGENCE_OFF_LABEL,
8171
+ detail: intelligenceOn ? HUD_INTELLIGENCE_ON_DETAIL : HUD_INTELLIGENCE_OFF_DETAIL,
8172
+ connected: intelligenceOn
8173
+ })}
8174
+ ${this.renderHudRow({
8175
+ id: "learning",
8176
+ label: learningOn ? HUD_LEARNING_ON_LABEL : HUD_LEARNING_OFF_LABEL,
8177
+ detail: learningOn ? HUD_LEARNING_ON_DETAIL : HUD_LEARNING_OFF_DETAIL,
8178
+ connected: learningOn
8179
+ })}
8180
+ </ul>
8181
+ </div>
7096
8182
  </div>
7097
8183
  `;
7098
8184
  }
@@ -7114,7 +8200,6 @@ ${argsString}</pre
7114
8200
  `;
7115
8201
  }
7116
8202
  renderInspectorSidebar(iconRail, automaticallyCollapsed, agentSelector) {
7117
- const unread = this.newsSignalArmed && this.announcementLoaded;
7118
8203
  const homeModel = this.getHomeModel();
7119
8204
  return lit.html`
7120
8205
  <aside
@@ -7156,7 +8241,7 @@ ${argsString}</pre
7156
8241
  </button>` : lit.nothing}
7157
8242
  ${items.map((item) => {
7158
8243
  const isSelected = this.selectedMenu === item.key;
7159
- const showBadge = item.key === NEWS_SIGNAL_DESTINATION && unread;
8244
+ const marker = this.getNavigationSignalFor(item.key);
7160
8245
  return lit.html`
7161
8246
  <button
7162
8247
  type="button"
@@ -7164,7 +8249,7 @@ ${argsString}</pre
7164
8249
  data-inspector-group=${group}
7165
8250
  data-inspector-menu-key=${item.key}
7166
8251
  aria-current=${isSelected ? "page" : lit.nothing}
7167
- aria-label=${showBadge ? `${item.label}, new content` : item.label}
8252
+ aria-label=${marker ? `${item.label}, ${marker.accessibleLabel}` : item.label}
7168
8253
  data-inspector-tooltip=${item.label}
7169
8254
  title=${iconRail ? lit.nothing : item.label}
7170
8255
  style=${INTERACTIVE_FOCUS_BASE_STYLE}
@@ -7178,8 +8263,12 @@ ${argsString}</pre
7178
8263
  ${item.key === "threads" ? (0, lit_directives_unsafe_html_js.unsafeHTML)(this.customTabIcons.threads) : this.renderIcon(item.icon)}
7179
8264
  </span>
7180
8265
  <span class="inspector-nav-label">${item.label}</span>
7181
- ${showBadge ? lit.html`
7182
- <span class="inspector-nav-signal-dot" aria-hidden="true"></span>
8266
+ ${marker ? lit.html`
8267
+ <span
8268
+ class="inspector-nav-signal-dot"
8269
+ data-cpk-signal-tone=${marker.tone}
8270
+ aria-hidden="true"
8271
+ ></span>
7183
8272
  ` : lit.nothing}
7184
8273
  </button>
7185
8274
  `;
@@ -7653,6 +8742,31 @@ ${argsString}</pre
7653
8742
  `)}
7654
8743
  </dl>
7655
8744
  </section>
8745
+ `;
8746
+ }
8747
+ renderEventErrorBanner(key) {
8748
+ const error = this.eventErrorDetails[key];
8749
+ if (!error) return lit.nothing;
8750
+ const guide = EVENT_ERROR_GUIDANCE[key];
8751
+ return lit.html`
8752
+ <div
8753
+ class="mx-3 mt-3 flex cursor-pointer items-start gap-2 rounded-lg border border-rose-200 bg-rose-50 px-3 py-2 text-left text-[11px] text-rose-950"
8754
+ role="alert"
8755
+ tabindex="0"
8756
+ data-cpk-event-error=${key}
8757
+ @click=${this.refocusEventErrorLanding}
8758
+ @keydown=${this.handleEventErrorBannerKeydown}
8759
+ >
8760
+ <span class="mt-0.5 shrink-0">${this.renderIcon("TriangleAlert")}</span>
8761
+ <div class="min-w-0 flex-1 space-y-1">
8762
+ <p class="font-semibold">${guide.title}</p>
8763
+ ${error.agentId ? lit.html`<p>Agent: ${error.agentId}</p>` : lit.nothing}
8764
+ ${error.toolName ? lit.html`<p>Tool: ${error.toolName}</p>` : lit.nothing}
8765
+ <p class="break-words leading-relaxed">${error.message}</p>
8766
+ ${guide.advice ? lit.html`<p class="leading-relaxed">${guide.advice}</p>` : lit.nothing}
8767
+ ${guide.highlight && this.hasEventErrorHighlight(key) ? lit.html`<p class="leading-relaxed">${guide.highlight}</p>` : lit.nothing}
8768
+ </div>
8769
+ </div>
7656
8770
  `;
7657
8771
  }
7658
8772
  handleHomeLastEventSelect(eventId, agentId) {
@@ -8375,17 +9489,29 @@ ${argsString}</pre
8375
9489
  if (this.isPoppedOut) return;
8376
9490
  if (this.isOpen) return;
8377
9491
  const hadUnseenAnnouncement = this.newsSignalArmed;
9492
+ const activeSignalAtOpen = this.getActiveLauncherSignal();
8378
9493
  const firstOpen = !this.hasOpenedInspector;
8379
9494
  this.hasOpenedInspector = true;
8380
9495
  this.homeViewedThisOpen = false;
8381
- if (this.newsSignalArmed && source === "floating_button") {
8382
- this.selectedMenu = "home";
8383
- this.lastSelectedMenuByGroup.home = "home";
9496
+ this.closeLauncherHud();
9497
+ const hudMenu = this.hudLandingMenu;
9498
+ this.hudLandingMenu = null;
9499
+ if (hudMenu) {
9500
+ this.selectedMenu = hudMenu;
9501
+ this.lastSelectedMenuByGroup[require_inspector_nav.getGroupForMenu(hudMenu)] = hudMenu;
9502
+ } else if (activeSignalAtOpen !== null && source === "floating_button") {
9503
+ const landing = LAUNCHER_SIGNALS[activeSignalAtOpen].landingTarget;
9504
+ this.selectedMenu = landing;
9505
+ this.lastSelectedMenuByGroup[require_inspector_nav.getGroupForMenu(landing)] = landing;
9506
+ if (landing === "agents" || landing === "ag-ui-events") {
9507
+ if (isEventErrorKey(activeSignalAtOpen)) this.applyEventErrorLanding(activeSignalAtOpen);
9508
+ }
8384
9509
  }
8385
9510
  this.ensureAnnouncementLoading();
8386
9511
  this.isOpen = true;
9512
+ this.cancelGestureTail();
8387
9513
  this.persistState();
8388
- this.trackOpened(source, hadUnseenAnnouncement, firstOpen);
9514
+ this.trackOpened(source, hadUnseenAnnouncement, firstOpen, activeSignalAtOpen);
8389
9515
  if (this.dockMode !== "floating") this.applyDockStyles();
8390
9516
  this.ensureWindowPlacement();
8391
9517
  this.requestUpdate();
@@ -8400,7 +9526,6 @@ ${argsString}</pre
8400
9526
  if (this.isPoppedOut) return;
8401
9527
  if (!this.isOpen) return;
8402
9528
  this.isOpen = false;
8403
- if (this.newsSignalPulsePending) this.startNewsSignalPulse();
8404
9529
  if (this.dockMode !== "floating") this.removeDockStyles();
8405
9530
  this.persistState();
8406
9531
  this.updateHostTransform("button");
@@ -8408,6 +9533,7 @@ ${argsString}</pre
8408
9533
  this.updateComplete.then(() => {
8409
9534
  this.measureContext("button");
8410
9535
  this.applyAnchorPosition("button");
9536
+ this.flushPendingSignalPulse();
8411
9537
  });
8412
9538
  }
8413
9539
  renderIcon(name) {
@@ -8588,6 +9714,7 @@ ${argsString}</pre
8588
9714
  contentText,
8589
9715
  contentRaw: raw.content !== void 0 ? this.sanitizeForLogging(raw.content) : void 0,
8590
9716
  toolCalls,
9717
+ toolCallId: typeof raw.toolCallId === "string" ? raw.toolCallId : void 0,
8591
9718
  activityType: typeof raw.activityType === "string" ? raw.activityType : void 0
8592
9719
  };
8593
9720
  }
@@ -8770,16 +9897,16 @@ ${argsString}</pre
8770
9897
  @submit=${this.handlePlaygroundSubmit}
8771
9898
  >
8772
9899
  ${this.playgroundError ? lit.html`<div
8773
- class="mx-auto mb-2 flex max-w-3xl items-start gap-2 rounded-lg border border-rose-200 bg-rose-50 px-2.5 py-2 text-[10px] text-rose-800"
9900
+ class="mx-auto mb-2 flex max-w-3xl items-start gap-2 rounded-lg border border-rose-200 bg-rose-50 px-2.5 py-2 text-[10px] text-rose-950"
8774
9901
  role="alert"
8775
9902
  data-playground-error
8776
9903
  >
8777
- <span class="mt-0.5 shrink-0 text-rose-600"
9904
+ <span class="mt-0.5 shrink-0"
8778
9905
  >${this.renderIcon("TriangleAlert")}</span
8779
9906
  >
8780
9907
  <div class="min-w-0 flex-1">
8781
9908
  <p class="font-semibold">Agent run failed</p>
8782
- <p class="mt-0.5 break-words leading-relaxed text-rose-700">
9909
+ <p class="mt-0.5 break-words leading-relaxed">
8783
9910
  ${this.playgroundError}
8784
9911
  </p>
8785
9912
  </div>
@@ -8829,7 +9956,14 @@ ${argsString}</pre
8829
9956
  if (this.settingsOpen) return this.renderSettingsPanel();
8830
9957
  if (this.selectedMenu === "home") return this.renderHomeView();
8831
9958
  if (this.selectedMenu === WHATS_NEW_MENU_KEY) return this.renderWhatsNewView();
8832
- if (this.selectedMenu === "ag-ui-events") return this.renderEventsTable();
9959
+ if (this.selectedMenu === "ag-ui-events") return lit.html`
9960
+ <div class="flex h-full min-h-0 flex-col">
9961
+ ${this.renderEventErrorBanner("run")}
9962
+ <div class="min-h-0 flex-1 overflow-hidden">
9963
+ ${this.renderEventsTable()}
9964
+ </div>
9965
+ </div>
9966
+ `;
8833
9967
  if (this.selectedMenu === "playground") return this.renderPlaygroundView();
8834
9968
  if (this.selectedMenu === "agents") return this.renderAgentsView();
8835
9969
  if (this.selectedMenu === "frontend-tools") return this.renderToolsView();
@@ -9214,8 +10348,9 @@ ${argsString}</pre
9214
10348
  </div>
9215
10349
  `;
9216
10350
  }
9217
- trackOpened(source, hadUnseenAnnouncement, firstOpen = false) {
10351
+ trackOpened(source, hadUnseenAnnouncement, firstOpen = false, activeSignal = null) {
9218
10352
  if (this.core?.telemetryDisabled) return;
10353
+ const errorSignal = activeSignal !== null && isErrorSignalKey(activeSignal) ? activeSignal : null;
9219
10354
  require_telemetry.trackInspectorOpened({
9220
10355
  open_source: source,
9221
10356
  ...this.runtimeStatus === "connected" ? {
@@ -9224,6 +10359,8 @@ ${argsString}</pre
9224
10359
  } : {},
9225
10360
  runtime_url_type: require_telemetry.getRuntimeUrlType(this.core?.runtimeUrl),
9226
10361
  has_unseen_announcement: hadUnseenAnnouncement,
10362
+ has_error_signal: errorSignal !== null,
10363
+ ...errorSignal === null ? {} : { error_signal_source: errorSignal },
9227
10364
  first_open: firstOpen
9228
10365
  });
9229
10366
  }
@@ -9387,6 +10524,51 @@ ${argsString}</pre
9387
10524
  example_kind: exampleKind
9388
10525
  });
9389
10526
  }
10527
+ subscribeToInspectorThreadBridge() {
10528
+ this.unsubscribeFromInspectorThreadBridge();
10529
+ if (!(0, _copilotkit_core.isInspectorThreadBridgeEnabled)()) return;
10530
+ this.inspectorBridgeUnsubscribers.push((0, _copilotkit_core.onInspectorActiveThread)((payload) => {
10531
+ if (payload.requestId !== this.activeViewInAppRequestId) return;
10532
+ this.inAppThreadId = payload.threadId;
10533
+ this.inAppAgentId = payload.agentId;
10534
+ this.inAppSource = payload.source;
10535
+ if (payload.source === "app") {
10536
+ this.activeViewInAppRequestId = null;
10537
+ this.viewInAppError = null;
10538
+ }
10539
+ this.requestUpdate();
10540
+ }), (0, _copilotkit_core.onInspectorViewThreadResult)((payload) => {
10541
+ if (payload.requestId !== this.activeViewInAppRequestId) return;
10542
+ if (payload.ok) {
10543
+ this.viewInAppError = null;
10544
+ this.inAppThreadId = payload.threadId;
10545
+ this.inAppAgentId = payload.agentId;
10546
+ this.inAppSource = "override";
10547
+ } else {
10548
+ this.activeViewInAppRequestId = null;
10549
+ this.inAppThreadId = null;
10550
+ this.inAppAgentId = null;
10551
+ this.inAppSource = null;
10552
+ this.viewInAppError = "The app could not load that thread. The previous chat is back.";
10553
+ }
10554
+ this.requestUpdate();
10555
+ }));
10556
+ }
10557
+ unsubscribeFromInspectorThreadBridge() {
10558
+ for (const unsubscribe of this.inspectorBridgeUnsubscribers) unsubscribe();
10559
+ this.inspectorBridgeUnsubscribers = [];
10560
+ }
10561
+ getViewInAppMode(thread, isExample) {
10562
+ if (!(0, _copilotkit_core.isInspectorThreadBridgeEnabled)()) return "hidden";
10563
+ if (!thread || isExample) return "hidden";
10564
+ if (this.activeViewInAppRequestId && this.inAppSource === "override" && this.inAppThreadId === thread.id) return "stop";
10565
+ return "view";
10566
+ }
10567
+ getSelectedRealThread() {
10568
+ if (!this.selectedThreadId) return null;
10569
+ if (this.selectedThreadId === this.selectedLocalExampleThreadId) return null;
10570
+ return this.getActiveThreadsState().displayThreads.find((thread) => thread.id === this.selectedThreadId) ?? null;
10571
+ }
9390
10572
  getCurrentExampleTourProps() {
9391
10573
  const exampleKind = this.selectedThreadId ? getExampleKind(this.selectedThreadId) : void 0;
9392
10574
  const tourPair = getExampleTourTelemetryPair(this.exampleTourStep);
@@ -10006,7 +11188,7 @@ ${argsString}</pre
10006
11188
  <line x1="12" y1="16" x2="12.01" y2="16" />
10007
11189
  </svg>
10008
11190
  <span style="font-size: 13px; color: #c0333a;">
10009
- Failed to load learning data
11191
+ ${MEMORY_LOAD_ERROR_LABEL}
10010
11192
  </span>
10011
11193
  <span
10012
11194
  style="
@@ -10019,6 +11201,17 @@ ${argsString}</pre
10019
11201
  >
10020
11202
  ${this._memoriesError.message}
10021
11203
  </span>
11204
+ <span
11205
+ style="
11206
+ max-width: 320px;
11207
+ text-align: center;
11208
+ font-size: 11px;
11209
+ line-height: 1.5;
11210
+ color: #c0333a;
11211
+ "
11212
+ >
11213
+ ${EVENT_ERROR_GUIDANCE.memory.advice}
11214
+ </span>
10022
11215
  </div>
10023
11216
  `;
10024
11217
  if (this._memoriesLoading && this._memories.length === 0) return lit.html`
@@ -10202,6 +11395,7 @@ ${argsString}</pre
10202
11395
  data-color-scheme=${this.colorScheme}
10203
11396
  .threads=${visibleThreads}
10204
11397
  .selectedThreadId=${this.selectedThreadId}
11398
+ .inAppThreadId=${this.inAppThreadId}
10205
11399
  .errorMessage=${threadsErrorMessage}
10206
11400
  .suppressEmptyState=${loadingWithoutRows}
10207
11401
  @threadSelected=${(e) => {
@@ -10271,6 +11465,10 @@ ${argsString}</pre
10271
11465
  .headers=${this._core?.headers ?? {}}
10272
11466
  .threadInspectionAvailable=${selectedThreadIsLocalExample || this.areThreadEndpointsAvailable() && this._core?.threadEndpoints?.inspect !== false}
10273
11467
  .liveMessageVersion=${this.liveMessageVersion.get(selectedThread.id) ?? 0}
11468
+ .viewInAppMode=${this.getViewInAppMode(selectedThread, selectedThreadIsLocalExample)}
11469
+ .viewInAppError=${this.viewInAppError}
11470
+ @viewInApp=${this.handleViewInApp}
11471
+ @stopViewing=${this.handleStopViewing}
10274
11472
  .focusMessageId=${this.focusedThreadMessageId}
10275
11473
  .focusRequestId=${this.threadFocusRequestId}
10276
11474
  .agentStateInput=${this.getLatestStateForAgent(selectedThread.agentId)}
@@ -10439,7 +11637,10 @@ ${argsString}</pre
10439
11637
  </div>
10440
11638
  </div>
10441
11639
  `;
10442
- else body = lit.html`
11640
+ else {
11641
+ const runError = this.eventErrorDetails.run;
11642
+ const failedRunEventId = runError ? this.findLatestRunErrorEvent(runError.agentId)?.id : void 0;
11643
+ body = lit.html`
10443
11644
  <div class="relative h-full w-full overflow-y-auto overflow-x-hidden">
10444
11645
  <table class="w-full table-fixed border-collapse text-xs box-border">
10445
11646
  <colgroup>
@@ -10451,10 +11652,10 @@ ${argsString}</pre
10451
11652
  <thead class="sticky top-0 z-10">
10452
11653
  <tr class="bg-white">
10453
11654
  ${[
10454
- "Agent",
10455
- "Time",
10456
- "Event Type"
10457
- ].map((label, col) => lit.html` <th
11655
+ "Agent",
11656
+ "Time",
11657
+ "Event Type"
11658
+ ].map((label, col) => lit.html` <th
10458
11659
  class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900"
10459
11660
  style="position:relative;overflow:hidden;"
10460
11661
  >
@@ -10476,16 +11677,18 @@ ${argsString}</pre
10476
11677
  </thead>
10477
11678
  <tbody>
10478
11679
  ${filteredEvents.map((event, index) => {
10479
- const rowBg = index % 2 === 0 ? "bg-white" : "bg-gray-50/50";
10480
- const badgeClasses = this.getEventBadgeClasses(event.type);
10481
- const extractedEvent = this.extractEventFromPayload(event.payload);
10482
- const inlineEvent = this.stringifyPayload(extractedEvent, false) || "—";
10483
- const prettyEvent = this.stringifyPayload(extractedEvent, true) || inlineEvent;
10484
- const isExpanded = this.expandedRows.has(event.id);
10485
- return lit.html`
11680
+ const isFailedRunEvent = failedRunEventId !== void 0 && event.id === failedRunEventId;
11681
+ const rowBg = isFailedRunEvent ? "bg-rose-50" : index % 2 === 0 ? "bg-white" : "bg-gray-50/50";
11682
+ const badgeClasses = this.getEventBadgeClasses(event.type);
11683
+ const extractedEvent = this.extractEventFromPayload(event.payload);
11684
+ const inlineEvent = this.stringifyPayload(extractedEvent, false) || "—";
11685
+ const prettyEvent = this.stringifyPayload(extractedEvent, true) || inlineEvent;
11686
+ const isExpanded = this.expandedRows.has(event.id);
11687
+ return lit.html`
10486
11688
  <tr
10487
11689
  class="${rowBg} cursor-pointer transition hover:bg-blue-50/50"
10488
11690
  data-inspector-event-id=${event.id}
11691
+ data-cpk-failed-run-event=${isFailedRunEvent ? event.id : void 0}
10489
11692
  @click=${(clickEvent) => this.toggleRowExpansion(event.id, clickEvent)}
10490
11693
  >
10491
11694
  <td
@@ -10518,9 +11721,9 @@ ${prettyEvent}</pre
10518
11721
  <button
10519
11722
  class="absolute right-0 top-0 cursor-pointer rounded px-2 py-1 text-[10px] opacity-0 transition group-hover:opacity-100 ${this.copiedEvents.has(event.id) ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900"}"
10520
11723
  @click=${(e) => {
10521
- e.stopPropagation();
10522
- this.copyToClipboard(prettyEvent, event.id, e);
10523
- }}
11724
+ e.stopPropagation();
11725
+ this.copyToClipboard(prettyEvent, event.id, e);
11726
+ }}
10524
11727
  >
10525
11728
  ${this.copiedEvents.has(event.id) ? lit.html`
10526
11729
  <span>✓ Copied</span>
@@ -10533,11 +11736,12 @@ ${prettyEvent}</pre
10533
11736
  </td>
10534
11737
  </tr>
10535
11738
  `;
10536
- })}
11739
+ })}
10537
11740
  </tbody>
10538
11741
  </table>
10539
11742
  </div>
10540
11743
  `;
11744
+ }
10541
11745
  return lit.html`
10542
11746
  <div
10543
11747
  class="${embedded ? "flex h-[28rem] min-h-[20rem] flex-col" : "flex h-full flex-col"}"
@@ -10602,6 +11806,7 @@ ${prettyEvent}</pre
10602
11806
  }
10603
11807
  renderAgentsView() {
10604
11808
  if (this.selectedContext === "all-agents") return lit.html`
11809
+ ${this.renderEventErrorBanner("tool")}
10605
11810
  <div
10606
11811
  class="flex h-full items-center justify-center px-4 py-8 text-center"
10607
11812
  >
@@ -10625,6 +11830,7 @@ ${prettyEvent}</pre
10625
11830
  const messages = this.getLatestMessagesForAgent(agentId);
10626
11831
  return lit.html`
10627
11832
  <div class="cpk-agent-view flex flex-col gap-4 p-4 overflow-auto">
11833
+ ${this.renderEventErrorBanner("tool")}
10628
11834
  <!-- Agent Overview Card -->
10629
11835
  <div
10630
11836
  class="cpk-agent-overview rounded-lg border border-gray-200 bg-white p-4"
@@ -10757,8 +11963,13 @@ ${prettyEvent}</pre
10757
11963
  const toolCalls = msg.toolCalls ?? [];
10758
11964
  const hasContent = rawContent.trim().length > 0;
10759
11965
  const contentFallback = toolCalls.length > 0 ? "Invoked tool call" : "—";
11966
+ const toolError = this.eventErrorDetails.tool;
11967
+ const isFailedResult = role === "tool" && toolError?.toolCallId !== void 0 && toolError.toolCallId === msg.toolCallId;
10760
11968
  return lit.html`
10761
- <div class="flex items-start">
11969
+ <div
11970
+ class=${isFailedResult ? "flex items-start bg-rose-50" : "flex items-start"}
11971
+ data-cpk-failed-tool-result=${isFailedResult ? msg.toolCallId : lit.nothing}
11972
+ >
10762
11973
  <div class="w-40 shrink-0 px-4 py-2">
10763
11974
  <span
10764
11975
  class="inline-flex rounded px-2 py-0.5 text-[10px] font-medium ${roleColors[role] || roleColors.unknown}"
@@ -10864,6 +12075,81 @@ ${prettyEvent}</pre
10864
12075
  </div>
10865
12076
  `;
10866
12077
  }
12078
+ /**
12079
+ * The Agent view is empty on "All Agents". Pick the agent that just failed
12080
+ * a tool, or the one with the most recent activity.
12081
+ */
12082
+ applyEventErrorLanding(key) {
12083
+ const error = this.eventErrorDetails[key];
12084
+ if (!error) {
12085
+ if (this.selectedMenu === "agents") this.focusAgentForView();
12086
+ return;
12087
+ }
12088
+ if (error.agentId && this.contextOptions.some((option) => option.key === error.agentId)) this.selectedContext = error.agentId;
12089
+ else if (this.selectedMenu === "agents") this.focusAgentForView(error);
12090
+ if (key === "tool" && error.toolCallId) {
12091
+ this.pendingScrollToToolCallId = error.toolCallId;
12092
+ return;
12093
+ }
12094
+ if (key === "run") {
12095
+ this.eventFilterText = "";
12096
+ this.eventTypeFilter = "all";
12097
+ const event = this.findLatestRunErrorEvent(error.agentId);
12098
+ if (!event) return;
12099
+ this.expandedRows.clear();
12100
+ this.expandedRows.add(event.id);
12101
+ this.pendingScrollToEventId = event.id;
12102
+ }
12103
+ }
12104
+ /**
12105
+ * Whether the landing view really carries the item the card points at.
12106
+ * Mirrors the two branches of `applyEventErrorLanding` that can bail out.
12107
+ */
12108
+ hasEventErrorHighlight(key) {
12109
+ const error = this.eventErrorDetails[key];
12110
+ if (!error) return false;
12111
+ if (key === "tool") return error.toolCallId !== void 0;
12112
+ if (key === "run") return this.findLatestRunErrorEvent(error.agentId) !== void 0;
12113
+ return false;
12114
+ }
12115
+ findLatestRunErrorEvent(agentId) {
12116
+ return (agentId && agentId !== "all-agents" ? this.agentEvents.get(agentId) ?? [] : this.flattenedEvents).find((event) => event.type === "RUN_ERROR");
12117
+ }
12118
+ flushErrorLandingScroll() {
12119
+ const eventId = this.pendingScrollToEventId;
12120
+ if (eventId) {
12121
+ this.pendingScrollToEventId = null;
12122
+ Array.from(this.activeRoot.querySelectorAll("[data-inspector-event-id]")).find((candidate) => candidate.dataset.inspectorEventId === eventId)?.scrollIntoView?.({ block: "center" });
12123
+ }
12124
+ const toolCallId = this.pendingScrollToToolCallId;
12125
+ if (toolCallId) {
12126
+ this.pendingScrollToToolCallId = null;
12127
+ const escaped = escapeSelectorValue(toolCallId);
12128
+ (this.activeRoot.querySelector(`[data-cpk-failed-tool-call="${escaped}"]`) ?? this.activeRoot.querySelector(`[data-cpk-failed-tool-result="${escaped}"]`))?.scrollIntoView?.({ block: "center" });
12129
+ }
12130
+ }
12131
+ /**
12132
+ * The Agent view is empty on "All Agents". Pick the agent that just failed
12133
+ * a tool, or the one with the most recent activity.
12134
+ */
12135
+ focusAgentForView(error) {
12136
+ const agentOptions = this.contextOptions.filter((opt) => opt.key !== "all-agents");
12137
+ if (agentOptions.length === 0) return;
12138
+ const errorAgentId = error?.agentId;
12139
+ if (errorAgentId && agentOptions.some((option) => option.key === errorAgentId)) {
12140
+ this.selectedContext = errorAgentId;
12141
+ return;
12142
+ }
12143
+ if (this.selectedContext !== "all-agents") return;
12144
+ const mostRecent = agentOptions.reduce((best, opt) => {
12145
+ const ts = this.getAgentStats(opt.key).lastActivity ?? -1;
12146
+ return best === null || ts > best.ts ? {
12147
+ key: opt.key,
12148
+ ts
12149
+ } : best;
12150
+ }, null);
12151
+ this.selectedContext = mostRecent ? mostRecent.key : agentOptions[0].key;
12152
+ }
10867
12153
  handleMenuSelect(key) {
10868
12154
  if (!this.menuItems.some((item) => item.key === key)) return;
10869
12155
  const previousMenu = this.selectedMenu;
@@ -10872,19 +12158,6 @@ ${prettyEvent}</pre
10872
12158
  this.selectedMenu = key;
10873
12159
  this.settingsOpen = false;
10874
12160
  this.lastSelectedMenuByGroup[require_inspector_nav.getGroupForMenu(key)] = key;
10875
- if (key === "agents" && this.selectedContext === "all-agents") {
10876
- const agentOptions = this.contextOptions.filter((opt) => opt.key !== "all-agents");
10877
- if (agentOptions.length > 0) {
10878
- const mostRecent = agentOptions.reduce((best, opt) => {
10879
- const ts = this.getAgentStats(opt.key).lastActivity ?? -1;
10880
- return best === null || ts > best.ts ? {
10881
- key: opt.key,
10882
- ts
10883
- } : best;
10884
- }, null);
10885
- this.selectedContext = mostRecent ? mostRecent.key : agentOptions[0].key;
10886
- }
10887
- }
10888
12161
  if (previousMenu === "agents" && key !== "agents") {
10889
12162
  if (this.contextOptions.filter((opt) => opt.key !== "all-agents").length > 1) this.selectedContext = "all-agents";
10890
12163
  }
@@ -10898,10 +12171,12 @@ ${prettyEvent}</pre
10898
12171
  if (previousMenu !== "memories" && !this.core?.telemetryDisabled) require_telemetry.trackMemoriesTabClicked(this.getMemoriesTelemetryProps());
10899
12172
  }
10900
12173
  if (key === "home" && previousMenu !== "home") this.homeViewedThisOpen = false;
10901
- if (key === "ag-ui-events" || key === "agents") requestAnimationFrame(() => {
10902
- const scroller = this.activeRoot.querySelector("#cpk-main-scroll");
10903
- if (scroller) scroller.scrollTop = 0;
10904
- });
12174
+ if (key === "ag-ui-events" || key === "agents") {
12175
+ if (!(this.pendingScrollToEventId !== null || this.pendingScrollToToolCallId !== null)) requestAnimationFrame(() => {
12176
+ const scroller = this.activeRoot.querySelector("#cpk-main-scroll");
12177
+ if (scroller) scroller.scrollTop = 0;
12178
+ });
12179
+ }
10905
12180
  this.contextMenuOpen = false;
10906
12181
  this.layoutMenuOpen = false;
10907
12182
  this.persistState();
@@ -11537,50 +12812,443 @@ ${prettyEvent}</pre
11537
12812
  else this.expandedRows.add(eventId);
11538
12813
  this.requestUpdate();
11539
12814
  }
12815
+ /** Whether a given subject currently has something to say. */
12816
+ isSignalArmed(key) {
12817
+ if (isWiringErrorKey(key)) return this.errorSignalArmed[key];
12818
+ if (isEventErrorKey(key)) return this.eventErrorArmed[key];
12819
+ return this.newsSignalArmed;
12820
+ }
12821
+ /**
12822
+ * The signal that owns the single launcher dot, or null when the launcher is
12823
+ * quiet. Precedence rather than replacement: a suppressed signal stays armed
12824
+ * and takes the dot as soon as the higher-priority one clears, and its own
12825
+ * navigation marker is visible the whole time.
12826
+ */
12827
+ getActiveLauncherSignal() {
12828
+ for (const key of LAUNCHER_SIGNAL_PRIORITY_ORDER) if (this.isSignalArmed(key)) return key;
12829
+ return null;
12830
+ }
12831
+ /**
12832
+ * The marker a navigation entry carries, or null for an unmarked entry.
12833
+ *
12834
+ * Markers are independent of the launcher's single dot, so a suppressed
12835
+ * signal is never actually hidden once the panel is open. They also render
12836
+ * on the entry that is *currently selected*: for the news signal that never
12837
+ * mattered, because its marker clears as soon as the view renders, but a
12838
+ * state mirror stays true while it is being read and suppressing it on the
12839
+ * active entry would make it reappear on navigating away.
12840
+ */
12841
+ getNavigationSignalFor(key) {
12842
+ for (const signalKey of LAUNCHER_SIGNAL_PRIORITY_ORDER) {
12843
+ const signal = LAUNCHER_SIGNALS[signalKey];
12844
+ if (signal.markerTarget !== key) continue;
12845
+ if (!this.isSignalArmed(signalKey)) continue;
12846
+ if (signalKey === NEWS_SIGNAL_ID && !this.announcementLoaded) continue;
12847
+ return signal;
12848
+ }
12849
+ return null;
12850
+ }
12851
+ /** Whether a wiring error source is currently red. */
12852
+ hasArmedErrorSignal() {
12853
+ return WIRING_ERROR_KEYS.some((source) => this.errorSignalArmed[source]);
12854
+ }
12855
+ armEventErrorFromCode(code, message, context) {
12856
+ const key = eventErrorKeyForCode(code);
12857
+ if (key === null) return;
12858
+ const agentId = typeof context?.agentId === "string" && context.agentId.length > 0 ? context.agentId : void 0;
12859
+ const toolName = typeof context?.toolName === "string" && context.toolName.length > 0 ? context.toolName : void 0;
12860
+ const toolCallId = typeof context?.toolCallId === "string" && context.toolCallId.length > 0 ? context.toolCallId : void 0;
12861
+ this.armEventError(key, message, {
12862
+ agentId,
12863
+ toolName,
12864
+ toolCallId
12865
+ });
12866
+ }
12867
+ armEventError(key, message, extras = {}) {
12868
+ this.eventErrorDetails[key] = {
12869
+ message,
12870
+ ...extras
12871
+ };
12872
+ const wasArmed = this.eventErrorArmed[key];
12873
+ this.eventErrorArmed[key] = true;
12874
+ if (!wasArmed) this.startSignalPulse(key);
12875
+ if (this.isOpen && !this.settingsOpen && this.selectedMenu === LAUNCHER_SIGNALS[key].landingTarget) this.applyEventErrorLanding(key);
12876
+ this.requestUpdate();
12877
+ }
12878
+ clearEventError(key) {
12879
+ if (!this.eventErrorArmed[key]) return;
12880
+ this.eventErrorArmed[key] = false;
12881
+ this.errorSignalViewedSources.delete(key);
12882
+ this.retireSignal(key);
12883
+ this.requestUpdate();
12884
+ }
12885
+ clearAllEventErrors() {
12886
+ for (const key of EVENT_ERROR_KEYS) {
12887
+ this.eventErrorDetails[key] = null;
12888
+ if (!this.eventErrorArmed[key]) continue;
12889
+ this.eventErrorArmed[key] = false;
12890
+ this.retireSignal(key);
12891
+ }
12892
+ }
12893
+ /**
12894
+ * An event error is unread until its landing view is actually on screen.
12895
+ * Opening the Inspector for a different leaf must not burn it.
12896
+ */
12897
+ maybeCompleteEventErrorView() {
12898
+ if (!this.isOpen || this.settingsOpen) return;
12899
+ for (const key of EVENT_ERROR_KEYS) {
12900
+ if (!this.eventErrorArmed[key]) continue;
12901
+ if (this.selectedMenu !== LAUNCHER_SIGNALS[key].landingTarget) continue;
12902
+ this.clearEventError(key);
12903
+ }
12904
+ }
12905
+ isReducedMotionPreferred() {
12906
+ return typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
12907
+ }
11540
12908
  armNewsSignal(options) {
11541
12909
  const wasArmed = this.newsSignalArmed;
11542
12910
  this.newsSignalArmed = true;
11543
- if (options.pulse && this.isOpen) {
11544
- this.newsSignalPulsePending = true;
11545
- if (!wasArmed) this.requestUpdate();
11546
- } else if (options.pulse) this.startNewsSignalPulse();
12911
+ if (options.pulse) this.startSignalPulse(NEWS_SIGNAL_ID);
11547
12912
  else if (!wasArmed) this.requestUpdate();
11548
12913
  }
11549
12914
  clearNewsSignal() {
11550
12915
  if (!this.newsSignalArmed) return;
11551
12916
  this.newsSignalArmed = false;
11552
- this.newsSignalPulsePending = false;
11553
12917
  if (this.announcementTimestamp) require_persistence.saveAnnouncementReadTimestamp(this.announcementTimestamp);
11554
- if (this.newsSignalPulsing) this.stopNewsSignalPulse();
12918
+ this.retireSignal(NEWS_SIGNAL_ID);
11555
12919
  this.requestUpdate();
11556
12920
  }
11557
- startNewsSignalPulse() {
11558
- this.stopNewsSignalPulse();
11559
- if (typeof document !== "undefined" && document.visibilityState !== "visible") {
11560
- this.newsSignalPulsePending = true;
12921
+ /**
12922
+ * Requests one beat for a signal, running it now or deferring it.
12923
+ *
12924
+ * There is a single pending slot and four reasons a beat cannot land. All
12925
+ * four are the same situation — "cannot land now, run later" — and treating
12926
+ * them alike is the point: three separate behaviours for one situation would
12927
+ * not survive a third signal.
12928
+ *
12929
+ * Reason 3 is not cosmetic. Starting a beat while one runs does not restart
12930
+ * the animation, because the attribute it binds to does not change value and
12931
+ * the pseudo-element selectors match on attribute *presence*; the running
12932
+ * beat would merely change colour mid-flight. A failure that arms during an
12933
+ * announcement beat must wait for that beat to end, or it would run only as
12934
+ * its final fraction.
12935
+ *
12936
+ * A failure's beat is followed by a pill, and the whole 3.4-second gesture
12937
+ * holds this one slot for its full duration. That is not a second scheduling
12938
+ * concept: reason 3 already says "another beat is running", and a gesture is
12939
+ * simply a longer beat.
12940
+ */
12941
+ startSignalPulse(key) {
12942
+ if (this.isOpen || typeof document !== "undefined" && document.visibilityState !== "visible" || this.gestureSlotSignal !== null && this.gestureSlotSignal !== key || this.getActiveLauncherSignal() !== key) {
12943
+ const pending = this.pendingPulseSignal;
12944
+ if (pending === null || LAUNCHER_SIGNALS[key].priority >= LAUNCHER_SIGNALS[pending].priority) this.pendingPulseSignal = key;
11561
12945
  this.requestUpdate();
11562
12946
  return;
11563
12947
  }
11564
- this.newsSignalPulsePending = false;
11565
- this.newsSignalPulsing = true;
11566
- if (this.announcementTimestamp) require_persistence.saveAnnouncementPulsedTimestamp(this.announcementTimestamp);
12948
+ this.stopSignalPulse();
12949
+ this.pendingPulseSignal = null;
12950
+ this.pulsingSignal = key;
12951
+ if (isWiringErrorKey(key)) this.errorBeatSpent = true;
12952
+ else if (this.announcementTimestamp && key === NEWS_SIGNAL_ID) require_persistence.saveAnnouncementPulsedTimestamp(this.announcementTimestamp);
12953
+ this.beginGestureTail(key);
11567
12954
  this.requestUpdate();
11568
12955
  if (typeof window === "undefined") return;
11569
12956
  this.pulseTimeoutId = setTimeout(() => {
11570
12957
  this.pulseTimeoutId = null;
11571
- this.newsSignalPulsing = false;
12958
+ this.pulsingSignal = null;
11572
12959
  this.requestUpdate();
11573
- }, NEWS_SIGNAL_CADENCE_MS);
12960
+ if (this.gestureSignal === key && this.pillPhase === "closed") {
12961
+ this.openPill();
12962
+ return;
12963
+ }
12964
+ if (this.gestureSignal === key) {
12965
+ this.endGesture();
12966
+ return;
12967
+ }
12968
+ this.flushPendingSignalPulse();
12969
+ }, LAUNCHER_SIGNALS[key].cadence);
11574
12970
  }
11575
- stopNewsSignalPulse() {
12971
+ stopSignalPulse() {
11576
12972
  if (this.pulseTimeoutId !== null) {
11577
12973
  clearTimeout(this.pulseTimeoutId);
11578
12974
  this.pulseTimeoutId = null;
11579
12975
  }
11580
- this.newsSignalPulsing = false;
12976
+ this.pulsingSignal = null;
12977
+ }
12978
+ /**
12979
+ * Runs a deferred beat if it can land now, and drops it if its reason to
12980
+ * exist has gone: a nudge about a problem that no longer exists is worse
12981
+ * than no nudge at all.
12982
+ */
12983
+ flushPendingSignalPulse() {
12984
+ const key = this.pendingPulseSignal;
12985
+ if (key === null) return;
12986
+ if (!this.isSignalArmed(key)) {
12987
+ this.pendingPulseSignal = null;
12988
+ this.requestUpdate();
12989
+ return;
12990
+ }
12991
+ this.startSignalPulse(key);
12992
+ }
12993
+ /**
12994
+ * Drops a signal's gesture, pending or running, when the signal goes quiet.
12995
+ *
12996
+ * The pill closes early: it states a condition, and the condition has
12997
+ * stopped being true. The beat is left to finish, because a beat asserts
12998
+ * nothing — it says *here*, and "here" is still true.
12999
+ */
13000
+ retireSignal(key) {
13001
+ if (this.pendingPulseSignal === key) this.pendingPulseSignal = null;
13002
+ if (this.gestureSignal === key) this.closePillEarly();
13003
+ this.flushPendingSignalPulse();
13004
+ }
13005
+ /**
13006
+ * The signal holding the single gesture slot: a beat in flight, or the pill
13007
+ * and spoken sentence that follow it. One slot, not two — see reason 3 in
13008
+ * `startSignalPulse`.
13009
+ */
13010
+ get gestureSlotSignal() {
13011
+ return this.pulsingSignal ?? this.gestureSignal;
13012
+ }
13013
+ /**
13014
+ * Opens the gesture's tail alongside the beat, for a signal that carries a
13015
+ * pill. The pill is rendered immediately — clipped to nothing, so it shows
13016
+ * nothing — because its full width has to be on the page before the room
13017
+ * either side of the launcher can be measured.
13018
+ *
13019
+ * A signal with no pill label gets no tail at all, so the announcement's
13020
+ * gesture is exactly the beat it has always been.
13021
+ */
13022
+ beginGestureTail(key) {
13023
+ this.cancelPillTimeout();
13024
+ if (LAUNCHER_SIGNALS[key].pillLabel === void 0) {
13025
+ this.gestureSignal = null;
13026
+ this.pillPhase = null;
13027
+ this.pillDirection = null;
13028
+ return;
13029
+ }
13030
+ this.gestureSignal = key;
13031
+ this.pillPhase = "closed";
13032
+ this.pillDirection = null;
13033
+ this.closeLauncherHud();
13034
+ }
13035
+ /**
13036
+ * Chooses the side, or suppresses the pill, from the room actually available
13037
+ * at gesture start. Measured once, from the DOM, because the launcher is
13038
+ * draggable and its position persists: a reader who parked it near the left
13039
+ * edge would otherwise get a permanently truncated pill.
13040
+ *
13041
+ * Where neither side has room there is no pill at all rather than a cut-off
13042
+ * one — the dot and the beat still fire, so the signal is intact and only
13043
+ * the label is lost. Constraining where the reader may drag the control to
13044
+ * protect this animation was considered and rejected: the page is theirs.
13045
+ */
13046
+ resolvePillDirection() {
13047
+ if (this.pillDirection !== null || this.pillPhase === null) return;
13048
+ const wrapper = this.activeRoot.querySelector(".console-button-wrapper");
13049
+ const button = wrapper?.querySelector(".console-button");
13050
+ const pill = wrapper?.querySelector(".cpk-launcher-pill");
13051
+ if (!button || !pill || typeof window === "undefined") return;
13052
+ const mark = button.getBoundingClientRect();
13053
+ const overhang = Math.max(0, pill.getBoundingClientRect().width - mark.width);
13054
+ const viewportWidth = window.innerWidth;
13055
+ if (overhang === 0) {
13056
+ this.setPillOutcome("left");
13057
+ return;
13058
+ }
13059
+ if (mark.left - overhang >= EDGE_MARGIN) {
13060
+ this.setPillOutcome("left");
13061
+ return;
13062
+ }
13063
+ if (mark.right + overhang <= viewportWidth - EDGE_MARGIN) {
13064
+ this.setPillOutcome("right");
13065
+ return;
13066
+ }
13067
+ this.setPillOutcome(null);
13068
+ }
13069
+ /** Records the measurement's verdict, and drops the pill when it is null. */
13070
+ setPillOutcome(direction) {
13071
+ if (direction === null) {
13072
+ this.pillPhase = null;
13073
+ this.pillDirection = null;
13074
+ this.pillOutcome = "suppressed";
13075
+ this.requestUpdate();
13076
+ return;
13077
+ }
13078
+ this.pillDirection = direction;
13079
+ this.pillOutcome = "shown";
13080
+ this.requestUpdate();
13081
+ }
13082
+ /** Runs the pill's three phases in series once the beat has finished. */
13083
+ openPill() {
13084
+ this.resolvePillDirection();
13085
+ if (this.pillPhase === null) {
13086
+ this.endGesture();
13087
+ return;
13088
+ }
13089
+ this.advancePill("opening", ERROR_GESTURE_MS.open, () => {
13090
+ this.advancePill("holding", ERROR_GESTURE_MS.hold, () => {
13091
+ this.advancePill("closing", ERROR_GESTURE_MS.close, () => {
13092
+ this.endGesture();
13093
+ });
13094
+ });
13095
+ });
13096
+ }
13097
+ advancePill(phase, duration, next) {
13098
+ this.cancelPillTimeout();
13099
+ this.pillPhase = phase;
13100
+ this.requestUpdate();
13101
+ if (typeof window === "undefined") return;
13102
+ this.pillTimeoutId = setTimeout(() => {
13103
+ this.pillTimeoutId = null;
13104
+ next();
13105
+ }, duration);
13106
+ }
13107
+ /**
13108
+ * Closes the pill before its hold is out, because the failure it names has
13109
+ * been fixed. A pill that has not opened yet is simply dropped; the beat is
13110
+ * never cut short.
13111
+ */
13112
+ closePillEarly() {
13113
+ if (this.pillPhase === null) {
13114
+ this.endGesture();
13115
+ return;
13116
+ }
13117
+ if (this.pillPhase === "closing") return;
13118
+ if (this.pillPhase === "closed") {
13119
+ this.pillPhase = null;
13120
+ this.requestUpdate();
13121
+ return;
13122
+ }
13123
+ this.advancePill("closing", ERROR_GESTURE_MS.close, () => {
13124
+ this.endGesture();
13125
+ });
13126
+ }
13127
+ /** Releases the slot and leaves the plain mark with its dot behind. */
13128
+ endGesture() {
13129
+ this.cancelGestureTail();
13130
+ this.requestUpdate();
13131
+ this.flushPendingSignalPulse();
13132
+ }
13133
+ /**
13134
+ * Drops the gesture's tail outright, with no closing animation, for the
13135
+ * cases where the launcher itself has gone: the panel opened over it, or the
13136
+ * element was removed from the page.
13137
+ */
13138
+ cancelGestureTail() {
13139
+ this.cancelPillTimeout();
13140
+ this.gestureSignal = null;
13141
+ this.pillPhase = null;
13142
+ this.pillDirection = null;
13143
+ }
13144
+ cancelPillTimeout() {
13145
+ if (this.pillTimeoutId !== null) {
13146
+ clearTimeout(this.pillTimeoutId);
13147
+ this.pillTimeoutId = null;
13148
+ }
13149
+ }
13150
+ /**
13151
+ * Whether each error source is currently broken.
13152
+ *
13153
+ * Only two *wiring* conditions qualify. App errors (runs, tools, memory)
13154
+ * are unread events on a different latch — they name themselves on the pill
13155
+ * and clear when their landing view is read. Notably absent from *this*
13156
+ * latch:
13157
+ *
13158
+ * - **A failed agent run.** A run is an event. It arms `run`, not this
13159
+ * state. The resting wiring dot must not stay red for the rest of a debug
13160
+ * hour.
13161
+ * - **The core error channel as a wiring source.** Handshake failure already
13162
+ * sets the connection state. Other codes arm `run` or `tool`.
13163
+ * - **Memory failures before Learning is live.** The memory store is lazy
13164
+ * because creating it opens a realtime connection. Once it exists, a load
13165
+ * failure arms `memory`.
13166
+ * - **Product states.** An unconfigured Intelligence and an unentitled
13167
+ * Memory plan are not defects. The signal fires only where wiring is
13168
+ * present and the call still fails — which for threads is guaranteed by
13169
+ * `_threadsErrorByAgent` only ever being written while the thread
13170
+ * endpoints are available.
13171
+ *
13172
+ * Known limitation: the runtime handshake runs once, on connect. A server
13173
+ * that dies *after* the page loaded leaves the connection state at connected
13174
+ * and raises nothing; the next page load re-runs the handshake and the
13175
+ * signal appears then. The signal reports the wiring state as last
13176
+ * established. Closing that gap means a re-probe in the core, which is a
13177
+ * runtime concern.
13178
+ */
13179
+ isErrorSourceBroken(source) {
13180
+ if (source === "connection") {
13181
+ const state = this.getCoreStatusSummary().state;
13182
+ if (require_home_briefing.runtimeConnectionNeedsAttention(state)) return true;
13183
+ return this.errorSignalArmed.connection && state === "connecting";
13184
+ }
13185
+ return this._threadsErrorByAgent.size > 0;
13186
+ }
13187
+ /**
13188
+ * Mirrors both error latches onto the live state. Called from `willUpdate`,
13189
+ * because every mutation of the underlying state already requests an update,
13190
+ * so the resting dot follows the state within the same render.
13191
+ */
13192
+ evaluateErrorSignals() {
13193
+ const wasArmed = this.hasArmedErrorSignal();
13194
+ for (const source of WIRING_ERROR_KEYS) {
13195
+ if (this.isErrorSourceBroken(source)) {
13196
+ if (this.errorSignalArmed[source]) continue;
13197
+ this.errorSignalArmed[source] = true;
13198
+ continue;
13199
+ }
13200
+ if (!this.errorSignalArmed[source]) continue;
13201
+ this.errorSignalArmed[source] = false;
13202
+ this.errorSignalViewedSources.delete(source);
13203
+ this.retireSignal(source);
13204
+ }
13205
+ this.onErrorSignalsChanged(wasArmed);
13206
+ }
13207
+ /**
13208
+ * Applies the rising-edge rule after a latch changed.
13209
+ *
13210
+ * The beat fires on the transition from "no failure" to "at least one
13211
+ * failure", evaluated globally across the sources and never again while
13212
+ * anything is red — one root cause, one nudge.
13213
+ */
13214
+ onErrorSignalsChanged(wasArmed) {
13215
+ if (!this.hasArmedErrorSignal()) {
13216
+ this.errorBeatSpent = false;
13217
+ this.pillOutcome = null;
13218
+ return;
13219
+ }
13220
+ if (wasArmed || this.errorBeatSpent) return;
13221
+ const active = this.getActiveLauncherSignal();
13222
+ if (active !== null && isErrorSignalKey(active)) this.startSignalPulse(active);
13223
+ }
13224
+ /**
13225
+ * Records `oss.inspector.error_signal_viewed` once per source per outage,
13226
+ * when the dot is actually on screen.
13227
+ *
13228
+ * Unlike the announcement's launcher event this fires immediately rather
13229
+ * than waiting for the runtime handshake to report `telemetryDisabled`.
13230
+ * The held-queue would never drain for the connection source — a connection
13231
+ * failure means the handshake did not complete — so queuing would guarantee
13232
+ * zero data for the case this event exists to measure. `trackOpened` already
13233
+ * sends on the same terms. Both opt-out layers still gate it: the local
13234
+ * opt-out inside `track`, and the runtime's flag once it is known.
13235
+ */
13236
+ maybeTrackErrorSignalViewed() {
13237
+ if (this.isOpen || typeof document === "undefined" || document.visibilityState !== "visible") return;
13238
+ const active = this.getActiveLauncherSignal();
13239
+ if (active === null || !isErrorSignalKey(active)) return;
13240
+ if (this.errorSignalViewedSources.has(active)) return;
13241
+ if (this.pillOutcome === null && LAUNCHER_SIGNALS[active].pillLabel !== void 0) return;
13242
+ if (this.core?.telemetryDisabled) return;
13243
+ this.errorSignalViewedSources.add(active);
13244
+ require_telemetry.trackErrorSignalViewed({
13245
+ source: active,
13246
+ presentation: this.isReducedMotionPreferred() ? "reduced_motion" : "animated",
13247
+ label: this.pillOutcome ?? "suppressed"
13248
+ });
11581
13249
  }
11582
13250
  maybeTrackNewsSignalViewed() {
11583
- if (!this.newsSignalArmed || !this.newsSignalPulsing || this.isOpen || typeof document === "undefined" || document.visibilityState !== "visible") return;
13251
+ if (!this.newsSignalArmed || this.pulsingSignal !== NEWS_SIGNAL_ID || this.isOpen || typeof document === "undefined" || document.visibilityState !== "visible") return;
11584
13252
  const id = this.announcementTimestamp;
11585
13253
  if (!id || this.viewedNewsSignalIds.has(id)) return;
11586
13254
  this.viewedNewsSignalIds.add(id);