@cryer/star-cli 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -44,7 +44,7 @@ import {
44
44
  resolveSnapshotContent,
45
45
  rewindToSnapshot,
46
46
  summarizeMessages
47
- } from "./chunk-K3R42LOX.js";
47
+ } from "./chunk-I6FDXAWX.js";
48
48
  import {
49
49
  envFilePath,
50
50
  globalConfigPath,
@@ -57,6 +57,198 @@ import {
57
57
  // src/main.tsx
58
58
  import { Command } from "commander";
59
59
 
60
+ // src/cli/markdown.ts
61
+ var ESC = "\x1B[";
62
+ var RESET = `${ESC}0m`;
63
+ function styled(open, text, base) {
64
+ const reopen = base ? `${ESC}${base}m` : "";
65
+ return `${ESC}${open}m${text}${RESET}${reopen}`;
66
+ }
67
+ var bold = (t, b) => styled("1", t, b);
68
+ var dim = (t, b) => styled("2", t, b);
69
+ var italic = (t, b) => styled("3", t, b);
70
+ var underline = (t, b) => styled("4", t, b);
71
+ var strike = (t, b) => styled("9", t, b);
72
+ var yellow = (t, b) => styled("33", t, b);
73
+ var cyanBold = (t, b) => styled("1;36", t, b);
74
+ function cellWidth(text) {
75
+ let width = 0;
76
+ for (const ch of text) {
77
+ const cp = ch.codePointAt(0) ?? 0;
78
+ if (cp >= 768 && cp <= 879 || cp === 8205 || cp >= 65024 && cp <= 65039) {
79
+ continue;
80
+ }
81
+ if (cp >= 4352 && cp <= 4447 || cp >= 11904 && cp <= 40959 || cp >= 40960 && cp <= 42191 || cp >= 44032 && cp <= 55203 || cp >= 63744 && cp <= 64255 || cp >= 65072 && cp <= 65103 || cp >= 65280 && cp <= 65376 || cp >= 65504 && cp <= 65510 || cp >= 126976 && cp <= 129791 || cp >= 131072 && cp <= 262141) {
82
+ width += 2;
83
+ } else {
84
+ width += 1;
85
+ }
86
+ }
87
+ return width;
88
+ }
89
+ var IMAGE_RE = /!\[([^\]]*)\]\(([^)\s]+)\)/g;
90
+ var LINK_RE = /\[([^\]]+)\]\(([^)\s]+)\)/g;
91
+ var BOLD_RE = /\*\*([^*\n]+)\*\*|__([^_\n]+)__/g;
92
+ var STRIKE_RE = /~~([^~\n]+)~~/g;
93
+ var STAR_ITALIC_RE = /\*([^*\n]+)\*/g;
94
+ var UNDERSCORE_ITALIC_RE = /(^|[^\w])_([^_\n]+)_(?=[^\w]|$)/g;
95
+ function renderInline(text, base = "") {
96
+ return text.split(/`([^`\n]+)`/).map((segment, index) => {
97
+ if (index % 2 === 1) return yellow(segment, base);
98
+ return segment.replace(IMAGE_RE, (_m, alt) => dim(`[image: ${alt}]`, base)).replace(
99
+ LINK_RE,
100
+ (_m, label, url) => `${underline(label, base)}${dim(` (${url})`, base)}`
101
+ ).replace(BOLD_RE, (_m, star, under) => bold(star ?? under, base)).replace(STRIKE_RE, (_m, t) => strike(t, base)).replace(STAR_ITALIC_RE, (_m, t) => italic(t, base)).replace(UNDERSCORE_ITALIC_RE, (_m, pre, t) => pre + italic(t, base));
102
+ }).join("");
103
+ }
104
+ var HEADING_RE = /^#{1,6}\s+(.*)$/;
105
+ var HR_RE = /^\s{0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/;
106
+ var QUOTE_RE = /^\s{0,3}>\s?(.*)$/;
107
+ var FENCE_RE = /^\s{0,3}(```+|~~~+)/;
108
+ var TABLE_SEPARATOR_CELL_RE = /^:?-{3,}:?$/;
109
+ function isTableSeparator(line) {
110
+ const cells = splitTableRow(line);
111
+ return cells !== null && cells.length > 0 && cells.every((cell) => TABLE_SEPARATOR_CELL_RE.test(cell.trim()));
112
+ }
113
+ function splitTableRow(line) {
114
+ const trimmed = line.trim();
115
+ if (!trimmed.includes("|")) return null;
116
+ const stripped = trimmed.replace(/^\|/, "").replace(/\|$/, "");
117
+ return stripped.split("|").map((cell) => cell.trim());
118
+ }
119
+ var MIN_COLUMN_WIDTH = 4;
120
+ function wrapCell(text, width) {
121
+ const lines = [];
122
+ let current = "";
123
+ let currentWidth = 0;
124
+ for (const ch of text) {
125
+ const w = cellWidth(ch);
126
+ if (currentWidth > 0 && currentWidth + w > width) {
127
+ const lastSpace = current.lastIndexOf(" ");
128
+ if (lastSpace > 0 && ch !== " ") {
129
+ lines.push(current.slice(0, lastSpace));
130
+ current = current.slice(lastSpace + 1) + ch;
131
+ } else {
132
+ lines.push(current);
133
+ current = ch === " " ? "" : ch;
134
+ }
135
+ currentWidth = cellWidth(current);
136
+ } else {
137
+ current += ch;
138
+ currentWidth += w;
139
+ }
140
+ }
141
+ if (current.length > 0 || lines.length === 0) lines.push(current);
142
+ return lines;
143
+ }
144
+ function renderTable(lines, base, maxWidth) {
145
+ const rows = [];
146
+ for (const line of lines) {
147
+ const cells = splitTableRow(line);
148
+ if (cells) rows.push(cells);
149
+ }
150
+ if (rows.length < 2) return lines;
151
+ const columnCount = Math.max(...rows.map((row) => row.length));
152
+ const widths = Array.from(
153
+ { length: columnCount },
154
+ (_, column) => Math.max(1, ...rows.map((row) => cellWidth(row[column] ?? "")))
155
+ );
156
+ const totalWidth = () => 2 + widths.reduce((sum, width) => sum + width, 0) + 3 * (columnCount - 1);
157
+ while (totalWidth() > maxWidth) {
158
+ const widest = widths.indexOf(Math.max(...widths));
159
+ if (widths[widest] === void 0 || widths[widest] <= MIN_COLUMN_WIDTH) break;
160
+ widths[widest] -= 1;
161
+ }
162
+ const joiner = ` ${dim("\u2502", base)} `;
163
+ const formatRow = (row, style) => {
164
+ const wrapped = widths.map((width, column) => wrapCell(row[column] ?? "", width));
165
+ const height = Math.max(...wrapped.map((cell) => cell.length));
166
+ const out = [];
167
+ for (let line = 0; line < height; line++) {
168
+ out.push(
169
+ ` ${widths.map((width, column) => {
170
+ const raw = wrapped[column]?.[line] ?? "";
171
+ return style(raw) + " ".repeat(Math.max(0, width - cellWidth(raw)));
172
+ }).join(joiner).trimEnd()}`
173
+ );
174
+ }
175
+ return out;
176
+ };
177
+ const separator = dim(` ${widths.map((width) => "\u2500".repeat(width)).join("\u2500\u253C\u2500")}`, base);
178
+ const [header, , ...body] = rows;
179
+ return [
180
+ ...formatRow(header, (cell) => bold(cell, base)),
181
+ separator,
182
+ ...body.flatMap((row, index) => {
183
+ const lines2 = formatRow(row, (cell) => renderInline(cell, base));
184
+ return index < body.length - 1 ? [...lines2, separator] : lines2;
185
+ })
186
+ ];
187
+ }
188
+ function committableLineCount(lines) {
189
+ let inFence = false;
190
+ let safe = 0;
191
+ for (let i = 0; i < lines.length; i++) {
192
+ const line = lines[i] ?? "";
193
+ if (FENCE_RE.test(line)) inFence = !inFence;
194
+ const isPipe = line.includes("|");
195
+ const nextIsPipe = i + 1 >= lines.length || (lines[i + 1] ?? "").includes("|");
196
+ if (!inFence && !(isPipe && nextIsPipe)) safe = i + 1;
197
+ }
198
+ return safe;
199
+ }
200
+ function renderMarkdown(text, base = "", maxWidth = 80) {
201
+ const lines = text.split("\n");
202
+ const out = [];
203
+ let inFence = false;
204
+ for (let i = 0; i < lines.length; i++) {
205
+ const line = lines[i] ?? "";
206
+ if (inFence) {
207
+ if (FENCE_RE.test(line)) {
208
+ inFence = false;
209
+ } else {
210
+ out.push(`${dim(" \u258E", base)}${line}`);
211
+ }
212
+ continue;
213
+ }
214
+ const fence = FENCE_RE.exec(line);
215
+ if (fence) {
216
+ inFence = true;
217
+ const lang = line.trim().slice(fence[1]?.length ?? 0).trim();
218
+ if (lang) out.push(dim(` \u258E ${lang}`, base));
219
+ continue;
220
+ }
221
+ const heading = HEADING_RE.exec(line);
222
+ if (heading) {
223
+ out.push(cyanBold(heading[1] ?? "", base));
224
+ continue;
225
+ }
226
+ if (HR_RE.test(line)) {
227
+ out.push(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", base));
228
+ continue;
229
+ }
230
+ const quote = QUOTE_RE.exec(line);
231
+ if (quote) {
232
+ out.push(`${dim(" \u258E", base)}${italic(renderInline(quote[1] ?? "", base), base)}`);
233
+ continue;
234
+ }
235
+ const headerCells = splitTableRow(line);
236
+ if (headerCells && i + 1 < lines.length && isTableSeparator(lines[i + 1] ?? "")) {
237
+ const block = [line];
238
+ let j = i + 1;
239
+ while (j < lines.length && splitTableRow(lines[j] ?? "") !== null) {
240
+ block.push(lines[j] ?? "");
241
+ j += 1;
242
+ }
243
+ out.push(...renderTable(block, base, maxWidth));
244
+ i = j - 1;
245
+ continue;
246
+ }
247
+ out.push(renderInline(line, base));
248
+ }
249
+ return out.join("\n");
250
+ }
251
+
60
252
  // src/cli/terminal-text.ts
61
253
  var UNSAFE_CHARS = /[\x00-\x09\x0b-\x1f\x7f]/;
62
254
  var CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f]/g;
@@ -85,9 +277,11 @@ function splitCommittableLines(text, minCompleteLines) {
85
277
  const lastNewline = text.lastIndexOf("\n");
86
278
  if (lastNewline < 0) return null;
87
279
  const complete = text.slice(0, lastNewline);
88
- const completeLines = complete.split("\n").length;
89
- if (completeLines < minCompleteLines) return null;
90
- return { committed: complete, rest: text.slice(lastNewline + 1) };
280
+ const lines = complete.split("\n");
281
+ const count = committableLineCount(lines);
282
+ if (count < minCompleteLines) return null;
283
+ const committed = lines.slice(0, count).join("\n");
284
+ return { committed, rest: text.slice(committed.length + 1) };
91
285
  }
92
286
  function formatStreamError(error) {
93
287
  if (error.message.includes("Unexpected end of JSON input")) {
@@ -491,8 +685,8 @@ var UsageTracker = class {
491
685
  };
492
686
 
493
687
  // src/cli/repl.tsx
494
- import { Box as Box13, Text as Text14, render, useApp, useInput as useInput8 } from "ink";
495
- import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef5, useState as useState5 } from "react";
688
+ import { Box as Box13, Text as Text14, render, useApp } from "ink";
689
+ import { useCallback, useEffect as useEffect3, useMemo, useRef as useRef6, useState as useState5 } from "react";
496
690
 
497
691
  // src/config/save.ts
498
692
  import fs from "fs";
@@ -600,6 +794,75 @@ async function savePermissionMode(mode) {
600
794
  await fs.promises.mkdir(path3.dirname(filePath), { recursive: true });
601
795
  await writeFileAtomic(filePath, upsertTopLevelKeyText(content, "permissionMode", mode));
602
796
  }
797
+ function unquoteTomlString(raw) {
798
+ if (raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1);
799
+ try {
800
+ return JSON.parse(raw);
801
+ } catch {
802
+ return null;
803
+ }
804
+ }
805
+ function setReasoningEffortInToml(content, modelName, effort) {
806
+ const lines = content.split("\n");
807
+ const header = /^\s*\[\[\s*models\s*\]\]\s*(?:#.*)?$/;
808
+ const anyHeader = /^\s*\[/;
809
+ const nameLine = /^\s*name\s*=\s*("(?:[^"\\]|\\.)*"|'[^']*')/;
810
+ for (let start = 0; start < lines.length; start++) {
811
+ if (!header.test(lines[start] ?? "")) continue;
812
+ let end = lines.length;
813
+ for (let i = start + 1; i < lines.length; i++) {
814
+ if (anyHeader.test(lines[i] ?? "")) {
815
+ end = i;
816
+ break;
817
+ }
818
+ }
819
+ let nameIndex = -1;
820
+ for (let i = start + 1; i < end; i++) {
821
+ const match = nameLine.exec(lines[i] ?? "");
822
+ if (match?.[1] && unquoteTomlString(match[1]) === modelName) {
823
+ nameIndex = i;
824
+ break;
825
+ }
826
+ }
827
+ if (nameIndex === -1) continue;
828
+ const keyLine = /^\s*reasoningEffort\s*=/;
829
+ for (let i = start + 1; i < end; i++) {
830
+ if (!keyLine.test(lines[i] ?? "")) continue;
831
+ if (effort === void 0) {
832
+ lines.splice(i, 1);
833
+ } else {
834
+ const comment = /\s+#.*$/.exec(lines[i] ?? "");
835
+ lines[i] = `reasoningEffort = ${tomlString(effort)}${comment?.[0] ?? ""}`;
836
+ }
837
+ return lines.join("\n");
838
+ }
839
+ if (effort === void 0) return content;
840
+ let insertAt = nameIndex + 1;
841
+ for (let i = start + 1; i < end; i++) {
842
+ if (/^\s*model\s*=/.test(lines[i] ?? "")) {
843
+ insertAt = i + 1;
844
+ break;
845
+ }
846
+ }
847
+ lines.splice(insertAt, 0, `reasoningEffort = ${tomlString(effort)}`);
848
+ return lines.join("\n");
849
+ }
850
+ return null;
851
+ }
852
+ async function saveReasoningEffort(modelName, effort) {
853
+ const filePath = globalConfigPath();
854
+ let content = "";
855
+ try {
856
+ content = await fs.promises.readFile(filePath, "utf8");
857
+ } catch (error) {
858
+ if (error.code !== "ENOENT") throw error;
859
+ }
860
+ const updated = setReasoningEffortInToml(content, modelName, effort);
861
+ if (updated === null) return false;
862
+ await fs.promises.mkdir(path3.dirname(filePath), { recursive: true });
863
+ await writeFileAtomic(filePath, updated);
864
+ return true;
865
+ }
603
866
 
604
867
  // src/llm/provider.ts
605
868
  import { createAnthropic } from "@ai-sdk/anthropic";
@@ -734,6 +997,14 @@ function createModel(config, modelName) {
734
997
  }).responses(modelConfig.model);
735
998
  }
736
999
  }
1000
+ function reasoningEffortMetadata(config, modelName) {
1001
+ const modelConfig = resolveModelConfig(config, modelName);
1002
+ if (!modelConfig.reasoningEffort) return void 0;
1003
+ const provider = config.providers.find((p) => p.name === modelConfig.provider);
1004
+ if (!provider || provider.protocol === "anthropic") return void 0;
1005
+ const key = provider.protocol === "openai-responses" ? "openai" : provider.name;
1006
+ return { [key]: { reasoningEffort: modelConfig.reasoningEffort } };
1007
+ }
737
1008
 
738
1009
  // src/session/checkpoints.ts
739
1010
  import fs2 from "fs/promises";
@@ -827,6 +1098,12 @@ function debugWarn(message) {
827
1098
  if (process.env.STAR_DEBUG === "1") process.stderr.write(`[star-cli] ${message}
828
1099
  `);
829
1100
  }
1101
+ var META_WRITE_MAX_ATTEMPTS = 5;
1102
+ var META_WRITE_RETRY_DELAY_MS = 25;
1103
+ function isTransientFsError(error) {
1104
+ const code = error.code;
1105
+ return code === "EPERM" || code === "EACCES" || code === "EBUSY";
1106
+ }
830
1107
  var FALLBACK_META = /* @__PURE__ */ Symbol("star.fallbackMeta");
831
1108
  function isFallbackMeta(meta) {
832
1109
  return meta[FALLBACK_META] === true;
@@ -901,17 +1178,33 @@ var SessionStore = class _SessionStore {
901
1178
  // writers race the loop's own appends, and a truncated read once turned
902
1179
  // fallbackMeta's empty defaults into a persisted wipe of model/cwd/usage.
903
1180
  // A branded fallback meta is never written back for the same reason.
1181
+ // A write that still fails after retries is dropped rather than thrown:
1182
+ // meta.json is bookkeeping the cached meta rebuilds on the next write,
1183
+ // while the conversation itself lives in messages.jsonl — killing the
1184
+ // agent turn over a stale updatedAt would be the wrong trade.
904
1185
  writeMeta(meta) {
905
1186
  if (isFallbackMeta(meta)) return Promise.resolve();
906
1187
  const tmp = `${this.metaPath()}.tmp-${process.pid}-${this.metaTmpSeq++}`;
907
1188
  const run = this.metaWriteQueue.then(async () => {
908
- try {
909
- await fs3.writeFile(tmp, JSON.stringify(meta, null, 2));
910
- await fs3.rename(tmp, this.metaPath());
911
- } catch (error) {
912
- await fs3.unlink(tmp).catch(() => {
913
- });
914
- throw error;
1189
+ for (let attempt = 0; attempt < META_WRITE_MAX_ATTEMPTS; attempt++) {
1190
+ try {
1191
+ await fs3.writeFile(tmp, JSON.stringify(meta, null, 2));
1192
+ await fs3.rename(tmp, this.metaPath());
1193
+ return;
1194
+ } catch (error) {
1195
+ if (isTransientFsError(error) && attempt + 1 < META_WRITE_MAX_ATTEMPTS) {
1196
+ await new Promise(
1197
+ (resolve) => setTimeout(resolve, META_WRITE_RETRY_DELAY_MS * (attempt + 1))
1198
+ );
1199
+ continue;
1200
+ }
1201
+ await fs3.unlink(tmp).catch(() => {
1202
+ });
1203
+ debugWarn(
1204
+ `session ${this.id}: meta write failed (${error.code ?? String(error)}), skipping`
1205
+ );
1206
+ return;
1207
+ }
915
1208
  }
916
1209
  });
917
1210
  this.metaWriteQueue = run.catch(() => {
@@ -3162,17 +3455,295 @@ async function buildUndoTreeDiffs(cwd, tree) {
3162
3455
  }
3163
3456
 
3164
3457
  // src/cli/components/ConnectWizard.tsx
3165
- import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
3166
- import { useRef as useRef4, useState as useState4 } from "react";
3458
+ import { Box as Box4, Text as Text4 } from "ink";
3459
+ import { useRef as useRef5, useState as useState4 } from "react";
3460
+
3461
+ // src/cli/use-input.ts
3462
+ import { useStdin } from "ink";
3463
+ import { useEffect, useRef } from "react";
3464
+
3465
+ // src/cli/keypress.ts
3466
+ var metaKeyCodeRe = /^(?:\u001B)([a-zA-Z0-9])$/;
3467
+ var fnKeyRe = /^(?:\u001B+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/;
3468
+ var keyName = {
3469
+ OP: "f1",
3470
+ OQ: "f2",
3471
+ OR: "f3",
3472
+ OS: "f4",
3473
+ "[11~": "f1",
3474
+ "[12~": "f2",
3475
+ "[13~": "f3",
3476
+ "[14~": "f4",
3477
+ "[[A": "f1",
3478
+ "[[B": "f2",
3479
+ "[[C": "f3",
3480
+ "[[D": "f4",
3481
+ "[[E": "f5",
3482
+ "[15~": "f5",
3483
+ "[17~": "f6",
3484
+ "[18~": "f7",
3485
+ "[19~": "f8",
3486
+ "[20~": "f9",
3487
+ "[21~": "f10",
3488
+ "[23~": "f11",
3489
+ "[24~": "f12",
3490
+ "[A": "up",
3491
+ "[B": "down",
3492
+ "[C": "right",
3493
+ "[D": "left",
3494
+ "[E": "clear",
3495
+ "[F": "end",
3496
+ "[H": "home",
3497
+ OA: "up",
3498
+ OB: "down",
3499
+ OC: "right",
3500
+ OD: "left",
3501
+ OE: "clear",
3502
+ OF: "end",
3503
+ OH: "home",
3504
+ "[1~": "home",
3505
+ "[2~": "insert",
3506
+ "[3~": "delete",
3507
+ "[4~": "end",
3508
+ "[5~": "pageup",
3509
+ "[6~": "pagedown",
3510
+ "[[5~": "pageup",
3511
+ "[[6~": "pagedown",
3512
+ "[7~": "home",
3513
+ "[8~": "end",
3514
+ "[a": "up",
3515
+ "[b": "down",
3516
+ "[c": "right",
3517
+ "[d": "left",
3518
+ "[e": "clear",
3519
+ "[2$": "insert",
3520
+ "[3$": "delete",
3521
+ "[5$": "pageup",
3522
+ "[6$": "pagedown",
3523
+ "[7$": "home",
3524
+ "[8$": "end",
3525
+ Oa: "up",
3526
+ Ob: "down",
3527
+ Oc: "right",
3528
+ Od: "left",
3529
+ Oe: "clear",
3530
+ "[2^": "insert",
3531
+ "[3^": "delete",
3532
+ "[5^": "pageup",
3533
+ "[6^": "pagedown",
3534
+ "[7^": "home",
3535
+ "[8^": "end",
3536
+ "[Z": "tab"
3537
+ };
3538
+ var nonAlphanumericKeys = [...Object.values(keyName), "backspace"];
3539
+ var shiftKeyCodes = /* @__PURE__ */ new Set([
3540
+ "[a",
3541
+ "[b",
3542
+ "[c",
3543
+ "[d",
3544
+ "[e",
3545
+ "[2$",
3546
+ "[3$",
3547
+ "[5$",
3548
+ "[6$",
3549
+ "[7$",
3550
+ "[8$",
3551
+ "[Z"
3552
+ ]);
3553
+ var ctrlKeyCodes = /* @__PURE__ */ new Set([
3554
+ "Oa",
3555
+ "Ob",
3556
+ "Oc",
3557
+ "Od",
3558
+ "Oe",
3559
+ "[2^",
3560
+ "[3^",
3561
+ "[5^",
3562
+ "[6^",
3563
+ "[7^",
3564
+ "[8^"
3565
+ ]);
3566
+ function parseKeypress(s) {
3567
+ const key = {
3568
+ name: void 0,
3569
+ ctrl: false,
3570
+ meta: false,
3571
+ shift: false,
3572
+ option: false,
3573
+ sequence: s
3574
+ };
3575
+ if (s === "\r") {
3576
+ key.name = "return";
3577
+ } else if (s === "\n") {
3578
+ key.name = "enter";
3579
+ } else if (s === " ") {
3580
+ key.name = "tab";
3581
+ } else if (s === "\b" || s === "\x1B\b") {
3582
+ key.name = "backspace";
3583
+ key.meta = s.charAt(0) === "\x1B";
3584
+ } else if (s === "\x7F" || s === "\x1B\x7F") {
3585
+ key.name = "delete";
3586
+ key.meta = s.charAt(0) === "\x1B";
3587
+ } else if (s === "\x1B" || s === "\x1B\x1B") {
3588
+ key.name = "escape";
3589
+ key.meta = s.length === 2;
3590
+ } else if (s === " " || s === "\x1B ") {
3591
+ key.name = "space";
3592
+ key.meta = s.length === 2;
3593
+ } else if (s.length === 1 && s <= "") {
3594
+ key.name = String.fromCharCode(s.charCodeAt(0) + "a".charCodeAt(0) - 1);
3595
+ key.ctrl = true;
3596
+ } else if (s.length === 1 && s >= "0" && s <= "9") {
3597
+ key.name = "number";
3598
+ } else if (s.length === 1 && s >= "a" && s <= "z") {
3599
+ key.name = s;
3600
+ } else if (s.length === 1 && s >= "A" && s <= "Z") {
3601
+ key.name = s.toLowerCase();
3602
+ key.shift = true;
3603
+ }
3604
+ const metaMatch = key.name === void 0 ? metaKeyCodeRe.exec(s) : null;
3605
+ const fnMatch = key.name === void 0 && metaMatch === null ? fnKeyRe.exec(s) : null;
3606
+ if (metaMatch) {
3607
+ key.meta = true;
3608
+ key.shift = /^[A-Z]$/.test(metaMatch[1] ?? "");
3609
+ } else if (fnMatch) {
3610
+ const segs = [...s];
3611
+ if (segs[0] === "\x1B" && segs[1] === "\x1B") {
3612
+ key.option = true;
3613
+ }
3614
+ const code = [fnMatch[1], fnMatch[2], fnMatch[4], fnMatch[6]].filter(Boolean).join("");
3615
+ const modifier = Number(fnMatch[3] || fnMatch[5] || 1) - 1;
3616
+ key.ctrl = !!(modifier & 4);
3617
+ key.meta = !!(modifier & 10);
3618
+ key.shift = !!(modifier & 1);
3619
+ key.code = code;
3620
+ key.name = keyName[code];
3621
+ key.shift = shiftKeyCodes.has(code) || key.shift;
3622
+ key.ctrl = ctrlKeyCodes.has(code) || key.ctrl;
3623
+ }
3624
+ return key;
3625
+ }
3626
+ function splitKeypresses(chunk) {
3627
+ const units = [];
3628
+ let i = 0;
3629
+ const csiLength = (start) => {
3630
+ let j = start;
3631
+ while (j < chunk.length) {
3632
+ const code = chunk.charCodeAt(j);
3633
+ j += 1;
3634
+ if (code >= 64 && code <= 126) break;
3635
+ }
3636
+ return j;
3637
+ };
3638
+ while (i < chunk.length) {
3639
+ if (chunk[i] !== "\x1B") {
3640
+ const next2 = chunk.indexOf("\x1B", i);
3641
+ const end = next2 === -1 ? chunk.length : next2;
3642
+ units.push(chunk.slice(i, end));
3643
+ i = end;
3644
+ continue;
3645
+ }
3646
+ const next = chunk[i + 1];
3647
+ if (next === void 0) {
3648
+ units.push("\x1B");
3649
+ i += 1;
3650
+ } else if (next === "[") {
3651
+ const end = csiLength(i + 2);
3652
+ units.push(chunk.slice(i, end));
3653
+ i = end;
3654
+ } else if (next === "O") {
3655
+ const end = Math.min(i + 3, chunk.length);
3656
+ units.push(chunk.slice(i, end));
3657
+ i = end;
3658
+ } else if (next === "\x1B" && (chunk[i + 2] === "[" || chunk[i + 2] === "O")) {
3659
+ const end = chunk[i + 2] === "[" ? csiLength(i + 3) : Math.min(i + 4, chunk.length);
3660
+ units.push(chunk.slice(i, end));
3661
+ i = end;
3662
+ } else if (next === "\x1B") {
3663
+ units.push("\x1B");
3664
+ i += 1;
3665
+ } else {
3666
+ units.push(chunk.slice(i, i + 2));
3667
+ i += 2;
3668
+ }
3669
+ }
3670
+ return units;
3671
+ }
3672
+ var INCOMPLETE_TAIL_RE = /\u001B(?:\[(?:[0-9;?]*)?|O|\u001B(?:\[(?:[0-9;?]*)?|O))$/;
3673
+ function splitTrailingIncomplete(chunk) {
3674
+ const match = INCOMPLETE_TAIL_RE.exec(chunk);
3675
+ if (!match) return { head: chunk, tail: "" };
3676
+ return { head: chunk.slice(0, chunk.length - match[0].length), tail: match[0] };
3677
+ }
3678
+ function toInputKey(unit) {
3679
+ const keypress = parseKeypress(unit);
3680
+ const key = {
3681
+ upArrow: keypress.name === "up",
3682
+ downArrow: keypress.name === "down",
3683
+ leftArrow: keypress.name === "left",
3684
+ rightArrow: keypress.name === "right",
3685
+ pageDown: keypress.name === "pagedown",
3686
+ pageUp: keypress.name === "pageup",
3687
+ return: keypress.name === "return",
3688
+ escape: keypress.name === "escape",
3689
+ ctrl: keypress.ctrl,
3690
+ shift: keypress.shift,
3691
+ tab: keypress.name === "tab",
3692
+ backspace: keypress.name === "backspace",
3693
+ delete: keypress.name === "delete",
3694
+ meta: keypress.meta || keypress.name === "escape" || keypress.option
3695
+ };
3696
+ let input = keypress.ctrl ? keypress.name ?? "" : keypress.sequence;
3697
+ if (nonAlphanumericKeys.includes(keypress.name ?? "")) {
3698
+ input = "";
3699
+ }
3700
+ if (input.startsWith("\x1B")) {
3701
+ input = input.slice(1);
3702
+ }
3703
+ if (input.length === 1 && /[A-Z]/.test(input)) {
3704
+ key.shift = true;
3705
+ }
3706
+ return { input, key };
3707
+ }
3708
+
3709
+ // src/cli/use-input.ts
3710
+ function useInput(inputHandler, options = {}) {
3711
+ const { setRawMode, internal_exitOnCtrlC, internal_eventEmitter } = useStdin();
3712
+ const pendingRef = useRef("");
3713
+ useEffect(() => {
3714
+ if (options.isActive === false) return;
3715
+ setRawMode(true);
3716
+ return () => {
3717
+ setRawMode(false);
3718
+ };
3719
+ }, [options.isActive, setRawMode]);
3720
+ useEffect(() => {
3721
+ if (options.isActive === false) return;
3722
+ const handleData = (data) => {
3723
+ const { head, tail } = splitTrailingIncomplete(pendingRef.current + data);
3724
+ pendingRef.current = tail;
3725
+ for (const unit of splitKeypresses(head)) {
3726
+ const { input, key } = toInputKey(unit);
3727
+ if (!(input === "c" && key.ctrl) || !internal_exitOnCtrlC) {
3728
+ inputHandler(input, key);
3729
+ }
3730
+ }
3731
+ };
3732
+ internal_eventEmitter?.on("input", handleData);
3733
+ return () => {
3734
+ internal_eventEmitter?.removeListener("input", handleData);
3735
+ };
3736
+ }, [options.isActive, internal_exitOnCtrlC, internal_eventEmitter, inputHandler]);
3737
+ }
3167
3738
 
3168
3739
  // src/cli/components/SelectPrompt.tsx
3169
- import { Box, Text, useInput } from "ink";
3170
- import { useRef, useState } from "react";
3740
+ import { Box, Text } from "ink";
3741
+ import { useRef as useRef2, useState } from "react";
3171
3742
  import { jsx, jsxs } from "react/jsx-runtime";
3172
3743
  var SELECT_PROMPT_MAX_VISIBLE = 9;
3173
3744
  function SelectPrompt({ title, options, onSelect, onCancel }) {
3174
3745
  const [index, setIndex] = useState(0);
3175
- const indexRef = useRef(0);
3746
+ const indexRef = useRef2(0);
3176
3747
  const move = (delta) => {
3177
3748
  indexRef.current = Math.max(0, Math.min(indexRef.current + delta, options.length - 1));
3178
3749
  setIndex(indexRef.current);
@@ -3233,12 +3804,12 @@ function SelectPrompt({ title, options, onSelect, onCancel }) {
3233
3804
  }
3234
3805
 
3235
3806
  // src/cli/components/TextField.tsx
3236
- import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
3237
- import { useRef as useRef3, useState as useState3 } from "react";
3807
+ import { Box as Box3, Text as Text3 } from "ink";
3808
+ import { useRef as useRef4, useState as useState3 } from "react";
3238
3809
 
3239
3810
  // src/cli/components/InputBox.tsx
3240
- import { Box as Box2, Text as Text2, useInput as useInput2, useStdout } from "ink";
3241
- import { useEffect, useRef as useRef2, useState as useState2 } from "react";
3811
+ import { Box as Box2, Text as Text2, useStdout } from "ink";
3812
+ import { useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
3242
3813
 
3243
3814
  // src/cli/path-suggest.ts
3244
3815
  import { readdir as readdir2 } from "fs/promises";
@@ -3388,25 +3959,25 @@ function InputBox({
3388
3959
  const [highlight, setHighlight] = useState2(0);
3389
3960
  const [suggestionsDismissed, setSuggestionsDismissed] = useState2(false);
3390
3961
  const [pathSuggestions, setPathSuggestions] = useState2([]);
3391
- const historyIndexRef = useRef2(null);
3392
- const draftRef = useRef2("");
3393
- const refillSeqRef = useRef2(0);
3962
+ const historyIndexRef = useRef3(null);
3963
+ const draftRef = useRef3("");
3964
+ const refillSeqRef = useRef3(0);
3394
3965
  const [search, setSearch] = useState2(null);
3395
- const searchSavedRef = useRef2(null);
3396
- const pastesRef = useRef2(/* @__PURE__ */ new Map());
3397
- const pasteSeqRef = useRef2(0);
3398
- const bracketRef = useRef2({ active: false, buffer: "", pending: "" });
3399
- const lastPasteRef = useRef2(
3966
+ const searchSavedRef = useRef3(null);
3967
+ const pastesRef = useRef3(/* @__PURE__ */ new Map());
3968
+ const pasteSeqRef = useRef3(0);
3969
+ const bracketRef = useRef3({ active: false, buffer: "", pending: "" });
3970
+ const lastPasteRef = useRef3(
3400
3971
  null
3401
3972
  );
3402
- const valueRef = useRef2(value);
3403
- const cursorRef = useRef2(cursor);
3404
- useEffect(() => {
3973
+ const valueRef = useRef3(value);
3974
+ const cursorRef = useRef3(cursor);
3975
+ useEffect2(() => {
3405
3976
  valueRef.current = value;
3406
3977
  cursorRef.current = cursor;
3407
3978
  });
3408
3979
  const { stdout } = useStdout();
3409
- useEffect(() => {
3980
+ useEffect2(() => {
3410
3981
  if (!stdout?.isTTY) return;
3411
3982
  stdout.write("\x1B[?2004h");
3412
3983
  return () => {
@@ -3495,7 +4066,7 @@ function InputBox({
3495
4066
  }
3496
4067
  }
3497
4068
  };
3498
- useEffect(() => {
4069
+ useEffect2(() => {
3499
4070
  if (refill && refill.seq !== refillSeqRef.current) {
3500
4071
  refillSeqRef.current = refill.seq;
3501
4072
  edit(refill.text, refill.text.length);
@@ -3503,7 +4074,7 @@ function InputBox({
3503
4074
  });
3504
4075
  const suggestionsEnabled = !isStreaming && !disabled && !suggestionsDismissed && !search;
3505
4076
  const suggestions = suggestionsEnabled && commands ? filterCommands(value, commands) : [];
3506
- useEffect(() => {
4077
+ useEffect2(() => {
3507
4078
  if (!suggestionsEnabled || !cwd) {
3508
4079
  setPathSuggestions([]);
3509
4080
  return;
@@ -3541,7 +4112,7 @@ function InputBox({
3541
4112
  token.start + replacement.length
3542
4113
  );
3543
4114
  };
3544
- useInput2((input, key) => {
4115
+ useInput((input, key) => {
3545
4116
  if (!disabled) {
3546
4117
  const bracket = bracketRef.current;
3547
4118
  const feedable = input.length > 0 && !key.ctrl && !key.meta;
@@ -3774,9 +4345,9 @@ ${value.slice(cursor)}`, cursor + 1);
3774
4345
  const after = value.slice(cursor + 1);
3775
4346
  const cursorChar = at === "\n" || at === void 0 || at === PASTE_DELIM ? " " : at;
3776
4347
  const tail = (at === "\n" ? "\n" : "") + stylePasteTokens(after);
3777
- const styled = search ? `\x1B[36m(reverse-search) \x1B[39m'${search.query}': ${search.match === null ? "\x1B[2mno match\x1B[22m\x1B[7m \x1B[27m" : `${history[search.match]}\x1B[7m \x1B[27m`}` : `\x1B[36m> \x1B[39m${stylePasteTokens(before)}\x1B[7m${cursorChar}\x1B[27m${tail}`;
4348
+ const styled2 = search ? `\x1B[36m(reverse-search) \x1B[39m'${search.query}': ${search.match === null ? "\x1B[2mno match\x1B[22m\x1B[7m \x1B[27m" : `${history[search.match]}\x1B[7m \x1B[27m`}` : `\x1B[36m\u276F \x1B[39m${stylePasteTokens(before)}\x1B[7m${cursorChar}\x1B[27m${tail}`;
3778
4349
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
3779
- /* @__PURE__ */ jsx2(Box2, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { children: styled }) }),
4350
+ /* @__PURE__ */ jsx2(Box2, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { children: styled2 }) }),
3780
4351
  suggestions.length > 0 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: suggestions.map((cmd, index) => {
3781
4352
  const label = cmd.usage ?? `/${cmd.name}`;
3782
4353
  return index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `${label} - ${cmd.description}` }, cmd.name) : /* @__PURE__ */ jsxs2(Text2, { children: [
@@ -3805,8 +4376,8 @@ function TextField({
3805
4376
  onCancel
3806
4377
  }) {
3807
4378
  const [value, setValue] = useState3(initialValue);
3808
- const valueRef = useRef3(initialValue);
3809
- const bracketRef = useRef3({ active: false, buffer: "", pending: "" });
4379
+ const valueRef = useRef4(initialValue);
4380
+ const bracketRef = useRef4({ active: false, buffer: "", pending: "" });
3810
4381
  const flushBracket = () => {
3811
4382
  const bracket = bracketRef.current;
3812
4383
  const content = bracket.buffer.replace(/[\r\n]+/g, "");
@@ -3818,7 +4389,7 @@ function TextField({
3818
4389
  setValue(valueRef.current);
3819
4390
  }
3820
4391
  };
3821
- useInput3((input, key) => {
4392
+ useInput((input, key) => {
3822
4393
  if (key.escape) {
3823
4394
  onCancel();
3824
4395
  return;
@@ -3857,7 +4428,7 @@ function TextField({
3857
4428
  // src/cli/components/ConnectWizard.tsx
3858
4429
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
3859
4430
  function YN({ question, onAnswer }) {
3860
- useInput4((input, key) => {
4431
+ useInput((input, key) => {
3861
4432
  const ch = input.toLowerCase();
3862
4433
  if (ch === "y") onAnswer(true);
3863
4434
  else if (ch === "n" || key.escape) onAnswer(false);
@@ -3887,8 +4458,8 @@ function ConnectWizard({
3887
4458
  const [error, setError] = useState4(null);
3888
4459
  const [busy, setBusy] = useState4(false);
3889
4460
  const [notes, setNotes] = useState4([]);
3890
- const busyRef = useRef4(false);
3891
- const notesRef = useRef4([]);
4461
+ const busyRef = useRef5(false);
4462
+ const notesRef = useRef5([]);
3892
4463
  const pushNote = (note) => {
3893
4464
  notesRef.current = [...notesRef.current, note];
3894
4465
  setNotes(notesRef.current);
@@ -4071,10 +4642,10 @@ function ConnectWizard({
4071
4642
  }
4072
4643
 
4073
4644
  // src/cli/components/ExitPlanPrompt.tsx
4074
- import { Box as Box5, Text as Text5, useInput as useInput5 } from "ink";
4645
+ import { Box as Box5, Text as Text5 } from "ink";
4075
4646
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
4076
4647
  function ExitPlanPrompt({ onDecision }) {
4077
- useInput5((input, key) => {
4648
+ useInput((input, key) => {
4078
4649
  const ch = input.toLowerCase();
4079
4650
  if (ch === "y") onDecision("yes");
4080
4651
  else if (ch === "n" || key.escape) onDecision("no");
@@ -4087,7 +4658,7 @@ function ExitPlanPrompt({ onDecision }) {
4087
4658
  }
4088
4659
 
4089
4660
  // src/cli/components/MessageList.tsx
4090
- import { Box as Box6, Static, Text as Text7 } from "ink";
4661
+ import { Box as Box6, Static, Text as Text7, useStdout as useStdout2 } from "ink";
4091
4662
  import { memo } from "react";
4092
4663
 
4093
4664
  // src/cli/components/DiffLines.tsx
@@ -4119,26 +4690,44 @@ function DiffLines({ lines }) {
4119
4690
  // src/cli/components/MessageList.tsx
4120
4691
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
4121
4692
  var roleStyles = {
4122
- user: { label: "you", color: "cyan" },
4123
- assistant: { label: "star", color: "green" },
4124
- system: { label: "system", color: "yellow" },
4125
- tool: { label: "tool", color: "magenta" }
4693
+ assistant: { label: "\u2726 star", color: "green" },
4694
+ system: { label: "\u25CF system", color: "yellow" }
4126
4695
  };
4127
4696
  var interruptedMarker = (text) => /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: text.length > 0 ? " [interrupted]" : "[interrupted]" });
4128
4697
  var MessageList = memo(function MessageList2({ messages }) {
4698
+ const { stdout } = useStdout2();
4699
+ const termWidth = stdout.columns ?? 80;
4129
4700
  return /* @__PURE__ */ jsx7(Static, { items: messages, children: (message) => {
4130
4701
  const marginBottom = message.tight ? 0 : 1;
4131
4702
  if (message.role === "assistant-cont") {
4132
4703
  return /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", marginBottom, children: /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
4133
- message.text,
4704
+ renderMarkdown(message.text, "32", termWidth),
4134
4705
  message.interrupted && interruptedMarker(message.text)
4135
4706
  ] }) }, message.id);
4136
4707
  }
4708
+ if (message.role === "user") {
4709
+ return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", marginBottom, children: [
4710
+ /* @__PURE__ */ jsxs7(Box6, { children: [
4711
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "\u276F " }),
4712
+ /* @__PURE__ */ jsxs7(Text7, { color: "cyan", children: [
4713
+ message.text,
4714
+ message.interrupted && interruptedMarker(message.text)
4715
+ ] })
4716
+ ] }),
4717
+ message.note && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4718
+ " ",
4719
+ message.note
4720
+ ] })
4721
+ ] }, message.id);
4722
+ }
4723
+ if (message.role === "tool") {
4724
+ return /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", marginBottom, children: /* @__PURE__ */ jsx7(Text7, { color: "magenta", children: message.text }) }, message.id);
4725
+ }
4137
4726
  const style = roleStyles[message.role];
4138
4727
  return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", marginBottom, children: [
4139
4728
  /* @__PURE__ */ jsx7(Text7, { bold: true, color: style.color, children: style.label }),
4140
4729
  /* @__PURE__ */ jsxs7(Text7, { color: style.color, children: [
4141
- message.text,
4730
+ message.role === "assistant" ? renderMarkdown(message.text, "32", termWidth) : message.text,
4142
4731
  message.interrupted && interruptedMarker(message.text)
4143
4732
  ] }),
4144
4733
  message.diff && /* @__PURE__ */ jsx7(DiffLines, { lines: message.diff }),
@@ -4148,7 +4737,7 @@ var MessageList = memo(function MessageList2({ messages }) {
4148
4737
  });
4149
4738
 
4150
4739
  // src/cli/components/PermissionPrompt.tsx
4151
- import { Box as Box7, Text as Text8, useInput as useInput6 } from "ink";
4740
+ import { Box as Box7, Text as Text8 } from "ink";
4152
4741
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
4153
4742
  var BASH_COMMAND_MAX = 1e3;
4154
4743
  function middleTruncate(text, max) {
@@ -4162,7 +4751,7 @@ function bashCommand(args) {
4162
4751
  return typeof command === "string" ? command : null;
4163
4752
  }
4164
4753
  function PermissionPrompt({ request, preview, onDecision }) {
4165
- useInput6((input) => {
4754
+ useInput((input) => {
4166
4755
  const ch = input.toLowerCase();
4167
4756
  if (ch === "y") onDecision("yes");
4168
4757
  else if (ch === "n") onDecision("no");
@@ -4189,7 +4778,7 @@ function PermissionPrompt({ request, preview, onDecision }) {
4189
4778
  }
4190
4779
 
4191
4780
  // src/cli/components/RewindConfirmPrompt.tsx
4192
- import { Box as Box8, Text as Text9, useInput as useInput7 } from "ink";
4781
+ import { Box as Box8, Text as Text9 } from "ink";
4193
4782
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
4194
4783
  function RewindConfirmPrompt({
4195
4784
  summary,
@@ -4198,7 +4787,7 @@ function RewindConfirmPrompt({
4198
4787
  diffs,
4199
4788
  onDecision
4200
4789
  }) {
4201
- useInput7((input, key) => {
4790
+ useInput((input, key) => {
4202
4791
  const ch = input.toLowerCase();
4203
4792
  if (ch === "y") onDecision("yes");
4204
4793
  else if (ch === "n" || key.escape) onDecision("no");
@@ -4219,8 +4808,74 @@ function RewindConfirmPrompt({
4219
4808
  }
4220
4809
 
4221
4810
  // src/cli/components/StatusBar.tsx
4222
- import { Box as Box9, Text as Text10, useStdout as useStdout2 } from "ink";
4811
+ import { Box as Box9, Text as Text10, useStdout as useStdout3 } from "ink";
4223
4812
  import { memo as memo2 } from "react";
4813
+
4814
+ // src/cli/icons.ts
4815
+ var EMOJI_TERM_PROGRAMS = /* @__PURE__ */ new Set([
4816
+ "vscode",
4817
+ "WezTerm",
4818
+ "iTerm.app",
4819
+ "Apple_Terminal",
4820
+ "ghostty",
4821
+ "kitty",
4822
+ "Hyper",
4823
+ "WarpTerminal"
4824
+ ]);
4825
+ function resolveIconMode(env = process.env) {
4826
+ const override = env.STAR_ICONS;
4827
+ if (override === "emoji" || override === "plain") return override;
4828
+ if (env.WT_SESSION) return "emoji";
4829
+ if (EMOJI_TERM_PROGRAMS.has(env.TERM_PROGRAM ?? "")) return "emoji";
4830
+ const term = env.TERM ?? "";
4831
+ if (term.includes("kitty") || term.includes("alacritty")) return "emoji";
4832
+ return "plain";
4833
+ }
4834
+ var EMOJI_TOOL_ICONS = {
4835
+ bash: "\u{1F4BB}",
4836
+ read_file: "\u{1F4C4}",
4837
+ write_file: "\u{1F4DD}",
4838
+ edit_file: "\u270F\uFE0F",
4839
+ glob: "\u{1F4C2}",
4840
+ grep: "\u{1F50D}",
4841
+ web_search: "\u{1F50E}",
4842
+ web_fetch: "\u{1F310}",
4843
+ todo_write: "\u{1F4CB}",
4844
+ todo_read: "\u{1F4CB}",
4845
+ task_list: "\u{1F9F5}",
4846
+ task_output: "\u{1F9F5}",
4847
+ task_kill: "\u{1F6D1}",
4848
+ subagent: "\u{1F916}",
4849
+ skill: "\u2728",
4850
+ remember: "\u{1F9E0}"
4851
+ };
4852
+ var PLAIN_TOOL_ICONS = {
4853
+ bash: "$",
4854
+ read_file: "\u2261",
4855
+ write_file: "+",
4856
+ edit_file: "\u270E",
4857
+ glob: "*",
4858
+ grep: "/",
4859
+ web_search: "?",
4860
+ web_fetch: "\u21D3",
4861
+ todo_write: "\u2023",
4862
+ todo_read: "\u2023",
4863
+ task_list: "\xB6",
4864
+ task_output: "\xB6",
4865
+ task_kill: "\xD7",
4866
+ subagent: "\u25C9",
4867
+ skill: "\u2727",
4868
+ remember: "\u203B"
4869
+ };
4870
+ var iconMode = resolveIconMode();
4871
+ function toolIcon(name) {
4872
+ const map = iconMode === "emoji" ? EMOJI_TOOL_ICONS : PLAIN_TOOL_ICONS;
4873
+ return map[name] ?? (iconMode === "emoji" ? "\u{1F527}" : "\u25B8");
4874
+ }
4875
+ var thinkingIcon = iconMode === "emoji" ? "\u{1F4AD}" : "\u2727";
4876
+ var folderIcon = iconMode === "emoji" ? "\u{1F4C1} " : "";
4877
+
4878
+ // src/cli/components/StatusBar.tsx
4224
4879
  import { jsxs as jsxs10 } from "react/jsx-runtime";
4225
4880
  var BG_LABEL_MAX = 40;
4226
4881
  var WIDE_COLUMNS = 120;
@@ -4240,12 +4895,13 @@ var StatusBar = memo2(function StatusBar2({
4240
4895
  sessionCostUsd,
4241
4896
  backgroundTasks = []
4242
4897
  }) {
4243
- const { stdout } = useStdout2();
4898
+ const { stdout } = useStdout3();
4244
4899
  const wide = (stdout?.columns ?? 80) >= WIDE_COLUMNS;
4245
4900
  const joined = backgroundTasks.map((label) => toTerminalSafe(label)).join(", ");
4246
4901
  const labels = joined.length > BG_LABEL_MAX ? `${joined.slice(0, BG_LABEL_MAX)}\u2026` : joined;
4247
4902
  return /* @__PURE__ */ jsxs10(Box9, { justifyContent: "space-between", children: [
4248
4903
  /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
4904
+ folderIcon,
4249
4905
  cwdLabel(cwd, wide),
4250
4906
  gitBranch ? ` [${gitBranch}]` : ""
4251
4907
  ] }),
@@ -4257,7 +4913,7 @@ var StatusBar = memo2(function StatusBar2({
4257
4913
  ")"
4258
4914
  ] }),
4259
4915
  /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
4260
- "model: ",
4916
+ "\u2726 ",
4261
4917
  model
4262
4918
  ] }),
4263
4919
  contextPercent != null && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
@@ -4285,23 +4941,24 @@ var StatusBar = memo2(function StatusBar2({
4285
4941
  });
4286
4942
 
4287
4943
  // src/cli/components/StreamingMessage.tsx
4288
- import { Box as Box10, Text as Text11 } from "ink";
4944
+ import { Box as Box10, Text as Text11, useStdout as useStdout4 } from "ink";
4289
4945
  import { memo as memo3 } from "react";
4290
4946
  import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
4291
4947
  var StreamingMessage = memo3(function StreamingMessage2({
4292
4948
  text,
4293
4949
  continuation
4294
4950
  }) {
4951
+ const { stdout } = useStdout4();
4295
4952
  return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", marginBottom: 1, children: [
4296
- !continuation && /* @__PURE__ */ jsx10(Text11, { bold: true, color: "green", children: "star" }),
4297
- /* @__PURE__ */ jsx10(Text11, { color: "green", children: text })
4953
+ !continuation && /* @__PURE__ */ jsx10(Text11, { bold: true, color: "green", children: "\u2726 star" }),
4954
+ /* @__PURE__ */ jsx10(Text11, { color: "green", children: renderMarkdown(text, "32", stdout.columns ?? 80) })
4298
4955
  ] });
4299
4956
  });
4300
4957
 
4301
4958
  // src/cli/components/ThinkingIndicator.tsx
4302
4959
  import { Box as Box11, Text as Text12 } from "ink";
4303
4960
  import { jsxs as jsxs12 } from "react/jsx-runtime";
4304
- var SPINNER_FRAMES = ["-", "\\", "|", "/"];
4961
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4305
4962
  var REASONING_TAIL_LENGTH = 200;
4306
4963
  var THOUGHT_SUMMARY_LENGTH = 100;
4307
4964
  function truncateTail(text, max) {
@@ -4316,14 +4973,15 @@ function ThinkingIndicator({
4316
4973
  }) {
4317
4974
  const tail = reasoning ? truncateTail(reasoning, REASONING_TAIL_LENGTH) : "";
4318
4975
  const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
4976
+ const label = activity ?? `${thinkingIcon} star is thinking\u2026`;
4319
4977
  return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", children: [
4320
4978
  /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
4321
4979
  spinner,
4322
4980
  " ",
4323
- activity ?? "star is thinking\u2026"
4981
+ label
4324
4982
  ] }),
4325
4983
  tail !== "" && /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
4326
- " ",
4984
+ " ",
4327
4985
  tail
4328
4986
  ] })
4329
4987
  ] });
@@ -4333,9 +4991,9 @@ function ThinkingIndicator({
4333
4991
  import { Box as Box12, Text as Text13 } from "ink";
4334
4992
  import { jsxs as jsxs13 } from "react/jsx-runtime";
4335
4993
  var SYMBOLS = {
4336
- pending: "[ ]",
4337
- in_progress: "[~]",
4338
- done: "[x]"
4994
+ pending: "\u25CB",
4995
+ in_progress: "\u25D0",
4996
+ done: "\u25CF"
4339
4997
  };
4340
4998
  var MAX_VISIBLE_TODOS = 10;
4341
4999
  function visibleTodoWindow(todos, max = MAX_VISIBLE_TODOS) {
@@ -4362,6 +5020,7 @@ function TodoPanel({ todos }) {
4362
5020
  Text13,
4363
5021
  {
4364
5022
  dimColor: todo.status === "done",
5023
+ strikethrough: todo.status === "done",
4365
5024
  color: todo.status === "in_progress" ? "cyan" : void 0,
4366
5025
  children: [
4367
5026
  SYMBOLS[todo.status],
@@ -4381,7 +5040,7 @@ function TodoPanel({ todos }) {
4381
5040
 
4382
5041
  // src/cli/components/ToolCallCard.tsx
4383
5042
  function formatToolCard(card) {
4384
- const header = `${card.name} ${card.argsSummary}${card.isError ? " [error]" : ""}`;
5043
+ const header = `${toolIcon(card.name)} ${card.name} ${card.argsSummary}${card.isError ? " [error]" : ""}`;
4385
5044
  if (card.result === void 0) return header;
4386
5045
  const preview = previewLines(toTerminalSafe(card.result), 10);
4387
5046
  return `${header}
@@ -4609,6 +5268,7 @@ function formatUsage(usage) {
4609
5268
  var SESSION_PICKER_CAP = 20;
4610
5269
  var SESSION_PREVIEW_MAX = 60;
4611
5270
  var MAX_VISIBLE_QUEUED = 5;
5271
+ var REASONING_EFFORT_LEVELS = ["minimal", "low", "medium", "high", "max"];
4612
5272
  function Repl({
4613
5273
  backend,
4614
5274
  model,
@@ -4640,47 +5300,47 @@ function Repl({
4640
5300
  const [pendingRewind, setPendingRewindState] = useState5(null);
4641
5301
  const [picker, setPickerState] = useState5(null);
4642
5302
  const [pendingConnect, setPendingConnectState] = useState5(null);
4643
- const backendRef = useRef5(backend);
4644
- const nextIdRef = useRef5(initialDisplay.length);
4645
- const abortRef = useRef5(null);
4646
- const streamedRef = useRef5("");
4647
- const lastCommittedLenRef = useRef5(0);
4648
- const thinkingRef = useRef5(false);
4649
- const reasoningRef = useRef5("");
4650
- const tickerStopRef = useRef5(null);
4651
- const flushedRef = useRef5({ streamed: "", reasoning: "" });
4652
- const toolCardsRef = useRef5(/* @__PURE__ */ new Map());
4653
- const pendingRef = useRef5(null);
4654
- const permissionChainRef = useRef5(Promise.resolve());
4655
- const alwaysAllowedRef = useRef5(/* @__PURE__ */ new Set());
4656
- const prevModeRef = useRef5(null);
4657
- const planApprovalRef = useRef5(false);
4658
- const pendingRewindRef = useRef5(null);
4659
- const pickerRef = useRef5(null);
4660
- const pendingConnectRef = useRef5(null);
4661
- const modelNameRef = useRef5(model);
4662
- const sessionStoreRef = useRef5(sessionStore);
4663
- const configRef = useRef5(config);
4664
- useEffect2(() => {
5303
+ const backendRef = useRef6(backend);
5304
+ const nextIdRef = useRef6(initialDisplay.length);
5305
+ const abortRef = useRef6(null);
5306
+ const streamedRef = useRef6("");
5307
+ const lastCommittedLenRef = useRef6(0);
5308
+ const thinkingRef = useRef6(false);
5309
+ const reasoningRef = useRef6("");
5310
+ const tickerStopRef = useRef6(null);
5311
+ const flushedRef = useRef6({ streamed: "", reasoning: "" });
5312
+ const toolCardsRef = useRef6(/* @__PURE__ */ new Map());
5313
+ const pendingRef = useRef6(null);
5314
+ const permissionChainRef = useRef6(Promise.resolve());
5315
+ const alwaysAllowedRef = useRef6(/* @__PURE__ */ new Set());
5316
+ const prevModeRef = useRef6(null);
5317
+ const planApprovalRef = useRef6(false);
5318
+ const pendingRewindRef = useRef6(null);
5319
+ const pickerRef = useRef6(null);
5320
+ const pendingConnectRef = useRef6(null);
5321
+ const modelNameRef = useRef6(model);
5322
+ const sessionStoreRef = useRef6(sessionStore);
5323
+ const configRef = useRef6(config);
5324
+ useEffect3(() => {
4665
5325
  configRef.current = config;
4666
5326
  }, [config]);
4667
- const usageRef = useRef5(initialUsage ? { ...initialUsage } : emptyUsage());
4668
- const turnChunksRef = useRef5(0);
4669
- const messagesRef = useRef5(initialDisplay);
5327
+ const usageRef = useRef6(initialUsage ? { ...initialUsage } : emptyUsage());
5328
+ const turnChunksRef = useRef6(0);
5329
+ const messagesRef = useRef6(initialDisplay);
4670
5330
  const [queueItems, setQueueItems] = useState5([]);
4671
- const queueRef = useRef5(new PromptQueue());
5331
+ const queueRef = useRef6(new PromptQueue());
4672
5332
  const [pendingImages, setPendingImages] = useState5([]);
4673
- const pendingImagesRef = useRef5([]);
4674
- const imageSeqRef = useRef5(1);
5333
+ const pendingImagesRef = useRef6([]);
5334
+ const imageSeqRef = useRef6(1);
4675
5335
  const [inputRefill, setInputRefill] = useState5();
4676
- const refillSeqRef = useRef5(0);
4677
- const lastEscRef = useRef5(null);
5336
+ const refillSeqRef = useRef6(0);
5337
+ const lastEscRef = useRef6(null);
4678
5338
  const [gitBranch, setGitBranch] = useState5(null);
4679
5339
  const [contextPercent, setContextPercent] = useState5(null);
4680
5340
  const [initialHistory] = useState5(() => loadHistory());
4681
- const budgetWarnedRef = useRef5(false);
4682
- const budgetExceededRef = useRef5(false);
4683
- const runStreamRef = useRef5(
5341
+ const budgetWarnedRef = useRef6(false);
5342
+ const budgetExceededRef = useRef6(false);
5343
+ const runStreamRef = useRef6(
4684
5344
  null
4685
5345
  );
4686
5346
  const pushMessage = useCallback(
@@ -4860,7 +5520,7 @@ function Repl({
4860
5520
  },
4861
5521
  [setPendingPermission, cwd, pushMessage]
4862
5522
  );
4863
- useEffect2(() => {
5523
+ useEffect3(() => {
4864
5524
  attachConfirmHandler(backendRef.current);
4865
5525
  return () => {
4866
5526
  tickerStopRef.current?.();
@@ -4868,19 +5528,19 @@ function Repl({
4868
5528
  pendingRef.current?.resolve(false);
4869
5529
  };
4870
5530
  }, [attachConfirmHandler]);
4871
- useEffect2(() => {
5531
+ useEffect3(() => {
4872
5532
  if (process.env.STAR_NO_UPDATE_CHECK === "1") return;
4873
5533
  void checkForUpdate(VERSION).then((message) => {
4874
5534
  if (message) pushMessage("system", message);
4875
5535
  });
4876
5536
  }, [pushMessage]);
4877
5537
  const resumedAtStart = initialMessages !== void 0;
4878
- useEffect2(() => {
5538
+ useEffect3(() => {
4879
5539
  if (!resumedAtStart) return;
4880
5540
  loadTodos(cwd).then(setTodos).catch(() => {
4881
5541
  });
4882
5542
  }, [cwd, resumedAtStart]);
4883
- useEffect2(() => {
5543
+ useEffect3(() => {
4884
5544
  const onUpdate = (task) => {
4885
5545
  setBgLabels(runningTaskLabels());
4886
5546
  pushMessage(
@@ -4963,7 +5623,7 @@ function Repl({
4963
5623
  }
4964
5624
  exit();
4965
5625
  }, [exit, pushMessage]);
4966
- useInput8((_input, key) => {
5626
+ useInput((_input, key) => {
4967
5627
  if (key.escape) {
4968
5628
  const busy = abortRef.current !== null || pendingRef.current !== null || planApprovalRef.current || pendingRewindRef.current !== null || pendingConnectRef.current !== null || pickerRef.current !== null;
4969
5629
  if (busy) {
@@ -5022,7 +5682,8 @@ function Repl({
5022
5682
  cwd,
5023
5683
  system: SYSTEM_PROMPT,
5024
5684
  sessionStore: sessionStoreRef.current,
5025
- contextMaxTokens: contextWindowTokens(config, name)
5685
+ contextMaxTokens: contextWindowTokens(config, name),
5686
+ providerMetadata: reasoningEffortMetadata(config, name)
5026
5687
  });
5027
5688
  const prev = backendRef.current;
5028
5689
  if (prev instanceof AgentLoop) {
@@ -5039,6 +5700,42 @@ function Repl({
5039
5700
  },
5040
5701
  [config, cwd, attachConfirmHandler]
5041
5702
  );
5703
+ const pickReasoningEffort = useCallback(
5704
+ async (name) => {
5705
+ const modelConfig = config.models.find((m) => m.name === name);
5706
+ const current = modelConfig?.reasoningEffort;
5707
+ const levels = current && !REASONING_EFFORT_LEVELS.includes(current) ? [...REASONING_EFFORT_LEVELS, current] : REASONING_EFFORT_LEVELS;
5708
+ const options = [
5709
+ {
5710
+ value: "",
5711
+ label: "default",
5712
+ description: "send no effort field \u2014 the provider's server-side default applies",
5713
+ hint: current === void 0 ? "current" : void 0
5714
+ },
5715
+ ...levels.map((level) => ({
5716
+ value: level,
5717
+ label: level,
5718
+ description: "only works if the model/provider supports this level",
5719
+ hint: level === current ? "current" : void 0
5720
+ }))
5721
+ ];
5722
+ const picked = await showPicker(`Reasoning effort for "${name}"`, options);
5723
+ if (picked === null || picked === "" && current === void 0 || picked === current) {
5724
+ return `Reasoning effort unchanged${current ? ` \u2014 still "${current}"` : " \u2014 server default"}.`;
5725
+ }
5726
+ const effort = picked === "" ? void 0 : picked;
5727
+ if (modelConfig) modelConfig.reasoningEffort = effort;
5728
+ const backend2 = backendRef.current;
5729
+ if (backend2 instanceof AgentLoop) {
5730
+ backend2.setProviderMetadata(reasoningEffortMetadata(config, name));
5731
+ }
5732
+ const saved = await saveReasoningEffort(name, effort);
5733
+ const applied = effort === void 0 ? "server default" : `"${effort}"`;
5734
+ const suffix = saved ? "Saved to config." : `This session only \u2014 "${name}" has no [[models]] block in the global config file; add reasoningEffort there to persist it.`;
5735
+ return `Reasoning effort for "${name}" set to ${applied}. ${suffix}`;
5736
+ },
5737
+ [config, showPicker]
5738
+ );
5042
5739
  const handleConnectSetDefault = useCallback(
5043
5740
  async (modelName2) => {
5044
5741
  saveDefaultModel(modelName2);
@@ -5148,7 +5845,7 @@ function Repl({
5148
5845
  setThinking(false);
5149
5846
  if (reasoningRef.current.length > 0) {
5150
5847
  setThoughtSummary(
5151
- `thought: ${truncateTail(reasoningRef.current, THOUGHT_SUMMARY_LENGTH)}`
5848
+ `${thinkingIcon} thought: ${truncateTail(reasoningRef.current, THOUGHT_SUMMARY_LENGTH)}`
5152
5849
  );
5153
5850
  }
5154
5851
  }
@@ -5166,7 +5863,9 @@ function Repl({
5166
5863
  const list = parseTodoArgs(event.args);
5167
5864
  if (list) setTodos(list);
5168
5865
  }
5169
- setActivity(`running ${event.name}: ${summarizeArgs(event.args, 60)}`);
5866
+ setActivity(
5867
+ `${toolIcon(event.name)} running ${event.name}: ${summarizeArgs(event.args, 60)}`
5868
+ );
5170
5869
  } else if (event.type === "tool-result") {
5171
5870
  const card = toolCardsRef.current.get(event.id) ?? {
5172
5871
  id: event.id,
@@ -5315,7 +6014,7 @@ function Repl({
5315
6014
  sessionCostUsd
5316
6015
  ]
5317
6016
  );
5318
- useEffect2(() => {
6017
+ useEffect3(() => {
5319
6018
  runStreamRef.current = runStream;
5320
6019
  }, [runStream]);
5321
6020
  const handlePlanDecision = useCallback(
@@ -5369,7 +6068,7 @@ function Repl({
5369
6068
  const models = listModels(config);
5370
6069
  if (models.length === 0) return "No models configured.";
5371
6070
  const lines = models.map(
5372
- (m) => `${m.name === modelNameRef.current ? "*" : " "} ${m.name} (${m.provider}/${m.model})`
6071
+ (m) => `${m.name === modelNameRef.current ? "*" : " "} ${m.name} (${m.provider}/${m.model})${m.reasoningEffort ? ` [effort: ${m.reasoningEffort}]` : ""}`
5373
6072
  );
5374
6073
  return `Models (* = current):
5375
6074
  ${lines.join("\n")}`;
@@ -5388,6 +6087,7 @@ ${lines.join("\n")}`;
5388
6087
  if (models.length === 0) return "No models configured.";
5389
6088
  const options = models.map((m) => {
5390
6089
  const parts = [`${m.provider}/${m.model}`, `${contextWindowTokens(config, m.name)} ctx`];
6090
+ if (m.reasoningEffort) parts.push(`effort: ${m.reasoningEffort}`);
5391
6091
  if (m.promptPrice !== void 0 && m.completionPrice !== void 0) {
5392
6092
  parts.push(`$${m.promptPrice}/$${m.completionPrice} per 1M tokens`);
5393
6093
  }
@@ -5399,10 +6099,17 @@ ${lines.join("\n")}`;
5399
6099
  };
5400
6100
  });
5401
6101
  const picked = await showPicker("Select a model", options);
5402
- if (!picked || picked === modelNameRef.current) {
5403
- return `Model unchanged \u2014 still "${modelNameRef.current}".`;
6102
+ if (!picked) return `Model unchanged \u2014 still "${modelNameRef.current}".`;
6103
+ const results = [];
6104
+ if (picked === modelNameRef.current) {
6105
+ results.push(`Model unchanged \u2014 still "${picked}".`);
6106
+ } else {
6107
+ const switched = await switchModel(picked);
6108
+ if (switched.startsWith("Failed to switch model:")) return switched;
6109
+ results.push(switched);
5404
6110
  }
5405
- return switchModel(picked);
6111
+ results.push(await pickReasoningEffort(picked));
6112
+ return results.join("\n");
5406
6113
  },
5407
6114
  pickPermissionMode: async () => {
5408
6115
  const current = config.permissionMode;
@@ -5719,7 +6426,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
5719
6426
  `permissionMode: ${config.permissionMode}`,
5720
6427
  `providers (${config.providers.length}): ${config.providers.map((p) => p.name).join(", ") || "(none)"}`,
5721
6428
  `models (${config.models.length}): ${config.models.map((m) => m.name).join(", ") || "(none)"}`,
5722
- `maxSteps: ${config.maxSteps}`,
6429
+ `maxSteps: ${config.maxSteps === 0 ? "unlimited" : config.maxSteps}`,
5723
6430
  `contextMaxTokens: ${config.contextMaxTokens}`,
5724
6431
  `compactThresholdTokens: ${config.compactThresholdTokens ?? "(context window)"}`,
5725
6432
  `streamIdleTimeoutSec: ${config.streamIdleTimeoutSec}`,
@@ -5740,6 +6447,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
5740
6447
  config,
5741
6448
  cwd,
5742
6449
  switchModel,
6450
+ pickReasoningEffort,
5743
6451
  resume,
5744
6452
  runStream,
5745
6453
  applyMessages,
@@ -5843,7 +6551,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
5843
6551
  })),
5844
6552
  [registry]
5845
6553
  );
5846
- useEffect2(() => {
6554
+ useEffect3(() => {
5847
6555
  setGitBranch(getGitSummary(cwd)?.branch ?? null);
5848
6556
  const current = backendRef.current;
5849
6557
  if (current instanceof AgentLoop) {
@@ -6059,7 +6767,8 @@ async function createLoop(config, modelName, cwd, sessionStore) {
6059
6767
  cwd,
6060
6768
  system: SYSTEM_PROMPT,
6061
6769
  sessionStore,
6062
- contextMaxTokens: contextWindowTokens(config, modelName)
6770
+ contextMaxTokens: contextWindowTokens(config, modelName),
6771
+ providerMetadata: reasoningEffortMetadata(config, modelName)
6063
6772
  });
6064
6773
  }
6065
6774
  async function printMode(loop, prompt, cwd, json, imagePaths, sessionStore) {