zidane 6.6.4 → 7.0.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.
- package/dist/{acp-BQqhozmt.js → acp-Wr8l4IDI.js} +7 -180
- package/dist/acp-Wr8l4IDI.js.map +1 -0
- package/dist/acp-cli.js +6 -6
- package/dist/acp.d.ts +1 -1
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +2 -1
- package/dist/{agent-DTxH7RHW.js → agent-C-camKQW.js} +162 -12
- package/dist/agent-C-camKQW.js.map +1 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/{anthropic-DRNRnd6W.js → anthropic-BDOGzhtj.js} +3 -3
- package/dist/{anthropic-DRNRnd6W.js.map → anthropic-BDOGzhtj.js.map} +1 -1
- package/dist/{transcript-anchors-BjH-68k2.js → approvals-CqqxE5Vh.js} +4924 -275
- package/dist/approvals-CqqxE5Vh.js.map +1 -0
- package/dist/{auth-7rv6ZfWk.js → auth-BdwDROYg.js} +12 -8
- package/dist/{auth-7rv6ZfWk.js.map → auth-BdwDROYg.js.map} +1 -1
- package/dist/chat/pure.d.ts +221 -4
- package/dist/chat/pure.d.ts.map +1 -0
- package/dist/chat/pure.js +4 -2
- package/dist/chat.d.ts +6 -2911
- package/dist/chat.js +8 -261
- package/dist/{context-transport-D8yeGdpd.js → context-transport-BqYwzJYE.js} +2 -2
- package/dist/{context-transport-D8yeGdpd.js.map → context-transport-BqYwzJYE.js.map} +1 -1
- package/dist/contexts/daytona.d.ts +1 -1
- package/dist/contexts/daytona.js +1 -1
- package/dist/contexts/e2b.d.ts +1 -1
- package/dist/contexts/e2b.js +1 -1
- package/dist/daemon-cli.d.ts +1 -0
- package/dist/daemon-cli.js +90 -0
- package/dist/daemon-cli.js.map +1 -0
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +1 -1
- package/dist/extensions.d.ts +2 -2
- package/dist/extensions.js +2 -2
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +4 -4
- package/dist/{index-D0O-QO7Z.d.ts → index-B-FSaM3_.d.ts} +2863 -2375
- package/dist/index-B-FSaM3_.d.ts.map +1 -0
- package/dist/index-DgdzXRPH.d.ts +3590 -0
- package/dist/index-DgdzXRPH.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +14 -1179
- package/dist/ipc-CIv87xuH.js +331 -0
- package/dist/ipc-CIv87xuH.js.map +1 -0
- package/dist/ipc.d.ts +849 -0
- package/dist/ipc.d.ts.map +1 -0
- package/dist/ipc.js +4 -0
- package/dist/json-rpc-rSWMUh9b.js +192 -0
- package/dist/json-rpc-rSWMUh9b.js.map +1 -0
- package/dist/{logger-DBX9uYOw.js → logger--C1VQjwJ.js} +3 -13
- package/dist/logger--C1VQjwJ.js.map +1 -0
- package/dist/logs-view-CWG8aUAG.js +44 -0
- package/dist/logs-view-CWG8aUAG.js.map +1 -0
- package/dist/{mcp-CVtSQTWg.js → mcp-BIWa57eY.js} +3 -3
- package/dist/{mcp-CVtSQTWg.js.map → mcp-BIWa57eY.js.map} +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/{messages-CZMUuKzo.js → messages-21XfHxyF.js} +3 -3
- package/dist/{messages-CZMUuKzo.js.map → messages-21XfHxyF.js.map} +1 -1
- package/dist/{openai-compat-BoTi-9fd.js → openai-compat-DY-AKxCg.js} +2 -2
- package/dist/{openai-compat-BoTi-9fd.js.map → openai-compat-DY-AKxCg.js.map} +1 -1
- package/dist/output/stream-json.d.ts +1 -1
- package/dist/output/terminal.d.ts +1 -1
- package/dist/{plugins-onDvTHEm.js → plugins-B531Tz5b.js} +2 -2
- package/dist/{plugins-onDvTHEm.js.map → plugins-B531Tz5b.js.map} +1 -1
- package/dist/plugins.d.ts +1 -1
- package/dist/plugins.js +1 -1
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -1
- package/dist/providers/anthropic.d.ts +1 -1
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/arcee.js +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/baseten.js +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/cerebras.js +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/local.js +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +3 -3
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +1 -1
- package/dist/{providers-CmhdtD7w.js → providers-DX91jwir.js} +2 -2
- package/dist/{providers-CmhdtD7w.js.map → providers-DX91jwir.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +3 -3
- package/dist/restate.d.ts +1 -1
- package/dist/restate.js +8 -8
- package/dist/restate.js.map +1 -1
- package/dist/server-DJqp9dIR.js +1317 -0
- package/dist/server-DJqp9dIR.js.map +1 -0
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +42 -4
- package/dist/session/sqlite.js.map +1 -1
- package/dist/{session-CyRLSsVa.js → session-K3icbPH9.js} +8 -2
- package/dist/session-K3icbPH9.js.map +1 -0
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/{skills-BMGH_KK1.js → skills-PJyiSRCp.js} +3 -21
- package/dist/skills-PJyiSRCp.js.map +1 -0
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +1 -1
- package/dist/src-DzzhE2YO.js +2461 -0
- package/dist/src-DzzhE2YO.js.map +1 -0
- package/dist/todo-edits-Dw2oO2ON.js +378 -0
- package/dist/todo-edits-Dw2oO2ON.js.map +1 -0
- package/dist/{tool-formatters-NrzD6Fkk.d.ts → tool-formatters-DO56YBBw.d.ts} +104 -4
- package/dist/tool-formatters-DO56YBBw.d.ts.map +1 -0
- package/dist/tools/fetch-url.d.ts +1 -1
- package/dist/tools/web-search.d.ts +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +1 -1
- package/dist/{transcript-anchors-UhC58Tmj.d.ts → transcript-anchors-DXqWWGbq.d.ts} +53 -4
- package/dist/transcript-anchors-DXqWWGbq.d.ts.map +1 -0
- package/dist/tui.d.ts +94 -13
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +2241 -1009
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-mMxDRgLY.js → turn-operations-CZLsNgw-.js} +896 -11
- package/dist/turn-operations-CZLsNgw-.js.map +1 -0
- package/dist/{turn-operations-Cuc9pwiB.d.ts → turn-operations-DKFYWP3O.d.ts} +501 -4
- package/dist/turn-operations-DKFYWP3O.d.ts.map +1 -0
- package/dist/{types-BIarq1qE.js → types-CWgENVLY.js} +24 -2
- package/dist/types-CWgENVLY.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/xml-D5HBhJle.js +22 -0
- package/dist/xml-D5HBhJle.js.map +1 -0
- package/docs/CHAT.md +39 -0
- package/docs/IPC.md +420 -0
- package/docs/TUI.md +62 -0
- package/package.json +9 -3
- package/dist/acp-BQqhozmt.js.map +0 -1
- package/dist/agent-DTxH7RHW.js.map +0 -1
- package/dist/chat.d.ts.map +0 -1
- package/dist/chat.js.map +0 -1
- package/dist/index-D0O-QO7Z.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/inline-autocomplete-D0MPvBaS.js +0 -596
- package/dist/inline-autocomplete-D0MPvBaS.js.map +0 -1
- package/dist/logger-DBX9uYOw.js.map +0 -1
- package/dist/login-CB23LyI1.js +0 -707
- package/dist/login-CB23LyI1.js.map +0 -1
- package/dist/session-CyRLSsVa.js.map +0 -1
- package/dist/skills-BMGH_KK1.js.map +0 -1
- package/dist/tool-formatters-NrzD6Fkk.d.ts.map +0 -1
- package/dist/transcript-anchors-BjH-68k2.js.map +0 -1
- package/dist/transcript-anchors-UhC58Tmj.d.ts.map +0 -1
- package/dist/turn-operations-Cuc9pwiB.d.ts.map +0 -1
- package/dist/turn-operations-mMxDRgLY.js.map +0 -1
- 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(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/&/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
|
-
|
|
1118
|
-
|
|
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
|
|
1122
|
-
const texts = new Set(
|
|
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) ||
|
|
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:
|
|
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 {
|
|
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-
|
|
3890
|
+
//# sourceMappingURL=turn-operations-CZLsNgw-.js.map
|