zidane 6.6.4 → 7.0.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.
Files changed (157) hide show
  1. package/dist/{acp-BQqhozmt.js → acp-Wr8l4IDI.js} +7 -180
  2. package/dist/acp-Wr8l4IDI.js.map +1 -0
  3. package/dist/acp-cli.js +6 -6
  4. package/dist/acp.d.ts +1 -1
  5. package/dist/acp.d.ts.map +1 -1
  6. package/dist/acp.js +2 -1
  7. package/dist/{agent-DTxH7RHW.js → agent-C-camKQW.js} +162 -12
  8. package/dist/agent-C-camKQW.js.map +1 -0
  9. package/dist/agent.d.ts +1 -1
  10. package/dist/agent.js +1 -1
  11. package/dist/{anthropic-DRNRnd6W.js → anthropic-BDOGzhtj.js} +3 -3
  12. package/dist/{anthropic-DRNRnd6W.js.map → anthropic-BDOGzhtj.js.map} +1 -1
  13. package/dist/{transcript-anchors-BjH-68k2.js → approvals-CqqxE5Vh.js} +4924 -275
  14. package/dist/approvals-CqqxE5Vh.js.map +1 -0
  15. package/dist/{auth-7rv6ZfWk.js → auth-BdwDROYg.js} +12 -8
  16. package/dist/{auth-7rv6ZfWk.js.map → auth-BdwDROYg.js.map} +1 -1
  17. package/dist/chat/pure.d.ts +221 -4
  18. package/dist/chat/pure.d.ts.map +1 -0
  19. package/dist/chat/pure.js +4 -2
  20. package/dist/chat.d.ts +6 -2911
  21. package/dist/chat.js +8 -261
  22. package/dist/{context-transport-D8yeGdpd.js → context-transport-BqYwzJYE.js} +2 -2
  23. package/dist/{context-transport-D8yeGdpd.js.map → context-transport-BqYwzJYE.js.map} +1 -1
  24. package/dist/contexts/daytona.d.ts +1 -1
  25. package/dist/contexts/daytona.js +1 -1
  26. package/dist/contexts/e2b.d.ts +1 -1
  27. package/dist/contexts/e2b.js +1 -1
  28. package/dist/daemon-cli.d.ts +1 -0
  29. package/dist/daemon-cli.js +90 -0
  30. package/dist/daemon-cli.js.map +1 -0
  31. package/dist/eval.d.ts +1 -1
  32. package/dist/eval.js +1 -1
  33. package/dist/extensions.d.ts +2 -2
  34. package/dist/extensions.js +2 -2
  35. package/dist/headless.d.ts +1 -1
  36. package/dist/headless.js +4 -4
  37. package/dist/{index-D0O-QO7Z.d.ts → index-B-FSaM3_.d.ts} +2863 -2375
  38. package/dist/index-B-FSaM3_.d.ts.map +1 -0
  39. package/dist/index-DgdzXRPH.d.ts +3590 -0
  40. package/dist/index-DgdzXRPH.d.ts.map +1 -0
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.js +14 -1179
  43. package/dist/ipc-CIv87xuH.js +331 -0
  44. package/dist/ipc-CIv87xuH.js.map +1 -0
  45. package/dist/ipc.d.ts +849 -0
  46. package/dist/ipc.d.ts.map +1 -0
  47. package/dist/ipc.js +4 -0
  48. package/dist/json-rpc-rSWMUh9b.js +192 -0
  49. package/dist/json-rpc-rSWMUh9b.js.map +1 -0
  50. package/dist/{logger-DBX9uYOw.js → logger--C1VQjwJ.js} +3 -13
  51. package/dist/logger--C1VQjwJ.js.map +1 -0
  52. package/dist/logs-view-CWG8aUAG.js +44 -0
  53. package/dist/logs-view-CWG8aUAG.js.map +1 -0
  54. package/dist/{mcp-CVtSQTWg.js → mcp-BIWa57eY.js} +3 -3
  55. package/dist/{mcp-CVtSQTWg.js.map → mcp-BIWa57eY.js.map} +1 -1
  56. package/dist/mcp.d.ts +1 -1
  57. package/dist/mcp.js +1 -1
  58. package/dist/{messages-CZMUuKzo.js → messages-21XfHxyF.js} +3 -3
  59. package/dist/{messages-CZMUuKzo.js.map → messages-21XfHxyF.js.map} +1 -1
  60. package/dist/{openai-compat-BoTi-9fd.js → openai-compat-DY-AKxCg.js} +2 -2
  61. package/dist/{openai-compat-BoTi-9fd.js.map → openai-compat-DY-AKxCg.js.map} +1 -1
  62. package/dist/output/stream-json.d.ts +1 -1
  63. package/dist/output/terminal.d.ts +1 -1
  64. package/dist/{plugins-onDvTHEm.js → plugins-B531Tz5b.js} +2 -2
  65. package/dist/{plugins-onDvTHEm.js.map → plugins-B531Tz5b.js.map} +1 -1
  66. package/dist/plugins.d.ts +1 -1
  67. package/dist/plugins.js +1 -1
  68. package/dist/presets.d.ts +1 -1
  69. package/dist/presets.js +1 -1
  70. package/dist/providers/anthropic.d.ts +1 -1
  71. package/dist/providers/anthropic.js +1 -1
  72. package/dist/providers/arcee.d.ts +1 -1
  73. package/dist/providers/arcee.js +1 -1
  74. package/dist/providers/baseten.d.ts +1 -1
  75. package/dist/providers/baseten.js +1 -1
  76. package/dist/providers/cerebras.d.ts +1 -1
  77. package/dist/providers/cerebras.js +1 -1
  78. package/dist/providers/local.d.ts +1 -1
  79. package/dist/providers/local.js +1 -1
  80. package/dist/providers/openai-compat.d.ts +1 -1
  81. package/dist/providers/openai-compat.js +1 -1
  82. package/dist/providers/openai.d.ts +1 -1
  83. package/dist/providers/openai.js +3 -3
  84. package/dist/providers/openrouter.d.ts +1 -1
  85. package/dist/providers/openrouter.js +1 -1
  86. package/dist/providers/xai.d.ts +1 -1
  87. package/dist/providers/xai.js +1 -1
  88. package/dist/{providers-CmhdtD7w.js → providers-DX91jwir.js} +2 -2
  89. package/dist/{providers-CmhdtD7w.js.map → providers-DX91jwir.js.map} +1 -1
  90. package/dist/providers.d.ts +1 -1
  91. package/dist/providers.js +3 -3
  92. package/dist/restate.d.ts +1 -1
  93. package/dist/restate.js +8 -8
  94. package/dist/restate.js.map +1 -1
  95. package/dist/server-DJqp9dIR.js +1317 -0
  96. package/dist/server-DJqp9dIR.js.map +1 -0
  97. package/dist/session/sqlite.d.ts +1 -1
  98. package/dist/session/sqlite.d.ts.map +1 -1
  99. package/dist/session/sqlite.js +42 -4
  100. package/dist/session/sqlite.js.map +1 -1
  101. package/dist/{session-CyRLSsVa.js → session-K3icbPH9.js} +8 -2
  102. package/dist/session-K3icbPH9.js.map +1 -0
  103. package/dist/session.d.ts +1 -1
  104. package/dist/session.js +2 -2
  105. package/dist/{skills-BMGH_KK1.js → skills-PJyiSRCp.js} +3 -21
  106. package/dist/skills-PJyiSRCp.js.map +1 -0
  107. package/dist/skills.d.ts +1 -1
  108. package/dist/skills.js +1 -1
  109. package/dist/src-DzzhE2YO.js +2461 -0
  110. package/dist/src-DzzhE2YO.js.map +1 -0
  111. package/dist/todo-edits-Dw2oO2ON.js +378 -0
  112. package/dist/todo-edits-Dw2oO2ON.js.map +1 -0
  113. package/dist/{tool-formatters-NrzD6Fkk.d.ts → tool-formatters-DO56YBBw.d.ts} +104 -4
  114. package/dist/tool-formatters-DO56YBBw.d.ts.map +1 -0
  115. package/dist/tools/fetch-url.d.ts +1 -1
  116. package/dist/tools/web-search.d.ts +1 -1
  117. package/dist/tools.d.ts +1 -1
  118. package/dist/tools.js +1 -1
  119. package/dist/{transcript-anchors-UhC58Tmj.d.ts → transcript-anchors-DXqWWGbq.d.ts} +53 -4
  120. package/dist/transcript-anchors-DXqWWGbq.d.ts.map +1 -0
  121. package/dist/tui.d.ts +94 -13
  122. package/dist/tui.d.ts.map +1 -1
  123. package/dist/tui.js +2241 -1009
  124. package/dist/tui.js.map +1 -1
  125. package/dist/{turn-operations-mMxDRgLY.js → turn-operations-CZLsNgw-.js} +896 -11
  126. package/dist/turn-operations-CZLsNgw-.js.map +1 -0
  127. package/dist/{turn-operations-Cuc9pwiB.d.ts → turn-operations-DKFYWP3O.d.ts} +501 -4
  128. package/dist/turn-operations-DKFYWP3O.d.ts.map +1 -0
  129. package/dist/{types-BIarq1qE.js → types-CWgENVLY.js} +24 -2
  130. package/dist/types-CWgENVLY.js.map +1 -0
  131. package/dist/types.d.ts +1 -1
  132. package/dist/types.js +1 -1
  133. package/dist/xml-D5HBhJle.js +22 -0
  134. package/dist/xml-D5HBhJle.js.map +1 -0
  135. package/docs/CHAT.md +39 -0
  136. package/docs/IPC.md +420 -0
  137. package/docs/TUI.md +62 -0
  138. package/package.json +8 -2
  139. package/dist/acp-BQqhozmt.js.map +0 -1
  140. package/dist/agent-DTxH7RHW.js.map +0 -1
  141. package/dist/chat.d.ts.map +0 -1
  142. package/dist/chat.js.map +0 -1
  143. package/dist/index-D0O-QO7Z.d.ts.map +0 -1
  144. package/dist/index.js.map +0 -1
  145. package/dist/inline-autocomplete-D0MPvBaS.js +0 -596
  146. package/dist/inline-autocomplete-D0MPvBaS.js.map +0 -1
  147. package/dist/logger-DBX9uYOw.js.map +0 -1
  148. package/dist/login-CB23LyI1.js +0 -707
  149. package/dist/login-CB23LyI1.js.map +0 -1
  150. package/dist/session-CyRLSsVa.js.map +0 -1
  151. package/dist/skills-BMGH_KK1.js.map +0 -1
  152. package/dist/tool-formatters-NrzD6Fkk.d.ts.map +0 -1
  153. package/dist/transcript-anchors-BjH-68k2.js.map +0 -1
  154. package/dist/transcript-anchors-UhC58Tmj.d.ts.map +0 -1
  155. package/dist/turn-operations-Cuc9pwiB.d.ts.map +0 -1
  156. package/dist/turn-operations-mMxDRgLY.js.map +0 -1
  157. package/dist/types-BIarq1qE.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import { i as resolveOldString, o as styleReplacementForVia } from "./context-segments-CJCtJ9sU.js";
2
2
  import { r as utf8ByteLength } from "./utils-ngQzYzZD.js";
3
- import { l as shortId, t as ageString } from "./format-BF745pLL.js";
3
+ import { a as formatDuration, i as fmtTokens, l as shortId, t as ageString } from "./format-BF745pLL.js";
4
+ import { t as escapeXml } from "./xml-D5HBhJle.js";
4
5
  import { t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
5
6
  import { a as searchHighlightRanges } from "./history-search-C4bk1Wz_.js";
6
7
  import { Fzf } from "fzf";
@@ -1011,6 +1012,510 @@ function filetypeFromPath(path) {
1011
1012
  const ext = cleaned.slice(lastDot + 1).toLowerCase();
1012
1013
  return FILETYPE_BY_EXT[ext];
1013
1014
  }
1015
+ /** `1.2k` / `48k` / `1.3M` — token counts are read at a glance, not counted. */
1016
+ function formatTokenCount(tokens) {
1017
+ if (tokens >= 1e6) return `${(tokens / 1e6).toFixed(1)}M`;
1018
+ if (tokens >= 1e3) return `${(tokens / 1e3).toFixed(tokens >= 1e4 ? 0 : 1)}k`;
1019
+ return String(tokens);
1020
+ }
1021
+ /** `2m18s` / `48s` / `<1s` / `1h05m`. Compact — this is a receipt, not a profiler. */
1022
+ function formatRunDuration(ms) {
1023
+ if (ms < 1e3) return "<1s";
1024
+ const s = Math.round(ms / 1e3);
1025
+ if (s < 60) return `${s}s`;
1026
+ const m = Math.floor(s / 60);
1027
+ const rest = s % 60;
1028
+ if (m < 60) return rest ? `${m}m${String(rest).padStart(2, "0")}s` : `${m}m`;
1029
+ return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
1030
+ }
1031
+ /** `$0.04`, or four decimals under a cent so a cheap run is not shown as `$0.00`. */
1032
+ function formatCostUsd(cost) {
1033
+ return `$${cost.toFixed(cost < .01 ? 4 : 2)}`;
1034
+ }
1035
+ /**
1036
+ * Build the receipt for a run record. `now` stands in for `endedAt` when the
1037
+ * record is not finalized yet (`run:end` is broadcast a beat before it is).
1038
+ */
1039
+ function runReceiptFromRun(run, now = Date.now()) {
1040
+ const sumIn = (u) => u.input + (u.cacheRead ?? 0) + (u.cacheCreation ?? 0);
1041
+ const usage = run.totalUsage ? {
1042
+ input: sumIn(run.totalUsage),
1043
+ output: run.totalUsage.output
1044
+ } : run.turnUsage?.reduce((acc, u) => ({
1045
+ input: acc.input + sumIn(u),
1046
+ output: acc.output + u.output
1047
+ }), {
1048
+ input: 0,
1049
+ output: 0
1050
+ });
1051
+ const models = (run.turnUsage ?? []).map((u) => u.modelId).filter((m) => typeof m === "string" && m.length > 0);
1052
+ const modelId = models.at(-1);
1053
+ const otherModels = modelId ? new Set(models).size - 1 : 0;
1054
+ const durationMs = (run.endedAt ?? now) - run.startedAt;
1055
+ const cost = run.cost ?? run.turnUsage?.reduce((n, u) => n + (u.cost ?? 0), 0) ?? 0;
1056
+ const turns = run.turns ?? run.turnUsage?.length ?? 0;
1057
+ let genMs = 0;
1058
+ let genOut = 0;
1059
+ for (const u of run.turnUsage ?? []) {
1060
+ if (u.streamDurationMs === void 0 || u.output <= 0) continue;
1061
+ const ms = u.streamDurationMs - (u.timeToFirstTokenMs ?? 0);
1062
+ if (ms <= 0) continue;
1063
+ genMs += ms;
1064
+ genOut += u.output;
1065
+ }
1066
+ const tokensPerSecond = genMs > 0 && genOut > 0 ? genOut * 1e3 / genMs : void 0;
1067
+ return {
1068
+ ...modelId ? { modelId } : {},
1069
+ ...otherModels > 0 ? { otherModels } : {},
1070
+ ...durationMs > 0 ? { durationMs } : {},
1071
+ ...turns > 0 ? { turns } : {},
1072
+ ...cost > 0 ? { costUsd: cost } : {},
1073
+ ...usage && usage.input > 0 ? { tokensIn: usage.input } : {},
1074
+ ...usage && usage.output > 0 ? { tokensOut: usage.output } : {},
1075
+ ...tokensPerSecond !== void 0 ? { tokensPerSecond } : {}
1076
+ };
1077
+ }
1078
+ /** `42 tk/s`; one decimal under 10 so a slow model is not shown as `0 tk/s`. */
1079
+ function formatTokensPerSecond(tps) {
1080
+ return `${tps < 10 ? tps.toFixed(1) : Math.round(tps).toString()} tk/s`;
1081
+ }
1082
+ /**
1083
+ * The receipt's cells, in display order. Renderers join them with their own
1084
+ * separator, tint them by `kind`, and truncate to their own width; the CONTENT
1085
+ * is fixed here.
1086
+ */
1087
+ function formatRunReceiptCells(r) {
1088
+ const cells = [];
1089
+ if (r.modelId) cells.push({
1090
+ kind: "model",
1091
+ text: r.otherModels ? `${r.modelId} +${r.otherModels}` : r.modelId
1092
+ });
1093
+ if (r.durationMs !== void 0) cells.push({
1094
+ kind: "duration",
1095
+ text: formatRunDuration(r.durationMs)
1096
+ });
1097
+ if (r.turns !== void 0) cells.push({
1098
+ kind: "turns",
1099
+ text: `↻${r.turns}`
1100
+ });
1101
+ if (r.costUsd !== void 0) cells.push({
1102
+ kind: "cost",
1103
+ text: formatCostUsd(r.costUsd)
1104
+ });
1105
+ if (r.tokensIn !== void 0 || r.tokensOut !== void 0) {
1106
+ const segments = [{
1107
+ kind: "tokens-in",
1108
+ text: `↑${formatTokenCount(r.tokensIn ?? 0)}`
1109
+ }, {
1110
+ kind: "tokens-out",
1111
+ text: `↓${formatTokenCount(r.tokensOut ?? 0)}`
1112
+ }];
1113
+ cells.push({
1114
+ kind: "tokens",
1115
+ text: segments.map((seg) => seg.text).join(" "),
1116
+ segments
1117
+ });
1118
+ }
1119
+ if (r.tokensPerSecond !== void 0) cells.push({
1120
+ kind: "speed",
1121
+ text: formatTokensPerSecond(r.tokensPerSecond)
1122
+ });
1123
+ return cells;
1124
+ }
1125
+ /** The glyph that leads the line on every surface. */
1126
+ const RUN_RECEIPT_GLYPH = "≡";
1127
+ /**
1128
+ * Fit cells to `width` columns for a tinted renderer: returns the cells that
1129
+ * fit whole, plus whether the last one was cut. A cut cell is truncated with
1130
+ * `…`; anything after it is dropped. Never wraps. `prefix` is the columns the
1131
+ * renderer already spent (glyph + space).
1132
+ */
1133
+ function fitRunReceiptCells(cells, width, prefix = 0) {
1134
+ let used = prefix;
1135
+ const out = [];
1136
+ for (const cell of cells) {
1137
+ const need = (out.length ? 3 : 0) + cell.text.length;
1138
+ if (used + need <= width) {
1139
+ out.push(cell);
1140
+ used += need;
1141
+ continue;
1142
+ }
1143
+ const room = width - used - (out.length ? 3 : 0);
1144
+ if (room >= 2) out.push({
1145
+ kind: cell.kind,
1146
+ text: `${cell.text.slice(0, room - 1).trimEnd()}…`
1147
+ });
1148
+ break;
1149
+ }
1150
+ return out;
1151
+ }
1152
+ /** One line, ` · `-joined, ellipsized to `width` columns. Never wraps. */
1153
+ function formatRunReceiptLine(r, width = Number.POSITIVE_INFINITY) {
1154
+ const line = formatRunReceiptCells(r).map((c) => c.text).join(" · ");
1155
+ if (line.length <= width) return line;
1156
+ if (width <= 1) return width === 1 ? "…" : "";
1157
+ return `${line.slice(0, width - 1).trimEnd()}…`;
1158
+ }
1159
+ /** Anything to show? A run with no record data renders no line at all. */
1160
+ function hasRunReceipt(r) {
1161
+ return formatRunReceiptCells(r).length > 0;
1162
+ }
1163
+ //#endregion
1164
+ //#region src/chat/orchestration.ts
1165
+ /**
1166
+ * Orchestration — the pure model for a "main" session that drives others.
1167
+ *
1168
+ * A main session is an ordinary session with a role, and a worker is an
1169
+ * ordinary session with a parent pointer. Both are `metadata` keys, so every
1170
+ * existing surface (sidebar, history, attach, search, replay) works on them
1171
+ * with no special cases; only the surfaces that WANT to treat them specially
1172
+ * (pinning, the event row, the jump shortcut) read these keys.
1173
+ *
1174
+ * This is deliberately not `spawn`. A spawned child is ephemeral, inline, and
1175
+ * dies with the parent's run; a worker is a persistent, resumable session with
1176
+ * its own cwd, model chain, todos and history, visible in every window, and it
1177
+ * outlives the main's run. The parent pointer is what makes them a tree.
1178
+ *
1179
+ * Node-free: re-exported from `zidane/chat/pure` for the Electron renderer,
1180
+ * which needs `isMainSession` / `orchestratedBy` for pinning and the event row.
1181
+ */
1182
+ /** Title the main session is created with. Users can rename it; the ROLE is what matters. */
1183
+ const MAIN_SESSION_TITLE = "Main";
1184
+ /** `session.metadata.role` — present and `'main'` on the one orchestrating session. */
1185
+ const SESSION_ROLE_KEY = "role";
1186
+ /** `session.metadata.orchestratedBy` — the main session's id, on a worker. */
1187
+ const ORCHESTRATED_BY_KEY = "orchestratedBy";
1188
+ /** Type guard over untrusted metadata. */
1189
+ function isMainSession(metadata) {
1190
+ return metadata?.[SESSION_ROLE_KEY] === "main";
1191
+ }
1192
+ /** The main session id a worker reports to, or `null` for an ordinary session. */
1193
+ function orchestratedBy(metadata) {
1194
+ const value = metadata?.[ORCHESTRATED_BY_KEY];
1195
+ return typeof value === "string" && value.length > 0 ? value : null;
1196
+ }
1197
+ /** Tools whose `path` input means "this file changed". */
1198
+ const CHANGE_TOOLS = /* @__PURE__ */ new Set([
1199
+ "edit",
1200
+ "multi_edit",
1201
+ "write_file"
1202
+ ]);
1203
+ const ARTIFACT_PATH_CAP = 12;
1204
+ /**
1205
+ * What a run touched, read off its assistant turns' tool calls. Only the
1206
+ * tools that MUTATE count as "changed" — reads are not artifacts. Capped so a
1207
+ * refactor across a hundred files does not turn the event into a manifest;
1208
+ * the count still tells the truth.
1209
+ */
1210
+ function artifactsFromTurns(turns) {
1211
+ const changed = [];
1212
+ const seen = /* @__PURE__ */ new Set();
1213
+ let commands = 0;
1214
+ let toolCalls = 0;
1215
+ for (const turn of turns) {
1216
+ if (turn.role !== "assistant") continue;
1217
+ for (const block of turn.content) {
1218
+ if (block.type !== "tool_call" || !block.name) continue;
1219
+ toolCalls++;
1220
+ if (block.name === "shell") commands++;
1221
+ if (CHANGE_TOOLS.has(block.name)) {
1222
+ const path = block.input?.path;
1223
+ if (typeof path === "string" && path && !seen.has(path)) {
1224
+ seen.add(path);
1225
+ changed.push(path);
1226
+ }
1227
+ }
1228
+ }
1229
+ }
1230
+ if (toolCalls === 0) return void 0;
1231
+ return {
1232
+ changed: changed.slice(0, ARTIFACT_PATH_CAP),
1233
+ commands,
1234
+ toolCalls
1235
+ };
1236
+ }
1237
+ const STATUSES = [
1238
+ "done",
1239
+ "error",
1240
+ "cancelled",
1241
+ "needs-attention"
1242
+ ];
1243
+ /**
1244
+ * Wire format between the harness and the model — and, because it is persisted
1245
+ * inside the user turn's text block, between a live session and its replay.
1246
+ *
1247
+ * Follows `<task-notification>` exactly: child-tag XML, one block per event,
1248
+ * escaped with `escapeXml`. The model already knows this shape as "the harness
1249
+ * telling me something", which is the point — a main that mistook a worker
1250
+ * report for a human instruction would act on it as one. `eventsFromTurns`
1251
+ * detects the block on replay and re-synthesizes the structured row instead of
1252
+ * painting the raw XML as a user prompt.
1253
+ */
1254
+ function renderSessionEventXml(event) {
1255
+ const tag = (name, value) => value === void 0 ? [] : [` <${name}>${escapeXml(String(value))}</${name}>`];
1256
+ return [
1257
+ "<session-event>",
1258
+ ...tag("session-id", event.sessionId),
1259
+ ...tag("title", event.title),
1260
+ ...tag("status", event.status),
1261
+ ...tag("at", event.at),
1262
+ ...tag("cwd", event.cwd ?? ""),
1263
+ ...tag("cost-usd", event.costUsd),
1264
+ ...tag("duration-ms", event.durationMs),
1265
+ ...tag("tokens", event.tokens),
1266
+ ...tag("turns", event.turns),
1267
+ ...tag("worker", event.worker === void 0 ? void 0 : String(event.worker)),
1268
+ ...tag("seq", event.seq),
1269
+ ...tag("finish-reason", event.finishReason),
1270
+ ...tag("error", event.error),
1271
+ ...tag("detail", event.detail),
1272
+ ...tag("goal", event.goal),
1273
+ ...event.artifacts ? [
1274
+ ` <artifacts tool-calls="${event.artifacts.toolCalls}" commands="${event.artifacts.commands}">`,
1275
+ ...event.artifacts.changed.map((p) => ` <changed>${escapeXml(p)}</changed>`),
1276
+ " </artifacts>"
1277
+ ] : [],
1278
+ ...tag("summary", event.summary),
1279
+ "</session-event>"
1280
+ ].join("\n");
1281
+ }
1282
+ const SESSION_EVENT_RE = /^\s*<session-event>([\s\S]*?)<\/session-event>\s*$/;
1283
+ const FIELD = {
1284
+ sessionId: /<session-id>([\s\S]*?)<\/session-id>/,
1285
+ title: /<title>([\s\S]*?)<\/title>/,
1286
+ status: /<status>([\s\S]*?)<\/status>/,
1287
+ at: /<at>([\s\S]*?)<\/at>/,
1288
+ cwd: /<cwd>([\s\S]*?)<\/cwd>/,
1289
+ costUsd: /<cost-usd>([\s\S]*?)<\/cost-usd>/,
1290
+ durationMs: /<duration-ms>([\s\S]*?)<\/duration-ms>/,
1291
+ tokens: /<tokens>([\s\S]*?)<\/tokens>/,
1292
+ turns: /<turns>([\s\S]*?)<\/turns>/,
1293
+ worker: /<worker>([\s\S]*?)<\/worker>/,
1294
+ error: /<error>([\s\S]*?)<\/error>/,
1295
+ detail: /<detail>([\s\S]*?)<\/detail>/,
1296
+ seq: /<seq>([\s\S]*?)<\/seq>/,
1297
+ goal: /<goal>([\s\S]*?)<\/goal>/,
1298
+ finishReason: /<finish-reason>([\s\S]*?)<\/finish-reason>/,
1299
+ artifacts: /<artifacts tool-calls="(\d+)" commands="(\d+)">([\s\S]*?)<\/artifacts>/,
1300
+ summary: /<summary>([\s\S]*?)<\/summary>/
1301
+ };
1302
+ const CHANGED_RE = /<changed>([\s\S]*?)<\/changed>/g;
1303
+ const FINISH_REASONS = /* @__PURE__ */ new Set([
1304
+ "length",
1305
+ "pause",
1306
+ "content-filter"
1307
+ ]);
1308
+ /**
1309
+ * Inverse of {@link renderSessionEventXml}. Untrusted input — it comes off disk
1310
+ * and outlives this module's versions — so it validates each field and returns
1311
+ * `null` for anything malformed, in which case the turn replays as the plain
1312
+ * user text it structurally is.
1313
+ */
1314
+ function parseSessionEventXml(text) {
1315
+ const m = text.match(SESSION_EVENT_RE);
1316
+ if (!m) return null;
1317
+ const body = m[1];
1318
+ const pick = (re) => {
1319
+ const inner = body.match(re);
1320
+ return inner ? unescapeXml(inner[1].trim()) : void 0;
1321
+ };
1322
+ const sessionId = pick(FIELD.sessionId);
1323
+ const title = pick(FIELD.title);
1324
+ const status = pick(FIELD.status);
1325
+ const at = Number(pick(FIELD.at));
1326
+ const summary = pick(FIELD.summary);
1327
+ if (!sessionId || title === void 0 || summary === void 0) return null;
1328
+ if (!status || !STATUSES.includes(status)) return null;
1329
+ if (!Number.isFinite(at)) return null;
1330
+ const cost = Number(pick(FIELD.costUsd));
1331
+ const cwd = pick(FIELD.cwd);
1332
+ const positive = (raw) => {
1333
+ const n = Number(raw);
1334
+ return Number.isFinite(n) && n > 0 ? n : void 0;
1335
+ };
1336
+ const durationMs = positive(pick(FIELD.durationMs));
1337
+ const tokens = positive(pick(FIELD.tokens));
1338
+ const turns = positive(pick(FIELD.turns));
1339
+ const worker = pick(FIELD.worker);
1340
+ const error = pick(FIELD.error);
1341
+ const detail = pick(FIELD.detail);
1342
+ const seq = positive(pick(FIELD.seq));
1343
+ const goal = pick(FIELD.goal);
1344
+ const finishReason = pick(FIELD.finishReason);
1345
+ const art = body.match(FIELD.artifacts);
1346
+ const artifacts = art ? {
1347
+ toolCalls: Number(art[1]),
1348
+ commands: Number(art[2]),
1349
+ changed: [...art[3].matchAll(CHANGED_RE)].map((m) => unescapeXml(m[1].trim()))
1350
+ } : void 0;
1351
+ return {
1352
+ sessionId,
1353
+ title,
1354
+ status,
1355
+ summary,
1356
+ at,
1357
+ ...Number.isFinite(cost) && cost > 0 ? { costUsd: cost } : {},
1358
+ ...cwd !== void 0 ? { cwd } : {},
1359
+ ...durationMs !== void 0 ? { durationMs } : {},
1360
+ ...tokens !== void 0 ? { tokens } : {},
1361
+ ...turns !== void 0 ? { turns } : {},
1362
+ ...worker === "true" ? { worker: true } : worker === "false" ? { worker: false } : {},
1363
+ ...error ? { error } : {},
1364
+ ...detail ? { detail } : {},
1365
+ ...seq !== void 0 ? { seq } : {},
1366
+ ...goal ? { goal } : {},
1367
+ ...finishReason && FINISH_REASONS.has(finishReason) ? { finishReason } : {},
1368
+ ...artifacts ? { artifacts } : {}
1369
+ };
1370
+ }
1371
+ function unescapeXml(s) {
1372
+ return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&apos;/g, "'").replace(/&amp;/g, "&");
1373
+ }
1374
+ /**
1375
+ * The short label a transcript row shows — `api-refactor · done`. Status words
1376
+ * are chosen to read as a verb phrase after the title.
1377
+ */
1378
+ function sessionEventLabel(event) {
1379
+ return `${event.title} ${{
1380
+ "done": "finished",
1381
+ "error": "failed",
1382
+ "cancelled": "was cancelled",
1383
+ "needs-attention": "needs attention"
1384
+ }[event.status]}`;
1385
+ }
1386
+ /**
1387
+ * Status → how it reads. `needs-attention` is deliberately the loudest: it is
1388
+ * the one status where the human, not the main, has to act.
1389
+ */
1390
+ const SESSION_EVENT_STATUS_STYLE = {
1391
+ "done": {
1392
+ glyph: "✓",
1393
+ tone: "accent",
1394
+ verb: "finished"
1395
+ },
1396
+ "error": {
1397
+ glyph: "✗",
1398
+ tone: "error",
1399
+ verb: "failed"
1400
+ },
1401
+ "cancelled": {
1402
+ glyph: "⊘",
1403
+ tone: "dim",
1404
+ verb: "was cancelled"
1405
+ },
1406
+ "needs-attention": {
1407
+ glyph: "!",
1408
+ tone: "warn",
1409
+ verb: "needs your input"
1410
+ }
1411
+ };
1412
+ /**
1413
+ * The directory name a session lives in — the same basename label the sidebar
1414
+ * groups by. `undefined` for a directory-less session (the main) or no cwd.
1415
+ */
1416
+ function sessionDirLabel(cwd) {
1417
+ if (!cwd) return void 0;
1418
+ const parts = cwd.split(/[\\/]+/).filter(Boolean);
1419
+ return parts[parts.length - 1];
1420
+ }
1421
+ /** Short, stable id fragment for the header — enough to disambiguate, not to read. */
1422
+ function shortSessionId(id) {
1423
+ return id.length <= 8 ? id : id.slice(0, 8);
1424
+ }
1425
+ /**
1426
+ * The header's metadata cells, in display order, omitting anything unknown.
1427
+ * Renderers join them with their own separator; the CONTENT is fixed here so
1428
+ * both surfaces show the same receipt for the same run.
1429
+ *
1430
+ * The session id is deliberately NOT a cell: it is the least legible datum and
1431
+ * exists only to disambiguate. Renderers append `shortSessionId` LAST and
1432
+ * dimmer than the rest.
1433
+ */
1434
+ function sessionEventMeta(event) {
1435
+ const cells = [];
1436
+ const dir = sessionDirLabel(event.cwd);
1437
+ if (dir) cells.push(dir);
1438
+ if (event.durationMs !== void 0) cells.push(formatRunDuration(event.durationMs));
1439
+ if (event.tokens !== void 0) cells.push(`${formatTokenCount(event.tokens)} tok`);
1440
+ if (event.turns !== void 0) cells.push(`${event.turns} turn${event.turns === 1 ? "" : "s"}`);
1441
+ if (event.costUsd !== void 0) cells.push(formatCostUsd(event.costUsd));
1442
+ return cells;
1443
+ }
1444
+ /**
1445
+ * The dimmed second line: the last thing the session said, collapsed to one
1446
+ * paragraph and capped. Renderers may clamp further (line-clamp in the GUI,
1447
+ * width in the TUI); this is the upper bound and the shared ellipsis rule.
1448
+ */
1449
+ function sessionEventExcerpt(event, max = 240) {
1450
+ const flat = (event.status === "needs-attention" && event.detail ? event.detail : event.status === "error" && event.error ? event.error : event.summary).replace(/\s+/g, " ").trim();
1451
+ return flat.length <= max ? flat : `${flat.slice(0, max - 1).trimEnd()}…`;
1452
+ }
1453
+ /** Ellipsize a title to `max` characters; titles are user text and can be a paragraph. */
1454
+ function truncateTitle(title, max = 48) {
1455
+ const flat = title.replace(/\s+/g, " ").trim();
1456
+ return flat.length <= max ? flat : `${flat.slice(0, max - 1).trimEnd()}…`;
1457
+ }
1458
+ /**
1459
+ * System-prompt fragment appended to every WORKER's agent. A worker's last
1460
+ * message becomes the main's `summary`, so it must be a report, not a sign-off
1461
+ * — the main should be able to act on it without reading the transcript.
1462
+ */
1463
+ const WORKER_REPORT_FRAGMENT = `You are a worker session created by an orchestrating main session. Your final message of a run is delivered to it verbatim as your report, so end every run with a short structured report:
1464
+
1465
+ ## Report
1466
+ - **Goal**: what you were asked, in one line
1467
+ - **Outcome**: done / partial / blocked — one line, and why if not done
1468
+ - **Changed**: files or artifacts touched (paths), or "none"
1469
+ - **Checks**: what you ran and the result (tests, typecheck, lint), or "none"
1470
+ - **Open questions**: anything the main or a human must decide, or "none"
1471
+ - **Next**: the concrete next step you recommend, or "none"
1472
+
1473
+ Keep it factual and under ~15 lines. This block is what the main decides on; prose before it is fine, prose instead of it is not.`;
1474
+ /**
1475
+ * System-prompt fragment appended to the MAIN's agent: how to delegate without
1476
+ * wasting calls. The `sessions` tool description says what each verb does;
1477
+ * this says which one to reach for, and when not to call at all.
1478
+ */
1479
+ const MAIN_PLAYBOOK_FRAGMENT = `You are the main session: an orchestrator. Work happens in worker sessions; you brief them, integrate their reports, and keep the human informed. Do not do the workers' work here.
1480
+
1481
+ Work efficiently with the \`sessions\` tool:
1482
+ - Never poll. After create or send you WILL receive a <session-event> when the worker finishes, fails, or blocks. Do not read or list a worker that has not reported yet.
1483
+ - Reuse before creating. To continue work in a project, send to its existing idle session (listed under Projects below; its context is already warm). Create a worker only for a new, independent unit of work, or when the project has no suitable session.
1484
+ - You already know the ids. Take them from Projects below, from a <session-event>, or from an earlier list. Do not list just to find an id you have.
1485
+ - send vs steer: send when the worker is idle or the new prompt should queue after its current run; steer only to change course mid-run.
1486
+ - Fire-and-forget when you can. For work you will not act on when it lands (docs, cleanup, low-stakes checks), pass wake: false on create or send; the report still reaches your history, you just are not woken for it.
1487
+ - Brief completely. A worker prompt states the goal, the constraints, what to check (tests, typecheck, lint), and that it must end with its report. Follow-up questions cost a full round trip.
1488
+ - read is cheap (last message); transcript is not. Use transcript only when a report is unclear.
1489
+ - Report to the human briefly: what went where, what came back, what needs a decision. Do not re-narrate worker reports.`;
1490
+ /**
1491
+ * The main's "Projects" briefing: which directories have ongoing work and
1492
+ * the session to address in each, so the main does not rediscover them with
1493
+ * a list call every run. Snapshot at agent start — live state is one `list`
1494
+ * away and the text says so.
1495
+ */
1496
+ function renderProjectsBriefing(projects, now) {
1497
+ const lines = ["## Projects", "Known as of this session's start; live state comes from `sessions list`. Directory-less sessions are omitted."];
1498
+ if (projects.length === 0) {
1499
+ lines.push("- none yet");
1500
+ return lines.join("\n");
1501
+ }
1502
+ for (const p of projects) {
1503
+ const cells = [`${p.sessions} session${p.sessions === 1 ? "" : "s"}`, `last active ${agoLabel(now - p.lastActiveAt)}`];
1504
+ if (p.active > 0) cells.push(`${p.active} active`);
1505
+ lines.push(`- ${sessionDirLabel(p.root) ?? p.root} — ${p.root} · ${cells.join(" · ")}`);
1506
+ lines.push(` latest: ${p.latest.sessionId} "${truncateTitle(p.latest.title, 60)}" (${p.latest.state})`);
1507
+ }
1508
+ return lines.join("\n");
1509
+ }
1510
+ /** Coarse relative time for the briefing: `just now`, `12m ago`, `3h ago`, `5d ago`. */
1511
+ function agoLabel(elapsedMs) {
1512
+ const m = Math.floor(Math.max(0, elapsedMs) / 6e4);
1513
+ if (m < 1) return "just now";
1514
+ if (m < 60) return `${m}m ago`;
1515
+ const h = Math.floor(m / 60);
1516
+ if (h < 48) return `${h}h ago`;
1517
+ return `${Math.floor(h / 24)}d ago`;
1518
+ }
1014
1519
  //#endregion
1015
1520
  //#region src/chat/turn-selection.ts
1016
1521
  /** Tools whose `tool-result` event is suppressed when `showEditDiffs` is on. */
@@ -1111,24 +1616,48 @@ function promptHistoryFromEvents(events) {
1111
1616
  * can cheaply skip a re-render.
1112
1617
  */
1113
1618
  function retagUserPromptEvents(events, appendedTurns) {
1619
+ return retagUserPromptEventsFromRefs(events, promptTurnRefs(appendedTurns));
1620
+ }
1621
+ /**
1622
+ * Reduce persisted turns to the prompt refs the retag needs, dropping
1623
+ * non-prompt turns (assistant, tool results) entirely.
1624
+ *
1625
+ * Shared by the in-process path and the daemon broadcast so both select the
1626
+ * same turns as "prompts" — a divergence here would make a prompt selectable
1627
+ * on one surface and not the other.
1628
+ */
1629
+ function promptTurnRefs(turns) {
1114
1630
  const isMediaBlock = (b) => b.type === "document" || b.type === "image" || b.type === "audio" || b.type === "video";
1115
1631
  const hasPromptText = (t) => t.content.some((b) => b.type === "text" && b.text.trim().length > 0);
1116
1632
  const isAttachmentOnly = (t) => t.content.some(isMediaBlock) && !t.content.some((b) => b.type === "tool_result");
1117
- const promptTurns = appendedTurns.filter((t) => t.role === "user" && (hasPromptText(t) || isAttachmentOnly(t)));
1118
- if (promptTurns.length === 0) return events;
1633
+ return turns.filter((t) => t.role === "user" && (hasPromptText(t) || isAttachmentOnly(t))).map((turn) => ({
1634
+ id: turn.id,
1635
+ texts: [...turn.content.filter((b) => b.type === "text").map((b) => b.text), ...turn.display ? [turn.display] : []],
1636
+ attachmentOnly: !hasPromptText(turn) && isAttachmentOnly(turn)
1637
+ }));
1638
+ }
1639
+ /**
1640
+ * Attach persisted turn ids to the live `user-prompt` events they correspond
1641
+ * to, so a just-sent prompt becomes selectable (hover, click, edit, delete)
1642
+ * without waiting for a session reload.
1643
+ *
1644
+ * Matching is newest-first and one-to-one: identical prompt text sent twice
1645
+ * must bind to two different turns, so each ref consumes exactly one untagged
1646
+ * event.
1647
+ */
1648
+ function retagUserPromptEventsFromRefs(events, refs) {
1649
+ if (refs.length === 0) return events;
1119
1650
  let changed = false;
1120
1651
  const next = [...events];
1121
- for (const turn of promptTurns) {
1122
- const texts = new Set(turn.content.filter((b) => b.type === "text").map((b) => b.text));
1123
- if (turn.display) texts.add(turn.display);
1124
- const matchesBlank = !hasPromptText(turn) && isAttachmentOnly(turn);
1652
+ for (const ref of refs) {
1653
+ const texts = new Set(ref.texts);
1125
1654
  for (let i = next.length - 1; i >= 0; i--) {
1126
1655
  const e = next[i];
1127
1656
  if (e.kind !== "user-prompt" || e.turnId) continue;
1128
- if (texts.has(e.text) || matchesBlank && e.text.trim().length === 0 && (e.attachments?.length ?? 0) > 0) {
1657
+ if (texts.has(e.text) || ref.attachmentOnly && e.text.trim().length === 0 && (e.attachments?.length ?? 0) > 0) {
1129
1658
  next[i] = {
1130
1659
  ...e,
1131
- turnId: turn.id
1660
+ turnId: ref.id
1132
1661
  };
1133
1662
  changed = true;
1134
1663
  break;
@@ -1172,6 +1701,7 @@ function isVisible(event, settings) {
1172
1701
  case "thinking": return settings.showThinking;
1173
1702
  case "tool": return settings.toolCallDisplay !== "hidden" || isUserBashEvent(event);
1174
1703
  case "tool-result": return settings.showToolResults || isUserBashEvent(event);
1704
+ case "run-summary": return settings.showRunReceipt;
1175
1705
  default: return true;
1176
1706
  }
1177
1707
  }
@@ -1835,6 +2365,254 @@ function uniqueSkillNamesFromReferences(references) {
1835
2365
  return out;
1836
2366
  }
1837
2367
  //#endregion
2368
+ //#region src/chat/model-chain.ts
2369
+ /** Identity of a chain entry — the pair, not object identity. */
2370
+ function chainEntryKey(entry) {
2371
+ return `${entry.provider}\u0000${entry.model}`;
2372
+ }
2373
+ /** Toggle `entry`: absent → appended, present → removed. Pure. */
2374
+ function toggleKeyed(chain, entry, keyOf) {
2375
+ const key = keyOf(entry);
2376
+ const idx = chain.findIndex((e) => keyOf(e) === key);
2377
+ if (idx === -1) return [...chain, entry];
2378
+ return [...chain.slice(0, idx), ...chain.slice(idx + 1)];
2379
+ }
2380
+ /**
2381
+ * Make `entry` the primary (position 0), keeping the rest in order. Inserts
2382
+ * when absent, promotes when present; no-op when it already leads.
2383
+ *
2384
+ * This is `↵` in the picker. It exists because toggling APPENDS: with a chain
2385
+ * of `[current]`, toggling a new model produced `[current, new]` — the pick
2386
+ * became a fallback and the active model never changed. "Highlight and ↵"
2387
+ * means "use this one", so that gesture promotes; building a fallback list is
2388
+ * the secondary gesture (`⇥`) and keeps toggle semantics.
2389
+ */
2390
+ function pickPrimaryKeyed(chain, entry, keyOf) {
2391
+ const key = keyOf(entry);
2392
+ return [entry, ...chain.filter((e) => keyOf(e) !== key)];
2393
+ }
2394
+ /** 1-based position of `key` in the chain, or `null` when absent. */
2395
+ function positionKeyed(chain, key, keyOf) {
2396
+ const idx = chain.findIndex((e) => keyOf(e) === key);
2397
+ return idx === -1 ? null : idx + 1;
2398
+ }
2399
+ /** Same entries in the same order. */
2400
+ function chainsEqualKeyed(a, b, keyOf) {
2401
+ return a.length === b.length && a.every((e, i) => keyOf(e) === keyOf(b[i]));
2402
+ }
2403
+ /** Move the entry with `key` by one priority slot; boundary or missing = identity. */
2404
+ function reorderKeyed(chain, key, delta, keyOf) {
2405
+ const from = chain.findIndex((e) => keyOf(e) === key);
2406
+ const to = from + delta;
2407
+ if (from < 0 || to < 0 || to >= chain.length) return chain;
2408
+ const next = [...chain];
2409
+ const [entry] = next.splice(from, 1);
2410
+ next.splice(to, 0, entry);
2411
+ return next;
2412
+ }
2413
+ function toggleChainEntry(chain, entry) {
2414
+ return toggleKeyed(chain, entry, chainEntryKey);
2415
+ }
2416
+ function pickChainPrimary(chain, entry) {
2417
+ return pickPrimaryKeyed(chain, entry, chainEntryKey);
2418
+ }
2419
+ /** 1-based chain position of a provider+model pair, or `null` when not in the chain. */
2420
+ function chainPositionOf(chain, provider, model) {
2421
+ return positionKeyed(chain, chainEntryKey({
2422
+ provider,
2423
+ model
2424
+ }), chainEntryKey);
2425
+ }
2426
+ function chainsEqual(a, b) {
2427
+ return chainsEqualKeyed(a, b, chainEntryKey);
2428
+ }
2429
+ /**
2430
+ * Did the SET of provider keys change between two chains? The agent is
2431
+ * built with one provider instance per distinct chain key, so a set
2432
+ * change requires a session re-activation (rebuild); a pure model /
2433
+ * order change within the same providers does not.
2434
+ */
2435
+ function providerKeySetChanged(prev, next) {
2436
+ const prevKeys = new Set(prev.map((e) => e.provider));
2437
+ const nextKeys = new Set(next.map((e) => e.provider));
2438
+ if (prevKeys.size !== nextKeys.size) return true;
2439
+ for (const k of nextKeys) if (!prevKeys.has(k)) return true;
2440
+ return false;
2441
+ }
2442
+ /**
2443
+ * Assemble the `agent.run` `model` option from a chain.
2444
+ *
2445
+ * - Single-entry chain → the bare model id string (byte-identical to the
2446
+ * pre-chain behavior).
2447
+ * - Multi-entry chain → pinned `{ provider, model }` targets, where
2448
+ * `provider` is the runtime `Provider.name` resolved via
2449
+ * `providerNameFor` (chain entries carry chat-level `ProviderKey`s,
2450
+ * which need not equal the instance names the agent loop matches on).
2451
+ *
2452
+ * Throws only on an empty chain — a programmer error, since chains
2453
+ * always carry a primary. Stale or unresolvable entries degrade
2454
+ * defensively instead (never throw, never produce an entry the agent
2455
+ * would reject):
2456
+ * - Entries whose key has no resolvable name (provider not constructed
2457
+ * into the current agent) are dropped.
2458
+ * - If the PRIMARY itself doesn't resolve, or fewer than two entries
2459
+ * survive, fall back to the bare primary model string.
2460
+ *
2461
+ * `contextWindowFor` (optional) stamps each target with its model's
2462
+ * context window from the host catalog, so the loop re-bases the
2463
+ * auto-compact trigger when a fallback advances onto it (the host's
2464
+ * explicit window is sized for the PRIMARY otherwise).
2465
+ */
2466
+ function buildRunModel(chain, providerNameFor, contextWindowFor) {
2467
+ const primary = chain[0];
2468
+ if (!primary) throw new Error("buildRunModel: chain must not be empty");
2469
+ if (chain.length === 1 || providerNameFor(primary.provider) === void 0) return primary.model;
2470
+ const resolved = [];
2471
+ for (const entry of chain) {
2472
+ const name = providerNameFor(entry.provider);
2473
+ if (name === void 0) continue;
2474
+ const contextWindow = contextWindowFor?.(entry);
2475
+ resolved.push({
2476
+ provider: name,
2477
+ model: entry.model,
2478
+ ...typeof contextWindow === "number" && contextWindow > 0 ? { contextWindow } : {}
2479
+ });
2480
+ }
2481
+ return resolved.length > 1 ? resolved : primary.model;
2482
+ }
2483
+ /**
2484
+ * Validate a persisted chain read back from `state.json`. The state file
2485
+ * is user-editable JSON, so the value can be arbitrarily malformed —
2486
+ * return only the well-shaped `{ provider, model }` entries (both
2487
+ * non-empty strings), or `[]` for anything else. Never throws.
2488
+ */
2489
+ function sanitizeModelChain(value) {
2490
+ if (!Array.isArray(value)) return [];
2491
+ const entries = [];
2492
+ for (const item of value) {
2493
+ if (typeof item !== "object" || item === null || !("provider" in item) || !("model" in item)) continue;
2494
+ const { provider, model } = item;
2495
+ if (typeof provider === "string" && provider.length > 0 && typeof model === "string" && model.length > 0) entries.push({
2496
+ provider,
2497
+ model
2498
+ });
2499
+ }
2500
+ return entries;
2501
+ }
2502
+ /**
2503
+ * Restore the fallback tail of a persisted chain for a resolved primary
2504
+ * pair. Returns entries 1+ only when the persisted chain's entry 0
2505
+ * matches `primary` — a divergent chain belongs to a different selection
2506
+ * (e.g. a `--model` launch override) and silently attaching it would
2507
+ * surprise. Entries whose provider isn't registered, entries equal to
2508
+ * the primary, and exact duplicates drop. Accepts the raw persisted
2509
+ * value (`state.json` is user-editable) — shape validation included.
2510
+ */
2511
+ function restoreChainFallbacks(saved, primary, isRegistered) {
2512
+ const chain = sanitizeModelChain(saved);
2513
+ const head = chain[0];
2514
+ if (chain.length < 2 || !head || head.provider !== primary.provider || head.model !== primary.model) return [];
2515
+ return filterChainFallbacks(chain.slice(1), primary, isRegistered);
2516
+ }
2517
+ /**
2518
+ * Filter candidate fallback entries against a primary pair, preserving
2519
+ * picked order: entries whose provider `isAvailable` rejects, entries
2520
+ * equal to the primary, and exact duplicates drop. Shared by the
2521
+ * picker's apply path (live-auth availability) and persisted-chain
2522
+ * restore (registry availability).
2523
+ */
2524
+ function filterChainFallbacks(entries, primary, isAvailable) {
2525
+ const fallbacks = [];
2526
+ for (const entry of entries) {
2527
+ if (!isAvailable(entry.provider)) continue;
2528
+ if (entry.provider === primary.provider && entry.model === primary.model) continue;
2529
+ if (fallbacks.some((f) => f.provider === entry.provider && f.model === entry.model)) continue;
2530
+ fallbacks.push({
2531
+ provider: entry.provider,
2532
+ model: entry.model
2533
+ });
2534
+ }
2535
+ return fallbacks;
2536
+ }
2537
+ //#endregion
2538
+ //#region src/chat/daemon-health.ts
2539
+ const DAEMON_HEALTH_GLYPH = "●";
2540
+ const DAEMON_HEALTH_STYLE = {
2541
+ connected: {
2542
+ tone: "accent",
2543
+ label: "daemon connected"
2544
+ },
2545
+ connecting: {
2546
+ tone: "warn",
2547
+ label: "connecting to daemon…"
2548
+ },
2549
+ disconnected: {
2550
+ tone: "error",
2551
+ label: "daemon unreachable"
2552
+ }
2553
+ };
2554
+ //#endregion
2555
+ //#region src/chat/footer-chips.ts
2556
+ /**
2557
+ * Abbreviate a re-billed token count: `1200` → `1.2k`, `800` → `800`.
2558
+ * Thousands only — a cache prefix large enough to need `M` would be past any
2559
+ * provider's window.
2560
+ */
2561
+ function formatTokens(tokens) {
2562
+ return tokens >= 1e3 ? `${(tokens / 1e3).toFixed(1)}k` : String(tokens);
2563
+ }
2564
+ /**
2565
+ * Build the chip for a cache break.
2566
+ *
2567
+ * `costUsd` is OPTIONAL and omitted entirely on a rate miss — never rendered
2568
+ * as `$0.00`. A fabricated zero would read as "this was free", which is the
2569
+ * opposite of what the chip exists to say. When present it is prefixed `~`
2570
+ * because it is an estimate (registry rates × re-billed prefix), and clamped
2571
+ * to `<0.01` rather than rounding a real charge down to nothing.
2572
+ */
2573
+ function cacheBreakChip(info) {
2574
+ const rebilled = formatTokens(info.rebilledTokens);
2575
+ const cost = info.costUsd !== void 0 ? ` · ~$${info.costUsd < .01 ? "<0.01" : info.costUsd.toFixed(2)}` : "";
2576
+ if (info.changed.length === 0) return {
2577
+ tone: "dim",
2578
+ glyph: "⚡",
2579
+ label: `cache expired · +${rebilled} tok${cost}`
2580
+ };
2581
+ return {
2582
+ tone: "warn",
2583
+ glyph: "⚡",
2584
+ label: `cache break · ${info.changed.join(",")} · +${rebilled} tok${cost}`
2585
+ };
2586
+ }
2587
+ /**
2588
+ * Provider-retry label — `retrying 2/5`, or `retrying 2/5 · child-3` when the
2589
+ * stalled stream belongs to a subagent rather than the parent.
2590
+ *
2591
+ * Naming the source matters: with subagents in flight, an unattributed
2592
+ * "retrying" leaves the user unable to tell whether their own turn is stuck or
2593
+ * a delegated worker is.
2594
+ */
2595
+ function retryLabel(info) {
2596
+ const suffix = info.source === "parent" ? "" : ` · ${info.source}`;
2597
+ return `retrying ${info.attempt}/${info.max}${suffix}`;
2598
+ }
2599
+ /**
2600
+ * Footer label while a compaction summary is being generated:
2601
+ * `Compacting...`, then `~12k` once the loop reports a token estimate, then
2602
+ * ` · 8s` once a second has passed. Same rule on every surface so a
2603
+ * minutes-long compaction of a large transcript reads as progress — not a
2604
+ * hang — identically in the TUI and the GUI.
2605
+ *
2606
+ * `now` is injectable so callers driving a 1 Hz tick don't have to re-read
2607
+ * the clock, and so tests are deterministic.
2608
+ */
2609
+ function compactingLabel(estTokens, startedAt, now = Date.now()) {
2610
+ const base = "Compacting...";
2611
+ const elapsedMs = typeof startedAt === "number" ? now - startedAt : 0;
2612
+ const elapsed = elapsedMs >= 1e3 ? ` · ${formatDuration(elapsedMs)}` : "";
2613
+ return typeof estTokens === "number" && estTokens > 0 ? `${base} ~${fmtTokens(estTokens)}${elapsed}` : `${base}${elapsed}`;
2614
+ }
2615
+ //#endregion
1838
2616
  //#region src/chat/markdown-segments.ts
1839
2617
  /**
1840
2618
  * Split markdown text into alternating prose / fenced-code segments.
@@ -2412,6 +3190,60 @@ function splitPromptSegments(text, refs) {
2412
3190
  return out;
2413
3191
  }
2414
3192
  //#endregion
3193
+ //#region src/chat/provider-usage-format.ts
3194
+ /**
3195
+ * The window closest to its ceiling, which is the one that will bite first and
3196
+ * the only one a single-line indicator has room for. `null` when the snapshot
3197
+ * carries no windows.
3198
+ */
3199
+ function mostConstrainedWindow(snapshot) {
3200
+ let worst = null;
3201
+ for (const win of snapshot.windows) if (!worst || win.usedPercent > worst.usedPercent) worst = win;
3202
+ return worst;
3203
+ }
3204
+ /**
3205
+ * Percent for display: integers print bare, fractions keep one decimal (so a
3206
+ * status bar never reads `100%` for `99.9`).
3207
+ */
3208
+ function formatUsagePercent(usedPercent) {
3209
+ return Number.isInteger(usedPercent) ? String(usedPercent) : usedPercent.toFixed(1);
3210
+ }
3211
+ function usageTone(usedPercent) {
3212
+ if (usedPercent >= 85) return "error";
3213
+ if (usedPercent >= 60) return "warn";
3214
+ return "ok";
3215
+ }
3216
+ /**
3217
+ * Time to reset, coarsened to whole days past 24h. `null` when the provider
3218
+ * reported no reset, `resetting` once it is due.
3219
+ *
3220
+ * Every step rounds DOWN, like the percent: 36h reads as `1d`, not `2d`.
3221
+ * Overstating the headroom left on a window is the wrong direction to be
3222
+ * wrong in.
3223
+ */
3224
+ function resetLabel(resetsAt, now = Date.now()) {
3225
+ if (resetsAt === null) return null;
3226
+ const remaining = resetsAt - now;
3227
+ if (remaining <= 0) return "resetting";
3228
+ const minutes = Math.floor(remaining / 6e4);
3229
+ if (minutes < 60) return `resets in ${Math.max(1, minutes)}m`;
3230
+ const hours = Math.floor(minutes / 60);
3231
+ if (hours < 24) {
3232
+ const remMinutes = minutes % 60;
3233
+ return `resets in ${hours}h${remMinutes > 0 ? `${remMinutes}m` : ""}`;
3234
+ }
3235
+ return `resets in ${Math.max(1, Math.floor(hours / 24))}d`;
3236
+ }
3237
+ /** Balances are dollars for OpenRouter and opaque units elsewhere. */
3238
+ function formatCredits(value) {
3239
+ return value >= 100 ? value.toFixed(0) : value.toFixed(2);
3240
+ }
3241
+ /** `20.00 / 25.00`, or just the remainder when no total is reported. */
3242
+ function formatBalance(credits) {
3243
+ const remaining = formatCredits(credits.remaining);
3244
+ return credits.total === null ? remaining : `${remaining} / ${formatCredits(credits.total)}`;
3245
+ }
3246
+ //#endregion
2415
3247
  //#region src/chat/streaming-pure.ts
2416
3248
  const PARENT_OWNER = "parent";
2417
3249
  function ownerOf(event) {
@@ -2582,6 +3414,15 @@ function capTranscriptEvents(events, max = MAX_TRANSCRIPT_EVENTS, trimTo = TRANS
2582
3414
  * the same shape in their own style. Lives in `zidane/chat` because
2583
3415
  * it has no rendering concerns; the TUI just consumes it.
2584
3416
  */
3417
+ const SESSIONS_VERB_LABEL = {
3418
+ create: "Create session",
3419
+ send: "Send to session",
3420
+ steer: "Steer session",
3421
+ cancel: "Cancel session",
3422
+ read: "Read session",
3423
+ transcript: "Session transcript",
3424
+ list: "List sessions"
3425
+ };
2585
3426
  const TOOL_DISPLAY = {
2586
3427
  read_file: {
2587
3428
  displayName: "Read",
@@ -2815,6 +3656,50 @@ const TOOL_DISPLAY = {
2815
3656
  if (!title) return null;
2816
3657
  return { target: title };
2817
3658
  }
3659
+ },
3660
+ sessions: {
3661
+ displayName: (input) => SESSIONS_VERB_LABEL[stringField(input ?? {}, "verb") ?? ""] ?? "Sessions",
3662
+ format: (input) => {
3663
+ const verb = stringField(input, "verb");
3664
+ const id = stringField(input, "sessionId");
3665
+ const limit = numberField(input, "limit");
3666
+ const ref = id ? {
3667
+ target: shortSessionId(id),
3668
+ sessionId: id
3669
+ } : null;
3670
+ switch (verb) {
3671
+ case "create": {
3672
+ const prompt = stringField(input, "prompt");
3673
+ if (!prompt) return null;
3674
+ const cwd = stringField(input, "cwd");
3675
+ return {
3676
+ target: stringField(input, "title") ?? "new worker",
3677
+ meta: cwd ? [`in ${cwd}`] : [],
3678
+ detail: prompt
3679
+ };
3680
+ }
3681
+ case "send":
3682
+ case "steer": {
3683
+ const text = stringField(input, verb === "send" ? "prompt" : "message");
3684
+ if (!text || !ref) return null;
3685
+ return {
3686
+ ...ref,
3687
+ detail: text
3688
+ };
3689
+ }
3690
+ case "cancel":
3691
+ case "read": return ref;
3692
+ case "transcript": return ref ? {
3693
+ ...ref,
3694
+ meta: limit !== void 0 ? [`last ${limit} turns`] : []
3695
+ } : null;
3696
+ case "list": return {
3697
+ target: "all sessions",
3698
+ meta: limit !== void 0 ? [`limit ${limit}`] : []
3699
+ };
3700
+ default: return null;
3701
+ }
3702
+ }
2818
3703
  }
2819
3704
  };
2820
3705
  /**
@@ -3000,6 +3885,6 @@ function countNeighbors(turnIds, turnId) {
3000
3885
  };
3001
3886
  }
3002
3887
  //#endregion
3003
- export { applyEditPayload as $, SKILLS_TRIGGER as A, BASH_CALL_ID_PREFIX as B, MARKDOWN_RENDER_CHUNK_TARGET_BYTES as C, normalizeMarkdownCodeFences as D, codeTokenHasClosingFence as E, uniqueFilesFromReferences as F, isVisible as G, isEditErrorResult as H, COMMANDS_TRIGGER as I, retagUserPromptEvents as J, isVisibleDuringSearch as K, createCommandsCompletionProvider as L, uniqueSkillNamesFromReferences as M, FILES_TRIGGER as N, splitMarkdownCodeBlocks as O, createFilesCompletionProvider as P, turnSelectionOwnership as Q, blendHsl as R, parseCustomModelQuery as S, uniqueChatIdsFromReferences as St, closeDanglingCodeFence as T, isTurnHighlighted as U, EDIT_TOOL_NAMES as V, isUserBashEvent as W, selectionAfterTurnRemoval as X, selectableTurnIds as Y, streamEventSearchText as Z, turnContextSize as _, stripEditOutcomesAnnotation as _t, TOOL_DISPLAY as a, extractEditPayload as at, filterModelCatalog as b, chatReferenceTokensFromText as bt, MAX_TRANSCRIPT_EVENTS as c, splitLines as ct, finalizeStreamingMarkdown as d, buildEditOutcomesAnnotation as dt, buildContextualDiff as et, finalizeStreamingMarkdownForOwner as f, maskToOutcomeKinds as ft, revealEarlierTranscriptEvents as g, rewriteMultiEditHeader as gt, replaceTranscriptTurnEvents as h, resolveApprovalForPayload as ht, turnAsText as i, computeLineDiff as it, createSkillsCompletionProvider as j, stripHtmlCommentsOutsideCode as k, TRANSCRIPT_TRIM_TO_EVENTS as l, summarizeEditPayload as lt, ownerOf as m, parseEditOutcomesFromResult as mt, deleteTurnSafely as n, clipDiffForRender as nt, displayNameFor as o, filetypeFromPath as ot, hiddenEarlierTranscriptCount as p, mergeApprovalAndBodyOutcomes as pt, promptHistoryFromEvents as q, truncateTurnsAt as r, computeInlineDiff as rt, formatToolCall as s, previewEditPayload as st, countNeighbors as t, buildUnifiedDiff as tt, capTranscriptEvents as u, tokenize as ut, splitPromptSegments as v, summarizeOutcomes as vt, chunkMarkdownForRender as w, indexOfEntry as x, createChatsCompletionProvider as xt, buildModelCatalog as y, CHATS_TRIGGER as yt, buildLinearRamp as z };
3888
+ export { sanitizeModelChain as $, hasRunReceipt as $t, chunkMarkdownForRender as A, SESSION_EVENT_STATUS_STYLE as At, DAEMON_HEALTH_STYLE as B, sessionEventExcerpt as Bt, usageTone as C, createChatsCompletionProvider as Cn, selectableTurnIds as Ct, indexOfEntry as D, MAIN_PLAYBOOK_FRAGMENT as Dt, filterModelCatalog as E, turnSelectionOwnership as Et, stripHtmlCommentsOutsideCode as F, orchestratedBy as Ft, chainsEqualKeyed as G, RUN_RECEIPT_GLYPH as Gt, chainEntryKey as H, sessionEventMeta as Ht, cacheBreakChip as I, parseSessionEventXml as It, pickPrimaryKeyed as J, formatRunDuration as Jt, filterChainFallbacks as K, fitRunReceiptCells as Kt, compactingLabel as L, renderProjectsBriefing as Lt, codeTokenHasClosingFence as M, WORKER_REPORT_FRAGMENT as Mt, normalizeMarkdownCodeFences as N, artifactsFromTurns as Nt, parseCustomModelQuery as O, MAIN_SESSION_TITLE as Ot, splitMarkdownCodeBlocks as P, isMainSession as Pt, restoreChainFallbacks as Q, formatTokensPerSecond as Qt, retryLabel as R, renderSessionEventXml as Rt, resetLabel as S, chatReferenceTokensFromText as Sn, retagUserPromptEventsFromRefs as St, buildModelCatalog as T, streamEventSearchText as Tt, chainPositionOf as U, shortSessionId as Ut, buildRunModel as V, sessionEventLabel as Vt, chainsEqual as W, truncateTitle as Wt, providerKeySetChanged as X, formatRunReceiptLine as Xt, positionKeyed as Y, formatRunReceiptCells as Yt, reorderKeyed as Z, formatTokenCount as Zt, turnContextSize as _, resolveApprovalForPayload as _n, isVisible as _t, TOOL_DISPLAY as a, computeInlineDiff as an, FILES_TRIGGER as at, formatUsagePercent as b, summarizeOutcomes as bn, promptTurnRefs as bt, MAX_TRANSCRIPT_EVENTS as c, filetypeFromPath as cn, COMMANDS_TRIGGER as ct, finalizeStreamingMarkdown as d, summarizeEditPayload as dn, buildLinearRamp as dt, runReceiptFromRun as en, toggleChainEntry as et, finalizeStreamingMarkdownForOwner as f, tokenize as fn, BASH_CALL_ID_PREFIX as ft, revealEarlierTranscriptEvents as g, parseEditOutcomesFromResult as gn, isUserBashEvent as gt, replaceTranscriptTurnEvents as h, mergeApprovalAndBodyOutcomes as hn, isTurnHighlighted as ht, turnAsText as i, clipDiffForRender as in, uniqueSkillNamesFromReferences as it, closeDanglingCodeFence as j, SESSION_ROLE_KEY as jt, MARKDOWN_RENDER_CHUNK_TARGET_BYTES as k, ORCHESTRATED_BY_KEY as kt, TRANSCRIPT_TRIM_TO_EVENTS as l, previewEditPayload as ln, createCommandsCompletionProvider as lt, ownerOf as m, maskToOutcomeKinds as mn, isEditErrorResult as mt, deleteTurnSafely as n, buildContextualDiff as nn, SKILLS_TRIGGER as nt, displayNameFor as o, computeLineDiff as on, createFilesCompletionProvider as ot, hiddenEarlierTranscriptCount as p, buildEditOutcomesAnnotation as pn, EDIT_TOOL_NAMES as pt, pickChainPrimary as q, formatCostUsd as qt, truncateTurnsAt as r, buildUnifiedDiff as rn, createSkillsCompletionProvider as rt, formatToolCall as s, extractEditPayload as sn, uniqueFilesFromReferences as st, countNeighbors as t, applyEditPayload as tn, toggleKeyed as tt, capTranscriptEvents as u, splitLines as un, blendHsl as ut, formatBalance as v, rewriteMultiEditHeader as vn, isVisibleDuringSearch as vt, splitPromptSegments as w, uniqueChatIdsFromReferences as wn, selectionAfterTurnRemoval as wt, mostConstrainedWindow as x, CHATS_TRIGGER as xn, retagUserPromptEvents as xt, formatCredits as y, stripEditOutcomesAnnotation as yn, promptHistoryFromEvents as yt, DAEMON_HEALTH_GLYPH as z, sessionDirLabel as zt };
3004
3889
 
3005
- //# sourceMappingURL=turn-operations-mMxDRgLY.js.map
3890
+ //# sourceMappingURL=turn-operations-CZLsNgw-.js.map