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