codeam-cli 1.3.8 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +93 -25
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -114,7 +114,7 @@ var import_picocolors = __toESM(require("picocolors"));
114
114
  // package.json
115
115
  var package_default = {
116
116
  name: "codeam-cli",
117
- version: "1.3.8",
117
+ version: "1.4.0",
118
118
  description: "Remote control Claude Code from your mobile device",
119
119
  main: "dist/index.js",
120
120
  bin: {
@@ -802,32 +802,41 @@ var ClaudeService = class {
802
802
  this.strategy.write(text + "\r");
803
803
  }
804
804
  /**
805
- * Navigate a React Ink selector to the given 0-based index and confirm.
805
+ * Navigate a React Ink selector to the given 0-based target index and confirm.
806
+ *
807
+ * `fromIndex` is the current highlighted position (defaults to 0 for
808
+ * numbered selectors which always start at the first option). For list-style
809
+ * selectors (e.g. /mcp), the CLI sends `currentIndex` in the select_prompt
810
+ * chunk so the client can pass it back here as `fromIndex`, enabling both
811
+ * up-arrow and down-arrow navigation without always rewinding to position 0.
806
812
  *
807
813
  * Why not sendCommand(arrows + Enter) in one write()?
808
814
  * All bytes arrive as one chunk → readline fires all keypress events in the
809
815
  * same synchronous run → React Ink batches the state updates → each arrow
810
816
  * sees selectedIndex=0 → final state is still 0 or 1 → wrong option selected.
811
817
  *
812
- * Fix: send each down-arrow in a separate write(), ARROW_MS apart, so React
813
- * has time to process and re-render between each keystroke. Enter is sent
818
+ * Fix: send each arrow in a separate write(), ARROW_MS apart, so React has
819
+ * time to process and re-render between each keystroke. Enter is sent
814
820
  * ENTER_MS after the last arrow.
815
821
  */
816
- selectOption(index) {
817
- if (index <= 0) {
822
+ selectOption(targetIndex, fromIndex = 0) {
823
+ const delta = targetIndex - fromIndex;
824
+ const steps = Math.abs(delta);
825
+ const arrow = delta >= 0 ? "\x1B[B" : "\x1B[A";
826
+ const ARROW_MS = 80;
827
+ const ENTER_MS = 200;
828
+ if (steps === 0) {
818
829
  this.strategy.write("\r");
819
830
  return;
820
831
  }
821
- const ARROW_MS = 80;
822
- const ENTER_MS = 200;
823
- for (let i = 0; i < index; i++) {
832
+ for (let i = 0; i < steps; i++) {
824
833
  setTimeout(() => {
825
- this.strategy.write("\x1B[B");
834
+ this.strategy.write(arrow);
826
835
  }, i * ARROW_MS);
827
836
  }
828
837
  setTimeout(() => {
829
838
  this.strategy.write("\r");
830
- }, index * ARROW_MS + ENTER_MS);
839
+ }, steps * ARROW_MS + ENTER_MS);
831
840
  }
832
841
  /** Send Escape key to Claude (cancels interactive prompts). */
833
842
  sendEscape() {
@@ -953,10 +962,14 @@ function renderToLines(raw) {
953
962
  }
954
963
  function detectSelector(lines) {
955
964
  if (lines.some((l) => /\?\s+for\s+shortcuts/i.test(l.trim()))) return null;
956
- if (!lines.some((l) => /^❯\s*\d+\./.test(l.trim()))) return null;
965
+ const clean = lines.map(
966
+ (l) => l.replace(/^[│╭╰╮╯┌└┐┘├┤┬┴┼]\s?/, "").replace(/\s*[│╭╰╮╯┌└┐┘├┤┬┴┼─━═]+\s*$/, "")
967
+ // strip trailing fill + border
968
+ );
969
+ if (!clean.some((l) => /^❯\s*\d+\./.test(l.trim()))) return null;
957
970
  let optionStartIdx = -1;
958
- for (let i = 0; i < lines.length; i++) {
959
- if (/^(?:❯\s*)?\d+\.\s/.test(lines[i].trim())) {
971
+ for (let i = 0; i < clean.length; i++) {
972
+ if (/^(?:❯\s*)?\d+\.\s/.test(clean[i].trim())) {
960
973
  optionStartIdx = i;
961
974
  break;
962
975
  }
@@ -964,7 +977,7 @@ function detectSelector(lines) {
964
977
  if (optionStartIdx === -1) return null;
965
978
  const questionParts = [];
966
979
  for (let i = 0; i < optionStartIdx; i++) {
967
- const t = lines[i].trim();
980
+ const t = clean[i].trim();
968
981
  if (!t) continue;
969
982
  if (/^[─━—═\-]{3,}$/.test(t)) continue;
970
983
  if (/^\[.*\]$/.test(t)) continue;
@@ -975,8 +988,8 @@ function detectSelector(lines) {
975
988
  const optionLabels = /* @__PURE__ */ new Map();
976
989
  const optionDescs = /* @__PURE__ */ new Map();
977
990
  let currentNum = -1;
978
- for (let i = optionStartIdx; i < lines.length; i++) {
979
- const t = lines[i].trim();
991
+ for (let i = optionStartIdx; i < clean.length; i++) {
992
+ const t = clean[i].trim();
980
993
  if (!t) continue;
981
994
  const m = t.match(/^(?:❯\s*)?(\d+)\.\s+(.+)/);
982
995
  if (m) {
@@ -986,7 +999,7 @@ function detectSelector(lines) {
986
999
  optionDescs.set(num, []);
987
1000
  }
988
1001
  currentNum = num;
989
- } else if (currentNum !== -1 && !/^Enter to/i.test(t) && !/^[─━—═\-]{3,}$/.test(t) && !/↑.*↓.*navigate/i.test(t) && !/Esc to cancel/i.test(t)) {
1002
+ } else if (currentNum !== -1 && !/^Enter to/i.test(t) && !/^[─━—═\-]{3,}$/.test(t) && !/↑.*↓.*navigate/i.test(t) && !/Esc to/i.test(t)) {
990
1003
  optionDescs.get(currentNum)?.push(t);
991
1004
  }
992
1005
  }
@@ -995,7 +1008,60 @@ function detectSelector(lines) {
995
1008
  return {
996
1009
  question,
997
1010
  options: keys.map((k) => optionLabels.get(k)),
998
- optionDescriptions: keys.map((k) => (optionDescs.get(k) ?? []).join(" ").trim())
1011
+ optionDescriptions: keys.map((k) => (optionDescs.get(k) ?? []).join(" ").trim()),
1012
+ currentIndex: 0
1013
+ };
1014
+ }
1015
+ function detectListSelector(lines) {
1016
+ if (!lines.some((l) => /[↑↓].*navigate/i.test(l.trim()))) return null;
1017
+ if (lines.some((l) => /^❯\s*\d+\./.test(l.trim()))) return null;
1018
+ if (!lines.some((l) => /^\s+❯\s+\S/.test(l))) return null;
1019
+ const isSelected = (line) => /^\s+❯\s+\S/.test(line);
1020
+ const isUnselected = (line) => /^ \S/.test(line);
1021
+ const isItem = (line) => isSelected(line) || isUnselected(line);
1022
+ let optionStartIdx = -1;
1023
+ for (let i = 0; i < lines.length; i++) {
1024
+ if (isItem(lines[i])) {
1025
+ optionStartIdx = i;
1026
+ break;
1027
+ }
1028
+ }
1029
+ if (optionStartIdx === -1) return null;
1030
+ const questionParts = [];
1031
+ for (let i = 0; i < optionStartIdx; i++) {
1032
+ const t = lines[i].trim();
1033
+ if (!t) continue;
1034
+ if (/^[─━—═\-]{3,}$/.test(t)) continue;
1035
+ if (/[┌└│┐┘├┤┬┴┼]/.test(t)) {
1036
+ const inner = t.replace(/[│┌└┐┘├┤┬┴┼─]/g, "").trim();
1037
+ if (inner) questionParts.push(inner);
1038
+ continue;
1039
+ }
1040
+ if (/^[>❯]\s/.test(t)) continue;
1041
+ if (/[↑↓].*navigate/i.test(t)) continue;
1042
+ questionParts.push(t);
1043
+ }
1044
+ const question = questionParts.join(" ").trim();
1045
+ const options = [];
1046
+ let currentIndex = 0;
1047
+ for (const line of lines.slice(optionStartIdx)) {
1048
+ const t = line.trim();
1049
+ if (!t) continue;
1050
+ if (/[↑↓].*navigate/i.test(t)) break;
1051
+ if (/^[─━—═\-]{3,}$/.test(t)) continue;
1052
+ if (isSelected(line)) {
1053
+ currentIndex = options.length;
1054
+ options.push(t.replace(/^❯\s+/, "").trim());
1055
+ } else if (isUnselected(line)) {
1056
+ options.push(t);
1057
+ }
1058
+ }
1059
+ if (options.length < 2) return null;
1060
+ return {
1061
+ question,
1062
+ options,
1063
+ optionDescriptions: options.map(() => ""),
1064
+ currentIndex
999
1065
  };
1000
1066
  }
1001
1067
  function filterChrome(lines) {
@@ -1077,13 +1143,13 @@ var OutputService = class _OutputService {
1077
1143
  return;
1078
1144
  }
1079
1145
  const lines = renderToLines(this.rawBuffer);
1080
- const selector = detectSelector(lines);
1146
+ const selector = detectSelector(lines) ?? detectListSelector(lines);
1081
1147
  if (selector) {
1082
1148
  const idleMs2 = this.lastPushTime > 0 ? now - this.lastPushTime : elapsed;
1083
1149
  if (idleMs2 >= _OutputService.SELECTOR_IDLE_MS) {
1084
1150
  this.stopPoll();
1085
1151
  this.active = false;
1086
- this.postChunk({ type: "select_prompt", content: selector.question, options: selector.options, optionDescriptions: selector.optionDescriptions, done: true }).catch(() => {
1152
+ this.postChunk({ type: "select_prompt", content: selector.question, options: selector.options, optionDescriptions: selector.optionDescriptions, currentIndex: selector.currentIndex, done: true }).catch(() => {
1087
1153
  });
1088
1154
  }
1089
1155
  return;
@@ -1106,11 +1172,11 @@ var OutputService = class _OutputService {
1106
1172
  }
1107
1173
  finalize() {
1108
1174
  const lines = renderToLines(this.rawBuffer);
1109
- const selector = detectSelector(lines);
1175
+ const selector = detectSelector(lines) ?? detectListSelector(lines);
1110
1176
  this.stopPoll();
1111
1177
  this.active = false;
1112
1178
  if (selector) {
1113
- this.postChunk({ type: "select_prompt", content: selector.question, options: selector.options, optionDescriptions: selector.optionDescriptions, done: true }).catch(() => {
1179
+ this.postChunk({ type: "select_prompt", content: selector.question, options: selector.options, optionDescriptions: selector.optionDescriptions, currentIndex: selector.currentIndex, done: true }).catch(() => {
1114
1180
  });
1115
1181
  } else {
1116
1182
  const content = filterChrome(lines).join("\n").replace(/\n{3,}/g, "\n\n").trim();
@@ -1232,8 +1298,9 @@ async function start() {
1232
1298
  }
1233
1299
  case "select_option": {
1234
1300
  const index = cmd.payload.index ?? 0;
1301
+ const from = cmd.payload.from ?? 0;
1235
1302
  outputSvc.newTurn();
1236
- claude.selectOption(index);
1303
+ claude.selectOption(index, from);
1237
1304
  break;
1238
1305
  }
1239
1306
  case "escape_key":
@@ -1279,8 +1346,9 @@ async function start() {
1279
1346
  if (input) sendPrompt(input);
1280
1347
  } else if (cmdType === "select_option") {
1281
1348
  const index = inner.index ?? 0;
1349
+ const from = inner.from ?? 0;
1282
1350
  outputSvc.newTurn();
1283
- claude.selectOption(index);
1351
+ claude.selectOption(index, from);
1284
1352
  } else if (cmdType === "escape_key") {
1285
1353
  outputSvc.newTurn();
1286
1354
  claude.sendEscape();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "1.3.8",
3
+ "version": "1.4.0",
4
4
  "description": "Remote control Claude Code from your mobile device",
5
5
  "main": "dist/index.js",
6
6
  "bin": {