@mtreeai/msapling-cli 2.3.6-beta.60 → 2.3.6-beta.62

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.
Files changed (2) hide show
  1. package/dist/index.js +525 -197
  2. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -14450,6 +14450,14 @@ var init_Settings = __esm({
14450
14450
  });
14451
14451
 
14452
14452
  // ../core/src/agent/localModelPolicy.ts
14453
+ function buildDeterministicReadPlan(prompt4, availableTools) {
14454
+ if (MUTATING_INTENT.test(prompt4) || !LIST_DIRECTORY_INTENT.test(prompt4)) return null;
14455
+ if (!new Set(availableTools).has("list_directory")) return null;
14456
+ const absolute = (prompt4.match(QUOTED_WINDOWS_PATH)?.[1] ?? prompt4.match(WINDOWS_DRIVE_ROOT)?.[1] ?? prompt4.match(WINDOWS_ABSOLUTE_TOKEN)?.[1])?.trim();
14457
+ const drive = prompt4.match(WINDOWS_DRIVE_WORDS)?.[1] ?? prompt4.match(WINDOWS_DIRECTORY_THEN_DRIVE)?.[1];
14458
+ const path3 = absolute || (drive ? `${drive.toUpperCase()}:\\` : ".");
14459
+ return { tool: "list_directory", args: { path: path3 } };
14460
+ }
14453
14461
  function buildReadToolRecovery(prompt4, response, availableTools) {
14454
14462
  if (MUTATING_INTENT.test(prompt4)) return null;
14455
14463
  const hasReadIntent = SAFE_READ_INTENT.test(prompt4) || SAFE_READ_VERB.test(prompt4);
@@ -14478,7 +14486,7 @@ function inferCapability(model) {
14478
14486
  if (sizes.some((size) => size <= 7)) return "small";
14479
14487
  return "unknown";
14480
14488
  }
14481
- var LOCAL_ACCESS_DENIAL, SAFE_READ_INTENT, SAFE_READ_VERB, PATH_LIKE_INTENT, MUTATING_INTENT, READ_RECOVERY_TOOLS, SAFE_TOOLS;
14489
+ var LOCAL_ACCESS_DENIAL, SAFE_READ_INTENT, SAFE_READ_VERB, PATH_LIKE_INTENT, MUTATING_INTENT, READ_RECOVERY_TOOLS, LIST_DIRECTORY_INTENT, QUOTED_WINDOWS_PATH, WINDOWS_DRIVE_ROOT, WINDOWS_ABSOLUTE_TOKEN, WINDOWS_DRIVE_WORDS, WINDOWS_DIRECTORY_THEN_DRIVE, SAFE_TOOLS;
14482
14490
  var init_localModelPolicy = __esm({
14483
14491
  "../core/src/agent/localModelPolicy.ts"() {
14484
14492
  "use strict";
@@ -14489,6 +14497,12 @@ var init_localModelPolicy = __esm({
14489
14497
  PATH_LIKE_INTENT = /(?:[A-Za-z]:[\\/]|\/{1,2}[A-Za-z0-9_.-]+\/|\.\.?[\\/])[A-Za-z0-9_.\\/ -]*/;
14490
14498
  MUTATING_INTENT = /\b(?:create|overwrite|write|edit|modify|change|delete|remove|move|rename|execute|run|install|uninstall|commit|push|upload)\b/i;
14491
14499
  READ_RECOVERY_TOOLS = ["read_file", "list_directory", "glob_files", "grep_search"];
14500
+ LIST_DIRECTORY_INTENT = /\b(?:list|show|display|enumerate|review|inspect|what(?:'s| is)? (?:in|inside))\b[^\n]{0,120}\b(?:files?|folders?|director(?:y|ies)|drive|workspace|project)\b/i;
14501
+ QUOTED_WINDOWS_PATH = /["']([A-Za-z]:[\\/][^"']*)["']/;
14502
+ WINDOWS_DRIVE_ROOT = /\b([A-Za-z]:[\\/])(?=\s|$|[.,;:)])/;
14503
+ WINDOWS_ABSOLUTE_TOKEN = /\b([A-Za-z]:[\\/][^\s,;]*)/;
14504
+ WINDOWS_DRIVE_WORDS = /\b([A-Za-z])\s+(?:drive|directory)\b/i;
14505
+ WINDOWS_DIRECTORY_THEN_DRIVE = /\b(?:drive|directory)\s+([A-Za-z])\b/i;
14492
14506
  SAFE_TOOLS = /* @__PURE__ */ new Set([
14493
14507
  "read_file",
14494
14508
  "list_directory",
@@ -15028,6 +15042,33 @@ var init_Agent = __esm({
15028
15042
  }
15029
15043
  return this.executor.execute(toolName, args2, this.projectRoot);
15030
15044
  }
15045
+ async executeDeterministicReadAdapter(chatId, turnId, plan) {
15046
+ const toolCallId = `client-read-${randomUUID6()}`;
15047
+ const request = {
15048
+ toolName: plan.tool,
15049
+ toolCallId,
15050
+ turnId,
15051
+ input: plan.args
15052
+ };
15053
+ await this.modeContract?.recordToolRequest(request);
15054
+ try {
15055
+ const result = await this.executeTurnTool(plan.tool, plan.args, false);
15056
+ await this.modeContract?.recordToolResult(request, result.content, !!result.isError);
15057
+ this.recordRemoteToolTelemetry(
15058
+ chatId,
15059
+ turnId,
15060
+ toolCallId,
15061
+ plan.tool,
15062
+ result.isError ? "failed" : "completed"
15063
+ );
15064
+ return result;
15065
+ } catch (error) {
15066
+ const content = SafetyGuard.redact(error instanceof Error ? error.message : String(error));
15067
+ await this.modeContract?.recordToolResult(request, content, true);
15068
+ this.recordRemoteToolTelemetry(chatId, turnId, toolCallId, plan.tool, "failed");
15069
+ throw error;
15070
+ }
15071
+ }
15031
15072
  recordRemoteToolTelemetry(chatId, turnId, toolCallId, toolName, outcome) {
15032
15073
  if (this.executionMode !== "remote") return;
15033
15074
  if (this.toolTelemetryQueue.length >= TOOL_TELEMETRY_QUEUE_MAX) {
@@ -15277,7 +15318,37 @@ var init_Agent = __esm({
15277
15318
  structuredToolResults = false;
15278
15319
  }
15279
15320
  }
15280
- const queue = [{ prompt: prompt4, allowReadRecovery: true }];
15321
+ let initialQueueItem = { prompt: prompt4, allowReadRecovery: true };
15322
+ if (!localTurn) {
15323
+ const initialToolNames = this.getTurnToolSchemas(false).map((tool) => String(tool.name));
15324
+ const deterministicPlan = buildDeterministicReadPlan(prompt4, initialToolNames);
15325
+ if (deterministicPlan) {
15326
+ const diagnostic = `[MSapling: running safe local ${deterministicPlan.tool} before the connected model.]
15327
+ `;
15328
+ await this.modeContract?.recordOutput(chatId, contractTurnId, diagnostic);
15329
+ onContent(diagnostic);
15330
+ const result = await this.executeDeterministicReadAdapter(
15331
+ chatId,
15332
+ contractTurnId,
15333
+ deterministicPlan
15334
+ );
15335
+ const evidence = boundToolResultForModel(result.content);
15336
+ initialQueueItem = {
15337
+ prompt: `A trusted MSapling client read-only adapter executed ${deterministicPlan.tool} with ${JSON.stringify(deterministicPlan.args)} for the original request below.
15338
+
15339
+ <client_tool_result error="${result.isError === true}">
15340
+ ${evidence}
15341
+ </client_tool_result>
15342
+
15343
+ Original request: ${prompt4}
15344
+
15345
+ Answer directly from the tool result. Do not claim local access is unavailable, do not emit a patch or diff, and do not invent entries.`,
15346
+ allowedToolNames: [],
15347
+ allowReadRecovery: false
15348
+ };
15349
+ }
15350
+ }
15351
+ const queue = [initialQueueItem];
15281
15352
  let rounds = 0;
15282
15353
  let toolCallSeq = 0;
15283
15354
  let streamUsage = null;
@@ -15450,16 +15521,44 @@ ${next.prompt}`
15450
15521
  const recovery = !localTurn && allowReadRecovery && !remoteReadRecoveryUsed && !assistantResponse.includes("[MSapling: local model denied available file tools;") ? buildReadToolRecovery(prompt4, assistantResponse, turnTools.map((tool) => String(tool.name))) : null;
15451
15522
  if (recovery) {
15452
15523
  remoteReadRecoveryUsed = true;
15453
- const diagnostic = `
15524
+ const deterministicPlan = buildDeterministicReadPlan(prompt4, recovery.tools);
15525
+ if (deterministicPlan) {
15526
+ const diagnostic = `
15527
+
15528
+ [MSapling: connected model did not use the required read tool; running safe local ${deterministicPlan.tool}.]
15529
+ `;
15530
+ await this.modeContract?.recordOutput(chatId, contractTurnId, diagnostic);
15531
+ onContent(diagnostic);
15532
+ const result = await this.executeDeterministicReadAdapter(
15533
+ chatId,
15534
+ contractTurnId,
15535
+ deterministicPlan
15536
+ );
15537
+ const evidence = boundToolResultForModel(result.content);
15538
+ queue.push({
15539
+ prompt: `A trusted MSapling client read-only adapter executed ${deterministicPlan.tool} with ${JSON.stringify(deterministicPlan.args)} for the original request below.
15540
+
15541
+ <client_tool_result error="${result.isError === true}">
15542
+ ${evidence}
15543
+ </client_tool_result>
15544
+
15545
+ Original request: ${prompt4}
15546
+
15547
+ Answer from the tool result. Do not claim local access is unavailable.`,
15548
+ allowedToolNames: []
15549
+ });
15550
+ } else {
15551
+ const diagnostic = `
15454
15552
 
15455
15553
  [MSapling: connected model did not use required client file tools; retrying once with ${recovery.tools.join(", ")}.]
15456
15554
  `;
15457
- await this.modeContract?.recordOutput(chatId, contractTurnId, diagnostic);
15458
- onContent(diagnostic);
15459
- queue.push({
15460
- prompt: recovery.instruction,
15461
- allowedToolNames: recovery.tools
15462
- });
15555
+ await this.modeContract?.recordOutput(chatId, contractTurnId, diagnostic);
15556
+ onContent(diagnostic);
15557
+ queue.push({
15558
+ prompt: recovery.instruction,
15559
+ allowedToolNames: recovery.tools
15560
+ });
15561
+ }
15463
15562
  }
15464
15563
  continue;
15465
15564
  }
@@ -19163,6 +19262,105 @@ var init_src3 = __esm({
19163
19262
  }
19164
19263
  });
19165
19264
 
19265
+ // src/runtime/terminalScreen.ts
19266
+ function sanitizeTerminalText(value, preserveNewlines = true) {
19267
+ const text = String(value ?? "");
19268
+ const escape = String.fromCharCode(27);
19269
+ const bell = String.fromCharCode(7);
19270
+ const withoutEscapes = text.replace(new RegExp(`${escape}\\](?:[^${bell}${escape}]|${escape}(?!\\\\))*?(?:${bell}|${escape}\\\\)`, "g"), "").replace(new RegExp(`${escape}P(?:[^${escape}]|${escape}(?!\\\\))*?(?:${escape}\\\\)`, "g"), "").replace(new RegExp(`${escape}\\[[0-?]*[ -/]*[@-~]`, "g"), "").replace(new RegExp(`${escape}[()][0-2A-Z]`, "g"), "").replace(new RegExp(`${escape}.`, "g"), "");
19271
+ let safe = "";
19272
+ for (const character of withoutEscapes) {
19273
+ const code = character.codePointAt(0) ?? 0;
19274
+ const isNewline = code === 10;
19275
+ const isControl = code < 32 || code === 127;
19276
+ if (isControl && (!preserveNewlines || !isNewline)) continue;
19277
+ safe += character;
19278
+ }
19279
+ return safe;
19280
+ }
19281
+ function enterAlternateScreen(output = process.stdout, env = process.env) {
19282
+ if (alternateScreenActive || !output.isTTY || env.MSAPLING_NO_ALT_SCREEN === "1" || env.TERM === "dumb") return false;
19283
+ output.write(ENTER_ALTERNATE_SCREEN + (env.MSAPLING_NO_MOUSE_SCROLL === "1" ? "" : ENABLE_MOUSE_SCROLL));
19284
+ alternateScreenActive = true;
19285
+ return true;
19286
+ }
19287
+ function isSgrMouseReport(input) {
19288
+ const escape = String.fromCharCode(27);
19289
+ return new RegExp(`${escape}\\[<[0-9]+;[0-9]+;[0-9]+[mM]`).test(input);
19290
+ }
19291
+ function parseMouseWheel(input) {
19292
+ const escape = String.fromCharCode(27);
19293
+ const match = new RegExp(`${escape}\\[<([0-9]+);[0-9]+;[0-9]+[mM]`).exec(input);
19294
+ if (!match) return null;
19295
+ const button = Number(match[1]);
19296
+ if (button === 64) return "up";
19297
+ if (button === 65) return "down";
19298
+ return null;
19299
+ }
19300
+ function leaveAlternateScreen(output = process.stdout) {
19301
+ if (!alternateScreenActive) return false;
19302
+ output.write(LEAVE_ALTERNATE_SCREEN);
19303
+ alternateScreenActive = false;
19304
+ return true;
19305
+ }
19306
+ var ENTER_ALTERNATE_SCREEN, ENABLE_MOUSE_SCROLL, LEAVE_ALTERNATE_SCREEN, alternateScreenActive, MAX_PROTOCOL_BUFFER, TerminalInputDecoder;
19307
+ var init_terminalScreen = __esm({
19308
+ "src/runtime/terminalScreen.ts"() {
19309
+ "use strict";
19310
+ init_esm_shims();
19311
+ ENTER_ALTERNATE_SCREEN = "\x1B[?1049h\x1B[H";
19312
+ ENABLE_MOUSE_SCROLL = "\x1B[?1000h\x1B[?1006h";
19313
+ LEAVE_ALTERNATE_SCREEN = "\x1B[?1006l\x1B[?1000l\x1B[?1049l\x1B[?25h";
19314
+ alternateScreenActive = false;
19315
+ MAX_PROTOCOL_BUFFER = 64 * 1024;
19316
+ TerminalInputDecoder = class {
19317
+ pending = "";
19318
+ inBracketedPaste = false;
19319
+ feed(input) {
19320
+ const escape = String.fromCharCode(27);
19321
+ if (this.inBracketedPaste) {
19322
+ const candidate2 = this.pending + input;
19323
+ const endMarker = `${escape}[201~`;
19324
+ const end = candidate2.indexOf(endMarker);
19325
+ if (end < 0) {
19326
+ if (candidate2.length > MAX_PROTOCOL_BUFFER) this.reset();
19327
+ else this.pending = candidate2;
19328
+ return { protocol: true, text: "", mouseWheel: null };
19329
+ }
19330
+ const text = candidate2.slice(0, end);
19331
+ const remainder = candidate2.slice(end + endMarker.length);
19332
+ this.reset();
19333
+ return { protocol: true, text: text + remainder, mouseWheel: null };
19334
+ }
19335
+ const candidate = this.pending + input;
19336
+ this.pending = "";
19337
+ const pasteStart = `${escape}[200~`;
19338
+ if (candidate.startsWith(pasteStart)) {
19339
+ this.inBracketedPaste = true;
19340
+ const remainder = candidate.slice(pasteStart.length);
19341
+ return this.feed(remainder);
19342
+ }
19343
+ if (!candidate.startsWith(escape)) return { protocol: false, text: input, mouseWheel: null };
19344
+ const wheel = parseMouseWheel(candidate);
19345
+ if (wheel || isSgrMouseReport(candidate)) {
19346
+ return { protocol: true, text: "", mouseWheel: wheel };
19347
+ }
19348
+ const sgrPrefix = `${escape}[<`;
19349
+ const isPartialSgr = sgrPrefix.startsWith(candidate) || candidate.startsWith(sgrPrefix);
19350
+ if (isPartialSgr && candidate.length <= MAX_PROTOCOL_BUFFER) {
19351
+ this.pending = candidate;
19352
+ return { protocol: true, text: "", mouseWheel: null };
19353
+ }
19354
+ return { protocol: true, text: "", mouseWheel: null };
19355
+ }
19356
+ reset() {
19357
+ this.pending = "";
19358
+ this.inBracketedPaste = false;
19359
+ }
19360
+ };
19361
+ }
19362
+ });
19363
+
19166
19364
  // src/runtime/errorPresentation.ts
19167
19365
  function errorRecord(error) {
19168
19366
  return error && typeof error === "object" ? error : {};
@@ -19286,19 +19484,20 @@ function renderCliError(error) {
19286
19484
  tool: "Local tool guardrail",
19287
19485
  cli: "Local CLI"
19288
19486
  }[error.surface];
19289
- return [
19487
+ return sanitizeTerminalText([
19290
19488
  `[${error.code}] ${error.summary}`,
19291
19489
  `Surface: ${surface}`,
19292
19490
  `Why: ${error.explanation}`,
19293
19491
  `Next: ${error.action}`,
19294
19492
  ...error.detail ? [`Detail: ${error.detail}`] : []
19295
- ].join("\n");
19493
+ ].join("\n"));
19296
19494
  }
19297
19495
  var init_errorPresentation = __esm({
19298
19496
  "src/runtime/errorPresentation.ts"() {
19299
19497
  "use strict";
19300
19498
  init_esm_shims();
19301
19499
  init_src3();
19500
+ init_terminalScreen();
19302
19501
  }
19303
19502
  });
19304
19503
 
@@ -19752,7 +19951,6 @@ var init_toggles = __esm({
19752
19951
  sessionToggles = /* @__PURE__ */ new Map([
19753
19952
  ["effort", "medium"],
19754
19953
  // 'low' | 'medium' | 'high'
19755
- ["vimMode", false],
19756
19954
  ["fastMode", false],
19757
19955
  ["simpleMode", false],
19758
19956
  ["filterRegex", null]
@@ -19807,12 +20005,18 @@ var init_toggles = __esm({
19807
20005
  context.addMessage("system", `effort set to: ${arg}`);
19808
20006
  }
19809
20007
  };
19810
- vimCommand = makeToggle(
19811
- "vim",
19812
- "vimMode",
19813
- "vim mode",
19814
- "Toggle vim mode for REPL input (vim [on|off] to set explicitly)"
19815
- );
20008
+ vimCommand = {
20009
+ name: "vim",
20010
+ args: "[on|off]",
20011
+ description: "Show Vim editor-mode availability (not yet implemented)",
20012
+ category: "config",
20013
+ handler: (_args, context) => {
20014
+ context.addMessage(
20015
+ "system",
20016
+ "Vim input mode is not available yet. /vim does not change editor behavior; use the standard cursor and history keys shown by /shortcuts."
20017
+ );
20018
+ }
20019
+ };
19816
20020
  fastCommand = makeToggle(
19817
20021
  "fast",
19818
20022
  "fastMode",
@@ -23566,7 +23770,7 @@ var init_version = __esm({
23566
23770
  description: "Show version information for CLI and core packages",
23567
23771
  category: "debug",
23568
23772
  handler: async (_args, context) => {
23569
- const cliVersion = true ? "2.3.6-beta.60" : "(dev)";
23773
+ const cliVersion = true ? "2.3.6-beta.62" : "(dev)";
23570
23774
  const coreVersion = true ? "2.3.6-beta.43" : "(dev)";
23571
23775
  const runtime = process.version;
23572
23776
  context.addMessage("system", "MSapling Version Info");
@@ -23840,7 +24044,7 @@ var init_shortcuts = __esm({
23840
24044
  shortcutsCommand = {
23841
24045
  name: "shortcuts",
23842
24046
  aliases: ["sc"],
23843
- description: "List all registered slash commands with descriptions and aliases",
24047
+ description: "List keyboard controls and registered slash commands",
23844
24048
  category: "debug",
23845
24049
  handler: (args2, context) => {
23846
24050
  const { commands: commands2 } = (init_commands(), __toCommonJS(commands_exports));
@@ -23876,7 +24080,19 @@ ${matching.length} command(s) found.`);
23876
24080
  ...CATEGORY_ORDER2.filter((c) => grouped[c]),
23877
24081
  ...Object.keys(grouped).filter((c) => !CATEGORY_ORDER2.includes(c)).sort()
23878
24082
  ];
23879
- context.addMessage("system", "Registered Slash Commands");
24083
+ context.addMessage("system", [
24084
+ "Keyboard Controls",
24085
+ " \u2191/\u2193 (empty prompt) Scroll transcript one row",
24086
+ " PageUp/PageDown Scroll transcript one page",
24087
+ " Ctrl+E Return to newest transcript row",
24088
+ " \u2190/\u2192 Move the prompt cursor",
24089
+ " Ctrl+A Move to start of prompt",
24090
+ " Ctrl+P/Ctrl+N Previous/next prompt history",
24091
+ " Shift+Enter Insert a newline",
24092
+ " Escape/Ctrl+C Cancel current operation",
24093
+ "",
24094
+ "Registered Slash Commands"
24095
+ ].join("\n"));
23880
24096
  context.addMessage("system", tableSep());
23881
24097
  for (const cat of cats) {
23882
24098
  const cmds = grouped[cat].sort((a, b) => a.name.localeCompare(b.name));
@@ -29593,7 +29809,7 @@ import { render } from "ink";
29593
29809
 
29594
29810
  // src/App.tsx
29595
29811
  init_esm_shims();
29596
- import { useState as useState5, useEffect as useEffect4, useCallback as useCallback2, useRef as useRef2 } from "react";
29812
+ import { useState as useState5, useEffect as useEffect3, useCallback as useCallback2, useRef as useRef2 } from "react";
29597
29813
  import { randomUUID as randomUUID10 } from "crypto";
29598
29814
  import { Box as Box7, Text as Text7, useApp, useInput as useInput4, useStdout } from "ink";
29599
29815
 
@@ -29603,11 +29819,11 @@ import { Box, Text } from "ink";
29603
29819
  import { jsx, jsxs } from "react/jsx-runtime";
29604
29820
  var Header = ({ compact: compact2 = false }) => compact2 ? /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
29605
29821
  "\u25CF MSapling v",
29606
- "2.3.6-beta.60"
29607
- ] }) : /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [
29822
+ "2.3.6-beta.62"
29823
+ ] }) : /* @__PURE__ */ jsxs(Box, { width: "100%", borderStyle: "single", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [
29608
29824
  /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
29609
29825
  "\u25CF MSapling CLI v",
29610
- "2.3.6-beta.60"
29826
+ "2.3.6-beta.62"
29611
29827
  ] }),
29612
29828
  /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "Platinum Tier Architecture" }) })
29613
29829
  ] });
@@ -29615,6 +29831,7 @@ var Header = ({ compact: compact2 = false }) => compact2 ? /* @__PURE__ */ jsxs(
29615
29831
  // src/components/Footer.tsx
29616
29832
  init_esm_shims();
29617
29833
  init_src3();
29834
+ init_terminalScreen();
29618
29835
  import { Box as Box2, Text as Text2 } from "ink";
29619
29836
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
29620
29837
  var PRO_TIERS2 = /* @__PURE__ */ new Set(["pro", "monthly", "lifetime", "enterprise", "admin", "superadmin"]);
@@ -29660,18 +29877,21 @@ var Footer = ({ user, options, model, project, chatId, cwd, mode, lastCost, sess
29660
29877
  const showCapMeter = user ? shouldShowCapMeter(user) : false;
29661
29878
  const dailyPct = user && showCapMeter ? Math.min(100, Math.round(user.daily_tokens_used / user.daily_tokens_limit * 100)) : 0;
29662
29879
  const standaloneSearchCost = getSessionStats().snapshot().standaloneSearch.estimatedCostUsd;
29663
- return /* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1, flexDirection: "column", children: [
29880
+ const safeModel = sanitizeTerminalText(model, false);
29881
+ const safeProject = sanitizeTerminalText(project, false);
29882
+ const safeChatId = chatId ? sanitizeTerminalText(chatId, false) : null;
29883
+ return /* @__PURE__ */ jsxs2(Box2, { width: "100%", borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1, flexDirection: "column", children: [
29664
29884
  /* @__PURE__ */ jsxs2(Box2, { justifyContent: "space-between", children: [
29665
29885
  /* @__PURE__ */ jsx2(Text2, { bold: true, children: "CLI telemetry" }),
29666
29886
  /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: formatAccountSummary(user, { billingProfile, isStale, usageError }) })
29667
29887
  ] }),
29668
29888
  expanded && options.chat && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
29669
29889
  "project: ",
29670
- project,
29890
+ safeProject,
29671
29891
  " chat: ",
29672
- chatId ?? "none",
29892
+ safeChatId ?? "none",
29673
29893
  " model: ",
29674
- model
29894
+ safeModel
29675
29895
  ] }),
29676
29896
  options.location && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
29677
29897
  "cwd: ",
@@ -29709,44 +29929,9 @@ function promptLabel(user) {
29709
29929
 
29710
29930
  // src/components/ApprovalDialog.tsx
29711
29931
  init_esm_shims();
29932
+ init_terminalScreen();
29712
29933
  import { useState, useMemo } from "react";
29713
29934
  import { Box as Box3, Text as Text3, useInput } from "ink";
29714
-
29715
- // src/hooks/useTerminalMouseScroll.ts
29716
- init_esm_shims();
29717
- import { useEffect, useRef } from "react";
29718
- var ESC = String.fromCharCode(27);
29719
- function isMouseReport(input) {
29720
- return input.includes(`${ESC}[<`) && /\d+;\d+;\d+[mM]/.test(input);
29721
- }
29722
- function parseMouseScroll(input) {
29723
- const directions = [];
29724
- const pattern = new RegExp(`${ESC}\\[<(64|65);\\d+;\\d+[mM]`, "g");
29725
- for (const match of input.matchAll(pattern)) {
29726
- directions.push(match[1] === "64" ? "up" : "down");
29727
- }
29728
- return directions;
29729
- }
29730
- function useTerminalMouseScroll(onScroll) {
29731
- const callback = useRef(onScroll);
29732
- callback.current = onScroll;
29733
- useEffect(() => {
29734
- if (!process.stdin.isTTY || !process.stdout.isTTY) return;
29735
- const handleData = (chunk) => {
29736
- for (const direction of parseMouseScroll(String(chunk))) {
29737
- callback.current(direction);
29738
- }
29739
- };
29740
- process.stdout.write("\x1B[?1000h\x1B[?1006h");
29741
- process.stdin.on("data", handleData);
29742
- return () => {
29743
- process.stdin.off("data", handleData);
29744
- process.stdout.write("\x1B[?1006l\x1B[?1000l");
29745
- };
29746
- }, []);
29747
- }
29748
-
29749
- // src/components/ApprovalDialog.tsx
29750
29935
  import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
29751
29936
  function parseDiffLines(diffText) {
29752
29937
  if (!diffText) return [];
@@ -29810,10 +29995,13 @@ var ApprovalDialog = ({
29810
29995
  onResolve
29811
29996
  }) => {
29812
29997
  const [showFullDiff, setShowFullDiff] = useState(false);
29813
- const { hasDiff, diffText, title } = useMemo(() => extractDiffFromCommand(command, diff), [command, diff]);
29998
+ const safeCommand = sanitizeTerminalText(command, false);
29999
+ const safeReason = sanitizeTerminalText(reason);
30000
+ const safePath = targetPath ? sanitizeTerminalText(targetPath, false) : void 0;
30001
+ const safeDiff = diff ? sanitizeTerminalText(diff) : void 0;
30002
+ const { hasDiff, diffText, title } = useMemo(() => extractDiffFromCommand(safeCommand, safeDiff), [safeCommand, safeDiff]);
29814
30003
  const diffLines = useMemo(() => parseDiffLines(diffText), [diffText]);
29815
30004
  useInput((input, key) => {
29816
- if (isMouseReport(input)) return;
29817
30005
  if (key.escape) {
29818
30006
  onResolve("no");
29819
30007
  return;
@@ -29833,16 +30021,16 @@ var ApprovalDialog = ({
29833
30021
  const isTruncated = !showFullDiff && diffLines.length > MAX_DIFF_COLLAPSED_LINES;
29834
30022
  return /* @__PURE__ */ jsxs3(Box3, { borderStyle: "double", borderColor: "yellow", paddingX: 1, marginY: 1, flexDirection: "column", children: [
29835
30023
  /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: "SECURITY APPROVAL REQUIRED" }),
29836
- /* @__PURE__ */ jsx3(Text3, { italic: true, dimColor: true, children: reason }),
30024
+ /* @__PURE__ */ jsx3(Text3, { italic: true, dimColor: true, children: safeReason }),
29837
30025
  /* @__PURE__ */ jsx3(Box3, { marginTop: 1, paddingX: 1, borderStyle: "round", borderColor: "gray", children: /* @__PURE__ */ jsxs3(Text3, { color: "white", children: [
29838
30026
  "$ ",
29839
- command
30027
+ safeCommand
29840
30028
  ] }) }),
29841
30029
  hasDiff && /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [
29842
30030
  /* @__PURE__ */ jsxs3(Box3, { justifyContent: "space-between", children: [
29843
30031
  /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "cyan", children: [
29844
30032
  "DIFF REVIEW ",
29845
- targetPath || title ? `(${targetPath || title})` : "",
30033
+ safePath || title ? `(${safePath || sanitizeTerminalText(title, false)})` : "",
29846
30034
  ":"
29847
30035
  ] }),
29848
30036
  /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
@@ -29893,6 +30081,7 @@ var ApprovalDialog = ({
29893
30081
 
29894
30082
  // src/components/AskUserQuestion.tsx
29895
30083
  init_esm_shims();
30084
+ init_terminalScreen();
29896
30085
  import { useState as useState2 } from "react";
29897
30086
  import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
29898
30087
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
@@ -29900,7 +30089,6 @@ var AskUserQuestion = ({ question, options, multiSelect, onResolve }) => {
29900
30089
  const [selectedIndex, setSelectedIndex] = useState2(0);
29901
30090
  const [selectedIndices, setSelectedIndices] = useState2(/* @__PURE__ */ new Set());
29902
30091
  useInput2((input, key) => {
29903
- if (isMouseReport(input)) return;
29904
30092
  if (key.upArrow) {
29905
30093
  setSelectedIndex((prev) => prev > 0 ? prev - 1 : options.length - 1);
29906
30094
  } else if (key.downArrow) {
@@ -29928,7 +30116,7 @@ var AskUserQuestion = ({ question, options, multiSelect, onResolve }) => {
29928
30116
  });
29929
30117
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", padding: 1, children: [
29930
30118
  /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "QUESTION FROM AGENT" }),
29931
- /* @__PURE__ */ jsx4(Text4, { children: question }),
30119
+ /* @__PURE__ */ jsx4(Text4, { children: sanitizeTerminalText(question) }),
29932
30120
  /* @__PURE__ */ jsx4(Box4, { marginTop: 1, flexDirection: "column", children: options.map((opt, i) => {
29933
30121
  const isHighlighted = i === selectedIndex;
29934
30122
  const isSelected = selectedIndices.has(i);
@@ -29939,9 +30127,9 @@ var AskUserQuestion = ({ question, options, multiSelect, onResolve }) => {
29939
30127
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
29940
30128
  /* @__PURE__ */ jsxs4(Text4, { color: isHighlighted ? "blue" : "white", bold: isHighlighted, children: [
29941
30129
  marker,
29942
- opt.text
30130
+ sanitizeTerminalText(opt.text, false)
29943
30131
  ] }),
29944
- isHighlighted && opt.preview && /* @__PURE__ */ jsx4(Box4, { marginLeft: 4, borderStyle: "single", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: opt.preview }) })
30132
+ isHighlighted && opt.preview && /* @__PURE__ */ jsx4(Box4, { marginLeft: 4, borderStyle: "single", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: sanitizeTerminalText(opt.preview) }) })
29945
30133
  ] }, i);
29946
30134
  }) }),
29947
30135
  /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
@@ -29954,37 +30142,12 @@ var AskUserQuestion = ({ question, options, multiSelect, onResolve }) => {
29954
30142
 
29955
30143
  // src/components/VirtualizedMessageList.tsx
29956
30144
  init_esm_shims();
30145
+ init_terminalScreen();
29957
30146
  import React3 from "react";
29958
30147
  import { Box as Box5, Text as Text5 } from "ink";
30148
+ import stringWidth from "string-width";
30149
+ import stripAnsi2 from "strip-ansi";
29959
30150
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
29960
- function estimateLineCount(text, cols) {
29961
- const effectiveCols = cols > 0 ? cols : 80;
29962
- const physicalLines = text.split("\n");
29963
- let total = 0;
29964
- for (const line of physicalLines) {
29965
- total += line.length === 0 ? 1 : Math.ceil(line.length / effectiveCols);
29966
- }
29967
- return total;
29968
- }
29969
- function computeViewport(messages, visibleLines, termColumns, offsetFromEnd = 0) {
29970
- const cols = termColumns > 0 ? termColumns : 80;
29971
- const budget = Math.max(visibleLines, 1);
29972
- let linesUsed = 0;
29973
- const endIdx = Math.max(0, messages.length - Math.max(0, Math.floor(offsetFromEnd)));
29974
- let startIdx = endIdx;
29975
- for (let i = endIdx - 1; i >= 0; i--) {
29976
- const contentLines = estimateLineCount(messages[i].content, cols);
29977
- const msgCost = contentLines + 2;
29978
- if (linesUsed + msgCost > budget) break;
29979
- linesUsed += msgCost;
29980
- startIdx = i;
29981
- }
29982
- return {
29983
- displayMessages: messages.slice(startIdx, endIdx),
29984
- hiddenCount: startIdx,
29985
- hiddenAfter: messages.length - endIdx
29986
- };
29987
- }
29988
30151
  var ROLE_COLOR = {
29989
30152
  user: "green",
29990
30153
  assistant: "cyan",
@@ -29997,6 +30160,96 @@ var ROLE_LABEL = {
29997
30160
  error: "Error: ",
29998
30161
  system: ""
29999
30162
  };
30163
+ function cleanTerminalText(value) {
30164
+ return sanitizeTerminalText(stripAnsi2(value)).replace(/\r\n?/g, "\n").replace(/\t/g, " ");
30165
+ }
30166
+ function graphemes(value) {
30167
+ if (typeof Intl.Segmenter === "function") {
30168
+ const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
30169
+ return Array.from(segmenter.segment(value), (segment) => segment.segment);
30170
+ }
30171
+ return Array.from(value);
30172
+ }
30173
+ function wrapTerminalLine(value, columns) {
30174
+ const width = Math.max(1, Math.floor(columns));
30175
+ if (value.length === 0) return [""];
30176
+ const rows = [];
30177
+ let row3 = "";
30178
+ let rowWidth = 0;
30179
+ for (const grapheme of graphemes(value)) {
30180
+ const cellWidth = Math.max(0, stringWidth(grapheme));
30181
+ if (row3.length > 0 && rowWidth + cellWidth > width) {
30182
+ rows.push(row3);
30183
+ row3 = "";
30184
+ rowWidth = 0;
30185
+ }
30186
+ row3 += grapheme;
30187
+ rowWidth += cellWidth;
30188
+ }
30189
+ rows.push(row3);
30190
+ return rows;
30191
+ }
30192
+ function buildTranscriptRows(messages, termColumns, compact2 = false) {
30193
+ const columns = termColumns > 0 ? Math.floor(termColumns) : 80;
30194
+ const separator = "\u2500".repeat(Math.max(1, Math.min(columns, 60)));
30195
+ const rows = [];
30196
+ messages.forEach((message2, messageIndex) => {
30197
+ if (compact2 && messageIndex > 0) {
30198
+ rows.push({ messageIndex, role: message2.role, prefix: "", content: separator, separator: true });
30199
+ }
30200
+ const label = ROLE_LABEL[message2.role];
30201
+ const labelWidth = stringWidth(label);
30202
+ const contentWidth = Math.max(1, columns - labelWidth);
30203
+ const continuationPrefix = " ".repeat(labelWidth);
30204
+ let firstVisualRow = true;
30205
+ for (const physicalLine of cleanTerminalText(message2.content).split("\n")) {
30206
+ const wrapped = wrapTerminalLine(physicalLine, contentWidth);
30207
+ for (const content of wrapped) {
30208
+ rows.push({
30209
+ messageIndex,
30210
+ role: message2.role,
30211
+ prefix: firstVisualRow ? label : continuationPrefix,
30212
+ content
30213
+ });
30214
+ firstVisualRow = false;
30215
+ }
30216
+ }
30217
+ if (!compact2 && message2.role === "assistant") {
30218
+ rows.push({ messageIndex, role: message2.role, prefix: "", content: "", margin: true });
30219
+ }
30220
+ });
30221
+ return rows;
30222
+ }
30223
+ function uniqueMessageCount(rows) {
30224
+ return new Set(rows.map((row3) => row3.messageIndex)).size;
30225
+ }
30226
+ function computeViewport(messages, visibleLines, termColumns, offsetFromEnd = 0, compact2 = false) {
30227
+ const allRows = buildTranscriptRows(messages, termColumns, compact2);
30228
+ const totalRows = allRows.length;
30229
+ const budget = Math.max(1, Math.floor(visibleLines));
30230
+ const hiddenRowsAfter = Math.min(totalRows, Math.max(0, Math.floor(offsetFromEnd)));
30231
+ const end = totalRows - hiddenRowsAfter;
30232
+ let contentBudget = budget;
30233
+ let start = Math.max(0, end - contentBudget);
30234
+ if (start > 0 || hiddenRowsAfter > 0) {
30235
+ contentBudget = Math.max(0, budget - 1);
30236
+ start = Math.max(0, end - contentBudget);
30237
+ }
30238
+ const displayRows = allRows.slice(start, end);
30239
+ const visibleIndices = [...new Set(displayRows.map((row3) => row3.messageIndex))];
30240
+ const displayMessages = visibleIndices.map((index) => messages[index]);
30241
+ const beforeRows = allRows.slice(0, start);
30242
+ const afterRows = allRows.slice(end);
30243
+ return {
30244
+ displayMessages,
30245
+ displayRows,
30246
+ hiddenCount: uniqueMessageCount(beforeRows),
30247
+ hiddenAfter: uniqueMessageCount(afterRows),
30248
+ hiddenRowsBefore: start,
30249
+ hiddenRowsAfter,
30250
+ totalRows
30251
+ };
30252
+ }
30000
30253
  var VirtualizedMessageList = ({
30001
30254
  messages,
30002
30255
  visibleLines,
@@ -30004,45 +30257,27 @@ var VirtualizedMessageList = ({
30004
30257
  compact: compact2 = false,
30005
30258
  offsetFromEnd = 0
30006
30259
  }) => {
30007
- const { displayMessages, hiddenCount, hiddenAfter } = React3.useMemo(
30008
- () => computeViewport(messages, visibleLines, termColumns, offsetFromEnd),
30009
- [messages, visibleLines, termColumns, offsetFromEnd]
30260
+ const { displayRows, hiddenRowsBefore, hiddenRowsAfter } = React3.useMemo(
30261
+ () => computeViewport(messages, visibleLines, termColumns, offsetFromEnd, compact2),
30262
+ [messages, visibleLines, termColumns, offsetFromEnd, compact2]
30010
30263
  );
30011
- const separatorWidth = termColumns > 0 ? Math.min(termColumns, 60) : 60;
30012
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", flexGrow: 1, children: [
30013
- (hiddenCount > 0 || hiddenAfter > 0) && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `[\u2191 ${hiddenCount} older \xB7 \u2193 ${hiddenAfter} newer \xB7 wheel/\u2191/\u2193 or PageUp/PageDown \xB7 Ctrl+E latest]` }),
30014
- displayMessages.map((msg, i) => /* @__PURE__ */ jsxs5(
30015
- Box5,
30016
- {
30017
- flexDirection: "column",
30018
- marginBottom: compact2 ? 0 : msg.role === "assistant" ? 1 : 0,
30019
- children: [
30020
- compact2 && i > 0 && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2500".repeat(separatorWidth) }),
30021
- /* @__PURE__ */ jsxs5(Box5, { children: [
30022
- /* @__PURE__ */ jsx5(
30023
- Text5,
30024
- {
30025
- color: ROLE_COLOR[msg.role],
30026
- bold: msg.role !== "assistant",
30027
- children: ROLE_LABEL[msg.role]
30028
- }
30029
- ),
30030
- /* @__PURE__ */ jsx5(Text5, { wrap: "wrap", children: msg.content })
30031
- ] })
30032
- ]
30033
- },
30034
- i
30035
- ))
30264
+ return /* @__PURE__ */ jsxs5(Box5, { width: "100%", flexDirection: "column", flexGrow: 1, children: [
30265
+ (hiddenRowsBefore > 0 || hiddenRowsAfter > 0) && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `[\u2191 ${hiddenRowsBefore} rows \xB7 \u2193 ${hiddenRowsAfter} \xB7 PgUp/PgDn \xB7 Ctrl+E latest]` }),
30266
+ displayRows.map((row3, index) => row3.separator ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: row3.content }, `${row3.messageIndex}-${index}`) : row3.margin ? /* @__PURE__ */ jsx5(Text5, { children: " " }, `${row3.messageIndex}-${index}`) : /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate-end", children: [
30267
+ /* @__PURE__ */ jsx5(Text5, { color: ROLE_COLOR[row3.role], bold: row3.role !== "assistant", children: row3.prefix }),
30268
+ row3.content
30269
+ ] }, `${row3.messageIndex}-${index}`))
30036
30270
  ] });
30037
30271
  };
30038
30272
 
30039
30273
  // src/App.tsx
30274
+ init_terminalScreen();
30040
30275
  init_src();
30041
30276
  init_src3();
30042
30277
 
30043
30278
  // src/ui/TextInput.tsx
30044
30279
  init_esm_shims();
30045
- import { useState as useState3, useEffect as useEffect2 } from "react";
30280
+ import React4, { useState as useState3, useEffect } from "react";
30046
30281
  import { Box as Box6, Text as Text6, useInput as useInput3 } from "ink";
30047
30282
 
30048
30283
  // src/state/commandHandler.ts
@@ -30629,7 +30864,42 @@ ${finalCmd}`;
30629
30864
  }
30630
30865
 
30631
30866
  // src/ui/TextInput.tsx
30867
+ init_terminalScreen();
30632
30868
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
30869
+ function sanitizePromptInput(value) {
30870
+ return sanitizeTerminalText(value);
30871
+ }
30872
+ function cursorBoundaries(value) {
30873
+ if (typeof Intl.Segmenter !== "function") {
30874
+ return Array.from({ length: value.length + 1 }, (_, index) => index);
30875
+ }
30876
+ const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
30877
+ return [0, ...Array.from(segmenter.segment(value), (segment) => segment.index + segment.segment.length)];
30878
+ }
30879
+ function moveCursorLeft(value, cursor) {
30880
+ const boundaries = cursorBoundaries(value);
30881
+ return boundaries[Math.max(0, boundaries.findIndex((boundary) => boundary >= cursor) - 1)] ?? 0;
30882
+ }
30883
+ function moveCursorRight(value, cursor) {
30884
+ const boundaries = cursorBoundaries(value);
30885
+ return boundaries.find((boundary) => boundary > cursor) ?? value.length;
30886
+ }
30887
+ function moveCursorWordLeft(value, cursor) {
30888
+ const boundaries = cursorBoundaries(value);
30889
+ let index = boundaries.findIndex((boundary) => boundary >= cursor);
30890
+ if (index < 0) index = boundaries.length - 1;
30891
+ while (index > 0 && /\s/.test(value.slice(boundaries[index - 1], boundaries[index]))) index--;
30892
+ while (index > 0 && !/\s/.test(value.slice(boundaries[index - 1], boundaries[index]))) index--;
30893
+ return boundaries[index] ?? 0;
30894
+ }
30895
+ function moveCursorWordRight(value, cursor) {
30896
+ const boundaries = cursorBoundaries(value);
30897
+ let index = boundaries.findIndex((boundary) => boundary > cursor);
30898
+ if (index < 0) return value.length;
30899
+ while (index < boundaries.length - 1 && !/\s/.test(value.slice(boundaries[index], boundaries[index + 1]))) index++;
30900
+ while (index < boundaries.length - 1 && /\s/.test(value.slice(boundaries[index], boundaries[index + 1]))) index++;
30901
+ return boundaries[index] ?? value.length;
30902
+ }
30633
30903
  var TextInput = ({
30634
30904
  value,
30635
30905
  onChange,
@@ -30641,12 +30911,24 @@ var TextInput = ({
30641
30911
  transcriptNavigationActive = false
30642
30912
  }) => {
30643
30913
  const [history, setHistory] = useState3([]);
30914
+ const terminalInputDecoder = React4.useRef(new TerminalInputDecoder());
30644
30915
  const [historyIndex, setHistoryIndex] = useState3(-1);
30645
- useEffect2(() => {
30916
+ const [cursor, setCursor] = useState3(value.length);
30917
+ useEffect(() => {
30646
30918
  storage.loadHistory().then((entries) => setHistory(filterSafeHistory(entries)));
30647
30919
  }, [storage]);
30920
+ useEffect(() => {
30921
+ setCursor((current) => Math.min(current, value.length));
30922
+ }, [value]);
30648
30923
  useInput3((input, key) => {
30649
- if (isMouseReport(input)) return;
30924
+ const decoded = terminalInputDecoder.current.feed(input);
30925
+ if (decoded.protocol && key.escape) {
30926
+ terminalInputDecoder.current.reset();
30927
+ } else if (decoded.protocol) {
30928
+ if (!decoded.text) return;
30929
+ input = decoded.text;
30930
+ }
30931
+ const extendedKey = key;
30650
30932
  if (key.ctrl && input === "c" || key.escape) {
30651
30933
  onCancel?.();
30652
30934
  return;
@@ -30654,39 +30936,70 @@ var TextInput = ({
30654
30936
  if (disabled) return;
30655
30937
  if (key.return) {
30656
30938
  if (key.shift) {
30657
- onChange(value + "\n");
30939
+ onChange(value.slice(0, cursor) + "\n" + value.slice(cursor));
30940
+ setCursor(cursor + 1);
30658
30941
  } else {
30659
30942
  onSubmit(value);
30660
30943
  onChange("");
30944
+ setCursor(0);
30661
30945
  setHistoryIndex(-1);
30662
30946
  }
30663
- } else if (key.backspace || key.delete) {
30664
- onChange(value.slice(0, -1));
30947
+ } else if (key.backspace) {
30948
+ if (cursor > 0) {
30949
+ onChange(value.slice(0, cursor - 1) + value.slice(cursor));
30950
+ setCursor(cursor - 1);
30951
+ }
30952
+ } else if (key.delete) {
30953
+ if (cursor < value.length) onChange(value.slice(0, cursor) + value.slice(cursor + 1));
30954
+ } else if (extendedKey.home || key.ctrl && input === "a") {
30955
+ setCursor(0);
30956
+ } else if (extendedKey.end) {
30957
+ setCursor(value.length);
30958
+ } else if (key.leftArrow && key.ctrl) {
30959
+ setCursor((current) => moveCursorWordLeft(value, current));
30960
+ } else if (key.rightArrow && key.ctrl) {
30961
+ setCursor((current) => moveCursorWordRight(value, current));
30962
+ } else if (key.leftArrow) {
30963
+ setCursor((current) => moveCursorLeft(value, current));
30964
+ } else if (key.rightArrow) {
30965
+ setCursor((current) => moveCursorRight(value, current));
30665
30966
  } else if (key.ctrl && input === "p" || key.upArrow && !transcriptNavigationActive) {
30666
30967
  const nextIndex = historyIndex + 1;
30667
30968
  if (nextIndex < history.length) {
30668
30969
  setHistoryIndex(nextIndex);
30669
- onChange(history[history.length - 1 - nextIndex]);
30970
+ const entry = history[history.length - 1 - nextIndex];
30971
+ const safeEntry = sanitizePromptInput(entry);
30972
+ onChange(safeEntry);
30973
+ setCursor(safeEntry.length);
30670
30974
  }
30671
30975
  } else if (key.ctrl && input === "n" || key.downArrow && !transcriptNavigationActive) {
30672
30976
  const nextIndex = historyIndex - 1;
30673
30977
  if (nextIndex >= 0) {
30674
30978
  setHistoryIndex(nextIndex);
30675
- onChange(history[history.length - 1 - nextIndex]);
30979
+ const entry = history[history.length - 1 - nextIndex];
30980
+ const safeEntry = sanitizePromptInput(entry);
30981
+ onChange(safeEntry);
30982
+ setCursor(safeEntry.length);
30676
30983
  } else {
30677
30984
  setHistoryIndex(-1);
30678
30985
  onChange("");
30986
+ setCursor(0);
30679
30987
  }
30680
30988
  } else if ((key.upArrow || key.downArrow) && transcriptNavigationActive) {
30681
30989
  return;
30682
30990
  } else if (input && !key.ctrl && !key.meta) {
30683
- onChange(value + input);
30991
+ const safeInput = sanitizePromptInput(input);
30992
+ if (safeInput.length > 0) {
30993
+ onChange(value.slice(0, cursor) + safeInput + value.slice(cursor));
30994
+ setCursor(cursor + safeInput.length);
30995
+ }
30684
30996
  }
30685
30997
  });
30686
- return /* @__PURE__ */ jsxs6(Box6, { children: [
30998
+ return /* @__PURE__ */ jsxs6(Box6, { width: "100%", children: [
30687
30999
  /* @__PURE__ */ jsx6(Text6, { bold: true, color: promptColor, children: "\u276F " }),
30688
- /* @__PURE__ */ jsx6(Text6, { children: value }),
30689
- !disabled && /* @__PURE__ */ jsx6(Text6, { backgroundColor: "white", color: "black", children: " " })
31000
+ /* @__PURE__ */ jsx6(Text6, { children: value.slice(0, cursor) }),
31001
+ !disabled && /* @__PURE__ */ jsx6(Text6, { backgroundColor: "white", color: "black", children: value[cursor] ?? " " }),
31002
+ /* @__PURE__ */ jsx6(Text6, { children: disabled ? value.slice(cursor) : value.slice(cursor + 1) })
30690
31003
  ] });
30691
31004
  };
30692
31005
  var PROMPT_COLOR_BY_MODE = {
@@ -30869,26 +31182,38 @@ init_errorPresentation();
30869
31182
 
30870
31183
  // src/hooks/useTerminalResize.ts
30871
31184
  init_esm_shims();
30872
- import { useState as useState4, useEffect as useEffect3, useCallback } from "react";
30873
- function getCurrentDimensions() {
31185
+ import { useState as useState4, useEffect as useEffect2, useCallback, useRef } from "react";
31186
+ function normalizeTerminalDimensions(columns, rows) {
30874
31187
  return {
30875
- columns: process.stdout.columns ?? 80,
30876
- rows: process.stdout.rows ?? 24
31188
+ columns: Number.isFinite(columns) && columns > 0 ? Math.floor(columns) : 80,
31189
+ rows: Number.isFinite(rows) && rows > 0 ? Math.floor(rows) : 24
30877
31190
  };
30878
31191
  }
31192
+ function getCurrentDimensions() {
31193
+ return normalizeTerminalDimensions(process.stdout.columns ?? 80, process.stdout.rows ?? 24);
31194
+ }
30879
31195
  function useTerminalResize() {
30880
31196
  const [dimensions, setDimensions] = useState4(
30881
31197
  getCurrentDimensions
30882
31198
  );
31199
+ const resizeTimer = useRef(null);
31200
+ const resizeEpoch = useRef(0);
30883
31201
  const handleResize = useCallback(() => {
30884
- setDimensions(getCurrentDimensions());
31202
+ const epoch = ++resizeEpoch.current;
31203
+ if (resizeTimer.current !== null) clearTimeout(resizeTimer.current);
31204
+ resizeTimer.current = setTimeout(() => {
31205
+ resizeTimer.current = null;
31206
+ if (epoch === resizeEpoch.current) setDimensions(getCurrentDimensions());
31207
+ }, 16);
30885
31208
  }, []);
30886
- useEffect3(() => {
31209
+ useEffect2(() => {
30887
31210
  process.stdout.on("resize", handleResize);
30888
31211
  const onSigwinch = () => handleResize();
30889
31212
  process.on("SIGWINCH", onSigwinch);
30890
31213
  handleResize();
30891
31214
  return () => {
31215
+ if (resizeTimer.current !== null) clearTimeout(resizeTimer.current);
31216
+ resizeTimer.current = null;
30892
31217
  process.stdout.off("resize", handleResize);
30893
31218
  process.off("SIGWINCH", onSigwinch);
30894
31219
  };
@@ -30961,6 +31286,7 @@ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
30961
31286
  var App = ({ compact: compact2 = false, continueSession: continueSession2 = false, executionMode: executionMode2 = "remote" }) => {
30962
31287
  const [user, setUser] = useState5(null);
30963
31288
  const [input, setInput] = useState5("");
31289
+ const terminalInputDecoder = useRef2(new TerminalInputDecoder());
30964
31290
  const [history, setHistory] = useState5([]);
30965
31291
  const [historyView, setHistoryView] = useState5(null);
30966
31292
  const [historyOffset, setHistoryOffset] = useState5(0);
@@ -31034,15 +31360,15 @@ var App = ({ compact: compact2 = false, continueSession: continueSession2 = fals
31034
31360
  const trustStore = useRef2(new TrustStore()).current;
31035
31361
  const lastActivityRef = useRef2(Date.now());
31036
31362
  const pollingIntervalRef = useRef2(null);
31037
- useEffect4(() => {
31363
+ useEffect3(() => {
31038
31364
  agent.setApprovalCallback(requestApproval);
31039
31365
  }, [agent, requestApproval]);
31040
- useEffect4(() => {
31366
+ useEffect3(() => {
31041
31367
  if (bypassExpiry === null) return;
31042
31368
  const timer = setInterval(() => setPermissionNow(Date.now()), 1e3);
31043
31369
  return () => clearInterval(timer);
31044
31370
  }, [bypassExpiry]);
31045
- useEffect4(() => {
31371
+ useEffect3(() => {
31046
31372
  agent.setOnModeChange((m) => {
31047
31373
  setModeState(m);
31048
31374
  addMessage("system", `Mode changed to: ${m} (via plan-mode tool)`);
@@ -31071,7 +31397,7 @@ var App = ({ compact: compact2 = false, continueSession: continueSession2 = fals
31071
31397
  setModeState(m);
31072
31398
  agent.setMode(m, source);
31073
31399
  }, [agent]);
31074
- useEffect4(() => {
31400
+ useEffect3(() => {
31075
31401
  if (mode !== "bypassPermissions" || !isBypassExpired(bypassExpiry, permissionNow)) return;
31076
31402
  setMode("default");
31077
31403
  setBypassExpiry(null);
@@ -31173,7 +31499,7 @@ ${renderCliError(classifyCliError(error, { executionMode: executionMode2 }))}`);
31173
31499
  client.setToken("");
31174
31500
  setStatus("Session expired - run /login");
31175
31501
  }, [client, storage]);
31176
- useEffect4(() => {
31502
+ useEffect3(() => {
31177
31503
  agent.setAuthFailureHandler(handle401);
31178
31504
  return () => agent.setAuthFailureHandler(null);
31179
31505
  }, [agent, handle401]);
@@ -31194,7 +31520,7 @@ ${renderCliError(classifyCliError(error, { executionMode: executionMode2 }))}`);
31194
31520
  setStatus(classifyCliError(error, { executionMode: executionMode2 }).summary);
31195
31521
  }
31196
31522
  }, [client, activeChatId, setProjectId, handle401]);
31197
- useEffect4(() => {
31523
+ useEffect3(() => {
31198
31524
  (async () => {
31199
31525
  try {
31200
31526
  if (executionMode2 === "local") {
@@ -31306,14 +31632,14 @@ ${renderCliError(classifyCliError(error, { executionMode: executionMode2 }))}`);
31306
31632
  }
31307
31633
  })();
31308
31634
  }, []);
31309
- useEffect4(() => {
31635
+ useEffect3(() => {
31310
31636
  return () => {
31311
31637
  void agent.flushPendingToolTelemetry();
31312
31638
  void agent.flushPendingUsageTelemetry();
31313
31639
  agent.fireLifecycleHook("session-end", { cwd: process.cwd() });
31314
31640
  };
31315
31641
  }, []);
31316
- useEffect4(() => {
31642
+ useEffect3(() => {
31317
31643
  if (!user) return;
31318
31644
  createIdleAwarePoll(pollingIntervalRef, lastActivityRef, client, setUser, setUsageError);
31319
31645
  return () => {
@@ -31414,7 +31740,7 @@ ${renderCliError(classifyCliError(error, { executionMode: executionMode2 }))}`);
31414
31740
  });
31415
31741
  const relayCommandRef = useRef2(handleCommand);
31416
31742
  relayCommandRef.current = handleCommand;
31417
- useEffect4(() => {
31743
+ useEffect3(() => {
31418
31744
  if (executionMode2 !== "remote" || process.env.MSAPLING_RELAY_ENABLED !== "1" || !activeProjectId) return;
31419
31745
  let listener = null;
31420
31746
  let cancelled = false;
@@ -31451,24 +31777,45 @@ ${renderCliError(classifyCliError(error, { executionMode: executionMode2 }))}`);
31451
31777
  const activityRows = pendingApproval ? 6 : pendingAskUser ? 10 : isRunning ? 2 : 0;
31452
31778
  const visibleLines = Math.max(termHeight - (terminalLayout.fixedRows + footerRows + modeRows + activityRows), 1);
31453
31779
  const displayedHistory = historyView ?? history;
31454
- const historyPageStep = Math.max(1, Math.floor(visibleLines / 3));
31455
- const viewport = computeViewport(displayedHistory, visibleLines, termColumns, historyOffset);
31456
- const canScrollOlder = viewport.hiddenCount > 0;
31780
+ const historyPageStep = Math.max(1, visibleLines - 1);
31781
+ const viewport = computeViewport(displayedHistory, visibleLines, termColumns, historyOffset, compactPresentation);
31782
+ const safeStatus = sanitizeTerminalText(status2, false);
31783
+ const safeProjectId = sanitizeTerminalText(activeProjectId || "none", false);
31784
+ const safeModel = sanitizeTerminalText(activeModel, false);
31785
+ const safeEnvironment = sanitizeTerminalText(remoteEnvironment.label, false);
31786
+ const canScrollOlder = viewport.hiddenRowsBefore > 0;
31457
31787
  const canScrollNewer = historyOffset > 0;
31788
+ const previousTranscriptRowsRef = useRef2(viewport.totalRows);
31789
+ useEffect3(() => {
31790
+ const previousRows = previousTranscriptRowsRef.current;
31791
+ previousTranscriptRowsRef.current = viewport.totalRows;
31792
+ const rowDelta = viewport.totalRows - previousRows;
31793
+ if (historyOffset > 0 && rowDelta !== 0) {
31794
+ setHistoryOffset((current) => Math.min(
31795
+ Math.max(0, viewport.totalRows - 1),
31796
+ Math.max(0, current + rowDelta)
31797
+ ));
31798
+ }
31799
+ }, [historyOffset, viewport.totalRows]);
31458
31800
  const scrollTranscript = useCallback2((direction, page = false) => {
31459
31801
  const amount = page ? historyPageStep : 1;
31460
31802
  if (direction === "up") {
31461
- setHistoryOffset((current) => Math.min(Math.max(0, displayedHistory.length - 1), current + amount));
31803
+ setHistoryOffset((current) => Math.min(Math.max(0, viewport.totalRows - 1), current + amount));
31462
31804
  } else {
31463
31805
  setHistoryOffset((current) => Math.max(0, current - amount));
31464
31806
  }
31465
- }, [displayedHistory.length, historyPageStep]);
31466
- useTerminalMouseScroll((direction) => {
31467
- if (!pendingApproval && !pendingAskUser) scrollTranscript(direction);
31468
- });
31807
+ }, [historyPageStep, viewport.totalRows]);
31469
31808
  useInput4((input2, key) => {
31470
31809
  if (pendingApproval || pendingAskUser) return;
31471
- if (key.pageUp) {
31810
+ const decoded = terminalInputDecoder.current.feed(input2);
31811
+ if (decoded.protocol && !decoded.mouseWheel) return;
31812
+ if (isSgrMouseReport(input2) && !parseMouseWheel(input2)) return;
31813
+ const mouseWheel = decoded.mouseWheel ?? parseMouseWheel(input2);
31814
+ if (mouseWheel === "up") {
31815
+ scrollTranscript("up");
31816
+ } else if (mouseWheel === "down") {
31817
+ scrollTranscript("down");
31818
+ } else if (key.pageUp) {
31472
31819
  scrollTranscript("up", true);
31473
31820
  } else if (key.pageDown) {
31474
31821
  scrollTranscript("down", true);
@@ -31481,9 +31828,9 @@ ${renderCliError(classifyCliError(error, { executionMode: executionMode2 }))}`);
31481
31828
  setHistoryView(null);
31482
31829
  }
31483
31830
  });
31484
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", padding: terminalLayout.outerPadding, children: [
31831
+ return /* @__PURE__ */ jsxs7(Box7, { width: termColumns, height: termHeight, flexDirection: "column", padding: terminalLayout.outerPadding, overflow: "hidden", children: [
31485
31832
  /* @__PURE__ */ jsx7(Header, { compact: !terminalLayout.showFramedHeader }),
31486
- /* @__PURE__ */ jsx7(Box7, { marginBottom: compactPresentation ? 0 : 1, children: /* @__PURE__ */ jsx7(
31833
+ /* @__PURE__ */ jsx7(Box7, { width: "100%", flexGrow: 1, overflow: "hidden", children: /* @__PURE__ */ jsx7(
31487
31834
  VirtualizedMessageList,
31488
31835
  {
31489
31836
  messages: displayedHistory,
@@ -31558,7 +31905,7 @@ ${renderCliError(classifyCliError(error, { executionMode: executionMode2 }))}`);
31558
31905
  /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
31559
31906
  /* @__PURE__ */ jsxs7(Text7, { color: executionMode2 === "local" ? "yellow" : "cyan", children: [
31560
31907
  "Execution: ",
31561
- executionMode2 === "local" ? "STANDALONE LOCAL" : remoteEnvironment.label
31908
+ executionMode2 === "local" ? "STANDALONE LOCAL" : safeEnvironment
31562
31909
  ] }),
31563
31910
  /* @__PURE__ */ jsxs7(Text7, { color: billingProfile === "account-metered" ? "cyan" : "yellow", children: [
31564
31911
  "Billing: ",
@@ -31571,15 +31918,15 @@ ${renderCliError(classifyCliError(error, { executionMode: executionMode2 }))}`);
31571
31918
  ] }),
31572
31919
  /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
31573
31920
  "Status: ",
31574
- status2
31921
+ safeStatus
31575
31922
  ] }),
31576
31923
  /* @__PURE__ */ jsxs7(Text7, { color: "gray", children: [
31577
31924
  "Project: ",
31578
- activeProjectId || "none"
31925
+ safeProjectId
31579
31926
  ] }),
31580
31927
  /* @__PURE__ */ jsxs7(Text7, { color: "gray", children: [
31581
31928
  "Model: ",
31582
- activeModel
31929
+ safeModel
31583
31930
  ] })
31584
31931
  ] }),
31585
31932
  /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", alignItems: "flex-end", children: [
@@ -31594,9 +31941,9 @@ ${renderCliError(classifyCliError(error, { executionMode: executionMode2 }))}`);
31594
31941
  contextBudgetSnap !== null && /* @__PURE__ */ jsx7(Text7, { color: contextBudgetColor(contextBudgetSnap.usedPct), children: formatContextBudgetLabel(contextBudgetSnap) })
31595
31942
  ] })
31596
31943
  ] }),
31597
- terminalLayout.density === "condensed" && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
31944
+ terminalLayout.density === "condensed" && /* @__PURE__ */ jsxs7(Box7, { width: "100%", flexDirection: "column", children: [
31598
31945
  /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate-end", color: executionMode2 === "local" ? "yellow" : "cyan", children: [
31599
- executionMode2 === "local" ? "LOCAL" : remoteEnvironment.label.replace("CONNECTED ", ""),
31946
+ executionMode2 === "local" ? "LOCAL" : safeEnvironment.replace("CONNECTED ", ""),
31600
31947
  " \xB7 ",
31601
31948
  mode,
31602
31949
  " \xB7 ",
@@ -31605,17 +31952,17 @@ ${renderCliError(classifyCliError(error, { executionMode: executionMode2 }))}`);
31605
31952
  continuityProfile
31606
31953
  ] }),
31607
31954
  /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate-end", dimColor: true, children: [
31608
- activeModel,
31955
+ safeModel,
31609
31956
  contextBudgetSnap ? ` \xB7 ${formatContextBudgetLabel(contextBudgetSnap)}` : "",
31610
31957
  lastCost > 0 ? ` \xB7 last $${lastCost.toFixed(4)}` : ""
31611
31958
  ] })
31612
31959
  ] }),
31613
31960
  terminalLayout.density === "minimal" && /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate-end", dimColor: true, children: [
31614
- executionMode2 === "local" ? "LOCAL" : remoteEnvironment.label.replace("CONNECTED ", ""),
31961
+ executionMode2 === "local" ? "LOCAL" : safeEnvironment.replace("CONNECTED ", ""),
31615
31962
  " \xB7 ",
31616
31963
  mode,
31617
31964
  " \xB7 ",
31618
- activeModel,
31965
+ safeModel,
31619
31966
  lastCost > 0 ? ` \xB7 $${lastCost.toFixed(4)}` : "",
31620
31967
  contextBudgetSnap ? ` \xB7 ctx ${contextBudgetSnap.usedPct}%` : ""
31621
31968
  ] }),
@@ -31834,26 +32181,7 @@ function handleCliArgs(args2) {
31834
32181
  // src/index.tsx
31835
32182
  init_src3();
31836
32183
  init_errorPresentation();
31837
-
31838
- // src/runtime/terminalScreen.ts
31839
- init_esm_shims();
31840
- var ENTER_ALTERNATE_SCREEN = "\x1B[?1049h\x1B[H";
31841
- var LEAVE_ALTERNATE_SCREEN = "\x1B[?1049l\x1B[?25h";
31842
- var alternateScreenActive = false;
31843
- function enterAlternateScreen(output = process.stdout, env = process.env) {
31844
- if (alternateScreenActive || !output.isTTY || env.MSAPLING_NO_ALT_SCREEN === "1" || env.TERM === "dumb") return false;
31845
- output.write(ENTER_ALTERNATE_SCREEN);
31846
- alternateScreenActive = true;
31847
- return true;
31848
- }
31849
- function leaveAlternateScreen(output = process.stdout) {
31850
- if (!alternateScreenActive) return false;
31851
- output.write(LEAVE_ALTERNATE_SCREEN);
31852
- alternateScreenActive = false;
31853
- return true;
31854
- }
31855
-
31856
- // src/index.tsx
32184
+ init_terminalScreen();
31857
32185
  import { jsx as jsx8 } from "react/jsx-runtime";
31858
32186
  var index_default = App;
31859
32187
  function restoreTerminalMode() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mtreeai/msapling-cli",
3
- "version": "2.3.6-beta.60",
3
+ "version": "2.3.6-beta.62",
4
4
  "description": "MSapling CLI by MTreeAI — React/Ink terminal client for the MSapling backend: chat, projects, MDrive, agent tools, MCP server.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "MSapling Team",
@@ -55,6 +55,8 @@
55
55
  "proper-lockfile": "^4.1.2",
56
56
  "react": "^18.3.1",
57
57
  "shell-quote": "^1.8.1",
58
+ "string-width": "^5.1.2",
59
+ "strip-ansi": "^7.1.0",
58
60
  "yaml": "^2.8.3"
59
61
  },
60
62
  "optionalDependencies": {