scream-code 0.15.8 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,8 @@ import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
6
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
7
- import { $t as argsRecord, A as renderDiffLinesClustered, An as detectShellEnvironment, At as DISABLE_TERMINAL_THEME_REPORTING, B as BackgroundAgentStatusComponent, Bn as fetchCatalog, Bt as TERMINAL_THEME_DARK, Cn as startManualEmbeddingDownload, Ct as createMarkdownTheme, D as toggleEmptySessionHint, Dn as TuiLikePreferencesSchema, Dt as detectTerminalTheme, E as isTurnElapsedEnabled, En as TuiConfigParseError, Et as getColorPalette, F as estimateTokens, Fn as CLI_COMMAND_NAME, Ft as OSC11_RESPONSE_PREFIX, G as getBreathingFrame, Gn as MemoryMemoStore, Gt as lerpGradient, H as AgentGroupComponent, Hn as ScreamHarness, Ht as isBusy, I as getSharedSpeedTracker, In as CLI_UI_MODE, It as OSC11_RESPONSE_PREFIX_NO_ESC, J as refineGoal, Jn as resolveGlobalLogPath, K as resetBreathingClock, Kn as flushDiagnosticLogs, L as SkillActivationComponent, Ln as CLI_USER_AGENT_PRODUCT, Lt as QUERY_TERMINAL_THEME, M as langFromPath, Mn as getInputHistoryFile, Mt as ENABLE_TERMINAL_THEME_REPORTING, N as CachedContainer, Nn as getLogDir, Nt as OSC11_QUERY, O as ToolCallComponent, On as loadTuiConfig, Ot as parseOsc11BackgroundTheme, P as ThinkingComponent, Pn as detectInstallSource, Pt as OSC11_RESPONSE, Qn as SCREAM_ERROR_INFO, Qt as appendStreamingArgsPreview, R as ReadGroupComponent, Rn as PRODUCT_NAME, Rt as TERMINAL_FOCUS_IN, S as getDaemonInstructions, Sn as isEmbeddingModelCached, St as createEditorTheme, T as isEmptySessionHintDismissed, Tn as PULSE_WAVE_FRAMES, Tt as contrastTextHex, U as WelcomeComponent, Ut as isStreaming, V as AssistantMessageComponent, Vn as saveCatalogCache, Vt as TERMINAL_THEME_LIGHT, W as BREATHE_CYCLE_MS, Wn as resolveScreamHome, Wt as FooterComponent, Xn as isOrphanedToolCallError, Xt as printableChar, Yn as isScreamError, Zn as ErrorCodes, Zt as STATUS_BULLET, _ as refreshUpdateCache, _n as BUILTIN_SLASH_COMMANDS, _t as supportsBalance, an as truncateErrorMessage, b as readJsonlFile, bn as setExperimentalFlags, c as hasDispose, cn as EXIT_CONFIRM_WINDOW_MS, d as formatMemoryMemoForInjection, dn as TIP_ROTATION_INTERVAL_MS, en as formatErrorMessage, et as toTerminalHyperlink, fn as getCtrlCHint, g as selectUpdateTarget, gn as buildSkillSlashCommands, gt as refreshProviderBalance, hn as getNoActiveSessionMessage, i as buildRoleAdditionalText, in as stringValue, j as highlightLines, jn as getDataDir, jt as ENABLE_TERMINAL_FOCUS_REPORTING, k as renderDiffLines, kn as saveTuiConfig, kt as DISABLE_TERMINAL_FOCUS_REPORTING, l as isPlanExpandable, ln as MAIN_AGENT_ID, lt as handlePlanCommand, mn as getLlmNotSetMessage, n as clearEvalPanelState, nn as parseStreamingArgs, nt as getModelCycleLevel, on as CHARS_PER_TOKEN, pn as getCtrlDHint, q as clearGoalState, qn as log, r as openUrl, rn as serializeToolResultOutput, s as disposeChildren, sn as EMPTY_SESSION_HINT_URL, st as handleFusionPlanCommand, t as dispatchInput, tn as isTodoItemShape, tt as changeThinkingLevel, u as MoonLoader, un as SESSION_TIPS, v as readUpdateCache, vn as sortSlashCommands, vt as clearInfoPanelState, w as UserMessageComponent, wn as PIXEL_PULSE_FRAMES, wt as createThemeStyles, xn as getKnowledgeStore, xt as resolveThemeSync, y as appendJsonlLine, yn as isExperimentalFlagEnabled, z as parseReadGroupOutput, zn as DEFAULT_CATALOG_URL, zt as TERMINAL_FOCUS_OUT } from "./dispatch-BaAsj0v5.mjs";
8
- import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-C5pDKZ-c.mjs";
7
+ import { $n as isOrphanedToolCallError, $t as appendStreamingArgsPreview, A as renderDiffLinesClustered, An as saveTuiConfig, At as DISABLE_TERMINAL_THEME_REPORTING, B as BackgroundAgentStatusComponent, Bn as DEFAULT_CATALOG_URL, Bt as TERMINAL_THEME_DARK, Cn as isEmbeddingModelCached, Ct as createMarkdownTheme, D as toggleEmptySessionHint, Dn as TuiConfigParseError, Dt as detectTerminalTheme, E as isTurnElapsedEnabled, En as PULSE_WAVE_FRAMES, Et as getColorPalette, F as estimateTokens, Fn as detectInstallSource, Ft as OSC11_RESPONSE_PREFIX, G as getBreathingFrame, Gn as appendSessionIndexEntry, Gt as lerpGradient, H as AgentGroupComponent, Hn as saveCatalogCache, Ht as isBusy, I as getSharedSpeedTracker, In as CLI_COMMAND_NAME, It as OSC11_RESPONSE_PREFIX_NO_ESC, J as refineGoal, Jn as MemoryMemoStore, K as resetBreathingClock, L as SkillActivationComponent, Ln as CLI_UI_MODE, Lt as QUERY_TERMINAL_THEME, M as langFromPath, Mn as getDataDir, Mt as ENABLE_TERMINAL_THEME_REPORTING, N as CachedContainer, Nn as getInputHistoryFile, Nt as OSC11_QUERY, O as ToolCallComponent, On as TuiLikePreferencesSchema, Ot as parseOsc11BackgroundTheme, P as ThinkingComponent, Pn as getLogDir, Pt as OSC11_RESPONSE, Qn as isScreamError, Qt as STATUS_BULLET, R as ReadGroupComponent, Rn as CLI_USER_AGENT_PRODUCT, Rt as TERMINAL_FOCUS_IN, S as getDaemonInstructions, Sn as getKnowledgeStore, St as createEditorTheme, T as isEmptySessionHintDismissed, Tn as PIXEL_PULSE_FRAMES, Tt as contrastTextHex, U as WelcomeComponent, Un as ScreamHarness, Ut as isStreaming, V as AssistantMessageComponent, Vn as fetchCatalog, Vt as TERMINAL_THEME_LIGHT, W as BREATHE_CYCLE_MS, Wn as encodeWorkDirKey, Wt as FooterComponent, Xn as log, Xt as printableChar, Yn as flushDiagnosticLogs, Zn as resolveGlobalLogPath, Zt as INTERJECTION_BULLET, _ as refreshUpdateCache, _n as buildSkillSlashCommands, _t as supportsBalance, an as stringValue, b as readJsonlFile, bn as isExperimentalFlagEnabled, c as hasDispose, cn as EMPTY_SESSION_HINT_URL, d as formatMemoryMemoForInjection, dn as SESSION_TIPS, en as argsRecord, er as ErrorCodes, et as toTerminalHyperlink, fn as TIP_ROTATION_INTERVAL_MS, g as selectUpdateTarget, gn as getNoActiveSessionMessage, gt as refreshProviderBalance, hn as getLlmNotSetMessage, i as buildRoleAdditionalText, in as serializeToolResultOutput, j as highlightLines, jn as detectShellEnvironment, jt as ENABLE_TERMINAL_FOCUS_REPORTING, k as renderDiffLines, kn as loadTuiConfig, kt as DISABLE_TERMINAL_FOCUS_REPORTING, l as isPlanExpandable, ln as EXIT_CONFIRM_WINDOW_MS, lt as handlePlanCommand, mn as getCtrlDHint, n as clearEvalPanelState, nn as isTodoItemShape, nt as getModelCycleLevel, on as truncateErrorMessage, pn as getCtrlCHint, q as clearGoalState, qn as resolveScreamHome, r as openUrl, rn as parseStreamingArgs, s as disposeChildren, sn as CHARS_PER_TOKEN, st as handleFusionPlanCommand, t as dispatchInput, tn as formatErrorMessage, tr as SCREAM_ERROR_INFO, tt as changeThinkingLevel, u as MoonLoader, un as MAIN_AGENT_ID, v as readUpdateCache, vn as BUILTIN_SLASH_COMMANDS, vt as clearInfoPanelState, w as UserMessageComponent, wn as startManualEmbeddingDownload, wt as createThemeStyles, xn as setExperimentalFlags, xt as resolveThemeSync, y as appendJsonlLine, yn as sortSlashCommands, z as parseReadGroupOutput, zn as PRODUCT_NAME, zt as TERMINAL_FOCUS_OUT } from "./dispatch-CsrteWRw.mjs";
8
+ import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-BVQaKy1Z.mjs";
9
9
  import { createRequire } from "node:module";
10
10
  import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
11
11
  import { access, chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
@@ -32,7 +32,7 @@ function optionalBuildString(value) {
32
32
  return typeof value === "string" && value.length > 0 ? value : void 0;
33
33
  }
34
34
  const SCREAM_BUILD_INFO = {
35
- version: optionalBuildString("0.15.8"),
35
+ version: optionalBuildString("0.16.0"),
36
36
  channel: optionalBuildString(""),
37
37
  commit: optionalBuildString(""),
38
38
  buildTarget: optionalBuildString("darwin-arm64")
@@ -1995,10 +1995,11 @@ var StatusMessageComponent = class extends Container {
1995
1995
  }
1996
1996
  };
1997
1997
  var NoticeMessageComponent = class extends Container {
1998
- constructor(title, detail, colors) {
1998
+ constructor(title, detail, colors, markerColor) {
1999
1999
  super();
2000
+ const marker = markerColor === void 0 ? " " : chalk.hex(markerColor)(INTERJECTION_BULLET);
2000
2001
  this.addChild(new Spacer(1));
2001
- this.addChild(new Text(` ${chalk.hex(colors.textStrong)(replaceTabs(title))}`, 0, 0));
2002
+ this.addChild(new Text(`${marker}${chalk.hex(colors.textStrong)(replaceTabs(title))}`, 0, 0));
2002
2003
  if (detail !== void 0 && detail.length > 0) this.addChild(new Text(` ${chalk.hex(colors.textDim)(replaceTabs(detail))}`, 0, 0));
2003
2004
  }
2004
2005
  };
@@ -2025,6 +2026,158 @@ function formatBackgroundAgentTranscript(phase, meta, extras = void 0) {
2025
2026
  };
2026
2027
  }
2027
2028
  //#endregion
2029
+ //#region src/tui/utils/child-request-notice.ts
2030
+ /**
2031
+ * Child→parent collaboration requests (`ContactParent`) shown in the main
2032
+ * transcript as a two-line notice: a bold title naming the requester, then a
2033
+ * dim detail line carrying the request itself.
2034
+ *
2035
+ * Two producers share this formatter so live and replay never disagree:
2036
+ * - the live path reads the subagent's `tool.call.started` args;
2037
+ * - session replay recovers the same fields from the `<notification>` XML the
2038
+ * kernel injects into the parent's context.
2039
+ *
2040
+ * Replay cannot recover the subagent's display name (spawn events are not part
2041
+ * of the persisted context), so it renders the name-less title variant.
2042
+ */
2043
+ const CHILD_REQUEST_KINDS = [
2044
+ "info",
2045
+ "handoff",
2046
+ "escalate"
2047
+ ];
2048
+ /** Cap each payload list so a 20-entry artifact array cannot explode the transcript. */
2049
+ const LIST_PREVIEW = 3;
2050
+ /**
2051
+ * Longest previewed value kept on a transcript row — request text, `needs`, or
2052
+ * a payload list. The kernel allows 8192 chars (`contact-parent.ts`), and
2053
+ * transcript folding counts components rather than lines, so an uncapped row
2054
+ * can never collapse. The full text stays available: it is in the parent's
2055
+ * context and in the agent card's sub-tool-call args.
2056
+ */
2057
+ const TEXT_PREVIEW = 200;
2058
+ /** Collapse the request onto previewable single-paragraph form. */
2059
+ function previewText(value) {
2060
+ const flattened = value.replaceAll(/\s*\n\s*/g, " ").trim();
2061
+ return flattened.length > TEXT_PREVIEW ? `${flattened.slice(0, TEXT_PREVIEW)}…` : flattened;
2062
+ }
2063
+ const TYPE_KEY = {
2064
+ info: "transcript.child_request_type_info",
2065
+ handoff: "transcript.child_request_type_handoff",
2066
+ escalate: "transcript.child_request_type_escalate"
2067
+ };
2068
+ const LIST_KEY = {
2069
+ artifacts: "transcript.child_request_artifacts",
2070
+ evidence: "transcript.child_request_evidence",
2071
+ missing: "transcript.child_request_missing"
2072
+ };
2073
+ function kindOf(raw) {
2074
+ return typeof raw === "string" && CHILD_REQUEST_KINDS.includes(raw) ? raw : "info";
2075
+ }
2076
+ function textOf(raw) {
2077
+ if (typeof raw !== "string") return void 0;
2078
+ const trimmed = raw.trim();
2079
+ return trimmed.length > 0 ? trimmed : void 0;
2080
+ }
2081
+ function stringifyList(raw) {
2082
+ const shown = previewText(raw.slice(0, LIST_PREVIEW).join(", "));
2083
+ const hidden = raw.length - Math.min(raw.length, LIST_PREVIEW);
2084
+ return hidden > 0 ? `${shown} +${hidden}` : shown;
2085
+ }
2086
+ function previewedList(field, items) {
2087
+ if (items === void 0 || items.length === 0) return void 0;
2088
+ return t(LIST_KEY[field], { value: stringifyList(items) });
2089
+ }
2090
+ /** Live producer: the args the model handed to `ContactParent`. */
2091
+ function childRequestFieldsFromArgs(args) {
2092
+ const message = textOf(args["message"]);
2093
+ if (message === void 0) return null;
2094
+ const payload = typeof args["payload"] === "object" && args["payload"] !== null && !Array.isArray(args["payload"]) ? args["payload"] : {};
2095
+ const listOf = (key) => {
2096
+ const raw = payload[key];
2097
+ if (!Array.isArray(raw)) return void 0;
2098
+ const items = raw.map((item) => textOf(item)).filter((item) => item !== void 0);
2099
+ return items.length > 0 ? items : void 0;
2100
+ };
2101
+ return {
2102
+ requestType: kindOf(args["request_type"]),
2103
+ message,
2104
+ needs: textOf(args["needs"]),
2105
+ artifacts: listOf("artifacts"),
2106
+ evidence: listOf("evidence"),
2107
+ missing: listOf("missing")
2108
+ };
2109
+ }
2110
+ /** Parse `artifacts: [a, b]` back into its items. */
2111
+ function bracketedListOf(value) {
2112
+ return (value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value).split(",").map((item) => item.trim()).filter((item) => item.length > 0);
2113
+ }
2114
+ /**
2115
+ * Replay producer: recover the fields from the injected notification. The body
2116
+ * shape is owned by `submitChildRequest` in agent-core:
2117
+ * `<type>: <message>` then optional `needs:` / `artifacts:` / `evidence:` /
2118
+ * `missing:` lines. A multi-line message keeps its extra lines attached.
2119
+ */
2120
+ function childRequestFieldsFromNotification(text) {
2121
+ const lines = text.split("\n");
2122
+ const open = lines.findIndex((line) => line.startsWith("<notification ") && line.includes("type=\"child_request\""));
2123
+ if (open < 0) return null;
2124
+ const body = [];
2125
+ for (let index = open + 1; index < lines.length; index += 1) {
2126
+ const line = lines[index] ?? "";
2127
+ if (line === "</notification>") break;
2128
+ if (line.startsWith("Title: ") || line.startsWith("Severity: ")) continue;
2129
+ body.push(line);
2130
+ }
2131
+ const first = body[0];
2132
+ if (first === void 0) return null;
2133
+ const head = /^(info|handoff|escalate):\s([\s\S]*)$/.exec(first);
2134
+ const requestType = head !== null ? kindOf(head[1]) : "info";
2135
+ const messageParts = [head !== null && head[2] !== void 0 ? head[2] : first];
2136
+ let needs;
2137
+ const lists = {};
2138
+ for (const line of body.slice(1)) {
2139
+ const separator = line.indexOf(": ");
2140
+ const key = separator < 0 ? "" : line.slice(0, separator);
2141
+ const value = separator < 0 ? "" : line.slice(separator + 2).trim();
2142
+ const listKey = (key === "artifacts" || key === "evidence" || key === "missing") && value.startsWith("[") && value.endsWith("]") ? key : void 0;
2143
+ if (key === "needs" && value.length > 0) needs = value;
2144
+ else if (listKey !== void 0) lists[listKey] = bracketedListOf(value);
2145
+ else messageParts.push(line);
2146
+ }
2147
+ const message = messageParts.join("\n").trim();
2148
+ if (message.length === 0) return null;
2149
+ return {
2150
+ requestType,
2151
+ message,
2152
+ needs,
2153
+ artifacts: lists["artifacts"],
2154
+ evidence: lists["evidence"],
2155
+ missing: lists["missing"]
2156
+ };
2157
+ }
2158
+ /** Build the transcript notice. `name` is absent on replay. */
2159
+ function renderChildRequestNotice(fields, name) {
2160
+ const type = t(TYPE_KEY[fields.requestType]);
2161
+ const title = name === void 0 ? t("transcript.child_request_anon", { type }) : t("transcript.child_request", {
2162
+ name,
2163
+ type
2164
+ });
2165
+ const parts = [previewText(fields.message)];
2166
+ if (fields.needs !== void 0) parts.push(t("transcript.child_request_needs", { value: previewText(fields.needs) }));
2167
+ for (const entry of [
2168
+ ["artifacts", fields.artifacts],
2169
+ ["evidence", fields.evidence],
2170
+ ["missing", fields.missing]
2171
+ ]) {
2172
+ const preview = previewedList(entry[0], entry[1]);
2173
+ if (preview !== void 0) parts.push(preview);
2174
+ }
2175
+ return {
2176
+ title,
2177
+ detail: parts.join(" · ")
2178
+ };
2179
+ }
2180
+ //#endregion
2028
2181
  //#region src/tui/utils/subagent-slots.ts
2029
2182
  /** Default subagent types, in the fixed display order of the slots. */
2030
2183
  const DEFAULT_SUBAGENT_TYPES = [
@@ -2037,6 +2190,10 @@ const DEFAULT_SUBAGENT_TYPES = [
2037
2190
  "worker",
2038
2191
  "writer"
2039
2192
  ];
2193
+ /** How long a `requesting` overlay stays visible after a subagent asks the
2194
+ * parent for help. The request itself returns at once and the agent keeps
2195
+ * working, so this is a display window, not a blocked state. */
2196
+ const REQUESTING_WINDOW_MS = 5e3;
2040
2197
  var SubagentSlots = class {
2041
2198
  slots = /* @__PURE__ */ new Map();
2042
2199
  instances = /* @__PURE__ */ new Map();
@@ -2050,6 +2207,10 @@ var SubagentSlots = class {
2050
2207
  * the first open window; concurrent windows on the same type keep counting
2051
2208
  * and the slot only exits messaging when the last window closes. */
2052
2209
  messagingPrev = /* @__PURE__ */ new Map();
2210
+ /** Slot type → open `requesting` overlay. Keyed by type so several live
2211
+ * instances of one type share a single marker; the real activity state stays
2212
+ * untouched in the slot and shows back up once the overlay expires. */
2213
+ requesting = /* @__PURE__ */ new Map();
2053
2214
  /** Count of open messaging windows per type (handles same-type siblings). */
2054
2215
  messagingCount = /* @__PURE__ */ new Map();
2055
2216
  reset() {
@@ -2059,6 +2220,7 @@ var SubagentSlots = class {
2059
2220
  this.messagingByCall.clear();
2060
2221
  this.messagingPrev.clear();
2061
2222
  this.messagingCount.clear();
2223
+ this.requesting.clear();
2062
2224
  }
2063
2225
  /**
2064
2226
  * subagent.spawned. A first spawn starts the slot as working; a second
@@ -2098,6 +2260,7 @@ var SubagentSlots = class {
2098
2260
  onTerminated(agentId) {
2099
2261
  const instance = this.instances.get(agentId);
2100
2262
  if (instance === void 0) return;
2263
+ if (this.requesting.get(instance.type)?.agentId === agentId) this.requesting.delete(instance.type);
2101
2264
  const remaining = (this.runningByType.get(instance.type) ?? 1) - 1;
2102
2265
  if (remaining > 0) {
2103
2266
  this.runningByType.set(instance.type, remaining);
@@ -2126,6 +2289,21 @@ var SubagentSlots = class {
2126
2289
  }
2127
2290
  this.setState(instance.type, kind === "tool" ? "working" : "outputting", agentId, detail);
2128
2291
  }
2292
+ /** A subagent asked the parent for help. The request returns immediately and
2293
+ * the agent keeps working, so this only raises a transient overlay — the real
2294
+ * activity state stays in the slot and shows back up when it expires. While
2295
+ * `messaging` is on screen the overlay loses (see visibleSlot). */
2296
+ onRequesting(agentId, at = Date.now()) {
2297
+ const instance = this.instances.get(agentId);
2298
+ if (instance === void 0) return;
2299
+ this.requesting.set(instance.type, {
2300
+ until: at + REQUESTING_WINDOW_MS,
2301
+ agentId
2302
+ });
2303
+ const slot = this.ensureSlot(instance.type);
2304
+ slot.agentId = agentId;
2305
+ slot.lastActivityAt = at;
2306
+ }
2129
2307
  /**
2130
2308
  * Main-side SendSubagentMessage tool call started: the target subagent's
2131
2309
  * slot shows messaging until the tool call returns.
@@ -2135,6 +2313,7 @@ var SubagentSlots = class {
2135
2313
  const instance = this.instances.get(targetAgentId);
2136
2314
  if (instance === void 0) return;
2137
2315
  this.messagingByCall.set(toolCallId, targetAgentId);
2316
+ this.requesting.delete(instance.type);
2138
2317
  const slot = this.ensureSlot(instance.type);
2139
2318
  if (slot.status !== "messaging") this.messagingPrev.set(instance.type, slot.status);
2140
2319
  this.messagingCount.set(instance.type, (this.messagingCount.get(instance.type) ?? 0) + 1);
@@ -2166,24 +2345,31 @@ var SubagentSlots = class {
2166
2345
  * All slots in fixed display order: the eight default types first (always
2167
2346
  * present, idle when never used), then extra types in spawn order.
2168
2347
  */
2169
- getSlots() {
2348
+ getSlots(at = Date.now()) {
2349
+ this.dropExpiredOverlays(at);
2170
2350
  const ordered = [];
2171
- for (const type of DEFAULT_SUBAGENT_TYPES) {
2172
- const slot = this.ensureSlot(type);
2173
- ordered.push({
2174
- ...slot,
2175
- count: this.runningByType.get(type) ?? 0
2176
- });
2177
- }
2351
+ for (const type of DEFAULT_SUBAGENT_TYPES) ordered.push(this.visibleSlot(type));
2178
2352
  for (const slot of this.slots.values()) {
2179
2353
  if (DEFAULT_SUBAGENT_TYPES.includes(slot.type)) continue;
2180
- ordered.push({
2181
- ...slot,
2182
- count: this.runningByType.get(slot.type) ?? 0
2183
- });
2354
+ ordered.push(this.visibleSlot(slot.type));
2184
2355
  }
2185
2356
  return ordered;
2186
2357
  }
2358
+ /** Display view of one slot: the real state with an unexpired `requesting`
2359
+ * overlay on top. Read-path only — it never mutates. Callers must run
2360
+ * {@link dropExpiredOverlays} first, and messaging clears the overlay in
2361
+ * {@link onMessagingStart}; the `messaging` guard only covers same-tick ordering. */
2362
+ visibleSlot(type) {
2363
+ const copy = {
2364
+ ...this.ensureSlot(type),
2365
+ count: this.runningByType.get(type) ?? 0
2366
+ };
2367
+ if (this.requesting.has(type) && copy.status !== "messaging") copy.status = "requesting";
2368
+ return copy;
2369
+ }
2370
+ dropExpiredOverlays(at) {
2371
+ for (const [type, overlay] of this.requesting) if (at >= overlay.until) this.requesting.delete(type);
2372
+ }
2187
2373
  ensureSlot(type) {
2188
2374
  let slot = this.slots.get(type);
2189
2375
  if (slot === void 0) {
@@ -2580,6 +2766,15 @@ var SessionEventHandler = class {
2580
2766
  subagentSlots = new SubagentSlots();
2581
2767
  /** Tool callId of the in-flight UpdateGoal(complete) (judging), if any. */
2582
2768
  goalJudgeCallId;
2769
+ /** Rolling first-token latencies of real requests, feeding the sidebar Hub
2770
+ * provider row. In-process only, and dropped whenever the model changes —
2771
+ * a different provider is a different network path. */
2772
+ providerLatencySamples = [];
2773
+ providerLatencyModel;
2774
+ providerLatencyAt;
2775
+ /** Enough samples to smooth one slow request, few enough that a recovered
2776
+ * endpoint reads as recovered again quickly. */
2777
+ providerLatencyWindow = 3;
2583
2778
  renderedSkillActivationIds = /* @__PURE__ */ new Set();
2584
2779
  renderedMcpServerStatusKeys = /* @__PURE__ */ new Map();
2585
2780
  mcpServerStatusSpinners = /* @__PURE__ */ new Map();
@@ -2595,6 +2790,9 @@ var SessionEventHandler = class {
2595
2790
  this.pendingSkillCandidates = [];
2596
2791
  this.promptedSkillCandidates.clear();
2597
2792
  this.goalJudgeCallId = void 0;
2793
+ this.providerLatencySamples = [];
2794
+ this.providerLatencyModel = void 0;
2795
+ this.providerLatencyAt = void 0;
2598
2796
  this.subagentUsageSeen.clear();
2599
2797
  this.host.setAppState({
2600
2798
  subagentUsage: {},
@@ -2768,6 +2966,41 @@ var SessionEventHandler = class {
2768
2966
  getSubagentSlots() {
2769
2967
  return this.subagentSlots.getSlots();
2770
2968
  }
2969
+ /** Keep the last few first-token latencies, per provider. Steps without a
2970
+ * measured latency (no stream, cached turn) are ignored rather than counted
2971
+ * as a zero. */
2972
+ recordProviderLatency(event) {
2973
+ const latency = event.llmFirstTokenLatencyMs;
2974
+ if (latency === void 0) return;
2975
+ const model = this.host.state.appState.model;
2976
+ if (this.providerLatencyModel !== void 0 && model !== this.providerLatencyModel) this.providerLatencySamples = [];
2977
+ this.providerLatencyModel = model;
2978
+ this.providerLatencySamples.push(latency);
2979
+ if (this.providerLatencySamples.length > this.providerLatencyWindow) this.providerLatencySamples.shift();
2980
+ this.providerLatencyAt = Date.now();
2981
+ }
2982
+ /** Sidebar Hub: average measured response time for the provider in use. The
2983
+ * caller owns tone and staleness — this stays a plain reading. */
2984
+ getProviderLatency() {
2985
+ if (this.providerLatencySamples.length === 0) return {
2986
+ ms: void 0,
2987
+ sampledAt: void 0
2988
+ };
2989
+ if (this.host.state.appState.model !== this.providerLatencyModel) {
2990
+ this.providerLatencySamples = [];
2991
+ this.providerLatencyModel = void 0;
2992
+ this.providerLatencyAt = void 0;
2993
+ return {
2994
+ ms: void 0,
2995
+ sampledAt: void 0
2996
+ };
2997
+ }
2998
+ const total = this.providerLatencySamples.reduce((sum, value) => sum + value, 0);
2999
+ return {
3000
+ ms: Math.round(total / this.providerLatencySamples.length),
3001
+ sampledAt: this.providerLatencyAt
3002
+ };
3003
+ }
2771
3004
  /** Feed routed subagent activity into the slot state machine. */
2772
3005
  feedSubagentSlots(event, agentId) {
2773
3006
  switch (event.type) {
@@ -2782,7 +3015,10 @@ var SessionEventHandler = class {
2782
3015
  this.subagentSlots.onTerminated(event.subagentId);
2783
3016
  break;
2784
3017
  case "tool.call.started":
2785
- this.subagentSlots.onActivity(agentId, "tool", `tool: ${event.name}`);
3018
+ if (event.name === "ContactParent") {
3019
+ this.subagentSlots.onRequesting(agentId);
3020
+ this.appendChildRequestNotice(agentId, event.args);
3021
+ } else this.subagentSlots.onActivity(agentId, "tool", `tool: ${event.name}`);
2786
3022
  break;
2787
3023
  case "tool.call.delta": break;
2788
3024
  case "tool.result":
@@ -2795,6 +3031,26 @@ var SessionEventHandler = class {
2795
3031
  default: break;
2796
3032
  }
2797
3033
  }
3034
+ /**
3035
+ * Surface a child→parent collaboration request in the transcript the moment
3036
+ * the subagent asks, so the human sees it without expanding the agent card.
3037
+ * The request always targets the main agent (one subagent host per session),
3038
+ * which is why a top-level transcript entry is the right home for it.
3039
+ */
3040
+ appendChildRequestNotice(agentId, args) {
3041
+ const fields = childRequestFieldsFromArgs(argsRecord(args));
3042
+ if (fields === null) return;
3043
+ const notice = renderChildRequestNotice(fields, this.subagentInfo.get(agentId)?.name);
3044
+ this.host.appendTranscriptEntry({
3045
+ id: nextTranscriptId(),
3046
+ kind: "status",
3047
+ turnId: this.host.streamingUI.getTurnContext().turnId,
3048
+ renderMode: "notice",
3049
+ content: notice.title,
3050
+ detail: notice.detail,
3051
+ noticeMarkerColor: this.host.state.theme.colors.warning
3052
+ });
3053
+ }
2798
3054
  routeSubagentEvent(event) {
2799
3055
  const subagentId = event.agentId;
2800
3056
  if (subagentId === "main") return false;
@@ -2912,6 +3168,7 @@ var SessionEventHandler = class {
2912
3168
  handleStepCompleted(event) {
2913
3169
  this.host.streamingUI.flushNow();
2914
3170
  this.maybeShowDebugTiming(event);
3171
+ this.recordProviderLatency(event);
2915
3172
  this.drainQueuedMessagesIntoSteer();
2916
3173
  const prev = this.host.state.appState.sessionUsage;
2917
3174
  const apiCalls = this.host.state.appState.sessionApiCalls + 1;
@@ -4018,6 +4275,10 @@ var SessionReplayRenderer = class {
4018
4275
  this.renderCronMissed(context, message.origin);
4019
4276
  return;
4020
4277
  }
4278
+ if (message.origin?.kind === "system_trigger") {
4279
+ this.renderSystemTrigger(context, message);
4280
+ return;
4281
+ }
4021
4282
  this.flushAssistant(context);
4022
4283
  const skill = skillActivationFromOrigin(message.origin);
4023
4284
  if (skill !== void 0) {
@@ -4028,6 +4289,26 @@ var SessionReplayRenderer = class {
4028
4289
  this.advanceTurn(context);
4029
4290
  this.host.appendTranscriptEntry(replayEntry(context, "user", contentPartsToText(message.content), "plain"));
4030
4291
  }
4292
+ /**
4293
+ * `system_trigger` messages are internal injections, not user speech: the
4294
+ * live transcript never prints them and markdown export filters them out, so
4295
+ * replay must not surface them as a user row either. The single exception is
4296
+ * a child→parent collaboration request, which gets the same notice the live
4297
+ * path shows. Replay has no spawn events, so the subagent's display name is
4298
+ * gone and the name-less title variant keeps the notice honest.
4299
+ */
4300
+ renderSystemTrigger(context, message) {
4301
+ const origin = message.origin;
4302
+ if (origin?.kind !== "system_trigger" || origin.name !== "child_request") return;
4303
+ const fields = childRequestFieldsFromNotification(contentPartsToText(message.content));
4304
+ if (fields === null) return;
4305
+ const notice = renderChildRequestNotice(fields);
4306
+ this.flushAssistant(context);
4307
+ this.host.appendTranscriptEntry({
4308
+ ...replayEntry(context, "status", notice.title, "notice", { detail: notice.detail }),
4309
+ noticeMarkerColor: this.host.state.theme.colors.warning
4310
+ });
4311
+ }
4031
4312
  renderToolCalls(context, toolCalls) {
4032
4313
  if (toolCalls.length === 0) return;
4033
4314
  const { streamingUI } = this.host;
@@ -6267,7 +6548,7 @@ var CommittedMessageComponent = class {
6267
6548
  return new Text(` ${chalk.hex(colors.textDim)(t("transcript.tool_name", { name }))}${summary}`, 0, 0).render(width);
6268
6549
  }
6269
6550
  case "status":
6270
- if (entry.renderMode === "notice") return new NoticeMessageComponent(entry.content, entry.detail, colors).render(width);
6551
+ if (entry.renderMode === "notice") return new NoticeMessageComponent(entry.content, entry.detail, colors, entry.noticeMarkerColor).render(width);
6271
6552
  return new StatusMessageComponent(entry.content, colors, entry.color).render(width);
6272
6553
  case "skill_activation": {
6273
6554
  const text = entry.skillName ?? entry.content;
@@ -6416,10 +6697,10 @@ var TranscriptController = class TranscriptController {
6416
6697
  return tc;
6417
6698
  }
6418
6699
  if (entry.backgroundAgentStatus !== void 0) return new BackgroundAgentStatusComponent(entry.backgroundAgentStatus, state.theme.colors);
6419
- return entry.renderMode === "notice" ? new NoticeMessageComponent(entry.content, entry.detail, state.theme.colors) : new StatusMessageComponent(entry.content, state.theme.colors, entry.color);
6700
+ return entry.renderMode === "notice" ? new NoticeMessageComponent(entry.content, entry.detail, state.theme.colors, entry.noticeMarkerColor) : new StatusMessageComponent(entry.content, state.theme.colors, entry.color);
6420
6701
  case "status":
6421
6702
  if (entry.backgroundAgentStatus !== void 0) return new BackgroundAgentStatusComponent(entry.backgroundAgentStatus, state.theme.colors);
6422
- return entry.renderMode === "notice" ? new NoticeMessageComponent(entry.content, entry.detail, state.theme.colors) : new StatusMessageComponent(entry.content, state.theme.colors, entry.color);
6703
+ return entry.renderMode === "notice" ? new NoticeMessageComponent(entry.content, entry.detail, state.theme.colors, entry.noticeMarkerColor) : new StatusMessageComponent(entry.content, state.theme.colors, entry.color);
6423
6704
  case "cron":
6424
6705
  if (entry.cronData === void 0) return null;
6425
6706
  return new CronMessageComponent(entry.content, entry.cronData, state.theme.colors);
@@ -6668,6 +6949,55 @@ function clampSidebarWidth(cols) {
6668
6949
  return Math.min(60, Math.max(24, Math.round(cols)));
6669
6950
  }
6670
6951
  //#endregion
6952
+ //#region src/tui/utils/display-width.ts
6953
+ /** Display-column width of a string, matching the terminal layout engine
6954
+ * (ANSI sequences stripped, CJK/wide chars counted as 2 columns). */
6955
+ function displayWidth(text) {
6956
+ return visibleWidth(text);
6957
+ }
6958
+ /** Pad `label` to `target` display columns with spaces (CJK-aware). */
6959
+ function padLabel(label, target) {
6960
+ return label + " ".repeat(Math.max(0, target - displayWidth(label)));
6961
+ }
6962
+ function layoutSidebarGrid(rows, width) {
6963
+ const valueCols = Math.max(0, ...rows.map((row) => displayWidth(row.value ?? "")));
6964
+ const labelBudget = Math.max(1, width - 2 - Math.max(valueCols, 1) - 1);
6965
+ const labelCols = Math.min(12, labelBudget);
6966
+ const valueStart = Math.max(2 + labelCols + 1, width - valueCols);
6967
+ return rows.map((row) => {
6968
+ const marker = padLabel(row.marker ?? "", 2);
6969
+ const label = truncateToWidth(row.label, labelCols);
6970
+ const used = 2 + displayWidth(label);
6971
+ if (row.value !== void 0) {
6972
+ const gap = Math.max(1, width - used - displayWidth(row.value));
6973
+ const room = Math.max(1, width - used - gap);
6974
+ return {
6975
+ marker,
6976
+ label,
6977
+ gap,
6978
+ value: truncateToWidth(row.value, room)
6979
+ };
6980
+ }
6981
+ if (row.note !== void 0) {
6982
+ const noteCols = displayWidth(row.note);
6983
+ const aligned = Math.max(1, valueStart - used);
6984
+ const fits = Math.max(1, width - used - noteCols);
6985
+ const gap = Math.min(aligned, fits);
6986
+ return {
6987
+ marker,
6988
+ label,
6989
+ gap,
6990
+ note: truncateToWidth(row.note, Math.max(1, width - used - gap))
6991
+ };
6992
+ }
6993
+ return {
6994
+ marker,
6995
+ label,
6996
+ gap: 1
6997
+ };
6998
+ });
6999
+ }
7000
+ //#endregion
6671
7001
  //#region src/tui/components/sidebar/panels/git-panel.ts
6672
7002
  /**
6673
7003
  * Summary-only Git panel (摘要式): the workdir name plus added (green) /
@@ -6683,12 +7013,13 @@ var GitPanelContent = class {
6683
7013
  }
6684
7014
  invalidate() {}
6685
7015
  render(width) {
7016
+ const indent = " ".repeat(2);
6686
7017
  const git = this.ctx.getData().git;
6687
- if (git === void 0) return [chalk.hex(this.colors.textDim)(t("sidebar.git_no_repo"))];
7018
+ if (git === void 0) return [`${indent}${chalk.hex(this.colors.textDim)(t("sidebar.git_no_repo"))}`];
6688
7019
  const dim = (s) => chalk.hex(this.colors.textDim)(s);
6689
- const workDir = truncateToWidth(chalk.hex(this.colors.text)(basename(git.workDir)), width);
6690
- if (git.diffAdded === 0 && git.diffDeleted === 0 && git.filesCount === 0) return [workDir, dim(t("sidebar.git_clean"))];
6691
- return [workDir, truncateToWidth(`${chalk.hex(this.colors.success)(`+${git.diffAdded}`)} ${chalk.hex(this.colors.error)(`−${git.diffDeleted}`)} ${dim(t("sidebar.git_files", { count: git.filesCount }))}`, width)];
7020
+ const workDir = truncateToWidth(`${indent}${chalk.hex(this.colors.text)(basename(git.workDir))}`, width);
7021
+ if (git.diffAdded === 0 && git.diffDeleted === 0 && git.filesCount === 0) return [workDir, dim(`${indent}${t("sidebar.git_clean")}`)];
7022
+ return [workDir, truncateToWidth(`${indent}${chalk.hex(this.colors.success)(`+${git.diffAdded}`)} ${chalk.hex(this.colors.error)(`−${git.diffDeleted}`)} ${dim(t("sidebar.git_files", { count: git.filesCount }))}`, width)];
6692
7023
  }
6693
7024
  };
6694
7025
  const gitPanel = {
@@ -6702,35 +7033,6 @@ const gitPanel = {
6702
7033
  }
6703
7034
  };
6704
7035
  //#endregion
6705
- //#region src/tui/utils/display-width.ts
6706
- /** Display-column width of a string, matching the terminal layout engine
6707
- * (ANSI sequences stripped, CJK/wide chars counted as 2 columns). */
6708
- function displayWidth(text) {
6709
- return visibleWidth(text);
6710
- }
6711
- /** Pad `label` to `target` display columns with spaces (CJK-aware). */
6712
- function padLabel(label, target) {
6713
- return label + " ".repeat(Math.max(0, target - displayWidth(label)));
6714
- }
6715
- /** Pad `value` on the LEFT to `target` display columns (CJK-aware), which is
6716
- * what right-aligning a numeric column means in terminal space. */
6717
- function padValue(value, target) {
6718
- return " ".repeat(Math.max(0, target - displayWidth(value))) + value;
6719
- }
6720
- function alignMetricRows(rows, opts = {}) {
6721
- const gap = opts.gap ?? 2;
6722
- const labels = [...rows.map(([label]) => label), ...opts.extraLabels ?? []];
6723
- const labelCols = labels.length === 0 ? 0 : Math.max(...labels.map((label) => displayWidth(label)));
6724
- const measured = rows.length === 0 ? 0 : Math.max(...rows.map(([, value]) => displayWidth(value)));
6725
- const valueCols = opts.maxValueCols === void 0 ? measured : Math.min(measured, Math.max(0, opts.maxValueCols));
6726
- const separator = " ".repeat(Math.max(0, gap));
6727
- return {
6728
- labelCols,
6729
- valueCols,
6730
- rows: rows.map(([label, value]) => [padLabel(label, labelCols) + separator, padValue(truncateToWidth(value, valueCols), valueCols)])
6731
- };
6732
- }
6733
- //#endregion
6734
7036
  //#region src/tui/components/sidebar/panels/session-panel.ts
6735
7037
  function formatDuration(ms) {
6736
7038
  const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
@@ -6766,10 +7068,10 @@ function formatCompact(n) {
6766
7068
  if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
6767
7069
  return String(n);
6768
7070
  }
6769
- /** True when a two-column grid fits `width` display columns. */
7071
+ /** True when the shared grid can show these values at full width: the label
7072
+ * column is fixed across panels, so only the value column decides the fit. */
6770
7073
  function gridFits(rows, width) {
6771
- const grid = alignMetricRows(rows);
6772
- return grid.labelCols + 2 + grid.valueCols <= width;
7074
+ return 15 + Math.max(0, ...rows.map(([, value]) => displayWidth(value))) <= width;
6773
7075
  }
6774
7076
  /**
6775
7077
  * Session summary, live-rendered each frame as a strict two-column table:
@@ -6806,10 +7108,11 @@ var SessionPanelContent = class {
6806
7108
  ];
6807
7109
  const withTokens = (compact) => [...tokenRows.map(([name, n]) => [name, compact ? formatCompact(n) : formatCount$1(n)]), ...countRows];
6808
7110
  const full = withTokens(false);
6809
- const rows = gridFits(full, width) ? full : withTokens(true);
6810
- const probe = alignMetricRows(rows);
6811
- const budget = Math.max(1, width - probe.labelCols - 2);
6812
- return (probe.valueCols <= budget ? probe : alignMetricRows(rows, { maxValueCols: budget })).rows.map(([k, v]) => label(k) + value(v));
7111
+ return layoutSidebarGrid((gridFits(full, width) ? full : withTokens(true)).map(([name, reading]) => ({
7112
+ marker: "●",
7113
+ label: name,
7114
+ value: reading
7115
+ })), width).map((cell) => `${label(cell.marker)}${label(cell.label)}${" ".repeat(cell.gap)}${value(cell.value ?? "")}`);
6813
7116
  }
6814
7117
  };
6815
7118
  const sessionPanel = {
@@ -6836,6 +7139,7 @@ function statusText(status) {
6836
7139
  case "outputting": return t("sidebar.agent_outputting");
6837
7140
  case "messaging": return t("sidebar.agent_messaging");
6838
7141
  case "reworking": return t("sidebar.agent_reworking");
7142
+ case "requesting": return t("sidebar.agent_requesting");
6839
7143
  }
6840
7144
  }
6841
7145
  /**
@@ -6854,17 +7158,22 @@ var AgentsPanelContent = class {
6854
7158
  render(width) {
6855
7159
  const agents = this.ctx.getData().agents;
6856
7160
  if (agents === void 0 || agents.length === 0) return [];
6857
- const lines = [];
6858
- const nameWidth = Math.max(...agents.map((a) => displayWidth(a.type)));
6859
- for (const [index, slot] of agents.entries()) lines.push(this.renderSlot(slot, index, nameWidth, width));
6860
- return lines;
7161
+ return layoutSidebarGrid(agents.map((slot) => ({
7162
+ marker: slot.status === "idle" ? "○" : "●",
7163
+ label: slot.type,
7164
+ value: statusText(slot.status) + (slot.count > 1 ? ` ×${slot.count}` : "")
7165
+ })), width).map((cell, index) => this.renderSlot(cell, agents[index], index, width));
6861
7166
  }
6862
- renderSlot(slot, index, nameWidth, width) {
7167
+ renderSlot(cell, slot, index, width) {
6863
7168
  const dim = (s) => chalk.hex(this.colors.textDim)(s);
6864
- if (slot.status === "idle") return `${dim("○")} ${dim(padLabel(slot.type, nameWidth))} ${dim(statusText("idle"))}`;
7169
+ const idle = slot.status === "idle";
6865
7170
  const phase = (Date.now() % AGENT_GRADIENT_CYCLE_MS / AGENT_GRADIENT_CYCLE_MS + index * .125) % 1;
6866
7171
  const active = slot.status === "working" || slot.status === "outputting" ? chalk.hex(lerpGradient(phase)).bold : void 0;
6867
- return truncateToWidth(`${active ? active("●") : chalk.hex(this.statusColor(slot.status))("●")} ${chalk.hex(this.colors.textStrong)(padLabel(slot.type, nameWidth))} ${active ? active(statusText(slot.status)) : chalk.hex(this.colors.text)(statusText(slot.status))}${slot.count > 1 ? ` ${dim("×" + slot.count)}` : ""}`, width);
7172
+ const marker = padLabel((active ?? chalk.hex(this.statusColor(slot.status)))(idle ? "" : ""), 2);
7173
+ const label = (idle ? dim : chalk.hex(this.colors.textStrong))(cell.label);
7174
+ const paint = active ?? (idle ? dim : slot.status === "requesting" ? chalk.hex(this.colors.warning) : chalk.hex(this.colors.text));
7175
+ const count = slot.count > 1 ? dim(` ×${slot.count}`) : "";
7176
+ return truncateToWidth(`${marker}${label}${" ".repeat(cell.gap)}${paint(statusText(slot.status))}${count}`, width);
6868
7177
  }
6869
7178
  statusColor(status) {
6870
7179
  switch (status) {
@@ -6872,6 +7181,7 @@ var AgentsPanelContent = class {
6872
7181
  case "outputting": return this.colors.primary;
6873
7182
  case "messaging": return this.colors.accent;
6874
7183
  case "reworking": return this.colors.warning;
7184
+ case "requesting": return this.colors.warning;
6875
7185
  case "idle": return this.colors.textDim;
6876
7186
  }
6877
7187
  }
@@ -6929,26 +7239,48 @@ var GoalPanelContent = class {
6929
7239
  const wallMs = goal.status === "active" ? goal.wallClockMs + Math.max(0, now - goal.wallClockBaseAt) : goal.wallClockMs;
6930
7240
  const lines = [];
6931
7241
  lines.push(this.renderBadge(goal.status, goal.judge, dim));
6932
- lines.push(truncateToWidth(dim(goal.objective), width));
7242
+ lines.push(truncateToWidth(dim(`● ${goal.objective}`), width));
6933
7243
  const rows = [
6934
- [t("sidebar.goal_tokens"), formatCount(goal.tokensUsed)],
6935
- [t("sidebar.goal_tokens_input"), goal.inputTokens === null ? "" : formatCount(goal.inputTokens)],
6936
- [t("sidebar.goal_tokens_output"), goal.outputTokens === null ? "—" : formatCount(goal.outputTokens)],
6937
- [t("sidebar.goal_turns"), String(goal.turnsUsed)],
6938
- [t("sidebar.goal_wall"), formatWallClock(wallMs)],
6939
- [t("sidebar.goal_judge"), judgeText(goal.judge)]
7244
+ {
7245
+ marker: "",
7246
+ label: t("sidebar.goal_tokens"),
7247
+ value: formatCount(goal.tokensUsed)
7248
+ },
7249
+ {
7250
+ marker: "●",
7251
+ label: t("sidebar.goal_tokens_input"),
7252
+ value: goal.inputTokens === null ? "—" : formatCount(goal.inputTokens)
7253
+ },
7254
+ {
7255
+ marker: "●",
7256
+ label: t("sidebar.goal_tokens_output"),
7257
+ value: goal.outputTokens === null ? "—" : formatCount(goal.outputTokens)
7258
+ },
7259
+ {
7260
+ marker: "●",
7261
+ label: t("sidebar.goal_turns"),
7262
+ value: String(goal.turnsUsed)
7263
+ },
7264
+ {
7265
+ marker: "●",
7266
+ label: t("sidebar.goal_wall"),
7267
+ value: formatWallClock(wallMs)
7268
+ },
7269
+ {
7270
+ marker: "●",
7271
+ label: t("sidebar.goal_judge"),
7272
+ value: judgeText(goal.judge)
7273
+ }
6940
7274
  ];
6941
- const hasCriterion = goal.completionCriterion !== null;
6942
- const criterionLabel = t("sidebar.goal_criterion");
6943
- const extraLabels = hasCriterion ? [criterionLabel] : [];
6944
- const probe = alignMetricRows(rows, { extraLabels });
6945
- const budget = Math.max(4, width - probe.labelCols - 2);
6946
- const grid = probe.valueCols <= budget ? probe : alignMetricRows(rows, {
6947
- extraLabels,
6948
- maxValueCols: budget
6949
- });
6950
- for (const [label, value] of grid.rows) lines.push(dim(label) + text(value));
6951
- if (hasCriterion) lines.push(dim(padLabel(criterionLabel, grid.labelCols) + " ".repeat(2)) + truncateToWidth(text(goal.completionCriterion), budget));
7275
+ const gridRows = goal.completionCriterion !== null ? [...rows, {
7276
+ marker: "",
7277
+ label: t("sidebar.goal_criterion"),
7278
+ note: goal.completionCriterion ?? ""
7279
+ }] : rows;
7280
+ for (const cell of layoutSidebarGrid(gridRows, width)) {
7281
+ const reading = cell.value ?? cell.note ?? "";
7282
+ lines.push(`${dim(`${cell.marker}${cell.label}`)}${" ".repeat(cell.gap)}${text(reading)}`);
7283
+ }
6952
7284
  return lines;
6953
7285
  }
6954
7286
  /** State badge: judging wins over everything; otherwise map the snapshot
@@ -6974,6 +7306,212 @@ const goalPanel = {
6974
7306
  return new GoalPanelContent(ctx, ctx.colors);
6975
7307
  }
6976
7308
  };
7309
+ /** Per-request budget: long enough for a slow CDN, short enough to never hang. */
7310
+ const HUB_PROBE_TIMEOUT_MS = 1500;
7311
+ /** Row id for the model provider, whose latency is measured, never probed. */
7312
+ const HUB_MODEL_ROW_ID = "model";
7313
+ /** A measured provider reading older than this is dimmed: nothing was asked lately. */
7314
+ const HUB_MODEL_STALE_MS = 5 * 6e4;
7315
+ /** Default probe targets: the endpoints day-to-day development tends to need. */
7316
+ const HUB_ENDPOINTS = Object.freeze([
7317
+ {
7318
+ id: "github",
7319
+ url: "https://github.com/"
7320
+ },
7321
+ {
7322
+ id: "google",
7323
+ url: "https://www.google.com/"
7324
+ },
7325
+ {
7326
+ id: "x",
7327
+ url: "https://x.com/",
7328
+ label: "x (twitter)"
7329
+ },
7330
+ {
7331
+ id: "huggingface",
7332
+ url: "https://huggingface.co/"
7333
+ },
7334
+ {
7335
+ id: "alibaba",
7336
+ url: "https://www.aliyun.com/",
7337
+ label: "Alibaba"
7338
+ },
7339
+ {
7340
+ id: "tencent",
7341
+ url: "https://cloud.tencent.com/",
7342
+ label: "Tencent"
7343
+ }
7344
+ ]);
7345
+ function defaultFetch(url, init) {
7346
+ return globalThis.fetch(url, init);
7347
+ }
7348
+ function createHubProbe(options = {}) {
7349
+ const endpoints = options.endpoints ?? HUB_ENDPOINTS;
7350
+ const now = options.now ?? Date.now;
7351
+ const fetchImpl = options.fetchImpl ?? defaultFetch;
7352
+ const sites = /* @__PURE__ */ new Map();
7353
+ for (const endpoint of endpoints) sites.set(endpoint.id, {
7354
+ ms: void 0,
7355
+ failures: 0
7356
+ });
7357
+ let roundAt;
7358
+ let inFlight = false;
7359
+ const runRound = async () => {
7360
+ const startedAt = now();
7361
+ const results = await Promise.allSettled(endpoints.map(async (endpoint) => {
7362
+ const response = await fetchImpl(endpoint.url, {
7363
+ method: "HEAD",
7364
+ redirect: "manual",
7365
+ signal: AbortSignal.timeout(HUB_PROBE_TIMEOUT_MS)
7366
+ });
7367
+ return {
7368
+ id: endpoint.id,
7369
+ ms: Math.max(0, now() - startedAt),
7370
+ status: response.status
7371
+ };
7372
+ }));
7373
+ for (const [index, result] of results.entries()) {
7374
+ const site = sites.get(endpoints[index].id);
7375
+ if (site === void 0) continue;
7376
+ if (result.status === "fulfilled") {
7377
+ site.ms = result.value.ms;
7378
+ site.failures = 0;
7379
+ } else {
7380
+ site.failures += 1;
7381
+ if (site.failures >= 2) site.ms = void 0;
7382
+ }
7383
+ }
7384
+ roundAt = now();
7385
+ inFlight = false;
7386
+ };
7387
+ return {
7388
+ sampleIfStale: () => {
7389
+ if (inFlight) return;
7390
+ const at = now();
7391
+ if (roundAt !== void 0 && at - roundAt < 3e4) return;
7392
+ inFlight = true;
7393
+ runRound().catch(() => {
7394
+ inFlight = false;
7395
+ });
7396
+ },
7397
+ snapshot: () => {
7398
+ const at = now();
7399
+ const stale = roundAt === void 0 || at - roundAt >= 3e4 * 2;
7400
+ const samples = [];
7401
+ for (const endpoint of endpoints) {
7402
+ const site = sites.get(endpoint.id);
7403
+ samples.push({
7404
+ id: endpoint.id,
7405
+ label: endpoint.label ?? endpoint.id,
7406
+ ms: site.ms,
7407
+ tone: toneFor(site, stale)
7408
+ });
7409
+ }
7410
+ return {
7411
+ samples,
7412
+ roundAt,
7413
+ stale,
7414
+ pending: inFlight
7415
+ };
7416
+ }
7417
+ };
7418
+ }
7419
+ function toneFor(site, stale) {
7420
+ if (stale) return "dim";
7421
+ if (site.failures >= 2) return "down";
7422
+ return hubLatencyTone(site.ms, site.failures > 0);
7423
+ }
7424
+ /**
7425
+ * Merge the measured provider reading with the probe samples into Hub rows.
7426
+ * The provider row always leads (it answers "is the model I'm using reachable"),
7427
+ * and it staleness-checks against its own budget: a long idle stretch means
7428
+ * nothing was asked, not that the network died.
7429
+ */
7430
+ function buildHubSamples(snapshot, provider, now = Date.now()) {
7431
+ const ageMs = provider.sampledAt === void 0 ? 0 : now - provider.sampledAt;
7432
+ return [{
7433
+ id: HUB_MODEL_ROW_ID,
7434
+ ms: provider.ms,
7435
+ tone: hubLatencyTone(provider.ms, ageMs >= HUB_MODEL_STALE_MS)
7436
+ }, ...snapshot.samples];
7437
+ }
7438
+ /** Shared tone rule for every Hub row (probe sites and the measured provider). */
7439
+ function hubLatencyTone(ms, stale = false) {
7440
+ if (ms === void 0 || stale) return "dim";
7441
+ return ms <= 100 ? "ok" : "warn";
7442
+ }
7443
+ /**
7444
+ * Compact latency reading: whole milliseconds below a second, one-decimal
7445
+ * seconds above — a probe that never answers must never read as `65400ms`.
7446
+ */
7447
+ function formatHubLatency(ms) {
7448
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
7449
+ return `${(ms / 1e3).toFixed(1)}s`;
7450
+ }
7451
+ //#endregion
7452
+ //#region src/tui/components/sidebar/panels/hub-panel.ts
7453
+ /** Pending marker: three middle dots (Latin-1, so terminal width is stable). */
7454
+ const PENDING_MARK = "···";
7455
+ /** No reading at all — a site that stopped answering, or nothing measured yet. */
7456
+ const EMPTY_MARK = "--";
7457
+ /**
7458
+ * Hub panel: network health at a glance. One row per reading — the measured
7459
+ * model-provider response time first, then a probe round trip for each
7460
+ * well-known endpoint.
7461
+ *
7462
+ * Values share one right edge through the same two-column engine the Session
7463
+ * panel uses, so rows never jitter as numbers change width, and only the value
7464
+ * carries colour (green ≤100ms, amber above it, red when a site stopped
7465
+ * answering). The panel is a pure view: probing happens in the data provider,
7466
+ * which is what keeps a slow endpoint from ever touching a frame.
7467
+ */
7468
+ var HubPanelContent = class {
7469
+ ctx;
7470
+ colors;
7471
+ constructor(ctx, colors) {
7472
+ this.ctx = ctx;
7473
+ this.colors = colors;
7474
+ }
7475
+ invalidate() {}
7476
+ render(width) {
7477
+ const hub = this.ctx.getData().hub;
7478
+ if (hub === void 0 || hub.samples.length === 0) return [];
7479
+ return layoutSidebarGrid(hub.samples.map((sample) => ({
7480
+ marker: "●",
7481
+ label: this.labelOf(sample),
7482
+ value: this.reading(sample, hub)
7483
+ })), width).map((cell, index) => {
7484
+ const tone = hub.samples[index]?.tone ?? "dim";
7485
+ return `${chalk.hex(this.colors.textDim)(`${cell.marker}${cell.label}`)}${" ".repeat(cell.gap)}${chalk.hex(this.toneColor(tone))(cell.value ?? "")}`;
7486
+ });
7487
+ }
7488
+ labelOf(sample) {
7489
+ return sample.id === "model" ? t("sidebar.hub_model") : sample.label ?? sample.id;
7490
+ }
7491
+ reading(sample, hub) {
7492
+ if (sample.ms !== void 0) return formatHubLatency(sample.ms);
7493
+ if (hub.pending && sample.tone === "dim" && sample.id !== "model") return PENDING_MARK;
7494
+ return EMPTY_MARK;
7495
+ }
7496
+ toneColor(tone) {
7497
+ switch (tone) {
7498
+ case "ok": return this.colors.success;
7499
+ case "warn": return this.colors.warning;
7500
+ case "down": return this.colors.error;
7501
+ case "dim": return this.colors.textDim;
7502
+ }
7503
+ }
7504
+ };
7505
+ const hubPanel = {
7506
+ id: "hub",
7507
+ get title() {
7508
+ return t("sidebar.hub");
7509
+ },
7510
+ width: 30,
7511
+ build(ctx) {
7512
+ return new HubPanelContent(ctx, ctx.colors);
7513
+ }
7514
+ };
6977
7515
  //#endregion
6978
7516
  //#region src/utils/git/git-status.ts
6979
7517
  /**
@@ -7533,6 +8071,9 @@ var LifecycleController = class LifecycleController {
7533
8071
  /** Sidebar-only git cache (the footer owns its own). Rebuilt when the
7534
8072
  * workDir changes; `getStatus()` is TTL-cached so polling is cheap. */
7535
8073
  sidebarGitCache;
8074
+ /** Sidebar Hub probe: read-through TTL, no timer — probing happens because a
8075
+ * frame asked, so a closed sidebar produces no traffic. */
8076
+ hubProbe = createHubProbe();
7536
8077
  sidebarGitCacheWorkDir = "";
7537
8078
  /** Session start time for the sidebar uptime counter. Reset on session
7538
8079
  * switch so a resumed old session doesn't show the TUI process age. */
@@ -7729,6 +8270,7 @@ var LifecycleController = class LifecycleController {
7729
8270
  if (!m.allPanels.some((p) => p.id === "session")) m.register(sessionPanel);
7730
8271
  if (!m.allPanels.some((p) => p.id === "agents")) m.register(agentsPanel);
7731
8272
  if (!m.allPanels.some((p) => p.id === "goal")) m.register(goalPanel);
8273
+ if (!m.allPanels.some((p) => p.id === "hub")) m.register(hubPanel);
7732
8274
  m.setDataProvider(() => this.buildSidebarData());
7733
8275
  }
7734
8276
  const sidebar = this.host.state.sidebarPane;
@@ -7770,9 +8312,25 @@ var LifecycleController = class LifecycleController {
7770
8312
  git: this.readSidebarGit(appState.workDir),
7771
8313
  sessionStats: this.readSidebarSessionStats(appState),
7772
8314
  agents: this.host.sessionEventHandler.getSubagentSlots(),
8315
+ hub: this.readSidebarHub(),
7773
8316
  goal: this.readSidebarGoal(appState)
7774
8317
  };
7775
8318
  }
8319
+ /**
8320
+ * Hub block. Probe rounds are kicked off from the render path only (the
8321
+ * sidebar container stops rendering while closed), and the round is always
8322
+ * fire-and-forget — a dead endpoint can never hold up a frame. The model row
8323
+ * is different: it reads the first-token latency of real requests, so it is
8324
+ * measured rather than probed, and it goes stale on its own budget.
8325
+ */
8326
+ readSidebarHub() {
8327
+ if (this.host.state.sidebarManager?.isOpen === true) this.hubProbe.sampleIfStale();
8328
+ const snapshot = this.hubProbe.snapshot();
8329
+ return {
8330
+ samples: buildHubSamples(snapshot, this.host.sessionEventHandler.getProviderLatency()),
8331
+ pending: snapshot.pending
8332
+ };
8333
+ }
7776
8334
  readSidebarGit(workDir) {
7777
8335
  if (this.sidebarGitCache === void 0 || this.sidebarGitCacheWorkDir !== workDir) {
7778
8336
  this.sidebarGitCache = createGitStatusCache(workDir);
@@ -13632,7 +14190,7 @@ var ScreamTUI = class {
13632
14190
  const card = new OnboardingCardComponent({
13633
14191
  colors: this.state.theme.colors,
13634
14192
  onConfigure: () => {
13635
- import("./dispatch-CEK-syAX.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14193
+ import("./dispatch-5S52okPl.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
13636
14194
  this.restoreEditor();
13637
14195
  onDone?.();
13638
14196
  });
@@ -14459,15 +15017,13 @@ async function runChannelSetup() {
14459
15017
  //#endregion
14460
15018
  //#region src/cli/run-stream-json.ts
14461
15019
  /**
14462
- * Claude Code stream-json I/O adapter.
14463
- *
14464
- * Reads stdin in Claude Code stream-json dialect, processes messages through
14465
- * the ScreamCode agent, and writes stdout in Claude Code dialect. This lets
14466
- * cc-connect (and any tool that speaks the Claude Code stdio protocol) use
14467
- * ScreamCode as a drop-in agent backend.
15020
+ * Stream-json stdio adapter (cc-connect compat).
14468
15021
  *
14469
- * Protocol reference:
14470
- * https://docs.anthropic.com/en/docs/claude-code/stdio-stream-json
15022
+ * Reads stdin as line-based stream-json messages, processes them through the
15023
+ * ScreamCode agent, and writes stdout as stream-json events. This is the
15024
+ * integration surface cc-connect drives when it spawns `scream stream-json`,
15025
+ * and any tool that speaks the same stdio protocol can use ScreamCode as an
15026
+ * agent backend through it.
14471
15027
  */
14472
15028
  function buildStreamJsonRuntimePrompt(input) {
14473
15029
  const appendParts = [
@@ -14480,7 +15036,7 @@ function buildStreamJsonRuntimePrompt(input) {
14480
15036
  append: appendParts.join("\n\n")
14481
15037
  };
14482
15038
  }
14483
- var ClaudeStreamJsonWriter = class {
15039
+ var StreamJsonWriter = class {
14484
15040
  writeLine;
14485
15041
  sessionId = "";
14486
15042
  msgCounter = 0;
@@ -14611,7 +15167,7 @@ var ClaudeStreamJsonWriter = class {
14611
15167
  output: 0
14612
15168
  };
14613
15169
  }
14614
- /** Emit a resume hint as a meta message (same format as Claude Code). */
15170
+ /** Emit a resume hint as a stream-json meta message. */
14615
15171
  emitResumeHint(sessionId) {
14616
15172
  this.writeJson({
14617
15173
  role: "meta",
@@ -14686,7 +15242,7 @@ async function* readStdinMessages() {
14686
15242
  else log.debug("stream-json: ignoring unknown stdin message type", { type: msg.type });
14687
15243
  }
14688
15244
  }
14689
- /** Extract plain text from a Claude Code-style user message. */
15245
+ /** Extract plain text from a stream-json user message. */
14690
15246
  function extractUserText(msg) {
14691
15247
  const content = msg.message.content;
14692
15248
  if (typeof content === "string") return content;
@@ -14727,7 +15283,7 @@ function mapCcConnectMode(mode) {
14727
15283
  }
14728
15284
  /**
14729
15285
  * Install the stdout EPIPE guard and return the line writer handed to
14730
- * {@link ClaudeStreamJsonWriter}.
15286
+ * {@link StreamJsonWriter}.
14731
15287
  *
14732
15288
  * cc-connect closes our stdout when it kills/restarts the agent (idle
14733
15289
  * timeout, /restart, config change). Writing after that raises EPIPE as an
@@ -14783,7 +15339,7 @@ async function runStreamJson(opts) {
14783
15339
  }
14784
15340
  const streamSubagentModels = streamTuiConfig.subagentModels;
14785
15341
  harness.setSubagentModelBindings(() => streamSubagentModels);
14786
- const writer = new ClaudeStreamJsonWriter(installStdoutEpipeGuard());
15342
+ const writer = new StreamJsonWriter(installStdoutEpipeGuard());
14787
15343
  let session;
14788
15344
  let currentSessionId;
14789
15345
  let sessionKey = "cc-connect-main";
@@ -15643,6 +16199,14 @@ const contentTypes = {
15643
16199
  function cloneSnapshot(value) {
15644
16200
  return structuredClone(value);
15645
16201
  }
16202
+ /**
16203
+ * Duck-typed SESSION_NOT_FOUND check: covers both same-realm ScreamError
16204
+ * instances and errors that crossed a serialization boundary carrying `code`.
16205
+ */
16206
+ function isSessionNotFoundError(error) {
16207
+ if (isScreamError(error)) return error.code === ErrorCodes.SESSION_NOT_FOUND;
16208
+ return typeof error === "object" && error !== null && error.code === ErrorCodes.SESSION_NOT_FOUND;
16209
+ }
15646
16210
  function errorMessage(error) {
15647
16211
  return error instanceof Error ? error.message : String(error);
15648
16212
  }
@@ -17388,7 +17952,20 @@ var SessionManager = class {
17388
17952
  if (!existing) return null;
17389
17953
  if (existing.isActive) return existing;
17390
17954
  const meta = existing.getMetadata();
17391
- const session = await this.harness.resumeSession({ id: meta.coreSessionId ?? meta.sessionId });
17955
+ const coreId = meta.coreSessionId ?? meta.sessionId;
17956
+ let session;
17957
+ try {
17958
+ session = await this.harness.resumeSession({ id: coreId });
17959
+ } catch (error) {
17960
+ if (!isSessionNotFoundError(error)) throw error;
17961
+ if (!await this.reindexCoreSession(coreId, meta.workDir)) return null;
17962
+ try {
17963
+ session = await this.harness.resumeSession({ id: coreId });
17964
+ } catch (retryError) {
17965
+ if (isSessionNotFoundError(retryError)) return null;
17966
+ throw retryError;
17967
+ }
17968
+ }
17392
17969
  const reactivated = new WebSession(session, {
17393
17970
  sessionId,
17394
17971
  workDir: meta.workDir,
@@ -17414,6 +17991,30 @@ var SessionManager = class {
17414
17991
  log.info("web: session reactivated", { sessionId });
17415
17992
  return reactivated;
17416
17993
  }
17994
+ /**
17995
+ * Self-heal for "meta exists but core session missing from the global
17996
+ * index": re-append the index entry when the session directory is still on
17997
+ * disk. Returns false when the directory is truly gone.
17998
+ */
17999
+ async reindexCoreSession(coreId, workDir) {
18000
+ const sessionDir = join(this.homeDir, "sessions", encodeWorkDirKey(workDir), coreId);
18001
+ if (!existsSync(sessionDir)) return false;
18002
+ try {
18003
+ await appendSessionIndexEntry(this.homeDir, {
18004
+ sessionId: coreId,
18005
+ sessionDir,
18006
+ workDir
18007
+ });
18008
+ log.info("web: re-indexed core session from web metadata", { sessionId: coreId });
18009
+ return true;
18010
+ } catch (error) {
18011
+ log.warn("web: core session reindex failed", {
18012
+ sessionId: coreId,
18013
+ error: errorMessage(error)
18014
+ });
18015
+ return false;
18016
+ }
18017
+ }
17417
18018
  get(sessionId) {
17418
18019
  return this.sessions.get(sessionId);
17419
18020
  }