dsh-code 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { n as __require, r as __toESM, t as __commonJSMin } from "./rolldown-runtime-CMFfr-1z.mjs";
2
+ import { a as getPalette, c as parseThemeName, i as error, l as setTheme, n as brand, o as getTheme, r as dim, s as inkColor, u as chalk } from "./theme-BEi4i_aN.mjs";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import * as fs from "node:fs";
4
5
  import { readFileSync } from "node:fs";
@@ -7,11 +8,10 @@ import { mkdir, readdir, writeFile } from "node:fs/promises";
7
8
  import { basename, dirname, join, resolve } from "node:path";
8
9
  import z from "@deepseek-ai/schemastery";
9
10
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
10
- import { MessageId, assertNever, boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
11
+ import { MessageId, ReasoningEffortId, assertNever, boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
11
12
  import { SessionId } from "@deepseek-ai/dsh-session";
12
13
  import { PassThrough, Stream } from "node:stream";
13
14
  import process$1, { cwd, env } from "node:process";
14
- import tty from "node:tty";
15
15
  import { EventEmitter } from "node:events";
16
16
  import { Buffer as Buffer$1 } from "node:buffer";
17
17
  import { formatSessionReferenceMention, parseSessionReferenceText } from "@deepseek-ai/dsh-session-reference";
@@ -21707,11 +21707,11 @@ const measureText = (text) => {
21707
21707
  };
21708
21708
  //#endregion
21709
21709
  //#region node_modules/.pnpm/ansi-styles@6.2.3/node_modules/ansi-styles/index.js
21710
- const ANSI_BACKGROUND_OFFSET$1 = 10;
21711
- const wrapAnsi16$1 = (offset = 0) => (code) => `\u001B[${code + offset}m`;
21712
- const wrapAnsi256$1 = (offset = 0) => (code) => `\u001B[${38 + offset};5;${code}m`;
21713
- const wrapAnsi16m$1 = (offset = 0) => (red, green, blue) => `\u001B[${38 + offset};2;${red};${green};${blue}m`;
21714
- const styles$3 = {
21710
+ const ANSI_BACKGROUND_OFFSET = 10;
21711
+ const wrapAnsi16 = (offset = 0) => (code) => `\u001B[${code + offset}m`;
21712
+ const wrapAnsi256 = (offset = 0) => (code) => `\u001B[${38 + offset};5;${code}m`;
21713
+ const wrapAnsi16m = (offset = 0) => (red, green, blue) => `\u001B[${38 + offset};2;${red};${green};${blue}m`;
21714
+ const styles$1 = {
21715
21715
  modifier: {
21716
21716
  reset: [0, 0],
21717
21717
  bold: [1, 22],
@@ -21764,39 +21764,39 @@ const styles$3 = {
21764
21764
  bgWhiteBright: [107, 49]
21765
21765
  }
21766
21766
  };
21767
- Object.keys(styles$3.modifier);
21768
- const foregroundColorNames$1 = Object.keys(styles$3.color);
21769
- const backgroundColorNames$1 = Object.keys(styles$3.bgColor);
21770
- [...foregroundColorNames$1, ...backgroundColorNames$1];
21771
- function assembleStyles$1() {
21767
+ Object.keys(styles$1.modifier);
21768
+ const foregroundColorNames = Object.keys(styles$1.color);
21769
+ const backgroundColorNames = Object.keys(styles$1.bgColor);
21770
+ [...foregroundColorNames, ...backgroundColorNames];
21771
+ function assembleStyles() {
21772
21772
  const codes = /* @__PURE__ */ new Map();
21773
- for (const [groupName, group] of Object.entries(styles$3)) {
21773
+ for (const [groupName, group] of Object.entries(styles$1)) {
21774
21774
  for (const [styleName, style] of Object.entries(group)) {
21775
- styles$3[styleName] = {
21775
+ styles$1[styleName] = {
21776
21776
  open: `\u001B[${style[0]}m`,
21777
21777
  close: `\u001B[${style[1]}m`
21778
21778
  };
21779
- group[styleName] = styles$3[styleName];
21779
+ group[styleName] = styles$1[styleName];
21780
21780
  codes.set(style[0], style[1]);
21781
21781
  }
21782
- Object.defineProperty(styles$3, groupName, {
21782
+ Object.defineProperty(styles$1, groupName, {
21783
21783
  value: group,
21784
21784
  enumerable: false
21785
21785
  });
21786
21786
  }
21787
- Object.defineProperty(styles$3, "codes", {
21787
+ Object.defineProperty(styles$1, "codes", {
21788
21788
  value: codes,
21789
21789
  enumerable: false
21790
21790
  });
21791
- styles$3.color.close = "\x1B[39m";
21792
- styles$3.bgColor.close = "\x1B[49m";
21793
- styles$3.color.ansi = wrapAnsi16$1();
21794
- styles$3.color.ansi256 = wrapAnsi256$1();
21795
- styles$3.color.ansi16m = wrapAnsi16m$1();
21796
- styles$3.bgColor.ansi = wrapAnsi16$1(ANSI_BACKGROUND_OFFSET$1);
21797
- styles$3.bgColor.ansi256 = wrapAnsi256$1(ANSI_BACKGROUND_OFFSET$1);
21798
- styles$3.bgColor.ansi16m = wrapAnsi16m$1(ANSI_BACKGROUND_OFFSET$1);
21799
- Object.defineProperties(styles$3, {
21791
+ styles$1.color.close = "\x1B[39m";
21792
+ styles$1.bgColor.close = "\x1B[49m";
21793
+ styles$1.color.ansi = wrapAnsi16();
21794
+ styles$1.color.ansi256 = wrapAnsi256();
21795
+ styles$1.color.ansi16m = wrapAnsi16m();
21796
+ styles$1.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
21797
+ styles$1.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
21798
+ styles$1.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
21799
+ Object.defineProperties(styles$1, {
21800
21800
  rgbToAnsi256: {
21801
21801
  value(red, green, blue) {
21802
21802
  if (red === green && green === blue) {
@@ -21828,7 +21828,7 @@ function assembleStyles$1() {
21828
21828
  enumerable: false
21829
21829
  },
21830
21830
  hexToAnsi256: {
21831
- value: (hex) => styles$3.rgbToAnsi256(...styles$3.hexToRgb(hex)),
21831
+ value: (hex) => styles$1.rgbToAnsi256(...styles$1.hexToRgb(hex)),
21832
21832
  enumerable: false
21833
21833
  },
21834
21834
  ansi256ToAnsi: {
@@ -21858,17 +21858,17 @@ function assembleStyles$1() {
21858
21858
  enumerable: false
21859
21859
  },
21860
21860
  rgbToAnsi: {
21861
- value: (red, green, blue) => styles$3.ansi256ToAnsi(styles$3.rgbToAnsi256(red, green, blue)),
21861
+ value: (red, green, blue) => styles$1.ansi256ToAnsi(styles$1.rgbToAnsi256(red, green, blue)),
21862
21862
  enumerable: false
21863
21863
  },
21864
21864
  hexToAnsi: {
21865
- value: (hex) => styles$3.ansi256ToAnsi(styles$3.hexToAnsi256(hex)),
21865
+ value: (hex) => styles$1.ansi256ToAnsi(styles$1.hexToAnsi256(hex)),
21866
21866
  enumerable: false
21867
21867
  }
21868
21868
  });
21869
- return styles$3;
21869
+ return styles$1;
21870
21870
  }
21871
- const ansiStyles$1 = assembleStyles$1();
21871
+ const ansiStyles = assembleStyles();
21872
21872
  //#endregion
21873
21873
  //#region node_modules/.pnpm/wrap-ansi@9.0.2/node_modules/wrap-ansi/index.js
21874
21874
  const ESCAPES$3 = /* @__PURE__ */ new Set(["\x1B", "›"]);
@@ -21977,7 +21977,7 @@ const exec = (string, columns, options = {}) => {
21977
21977
  escapeCode = code === END_CODE ? void 0 : code;
21978
21978
  } else if (groups.uri !== void 0) escapeUrl = groups.uri.length === 0 ? void 0 : groups.uri;
21979
21979
  }
21980
- const code = ansiStyles$1.codes.get(Number(escapeCode));
21980
+ const code = ansiStyles.codes.get(Number(escapeCode));
21981
21981
  if (pre[index + 1] === "\n") {
21982
21982
  if (escapeUrl) returnValue += wrapAnsiHyperlink("");
21983
21983
  if (escapeCode && code) returnValue += wrapAnsiCode(code);
@@ -22009,7 +22009,7 @@ const checkAnsi = (ansiCodes, isEscapes, endAnsiCode) => {
22009
22009
  for (let ansiCode of ansiCodes) {
22010
22010
  const ansiCodeOrigin = ansiCode;
22011
22011
  if (ansiCode.includes(";")) ansiCode = ansiCode.split(";")[0][0] + "0";
22012
- const item = ansiStyles$1.codes.get(Number.parseInt(ansiCode, 10));
22012
+ const item = ansiStyles.codes.get(Number.parseInt(ansiCode, 10));
22013
22013
  if (item) {
22014
22014
  const indexEscape = ansiCodes.indexOf(item.toString());
22015
22015
  if (indexEscape === -1) output.push(wrapAnsi(isEscapes ? item : ansiCodeOrigin));
@@ -22022,7 +22022,7 @@ const checkAnsi = (ansiCodes, isEscapes, endAnsiCode) => {
22022
22022
  if (isEscapes) {
22023
22023
  output = output.filter((element, index) => output.indexOf(element) === index);
22024
22024
  if (endAnsiCode !== void 0) {
22025
- const fistEscapeCode = wrapAnsi(ansiStyles$1.codes.get(Number.parseInt(endAnsiCode, 10)));
22025
+ const fistEscapeCode = wrapAnsi(ansiStyles.codes.get(Number.parseInt(endAnsiCode, 10)));
22026
22026
  output = output.reduce((current, next) => next === fistEscapeCode ? [next, ...current] : [...current, next], []);
22027
22027
  }
22028
22028
  }
@@ -22325,7 +22325,7 @@ const applyGapStyles = (node, style) => {
22325
22325
  if ("columnGap" in style) node.setGap(Yoga.GUTTER_COLUMN, style.columnGap ?? 0);
22326
22326
  if ("rowGap" in style) node.setGap(Yoga.GUTTER_ROW, style.rowGap ?? 0);
22327
22327
  };
22328
- const styles$2 = (node, style = {}) => {
22328
+ const styles = (node, style = {}) => {
22329
22329
  applyPositionStyles(node, style);
22330
22330
  applyMarginStyles(node, style);
22331
22331
  applyPaddingStyles(node, style);
@@ -22400,7 +22400,7 @@ var reconciler_default = (0, import_react_reconciler.default)({
22400
22400
  if (key === "children") continue;
22401
22401
  if (key === "style") {
22402
22402
  setStyle(node, value);
22403
- if (node.yogaNode) styles$2(node.yogaNode, value);
22403
+ if (node.yogaNode) styles(node.yogaNode, value);
22404
22404
  continue;
22405
22405
  }
22406
22406
  if (key === "internal_transform") {
@@ -22489,7 +22489,7 @@ var reconciler_default = (0, import_react_reconciler.default)({
22489
22489
  }
22490
22490
  setAttribute(node, key, value);
22491
22491
  }
22492
- if (style && node.yogaNode) styles$2(node.yogaNode, style);
22492
+ if (style && node.yogaNode) styles(node.yogaNode, style);
22493
22493
  },
22494
22494
  commitTextUpdate(node, _oldText, newText) {
22495
22495
  setTextNodeValue(node, newText);
@@ -22603,414 +22603,12 @@ var require_boxes = /* @__PURE__ */ __commonJSMin(((exports, module) => {
22603
22603
  };
22604
22604
  }));
22605
22605
  //#endregion
22606
- //#region node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
22606
+ //#region node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/colorize.js
22607
22607
  var import_cli_boxes = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
22608
22608
  const cliBoxes = require_boxes();
22609
22609
  module.exports = cliBoxes;
22610
22610
  module.exports.default = cliBoxes;
22611
22611
  })))(), 1);
22612
- const ANSI_BACKGROUND_OFFSET = 10;
22613
- const wrapAnsi16 = (offset = 0) => (code) => `\u001B[${code + offset}m`;
22614
- const wrapAnsi256 = (offset = 0) => (code) => `\u001B[${38 + offset};5;${code}m`;
22615
- const wrapAnsi16m = (offset = 0) => (red, green, blue) => `\u001B[${38 + offset};2;${red};${green};${blue}m`;
22616
- const styles$1 = {
22617
- modifier: {
22618
- reset: [0, 0],
22619
- bold: [1, 22],
22620
- dim: [2, 22],
22621
- italic: [3, 23],
22622
- underline: [4, 24],
22623
- overline: [53, 55],
22624
- inverse: [7, 27],
22625
- hidden: [8, 28],
22626
- strikethrough: [9, 29]
22627
- },
22628
- color: {
22629
- black: [30, 39],
22630
- red: [31, 39],
22631
- green: [32, 39],
22632
- yellow: [33, 39],
22633
- blue: [34, 39],
22634
- magenta: [35, 39],
22635
- cyan: [36, 39],
22636
- white: [37, 39],
22637
- blackBright: [90, 39],
22638
- gray: [90, 39],
22639
- grey: [90, 39],
22640
- redBright: [91, 39],
22641
- greenBright: [92, 39],
22642
- yellowBright: [93, 39],
22643
- blueBright: [94, 39],
22644
- magentaBright: [95, 39],
22645
- cyanBright: [96, 39],
22646
- whiteBright: [97, 39]
22647
- },
22648
- bgColor: {
22649
- bgBlack: [40, 49],
22650
- bgRed: [41, 49],
22651
- bgGreen: [42, 49],
22652
- bgYellow: [43, 49],
22653
- bgBlue: [44, 49],
22654
- bgMagenta: [45, 49],
22655
- bgCyan: [46, 49],
22656
- bgWhite: [47, 49],
22657
- bgBlackBright: [100, 49],
22658
- bgGray: [100, 49],
22659
- bgGrey: [100, 49],
22660
- bgRedBright: [101, 49],
22661
- bgGreenBright: [102, 49],
22662
- bgYellowBright: [103, 49],
22663
- bgBlueBright: [104, 49],
22664
- bgMagentaBright: [105, 49],
22665
- bgCyanBright: [106, 49],
22666
- bgWhiteBright: [107, 49]
22667
- }
22668
- };
22669
- Object.keys(styles$1.modifier);
22670
- const foregroundColorNames = Object.keys(styles$1.color);
22671
- const backgroundColorNames = Object.keys(styles$1.bgColor);
22672
- [...foregroundColorNames, ...backgroundColorNames];
22673
- function assembleStyles() {
22674
- const codes = /* @__PURE__ */ new Map();
22675
- for (const [groupName, group] of Object.entries(styles$1)) {
22676
- for (const [styleName, style] of Object.entries(group)) {
22677
- styles$1[styleName] = {
22678
- open: `\u001B[${style[0]}m`,
22679
- close: `\u001B[${style[1]}m`
22680
- };
22681
- group[styleName] = styles$1[styleName];
22682
- codes.set(style[0], style[1]);
22683
- }
22684
- Object.defineProperty(styles$1, groupName, {
22685
- value: group,
22686
- enumerable: false
22687
- });
22688
- }
22689
- Object.defineProperty(styles$1, "codes", {
22690
- value: codes,
22691
- enumerable: false
22692
- });
22693
- styles$1.color.close = "\x1B[39m";
22694
- styles$1.bgColor.close = "\x1B[49m";
22695
- styles$1.color.ansi = wrapAnsi16();
22696
- styles$1.color.ansi256 = wrapAnsi256();
22697
- styles$1.color.ansi16m = wrapAnsi16m();
22698
- styles$1.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
22699
- styles$1.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
22700
- styles$1.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
22701
- Object.defineProperties(styles$1, {
22702
- rgbToAnsi256: {
22703
- value(red, green, blue) {
22704
- if (red === green && green === blue) {
22705
- if (red < 8) return 16;
22706
- if (red > 248) return 231;
22707
- return Math.round((red - 8) / 247 * 24) + 232;
22708
- }
22709
- return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green / 255 * 5) + Math.round(blue / 255 * 5);
22710
- },
22711
- enumerable: false
22712
- },
22713
- hexToRgb: {
22714
- value(hex) {
22715
- const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16));
22716
- if (!matches) return [
22717
- 0,
22718
- 0,
22719
- 0
22720
- ];
22721
- let [colorString] = matches;
22722
- if (colorString.length === 3) colorString = [...colorString].map((character) => character + character).join("");
22723
- const integer = Number.parseInt(colorString, 16);
22724
- return [
22725
- integer >> 16 & 255,
22726
- integer >> 8 & 255,
22727
- integer & 255
22728
- ];
22729
- },
22730
- enumerable: false
22731
- },
22732
- hexToAnsi256: {
22733
- value: (hex) => styles$1.rgbToAnsi256(...styles$1.hexToRgb(hex)),
22734
- enumerable: false
22735
- },
22736
- ansi256ToAnsi: {
22737
- value(code) {
22738
- if (code < 8) return 30 + code;
22739
- if (code < 16) return 90 + (code - 8);
22740
- let red;
22741
- let green;
22742
- let blue;
22743
- if (code >= 232) {
22744
- red = ((code - 232) * 10 + 8) / 255;
22745
- green = red;
22746
- blue = red;
22747
- } else {
22748
- code -= 16;
22749
- const remainder = code % 36;
22750
- red = Math.floor(code / 36) / 5;
22751
- green = Math.floor(remainder / 6) / 5;
22752
- blue = remainder % 6 / 5;
22753
- }
22754
- const value = Math.max(red, green, blue) * 2;
22755
- if (value === 0) return 30;
22756
- let result = 30 + (Math.round(blue) << 2 | Math.round(green) << 1 | Math.round(red));
22757
- if (value === 2) result += 60;
22758
- return result;
22759
- },
22760
- enumerable: false
22761
- },
22762
- rgbToAnsi: {
22763
- value: (red, green, blue) => styles$1.ansi256ToAnsi(styles$1.rgbToAnsi256(red, green, blue)),
22764
- enumerable: false
22765
- },
22766
- hexToAnsi: {
22767
- value: (hex) => styles$1.ansi256ToAnsi(styles$1.hexToAnsi256(hex)),
22768
- enumerable: false
22769
- }
22770
- });
22771
- return styles$1;
22772
- }
22773
- const ansiStyles = assembleStyles();
22774
- //#endregion
22775
- //#region node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/supports-color/index.js
22776
- function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process$1.argv) {
22777
- const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
22778
- const position = argv.indexOf(prefix + flag);
22779
- const terminatorPosition = argv.indexOf("--");
22780
- return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
22781
- }
22782
- const { env: env$1 } = process$1;
22783
- let flagForceColor;
22784
- if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) flagForceColor = 0;
22785
- else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) flagForceColor = 1;
22786
- function envForceColor() {
22787
- if ("FORCE_COLOR" in env$1) {
22788
- if (env$1.FORCE_COLOR === "true") return 1;
22789
- if (env$1.FORCE_COLOR === "false") return 0;
22790
- return env$1.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env$1.FORCE_COLOR, 10), 3);
22791
- }
22792
- }
22793
- function translateLevel(level) {
22794
- if (level === 0) return false;
22795
- return {
22796
- level,
22797
- hasBasic: true,
22798
- has256: level >= 2,
22799
- has16m: level >= 3
22800
- };
22801
- }
22802
- function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
22803
- const noFlagForceColor = envForceColor();
22804
- if (noFlagForceColor !== void 0) flagForceColor = noFlagForceColor;
22805
- const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
22806
- if (forceColor === 0) return 0;
22807
- if (sniffFlags) {
22808
- if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) return 3;
22809
- if (hasFlag("color=256")) return 2;
22810
- }
22811
- if ("TF_BUILD" in env$1 && "AGENT_NAME" in env$1) return 1;
22812
- if (haveStream && !streamIsTTY && forceColor === void 0) return 0;
22813
- const min = forceColor || 0;
22814
- if (env$1.TERM === "dumb") return min;
22815
- if (process$1.platform === "win32") {
22816
- const osRelease = os.release().split(".");
22817
- if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
22818
- return 1;
22819
- }
22820
- if ("CI" in env$1) {
22821
- if ([
22822
- "GITHUB_ACTIONS",
22823
- "GITEA_ACTIONS",
22824
- "CIRCLECI"
22825
- ].some((key) => key in env$1)) return 3;
22826
- if ([
22827
- "TRAVIS",
22828
- "APPVEYOR",
22829
- "GITLAB_CI",
22830
- "BUILDKITE",
22831
- "DRONE"
22832
- ].some((sign) => sign in env$1) || env$1.CI_NAME === "codeship") return 1;
22833
- return min;
22834
- }
22835
- if ("TEAMCITY_VERSION" in env$1) return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env$1.TEAMCITY_VERSION) ? 1 : 0;
22836
- if (env$1.COLORTERM === "truecolor") return 3;
22837
- if (env$1.TERM === "xterm-kitty") return 3;
22838
- if (env$1.TERM === "xterm-ghostty") return 3;
22839
- if (env$1.TERM === "wezterm") return 3;
22840
- if ("TERM_PROGRAM" in env$1) {
22841
- const version = Number.parseInt((env$1.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
22842
- switch (env$1.TERM_PROGRAM) {
22843
- case "iTerm.app": return version >= 3 ? 3 : 2;
22844
- case "Apple_Terminal": return 2;
22845
- }
22846
- }
22847
- if (/-256(color)?$/i.test(env$1.TERM)) return 2;
22848
- if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env$1.TERM)) return 1;
22849
- if ("COLORTERM" in env$1) return 1;
22850
- return min;
22851
- }
22852
- function createSupportsColor(stream, options = {}) {
22853
- return translateLevel(_supportsColor(stream, {
22854
- streamIsTTY: stream && stream.isTTY,
22855
- ...options
22856
- }));
22857
- }
22858
- const supportsColor = {
22859
- stdout: createSupportsColor({ isTTY: tty.isatty(1) }),
22860
- stderr: createSupportsColor({ isTTY: tty.isatty(2) })
22861
- };
22862
- //#endregion
22863
- //#region node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/utilities.js
22864
- function stringReplaceAll(string, substring, replacer) {
22865
- let index = string.indexOf(substring);
22866
- if (index === -1) return string;
22867
- const substringLength = substring.length;
22868
- let endIndex = 0;
22869
- let returnValue = "";
22870
- do {
22871
- returnValue += string.slice(endIndex, index) + substring + replacer;
22872
- endIndex = index + substringLength;
22873
- index = string.indexOf(substring, endIndex);
22874
- } while (index !== -1);
22875
- returnValue += string.slice(endIndex);
22876
- return returnValue;
22877
- }
22878
- function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
22879
- let endIndex = 0;
22880
- let returnValue = "";
22881
- do {
22882
- const gotCR = string[index - 1] === "\r";
22883
- returnValue += string.slice(endIndex, gotCR ? index - 1 : index) + prefix + (gotCR ? "\r\n" : "\n") + postfix;
22884
- endIndex = index + 1;
22885
- index = string.indexOf("\n", endIndex);
22886
- } while (index !== -1);
22887
- returnValue += string.slice(endIndex);
22888
- return returnValue;
22889
- }
22890
- //#endregion
22891
- //#region node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/index.js
22892
- const { stdout: stdoutColor, stderr: stderrColor } = supportsColor;
22893
- const GENERATOR = Symbol("GENERATOR");
22894
- const STYLER = Symbol("STYLER");
22895
- const IS_EMPTY = Symbol("IS_EMPTY");
22896
- const levelMapping = [
22897
- "ansi",
22898
- "ansi",
22899
- "ansi256",
22900
- "ansi16m"
22901
- ];
22902
- const styles = Object.create(null);
22903
- const applyOptions = (object, options = {}) => {
22904
- if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) throw new Error("The `level` option should be an integer from 0 to 3");
22905
- const colorLevel = stdoutColor ? stdoutColor.level : 0;
22906
- object.level = options.level === void 0 ? colorLevel : options.level;
22907
- };
22908
- const chalkFactory = (options) => {
22909
- const chalk = (...strings) => strings.join(" ");
22910
- applyOptions(chalk, options);
22911
- Object.setPrototypeOf(chalk, createChalk.prototype);
22912
- return chalk;
22913
- };
22914
- function createChalk(options) {
22915
- return chalkFactory(options);
22916
- }
22917
- Object.setPrototypeOf(createChalk.prototype, Function.prototype);
22918
- for (const [styleName, style] of Object.entries(ansiStyles)) styles[styleName] = { get() {
22919
- const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
22920
- Object.defineProperty(this, styleName, { value: builder });
22921
- return builder;
22922
- } };
22923
- styles.visible = { get() {
22924
- const builder = createBuilder(this, this[STYLER], true);
22925
- Object.defineProperty(this, "visible", { value: builder });
22926
- return builder;
22927
- } };
22928
- const getModelAnsi = (model, level, type, ...arguments_) => {
22929
- if (model === "rgb") {
22930
- if (level === "ansi16m") return ansiStyles[type].ansi16m(...arguments_);
22931
- if (level === "ansi256") return ansiStyles[type].ansi256(ansiStyles.rgbToAnsi256(...arguments_));
22932
- return ansiStyles[type].ansi(ansiStyles.rgbToAnsi(...arguments_));
22933
- }
22934
- if (model === "hex") return getModelAnsi("rgb", level, type, ...ansiStyles.hexToRgb(...arguments_));
22935
- return ansiStyles[type][model](...arguments_);
22936
- };
22937
- for (const model of [
22938
- "rgb",
22939
- "hex",
22940
- "ansi256"
22941
- ]) {
22942
- styles[model] = { get() {
22943
- const { level } = this;
22944
- return function(...arguments_) {
22945
- const styler = createStyler(getModelAnsi(model, levelMapping[level], "color", ...arguments_), ansiStyles.color.close, this[STYLER]);
22946
- return createBuilder(this, styler, this[IS_EMPTY]);
22947
- };
22948
- } };
22949
- const bgModel = "bg" + model[0].toUpperCase() + model.slice(1);
22950
- styles[bgModel] = { get() {
22951
- const { level } = this;
22952
- return function(...arguments_) {
22953
- const styler = createStyler(getModelAnsi(model, levelMapping[level], "bgColor", ...arguments_), ansiStyles.bgColor.close, this[STYLER]);
22954
- return createBuilder(this, styler, this[IS_EMPTY]);
22955
- };
22956
- } };
22957
- }
22958
- const proto = Object.defineProperties(() => {}, {
22959
- ...styles,
22960
- level: {
22961
- enumerable: true,
22962
- get() {
22963
- return this[GENERATOR].level;
22964
- },
22965
- set(level) {
22966
- this[GENERATOR].level = level;
22967
- }
22968
- }
22969
- });
22970
- const createStyler = (open, close, parent) => {
22971
- let openAll;
22972
- let closeAll;
22973
- if (parent === void 0) {
22974
- openAll = open;
22975
- closeAll = close;
22976
- } else {
22977
- openAll = parent.openAll + open;
22978
- closeAll = close + parent.closeAll;
22979
- }
22980
- return {
22981
- open,
22982
- close,
22983
- openAll,
22984
- closeAll,
22985
- parent
22986
- };
22987
- };
22988
- const createBuilder = (self, _styler, _isEmpty) => {
22989
- const builder = (...arguments_) => applyStyle(builder, arguments_.length === 1 ? "" + arguments_[0] : arguments_.join(" "));
22990
- Object.setPrototypeOf(builder, proto);
22991
- builder[GENERATOR] = self;
22992
- builder[STYLER] = _styler;
22993
- builder[IS_EMPTY] = _isEmpty;
22994
- return builder;
22995
- };
22996
- const applyStyle = (self, string) => {
22997
- if (self.level <= 0 || !string) return self[IS_EMPTY] ? "" : string;
22998
- let styler = self[STYLER];
22999
- if (styler === void 0) return string;
23000
- const { openAll, closeAll } = styler;
23001
- if (string.includes("\x1B")) while (styler !== void 0) {
23002
- string = stringReplaceAll(string, styler.close, styler.open);
23003
- styler = styler.parent;
23004
- }
23005
- const lfIndex = string.indexOf("\n");
23006
- if (lfIndex !== -1) string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
23007
- return openAll + string + closeAll;
23008
- };
23009
- Object.defineProperties(createChalk.prototype, styles);
23010
- const chalk = createChalk();
23011
- createChalk({ level: stderrColor ? stderrColor.level : 0 });
23012
- //#endregion
23013
- //#region node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/colorize.js
23014
22612
  const rgbRegex = /^rgb\(\s?(\d+),\s?(\d+),\s?(\d+)\s?\)$/;
23015
22613
  const ansiRegex = /^ansi256\(\s?(\d+)\s?\)$/;
23016
22614
  const isNamedColor = (color) => {
@@ -23157,18 +22755,18 @@ const CODE_POINT_9 = "9".codePointAt(0);
23157
22755
  const MAX_ANSI_SEQUENCE_LENGTH = 19;
23158
22756
  const endCodesSet$1 = /* @__PURE__ */ new Set();
23159
22757
  const endCodesMap$1 = /* @__PURE__ */ new Map();
23160
- for (const [start, end] of ansiStyles$1.codes) {
23161
- endCodesSet$1.add(ansiStyles$1.color.ansi(end));
23162
- endCodesMap$1.set(ansiStyles$1.color.ansi(start), ansiStyles$1.color.ansi(end));
22758
+ for (const [start, end] of ansiStyles.codes) {
22759
+ endCodesSet$1.add(ansiStyles.color.ansi(end));
22760
+ endCodesMap$1.set(ansiStyles.color.ansi(start), ansiStyles.color.ansi(end));
23163
22761
  }
23164
22762
  function getEndCode$1(code) {
23165
22763
  if (endCodesSet$1.has(code)) return code;
23166
22764
  if (endCodesMap$1.has(code)) return endCodesMap$1.get(code);
23167
22765
  code = code.slice(2);
23168
22766
  if (code.includes(";")) code = code[0] + "0";
23169
- const returnValue = ansiStyles$1.codes.get(Number.parseInt(code, 10));
23170
- if (returnValue) return ansiStyles$1.color.ansi(returnValue);
23171
- return ansiStyles$1.reset.open;
22767
+ const returnValue = ansiStyles.codes.get(Number.parseInt(code, 10));
22768
+ if (returnValue) return ansiStyles.color.ansi(returnValue);
22769
+ return ansiStyles.reset.open;
23172
22770
  }
23173
22771
  function findNumberIndex$1(string) {
23174
22772
  for (let index = 0; index < string.length; index++) {
@@ -23219,7 +22817,7 @@ function tokenize$1(string, endCharacter = Number.POSITIVE_INFINITY) {
23219
22817
  }
23220
22818
  function reduceAnsiCodes$1(codes) {
23221
22819
  let returnValue = [];
23222
- for (const code of codes) if (code.code === ansiStyles$1.reset.open) returnValue = [];
22820
+ for (const code of codes) if (code.code === ansiStyles.reset.open) returnValue = [];
23223
22821
  else if (endCodesSet$1.has(code.code)) returnValue = returnValue.filter((returnValueCode) => returnValueCode.endCode !== code.code);
23224
22822
  else {
23225
22823
  returnValue = returnValue.filter((returnValueCode) => returnValueCode.endCode !== code.endCode);
@@ -23259,9 +22857,9 @@ function sliceAnsi(string, start, end) {
23259
22857
  const ESCAPES = /* @__PURE__ */ new Set([27, 155]);
23260
22858
  const endCodesSet = /* @__PURE__ */ new Set();
23261
22859
  const endCodesMap = /* @__PURE__ */ new Map();
23262
- for (const [start, end] of ansiStyles$1.codes) {
23263
- endCodesSet.add(ansiStyles$1.color.ansi(end));
23264
- endCodesMap.set(ansiStyles$1.color.ansi(start), ansiStyles$1.color.ansi(end));
22860
+ for (const [start, end] of ansiStyles.codes) {
22861
+ endCodesSet.add(ansiStyles.color.ansi(end));
22862
+ endCodesMap.set(ansiStyles.color.ansi(start), ansiStyles.color.ansi(end));
23265
22863
  }
23266
22864
  const linkStartCodePrefixCharCodes = "\x1B]8;;".split("").map((char) => char.charCodeAt(0));
23267
22865
  "\x07".charCodeAt(0);
@@ -23272,9 +22870,9 @@ function getEndCode(code) {
23272
22870
  if (code.startsWith("\x1B]8;;")) return linkEndCode;
23273
22871
  code = code.slice(2);
23274
22872
  if (code.includes(";")) code = code[0] + "0";
23275
- const ret = ansiStyles$1.codes.get(parseInt(code, 10));
23276
- if (ret) return ansiStyles$1.color.ansi(ret);
23277
- else return ansiStyles$1.reset.open;
22873
+ const ret = ansiStyles.codes.get(parseInt(code, 10));
22874
+ if (ret) return ansiStyles.color.ansi(ret);
22875
+ else return ansiStyles.reset.open;
23278
22876
  }
23279
22877
  function ansiCodesToString(codes) {
23280
22878
  return codes.map((code) => code.code).join("");
@@ -23288,7 +22886,7 @@ function reduceAnsiCodes(codes) {
23288
22886
  /** Like {@link reduceAnsiCodes}, but assumes that `codes` is already reduced. Further reductions are only done for the items in `newCodes`. */
23289
22887
  function reduceAnsiCodesIncremental(codes, newCodes) {
23290
22888
  let ret = [...codes];
23291
- for (const code of newCodes) if (code.code === ansiStyles$1.reset.open) ret = [];
22889
+ for (const code of newCodes) if (code.code === ansiStyles.reset.open) ret = [];
23292
22890
  else if (endCodesSet.has(code.code)) ret = ret.filter((retCode) => retCode.endCode !== code.code);
23293
22891
  else {
23294
22892
  ret = ret.filter((retCode) => retCode.endCode !== code.endCode);
@@ -24692,87 +24290,253 @@ const useInput = (inputHandler, options = {}) => {
24692
24290
  */
24693
24291
  const useStdout = () => (0, import_react.useContext)(StdoutContext);
24694
24292
  //#endregion
24695
- //#region src/theme.ts
24696
- /**
24697
- * Terminal color tokens for the dsh TUI, mapped from the product design
24698
- * platform's DeepSeek palette
24699
- * (`packages/client/ui-theme/src/styles/design-platform.css`). Truecolor RGB
24700
- * rides chalk, which degrades automatically on terminals without truecolor.
24701
- *
24702
- * @module @deepseek-ai/dsh-tui/theme
24703
- */
24293
+ //#region src/render/inspector.ts
24294
+ /** Composer (3) + two-row status chrome, plus one optional fixed-height local notice row. */
24295
+ const INSPECTOR_CHROME_ROWS = 6;
24296
+ /** One transcript-to-composer gutter, collapsed on short terminals. */
24297
+ function layoutGutterRows(rows) {
24298
+ return Math.max(1, Math.floor(rows)) >= 14 ? 1 : 0;
24299
+ }
24704
24300
  /**
24705
- * RGB triples for the TUI, one entry per design-platform token in use.
24706
- * Keep names and values in sync with the CSS custom properties cited inline.
24301
+ * Keep the inspector plus its persistent status/composer chrome below
24302
+ * `stdout.rows`: at equality Ink clears the terminal and rewrites all
24303
+ * accumulated `<Static>` output on every frame.
24707
24304
  */
24708
- const TUI_RGB = {
24709
- /** Primary brand blue — `--dsw-static-deepseek-500`. */
24710
- brand: [
24711
- 65,
24712
- 118,
24713
- 230
24714
- ],
24715
- /** Brighter brand blue for live/streaming emphasis — `--dsw-static-deepseek-400`. */
24716
- brandBright: [
24717
- 103,
24718
- 158,
24719
- 254
24720
- ],
24721
- /** Deep brand blue for secondary chrome — `--dsw-static-deepseek-600`. */
24722
- brandDeep: [
24723
- 72,
24724
- 104,
24725
- 178
24726
- ],
24727
- /** Muted caption gray — `--dsw-static-neutral-bluish-600`. */
24728
- dim: [
24729
- 129,
24730
- 133,
24731
- 140
24732
- ],
24733
- /** Success green `--dsw-static-green-500`. */
24734
- success: [
24735
- 34,
24736
- 197,
24737
- 94
24738
- ],
24739
- /** Error red `--dsw-static-red-500`. */
24740
- error: [
24741
- 239,
24742
- 68,
24743
- 68
24744
- ],
24745
- /** Warning amber `--dsw-static-amber-500`. */
24746
- warn: [
24747
- 245,
24748
- 158,
24749
- 11
24750
- ],
24751
- /** Default foreground text `--dsw-static-neutral-50`. */
24752
- text: [
24753
- 236,
24754
- 240,
24755
- 246
24756
- ],
24757
- /** Inline/fenced code — soft sky blue, distinct from brand accents. */
24758
- code: [
24759
- 125,
24760
- 211,
24761
- 252
24762
- ]
24763
- };
24764
- /** Paint with the primary brand blue: whale, wordmark, tool names, accents. */
24765
- function brand(text) {
24766
- return chalk.rgb(...TUI_RGB.brand)(text);
24767
- }
24768
- /** Paint muted captions, hints, and meta lines. */
24769
- function dim(text) {
24770
- return chalk.rgb(...TUI_RGB.dim)(text);
24771
- }
24772
- /** Paint failures and error entries. */
24773
- function error(text) {
24774
- return chalk.rgb(...TUI_RGB.error)(text);
24775
- }
24305
+ function panelViewport(columns, rows) {
24306
+ const safeColumns = Math.max(1, Math.floor(columns));
24307
+ const safeRows = Math.max(1, Math.floor(rows));
24308
+ const maxHeight = Math.max(0, Math.min(safeRows - 2 - INSPECTOR_CHROME_ROWS - layoutGutterRows(safeRows), Math.floor(safeRows / 2)));
24309
+ const compact = maxHeight < 5 || safeColumns < 8;
24310
+ const gapRows = !compact && maxHeight >= 7 ? 2 : 0;
24311
+ const outerColumns = compact ? safeColumns : Math.max(1, safeColumns - 1);
24312
+ return {
24313
+ maxHeight,
24314
+ bodyRows: compact ? 0 : maxHeight - 4 - gapRows,
24315
+ gapRows,
24316
+ contentColumns: compact ? Math.max(1, safeColumns - 1) : Math.max(1, outerColumns - 4),
24317
+ outerColumns,
24318
+ compact
24319
+ };
24320
+ }
24321
+ /** Backward-compatible name for the Ctrl+O-specific caller and tests. */
24322
+ function inspectorViewport(columns, rows) {
24323
+ return panelViewport(columns, rows);
24324
+ }
24325
+ /** Clamp a first-visible row to the range representable by one viewport. */
24326
+ function clampScroll(offset, totalRows, visibleRows) {
24327
+ const last = Math.max(0, Math.max(0, Math.floor(totalRows)) - Math.max(0, Math.floor(visibleRows)));
24328
+ return Math.max(0, Math.min(Math.floor(offset), last));
24329
+ }
24330
+ /** Move a viewport by a signed row delta without escaping its content. */
24331
+ function moveScroll(offset, delta, totalRows, visibleRows) {
24332
+ return clampScroll(offset + delta, totalRows, visibleRows);
24333
+ }
24334
+ /** Keep one focused row visible while preserving the current window when possible. */
24335
+ function revealRow(offset, row, totalRows, visibleRows) {
24336
+ const size = Math.max(1, Math.floor(visibleRows));
24337
+ const target = Math.max(0, Math.min(Math.floor(row), Math.max(0, totalRows - 1)));
24338
+ if (target < offset) return clampScroll(target, totalRows, size);
24339
+ if (target >= offset + size) return clampScroll(target - size + 1, totalRows, size);
24340
+ return clampScroll(offset, totalRows, size);
24341
+ }
24342
+ /** Center a selected list row where possible, clamped at both ends. */
24343
+ function selectionWindow(cursor, totalRows, visibleRows) {
24344
+ return clampScroll(cursor - Math.floor(Math.max(1, visibleRows) / 2), totalRows, visibleRows);
24345
+ }
24346
+ /** Follow appended history only while the inspector cursor was at the tail. */
24347
+ function followInspectorCursor(cursor, previousLength, nextLength) {
24348
+ const nextLast = Math.max(0, nextLength - 1);
24349
+ if (cursor >= Math.max(0, previousLength - 1)) return nextLast;
24350
+ return Math.min(cursor, nextLast);
24351
+ }
24352
+ //#endregion
24353
+ //#region src/render/text.ts
24354
+ /**
24355
+ * Display-boundary sanitization for externally sourced text (model output,
24356
+ * tool payloads, skill descriptions). Control characters — including ANSI
24357
+ * CSI/OSC escape sequences — would otherwise pass through Ink into the
24358
+ * terminal, letting output rewrite the screen or inject prompts. Newlines
24359
+ * and tabs survive; everything else in C0/C1 plus DEL becomes a visible
24360
+ * `\xNN` escape.
24361
+ *
24362
+ * @module @deepseek-ai/dsh-code/render/text
24363
+ */
24364
+ /** C0 controls except tab (0x09) and newline (0x0a), plus DEL and C1. */
24365
+ const CONTROL_ESCAPE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/gu;
24366
+ /**
24367
+ * Escape control characters so externally sourced text cannot drive the
24368
+ * terminal.
24369
+ * @param text - raw text from a session event, tool payload, or catalog.
24370
+ * @returns text with every control character (except `\n`, `\t`) rendered
24371
+ * as a literal `\xNN` escape.
24372
+ */
24373
+ function displayText(text) {
24374
+ return text.replace(CONTROL_ESCAPE, (char) => `\\x${char.charCodeAt(0).toString(16).padStart(2, "0")}`);
24375
+ }
24376
+ /** Collapse external text to one terminal-safe logical row. */
24377
+ function singleLineText(text) {
24378
+ return displayText(text).replace(/\r?\n/gu, " ↵ ").replace(/\t/gu, " ");
24379
+ }
24380
+ /** Terminal-cell width matching the TUI's existing CJK-aware wrapping rule. */
24381
+ function cellWidth(text) {
24382
+ let columns = 0;
24383
+ for (const char of text) columns += (char.codePointAt(0) ?? 0) > 11903 ? 2 : 1;
24384
+ return columns;
24385
+ }
24386
+ /**
24387
+ * Truncate one display-safe row without ever exceeding its physical-column
24388
+ * budget. The ellipsis is included inside the budget, matching Codex's popup
24389
+ * truncation contract; the previous app-local helper appended it after the
24390
+ * row was already full and could force an extra terminal wrap.
24391
+ */
24392
+ function truncateColumns(text, columns) {
24393
+ const limit = Math.max(0, Math.floor(columns));
24394
+ if (limit === 0) return "";
24395
+ if (cellWidth(text) <= limit) return text;
24396
+ const contentLimit = limit - 1;
24397
+ let used = 0;
24398
+ let result = "";
24399
+ for (const char of text) {
24400
+ const width = cellWidth(char);
24401
+ if (used + width > contentLimit) break;
24402
+ result += char;
24403
+ used += width;
24404
+ }
24405
+ return `${result}…`;
24406
+ }
24407
+ /** Read one Unicode character immediately before `end`. */
24408
+ function previousCharacter(text, end) {
24409
+ const last = text.charCodeAt(end - 1);
24410
+ if (last >= 56320 && last <= 57343 && end >= 2) {
24411
+ const first = text.charCodeAt(end - 2);
24412
+ if (first >= 55296 && first <= 56319) return {
24413
+ char: text.slice(end - 2, end),
24414
+ start: end - 2
24415
+ };
24416
+ }
24417
+ return {
24418
+ char: text.slice(end - 1, end),
24419
+ start: end - 1
24420
+ };
24421
+ }
24422
+ /**
24423
+ * Keep only the newest display-safe text that fits a terminal rectangle.
24424
+ * The scan walks backward and stops as soon as the suffix is full, so a long
24425
+ * reasoning stream does not rescan its entire accumulated prefix per chunk.
24426
+ * Explicit newlines and terminal wrapping both consume rows.
24427
+ * @param text - raw externally sourced text.
24428
+ * @param columns - available terminal columns.
24429
+ * @param rows - available terminal rows.
24430
+ * @returns a sanitized bounded suffix and whether an earlier prefix was cut.
24431
+ */
24432
+ function displayTail(text, columns, rows) {
24433
+ const columnLimit = Math.max(1, Math.floor(columns));
24434
+ const rowLimit = Math.max(1, Math.floor(rows));
24435
+ const reversed = [];
24436
+ let row = 1;
24437
+ let used = 0;
24438
+ let end = text.length;
24439
+ while (end > 0) {
24440
+ const previous = previousCharacter(text, end);
24441
+ if (previous.char === "\n") {
24442
+ if (row >= rowLimit) break;
24443
+ reversed.push("\n");
24444
+ row += 1;
24445
+ used = 0;
24446
+ end = previous.start;
24447
+ continue;
24448
+ }
24449
+ const safe = displayText(previous.char);
24450
+ const width = cellWidth(safe);
24451
+ if (used > 0 && used + width > columnLimit) {
24452
+ if (row >= rowLimit) break;
24453
+ reversed.push("\n");
24454
+ row += 1;
24455
+ used = 0;
24456
+ }
24457
+ const extraRows = Math.floor(Math.max(0, width - 1) / columnLimit);
24458
+ if (row + extraRows > rowLimit) break;
24459
+ row += extraRows;
24460
+ reversed.push(safe);
24461
+ used = extraRows === 0 ? used + width : width - extraRows * columnLimit;
24462
+ end = previous.start;
24463
+ }
24464
+ return {
24465
+ text: reversed.reverse().join(""),
24466
+ truncated: end > 0
24467
+ };
24468
+ }
24469
+ //#endregion
24470
+ //#region src/theme-panel.ts
24471
+ /**
24472
+ * The `/theme` picker (the Codex `/theme` contract): one bounded list over
24473
+ * the three color themes — dark, light, and auto (terminal-sensed; auto
24474
+ * falls back to dark until OSC-11 detection lands). Enter applies the row
24475
+ * and the runner persists it; Esc closes without changing the theme.
24476
+ *
24477
+ * @module @deepseek-ai/dsh-tui/theme-panel
24478
+ */
24479
+ /** The three theme rows in canonical order (the /theme selection surface). */
24480
+ const THEME_ROWS = [
24481
+ {
24482
+ id: "dark",
24483
+ label: "dark",
24484
+ description: "DeepSeek dark palette (default)"
24485
+ },
24486
+ {
24487
+ id: "light",
24488
+ label: "light",
24489
+ description: "light palette for bright terminals"
24490
+ },
24491
+ {
24492
+ id: "auto",
24493
+ label: "auto",
24494
+ description: "follow the terminal; dark until detection lands"
24495
+ }
24496
+ ];
24497
+ /**
24498
+ * The /theme list: one row per theme, the current one marked with ●, the
24499
+ * focused one with ›. Enter applies the focused theme (the runner persists
24500
+ * it), Esc/q closes without changing anything. Colors read the ACTIVE
24501
+ * palette, so the panel itself adapts to a light theme once applied.
24502
+ */
24503
+ function ThemePanel({ current, select, close }) {
24504
+ const [cursor, setCursor] = (0, import_react.useState)(() => {
24505
+ const index = THEME_ROWS.findIndex((theme) => theme.id === current);
24506
+ return index < 0 ? 0 : index;
24507
+ });
24508
+ const stdout = useStdout().stdout;
24509
+ const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
24510
+ useInput((input, key) => {
24511
+ if (key.escape || input === "q") return close();
24512
+ if (key.upArrow) return setCursor((value) => (value + THEME_ROWS.length - 1) % THEME_ROWS.length);
24513
+ if (key.downArrow) return setCursor((value) => (value + 1) % THEME_ROWS.length);
24514
+ if (key.return) return select(THEME_ROWS[cursor].id);
24515
+ });
24516
+ if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
24517
+ if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("/theme · esc close", viewport.contentColumns));
24518
+ return (0, import_react.createElement)(Box, {
24519
+ width: viewport.outerColumns,
24520
+ borderStyle: "round",
24521
+ borderColor: inkColor(getPalette().dim),
24522
+ flexDirection: "column",
24523
+ paddingX: 1
24524
+ }, (0, import_react.createElement)(Text, {
24525
+ color: inkColor(getPalette().brandBright),
24526
+ wrap: "truncate-end"
24527
+ }, truncateColumns("/theme — color palette", viewport.contentColumns)), ...THEME_ROWS.map((theme, index) => {
24528
+ const selected = index === cursor;
24529
+ const active = theme.id === current;
24530
+ return (0, import_react.createElement)(Text, {
24531
+ key: theme.id,
24532
+ color: selected ? inkColor(getPalette().brandBright) : void 0,
24533
+ wrap: "truncate-end"
24534
+ }, truncateColumns(`${selected ? "› " : " "}${active ? "● " : "○ "}${theme.label}${active ? " · current" : ""} · ${theme.description}`, viewport.contentColumns));
24535
+ }), (0, import_react.createElement)(Text, {
24536
+ dimColor: true,
24537
+ wrap: "truncate-end"
24538
+ }, truncateColumns("↑↓ choose · enter apply · esc/q close", viewport.contentColumns)));
24539
+ }
24776
24540
  //#endregion
24777
24541
  //#region src/whale-glyph.ts
24778
24542
  /** Half-block whale glyph rows; render with the brand color. */
@@ -24999,6 +24763,21 @@ function textOf(content) {
24999
24763
  function reasoningOf(content) {
25000
24764
  return content.filter((block) => block.type === "reasoning").map((block) => block.text).join("");
25001
24765
  }
24766
+ /**
24767
+ * Rough token estimate for the segmented context bar (pi-nano-context's ~4
24768
+ * chars/token heuristic, CJK-aware so a Chinese prompt is not quartered):
24769
+ * CJK/wide chars cost ~1 token each, ASCII ~4 chars per token. Estimates
24770
+ * drive bar PROPORTIONS, never billing, so precision is not required.
24771
+ * @param text - the text to estimate.
24772
+ * @returns an integer token estimate, 0 for empty text.
24773
+ */
24774
+ function estimateTokens(text) {
24775
+ let wide = 0;
24776
+ let narrow = 0;
24777
+ for (const char of text) if ((char.codePointAt(0) ?? 0) > 11903) wide += 1;
24778
+ else narrow += 1;
24779
+ return wide + Math.ceil(narrow / 4);
24780
+ }
25002
24781
  /** A fresh, empty transcript view. */
25003
24782
  function createTranscriptView() {
25004
24783
  return {
@@ -25030,10 +24809,18 @@ function createTranscriptView() {
25030
24809
  },
25031
24810
  lastPromptTokens: 0,
25032
24811
  contextWindow: 0,
24812
+ contextSegments: {
24813
+ system: 0,
24814
+ prompt: 0,
24815
+ assistant: 0,
24816
+ thinking: 0,
24817
+ tools: 0
24818
+ },
25033
24819
  ttftMs: 0,
25034
24820
  ttftSteps: 0,
25035
24821
  decodeMs: 0,
25036
- decodeTokens: 0
24822
+ decodeTokens: 0,
24823
+ reasoningEffort: ""
25037
24824
  },
25038
24825
  anchors: {
25039
24826
  stepStart: /* @__PURE__ */ new Map(),
@@ -25070,24 +24857,40 @@ function projectEvent(view, event) {
25070
24857
  };
25071
24858
  entries = entries.filter((entry) => !(entry.kind === "pending" && entry.messageId === message.id));
25072
24859
  }
24860
+ const text = textOf(message.content);
25073
24861
  if (message.source.kind === "user") return {
25074
24862
  ...view,
25075
24863
  pending,
25076
24864
  entries: [...entries, {
25077
24865
  kind: "user",
25078
- text: textOf(message.content),
24866
+ text,
25079
24867
  notice: false
25080
- }]
24868
+ }],
24869
+ stats: {
24870
+ ...view.stats,
24871
+ contextSegments: {
24872
+ ...view.stats.contextSegments,
24873
+ prompt: view.stats.contextSegments.prompt + estimateTokens(text)
24874
+ }
24875
+ }
25081
24876
  };
25082
24877
  const notice = message.source.kind === "plugin" && message.source.form === "notice" ? message.source.summary : message.source.kind;
24878
+ const summary = boundContextSummary(notice);
25083
24879
  return {
25084
24880
  ...view,
25085
24881
  pending,
25086
24882
  entries: [...entries, {
25087
24883
  kind: "user",
25088
- text: boundContextSummary(notice),
24884
+ text: summary,
25089
24885
  notice: true
25090
- }]
24886
+ }],
24887
+ stats: {
24888
+ ...view.stats,
24889
+ contextSegments: {
24890
+ ...view.stats.contextSegments,
24891
+ system: view.stats.contextSegments.system + estimateTokens(summary)
24892
+ }
24893
+ }
25091
24894
  };
25092
24895
  }
25093
24896
  case "agent/inbox/spliced": {
@@ -25153,14 +24956,16 @@ function projectEvent(view, event) {
25153
24956
  view.anchors.firstChunkAt.delete(key);
25154
24957
  const usage = event.data.usage;
25155
24958
  const totals = view.stats.usage;
24959
+ const text = textOf(event.data.message.content);
24960
+ const reasoning = reasoningOf(event.data.message.content);
25156
24961
  return {
25157
24962
  ...view,
25158
24963
  streaming: "",
25159
24964
  streamingReasoning: "",
25160
24965
  entries: [...view.entries, {
25161
24966
  kind: "assistant",
25162
- text: textOf(event.data.message.content),
25163
- reasoning: reasoningOf(event.data.message.content)
24967
+ text,
24968
+ reasoning
25164
24969
  }],
25165
24970
  stats: {
25166
24971
  ...view.stats,
@@ -25172,7 +24977,12 @@ function projectEvent(view, event) {
25172
24977
  },
25173
24978
  lastPromptTokens: usage === void 0 ? view.stats.lastPromptTokens : usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0),
25174
24979
  decodeMs: view.stats.decodeMs + (firstChunk === void 0 ? 0 : Math.max(0, event.time - firstChunk)),
25175
- decodeTokens: view.stats.decodeTokens + (firstChunk === void 0 || usage === void 0 ? 0 : usage.outputTokens)
24980
+ decodeTokens: view.stats.decodeTokens + (firstChunk === void 0 || usage === void 0 ? 0 : usage.outputTokens),
24981
+ contextSegments: {
24982
+ ...view.stats.contextSegments,
24983
+ thinking: view.stats.contextSegments.thinking + estimateTokens(reasoning),
24984
+ assistant: view.stats.contextSegments.assistant + estimateTokens(text)
24985
+ }
25176
24986
  }
25177
24987
  };
25178
24988
  }
@@ -25190,7 +25000,14 @@ function projectEvent(view, event) {
25190
25000
  state: "running",
25191
25001
  summary: "",
25192
25002
  detail: void 0
25193
- }]
25003
+ }],
25004
+ stats: {
25005
+ ...view.stats,
25006
+ contextSegments: {
25007
+ ...view.stats.contextSegments,
25008
+ tools: view.stats.contextSegments.tools + (typeof data.arguments === "string" ? estimateTokens(data.arguments) : 0)
25009
+ }
25010
+ }
25194
25011
  };
25195
25012
  }
25196
25013
  case "tool/result": {
@@ -25219,7 +25036,11 @@ function projectEvent(view, event) {
25219
25036
  entries,
25220
25037
  stats: {
25221
25038
  ...view.stats,
25222
- toolMs: view.stats.toolMs + (started === void 0 ? 0 : Math.max(0, event.time - started))
25039
+ toolMs: view.stats.toolMs + (started === void 0 ? 0 : Math.max(0, event.time - started)),
25040
+ contextSegments: {
25041
+ ...view.stats.contextSegments,
25042
+ tools: view.stats.contextSegments.tools + estimateTokens(rawText)
25043
+ }
25223
25044
  }
25224
25045
  };
25225
25046
  }
@@ -25378,7 +25199,15 @@ function projectEvent(view, event) {
25378
25199
  const config = event.data.header.config;
25379
25200
  return {
25380
25201
  ...view,
25381
- model: `${config.provider}/${config.model}`
25202
+ model: `${config.provider}/${config.model}`,
25203
+ stats: {
25204
+ ...view.stats,
25205
+ reasoningEffort: config.reasoningEffort === void 0 ? "" : String(config.reasoningEffort),
25206
+ contextSegments: {
25207
+ ...view.stats.contextSegments,
25208
+ system: estimateTokens(event.data.header.system ?? "")
25209
+ }
25210
+ }
25382
25211
  };
25383
25212
  }
25384
25213
  case "plan/mode": return {
@@ -25430,19 +25259,23 @@ function projectEvents(events) {
25430
25259
  return events.reduce(projectEvent, createTranscriptView());
25431
25260
  }
25432
25261
  /**
25433
- * How many leading transcript entries can never change again: only a
25434
- * `running` tool or retry can still mutate in place everything before the
25435
- * first one (including a completed tail: later events only APPEND new rows)
25436
- * is final. The renderer currently draws the whole transcript dynamically
25437
- * (a `<Static>` flush proved unstable with CJK wrapping on real terminals);
25438
- * this boundary stays as the append-only contract for when flushing is
25439
- * reintroduced.
25262
+ * The append-only flush boundary for a transcript view: the count of entries
25263
+ * no later event can remove. Entries at or beyond this index are mutable and
25264
+ * must stay in the live tree.
25265
+ *
25266
+ * `pending` rows are excluded even though they are not a running tool/retry:
25267
+ * the inbox claims or cancels them durably (`agent/inbox/spliced` removals,
25268
+ * `user/message` retirement), and an append-only `<Static>` flush cannot
25269
+ * erase a row that vanishes from the view — the retired row would ghost on
25270
+ * screen until the next source-backed replay. Everything else (including a
25271
+ * completed tail) is final: later events only APPEND new rows.
25440
25272
  * @param entries - the view's transcript entries in order.
25441
25273
  * @returns the count of entries safe to flush (0 for an empty transcript).
25442
25274
  */
25443
25275
  function settledEntryCount(entries) {
25444
25276
  for (let index = 0; index < entries.length; index++) {
25445
25277
  const entry = entries[index];
25278
+ if (entry.kind === "pending") return index;
25446
25279
  if (entry.kind === "tool" && entry.state === "running") return index;
25447
25280
  if (entry.kind === "retry" && entry.state === "running") return index;
25448
25281
  }
@@ -25681,16 +25514,6 @@ function renderMarkdown(text, width) {
25681
25514
  }
25682
25515
  //#endregion
25683
25516
  //#region src/render/animations.ts
25684
- /**
25685
- * Terminal animation frame tables derived from the web design language:
25686
- * the StateDot "ongoing" pixel chase (3×3 ring, 125ms flat-hold brightness
25687
- * steps, 1s cycle) becomes the single-cell stepped pulse below and the
25688
- * full-ring clockwise braille chase in {@link BUSY_CHASE_FRAMES}, and the
25689
- * streaming caret blink is the Claude-Code convention. Pure functions only —
25690
- * the Ink layer owns timers and colors.
25691
- *
25692
- * @module @deepseek-ai/dsh-code/render/animations
25693
- */
25694
25517
  /** Single-cell stepped pulse: flat holds mirroring the web's 125ms keyframes. */
25695
25518
  const PULSE_FRAMES = [
25696
25519
  "█",
@@ -25729,182 +25552,329 @@ function busyChaseFrame(tick) {
25729
25552
  function caretVisible(tick) {
25730
25553
  return tick % 2 === 0;
25731
25554
  }
25732
- //#endregion
25733
- //#region src/render/inspector.ts
25734
- /** Composer (3) + two-row status chrome, plus one optional fixed-height local notice row. */
25735
- const INSPECTOR_CHROME_ROWS = 6;
25736
- /** One transcript-to-composer gutter, collapsed on short terminals. */
25737
- function layoutGutterRows(rows) {
25738
- return Math.max(1, Math.floor(rows)) >= 14 ? 1 : 0;
25739
- }
25555
+ /** All styles in canonical order, for random selection and tests. */
25556
+ const DEEPSEEK_WAVE_STYLES = [
25557
+ "wave",
25558
+ "aurora",
25559
+ "pulse"
25560
+ ];
25561
+ /** Pulse ring half-width in columns Codex PULSE_HALF_WIDTH (4.5). */
25562
+ const PULSE_HALF_WIDTH = 4.5;
25563
+ /** Sparkle glyphs in frame order — Codex SPARK_GLYPHS (`· ✦ ✧`). */
25564
+ const SPARK_GLYPHS = [
25565
+ "·",
25566
+ "✦",
25567
+ "✧"
25568
+ ];
25569
+ const DEEPSEEK_WAVE_BANDS = {
25570
+ wave: {
25571
+ flash: [[
25572
+ .1,
25573
+ .75,
25574
+ 1
25575
+ ]],
25576
+ deepseek: [[
25577
+ .1,
25578
+ .7,
25579
+ 1
25580
+ ], [
25581
+ .35,
25582
+ .55,
25583
+ 1
25584
+ ]]
25585
+ },
25586
+ aurora: {
25587
+ flash: [[
25588
+ .35,
25589
+ .15,
25590
+ 0
25591
+ ], [
25592
+ -.5,
25593
+ .6,
25594
+ 1
25595
+ ]],
25596
+ deepseek: [
25597
+ [
25598
+ .35,
25599
+ .15,
25600
+ 0
25601
+ ],
25602
+ [
25603
+ -.5,
25604
+ .6,
25605
+ 1
25606
+ ],
25607
+ [
25608
+ .75,
25609
+ .35,
25610
+ 2
25611
+ ]
25612
+ ]
25613
+ },
25614
+ pulse: {
25615
+ flash: [[
25616
+ .1,
25617
+ .6,
25618
+ 1
25619
+ ]],
25620
+ deepseek: [[
25621
+ .1,
25622
+ .55,
25623
+ .8
25624
+ ], [
25625
+ .45,
25626
+ .55,
25627
+ 1.1
25628
+ ]]
25629
+ }
25630
+ };
25740
25631
  /**
25741
- * Keep the inspector plus its persistent status/composer chrome below
25742
- * `stdout.rows`: at equality Ink clears the terminal and rewrites all
25743
- * accumulated `<Static>` output on every frame.
25632
+ * Total animation duration Codex `IgnitionStyle::total_duration`: three
25633
+ * styles × two tiers.
25634
+ * @param tier - the active wave tier.
25635
+ * @param style - the active ignition style.
25636
+ * @returns the duration in milliseconds.
25744
25637
  */
25745
- function panelViewport(columns, rows) {
25746
- const safeColumns = Math.max(1, Math.floor(columns));
25747
- const safeRows = Math.max(1, Math.floor(rows));
25748
- const maxHeight = Math.max(0, Math.min(safeRows - 2 - INSPECTOR_CHROME_ROWS - layoutGutterRows(safeRows), Math.floor(safeRows / 2)));
25749
- const compact = maxHeight < 5 || safeColumns < 8;
25750
- const gapRows = !compact && maxHeight >= 7 ? 2 : 0;
25751
- const outerColumns = compact ? safeColumns : Math.max(1, safeColumns - 1);
25752
- return {
25753
- maxHeight,
25754
- bodyRows: compact ? 0 : maxHeight - 4 - gapRows,
25755
- gapRows,
25756
- contentColumns: compact ? Math.max(1, safeColumns - 1) : Math.max(1, outerColumns - 4),
25757
- outerColumns,
25758
- compact
25759
- };
25760
- }
25761
- /** Backward-compatible name for the Ctrl+O-specific caller and tests. */
25762
- function inspectorViewport(columns, rows) {
25763
- return panelViewport(columns, rows);
25764
- }
25765
- /** Clamp a first-visible row to the range representable by one viewport. */
25766
- function clampScroll(offset, totalRows, visibleRows) {
25767
- const last = Math.max(0, Math.max(0, Math.floor(totalRows)) - Math.max(0, Math.floor(visibleRows)));
25768
- return Math.max(0, Math.min(Math.floor(offset), last));
25769
- }
25770
- /** Move a viewport by a signed row delta without escaping its content. */
25771
- function moveScroll(offset, delta, totalRows, visibleRows) {
25772
- return clampScroll(offset + delta, totalRows, visibleRows);
25638
+ function deepseekWaveDuration(tier, style = "wave") {
25639
+ switch (style) {
25640
+ case "aurora": return tier === "flash" ? 1300 : 1600;
25641
+ case "pulse": return tier === "flash" ? 900 : 1250;
25642
+ case "wave": return tier === "flash" ? 1e3 : 1300;
25643
+ }
25773
25644
  }
25774
- /** Keep one focused row visible while preserving the current window when possible. */
25775
- function revealRow(offset, row, totalRows, visibleRows) {
25776
- const size = Math.max(1, Math.floor(visibleRows));
25777
- const target = Math.max(0, Math.min(Math.floor(row), Math.max(0, totalRows - 1)));
25778
- if (target < offset) return clampScroll(target, totalRows, size);
25779
- if (target >= offset + size) return clampScroll(target - size + 1, totalRows, size);
25780
- return clampScroll(offset, totalRows, size);
25645
+ /**
25646
+ * Pick one ignition style at random, never repeating the previous one —
25647
+ * Codex `IgnitionStyle::random`. Falls back to the remaining styles.
25648
+ * @param previous - the style of the last trigger, if any.
25649
+ * @returns a style different from `previous`.
25650
+ */
25651
+ function deepseekWaveStyleRandom(previous) {
25652
+ const candidates = DEEPSEEK_WAVE_STYLES.filter((style) => style !== previous);
25653
+ return candidates[Math.floor(Math.random() * candidates.length)] ?? "wave";
25781
25654
  }
25782
- /** Center a selected list row where possible, clamped at both ends. */
25783
- function selectionWindow(cursor, totalRows, visibleRows) {
25784
- return clampScroll(cursor - Math.floor(Math.max(1, visibleRows) / 2), totalRows, visibleRows);
25655
+ /**
25656
+ * Blank-cell background the wave tint blends toward — fixed approximations
25657
+ * of the terminal's default background, mirroring Codex's
25658
+ * `user_message_bg_rgb` (which derives a near-black / near-white bubble tint
25659
+ * from the terminal background). The Ink layer picks the active theme's one.
25660
+ */
25661
+ const WAVE_BASE_DARK = [
25662
+ 16,
25663
+ 18,
25664
+ 24
25665
+ ];
25666
+ const WAVE_BASE_LIGHT = [
25667
+ 242,
25668
+ 244,
25669
+ 248
25670
+ ];
25671
+ /**
25672
+ * Tier for a `provider/model` label: a model id containing `flash` runs the
25673
+ * single-band flash tier; everything else (pro/reasoner/chat) runs the
25674
+ * dual-band deepseek tier. Mirrors Codex's Max→Ultra mapping.
25675
+ * @param model - the `provider/model` label of the applied model.
25676
+ * @returns the wave tier for that model.
25677
+ */
25678
+ function deepseekWaveTier(model) {
25679
+ return model.toLowerCase().includes("flash") ? "flash" : "deepseek";
25785
25680
  }
25786
- /** Follow appended history only while the inspector cursor was at the tail. */
25787
- function followInspectorCursor(cursor, previousLength, nextLength) {
25788
- const nextLast = Math.max(0, nextLength - 1);
25789
- if (cursor >= Math.max(0, previousLength - 1)) return nextLast;
25790
- return Math.min(cursor, nextLast);
25681
+ /**
25682
+ * Cosine window — Codex `crest`: 1 exactly under the wave center, 0 from
25683
+ * one half-width away.
25684
+ * @param distance - distance from the crest center in half-widths.
25685
+ * @returns the crest strength in 0..1.
25686
+ */
25687
+ function crest(distance) {
25688
+ if (distance >= 1) return 0;
25689
+ return .5 * (1 + Math.cos(Math.PI * distance));
25791
25690
  }
25792
- //#endregion
25793
- //#region src/render/text.ts
25794
25691
  /**
25795
- * Display-boundary sanitization for externally sourced text (model output,
25796
- * tool payloads, skill descriptions). Control characters including ANSI
25797
- * CSI/OSC escape sequences would otherwise pass through Ink into the
25798
- * terminal, letting output rewrite the screen or inject prompts. Newlines
25799
- * and tabs survive; everything else in C0/C1 plus DEL becomes a visible
25800
- * `\xNN` escape.
25801
- *
25802
- * @module @deepseek-ai/dsh-code/render/text
25692
+ * Cubic ease-in-out Codex `ease_in_out`: flat at both ends, steepest in
25693
+ * the middle, so the crest accelerates and eases instead of sliding linearly.
25694
+ * @param progress - raw progress (clamped to 0..1).
25695
+ * @returns the eased progress in 0..1.
25803
25696
  */
25804
- /** C0 controls except tab (0x09) and newline (0x0a), plus DEL and C1. */
25805
- const CONTROL_ESCAPE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/gu;
25697
+ function easeInOut(progress) {
25698
+ const p = Math.min(1, Math.max(0, progress));
25699
+ if (p < .5) return 4 * p * p * p;
25700
+ const inverse = -2 * p + 2;
25701
+ return 1 - inverse * inverse * inverse / 2;
25702
+ }
25806
25703
  /**
25807
- * Escape control characters so externally sourced text cannot drive the
25808
- * terminal.
25809
- * @param text - raw text from a session event, tool payload, or catalog.
25810
- * @returns text with every control character (except `\n`, `\t`) rendered
25811
- * as a literal `\xNN` escape.
25704
+ * Fade-in/fade-out envelope Codex `envelope`: linear ramp over `fadeIn`
25705
+ * at the start and `fadeOut` at the end, plateau at 1 between, 0 outside the
25706
+ * total. The Wave style keeps the envelope at 1 (Codex paints Wave without
25707
+ * an envelope); exported for the Aurora-style fades and for tests.
25708
+ * @param elapsed - seconds since the animation started.
25709
+ * @param total - total duration in seconds.
25710
+ * @param fadeIn - seconds of fade-in.
25711
+ * @param fadeOut - seconds of fade-out.
25712
+ * @returns the envelope value in 0..1.
25812
25713
  */
25813
- function displayText(text) {
25814
- return text.replace(CONTROL_ESCAPE, (char) => `\\x${char.charCodeAt(0).toString(16).padStart(2, "0")}`);
25714
+ function envelope(elapsed, total, fadeIn, fadeOut) {
25715
+ if (elapsed <= 0 || elapsed >= total) return 0;
25716
+ const rise = elapsed / Math.max(fadeIn, Number.EPSILON);
25717
+ const fall = (total - elapsed) / Math.max(fadeOut, Number.EPSILON);
25718
+ return Math.min(Math.max(Math.min(rise, fall), 0), 1);
25815
25719
  }
25816
- /** Collapse external text to one terminal-safe logical row. */
25817
- function singleLineText(text) {
25818
- return displayText(text).replace(/\r?\n/gu, " ").replace(/\t/gu, " ");
25720
+ /**
25721
+ * One band's contribution at a column — Codex `band_sample`, all three
25722
+ * branches: Wave sweeps an eased crest across the row; Aurora drifts a
25723
+ * sinusoidal center carrying a hue index; Pulse expands a ring from the row
25724
+ * center with cubic ease and decaying strength.
25725
+ * @param style - the ignition style.
25726
+ * @param band - the band triple (meaning depends on the style).
25727
+ * @param elapsed - seconds since the animation started.
25728
+ * @param column - column index in the content row (0..width-1).
25729
+ * @param width - content-row width in columns.
25730
+ * @returns `[hueIndex, strength]`.
25731
+ */
25732
+ function bandSample(style, band, elapsed, column, width) {
25733
+ const [first, second, third] = band;
25734
+ switch (style) {
25735
+ case "wave": {
25736
+ const progress = (elapsed - first) / second;
25737
+ if (progress < 0 || progress > 1) return [0, 0];
25738
+ const center = easeInOut(progress) * (width + 18) - 9;
25739
+ return [0, crest(Math.abs(column - center) / 9)];
25740
+ }
25741
+ case "aurora": {
25742
+ const center = (.5 + .38 * Math.sin(Math.PI * 2 * (first * elapsed + second))) * width;
25743
+ const halfWidth = Math.max(width * .22, 4);
25744
+ return [Math.trunc(third), crest(Math.abs(column - center) / halfWidth)];
25745
+ }
25746
+ case "pulse": {
25747
+ const progress = (elapsed - first) / second;
25748
+ if (progress < 0 || progress > 1) return [0, 0];
25749
+ const inverse = 1 - progress;
25750
+ const radius = (1 - inverse * inverse * inverse) * (width / 2 + 2 * PULSE_HALF_WIDTH);
25751
+ const distance = Math.abs(column - width / 2);
25752
+ return [0, crest(Math.abs(distance - radius) / PULSE_HALF_WIDTH) * third * (1 - .6 * progress)];
25753
+ }
25754
+ }
25819
25755
  }
25820
- /** Terminal-cell width matching the TUI's existing CJK-aware wrapping rule. */
25821
- function cellWidth(text) {
25822
- let columns = 0;
25823
- for (const char of text) columns += (char.codePointAt(0) ?? 0) > 11903 ? 2 : 1;
25824
- return columns;
25756
+ /** Linear RGB blend Codex `blend`: `fg * alpha + bg * (1 - alpha)`. */
25757
+ function blendRgb(fg, bg, alpha) {
25758
+ return [
25759
+ Math.round(fg[0] * alpha + bg[0] * (1 - alpha)),
25760
+ Math.round(fg[1] * alpha + bg[1] * (1 - alpha)),
25761
+ Math.round(fg[2] * alpha + bg[2] * (1 - alpha))
25762
+ ];
25825
25763
  }
25826
25764
  /**
25827
- * Truncate one display-safe row without ever exceeding its physical-column
25828
- * budget. The ellipsis is included inside the budget, matching Codex's popup
25829
- * truncation contract; the previous app-local helper appended it after the
25830
- * row was already full and could force an extra terminal wrap.
25765
+ * The background color for one composer-row column at a tick — Codex
25766
+ * `paint_bands` + `Canvas::tint` for all three styles. Bands overlap with a
25767
+ * max for Wave/Pulse and a SUM for Aurora (Codex differs by style), the
25768
+ * weighted hues mix per column (Wave/Pulse always end on hue 0), the tint
25769
+ * blends the mixed hue toward the blank-cell base at the style's alpha cap,
25770
+ * and Aurora applies its own fade envelope. Returns `null` when the column
25771
+ * should stay transparent, so the row returns to no `backgroundColor` on
25772
+ * both ends.
25773
+ * @param tick - wave frame (0, 1, … at DEEPSEEK_WAVE_TICK_MS).
25774
+ * @param column - column index in the content row (0..width-1).
25775
+ * @param width - content-row width in columns.
25776
+ * @param tier - the wave tier (flash = Max, deepseek = Ultra parameters).
25777
+ * @param style - the ignition style.
25778
+ * @param hues - the tier's three hues.
25779
+ * @param base - the blank-cell base color the tint blends toward.
25780
+ * @returns the blended RGB background, or null for transparent.
25831
25781
  */
25832
- function truncateColumns(text, columns) {
25833
- const limit = Math.max(0, Math.floor(columns));
25834
- if (limit === 0) return "";
25835
- if (cellWidth(text) <= limit) return text;
25836
- const contentLimit = limit - 1;
25837
- let used = 0;
25838
- let result = "";
25839
- for (const char of text) {
25840
- const width = cellWidth(char);
25841
- if (used + width > contentLimit) break;
25842
- result += char;
25843
- used += width;
25782
+ function deepseekWaveColumnBg(tick, column, width, tier, style, hues, base) {
25783
+ const total = deepseekWaveDuration(tier, style) / 1e3;
25784
+ const elapsed = tick * 33 / 1e3;
25785
+ const fade = style === "aurora" ? envelope(elapsed, total, .25, .4) : 1;
25786
+ const weights = [
25787
+ 0,
25788
+ 0,
25789
+ 0
25790
+ ];
25791
+ for (const band of DEEPSEEK_WAVE_BANDS[style][tier]) {
25792
+ const [hue, strength] = bandSample(style, band, elapsed, column, width);
25793
+ weights[hue] = style === "aurora" ? weights[hue] + strength : Math.max(weights[hue], strength);
25844
25794
  }
25845
- return `${result}…`;
25846
- }
25847
- /** Read one Unicode character immediately before `end`. */
25848
- function previousCharacter(text, end) {
25849
- const last = text.charCodeAt(end - 1);
25850
- if (last >= 56320 && last <= 57343 && end >= 2) {
25851
- const first = text.charCodeAt(end - 2);
25852
- if (first >= 55296 && first <= 56319) return {
25853
- char: text.slice(end - 2, end),
25854
- start: end - 2
25855
- };
25795
+ const weight = weights[0] + weights[1] + weights[2];
25796
+ if (weight <= .01) return null;
25797
+ let red = 0;
25798
+ let green = 0;
25799
+ let blue = 0;
25800
+ for (let index = 0; index < 3; index += 1) {
25801
+ red += weights[index] * hues[index][0];
25802
+ green += weights[index] * hues[index][1];
25803
+ blue += weights[index] * hues[index][2];
25856
25804
  }
25857
- return {
25858
- char: text.slice(end - 1, end),
25859
- start: end - 1
25860
- };
25805
+ const mixed = [
25806
+ Math.round(red / weight),
25807
+ Math.round(green / weight),
25808
+ Math.round(blue / weight)
25809
+ ];
25810
+ const alpha = style === "aurora" ? Math.min(weight * .4, .5) * fade : weight * .55;
25811
+ if (alpha < .02) return null;
25812
+ return blendRgb(mixed, base, alpha);
25813
+ }
25814
+ /**
25815
+ * The sparkle glyph for a tick — Codex `spark_frame`: from 900ms on, one
25816
+ * glyph every 100ms through `· ✦ ✧`, then silent. The deepseek (Ultra) tier
25817
+ * only; the Ink layer still must skip occupied cells.
25818
+ * @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
25819
+ * @returns the sparkle glyph, or null outside the 900..1200ms window.
25820
+ */
25821
+ function deepseekWaveSpark(tick) {
25822
+ const elapsed = tick * 33;
25823
+ if (elapsed < 900) return null;
25824
+ const frame = Math.floor((elapsed - 900) / 100);
25825
+ return SPARK_GLYPHS[frame] ?? null;
25826
+ }
25827
+ /**
25828
+ * The composer BORDER color at a tick: the frame breathes with the wave —
25829
+ * the palette's static dim blends toward the tier accent as the crest is
25830
+ * alive and back, so the frame glows up while the wave sweeps and settles to
25831
+ * dim on both ends (first==last frame==dim, no hard jump).
25832
+ * @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
25833
+ * @param tier - the wave tier.
25834
+ * @param hues - the tier's three hues; the border blends toward hues[0].
25835
+ * @param dim - the palette's static dim RGB (the resting border color).
25836
+ * @returns the blended border RGB.
25837
+ */
25838
+ function deepseekWaveBorderColor(tick, tier, style, hues, dim) {
25839
+ const total = deepseekWaveDuration(tier, style) / 1e3;
25840
+ const glow = envelope(tick * 33 / 1e3, total, total * .25, total * .4);
25841
+ return blendRgb(hues[0], dim, .25 + glow * .6);
25861
25842
  }
25862
25843
  /**
25863
- * Keep only the newest display-safe text that fits a terminal rectangle.
25864
- * The scan walks backward and stops as soon as the suffix is full, so a long
25865
- * reasoning stream does not rescan its entire accumulated prefix per chunk.
25866
- * Explicit newlines and terminal wrapping both consume rows.
25867
- * @param text - raw externally sourced text.
25868
- * @param columns - available terminal columns.
25869
- * @param rows - available terminal rows.
25870
- * @returns a sanitized bounded suffix and whether an earlier prefix was cut.
25844
+ * Whether the `deepseek` wordmark rides the wave at this tick: it fades in
25845
+ * shortly after the first crest launches and out before the wave settles,
25846
+ * so the brand name surfaces through the sweep's middle. The Ink layer
25847
+ * places it in the row's blank mid-section (never over real draft text).
25848
+ * @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
25849
+ * @param tier - the wave tier.
25850
+ * @returns true while the wordmark should be visible.
25871
25851
  */
25872
- function displayTail(text, columns, rows) {
25873
- const columnLimit = Math.max(1, Math.floor(columns));
25874
- const rowLimit = Math.max(1, Math.floor(rows));
25875
- const reversed = [];
25876
- let row = 1;
25877
- let used = 0;
25878
- let end = text.length;
25879
- while (end > 0) {
25880
- const previous = previousCharacter(text, end);
25881
- if (previous.char === "\n") {
25882
- if (row >= rowLimit) break;
25883
- reversed.push("\n");
25884
- row += 1;
25885
- used = 0;
25886
- end = previous.start;
25887
- continue;
25888
- }
25889
- const safe = displayText(previous.char);
25890
- const width = cellWidth(safe);
25891
- if (used > 0 && used + width > columnLimit) {
25892
- if (row >= rowLimit) break;
25893
- reversed.push("\n");
25894
- row += 1;
25895
- used = 0;
25896
- }
25897
- const extraRows = Math.floor(Math.max(0, width - 1) / columnLimit);
25898
- if (row + extraRows > rowLimit) break;
25899
- row += extraRows;
25900
- reversed.push(safe);
25901
- used = extraRows === 0 ? used + width : width - extraRows * columnLimit;
25902
- end = previous.start;
25903
- }
25904
- return {
25905
- text: reversed.reverse().join(""),
25906
- truncated: end > 0
25907
- };
25852
+ function deepseekWaveWordVisible(tick, tier, style = "wave") {
25853
+ const total = deepseekWaveDuration(tier, style) / 1e3;
25854
+ return envelope(tick * 33 / 1e3, total, total * .2, total * .35) > .25;
25855
+ }
25856
+ /**
25857
+ * The per-character color for the `deepseek` wordmark: the tier's hues
25858
+ * cycled per character (d→hue0, e→hue1, e→hue2, …), a brand-gradient text.
25859
+ * @param index - character index in the wordmark.
25860
+ * @param hues - the tier's three hues.
25861
+ * @returns the hue for that character.
25862
+ */
25863
+ function deepseekWaveWordHue(index, hues) {
25864
+ return hues[index % hues.length];
25865
+ }
25866
+ /**
25867
+ * True when a `provider/model` status label addresses the official DeepSeek
25868
+ * route: either segment contains `deepseek` (case-insensitive), covering the
25869
+ * `deepseek-official` provider route and its `deepseek-*` model ids.
25870
+ * @param label - the status bar model label (`provider/model`).
25871
+ * @returns whether the label names an official DeepSeek model.
25872
+ */
25873
+ function isOfficialDeepSeekLabel(label) {
25874
+ const slash = label.indexOf("/");
25875
+ const provider = slash < 0 ? label : label.slice(0, slash);
25876
+ const model = slash < 0 ? "" : label.slice(slash + 1);
25877
+ return provider.toLowerCase().includes("deepseek") || model.toLowerCase().includes("deepseek");
25908
25878
  }
25909
25879
  //#endregion
25910
25880
  //#region src/render/status.ts
@@ -25965,39 +25935,152 @@ function cacheHitPercent(usage) {
25965
25935
  const STATUS_ITEM_SEPARATOR = " · ";
25966
25936
  /** The Codex-style mode cycle hint appended to the permission badge. */
25967
25937
  const STATUS_CYCLE_HINT = " (shift+tab to cycle)";
25938
+ /** Free-tail floor in columns: wide enough for the bare percent readout, so
25939
+ * the warning stays visible even at 100%+ occupancy. */
25940
+ const CONTEXT_MIN_FREE = 5;
25941
+ /** The five content types in conversation order, dark → light blue. */
25942
+ const CONTEXT_SEGMENTS = [
25943
+ {
25944
+ key: "system",
25945
+ tone: "ctxSystem",
25946
+ labels: [
25947
+ "system",
25948
+ "sys",
25949
+ "s"
25950
+ ]
25951
+ },
25952
+ {
25953
+ key: "prompt",
25954
+ tone: "ctxPrompt",
25955
+ labels: [
25956
+ "prompt",
25957
+ "pr",
25958
+ "p"
25959
+ ]
25960
+ },
25961
+ {
25962
+ key: "assistant",
25963
+ tone: "ctxAssistant",
25964
+ labels: [
25965
+ "assistant",
25966
+ "ast",
25967
+ "a"
25968
+ ]
25969
+ },
25970
+ {
25971
+ key: "thinking",
25972
+ tone: "ctxThinking",
25973
+ labels: [
25974
+ "think",
25975
+ "th",
25976
+ "t"
25977
+ ]
25978
+ },
25979
+ {
25980
+ key: "tools",
25981
+ tone: "ctxTools",
25982
+ labels: [
25983
+ "tools",
25984
+ "tl",
25985
+ "x"
25986
+ ]
25987
+ }
25988
+ ];
25989
+ /** First label whose visible width fits the segment; empty only when none do. */
25990
+ function chooseContextLabel(labels, width) {
25991
+ for (const label of labels) if (visibleColumns(label) <= width) return label;
25992
+ return "";
25993
+ }
25994
+ /** Center a label inside its segment width; the padding spaces carry the tone. */
25995
+ function centerInSegment(text, width) {
25996
+ const textWidth = visibleColumns(text);
25997
+ const left = Math.floor((width - textWidth) / 2);
25998
+ return " ".repeat(Math.max(0, left)) + text + " ".repeat(Math.max(0, width - textWidth - left));
25999
+ }
26000
+ /**
26001
+ * Largest-remainder allocation: distribute `columns` across `values`
26002
+ * proportionally, handing each leftover column to the largest remainder.
26003
+ */
26004
+ function allocateProportionally(values, columns) {
26005
+ if (columns <= 0) return values.map(() => 0);
26006
+ const total = values.reduce((sum, value) => sum + value, 0);
26007
+ if (total <= 0) return values.map(() => 0);
26008
+ const raw = values.map((value) => value / total * columns);
26009
+ const allocated = raw.map(Math.floor);
26010
+ let remaining = columns - allocated.reduce((sum, value) => sum + value, 0);
26011
+ const remainders = raw.map((value, index) => ({
26012
+ index,
26013
+ remainder: value - Math.floor(value)
26014
+ })).sort((left, right) => right.remainder - left.remainder);
26015
+ for (const slot of remainders) {
26016
+ if (remaining <= 0) break;
26017
+ allocated[slot.index] = (allocated[slot.index] ?? 0) + 1;
26018
+ remaining -= 1;
26019
+ }
26020
+ return allocated;
26021
+ }
26022
+ /** Bar column widths: every non-zero segment keeps at least one column before
26023
+ * the remaining columns share by token proportion. */
26024
+ function allocateBarColumns(values, width) {
26025
+ const visible = values.map((value, index) => value > 0 ? index : -1).filter((index) => index >= 0);
26026
+ if (visible.length === 0 || visible.length >= width) return allocateProportionally(values, width);
26027
+ const minimum = values.map(() => 0);
26028
+ for (const index of visible) minimum[index] = 1;
26029
+ const remaining = allocateProportionally(values, width - visible.length);
26030
+ return minimum.map((min, index) => min + (remaining[index] ?? 0));
26031
+ }
25968
26032
  /**
25969
- * Render a context-occupancy percent as a bracketed fixed-width progress bar
25970
- * plus the percentage: `[▰▰▰▱▱▱▱▱▱▱] 25%`. Filled cells and the percent read
25971
- * in brand blue (accent), empty cells and the brackets read dim (label), and
25972
- * the whole meter flips to one amber warning once occupancy reaches the
25973
- * warning threshold. The bar fill clamps to 100 while the printed percent
25974
- * keeps the raw value so an over-budget session reads as such.
25975
- * @param percent - occupancy percent (may exceed 100).
26033
+ * Render context occupancy as a segmented bar: one DeepSeek-blue run per
26034
+ * content type (system/prompt/assistant/thinking/tools), column widths
26035
+ * proportional to their estimated token share, each with a centered label
26036
+ * that shortens to fit (system→sys→s). The remaining free tail is a dim
26037
+ * track whose right edge carries the usage readout (`12.3K/1.0M 25%`,
26038
+ * shrinking to the bare percent as the tail narrows). The readout flips to
26039
+ * amber once occupancy reaches the warning threshold; the segment blues stay
26040
+ * untouched so the composition remains readable at full context. The used
26041
+ * total comes from the reported `lastPromptTokens`, never from the estimates.
26042
+ * @param segments - estimated used tokens per content type.
26043
+ * @param usedTokens - reported used tokens (drives the readout and percent).
26044
+ * @param contextWindow - route capacity.
26045
+ * @param width - total bar interior columns.
25976
26046
  * @returns tone-split spans for the footer to paint.
25977
26047
  */
25978
- function contextBar(percent) {
25979
- const clamped = Math.max(0, Math.min(100, Math.round(percent)));
25980
- const filled = Math.round(clamped / 100 * 10);
25981
- const tone = clamped >= 90 ? "warn" : "accent";
25982
- const percentText = " " + Math.max(0, Math.min(999, Math.round(percent))) + "%";
25983
- const spans = [{
25984
- text: "[",
25985
- tone: "label"
25986
- }];
25987
- if (filled > 0) spans.push({
25988
- text: "".repeat(filled),
25989
- tone
25990
- });
25991
- if (filled < 10) spans.push({
25992
- text: "▱".repeat(10 - filled),
26048
+ function contextBar(segments, usedTokens, contextWindow, width) {
26049
+ if (width <= 0 || contextWindow <= 0) return [];
26050
+ const used = Math.max(0, usedTokens);
26051
+ const percent = Math.round(used / contextWindow * 100);
26052
+ const readoutTone = percent >= 90 ? "warn" : "value";
26053
+ const freeShare = Math.round(Math.max(0, contextWindow - used) / contextWindow * width);
26054
+ const freeColumns = Math.min(width, Math.max(freeShare, CONTEXT_MIN_FREE));
26055
+ const usedColumns = Math.max(0, width - freeColumns);
26056
+ const total = `${formatTokens(used)}/${formatTokens(contextWindow)}`;
26057
+ const percentText = `${percent}%`;
26058
+ const readout = freeColumns >= visibleColumns(`${total} ${percentText}`) ? `${total} ${percentText}` : freeColumns >= visibleColumns(percentText) ? percentText : "";
26059
+ const values = CONTEXT_SEGMENTS.map((segment) => segments[segment.key]);
26060
+ const allocation = allocateBarColumns(values.reduce((sum, value) => sum + value, 0) > 0 ? values : [
26061
+ 0,
26062
+ used,
26063
+ 0,
26064
+ 0,
26065
+ 0
26066
+ ], usedColumns);
26067
+ const spans = [];
26068
+ for (let index = 0; index < CONTEXT_SEGMENTS.length; index += 1) {
26069
+ const columns = allocation[index] ?? 0;
26070
+ if (columns <= 0) continue;
26071
+ spans.push({
26072
+ text: centerInSegment(chooseContextLabel(CONTEXT_SEGMENTS[index].labels, columns), columns),
26073
+ tone: CONTEXT_SEGMENTS[index].tone
26074
+ });
26075
+ }
26076
+ const pad = freeColumns - visibleColumns(readout);
26077
+ if (pad > 0) spans.push({
26078
+ text: "▱".repeat(pad),
25993
26079
  tone: "label"
25994
26080
  });
25995
- spans.push({
25996
- text: "]",
25997
- tone: "label"
25998
- }, {
25999
- text: percentText,
26000
- tone
26081
+ if (readout !== "") spans.push({
26082
+ text: readout,
26083
+ tone: readoutTone
26001
26084
  });
26002
26085
  return spans;
26003
26086
  }
@@ -26181,10 +26264,13 @@ function buildCandidates(facts, stats, busy, enabled) {
26181
26264
  identity.push(span);
26182
26265
  };
26183
26266
  const model = safe(facts.model);
26184
- if (model !== "" && enabled.has("model")) push({
26185
- text: model,
26186
- tone: "model"
26187
- });
26267
+ if (model !== "" && enabled.has("model")) {
26268
+ const effort = safe(stats.reasoningEffort);
26269
+ push({
26270
+ text: effort === "" ? model : `${model}@${effort}`,
26271
+ tone: "model"
26272
+ });
26273
+ }
26188
26274
  const cwd = safe(facts.cwd);
26189
26275
  if (cwd !== "" && enabled.has("cwd")) push({
26190
26276
  text: cwd,
@@ -26287,7 +26373,7 @@ function buildCandidates(facts, stats, busy, enabled) {
26287
26373
  group: { spans: [{
26288
26374
  text: "context ",
26289
26375
  tone: "label"
26290
- }, ...contextBar(stats.lastPromptTokens / stats.contextWindow * 100)] },
26376
+ }, ...contextBar(stats.contextSegments, stats.lastPromptTokens, stats.contextWindow, 24)] },
26291
26377
  rank: RANK2_CONTEXT,
26292
26378
  id: "context"
26293
26379
  });
@@ -26591,9 +26677,6 @@ function transcriptEntryLines(entry, columns) {
26591
26677
  //#endregion
26592
26678
  //#region src/kernel-panels.ts
26593
26679
  /** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
26594
- function color(rgb) {
26595
- return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
26596
- }
26597
26680
  function ListFrame(props) {
26598
26681
  const stdout = useStdout().stdout;
26599
26682
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
@@ -26615,11 +26698,11 @@ function ListFrame(props) {
26615
26698
  return (0, import_react.createElement)(Box, {
26616
26699
  width: viewport.outerColumns,
26617
26700
  borderStyle: "round",
26618
- borderColor: color(TUI_RGB.dim),
26701
+ borderColor: inkColor(getPalette().dim),
26619
26702
  flexDirection: "column",
26620
26703
  paddingX: 1
26621
26704
  }, (0, import_react.createElement)(Text, {
26622
- color: color(TUI_RGB.brandBright),
26705
+ color: inkColor(getPalette().brandBright),
26623
26706
  wrap: "truncate-end"
26624
26707
  }, truncateColumns(props.title, viewport.contentColumns)), (0, import_react.createElement)(Text, {
26625
26708
  dimColor: true,
@@ -26629,7 +26712,7 @@ function ListFrame(props) {
26629
26712
  const selected = !props.loading && props.error === void 0 && props.rows.length > 0 && absolute === props.cursor;
26630
26713
  return (0, import_react.createElement)(Text, {
26631
26714
  key: row.key,
26632
- color: selected ? color(TUI_RGB.brandBright) : row.disabled ? color(TUI_RGB.dim) : void 0,
26715
+ color: selected ? inkColor(getPalette().brandBright) : row.disabled ? inkColor(getPalette().dim) : void 0,
26633
26716
  dimColor: row.disabled,
26634
26717
  wrap: "truncate-end"
26635
26718
  }, truncateColumns(`${selected ? "› " : " "}${row.text}`, viewport.contentColumns));
@@ -26861,11 +26944,11 @@ function DocumentPanel({ title, text, error, close }) {
26861
26944
  return (0, import_react.createElement)(Box, {
26862
26945
  width: viewport.outerColumns,
26863
26946
  borderStyle: "round",
26864
- borderColor: color(TUI_RGB.dim),
26947
+ borderColor: inkColor(getPalette().dim),
26865
26948
  flexDirection: "column",
26866
26949
  paddingX: 1
26867
26950
  }, (0, import_react.createElement)(Text, {
26868
- color: color(TUI_RGB.brandBright),
26951
+ color: inkColor(getPalette().brandBright),
26869
26952
  wrap: "truncate-end"
26870
26953
  }, truncateColumns(title, viewport.contentColumns)), ...body.map((line, index) => (0, import_react.createElement)(Text, {
26871
26954
  key: `${scroll}-${index}`,
@@ -26917,11 +27000,11 @@ function HistoryPanel({ entries, fill, close }) {
26917
27000
  return (0, import_react.createElement)(Box, {
26918
27001
  width: viewport.outerColumns,
26919
27002
  borderStyle: "round",
26920
- borderColor: color(TUI_RGB.dim),
27003
+ borderColor: inkColor(getPalette().dim),
26921
27004
  flexDirection: "column",
26922
27005
  paddingX: 1
26923
27006
  }, (0, import_react.createElement)(Text, {
26924
- color: color(TUI_RGB.brandBright),
27007
+ color: inkColor(getPalette().brandBright),
26925
27008
  wrap: "truncate-end"
26926
27009
  }, truncateColumns(header, viewport.contentColumns)), (0, import_react.createElement)(Text, {
26927
27010
  dimColor: true,
@@ -26935,7 +27018,7 @@ function HistoryPanel({ entries, fill, close }) {
26935
27018
  const selected = absolute === cursor;
26936
27019
  return (0, import_react.createElement)(Text, {
26937
27020
  key: `history-${absolute}`,
26938
- color: selected ? color(TUI_RGB.brandBright) : void 0,
27021
+ color: selected ? inkColor(getPalette().brandBright) : void 0,
26939
27022
  wrap: "truncate-end"
26940
27023
  }, truncateColumns((selected ? "› " : " ") + displayText(entry), viewport.contentColumns));
26941
27024
  }), (0, import_react.createElement)(Text, {
@@ -27003,18 +27086,18 @@ function StatuslinePanel({ enabled, change, close }) {
27003
27086
  return (0, import_react.createElement)(Box, {
27004
27087
  width: viewport.outerColumns,
27005
27088
  borderStyle: "round",
27006
- borderColor: color(TUI_RGB.dim),
27089
+ borderColor: inkColor(getPalette().dim),
27007
27090
  flexDirection: "column",
27008
27091
  paddingX: 1
27009
27092
  }, (0, import_react.createElement)(Text, {
27010
- color: color(TUI_RGB.brandBright),
27093
+ color: inkColor(getPalette().brandBright),
27011
27094
  wrap: "truncate-end"
27012
27095
  }, truncateColumns("/statusline · items apply to the live status line below", viewport.contentColumns)), ...visible.map((id, index) => {
27013
27096
  const selected = offset + index === cursor;
27014
27097
  const info = meta.get(id);
27015
27098
  return (0, import_react.createElement)(Text, {
27016
27099
  key: id,
27017
- color: selected ? color(TUI_RGB.brandBright) : void 0,
27100
+ color: selected ? inkColor(getPalette().brandBright) : void 0,
27018
27101
  dimColor: !on.has(id) || void 0,
27019
27102
  wrap: "truncate-end"
27020
27103
  }, truncateColumns((selected ? "› " : " ") + (on.has(id) ? "● " : "○ ") + (info?.label ?? id) + (info === void 0 ? "" : " · " + info.description + " · " + info.side), viewport.contentColumns));
@@ -27023,6 +27106,56 @@ function StatuslinePanel({ enabled, change, close }) {
27023
27106
  wrap: "truncate-end"
27024
27107
  }, truncateColumns("↑↓ move · space toggle · ←→ reorder · d default · esc close", viewport.contentColumns)));
27025
27108
  }
27109
+ /**
27110
+ * The `/model` reasoning-effort stage (the Codex model → reasoning popup
27111
+ * contract): one bounded list over the selected model's adapter-advertised
27112
+ * effort levels, with the effective effort and the model default marked.
27113
+ * A model WITHOUT an adapter-declared default leads with a "Default"
27114
+ * (provider-default) row — the web effort pane's first entry — so the user
27115
+ * can clear a picked level back to provider behavior instead of being forced
27116
+ * to choose an advertised one. Enter applies one level; Esc returns to the
27117
+ * model list without applying.
27118
+ */
27119
+ function EffortPanel({ row, current, select, back }) {
27120
+ const [cursor, setCursor] = (0, import_react.useState)(0);
27121
+ const efforts = row.reasoning?.efforts ?? [];
27122
+ const rows = row.reasoning !== void 0 && row.reasoning.defaultEffort === void 0 ? [{
27123
+ id: "",
27124
+ name: "Default"
27125
+ }, ...efforts] : efforts;
27126
+ const effective = current === void 0 || current === "" ? "" : current;
27127
+ (0, import_react.useEffect)(() => {
27128
+ if (rows.length === 0) {
27129
+ if (cursor !== 0) setCursor(0);
27130
+ return;
27131
+ }
27132
+ if (cursor >= rows.length) setCursor(rows.length - 1);
27133
+ }, [rows.length, cursor]);
27134
+ useInput((input, key) => {
27135
+ if (key.escape || input === "q") return back();
27136
+ if (rows.length === 0) return;
27137
+ if (key.upArrow) {
27138
+ setCursor(cursor > 0 ? cursor - 1 : rows.length - 1);
27139
+ return;
27140
+ }
27141
+ if (key.downArrow) {
27142
+ setCursor(cursor < rows.length - 1 ? cursor + 1 : 0);
27143
+ return;
27144
+ }
27145
+ if (key.return && rows[cursor] !== void 0) select(rows[cursor].id);
27146
+ });
27147
+ return (0, import_react.createElement)(ListFrame, {
27148
+ title: `/model — effort for ${row.providerName} · ${row.modelName}`,
27149
+ rows: rows.map((effort) => ({
27150
+ key: effort.id,
27151
+ text: `${effort.id === effective ? "●" : "○"} ${effort.name}${effort.id === row.reasoning?.defaultEffort ? " · default" : ""}${effort.description === void 0 ? "" : ` · ${effort.description}`}`
27152
+ })),
27153
+ cursor,
27154
+ loading: false,
27155
+ query: "",
27156
+ footer: "↑↓ choose · enter apply · esc/q back"
27157
+ });
27158
+ }
27026
27159
  /** Encode one entry for the history file (JSON keeps multi-line drafts intact). */
27027
27160
  function serializeHistoryEntry(text) {
27028
27161
  return JSON.stringify(text);
@@ -27172,10 +27305,6 @@ function recallNewer(state) {
27172
27305
  const RESIZE_REFLOW_DELAY_MS = 75;
27173
27306
  /** Reset region/style, clear the visible screen and scrollback, then home. */
27174
27307
  const RESIZE_REFLOW_CLEAR = "\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[3J\x1B[H";
27175
- /** Ink `color` string for one palette triple. */
27176
- function inkColor(triple) {
27177
- return `rgb(${triple[0]}, ${triple[1]}, ${triple[2]})`;
27178
- }
27179
27308
  /** Pad text with spaces to a visible-column target (menu name column). */
27180
27309
  function padColumns(text, width) {
27181
27310
  const clipped = truncateColumns(singleLineText(text), width);
@@ -27209,7 +27338,7 @@ function useStableInput(handler, active) {
27209
27338
  /** Single-cell stepped pulse: the web's 125ms flat-hold brightness steps over 1s. */
27210
27339
  function Pulse() {
27211
27340
  const tick = useFrames(125);
27212
- return (0, import_react.createElement)(Text, { color: inkColor(TUI_RGB.brandBright) }, pulseFrame(tick));
27341
+ return (0, import_react.createElement)(Text, { color: inkColor(getPalette().brandBright) }, pulseFrame(tick));
27213
27342
  }
27214
27343
  /**
27215
27344
  * The web StateDot "ongoing" chase in terminal form: three cells of the 3×3
@@ -27219,7 +27348,7 @@ function Pulse() {
27219
27348
  */
27220
27349
  function BusyChase() {
27221
27350
  const tick = useFrames(125);
27222
- return (0, import_react.createElement)(Text, { color: inkColor(TUI_RGB.brandBright) }, busyChaseFrame(tick) + " ");
27351
+ return (0, import_react.createElement)(Text, { color: inkColor(getPalette().brandBright) }, busyChaseFrame(tick) + " ");
27223
27352
  }
27224
27353
  /** Blinking block caret appended to streaming text. */
27225
27354
  function Caret() {
@@ -27269,19 +27398,19 @@ function StreamTail({ text, dim, maxRows, prefix, children }) {
27269
27398
  function segmentProps(style) {
27270
27399
  switch (style) {
27271
27400
  case "accent": return {
27272
- color: inkColor(TUI_RGB.brandBright),
27401
+ color: inkColor(getPalette().brandBright),
27273
27402
  bold: void 0,
27274
27403
  italic: void 0,
27275
27404
  strikethrough: void 0
27276
27405
  };
27277
27406
  case "code": return {
27278
- color: inkColor(TUI_RGB.code),
27407
+ color: inkColor(getPalette().code),
27279
27408
  bold: void 0,
27280
27409
  italic: void 0,
27281
27410
  strikethrough: void 0
27282
27411
  };
27283
27412
  case "dim": return {
27284
- color: inkColor(TUI_RGB.dim),
27413
+ color: inkColor(getPalette().dim),
27285
27414
  bold: void 0,
27286
27415
  italic: void 0,
27287
27416
  strikethrough: void 0
@@ -27305,7 +27434,7 @@ function segmentProps(style) {
27305
27434
  strikethrough: void 0
27306
27435
  };
27307
27436
  case "strike": return {
27308
- color: inkColor(TUI_RGB.dim),
27437
+ color: inkColor(getPalette().dim),
27309
27438
  bold: void 0,
27310
27439
  italic: void 0,
27311
27440
  strikethrough: true
@@ -27322,28 +27451,28 @@ function segmentProps(style) {
27322
27451
  function lineStyleProps(style) {
27323
27452
  switch (style) {
27324
27453
  case "brand": return {
27325
- color: inkColor(TUI_RGB.brandBright),
27454
+ color: inkColor(getPalette().brandBright),
27326
27455
  bold: void 0,
27327
27456
  italic: void 0,
27328
27457
  strikethrough: void 0,
27329
27458
  dimColor: void 0
27330
27459
  };
27331
27460
  case "success": return {
27332
- color: inkColor(TUI_RGB.success),
27461
+ color: inkColor(getPalette().success),
27333
27462
  bold: void 0,
27334
27463
  italic: void 0,
27335
27464
  strikethrough: void 0,
27336
27465
  dimColor: void 0
27337
27466
  };
27338
27467
  case "error": return {
27339
- color: inkColor(TUI_RGB.error),
27468
+ color: inkColor(getPalette().error),
27340
27469
  bold: void 0,
27341
27470
  italic: void 0,
27342
27471
  strikethrough: void 0,
27343
27472
  dimColor: void 0
27344
27473
  };
27345
27474
  case "warn": return {
27346
- color: inkColor(TUI_RGB.warn),
27475
+ color: inkColor(getPalette().warn),
27347
27476
  bold: void 0,
27348
27477
  italic: void 0,
27349
27478
  strikethrough: void 0,
@@ -27408,7 +27537,7 @@ function ToolDetailBody({ detail }) {
27408
27537
  wrap: "truncate-end"
27409
27538
  }, ` ── ${displayText(diff.path)}${diff.truncated ? " (diff truncated)" : ""}`), ...diff.lines.map((line, at) => (0, import_react.createElement)(Text, {
27410
27539
  key: at,
27411
- color: line.mark === "+" ? inkColor(TUI_RGB.success) : line.mark === "-" ? inkColor(TUI_RGB.error) : inkColor(TUI_RGB.dim),
27540
+ color: line.mark === "+" ? inkColor(getPalette().success) : line.mark === "-" ? inkColor(getPalette().error) : inkColor(getPalette().dim),
27412
27541
  wrap: "truncate-end"
27413
27542
  }, ` ${line.mark}${displayText(line.text)}`)))));
27414
27543
  case "read": return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(Text, {
@@ -27447,16 +27576,16 @@ function EntryLine({ entry, showReasoning, verbose }) {
27447
27576
  indent: 2
27448
27577
  }));
27449
27578
  case "tool": {
27450
- const mark = entry.state === "running" ? (0, import_react.createElement)(Pulse) : entry.state === "error" ? (0, import_react.createElement)(Text, { color: inkColor(TUI_RGB.error) }, "⨯") : (0, import_react.createElement)(Text, { color: inkColor(TUI_RGB.success) }, "⏺");
27579
+ const mark = entry.state === "running" ? (0, import_react.createElement)(Pulse) : entry.state === "error" ? (0, import_react.createElement)(Text, { color: inkColor(getPalette().error) }, "⨯") : (0, import_react.createElement)(Text, { color: inkColor(getPalette().success) }, "⏺");
27451
27580
  return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(Text, { wrap: verbose ? "truncate-end" : void 0 }, mark, " ", brand(entry.name), entry.preview === "" ? "" : ` ${dim(displayText(entry.preview))}`), entry.summary === "" ? void 0 : (0, import_react.createElement)(Text, {
27452
- color: entry.state === "error" ? inkColor(TUI_RGB.error) : inkColor(TUI_RGB.dim),
27581
+ color: entry.state === "error" ? inkColor(getPalette().error) : inkColor(getPalette().dim),
27453
27582
  wrap: verbose ? "truncate-end" : void 0
27454
27583
  }, ` ⎿ ${displayText(entry.summary)}`), verbose && entry.detail !== void 0 ? (0, import_react.createElement)(ToolDetailBody, { detail: entry.detail }) : void 0);
27455
27584
  }
27456
27585
  case "command": {
27457
- const mark = entry.state === "running" ? (0, import_react.createElement)(Pulse) : entry.state === "error" ? (0, import_react.createElement)(Text, { color: inkColor(TUI_RGB.error) }, "⨯") : (0, import_react.createElement)(Text, { color: inkColor(TUI_RGB.success) }, "⏺");
27586
+ const mark = entry.state === "running" ? (0, import_react.createElement)(Pulse) : entry.state === "error" ? (0, import_react.createElement)(Text, { color: inkColor(getPalette().error) }, "⨯") : (0, import_react.createElement)(Text, { color: inkColor(getPalette().success) }, "⏺");
27458
27587
  return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(Text, { wrap: verbose ? "truncate-end" : void 0 }, mark, " ", brand(`/${entry.name}`), entry.args === "" ? "" : ` ${dim(displayText(entry.args))}`), entry.summary === "" ? void 0 : (0, import_react.createElement)(Text, {
27459
- color: inkColor(TUI_RGB.dim),
27588
+ color: inkColor(getPalette().dim),
27460
27589
  wrap: verbose ? "truncate-end" : void 0
27461
27590
  }, ` ⎿ ${displayText(entry.summary)}`));
27462
27591
  }
@@ -27469,7 +27598,7 @@ function EntryLine({ entry, showReasoning, verbose }) {
27469
27598
  wrap: verbose ? "truncate-end" : void 0
27470
27599
  }, entry.ok ? ` ⧉ compacted ~${formatTokens(entry.tokens)} tokens` : ` ⧉ compaction failed: ${displayText(entry.error)}`);
27471
27600
  case "retry": return (0, import_react.createElement)(Text, {
27472
- color: entry.state === "running" ? inkColor(TUI_RGB.warn) : inkColor(TUI_RGB.dim),
27601
+ color: entry.state === "running" ? inkColor(getPalette().warn) : inkColor(getPalette().dim),
27473
27602
  wrap: verbose ? "truncate-end" : void 0
27474
27603
  }, ` ↻ retry ${entry.attempt}/${entry.max} · ${displayText(entry.code)} · ${Math.round(entry.delayMs / 100) / 10}s`);
27475
27604
  case "files": {
@@ -27499,18 +27628,18 @@ function Header({ resumed }) {
27499
27628
  flexDirection: "row",
27500
27629
  gap: 1,
27501
27630
  borderStyle: "round",
27502
- borderColor: inkColor(TUI_RGB.brand),
27631
+ borderColor: inkColor(getPalette().brand),
27503
27632
  paddingX: 1,
27504
27633
  alignSelf: "flex-start"
27505
27634
  }, (0, import_react.createElement)(Text, {
27506
- color: inkColor(TUI_RGB.brandBright),
27635
+ color: inkColor(getPalette().brandBright),
27507
27636
  bold: true
27508
27637
  }, "DeepSeek Harness"), (0, import_react.createElement)(Text, { dimColor: true }, hint));
27509
27638
  return (0, import_react.createElement)(Box, {
27510
27639
  flexDirection: "row",
27511
27640
  gap: 2,
27512
27641
  borderStyle: "round",
27513
- borderColor: inkColor(TUI_RGB.brand),
27642
+ borderColor: inkColor(getPalette().brand),
27514
27643
  paddingX: 1,
27515
27644
  alignSelf: "flex-start"
27516
27645
  }, (0, import_react.createElement)(Box, {
@@ -27519,12 +27648,12 @@ function Header({ resumed }) {
27519
27648
  justifyContent: "center"
27520
27649
  }, ...WHALE_GLYPH.map((row, index) => (0, import_react.createElement)(Text, {
27521
27650
  key: index,
27522
- color: inkColor(TUI_RGB.brand)
27651
+ color: inkColor(getPalette().brand)
27523
27652
  }, row))), (0, import_react.createElement)(Box, {
27524
27653
  flexDirection: "column",
27525
27654
  justifyContent: "center"
27526
27655
  }, (0, import_react.createElement)(Text, {
27527
- color: inkColor(TUI_RGB.brandBright),
27656
+ color: inkColor(getPalette().brandBright),
27528
27657
  bold: true
27529
27658
  }, "DeepSeek Harness"), (0, import_react.createElement)(Text, { dimColor: true }, hint)));
27530
27659
  }
@@ -27540,74 +27669,99 @@ function TodoPanel({ todos }) {
27540
27669
  const pending = todos.length - completed - inProgress;
27541
27670
  const current = todos.find((todo) => todo.status === "in_progress");
27542
27671
  return (0, import_react.createElement)(Box, { paddingX: 1 }, (0, import_react.createElement)(Text, {
27543
- color: inkColor(TUI_RGB.brand),
27672
+ color: inkColor(getPalette().brand),
27544
27673
  bold: true,
27545
27674
  wrap: "truncate-end"
27546
- }, `todos ${completed}/${todos.length}`, (0, import_react.createElement)(Text, { dimColor: true }, ` · ${inProgress} active · ${pending} pending`), current === void 0 ? "" : (0, import_react.createElement)(Text, { color: inkColor(TUI_RGB.brandBright) }, ` · ${todoMark(current.status)} ${displayText(current.content)}`)));
27675
+ }, `todos ${completed}/${todos.length}`, (0, import_react.createElement)(Text, { dimColor: true }, ` · ${inProgress} active · ${pending} pending`), current === void 0 ? "" : (0, import_react.createElement)(Text, { color: inkColor(getPalette().brandBright) }, ` · ${todoMark(current.status)} ${displayText(current.content)}`)));
27547
27676
  }
27548
27677
  /**
27549
27678
  * Ink props for one status tone: the Codex status-line accent mapping over
27550
27679
  * the DeepSeek palette, all blue by design — the status bar speaks only in
27551
- * degrees of blue (deep accent, primary figures, bright model identity, sky
27680
+ * degrees of blue (deep accent, primary figures, model identity, sky
27552
27681
  * paths and done states), with amber/red reserved for warnings and errors.
27553
27682
  */
27554
27683
  function statusToneProps(tone) {
27555
27684
  switch (tone) {
27556
27685
  case "model": return {
27557
- color: inkColor(TUI_RGB.brandBright),
27686
+ color: inkColor(getPalette().code),
27558
27687
  bold: true,
27559
27688
  dimColor: void 0
27560
27689
  };
27561
27690
  case "live": return {
27562
- color: inkColor(TUI_RGB.brandBright),
27691
+ color: inkColor(getPalette().brandBright),
27563
27692
  bold: void 0,
27564
27693
  dimColor: void 0
27565
27694
  };
27566
27695
  case "path": return {
27567
- color: inkColor(TUI_RGB.code),
27696
+ color: inkColor(getPalette().code),
27568
27697
  bold: void 0,
27569
27698
  dimColor: void 0
27570
27699
  };
27571
27700
  case "branch": return {
27572
- color: inkColor(TUI_RGB.text),
27701
+ color: inkColor(getPalette().text),
27573
27702
  bold: void 0,
27574
27703
  dimColor: void 0
27575
27704
  };
27576
27705
  case "value": return {
27577
- color: inkColor(TUI_RGB.brand),
27706
+ color: inkColor(getPalette().brand),
27578
27707
  bold: void 0,
27579
27708
  dimColor: void 0
27580
27709
  };
27581
27710
  case "label":
27582
27711
  case "meta": return {
27583
- color: void 0,
27712
+ color: inkColor(getPalette().dim),
27584
27713
  bold: void 0,
27585
- dimColor: true
27714
+ dimColor: void 0
27586
27715
  };
27587
27716
  case "accent": return {
27588
- color: inkColor(TUI_RGB.brandDeep),
27717
+ color: inkColor(getPalette().brandDeep),
27718
+ bold: void 0,
27719
+ dimColor: void 0
27720
+ };
27721
+ case "ctxSystem": return {
27722
+ color: inkColor(getPalette().brandDeep),
27723
+ bold: void 0,
27724
+ dimColor: void 0
27725
+ };
27726
+ case "ctxPrompt": return {
27727
+ color: inkColor(getPalette().brand),
27728
+ bold: void 0,
27729
+ dimColor: void 0
27730
+ };
27731
+ case "ctxAssistant": return {
27732
+ color: inkColor(getPalette().brandMid),
27733
+ bold: void 0,
27734
+ dimColor: void 0
27735
+ };
27736
+ case "ctxThinking": return {
27737
+ color: inkColor(getPalette().brandBright),
27738
+ bold: void 0,
27739
+ dimColor: void 0
27740
+ };
27741
+ case "ctxTools": return {
27742
+ color: inkColor(getPalette().code),
27589
27743
  bold: void 0,
27590
27744
  dimColor: void 0
27591
27745
  };
27592
27746
  case "success": return {
27593
- color: inkColor(TUI_RGB.code),
27747
+ color: inkColor(getPalette().code),
27594
27748
  bold: true,
27595
27749
  dimColor: void 0
27596
27750
  };
27597
27751
  case "warn": return {
27598
- color: inkColor(TUI_RGB.warn),
27752
+ color: inkColor(getPalette().warn),
27599
27753
  bold: true,
27600
27754
  dimColor: void 0
27601
27755
  };
27602
27756
  case "error": return {
27603
- color: inkColor(TUI_RGB.error),
27757
+ color: inkColor(getPalette().error),
27604
27758
  bold: true,
27605
27759
  dimColor: void 0
27606
27760
  };
27607
27761
  default: return {
27608
- color: void 0,
27762
+ color: inkColor(getPalette().dim),
27609
27763
  bold: void 0,
27610
- dimColor: true
27764
+ dimColor: void 0
27611
27765
  };
27612
27766
  }
27613
27767
  }
@@ -27620,7 +27774,32 @@ function statusToneProps(tone) {
27620
27774
  * content, so the footer degrades to a single row on narrow terminals. Both
27621
27775
  * layouts arrive pre-measured from the pure reducer, so Ink only paints;
27622
27776
  * truncation degrades groups, it never wraps a row.
27777
+ *
27778
+ * The DeepSeek easter egg: when the model label *switches* to an official
27779
+ * DeepSeek route, the composer's INPUT ROW (not the frame) plays Codex's
27780
+ * effort-ignition "Wave" — a blue crest sweeping the content row column by
27781
+ * column, with the `· ✦ ✧` sparkles on the deepseek tier — and the prompt
27782
+ * marker keeps the tier accent afterwards. The border stays a constant
27783
+ * static dim; only the row's per-column background tints during the wave,
27784
+ * so the row and column budget is untouched throughout.
27623
27785
  */
27786
+ /** Theme anchors for the one-shot composer wave, read from the active palette
27787
+ * so the wave stays coordinated in both themes. The flash tier runs the
27788
+ * brand blues; the deepseek tier swaps in the code sky-blue for a brighter,
27789
+ * richer mix. Codex's Wave bands carry no hue index (only hues[0] tints the
27790
+ * row), so the accent the prompt keeps is always hues[0]. */
27791
+ function deepseekWaveHues(tier) {
27792
+ const palette = getPalette();
27793
+ return tier === "flash" ? [
27794
+ palette.brandBright,
27795
+ palette.brand,
27796
+ palette.brandMid
27797
+ ] : [
27798
+ palette.brandBright,
27799
+ palette.code,
27800
+ palette.brandMid
27801
+ ];
27802
+ }
27624
27803
  function StatusLine({ facts, stats, busy, columns, items }) {
27625
27804
  const layout = layoutStatusBar(facts, stats, Math.max(8, columns - 2), {
27626
27805
  busy,
@@ -27631,7 +27810,7 @@ function StatusLine({ facts, stats, busy, columns, items }) {
27631
27810
  row.left.forEach((group, groupIndex) => {
27632
27811
  if (groupIndex > 0) leftParts.push((0, import_react.createElement)(Text, {
27633
27812
  key: key + "gs" + groupIndex,
27634
- dimColor: true
27813
+ color: inkColor(getPalette().dim)
27635
27814
  }, " | "));
27636
27815
  group.spans.forEach((span, spanIndex) => {
27637
27816
  leftParts.push((0, import_react.createElement)(Text, {
@@ -27645,7 +27824,7 @@ function StatusLine({ facts, stats, busy, columns, items }) {
27645
27824
  row.right.forEach((span, index) => {
27646
27825
  if (index > 0) rightParts.push((0, import_react.createElement)(Text, {
27647
27826
  key: key + "rs" + index,
27648
- dimColor: true
27827
+ color: inkColor(getPalette().dim)
27649
27828
  }, STATUS_ITEM_SEPARATOR));
27650
27829
  rightParts.push((0, import_react.createElement)(Text, {
27651
27830
  key: key + "r" + index,
@@ -27655,7 +27834,7 @@ function StatusLine({ facts, stats, busy, columns, items }) {
27655
27834
  });
27656
27835
  if (row.hint) rightParts.push((0, import_react.createElement)(Text, {
27657
27836
  key: key + "hint",
27658
- dimColor: true
27837
+ color: inkColor(getPalette().dim)
27659
27838
  }, STATUS_CYCLE_HINT));
27660
27839
  return (0, import_react.createElement)(Box, {
27661
27840
  paddingLeft: 2,
@@ -27671,7 +27850,7 @@ function StatusLine({ facts, stats, busy, columns, items }) {
27671
27850
  * above the composer.
27672
27851
  */
27673
27852
  function NoticeLine({ text, tone, columns }) {
27674
- const color = tone === "error" ? TUI_RGB.error : tone === "warning" ? TUI_RGB.warn : TUI_RGB.brandBright;
27853
+ const color = tone === "error" ? getPalette().error : tone === "warning" ? getPalette().warn : getPalette().brandBright;
27675
27854
  const mark = tone === "error" ? "⨯" : tone === "warning" ? "!" : "•";
27676
27855
  return (0, import_react.createElement)(Box, { paddingLeft: 2 }, (0, import_react.createElement)(Text, {
27677
27856
  color: inkColor(color),
@@ -27727,9 +27906,9 @@ function ApprovalBar({ snapshot, locked }) {
27727
27906
  width: viewport.outerColumns,
27728
27907
  paddingX: 1,
27729
27908
  borderStyle: "round",
27730
- borderColor: inkColor(TUI_RGB.warn)
27909
+ borderColor: inkColor(getPalette().warn)
27731
27910
  }, (0, import_react.createElement)(Text, {
27732
- color: inkColor(TUI_RGB.warn),
27911
+ color: inkColor(getPalette().warn),
27733
27912
  bold: true,
27734
27913
  wrap: "truncate-end"
27735
27914
  }, truncateColumns(`⏸ waiting for approval · lines ${content.length === 0 ? 0 : visibleScroll + 1}-${Math.min(content.length, visibleScroll + viewport.bodyRows)}/${content.length}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(StyledRows, { lines: content.slice(visibleScroll, visibleScroll + viewport.bodyRows) }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
@@ -27970,9 +28149,9 @@ function QuestionBar({ store, snapshot, locked }) {
27970
28149
  width: viewport.outerColumns,
27971
28150
  paddingX: 1,
27972
28151
  borderStyle: "round",
27973
- borderColor: inkColor(isPlan ? TUI_RGB.brand : TUI_RGB.brandDeep)
28152
+ borderColor: inkColor(isPlan ? getPalette().brand : getPalette().brandDeep)
27974
28153
  }, (0, import_react.createElement)(Text, {
27975
- color: inkColor(isPlan ? TUI_RGB.brand : TUI_RGB.brandDeep),
28154
+ color: inkColor(isPlan ? getPalette().brand : getPalette().brandDeep),
27976
28155
  bold: true,
27977
28156
  wrap: "truncate-end"
27978
28157
  }, truncateColumns(`${isPlan ? "📋 plan review" : "❓ question"} ${index + 1}/${pending.request.questions.length} · lines ${rendered.lines.length === 0 ? 0 : visibleScroll + 1}-${Math.min(rendered.lines.length, visibleScroll + viewport.bodyRows)}/${rendered.lines.length}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(StyledRows, { lines: rendered.lines.slice(visibleScroll, visibleScroll + viewport.bodyRows) }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
@@ -28037,11 +28216,11 @@ function ModelPanel({ directory, error, onSelect, onRetry, onClose }) {
28037
28216
  wrap: "truncate-end"
28038
28217
  }, " loading models…")] : error !== void 0 ? [(0, import_react.createElement)(Text, {
28039
28218
  key: "error",
28040
- color: inkColor(TUI_RGB.error),
28219
+ color: inkColor(getPalette().error),
28041
28220
  wrap: "truncate-end"
28042
28221
  }, truncateColumns(` ${singleLineText(error)}`, viewport.contentColumns))] : [...directory?.failures.length === 0 ? [] : [(0, import_react.createElement)(Text, {
28043
28222
  key: "failures",
28044
- color: inkColor(TUI_RGB.warn),
28223
+ color: inkColor(getPalette().warn),
28045
28224
  wrap: "truncate-end"
28046
28225
  }, truncateColumns(` unavailable providers: ${directory?.failures.join(", ")}`, viewport.contentColumns))], ...rows.length === 0 ? [(0, import_react.createElement)(Text, {
28047
28226
  key: "empty",
@@ -28056,9 +28235,9 @@ function ModelPanel({ directory, error, onSelect, onRetry, onClose }) {
28056
28235
  width: viewport.outerColumns,
28057
28236
  paddingX: 1,
28058
28237
  borderStyle: "round",
28059
- borderColor: inkColor(TUI_RGB.brand)
28238
+ borderColor: inkColor(getPalette().brand)
28060
28239
  }, (0, import_react.createElement)(Text, {
28061
- color: inkColor(TUI_RGB.brand),
28240
+ color: inkColor(getPalette().brand),
28062
28241
  bold: true,
28063
28242
  wrap: "truncate-end"
28064
28243
  }, truncateColumns(`/model — select model${rows.length === 0 ? "" : ` · ${cursor + 1}/${rows.length}`}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...stateRows, ...visible.map((row) => {
@@ -28066,7 +28245,7 @@ function ModelPanel({ directory, error, onSelect, onRetry, onClose }) {
28066
28245
  const label = displayText(`${row.providerName} · ${row.modelName}`);
28067
28246
  return (0, import_react.createElement)(Text, {
28068
28247
  key: `${row.provider}/${row.model}`,
28069
- color: index === cursor ? inkColor(TUI_RGB.brandBright) : inkColor(TUI_RGB.dim),
28248
+ color: index === cursor ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
28070
28249
  wrap: "truncate-end"
28071
28250
  }, truncateColumns(`${index === cursor ? "❯ " : " "}${label}`, viewport.contentColumns));
28072
28251
  }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
@@ -28133,16 +28312,18 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
28133
28312
  }, " commands"),
28134
28313
  ...commandError === void 0 ? [] : [(0, import_react.createElement)(Text, {
28135
28314
  key: "commands-error",
28136
- color: inkColor(TUI_RGB.error),
28315
+ color: inkColor(getPalette().error),
28137
28316
  wrap: "truncate-end"
28138
28317
  }, truncateColumns(` command catalog unavailable: ${singleLineText(commandError)}`, viewport.contentColumns))],
28139
28318
  (0, import_react.createElement)(Box, { key: "local-help" }, row("/help", "show this overlay")),
28140
28319
  (0, import_react.createElement)(Box, { key: "local-model" }, row("/model", "switch the model")),
28320
+ (0, import_react.createElement)(Box, { key: "local-effort" }, row("/effort", "adjust reasoning effort for the current model")),
28141
28321
  (0, import_react.createElement)(Box, { key: "local-mode" }, row("/mode", "inspect or select the agent preset (/mode [preset])")),
28142
28322
  (0, import_react.createElement)(Box, { key: "local-new" }, row("/new", "create and switch to a fresh session (/new [preset])")),
28143
28323
  (0, import_react.createElement)(Box, { key: "local-resume" }, row("/resume", "browse or switch root sessions (/resume [id|prefix])")),
28144
28324
  (0, import_react.createElement)(Box, { key: "local-plugin" }, row("/plugin", "inspect the live plugin composition")),
28145
28325
  (0, import_react.createElement)(Box, { key: "local-statusline" }, row("/statusline", "customize the status line items")),
28326
+ (0, import_react.createElement)(Box, { key: "local-theme" }, row("/theme", "switch the color theme")),
28146
28327
  (0, import_react.createElement)(Box, { key: "local-history" }, row("/history", "search and recall past prompts")),
28147
28328
  (0, import_react.createElement)(Box, { key: "local-clear" }, row("/clear", "clear the screen")),
28148
28329
  (0, import_react.createElement)(Box, { key: "local-export" }, row("/export", "export the transcript to markdown (/export [path])")),
@@ -28160,7 +28341,7 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
28160
28341
  }, " skills")],
28161
28342
  ...skillError === void 0 ? [] : [(0, import_react.createElement)(Text, {
28162
28343
  key: "skills-error",
28163
- color: inkColor(TUI_RGB.error),
28344
+ color: inkColor(getPalette().error),
28164
28345
  wrap: "truncate-end"
28165
28346
  }, truncateColumns(` skill catalog unavailable: ${singleLineText(skillError)}`, viewport.contentColumns))],
28166
28347
  ...skills.map((skill) => (0, import_react.createElement)(Text, {
@@ -28195,9 +28376,9 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
28195
28376
  width: viewport.outerColumns,
28196
28377
  paddingX: 1,
28197
28378
  borderStyle: "round",
28198
- borderColor: inkColor(TUI_RGB.brand)
28379
+ borderColor: inkColor(getPalette().brand)
28199
28380
  }, (0, import_react.createElement)(Text, {
28200
- color: inkColor(TUI_RGB.brand),
28381
+ color: inkColor(getPalette().brand),
28201
28382
  bold: true,
28202
28383
  wrap: "truncate-end"
28203
28384
  }, truncateColumns(`/help — keys and commands · rows ${content.length === 0 ? 0 : visibleScroll + 1}-${Math.min(content.length, visibleScroll + viewport.bodyRows)}/${content.length}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...content.slice(visibleScroll, visibleScroll + viewport.bodyRows), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
@@ -28224,6 +28405,38 @@ function editorWindow(value, cursor, columns) {
28224
28405
  after: afterBudget === 0 ? "" : truncateColumns(normalize(value.slice(cursor + 1)), afterBudget)
28225
28406
  };
28226
28407
  }
28408
+ /** The empty-composer placeholder text (shared by the static and wave paths). */
28409
+ const COMPOSER_PLACEHOLDER = "type a message · / commands · @ mentions";
28410
+ /** Adjacent cells with identical styling merge into one styled Text span. */
28411
+ function sameCellStyle(a, b) {
28412
+ return a.color === b.color && a.backgroundColor === b.backgroundColor && a.bold === b.bold && a.inverse === b.inverse && a.dim === b.dim;
28413
+ }
28414
+ /**
28415
+ * Render the wave row as one Text whose cells carry per-column
28416
+ * `backgroundColor` runs: the Codex Wave crest paints a smooth gradient
28417
+ * (one SGR run per sampled column) over the prompt, draft, cursor,
28418
+ * placeholder, and the trailing blank fill — the draft stays readable
28419
+ * because the tint blends at ≤ 0.55 toward the theme's blank-cell base.
28420
+ */
28421
+ function waveRowSpans(cells) {
28422
+ const spans = [];
28423
+ let start = 0;
28424
+ while (start < cells.length) {
28425
+ const cell = cells[start];
28426
+ let end = start + 1;
28427
+ while (end < cells.length && sameCellStyle(cells[end], cell)) end += 1;
28428
+ spans.push((0, import_react.createElement)(Text, {
28429
+ key: start,
28430
+ color: cell.color,
28431
+ backgroundColor: cell.backgroundColor,
28432
+ bold: cell.bold,
28433
+ inverse: cell.inverse,
28434
+ dimColor: cell.dim
28435
+ }, cells.slice(start, end).map((c) => c.char).join("")));
28436
+ start = end;
28437
+ }
28438
+ return spans;
28439
+ }
28227
28440
  /**
28228
28441
  * The Ctrl+O transcript inspector: one selected durable entry at a time,
28229
28442
  * with independent history selection and content scrolling. The complete
@@ -28320,9 +28533,9 @@ function VerbosePanel({ entries, onClose }) {
28320
28533
  width: viewport.outerColumns,
28321
28534
  paddingX: 1,
28322
28535
  borderStyle: "round",
28323
- borderColor: inkColor(TUI_RGB.brand)
28536
+ borderColor: inkColor(getPalette().brand)
28324
28537
  }, (0, import_react.createElement)(Text, {
28325
- color: inkColor(TUI_RGB.brand),
28538
+ color: inkColor(getPalette().brand),
28326
28539
  bold: true,
28327
28540
  wrap: "truncate-end"
28328
28541
  }, truncateColumns(title, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Box, { flexDirection: "column" }, entry === void 0 ? (0, import_react.createElement)(Text, { dimColor: true }, " no durable entries yet") : (0, import_react.createElement)(StyledRows, { lines: visible })), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
@@ -28347,7 +28560,12 @@ const MemoStaticTranscript = (0, import_react.memo)(StaticTranscript);
28347
28560
  * Resolve completion candidates for the current input: TUI-local commands,
28348
28561
  * the live registry descriptors, and user-invocable skills, filtered by the
28349
28562
  * typed prefix. Command names win collisions (the dispatch tries the
28350
- * registry first and only then falls through to the skill gesture).
28563
+ * registry first and only then falls through to the skill gesture), and a
28564
+ * later duplicate name never renders twice.
28565
+ *
28566
+ * A bare `/` returns the FULL merged list — Codex's command popup shows every
28567
+ * command inside a scroll window on an empty filter, and the menu's own
28568
+ * selection window bounds the visible rows, so no slice cap is needed.
28351
28569
  */
28352
28570
  function completionCandidates(value, descriptors, skills) {
28353
28571
  if (!value.startsWith("/")) return [];
@@ -28363,6 +28581,11 @@ function completionCandidates(value, descriptors, skills) {
28363
28581
  description: "switch the model",
28364
28582
  origin: "command"
28365
28583
  },
28584
+ {
28585
+ label: "/effort",
28586
+ description: "adjust reasoning effort for the current model",
28587
+ origin: "command"
28588
+ },
28366
28589
  {
28367
28590
  label: "/mode",
28368
28591
  description: "select the agent preset",
@@ -28388,6 +28611,11 @@ function completionCandidates(value, descriptors, skills) {
28388
28611
  description: "customize the status line",
28389
28612
  origin: "command"
28390
28613
  },
28614
+ {
28615
+ label: "/theme",
28616
+ description: "switch the color theme",
28617
+ origin: "command"
28618
+ },
28391
28619
  {
28392
28620
  label: "/history",
28393
28621
  description: "search and recall past prompts",
@@ -28426,13 +28654,20 @@ function completionCandidates(value, descriptors, skills) {
28426
28654
  description: skill.modelInvocable ? `skill · ${skill.description}` : `skill (user only) · ${skill.description}`,
28427
28655
  origin: "skill"
28428
28656
  }));
28429
- const all = [
28657
+ const seen = /* @__PURE__ */ new Set();
28658
+ const all = [];
28659
+ for (const candidate of [
28430
28660
  ...local,
28431
28661
  ...registry,
28432
28662
  ...skillRows
28433
- ];
28434
- if (prefix === "") return all.slice(0, 11);
28435
- return all.filter((candidate) => candidate.label.slice(1).startsWith(prefix)).slice(0, 11);
28663
+ ]) {
28664
+ const name = candidate.label.slice(1);
28665
+ if (seen.has(name)) continue;
28666
+ seen.add(name);
28667
+ all.push(candidate);
28668
+ }
28669
+ if (prefix === "") return all;
28670
+ return all.filter((candidate) => candidate.label.slice(1).startsWith(prefix));
28436
28671
  }
28437
28672
  /**
28438
28673
  * The completion menu, rendered inside the composer's subtree directly above
@@ -28457,6 +28692,7 @@ function CompletionMenu({ active, mention, index, rows }) {
28457
28692
  const selected = rows.length === 0 ? 0 : index % rows.length;
28458
28693
  const first = selectionWindow(selected, rows.length, limit);
28459
28694
  const visible = rows.slice(first, first + limit);
28695
+ const hidden = rows.length - visible.length;
28460
28696
  return (0, import_react.createElement)(Box, {
28461
28697
  flexDirection: "column",
28462
28698
  marginLeft: 2,
@@ -28468,13 +28704,17 @@ function CompletionMenu({ active, mention, index, rows }) {
28468
28704
  const absolute = first + at;
28469
28705
  return (0, import_react.createElement)(Text, {
28470
28706
  key: candidate.label,
28471
- color: absolute === selected ? inkColor(TUI_RGB.brandBright) : inkColor(TUI_RGB.dim),
28707
+ color: absolute === selected ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
28472
28708
  wrap: "truncate-end"
28473
28709
  }, `${absolute === selected ? "❯ " : " "}${padColumns(candidate.label, nameWidth)}${dim(truncateColumns(displayText(candidate.description), descBudget))}`);
28474
- }), showFooter ? (0, import_react.createElement)(Text, {
28475
- dimColor: true,
28710
+ }), hidden > 0 ? (0, import_react.createElement)(Text, {
28711
+ key: "more",
28712
+ color: inkColor(getPalette().dim),
28476
28713
  wrap: "truncate-end"
28477
- }, dim(mention ? "↑↓ choose · tab insert" : "↑↓ choose · tab complete")) : void 0);
28714
+ }, dim(` … +${hidden} more`)) : void 0, showFooter ? (0, import_react.createElement)(Text, {
28715
+ color: inkColor(getPalette().dim),
28716
+ wrap: "truncate-end"
28717
+ }, dim(mention ? `↑↓ choose · ${rows.length} items · tab insert` : `↑↓ choose · ${rows.length} items · tab complete`)) : void 0);
28478
28718
  }
28479
28719
  /**
28480
28720
  * The prompt box: TUI-local slash commands handled locally, other lines
@@ -28482,7 +28722,7 @@ function CompletionMenu({ active, mention, index, rows }) {
28482
28722
  * While a modal (approval / question / model panel) owns the keys, the
28483
28723
  * box passes every key through untouched.
28484
28724
  */
28485
- function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openHelp, openMode, openResume, openPlugin, openStatusline, openHistory, createSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, cyclePermission, exportTranscript, renameTitle, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed }) {
28725
+ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openResume, openPlugin, openStatusline, openTheme, openHistory, createSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, cyclePermission, exportTranscript, renameTitle, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTick, waveTier, waveStyle }) {
28486
28726
  const columns = useStdout().stdout?.columns ?? 80;
28487
28727
  const [value, setValue] = (0, import_react.useState)("");
28488
28728
  const [cursor, setCursor] = (0, import_react.useState)(0);
@@ -28678,6 +28918,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
28678
28918
  openModel();
28679
28919
  return;
28680
28920
  }
28921
+ if (text === "/effort" || text.startsWith("/effort ")) {
28922
+ openEffort();
28923
+ return;
28924
+ }
28681
28925
  if (text === "/mode" || text.startsWith("/mode ")) {
28682
28926
  if (text.slice(5).trim() === "") openMode();
28683
28927
  else dispatch(text);
@@ -28704,6 +28948,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
28704
28948
  openStatusline();
28705
28949
  return;
28706
28950
  }
28951
+ if (text === "/theme") {
28952
+ openTheme();
28953
+ return;
28954
+ }
28707
28955
  if (text === "/history") {
28708
28956
  openHistory();
28709
28957
  return;
@@ -28818,27 +29066,118 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
28818
29066
  setDismissedMenuValue(void 0);
28819
29067
  }
28820
29068
  });
29069
+ const tierActive = waveTier !== null;
29070
+ const tierHues = waveTier === null ? null : deepseekWaveHues(waveTier);
29071
+ const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0]);
29072
+ const promptGlyph = waveTier === "flash" ? "›" : waveTier === "deepseek" ? "»" : "❯";
28821
29073
  if (frozen) {
28822
29074
  const frozen = value === "" ? "type a message" : verboseLine(value, Math.max(1, columns - 6));
28823
29075
  return (0, import_react.createElement)(Box, {
28824
29076
  width: Math.max(1, columns - 1),
28825
29077
  borderStyle: "round",
28826
- borderColor: inkColor(TUI_RGB.dim),
29078
+ borderColor: inkColor(getPalette().dim),
28827
29079
  paddingX: 1
28828
- }, (0, import_react.createElement)(Text, { wrap: "truncate-end" }, (0, import_react.createElement)(Text, { color: inkColor(TUI_RGB.brand) }, busy ? "… " : "❯ "), frozen));
29080
+ }, (0, import_react.createElement)(Text, { wrap: "truncate-end" }, (0, import_react.createElement)(Text, {
29081
+ color: promptColor,
29082
+ bold: tierActive ? true : void 0
29083
+ }, busy ? "… " : `${promptGlyph} `), frozen));
28829
29084
  }
28830
- const editor = editorWindow(value, cursor, Math.max(1, columns - 6));
28831
- return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(CompletionMenu, {
29085
+ const frame = (row, borderRgb) => (0, import_react.createElement)(Box, {
29086
+ width: Math.max(1, columns - 1),
29087
+ borderStyle: "round",
29088
+ borderColor: inkColor(borderRgb ?? getPalette().dim),
29089
+ paddingX: 1
29090
+ }, row);
29091
+ const menu = (0, import_react.createElement)(CompletionMenu, {
28832
29092
  active: menuActive,
28833
29093
  mention: mentionActive,
28834
29094
  index: completionIndex,
28835
29095
  rows: menuRows
28836
- }), (0, import_react.createElement)(Box, {
28837
- width: Math.max(1, columns - 1),
28838
- borderStyle: "round",
28839
- borderColor: inkColor(TUI_RGB.dim),
28840
- paddingX: 1
28841
- }, (0, import_react.createElement)(Text, { wrap: "truncate-end" }, busy ? (0, import_react.createElement)(BusyChase) : (0, import_react.createElement)(Text, { color: inkColor(TUI_RGB.brand) }, "❯ "), value === "" ? void 0 : editor.before, (0, import_react.createElement)(CursorBlock, { char: editor.caret }), value === "" && !busy ? (0, import_react.createElement)(Text, { dimColor: true }, "type a message · / commands · @ mentions") : editor.after)));
29096
+ });
29097
+ const editor = editorWindow(value, cursor, Math.max(1, columns - 6));
29098
+ const staticRow = (0, import_react.createElement)(Text, { wrap: "truncate-end" }, busy ? (0, import_react.createElement)(BusyChase) : (0, import_react.createElement)(Text, {
29099
+ color: promptColor,
29100
+ bold: tierActive ? true : void 0
29101
+ }, `${promptGlyph} `), value === "" ? void 0 : editor.before, (0, import_react.createElement)(CursorBlock, { char: editor.caret }), value === "" && !busy ? (0, import_react.createElement)(Text, { dimColor: true }, COMPOSER_PLACEHOLDER) : editor.after);
29102
+ const waveRow = () => {
29103
+ const contentWidth = Math.max(1, columns - 5);
29104
+ const waveEditor = editorWindow(value, cursor, Math.max(1, contentWidth - 2));
29105
+ const hues = deepseekWaveHues(waveTier);
29106
+ const style = waveStyle;
29107
+ const base = getTheme() === "light" ? WAVE_BASE_LIGHT : WAVE_BASE_DARK;
29108
+ const waveBg = (column) => {
29109
+ const rgb = deepseekWaveColumnBg(waveTick, column, contentWidth, waveTier, style, hues, base);
29110
+ return rgb === null ? void 0 : inkColor(rgb);
29111
+ };
29112
+ const cells = [];
29113
+ cells.push({
29114
+ char: promptGlyph,
29115
+ color: promptColor,
29116
+ bold: true,
29117
+ backgroundColor: waveBg(0)
29118
+ });
29119
+ cells.push({
29120
+ char: " ",
29121
+ color: promptColor,
29122
+ backgroundColor: waveBg(1)
29123
+ });
29124
+ for (const char of waveEditor.before) cells.push({
29125
+ char,
29126
+ backgroundColor: waveBg(cells.length)
29127
+ });
29128
+ cells.push({
29129
+ char: waveEditor.caret,
29130
+ inverse: true,
29131
+ backgroundColor: waveBg(cells.length)
29132
+ });
29133
+ if (value === "" && !busy) for (let at = 0; at < 40; at += 1) cells.push({
29134
+ char: COMPOSER_PLACEHOLDER[at],
29135
+ dim: true,
29136
+ backgroundColor: waveBg(cells.length)
29137
+ });
29138
+ else for (const char of waveEditor.after) cells.push({
29139
+ char,
29140
+ backgroundColor: waveBg(cells.length)
29141
+ });
29142
+ while (cells.length < contentWidth) cells.push({
29143
+ char: " ",
29144
+ backgroundColor: waveBg(cells.length)
29145
+ });
29146
+ if (deepseekWaveWordVisible(waveTick, waveTier, style)) {
29147
+ const word = "deepseek";
29148
+ const start = Math.max(2, Math.floor((contentWidth - 8) / 2));
29149
+ let clear = true;
29150
+ for (let at = 0; at < 8; at += 1) {
29151
+ const cell = cells[start + at];
29152
+ if (cell === void 0 || cell.char !== " " && cell.dim !== true) {
29153
+ clear = false;
29154
+ break;
29155
+ }
29156
+ }
29157
+ if (clear) for (let at = 0; at < 8; at += 1) {
29158
+ const cell = cells[start + at];
29159
+ cell.char = word[at];
29160
+ cell.color = inkColor(deepseekWaveWordHue(at, hues));
29161
+ cell.bold = true;
29162
+ cell.dim = false;
29163
+ }
29164
+ }
29165
+ if (waveTier === "deepseek" && style === "wave") {
29166
+ const spark = deepseekWaveSpark(waveTick);
29167
+ if (spark !== null) {
29168
+ const last = cells[cells.length - 1];
29169
+ if (last !== void 0 && last.char === " ") {
29170
+ last.char = spark;
29171
+ last.color = promptColor;
29172
+ last.bold = true;
29173
+ last.dim = false;
29174
+ }
29175
+ }
29176
+ }
29177
+ const borderRgb = deepseekWaveBorderColor(waveTick, waveTier, style, hues, getPalette().dim);
29178
+ return frame((0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...waveRowSpans(cells)), borderRgb);
29179
+ };
29180
+ return (0, import_react.createElement)(Box, { flexDirection: "column" }, menu, waveTick !== null && waveTier !== null && !busy ? waveRow() : frame(staticRow));
28842
29181
  }
28843
29182
  /** The whole terminal app; state arrives via the store, output via Ink. */
28844
29183
  function App(props) {
@@ -28847,6 +29186,60 @@ function App(props) {
28847
29186
  const skills = (0, import_react.useSyncExternalStore)(props.skills.subscribe, () => props.skills.rows);
28848
29187
  const [modelLabel, setModelLabel] = (0, import_react.useState)(props.model);
28849
29188
  const [modelOpen, setModelOpen] = (0, import_react.useState)(false);
29189
+ /** The model row whose effort levels the /model stage lists; undefined shows the model list. */
29190
+ const [effortFor, setEffortFor] = (0, import_react.useState)(void 0);
29191
+ /** Effective reasoning effort, shown in the /model picker and switch notice. */
29192
+ const [effortLabel, setEffortLabel] = (0, import_react.useState)(props.effort);
29193
+ /** DeepSeek easter egg: switching INTO an official DeepSeek route plays
29194
+ * one of Codex's three ignition styles (Wave / Aurora / Pulse, picked at
29195
+ * random without repeating) across the composer's padded band (33ms tick,
29196
+ * per-style durations), then the band returns to static while the prompt
29197
+ * marker keeps the tier accent. The trigger follows the applied model
29198
+ * label (what the status bar actually shows), never the initial paint,
29199
+ * and the tier is derived from the label and cached at the switch. */
29200
+ const [waveTick, setWaveTick] = (0, import_react.useState)(null);
29201
+ const [waveTier, setWaveTier] = (0, import_react.useState)(null);
29202
+ const [waveStyle, setWaveStyle] = (0, import_react.useState)(null);
29203
+ const previousModel = (0, import_react.useRef)(void 0);
29204
+ const previousEffort = (0, import_react.useRef)(props.effort);
29205
+ const previousStyle = (0, import_react.useRef)(void 0);
29206
+ (0, import_react.useEffect)(() => {
29207
+ const previous = previousModel.current;
29208
+ previousModel.current = modelLabel;
29209
+ const effortChanged = previousEffort.current !== effortLabel;
29210
+ previousEffort.current = effortLabel;
29211
+ const modelChanged = previous !== void 0 && previous !== modelLabel;
29212
+ if (!isOfficialDeepSeekLabel(modelLabel)) {
29213
+ setWaveTier(null);
29214
+ setWaveStyle(null);
29215
+ setWaveTick(null);
29216
+ return;
29217
+ }
29218
+ if (modelChanged || effortChanged) {
29219
+ setWaveTier(deepseekWaveTier(modelLabel));
29220
+ const nextStyle = deepseekWaveStyleRandom(previousStyle.current);
29221
+ previousStyle.current = nextStyle;
29222
+ setWaveStyle(nextStyle);
29223
+ setWaveTick(0);
29224
+ }
29225
+ }, [modelLabel, effortLabel]);
29226
+ const waveActive = waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 < deepseekWaveDuration(waveTier, waveStyle);
29227
+ (0, import_react.useEffect)(() => {
29228
+ if (!waveActive) return;
29229
+ const id = setInterval(() => {
29230
+ setWaveTick((current) => current === null ? 0 : current + 1);
29231
+ }, 33);
29232
+ return () => {
29233
+ clearInterval(id);
29234
+ };
29235
+ }, [waveActive]);
29236
+ (0, import_react.useEffect)(() => {
29237
+ if (waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 >= deepseekWaveDuration(waveTier, waveStyle)) setWaveTick(null);
29238
+ }, [
29239
+ waveTick,
29240
+ waveTier,
29241
+ waveStyle
29242
+ ]);
28850
29243
  const [directory, setDirectory] = (0, import_react.useState)(void 0);
28851
29244
  const [modelError, setModelError] = (0, import_react.useState)(void 0);
28852
29245
  const [modelLoadEpoch, setModelLoadEpoch] = (0, import_react.useState)(0);
@@ -28888,6 +29281,7 @@ function App(props) {
28888
29281
  const [pluginQuery, setPluginQuery] = (0, import_react.useState)("");
28889
29282
  const [statuslineOpen, setStatuslineOpen] = (0, import_react.useState)(false);
28890
29283
  const [statuslineItems, setStatuslineItems] = (0, import_react.useState)(() => parseStatuslineItems(props.statusline));
29284
+ const [themeOpen, setThemeOpen] = (0, import_react.useState)(false);
28891
29285
  const [historyOpen, setHistoryOpen] = (0, import_react.useState)(false);
28892
29286
  /** The /history panel's accepted entry: text plus its recall-space index. */
28893
29287
  const [historyFill, setHistoryFill] = (0, import_react.useState)(void 0);
@@ -28908,15 +29302,17 @@ function App(props) {
28908
29302
  const questionSnapshot = (0, import_react.useSyncExternalStore)(props.questions.subscribe, props.questions.getSnapshot);
28909
29303
  const approvalPending = approvalSnapshot.pending !== void 0;
28910
29304
  const questionPending = questionSnapshot.pending !== void 0;
28911
- const inputActive = !modelOpen && !helpOpen && !modeOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending;
29305
+ const inputActive = !modelOpen && !helpOpen && !modeOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending;
28912
29306
  (0, import_react.useEffect)(() => {
28913
29307
  if (!approvalPending && !questionPending) return;
28914
29308
  setModelOpen(false);
29309
+ setEffortFor(void 0);
28915
29310
  setHelpOpen(false);
28916
29311
  setModeOpen(false);
28917
29312
  setResumeOpen(false);
28918
29313
  setPluginOpen(false);
28919
29314
  setStatuslineOpen(false);
29315
+ setThemeOpen(false);
28920
29316
  setHistoryOpen(false);
28921
29317
  setVerboseOpen(false);
28922
29318
  }, [approvalPending, questionPending]);
@@ -28998,8 +29394,8 @@ function App(props) {
28998
29394
  const streamRows = Math.max(1, dynamicRows - visibleLiveLines.length);
28999
29395
  const reasoningRows = view.streamingReasoning === "" ? 0 : view.streaming === "" ? streamRows : streamRows <= 1 ? 0 : showReasoning ? Math.max(1, Math.floor(streamRows / 3)) : 1;
29000
29396
  const answerRows = view.streaming === "" ? 0 : Math.max(1, streamRows - reasoningRows);
29001
- const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending;
29002
- const modalVisible = modelOpen || helpOpen || modeOpen || resumeOpen || pluginOpen || statuslineOpen || historyOpen || verboseOpen && !approvalPending && !questionPending || approvalPending || questionPending;
29397
+ const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending;
29398
+ const modalVisible = modelOpen || helpOpen || modeOpen || resumeOpen || pluginOpen || statuslineOpen || themeOpen || historyOpen || verboseOpen && !approvalPending && !questionPending || approvalPending || questionPending;
29003
29399
  const closeInspector = (0, import_react.useCallback)(() => {
29004
29400
  setVerboseOpen(false);
29005
29401
  }, []);
@@ -29007,6 +29403,19 @@ function App(props) {
29007
29403
  if (appStdout !== void 0) appStdout.write("\x1B[2J\x1B[3J\x1B[H");
29008
29404
  setRefreshEpoch((epoch) => epoch + 1);
29009
29405
  };
29406
+ /** Apply one /model pick: record the selection, close the panel, report via notice. */
29407
+ const applyModel = (row, effortId) => {
29408
+ try {
29409
+ const label = props.selectModel(row, effortId);
29410
+ setModelLabel(label);
29411
+ setEffortLabel(effortId);
29412
+ notify(`model → next step uses ${label}${effortId === void 0 || effortId === "" ? "" : `@${effortId}`}`);
29413
+ setModelOpen(false);
29414
+ setEffortFor(void 0);
29415
+ } catch (error) {
29416
+ notify(`model switch failed: ${error instanceof Error ? error.message : String(error)}`, "error");
29417
+ }
29418
+ };
29010
29419
  return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(MemoStaticTranscript, {
29011
29420
  key: refreshEpoch,
29012
29421
  items: settledRows
@@ -29030,24 +29439,29 @@ function App(props) {
29030
29439
  }), (0, import_react.createElement)(ApprovalBar, {
29031
29440
  snapshot: approvalSnapshot,
29032
29441
  locked: questionPending
29033
- }), modelOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(ModelPanel, {
29442
+ }), modelOpen && !approvalPending && !questionPending ? effortFor === void 0 ? (0, import_react.createElement)(ModelPanel, {
29034
29443
  directory,
29035
29444
  error: modelError,
29036
29445
  onSelect: (row) => {
29037
- try {
29038
- setModelLabel(props.selectModel(row));
29039
- notify(`model → next step uses ${row.provider}/${row.model}`);
29040
- setModelOpen(false);
29041
- } catch (error) {
29042
- notify(`model switch failed: ${error instanceof Error ? error.message : String(error)}`, "error");
29446
+ if (row.reasoning !== void 0 && row.reasoning.efforts.length > 1) {
29447
+ setEffortFor(row);
29448
+ return;
29043
29449
  }
29450
+ const effortId = row.reasoning?.efforts.length === 1 ? row.reasoning.efforts[0].id : void 0;
29451
+ applyModel(row, effortId);
29044
29452
  },
29045
29453
  onRetry: () => {
29046
29454
  setModelLoadEpoch((epoch) => epoch + 1);
29047
29455
  },
29048
29456
  onClose: () => {
29049
29457
  setModelOpen(false);
29458
+ setEffortFor(void 0);
29050
29459
  }
29460
+ }) : (0, import_react.createElement)(EffortPanel, {
29461
+ row: effortFor,
29462
+ current: effortLabel,
29463
+ select: (effortId) => applyModel(effortFor, effortId),
29464
+ back: () => setEffortFor(void 0)
29051
29465
  }) : void 0, helpOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(HelpPanel, {
29052
29466
  descriptors,
29053
29467
  skills,
@@ -29089,6 +29503,15 @@ function App(props) {
29089
29503
  props.saveStatusline(items);
29090
29504
  },
29091
29505
  close: () => setStatuslineOpen(false)
29506
+ }) : void 0, themeOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(ThemePanel, {
29507
+ current: getTheme(),
29508
+ select: (name) => {
29509
+ setTheme(name);
29510
+ props.saveTheme?.(name);
29511
+ notify(`theme → ${name}`);
29512
+ setThemeOpen(false);
29513
+ },
29514
+ close: () => setThemeOpen(false)
29092
29515
  }) : void 0, historyOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(HistoryPanel, {
29093
29516
  entries: recallSpace,
29094
29517
  fill: (text, index) => {
@@ -29119,8 +29542,32 @@ function App(props) {
29119
29542
  openModel: () => {
29120
29543
  setDirectory(void 0);
29121
29544
  setModelError(void 0);
29545
+ setEffortFor(void 0);
29122
29546
  setModelOpen(true);
29123
29547
  },
29548
+ openEffort: () => {
29549
+ props.loadModels().then((loaded) => {
29550
+ const [provider, model] = modelLabel.split("/");
29551
+ const row = loaded.rows.find((candidate) => candidate.provider === provider && candidate.model === model) ?? loaded.rows.find((candidate) => candidate.model === model && candidate.reasoning !== void 0) ?? loaded.rows.find((candidate) => candidate.model === model);
29552
+ if (row === void 0) {
29553
+ notify("current model is not in the catalog", "warning");
29554
+ return;
29555
+ }
29556
+ const rowTag = `${row.provider}/${row.model}`;
29557
+ if (loaded.reasoningFailures?.includes(rowTag) === true) {
29558
+ notify("reasoning levels temporarily unavailable (capability lookup failed) — try again", "warning");
29559
+ return;
29560
+ }
29561
+ if (row.reasoning === void 0 || row.reasoning.efforts.length === 0) {
29562
+ notify("current model does not expose reasoning efforts", "warning");
29563
+ return;
29564
+ }
29565
+ setEffortFor(row);
29566
+ setModelOpen(true);
29567
+ }, (error) => {
29568
+ notify(`model lookup failed: ${error instanceof Error ? error.message : String(error)}`, "error");
29569
+ });
29570
+ },
29124
29571
  openHelp: () => {
29125
29572
  setHelpOpen(true);
29126
29573
  },
@@ -29131,6 +29578,7 @@ function App(props) {
29131
29578
  setPluginOpen(true);
29132
29579
  },
29133
29580
  openStatusline: () => setStatuslineOpen(true),
29581
+ openTheme: () => setThemeOpen(true),
29134
29582
  openHistory: () => setHistoryOpen(true),
29135
29583
  createSession: props.createSession,
29136
29584
  cancelSessionSwitch: props.cancelSessionSwitch,
@@ -29160,7 +29608,10 @@ function App(props) {
29160
29608
  queued: queuedRows,
29161
29609
  cancelQueued: props.cancelQueued,
29162
29610
  historyFill,
29163
- historyConsumed
29611
+ historyConsumed,
29612
+ waveTick,
29613
+ waveTier,
29614
+ waveStyle
29164
29615
  }), (0, import_react.createElement)(StatusLine, {
29165
29616
  facts: {
29166
29617
  model: modelLabel,
@@ -29346,11 +29797,66 @@ const internals = {
29346
29797
  };
29347
29798
  //#endregion
29348
29799
  //#region src/models.ts
29800
+ /** Map an adapter's reasoning capability onto the panel's plain-id shape. */
29801
+ function mapReasoning(reasoning) {
29802
+ return {
29803
+ efforts: reasoning.efforts.map((effort) => ({
29804
+ id: effort.id,
29805
+ name: effort.name,
29806
+ ...effort.description === void 0 ? {} : { description: effort.description }
29807
+ })),
29808
+ ...reasoning.defaultEffort === void 0 ? {} : { defaultEffort: reasoning.defaultEffort }
29809
+ };
29810
+ }
29811
+ /**
29812
+ * Resolve the effective model selection for one live session, in the
29813
+ * documented precedence: the in-process explicit pick, then the session's
29814
+ * last `request/header` config, then the deployment default.
29815
+ * @param picked - the explicit selection made in this process, when any.
29816
+ * @param logged - the last logged request header config, when any.
29817
+ * @param defaults - the deployment default selection.
29818
+ * @returns the effective selection, carrying a reasoning effort when one is in force.
29819
+ */
29820
+ function resolveEffectiveSelection(picked, logged, defaults) {
29821
+ if (picked !== void 0) return picked;
29822
+ if (logged !== void 0) return {
29823
+ provider: logged.provider,
29824
+ model: logged.model,
29825
+ ...logged.reasoningEffort === void 0 ? {} : { reasoningEffort: ReasoningEffortId(logged.reasoningEffort) }
29826
+ };
29827
+ return defaults;
29828
+ }
29829
+ /**
29830
+ * Build the selection one `/model` pick applies, rejecting an effort the
29831
+ * row does not advertise. The row is the picker's source of truth, so a
29832
+ * stale directory cannot smuggle an unsupported effort into the next step
29833
+ * (the request pipeline would reject it before network I/O regardless). The
29834
+ * empty string is the picker's "provider default" sentinel — an explicit
29835
+ * choice to leave the effort to the model's own default, exactly like an
29836
+ * absent effort.
29837
+ * @param row - the picked model row.
29838
+ * @param effortId - the chosen advertised effort, '' or undefined for the model default.
29839
+ * @returns the selection the runner records for the next assembled step.
29840
+ */
29841
+ function buildModelSelection(row, effortId) {
29842
+ const selected = effortId === void 0 || effortId === "" ? void 0 : effortId;
29843
+ if (selected !== void 0 && (row.reasoning === void 0 || !row.reasoning.efforts.some((effort) => effort.id === selected))) throw new Error(`model ${row.provider}/${row.model} does not support reasoning effort "${selected}"`);
29844
+ return {
29845
+ provider: row.provider,
29846
+ model: row.model,
29847
+ ...selected === void 0 ? {} : { reasoningEffort: ReasoningEffortId(selected) }
29848
+ };
29849
+ }
29349
29850
  /**
29350
29851
  * Load the selectable model directory from the live `ctx.llm` registry.
29351
29852
  * Providers are listed synchronously; each provider's models are discovered
29352
29853
  * with a bounded parallel fan-out whose failures degrade to that provider
29353
29854
  * contributing no rows (mirrors the web catalog's per-provider failures).
29855
+ * Each row's reasoning levels are resolved per exact model like the web
29856
+ * catalog (`buildModelCatalog`); a single model's capability lookup failure
29857
+ * degrades to that row having no effort picker rather than hiding the model,
29858
+ * and the failure rides `reasoningFailures` so the caller can tell "no
29859
+ * advertised reasoning" from "capability lookup failed".
29354
29860
  * @param ctx - context carrying the `llm` service.
29355
29861
  * @returns the resolved directory; empty rows when `llm` is unavailable.
29356
29862
  */
@@ -29358,21 +29864,39 @@ async function loadModelDirectory(ctx) {
29358
29864
  const llm = ctx.get("llm");
29359
29865
  if (llm === void 0) return {
29360
29866
  rows: [],
29361
- failures: []
29867
+ failures: [],
29868
+ reasoningFailures: []
29362
29869
  };
29870
+ const llmResolve = llm;
29363
29871
  const providers = llm.listProviders();
29364
29872
  const listed = await Promise.all(providers.map(async (provider) => {
29365
29873
  try {
29366
29874
  const models = await llm.listModels(provider.id);
29367
- return {
29368
- provider: provider.id,
29369
- providerName: provider.name,
29370
- models: models.map((model) => ({
29875
+ const reasoningFailures = [];
29876
+ const rows = await Promise.all(models.map(async (model) => {
29877
+ const row = {
29371
29878
  provider: provider.id,
29372
29879
  providerName: provider.name,
29373
29880
  model: model.id,
29374
29881
  modelName: model.name
29375
- }))
29882
+ };
29883
+ if (llmResolve.resolveModelInfo === void 0) return row;
29884
+ try {
29885
+ const resolved = await llmResolve.resolveModelInfo(provider.id, model.id);
29886
+ return resolved.reasoning === void 0 ? row : {
29887
+ ...row,
29888
+ reasoning: mapReasoning(resolved.reasoning)
29889
+ };
29890
+ } catch {
29891
+ reasoningFailures.push(`${provider.id}/${model.id}`);
29892
+ return row;
29893
+ }
29894
+ }));
29895
+ return {
29896
+ provider: provider.id,
29897
+ providerName: provider.name,
29898
+ models: rows,
29899
+ reasoningFailures
29376
29900
  };
29377
29901
  } catch {
29378
29902
  return {
@@ -29385,20 +29909,26 @@ async function loadModelDirectory(ctx) {
29385
29909
  }));
29386
29910
  return {
29387
29911
  rows: listed.flatMap((entry) => entry.models),
29388
- failures: listed.filter((entry) => "failed" in entry && entry.failed === true).map((entry) => entry.provider)
29912
+ failures: listed.filter((entry) => "failed" in entry && entry.failed === true).map((entry) => entry.provider),
29913
+ reasoningFailures: listed.flatMap((entry) => "failed" in entry ? [] : entry.reasoningFailures)
29389
29914
  };
29390
29915
  }
29391
29916
  //#endregion
29392
29917
  //#region src/mentions.ts
29393
29918
  /**
29394
- * Workspace @mention support: file candidates from a bounded async scan of
29395
- * the session cwd, session candidates from the opt-in `sessionReferenceResolver`
29396
- * service, and submission preparation through its `prepare()` API. Picked
29397
- * session mentions land as canonical `@[label](dsh-session:…)` tokens; on
29398
- * submit the text is parsed back into readable `@label` text plus structured
29399
- * references, snapshots are injected via `agent.inject()` before the readable
29400
- * message wakes the driver (`followup` idle, `steer` running) exactly the
29401
- * upstream README's wiring.
29919
+ * Workspace @mention support: file and directory candidates from a bounded
29920
+ * async scan of the session cwd, session candidates from the opt-in
29921
+ * `sessionReferenceResolver` service, and submission preparation through its
29922
+ * `prepare()` API. Picked session mentions land as canonical
29923
+ * `@[label](dsh-session:…)` tokens; on submit the text is parsed back into
29924
+ * readable `@label` text plus structured references, snapshots are injected
29925
+ * via `agent.inject()` before the readable message wakes the driver
29926
+ * (`followup` idle, `steer` running) — exactly the upstream README's wiring.
29927
+ *
29928
+ * Harness exposes no workspace-file mention service (only session references
29929
+ * plus a post-hoc produced-file linker), so the file index is the lightweight
29930
+ * bounded scan below, kept deliberately smaller than Codex's streaming
29931
+ * gitignore-aware walker.
29402
29932
  *
29403
29933
  * @module @deepseek-ai/dsh-code/mentions
29404
29934
  */
@@ -29414,7 +29944,16 @@ const SKIP_DIRS = /* @__PURE__ */ new Set([
29414
29944
  ]);
29415
29945
  const MAX_FILES = 4e3;
29416
29946
  const MAX_DEPTH = 12;
29417
- /** Bounded async BFS scan of a workspace; unreadable entries are skipped. */
29947
+ /** Empty-query default rows: proves the index exists without typing (Codex's
29948
+ * popups show something on a bare sigil too). */
29949
+ const EMPTY_QUERY_ROWS = 20;
29950
+ /**
29951
+ * Bounded async BFS scan of a workspace; unreadable entries are skipped.
29952
+ * Both files and directories are indexed (directories insert with a trailing
29953
+ * slash), mirroring Codex's `MatchType::{File,Directory}` index. Dotfiles and
29954
+ * the {@link SKIP_DIRS} list are excluded, which is a coarser filter than
29955
+ * Codex's gitignore-aware walker but stays dependency-free and bounded.
29956
+ */
29418
29957
  async function scanWorkspaceFiles(root, signal) {
29419
29958
  const found = [];
29420
29959
  const pending = [{
@@ -29438,6 +29977,10 @@ async function scanWorkspaceFiles(root, signal) {
29438
29977
  const relative = current.relative === "" ? entry.name : `${current.relative}/${entry.name}`;
29439
29978
  if (entry.isDirectory()) {
29440
29979
  if (SKIP_DIRS.has(entry.name) || current.depth + 1 > MAX_DEPTH) continue;
29980
+ found.push({
29981
+ path: relative,
29982
+ kind: "directory"
29983
+ });
29441
29984
  pending.push({
29442
29985
  absolute: join(current.absolute, entry.name),
29443
29986
  relative,
@@ -29474,38 +30017,47 @@ function scoreFile(path, query) {
29474
30017
  /**
29475
30018
  * Create the mention API for one agent's workspace. A missing
29476
30019
  * session-reference service degrades to file mentions only (the scan still
29477
- * works); `prepare` then passes text through untouched.
30020
+ * works); `prepare` then passes text through untouched. An undefined agent
30021
+ * (a bare launch before any session exists) also degrades to file-only
30022
+ * mentions, so `@` file completion works before the first message.
30023
+ *
30024
+ * `files` is hoisted into the closure so `candidates` never reaches for
30025
+ * `this` — the runner hands `mentions.candidates` to the input editor as a
30026
+ * detached callback, and a `this`-bound method would throw on every `@` key.
29478
30027
  * @param ctx - context carrying the optional `sessionReferenceResolver`.
29479
30028
  * @param agent - the session owner; excluded from its own candidates.
29480
30029
  * @param cwd - workspace root to scan.
29481
30030
  */
29482
30031
  function createMentions(ctx, agent, cwd) {
29483
30032
  const resolver = ctx.get("sessionReferenceResolver");
30033
+ const sessionCapable = agent !== void 0 && resolver !== void 0;
29484
30034
  let filesPromise;
30035
+ const files = () => {
30036
+ filesPromise ??= scanWorkspaceFiles(cwd);
30037
+ return filesPromise;
30038
+ };
29485
30039
  return {
29486
- files() {
29487
- filesPromise ??= scanWorkspaceFiles(cwd);
29488
- return filesPromise;
29489
- },
30040
+ files,
29490
30041
  async candidates(query, signal) {
29491
30042
  const needle = query.trim();
29492
- const [files, sessions] = await Promise.all([this.files(), resolver === void 0 ? Promise.resolve([]) : resolver.listCandidates(agent, needle, 10, signal).catch(() => [])]);
29493
- const fileRows = files.filter((candidate) => scoreFile(candidate.path, needle) > 0).sort((left, right) => scoreFile(right.path, needle) - scoreFile(left.path, needle)).slice(0, 20).map((candidate) => ({
30043
+ const [scanned, sessions] = await Promise.all([files(), sessionCapable && needle !== "" && agent !== void 0 ? resolver.listCandidates(agent, needle, 10, signal).catch(() => []) : Promise.resolve([])]);
30044
+ const fileRows = (needle === "" ? scanned.slice(0, EMPTY_QUERY_ROWS) : scanned.filter((candidate) => scoreFile(candidate.path, needle) > 0).sort((left, right) => scoreFile(right.path, needle) - scoreFile(left.path, needle)).slice(0, 20)).map((candidate) => ({
29494
30045
  label: candidate.path,
29495
30046
  description: candidate.kind === "directory" ? "Folder" : "File",
29496
30047
  kind: candidate.kind
29497
30048
  }));
29498
- return [...sessions.map((candidate) => ({
30049
+ const sessionRows = sessions.map((candidate) => ({
29499
30050
  label: formatSessionReferenceMention(candidate),
29500
30051
  description: `Session · ${candidate.cwd ?? "(no cwd)"}`,
29501
30052
  kind: "session"
29502
- })), ...fileRows];
30053
+ }));
30054
+ return [...fileRows, ...sessionRows];
29503
30055
  },
29504
30056
  parse(text) {
29505
30057
  return parseSessionReferenceText(text);
29506
30058
  },
29507
30059
  async prepare(parsed, signal) {
29508
- if (parsed.references.length === 0 || resolver === void 0) return {
30060
+ if (parsed.references.length === 0 || resolver === void 0 || agent === void 0) return {
29509
30061
  text: parsed.text,
29510
30062
  references: parsed.references
29511
30063
  };
@@ -29937,7 +30489,8 @@ const inject = [
29937
30489
  const Config = z.object({ startup: z.object({
29938
30490
  kind: z.string().required(),
29939
30491
  sessionId: z.string(),
29940
- mode: z.string()
30492
+ mode: z.string(),
30493
+ theme: z.string()
29941
30494
  }) });
29942
30495
  /** Report an unexpected direct-driver failure and request a failing exit. */
29943
30496
  function fail(io, error) {
@@ -30034,7 +30587,7 @@ async function run(ctx, startup, io) {
30034
30587
  /** Prepare a complete next session before disturbing the currently visible one. */
30035
30588
  const prepare = async (next) => {
30036
30589
  const nextCwd = next.cwd ?? cwd;
30037
- const selectionState = {};
30590
+ const selectionState = pendingSelection === void 0 ? {} : { picked: pendingSelection };
30038
30591
  let mode = next.mode;
30039
30592
  if (!next.resume) mode = (await presets.resolve(mode)).id;
30040
30593
  const setup = async (agentCtx) => {
@@ -30042,14 +30595,7 @@ async function run(ctx, startup, io) {
30042
30595
  mode = (await presets.mount(agentCtx, sessionPreset)).id;
30043
30596
  installModelSelection(agentCtx, {
30044
30597
  get current() {
30045
- if (selectionState.picked !== void 0) return selectionState.picked;
30046
- const logged = agentCtx.agent?.session.requestHeader()?.config;
30047
- if (logged !== void 0) return {
30048
- provider: logged.provider,
30049
- model: logged.model,
30050
- ...logged.reasoningEffort === void 0 ? {} : { reasoningEffort: logged.reasoningEffort }
30051
- };
30052
- return defaults;
30598
+ return resolveEffectiveSelection(selectionState.picked, agentCtx.agent?.session.requestHeader()?.config, defaults);
30053
30599
  },
30054
30600
  set current(value) {
30055
30601
  selectionState.picked = value;
@@ -30093,7 +30639,9 @@ async function run(ctx, startup, io) {
30093
30639
  let agent;
30094
30640
  let session;
30095
30641
  let store = createTranscriptStore();
30096
- let mentions;
30642
+ let mentions = createMentions(ctx, void 0, cwd);
30643
+ /** Explicit model pick made before any session exists (a bare launch). */
30644
+ let pendingSelection;
30097
30645
  if (!lazy) {
30098
30646
  const prepared = await prepare(await resolveTarget(startup, persistence, cwd));
30099
30647
  active = prepared;
@@ -30127,6 +30675,20 @@ async function run(ctx, startup, io) {
30127
30675
  bridge.notify("statusline save failed: " + (writeError instanceof Error ? writeError.message : String(writeError)), "error");
30128
30676
  });
30129
30677
  };
30678
+ const themePath = join(homedir(), ".dsh", "dsh-code", "theme.json");
30679
+ let themeWarning;
30680
+ if (startup.theme === void 0) try {
30681
+ setTheme(parseThemeName(JSON.parse(readFileSync(themePath, "utf8")).theme));
30682
+ } catch (error) {
30683
+ if (error.code !== "ENOENT") themeWarning = error instanceof Error ? error.message : String(error);
30684
+ }
30685
+ else setTheme(startup.theme);
30686
+ const saveTheme = (name) => {
30687
+ setTheme(name);
30688
+ mkdir(dirname(themePath), { recursive: true }).then(() => writeFile(themePath, JSON.stringify({ theme: name }, null, 2) + "\n", "utf8")).catch((writeError) => {
30689
+ bridge.notify("theme save failed: " + (writeError instanceof Error ? writeError.message : String(writeError)), "error");
30690
+ });
30691
+ };
30130
30692
  const historyPath = join(homedir(), ".dsh", "dsh-code", "history.jsonl");
30131
30693
  let inputHistory = [];
30132
30694
  try {
@@ -30276,6 +30838,7 @@ async function run(ctx, startup, io) {
30276
30838
  mentions = next.mentions;
30277
30839
  commands.setAgent(agent);
30278
30840
  skills.setAgent(agent);
30841
+ process.stdout.write("\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[3J\x1B[H");
30279
30842
  renderCurrent();
30280
30843
  const queued = pendingInputs.splice(0);
30281
30844
  for (const item of queued) deliverLine(item.text, item.mode);
@@ -30347,13 +30910,16 @@ async function run(ctx, startup, io) {
30347
30910
  return "";
30348
30911
  }
30349
30912
  };
30350
- /** Apply one /model selection: takes effect from the next assembled step. */
30351
- const selectModel = (row) => {
30352
- if (active === void 0) throw new Error("no session yet submit a message to start");
30353
- active.selection.picked = {
30354
- provider: row.provider,
30355
- model: row.model
30356
- };
30913
+ /**
30914
+ * Apply one /model selection: takes effect from the next assembled step.
30915
+ * The optional reasoning effort must be one the row advertises (the picker
30916
+ * only offers those), so an unsupported value cannot reach the request
30917
+ * pipeline; an absent effort restores the model's own default.
30918
+ */
30919
+ const selectModel = (row, effortId) => {
30920
+ const selection = buildModelSelection(row, effortId);
30921
+ if (active === void 0) pendingSelection = selection;
30922
+ else active.selection.picked = selection;
30357
30923
  return `${row.provider}/${row.model}`;
30358
30924
  };
30359
30925
  /**
@@ -30438,7 +31004,7 @@ async function run(ctx, startup, io) {
30438
31004
  agent = previous?.agent;
30439
31005
  session = previous?.session;
30440
31006
  store = previous === void 0 ? createTranscriptStore() : previous.store;
30441
- mentions = previous?.mentions;
31007
+ mentions = previous === void 0 ? createMentions(ctx, void 0, cwd) : previous.mentions;
30442
31008
  if (agent !== void 0) commands.setAgent(agent);
30443
31009
  if (agent !== void 0) skills.setAgent(agent);
30444
31010
  await next.handle.dispose();
@@ -30536,7 +31102,8 @@ async function run(ctx, startup, io) {
30536
31102
  };
30537
31103
  const appElement = () => {
30538
31104
  const sessionCwd = session?.header.cwd ?? cwd;
30539
- const model = store.getView().model !== "" ? store.getView().model : `${defaults.provider}/${defaults.model}`;
31105
+ const model = store.getView().model !== "" ? store.getView().model : pendingSelection !== void 0 ? `${pendingSelection.provider}/${pendingSelection.model}` : `${defaults.provider}/${defaults.model}`;
31106
+ const effort = resolveEffectiveSelection(active?.selection.picked ?? pendingSelection, session?.requestHeader()?.config, defaults).reasoningEffort;
30540
31107
  return (0, import_react.createElement)(App, {
30541
31108
  key: session?.id ?? "pending",
30542
31109
  store,
@@ -30545,6 +31112,7 @@ async function run(ctx, startup, io) {
30545
31112
  commands,
30546
31113
  skills,
30547
31114
  model,
31115
+ effort,
30548
31116
  cwd: basename(sessionCwd),
30549
31117
  workspaceRoot: sessionCwd,
30550
31118
  branch: gitBranch(sessionCwd),
@@ -30556,7 +31124,7 @@ async function run(ctx, startup, io) {
30556
31124
  interrupt,
30557
31125
  quit,
30558
31126
  loadModels: () => loadModelDirectory(ctx),
30559
- loadMentions: mentions === void 0 ? () => Promise.resolve([]) : mentions.candidates,
31127
+ loadMentions: (query, signal) => mentions.candidates(query, signal),
30560
31128
  cyclePermission,
30561
31129
  selectModel,
30562
31130
  exportTranscript,
@@ -30571,6 +31139,7 @@ async function run(ctx, startup, io) {
30571
31139
  loadPlugins: () => listPluginRows(ctx),
30572
31140
  statusline: statuslineItems,
30573
31141
  saveStatusline,
31142
+ saveTheme,
30574
31143
  history: inputHistory,
30575
31144
  recordHistory,
30576
31145
  cancelQueued,
@@ -30586,6 +31155,9 @@ async function run(ctx, startup, io) {
30586
31155
  if (statuslineWarning !== void 0) setTimeout(() => {
30587
31156
  bridge.notify("statusline config unreadable, using defaults: " + statuslineWarning, "warning");
30588
31157
  }, 50);
31158
+ if (themeWarning !== void 0) setTimeout(() => {
31159
+ bridge.notify("theme config unreadable, using dark: " + themeWarning, "warning");
31160
+ }, 50);
30589
31161
  }
30590
31162
  /**
30591
31163
  * Mount the interactive terminal driver.
@@ -30593,16 +31165,23 @@ async function run(ctx, startup, io) {
30593
31165
  * @param config - validated startup config resolved from the tuiStartup provider.
30594
31166
  */
30595
31167
  function apply(ctx, config) {
31168
+ const theme = config.startup.theme === void 0 ? void 0 : parseThemeName(config.startup.theme);
30596
31169
  const startup = config.startup.kind === "resume" && config.startup.sessionId !== void 0 ? {
30597
31170
  kind: "resume",
30598
- sessionId: config.startup.sessionId
30599
- } : config.startup.kind === "latest" ? { kind: "latest" } : config.startup.kind === "named" && config.startup.sessionId !== void 0 ? {
31171
+ sessionId: config.startup.sessionId,
31172
+ ...theme === void 0 ? {} : { theme }
31173
+ } : config.startup.kind === "latest" ? {
31174
+ kind: "latest",
31175
+ ...theme === void 0 ? {} : { theme }
31176
+ } : config.startup.kind === "named" && config.startup.sessionId !== void 0 ? {
30600
31177
  kind: "named",
30601
31178
  sessionId: config.startup.sessionId,
30602
- ...config.startup.mode === void 0 ? {} : { mode: config.startup.mode }
31179
+ ...config.startup.mode === void 0 ? {} : { mode: config.startup.mode },
31180
+ ...theme === void 0 ? {} : { theme }
30603
31181
  } : {
30604
31182
  kind: "fresh",
30605
- ...config.startup.mode === void 0 ? {} : { mode: config.startup.mode }
31183
+ ...config.startup.mode === void 0 ? {} : { mode: config.startup.mode },
31184
+ ...theme === void 0 ? {} : { theme }
30606
31185
  };
30607
31186
  const exit = ctx.get("appExit");
30608
31187
  if (exit === void 0) throw new Error("tui-runner: the launcher must provide ctx.appExit before the tree mounts");