scream-code 0.15.9 → 0.16.1

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 { $n as ErrorCodes, $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, 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 flushDiagnosticLogs, K as resetBreathingClock, Kn as resolveScreamHome, 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 isOrphanedToolCallError, 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, Un as encodeWorkDirKey, Ut as isStreaming, V as AssistantMessageComponent, Vn as saveCatalogCache, Vt as TERMINAL_THEME_LIGHT, W as BREATHE_CYCLE_MS, Wn as appendSessionIndexEntry, Wt as FooterComponent, Xn as resolveGlobalLogPath, Xt as printableChar, Yn as log, Zn as isScreamError, 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, er as SCREAM_ERROR_INFO, 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 MemoryMemoStore, 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-D8ot3OUd.mjs";
8
- import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-Dx7LJTG6.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-DqeJdQZQ.mjs";
8
+ import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-BABWY9vy.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.9"),
35
+ version: optionalBuildString("0.16.1"),
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,163 @@ 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
+ expecting: textOf(payload["expecting"]),
2106
+ artifacts: listOf("artifacts"),
2107
+ evidence: listOf("evidence"),
2108
+ missing: listOf("missing")
2109
+ };
2110
+ }
2111
+ /** Parse `artifacts: [a, b]` back into its items. */
2112
+ function bracketedListOf(value) {
2113
+ return (value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value).split(",").map((item) => item.trim()).filter((item) => item.length > 0);
2114
+ }
2115
+ /**
2116
+ * Replay producer: recover the fields from the injected notification. The body
2117
+ * shape is owned by `submitChildRequest` in agent-core:
2118
+ * `<type>: <message>` then optional `needs:` / `artifacts:` / `evidence:` /
2119
+ * `missing:` lines. A multi-line message keeps its extra lines attached.
2120
+ */
2121
+ function childRequestFieldsFromNotification(text) {
2122
+ const lines = text.split("\n");
2123
+ const open = lines.findIndex((line) => line.startsWith("<notification ") && line.includes("type=\"child_request\""));
2124
+ if (open < 0) return null;
2125
+ const body = [];
2126
+ for (let index = open + 1; index < lines.length; index += 1) {
2127
+ const line = lines[index] ?? "";
2128
+ if (line === "</notification>") break;
2129
+ if (line.startsWith("Title: ") || line.startsWith("Severity: ")) continue;
2130
+ body.push(line);
2131
+ }
2132
+ const first = body[0];
2133
+ if (first === void 0) return null;
2134
+ const head = /^(info|handoff|escalate):\s([\s\S]*)$/.exec(first);
2135
+ const requestType = head !== null ? kindOf(head[1]) : "info";
2136
+ const messageParts = [head !== null && head[2] !== void 0 ? head[2] : first];
2137
+ let needs;
2138
+ let expecting;
2139
+ const lists = {};
2140
+ for (const line of body.slice(1)) {
2141
+ const separator = line.indexOf(": ");
2142
+ const key = separator < 0 ? "" : line.slice(0, separator);
2143
+ const value = separator < 0 ? "" : line.slice(separator + 2).trim();
2144
+ const listKey = (key === "artifacts" || key === "evidence" || key === "missing") && value.startsWith("[") && value.endsWith("]") ? key : void 0;
2145
+ if (key === "needs" && value.length > 0) needs = value;
2146
+ else if (key === "expecting" && value.length > 0) expecting = value;
2147
+ else if (listKey !== void 0) lists[listKey] = bracketedListOf(value);
2148
+ else messageParts.push(line);
2149
+ }
2150
+ const message = messageParts.join("\n").trim();
2151
+ if (message.length === 0) return null;
2152
+ return {
2153
+ requestType,
2154
+ message,
2155
+ needs,
2156
+ expecting,
2157
+ artifacts: lists["artifacts"],
2158
+ evidence: lists["evidence"],
2159
+ missing: lists["missing"]
2160
+ };
2161
+ }
2162
+ /** Build the transcript notice. `name` is absent on replay. */
2163
+ function renderChildRequestNotice(fields, name) {
2164
+ const type = t(TYPE_KEY[fields.requestType]);
2165
+ const title = name === void 0 ? t("transcript.child_request_anon", { type }) : t("transcript.child_request", {
2166
+ name,
2167
+ type
2168
+ });
2169
+ const parts = [previewText(fields.message)];
2170
+ if (fields.needs !== void 0) parts.push(t("transcript.child_request_needs", { value: previewText(fields.needs) }));
2171
+ if (fields.expecting !== void 0) parts.push(t("transcript.child_request_expecting", { value: previewText(fields.expecting) }));
2172
+ for (const entry of [
2173
+ ["artifacts", fields.artifacts],
2174
+ ["evidence", fields.evidence],
2175
+ ["missing", fields.missing]
2176
+ ]) {
2177
+ const preview = previewedList(entry[0], entry[1]);
2178
+ if (preview !== void 0) parts.push(preview);
2179
+ }
2180
+ return {
2181
+ title,
2182
+ detail: parts.join(" · ")
2183
+ };
2184
+ }
2185
+ //#endregion
2028
2186
  //#region src/tui/utils/subagent-slots.ts
2029
2187
  /** Default subagent types, in the fixed display order of the slots. */
2030
2188
  const DEFAULT_SUBAGENT_TYPES = [
@@ -2037,6 +2195,10 @@ const DEFAULT_SUBAGENT_TYPES = [
2037
2195
  "worker",
2038
2196
  "writer"
2039
2197
  ];
2198
+ /** How long a `requesting` overlay stays visible after a subagent asks the
2199
+ * parent for help. The request itself returns at once and the agent keeps
2200
+ * working, so this is a display window, not a blocked state. */
2201
+ const REQUESTING_WINDOW_MS = 5e3;
2040
2202
  var SubagentSlots = class {
2041
2203
  slots = /* @__PURE__ */ new Map();
2042
2204
  instances = /* @__PURE__ */ new Map();
@@ -2050,6 +2212,10 @@ var SubagentSlots = class {
2050
2212
  * the first open window; concurrent windows on the same type keep counting
2051
2213
  * and the slot only exits messaging when the last window closes. */
2052
2214
  messagingPrev = /* @__PURE__ */ new Map();
2215
+ /** Slot type → open `requesting` overlay. Keyed by type so several live
2216
+ * instances of one type share a single marker; the real activity state stays
2217
+ * untouched in the slot and shows back up once the overlay expires. */
2218
+ requesting = /* @__PURE__ */ new Map();
2053
2219
  /** Count of open messaging windows per type (handles same-type siblings). */
2054
2220
  messagingCount = /* @__PURE__ */ new Map();
2055
2221
  reset() {
@@ -2059,6 +2225,7 @@ var SubagentSlots = class {
2059
2225
  this.messagingByCall.clear();
2060
2226
  this.messagingPrev.clear();
2061
2227
  this.messagingCount.clear();
2228
+ this.requesting.clear();
2062
2229
  }
2063
2230
  /**
2064
2231
  * subagent.spawned. A first spawn starts the slot as working; a second
@@ -2098,6 +2265,7 @@ var SubagentSlots = class {
2098
2265
  onTerminated(agentId) {
2099
2266
  const instance = this.instances.get(agentId);
2100
2267
  if (instance === void 0) return;
2268
+ if (this.requesting.get(instance.type)?.agentId === agentId) this.requesting.delete(instance.type);
2101
2269
  const remaining = (this.runningByType.get(instance.type) ?? 1) - 1;
2102
2270
  if (remaining > 0) {
2103
2271
  this.runningByType.set(instance.type, remaining);
@@ -2126,6 +2294,21 @@ var SubagentSlots = class {
2126
2294
  }
2127
2295
  this.setState(instance.type, kind === "tool" ? "working" : "outputting", agentId, detail);
2128
2296
  }
2297
+ /** A subagent asked the parent for help. The request returns immediately and
2298
+ * the agent keeps working, so this only raises a transient overlay — the real
2299
+ * activity state stays in the slot and shows back up when it expires. While
2300
+ * `messaging` is on screen the overlay loses (see visibleSlot). */
2301
+ onRequesting(agentId, at = Date.now()) {
2302
+ const instance = this.instances.get(agentId);
2303
+ if (instance === void 0) return;
2304
+ this.requesting.set(instance.type, {
2305
+ until: at + REQUESTING_WINDOW_MS,
2306
+ agentId
2307
+ });
2308
+ const slot = this.ensureSlot(instance.type);
2309
+ slot.agentId = agentId;
2310
+ slot.lastActivityAt = at;
2311
+ }
2129
2312
  /**
2130
2313
  * Main-side SendSubagentMessage tool call started: the target subagent's
2131
2314
  * slot shows messaging until the tool call returns.
@@ -2135,6 +2318,7 @@ var SubagentSlots = class {
2135
2318
  const instance = this.instances.get(targetAgentId);
2136
2319
  if (instance === void 0) return;
2137
2320
  this.messagingByCall.set(toolCallId, targetAgentId);
2321
+ this.requesting.delete(instance.type);
2138
2322
  const slot = this.ensureSlot(instance.type);
2139
2323
  if (slot.status !== "messaging") this.messagingPrev.set(instance.type, slot.status);
2140
2324
  this.messagingCount.set(instance.type, (this.messagingCount.get(instance.type) ?? 0) + 1);
@@ -2166,24 +2350,31 @@ var SubagentSlots = class {
2166
2350
  * All slots in fixed display order: the eight default types first (always
2167
2351
  * present, idle when never used), then extra types in spawn order.
2168
2352
  */
2169
- getSlots() {
2353
+ getSlots(at = Date.now()) {
2354
+ this.dropExpiredOverlays(at);
2170
2355
  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
- }
2356
+ for (const type of DEFAULT_SUBAGENT_TYPES) ordered.push(this.visibleSlot(type));
2178
2357
  for (const slot of this.slots.values()) {
2179
2358
  if (DEFAULT_SUBAGENT_TYPES.includes(slot.type)) continue;
2180
- ordered.push({
2181
- ...slot,
2182
- count: this.runningByType.get(slot.type) ?? 0
2183
- });
2359
+ ordered.push(this.visibleSlot(slot.type));
2184
2360
  }
2185
2361
  return ordered;
2186
2362
  }
2363
+ /** Display view of one slot: the real state with an unexpired `requesting`
2364
+ * overlay on top. Read-path only — it never mutates. Callers must run
2365
+ * {@link dropExpiredOverlays} first, and messaging clears the overlay in
2366
+ * {@link onMessagingStart}; the `messaging` guard only covers same-tick ordering. */
2367
+ visibleSlot(type) {
2368
+ const copy = {
2369
+ ...this.ensureSlot(type),
2370
+ count: this.runningByType.get(type) ?? 0
2371
+ };
2372
+ if (this.requesting.has(type) && copy.status !== "messaging") copy.status = "requesting";
2373
+ return copy;
2374
+ }
2375
+ dropExpiredOverlays(at) {
2376
+ for (const [type, overlay] of this.requesting) if (at >= overlay.until) this.requesting.delete(type);
2377
+ }
2187
2378
  ensureSlot(type) {
2188
2379
  let slot = this.slots.get(type);
2189
2380
  if (slot === void 0) {
@@ -2580,6 +2771,15 @@ var SessionEventHandler = class {
2580
2771
  subagentSlots = new SubagentSlots();
2581
2772
  /** Tool callId of the in-flight UpdateGoal(complete) (judging), if any. */
2582
2773
  goalJudgeCallId;
2774
+ /** Rolling first-token latencies of real requests, feeding the sidebar Hub
2775
+ * provider row. In-process only, and dropped whenever the model changes —
2776
+ * a different provider is a different network path. */
2777
+ providerLatencySamples = [];
2778
+ providerLatencyModel;
2779
+ providerLatencyAt;
2780
+ /** Enough samples to smooth one slow request, few enough that a recovered
2781
+ * endpoint reads as recovered again quickly. */
2782
+ providerLatencyWindow = 3;
2583
2783
  renderedSkillActivationIds = /* @__PURE__ */ new Set();
2584
2784
  renderedMcpServerStatusKeys = /* @__PURE__ */ new Map();
2585
2785
  mcpServerStatusSpinners = /* @__PURE__ */ new Map();
@@ -2595,6 +2795,9 @@ var SessionEventHandler = class {
2595
2795
  this.pendingSkillCandidates = [];
2596
2796
  this.promptedSkillCandidates.clear();
2597
2797
  this.goalJudgeCallId = void 0;
2798
+ this.providerLatencySamples = [];
2799
+ this.providerLatencyModel = void 0;
2800
+ this.providerLatencyAt = void 0;
2598
2801
  this.subagentUsageSeen.clear();
2599
2802
  this.host.setAppState({
2600
2803
  subagentUsage: {},
@@ -2768,6 +2971,41 @@ var SessionEventHandler = class {
2768
2971
  getSubagentSlots() {
2769
2972
  return this.subagentSlots.getSlots();
2770
2973
  }
2974
+ /** Keep the last few first-token latencies, per provider. Steps without a
2975
+ * measured latency (no stream, cached turn) are ignored rather than counted
2976
+ * as a zero. */
2977
+ recordProviderLatency(event) {
2978
+ const latency = event.llmFirstTokenLatencyMs;
2979
+ if (latency === void 0) return;
2980
+ const model = this.host.state.appState.model;
2981
+ if (this.providerLatencyModel !== void 0 && model !== this.providerLatencyModel) this.providerLatencySamples = [];
2982
+ this.providerLatencyModel = model;
2983
+ this.providerLatencySamples.push(latency);
2984
+ if (this.providerLatencySamples.length > this.providerLatencyWindow) this.providerLatencySamples.shift();
2985
+ this.providerLatencyAt = Date.now();
2986
+ }
2987
+ /** Sidebar Hub: average measured response time for the provider in use. The
2988
+ * caller owns tone and staleness — this stays a plain reading. */
2989
+ getProviderLatency() {
2990
+ if (this.providerLatencySamples.length === 0) return {
2991
+ ms: void 0,
2992
+ sampledAt: void 0
2993
+ };
2994
+ if (this.host.state.appState.model !== this.providerLatencyModel) {
2995
+ this.providerLatencySamples = [];
2996
+ this.providerLatencyModel = void 0;
2997
+ this.providerLatencyAt = void 0;
2998
+ return {
2999
+ ms: void 0,
3000
+ sampledAt: void 0
3001
+ };
3002
+ }
3003
+ const total = this.providerLatencySamples.reduce((sum, value) => sum + value, 0);
3004
+ return {
3005
+ ms: Math.round(total / this.providerLatencySamples.length),
3006
+ sampledAt: this.providerLatencyAt
3007
+ };
3008
+ }
2771
3009
  /** Feed routed subagent activity into the slot state machine. */
2772
3010
  feedSubagentSlots(event, agentId) {
2773
3011
  switch (event.type) {
@@ -2782,7 +3020,10 @@ var SessionEventHandler = class {
2782
3020
  this.subagentSlots.onTerminated(event.subagentId);
2783
3021
  break;
2784
3022
  case "tool.call.started":
2785
- this.subagentSlots.onActivity(agentId, "tool", `tool: ${event.name}`);
3023
+ if (event.name === "ContactParent") {
3024
+ this.subagentSlots.onRequesting(agentId);
3025
+ this.appendChildRequestNotice(agentId, event.args);
3026
+ } else this.subagentSlots.onActivity(agentId, "tool", `tool: ${event.name}`);
2786
3027
  break;
2787
3028
  case "tool.call.delta": break;
2788
3029
  case "tool.result":
@@ -2795,6 +3036,26 @@ var SessionEventHandler = class {
2795
3036
  default: break;
2796
3037
  }
2797
3038
  }
3039
+ /**
3040
+ * Surface a child→parent collaboration request in the transcript the moment
3041
+ * the subagent asks, so the human sees it without expanding the agent card.
3042
+ * The request always targets the main agent (one subagent host per session),
3043
+ * which is why a top-level transcript entry is the right home for it.
3044
+ */
3045
+ appendChildRequestNotice(agentId, args) {
3046
+ const fields = childRequestFieldsFromArgs(argsRecord(args));
3047
+ if (fields === null) return;
3048
+ const notice = renderChildRequestNotice(fields, this.subagentInfo.get(agentId)?.name);
3049
+ this.host.appendTranscriptEntry({
3050
+ id: nextTranscriptId(),
3051
+ kind: "status",
3052
+ turnId: this.host.streamingUI.getTurnContext().turnId,
3053
+ renderMode: "notice",
3054
+ content: notice.title,
3055
+ detail: notice.detail,
3056
+ noticeMarkerColor: this.host.state.theme.colors.warning
3057
+ });
3058
+ }
2798
3059
  routeSubagentEvent(event) {
2799
3060
  const subagentId = event.agentId;
2800
3061
  if (subagentId === "main") return false;
@@ -2912,6 +3173,7 @@ var SessionEventHandler = class {
2912
3173
  handleStepCompleted(event) {
2913
3174
  this.host.streamingUI.flushNow();
2914
3175
  this.maybeShowDebugTiming(event);
3176
+ this.recordProviderLatency(event);
2915
3177
  this.drainQueuedMessagesIntoSteer();
2916
3178
  const prev = this.host.state.appState.sessionUsage;
2917
3179
  const apiCalls = this.host.state.appState.sessionApiCalls + 1;
@@ -4018,6 +4280,10 @@ var SessionReplayRenderer = class {
4018
4280
  this.renderCronMissed(context, message.origin);
4019
4281
  return;
4020
4282
  }
4283
+ if (message.origin?.kind === "system_trigger") {
4284
+ this.renderSystemTrigger(context, message);
4285
+ return;
4286
+ }
4021
4287
  this.flushAssistant(context);
4022
4288
  const skill = skillActivationFromOrigin(message.origin);
4023
4289
  if (skill !== void 0) {
@@ -4028,6 +4294,26 @@ var SessionReplayRenderer = class {
4028
4294
  this.advanceTurn(context);
4029
4295
  this.host.appendTranscriptEntry(replayEntry(context, "user", contentPartsToText(message.content), "plain"));
4030
4296
  }
4297
+ /**
4298
+ * `system_trigger` messages are internal injections, not user speech: the
4299
+ * live transcript never prints them and markdown export filters them out, so
4300
+ * replay must not surface them as a user row either. The single exception is
4301
+ * a child→parent collaboration request, which gets the same notice the live
4302
+ * path shows. Replay has no spawn events, so the subagent's display name is
4303
+ * gone and the name-less title variant keeps the notice honest.
4304
+ */
4305
+ renderSystemTrigger(context, message) {
4306
+ const origin = message.origin;
4307
+ if (origin?.kind !== "system_trigger" || origin.name !== "child_request") return;
4308
+ const fields = childRequestFieldsFromNotification(contentPartsToText(message.content));
4309
+ if (fields === null) return;
4310
+ const notice = renderChildRequestNotice(fields);
4311
+ this.flushAssistant(context);
4312
+ this.host.appendTranscriptEntry({
4313
+ ...replayEntry(context, "status", notice.title, "notice", { detail: notice.detail }),
4314
+ noticeMarkerColor: this.host.state.theme.colors.warning
4315
+ });
4316
+ }
4031
4317
  renderToolCalls(context, toolCalls) {
4032
4318
  if (toolCalls.length === 0) return;
4033
4319
  const { streamingUI } = this.host;
@@ -6267,7 +6553,7 @@ var CommittedMessageComponent = class {
6267
6553
  return new Text(` ${chalk.hex(colors.textDim)(t("transcript.tool_name", { name }))}${summary}`, 0, 0).render(width);
6268
6554
  }
6269
6555
  case "status":
6270
- if (entry.renderMode === "notice") return new NoticeMessageComponent(entry.content, entry.detail, colors).render(width);
6556
+ if (entry.renderMode === "notice") return new NoticeMessageComponent(entry.content, entry.detail, colors, entry.noticeMarkerColor).render(width);
6271
6557
  return new StatusMessageComponent(entry.content, colors, entry.color).render(width);
6272
6558
  case "skill_activation": {
6273
6559
  const text = entry.skillName ?? entry.content;
@@ -6416,10 +6702,10 @@ var TranscriptController = class TranscriptController {
6416
6702
  return tc;
6417
6703
  }
6418
6704
  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);
6705
+ 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
6706
  case "status":
6421
6707
  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);
6708
+ 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
6709
  case "cron":
6424
6710
  if (entry.cronData === void 0) return null;
6425
6711
  return new CronMessageComponent(entry.content, entry.cronData, state.theme.colors);
@@ -6668,6 +6954,55 @@ function clampSidebarWidth(cols) {
6668
6954
  return Math.min(60, Math.max(24, Math.round(cols)));
6669
6955
  }
6670
6956
  //#endregion
6957
+ //#region src/tui/utils/display-width.ts
6958
+ /** Display-column width of a string, matching the terminal layout engine
6959
+ * (ANSI sequences stripped, CJK/wide chars counted as 2 columns). */
6960
+ function displayWidth(text) {
6961
+ return visibleWidth(text);
6962
+ }
6963
+ /** Pad `label` to `target` display columns with spaces (CJK-aware). */
6964
+ function padLabel(label, target) {
6965
+ return label + " ".repeat(Math.max(0, target - displayWidth(label)));
6966
+ }
6967
+ function layoutSidebarGrid(rows, width) {
6968
+ const valueCols = Math.max(0, ...rows.map((row) => displayWidth(row.value ?? "")));
6969
+ const labelBudget = Math.max(1, width - 2 - Math.max(valueCols, 1) - 1);
6970
+ const labelCols = Math.min(12, labelBudget);
6971
+ const valueStart = Math.max(2 + labelCols + 1, width - valueCols);
6972
+ return rows.map((row) => {
6973
+ const marker = padLabel(row.marker ?? "", 2);
6974
+ const label = truncateToWidth(row.label, labelCols);
6975
+ const used = 2 + displayWidth(label);
6976
+ if (row.value !== void 0) {
6977
+ const gap = Math.max(1, width - used - displayWidth(row.value));
6978
+ const room = Math.max(1, width - used - gap);
6979
+ return {
6980
+ marker,
6981
+ label,
6982
+ gap,
6983
+ value: truncateToWidth(row.value, room)
6984
+ };
6985
+ }
6986
+ if (row.note !== void 0) {
6987
+ const noteCols = displayWidth(row.note);
6988
+ const aligned = Math.max(1, valueStart - used);
6989
+ const fits = Math.max(1, width - used - noteCols);
6990
+ const gap = Math.min(aligned, fits);
6991
+ return {
6992
+ marker,
6993
+ label,
6994
+ gap,
6995
+ note: truncateToWidth(row.note, Math.max(1, width - used - gap))
6996
+ };
6997
+ }
6998
+ return {
6999
+ marker,
7000
+ label,
7001
+ gap: 1
7002
+ };
7003
+ });
7004
+ }
7005
+ //#endregion
6671
7006
  //#region src/tui/components/sidebar/panels/git-panel.ts
6672
7007
  /**
6673
7008
  * Summary-only Git panel (摘要式): the workdir name plus added (green) /
@@ -6683,12 +7018,13 @@ var GitPanelContent = class {
6683
7018
  }
6684
7019
  invalidate() {}
6685
7020
  render(width) {
7021
+ const indent = " ".repeat(2);
6686
7022
  const git = this.ctx.getData().git;
6687
- if (git === void 0) return [chalk.hex(this.colors.textDim)(t("sidebar.git_no_repo"))];
7023
+ if (git === void 0) return [`${indent}${chalk.hex(this.colors.textDim)(t("sidebar.git_no_repo"))}`];
6688
7024
  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)];
7025
+ const workDir = truncateToWidth(`${indent}${chalk.hex(this.colors.text)(basename(git.workDir))}`, width);
7026
+ if (git.diffAdded === 0 && git.diffDeleted === 0 && git.filesCount === 0) return [workDir, dim(`${indent}${t("sidebar.git_clean")}`)];
7027
+ 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
7028
  }
6693
7029
  };
6694
7030
  const gitPanel = {
@@ -6702,35 +7038,6 @@ const gitPanel = {
6702
7038
  }
6703
7039
  };
6704
7040
  //#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
7041
  //#region src/tui/components/sidebar/panels/session-panel.ts
6735
7042
  function formatDuration(ms) {
6736
7043
  const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
@@ -6766,10 +7073,10 @@ function formatCompact(n) {
6766
7073
  if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
6767
7074
  return String(n);
6768
7075
  }
6769
- /** True when a two-column grid fits `width` display columns. */
7076
+ /** True when the shared grid can show these values at full width: the label
7077
+ * column is fixed across panels, so only the value column decides the fit. */
6770
7078
  function gridFits(rows, width) {
6771
- const grid = alignMetricRows(rows);
6772
- return grid.labelCols + 2 + grid.valueCols <= width;
7079
+ return 15 + Math.max(0, ...rows.map(([, value]) => displayWidth(value))) <= width;
6773
7080
  }
6774
7081
  /**
6775
7082
  * Session summary, live-rendered each frame as a strict two-column table:
@@ -6806,10 +7113,11 @@ var SessionPanelContent = class {
6806
7113
  ];
6807
7114
  const withTokens = (compact) => [...tokenRows.map(([name, n]) => [name, compact ? formatCompact(n) : formatCount$1(n)]), ...countRows];
6808
7115
  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));
7116
+ return layoutSidebarGrid((gridFits(full, width) ? full : withTokens(true)).map(([name, reading]) => ({
7117
+ marker: "●",
7118
+ label: name,
7119
+ value: reading
7120
+ })), width).map((cell) => `${label(cell.marker)}${label(cell.label)}${" ".repeat(cell.gap)}${value(cell.value ?? "")}`);
6813
7121
  }
6814
7122
  };
6815
7123
  const sessionPanel = {
@@ -6836,6 +7144,7 @@ function statusText(status) {
6836
7144
  case "outputting": return t("sidebar.agent_outputting");
6837
7145
  case "messaging": return t("sidebar.agent_messaging");
6838
7146
  case "reworking": return t("sidebar.agent_reworking");
7147
+ case "requesting": return t("sidebar.agent_requesting");
6839
7148
  }
6840
7149
  }
6841
7150
  /**
@@ -6854,17 +7163,22 @@ var AgentsPanelContent = class {
6854
7163
  render(width) {
6855
7164
  const agents = this.ctx.getData().agents;
6856
7165
  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;
7166
+ return layoutSidebarGrid(agents.map((slot) => ({
7167
+ marker: slot.status === "idle" ? "○" : "●",
7168
+ label: slot.type,
7169
+ value: statusText(slot.status) + (slot.count > 1 ? ` ×${slot.count}` : "")
7170
+ })), width).map((cell, index) => this.renderSlot(cell, agents[index], index, width));
6861
7171
  }
6862
- renderSlot(slot, index, nameWidth, width) {
7172
+ renderSlot(cell, slot, index, width) {
6863
7173
  const dim = (s) => chalk.hex(this.colors.textDim)(s);
6864
- if (slot.status === "idle") return `${dim("○")} ${dim(padLabel(slot.type, nameWidth))} ${dim(statusText("idle"))}`;
7174
+ const idle = slot.status === "idle";
6865
7175
  const phase = (Date.now() % AGENT_GRADIENT_CYCLE_MS / AGENT_GRADIENT_CYCLE_MS + index * .125) % 1;
6866
7176
  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);
7177
+ const marker = padLabel((active ?? chalk.hex(this.statusColor(slot.status)))(idle ? "" : ""), 2);
7178
+ const label = (idle ? dim : chalk.hex(this.colors.textStrong))(cell.label);
7179
+ const paint = active ?? (idle ? dim : slot.status === "requesting" ? chalk.hex(this.colors.warning) : chalk.hex(this.colors.text));
7180
+ const count = slot.count > 1 ? dim(` ×${slot.count}`) : "";
7181
+ return truncateToWidth(`${marker}${label}${" ".repeat(cell.gap)}${paint(statusText(slot.status))}${count}`, width);
6868
7182
  }
6869
7183
  statusColor(status) {
6870
7184
  switch (status) {
@@ -6872,6 +7186,7 @@ var AgentsPanelContent = class {
6872
7186
  case "outputting": return this.colors.primary;
6873
7187
  case "messaging": return this.colors.accent;
6874
7188
  case "reworking": return this.colors.warning;
7189
+ case "requesting": return this.colors.warning;
6875
7190
  case "idle": return this.colors.textDim;
6876
7191
  }
6877
7192
  }
@@ -6929,26 +7244,48 @@ var GoalPanelContent = class {
6929
7244
  const wallMs = goal.status === "active" ? goal.wallClockMs + Math.max(0, now - goal.wallClockBaseAt) : goal.wallClockMs;
6930
7245
  const lines = [];
6931
7246
  lines.push(this.renderBadge(goal.status, goal.judge, dim));
6932
- lines.push(truncateToWidth(dim(goal.objective), width));
7247
+ lines.push(truncateToWidth(dim(`● ${goal.objective}`), width));
6933
7248
  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)]
7249
+ {
7250
+ marker: "",
7251
+ label: t("sidebar.goal_tokens"),
7252
+ value: formatCount(goal.tokensUsed)
7253
+ },
7254
+ {
7255
+ marker: "●",
7256
+ label: t("sidebar.goal_tokens_input"),
7257
+ value: goal.inputTokens === null ? "—" : formatCount(goal.inputTokens)
7258
+ },
7259
+ {
7260
+ marker: "●",
7261
+ label: t("sidebar.goal_tokens_output"),
7262
+ value: goal.outputTokens === null ? "—" : formatCount(goal.outputTokens)
7263
+ },
7264
+ {
7265
+ marker: "●",
7266
+ label: t("sidebar.goal_turns"),
7267
+ value: String(goal.turnsUsed)
7268
+ },
7269
+ {
7270
+ marker: "●",
7271
+ label: t("sidebar.goal_wall"),
7272
+ value: formatWallClock(wallMs)
7273
+ },
7274
+ {
7275
+ marker: "●",
7276
+ label: t("sidebar.goal_judge"),
7277
+ value: judgeText(goal.judge)
7278
+ }
6940
7279
  ];
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));
7280
+ const gridRows = goal.completionCriterion !== null ? [...rows, {
7281
+ marker: "",
7282
+ label: t("sidebar.goal_criterion"),
7283
+ note: goal.completionCriterion ?? ""
7284
+ }] : rows;
7285
+ for (const cell of layoutSidebarGrid(gridRows, width)) {
7286
+ const reading = cell.value ?? cell.note ?? "";
7287
+ lines.push(`${dim(`${cell.marker}${cell.label}`)}${" ".repeat(cell.gap)}${text(reading)}`);
7288
+ }
6952
7289
  return lines;
6953
7290
  }
6954
7291
  /** State badge: judging wins over everything; otherwise map the snapshot
@@ -6974,6 +7311,212 @@ const goalPanel = {
6974
7311
  return new GoalPanelContent(ctx, ctx.colors);
6975
7312
  }
6976
7313
  };
7314
+ /** Per-request budget: long enough for a slow CDN, short enough to never hang. */
7315
+ const HUB_PROBE_TIMEOUT_MS = 1500;
7316
+ /** Row id for the model provider, whose latency is measured, never probed. */
7317
+ const HUB_MODEL_ROW_ID = "model";
7318
+ /** A measured provider reading older than this is dimmed: nothing was asked lately. */
7319
+ const HUB_MODEL_STALE_MS = 5 * 6e4;
7320
+ /** Default probe targets: the endpoints day-to-day development tends to need. */
7321
+ const HUB_ENDPOINTS = Object.freeze([
7322
+ {
7323
+ id: "github",
7324
+ url: "https://github.com/"
7325
+ },
7326
+ {
7327
+ id: "google",
7328
+ url: "https://www.google.com/"
7329
+ },
7330
+ {
7331
+ id: "x",
7332
+ url: "https://x.com/",
7333
+ label: "x (twitter)"
7334
+ },
7335
+ {
7336
+ id: "huggingface",
7337
+ url: "https://huggingface.co/"
7338
+ },
7339
+ {
7340
+ id: "alibaba",
7341
+ url: "https://www.aliyun.com/",
7342
+ label: "Alibaba"
7343
+ },
7344
+ {
7345
+ id: "tencent",
7346
+ url: "https://cloud.tencent.com/",
7347
+ label: "Tencent"
7348
+ }
7349
+ ]);
7350
+ function defaultFetch(url, init) {
7351
+ return globalThis.fetch(url, init);
7352
+ }
7353
+ function createHubProbe(options = {}) {
7354
+ const endpoints = options.endpoints ?? HUB_ENDPOINTS;
7355
+ const now = options.now ?? Date.now;
7356
+ const fetchImpl = options.fetchImpl ?? defaultFetch;
7357
+ const sites = /* @__PURE__ */ new Map();
7358
+ for (const endpoint of endpoints) sites.set(endpoint.id, {
7359
+ ms: void 0,
7360
+ failures: 0
7361
+ });
7362
+ let roundAt;
7363
+ let inFlight = false;
7364
+ const runRound = async () => {
7365
+ const startedAt = now();
7366
+ const results = await Promise.allSettled(endpoints.map(async (endpoint) => {
7367
+ const response = await fetchImpl(endpoint.url, {
7368
+ method: "HEAD",
7369
+ redirect: "manual",
7370
+ signal: AbortSignal.timeout(HUB_PROBE_TIMEOUT_MS)
7371
+ });
7372
+ return {
7373
+ id: endpoint.id,
7374
+ ms: Math.max(0, now() - startedAt),
7375
+ status: response.status
7376
+ };
7377
+ }));
7378
+ for (const [index, result] of results.entries()) {
7379
+ const site = sites.get(endpoints[index].id);
7380
+ if (site === void 0) continue;
7381
+ if (result.status === "fulfilled") {
7382
+ site.ms = result.value.ms;
7383
+ site.failures = 0;
7384
+ } else {
7385
+ site.failures += 1;
7386
+ if (site.failures >= 2) site.ms = void 0;
7387
+ }
7388
+ }
7389
+ roundAt = now();
7390
+ inFlight = false;
7391
+ };
7392
+ return {
7393
+ sampleIfStale: () => {
7394
+ if (inFlight) return;
7395
+ const at = now();
7396
+ if (roundAt !== void 0 && at - roundAt < 3e4) return;
7397
+ inFlight = true;
7398
+ runRound().catch(() => {
7399
+ inFlight = false;
7400
+ });
7401
+ },
7402
+ snapshot: () => {
7403
+ const at = now();
7404
+ const stale = roundAt === void 0 || at - roundAt >= 3e4 * 2;
7405
+ const samples = [];
7406
+ for (const endpoint of endpoints) {
7407
+ const site = sites.get(endpoint.id);
7408
+ samples.push({
7409
+ id: endpoint.id,
7410
+ label: endpoint.label ?? endpoint.id,
7411
+ ms: site.ms,
7412
+ tone: toneFor(site, stale)
7413
+ });
7414
+ }
7415
+ return {
7416
+ samples,
7417
+ roundAt,
7418
+ stale,
7419
+ pending: inFlight
7420
+ };
7421
+ }
7422
+ };
7423
+ }
7424
+ function toneFor(site, stale) {
7425
+ if (stale) return "dim";
7426
+ if (site.failures >= 2) return "down";
7427
+ return hubLatencyTone(site.ms, site.failures > 0);
7428
+ }
7429
+ /**
7430
+ * Merge the measured provider reading with the probe samples into Hub rows.
7431
+ * The provider row always leads (it answers "is the model I'm using reachable"),
7432
+ * and it staleness-checks against its own budget: a long idle stretch means
7433
+ * nothing was asked, not that the network died.
7434
+ */
7435
+ function buildHubSamples(snapshot, provider, now = Date.now()) {
7436
+ const ageMs = provider.sampledAt === void 0 ? 0 : now - provider.sampledAt;
7437
+ return [{
7438
+ id: HUB_MODEL_ROW_ID,
7439
+ ms: provider.ms,
7440
+ tone: hubLatencyTone(provider.ms, ageMs >= HUB_MODEL_STALE_MS)
7441
+ }, ...snapshot.samples];
7442
+ }
7443
+ /** Shared tone rule for every Hub row (probe sites and the measured provider). */
7444
+ function hubLatencyTone(ms, stale = false) {
7445
+ if (ms === void 0 || stale) return "dim";
7446
+ return ms <= 100 ? "ok" : "warn";
7447
+ }
7448
+ /**
7449
+ * Compact latency reading: whole milliseconds below a second, one-decimal
7450
+ * seconds above — a probe that never answers must never read as `65400ms`.
7451
+ */
7452
+ function formatHubLatency(ms) {
7453
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
7454
+ return `${(ms / 1e3).toFixed(1)}s`;
7455
+ }
7456
+ //#endregion
7457
+ //#region src/tui/components/sidebar/panels/hub-panel.ts
7458
+ /** Pending marker: three middle dots (Latin-1, so terminal width is stable). */
7459
+ const PENDING_MARK = "···";
7460
+ /** No reading at all — a site that stopped answering, or nothing measured yet. */
7461
+ const EMPTY_MARK = "--";
7462
+ /**
7463
+ * Hub panel: network health at a glance. One row per reading — the measured
7464
+ * model-provider response time first, then a probe round trip for each
7465
+ * well-known endpoint.
7466
+ *
7467
+ * Values share one right edge through the same two-column engine the Session
7468
+ * panel uses, so rows never jitter as numbers change width, and only the value
7469
+ * carries colour (green ≤100ms, amber above it, red when a site stopped
7470
+ * answering). The panel is a pure view: probing happens in the data provider,
7471
+ * which is what keeps a slow endpoint from ever touching a frame.
7472
+ */
7473
+ var HubPanelContent = class {
7474
+ ctx;
7475
+ colors;
7476
+ constructor(ctx, colors) {
7477
+ this.ctx = ctx;
7478
+ this.colors = colors;
7479
+ }
7480
+ invalidate() {}
7481
+ render(width) {
7482
+ const hub = this.ctx.getData().hub;
7483
+ if (hub === void 0 || hub.samples.length === 0) return [];
7484
+ return layoutSidebarGrid(hub.samples.map((sample) => ({
7485
+ marker: "●",
7486
+ label: this.labelOf(sample),
7487
+ value: this.reading(sample, hub)
7488
+ })), width).map((cell, index) => {
7489
+ const tone = hub.samples[index]?.tone ?? "dim";
7490
+ return `${chalk.hex(this.colors.textDim)(`${cell.marker}${cell.label}`)}${" ".repeat(cell.gap)}${chalk.hex(this.toneColor(tone))(cell.value ?? "")}`;
7491
+ });
7492
+ }
7493
+ labelOf(sample) {
7494
+ return sample.id === "model" ? t("sidebar.hub_model") : sample.label ?? sample.id;
7495
+ }
7496
+ reading(sample, hub) {
7497
+ if (sample.ms !== void 0) return formatHubLatency(sample.ms);
7498
+ if (hub.pending && sample.tone === "dim" && sample.id !== "model") return PENDING_MARK;
7499
+ return EMPTY_MARK;
7500
+ }
7501
+ toneColor(tone) {
7502
+ switch (tone) {
7503
+ case "ok": return this.colors.success;
7504
+ case "warn": return this.colors.warning;
7505
+ case "down": return this.colors.error;
7506
+ case "dim": return this.colors.textDim;
7507
+ }
7508
+ }
7509
+ };
7510
+ const hubPanel = {
7511
+ id: "hub",
7512
+ get title() {
7513
+ return t("sidebar.hub");
7514
+ },
7515
+ width: 30,
7516
+ build(ctx) {
7517
+ return new HubPanelContent(ctx, ctx.colors);
7518
+ }
7519
+ };
6977
7520
  //#endregion
6978
7521
  //#region src/utils/git/git-status.ts
6979
7522
  /**
@@ -7533,6 +8076,9 @@ var LifecycleController = class LifecycleController {
7533
8076
  /** Sidebar-only git cache (the footer owns its own). Rebuilt when the
7534
8077
  * workDir changes; `getStatus()` is TTL-cached so polling is cheap. */
7535
8078
  sidebarGitCache;
8079
+ /** Sidebar Hub probe: read-through TTL, no timer — probing happens because a
8080
+ * frame asked, so a closed sidebar produces no traffic. */
8081
+ hubProbe = createHubProbe();
7536
8082
  sidebarGitCacheWorkDir = "";
7537
8083
  /** Session start time for the sidebar uptime counter. Reset on session
7538
8084
  * switch so a resumed old session doesn't show the TUI process age. */
@@ -7729,6 +8275,7 @@ var LifecycleController = class LifecycleController {
7729
8275
  if (!m.allPanels.some((p) => p.id === "session")) m.register(sessionPanel);
7730
8276
  if (!m.allPanels.some((p) => p.id === "agents")) m.register(agentsPanel);
7731
8277
  if (!m.allPanels.some((p) => p.id === "goal")) m.register(goalPanel);
8278
+ if (!m.allPanels.some((p) => p.id === "hub")) m.register(hubPanel);
7732
8279
  m.setDataProvider(() => this.buildSidebarData());
7733
8280
  }
7734
8281
  const sidebar = this.host.state.sidebarPane;
@@ -7770,9 +8317,25 @@ var LifecycleController = class LifecycleController {
7770
8317
  git: this.readSidebarGit(appState.workDir),
7771
8318
  sessionStats: this.readSidebarSessionStats(appState),
7772
8319
  agents: this.host.sessionEventHandler.getSubagentSlots(),
8320
+ hub: this.readSidebarHub(),
7773
8321
  goal: this.readSidebarGoal(appState)
7774
8322
  };
7775
8323
  }
8324
+ /**
8325
+ * Hub block. Probe rounds are kicked off from the render path only (the
8326
+ * sidebar container stops rendering while closed), and the round is always
8327
+ * fire-and-forget — a dead endpoint can never hold up a frame. The model row
8328
+ * is different: it reads the first-token latency of real requests, so it is
8329
+ * measured rather than probed, and it goes stale on its own budget.
8330
+ */
8331
+ readSidebarHub() {
8332
+ if (this.host.state.sidebarManager?.isOpen === true) this.hubProbe.sampleIfStale();
8333
+ const snapshot = this.hubProbe.snapshot();
8334
+ return {
8335
+ samples: buildHubSamples(snapshot, this.host.sessionEventHandler.getProviderLatency()),
8336
+ pending: snapshot.pending
8337
+ };
8338
+ }
7776
8339
  readSidebarGit(workDir) {
7777
8340
  if (this.sidebarGitCache === void 0 || this.sidebarGitCacheWorkDir !== workDir) {
7778
8341
  this.sidebarGitCache = createGitStatusCache(workDir);
@@ -13632,7 +14195,7 @@ var ScreamTUI = class {
13632
14195
  const card = new OnboardingCardComponent({
13633
14196
  colors: this.state.theme.colors,
13634
14197
  onConfigure: () => {
13635
- import("./dispatch-CxGKRLcZ.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14198
+ import("./dispatch-B6jbxY0D.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
13636
14199
  this.restoreEditor();
13637
14200
  onDone?.();
13638
14201
  });
@@ -14459,15 +15022,13 @@ async function runChannelSetup() {
14459
15022
  //#endregion
14460
15023
  //#region src/cli/run-stream-json.ts
14461
15024
  /**
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.
15025
+ * Stream-json stdio adapter (cc-connect compat).
14468
15026
  *
14469
- * Protocol reference:
14470
- * https://docs.anthropic.com/en/docs/claude-code/stdio-stream-json
15027
+ * Reads stdin as line-based stream-json messages, processes them through the
15028
+ * ScreamCode agent, and writes stdout as stream-json events. This is the
15029
+ * integration surface cc-connect drives when it spawns `scream stream-json`,
15030
+ * and any tool that speaks the same stdio protocol can use ScreamCode as an
15031
+ * agent backend through it.
14471
15032
  */
14472
15033
  function buildStreamJsonRuntimePrompt(input) {
14473
15034
  const appendParts = [
@@ -14480,7 +15041,7 @@ function buildStreamJsonRuntimePrompt(input) {
14480
15041
  append: appendParts.join("\n\n")
14481
15042
  };
14482
15043
  }
14483
- var ClaudeStreamJsonWriter = class {
15044
+ var StreamJsonWriter = class {
14484
15045
  writeLine;
14485
15046
  sessionId = "";
14486
15047
  msgCounter = 0;
@@ -14611,7 +15172,7 @@ var ClaudeStreamJsonWriter = class {
14611
15172
  output: 0
14612
15173
  };
14613
15174
  }
14614
- /** Emit a resume hint as a meta message (same format as Claude Code). */
15175
+ /** Emit a resume hint as a stream-json meta message. */
14615
15176
  emitResumeHint(sessionId) {
14616
15177
  this.writeJson({
14617
15178
  role: "meta",
@@ -14686,7 +15247,7 @@ async function* readStdinMessages() {
14686
15247
  else log.debug("stream-json: ignoring unknown stdin message type", { type: msg.type });
14687
15248
  }
14688
15249
  }
14689
- /** Extract plain text from a Claude Code-style user message. */
15250
+ /** Extract plain text from a stream-json user message. */
14690
15251
  function extractUserText(msg) {
14691
15252
  const content = msg.message.content;
14692
15253
  if (typeof content === "string") return content;
@@ -14727,7 +15288,7 @@ function mapCcConnectMode(mode) {
14727
15288
  }
14728
15289
  /**
14729
15290
  * Install the stdout EPIPE guard and return the line writer handed to
14730
- * {@link ClaudeStreamJsonWriter}.
15291
+ * {@link StreamJsonWriter}.
14731
15292
  *
14732
15293
  * cc-connect closes our stdout when it kills/restarts the agent (idle
14733
15294
  * timeout, /restart, config change). Writing after that raises EPIPE as an
@@ -14783,7 +15344,7 @@ async function runStreamJson(opts) {
14783
15344
  }
14784
15345
  const streamSubagentModels = streamTuiConfig.subagentModels;
14785
15346
  harness.setSubagentModelBindings(() => streamSubagentModels);
14786
- const writer = new ClaudeStreamJsonWriter(installStdoutEpipeGuard());
15347
+ const writer = new StreamJsonWriter(installStdoutEpipeGuard());
14787
15348
  let session;
14788
15349
  let currentSessionId;
14789
15350
  let sessionKey = "cc-connect-main";