dsh-code 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.mjs CHANGED
@@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto";
4
4
  import * as fs from "node:fs";
5
5
  import { readFileSync, realpathSync } from "node:fs";
6
6
  import os, { homedir } from "node:os";
7
- import { mkdir, open, readFile, rm, stat, writeFile } from "node:fs/promises";
7
+ import { mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
8
8
  import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
9
9
  import z from "@deepseek-ai/schemastery";
10
10
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
@@ -21657,16 +21657,16 @@ var import_emoji_regex = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(
21657
21657
  return /[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26D3\uFE0F?(?:\u200D\uD83D\uDCA5)?|\u26F9(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF43\uDF45-\uDF4A\uDF4C-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDF44(?:\u200D\uD83D\uDFEB)?|\uDF4B(?:\u200D\uD83D\uDFE9)?|\uDFC3(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E-\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4\uDEB5](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC25\uDC27-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE41\uDE43\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED8\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC08(?:\u200D\u2B1B)?|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC26(?:\u200D(?:\u2B1B|\uD83D\uDD25))?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFE])))?))?|\uDD75(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?|\uDE42(?:\u200D[\u2194\u2195]\uFE0F?)?|\uDEB6(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF8](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3C-\uDD3E\uDDB8\uDDB9\uDDCD\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE7C\uDE80-\uDE8A\uDE8E-\uDEC2\uDEC6\uDEC8\uDECD-\uDEDC\uDEDF-\uDEEA\uDEEF]|\uDDCE(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1|\uDDD1\u200D\uD83E\uDDD2(?:\u200D\uD83E\uDDD2)?|\uDDD2(?:\u200D\uD83E\uDDD2)?))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?)/g;
21658
21658
  };
21659
21659
  })))(), 1);
21660
- const segmenter$1 = new Intl.Segmenter();
21660
+ const segmenter$2 = new Intl.Segmenter();
21661
21661
  const defaultIgnorableCodePointRegex = /^\p{Default_Ignorable_Code_Point}$/u;
21662
- function stringWidth(string, options = {}) {
21662
+ function stringWidth$1(string, options = {}) {
21663
21663
  if (typeof string !== "string" || string.length === 0) return 0;
21664
21664
  const { ambiguousIsNarrow = true, countAnsiEscapeCodes = false } = options;
21665
21665
  if (!countAnsiEscapeCodes) string = stripAnsi(string);
21666
21666
  if (string.length === 0) return 0;
21667
21667
  let width = 0;
21668
21668
  const eastAsianWidthOptions = { ambiguousAsWide: !ambiguousIsNarrow };
21669
- for (const { segment: character } of segmenter$1.segment(string)) {
21669
+ for (const { segment: character } of segmenter$2.segment(string)) {
21670
21670
  const codePoint = character.codePointAt(0);
21671
21671
  if (codePoint <= 31 || codePoint >= 127 && codePoint <= 159) continue;
21672
21672
  if (codePoint >= 8203 && codePoint <= 8207 || codePoint === 65279) continue;
@@ -21686,7 +21686,7 @@ function stringWidth(string, options = {}) {
21686
21686
  //#region node_modules/.pnpm/widest-line@5.0.0/node_modules/widest-line/index.js
21687
21687
  function widestLine(string) {
21688
21688
  let lineWidth = 0;
21689
- for (const line of string.split("\n")) lineWidth = Math.max(lineWidth, stringWidth(line));
21689
+ for (const line of string.split("\n")) lineWidth = Math.max(lineWidth, stringWidth$1(line));
21690
21690
  return lineWidth;
21691
21691
  }
21692
21692
  //#endregion
@@ -21885,14 +21885,14 @@ const ANSI_SGR_TERMINATOR = "m";
21885
21885
  const ANSI_ESCAPE_LINK = `${ANSI_OSC}8;;`;
21886
21886
  const wrapAnsiCode = (code) => `${ESCAPES$3.values().next().value}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
21887
21887
  const wrapAnsiHyperlink = (url) => `${ESCAPES$3.values().next().value}${ANSI_ESCAPE_LINK}${url}${ANSI_ESCAPE_BELL}`;
21888
- const wordLengths = (string) => string.split(" ").map((character) => stringWidth(character));
21888
+ const wordLengths = (string) => string.split(" ").map((character) => stringWidth$1(character));
21889
21889
  const wrapWord = (rows, word, columns) => {
21890
21890
  const characters = [...word];
21891
21891
  let isInsideEscape = false;
21892
21892
  let isInsideLinkEscape = false;
21893
- let visible = stringWidth(stripAnsi(rows.at(-1)));
21893
+ let visible = stringWidth$1(stripAnsi(rows.at(-1)));
21894
21894
  for (const [index, character] of characters.entries()) {
21895
- const characterLength = stringWidth(character);
21895
+ const characterLength = stringWidth$1(character);
21896
21896
  if (visible + characterLength <= columns) rows[rows.length - 1] += character;
21897
21897
  else {
21898
21898
  rows.push(character);
@@ -21923,7 +21923,7 @@ const stringVisibleTrimSpacesRight = (string) => {
21923
21923
  const words = string.split(" ");
21924
21924
  let last = words.length;
21925
21925
  while (last > 0) {
21926
- if (stringWidth(words[last - 1]) > 0) break;
21926
+ if (stringWidth$1(words[last - 1]) > 0) break;
21927
21927
  last--;
21928
21928
  }
21929
21929
  if (last === words.length) return string;
@@ -21938,7 +21938,7 @@ const exec = (string, columns, options = {}) => {
21938
21938
  let rows = [""];
21939
21939
  for (const [index, word] of string.split(" ").entries()) {
21940
21940
  if (options.trim !== false) rows[rows.length - 1] = rows.at(-1).trimStart();
21941
- let rowLength = stringWidth(rows.at(-1));
21941
+ let rowLength = stringWidth$1(rows.at(-1));
21942
21942
  if (index !== 0) {
21943
21943
  if (rowLength >= columns && (options.wordWrap === false || options.trim === false)) {
21944
21944
  rows.push("");
@@ -22086,7 +22086,7 @@ function cliTruncate(text, columns, options = {}) {
22086
22086
  if (typeof columns !== "number") throw new TypeError(`Expected \`columns\` to be a number, got ${typeof columns}`);
22087
22087
  if (columns < 1) return "";
22088
22088
  if (columns === 1) return truncationCharacter;
22089
- const length = stringWidth(text);
22089
+ const length = stringWidth$1(text);
22090
22090
  if (length <= columns) return text;
22091
22091
  if (position === "start") {
22092
22092
  if (preferTruncationOnSpace) {
@@ -22094,7 +22094,7 @@ function cliTruncate(text, columns, options = {}) {
22094
22094
  return truncationCharacter + sliceAnsi$1(text, nearestSpace, length).trim();
22095
22095
  }
22096
22096
  if (space === true) truncationCharacter += " ";
22097
- return truncationCharacter + sliceAnsi$1(text, length - columns + stringWidth(truncationCharacter), length);
22097
+ return truncationCharacter + sliceAnsi$1(text, length - columns + stringWidth$1(truncationCharacter), length);
22098
22098
  }
22099
22099
  if (position === "middle") {
22100
22100
  if (space === true) truncationCharacter = ` ${truncationCharacter} `;
@@ -22104,12 +22104,12 @@ function cliTruncate(text, columns, options = {}) {
22104
22104
  const spaceNearSecondBreakPoint = getIndexOfNearestSpace(text, length - (columns - half) + 1, true);
22105
22105
  return sliceAnsi$1(text, 0, spaceNearFirstBreakPoint) + truncationCharacter + sliceAnsi$1(text, spaceNearSecondBreakPoint, length).trim();
22106
22106
  }
22107
- return sliceAnsi$1(text, 0, half) + truncationCharacter + sliceAnsi$1(text, length - (columns - half) + stringWidth(truncationCharacter), length);
22107
+ return sliceAnsi$1(text, 0, half) + truncationCharacter + sliceAnsi$1(text, length - (columns - half) + stringWidth$1(truncationCharacter), length);
22108
22108
  }
22109
22109
  if (position === "end") {
22110
22110
  if (preferTruncationOnSpace) return sliceAnsi$1(text, 0, getIndexOfNearestSpace(text, columns - 1)) + truncationCharacter;
22111
22111
  if (space === true) truncationCharacter = ` ${truncationCharacter}`;
22112
- return sliceAnsi$1(text, 0, columns - stringWidth(truncationCharacter)) + truncationCharacter;
22112
+ return sliceAnsi$1(text, 0, columns - stringWidth$1(truncationCharacter)) + truncationCharacter;
22113
22113
  }
22114
22114
  throw new Error(`Expected \`options.position\` to be either \`start\`, \`middle\` or \`end\`, got ${position}`);
22115
22115
  }
@@ -23064,7 +23064,7 @@ var Output = class {
23064
23064
  if (clipHorizontally) {
23065
23065
  lines = lines.map((line) => {
23066
23066
  const from = x < clip.x1 ? clip.x1 - x : 0;
23067
- const width = stringWidth(line);
23067
+ const width = stringWidth$1(line);
23068
23068
  return sliceAnsi(line, from, x + width > clip.x2 ? clip.x2 - x : width);
23069
23069
  });
23070
23070
  if (x < clip.x1) x = clip.x1;
@@ -24355,6 +24355,180 @@ function followInspectorCursor(cursor, previousLength, nextLength) {
24355
24355
  return Math.min(cursor, nextLast);
24356
24356
  }
24357
24357
  //#endregion
24358
+ //#region src/render/width.ts
24359
+ /**
24360
+ * Precise terminal-cell width measurement — the single authority every
24361
+ * budget, wrap, and truncation path shares. The previous heuristic
24362
+ * (`codePoint > 0x2e7f ? 2 : 1)) mis-sized Hangul Jamo (narrow), high
24363
+ * non-CJK code points (wide), and emoji: text-default glyphs like ✳ ⚠ ❤
24364
+ * counted 2 while terminals draw 1, and VS16 sequences counted 1 while
24365
+ * terminals draw 2 — the exact drift class the community dsh-TUI string
24366
+ * engine documents (a spinner glyph drifting one column per frame). This
24367
+ * module adapts that engine's rules without its Ink-fork renderer: an ASCII
24368
+ * fast path, grapheme-cluster iteration via Intl.Segmenter (code-point
24369
+ * fallback), a merged East-Asian-Wide/Fullwidth + Emoji_Presentation range
24370
+ * table, text-default emoji = 1, VS16 = 2, marks/selectors/ZWJ = 0.
24371
+ * @module @deepseek-ai/dsh-code/render/width
24372
+ */
24373
+ /**
24374
+ * Code-point ranges rendered two cells wide: East Asian Wide/Fullwidth
24375
+ * blocks merged with the Emoji_Presentation set (default-wide emoji) and
24376
+ * regional indicators (flags). Sorted, inclusive, binary-searched.
24377
+ */
24378
+ const WIDE_RANGES = [
24379
+ [4352, 4447],
24380
+ [8986, 8987],
24381
+ [9001, 9002],
24382
+ [9193, 9196],
24383
+ [9200, 9200],
24384
+ [9203, 9203],
24385
+ [9208, 9210],
24386
+ [9725, 9726],
24387
+ [9748, 9749],
24388
+ [9800, 9811],
24389
+ [9855, 9855],
24390
+ [9875, 9875],
24391
+ [9889, 9889],
24392
+ [9898, 9899],
24393
+ [9917, 9918],
24394
+ [9924, 9925],
24395
+ [9934, 9934],
24396
+ [9940, 9940],
24397
+ [9962, 9962],
24398
+ [9970, 9971],
24399
+ [9973, 9973],
24400
+ [9978, 9978],
24401
+ [9981, 9981],
24402
+ [9989, 9989],
24403
+ [9994, 9995],
24404
+ [10024, 10024],
24405
+ [10060, 10060],
24406
+ [10062, 10062],
24407
+ [10067, 10069],
24408
+ [10071, 10071],
24409
+ [10133, 10135],
24410
+ [10160, 10160],
24411
+ [10175, 10175],
24412
+ [11035, 11036],
24413
+ [11088, 11088],
24414
+ [11093, 11093],
24415
+ [11904, 12350],
24416
+ [12353, 13311],
24417
+ [13312, 19903],
24418
+ [19968, 42191],
24419
+ [43360, 43391],
24420
+ [44032, 55203],
24421
+ [63744, 64255],
24422
+ [65040, 65049],
24423
+ [65072, 65103],
24424
+ [65280, 65376],
24425
+ [65504, 65510],
24426
+ [94176, 94180],
24427
+ [94208, 101119],
24428
+ [110592, 111359],
24429
+ [126980, 126980],
24430
+ [127183, 127183],
24431
+ [127374, 127374],
24432
+ [127377, 127386],
24433
+ [127462, 127487],
24434
+ [127488, 127776],
24435
+ [127789, 127797],
24436
+ [127799, 127868],
24437
+ [127870, 127891],
24438
+ [127904, 127946],
24439
+ [127951, 127955],
24440
+ [127968, 127984],
24441
+ [127988, 127988],
24442
+ [127992, 128062],
24443
+ [128064, 128064],
24444
+ [128066, 128252],
24445
+ [128255, 128317],
24446
+ [128331, 128334],
24447
+ [128336, 128359],
24448
+ [128378, 128378],
24449
+ [128405, 128406],
24450
+ [128420, 128420],
24451
+ [128507, 128591],
24452
+ [128640, 128709],
24453
+ [128716, 128716],
24454
+ [128720, 128722],
24455
+ [128725, 128727],
24456
+ [128747, 128748],
24457
+ [128756, 128764],
24458
+ [128992, 129003],
24459
+ [129292, 129338],
24460
+ [129340, 129349],
24461
+ [129351, 129535],
24462
+ [129648, 129660],
24463
+ [129664, 129673],
24464
+ [129679, 129734],
24465
+ [129742, 129756],
24466
+ [129759, 129769],
24467
+ [129776, 129784],
24468
+ [131072, 196605],
24469
+ [196608, 262141]
24470
+ ];
24471
+ /** Whether one code point falls inside a wide range (binary search). */
24472
+ function isWideCodePoint(code) {
24473
+ let low = 0;
24474
+ let high = WIDE_RANGES.length - 1;
24475
+ while (low <= high) {
24476
+ const mid = low + high >> 1;
24477
+ const [start, end] = WIDE_RANGES[mid];
24478
+ if (code < start) high = mid - 1;
24479
+ else if (code > end) low = mid + 1;
24480
+ else return true;
24481
+ }
24482
+ return false;
24483
+ }
24484
+ /** Zero-width code points: combining marks, enclosing marks, format controls. */
24485
+ const ZERO_WIDTH = /^[\p{Mn}\p{Me}\p{Cf}]$/u;
24486
+ /** Module-cached grapheme segmenter; undefined when the runtime lacks it. */
24487
+ let segmenter$1;
24488
+ try {
24489
+ segmenter$1 = new Intl.Segmenter("en", { granularity: "grapheme" });
24490
+ } catch {
24491
+ segmenter$1 = void 0;
24492
+ }
24493
+ /** Split text into grapheme clusters (code points when Segmenter is absent). */
24494
+ function splitGraphemes$1(text) {
24495
+ if (segmenter$1 === void 0) return Array.from(text);
24496
+ return Array.from(segmenter$1.segment(text), (part) => part.segment);
24497
+ }
24498
+ /** Terminal-cell width of one grapheme cluster. */
24499
+ function graphemeWidth(cluster) {
24500
+ if (cluster.includes("️", 0)) return 2;
24501
+ const first = cluster.codePointAt(0) ?? 0;
24502
+ if (ZERO_WIDTH.test(String.fromCodePoint(first))) return 0;
24503
+ return isWideCodePoint(first) ? 2 : 1;
24504
+ }
24505
+ /**
24506
+ * Terminal-cell width of a string: an ASCII fast path avoids the segmenter
24507
+ * for the overwhelmingly common case; everything else sums grapheme clusters.
24508
+ * Control characters occupy no cells (display sanitization makes them
24509
+ * visible escapes before they ever reach a budget).
24510
+ * @param text - display-safe or raw text to measure.
24511
+ * @returns the column count the terminal will draw.
24512
+ */
24513
+ function stringWidth(text) {
24514
+ let asciiOnly = true;
24515
+ for (let index = 0; index < text.length; index += 1) if (text.charCodeAt(index) > 127) {
24516
+ asciiOnly = false;
24517
+ break;
24518
+ }
24519
+ if (asciiOnly) {
24520
+ let columns = 0;
24521
+ for (let index = 0; index < text.length; index += 1) {
24522
+ const code = text.charCodeAt(index);
24523
+ if (code >= 32 && code !== 127) columns += 1;
24524
+ }
24525
+ return columns;
24526
+ }
24527
+ let columns = 0;
24528
+ for (const cluster of splitGraphemes$1(text)) columns += graphemeWidth(cluster);
24529
+ return columns;
24530
+ }
24531
+ //#endregion
24358
24532
  //#region src/render/text.ts
24359
24533
  /**
24360
24534
  * Display-boundary sanitization for externally sourced text (model output,
@@ -24395,56 +24569,45 @@ function displayText(text) {
24395
24569
  function singleLineText(text) {
24396
24570
  return displayText(text).replace(/\r?\n/gu, " ↵ ").replace(/\t/gu, " ");
24397
24571
  }
24398
- /** Terminal-cell width matching the TUI's existing CJK-aware wrapping rule. */
24399
- function cellWidth(text) {
24400
- let columns = 0;
24401
- for (const char of text) columns += (char.codePointAt(0) ?? 0) > 11903 ? 2 : 1;
24402
- return columns;
24403
- }
24404
24572
  /**
24405
24573
  * Truncate one display-safe row without ever exceeding its physical-column
24406
24574
  * budget. The ellipsis is included inside the budget, matching Codex's popup
24407
- * truncation contract; the previous app-local helper appended it after the
24408
- * row was already full and could force an extra terminal wrap.
24575
+ * truncation contract; the cut walks grapheme clusters so emoji and
24576
+ * combining sequences never split mid-cluster.
24409
24577
  */
24410
24578
  function truncateColumns(text, columns) {
24411
24579
  const limit = Math.max(0, Math.floor(columns));
24412
24580
  if (limit === 0) return "";
24413
- if (cellWidth(text) <= limit) return text;
24581
+ if (stringWidth(text) <= limit) return text;
24414
24582
  const contentLimit = limit - 1;
24415
24583
  let used = 0;
24416
24584
  let result = "";
24417
- for (const char of text) {
24418
- const width = cellWidth(char);
24585
+ for (const cluster of splitGraphemes$1(text)) {
24586
+ const width = graphemeWidth(cluster);
24419
24587
  if (used + width > contentLimit) break;
24420
- result += char;
24588
+ result += cluster;
24421
24589
  used += width;
24422
24590
  }
24423
24591
  return `${result}…`;
24424
24592
  }
24425
- /** Read one Unicode character immediately before `end`. */
24426
- function previousCharacter(text, end) {
24427
- const last = text.charCodeAt(end - 1);
24428
- if (last >= 56320 && last <= 57343 && end >= 2) {
24429
- const first = text.charCodeAt(end - 2);
24430
- if (first >= 55296 && first <= 56319) return {
24431
- char: text.slice(end - 2, end),
24432
- start: end - 2
24433
- };
24434
- }
24435
- return {
24436
- char: text.slice(end - 1, end),
24437
- start: end - 1
24438
- };
24439
- }
24593
+ /** Punctuation that must never START a physical row (CJK kinsoku tail set). */
24594
+ const ROW_START_FORBIDDEN = ",。、;:!?)】」』〉》…‥";
24595
+ /** Punctuation that must never END a physical row (CJK kinsoku head set). */
24596
+ const ROW_END_FORBIDDEN = "(【「『〈《";
24440
24597
  /**
24441
- * Keep only the newest display-safe text that fits a terminal rectangle.
24442
- * The scan walks backward and stops as soon as the suffix is full, so a long
24443
- * reasoning stream does not rescan its entire accumulated prefix per chunk.
24444
- * Explicit newlines and terminal wrapping both consume rows; tabs expand to
24445
- * two spaces so terminal tab stops (which render at contextual column 8
24446
- * boundaries, not at the budgeted cell count) cannot inflate the physical
24447
- * row count of the live region.
24598
+ * Keep the newest display-safe text that fits a terminal rectangle, wrapping
24599
+ * FORWARD from the start of the text and slicing the tail rows.
24600
+ *
24601
+ * Forward wrapping is what keeps a streaming tail calm: rows already produced
24602
+ * never re-wrap as tokens append (a backward scan recomputes every wrap point
24603
+ * per chunk and the whole visible block jumps), and the wrap rules match the
24604
+ * settled text's renderer so the flush at turn end does not reflow the block
24605
+ * a second time. CJK kinsoku applies at both edges: closing punctuation
24606
+ * overhangs up to two cells onto the filled row instead of starting the next
24607
+ * one (within the caret column the caller reserves), and opening punctuation
24608
+ * moves down instead of dangling at a row end. Tabs expand to two spaces so
24609
+ * terminal tab stops cannot inflate the physical row count; clusters carry
24610
+ * emoji presentation and combining marks whole.
24448
24611
  * @param text - raw externally sourced text.
24449
24612
  * @param columns - available terminal columns.
24450
24613
  * @param rows - available terminal rows.
@@ -24453,38 +24616,43 @@ function previousCharacter(text, end) {
24453
24616
  function displayTail(text, columns, rows) {
24454
24617
  const columnLimit = Math.max(1, Math.floor(columns));
24455
24618
  const rowLimit = Math.max(1, Math.floor(rows));
24456
- const reversed = [];
24457
- let row = 1;
24619
+ const wrapped = [];
24620
+ let current = "";
24458
24621
  let used = 0;
24459
- let end = text.length;
24460
- while (end > 0) {
24461
- const previous = previousCharacter(text, end);
24462
- if (previous.char === "\n") {
24463
- if (row >= rowLimit) break;
24464
- reversed.push("\n");
24465
- row += 1;
24466
- used = 0;
24467
- end = previous.start;
24622
+ let lastCluster = "";
24623
+ const flush = () => {
24624
+ wrapped.push(current);
24625
+ current = "";
24626
+ used = 0;
24627
+ lastCluster = "";
24628
+ };
24629
+ for (const cluster of splitGraphemes$1(text)) {
24630
+ if (cluster === "\n") {
24631
+ flush();
24468
24632
  continue;
24469
24633
  }
24470
- const safe = previous.char === " " ? " " : displayText(previous.char);
24471
- const width = cellWidth(safe);
24634
+ const safe = cluster === " " ? " " : displayText(cluster);
24635
+ const width = stringWidth(safe);
24472
24636
  if (used > 0 && used + width > columnLimit) {
24473
- if (row >= rowLimit) break;
24474
- reversed.push("\n");
24475
- row += 1;
24476
- used = 0;
24477
- }
24478
- const extraRows = Math.floor(Math.max(0, width - 1) / columnLimit);
24479
- if (row + extraRows > rowLimit) break;
24480
- row += extraRows;
24481
- reversed.push(safe);
24482
- used = extraRows === 0 ? used + width : width - extraRows * columnLimit;
24483
- end = previous.start;
24637
+ if (!(width <= 2 && ROW_START_FORBIDDEN.includes(cluster))) {
24638
+ if (lastCluster !== "" && ROW_END_FORBIDDEN.includes(lastCluster)) {
24639
+ const carried = lastCluster;
24640
+ current = current.slice(0, current.length - carried.length);
24641
+ flush();
24642
+ current = carried;
24643
+ used = stringWidth(carried);
24644
+ } else flush();
24645
+ }
24646
+ }
24647
+ current += safe;
24648
+ used += width;
24649
+ lastCluster = cluster;
24484
24650
  }
24651
+ if (current !== "" || wrapped.length > 0 && text.endsWith("\n")) flush();
24652
+ const truncated = wrapped.length > rowLimit;
24485
24653
  return {
24486
- text: reversed.reverse().join(""),
24487
- truncated: end > 0
24654
+ text: (truncated ? wrapped.slice(-rowLimit) : wrapped).join("\n"),
24655
+ truncated
24488
24656
  };
24489
24657
  }
24490
24658
  //#endregion
@@ -24947,535 +25115,14 @@ function reasoningOf(content) {
24947
25115
  function estimateTokens(text) {
24948
25116
  let wide = 0;
24949
25117
  let narrow = 0;
24950
- for (const char of text) if ((char.codePointAt(0) ?? 0) > 11903) wide += 1;
25118
+ for (const cluster of splitGraphemes$1(text)) if (graphemeWidth(cluster) > 1) wide += 1;
24951
25119
  else narrow += 1;
24952
25120
  return wide + Math.ceil(narrow / 4);
24953
25121
  }
24954
- /** A fresh, empty transcript view. */
24955
- function createTranscriptView() {
24956
- return {
24957
- entries: [],
24958
- streaming: "",
24959
- streamingReasoning: "",
24960
- todos: [],
24961
- toolCallOrdinal: 0,
24962
- busy: false,
24963
- busySince: 0,
24964
- model: "",
24965
- plan: false,
24966
- permission: "",
24967
- title: "",
24968
- sandbox: "",
24969
- goal: void 0,
24970
- pending: {
24971
- "next-turn": [],
24972
- "next-step": []
24973
- },
24974
- stats: {
24975
- turns: 0,
24976
- steps: 0,
24977
- llmMs: 0,
24978
- toolMs: 0,
24979
- usage: {
24980
- inputTokens: 0,
24981
- outputTokens: 0,
24982
- cacheReadTokens: 0
24983
- },
24984
- lastPromptTokens: 0,
24985
- contextWindow: 0,
24986
- contextSegments: {
24987
- system: 0,
24988
- prompt: 0,
24989
- assistant: 0,
24990
- thinking: 0,
24991
- tools: 0
24992
- },
24993
- ttftMs: 0,
24994
- ttftSteps: 0,
24995
- decodeMs: 0,
24996
- decodeTokens: 0,
24997
- reasoningEffort: ""
24998
- },
24999
- anchors: {
25000
- stepStart: /* @__PURE__ */ new Map(),
25001
- toolStart: /* @__PURE__ */ new Map(),
25002
- firstChunkAt: /* @__PURE__ */ new Map(),
25003
- compactionTokens: /* @__PURE__ */ new Map(),
25004
- lastPruneTokens: 0,
25005
- turnFiles: /* @__PURE__ */ new Map(),
25006
- turnSteps: /* @__PURE__ */ new Map(),
25007
- turnTools: /* @__PURE__ */ new Map()
25008
- }
25009
- };
25010
- }
25011
25122
  /** Full prompt text of a queued message (identical to the durable user row it retires into). */
25012
25123
  function pendingText(content) {
25013
25124
  return textOf(content);
25014
25125
  }
25015
- /**
25016
- * Fold one session event into an updated view (copy-on-write).
25017
- * @param view - the view before the event.
25018
- * @param event - one durable session event from `session/event` or the log.
25019
- * @returns the view after the event; the input view is never mutated.
25020
- */
25021
- function projectEvent(view, event) {
25022
- switch (event.type) {
25023
- case "user/message": {
25024
- const message = event.data;
25025
- let entries = view.entries;
25026
- let pending = view.pending;
25027
- for (const target of ["next-turn", "next-step"]) {
25028
- const index = pending[target].indexOf(message.id);
25029
- if (index < 0) continue;
25030
- pending = {
25031
- ...pending,
25032
- [target]: pending[target].filter((_, i) => i !== index)
25033
- };
25034
- entries = entries.filter((entry) => !(entry.kind === "pending" && entry.messageId === message.id));
25035
- }
25036
- const text = textOf(message.content);
25037
- const images = imagesOf(message.content);
25038
- if (message.source.kind === "user") return {
25039
- ...view,
25040
- pending,
25041
- entries: [...entries, {
25042
- kind: "user",
25043
- text,
25044
- notice: false,
25045
- ...images.length === 0 ? {} : { images }
25046
- }],
25047
- stats: {
25048
- ...view.stats,
25049
- contextSegments: {
25050
- ...view.stats.contextSegments,
25051
- prompt: view.stats.contextSegments.prompt + estimateTokens(text)
25052
- }
25053
- }
25054
- };
25055
- const notice = message.source.kind === "plugin" && message.source.form === "notice" ? message.source.summary : message.source.kind;
25056
- const summary = boundContextSummary(notice);
25057
- return {
25058
- ...view,
25059
- pending,
25060
- entries: [...entries, {
25061
- kind: "user",
25062
- text: summary,
25063
- notice: true
25064
- }],
25065
- stats: {
25066
- ...view.stats,
25067
- contextSegments: {
25068
- ...view.stats.contextSegments,
25069
- system: view.stats.contextSegments.system + estimateTokens(summary)
25070
- }
25071
- }
25072
- };
25073
- }
25074
- case "agent/inbox/spliced": {
25075
- const { target, start, removedCount = 0, inserted } = event.data;
25076
- const ids = view.pending[target];
25077
- const removed = ids.slice(start, start + removedCount);
25078
- const nextIds = [
25079
- ...ids.slice(0, start),
25080
- ...ids.slice(start + removedCount),
25081
- ...inserted.map((message) => message.id)
25082
- ];
25083
- let entries = view.entries;
25084
- if (removed.length > 0) {
25085
- const removedSet = new Set(removed);
25086
- entries = entries.filter((entry) => !(entry.kind === "pending" && entry.target === target && removedSet.has(entry.messageId)));
25087
- }
25088
- for (const message of inserted) entries = [...entries, {
25089
- kind: "pending",
25090
- messageId: message.id,
25091
- target,
25092
- text: pendingText(message.content),
25093
- ...imagesOf(message.content).length === 0 ? {} : { images: imagesOf(message.content) }
25094
- }];
25095
- return {
25096
- ...view,
25097
- entries,
25098
- pending: {
25099
- ...view.pending,
25100
- [target]: nextIds
25101
- }
25102
- };
25103
- }
25104
- case "assistant/chunk": {
25105
- const chunk = event.data.chunk;
25106
- const key = `${event.data.turn}:${event.data.step}`;
25107
- const delta = chunk.type === "text-delta" || chunk.type === "reasoning-delta" ? chunk.text : "";
25108
- let stats = view.stats;
25109
- if (delta !== "" && !view.anchors.firstChunkAt.has(key)) {
25110
- view.anchors.firstChunkAt.set(key, event.time);
25111
- const started = view.anchors.stepStart.get(key);
25112
- if (started !== void 0) stats = {
25113
- ...stats,
25114
- ttftMs: stats.ttftMs + Math.max(0, event.time - started),
25115
- ttftSteps: stats.ttftSteps + 1
25116
- };
25117
- }
25118
- if (chunk.type === "text-delta") return {
25119
- ...view,
25120
- streaming: appendStreamingTail(view.streaming, chunk.text),
25121
- stats
25122
- };
25123
- if (chunk.type === "reasoning-delta") return {
25124
- ...view,
25125
- streamingReasoning: appendStreamingTail(view.streamingReasoning, chunk.text),
25126
- stats
25127
- };
25128
- return view;
25129
- }
25130
- case "assistant/message": {
25131
- const key = `${event.data.turn}:${event.data.step}`;
25132
- const started = view.anchors.stepStart.get(key);
25133
- view.anchors.stepStart.delete(key);
25134
- const firstChunk = view.anchors.firstChunkAt.get(key);
25135
- view.anchors.firstChunkAt.delete(key);
25136
- if (view.anchors.turnSteps.get(event.data.turn) === key) view.anchors.turnSteps.delete(event.data.turn);
25137
- const usage = event.data.usage;
25138
- const totals = view.stats.usage;
25139
- const text = textOf(event.data.message.content);
25140
- const reasoning = reasoningOf(event.data.message.content);
25141
- return {
25142
- ...view,
25143
- streaming: "",
25144
- streamingReasoning: "",
25145
- entries: [...view.entries, {
25146
- kind: "assistant",
25147
- text,
25148
- reasoning,
25149
- interrupted: event.data.interrupted === true ? true : void 0
25150
- }],
25151
- stats: {
25152
- ...view.stats,
25153
- llmMs: view.stats.llmMs + (started === void 0 ? 0 : Math.max(0, event.time - started)),
25154
- usage: usage === void 0 ? totals : {
25155
- inputTokens: totals.inputTokens + usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0),
25156
- outputTokens: totals.outputTokens + usage.outputTokens,
25157
- cacheReadTokens: totals.cacheReadTokens + (usage.cacheReadTokens ?? 0)
25158
- },
25159
- lastPromptTokens: usage === void 0 ? view.stats.lastPromptTokens : usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0),
25160
- decodeMs: view.stats.decodeMs + (firstChunk === void 0 ? 0 : Math.max(0, event.time - firstChunk)),
25161
- decodeTokens: view.stats.decodeTokens + (firstChunk === void 0 || usage === void 0 ? 0 : usage.outputTokens),
25162
- contextSegments: {
25163
- ...view.stats.contextSegments,
25164
- thinking: view.stats.contextSegments.thinking + estimateTokens(reasoning),
25165
- assistant: view.stats.contextSegments.assistant + estimateTokens(text)
25166
- }
25167
- }
25168
- };
25169
- }
25170
- case "tool/call": {
25171
- const data = event.data;
25172
- view.anchors.toolStart.set(data.callId, event.time);
25173
- const turnTools = view.anchors.turnTools.get(data.turn) ?? /* @__PURE__ */ new Set();
25174
- turnTools.add(data.callId);
25175
- view.anchors.turnTools.set(data.turn, turnTools);
25176
- const ordinal = view.toolCallOrdinal + 1;
25177
- return {
25178
- ...view,
25179
- toolCallOrdinal: ordinal,
25180
- entries: [...view.entries, {
25181
- kind: "tool",
25182
- callId: data.callId,
25183
- ordinal,
25184
- name: data.name,
25185
- arguments: data.arguments,
25186
- preview: toolArgumentsPreview(data.arguments, data.name),
25187
- prompt: toolPromptPreview(data.name, data.arguments),
25188
- state: "running",
25189
- summary: "",
25190
- detail: void 0
25191
- }],
25192
- stats: {
25193
- ...view.stats,
25194
- contextSegments: {
25195
- ...view.stats.contextSegments,
25196
- tools: view.stats.contextSegments.tools + (typeof data.arguments === "string" ? estimateTokens(data.arguments) : 0)
25197
- }
25198
- }
25199
- };
25200
- }
25201
- case "tool/result": {
25202
- const block = event.data.message.content[0];
25203
- const started = view.anchors.toolStart.get(block.toolCallId);
25204
- view.anchors.toolStart.delete(block.toolCallId);
25205
- const turnTools = view.anchors.turnTools.get(event.data.turn);
25206
- if (turnTools !== void 0) {
25207
- turnTools.delete(block.toolCallId);
25208
- if (turnTools.size === 0) view.anchors.turnTools.delete(event.data.turn);
25209
- }
25210
- const rawText = textOf(block.content);
25211
- const summary = boundContextSummary(rawText);
25212
- const detail = toolResultDetail(event.data.meta, rawText);
25213
- if (detail?.kind === "diff") {
25214
- const set = view.anchors.turnFiles.get(event.data.turn) ?? /* @__PURE__ */ new Set();
25215
- for (const diff of detail.diffs) set.add(diff.path);
25216
- view.anchors.turnFiles.set(event.data.turn, set);
25217
- }
25218
- const entries = view.entries.map((entry) => {
25219
- if (entry.kind !== "tool" || entry.callId !== block.toolCallId) return entry;
25220
- return {
25221
- ...entry,
25222
- state: block.isError === true ? "error" : "done",
25223
- summary,
25224
- detail
25225
- };
25226
- });
25227
- return {
25228
- ...view,
25229
- entries,
25230
- stats: {
25231
- ...view.stats,
25232
- toolMs: view.stats.toolMs + (started === void 0 ? 0 : Math.max(0, event.time - started)),
25233
- contextSegments: {
25234
- ...view.stats.contextSegments,
25235
- tools: view.stats.contextSegments.tools + estimateTokens(rawText)
25236
- }
25237
- }
25238
- };
25239
- }
25240
- case "todo/write": return {
25241
- ...view,
25242
- todos: event.data.todos
25243
- };
25244
- case "turn/start": return {
25245
- ...view,
25246
- busy: true,
25247
- busySince: view.busy ? view.busySince : event.time,
25248
- todos: [],
25249
- stats: {
25250
- ...view.stats,
25251
- turns: view.stats.turns + 1
25252
- }
25253
- };
25254
- case "step/start": {
25255
- const key = `${event.data.turn}:${event.data.step}`;
25256
- const previous = view.anchors.turnSteps.get(event.data.turn);
25257
- if (previous !== void 0 && previous !== key) {
25258
- view.anchors.stepStart.delete(previous);
25259
- view.anchors.firstChunkAt.delete(previous);
25260
- }
25261
- view.anchors.turnSteps.set(event.data.turn, key);
25262
- view.anchors.stepStart.set(key, event.time);
25263
- return {
25264
- ...view,
25265
- streaming: "",
25266
- streamingReasoning: "",
25267
- stats: {
25268
- ...view.stats,
25269
- steps: view.stats.steps + 1
25270
- }
25271
- };
25272
- }
25273
- case "turn/end": {
25274
- const reason = event.data.reason;
25275
- const appended = [];
25276
- if (reason.kind === "error") {
25277
- const recovery = reason.error.code === "MISSING_CREDENTIAL" ? " · open /model to add an API key" : "";
25278
- appended.push({
25279
- kind: "error",
25280
- text: `${reason.error.code}: ${reason.error.message}${recovery}`
25281
- });
25282
- } else {
25283
- const marker = reason.kind === "aborted" ? reason.reason.kind === "user" ? "turn cancelled by the user" : `turn cancelled (${reason.reason.kind})` : reason.kind === "max-tokens" ? "turn hit the output-token ceiling (max-tokens)" : reason.kind === "blocked" ? "turn ended blocked" : reason.kind === "interrupted" ? "turn was interrupted by a restart" : void 0;
25284
- if (marker !== void 0) appended.push({
25285
- kind: "turn-marker",
25286
- text: marker
25287
- });
25288
- }
25289
- const files = view.anchors.turnFiles.get(event.data.turn);
25290
- view.anchors.turnFiles.delete(event.data.turn);
25291
- if (files !== void 0 && files.size > 0) appended.push({
25292
- kind: "files",
25293
- paths: [...files].slice(0, 12)
25294
- });
25295
- const stepKey = view.anchors.turnSteps.get(event.data.turn);
25296
- if (stepKey !== void 0) {
25297
- view.anchors.stepStart.delete(stepKey);
25298
- view.anchors.firstChunkAt.delete(stepKey);
25299
- view.anchors.turnSteps.delete(event.data.turn);
25300
- }
25301
- const turnToolSet = view.anchors.turnTools.get(event.data.turn);
25302
- if (turnToolSet !== void 0) {
25303
- for (const callId of turnToolSet) view.anchors.toolStart.delete(callId);
25304
- view.anchors.turnTools.delete(event.data.turn);
25305
- }
25306
- if (appended.length === 0) return {
25307
- ...view,
25308
- busy: false,
25309
- busySince: 0,
25310
- streaming: "",
25311
- streamingReasoning: ""
25312
- };
25313
- return {
25314
- ...view,
25315
- busy: false,
25316
- busySince: 0,
25317
- streaming: "",
25318
- streamingReasoning: "",
25319
- entries: [...view.entries, ...appended]
25320
- };
25321
- }
25322
- case "llm/retry": {
25323
- const data = event.data;
25324
- return {
25325
- ...view,
25326
- streaming: "",
25327
- streamingReasoning: "",
25328
- entries: [...view.entries, {
25329
- kind: "retry",
25330
- retryId: data.retryId,
25331
- attempt: data.retry,
25332
- max: "maxRetries" in data ? data.maxRetries : data.retry,
25333
- code: data.failure.code,
25334
- delayMs: data.delayMs,
25335
- state: "running"
25336
- }]
25337
- };
25338
- }
25339
- case "llm/retry-started": {
25340
- const data = event.data;
25341
- const entries = view.entries.map((entry) => {
25342
- if (entry.kind !== "retry" || entry.retryId !== data.retryId) return entry;
25343
- return {
25344
- ...entry,
25345
- state: "done"
25346
- };
25347
- });
25348
- return {
25349
- ...view,
25350
- entries
25351
- };
25352
- }
25353
- case "sandbox/mode": return {
25354
- ...view,
25355
- sandbox: event.data.mode
25356
- };
25357
- case "goal/change": {
25358
- const data = event.data;
25359
- const clip = (text) => text.length > 60 ? `${text.slice(0, 59)}…` : text;
25360
- if (data.operation === "clear") return {
25361
- ...view,
25362
- goal: void 0,
25363
- entries: [...view.entries, {
25364
- kind: "turn-marker",
25365
- text: "◎ goal cleared"
25366
- }]
25367
- };
25368
- const goal = {
25369
- objective: data.goal.objective,
25370
- phase: data.goal.phase,
25371
- rounds: data.roundsStarted,
25372
- max: data.goal.maxGoalRounds,
25373
- blocked: data.goal.blockedReason?.message ?? ""
25374
- };
25375
- const line = data.operation === "create" ? `◎ goal: ${clip(data.goal.objective)}` : data.operation === "complete" ? "◎ goal complete" : data.operation === "pause" ? "◎ goal paused" : data.operation === "resume" ? "◎ goal resumed" : data.operation === "block" ? `◎ goal blocked: ${clip(goal.blocked)}` : void 0;
25376
- return {
25377
- ...view,
25378
- goal,
25379
- entries: line === void 0 ? view.entries : [...view.entries, {
25380
- kind: "turn-marker",
25381
- text: line
25382
- }]
25383
- };
25384
- }
25385
- case "session/title": return {
25386
- ...view,
25387
- title: event.data.title
25388
- };
25389
- case "compaction/summary":
25390
- if (view.anchors.compactionTokens.size >= MAX_COMPACTION_SUMMARY_RESIDUE) {
25391
- const oldest = view.anchors.compactionTokens.keys().next().value;
25392
- if (oldest !== void 0) view.anchors.compactionTokens.delete(oldest);
25393
- }
25394
- view.anchors.compactionTokens.set(event.data.compactionId, event.data.shadowedTokenCount);
25395
- return view;
25396
- case "compaction/prune": return {
25397
- ...view,
25398
- anchors: {
25399
- ...view.anchors,
25400
- lastPruneTokens: event.data.shadowedTokenCount
25401
- }
25402
- };
25403
- case "compaction/end": {
25404
- const ok = event.data.error === void 0;
25405
- const tokens = view.anchors.compactionTokens.get(event.data.compactionId) ?? view.anchors.lastPruneTokens;
25406
- view.anchors.compactionTokens.delete(event.data.compactionId);
25407
- return {
25408
- ...view,
25409
- entries: [...view.entries, {
25410
- kind: "compaction",
25411
- ok,
25412
- tokens,
25413
- error: event.data.error ?? ""
25414
- }]
25415
- };
25416
- }
25417
- case "request/context": return {
25418
- ...view,
25419
- stats: {
25420
- ...view.stats,
25421
- contextWindow: event.data.contextWindow ?? view.stats.contextWindow
25422
- }
25423
- };
25424
- case "request/header": {
25425
- const config = event.data.header.config;
25426
- return {
25427
- ...view,
25428
- model: `${config.provider}/${config.model}`,
25429
- stats: {
25430
- ...view.stats,
25431
- reasoningEffort: config.reasoningEffort === void 0 ? "" : String(config.reasoningEffort),
25432
- contextSegments: {
25433
- ...view.stats.contextSegments,
25434
- system: estimateTokens(event.data.header.system ?? "")
25435
- }
25436
- }
25437
- };
25438
- }
25439
- case "plan/mode": return {
25440
- ...view,
25441
- plan: event.data.active
25442
- };
25443
- case "permission/preset": return {
25444
- ...view,
25445
- permission: event.data.preset
25446
- };
25447
- case "command/run": {
25448
- const data = event.data;
25449
- return {
25450
- ...view,
25451
- entries: [...view.entries, {
25452
- kind: "command",
25453
- commandId: data.commandId,
25454
- name: data.name,
25455
- args: data.args ?? "",
25456
- state: "running",
25457
- summary: ""
25458
- }]
25459
- };
25460
- }
25461
- case "command/done": {
25462
- const data = event.data;
25463
- const entries = view.entries.map((entry) => {
25464
- if (entry.kind !== "command" || entry.commandId !== data.commandId) return entry;
25465
- return {
25466
- ...entry,
25467
- state: data.kind === "success" ? "done" : "error",
25468
- summary: boundContextSummary(data.text ?? "")
25469
- };
25470
- });
25471
- return {
25472
- ...view,
25473
- entries
25474
- };
25475
- }
25476
- default: return view;
25477
- }
25478
- }
25479
25126
  /** @internal A fresh replay accumulator whose state mirrors `createTranscriptView()`. */
25480
25127
  function createReplayAccumulator() {
25481
25128
  return {
@@ -25586,6 +25233,9 @@ function retireReplayEntry(acc, index) {
25586
25233
  * to a sequential fold; only the `entries` container operations are mutable.
25587
25234
  *
25588
25235
  * @internal Test-instrumentation path; `projectEvents` is the public entry.
25236
+ * @returns whether the event changed the accumulated state — the live store
25237
+ * stays silent and keeps its snapshot identity for ignored events, exactly
25238
+ * like the copy-on-write reducer returning its input view unchanged.
25589
25239
  */
25590
25240
  function replayProjectEvent(acc, event) {
25591
25241
  switch (event.type) {
@@ -25620,7 +25270,7 @@ function replayProjectEvent(acc, event) {
25620
25270
  prompt: acc.stats.contextSegments.prompt + estimateTokens(text)
25621
25271
  }
25622
25272
  };
25623
- return;
25273
+ return true;
25624
25274
  }
25625
25275
  const notice = message.source.kind === "plugin" && message.source.form === "notice" ? message.source.summary : message.source.kind;
25626
25276
  const summary = boundContextSummary(notice);
@@ -25636,7 +25286,7 @@ function replayProjectEvent(acc, event) {
25636
25286
  system: acc.stats.contextSegments.system + estimateTokens(summary)
25637
25287
  }
25638
25288
  };
25639
- return;
25289
+ return true;
25640
25290
  }
25641
25291
  case "agent/inbox/spliced": {
25642
25292
  const { target, start, removedCount = 0, inserted } = event.data;
@@ -25666,7 +25316,7 @@ function replayProjectEvent(acc, event) {
25666
25316
  ids.push(message.id);
25667
25317
  acc.ops += 1;
25668
25318
  }
25669
- return;
25319
+ return true;
25670
25320
  }
25671
25321
  case "assistant/chunk": {
25672
25322
  const chunk = event.data.chunk;
@@ -25682,13 +25332,13 @@ function replayProjectEvent(acc, event) {
25682
25332
  }
25683
25333
  if (chunk.type === "text-delta") {
25684
25334
  acc.streaming = appendStreamingTail(acc.streaming, chunk.text);
25685
- return;
25335
+ return true;
25686
25336
  }
25687
25337
  if (chunk.type === "reasoning-delta") {
25688
25338
  acc.streamingReasoning = appendStreamingTail(acc.streamingReasoning, chunk.text);
25689
- return;
25339
+ return true;
25690
25340
  }
25691
- return;
25341
+ return false;
25692
25342
  }
25693
25343
  case "assistant/message": {
25694
25344
  const key = `${event.data.turn}:${event.data.step}`;
@@ -25726,7 +25376,7 @@ function replayProjectEvent(acc, event) {
25726
25376
  assistant: acc.stats.contextSegments.assistant + estimateTokens(text)
25727
25377
  }
25728
25378
  };
25729
- return;
25379
+ return true;
25730
25380
  }
25731
25381
  case "tool/call": {
25732
25382
  const data = event.data;
@@ -25755,7 +25405,7 @@ function replayProjectEvent(acc, event) {
25755
25405
  tools: acc.stats.contextSegments.tools + (typeof data.arguments === "string" ? estimateTokens(data.arguments) : 0)
25756
25406
  }
25757
25407
  };
25758
- return;
25408
+ return true;
25759
25409
  }
25760
25410
  case "tool/result": {
25761
25411
  const block = event.data.message.content[0];
@@ -25789,11 +25439,11 @@ function replayProjectEvent(acc, event) {
25789
25439
  tools: acc.stats.contextSegments.tools + estimateTokens(rawText)
25790
25440
  }
25791
25441
  };
25792
- return;
25442
+ return true;
25793
25443
  }
25794
25444
  case "todo/write":
25795
25445
  acc.todos = event.data.todos;
25796
- return;
25446
+ return true;
25797
25447
  case "turn/start": {
25798
25448
  const wasBusy = acc.busy;
25799
25449
  acc.busy = true;
@@ -25803,7 +25453,7 @@ function replayProjectEvent(acc, event) {
25803
25453
  ...acc.stats,
25804
25454
  turns: acc.stats.turns + 1
25805
25455
  };
25806
- return;
25456
+ return true;
25807
25457
  }
25808
25458
  case "step/start": {
25809
25459
  const key = `${event.data.turn}:${event.data.step}`;
@@ -25820,7 +25470,7 @@ function replayProjectEvent(acc, event) {
25820
25470
  ...acc.stats,
25821
25471
  steps: acc.stats.steps + 1
25822
25472
  };
25823
- return;
25473
+ return true;
25824
25474
  }
25825
25475
  case "turn/end": {
25826
25476
  const reason = event.data.reason;
@@ -25860,7 +25510,7 @@ function replayProjectEvent(acc, event) {
25860
25510
  acc.busy = false;
25861
25511
  acc.busySince = 0;
25862
25512
  for (const entry of appended) appendReplayEntry(acc, entry);
25863
- return;
25513
+ return true;
25864
25514
  }
25865
25515
  case "llm/retry": {
25866
25516
  const data = event.data;
@@ -25876,7 +25526,7 @@ function replayProjectEvent(acc, event) {
25876
25526
  state: "running"
25877
25527
  });
25878
25528
  indexList(acc.retryIndex, data.retryId).push(acc.entries.length - 1);
25879
- return;
25529
+ return true;
25880
25530
  }
25881
25531
  case "llm/retry-started": {
25882
25532
  const data = event.data;
@@ -25884,11 +25534,11 @@ function replayProjectEvent(acc, event) {
25884
25534
  ...entry,
25885
25535
  state: "done"
25886
25536
  }));
25887
- return;
25537
+ return true;
25888
25538
  }
25889
25539
  case "sandbox/mode":
25890
25540
  acc.sandbox = event.data.mode;
25891
- return;
25541
+ return true;
25892
25542
  case "goal/change": {
25893
25543
  const data = event.data;
25894
25544
  const clip = (text) => text.length > 60 ? `${text.slice(0, 59)}…` : text;
@@ -25898,7 +25548,7 @@ function replayProjectEvent(acc, event) {
25898
25548
  kind: "turn-marker",
25899
25549
  text: "◎ goal cleared"
25900
25550
  });
25901
- return;
25551
+ return true;
25902
25552
  }
25903
25553
  const goal = {
25904
25554
  objective: data.goal.objective,
@@ -25913,21 +25563,21 @@ function replayProjectEvent(acc, event) {
25913
25563
  kind: "turn-marker",
25914
25564
  text: line
25915
25565
  });
25916
- return;
25566
+ return true;
25917
25567
  }
25918
25568
  case "session/title":
25919
25569
  acc.title = event.data.title;
25920
- return;
25570
+ return true;
25921
25571
  case "compaction/summary":
25922
25572
  if (acc.compactionTokens.size >= MAX_COMPACTION_SUMMARY_RESIDUE) {
25923
25573
  const oldest = acc.compactionTokens.keys().next().value;
25924
25574
  if (oldest !== void 0) acc.compactionTokens.delete(oldest);
25925
25575
  }
25926
25576
  acc.compactionTokens.set(event.data.compactionId, event.data.shadowedTokenCount);
25927
- return;
25577
+ return true;
25928
25578
  case "compaction/prune":
25929
25579
  acc.lastPruneTokens = event.data.shadowedTokenCount;
25930
- return;
25580
+ return true;
25931
25581
  case "compaction/end": {
25932
25582
  const ok = event.data.error === void 0;
25933
25583
  const tokens = acc.compactionTokens.get(event.data.compactionId) ?? acc.lastPruneTokens;
@@ -25938,14 +25588,14 @@ function replayProjectEvent(acc, event) {
25938
25588
  tokens,
25939
25589
  error: event.data.error ?? ""
25940
25590
  });
25941
- return;
25591
+ return true;
25942
25592
  }
25943
25593
  case "request/context":
25944
25594
  acc.stats = {
25945
25595
  ...acc.stats,
25946
25596
  contextWindow: event.data.contextWindow ?? acc.stats.contextWindow
25947
25597
  };
25948
- return;
25598
+ return true;
25949
25599
  case "request/header": {
25950
25600
  const config = event.data.header.config;
25951
25601
  acc.model = `${config.provider}/${config.model}`;
@@ -25957,14 +25607,14 @@ function replayProjectEvent(acc, event) {
25957
25607
  system: estimateTokens(event.data.header.system ?? "")
25958
25608
  }
25959
25609
  };
25960
- return;
25610
+ return true;
25961
25611
  }
25962
25612
  case "plan/mode":
25963
25613
  acc.plan = event.data.active;
25964
- return;
25614
+ return true;
25965
25615
  case "permission/preset":
25966
25616
  acc.permission = event.data.preset;
25967
- return;
25617
+ return true;
25968
25618
  case "command/run": {
25969
25619
  const data = event.data;
25970
25620
  appendReplayEntry(acc, {
@@ -25976,7 +25626,7 @@ function replayProjectEvent(acc, event) {
25976
25626
  summary: ""
25977
25627
  });
25978
25628
  indexList(acc.commandIndex, data.commandId).push(acc.entries.length - 1);
25979
- return;
25629
+ return true;
25980
25630
  }
25981
25631
  case "command/done": {
25982
25632
  const data = event.data;
@@ -25986,20 +25636,27 @@ function replayProjectEvent(acc, event) {
25986
25636
  summary: boundContextSummary(data.text ?? "")
25987
25637
  });
25988
25638
  updateReplayById(acc, acc.commandIndex, data.commandId, (entry) => entry.commandId === data.commandId, update);
25989
- return;
25639
+ return true;
25990
25640
  }
25991
- default: return;
25641
+ default: return false;
25992
25642
  }
25993
25643
  }
25994
25644
  /**
25995
- * Materialize the accumulated fold as a `TranscriptView`, compacting any
25996
- * retired tombstones. The anchors maps are handed through as-is (their
25997
- * content is identical to a sequential fold's).
25645
+ * Materialize the accumulated fold as a fresh immutable snapshot for the
25646
+ * live store. Unlike {@link finishReplay} the one-shot replay entry, which
25647
+ * hands the accumulator's own arrays through because the accumulator is
25648
+ * discarded — every array a renderer can hold is copied here, so later
25649
+ * folds never mutate a snapshot already handed out. Same fields, same
25650
+ * tombstone compaction.
25998
25651
  *
25999
- * @internal Test-instrumentation path; `projectEvents` is the public entry.
25652
+ * @internal Live-store path; `projectEvents` is the public entry.
26000
25653
  */
26001
- function finishReplay(acc) {
26002
- const entries = acc.removedCount === 0 ? acc.entries : acc.entries.filter((entry) => entry !== void 0);
25654
+ function snapshotReplayView(acc) {
25655
+ return materializeReplayView(acc, true);
25656
+ }
25657
+ /** Field-for-field materialization; `copy` selects snapshot array isolation. */
25658
+ function materializeReplayView(acc, copy) {
25659
+ const entries = acc.removedCount === 0 ? copy ? [...acc.entries] : acc.entries : acc.entries.filter((entry) => entry !== void 0);
26003
25660
  if (acc.removedCount > 0) acc.ops += acc.entries.length;
26004
25661
  return {
26005
25662
  entries,
@@ -26033,22 +25690,6 @@ function finishReplay(acc) {
26033
25690
  };
26034
25691
  }
26035
25692
  /**
26036
- * Fold a replayed event history into one view.
26037
- *
26038
- * Folding is near-linear in the log size: the mutable replay accumulator
26039
- * appends in place and resolves id-keyed updates through index maps, so a
26040
- * long persisted session replays without the O(N²) copy-on-write rebuilds a
26041
- * naive sequential fold would incur. The result is identical to folding
26042
- * {@link projectEvent} per event in order.
26043
- * @param events - events in `seq` order.
26044
- * @returns the folded view.
26045
- */
26046
- function projectEvents(events) {
26047
- const acc = createReplayAccumulator();
26048
- for (const event of events) replayProjectEvent(acc, event);
26049
- return finishReplay(acc);
26050
- }
26051
- /**
26052
25693
  * The append-only flush boundary for a transcript view: the count of entries
26053
25694
  * no later event can remove. Entries at or beyond this index are mutable and
26054
25695
  * must stay in the live tree.
@@ -26077,6 +25718,16 @@ function settledEntryCount(entries) {
26077
25718
  }
26078
25719
  //#endregion
26079
25720
  //#region src/render/markdown.ts
25721
+ /**
25722
+ * Terminal markdown renderer for assistant replies: a pure GFM-subset
25723
+ * block/inline parser producing styled line segments the Ink renderer maps
25724
+ * to colored text. No ANSI here — the app owns color mapping, tests own the
25725
+ * structure. The subset mirrors what agent replies actually emit: headings,
25726
+ * emphasis, inline/fenced code, flat lists, blockquotes, links, rules, GFM
25727
+ * tables, and wrapped paragraphs. Unknown syntax degrades to plain text.
25728
+ *
25729
+ * @module @deepseek-ai/dsh-code/render/markdown
25730
+ */
26080
25731
  /** Plain segment helper. */
26081
25732
  function seg(text, style = "plain") {
26082
25733
  return {
@@ -26084,14 +25735,9 @@ function seg(text, style = "plain") {
26084
25735
  style
26085
25736
  };
26086
25737
  }
26087
- /** Visible width of a run in columns (CJK counts double). */
25738
+ /** Visible width of a run in columns (grapheme-cluster and emoji aware). */
26088
25739
  function visibleColumns(text) {
26089
- let columns = 0;
26090
- for (const char of text) {
26091
- const code = char.codePointAt(0) ?? 0;
26092
- columns += code > 11903 ? 2 : 1;
26093
- }
26094
- return columns;
25740
+ return stringWidth(text);
26095
25741
  }
26096
25742
  /** Punctuation that should not become the first visible glyph of a row. */
26097
25743
  function isClosingPunctuation(text) {
@@ -27295,6 +26941,59 @@ function collectDeletionSubtree(records, id) {
27295
26941
  return [...doomed];
27296
26942
  }
27297
26943
  /**
26944
+ * Plan one session-subtree deletion with NO filesystem side effects: collect
26945
+ * the doomed lineage, refuse when the root or ANY member is live (a live
26946
+ * child would outlive its deleted parent) or missing from the listing, and
26947
+ * order the result children-first so the executor can never leave a deleted
26948
+ * parent behind surviving children. Artifact-location guards stay at the
26949
+ * call site; this is the pure preflight they complete.
26950
+ * @param records - the full directory listing.
26951
+ * @param id - the root session id to delete.
26952
+ * @returns the ordered plan, or a user-facing refusal reason.
26953
+ */
26954
+ function planSessionDeletion(records, id) {
26955
+ const target = records.find((record) => record.header.id === id);
26956
+ if (target === void 0) return {
26957
+ ok: false,
26958
+ reason: `no persisted session matches "${id}"`
26959
+ };
26960
+ if (target.live) return {
26961
+ ok: false,
26962
+ reason: "cannot delete a live session — it is open in this or another process"
26963
+ };
26964
+ const doomed = collectDeletionSubtree(records, id);
26965
+ const byId = new Map(records.map((record) => [record.header.id, record]));
26966
+ const parentOf = /* @__PURE__ */ new Map();
26967
+ for (const record of records) parentOf.set(record.header.id, record.header.parentSession);
26968
+ const nodes = [];
26969
+ for (const candidate of doomed) {
26970
+ const record = byId.get(candidate);
26971
+ if (record === void 0) return {
26972
+ ok: false,
26973
+ reason: `no persisted session matches "${candidate}"`
26974
+ };
26975
+ if (record.live) return {
26976
+ ok: false,
26977
+ reason: `cannot delete: child session ${candidate.slice(-12)} is live — close it (and any process using it) first`
26978
+ };
26979
+ let depth = 0;
26980
+ let ancestor = parentOf.get(candidate);
26981
+ while (ancestor !== void 0 && depth < 64) {
26982
+ depth += 1;
26983
+ ancestor = parentOf.get(ancestor);
26984
+ }
26985
+ nodes.push({
26986
+ id: candidate,
26987
+ depth
26988
+ });
26989
+ }
26990
+ nodes.sort((left, right) => right.depth - left.depth);
26991
+ return {
26992
+ ok: true,
26993
+ nodes
26994
+ };
26995
+ }
26996
+ /**
27298
26997
  * Codex-style relative time for session rows ("now", "5m ago", "3h ago",
27299
26998
  * "2d ago"; older than a week falls back to the local date).
27300
26999
  * @param timestamp - epoch milliseconds of the last activity.
@@ -28082,7 +27781,7 @@ const DEFAULT_TOOL_ROWS = 3;
28082
27781
  /** Keep the invocation visible while making hidden tool output discoverable. */
28083
27782
  function compactToolLines(lines, columns) {
28084
27783
  if (lines.length <= DEFAULT_TOOL_ROWS) return lines;
28085
- return [...lines.slice(0, 2), ...textLines(" … output hidden · Ctrl+R", columns, "dim").slice(0, 1)];
27784
+ return [...lines.slice(0, 2), ...textLines(" … output hidden · Ctrl/Alt+R", columns, "dim").slice(0, 1)];
28086
27785
  }
28087
27786
  /**
28088
27787
  * Convert one durable transcript entry to its complete scrollable row model.
@@ -28096,7 +27795,7 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
28096
27795
  case "user": return entry.notice ? hangingStyledLines([lineSegment(promptDisplayText(entry), "dim")], width, "⤷ ", "dim", " ", "dim") : hangingStyledLines([lineSegment(promptDisplayText(entry), "plain")], width, "❯ ", "brand", " ", "plain");
28097
27796
  case "pending": return hangingStyledLines([lineSegment(promptDisplayText(entry), "plain")], width, "❯ ", "brand", " ", "plain");
28098
27797
  case "assistant": {
28099
- const reasoning = entry.reasoning === "" ? [] : showReasoning ? reasoningLines(entry.reasoning, width) : textLines(`✻ Thinking (${entry.reasoning.length} chars${reasoningToggleHint ? ", Ctrl+R to expand" : ""})`, width, "dim");
27798
+ const reasoning = entry.reasoning === "" ? [] : showReasoning ? reasoningLines(entry.reasoning, width) : textLines(`✻ Thinking (${entry.reasoning.length} chars${reasoningToggleHint ? ", Ctrl/Alt+R to expand" : ""})`, width, "dim");
28100
27799
  const body = markdownLines(entry.text, Math.max(10, width - 2)).map((line) => ({ segments: [{
28101
27800
  text: " ",
28102
27801
  style: "plain"
@@ -28142,6 +27841,44 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
28142
27841
  function settledEntryLines(entry, columns, showReasoning) {
28143
27842
  return transcriptEntryLines(entry, columns, showReasoning, false, showReasoning);
28144
27843
  }
27844
+ /**
27845
+ * Clamp the live-region allocation so the flexible dynamic rows never exceed
27846
+ * the post-chrome budget. By construction the caller derives these rows from
27847
+ * the same budget; this is the runtime tripwire for a future edit that breaks
27848
+ * that derivation. Reduction order: answer first (the freshest content is the
27849
+ * live tail), then reasoning, then settled live rows; nothing goes negative.
27850
+ * @param allocation - the intended row allocation.
27851
+ * @param dynamicRows - the post-chrome row budget.
27852
+ * @returns the clamped allocation and a warning string when clamping fired.
27853
+ */
27854
+ function clampLiveAllocation(allocation, dynamicRows) {
27855
+ const live = Math.max(0, Math.floor(allocation.live));
27856
+ const reasoning = Math.max(0, Math.floor(allocation.reasoning));
27857
+ const answer = Math.max(0, Math.floor(allocation.answer));
27858
+ const budget = Math.max(0, Math.floor(dynamicRows));
27859
+ let excess = live + reasoning + answer - budget;
27860
+ if (excess <= 0) return { allocation: {
27861
+ live,
27862
+ reasoning,
27863
+ answer
27864
+ } };
27865
+ const take = (from) => {
27866
+ const cut = Math.min(from, excess);
27867
+ excess -= cut;
27868
+ return from - cut;
27869
+ };
27870
+ const clampedAnswer = take(answer);
27871
+ const clampedReasoning = excess > 0 ? take(reasoning) : reasoning;
27872
+ const clampedLive = excess > 0 ? take(live) : live;
27873
+ return {
27874
+ allocation: {
27875
+ live: clampedLive,
27876
+ reasoning: clampedReasoning,
27877
+ answer: clampedAnswer
27878
+ },
27879
+ warning: `live rows ${live} + ${reasoning} + ${answer} exceed the dynamic budget ${budget}; clamped to ${clampedLive}/${clampedReasoning}/${clampedAnswer}`
27880
+ };
27881
+ }
28145
27882
  //#endregion
28146
27883
  //#region src/render/editor.ts
28147
27884
  /**
@@ -30412,6 +30149,10 @@ const LOCAL_COMMANDS = [
30412
30149
  label: "/subagent",
30413
30150
  description: "choose the model delegated subagents run on"
30414
30151
  },
30152
+ {
30153
+ label: "/vscode-keys",
30154
+ description: "pass ctrl+r through the vs code terminal"
30155
+ },
30415
30156
  {
30416
30157
  label: "/delete",
30417
30158
  description: "delete a session and its subagent threads"
@@ -30508,15 +30249,13 @@ function useCursorBlink(active) {
30508
30249
  };
30509
30250
  }
30510
30251
  /**
30511
- * The busy line, web TurnStatus contract: a continuously moving blue gradient
30512
- * paints the complete `Deep diving...` label, with the elapsed clock appended
30513
- * only once the turn has clearly been running (15s) anchored to `turn/start`
30514
- * so a resumed mid-turn keeps the real time.
30252
+ * One bounded line painted with the deep-diving shimmer: a continuously
30253
+ * moving blue gradient across graphemes, the `✻` glyph in the breathing
30254
+ * spark color. Shared by the busy line and the collapsed thinking marker;
30255
+ * always exactly one row (truncate-end) so the live budget stays exact.
30515
30256
  */
30516
- function DeepDivingLine({ since }) {
30257
+ function ShimmerLine({ text }) {
30517
30258
  const tick = useFrames(33);
30518
- const elapsed = since === 0 ? 0 : Date.now() - since;
30519
- const text = elapsed >= 15e3 ? `✻ Deep diving... ${runClock(elapsed)}` : "✻ Deep diving...";
30520
30259
  const palette = getPalette();
30521
30260
  const graphemes = splitGraphemes(text);
30522
30261
  return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...graphemes.map((grapheme, index) => {
@@ -30529,6 +30268,17 @@ function DeepDivingLine({ since }) {
30529
30268
  }));
30530
30269
  }
30531
30270
  /**
30271
+ * The busy line, web TurnStatus contract: a continuously moving blue gradient
30272
+ * paints the complete `Deep diving...` label, with the elapsed clock appended
30273
+ * only once the turn has clearly been running (15s) — anchored to `turn/start`
30274
+ * so a resumed mid-turn keeps the real time.
30275
+ */
30276
+ function DeepDivingLine({ since }) {
30277
+ const elapsed = since === 0 ? 0 : Date.now() - since;
30278
+ const text = elapsed >= 15e3 ? `✻ Deep diving... ${runClock(elapsed)}` : "✻ Deep diving...";
30279
+ return (0, import_react.createElement)(ShimmerLine, { text });
30280
+ }
30281
+ /**
30532
30282
  * The streaming buffer rendered with a hard size cap: the live region must
30533
30283
  * ALWAYS fit the terminal, or Ink's erase/rewrite of a dynamic tree taller
30534
30284
  * than the screen freezes (cursor-up past the top, garbage, no scroll). The
@@ -30784,11 +30534,12 @@ function todoMark(status) {
30784
30534
  }
30785
30535
  /**
30786
30536
  * One-row live subagent summary (the Codex agent status feed, compressed to
30787
- * the transcript's budget): running count, total, and the most recently
30788
- * active child's current activity. One line, never more the full view is
30789
- * the /agents panel.
30537
+ * the transcript's budget): running count, the observed total (the row cap
30538
+ * is a display budget, not the fan-out size), and the most recently active
30539
+ * child's current activity. One line, never more — the full view is the
30540
+ * /agents panel.
30790
30541
  */
30791
- function AgentsLine({ rows }) {
30542
+ function AgentsLine({ rows, total }) {
30792
30543
  if (rows.length === 0) return void 0;
30793
30544
  const running = rows.filter((row) => row.state !== "done").length;
30794
30545
  const newest = [...rows].sort((left, right) => right.updatedAt - left.updatedAt)[0];
@@ -30797,7 +30548,7 @@ function AgentsLine({ rows }) {
30797
30548
  color: inkColor(getPalette().brand),
30798
30549
  bold: true,
30799
30550
  wrap: "truncate-end"
30800
- }, `agents ${running} live`, (0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, ` · ${rows.length} total · /agents`), (0, import_react.createElement)(Text, { color: inkColor(getPalette().text) }, ` · ${mark} ${newest.label} ${newest.activity}`)));
30551
+ }, `agents ${running} live`, (0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, ` · ${total} total · /agents`), (0, import_react.createElement)(Text, { color: inkColor(getPalette().text) }, ` · ${mark} ${newest.label} ${newest.activity}`)));
30801
30552
  }
30802
30553
  /** One-row todo summary: task count cannot grow the live Ink tree. */
30803
30554
  function TodoPanel({ todos }) {
@@ -32016,7 +31767,7 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
32016
31767
  key: "key-inspector",
32017
31768
  dimColor: true,
32018
31769
  wrap: "truncate-end"
32019
- }, " ctrl+o history details · ctrl+r thinking · shift+tab permission preset"),
31770
+ }, " ctrl+o history details · ctrl/alt+r thinking · shift+tab permission preset"),
32020
31771
  (0, import_react.createElement)(Text, {
32021
31772
  key: "key-cancel",
32022
31773
  dimColor: true,
@@ -32353,7 +32104,7 @@ function CompletionMenu({ active, mention, index, rows }) {
32353
32104
  * While a modal (approval / question / model panel) owns the keys, the
32354
32105
  * box passes every key through untouched.
32355
32106
  */
32356
- function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openJobs, openStatusline, openTheme, openHistory, openAgents, openSubagent, openTodos, openDelete, openDiff, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle, maxRows, onEditorRows }) {
32107
+ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openJobs, openStatusline, openTheme, openHistory, openAgents, openSubagent, openTodos, openDelete, openDiff, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, applyEditorKeys, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle, maxRows, onEditorRows }) {
32357
32108
  const columns = useStdout().stdout?.columns ?? 80;
32358
32109
  const editorColumns = Math.max(1, columns - 6);
32359
32110
  const stdin = useStdin().stdin;
@@ -32748,7 +32499,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32748
32499
  }
32749
32500
  return;
32750
32501
  }
32751
- if (key.ctrl && input === "r") {
32502
+ if ((key.ctrl || key.meta) && input === "r") {
32752
32503
  if (focusReporting && !terminalFocusedRef.current) return;
32753
32504
  toggleReasoning();
32754
32505
  return;
@@ -32960,6 +32711,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32960
32711
  openTodos();
32961
32712
  return;
32962
32713
  }
32714
+ if (text === "/vscode-keys" || text.startsWith("/vscode-keys ")) {
32715
+ applyEditorKeys().then((summary) => notify(summary), (error) => notify(`vscode-keys failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
32716
+ return;
32717
+ }
32963
32718
  if (text === "/subagent") {
32964
32719
  openSubagent();
32965
32720
  return;
@@ -33592,6 +33347,7 @@ function App(props) {
33592
33347
  }, [modelOpen, props.subscribeProviderAuthorizations]);
33593
33348
  const busy = view.busy;
33594
33349
  const [showReasoning, setShowReasoning] = (0, import_react.useState)(false);
33350
+ const budgetWarnRef = (0, import_react.useRef)(void 0);
33595
33351
  const [verboseOpen, setVerboseOpen] = (0, import_react.useState)(false);
33596
33352
  const [diffView, setDiffView] = (0, import_react.useState)(void 0);
33597
33353
  const [helpOpen, setHelpOpen] = (0, import_react.useState)(false);
@@ -33760,6 +33516,18 @@ function App(props) {
33760
33516
  const streamRows = Math.max(1, dynamicRows - visibleLiveLines.length);
33761
33517
  const reasoningRows = view.streamingReasoning === "" ? 0 : view.streaming === "" ? streamRows : streamRows <= 1 ? 0 : showReasoning ? Math.max(1, Math.floor(streamRows / 3)) : 1;
33762
33518
  const answerRows = view.streaming === "" ? 0 : Math.max(1, streamRows - reasoningRows);
33519
+ const liveAudit = clampLiveAllocation({
33520
+ live: visibleLiveLines.length,
33521
+ reasoning: reasoningRows,
33522
+ answer: answerRows
33523
+ }, dynamicRows);
33524
+ if (liveAudit.warning !== void 0 && budgetWarnRef.current !== liveAudit.warning) {
33525
+ budgetWarnRef.current = liveAudit.warning;
33526
+ console.warn(`[dsh-code] ${liveAudit.warning}`);
33527
+ }
33528
+ const auditedLiveLines = liveAudit.allocation.live === visibleLiveLines.length ? visibleLiveLines : visibleLiveLines.slice(-liveAudit.allocation.live);
33529
+ const auditedReasoningRows = liveAudit.allocation.reasoning;
33530
+ const auditedAnswerRows = liveAudit.allocation.answer;
33763
33531
  const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !jobsOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !todosOpen && !verboseOpen && diffView === void 0 && !approvalPending && !questionPending;
33764
33532
  const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || jobsOpen || statuslineOpen || themeOpen || historyOpen || agentsOpen || subagentOpen || todosOpen || verboseOpen && !approvalPending && !questionPending || diffView !== void 0 || approvalPending || questionPending;
33765
33533
  const closeInspector = (0, import_react.useCallback)(() => {
@@ -33997,18 +33765,27 @@ function App(props) {
33997
33765
  return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(MemoStaticTranscript, {
33998
33766
  key: refreshEpoch,
33999
33767
  items: settledRows
34000
- }), transcriptVisible ? (0, import_react.createElement)(Box, { flexDirection: "column" }, visibleLiveLines.length === 0 ? void 0 : (0, import_react.createElement)(StyledRows, { lines: visibleLiveLines }), view.streamingReasoning !== "" && reasoningRows > 0 ? (0, import_react.createElement)(StreamTail, {
34001
- text: showReasoning ? view.streamingReasoning : "Thinking… (Ctrl+R to expand)",
33768
+ }), transcriptVisible ? (0, import_react.createElement)(Box, { flexDirection: "column" }, auditedLiveLines.length === 0 ? void 0 : (0, import_react.createElement)(StyledRows, { lines: auditedLiveLines }), view.streamingReasoning !== "" && auditedReasoningRows > 0 ? showReasoning ? (0, import_react.createElement)(StreamTail, {
33769
+ text: view.streamingReasoning,
34002
33770
  prefix: "✻ ",
34003
33771
  continuationPrefix: " ",
34004
33772
  dim: true,
34005
- maxRows: reasoningRows
34006
- }) : void 0, view.streaming !== "" && answerRows > 0 ? (0, import_react.createElement)(StreamTail, {
33773
+ maxRows: auditedReasoningRows
33774
+ }) : view.streaming === "" ? (0, import_react.createElement)(ShimmerLine, { text: " Thinking… (Ctrl/Alt+R to expand)" }) : (0, import_react.createElement)(StreamTail, {
33775
+ text: "Thinking… (Ctrl/Alt+R to expand)",
33776
+ prefix: "✻ ",
33777
+ continuationPrefix: " ",
33778
+ dim: true,
33779
+ maxRows: auditedReasoningRows
33780
+ }) : void 0, view.streaming !== "" && auditedAnswerRows > 0 ? (0, import_react.createElement)(StreamTail, {
34007
33781
  text: view.streaming,
34008
33782
  dim: false,
34009
- maxRows: answerRows,
33783
+ maxRows: auditedAnswerRows,
34010
33784
  prefix: " "
34011
- }, busy ? (0, import_react.createElement)(Caret) : void 0) : void 0, deepDivingVisible ? (0, import_react.createElement)(DeepDivingLine, { since: view.busySince }) : void 0) : void 0, transcriptVisible ? (0, import_react.createElement)(TodoPanel, { todos: view.todos }) : void 0, transcriptVisible ? (0, import_react.createElement)(AgentsLine, { rows: agentRows }) : void 0, todosOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(MemoTodoListPanel, {
33785
+ }, busy ? (0, import_react.createElement)(Caret) : void 0) : void 0, deepDivingVisible ? (0, import_react.createElement)(DeepDivingLine, { since: view.busySince }) : void 0) : void 0, transcriptVisible ? (0, import_react.createElement)(TodoPanel, { todos: view.todos }) : void 0, transcriptVisible ? (0, import_react.createElement)(AgentsLine, {
33786
+ rows: agentRows,
33787
+ total: props.subagents.getTotalSeen()
33788
+ }) : void 0, todosOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(MemoTodoListPanel, {
34012
33789
  todos: view.todos,
34013
33790
  onClose: () => {
34014
33791
  setTodosOpen(false);
@@ -34141,6 +33918,7 @@ function App(props) {
34141
33918
  descriptors,
34142
33919
  skills,
34143
33920
  dispatch: props.dispatch,
33921
+ applyEditorKeys: props.applyEditorKeys,
34144
33922
  steer: props.steer,
34145
33923
  interrupt: props.interrupt,
34146
33924
  quit: props.quit,
@@ -34235,7 +34013,7 @@ function App(props) {
34235
34013
  refresh: refreshScreen,
34236
34014
  toggleReasoning: () => {
34237
34015
  setShowReasoning((current) => !current);
34238
- if (!busy && !streamingActive) refreshScreen();
34016
+ refreshScreen();
34239
34017
  },
34240
34018
  loadMentions: props.loadMentions,
34241
34019
  inspectImages: props.inspectImages,
@@ -34604,6 +34382,202 @@ async function loadModelDirectory(ctx) {
34604
34382
  };
34605
34383
  }
34606
34384
  //#endregion
34385
+ //#region src/model-capabilities.ts
34386
+ /** The settings namespace the llm-pi-ai plugin owns (identity via `settingsNamespace`). */
34387
+ const PI_AI_SETTINGS_NS = "llm-pi-ai";
34388
+ /** The effort level that means "send no reasoning parameter"; its wire value is always null. */
34389
+ const OFF_LEVEL = "off";
34390
+ /** Single-line a misbehaving error for the notice channel. */
34391
+ function singleLine$1(message) {
34392
+ return [...message].map((ch) => {
34393
+ const code = ch.charCodeAt(0);
34394
+ return code < 32 || code === 127 ? " " : ch;
34395
+ }).join("").split(" ").filter((part) => part !== "").join(" ");
34396
+ }
34397
+ /** Human text for a rejection value. */
34398
+ function messageOf$1(error) {
34399
+ return error instanceof Error ? error.message : String(error);
34400
+ }
34401
+ /** Read the value at a path through plain objects; undefined when any segment misses. */
34402
+ function getPath$1(value, path) {
34403
+ let current = value;
34404
+ for (const segment of path) {
34405
+ if (typeof current !== "object" || current === null) return void 0;
34406
+ current = current[segment];
34407
+ }
34408
+ return current;
34409
+ }
34410
+ /** Whether a raw settings value is a usable `reasoningEfforts` dict (non-empty, non-false). */
34411
+ function isDeclaredEfforts(value) {
34412
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
34413
+ }
34414
+ /**
34415
+ * The identity level map for an advertised effort list: `off` sends no
34416
+ * parameter, every other level keeps its own name — the wire spelling
34417
+ * OpenAI-compatible relays accept as-is.
34418
+ */
34419
+ function identityEfforts(levels) {
34420
+ const dict = {};
34421
+ for (const level of levels) dict[level] = level === OFF_LEVEL ? null : level;
34422
+ return dict;
34423
+ }
34424
+ /**
34425
+ * Plan the reasoning declarations to materialize. A model entry inherits
34426
+ * when it declares nothing (`reasoningEfforts` absent — a dict or `false` is
34427
+ * an explicit choice and is never touched) and its live row advertises no
34428
+ * efforts; the donor is the first sibling settings declaration for the same
34429
+ * id, copied verbatim so dialect wire spellings survive, otherwise the first
34430
+ * other-route row advertising efforts for that id, mapped by identity.
34431
+ * Entries never lose fields and keep their key order; a provider appears in
34432
+ * the result only when at least one entry changes.
34433
+ * @param input - the live model rows and the raw pi-ai profiles from settings.
34434
+ * @returns one plan per provider with at least one inheritance.
34435
+ */
34436
+ function planCapabilitySync(input) {
34437
+ const declared = /* @__PURE__ */ new Map();
34438
+ for (const [provider, profile] of input.profiles) for (const entry of profile.models) {
34439
+ const efforts = entry.reasoningEfforts;
34440
+ if (!isDeclaredEfforts(efforts)) continue;
34441
+ if (!declared.has(String(entry.id))) declared.set(String(entry.id), {
34442
+ dict: efforts,
34443
+ provider
34444
+ });
34445
+ }
34446
+ const advertised = /* @__PURE__ */ new Map();
34447
+ for (const row of input.rows) {
34448
+ const levels = row.reasoning?.efforts.map((effort) => effort.id) ?? [];
34449
+ if (levels.filter((level) => level !== OFF_LEVEL).length === 0) continue;
34450
+ if (!advertised.has(row.model)) advertised.set(row.model, {
34451
+ levels,
34452
+ provider: row.provider
34453
+ });
34454
+ }
34455
+ const plans = [];
34456
+ for (const [provider, profile] of input.profiles) {
34457
+ const inherited = [];
34458
+ const sources = [];
34459
+ const models = profile.models.map((entry) => {
34460
+ if (entry.reasoningEfforts !== void 0) return entry;
34461
+ const id = String(entry.id);
34462
+ const sibling = declared.get(id);
34463
+ if (sibling !== void 0 && sibling.provider !== provider) {
34464
+ inherited.push(id);
34465
+ if (!sources.includes(`${sibling.provider}/${id}`)) sources.push(`${sibling.provider}/${id}`);
34466
+ return {
34467
+ ...entry,
34468
+ reasoningEfforts: sibling.dict
34469
+ };
34470
+ }
34471
+ const donor = advertised.get(id);
34472
+ if (donor !== void 0 && donor.provider !== provider) {
34473
+ inherited.push(id);
34474
+ if (!sources.includes(`${donor.provider}/${id}`)) sources.push(`${donor.provider}/${id}`);
34475
+ return {
34476
+ ...entry,
34477
+ reasoningEfforts: identityEfforts(donor.levels)
34478
+ };
34479
+ }
34480
+ return entry;
34481
+ });
34482
+ if (inherited.length > 0) plans.push({
34483
+ provider,
34484
+ settingsNs: profile.settingsNs,
34485
+ settingsPath: profile.settingsPath,
34486
+ models,
34487
+ sourceFingerprint: JSON.stringify(profile.models),
34488
+ sourceRevision: profile.revision,
34489
+ inherited,
34490
+ sources
34491
+ });
34492
+ }
34493
+ return plans;
34494
+ }
34495
+ /** Provider -> the source snapshot and document revision its last write landed against. */
34496
+ const lastApplied = /* @__PURE__ */ new Map();
34497
+ /** Fresh revision of one namespace, or undefined when it does not resolve. */
34498
+ function revisionOf(settings, ns) {
34499
+ try {
34500
+ for (const descriptor of settings.describe({ redactSecrets: true })) if (descriptor.ns === ns) return descriptor.revision;
34501
+ } catch {
34502
+ return;
34503
+ }
34504
+ }
34505
+ /**
34506
+ * Materialize same-id reasoning declarations once per provider. Reads the
34507
+ * configurable directory, the redacted settings document, and the live model
34508
+ * rows; plans; then writes each provider's merged models array through
34509
+ * `settings.mutate` under a fresh revision (writes bump the section
34510
+ * revision, so per-plan revisions are re-read). Every failure converges to a
34511
+ * single-line notice — the caller's promise never rejects and the session
34512
+ * keeps running on the previous configuration.
34513
+ * @param ctx - context carrying the `llm` and `settings` services (optional).
34514
+ * @param notify - the app bridge's notice sink, when one is live.
34515
+ */
34516
+ async function syncModelCapabilities(ctx, notify) {
34517
+ try {
34518
+ const llm = ctx.get("llm");
34519
+ if (llm?.listConfigurableProviders === void 0) return;
34520
+ const settings = ctx.get("settings");
34521
+ if (settings === void 0) return;
34522
+ let entries;
34523
+ try {
34524
+ entries = llm.listConfigurableProviders();
34525
+ } catch {
34526
+ return;
34527
+ }
34528
+ const routed = entries.filter((entry) => entry.settingsNs === PI_AI_SETTINGS_NS);
34529
+ if (routed.length === 0) return;
34530
+ let descriptors;
34531
+ try {
34532
+ descriptors = settings.describe({ redactSecrets: true });
34533
+ } catch {
34534
+ return;
34535
+ }
34536
+ const namespaces = new Map(descriptors.map((descriptor) => [descriptor.ns, descriptor]));
34537
+ const profiles = /* @__PURE__ */ new Map();
34538
+ for (const entry of routed) {
34539
+ const namespace = namespaces.get(entry.settingsNs);
34540
+ if (namespace === void 0) continue;
34541
+ const profile = entry.settingsPath.length === 0 ? namespace.value : getPath$1(namespace.value, entry.settingsPath);
34542
+ const models = Array.isArray(profile?.models) ? profile.models : void 0;
34543
+ if (models === void 0) continue;
34544
+ profiles.set(entry.provider, {
34545
+ settingsNs: entry.settingsNs,
34546
+ settingsPath: entry.settingsPath,
34547
+ revision: namespace.revision,
34548
+ models
34549
+ });
34550
+ }
34551
+ if (profiles.size === 0) return;
34552
+ const directory = await loadModelDirectory(ctx);
34553
+ for (const plan of planCapabilitySync({
34554
+ rows: directory.rows,
34555
+ profiles
34556
+ })) {
34557
+ const applied = lastApplied.get(plan.provider);
34558
+ if (applied !== void 0 && applied.fingerprint === plan.sourceFingerprint && applied.revision === plan.sourceRevision) continue;
34559
+ const revision = revisionOf(settings, plan.settingsNs);
34560
+ if (revision === void 0) continue;
34561
+ try {
34562
+ await settings.mutate(plan.settingsNs, [{
34563
+ op: "set",
34564
+ path: [...plan.settingsPath, "models"],
34565
+ value: plan.models
34566
+ }], revision);
34567
+ lastApplied.set(plan.provider, {
34568
+ fingerprint: plan.sourceFingerprint,
34569
+ revision: plan.sourceRevision
34570
+ });
34571
+ const shown = plan.sources.slice(0, 3).join(", ");
34572
+ const more = plan.sources.length > 3 ? `, +${plan.sources.length - 3}` : "";
34573
+ notify?.(`inherited reasoning levels for ${plan.inherited.length} model${plan.inherited.length === 1 ? "" : "s"} on ${plan.provider} (from ${shown}${more})`, "info");
34574
+ } catch (error) {
34575
+ notify?.(`capability inheritance failed on ${plan.provider}: ${singleLine$1(messageOf$1(error))}`, "warning");
34576
+ }
34577
+ }
34578
+ } catch {}
34579
+ }
34580
+ //#endregion
34607
34581
  //#region src/provider-settings.ts
34608
34582
  /** Human text for a rejection value (mirrors the web page's `messageOf`). */
34609
34583
  function messageOf(error) {
@@ -34652,11 +34626,13 @@ function configurationOf(profile) {
34652
34626
  if (typeof value !== "object" || value === null) return [];
34653
34627
  const entry = value;
34654
34628
  if (typeof entry.id !== "string" || entry.id.trim() === "") return [];
34629
+ const { id: _id, name: _name, contextWindow: _contextWindow, maxTokens: _maxTokens, ...extras } = entry;
34655
34630
  return [{
34656
34631
  id: entry.id,
34657
34632
  ...typeof entry.name === "string" && entry.name.trim() !== "" ? { name: entry.name } : {},
34658
34633
  ...typeof entry.contextWindow === "number" && Number.isFinite(entry.contextWindow) ? { contextWindow: entry.contextWindow } : {},
34659
- ...typeof entry.maxTokens === "number" && Number.isFinite(entry.maxTokens) ? { maxTokens: entry.maxTokens } : {}
34634
+ ...typeof entry.maxTokens === "number" && Number.isFinite(entry.maxTokens) ? { maxTokens: entry.maxTokens } : {},
34635
+ ...Object.keys(extras).length > 0 ? { extras } : {}
34660
34636
  }];
34661
34637
  });
34662
34638
  return {
@@ -34872,6 +34848,7 @@ async function saveProviderConfiguration(ctx, target, configuration) {
34872
34848
  for (const [label, value] of [["context window", model.contextWindow], ["output window", model.maxTokens]]) if (value !== void 0 && (!Number.isSafeInteger(value) || value <= 0)) throw new ProviderSettingsError(`${label} must be a positive integer`);
34873
34849
  return {
34874
34850
  id,
34851
+ ...model.extras,
34875
34852
  ...model.name === void 0 || model.name.trim() === "" ? {} : { name: model.name.trim() },
34876
34853
  ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
34877
34854
  ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens }
@@ -35055,7 +35032,10 @@ const NOTIFY_FRAME_MS$1 = 16;
35055
35032
  * @returns the store the runner feeds and the renderer subscribes to.
35056
35033
  */
35057
35034
  function createTranscriptStore(replay) {
35058
- let view = replay === void 0 ? createTranscriptView() : projectEvents(replay);
35035
+ let acc = createReplayAccumulator();
35036
+ for (const event of replay ?? []) replayProjectEvent(acc, event);
35037
+ let view = snapshotReplayView(acc);
35038
+ let dirty = false;
35059
35039
  const listeners = /* @__PURE__ */ new Set();
35060
35040
  let scheduled = false;
35061
35041
  let lastNotifyAt = 0;
@@ -35072,7 +35052,13 @@ function createTranscriptStore(replay) {
35072
35052
  else setTimeout(dispatch, wait);
35073
35053
  };
35074
35054
  return {
35075
- getView: () => view,
35055
+ getView: () => {
35056
+ if (dirty) {
35057
+ view = snapshotReplayView(acc);
35058
+ dirty = false;
35059
+ }
35060
+ return view;
35061
+ },
35076
35062
  subscribe(listener) {
35077
35063
  listeners.add(listener);
35078
35064
  return () => {
@@ -35080,13 +35066,13 @@ function createTranscriptStore(replay) {
35080
35066
  };
35081
35067
  },
35082
35068
  apply(event) {
35083
- const next = projectEvent(view, event);
35084
- if (next === view) return;
35085
- view = next;
35069
+ if (!replayProjectEvent(acc, event)) return;
35070
+ dirty = true;
35086
35071
  notify();
35087
35072
  },
35088
35073
  reset() {
35089
- view = createTranscriptView();
35074
+ acc = createReplayAccumulator();
35075
+ dirty = true;
35090
35076
  notify();
35091
35077
  }
35092
35078
  };
@@ -35204,6 +35190,7 @@ function foldSubagentRow(previous, sessionId, event) {
35204
35190
  */
35205
35191
  function createSubagentFeed() {
35206
35192
  let rows = Object.freeze([]);
35193
+ const seen = /* @__PURE__ */ new Set();
35207
35194
  const listeners = /* @__PURE__ */ new Set();
35208
35195
  let scheduled = false;
35209
35196
  let lastNotifyAt = 0;
@@ -35224,12 +35211,30 @@ function createSubagentFeed() {
35224
35211
  const index = rows.findIndex((row) => row.id === sessionId);
35225
35212
  const previous = index === -1 ? void 0 : rows[index];
35226
35213
  const next = foldSubagentRow(previous, sessionId, event);
35227
- if (next === previous) return;
35228
- if (index === -1 && rows.length >= 8) return;
35229
- rows = Object.freeze(index === -1 ? [...rows, next] : rows.map((row, at) => at === index ? next : row));
35214
+ if (index !== -1) {
35215
+ if (next === previous) return;
35216
+ rows = Object.freeze(rows.map((row, at) => at === index ? next : row));
35217
+ notify();
35218
+ return;
35219
+ }
35220
+ const counted = !seen.has(sessionId);
35221
+ if (counted) seen.add(sessionId);
35222
+ if (rows.length >= 8) {
35223
+ const evict = rows.findIndex((row) => row.state === "done");
35224
+ if (evict === -1) {
35225
+ if (counted) notify();
35226
+ return;
35227
+ }
35228
+ rows = Object.freeze([
35229
+ ...rows.slice(0, evict),
35230
+ next,
35231
+ ...rows.slice(evict + 1)
35232
+ ]);
35233
+ } else rows = Object.freeze([...rows, next]);
35230
35234
  notify();
35231
35235
  },
35232
35236
  reset() {
35237
+ seen.clear();
35233
35238
  if (rows.length === 0) return;
35234
35239
  rows = Object.freeze([]);
35235
35240
  notify();
@@ -35242,6 +35247,9 @@ function createSubagentFeed() {
35242
35247
  },
35243
35248
  getSnapshot() {
35244
35249
  return rows;
35250
+ },
35251
+ getTotalSeen() {
35252
+ return seen.size;
35245
35253
  }
35246
35254
  };
35247
35255
  }
@@ -35431,6 +35439,312 @@ function latestAssistantText(view) {
35431
35439
  }
35432
35440
  }
35433
35441
  //#endregion
35442
+ //#region src/editor-keys.ts
35443
+ /**
35444
+ * VS Code-family terminal keybinding repair. VS Code hands Ctrl+R to the
35445
+ * workbench (Open Recent) even while an integrated terminal owns focus, so
35446
+ * the reasoning-fold key never reaches the TUI. Workspace-scoped keybindings
35447
+ * do not exist, so the fix is one user-level keybindings.json rule that
35448
+ * forwards the raw Ctrl byte via sendSequence under terminalFocus. This
35449
+ * module detects the hosting editor variant, resolves its user
35450
+ * keybindings.json, and merges the rule idempotently; pure merge/detect
35451
+ * helpers are separated from the fs orchestration so both stay testable.
35452
+ * @module @deepseek-ai/dsh-code/editor-keys
35453
+ */
35454
+ /** Install directory names per family ('vscode' covers stable and Insiders). */
35455
+ const FAMILY_DIRS = {
35456
+ vscode: ["Code", "Code - Insiders"],
35457
+ cursor: ["Cursor"],
35458
+ vscodium: ["VSCodium"],
35459
+ windsurf: ["Windsurf"]
35460
+ };
35461
+ /**
35462
+ * Detect the editor hosting this integrated terminal.
35463
+ * @param env - process environment (TERM_PROGRAM decides; case/whitespace tolerant).
35464
+ * @returns the family, or undefined outside VS Code-family terminals.
35465
+ */
35466
+ function detectEditorTerminalFamily(env = process.env) {
35467
+ const program = env.TERM_PROGRAM?.trim().toLowerCase();
35468
+ if (program === "vscode" || program === "cursor" || program === "vscodium" || program === "windsurf") return program;
35469
+ }
35470
+ /**
35471
+ * Whether the pty is hosted away from the editor UI (ssh/container/tunnel).
35472
+ * Keybindings live on the client machine, so a remote session must never
35473
+ * write them server-side.
35474
+ */
35475
+ function isRemoteTerminalEnv(env = process.env) {
35476
+ return (env.VSCODE_IPC_HOOK_CLI ?? "").trim() !== "";
35477
+ }
35478
+ /**
35479
+ * Resolve the user keybindings.json candidates for one family, most likely
35480
+ * install first. Only paths that exist on disk are repaired.
35481
+ */
35482
+ function editorKeybindingCandidates(family, context) {
35483
+ return FAMILY_DIRS[family].map((dir) => {
35484
+ if (context.platform === "win32") return join(context.appdata ?? join(context.homedir, "AppData", "Roaming"), dir, "User", "keybindings.json");
35485
+ if (context.platform === "darwin") return join(context.homedir, "Library", "Application Support", dir, "User", "keybindings.json");
35486
+ return join(context.homedir, ".config", dir, "User", "keybindings.json");
35487
+ });
35488
+ }
35489
+ /** The one workbench rule that hands Ctrl+R to the focused terminal. */
35490
+ const CTRL_R_PASSTHROUGH_RULE = {
35491
+ key: "ctrl+r",
35492
+ command: "workbench.action.terminal.sendSequence",
35493
+ args: { text: "" },
35494
+ when: "terminalFocus"
35495
+ };
35496
+ /** Serialized rule block (4-space indent, the editors' default style). */
35497
+ const RULE_BLOCK = [
35498
+ " {",
35499
+ " \"key\": \"ctrl+r\",",
35500
+ " \"command\": \"workbench.action.terminal.sendSequence\",",
35501
+ " \"args\": { \"text\": \"\\u0012\" },",
35502
+ " \"when\": \"terminalFocus\"",
35503
+ " }"
35504
+ ].join("\n");
35505
+ /** Fresh-file template carrying the editors' standard header comment. */
35506
+ const KEYBINDINGS_TEMPLATE = "// Place your key bindings in this file to override the defaults\n[\n" + RULE_BLOCK + "\n]\n";
35507
+ /** Whether one parsed keybindings entry already forwards Ctrl+R to the terminal. */
35508
+ function isCtrlRPassthroughEntry(entry) {
35509
+ if (typeof entry !== "object" || entry === null) return false;
35510
+ const record = entry;
35511
+ if (record.command !== CTRL_R_PASSTHROUGH_RULE.command) return false;
35512
+ if (typeof record.key !== "string" || record.key.trim().toLowerCase() !== CTRL_R_PASSTHROUGH_RULE.key) return false;
35513
+ const args = record.args;
35514
+ if (typeof args !== "object" || args === null) return false;
35515
+ const text = args.text;
35516
+ if (typeof text !== "string" || !text.includes("")) return false;
35517
+ return typeof record.when === "string" && /\bterminalFocus\b/u.test(record.when);
35518
+ }
35519
+ /**
35520
+ * Remove // and block comments from one JSONC document. Double-quoted strings
35521
+ * survive untouched, so comment markers inside string values are preserved.
35522
+ */
35523
+ function stripJsoncComments(text) {
35524
+ let out = "";
35525
+ let index = 0;
35526
+ let inString = false;
35527
+ while (index < text.length) {
35528
+ const char = text[index];
35529
+ if (inString) {
35530
+ out += char;
35531
+ if (char === "\\" && index + 1 < text.length) {
35532
+ out += text[index + 1];
35533
+ index += 2;
35534
+ continue;
35535
+ }
35536
+ if (char === "\"") inString = false;
35537
+ index += 1;
35538
+ continue;
35539
+ }
35540
+ if (char === "\"") {
35541
+ inString = true;
35542
+ out += char;
35543
+ index += 1;
35544
+ continue;
35545
+ }
35546
+ if (char === "/" && text[index + 1] === "/") {
35547
+ while (index < text.length && text[index] !== "\n") index += 1;
35548
+ continue;
35549
+ }
35550
+ if (char === "/" && text[index + 1] === "*") {
35551
+ index += 2;
35552
+ while (index < text.length && !(text[index] === "*" && text[index + 1] === "/")) {
35553
+ if (text[index] === "\n") out += "\n";
35554
+ index += 1;
35555
+ }
35556
+ index += 2;
35557
+ continue;
35558
+ }
35559
+ out += char;
35560
+ index += 1;
35561
+ }
35562
+ return out;
35563
+ }
35564
+ /** Drop commas directly before a closing bracket (string-aware). */
35565
+ function removeTrailingCommas(text) {
35566
+ let out = "";
35567
+ let index = 0;
35568
+ let inString = false;
35569
+ while (index < text.length) {
35570
+ const char = text[index];
35571
+ if (inString) {
35572
+ out += char;
35573
+ if (char === "\\" && index + 1 < text.length) {
35574
+ out += text[index + 1];
35575
+ index += 2;
35576
+ continue;
35577
+ }
35578
+ if (char === "\"") inString = false;
35579
+ index += 1;
35580
+ continue;
35581
+ }
35582
+ if (char === "\"") {
35583
+ inString = true;
35584
+ out += char;
35585
+ index += 1;
35586
+ continue;
35587
+ }
35588
+ if (char === ",") {
35589
+ let peek = index + 1;
35590
+ while (peek < text.length && (text[peek] === " " || text[peek] === " " || text[peek] === "\n" || text[peek] === "\r")) peek += 1;
35591
+ const next = text[peek];
35592
+ if (next === "}" || next === "]") {
35593
+ index += 1;
35594
+ continue;
35595
+ }
35596
+ }
35597
+ out += char;
35598
+ index += 1;
35599
+ }
35600
+ return out;
35601
+ }
35602
+ /** Parse one JSONC document; trailing commas are tolerated. */
35603
+ function parseJsonc(text) {
35604
+ return JSON.parse(removeTrailingCommas(stripJsoncComments(text)));
35605
+ }
35606
+ /** Raw index of the top-level rule array's opening bracket, or -1 when absent. */
35607
+ function rawOpenBracketIndex(text) {
35608
+ let index = 0;
35609
+ let inString = false;
35610
+ while (index < text.length) {
35611
+ const char = text[index];
35612
+ if (inString) {
35613
+ if (char === "\\") {
35614
+ index += 2;
35615
+ continue;
35616
+ }
35617
+ if (char === "\"") inString = false;
35618
+ index += 1;
35619
+ continue;
35620
+ }
35621
+ if (char === "\"") {
35622
+ inString = true;
35623
+ index += 1;
35624
+ continue;
35625
+ }
35626
+ if (char === "/" && text[index + 1] === "/") {
35627
+ while (index < text.length && text[index] !== "\n") index += 1;
35628
+ continue;
35629
+ }
35630
+ if (char === "/" && text[index + 1] === "*") {
35631
+ index += 2;
35632
+ while (index < text.length && !(text[index] === "*" && text[index + 1] === "/")) index += 1;
35633
+ index += 2;
35634
+ continue;
35635
+ }
35636
+ if (char === "[") return index;
35637
+ index += 1;
35638
+ }
35639
+ return -1;
35640
+ }
35641
+ /**
35642
+ * Merge the Ctrl+R passthrough into one keybindings.json document. The raw
35643
+ * text is preserved verbatim (comments included); the rule is inserted right
35644
+ * after the array opener so it cannot be shadowed by later conflicting user
35645
+ * rules. Missing files resolve to a fresh template.
35646
+ * @throws when the document does not carry a rule array.
35647
+ */
35648
+ function mergeCtrlRPassthrough(raw) {
35649
+ if (raw === void 0) return {
35650
+ status: "created",
35651
+ text: KEYBINDINGS_TEMPLATE
35652
+ };
35653
+ const parsed = parseJsonc(raw);
35654
+ if (!Array.isArray(parsed)) throw new Error("keybindings.json does not contain a rule array");
35655
+ if (parsed.some(isCtrlRPassthroughEntry)) return { status: "present" };
35656
+ const open = rawOpenBracketIndex(raw);
35657
+ if (open === -1) throw new Error("keybindings.json does not contain a rule array");
35658
+ const insert = parsed.length > 0 ? "\n" + RULE_BLOCK + "," : "\n" + RULE_BLOCK;
35659
+ return {
35660
+ status: "updated",
35661
+ text: raw.slice(0, open + 1) + insert + raw.slice(open + 1)
35662
+ };
35663
+ }
35664
+ /** Parse one flag file snapshot; missing or corrupt content degrades to unshown. */
35665
+ function parseEditorKeysFlag(raw) {
35666
+ if (raw === void 0) return {};
35667
+ try {
35668
+ const parsed = JSON.parse(raw);
35669
+ if (typeof parsed !== "object" || parsed === null) return {};
35670
+ const shown = parsed.hintShownAt;
35671
+ return typeof shown === "string" ? { hintShownAt: shown } : {};
35672
+ } catch {
35673
+ return {};
35674
+ }
35675
+ }
35676
+ /** Persist the shown marker; best-effort, the hint is cosmetic and never a gate. */
35677
+ async function markEditorKeysHintShown(path) {
35678
+ await mkdir(dirname(path), { recursive: true });
35679
+ await writeFile(path, JSON.stringify({ hintShownAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n", "utf8");
35680
+ }
35681
+ /** Read one file if it exists; undefined otherwise (ENOENT and unreadable both). */
35682
+ async function readIfPresent(path) {
35683
+ try {
35684
+ return await readFile(path, "utf8");
35685
+ } catch {
35686
+ return;
35687
+ }
35688
+ }
35689
+ /**
35690
+ * Apply the Ctrl+R passthrough to every local keybindings.json of the hosting
35691
+ * editor and mark the startup hint shown. Existing files get a .dsh-bak
35692
+ * backup before the first write.
35693
+ * @returns a one-line user-facing summary.
35694
+ * @throws with an actionable message when the environment cannot be repaired.
35695
+ */
35696
+ async function applyCtrlRPassthrough({ env, paths, flagPath }) {
35697
+ const family = detectEditorTerminalFamily(env);
35698
+ if (family === void 0) throw new Error("not a VS Code-family terminal (TERM_PROGRAM=" + (env.TERM_PROGRAM?.trim() || "unset") + "); add the ctrl+r rule to keybindings.json manually");
35699
+ if (isRemoteTerminalEnv(env)) throw new Error("remote terminal detected; apply the keybindings rule on the local machine instead");
35700
+ const candidates = editorKeybindingCandidates(family, paths);
35701
+ const targets = [];
35702
+ for (const candidate of candidates) if (await readIfPresent(candidate) !== void 0) targets.push(candidate);
35703
+ if (targets.length === 0) targets.push(candidates[0]);
35704
+ const updated = [];
35705
+ const present = [];
35706
+ for (const target of targets) {
35707
+ const raw = await readIfPresent(target);
35708
+ const merge = mergeCtrlRPassthrough(raw);
35709
+ if (merge.status === "present") {
35710
+ present.push(target);
35711
+ continue;
35712
+ }
35713
+ await mkdir(dirname(target), { recursive: true });
35714
+ if (raw !== void 0) await writeFile(target + ".dsh-bak", raw, "utf8");
35715
+ await writeFile(target, merge.text, "utf8");
35716
+ updated.push(target);
35717
+ }
35718
+ await markEditorKeysHintShown(flagPath).catch(() => {});
35719
+ const label = (target) => basename(dirname(dirname(target)));
35720
+ if (updated.length === 0) return "ctrl+r passthrough already configured in " + present.map(label).join(", ");
35721
+ return "ctrl+r passthrough written to " + updated.map(label).join(", ") + " — effective immediately";
35722
+ }
35723
+ /**
35724
+ * Resolve the one-shot startup hint for VS Code-family terminals. Fires at
35725
+ * most once per install (flag file), never when the passthrough rule is
35726
+ * already present, and never in remote ptys where the repair cannot run.
35727
+ * @returns the hint line, or undefined to stay silent.
35728
+ */
35729
+ async function resolveEditorKeysStartupHint({ env, paths, flagPath }) {
35730
+ if (parseEditorKeysFlag(await readIfPresent(flagPath)).hintShownAt !== void 0) return void 0;
35731
+ const family = detectEditorTerminalFamily(env);
35732
+ if (family === void 0 || isRemoteTerminalEnv(env)) return void 0;
35733
+ for (const candidate of editorKeybindingCandidates(family, paths)) {
35734
+ const raw = await readIfPresent(candidate);
35735
+ if (raw === void 0) continue;
35736
+ try {
35737
+ const parsed = parseJsonc(raw);
35738
+ if (Array.isArray(parsed) && parsed.some(isCtrlRPassthroughEntry)) {
35739
+ await markEditorKeysHintShown(flagPath).catch(() => {});
35740
+ return;
35741
+ }
35742
+ } catch {}
35743
+ }
35744
+ await markEditorKeysHintShown(flagPath).catch(() => {});
35745
+ return "run /vscode-keys to pass ctrl+r through this editor (alt+r works meanwhile)";
35746
+ }
35747
+ //#endregion
35434
35748
  //#region src/fork.ts
35435
35749
  /** Select a completed turn and trailing between-turn metadata. */
35436
35750
  function selectForkSeed(events, atSeq) {
@@ -35497,13 +35811,14 @@ function parseGitDiffSpec(argument) {
35497
35811
  ]
35498
35812
  };
35499
35813
  }
35500
- function executeGit(cwd, args) {
35814
+ function executeGit(cwd, args, signal) {
35501
35815
  return new Promise((resolve, reject) => {
35502
35816
  execFile("git", [...args], {
35503
35817
  cwd,
35504
35818
  encoding: "utf8",
35505
35819
  maxBuffer: 16777216,
35506
- windowsHide: true
35820
+ windowsHide: true,
35821
+ signal
35507
35822
  }, (error, stdout, stderr) => {
35508
35823
  if (error !== null) {
35509
35824
  reject(new Error(stderr.trim() || error.message));
@@ -35513,11 +35828,15 @@ function executeGit(cwd, args) {
35513
35828
  });
35514
35829
  });
35515
35830
  }
35516
- /** Load one complete textual diff without invoking external diff drivers. */
35517
- async function loadGitDiff(cwd, argument) {
35831
+ /**
35832
+ * Load one complete textual diff without invoking external diff drivers.
35833
+ * @param signal - aborted by the caller on session switches/quit, killing the
35834
+ * git subprocess instead of letting a stale repository's diff land later.
35835
+ */
35836
+ async function loadGitDiff(cwd, argument, signal) {
35518
35837
  const spec = parseGitDiffSpec(argument);
35519
35838
  try {
35520
- const text = await executeGit(cwd, spec.args);
35839
+ const text = await executeGit(cwd, spec.args, signal);
35521
35840
  return {
35522
35841
  title: `git diff - ${spec.label}`,
35523
35842
  files: parseGitDiffFiles(text)
@@ -35531,7 +35850,7 @@ async function loadGitDiff(cwd, argument) {
35531
35850
  "--no-ext-diff",
35532
35851
  "--unified=3",
35533
35852
  "--"
35534
- ]))
35853
+ ], signal))
35535
35854
  };
35536
35855
  }
35537
35856
  }
@@ -35699,6 +36018,48 @@ function listPluginRows(ctx) {
35699
36018
  return rows;
35700
36019
  }
35701
36020
  //#endregion
36021
+ //#region src/settings-file.ts
36022
+ /**
36023
+ * Serialized, crash-atomic persistence for the small user-level JSON files
36024
+ * under the DSH home (statusline.json, theme.json). Two guarantees the bare
36025
+ * floating `writeFile` path could not give:
36026
+ *
36027
+ * 1. Every save is appended to ONE chain, so rapid consecutive edits land
36028
+ * in submission order and the last snapshot is the one on disk (parallel
36029
+ * floating writes let an older snapshot finish last and win).
36030
+ * 2. Each write goes to a sibling temp file first and is renamed into
36031
+ * place, so a crash mid-write can never leave a half-written JSON
36032
+ * document behind.
36033
+ *
36034
+ * The chain itself never rejects: a failed write is reported to that
36035
+ * save's caller while later saves keep their turn.
36036
+ *
36037
+ * @module @deepseek-ai/dsh-code/settings-file
36038
+ */
36039
+ /**
36040
+ * Create the shared settings-write chain. One instance per process keeps
36041
+ * every user-level JSON file mutually serialized.
36042
+ * @returns the persistence handle.
36043
+ */
36044
+ function createUserSettingsPersistence() {
36045
+ let chain = Promise.resolve();
36046
+ return {
36047
+ save(path, text) {
36048
+ const write = chain.then(async () => {
36049
+ await mkdir(dirname(path), { recursive: true });
36050
+ const temp = `${path}.tmp`;
36051
+ await writeFile(temp, text, "utf8");
36052
+ await rename(temp, path);
36053
+ });
36054
+ chain = write.catch(() => {});
36055
+ return write;
36056
+ },
36057
+ flush() {
36058
+ return chain;
36059
+ }
36060
+ };
36061
+ }
36062
+ //#endregion
35702
36063
  //#region src/index.ts
35703
36064
  /**
35704
36065
  * @deepseek-ai/dsh-code — the interactive terminal driver. The bundle patch
@@ -36022,6 +36383,20 @@ async function run(ctx, startup, io) {
36022
36383
  });
36023
36384
  const questions = mountQuestionProvider(ctx);
36024
36385
  const bridge = { notify: () => {} };
36386
+ const capabilitySyncDebounceMs = 400;
36387
+ const runCapabilitySync = () => {
36388
+ syncModelCapabilities(ctx, bridge.notify);
36389
+ };
36390
+ let capabilitySyncTimer;
36391
+ const scheduleCapabilitySync = () => {
36392
+ if (capabilitySyncTimer !== void 0) clearTimeout(capabilitySyncTimer);
36393
+ capabilitySyncTimer = setTimeout(() => {
36394
+ capabilitySyncTimer = void 0;
36395
+ runCapabilitySync();
36396
+ }, capabilitySyncDebounceMs);
36397
+ };
36398
+ const offCapabilitySync = [ctx.on("settings/document-updated", scheduleCapabilitySync), ctx.on("llm/adapters-updated", scheduleCapabilitySync)];
36399
+ scheduleCapabilitySync();
36025
36400
  const statuslinePath = join(homedir(), ".dsh", "dsh-code", "statusline.json");
36026
36401
  let statuslineWarning;
36027
36402
  let statuslineItems = [];
@@ -36031,12 +36406,23 @@ async function run(ctx, startup, io) {
36031
36406
  statuslineItems = parseStatuslineItems(void 0);
36032
36407
  if (error.code !== "ENOENT") statuslineWarning = error instanceof Error ? error.message : String(error);
36033
36408
  }
36409
+ const settingsPersistence = createUserSettingsPersistence();
36034
36410
  const saveStatusline = (items) => {
36035
36411
  statuslineItems = [...items];
36036
- mkdir(dirname(statuslinePath), { recursive: true }).then(() => writeFile(statuslinePath, JSON.stringify({ items }, null, 2) + "\n", "utf8")).catch((writeError) => {
36412
+ settingsPersistence.save(statuslinePath, JSON.stringify({ items }, null, 2) + "\n").catch((writeError) => {
36037
36413
  bridge.notify("statusline save failed: " + (writeError instanceof Error ? writeError.message : String(writeError)), "error");
36038
36414
  });
36039
36415
  };
36416
+ const editorKeysEnv = {
36417
+ env: process.env,
36418
+ paths: {
36419
+ homedir: homedir(),
36420
+ appdata: process.env.APPDATA,
36421
+ platform: process.platform
36422
+ },
36423
+ flagPath: join(homedir(), ".dsh", "dsh-code", "editor-keys.json")
36424
+ };
36425
+ const applyEditorKeys = () => applyCtrlRPassthrough(editorKeysEnv);
36040
36426
  const themePath = join(homedir(), ".dsh", "dsh-code", "theme.json");
36041
36427
  let themeWarning;
36042
36428
  if (startup.theme === void 0) try {
@@ -36047,7 +36433,7 @@ async function run(ctx, startup, io) {
36047
36433
  else setTheme(startup.theme);
36048
36434
  const saveTheme = (name) => {
36049
36435
  setTheme(name);
36050
- mkdir(dirname(themePath), { recursive: true }).then(() => writeFile(themePath, JSON.stringify({ theme: name }, null, 2) + "\n", "utf8")).catch((writeError) => {
36436
+ settingsPersistence.save(themePath, JSON.stringify({ theme: name }, null, 2) + "\n").catch((writeError) => {
36051
36437
  bridge.notify("theme save failed: " + (writeError instanceof Error ? writeError.message : String(writeError)), "error");
36052
36438
  });
36053
36439
  };
@@ -36086,6 +36472,8 @@ async function run(ctx, startup, io) {
36086
36472
  quitAbort.abort();
36087
36473
  epoch += 1;
36088
36474
  off();
36475
+ for (const dispose of offCapabilitySync) dispose();
36476
+ if (capabilitySyncTimer !== void 0) clearTimeout(capabilitySyncTimer);
36089
36477
  mountRef.current?.unmount();
36090
36478
  const currentSession = session;
36091
36479
  const currentActive = active;
@@ -36109,6 +36497,10 @@ async function run(ctx, startup, io) {
36109
36497
  {
36110
36498
  name: "history",
36111
36499
  run: () => historyWriteChain
36500
+ },
36501
+ {
36502
+ name: "settings",
36503
+ run: () => settingsPersistence.flush()
36112
36504
  }
36113
36505
  ], io.exit, report);
36114
36506
  };
@@ -36428,10 +36820,21 @@ async function run(ctx, startup, io) {
36428
36820
  * Delete one session subtree (/delete, codex semantics: subagent threads go
36429
36821
  * with their root). The kernel persistence seam has NO deletion API by
36430
36822
  * design — logs accumulate "until removed externally" — so this is the
36431
- * controlled external removal: guards (live/current refusal, subtree
36432
- * collection, and the JSONL layout check `encodeSegment(id)/session.jsonl`)
36433
- * run before any filesystem touch, and only the backend-located artifacts
36434
- * are removed. Backends without a locatable artifact (SQLite) are refused.
36823
+ * controlled external removal, in three phases with a hard boundary
36824
+ * between planning and touching the filesystem:
36825
+ *
36826
+ * 1. `planSessionDeletion` collects the subtree and refuses when the root
36827
+ * or ANY member is live (a live child would outlive its deleted
36828
+ * parent), ordering the plan children-first.
36829
+ * 2. Every plan node must locate to a guarded artifact directory
36830
+ * (`encodeSegment(id)`/`session.jsonl` layout). Backends without a
36831
+ * locatable artifact (SQLite) refuse the WHOLE deletion here — no
36832
+ * file has been touched yet, so a backend or layout surprise can
36833
+ * never strand a half-deleted subtree.
36834
+ * 3. Artifacts are removed children-first: only an I/O error mid-delete
36835
+ * can stop it short (reported with removed/total counts), leaving the
36836
+ * shallowest lineage intact.
36837
+ *
36435
36838
  * @param id - the root session id to delete.
36436
36839
  * @returns the outcome line for the panel/notice.
36437
36840
  */
@@ -36439,19 +36842,22 @@ async function run(ctx, startup, io) {
36439
36842
  if (sessionQuery === void 0) return "session query is unavailable in this profile";
36440
36843
  if (session !== void 0 && session.id === id) return "cannot delete the session you are using — switch or /new first";
36441
36844
  const records = await sessionQuery.listSessions();
36442
- const target = records.find((record) => record.header.id === id);
36443
- if (target === void 0) return `no persisted session matches "${id}"`;
36444
- if (target.live) return "cannot delete a live session — it is open in this or another process";
36445
- const doomed = collectDeletionSubtree(records, id);
36845
+ const plan = planSessionDeletion(records, id);
36846
+ if (!plan.ok) return plan.reason;
36446
36847
  const byId = new Map(records.map((record) => [record.header.id, record]));
36447
- let removed = 0;
36448
- for (const candidate of doomed) {
36449
- const record = byId.get(candidate);
36450
- if (record === void 0 || record.live) continue;
36848
+ const dirs = /* @__PURE__ */ new Map();
36849
+ for (const node of plan.nodes) {
36850
+ const record = byId.get(node.id);
36851
+ if (record === void 0) return `no persisted session matches "${node.id}"`;
36451
36852
  const location = persistence?.locate(record.header);
36452
- if (location === void 0) return `session backend exposes no deletable artifact for ${candidate.slice(-12)} (deletion is unsupported on this backend)`;
36453
- const dir = sessionArtifactDirectory(location.path, candidate);
36853
+ if (location === void 0) return `session backend exposes no deletable artifact for ${node.id.slice(-12)} (deletion is unsupported on this backend)`;
36854
+ const dir = sessionArtifactDirectory(location.path, node.id);
36454
36855
  if (dir === void 0) return `refusing to delete: unexpected artifact layout at ${location.path}`;
36856
+ dirs.set(node.id, dir);
36857
+ }
36858
+ let removed = 0;
36859
+ for (const node of plan.nodes) {
36860
+ const dir = dirs.get(node.id);
36455
36861
  try {
36456
36862
  for (const name of SESSION_ARTIFACT_NAMES) await rm(join(dir, name), { force: true });
36457
36863
  await rm(dir, {
@@ -36460,7 +36866,7 @@ async function run(ctx, startup, io) {
36460
36866
  }).catch(() => {});
36461
36867
  removed += 1;
36462
36868
  } catch (error) {
36463
- return `delete failed for ${candidate.slice(-12)}: ${error instanceof Error ? error.message : String(error)}`;
36869
+ return `delete failed for ${node.id.slice(-12)} after ${removed} of ${plan.nodes.length}: ${error instanceof Error ? error.message : String(error)}`;
36464
36870
  }
36465
36871
  }
36466
36872
  return `deleted ${removed} session${removed === 1 ? "" : "s"}`;
@@ -36485,13 +36891,20 @@ async function run(ctx, startup, io) {
36485
36891
  pendingModeWork = choice.then(() => {}, () => {});
36486
36892
  return choice;
36487
36893
  }
36488
- const preset = await selectPreset(presets, currentAgent, id);
36489
- if (active === void 0) throw new Error("active Agent has no session state");
36490
- active.mode = preset.id;
36491
- commands.setAgent(currentAgent);
36492
- skills.setAgent(currentAgent);
36493
- renderCurrent();
36494
- return preset.id;
36894
+ const currentActive = active;
36895
+ const atEpoch = epoch;
36896
+ let selected;
36897
+ await compose(async () => {
36898
+ const preset = await selectPreset(presets, currentAgent, id);
36899
+ if (epoch !== atEpoch || agent !== currentAgent || active !== currentActive) throw new Error("session changed while switching mode — nothing applied; retry in the active session");
36900
+ if (active === void 0) throw new Error("active Agent has no session state");
36901
+ active.mode = preset.id;
36902
+ commands.setAgent(currentAgent);
36903
+ skills.setAgent(currentAgent);
36904
+ selected = preset.id;
36905
+ renderCurrent();
36906
+ });
36907
+ return selected;
36495
36908
  };
36496
36909
  const activate = (nextTarget) => {
36497
36910
  if (quitting) return Promise.resolve();
@@ -36605,7 +37018,21 @@ async function run(ctx, startup, io) {
36605
37018
  });
36606
37019
  };
36607
37020
  const reviewChanges = (argument) => {
36608
- loadGitDiff(session?.header.cwd ?? cwd, argument).then(({ title, files }) => {
37021
+ const currentAgent = agent;
37022
+ if (currentAgent === void 0) {
37023
+ bridge.notify("no session yet - submit a message to start", "warning");
37024
+ return;
37025
+ }
37026
+ const atEpoch = epoch;
37027
+ const reviewCwd = session?.header.cwd ?? cwd;
37028
+ const controller = new AbortController();
37029
+ pendingControllers.add(controller);
37030
+ const finish = () => {
37031
+ pendingControllers.delete(controller);
37032
+ };
37033
+ loadGitDiff(reviewCwd, argument, controller.signal).then(({ title, files }) => {
37034
+ finish();
37035
+ if (controller.signal.aborted || epoch !== atEpoch || agent !== currentAgent) return;
36609
37036
  try {
36610
37037
  setPermissionAction("read-only");
36611
37038
  } catch (error) {
@@ -36615,6 +37042,8 @@ async function run(ctx, startup, io) {
36615
37042
  send(buildReviewPrompt(files.flatMap((file) => file.lines).join("\n"), title), "followup");
36616
37043
  bridge.notify("review started under read-only permissions");
36617
37044
  }, (error) => {
37045
+ finish();
37046
+ if (controller.signal.aborted || epoch !== atEpoch) return;
36618
37047
  bridge.notify(`review failed: ${error instanceof Error ? error.message : String(error)}`, "error");
36619
37048
  });
36620
37049
  };
@@ -36742,6 +37171,7 @@ async function run(ctx, startup, io) {
36742
37171
  loadJobs: () => listJobs(ctx, active?.agent),
36743
37172
  statusline: statuslineItems,
36744
37173
  saveStatusline,
37174
+ applyEditorKeys,
36745
37175
  saveTheme,
36746
37176
  history: inputHistory,
36747
37177
  recordHistory,
@@ -36774,6 +37204,12 @@ async function run(ctx, startup, io) {
36774
37204
  if (themeWarning !== void 0) setTimeout(() => {
36775
37205
  bridge.notify("theme config unreadable, using dark: " + themeWarning, "warning");
36776
37206
  }, 50);
37207
+ resolveEditorKeysStartupHint(editorKeysEnv).then((hint) => {
37208
+ if (hint === void 0) return;
37209
+ setTimeout(() => {
37210
+ bridge.notify(hint);
37211
+ }, 50);
37212
+ }, () => {});
36777
37213
  }
36778
37214
  /**
36779
37215
  * Mount the interactive terminal driver.