scream-code 0.12.6 → 0.12.8

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.
@@ -7,7 +7,7 @@ import { i as __require, o as __toESM, r as __exportAll, t as __commonJSMin } fr
7
7
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
8
8
  import { C as join$1, D as resolve$1, E as relative$1, S as isAbsolute$1, T as parse$7, a as isSupportedFile, b as basename$1, i as ingestFile, r as ingestDirectory, t as multiSearch, w as normalize, x as dirname$2, y as KnowledgeStore } from "./src-BH9W5k24.mjs";
9
9
  import { t as require_base64_js } from "./base64-js-DzVmk6Nb.mjs";
10
- import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-BI6Q02oM.mjs";
10
+ import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-9ywV88Lh.mjs";
11
11
  import { createRequire } from "node:module";
12
12
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
13
13
  import * as fs$1 from "node:fs/promises";
@@ -5600,7 +5600,7 @@ var _BetaToolRunner_instances, _BetaToolRunner_consumed, _BetaToolRunner_mutated
5600
5600
  /**
5601
5601
  * Just Promise.withResolvers(), which is not available in all environments.
5602
5602
  */
5603
- function promiseWithResolvers() {
5603
+ function promiseWithResolvers$1() {
5604
5604
  let resolve;
5605
5605
  let reject;
5606
5606
  return {
@@ -5646,7 +5646,7 @@ var BetaToolRunner = class {
5646
5646
  ...options,
5647
5647
  headers: buildHeaders$2([{ "x-stainless-helper": helperValue }, options?.headers])
5648
5648
  }, "f");
5649
- __classPrivateFieldSet$1(this, _BetaToolRunner_completion, promiseWithResolvers(), "f");
5649
+ __classPrivateFieldSet$1(this, _BetaToolRunner_completion, promiseWithResolvers$1(), "f");
5650
5650
  if (params.compactionControl?.enabled) console.warn("Anthropic: The `compactionControl` parameter is deprecated and will be removed in a future version. Use server-side compaction instead by passing `edits: [{ type: \"compact_20260112\" }]` in the params passed to `toolRunner()`. See https://platform.claude.com/docs/en/build-with-claude/compaction");
5651
5651
  }
5652
5652
  async *[(_BetaToolRunner_consumed = /* @__PURE__ */ new WeakMap(), _BetaToolRunner_mutated = /* @__PURE__ */ new WeakMap(), _BetaToolRunner_state = /* @__PURE__ */ new WeakMap(), _BetaToolRunner_options = /* @__PURE__ */ new WeakMap(), _BetaToolRunner_message = /* @__PURE__ */ new WeakMap(), _BetaToolRunner_toolResponse = /* @__PURE__ */ new WeakMap(), _BetaToolRunner_completion = /* @__PURE__ */ new WeakMap(), _BetaToolRunner_iterationCount = /* @__PURE__ */ new WeakMap(), _BetaToolRunner_instances = /* @__PURE__ */ new WeakSet(), _BetaToolRunner_checkAndCompact = async function _BetaToolRunner_checkAndCompact() {
@@ -5742,7 +5742,7 @@ var BetaToolRunner = class {
5742
5742
  __classPrivateFieldSet$1(this, _BetaToolRunner_consumed, false, "f");
5743
5743
  __classPrivateFieldGet$1(this, _BetaToolRunner_completion, "f").promise.catch(() => {});
5744
5744
  __classPrivateFieldGet$1(this, _BetaToolRunner_completion, "f").reject(error);
5745
- __classPrivateFieldSet$1(this, _BetaToolRunner_completion, promiseWithResolvers(), "f");
5745
+ __classPrivateFieldSet$1(this, _BetaToolRunner_completion, promiseWithResolvers$1(), "f");
5746
5746
  throw error;
5747
5747
  }
5748
5748
  }
@@ -76344,14 +76344,84 @@ function maybeStatusCode(error) {
76344
76344
  }
76345
76345
  //#endregion
76346
76346
  //#region ../../packages/agent-core/src/agent/context/projector.ts
76347
- function project(history) {
76348
- const usable = history.filter((message) => {
76347
+ /** Synthetic error text used when a tool result is missing and must be
76348
+ * filled in so the provider accepts the message sequence. */
76349
+ const SYNTHETIC_TOOL_RESULT_TEXT = "<system>ERROR: The tool call did not complete because of an interruption. Do not assume the tool executed successfully, and do not invent its result.</system>";
76350
+ function project(history, options) {
76351
+ return repairToolExchangeAdjacency(mergeAdjacentUserMessages(history.filter((message) => {
76349
76352
  return message.partial !== true && !(message.role === "assistant" && message.content.length === 0 && message.toolCalls.length === 0);
76350
- });
76351
- const last = usable.at(-1);
76352
- return mergeAdjacentUserMessages(last?.role === "assistant" && last.toolCalls.length > 0 ? usable.slice(0, -1) : usable);
76353
+ }), options?.onAnomaly), options);
76353
76354
  }
76354
- function mergeAdjacentUserMessages(history) {
76355
+ /**
76356
+ * Closes every tool exchange whose assistant `tool_use` is not fully answered
76357
+ * by a matching `tool_result`. A mid-history orphan (a later user/assistant
76358
+ * message follows) can never be in-flight, so it is always closed by
76359
+ * synthesizing an error result. A trailing orphan is closed only when
76360
+ * `synthesizeMissing` is set — otherwise it is left for the trim / replay
76361
+ * synthesis. This prevents the provider error "must be followed by tool
76362
+ * messages responding to each tool_call_id" after an interruption (e.g. a
76363
+ * network drop mid-batch).
76364
+ */
76365
+ function repairToolExchangeAdjacency(messages, options) {
76366
+ let lastNonToolIndex = messages.length - 1;
76367
+ while (lastNonToolIndex >= 0 && messages[lastNonToolIndex]?.role === "tool") lastNonToolIndex -= 1;
76368
+ const out = [];
76369
+ const consumed = /* @__PURE__ */ new Set();
76370
+ for (let i = 0; i < messages.length; i++) {
76371
+ if (consumed.has(i)) continue;
76372
+ const message = messages[i];
76373
+ if (message.role !== "assistant" || message.toolCalls.length === 0) {
76374
+ out.push(message);
76375
+ continue;
76376
+ }
76377
+ out.push(message);
76378
+ const pending = new Set(message.toolCalls.map((toolCall) => toolCall.id));
76379
+ let foreignBetween = false;
76380
+ for (let j = i + 1; j < messages.length && pending.size > 0; j++) {
76381
+ if (consumed.has(j)) continue;
76382
+ const next = messages[j];
76383
+ const toolCallId = next.toolCallId;
76384
+ if (next.role === "tool" && toolCallId !== void 0 && pending.has(toolCallId)) {
76385
+ out.push(next);
76386
+ consumed.add(j);
76387
+ pending.delete(toolCallId);
76388
+ if (foreignBetween) options?.onAnomaly?.({
76389
+ kind: "tool_result_reordered",
76390
+ toolCallId
76391
+ });
76392
+ } else foreignBetween = true;
76393
+ }
76394
+ const isMidHistory = i < lastNonToolIndex;
76395
+ if (options?.synthesizeMissing === true || isMidHistory) for (const missingId of pending) {
76396
+ out.push(makeSyntheticToolResult(missingId));
76397
+ options?.onAnomaly?.({
76398
+ kind: "tool_result_synthesized",
76399
+ toolCallId: missingId,
76400
+ trailing: !isMidHistory
76401
+ });
76402
+ }
76403
+ }
76404
+ return out;
76405
+ }
76406
+ function makeSyntheticToolResult(toolCallId) {
76407
+ return {
76408
+ role: "tool",
76409
+ content: [{
76410
+ type: "text",
76411
+ text: SYNTHETIC_TOOL_RESULT_TEXT
76412
+ }],
76413
+ toolCalls: [],
76414
+ toolCallId
76415
+ };
76416
+ }
76417
+ /**
76418
+ * Drops a trailing open tool exchange from a projected message list: when the
76419
+ * last assistant message carries tool calls whose results never arrived and
76420
+ * nothing follows, truncate from that batch so the compacted history does not
76421
+ * carry an unterminated exchange (which would be rejected or, if synthesized,
76422
+ * pollute the compaction prompt).
76423
+ */
76424
+ function mergeAdjacentUserMessages(history, _onAnomaly) {
76355
76425
  const out = [];
76356
76426
  for (const message of history) {
76357
76427
  const previous = out.at(-1);
@@ -76395,10 +76465,10 @@ function stripContextMetadata(message) {
76395
76465
  }
76396
76466
  //#endregion
76397
76467
  //#region ../../packages/agent-core/src/agent/compaction/compaction-instruction.md
76398
- var compaction_instruction_default = "\n--- This message is a direct task, not part of the above conversation ---\n\nYou are now given a task to compact this conversation context according to specific priorities and output requirements.\n\nOutput text only. DO NOT CALL ANY TOOLS. Calling tools will be rejected and fails the task. You already have all the information you need in the conversation history. You have only one chance.\n\nThe goal of compaction is to keep essential code patterns, technical details, and architectural decisions for continuing development without losing context after the above messages are cleared work.\n\n{{ customInstruction }}\n\n<!-- Memory Memo Extraction (PRIORITY — do not skip) -->\n\n## 任务经验提取\n\nAFTER completing the compaction summary below, scan the messages being compacted for **completed task loops**. A task loop is \"completed\" when:\n- The user made a clear request or asked a specific question\n- You provided a solution or answer\n- The outcome is clear (success, partial success, or failure)\n\nFor each completed task loop found, output a structured experience record **at the very end of your response**:\n\n```memory-memo\n{\n \"userNeed\": \"<the user's need or goal, one sentence>\",\n \"approach\": \"<what was done — the approach taken, 2-4 sentences>\",\n \"outcome\": \"<final result, e.g. '完成', '部分完成', '失败: reason'>\",\n \"whatFailed\": \"<dead ends tried — things that didn't work, or 'none'>\",\n \"whatWorked\": \"<key actions that ultimately worked, or 'none'>\",\n \"tags\": [\"<tag1>\", \"<tag2>\", \"<tag3>\"],\n \"note\": \"<optional: a soft, AI-readable note/suggestion that makes the record easier for future turns to understand — e.g. '这类任务先查接口可用性再写代码'. Omit if none>\"\n}\n```\n\nGuidelines:\n- Record important failed attempts in \"whatFailed\" to help avoid repeating mistakes.\n- Record key successful actions in \"whatWorked\" to help reuse effective approaches.\n- Include 3-5 semantic \"tags\" summarizing the task domain, tech stack, or action type (e.g. [\"react\", \"auth\", \"部署\"]).\n- \"note\" is optional: a one-sentence advisory note that helps future AI understand/reuse the record faster. It is a soft suggestion, not a user-enforced rule.\n- Skip in-progress work unless it contains a valuable error+fix experience.\n- Merge closely related sub-tasks into a single record.\n- Use the exact field names and JSON format shown above (no extra fields beyond \"note\").\n\nIf no completed task loops are found in the compacted messages, output:\n```memory-memo\n{\"none\": true}\n```\n\n<!-- Compression Priorities (in order) -->\n\n1. **Current Task State**: What is being worked on RIGHT NOW\n2. **Errors & Solutions**: All encountered errors and their resolutions\n3. **Code Evolution**: Final working versions only (remove intermediate attempts)\n4. **System Context**: Project structure, dependencies, environment setup\n5. **Design Decisions**: Architectural choices and their rationale\n6. **TODO Items**: Unfinished tasks and known issues\n\n<!-- Required Output Structure -->\n\n## Current Focus\n\n[What we're working on now]\n\n## Environment\n\n- [Key setup/config points]\n- ...\n\n## Completed Tasks\n\n- [Task]: [Brief outcome]\n- ...\n\n## Active Issues\n\n- [Issue]: [Status/Next steps]\n- ...\n\n## Code State\n\n### [Critical file name]\n\n[Brief description of the file's purpose and current state]\n\n```\n[The latest version of critical code snippets in this file, <20 lines]\n```\n\n### [Critical file name]\n\n- [Useful classes/methods/functions]: [Brief description/usage]\n- ...\n\n<!-- Omit non-critical code, intermediate attempts, and resolved errors -->\n\n## Important Context\n\n- [Any crucial information not covered above]\n- ...\n\n## All User Messages\n\n- [Detailed non tool use user message]\n- ...\n\n## Skill candidates\n\nScan the compressed messages for reusable processes (project-specific build\nsteps, recurring debugging patterns, or tool workflows). If any is genuinely\nworth capturing as a reusable skill, output at the very end, at most one:\n\n[[skill-candidate: <name>|<one-line purpose>|<evidence>]]\n\nOmit if none.\n";
76468
+ var compaction_instruction_default = "\n--- This message is a direct task, not part of the above conversation ---\n\nYou are now given a task to compact this conversation context according to specific priorities and output requirements.\n\nOutput text only. DO NOT CALL ANY TOOLS. Calling tools will be rejected and fails the task. You already have all the information you need in the conversation history. You have only one chance.\n\nThe goal of compaction is to keep essential code patterns, technical details, and architectural decisions for continuing development without losing context after the above messages are cleared work.\n\n{{ customInstruction }}\n\n<!-- Memory Memo Extraction (PRIORITY — do not skip) -->\n\n## 任务经验提取\n\nAFTER completing the compaction summary below, scan the messages being compacted for **task loops**. A task loop is \"completed\" when:\n- The user made a clear request or asked a specific question\n- You provided a solution or answer\n- The outcome is clear (success, partial success, or failure)\n\n**You MUST output at least one memory-memo block** (or the `{\"none\": true}` marker) at the very end of your response — omitting the section entirely is not allowed. Record completed task loops as full experience records; record **in-progress work** as a lower-priority record whose \"outcome\" is \"进行中\" (so the ongoing task survives compaction and can be resumed later).\n\nFor each task loop found, output a structured experience record **at the very end of your response**:\n\n```memory-memo\n{\n \"userNeed\": \"<the user's need or goal, one sentence>\",\n \"approach\": \"<what was done — the approach taken, 2-4 sentences>\",\n \"outcome\": \"<final result, e.g. '完成', '部分完成', '失败: reason', or '进行中' for in-progress work>\",\n \"whatFailed\": \"<dead ends tried — things that didn't work, or 'none'>\",\n \"whatWorked\": \"<key actions that ultimately worked, or 'none'>\",\n \"tags\": [\"<tag1>\", \"<tag2>\", \"<tag3>\"],\n \"note\": \"<optional: a soft, AI-readable note/suggestion that makes the record easier for future turns to understand — e.g. '这类任务先查接口可用性再写代码'. Omit if none>\"\n}\n```\n\nGuidelines:\n- Record important failed attempts in \"whatFailed\" to help avoid repeating mistakes.\n- Record key successful actions in \"whatWorked\" to help reuse effective approaches.\n- Include 3-5 semantic \"tags\" summarizing the task domain, tech stack, or action type (e.g. [\"react\", \"auth\", \"部署\"]).\n- \"note\" is optional: a one-sentence advisory note that helps future AI understand/reuse the record faster. It is a soft suggestion, not a user-enforced rule.\n- For in-progress work: record it with \"outcome\": \"进行中\" and describe where the task stands and what remains — this is what lets the agent resume seamlessly after compaction.\n- Merge closely related sub-tasks into a single record.\n- Use the exact field names and JSON format shown above (no extra fields beyond \"note\").\n\nIf no task loops (completed or in-progress) are found in the compacted messages, output:\n```memory-memo\n{\"none\": true}\n```\n\n<!-- Compression Priorities (in order) -->\n\n1. **Current Task State**: What is being worked on RIGHT NOW\n2. **Errors & Solutions**: All encountered errors and their resolutions\n3. **Code Evolution**: Final working versions only (remove intermediate attempts)\n4. **System Context**: Project structure, dependencies, environment setup\n5. **Design Decisions**: Architectural choices and their rationale\n6. **Next Steps**: The ordered, actionable plan going forward\n7. **TODO Items**: Unfinished tasks and known issues\n\n<!-- Required Output Structure -->\n\n## Current Focus\n\n[What we're working on now]\n\n## Environment\n\n- [Key setup/config points]\n- ...\n\n## Completed Tasks\n\n- [Task]: [Brief outcome]\n- ...\n\n## Active Issues\n\n- [Issue]: [Status/Next steps]\n- ...\n\n## Key Decisions\n\n- [Decision]: [rationale] — preserve architectural choices and why they were made\n- ...\n\n## Next Steps\n\n1. [The very next actionable step]\n2. [Then this]\n3. ...\n\n## Code State\n\n### [Critical file name]\n\n[Brief description of the file's purpose and current state]\n\n```\n[The latest version of critical code snippets in this file, <20 lines]\n```\n\n### [Critical file name]\n\n- [Useful classes/methods/functions]: [Brief description/usage]\n- ...\n\n<!-- Omit non-critical code, intermediate attempts, and resolved errors -->\n\n## Important Context\n\n- [Any crucial information not covered above]\n- ...\n\n## All User Messages\n\n- [Detailed non tool use user message]\n- ...\n\n## Skill candidates\n\nScan the compressed messages for reusable processes (project-specific build\nsteps, recurring debugging patterns, or tool workflows). If any is genuinely\nworth capturing as a reusable skill, output at the very end, at most one:\n\n[[skill-candidate: <name>|<one-line purpose>|<evidence>]]\n\n- The evidence must be concrete and verifiable: exact file paths, commands, or\n step sequences from the conversation (e.g. \"run `pnpm vitest run -t compaction`\n in packages/agent-core to reproduce the compaction snapshots\"). Vague evidence\n like \"used a script\" is not acceptable.\n- Only emit a candidate when the process is genuinely reusable beyond this one\n task; do not emit for one-off actions.\n\nOmit if none.\n";
76399
76469
  //#endregion
76400
76470
  //#region ../../packages/agent-core/src/agent/compaction/compaction-update-instruction.md
76401
- var compaction_update_instruction_default = "\n--- This message is a direct task, not part of the above conversation ---\n\nYou are now given a task to UPDATE an existing compaction summary with new messages that came in after the last compaction.\n\nA previous compaction summary already exists at the top of the conversation (the first assistant message). You must NOT discard it. Instead, merge the new information into the existing structure, keeping all previously captured state intact unless explicitly contradicted or resolved by the new messages.\n\nOutput text only. DO NOT CALL ANY TOOLS. Calling tools will be rejected and fails the task. You have only one chance.\n\n{{ customInstruction }}\n\n<!-- Previous Summary Reference -->\n\nThe first assistant message above is the PREVIOUS summary. Treat it as the source of truth for everything that happened before the new messages. Do not restate it verbatim — produce an updated, merged summary.\n\n<!-- Memory Memo Extraction (PRIORITY — do not skip) -->\n\n## 任务经验提取\n\nAFTER completing the updated summary below, scan ONLY the new messages being compacted (not those already covered by the previous summary) for **completed task loops**. A task loop is \"completed\" when:\n- The user made a clear request or asked a specific question\n- You provided a solution or answer\n- The outcome is clear (success, partial success, or failure)\n\nFor each completed task loop found, output a structured experience record **at the very end of your response**:\n\n```memory-memo\n{\n \"userNeed\": \"<the user's need or goal, one sentence>\",\n \"approach\": \"<what was done — the approach taken, 2-4 sentences>\",\n \"outcome\": \"<final result, e.g. '完成', '部分完成', '失败: reason'>\",\n \"whatFailed\": \"<dead ends tried — things that didn't work, or 'none'>\",\n \"whatWorked\": \"<key actions that ultimately worked, or 'none'>\",\n \"tags\": [\"<tag1>\", \"<tag2>\", \"<tag3>\"]\n}\n```\n\nGuidelines:\n- Record important failed attempts in \"whatFailed\" to help avoid repeating mistakes.\n- Record key successful actions in \"whatWorked\" to help reuse effective approaches.\n- Include 3-5 semantic \"tags\" summarizing the task domain, tech stack, or action type.\n- Skip in-progress work unless it contains a valuable error+fix experience.\n- Merge closely related sub-tasks into a single record.\n- Use the exact field names and JSON format shown above.\n\nIf no completed task loops are found in the new compacted messages, output:\n```memory-memo\n{\"none\": true}\n```\n\n<!-- Update Rules -->\n\n1. Preserve the section structure of the previous summary (Current Focus, Environment, Completed Tasks, Active Issues, Code State, Important Context, All User Messages).\n2. Move newly-completed tasks from \"Current Focus\" / \"Active Issues\" into \"Completed Tasks\".\n3. Update \"Current Focus\" to reflect what is being worked on RIGHT NOW.\n4. Append new user messages to \"All User Messages\" — do not repeat those already captured.\n5. Refresh \"Code State\" code snippets only if newer versions exist in the new messages.\n6. Drop resolved issues from \"Active Issues\"; add newly-discovered ones.\n7. Do not invent new information. If the new messages say nothing about a section, carry the previous content forward unchanged.\n\n<!-- Required Output Structure -->\n\n## Current Focus\n\n[What we're working on now]\n\n## Environment\n\n- [Key setup/config points]\n- ...\n\n## Completed Tasks\n\n- [Task]: [Brief outcome]\n- ...\n\n## Active Issues\n\n- [Issue]: [Status/Next steps]\n- ...\n\n## Code State\n\n### [Critical file name]\n\n[Brief description of the file's purpose and current state]\n\n```\n[The latest version of critical code snippets in this file, <20 lines]\n```\n\n## Important Context\n\n- [Any crucial information not covered above]\n- ...\n\n## All User Messages\n\n- [Detailed non tool use user message]\n- ...\n";
76471
+ var compaction_update_instruction_default = "\n--- This message is a direct task, not part of the above conversation ---\n\nYou are now given a task to UPDATE an existing compaction summary with new messages that came in after the last compaction.\n\nA previous compaction summary already exists at the top of the conversation (the first assistant message). You must NOT discard it. Instead, merge the new information into the existing structure, keeping all previously captured state intact unless explicitly contradicted or resolved by the new messages.\n\nOutput text only. DO NOT CALL ANY TOOLS. Calling tools will be rejected and fails the task. You have only one chance.\n\n{{ customInstruction }}\n\n<!-- Previous Summary Reference -->\n\nThe first assistant message above is the PREVIOUS summary. Treat it as the source of truth for everything that happened before the new messages. Do not restate it verbatim — produce an updated, merged summary.\n\n<!-- Memory Memo Extraction (PRIORITY — do not skip) -->\n\n## 任务经验提取\n\nAFTER completing the updated summary below, scan ONLY the new messages being compacted (not those already covered by the previous summary) for **task loops**. A task loop is \"completed\" when:\n- The user made a clear request or asked a specific question\n- You provided a solution or answer\n- The outcome is clear (success, partial success, or failure)\n\n**You MUST output at least one memory-memo block** (or the `{\"none\": true}` marker) at the very end of your response — omitting the section entirely is not allowed. Record completed task loops as full experience records; record **in-progress work** as a lower-priority record whose \"outcome\" is \"进行中\" (so the ongoing task survives compaction and can be resumed later).\n\nFor each task loop found, output a structured experience record **at the very end of your response**:\n\n```memory-memo\n{\n \"userNeed\": \"<the user's need or goal, one sentence>\",\n \"approach\": \"<what was done — the approach taken, 2-4 sentences>\",\n \"outcome\": \"<final result, e.g. '完成', '部分完成', '失败: reason', or '进行中' for in-progress work>\",\n \"whatFailed\": \"<dead ends tried — things that didn't work, or 'none'>\",\n \"whatWorked\": \"<key actions that ultimately worked, or 'none'>\",\n \"tags\": [\"<tag1>\", \"<tag2>\", \"<tag3>\"],\n \"note\": \"<optional: a soft, AI-readable note/suggestion that makes the record easier for future turns to understand — e.g. '这类任务先查接口可用性再写代码'. Omit if none>\"\n}\n```\n\nGuidelines:\n- Record important failed attempts in \"whatFailed\" to help avoid repeating mistakes.\n- Record key successful actions in \"whatWorked\" to help reuse effective approaches.\n- Include 3-5 semantic \"tags\" summarizing the task domain, tech stack, or action type.\n- For in-progress work: record it with \"outcome\": \"进行中\" and describe where the task stands and what remains — this is what lets the agent resume seamlessly after compaction.\n- Merge closely related sub-tasks into a single record.\n- Use the exact field names and JSON format shown above.\n\nIf no task loops (completed or in-progress) are found in the new compacted messages, output:\n```memory-memo\n{\"none\": true}\n```\n\n<!-- Update Rules -->\n\n1. Preserve the section structure of the previous summary (Current Focus, Environment, Completed Tasks, Active Issues, Key Decisions, Next Steps, Code State, Important Context, All User Messages).\n2. Move newly-completed tasks from \"Current Focus\" / \"Active Issues\" into \"Completed Tasks\".\n3. Update \"Current Focus\" to reflect what is being worked on RIGHT NOW.\n4. Append new user messages to \"All User Messages\" — do not repeat those already captured.\n5. Refresh \"Code State\" code snippets only if newer versions exist in the new messages.\n6. Drop resolved issues from \"Active Issues\"; add newly-discovered ones.\n7. Merge new decisions into \"Key Decisions\" (keep prior decisions unless explicitly overturned); merge new steps into \"Next Steps\" (reorder if priorities changed).\n8. Do not invent new information. If the new messages say nothing about a section, carry the previous content forward unchanged.\n\n<!-- Required Output Structure -->\n\n## Current Focus\n\n[What we're working on now]\n\n## Environment\n\n- [Key setup/config points]\n- ...\n\n## Completed Tasks\n\n- [Task]: [Brief outcome]\n- ...\n\n## Active Issues\n\n- [Issue]: [Status/Next steps]\n- ...\n\n## Key Decisions\n\n- [Decision]: [rationale] — preserve architectural choices and why they were made\n- ...\n\n## Next Steps\n\n1. [The very next actionable step]\n2. [Then this]\n3. ...\n\n## Code State\n\n### [Critical file name]\n\n[Brief description of the file's purpose and current state]\n\n```\n[The latest version of critical code snippets in this file, <20 lines]\n```\n\n## Important Context\n\n- [Any crucial information not covered above]\n- ...\n\n## All User Messages\n\n- [Detailed non tool use user message]\n- ...\n";
76402
76472
  //#endregion
76403
76473
  //#region ../../packages/agent-core/src/agent/compaction/render-messages.ts
76404
76474
  const TOOL_RESULT_MAX_CHARS = 2e3;
@@ -76490,8 +76560,9 @@ const DEFAULT_COMPACTION_CONFIG = {
76490
76560
  blockRatio: .85,
76491
76561
  reservedContextSize: 5e4,
76492
76562
  maxCompactionPerTurn: 3,
76493
- maxRecentMessages: 4,
76494
- maxRecentUserMessages: 2,
76563
+ maxRecentMessages: 16,
76564
+ maxRecentUserMessages: 8,
76565
+ maxRecentTokens: 2e4,
76495
76566
  maxRecentSizeRatio: .2,
76496
76567
  minOverflowReductionRatio: .05,
76497
76568
  turnGrowthMultiplier: 2.5
@@ -76533,7 +76604,7 @@ var DefaultCompactionStrategy = class {
76533
76604
  if (m2.role === "user") recentUserMessages++;
76534
76605
  recentSize += estimateTokensForMessage(m2);
76535
76606
  if (canSplitAfter(messages, splitIndex)) bestN = splitIndex + 1;
76536
- if ((recentMessages >= this.config.maxRecentMessages || recentUserMessages >= this.config.maxRecentUserMessages || recentSize >= this.maxSize * this.config.maxRecentSizeRatio) && bestN !== void 0) break;
76607
+ if ((recentMessages >= this.config.maxRecentMessages || recentUserMessages >= this.config.maxRecentUserMessages || recentSize >= this.config.maxRecentTokens || recentSize >= this.maxSize * this.config.maxRecentSizeRatio) && bestN !== void 0) break;
76537
76608
  }
76538
76609
  return bestN ?? 0;
76539
76610
  }
@@ -76621,12 +76692,23 @@ function extractFileOpsFromMessage(message, ops) {
76621
76692
  }
76622
76693
  }
76623
76694
  }
76695
+ /** Projects a `FileOperations` accumulator into the persistent, sorted
76696
+ * file lists stored on `CompactionResult`. Unlike `formatFileOperations`
76697
+ * this keeps the full list (no `FILE_LIMIT` elision) so later compactions
76698
+ * can merge the previous round's file context without losing entries. */
76699
+ function computeFileLists(ops) {
76700
+ const modified = new Set([...ops.edited, ...ops.written]);
76701
+ return {
76702
+ readFiles: [...ops.read].toSorted(),
76703
+ modifiedFiles: [...modified].toSorted()
76704
+ };
76705
+ }
76624
76706
  const FILE_LIMIT = 20;
76625
76707
  function formatFileOperations(ops) {
76626
76708
  const modified = new Set([...ops.edited, ...ops.written]);
76627
- const readOnly = [...ops.read].filter((f) => !modified.has(f)).sort();
76628
- const modifiedFiles = [...modified].sort();
76629
- const all = [...new Set([...readOnly, ...modifiedFiles])].sort();
76709
+ const readOnly = [...ops.read].filter((f) => !modified.has(f)).toSorted();
76710
+ const modifiedFiles = [...modified].toSorted();
76711
+ const all = [...new Set([...readOnly, ...modifiedFiles])].toSorted();
76630
76712
  if (all.length === 0) return "";
76631
76713
  const mode = /* @__PURE__ */ new Map();
76632
76714
  for (const f of readOnly) mode.set(f, "Read");
@@ -76769,6 +76851,10 @@ var FullCompaction = class {
76769
76851
  * limit → overflow → compact cycle from consuming the entire
76770
76852
  * maxCompactionPerTurn budget with marginal savings. */
76771
76853
  reactiveAttempted = false;
76854
+ /** File lists from the most recent successful compaction. Merged into the
76855
+ * next compaction's file operations so file context accumulates across
76856
+ * repeated compactions instead of being reset each round. */
76857
+ lastCompactionFiles;
76772
76858
  compacting = null;
76773
76859
  _compactedHistory = [];
76774
76860
  strategy;
@@ -77000,15 +77086,25 @@ var FullCompaction = class {
77000
77086
  const recent = originalHistory.slice(compactedCount);
77001
77087
  const messagesToCompactForOps = originalHistory.slice(0, compactedCount);
77002
77088
  const fileOps = createFileOps();
77089
+ if (this.lastCompactionFiles !== void 0 && extractPreviousSummary(originalHistory) !== null) {
77090
+ for (const f of this.lastCompactionFiles.readFiles) fileOps.read.add(f);
77091
+ for (const f of this.lastCompactionFiles.modifiedFiles) fileOps.edited.add(f);
77092
+ } else this.lastCompactionFiles = void 0;
77003
77093
  for (const msg of messagesToCompactForOps) extractFileOpsFromMessage(msg, fileOps);
77004
77094
  const toolCallHistory = formatToolCallHistory(messagesToCompactForOps);
77005
- const processedSummary = this.postProcessSummary(summary, fileOps, toolCallHistory);
77095
+ const processedSummary = this.postProcessSummary(summary, fileOps, toolCallHistory, compactedCount);
77006
77096
  const tokensAfter = estimateTokens$1(processedSummary) + estimateTokensForMessages(recent);
77097
+ const fileLists = computeFileLists(fileOps);
77098
+ const MAX_PERSISTED_FILES = 100;
77099
+ const readFiles = fileLists.readFiles.slice(0, MAX_PERSISTED_FILES);
77100
+ const modifiedFiles = fileLists.modifiedFiles.slice(0, MAX_PERSISTED_FILES);
77007
77101
  const result = {
77008
77102
  summary: processedSummary,
77009
77103
  compactedCount,
77010
77104
  tokensBefore,
77011
77105
  tokensAfter,
77106
+ ...readFiles.length > 0 ? { readFiles } : {},
77107
+ ...modifiedFiles.length > 0 ? { modifiedFiles } : {},
77012
77108
  ...isUpdate ? { isUpdate: true } : {}
77013
77109
  };
77014
77110
  this.markCompleted();
@@ -77017,8 +77113,9 @@ var FullCompaction = class {
77017
77113
  result
77018
77114
  });
77019
77115
  this.agent.context.applyCompaction(result);
77116
+ this.lastCompactionFiles = fileLists;
77020
77117
  this.lowWaterMark = Math.floor(this.effectiveTokenCount * 1.1);
77021
- await this.extractAndStoreMemos(processedSummary);
77118
+ await this.extractAndStoreMemos(processedSummary, messagesToCompactForOps);
77022
77119
  this.triggerPostCompactHook(data, result);
77023
77120
  this.detectSkillCandidates(processedSummary);
77024
77121
  this.consecutiveCompactionFailures = 0;
@@ -77082,16 +77179,27 @@ var FullCompaction = class {
77082
77179
  }
77083
77180
  });
77084
77181
  }
77085
- /** Extract memory memos from compaction summary and store them. */
77086
- async extractAndStoreMemos(summary) {
77182
+ /** Extract memory memos from compaction summary and store them. When the
77183
+ * summary carries no memory-memo block, fall back to recording the most
77184
+ * recent real user message from the compacted history as a low-confidence
77185
+ * in-progress memo, so an ongoing task survives compaction even if the
77186
+ * model omitted the extraction section. */
77187
+ async extractAndStoreMemos(summary, messagesToCompact) {
77087
77188
  const memoStore = this.agent.memoStore;
77088
77189
  if (!memoStore) {
77089
77190
  this.agent.log.info("Memory memo store not available, skipping extraction");
77090
77191
  return;
77091
77192
  }
77092
77193
  this.agent.log.info("Scanning compaction summary for memory memos", { summaryLen: summary.length });
77093
- const memos = parseMemoryMemos(summary);
77194
+ let memos = parseMemoryMemos(summary);
77094
77195
  this.agent.log.info("Memory memo parse result", { memoCount: memos.length });
77196
+ if (memos.length === 0 && messagesToCompact !== void 0) {
77197
+ const fallback = /```memory-memo[\s\S]*?"none"\s*:\s*true[\s\S]*?```/.test(summary) ? void 0 : this.buildFallbackMemo(messagesToCompact);
77198
+ if (fallback !== void 0) {
77199
+ memos = [fallback];
77200
+ this.agent.log.info("Compaction summary carried no memory-memo; stored fallback memo", { userNeed: fallback.userNeed.slice(0, 120) });
77201
+ }
77202
+ }
77095
77203
  if (memos.length === 0) return;
77096
77204
  const sessionId = this.agent.homedir ? basename$1(dirname$2(dirname$2(this.agent.homedir))) : "unknown";
77097
77205
  const sessionTitle = await this.agent.getSessionTitle();
@@ -77110,6 +77218,28 @@ var FullCompaction = class {
77110
77218
  sessionId
77111
77219
  });
77112
77220
  }
77221
+ /** Build a low-confidence fallback memo from the most recent real user
77222
+ * message in the compacted history (origin.kind === 'user'), so an ongoing
77223
+ * task is not lost when the summary omits the memory-memo section. */
77224
+ buildFallbackMemo(messages) {
77225
+ for (let i = messages.length - 1; i >= 0; i--) {
77226
+ const msg = messages[i];
77227
+ if (msg === void 0 || msg.role !== "user" || msg.origin?.kind !== "user") continue;
77228
+ const text = msg.content.filter((p) => p.type === "text").map((p) => p.text).join("").trim();
77229
+ if (text.length === 0) continue;
77230
+ return createMemoryMemo({
77231
+ sourceSessionId: "",
77232
+ userNeed: text.slice(0, 300),
77233
+ approach: "(压缩时自动兜底记录:压缩摘要未生成完整任务经验提取,此处记录最近一条用户请求,用于跨压缩恢复进行中任务。请结合会话上下文核实。)",
77234
+ outcome: "进行中",
77235
+ whatFailed: "none",
77236
+ whatWorked: "none",
77237
+ tags: ["compaction-fallback"],
77238
+ note: "低置信度自动记录:建议后续在上下文恢复后核实并补充完整经验。",
77239
+ extractionSource: "compaction"
77240
+ });
77241
+ }
77242
+ }
77113
77243
  /**
77114
77244
  * Detects [[skill-candidate: <name>|<purpose>|<evidence>]] markers in the
77115
77245
  * compaction summary and emits a `skill_candidate` event for each one (at
@@ -77142,10 +77272,18 @@ var FullCompaction = class {
77142
77272
  * the compaction summary so active tasks and file context survive
77143
77273
  * compression. Without this, both are lost after compaction because the
77144
77274
  * original messages containing them are removed from the context window.
77275
+ *
77276
+ * When messages were actually compacted (`compactedCount > 0`), a leading
77277
+ * elision note tells the model how much of the conversation is missing and
77278
+ * that the summary is working notes, not verbatim history — so the tail
77279
+ * messages after the summary are not mistaken for a continuous conversation.
77145
77280
  */
77146
- postProcessSummary(summary, fileOps, toolCallHistory) {
77281
+ postProcessSummary(summary, fileOps, toolCallHistory, compactedCount) {
77147
77282
  const todos = this.agent.tools.storeData()["todo"] ?? [];
77148
- const sections = [summary.trim()];
77283
+ const base = summary.trim().replaceAll(/<files>[\s\S]*?<\/files>\s*/g, "").replace(/^> The conversation before this point was compacted:[\s\S]*?continue from here\.\s*\n+/m, "").trimEnd();
77284
+ const sections = [];
77285
+ if (compactedCount > 0) sections.push(`> The conversation before this point was compacted: ${String(compactedCount)} earlier message(s) were omitted and are covered by this summary. Treat the summary below as working notes, not verbatim history — the messages after it continue from here.`);
77286
+ if (base.length > 0) sections.push(base);
77149
77287
  if (todos.length > 0) {
77150
77288
  const lines = todos.map((t) => {
77151
77289
  return `- [${t.status === "done" ? "x" : t.status === "in_progress" ? "-" : " "}] ${t.title}`;
@@ -78804,12 +78942,15 @@ var ContextMemory = class {
78804
78942
  * message mutated (compaction summary, micro-compaction truncation, or a
78805
78943
  * projection repair) and the cache broke from that index.
78806
78944
  *
78807
- * Behavior is otherwise identical to the getter - this is observation
78808
- * only, it does not alter the messages returned.
78945
+ * Unlike the read-only `messages` getter, this path closes any trailing
78946
+ * in-flight tool call by synthesizing an error result (synthesizeMissing):
78947
+ * these messages go straight to the provider, which rejects an assistant
78948
+ * tool_calls message with no matching tool result (e.g. after a network
78949
+ * drop mid-batch).
78809
78950
  */
78810
78951
  messagesForLLM() {
78811
78952
  this.agent.microCompaction.detect();
78812
- const messages = project(this.agent.microCompaction.compact(this.history));
78953
+ const messages = project(this.agent.microCompaction.compact(this.history), { synthesizeMissing: true });
78813
78954
  this.observePrefixStability(messages);
78814
78955
  return messages;
78815
78956
  }
@@ -78911,6 +79052,31 @@ var ContextMemory = class {
78911
79052
  hasOpenToolExchange() {
78912
79053
  return this.pendingToolResultIds.size > 0;
78913
79054
  }
79055
+ /**
79056
+ * Defensive teardown for a live turn that ended — normally, cancelled, or
79057
+ * failed — while recorded tool calls were still awaiting results (e.g. the
79058
+ * batch's result dispatch died after a `tool.call` was already recorded,
79059
+ * like a network drop mid-execution). Synthesizes an error result for each
79060
+ * dangling call so the exchange closes: left open, the assistant tool_calls
79061
+ * message would have no matching tool message and the next request would be
79062
+ * rejected by the provider ("must be followed by tool messages responding
79063
+ * to each tool_call_id"). No-op when the exchange is already closed.
79064
+ */
79065
+ closeAbandonedToolExchange(output) {
79066
+ if (this.pendingToolResultIds.size === 0) return 0;
79067
+ const interruptedToolCallIds = [...this.pendingToolResultIds];
79068
+ for (const toolCallId of interruptedToolCallIds) this.appendLoopEvent({
79069
+ type: "tool.result",
79070
+ parentUuid: toolCallId,
79071
+ toolCallId,
79072
+ result: {
79073
+ output,
79074
+ isError: true
79075
+ }
79076
+ });
79077
+ this.flushDeferredMessagesIfToolExchangeClosed();
79078
+ return interruptedToolCallIds.length;
79079
+ }
78914
79080
  pushHistory(...messages) {
78915
79081
  this._history.push(...messages);
78916
79082
  for (const message of messages) {
@@ -96333,6 +96499,11 @@ var ToolCallDeduplicator = class {
96333
96499
  };
96334
96500
  //#endregion
96335
96501
  //#region ../../packages/agent-core/src/agent/turn/index.ts
96502
+ /** Builds the error text synthesized for tool calls abandoned when a live
96503
+ * turn ends (cancelled, failed, or completed) before their results arrived. */
96504
+ function abandonedToolResultOutput(ended) {
96505
+ return `Tool call did not complete: ${ended.reason === "cancelled" ? "the turn was cancelled" : ended.reason === "failed" ? `the turn failed${ended.error !== void 0 ? ` (${ended.error.message})` : ""}` : "the turn ended"} before its result was recorded. Do not assume the tool completed successfully.`;
96506
+ }
96336
96507
  const GOAL_CONTINUATION_PROMPT = [
96337
96508
  "Continue working toward the active goal.",
96338
96509
  "Keep the self-audit brief. Do not explore unrelated interpretations once the goal can be",
@@ -96567,6 +96738,11 @@ var TurnFlow = class {
96567
96738
  turnId,
96568
96739
  reason: "completed"
96569
96740
  };
96741
+ try {
96742
+ this.agent.context.closeAbandonedToolExchange(abandonedToolResultOutput(ended));
96743
+ } catch (error) {
96744
+ console.error("closeAbandonedToolExchange failed", error);
96745
+ }
96570
96746
  this.agent.usage.endTurn();
96571
96747
  this.agent.emitEvent(ended);
96572
96748
  return ended;
@@ -96646,6 +96822,11 @@ var TurnFlow = class {
96646
96822
  };
96647
96823
  }
96648
96824
  }
96825
+ try {
96826
+ this.agent.context.closeAbandonedToolExchange(abandonedToolResultOutput(ended));
96827
+ } catch (error) {
96828
+ console.error("closeAbandonedToolExchange failed", error);
96829
+ }
96649
96830
  if (this.currentId === turnId) this.agent.usage.endTurn();
96650
96831
  this.agent.emitEvent(ended);
96651
96832
  if (standalone && this.currentId === turnId) this.activeTurn = null;
@@ -102568,7 +102749,7 @@ var SessionSubagentHost = class {
102568
102749
  text: childPrompt
102569
102750
  }], origin);
102570
102751
  await runChildTurnToCompletion(child, options.signal);
102571
- let result = lastAssistantText(child);
102752
+ let result = lastAssistantText$1(child);
102572
102753
  let remainingContinuations = SUMMARY_CONTINUATION_ATTEMPTS;
102573
102754
  while (remainingContinuations > 0 && result.length < SUMMARY_MIN_LENGTH) {
102574
102755
  remainingContinuations -= 1;
@@ -102578,7 +102759,7 @@ var SessionSubagentHost = class {
102578
102759
  text: summary_continuation_default
102579
102760
  }], origin);
102580
102761
  await runChildTurnToCompletion(child, options.signal);
102581
- result = lastAssistantText(child);
102762
+ result = lastAssistantText$1(child);
102582
102763
  }
102583
102764
  const usage = child.usage.data().total;
102584
102765
  let findingsBlock = "";
@@ -102694,7 +102875,7 @@ async function runChildTurnToCompletion(child, signal) {
102694
102875
  function throwIfSubagentStoppedAtMaxTokens(stopReason) {
102695
102876
  if (stopReason === "max_tokens") throw new Error(`${SUBAGENT_MAX_TOKENS_ERROR}.`);
102696
102877
  }
102697
- function lastAssistantText(agent) {
102878
+ function lastAssistantText$1(agent) {
102698
102879
  for (const message of [...agent.context.history].toReversed()) {
102699
102880
  if (message.role !== "assistant") continue;
102700
102881
  const text = message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
@@ -120191,10 +120372,10 @@ var ScreamAuthFacade = class {
120191
120372
  };
120192
120373
  //#endregion
120193
120374
  //#region ../../packages/node-sdk/src/rpc.ts
120194
- const MAIN_AGENT_ID$2 = "main";
120375
+ const MAIN_AGENT_ID$3 = "main";
120195
120376
  var SDKRpcClient = class {
120196
120377
  core;
120197
- interactiveAgentId = MAIN_AGENT_ID$2;
120378
+ interactiveAgentId = MAIN_AGENT_ID$3;
120198
120379
  ready;
120199
120380
  rpc;
120200
120381
  eventListeners = /* @__PURE__ */ new Set();
@@ -120772,7 +120953,7 @@ function errorMessage$2(error) {
120772
120953
  }
120773
120954
  //#endregion
120774
120955
  //#region ../../packages/node-sdk/src/session.ts
120775
- const MAIN_AGENT_ID$1 = "main";
120956
+ const MAIN_AGENT_ID$2 = "main";
120776
120957
  var Session = class {
120777
120958
  id;
120778
120959
  workDir;
@@ -121212,7 +121393,7 @@ var Session = class {
121212
121393
  this.emit({
121213
121394
  type: "session.meta.updated",
121214
121395
  sessionId: this.id,
121215
- agentId: MAIN_AGENT_ID$1,
121396
+ agentId: MAIN_AGENT_ID$2,
121216
121397
  title: patch.title,
121217
121398
  patch
121218
121399
  });
@@ -121634,7 +121815,7 @@ function optionalBuildString(value) {
121634
121815
  return typeof value === "string" && value.length > 0 ? value : void 0;
121635
121816
  }
121636
121817
  const SCREAM_BUILD_INFO = {
121637
- version: optionalBuildString("0.12.6"),
121818
+ version: optionalBuildString("0.12.8"),
121638
121819
  channel: optionalBuildString(""),
121639
121820
  commit: optionalBuildString(""),
121640
121821
  buildTarget: optionalBuildString("darwin-arm64")
@@ -122621,28 +122802,46 @@ const BUILTIN_SLASH_COMMANDS = [
122621
122802
  name: "auto",
122622
122803
  aliases: [],
122623
122804
  description: "registry.auto_desc",
122624
- priority: 125,
122805
+ priority: 220,
122625
122806
  availability: "always"
122626
122807
  },
122627
122808
  {
122628
122809
  name: "yes",
122629
122810
  aliases: ["yolo"],
122630
122811
  description: "registry.yolo_desc",
122631
- priority: 124,
122812
+ priority: 219,
122632
122813
  availability: "always"
122633
122814
  },
122634
122815
  {
122635
122816
  name: "ask",
122636
122817
  aliases: ["ask"],
122637
122818
  description: "registry.ask_desc",
122638
- priority: 123,
122819
+ priority: 218,
122820
+ availability: "always"
122821
+ },
122822
+ {
122823
+ name: "goal",
122824
+ aliases: ["goaloff"],
122825
+ description: "registry.goal_desc",
122826
+ argumentHint: "[objective]",
122827
+ priority: 217,
122828
+ availability: (args) => {
122829
+ const trimmed = args.trim();
122830
+ return trimmed === "" || trimmed === "status" || trimmed === "pause" || trimmed === "off" ? "always" : "idle-only";
122831
+ }
122832
+ },
122833
+ {
122834
+ name: "wolfpack",
122835
+ aliases: ["wp"],
122836
+ description: "registry.wolfpack_desc",
122837
+ priority: 216,
122639
122838
  availability: "always"
122640
122839
  },
122641
122840
  {
122642
122841
  name: "rlm",
122643
122842
  aliases: ["rlm"],
122644
122843
  description: "registry.rlm_desc",
122645
- priority: 121,
122844
+ priority: 215,
122646
122845
  availability: "always"
122647
122846
  },
122648
122847
  {
@@ -122650,39 +122849,21 @@ const BUILTIN_SLASH_COMMANDS = [
122650
122849
  aliases: [],
122651
122850
  description: "registry.rlm_max_depth_desc",
122652
122851
  argumentHint: "[N]",
122653
- priority: 119,
122654
- availability: "always"
122655
- },
122656
- {
122657
- name: "wolfpack",
122658
- aliases: ["wp"],
122659
- description: "registry.wolfpack_desc",
122660
- priority: 122,
122852
+ priority: 214,
122661
122853
  availability: "always"
122662
122854
  },
122663
122855
  {
122664
122856
  name: "sessions",
122665
122857
  aliases: ["resume"],
122666
122858
  description: "registry.sessions_desc",
122667
- priority: 119
122668
- },
122669
- {
122670
- name: "goal",
122671
- aliases: ["goaloff"],
122672
- description: "registry.goal_desc",
122673
- argumentHint: "[objective]",
122674
- priority: 120,
122675
- availability: (args) => {
122676
- const trimmed = args.trim();
122677
- return trimmed === "" || trimmed === "status" || trimmed === "pause" || trimmed === "off" ? "always" : "idle-only";
122678
- }
122859
+ priority: 213
122679
122860
  },
122680
122861
  {
122681
122862
  name: "memory",
122682
122863
  aliases: ["memo", "mem"],
122683
122864
  description: "registry.memory_desc",
122684
122865
  argumentHint: "[query]",
122685
- priority: 120,
122866
+ priority: 212,
122686
122867
  availability: "always"
122687
122868
  },
122688
122869
  {
@@ -122690,96 +122871,61 @@ const BUILTIN_SLASH_COMMANDS = [
122690
122871
  aliases: ["know"],
122691
122872
  description: "registry.knowledge_desc",
122692
122873
  argumentHint: "[query]",
122693
- priority: 119,
122874
+ priority: 211,
122694
122875
  availability: "always"
122695
122876
  },
122696
- {
122697
- name: "new",
122698
- aliases: ["clear"],
122699
- description: "registry.new_desc",
122700
- priority: 120
122701
- },
122702
122877
  {
122703
122878
  name: "model",
122704
122879
  aliases: [],
122705
122880
  description: "registry.model_desc",
122706
122881
  argumentHint: "[alias]",
122707
- priority: 120
122882
+ priority: 210
122883
+ },
122884
+ {
122885
+ name: "new",
122886
+ aliases: ["clear"],
122887
+ description: "registry.new_desc",
122888
+ priority: 209
122708
122889
  },
122709
122890
  {
122710
122891
  name: "compact",
122711
122892
  aliases: [],
122712
122893
  description: "registry.compact_desc",
122713
- priority: 119
122894
+ priority: 208
122714
122895
  },
122715
122896
  {
122716
- name: "make-skill",
122717
- aliases: ["makeskill", "craftskill"],
122718
- description: "registry.make_skill_desc",
122719
- priority: 118,
122720
- availability: "idle-only"
122897
+ name: "fusionplan",
122898
+ aliases: ["fp"],
122899
+ description: "registry.fusionplan_desc",
122900
+ priority: 207,
122901
+ availability: (args) => args.trim().toLowerCase() === "clear" ? "idle-only" : "always"
122721
122902
  },
122722
122903
  {
122723
122904
  name: "plan",
122724
122905
  aliases: [],
122725
122906
  description: "registry.plan_desc",
122726
- priority: 118,
122727
- availability: (args) => args.trim().toLowerCase() === "clear" ? "idle-only" : "always"
122728
- },
122729
- {
122730
- name: "fusionplan",
122731
- aliases: ["fp"],
122732
- description: "registry.fusionplan_desc",
122733
- priority: 118,
122907
+ priority: 206,
122734
122908
  availability: (args) => args.trim().toLowerCase() === "clear" ? "idle-only" : "always"
122735
122909
  },
122736
122910
  {
122737
122911
  name: "tasks",
122738
122912
  aliases: ["task"],
122739
122913
  description: "registry.tasks_desc",
122740
- priority: 117,
122741
- availability: "always"
122742
- },
122743
- {
122744
- name: "help",
122745
- aliases: ["h", "?"],
122746
- description: "registry.help_desc",
122747
- priority: 116,
122748
- availability: "always"
122749
- },
122750
- {
122751
- name: "status",
122752
- aliases: [],
122753
- description: "registry.status_desc",
122754
- priority: 115,
122755
- availability: "always"
122756
- },
122757
- {
122758
- name: "usage",
122759
- aliases: [],
122760
- description: "registry.usage_desc",
122761
- priority: 114,
122914
+ priority: 205,
122762
122915
  availability: "always"
122763
122916
  },
122764
122917
  {
122765
122918
  name: "btw",
122766
122919
  aliases: [],
122767
122920
  description: "registry.btw_desc",
122768
- priority: 113,
122921
+ priority: 204,
122769
122922
  availability: "always"
122770
122923
  },
122771
122924
  {
122772
122925
  name: "like",
122773
122926
  aliases: [],
122774
122927
  description: "registry.like_desc",
122775
- priority: 113,
122776
- availability: "always"
122777
- },
122778
- {
122779
- name: "mcp",
122780
- aliases: [],
122781
- description: "registry.mcp_desc",
122782
- priority: 112,
122928
+ priority: 203,
122783
122929
  availability: "always"
122784
122930
  },
122785
122931
  {
@@ -122790,121 +122936,162 @@ const BUILTIN_SLASH_COMMANDS = [
122790
122936
  "plugins"
122791
122937
  ],
122792
122938
  description: "registry.skill_desc",
122793
- priority: 110,
122939
+ priority: 202,
122794
122940
  availability: "always"
122795
122941
  },
122796
122942
  {
122797
- name: "cc",
122943
+ name: "fork",
122798
122944
  aliases: [],
122799
- description: "registry.cc_desc",
122800
- priority: 109,
122945
+ description: "registry.fork_desc",
122946
+ priority: 201
122947
+ },
122948
+ {
122949
+ name: "title",
122950
+ aliases: ["rename"],
122951
+ description: "registry.title_desc",
122952
+ priority: 200,
122801
122953
  availability: "always"
122802
122954
  },
122803
122955
  {
122804
- name: "cc-connect",
122956
+ name: "config",
122805
122957
  aliases: [],
122806
- description: "registry.cc_connect_desc",
122807
- priority: 109,
122958
+ description: "registry.config_desc",
122959
+ priority: 199
122960
+ },
122961
+ {
122962
+ name: "help",
122963
+ aliases: ["h", "?"],
122964
+ description: "registry.help_desc",
122965
+ priority: 198,
122808
122966
  availability: "always"
122809
122967
  },
122810
122968
  {
122811
- name: "revoke",
122812
- aliases: [],
122813
- description: "registry.revoke_desc",
122814
- priority: 108,
122969
+ name: "make-skill",
122970
+ aliases: ["makeskill", "craftskill"],
122971
+ description: "registry.make_skill_desc",
122972
+ priority: 197,
122815
122973
  availability: "idle-only"
122816
122974
  },
122817
122975
  {
122818
- name: "fork",
122976
+ name: "mcp",
122819
122977
  aliases: [],
122820
- description: "registry.fork_desc",
122821
- priority: 105
122978
+ description: "registry.mcp_desc",
122979
+ priority: 196,
122980
+ availability: "always"
122822
122981
  },
122823
122982
  {
122824
- name: "title",
122825
- aliases: ["rename"],
122826
- description: "registry.title_desc",
122827
- priority: 104,
122983
+ name: "status",
122984
+ aliases: [],
122985
+ description: "registry.status_desc",
122986
+ priority: 195,
122828
122987
  availability: "always"
122829
122988
  },
122830
122989
  {
122831
- name: "config",
122990
+ name: "usage",
122832
122991
  aliases: [],
122833
- description: "registry.config_desc",
122834
- priority: 103
122992
+ description: "registry.usage_desc",
122993
+ priority: 194,
122994
+ availability: "always"
122835
122995
  },
122836
122996
  {
122837
- name: "permission",
122997
+ name: "revoke",
122838
122998
  aliases: [],
122839
- description: "registry.permission_desc",
122840
- priority: 102,
122999
+ description: "registry.revoke_desc",
123000
+ priority: 193,
123001
+ availability: "idle-only"
123002
+ },
123003
+ {
123004
+ name: "cc",
123005
+ aliases: [],
123006
+ description: "registry.cc_desc",
123007
+ priority: 192,
123008
+ availability: "always"
123009
+ },
123010
+ {
123011
+ name: "cc-connect",
123012
+ aliases: [],
123013
+ description: "registry.cc_connect_desc",
123014
+ priority: 191,
122841
123015
  availability: "always"
122842
123016
  },
122843
123017
  {
122844
123018
  name: "theme",
122845
123019
  aliases: [],
122846
123020
  description: "registry.theme_desc",
122847
- priority: 101,
123021
+ priority: 190,
122848
123022
  availability: "always"
122849
123023
  },
122850
123024
  {
122851
123025
  name: "language",
122852
123026
  aliases: ["lang"],
122853
123027
  description: "registry.language_desc",
122854
- priority: 102,
123028
+ priority: 189,
123029
+ availability: "always"
123030
+ },
123031
+ {
123032
+ name: "permission",
123033
+ aliases: [],
123034
+ description: "registry.permission_desc",
123035
+ priority: 188,
122855
123036
  availability: "always"
122856
123037
  },
122857
123038
  {
122858
123039
  name: "editor",
122859
123040
  aliases: [],
122860
123041
  description: "registry.editor_desc",
122861
- priority: 100,
123042
+ priority: 187,
122862
123043
  availability: "always"
122863
123044
  },
122864
123045
  {
122865
123046
  name: "settings",
122866
123047
  aliases: [],
122867
123048
  description: "registry.settings_desc",
122868
- priority: 99,
123049
+ priority: 186,
122869
123050
  availability: "always"
122870
123051
  },
122871
123052
  {
122872
123053
  name: "init",
122873
123054
  aliases: [],
122874
123055
  description: "registry.init_desc",
122875
- priority: 98
123056
+ priority: 185
122876
123057
  },
122877
123058
  {
122878
123059
  name: "export-md",
122879
123060
  aliases: ["export"],
122880
123061
  description: "registry.export_md_desc",
122881
- priority: 97
123062
+ priority: 184
122882
123063
  },
122883
123064
  {
122884
123065
  name: "export-debug-zip",
122885
123066
  aliases: [],
122886
123067
  description: "registry.export_debug_desc",
122887
- priority: 96
123068
+ priority: 183
123069
+ },
123070
+ {
123071
+ name: "eval",
123072
+ aliases: [],
123073
+ description: "registry.eval_desc",
123074
+ priority: 182
122888
123075
  },
122889
123076
  {
122890
123077
  name: "update",
122891
123078
  aliases: [],
122892
123079
  description: "registry.update_desc",
122893
- priority: 95,
123080
+ priority: 181,
122894
123081
  availability: "idle-only"
122895
123082
  },
122896
123083
  {
122897
123084
  name: "version",
122898
123085
  aliases: [],
122899
123086
  description: "registry.version_desc",
122900
- priority: 94,
123087
+ priority: 180,
122901
123088
  availability: "always"
122902
123089
  },
122903
123090
  {
122904
123091
  name: "logout",
122905
123092
  aliases: ["disconnect"],
122906
123093
  description: "registry.logout_desc",
122907
- priority: 93
123094
+ priority: 179
122908
123095
  },
122909
123096
  {
122910
123097
  name: "exit",
@@ -123022,7 +123209,7 @@ function getCtrlDHint() {
123022
123209
  function getCtrlCHint() {
123023
123210
  return t("constant.ctrl_c_hint");
123024
123211
  }
123025
- const MAIN_AGENT_ID = "main";
123212
+ const MAIN_AGENT_ID$1 = "main";
123026
123213
  const EXIT_CONFIRM_WINDOW_MS = 1500;
123027
123214
  function isManagedUsageProvider(providerKey) {
123028
123215
  return providerKey === DEFAULT_OAUTH_PROVIDER_NAME;
@@ -125546,7 +125733,15 @@ const HEADING_HASH_PREFIX = /^((?:\u001B\[[0-9;]*m)*)#{1,6}[ \t]+/;
125546
125733
  * takes one formatter function per token; tokens not listed here fall back to
125547
125734
  * its DEFAULT_THEME.
125548
125735
  */
125549
- function createCodeHighlightTheme(colors) {
125736
+ /**
125737
+ * Markdown code-block highlight theme: green-dominant mapping (keyword,
125738
+ * function, built_in → primary; strings → success; numbers → warning;
125739
+ * comments → textDim). Kept distinct from the shared preview theme in
125740
+ * code-highlight-theme.ts on purpose — markdown code blocks use the green
125741
+ * primary hue, while file-preview panels use the classic blue/red/yellow
125742
+ * scheme mapped to the same palette. Both follow the active theme.
125743
+ */
125744
+ function createMarkdownCodeHighlightTheme(colors) {
125550
125745
  const keyword = chalk.hex(colors.primary);
125551
125746
  const str = chalk.hex(colors.success);
125552
125747
  const comment = chalk.hex(colors.textDim);
@@ -125596,7 +125791,7 @@ function createMarkdownTheme(colors) {
125596
125791
  const stripHash = (text) => text.replace(HEADING_HASH_PREFIX, "$1");
125597
125792
  const muted = chalk.hex(colors.textMuted);
125598
125793
  const border = chalk.hex(colors.border);
125599
- const codeTheme = createCodeHighlightTheme(colors);
125794
+ const codeTheme = createMarkdownCodeHighlightTheme(colors);
125600
125795
  return {
125601
125796
  heading: (text) => chalk.bold.hex(colors.text)(stripHash(text)),
125602
125797
  link: (text) => chalk.hex(colors.mdLink)(text),
@@ -127448,7 +127643,7 @@ async function guidedGoalSetup(host) {
127448
127643
  host.showNotice(t("goal.storm_breaker"), t("goal.conflict_loop"));
127449
127644
  return;
127450
127645
  }
127451
- const { TextInputDialogComponent } = await import("./text-input-dialog-BViNsv5f.mjs");
127646
+ const { TextInputDialogComponent } = await import("./text-input-dialog-BIXonNNb.mjs");
127452
127647
  const initialDesc = await promptText(host, TextInputDialogComponent, {
127453
127648
  title: t("goal.setup_title_initial"),
127454
127649
  subtitle: t("goal.setup_desc_hint"),
@@ -127469,7 +127664,7 @@ async function guidedGoalSetup(host) {
127469
127664
  await showGoalConfigWizard(host, session, confirmed.trim() || objective, false);
127470
127665
  }
127471
127666
  async function showGoalConfigWizard(host, session, objective, replace) {
127472
- const { TextInputDialogComponent } = await import("./text-input-dialog-BViNsv5f.mjs");
127667
+ const { TextInputDialogComponent } = await import("./text-input-dialog-BIXonNNb.mjs");
127473
127668
  const turnInput = await promptNumber(host, TextInputDialogComponent, {
127474
127669
  title: t("goal.wizard_title", { objective }),
127475
127670
  subtitle: t("goal.budget_turns_hint"),
@@ -128834,6 +129029,60 @@ var CachedContainer = class extends Container {
128834
129029
  }
128835
129030
  };
128836
129031
  //#endregion
129032
+ //#region src/tui/theme/code-highlight-theme.ts
129033
+ function createCodeHighlightTheme(colors) {
129034
+ const keyword = chalk.hex(colors.mdCodeBlock);
129035
+ const str = chalk.hex(colors.error);
129036
+ const num = chalk.hex(colors.primary);
129037
+ const fn = chalk.hex(colors.warning);
129038
+ const builtin = chalk.hex(colors.planMode);
129039
+ const cls = chalk.hex(colors.accent);
129040
+ const text = chalk.hex(colors.text);
129041
+ return {
129042
+ keyword,
129043
+ built_in: builtin,
129044
+ type: keyword,
129045
+ literal: builtin,
129046
+ number: num,
129047
+ regexp: str,
129048
+ string: str,
129049
+ subst: str,
129050
+ symbol: num,
129051
+ class: cls,
129052
+ function: fn,
129053
+ title: fn,
129054
+ params: text,
129055
+ comment: num,
129056
+ doctag: num,
129057
+ meta: chalk.hex(colors.textMuted),
129058
+ "meta-keyword": keyword,
129059
+ "meta-string": str,
129060
+ section: keyword,
129061
+ tag: keyword,
129062
+ name: fn,
129063
+ "builtin-name": builtin,
129064
+ attr: fn,
129065
+ attribute: fn,
129066
+ variable: text,
129067
+ bullet: num,
129068
+ code: str,
129069
+ emphasis: (s) => chalk.italic(s),
129070
+ strong: (s) => chalk.bold(s),
129071
+ formula: text,
129072
+ link: chalk.hex(colors.mdLink),
129073
+ quote: chalk.hex(colors.mdQuote),
129074
+ addition: chalk.hex(colors.diffAdded),
129075
+ deletion: chalk.hex(colors.diffRemoved),
129076
+ "selector-tag": keyword,
129077
+ "selector-id": cls,
129078
+ "selector-class": cls,
129079
+ "selector-attr": fn,
129080
+ "selector-pseudo": fn,
129081
+ "template-tag": str,
129082
+ "template-variable": fn
129083
+ };
129084
+ }
129085
+ //#endregion
128837
129086
  //#region src/tui/components/media/code-highlight.ts
128838
129087
  /**
128839
129088
  * Shared syntax-highlighting helpers for code previews
@@ -128871,13 +129120,14 @@ function langFromPath(filePath) {
128871
129120
  const lang = EXT_LANG_MAP[ext] ?? ext;
128872
129121
  return supportsLanguage(lang) ? lang : void 0;
128873
129122
  }
128874
- function highlightLines(code, lang) {
129123
+ function highlightLines(code, lang, colors) {
128875
129124
  const normalizedLang = lang?.trim().toLowerCase();
128876
129125
  if (!normalizedLang || !supportsLanguage(normalizedLang)) return code.split("\n");
128877
129126
  try {
128878
129127
  return highlight(code, {
128879
129128
  language: normalizedLang,
128880
- ignoreIllegals: true
129129
+ ignoreIllegals: true,
129130
+ theme: createCodeHighlightTheme(colors)
128881
129131
  }).split("\n");
128882
129132
  } catch {
128883
129133
  return code.split("\n");
@@ -128939,10 +129189,12 @@ function visualizeIndent(line) {
128939
129189
  }
128940
129190
  /**
128941
129191
  * Render a diff line's code with leading-whitespace visualization (tabs as
128942
- * `->`, spaces as `·`, dimmed) and optional syntax highlighting. Highlighting
128943
- * runs on the raw code first; indent visualization then runs on the
128944
- * highlighted string - leading whitespace carries no token color, so it is
128945
- * still plain and can be replaced and dimmed without disturbing the tokens.
129192
+ * `->`, spaces as `·`, dimmed) and optional syntax highlighting. Indent
129193
+ * visualization runs on the raw code FIRST, then highlighting runs on the
129194
+ * stripped remainder: once the text is ANSI-tinted (the theme's `default`
129195
+ * color wraps whitespace too), a leading tab is no longer the first raw
129196
+ * character and would be missed. The dimmed indent itself carries no token
129197
+ * color.
128946
129198
  *
128947
129199
  * When highlighting is on and produced token colors, the diff line color is
128948
129200
  * layered as the base foreground: syntax tokens override it, but every ANSI
@@ -128951,17 +129203,18 @@ function visualizeIndent(line) {
128951
129203
  * When highlighting is off (streaming) or produced no token colors, the code
128952
129204
  * part is colored with the diff line color instead.
128953
129205
  */
128954
- function renderDiffCode(code, colorFn, highlight, lang) {
128955
- const { text, indentEnd } = visualizeIndent(highlight ? highlightLines(code, lang)[0] ?? code : code);
129206
+ function renderDiffCode(code, colorFn, highlight, lang, colors) {
129207
+ const { text, indentEnd } = visualizeIndent(code);
128956
129208
  const indent = text.slice(0, indentEnd);
128957
129209
  const rest = text.slice(indentEnd);
128958
129210
  const dimIndent = indent.length > 0 ? chalk.dim(indent) : indent;
128959
- if (highlight && ANSI_RE.test(rest)) {
129211
+ const source = highlight ? highlightLines(rest, lang, colors)[0] ?? rest : rest;
129212
+ if (highlight && ANSI_RE.test(source)) {
128960
129213
  const prefix = extractAnsiPrefix(colorFn);
128961
- if (prefix.length > 0) return dimIndent + prefix + rest.replace(ANSI_RESET_RE, (m) => m + prefix) + "\x1B[39m";
128962
- return dimIndent + rest;
129214
+ if (prefix.length > 0) return dimIndent + prefix + source.replace(ANSI_RESET_RE, (m) => m + prefix) + "\x1B[39m";
129215
+ return dimIndent + source;
128963
129216
  }
128964
- return dimIndent + colorFn(rest);
129217
+ return dimIndent + colorFn(source);
128965
129218
  }
128966
129219
  /**
128967
129220
  * Compute word-level diff between two single lines and highlight the changed
@@ -129095,7 +129348,7 @@ function renderDiffLines(oldText, newText, path, colors, isIncomplete = false, o
129095
129348
  const line = shown[i];
129096
129349
  const marker = line.kind === "add" ? "+" : "-";
129097
129350
  const color = line.kind === "add" ? s.add : s.del;
129098
- output.push(s.gutter(String(line.lineNum).padStart(4) + " ") + color(`${marker} `) + renderDiffCode(line.code, color, doHighlight, lang));
129351
+ output.push(s.gutter(String(line.lineNum).padStart(4) + " ") + color(`${marker} `) + renderDiffCode(line.code, color, doHighlight, lang, colors));
129099
129352
  i += 1;
129100
129353
  }
129101
129354
  const hidden = changedLines.length - shown.length;
@@ -129146,11 +129399,11 @@ function buildClusters(diffLines, contextLines) {
129146
129399
  removedCount: removed
129147
129400
  };
129148
129401
  }
129149
- function formatDiffRow(line, s, doHighlight, lang) {
129402
+ function formatDiffRow(line, s, doHighlight, lang, colors) {
129150
129403
  const gutter = s.gutter(String(line.lineNum).padStart(4) + " ");
129151
- if (line.kind === "add") return gutter + s.add("+ ") + renderDiffCode(line.code, s.add, doHighlight, lang);
129152
- if (line.kind === "delete") return gutter + s.del("- ") + renderDiffCode(line.code, s.del, doHighlight, lang);
129153
- return gutter + " " + renderDiffCode(line.code, (x) => x, doHighlight, lang);
129404
+ if (line.kind === "add") return gutter + s.add("+ ") + renderDiffCode(line.code, s.add, doHighlight, lang, colors);
129405
+ if (line.kind === "delete") return gutter + s.del("- ") + renderDiffCode(line.code, s.del, doHighlight, lang, colors);
129406
+ return gutter + " " + renderDiffCode(line.code, (x) => x, doHighlight, lang, colors);
129154
129407
  }
129155
129408
  /**
129156
129409
  * Render a diff with surrounding context, eliding unchanged middle
@@ -129221,7 +129474,7 @@ function renderDiffLinesClustered(oldText, newText, path, colors, opts = {}) {
129221
129474
  i += 2;
129222
129475
  continue;
129223
129476
  }
129224
- output.push(formatDiffRow(line, s, doHighlight, lang));
129477
+ output.push(formatDiffRow(line, s, doHighlight, lang, colors));
129225
129478
  body++;
129226
129479
  if (line.kind !== "context") shownChanges++;
129227
129480
  prevEnd = i;
@@ -131104,7 +131357,7 @@ var ToolCallComponent = class ToolCallComponent extends CachedContainer {
131104
131357
  if (name === "Write") {
131105
131358
  const content = str(this.toolCall.args["content"]);
131106
131359
  if (content.length === 0) return;
131107
- const allLines = highlightLines(content, langFromPath(str(this.toolCall.args["file_path"] ?? this.toolCall.args["path"])));
131360
+ const allLines = highlightLines(content, langFromPath(str(this.toolCall.args["file_path"] ?? this.toolCall.args["path"])), this.colors);
131108
131361
  const writeShouldCap = !this.expanded;
131109
131362
  const shown = writeShouldCap ? allLines.slice(0, 10) : allLines;
131110
131363
  const remaining = allLines.length - shown.length;
@@ -131209,7 +131462,7 @@ var ToolCallComponent = class ToolCallComponent extends CachedContainer {
131209
131462
  const fragmentEnd = Math.min(streamText.length, fragmentStart + ToolCallComponent.WRITE_STREAM_TAIL_RAW_CHARS);
131210
131463
  const fragment = unescapeJsonStringValue(streamText, fragmentStart, fragmentEnd);
131211
131464
  if (fragment.length === 0) return;
131212
- const lines = highlightLines(fragment, this.writeStreamLang);
131465
+ const lines = highlightLines(fragment, this.writeStreamLang, this.colors);
131213
131466
  const displayLines = lines.slice(-10);
131214
131467
  const firstFragmentLineNo = this.writeStreamNlCount + (tailStart > contentStart ? 2 : 1);
131215
131468
  const skipped = lines.length - displayLines.length;
@@ -136032,6 +136285,429 @@ async function handleKnowledgeCommand(host, _args) {
136032
136285
  showMenu();
136033
136286
  }
136034
136287
  //#endregion
136288
+ //#region ../../packages/evals/src/judge.ts
136289
+ /** Runs every rule against the input; a rule passes when its check is true. */
136290
+ function judgeOutput(input, rules) {
136291
+ const passed = [];
136292
+ const failed = [];
136293
+ for (const rule of rules) if (rule.check(input)) passed.push(rule.name);
136294
+ else failed.push(rule.name);
136295
+ return {
136296
+ passed,
136297
+ failed
136298
+ };
136299
+ }
136300
+ //#endregion
136301
+ //#region ../../packages/evals/src/harness.ts
136302
+ /**
136303
+ * Minimal end-to-end eval harness built on the public node-sdk
136304
+ * `ScreamHarness`. Each eval run gets an isolated temp **workspace** so runs
136305
+ * never touch real project files; the scream home defaults to the real one so
136306
+ * the provider/API-key configuration in `~/.scream-code/config.toml` is
136307
+ * honored (pass `screamHome` to override for fully isolated runs). Model
136308
+ * selection follows `SCREAM_EVAL_MODEL` (e.g. `provider/model`);
136309
+ * when unset the harness throws with a clear message.
136310
+ */
136311
+ /** The main agent id; only its events settle the eval turn wait. */
136312
+ const MAIN_AGENT_ID = "main";
136313
+ /** The current scream default model, when resolvable from config. */
136314
+ const DEFAULT_EVAL_MODEL = process.env["SCREAM_EVAL_MODEL"];
136315
+ /**
136316
+ * Runs a single end-to-end prompt against a fresh isolated session and
136317
+ * returns the final assistant text plus token usage. The session, workspace
136318
+ * and scream home are torn down afterwards.
136319
+ */
136320
+ async function runEvalPrompt(input, options = {}) {
136321
+ const workDir = options.workDir ?? await mkdtemp(join(tmpdir(), "scream-eval-ws-"));
136322
+ const screamHome = options.screamHome;
136323
+ const createdWorkDir = options.workDir === void 0;
136324
+ let harness;
136325
+ let session;
136326
+ try {
136327
+ for (const fixture of (typeof input === "string" ? [] : input.fixtures) ?? []) await writeFile(join(workDir, fixture.name), fixture.content, "utf-8");
136328
+ const prompt = typeof input === "string" ? input : input.prompt;
136329
+ const model = options.model ?? DEFAULT_EVAL_MODEL;
136330
+ if (model === void 0 || model.length === 0) throw new Error("No eval model configured. Set SCREAM_EVAL_MODEL, e.g. SCREAM_EVAL_MODEL=provider/model pnpm eval");
136331
+ const [provider, modelId, ...rest] = model.split("/");
136332
+ const usageProvider = rest.length > 0 ? model : provider ?? "unknown";
136333
+ const usageModel = modelId === void 0 || rest.length > 0 ? model : modelId;
136334
+ harness = new ScreamHarness({ homeDir: screamHome });
136335
+ session = await harness.createSession({
136336
+ workDir,
136337
+ model,
136338
+ thinking: options.thinking ?? "off",
136339
+ permission: "yolo"
136340
+ });
136341
+ const { timedOut } = await promptAndWaitForTurnEnd(session, prompt);
136342
+ let output = "";
136343
+ try {
136344
+ output = lastAssistantText((await session.getContext()).history);
136345
+ } catch {
136346
+ output = "";
136347
+ }
136348
+ const total = (await session.getUsage().catch(() => void 0))?.total;
136349
+ let verifiedFile = false;
136350
+ if (options.verifyFileAfterTurn !== void 0) try {
136351
+ verifiedFile = await readFile(join(workDir, options.verifyFileAfterTurn.path), "utf-8") === options.verifyFileAfterTurn.content;
136352
+ } catch {
136353
+ verifiedFile = false;
136354
+ }
136355
+ return {
136356
+ output,
136357
+ timedOut,
136358
+ verifiedFile,
136359
+ usage: {
136360
+ provider: usageProvider,
136361
+ model: usageModel,
136362
+ inputTokens: total?.inputOther ?? 0,
136363
+ outputTokens: total?.output ?? 0,
136364
+ totalTokens: (total?.inputOther ?? 0) + (total?.output ?? 0) + (total?.inputCacheRead ?? 0) + (total?.inputCacheCreation ?? 0)
136365
+ }
136366
+ };
136367
+ } finally {
136368
+ if (session !== void 0) await session.close({ extractMemories: false }).catch(() => {});
136369
+ if (createdWorkDir) await rm(workDir, {
136370
+ recursive: true,
136371
+ force: true
136372
+ }).catch(() => {});
136373
+ }
136374
+ }
136375
+ function lastAssistantText(history) {
136376
+ for (let i = history.length - 1; i >= 0; i -= 1) {
136377
+ const message = history[i];
136378
+ if (message.role !== "assistant") continue;
136379
+ const text = (message.content ?? []).filter((part) => part.type === "text" && part.text !== void 0).map((part) => part.text).join("");
136380
+ if (text.trim().length > 0) return text.trim();
136381
+ }
136382
+ return "";
136383
+ }
136384
+ /**
136385
+ * Sends a prompt and resolves when the main agent's turn ends. `Session.prompt`
136386
+ * only enqueues the RPC request; the assistant reply arrives asynchronously as
136387
+ * events, so we must wait for the `turn.ended` event to know the run finished.
136388
+ *
136389
+ * A hard timeout guards against a turn that never ends (e.g. a model looping
136390
+ * through tools instead of converging, or a network interruption). On timeout
136391
+ * we cancel the session so the caller's cleanup path still runs instead of
136392
+ * leaking the session and temp workspace. 90s is enough to complete any
136393
+ * legitimate single-turn tool round-trip; longer waits are almost always a
136394
+ * stuck model, so we fail fast instead of burning tokens.
136395
+ */
136396
+ const TURN_WAIT_TIMEOUT_MS = 9e4;
136397
+ /**
136398
+ * Resolves with `timedOut: true` when the turn hit the timeout (model looping
136399
+ * or stuck) instead of throwing, so callers can still inspect side effects
136400
+ * (e.g. a file the Write tool already created). Real errors (turn failed /
136401
+ * error event) still reject.
136402
+ */
136403
+ async function promptAndWaitForTurnEnd(session, prompt) {
136404
+ const { promise, resolve, reject } = promiseWithResolvers();
136405
+ let activeTurnId;
136406
+ let activeAgentId;
136407
+ let settled = false;
136408
+ let timeout;
136409
+ let graceTimer;
136410
+ /** Set once the timeout fires; guards against the cancel-triggered
136411
+ * `turn.ended('cancelled')` being misread as a real failure. */
136412
+ let timeoutFired = false;
136413
+ const finish = (result) => {
136414
+ if (settled) return;
136415
+ settled = true;
136416
+ if (timeout !== void 0) clearTimeout(timeout);
136417
+ if (graceTimer !== void 0) clearTimeout(graceTimer);
136418
+ if (result instanceof Error) reject(result);
136419
+ else resolve(result);
136420
+ };
136421
+ const unsubscribe = session.onEvent((event) => {
136422
+ if (event.agentId !== MAIN_AGENT_ID) return;
136423
+ if (event.type === "error") {
136424
+ finish(/* @__PURE__ */ new Error(`${event.code}: ${event.message}`));
136425
+ return;
136426
+ }
136427
+ if (event.type === "turn.started" && activeTurnId === void 0) {
136428
+ activeTurnId = event.turnId;
136429
+ activeAgentId = event.agentId;
136430
+ return;
136431
+ }
136432
+ if (activeTurnId === void 0 || activeAgentId === void 0 || !("turnId" in event) || event.turnId !== activeTurnId || event.agentId !== activeAgentId) return;
136433
+ if (event.type === "turn.ended") if (event.reason === "completed") finish({ timedOut: false });
136434
+ else if (timeoutFired && event.reason === "cancelled") return;
136435
+ else finish(/* @__PURE__ */ new Error(`Turn ended with reason: ${event.reason}`));
136436
+ });
136437
+ try {
136438
+ await session.prompt(prompt);
136439
+ timeout = setTimeout(() => {
136440
+ timeoutFired = true;
136441
+ session.cancel().catch(() => {});
136442
+ graceTimer = setTimeout(() => {
136443
+ finish({ timedOut: true });
136444
+ }, 500);
136445
+ }, TURN_WAIT_TIMEOUT_MS);
136446
+ return await promise;
136447
+ } finally {
136448
+ unsubscribe();
136449
+ if (timeout !== void 0) clearTimeout(timeout);
136450
+ if (graceTimer !== void 0) clearTimeout(graceTimer);
136451
+ }
136452
+ }
136453
+ function promiseWithResolvers() {
136454
+ let resolve;
136455
+ let reject;
136456
+ return {
136457
+ promise: new Promise((res, rej) => {
136458
+ resolve = res;
136459
+ reject = rej;
136460
+ }),
136461
+ resolve,
136462
+ reject
136463
+ };
136464
+ }
136465
+ const EVAL_CASES = [
136466
+ {
136467
+ id: "smoke",
136468
+ name: "Smoke — answers a trivial question",
136469
+ input: "What is the capital of France? Answer in one word.",
136470
+ rules: [{
136471
+ name: "answers the capital",
136472
+ check: (output) => {
136473
+ const lower = output.toLowerCase();
136474
+ return lower.includes("paris") || lower.includes("巴黎");
136475
+ }
136476
+ }],
136477
+ extra: (result) => {
136478
+ const failures = [];
136479
+ if (result.usage.totalTokens <= 0) failures.push("usage.totalTokens must be > 0");
136480
+ if (result.usage.model.length === 0) failures.push("usage.model must be non-empty");
136481
+ return failures;
136482
+ }
136483
+ },
136484
+ {
136485
+ id: "read-file",
136486
+ name: "Regression — reads a file via the Read tool",
136487
+ input: {
136488
+ prompt: "Read the file data.txt in the current directory using the Read tool, then tell me what it contains.",
136489
+ fixtures: [{
136490
+ name: "data.txt",
136491
+ content: "The quick brown fox jumps over the lazy dog."
136492
+ }]
136493
+ },
136494
+ rules: [{
136495
+ name: "reports file content",
136496
+ check: (output) => output.includes("quick brown fox")
136497
+ }]
136498
+ },
136499
+ {
136500
+ id: "write-file",
136501
+ name: "Regression — writes a file via the Write tool",
136502
+ input: { prompt: "Create a file named output.txt in the current directory using the Write tool. Write exactly the text: hello eval world. Make exactly one Write tool call and do not call any other tool afterwards. After the tool completes, immediately answer with the exact text you wrote and nothing else." },
136503
+ rules: [{
136504
+ name: "Write tool landed the file",
136505
+ check: (output) => output.includes("hello eval world")
136506
+ }],
136507
+ verifyFile: {
136508
+ path: "output.txt",
136509
+ content: "hello eval world"
136510
+ },
136511
+ extra: (result) => {
136512
+ const failures = [];
136513
+ if (!result.verifiedFile) failures.push("output.txt was not created with the exact content");
136514
+ return failures;
136515
+ }
136516
+ },
136517
+ {
136518
+ id: "isolated-workspace",
136519
+ name: "Regression — runs in an isolated temp workspace",
136520
+ input: {
136521
+ prompt: "Run \"pwd\" in a shell and answer with only the absolute path that the command printed.",
136522
+ fixtures: [{
136523
+ name: "marker.txt",
136524
+ content: "isolated"
136525
+ }]
136526
+ },
136527
+ rules: [{
136528
+ name: "prints an absolute path",
136529
+ check: (output) => output.match(/(?:\/[\w.\-/]+|[A-Za-z]:[\\/][\w.\\/-]*)/)?.[0] !== void 0
136530
+ }, {
136531
+ name: "path is not the repo",
136532
+ check: (output) => !output.match(/(?:\/[\w.\-/]+|[A-Za-z]:[\\/][\w.\\/-]*)/)?.[0]?.includes("scream-code")
136533
+ }]
136534
+ }
136535
+ ];
136536
+ /** Runs every eval case sequentially and returns a structured report. */
136537
+ async function runAllEvals(options = {}) {
136538
+ const results = [];
136539
+ let passedCount = 0;
136540
+ let failedCount = 0;
136541
+ for (const caseDef of EVAL_CASES) {
136542
+ let result;
136543
+ try {
136544
+ result = await runEvalPrompt(caseDef.input, {
136545
+ model: options.model,
136546
+ verifyFileAfterTurn: caseDef.verifyFile
136547
+ });
136548
+ } catch (error) {
136549
+ results.push({
136550
+ id: caseDef.id,
136551
+ name: caseDef.name,
136552
+ passed: false,
136553
+ failedRules: [],
136554
+ passedRules: [],
136555
+ extraFailures: [],
136556
+ output: "",
136557
+ error: error instanceof Error ? error.message : String(error),
136558
+ timedOut: false,
136559
+ toolCheckFailed: false
136560
+ });
136561
+ failedCount += 1;
136562
+ options.onProgress?.({
136563
+ completed: results.length,
136564
+ total: EVAL_CASES.length,
136565
+ currentId: caseDef.id,
136566
+ currentName: caseDef.name,
136567
+ passed: passedCount,
136568
+ failed: failedCount
136569
+ });
136570
+ continue;
136571
+ }
136572
+ const judged = judgeOutput(result.output, caseDef.rules);
136573
+ const extraFailures = caseDef.extra?.(result) ?? [];
136574
+ const passed = judged.failed.length === 0 && extraFailures.length === 0;
136575
+ if (passed) passedCount += 1;
136576
+ else failedCount += 1;
136577
+ const toolCheckFailed = caseDef.verifyFile !== void 0 && !result.verifiedFile;
136578
+ results.push({
136579
+ id: caseDef.id,
136580
+ name: caseDef.name,
136581
+ passed,
136582
+ failedRules: judged.failed,
136583
+ passedRules: judged.passed,
136584
+ extraFailures,
136585
+ output: result.output,
136586
+ timedOut: result.timedOut,
136587
+ toolCheckFailed
136588
+ });
136589
+ options.onProgress?.({
136590
+ completed: results.length,
136591
+ total: EVAL_CASES.length,
136592
+ currentId: caseDef.id,
136593
+ currentName: caseDef.name,
136594
+ passed: passedCount,
136595
+ failed: failedCount
136596
+ });
136597
+ }
136598
+ return {
136599
+ results,
136600
+ passed: passedCount,
136601
+ failed: failedCount
136602
+ };
136603
+ }
136604
+ //#endregion
136605
+ //#region src/tui/commands/eval.ts
136606
+ const EVAL_PANEL_DISMISS_MS = 6e4;
136607
+ let activeEvalPanel;
136608
+ let activeEvalTimer;
136609
+ /** Guards against overlapping runs (each run calls a real model and costs
136610
+ * tokens; a second /eval while one is running is almost certainly a mistake). */
136611
+ let evalRunning = false;
136612
+ function dismissEvalPanel(state) {
136613
+ if (activeEvalTimer !== void 0) {
136614
+ clearTimeout(activeEvalTimer);
136615
+ activeEvalTimer = void 0;
136616
+ }
136617
+ if (activeEvalPanel !== void 0) {
136618
+ state.transcriptContainer.removeChild(activeEvalPanel);
136619
+ activeEvalPanel = void 0;
136620
+ state.ui.requestRender();
136621
+ }
136622
+ }
136623
+ function clearEvalPanelState(state) {
136624
+ dismissEvalPanel(state);
136625
+ }
136626
+ /** Model selector for eval runs; falls back to the active session model. */
136627
+ function resolveEvalModel(host) {
136628
+ return host.state.appState.model;
136629
+ }
136630
+ /**
136631
+ * Runs the end-to-end evals in the background (they call a real model and can
136632
+ * take minutes) and renders the per-case report when done. Returns immediately
136633
+ * so the TUI keeps responding.
136634
+ */
136635
+ function runEvalCommand(host) {
136636
+ if (evalRunning) {
136637
+ host.showStatus(t("dispatch.eval_running"));
136638
+ return;
136639
+ }
136640
+ const model = resolveEvalModel(host);
136641
+ if (model === void 0 || model.length === 0) {
136642
+ host.showError(t("dispatch.eval_no_model"));
136643
+ return;
136644
+ }
136645
+ const sessionId = host.state.appState.sessionId;
136646
+ evalRunning = true;
136647
+ host.showStatus(t("dispatch.eval_started", { model }));
136648
+ (async () => {
136649
+ let summary;
136650
+ try {
136651
+ summary = await runAllEvals({
136652
+ model,
136653
+ onProgress: ({ completed, total, passed, failed, currentName }) => {
136654
+ host.showStatus(t("dispatch.eval_progress", {
136655
+ completed: String(completed),
136656
+ total: String(total),
136657
+ passed: String(passed),
136658
+ failed: String(failed),
136659
+ name: currentName
136660
+ }));
136661
+ }
136662
+ });
136663
+ } catch (error) {
136664
+ evalRunning = false;
136665
+ host.showError(t("dispatch.eval_failed", { error: error instanceof Error ? error.message : String(error) }));
136666
+ return;
136667
+ }
136668
+ evalRunning = false;
136669
+ if (sessionId !== void 0 && sessionId !== host.state.appState.sessionId) return;
136670
+ try {
136671
+ renderEvalSummary(host, summary);
136672
+ } catch (error) {
136673
+ host.showError(t("dispatch.eval_failed", { error: error instanceof Error ? error.message : String(error) }));
136674
+ }
136675
+ })();
136676
+ }
136677
+ function renderEvalSummary(host, summary) {
136678
+ const colors = host.state.theme.colors;
136679
+ const lines = [chalk.hex(colors.primary)(`${summary.passed} ${t("dispatch.eval_passed")} · ${summary.failed} ${t("dispatch.eval_failed_count")}`), ""];
136680
+ for (const result of summary.results) {
136681
+ const mark = result.passed ? "✓" : "×";
136682
+ const color = result.passed ? colors.success : colors.error;
136683
+ lines.push(`${chalk.hex(color)(mark)} ${result.name}`);
136684
+ if (result.passed) continue;
136685
+ lines.push(` ${chalk.hex(colors.error)(suggestionFor(result))}`);
136686
+ const details = [];
136687
+ for (const rule of result.failedRules) details.push(rule);
136688
+ for (const failure of result.extraFailures) details.push(failure);
136689
+ if (result.error !== void 0) details.push(result.error);
136690
+ if (result.output.length > 0) details.push(`output: ${result.output.slice(0, 160)}`);
136691
+ for (const detail of details) lines.push(` ${chalk.hex(colors.textDim)(`· ${detail}`)}`);
136692
+ }
136693
+ lines.push("");
136694
+ dismissEvalPanel(host.state);
136695
+ const panel = new UsagePanelComponent(lines, colors.primary, " Eval ");
136696
+ host.state.transcriptContainer.addChild(panel);
136697
+ activeEvalPanel = panel;
136698
+ activeEvalTimer = setTimeout(() => {
136699
+ dismissEvalPanel(host.state);
136700
+ }, EVAL_PANEL_DISMISS_MS);
136701
+ host.state.ui.requestRender();
136702
+ }
136703
+ /** Picks a user-facing self-check hint based on how the case failed. */
136704
+ function suggestionFor(result) {
136705
+ if (result.error !== void 0) return t("dispatch.eval_suggest_error");
136706
+ if (result.timedOut) return t("dispatch.eval_suggest_timeout");
136707
+ if (result.toolCheckFailed) return t("dispatch.eval_suggest_tool");
136708
+ return t("dispatch.eval_suggest_answer");
136709
+ }
136710
+ //#endregion
136035
136711
  //#region src/tui/commands/dispatch.ts
136036
136712
  function dispatchInput(host, text) {
136037
136713
  if (parseSlashInput(text) !== null) {
@@ -136205,6 +136881,9 @@ async function handleBuiltInSlashCommand(host, name, args) {
136205
136881
  case "logout":
136206
136882
  await handleLogoutCommand(host);
136207
136883
  return;
136884
+ case "eval":
136885
+ runEvalCommand(host);
136886
+ return;
136208
136887
  case "memory":
136209
136888
  await handleMemoryCommand(host, args);
136210
136889
  return;
@@ -137591,6 +138270,8 @@ var SessionEventHandler = class {
137591
138270
  this.renderedSkillActivationIds.clear();
137592
138271
  this.renderedMcpServerStatusKeys.clear();
137593
138272
  this.stopAllMcpServerStatusSpinners();
138273
+ this.pendingSkillCandidates = [];
138274
+ this.promptedSkillCandidates.clear();
137594
138275
  this.host.setAppState({ subagentUsage: {} });
137595
138276
  }
137596
138277
  startSubscription() {
@@ -137846,6 +138527,12 @@ var SessionEventHandler = class {
137846
138527
  const todos = this.host.state.todoPanel.getTodos();
137847
138528
  if (todos.length > 0 && todos.every((todo) => todo.status === "done")) this.host.streamingUI.setTodoList([]);
137848
138529
  this.host.streamingUI.resetToolUi();
138530
+ if (this.pendingSkillCandidates.length > 0 && this.host.state.queuedMessages.length === 0) {
138531
+ const pending = this.pendingSkillCandidates;
138532
+ this.pendingSkillCandidates = [];
138533
+ const session = this.host.session;
138534
+ if (session !== void 0) for (const candidate of pending) this.promptSkillCandidate(session, candidate.name, candidate.purpose);
138535
+ }
137849
138536
  this.host.streamingUI.finalizeTurn(sendQueued);
137850
138537
  }
137851
138538
  handleStepBegin(event) {
@@ -138220,6 +138907,10 @@ var SessionEventHandler = class {
138220
138907
  }
138221
138908
  /** Candidates already prompted this session, to avoid repeating the request. */
138222
138909
  promptedSkillCandidates = /* @__PURE__ */ new Set();
138910
+ /** Skill candidates detected while a turn was still active. Flushed at the
138911
+ * next turn end so the AskUserQuestion dialog never interrupts an ongoing
138912
+ * response. */
138913
+ pendingSkillCandidates = [];
138223
138914
  /**
138224
138915
  * Handles a `skill_candidate` event emitted after compaction detected a
138225
138916
  * reusable process in the summary. Best-effort and non-fatal: if the session
@@ -138227,6 +138918,10 @@ var SessionEventHandler = class {
138227
138918
  * no confirmation. When it works, the model runs an AskUserQuestion dialog
138228
138919
  * (确定/忽略) so the user can confirm or dismiss with a single keystroke,
138229
138920
  * then generates the skill via MakeSkillPlanTool on confirmation.
138921
+ *
138922
+ * If a turn is still streaming (the compaction finished while the agent was
138923
+ * mid-response), the candidate is queued and prompted at the next turn end —
138924
+ * never interrupting the running turn.
138230
138925
  */
138231
138926
  handleSkillCandidate(event) {
138232
138927
  try {
@@ -138236,10 +138931,25 @@ var SessionEventHandler = class {
138236
138931
  this.promptedSkillCandidates.add(name);
138237
138932
  const session = this.host.session;
138238
138933
  if (session === void 0) return;
138239
- const request = `检测到可复用过程「${name}」${purpose ? `(${purpose})` : ""}。请用 AskUserQuestion 工具询问用户是否将其保存为可复用技能(选项:生成 / 忽略)。用户选择「生成」后,用 MakeSkillPlanTool 生成该技能;选择「忽略」则直接说明已跳过。`;
138240
- session.prompt(request).catch(() => {});
138934
+ if (this.host.streamingUI.hasActiveTurn()) {
138935
+ this.pendingSkillCandidates.push({
138936
+ name,
138937
+ purpose
138938
+ });
138939
+ return;
138940
+ }
138941
+ this.promptSkillCandidate(session, name, purpose);
138241
138942
  } catch {}
138242
138943
  }
138944
+ /** Fire the AskUserQuestion prompt for a skill candidate. On failure, surface
138945
+ * a status hint instead of failing silently so the user still learns the
138946
+ * candidate was detected. */
138947
+ promptSkillCandidate(session, name, purpose) {
138948
+ const request = `检测到可复用过程「${name}」${purpose ? `(${purpose})` : ""}。请用 AskUserQuestion 工具询问用户是否将其保存为可复用技能(选项:生成 / 忽略)。用户选择「生成」后,用 MakeSkillPlanTool 生成该技能;选择「忽略」则直接说明已跳过。`;
138949
+ session.prompt(request).catch(() => {
138950
+ this.host.showStatus(`检测到可复用过程「${name}」,但询问失败。可稍后手动处理。`, this.host.state.theme.colors.warning);
138951
+ });
138952
+ }
138243
138953
  finishCompaction(sendQueued) {
138244
138954
  if (!this.host.streamingUI.hasActiveTurn()) {
138245
138955
  this.host.setAppState({
@@ -144537,7 +145247,7 @@ var SessionManager$1 = class {
144537
145247
  }
144538
145248
  resetSessionRuntime() {
144539
145249
  this.host.state.queuedMessages = [];
144540
- this.host.harness.interactiveAgentId = MAIN_AGENT_ID;
145250
+ this.host.harness.interactiveAgentId = MAIN_AGENT_ID$1;
144541
145251
  this.host.streamingUI.discardPending();
144542
145252
  this.host.streamingUI.resetToolCallState();
144543
145253
  this.host.streamingUI.resetToolUi();
@@ -144590,7 +145300,7 @@ function renderDisplayBlock(block, s, colors) {
144590
145300
  });
144591
145301
  case "file_content": {
144592
145302
  const lang = block.language ?? langFromPath(block.path);
144593
- const allLines = highlightLines(replaceTabs(block.content), lang);
145303
+ const allLines = highlightLines(replaceTabs(block.content), lang, colors);
144594
145304
  const shown = allLines.slice(0, CONTENT_SUMMARY_MAX_LINES);
144595
145305
  const lines = [s.strong(block.path)];
144596
145306
  for (const [i, line] of shown.entries()) lines.push(s.gutter(String(i + 1).padStart(4) + " ") + line);
@@ -145001,7 +145711,7 @@ function buildDiffBody(block, colors) {
145001
145711
  }
145002
145712
  function buildFileContentBody(block, colors) {
145003
145713
  const lang = block.language ?? langFromPath(block.path);
145004
- const highlighted = highlightLines(block.content, lang);
145714
+ const highlighted = highlightLines(block.content, lang, colors);
145005
145715
  const gutter = chalk.hex(colors.diffGutter);
145006
145716
  return {
145007
145717
  lines: highlighted.map((line, i) => gutter(String(i + 1).padStart(4) + " ") + line),
@@ -147194,6 +147904,7 @@ var ScreamTUI = class {
147194
147904
  this.sessionEventHandler.stopAllMcpServerStatusSpinners();
147195
147905
  clearGoalState();
147196
147906
  clearInfoPanelState(this.state);
147907
+ clearEvalPanelState(this.state);
147197
147908
  this.state.terminal.write("\x1B[3J");
147198
147909
  this.transcriptController.clearAndRedraw();
147199
147910
  this.state.ui.requestRender(true);