@rind-ai/cli 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -50,12 +50,11 @@ export function createRuntimeClient({
50
50
  cwd = process.cwd(),
51
51
  rindHome = process.env.RIND_HOME,
52
52
  runtimePath = process.env.RIND_RUNTIME_PATH || "",
53
- onEvent = () => {},
54
53
  onMessage = null,
55
54
  onStderr = () => {},
56
55
  onExit = () => {},
57
56
  }) {
58
- const handleEvent = onMessage || onEvent;
57
+ const handleEvent = onMessage;
59
58
  const launch = resolveRuntimeLaunch({ python, repoRoot, runtimePath, cliArgs });
60
59
 
61
60
  let nextId = 1;
@@ -120,13 +119,13 @@ export function createRuntimeClient({
120
119
  onExit(code, signal, { closing, error });
121
120
  }
122
121
 
123
- function request(method, params = {}) {
124
- const id = nextId++;
125
- return new Promise((resolve, reject) => {
126
- if (!child || !child.stdin.writable || child.destroyed) {
127
- reject(new Error("Runtime is not running. Start it before sending a request."));
128
- return;
129
- }
122
+ function request(method, params = {}) {
123
+ const id = nextId++;
124
+ return new Promise((resolve, reject) => {
125
+ if (!child || !child.stdin.writable || child.destroyed) {
126
+ reject(new Error("Runtime is not running. Start it before sending a request."));
127
+ return;
128
+ }
130
129
  const entry = { resolve, reject };
131
130
  if (!LONG_RUNNING_METHODS.has(method)) {
132
131
  entry.timer = setTimeout(() => {
@@ -145,8 +144,8 @@ export function createRuntimeClient({
145
144
  reject(error);
146
145
  });
147
146
  });
148
- }
149
-
147
+ }
148
+
150
149
  function receive(line) {
151
150
  let message;
152
151
  try {
@@ -239,8 +238,8 @@ export function createRuntimeClient({
239
238
  get child() {
240
239
  return child;
241
240
  },
242
- start,
243
- request,
241
+ start,
242
+ request,
244
243
  shutdown,
245
244
  forceShutdown,
246
245
  closeInput,
@@ -8,6 +8,7 @@ export const runtimeMethods = Object.freeze({
8
8
  sessionNew: "session/new",
9
9
  sessionList: "session/list",
10
10
  sessionSwitch: "session/switch",
11
+ sessionFork: "session/fork",
11
12
  sessionReplay: "session/replay",
12
13
  sessionPrompt: "session/prompt",
13
14
  sessionCancel: "session/cancel",
@@ -28,12 +29,15 @@ export const runtimeMethods = Object.freeze({
28
29
  goalSet: "rind/goal/set",
29
30
  goalStatus: "rind/goal/status",
30
31
  goalClear: "rind/goal/clear",
32
+ contextInspect: "rind/context/inspect",
33
+ usageSummary: "rind/usage/summary",
31
34
  });
32
35
 
33
36
  export const sessionScopedMethods = new Set([
34
37
  runtimeMethods.sessionPrompt,
35
38
  runtimeMethods.sessionReplay,
36
39
  runtimeMethods.sessionSwitch,
40
+ runtimeMethods.sessionFork,
37
41
  runtimeMethods.sessionCancel,
38
42
  runtimeMethods.modelSet,
39
43
  runtimeMethods.modelEffortSet,
@@ -51,6 +55,7 @@ export const sessionScopedMethods = new Set([
51
55
  runtimeMethods.goalSet,
52
56
  runtimeMethods.goalStatus,
53
57
  runtimeMethods.goalClear,
58
+ runtimeMethods.contextInspect,
54
59
  ]);
55
60
 
56
61
  export const turnScopedMethods = new Set([
package/lib/send.js ADDED
@@ -0,0 +1,53 @@
1
+ import { sendIpc } from "./ipc.js";
2
+ import { paint } from "./theme.js";
3
+
4
+ export const sendHelp = [
5
+ "Usage: rind send --session <id> \"<prompt>\"",
6
+ "",
7
+ "Sends a prompt to the running rind session with that id. Find the id in",
8
+ "the target session's startup banner or /status. Delivery is acknowledged",
9
+ "immediately; the reply appears in that session.",
10
+ ].join("\n");
11
+
12
+ export function parseSendArgs(args) {
13
+ const result = { prompt: null, session: null };
14
+ for (let index = 1; index < args.length; index += 1) {
15
+ const flag = args[index];
16
+ if (flag === "--session") {
17
+ const value = args[index + 1];
18
+ if (value === undefined || value.startsWith("--")) {
19
+ throw new Error("--session requires a value.");
20
+ }
21
+ index += 1;
22
+ if (result.session !== null) throw new Error("--session may only be specified once.");
23
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("Invalid session id.");
24
+ result.session = value;
25
+ continue;
26
+ }
27
+ if (flag.startsWith("--")) {
28
+ throw new Error(`Unknown send option: ${flag}`);
29
+ }
30
+ if (result.prompt !== null) {
31
+ throw new Error("send accepts exactly one prompt.");
32
+ }
33
+ result.prompt = flag;
34
+ }
35
+ if (!result.session) {
36
+ throw new Error("send requires --session <id>.");
37
+ }
38
+ if (!String(result.prompt || "").trim()) {
39
+ throw new Error("send requires a non-empty prompt.");
40
+ }
41
+ return result;
42
+ }
43
+
44
+ export async function runSend({ args, stdout = process.stdout, stderr = process.stderr }) {
45
+ const options = parseSendArgs(args);
46
+ const result = await sendIpc({ session: options.session, input: options.prompt });
47
+ if (!result.ok) {
48
+ stderr.write(`${result.message}\n`);
49
+ return 1;
50
+ }
51
+ stdout.write(`${paint.success("✓")} sent to rind · session ${options.session}\n`);
52
+ return 0;
53
+ }
@@ -16,11 +16,11 @@ export function createTaskMonitorController({
16
16
  let refreshTimer = null;
17
17
  let listInFlight = null;
18
18
  let monitorTimer = null;
19
- let monitorPollInFlight = false;
20
- let monitor = null;
21
- let monitorInputWasActive = false;
22
- let generation = 0;
23
- let pollToken = 0;
19
+ let monitorPollInFlight = false;
20
+ let monitor = null;
21
+ let monitorInputWasActive = false;
22
+ let generation = 0;
23
+ let pollToken = 0;
24
24
 
25
25
  function refresh() {
26
26
  if (!terminalUi || state.runtimeClosing) {
@@ -29,12 +29,12 @@ export function createTaskMonitorController({
29
29
  if (listInFlight) {
30
30
  return listInFlight;
31
31
  }
32
- const requestGeneration = generation;
33
- const promise = request(runtimeMethods.backgroundList)
34
- .then((result) => {
35
- if (requestGeneration !== generation) {
36
- return;
37
- }
32
+ const requestGeneration = generation;
33
+ const promise = request(runtimeMethods.backgroundList)
34
+ .then((result) => {
35
+ if (requestGeneration !== generation) {
36
+ return;
37
+ }
38
38
  const listed = Array.isArray(result?.tasks) ? result.tasks : [];
39
39
  const ids = new Set();
40
40
  for (const task of listed) {
@@ -50,19 +50,19 @@ export function createTaskMonitorController({
50
50
  tasks.delete(bgId);
51
51
  }
52
52
  }
53
- updateCount();
54
- if (monitor) {
55
- monitor.selectedIndex = clampIndex(monitor.selectedIndex);
56
- redraw();
57
- }
58
- })
59
- .finally(() => {
60
- if (listInFlight === promise) {
61
- listInFlight = null;
62
- }
63
- });
64
- listInFlight = promise;
65
- return promise;
53
+ updateCount();
54
+ if (monitor) {
55
+ monitor.selectedIndex = clampIndex(monitor.selectedIndex);
56
+ redraw();
57
+ }
58
+ })
59
+ .finally(() => {
60
+ if (listInFlight === promise) {
61
+ listInFlight = null;
62
+ }
63
+ });
64
+ listInFlight = promise;
65
+ return promise;
66
66
  }
67
67
 
68
68
  function updateCount() {
@@ -104,12 +104,12 @@ export function createTaskMonitorController({
104
104
  refreshTimer = null;
105
105
  }
106
106
 
107
- function clear() {
108
- generation += 1;
109
- listInFlight = null;
110
- pollToken += 1;
111
- monitorPollInFlight = false;
112
- tasks.clear();
107
+ function clear() {
108
+ generation += 1;
109
+ listInFlight = null;
110
+ pollToken += 1;
111
+ monitorPollInFlight = false;
112
+ tasks.clear();
113
113
  pendingCommands.clear();
114
114
  delegates.clear();
115
115
  stopRefresh();
@@ -266,15 +266,15 @@ export function createTaskMonitorController({
266
266
  if (!selected?.bg_id) {
267
267
  return;
268
268
  }
269
- const requestGeneration = generation;
270
- const requestToken = ++pollToken;
271
- monitorPollInFlight = true;
269
+ const requestGeneration = generation;
270
+ const requestToken = ++pollToken;
271
+ monitorPollInFlight = true;
272
272
  try {
273
273
  const result = await request(runtimeMethods.backgroundOutput, {
274
274
  bg_id: selected.bg_id,
275
275
  max_output_chars: 20000,
276
276
  });
277
- if (requestGeneration === generation && result?.task && typeof result.task === "object") {
277
+ if (requestGeneration === generation && result?.task && typeof result.task === "object") {
278
278
  tasks.set(selected.bg_id, {
279
279
  ...selected,
280
280
  ...result.task,
@@ -285,10 +285,10 @@ export function createTaskMonitorController({
285
285
  }
286
286
  } catch {
287
287
  // Periodic list refresh reconciles expired tasks without interrupting input.
288
- } finally {
289
- if (requestToken === pollToken) {
290
- monitorPollInFlight = false;
291
- }
288
+ } finally {
289
+ if (requestToken === pollToken) {
290
+ monitorPollInFlight = false;
291
+ }
292
292
  }
293
293
  }
294
294
 
@@ -369,12 +369,12 @@ export function createTaskMonitorController({
369
369
  };
370
370
  }
371
371
 
372
- function stop() {
373
- generation += 1;
374
- listInFlight = null;
375
- pollToken += 1;
376
- monitorPollInFlight = false;
377
- stopRefresh();
372
+ function stop() {
373
+ generation += 1;
374
+ listInFlight = null;
375
+ pollToken += 1;
376
+ monitorPollInFlight = false;
377
+ stopRefresh();
378
378
  stopMonitorPolling();
379
379
  monitor = null;
380
380
  pendingCommands.clear();
@@ -390,7 +390,7 @@ export function createTaskMonitorController({
390
390
  return tasks.size ? "background" : "delegates";
391
391
  }
392
392
 
393
- function clampIndex(index, page = monitor?.page || "background") {
393
+ function clampIndex(index, page = monitor?.page || "background") {
394
394
  const count = pageItems(page).length;
395
395
  if (!count) {
396
396
  return 0;
@@ -44,12 +44,40 @@ export function parseTerminalKey(raw = "") {
44
44
  return { kind: "text", name: "", text: value };
45
45
  }
46
46
 
47
- const modifiedArrow = value.match(/^\x1b\[1;([2-8])([ABCDHF])$/);
47
+ const kitty = value.match(/^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?u$/);
48
+ if (kitty) {
49
+ const codepoint = Number(kitty[1]);
50
+ const shiftedCodepoint = kitty[2] ? Number(kitty[2]) : codepoint;
51
+ const modifier = Number(kitty[4] || 1);
52
+ if (kitty[5] === "3") {
53
+ return null;
54
+ }
55
+ const special = kittySpecialKey(codepoint, modifier);
56
+ if (special) {
57
+ return special;
58
+ }
59
+ if (codepoint < 32 || codepoint === 127) {
60
+ return null;
61
+ }
62
+ const character = String.fromCodePoint(shiftedCodepoint);
63
+ if (modifier === 1 || modifier === 2) {
64
+ return { kind: "text", name: "", text: character };
65
+ }
66
+ return key(character.toLowerCase(), modifier);
67
+ }
68
+
69
+ const modifiedArrow = value.match(/^\x1b\[1;([0-9]+)(?::([1-3]))?([ABCDHF])$/);
48
70
  if (modifiedArrow) {
49
- return key(ARROW_KEYS[modifiedArrow[2]], Number(modifiedArrow[1]));
71
+ if (modifiedArrow[2] === "3") {
72
+ return null;
73
+ }
74
+ return key(ARROW_KEYS[modifiedArrow[3]], Number(modifiedArrow[1]));
50
75
  }
51
- const tilde = value.match(/^\x1b\[([0-9]+)(?:;([2-8]))?~$/);
76
+ const tilde = value.match(/^\x1b\[([0-9]+)(?:;([0-9]+))?(?::([1-3]))?~$/);
52
77
  if (tilde) {
78
+ if (tilde[3] === "3") {
79
+ return null;
80
+ }
53
81
  return Number(tilde[1]) === 3 ? key("delete", Number(tilde[2] || 1)) : null;
54
82
  }
55
83
  const csiKey = value.match(/^\x1b\[([ABCDHFZ])$/);
@@ -95,3 +123,19 @@ function key(name, modifier = 1) {
95
123
  text: "",
96
124
  };
97
125
  }
126
+
127
+ function kittySpecialKey(codepoint, modifier) {
128
+ if (codepoint === 13) {
129
+ return key("enter", modifier);
130
+ }
131
+ if (codepoint === 9) {
132
+ return key("tab", modifier);
133
+ }
134
+ if (codepoint === 127) {
135
+ return key("backspace", modifier);
136
+ }
137
+ if (codepoint === 27) {
138
+ return key("escape", modifier);
139
+ }
140
+ return null;
141
+ }
package/lib/text-width.js CHANGED
@@ -4,6 +4,8 @@ const segmenter = typeof Intl?.Segmenter === "function"
4
4
  ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
5
5
  : null;
6
6
 
7
+ export const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
8
+
7
9
  export function stripAnsi(value) {
8
10
  return String(value || "").replace(ANSI_RE, "");
9
11
  }
@@ -82,25 +84,6 @@ export function truncateToWidth(value, maxWidth, ellipsis = "...") {
82
84
  return `${takeStartCells(text, maxWidth - suffixWidth)}${ellipsis}`;
83
85
  }
84
86
 
85
- export function expandTabs(value, tabWidth = 4) {
86
- const text = String(value || "");
87
- if (!text.includes("\t")) {
88
- return text;
89
- }
90
- let column = 0;
91
- let output = "";
92
- for (const segment of graphemes(text)) {
93
- if (segment === "\t") {
94
- const spaces = tabWidth - (column % tabWidth);
95
- output += " ".repeat(spaces);
96
- column += spaces;
97
- continue;
98
- }
99
- output += segment;
100
- column += segmentWidth(segment);
101
- }
102
- return output;
103
- }
104
87
 
105
88
  export function wrapTextWithAnsi(value, firstWidth, continuationWidth = firstWidth) {
106
89
  const source = String(value ?? "");
@@ -133,7 +133,7 @@ function resultDataFromRaw(result) {
133
133
  };
134
134
  }
135
135
 
136
- export function renderToolRunning(context, width) {
136
+ export function renderToolRunning(context, width) {
137
137
  const renderer = TOOL_RENDERERS[context.name] || GENERIC_RENDERER;
138
138
  const elapsedSeconds = Math.floor((context.elapsedMs || 0) / 1000);
139
139
  const elapsed = ELAPSED_TITLE_TOOLS.has(context.name) ? dim(` · ${elapsedSeconds}s`) : "";
@@ -143,8 +143,8 @@ export function renderToolRunning(context, width) {
143
143
  if (context.progressMessage) {
144
144
  lines.push(dim(` ↳ ${clipText(context.progressMessage, width, 8)}`));
145
145
  }
146
- return indentToolLines(lines, width);
147
- }
146
+ return indentToolLines(lines, width);
147
+ }
148
148
 
149
149
  export function renderToolFinished(context, width) {
150
150
  const renderer = TOOL_RENDERERS[context.name] || GENERIC_RENDERER;
@@ -152,10 +152,10 @@ export function renderToolFinished(context, width) {
152
152
  const lines = [clipCells(renderer.finished(context, width, state), Math.max(1, width))];
153
153
  if (state.kind === "error") {
154
154
  const detail = errorDetail(context.event, width);
155
- if (detail) {
156
- lines.push(detail);
157
- }
158
- return indentToolLines(lines, width);
155
+ if (detail) {
156
+ lines.push(detail);
157
+ }
158
+ return indentToolLines(lines, width);
159
159
  }
160
160
  const bodyLimit = context.expanded ? EXPANDED_HARD_CAPS[context.name] ?? 200 : COLLAPSED_BODY_CAPS[context.name] ?? 0;
161
161
  const produced = renderer.body ? renderer.body(context, width, bodyLimit) : [];
@@ -164,20 +164,20 @@ export function renderToolFinished(context, width) {
164
164
  if (normalized.total > 0) {
165
165
  lines.push(bodyFooter(normalized.total, context.expanded));
166
166
  }
167
- return indentToolLines(lines, width);
167
+ return indentToolLines(lines, width);
168
168
  }
169
169
  lines.push(...normalized.lines);
170
170
  const hidden = Math.max(0, normalized.total - normalized.lines.length);
171
171
  if (hidden > 0) {
172
172
  lines.push(bodyFooter(hidden, context.expanded));
173
173
  }
174
- return indentToolLines(lines, width);
175
- }
176
-
177
- function indentToolLines(lines, width) {
178
- const limit = Math.max(1, Number(width) || 1);
179
- return lines.map((line) => clipCells(` ${line}`, limit));
180
- }
174
+ return indentToolLines(lines, width);
175
+ }
176
+
177
+ function indentToolLines(lines, width) {
178
+ const limit = Math.max(1, Number(width) || 1);
179
+ return lines.map((line) => clipCells(` ${line}`, limit));
180
+ }
181
181
 
182
182
  function normalizeBody(produced) {
183
183
  if (Array.isArray(produced)) {
@@ -233,7 +233,7 @@ function durationPart(name, event) {
233
233
  return dim(` · ${formatDuration(event?.duration_ms)}`);
234
234
  }
235
235
 
236
- function formatDuration(durationMs) {
236
+ export function formatDuration(durationMs) {
237
237
  const value = Number(durationMs || 0);
238
238
  if (!Number.isFinite(value) || value <= 0) {
239
239
  return "0ms";
@@ -388,12 +388,12 @@ const BASH_RENDERER = {
388
388
 
389
389
  const BASH_OUTPUT_RENDERER = {
390
390
  runningMain(context, width) {
391
- return `${bold("bg")} ${bgIdArg(context.args.bg_id, width, 10)}`;
391
+ return `${bold("bg")} ${commandArg(context.args.bg_id, width, 10)}`;
392
392
  },
393
393
  finished(context, width, state) {
394
394
  const { data } = resultData(context);
395
395
  const id = context.args.bg_id || data.bg_id;
396
- let main = `${bold("bg")} ${bgIdArg(id, width, 14)}`;
396
+ let main = `${bold("bg")} ${commandArg(id, width, 14)}`;
397
397
  if (state.kind === "cancelled") {
398
398
  main += ` ${dim("(cancelled)")}`;
399
399
  }
@@ -658,11 +658,6 @@ function commandArg(value, width, reserve) {
658
658
  return text ? clipText(text, width, reserve) : dim("…");
659
659
  }
660
660
 
661
- function bgIdArg(value, width, reserve) {
662
- const text = singleLineText(value);
663
- return text ? clipText(text, width, reserve) : dim("…");
664
- }
665
-
666
661
  export const TOOL_RENDERERS = {
667
662
  bash: BASH_RENDERER,
668
663
  bash_output: BASH_OUTPUT_RENDERER,
package/lib/tui/cursor.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { CURSOR_MARKER } from "./tui.js";
2
+
3
+ // Non-global: String.match must expose match.index for cursor placement.
4
+ const ANSI_SEQUENCE = /\[[0-?]*[ -/]*[@-~]/;
2
5
  import { graphemes, textWidth } from "../text-width.js";
3
6
 
4
- const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
5
7
 
6
8
  export function insertCursorMarker(line, column) {
7
9
  const text = String(line || "");
@@ -16,14 +18,21 @@ export function insertCursorMarker(line, column) {
16
18
  continue;
17
19
  }
18
20
  }
19
- const codePoint = text.codePointAt(position);
20
- const segment = String.fromCodePoint(codePoint);
21
- const segmentWidth = textWidth(segment);
22
- if (width + segmentWidth > target) {
23
- break;
21
+ const ansiIndex = text.indexOf("\x1b", position);
22
+ const end = ansiIndex === -1 ? text.length : ansiIndex;
23
+ const content = text.slice(position, end);
24
+ if (!content) {
25
+ position += 1;
26
+ continue;
27
+ }
28
+ for (const segment of graphemes(content)) {
29
+ const segmentWidth = textWidth(segment);
30
+ if (width + segmentWidth > target) {
31
+ return `${text.slice(0, position)}${CURSOR_MARKER}${text.slice(position)}`;
32
+ }
33
+ width += segmentWidth;
34
+ position += segment.length;
24
35
  }
25
- width += segmentWidth;
26
- position += segment.length;
27
36
  }
28
37
  return `${text.slice(0, position)}${CURSOR_MARKER}${text.slice(position)}`;
29
38
  }
@@ -3,6 +3,42 @@ const PASTE_START = "\x1b[200~";
3
3
  const PASTE_END = "\x1b[201~";
4
4
  const DEFAULT_INPUT_TIMEOUT_MS = 10;
5
5
 
6
+ function sequenceStatus(value) {
7
+ if (!value.startsWith(ESC)) {
8
+ return "not-escape";
9
+ }
10
+ if (value.length === 1) {
11
+ return "incomplete";
12
+ }
13
+
14
+ const type = value[1];
15
+ if (type === "[") {
16
+ if (value.startsWith(`${ESC}[M`)) {
17
+ return value.length >= 6 ? "complete" : "incomplete";
18
+ }
19
+ return csiStatus(value);
20
+ }
21
+ if (type === "O") {
22
+ return value.length >= 3 ? "complete" : "incomplete";
23
+ }
24
+ if (type === "]" || type === "P" || type === "_") {
25
+ return value.includes("\x07") || value.includes(`${ESC}\\`) ? "complete" : "incomplete";
26
+ }
27
+ const codepoint = value.codePointAt(1);
28
+ if (codepoint >= 0xd800 && codepoint <= 0xdbff && value.length < 3) {
29
+ return "incomplete";
30
+ }
31
+ return "complete";
32
+ }
33
+
34
+ function csiStatus(value) {
35
+ if (value.length < 3) {
36
+ return "incomplete";
37
+ }
38
+ const final = value.charCodeAt(value.length - 1);
39
+ return final >= 0x40 && final <= 0x7e ? "complete" : "incomplete";
40
+ }
41
+
6
42
  export function createInputBuffer(options = {}) {
7
43
  const onSequence = typeof options.onSequence === "function" ? options.onSequence : () => {};
8
44
  const onPaste = typeof options.onPaste === "function" ? options.onPaste : () => {};
@@ -14,6 +50,7 @@ export function createInputBuffer(options = {}) {
14
50
  let timer = null;
15
51
  let pasteMode = false;
16
52
  let pasteBuffer = "";
53
+ let pendingKittyCodepoint = null;
17
54
 
18
55
  function feed(data) {
19
56
  const value = Buffer.isBuffer(data) ? data.toString("utf8") : String(data || "");
@@ -39,6 +76,7 @@ export function createInputBuffer(options = {}) {
39
76
  buffer = "";
40
77
  pasteMode = false;
41
78
  pasteBuffer = "";
79
+ pendingKittyCodepoint = null;
42
80
  }
43
81
 
44
82
  function clearTimer() {
@@ -50,9 +88,20 @@ export function createInputBuffer(options = {}) {
50
88
  }
51
89
 
52
90
  function emit(sequence) {
53
- if (sequence) {
54
- onSequence(sequence);
91
+ if (!sequence) {
92
+ return;
93
+ }
94
+ if (pendingKittyCodepoint !== null) {
95
+ const codepoint = sequence.codePointAt(0);
96
+ if (!sequence.startsWith(ESC) && sequence.length === String.fromCodePoint(codepoint).length && codepoint === pendingKittyCodepoint) {
97
+ pendingKittyCodepoint = null;
98
+ return;
99
+ }
100
+ pendingKittyCodepoint = null;
55
101
  }
102
+ onSequence(sequence);
103
+ const kittyPrintable = sequence.match(/^\x1b\[(\d+)u$/);
104
+ pendingKittyCodepoint = kittyPrintable ? Number(kittyPrintable[1]) : null;
56
105
  }
57
106
 
58
107
  function processBuffer() {
@@ -77,6 +126,7 @@ export function createInputBuffer(options = {}) {
77
126
  pasteMode = true;
78
127
  pasteBuffer = pasteContent;
79
128
  buffer = "";
129
+ pendingKittyCodepoint = null;
80
130
  const endIndex = pasteBuffer.indexOf(PASTE_END);
81
131
  if (endIndex !== -1) {
82
132
  finishPaste(endIndex);
@@ -92,6 +142,7 @@ export function createInputBuffer(options = {}) {
92
142
  const remaining = pasteBuffer.slice(endIndex + PASTE_END.length);
93
143
  pasteMode = false;
94
144
  pasteBuffer = "";
145
+ pendingKittyCodepoint = null;
95
146
  onPaste(content);
96
147
  if (remaining) {
97
148
  feed(remaining);
@@ -130,10 +181,18 @@ export function splitSequences(value) {
130
181
  continue;
131
182
  }
132
183
 
133
- const end = findEscapeEnd(value, position);
184
+ let end = findEscapeEnd(value, position);
134
185
  if (end === -1) {
135
186
  return { sequences, remainder: value.slice(position) };
136
187
  }
188
+ if (value.slice(position, end) === `${ESC}${ESC}`) {
189
+ const next = value[end];
190
+ if (["[", "]", "O", "P", "_"].includes(next)) {
191
+ sequences.push(ESC);
192
+ position += 1;
193
+ continue;
194
+ }
195
+ }
137
196
  sequences.push(value.slice(position, end));
138
197
  position = end;
139
198
  }
@@ -141,32 +200,14 @@ export function splitSequences(value) {
141
200
  }
142
201
 
143
202
  function findEscapeEnd(value, start) {
144
- if (start + 1 >= value.length) {
145
- return -1;
146
- }
147
- const type = value[start + 1];
148
- if (type === "[") {
149
- for (let index = start + 2; index < value.length; index += 1) {
150
- const code = value.charCodeAt(index);
151
- if (code >= 0x40 && code <= 0x7e) {
152
- return index + 1;
153
- }
154
- }
155
- return -1;
156
- }
157
- if (type === "O") {
158
- return start + 3 <= value.length ? start + 3 : -1;
159
- }
160
- if (type === "]" || type === "P" || type === "_") {
161
- const bell = value.indexOf("\x07", start + 2);
162
- const stringTerminator = value.indexOf(`${ESC}\\`, start + 2);
163
- if (bell === -1 && stringTerminator === -1) {
164
- return -1;
203
+ for (let end = start + 1; end <= value.length; end += 1) {
204
+ const status = sequenceStatus(value.slice(start, end));
205
+ if (status === "complete") {
206
+ return end;
165
207
  }
166
- if (bell !== -1 && (stringTerminator === -1 || bell < stringTerminator)) {
167
- return bell + 1;
208
+ if (status === "not-escape") {
209
+ return start + 1;
168
210
  }
169
- return stringTerminator + 2;
170
211
  }
171
- return start + 1 + String.fromCodePoint(value.codePointAt(start + 1)).length;
212
+ return -1;
172
213
  }