dsh-code 0.6.1 → 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 {
@@ -25685,16 +25514,6 @@ function renderMarkdown(text, width) {
25685
25514
  }
25686
25515
  //#endregion
25687
25516
  //#region src/render/animations.ts
25688
- /**
25689
- * Terminal animation frame tables derived from the web design language:
25690
- * the StateDot "ongoing" pixel chase (3×3 ring, 125ms flat-hold brightness
25691
- * steps, 1s cycle) becomes the single-cell stepped pulse below and the
25692
- * full-ring clockwise braille chase in {@link BUSY_CHASE_FRAMES}, and the
25693
- * streaming caret blink is the Claude-Code convention. Pure functions only —
25694
- * the Ink layer owns timers and colors.
25695
- *
25696
- * @module @deepseek-ai/dsh-code/render/animations
25697
- */
25698
25517
  /** Single-cell stepped pulse: flat holds mirroring the web's 125ms keyframes. */
25699
25518
  const PULSE_FRAMES = [
25700
25519
  "█",
@@ -25733,182 +25552,329 @@ function busyChaseFrame(tick) {
25733
25552
  function caretVisible(tick) {
25734
25553
  return tick % 2 === 0;
25735
25554
  }
25736
- //#endregion
25737
- //#region src/render/inspector.ts
25738
- /** Composer (3) + two-row status chrome, plus one optional fixed-height local notice row. */
25739
- const INSPECTOR_CHROME_ROWS = 6;
25740
- /** One transcript-to-composer gutter, collapsed on short terminals. */
25741
- function layoutGutterRows(rows) {
25742
- return Math.max(1, Math.floor(rows)) >= 14 ? 1 : 0;
25743
- }
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
+ };
25744
25631
  /**
25745
- * Keep the inspector plus its persistent status/composer chrome below
25746
- * `stdout.rows`: at equality Ink clears the terminal and rewrites all
25747
- * 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.
25748
25637
  */
25749
- function panelViewport(columns, rows) {
25750
- const safeColumns = Math.max(1, Math.floor(columns));
25751
- const safeRows = Math.max(1, Math.floor(rows));
25752
- const maxHeight = Math.max(0, Math.min(safeRows - 2 - INSPECTOR_CHROME_ROWS - layoutGutterRows(safeRows), Math.floor(safeRows / 2)));
25753
- const compact = maxHeight < 5 || safeColumns < 8;
25754
- const gapRows = !compact && maxHeight >= 7 ? 2 : 0;
25755
- const outerColumns = compact ? safeColumns : Math.max(1, safeColumns - 1);
25756
- return {
25757
- maxHeight,
25758
- bodyRows: compact ? 0 : maxHeight - 4 - gapRows,
25759
- gapRows,
25760
- contentColumns: compact ? Math.max(1, safeColumns - 1) : Math.max(1, outerColumns - 4),
25761
- outerColumns,
25762
- compact
25763
- };
25764
- }
25765
- /** Backward-compatible name for the Ctrl+O-specific caller and tests. */
25766
- function inspectorViewport(columns, rows) {
25767
- return panelViewport(columns, rows);
25768
- }
25769
- /** Clamp a first-visible row to the range representable by one viewport. */
25770
- function clampScroll(offset, totalRows, visibleRows) {
25771
- const last = Math.max(0, Math.max(0, Math.floor(totalRows)) - Math.max(0, Math.floor(visibleRows)));
25772
- return Math.max(0, Math.min(Math.floor(offset), last));
25773
- }
25774
- /** Move a viewport by a signed row delta without escaping its content. */
25775
- function moveScroll(offset, delta, totalRows, visibleRows) {
25776
- 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
+ }
25777
25644
  }
25778
- /** Keep one focused row visible while preserving the current window when possible. */
25779
- function revealRow(offset, row, totalRows, visibleRows) {
25780
- const size = Math.max(1, Math.floor(visibleRows));
25781
- const target = Math.max(0, Math.min(Math.floor(row), Math.max(0, totalRows - 1)));
25782
- if (target < offset) return clampScroll(target, totalRows, size);
25783
- if (target >= offset + size) return clampScroll(target - size + 1, totalRows, size);
25784
- 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";
25785
25654
  }
25786
- /** Center a selected list row where possible, clamped at both ends. */
25787
- function selectionWindow(cursor, totalRows, visibleRows) {
25788
- 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";
25789
25680
  }
25790
- /** Follow appended history only while the inspector cursor was at the tail. */
25791
- function followInspectorCursor(cursor, previousLength, nextLength) {
25792
- const nextLast = Math.max(0, nextLength - 1);
25793
- if (cursor >= Math.max(0, previousLength - 1)) return nextLast;
25794
- 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));
25795
25690
  }
25796
- //#endregion
25797
- //#region src/render/text.ts
25798
25691
  /**
25799
- * Display-boundary sanitization for externally sourced text (model output,
25800
- * tool payloads, skill descriptions). Control characters including ANSI
25801
- * CSI/OSC escape sequences would otherwise pass through Ink into the
25802
- * terminal, letting output rewrite the screen or inject prompts. Newlines
25803
- * and tabs survive; everything else in C0/C1 plus DEL becomes a visible
25804
- * `\xNN` escape.
25805
- *
25806
- * @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.
25807
25696
  */
25808
- /** C0 controls except tab (0x09) and newline (0x0a), plus DEL and C1. */
25809
- 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
+ }
25810
25703
  /**
25811
- * Escape control characters so externally sourced text cannot drive the
25812
- * terminal.
25813
- * @param text - raw text from a session event, tool payload, or catalog.
25814
- * @returns text with every control character (except `\n`, `\t`) rendered
25815
- * 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.
25816
25713
  */
25817
- function displayText(text) {
25818
- 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);
25819
25719
  }
25820
- /** Collapse external text to one terminal-safe logical row. */
25821
- function singleLineText(text) {
25822
- 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
+ }
25823
25755
  }
25824
- /** Terminal-cell width matching the TUI's existing CJK-aware wrapping rule. */
25825
- function cellWidth(text) {
25826
- let columns = 0;
25827
- for (const char of text) columns += (char.codePointAt(0) ?? 0) > 11903 ? 2 : 1;
25828
- 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
+ ];
25829
25763
  }
25830
25764
  /**
25831
- * Truncate one display-safe row without ever exceeding its physical-column
25832
- * budget. The ellipsis is included inside the budget, matching Codex's popup
25833
- * truncation contract; the previous app-local helper appended it after the
25834
- * 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.
25835
25781
  */
25836
- function truncateColumns(text, columns) {
25837
- const limit = Math.max(0, Math.floor(columns));
25838
- if (limit === 0) return "";
25839
- if (cellWidth(text) <= limit) return text;
25840
- const contentLimit = limit - 1;
25841
- let used = 0;
25842
- let result = "";
25843
- for (const char of text) {
25844
- const width = cellWidth(char);
25845
- if (used + width > contentLimit) break;
25846
- result += char;
25847
- 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);
25848
25794
  }
25849
- return `${result}…`;
25850
- }
25851
- /** Read one Unicode character immediately before `end`. */
25852
- function previousCharacter(text, end) {
25853
- const last = text.charCodeAt(end - 1);
25854
- if (last >= 56320 && last <= 57343 && end >= 2) {
25855
- const first = text.charCodeAt(end - 2);
25856
- if (first >= 55296 && first <= 56319) return {
25857
- char: text.slice(end - 2, end),
25858
- start: end - 2
25859
- };
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];
25860
25804
  }
25861
- return {
25862
- char: text.slice(end - 1, end),
25863
- start: end - 1
25864
- };
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;
25865
25826
  }
25866
25827
  /**
25867
- * Keep only the newest display-safe text that fits a terminal rectangle.
25868
- * The scan walks backward and stops as soon as the suffix is full, so a long
25869
- * reasoning stream does not rescan its entire accumulated prefix per chunk.
25870
- * Explicit newlines and terminal wrapping both consume rows.
25871
- * @param text - raw externally sourced text.
25872
- * @param columns - available terminal columns.
25873
- * @param rows - available terminal rows.
25874
- * @returns a sanitized bounded suffix and whether an earlier prefix was cut.
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.
25875
25837
  */
25876
- function displayTail(text, columns, rows) {
25877
- const columnLimit = Math.max(1, Math.floor(columns));
25878
- const rowLimit = Math.max(1, Math.floor(rows));
25879
- const reversed = [];
25880
- let row = 1;
25881
- let used = 0;
25882
- let end = text.length;
25883
- while (end > 0) {
25884
- const previous = previousCharacter(text, end);
25885
- if (previous.char === "\n") {
25886
- if (row >= rowLimit) break;
25887
- reversed.push("\n");
25888
- row += 1;
25889
- used = 0;
25890
- end = previous.start;
25891
- continue;
25892
- }
25893
- const safe = displayText(previous.char);
25894
- const width = cellWidth(safe);
25895
- if (used > 0 && used + width > columnLimit) {
25896
- if (row >= rowLimit) break;
25897
- reversed.push("\n");
25898
- row += 1;
25899
- used = 0;
25900
- }
25901
- const extraRows = Math.floor(Math.max(0, width - 1) / columnLimit);
25902
- if (row + extraRows > rowLimit) break;
25903
- row += extraRows;
25904
- reversed.push(safe);
25905
- used = extraRows === 0 ? used + width : width - extraRows * columnLimit;
25906
- end = previous.start;
25907
- }
25908
- return {
25909
- text: reversed.reverse().join(""),
25910
- truncated: end > 0
25911
- };
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);
25842
+ }
25843
+ /**
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.
25851
+ */
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");
25912
25878
  }
25913
25879
  //#endregion
25914
25880
  //#region src/render/status.ts
@@ -25969,39 +25935,152 @@ function cacheHitPercent(usage) {
25969
25935
  const STATUS_ITEM_SEPARATOR = " · ";
25970
25936
  /** The Codex-style mode cycle hint appended to the permission badge. */
25971
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
+ }
25972
26032
  /**
25973
- * Render a context-occupancy percent as a bracketed fixed-width progress bar
25974
- * plus the percentage: `[▰▰▰▱▱▱▱▱▱▱] 25%`. Filled cells and the percent read
25975
- * in brand blue (accent), empty cells and the brackets read dim (label), and
25976
- * the whole meter flips to one amber warning once occupancy reaches the
25977
- * warning threshold. The bar fill clamps to 100 while the printed percent
25978
- * keeps the raw value so an over-budget session reads as such.
25979
- * @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.
25980
26046
  * @returns tone-split spans for the footer to paint.
25981
26047
  */
25982
- function contextBar(percent) {
25983
- const clamped = Math.max(0, Math.min(100, Math.round(percent)));
25984
- const filled = Math.round(clamped / 100 * 10);
25985
- const tone = clamped >= 90 ? "warn" : "accent";
25986
- const percentText = " " + Math.max(0, Math.min(999, Math.round(percent))) + "%";
25987
- const spans = [{
25988
- text: "[",
25989
- tone: "label"
25990
- }];
25991
- if (filled > 0) spans.push({
25992
- text: "".repeat(filled),
25993
- tone
25994
- });
25995
- if (filled < 10) spans.push({
25996
- 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),
25997
26079
  tone: "label"
25998
26080
  });
25999
- spans.push({
26000
- text: "]",
26001
- tone: "label"
26002
- }, {
26003
- text: percentText,
26004
- tone
26081
+ if (readout !== "") spans.push({
26082
+ text: readout,
26083
+ tone: readoutTone
26005
26084
  });
26006
26085
  return spans;
26007
26086
  }
@@ -26185,10 +26264,13 @@ function buildCandidates(facts, stats, busy, enabled) {
26185
26264
  identity.push(span);
26186
26265
  };
26187
26266
  const model = safe(facts.model);
26188
- if (model !== "" && enabled.has("model")) push({
26189
- text: model,
26190
- tone: "model"
26191
- });
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
+ }
26192
26274
  const cwd = safe(facts.cwd);
26193
26275
  if (cwd !== "" && enabled.has("cwd")) push({
26194
26276
  text: cwd,
@@ -26291,7 +26373,7 @@ function buildCandidates(facts, stats, busy, enabled) {
26291
26373
  group: { spans: [{
26292
26374
  text: "context ",
26293
26375
  tone: "label"
26294
- }, ...contextBar(stats.lastPromptTokens / stats.contextWindow * 100)] },
26376
+ }, ...contextBar(stats.contextSegments, stats.lastPromptTokens, stats.contextWindow, 24)] },
26295
26377
  rank: RANK2_CONTEXT,
26296
26378
  id: "context"
26297
26379
  });
@@ -26595,9 +26677,6 @@ function transcriptEntryLines(entry, columns) {
26595
26677
  //#endregion
26596
26678
  //#region src/kernel-panels.ts
26597
26679
  /** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
26598
- function color(rgb) {
26599
- return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
26600
- }
26601
26680
  function ListFrame(props) {
26602
26681
  const stdout = useStdout().stdout;
26603
26682
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
@@ -26619,11 +26698,11 @@ function ListFrame(props) {
26619
26698
  return (0, import_react.createElement)(Box, {
26620
26699
  width: viewport.outerColumns,
26621
26700
  borderStyle: "round",
26622
- borderColor: color(TUI_RGB.dim),
26701
+ borderColor: inkColor(getPalette().dim),
26623
26702
  flexDirection: "column",
26624
26703
  paddingX: 1
26625
26704
  }, (0, import_react.createElement)(Text, {
26626
- color: color(TUI_RGB.brandBright),
26705
+ color: inkColor(getPalette().brandBright),
26627
26706
  wrap: "truncate-end"
26628
26707
  }, truncateColumns(props.title, viewport.contentColumns)), (0, import_react.createElement)(Text, {
26629
26708
  dimColor: true,
@@ -26633,7 +26712,7 @@ function ListFrame(props) {
26633
26712
  const selected = !props.loading && props.error === void 0 && props.rows.length > 0 && absolute === props.cursor;
26634
26713
  return (0, import_react.createElement)(Text, {
26635
26714
  key: row.key,
26636
- 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,
26637
26716
  dimColor: row.disabled,
26638
26717
  wrap: "truncate-end"
26639
26718
  }, truncateColumns(`${selected ? "› " : " "}${row.text}`, viewport.contentColumns));
@@ -26865,11 +26944,11 @@ function DocumentPanel({ title, text, error, close }) {
26865
26944
  return (0, import_react.createElement)(Box, {
26866
26945
  width: viewport.outerColumns,
26867
26946
  borderStyle: "round",
26868
- borderColor: color(TUI_RGB.dim),
26947
+ borderColor: inkColor(getPalette().dim),
26869
26948
  flexDirection: "column",
26870
26949
  paddingX: 1
26871
26950
  }, (0, import_react.createElement)(Text, {
26872
- color: color(TUI_RGB.brandBright),
26951
+ color: inkColor(getPalette().brandBright),
26873
26952
  wrap: "truncate-end"
26874
26953
  }, truncateColumns(title, viewport.contentColumns)), ...body.map((line, index) => (0, import_react.createElement)(Text, {
26875
26954
  key: `${scroll}-${index}`,
@@ -26921,11 +27000,11 @@ function HistoryPanel({ entries, fill, close }) {
26921
27000
  return (0, import_react.createElement)(Box, {
26922
27001
  width: viewport.outerColumns,
26923
27002
  borderStyle: "round",
26924
- borderColor: color(TUI_RGB.dim),
27003
+ borderColor: inkColor(getPalette().dim),
26925
27004
  flexDirection: "column",
26926
27005
  paddingX: 1
26927
27006
  }, (0, import_react.createElement)(Text, {
26928
- color: color(TUI_RGB.brandBright),
27007
+ color: inkColor(getPalette().brandBright),
26929
27008
  wrap: "truncate-end"
26930
27009
  }, truncateColumns(header, viewport.contentColumns)), (0, import_react.createElement)(Text, {
26931
27010
  dimColor: true,
@@ -26939,7 +27018,7 @@ function HistoryPanel({ entries, fill, close }) {
26939
27018
  const selected = absolute === cursor;
26940
27019
  return (0, import_react.createElement)(Text, {
26941
27020
  key: `history-${absolute}`,
26942
- color: selected ? color(TUI_RGB.brandBright) : void 0,
27021
+ color: selected ? inkColor(getPalette().brandBright) : void 0,
26943
27022
  wrap: "truncate-end"
26944
27023
  }, truncateColumns((selected ? "› " : " ") + displayText(entry), viewport.contentColumns));
26945
27024
  }), (0, import_react.createElement)(Text, {
@@ -27007,18 +27086,18 @@ function StatuslinePanel({ enabled, change, close }) {
27007
27086
  return (0, import_react.createElement)(Box, {
27008
27087
  width: viewport.outerColumns,
27009
27088
  borderStyle: "round",
27010
- borderColor: color(TUI_RGB.dim),
27089
+ borderColor: inkColor(getPalette().dim),
27011
27090
  flexDirection: "column",
27012
27091
  paddingX: 1
27013
27092
  }, (0, import_react.createElement)(Text, {
27014
- color: color(TUI_RGB.brandBright),
27093
+ color: inkColor(getPalette().brandBright),
27015
27094
  wrap: "truncate-end"
27016
27095
  }, truncateColumns("/statusline · items apply to the live status line below", viewport.contentColumns)), ...visible.map((id, index) => {
27017
27096
  const selected = offset + index === cursor;
27018
27097
  const info = meta.get(id);
27019
27098
  return (0, import_react.createElement)(Text, {
27020
27099
  key: id,
27021
- color: selected ? color(TUI_RGB.brandBright) : void 0,
27100
+ color: selected ? inkColor(getPalette().brandBright) : void 0,
27022
27101
  dimColor: !on.has(id) || void 0,
27023
27102
  wrap: "truncate-end"
27024
27103
  }, truncateColumns((selected ? "› " : " ") + (on.has(id) ? "● " : "○ ") + (info?.label ?? id) + (info === void 0 ? "" : " · " + info.description + " · " + info.side), viewport.contentColumns));
@@ -27027,6 +27106,56 @@ function StatuslinePanel({ enabled, change, close }) {
27027
27106
  wrap: "truncate-end"
27028
27107
  }, truncateColumns("↑↓ move · space toggle · ←→ reorder · d default · esc close", viewport.contentColumns)));
27029
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
+ }
27030
27159
  /** Encode one entry for the history file (JSON keeps multi-line drafts intact). */
27031
27160
  function serializeHistoryEntry(text) {
27032
27161
  return JSON.stringify(text);
@@ -27176,10 +27305,6 @@ function recallNewer(state) {
27176
27305
  const RESIZE_REFLOW_DELAY_MS = 75;
27177
27306
  /** Reset region/style, clear the visible screen and scrollback, then home. */
27178
27307
  const RESIZE_REFLOW_CLEAR = "\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[3J\x1B[H";
27179
- /** Ink `color` string for one palette triple. */
27180
- function inkColor(triple) {
27181
- return `rgb(${triple[0]}, ${triple[1]}, ${triple[2]})`;
27182
- }
27183
27308
  /** Pad text with spaces to a visible-column target (menu name column). */
27184
27309
  function padColumns(text, width) {
27185
27310
  const clipped = truncateColumns(singleLineText(text), width);
@@ -27213,7 +27338,7 @@ function useStableInput(handler, active) {
27213
27338
  /** Single-cell stepped pulse: the web's 125ms flat-hold brightness steps over 1s. */
27214
27339
  function Pulse() {
27215
27340
  const tick = useFrames(125);
27216
- 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));
27217
27342
  }
27218
27343
  /**
27219
27344
  * The web StateDot "ongoing" chase in terminal form: three cells of the 3×3
@@ -27223,7 +27348,7 @@ function Pulse() {
27223
27348
  */
27224
27349
  function BusyChase() {
27225
27350
  const tick = useFrames(125);
27226
- 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) + " ");
27227
27352
  }
27228
27353
  /** Blinking block caret appended to streaming text. */
27229
27354
  function Caret() {
@@ -27273,19 +27398,19 @@ function StreamTail({ text, dim, maxRows, prefix, children }) {
27273
27398
  function segmentProps(style) {
27274
27399
  switch (style) {
27275
27400
  case "accent": return {
27276
- color: inkColor(TUI_RGB.brandBright),
27401
+ color: inkColor(getPalette().brandBright),
27277
27402
  bold: void 0,
27278
27403
  italic: void 0,
27279
27404
  strikethrough: void 0
27280
27405
  };
27281
27406
  case "code": return {
27282
- color: inkColor(TUI_RGB.code),
27407
+ color: inkColor(getPalette().code),
27283
27408
  bold: void 0,
27284
27409
  italic: void 0,
27285
27410
  strikethrough: void 0
27286
27411
  };
27287
27412
  case "dim": return {
27288
- color: inkColor(TUI_RGB.dim),
27413
+ color: inkColor(getPalette().dim),
27289
27414
  bold: void 0,
27290
27415
  italic: void 0,
27291
27416
  strikethrough: void 0
@@ -27309,7 +27434,7 @@ function segmentProps(style) {
27309
27434
  strikethrough: void 0
27310
27435
  };
27311
27436
  case "strike": return {
27312
- color: inkColor(TUI_RGB.dim),
27437
+ color: inkColor(getPalette().dim),
27313
27438
  bold: void 0,
27314
27439
  italic: void 0,
27315
27440
  strikethrough: true
@@ -27326,28 +27451,28 @@ function segmentProps(style) {
27326
27451
  function lineStyleProps(style) {
27327
27452
  switch (style) {
27328
27453
  case "brand": return {
27329
- color: inkColor(TUI_RGB.brandBright),
27454
+ color: inkColor(getPalette().brandBright),
27330
27455
  bold: void 0,
27331
27456
  italic: void 0,
27332
27457
  strikethrough: void 0,
27333
27458
  dimColor: void 0
27334
27459
  };
27335
27460
  case "success": return {
27336
- color: inkColor(TUI_RGB.success),
27461
+ color: inkColor(getPalette().success),
27337
27462
  bold: void 0,
27338
27463
  italic: void 0,
27339
27464
  strikethrough: void 0,
27340
27465
  dimColor: void 0
27341
27466
  };
27342
27467
  case "error": return {
27343
- color: inkColor(TUI_RGB.error),
27468
+ color: inkColor(getPalette().error),
27344
27469
  bold: void 0,
27345
27470
  italic: void 0,
27346
27471
  strikethrough: void 0,
27347
27472
  dimColor: void 0
27348
27473
  };
27349
27474
  case "warn": return {
27350
- color: inkColor(TUI_RGB.warn),
27475
+ color: inkColor(getPalette().warn),
27351
27476
  bold: void 0,
27352
27477
  italic: void 0,
27353
27478
  strikethrough: void 0,
@@ -27412,7 +27537,7 @@ function ToolDetailBody({ detail }) {
27412
27537
  wrap: "truncate-end"
27413
27538
  }, ` ── ${displayText(diff.path)}${diff.truncated ? " (diff truncated)" : ""}`), ...diff.lines.map((line, at) => (0, import_react.createElement)(Text, {
27414
27539
  key: at,
27415
- 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),
27416
27541
  wrap: "truncate-end"
27417
27542
  }, ` ${line.mark}${displayText(line.text)}`)))));
27418
27543
  case "read": return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(Text, {
@@ -27451,16 +27576,16 @@ function EntryLine({ entry, showReasoning, verbose }) {
27451
27576
  indent: 2
27452
27577
  }));
27453
27578
  case "tool": {
27454
- 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) }, "⏺");
27455
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, {
27456
- color: entry.state === "error" ? inkColor(TUI_RGB.error) : inkColor(TUI_RGB.dim),
27581
+ color: entry.state === "error" ? inkColor(getPalette().error) : inkColor(getPalette().dim),
27457
27582
  wrap: verbose ? "truncate-end" : void 0
27458
27583
  }, ` ⎿ ${displayText(entry.summary)}`), verbose && entry.detail !== void 0 ? (0, import_react.createElement)(ToolDetailBody, { detail: entry.detail }) : void 0);
27459
27584
  }
27460
27585
  case "command": {
27461
- 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) }, "⏺");
27462
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, {
27463
- color: inkColor(TUI_RGB.dim),
27588
+ color: inkColor(getPalette().dim),
27464
27589
  wrap: verbose ? "truncate-end" : void 0
27465
27590
  }, ` ⎿ ${displayText(entry.summary)}`));
27466
27591
  }
@@ -27473,7 +27598,7 @@ function EntryLine({ entry, showReasoning, verbose }) {
27473
27598
  wrap: verbose ? "truncate-end" : void 0
27474
27599
  }, entry.ok ? ` ⧉ compacted ~${formatTokens(entry.tokens)} tokens` : ` ⧉ compaction failed: ${displayText(entry.error)}`);
27475
27600
  case "retry": return (0, import_react.createElement)(Text, {
27476
- color: entry.state === "running" ? inkColor(TUI_RGB.warn) : inkColor(TUI_RGB.dim),
27601
+ color: entry.state === "running" ? inkColor(getPalette().warn) : inkColor(getPalette().dim),
27477
27602
  wrap: verbose ? "truncate-end" : void 0
27478
27603
  }, ` ↻ retry ${entry.attempt}/${entry.max} · ${displayText(entry.code)} · ${Math.round(entry.delayMs / 100) / 10}s`);
27479
27604
  case "files": {
@@ -27503,18 +27628,18 @@ function Header({ resumed }) {
27503
27628
  flexDirection: "row",
27504
27629
  gap: 1,
27505
27630
  borderStyle: "round",
27506
- borderColor: inkColor(TUI_RGB.brand),
27631
+ borderColor: inkColor(getPalette().brand),
27507
27632
  paddingX: 1,
27508
27633
  alignSelf: "flex-start"
27509
27634
  }, (0, import_react.createElement)(Text, {
27510
- color: inkColor(TUI_RGB.brandBright),
27635
+ color: inkColor(getPalette().brandBright),
27511
27636
  bold: true
27512
27637
  }, "DeepSeek Harness"), (0, import_react.createElement)(Text, { dimColor: true }, hint));
27513
27638
  return (0, import_react.createElement)(Box, {
27514
27639
  flexDirection: "row",
27515
27640
  gap: 2,
27516
27641
  borderStyle: "round",
27517
- borderColor: inkColor(TUI_RGB.brand),
27642
+ borderColor: inkColor(getPalette().brand),
27518
27643
  paddingX: 1,
27519
27644
  alignSelf: "flex-start"
27520
27645
  }, (0, import_react.createElement)(Box, {
@@ -27523,12 +27648,12 @@ function Header({ resumed }) {
27523
27648
  justifyContent: "center"
27524
27649
  }, ...WHALE_GLYPH.map((row, index) => (0, import_react.createElement)(Text, {
27525
27650
  key: index,
27526
- color: inkColor(TUI_RGB.brand)
27651
+ color: inkColor(getPalette().brand)
27527
27652
  }, row))), (0, import_react.createElement)(Box, {
27528
27653
  flexDirection: "column",
27529
27654
  justifyContent: "center"
27530
27655
  }, (0, import_react.createElement)(Text, {
27531
- color: inkColor(TUI_RGB.brandBright),
27656
+ color: inkColor(getPalette().brandBright),
27532
27657
  bold: true
27533
27658
  }, "DeepSeek Harness"), (0, import_react.createElement)(Text, { dimColor: true }, hint)));
27534
27659
  }
@@ -27544,74 +27669,99 @@ function TodoPanel({ todos }) {
27544
27669
  const pending = todos.length - completed - inProgress;
27545
27670
  const current = todos.find((todo) => todo.status === "in_progress");
27546
27671
  return (0, import_react.createElement)(Box, { paddingX: 1 }, (0, import_react.createElement)(Text, {
27547
- color: inkColor(TUI_RGB.brand),
27672
+ color: inkColor(getPalette().brand),
27548
27673
  bold: true,
27549
27674
  wrap: "truncate-end"
27550
- }, `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)}`)));
27551
27676
  }
27552
27677
  /**
27553
27678
  * Ink props for one status tone: the Codex status-line accent mapping over
27554
27679
  * the DeepSeek palette, all blue by design — the status bar speaks only in
27555
- * degrees of blue (deep accent, primary figures, bright model identity, sky
27680
+ * degrees of blue (deep accent, primary figures, model identity, sky
27556
27681
  * paths and done states), with amber/red reserved for warnings and errors.
27557
27682
  */
27558
27683
  function statusToneProps(tone) {
27559
27684
  switch (tone) {
27560
27685
  case "model": return {
27561
- color: inkColor(TUI_RGB.brandBright),
27686
+ color: inkColor(getPalette().code),
27562
27687
  bold: true,
27563
27688
  dimColor: void 0
27564
27689
  };
27565
27690
  case "live": return {
27566
- color: inkColor(TUI_RGB.brandBright),
27691
+ color: inkColor(getPalette().brandBright),
27567
27692
  bold: void 0,
27568
27693
  dimColor: void 0
27569
27694
  };
27570
27695
  case "path": return {
27571
- color: inkColor(TUI_RGB.code),
27696
+ color: inkColor(getPalette().code),
27572
27697
  bold: void 0,
27573
27698
  dimColor: void 0
27574
27699
  };
27575
27700
  case "branch": return {
27576
- color: inkColor(TUI_RGB.text),
27701
+ color: inkColor(getPalette().text),
27577
27702
  bold: void 0,
27578
27703
  dimColor: void 0
27579
27704
  };
27580
27705
  case "value": return {
27581
- color: inkColor(TUI_RGB.brand),
27706
+ color: inkColor(getPalette().brand),
27582
27707
  bold: void 0,
27583
27708
  dimColor: void 0
27584
27709
  };
27585
27710
  case "label":
27586
27711
  case "meta": return {
27587
- color: void 0,
27712
+ color: inkColor(getPalette().dim),
27588
27713
  bold: void 0,
27589
- dimColor: true
27714
+ dimColor: void 0
27590
27715
  };
27591
27716
  case "accent": return {
27592
- 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),
27593
27743
  bold: void 0,
27594
27744
  dimColor: void 0
27595
27745
  };
27596
27746
  case "success": return {
27597
- color: inkColor(TUI_RGB.code),
27747
+ color: inkColor(getPalette().code),
27598
27748
  bold: true,
27599
27749
  dimColor: void 0
27600
27750
  };
27601
27751
  case "warn": return {
27602
- color: inkColor(TUI_RGB.warn),
27752
+ color: inkColor(getPalette().warn),
27603
27753
  bold: true,
27604
27754
  dimColor: void 0
27605
27755
  };
27606
27756
  case "error": return {
27607
- color: inkColor(TUI_RGB.error),
27757
+ color: inkColor(getPalette().error),
27608
27758
  bold: true,
27609
27759
  dimColor: void 0
27610
27760
  };
27611
27761
  default: return {
27612
- color: void 0,
27762
+ color: inkColor(getPalette().dim),
27613
27763
  bold: void 0,
27614
- dimColor: true
27764
+ dimColor: void 0
27615
27765
  };
27616
27766
  }
27617
27767
  }
@@ -27624,7 +27774,32 @@ function statusToneProps(tone) {
27624
27774
  * content, so the footer degrades to a single row on narrow terminals. Both
27625
27775
  * layouts arrive pre-measured from the pure reducer, so Ink only paints;
27626
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.
27627
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
+ }
27628
27803
  function StatusLine({ facts, stats, busy, columns, items }) {
27629
27804
  const layout = layoutStatusBar(facts, stats, Math.max(8, columns - 2), {
27630
27805
  busy,
@@ -27635,7 +27810,7 @@ function StatusLine({ facts, stats, busy, columns, items }) {
27635
27810
  row.left.forEach((group, groupIndex) => {
27636
27811
  if (groupIndex > 0) leftParts.push((0, import_react.createElement)(Text, {
27637
27812
  key: key + "gs" + groupIndex,
27638
- dimColor: true
27813
+ color: inkColor(getPalette().dim)
27639
27814
  }, " | "));
27640
27815
  group.spans.forEach((span, spanIndex) => {
27641
27816
  leftParts.push((0, import_react.createElement)(Text, {
@@ -27649,7 +27824,7 @@ function StatusLine({ facts, stats, busy, columns, items }) {
27649
27824
  row.right.forEach((span, index) => {
27650
27825
  if (index > 0) rightParts.push((0, import_react.createElement)(Text, {
27651
27826
  key: key + "rs" + index,
27652
- dimColor: true
27827
+ color: inkColor(getPalette().dim)
27653
27828
  }, STATUS_ITEM_SEPARATOR));
27654
27829
  rightParts.push((0, import_react.createElement)(Text, {
27655
27830
  key: key + "r" + index,
@@ -27659,7 +27834,7 @@ function StatusLine({ facts, stats, busy, columns, items }) {
27659
27834
  });
27660
27835
  if (row.hint) rightParts.push((0, import_react.createElement)(Text, {
27661
27836
  key: key + "hint",
27662
- dimColor: true
27837
+ color: inkColor(getPalette().dim)
27663
27838
  }, STATUS_CYCLE_HINT));
27664
27839
  return (0, import_react.createElement)(Box, {
27665
27840
  paddingLeft: 2,
@@ -27675,7 +27850,7 @@ function StatusLine({ facts, stats, busy, columns, items }) {
27675
27850
  * above the composer.
27676
27851
  */
27677
27852
  function NoticeLine({ text, tone, columns }) {
27678
- 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;
27679
27854
  const mark = tone === "error" ? "⨯" : tone === "warning" ? "!" : "•";
27680
27855
  return (0, import_react.createElement)(Box, { paddingLeft: 2 }, (0, import_react.createElement)(Text, {
27681
27856
  color: inkColor(color),
@@ -27731,9 +27906,9 @@ function ApprovalBar({ snapshot, locked }) {
27731
27906
  width: viewport.outerColumns,
27732
27907
  paddingX: 1,
27733
27908
  borderStyle: "round",
27734
- borderColor: inkColor(TUI_RGB.warn)
27909
+ borderColor: inkColor(getPalette().warn)
27735
27910
  }, (0, import_react.createElement)(Text, {
27736
- color: inkColor(TUI_RGB.warn),
27911
+ color: inkColor(getPalette().warn),
27737
27912
  bold: true,
27738
27913
  wrap: "truncate-end"
27739
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, {
@@ -27974,9 +28149,9 @@ function QuestionBar({ store, snapshot, locked }) {
27974
28149
  width: viewport.outerColumns,
27975
28150
  paddingX: 1,
27976
28151
  borderStyle: "round",
27977
- borderColor: inkColor(isPlan ? TUI_RGB.brand : TUI_RGB.brandDeep)
28152
+ borderColor: inkColor(isPlan ? getPalette().brand : getPalette().brandDeep)
27978
28153
  }, (0, import_react.createElement)(Text, {
27979
- color: inkColor(isPlan ? TUI_RGB.brand : TUI_RGB.brandDeep),
28154
+ color: inkColor(isPlan ? getPalette().brand : getPalette().brandDeep),
27980
28155
  bold: true,
27981
28156
  wrap: "truncate-end"
27982
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, {
@@ -28041,11 +28216,11 @@ function ModelPanel({ directory, error, onSelect, onRetry, onClose }) {
28041
28216
  wrap: "truncate-end"
28042
28217
  }, " loading models…")] : error !== void 0 ? [(0, import_react.createElement)(Text, {
28043
28218
  key: "error",
28044
- color: inkColor(TUI_RGB.error),
28219
+ color: inkColor(getPalette().error),
28045
28220
  wrap: "truncate-end"
28046
28221
  }, truncateColumns(` ${singleLineText(error)}`, viewport.contentColumns))] : [...directory?.failures.length === 0 ? [] : [(0, import_react.createElement)(Text, {
28047
28222
  key: "failures",
28048
- color: inkColor(TUI_RGB.warn),
28223
+ color: inkColor(getPalette().warn),
28049
28224
  wrap: "truncate-end"
28050
28225
  }, truncateColumns(` unavailable providers: ${directory?.failures.join(", ")}`, viewport.contentColumns))], ...rows.length === 0 ? [(0, import_react.createElement)(Text, {
28051
28226
  key: "empty",
@@ -28060,9 +28235,9 @@ function ModelPanel({ directory, error, onSelect, onRetry, onClose }) {
28060
28235
  width: viewport.outerColumns,
28061
28236
  paddingX: 1,
28062
28237
  borderStyle: "round",
28063
- borderColor: inkColor(TUI_RGB.brand)
28238
+ borderColor: inkColor(getPalette().brand)
28064
28239
  }, (0, import_react.createElement)(Text, {
28065
- color: inkColor(TUI_RGB.brand),
28240
+ color: inkColor(getPalette().brand),
28066
28241
  bold: true,
28067
28242
  wrap: "truncate-end"
28068
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) => {
@@ -28070,7 +28245,7 @@ function ModelPanel({ directory, error, onSelect, onRetry, onClose }) {
28070
28245
  const label = displayText(`${row.providerName} · ${row.modelName}`);
28071
28246
  return (0, import_react.createElement)(Text, {
28072
28247
  key: `${row.provider}/${row.model}`,
28073
- color: index === cursor ? inkColor(TUI_RGB.brandBright) : inkColor(TUI_RGB.dim),
28248
+ color: index === cursor ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
28074
28249
  wrap: "truncate-end"
28075
28250
  }, truncateColumns(`${index === cursor ? "❯ " : " "}${label}`, viewport.contentColumns));
28076
28251
  }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
@@ -28137,16 +28312,18 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
28137
28312
  }, " commands"),
28138
28313
  ...commandError === void 0 ? [] : [(0, import_react.createElement)(Text, {
28139
28314
  key: "commands-error",
28140
- color: inkColor(TUI_RGB.error),
28315
+ color: inkColor(getPalette().error),
28141
28316
  wrap: "truncate-end"
28142
28317
  }, truncateColumns(` command catalog unavailable: ${singleLineText(commandError)}`, viewport.contentColumns))],
28143
28318
  (0, import_react.createElement)(Box, { key: "local-help" }, row("/help", "show this overlay")),
28144
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")),
28145
28321
  (0, import_react.createElement)(Box, { key: "local-mode" }, row("/mode", "inspect or select the agent preset (/mode [preset])")),
28146
28322
  (0, import_react.createElement)(Box, { key: "local-new" }, row("/new", "create and switch to a fresh session (/new [preset])")),
28147
28323
  (0, import_react.createElement)(Box, { key: "local-resume" }, row("/resume", "browse or switch root sessions (/resume [id|prefix])")),
28148
28324
  (0, import_react.createElement)(Box, { key: "local-plugin" }, row("/plugin", "inspect the live plugin composition")),
28149
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")),
28150
28327
  (0, import_react.createElement)(Box, { key: "local-history" }, row("/history", "search and recall past prompts")),
28151
28328
  (0, import_react.createElement)(Box, { key: "local-clear" }, row("/clear", "clear the screen")),
28152
28329
  (0, import_react.createElement)(Box, { key: "local-export" }, row("/export", "export the transcript to markdown (/export [path])")),
@@ -28164,7 +28341,7 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
28164
28341
  }, " skills")],
28165
28342
  ...skillError === void 0 ? [] : [(0, import_react.createElement)(Text, {
28166
28343
  key: "skills-error",
28167
- color: inkColor(TUI_RGB.error),
28344
+ color: inkColor(getPalette().error),
28168
28345
  wrap: "truncate-end"
28169
28346
  }, truncateColumns(` skill catalog unavailable: ${singleLineText(skillError)}`, viewport.contentColumns))],
28170
28347
  ...skills.map((skill) => (0, import_react.createElement)(Text, {
@@ -28199,9 +28376,9 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
28199
28376
  width: viewport.outerColumns,
28200
28377
  paddingX: 1,
28201
28378
  borderStyle: "round",
28202
- borderColor: inkColor(TUI_RGB.brand)
28379
+ borderColor: inkColor(getPalette().brand)
28203
28380
  }, (0, import_react.createElement)(Text, {
28204
- color: inkColor(TUI_RGB.brand),
28381
+ color: inkColor(getPalette().brand),
28205
28382
  bold: true,
28206
28383
  wrap: "truncate-end"
28207
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, {
@@ -28228,6 +28405,38 @@ function editorWindow(value, cursor, columns) {
28228
28405
  after: afterBudget === 0 ? "" : truncateColumns(normalize(value.slice(cursor + 1)), afterBudget)
28229
28406
  };
28230
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
+ }
28231
28440
  /**
28232
28441
  * The Ctrl+O transcript inspector: one selected durable entry at a time,
28233
28442
  * with independent history selection and content scrolling. The complete
@@ -28324,9 +28533,9 @@ function VerbosePanel({ entries, onClose }) {
28324
28533
  width: viewport.outerColumns,
28325
28534
  paddingX: 1,
28326
28535
  borderStyle: "round",
28327
- borderColor: inkColor(TUI_RGB.brand)
28536
+ borderColor: inkColor(getPalette().brand)
28328
28537
  }, (0, import_react.createElement)(Text, {
28329
- color: inkColor(TUI_RGB.brand),
28538
+ color: inkColor(getPalette().brand),
28330
28539
  bold: true,
28331
28540
  wrap: "truncate-end"
28332
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, {
@@ -28351,7 +28560,12 @@ const MemoStaticTranscript = (0, import_react.memo)(StaticTranscript);
28351
28560
  * Resolve completion candidates for the current input: TUI-local commands,
28352
28561
  * the live registry descriptors, and user-invocable skills, filtered by the
28353
28562
  * typed prefix. Command names win collisions (the dispatch tries the
28354
- * 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.
28355
28569
  */
28356
28570
  function completionCandidates(value, descriptors, skills) {
28357
28571
  if (!value.startsWith("/")) return [];
@@ -28367,6 +28581,11 @@ function completionCandidates(value, descriptors, skills) {
28367
28581
  description: "switch the model",
28368
28582
  origin: "command"
28369
28583
  },
28584
+ {
28585
+ label: "/effort",
28586
+ description: "adjust reasoning effort for the current model",
28587
+ origin: "command"
28588
+ },
28370
28589
  {
28371
28590
  label: "/mode",
28372
28591
  description: "select the agent preset",
@@ -28392,6 +28611,11 @@ function completionCandidates(value, descriptors, skills) {
28392
28611
  description: "customize the status line",
28393
28612
  origin: "command"
28394
28613
  },
28614
+ {
28615
+ label: "/theme",
28616
+ description: "switch the color theme",
28617
+ origin: "command"
28618
+ },
28395
28619
  {
28396
28620
  label: "/history",
28397
28621
  description: "search and recall past prompts",
@@ -28430,13 +28654,20 @@ function completionCandidates(value, descriptors, skills) {
28430
28654
  description: skill.modelInvocable ? `skill · ${skill.description}` : `skill (user only) · ${skill.description}`,
28431
28655
  origin: "skill"
28432
28656
  }));
28433
- const all = [
28657
+ const seen = /* @__PURE__ */ new Set();
28658
+ const all = [];
28659
+ for (const candidate of [
28434
28660
  ...local,
28435
28661
  ...registry,
28436
28662
  ...skillRows
28437
- ];
28438
- if (prefix === "") return all.slice(0, 11);
28439
- 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));
28440
28671
  }
28441
28672
  /**
28442
28673
  * The completion menu, rendered inside the composer's subtree directly above
@@ -28461,6 +28692,7 @@ function CompletionMenu({ active, mention, index, rows }) {
28461
28692
  const selected = rows.length === 0 ? 0 : index % rows.length;
28462
28693
  const first = selectionWindow(selected, rows.length, limit);
28463
28694
  const visible = rows.slice(first, first + limit);
28695
+ const hidden = rows.length - visible.length;
28464
28696
  return (0, import_react.createElement)(Box, {
28465
28697
  flexDirection: "column",
28466
28698
  marginLeft: 2,
@@ -28472,13 +28704,17 @@ function CompletionMenu({ active, mention, index, rows }) {
28472
28704
  const absolute = first + at;
28473
28705
  return (0, import_react.createElement)(Text, {
28474
28706
  key: candidate.label,
28475
- color: absolute === selected ? inkColor(TUI_RGB.brandBright) : inkColor(TUI_RGB.dim),
28707
+ color: absolute === selected ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
28476
28708
  wrap: "truncate-end"
28477
28709
  }, `${absolute === selected ? "❯ " : " "}${padColumns(candidate.label, nameWidth)}${dim(truncateColumns(displayText(candidate.description), descBudget))}`);
28478
- }), showFooter ? (0, import_react.createElement)(Text, {
28479
- dimColor: true,
28710
+ }), hidden > 0 ? (0, import_react.createElement)(Text, {
28711
+ key: "more",
28712
+ color: inkColor(getPalette().dim),
28713
+ wrap: "truncate-end"
28714
+ }, dim(` … +${hidden} more`)) : void 0, showFooter ? (0, import_react.createElement)(Text, {
28715
+ color: inkColor(getPalette().dim),
28480
28716
  wrap: "truncate-end"
28481
- }, dim(mention ? "↑↓ choose · tab insert" : "↑↓ choose · tab complete")) : void 0);
28717
+ }, dim(mention ? `↑↓ choose · ${rows.length} items · tab insert` : `↑↓ choose · ${rows.length} items · tab complete`)) : void 0);
28482
28718
  }
28483
28719
  /**
28484
28720
  * The prompt box: TUI-local slash commands handled locally, other lines
@@ -28486,7 +28722,7 @@ function CompletionMenu({ active, mention, index, rows }) {
28486
28722
  * While a modal (approval / question / model panel) owns the keys, the
28487
28723
  * box passes every key through untouched.
28488
28724
  */
28489
- 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 }) {
28490
28726
  const columns = useStdout().stdout?.columns ?? 80;
28491
28727
  const [value, setValue] = (0, import_react.useState)("");
28492
28728
  const [cursor, setCursor] = (0, import_react.useState)(0);
@@ -28682,6 +28918,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
28682
28918
  openModel();
28683
28919
  return;
28684
28920
  }
28921
+ if (text === "/effort" || text.startsWith("/effort ")) {
28922
+ openEffort();
28923
+ return;
28924
+ }
28685
28925
  if (text === "/mode" || text.startsWith("/mode ")) {
28686
28926
  if (text.slice(5).trim() === "") openMode();
28687
28927
  else dispatch(text);
@@ -28708,6 +28948,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
28708
28948
  openStatusline();
28709
28949
  return;
28710
28950
  }
28951
+ if (text === "/theme") {
28952
+ openTheme();
28953
+ return;
28954
+ }
28711
28955
  if (text === "/history") {
28712
28956
  openHistory();
28713
28957
  return;
@@ -28822,27 +29066,118 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
28822
29066
  setDismissedMenuValue(void 0);
28823
29067
  }
28824
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" ? "»" : "❯";
28825
29073
  if (frozen) {
28826
29074
  const frozen = value === "" ? "type a message" : verboseLine(value, Math.max(1, columns - 6));
28827
29075
  return (0, import_react.createElement)(Box, {
28828
29076
  width: Math.max(1, columns - 1),
28829
29077
  borderStyle: "round",
28830
- borderColor: inkColor(TUI_RGB.dim),
29078
+ borderColor: inkColor(getPalette().dim),
28831
29079
  paddingX: 1
28832
- }, (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));
28833
29084
  }
28834
- const editor = editorWindow(value, cursor, Math.max(1, columns - 6));
28835
- 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, {
28836
29092
  active: menuActive,
28837
29093
  mention: mentionActive,
28838
29094
  index: completionIndex,
28839
29095
  rows: menuRows
28840
- }), (0, import_react.createElement)(Box, {
28841
- width: Math.max(1, columns - 1),
28842
- borderStyle: "round",
28843
- borderColor: inkColor(TUI_RGB.dim),
28844
- paddingX: 1
28845
- }, (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));
28846
29181
  }
28847
29182
  /** The whole terminal app; state arrives via the store, output via Ink. */
28848
29183
  function App(props) {
@@ -28851,6 +29186,60 @@ function App(props) {
28851
29186
  const skills = (0, import_react.useSyncExternalStore)(props.skills.subscribe, () => props.skills.rows);
28852
29187
  const [modelLabel, setModelLabel] = (0, import_react.useState)(props.model);
28853
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
+ ]);
28854
29243
  const [directory, setDirectory] = (0, import_react.useState)(void 0);
28855
29244
  const [modelError, setModelError] = (0, import_react.useState)(void 0);
28856
29245
  const [modelLoadEpoch, setModelLoadEpoch] = (0, import_react.useState)(0);
@@ -28892,6 +29281,7 @@ function App(props) {
28892
29281
  const [pluginQuery, setPluginQuery] = (0, import_react.useState)("");
28893
29282
  const [statuslineOpen, setStatuslineOpen] = (0, import_react.useState)(false);
28894
29283
  const [statuslineItems, setStatuslineItems] = (0, import_react.useState)(() => parseStatuslineItems(props.statusline));
29284
+ const [themeOpen, setThemeOpen] = (0, import_react.useState)(false);
28895
29285
  const [historyOpen, setHistoryOpen] = (0, import_react.useState)(false);
28896
29286
  /** The /history panel's accepted entry: text plus its recall-space index. */
28897
29287
  const [historyFill, setHistoryFill] = (0, import_react.useState)(void 0);
@@ -28912,15 +29302,17 @@ function App(props) {
28912
29302
  const questionSnapshot = (0, import_react.useSyncExternalStore)(props.questions.subscribe, props.questions.getSnapshot);
28913
29303
  const approvalPending = approvalSnapshot.pending !== void 0;
28914
29304
  const questionPending = questionSnapshot.pending !== void 0;
28915
- 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;
28916
29306
  (0, import_react.useEffect)(() => {
28917
29307
  if (!approvalPending && !questionPending) return;
28918
29308
  setModelOpen(false);
29309
+ setEffortFor(void 0);
28919
29310
  setHelpOpen(false);
28920
29311
  setModeOpen(false);
28921
29312
  setResumeOpen(false);
28922
29313
  setPluginOpen(false);
28923
29314
  setStatuslineOpen(false);
29315
+ setThemeOpen(false);
28924
29316
  setHistoryOpen(false);
28925
29317
  setVerboseOpen(false);
28926
29318
  }, [approvalPending, questionPending]);
@@ -29002,8 +29394,8 @@ function App(props) {
29002
29394
  const streamRows = Math.max(1, dynamicRows - visibleLiveLines.length);
29003
29395
  const reasoningRows = view.streamingReasoning === "" ? 0 : view.streaming === "" ? streamRows : streamRows <= 1 ? 0 : showReasoning ? Math.max(1, Math.floor(streamRows / 3)) : 1;
29004
29396
  const answerRows = view.streaming === "" ? 0 : Math.max(1, streamRows - reasoningRows);
29005
- const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !historyOpen && !verboseOpen && !approvalPending && !questionPending;
29006
- 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;
29007
29399
  const closeInspector = (0, import_react.useCallback)(() => {
29008
29400
  setVerboseOpen(false);
29009
29401
  }, []);
@@ -29011,6 +29403,19 @@ function App(props) {
29011
29403
  if (appStdout !== void 0) appStdout.write("\x1B[2J\x1B[3J\x1B[H");
29012
29404
  setRefreshEpoch((epoch) => epoch + 1);
29013
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
+ };
29014
29419
  return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(MemoStaticTranscript, {
29015
29420
  key: refreshEpoch,
29016
29421
  items: settledRows
@@ -29034,24 +29439,29 @@ function App(props) {
29034
29439
  }), (0, import_react.createElement)(ApprovalBar, {
29035
29440
  snapshot: approvalSnapshot,
29036
29441
  locked: questionPending
29037
- }), modelOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(ModelPanel, {
29442
+ }), modelOpen && !approvalPending && !questionPending ? effortFor === void 0 ? (0, import_react.createElement)(ModelPanel, {
29038
29443
  directory,
29039
29444
  error: modelError,
29040
29445
  onSelect: (row) => {
29041
- try {
29042
- setModelLabel(props.selectModel(row));
29043
- notify(`model → next step uses ${row.provider}/${row.model}`);
29044
- setModelOpen(false);
29045
- } catch (error) {
29046
- 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;
29047
29449
  }
29450
+ const effortId = row.reasoning?.efforts.length === 1 ? row.reasoning.efforts[0].id : void 0;
29451
+ applyModel(row, effortId);
29048
29452
  },
29049
29453
  onRetry: () => {
29050
29454
  setModelLoadEpoch((epoch) => epoch + 1);
29051
29455
  },
29052
29456
  onClose: () => {
29053
29457
  setModelOpen(false);
29458
+ setEffortFor(void 0);
29054
29459
  }
29460
+ }) : (0, import_react.createElement)(EffortPanel, {
29461
+ row: effortFor,
29462
+ current: effortLabel,
29463
+ select: (effortId) => applyModel(effortFor, effortId),
29464
+ back: () => setEffortFor(void 0)
29055
29465
  }) : void 0, helpOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(HelpPanel, {
29056
29466
  descriptors,
29057
29467
  skills,
@@ -29093,6 +29503,15 @@ function App(props) {
29093
29503
  props.saveStatusline(items);
29094
29504
  },
29095
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)
29096
29515
  }) : void 0, historyOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(HistoryPanel, {
29097
29516
  entries: recallSpace,
29098
29517
  fill: (text, index) => {
@@ -29123,8 +29542,32 @@ function App(props) {
29123
29542
  openModel: () => {
29124
29543
  setDirectory(void 0);
29125
29544
  setModelError(void 0);
29545
+ setEffortFor(void 0);
29126
29546
  setModelOpen(true);
29127
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
+ },
29128
29571
  openHelp: () => {
29129
29572
  setHelpOpen(true);
29130
29573
  },
@@ -29135,6 +29578,7 @@ function App(props) {
29135
29578
  setPluginOpen(true);
29136
29579
  },
29137
29580
  openStatusline: () => setStatuslineOpen(true),
29581
+ openTheme: () => setThemeOpen(true),
29138
29582
  openHistory: () => setHistoryOpen(true),
29139
29583
  createSession: props.createSession,
29140
29584
  cancelSessionSwitch: props.cancelSessionSwitch,
@@ -29164,7 +29608,10 @@ function App(props) {
29164
29608
  queued: queuedRows,
29165
29609
  cancelQueued: props.cancelQueued,
29166
29610
  historyFill,
29167
- historyConsumed
29611
+ historyConsumed,
29612
+ waveTick,
29613
+ waveTier,
29614
+ waveStyle
29168
29615
  }), (0, import_react.createElement)(StatusLine, {
29169
29616
  facts: {
29170
29617
  model: modelLabel,
@@ -29350,11 +29797,66 @@ const internals = {
29350
29797
  };
29351
29798
  //#endregion
29352
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
+ }
29353
29850
  /**
29354
29851
  * Load the selectable model directory from the live `ctx.llm` registry.
29355
29852
  * Providers are listed synchronously; each provider's models are discovered
29356
29853
  * with a bounded parallel fan-out whose failures degrade to that provider
29357
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".
29358
29860
  * @param ctx - context carrying the `llm` service.
29359
29861
  * @returns the resolved directory; empty rows when `llm` is unavailable.
29360
29862
  */
@@ -29362,21 +29864,39 @@ async function loadModelDirectory(ctx) {
29362
29864
  const llm = ctx.get("llm");
29363
29865
  if (llm === void 0) return {
29364
29866
  rows: [],
29365
- failures: []
29867
+ failures: [],
29868
+ reasoningFailures: []
29366
29869
  };
29870
+ const llmResolve = llm;
29367
29871
  const providers = llm.listProviders();
29368
29872
  const listed = await Promise.all(providers.map(async (provider) => {
29369
29873
  try {
29370
29874
  const models = await llm.listModels(provider.id);
29371
- return {
29372
- provider: provider.id,
29373
- providerName: provider.name,
29374
- models: models.map((model) => ({
29875
+ const reasoningFailures = [];
29876
+ const rows = await Promise.all(models.map(async (model) => {
29877
+ const row = {
29375
29878
  provider: provider.id,
29376
29879
  providerName: provider.name,
29377
29880
  model: model.id,
29378
29881
  modelName: model.name
29379
- }))
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
29380
29900
  };
29381
29901
  } catch {
29382
29902
  return {
@@ -29389,20 +29909,26 @@ async function loadModelDirectory(ctx) {
29389
29909
  }));
29390
29910
  return {
29391
29911
  rows: listed.flatMap((entry) => entry.models),
29392
- 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)
29393
29914
  };
29394
29915
  }
29395
29916
  //#endregion
29396
29917
  //#region src/mentions.ts
29397
29918
  /**
29398
- * Workspace @mention support: file candidates from a bounded async scan of
29399
- * the session cwd, session candidates from the opt-in `sessionReferenceResolver`
29400
- * service, and submission preparation through its `prepare()` API. Picked
29401
- * session mentions land as canonical `@[label](dsh-session:…)` tokens; on
29402
- * submit the text is parsed back into readable `@label` text plus structured
29403
- * references, snapshots are injected via `agent.inject()` before the readable
29404
- * message wakes the driver (`followup` idle, `steer` running) exactly the
29405
- * 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.
29406
29932
  *
29407
29933
  * @module @deepseek-ai/dsh-code/mentions
29408
29934
  */
@@ -29418,7 +29944,16 @@ const SKIP_DIRS = /* @__PURE__ */ new Set([
29418
29944
  ]);
29419
29945
  const MAX_FILES = 4e3;
29420
29946
  const MAX_DEPTH = 12;
29421
- /** 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
+ */
29422
29957
  async function scanWorkspaceFiles(root, signal) {
29423
29958
  const found = [];
29424
29959
  const pending = [{
@@ -29442,6 +29977,10 @@ async function scanWorkspaceFiles(root, signal) {
29442
29977
  const relative = current.relative === "" ? entry.name : `${current.relative}/${entry.name}`;
29443
29978
  if (entry.isDirectory()) {
29444
29979
  if (SKIP_DIRS.has(entry.name) || current.depth + 1 > MAX_DEPTH) continue;
29980
+ found.push({
29981
+ path: relative,
29982
+ kind: "directory"
29983
+ });
29445
29984
  pending.push({
29446
29985
  absolute: join(current.absolute, entry.name),
29447
29986
  relative,
@@ -29478,38 +30017,47 @@ function scoreFile(path, query) {
29478
30017
  /**
29479
30018
  * Create the mention API for one agent's workspace. A missing
29480
30019
  * session-reference service degrades to file mentions only (the scan still
29481
- * 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.
29482
30027
  * @param ctx - context carrying the optional `sessionReferenceResolver`.
29483
30028
  * @param agent - the session owner; excluded from its own candidates.
29484
30029
  * @param cwd - workspace root to scan.
29485
30030
  */
29486
30031
  function createMentions(ctx, agent, cwd) {
29487
30032
  const resolver = ctx.get("sessionReferenceResolver");
30033
+ const sessionCapable = agent !== void 0 && resolver !== void 0;
29488
30034
  let filesPromise;
30035
+ const files = () => {
30036
+ filesPromise ??= scanWorkspaceFiles(cwd);
30037
+ return filesPromise;
30038
+ };
29489
30039
  return {
29490
- files() {
29491
- filesPromise ??= scanWorkspaceFiles(cwd);
29492
- return filesPromise;
29493
- },
30040
+ files,
29494
30041
  async candidates(query, signal) {
29495
30042
  const needle = query.trim();
29496
- const [files, sessions] = await Promise.all([this.files(), resolver === void 0 ? Promise.resolve([]) : resolver.listCandidates(agent, needle, 10, signal).catch(() => [])]);
29497
- 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) => ({
29498
30045
  label: candidate.path,
29499
30046
  description: candidate.kind === "directory" ? "Folder" : "File",
29500
30047
  kind: candidate.kind
29501
30048
  }));
29502
- return [...sessions.map((candidate) => ({
30049
+ const sessionRows = sessions.map((candidate) => ({
29503
30050
  label: formatSessionReferenceMention(candidate),
29504
30051
  description: `Session · ${candidate.cwd ?? "(no cwd)"}`,
29505
30052
  kind: "session"
29506
- })), ...fileRows];
30053
+ }));
30054
+ return [...fileRows, ...sessionRows];
29507
30055
  },
29508
30056
  parse(text) {
29509
30057
  return parseSessionReferenceText(text);
29510
30058
  },
29511
30059
  async prepare(parsed, signal) {
29512
- if (parsed.references.length === 0 || resolver === void 0) return {
30060
+ if (parsed.references.length === 0 || resolver === void 0 || agent === void 0) return {
29513
30061
  text: parsed.text,
29514
30062
  references: parsed.references
29515
30063
  };
@@ -29941,7 +30489,8 @@ const inject = [
29941
30489
  const Config = z.object({ startup: z.object({
29942
30490
  kind: z.string().required(),
29943
30491
  sessionId: z.string(),
29944
- mode: z.string()
30492
+ mode: z.string(),
30493
+ theme: z.string()
29945
30494
  }) });
29946
30495
  /** Report an unexpected direct-driver failure and request a failing exit. */
29947
30496
  function fail(io, error) {
@@ -30038,7 +30587,7 @@ async function run(ctx, startup, io) {
30038
30587
  /** Prepare a complete next session before disturbing the currently visible one. */
30039
30588
  const prepare = async (next) => {
30040
30589
  const nextCwd = next.cwd ?? cwd;
30041
- const selectionState = {};
30590
+ const selectionState = pendingSelection === void 0 ? {} : { picked: pendingSelection };
30042
30591
  let mode = next.mode;
30043
30592
  if (!next.resume) mode = (await presets.resolve(mode)).id;
30044
30593
  const setup = async (agentCtx) => {
@@ -30046,14 +30595,7 @@ async function run(ctx, startup, io) {
30046
30595
  mode = (await presets.mount(agentCtx, sessionPreset)).id;
30047
30596
  installModelSelection(agentCtx, {
30048
30597
  get current() {
30049
- if (selectionState.picked !== void 0) return selectionState.picked;
30050
- const logged = agentCtx.agent?.session.requestHeader()?.config;
30051
- if (logged !== void 0) return {
30052
- provider: logged.provider,
30053
- model: logged.model,
30054
- ...logged.reasoningEffort === void 0 ? {} : { reasoningEffort: logged.reasoningEffort }
30055
- };
30056
- return defaults;
30598
+ return resolveEffectiveSelection(selectionState.picked, agentCtx.agent?.session.requestHeader()?.config, defaults);
30057
30599
  },
30058
30600
  set current(value) {
30059
30601
  selectionState.picked = value;
@@ -30097,7 +30639,9 @@ async function run(ctx, startup, io) {
30097
30639
  let agent;
30098
30640
  let session;
30099
30641
  let store = createTranscriptStore();
30100
- 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;
30101
30645
  if (!lazy) {
30102
30646
  const prepared = await prepare(await resolveTarget(startup, persistence, cwd));
30103
30647
  active = prepared;
@@ -30131,6 +30675,20 @@ async function run(ctx, startup, io) {
30131
30675
  bridge.notify("statusline save failed: " + (writeError instanceof Error ? writeError.message : String(writeError)), "error");
30132
30676
  });
30133
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
+ };
30134
30692
  const historyPath = join(homedir(), ".dsh", "dsh-code", "history.jsonl");
30135
30693
  let inputHistory = [];
30136
30694
  try {
@@ -30352,13 +30910,16 @@ async function run(ctx, startup, io) {
30352
30910
  return "";
30353
30911
  }
30354
30912
  };
30355
- /** Apply one /model selection: takes effect from the next assembled step. */
30356
- const selectModel = (row) => {
30357
- if (active === void 0) throw new Error("no session yet submit a message to start");
30358
- active.selection.picked = {
30359
- provider: row.provider,
30360
- model: row.model
30361
- };
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;
30362
30923
  return `${row.provider}/${row.model}`;
30363
30924
  };
30364
30925
  /**
@@ -30443,7 +31004,7 @@ async function run(ctx, startup, io) {
30443
31004
  agent = previous?.agent;
30444
31005
  session = previous?.session;
30445
31006
  store = previous === void 0 ? createTranscriptStore() : previous.store;
30446
- mentions = previous?.mentions;
31007
+ mentions = previous === void 0 ? createMentions(ctx, void 0, cwd) : previous.mentions;
30447
31008
  if (agent !== void 0) commands.setAgent(agent);
30448
31009
  if (agent !== void 0) skills.setAgent(agent);
30449
31010
  await next.handle.dispose();
@@ -30541,7 +31102,8 @@ async function run(ctx, startup, io) {
30541
31102
  };
30542
31103
  const appElement = () => {
30543
31104
  const sessionCwd = session?.header.cwd ?? cwd;
30544
- 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;
30545
31107
  return (0, import_react.createElement)(App, {
30546
31108
  key: session?.id ?? "pending",
30547
31109
  store,
@@ -30550,6 +31112,7 @@ async function run(ctx, startup, io) {
30550
31112
  commands,
30551
31113
  skills,
30552
31114
  model,
31115
+ effort,
30553
31116
  cwd: basename(sessionCwd),
30554
31117
  workspaceRoot: sessionCwd,
30555
31118
  branch: gitBranch(sessionCwd),
@@ -30561,7 +31124,7 @@ async function run(ctx, startup, io) {
30561
31124
  interrupt,
30562
31125
  quit,
30563
31126
  loadModels: () => loadModelDirectory(ctx),
30564
- loadMentions: mentions === void 0 ? () => Promise.resolve([]) : mentions.candidates,
31127
+ loadMentions: (query, signal) => mentions.candidates(query, signal),
30565
31128
  cyclePermission,
30566
31129
  selectModel,
30567
31130
  exportTranscript,
@@ -30576,6 +31139,7 @@ async function run(ctx, startup, io) {
30576
31139
  loadPlugins: () => listPluginRows(ctx),
30577
31140
  statusline: statuslineItems,
30578
31141
  saveStatusline,
31142
+ saveTheme,
30579
31143
  history: inputHistory,
30580
31144
  recordHistory,
30581
31145
  cancelQueued,
@@ -30591,6 +31155,9 @@ async function run(ctx, startup, io) {
30591
31155
  if (statuslineWarning !== void 0) setTimeout(() => {
30592
31156
  bridge.notify("statusline config unreadable, using defaults: " + statuslineWarning, "warning");
30593
31157
  }, 50);
31158
+ if (themeWarning !== void 0) setTimeout(() => {
31159
+ bridge.notify("theme config unreadable, using dark: " + themeWarning, "warning");
31160
+ }, 50);
30594
31161
  }
30595
31162
  /**
30596
31163
  * Mount the interactive terminal driver.
@@ -30598,16 +31165,23 @@ async function run(ctx, startup, io) {
30598
31165
  * @param config - validated startup config resolved from the tuiStartup provider.
30599
31166
  */
30600
31167
  function apply(ctx, config) {
31168
+ const theme = config.startup.theme === void 0 ? void 0 : parseThemeName(config.startup.theme);
30601
31169
  const startup = config.startup.kind === "resume" && config.startup.sessionId !== void 0 ? {
30602
31170
  kind: "resume",
30603
- sessionId: config.startup.sessionId
30604
- } : 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 ? {
30605
31177
  kind: "named",
30606
31178
  sessionId: config.startup.sessionId,
30607
- ...config.startup.mode === void 0 ? {} : { mode: config.startup.mode }
31179
+ ...config.startup.mode === void 0 ? {} : { mode: config.startup.mode },
31180
+ ...theme === void 0 ? {} : { theme }
30608
31181
  } : {
30609
31182
  kind: "fresh",
30610
- ...config.startup.mode === void 0 ? {} : { mode: config.startup.mode }
31183
+ ...config.startup.mode === void 0 ? {} : { mode: config.startup.mode },
31184
+ ...theme === void 0 ? {} : { theme }
30611
31185
  };
30612
31186
  const exit = ctx.get("appExit");
30613
31187
  if (exit === void 0) throw new Error("tui-runner: the launcher must provide ctx.appExit before the tree mounts");