@giovannijecha/jecode 0.2.1 → 0.2.2

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.
@@ -38,41 +38,58 @@ export async function runTurn(history, options, events, signal) {
38
38
  throw new Error(`provider returned ${calls.length} tool calls in one step (maximum ${MAX_TOOL_CALLS_PER_STEP})`);
39
39
  }
40
40
  history.push(assistant);
41
- if (assistant.usage !== undefined)
42
- events.onUsage?.(assistant.usage);
43
- if (calls.length === 0)
41
+ if (calls.length === 0) {
42
+ if (assistant.usage !== undefined)
43
+ events.onUsage?.(assistant.usage);
44
44
  return; // the model is done — hand back to the user
45
+ }
45
46
  // Calls run one after another because approval prompts serialise anyway,
46
47
  // but every result from this step goes back in a SINGLE message. Splitting
47
48
  // them teaches the model to stop batching its calls.
48
49
  const results = [];
49
50
  const announced = new Set();
50
51
  try {
52
+ if (assistant.usage !== undefined)
53
+ events.onUsage?.(assistant.usage);
51
54
  for (let index = 0; index < calls.length; index++) {
52
55
  throwIfAborted(signal);
53
56
  const call = calls[index];
54
57
  events.onToolProgress?.(index + 1, calls.length);
55
58
  const preview = await look(call, options, signal);
56
59
  throwIfAborted(signal);
57
- events.onToolCall(call, preview);
58
60
  announced.add(call.id);
61
+ events.onToolCall(call, preview);
59
62
  const { result, summary } = await settle(call, options, events, signal, preview);
60
- events.onToolResult(call, result, summary);
61
63
  results.push(result);
64
+ events.onToolResult(call, result, summary);
62
65
  }
63
66
  }
64
67
  catch (error) {
65
- if (signal?.aborted !== true)
66
- throw error;
68
+ const interrupted = signal?.aborted === true;
69
+ const repairs = [];
67
70
  for (const call of calls.slice(results.length)) {
68
- const interrupted = refuse(call, "interrupted before completion", "interrupted");
69
- if (announced.has(call.id)) {
70
- events.onToolResult(call, interrupted.result, interrupted.summary);
71
- }
72
- results.push(interrupted.result);
71
+ const run = interrupted
72
+ ? refuse(call, "interrupted before completion", "interrupted")
73
+ : refuse(call, "tool processing stopped before completion", "failed");
74
+ repairs.push({ call, run });
75
+ results.push(run.result);
73
76
  }
74
77
  history.push({ role: "user", content: results });
75
- throw abortReason(signal);
78
+ // History repair is the invariant. UI recovery is best-effort and must
79
+ // never replace the original exception or leave the conversation open.
80
+ for (const { call, run } of repairs) {
81
+ if (!announced.has(call.id))
82
+ continue;
83
+ try {
84
+ events.onToolResult(call, run.result, run.summary);
85
+ }
86
+ catch {
87
+ // The surface is already failing; the next turn can still proceed.
88
+ }
89
+ }
90
+ if (interrupted)
91
+ throw abortReason(signal);
92
+ throw error;
76
93
  }
77
94
  history.push({ role: "user", content: results });
78
95
  }
@@ -2,6 +2,12 @@
2
2
  import { credentialValues } from "./credentials.js";
3
3
  import { accountValues } from "./accounts.js";
4
4
  const REDACTED = "[credential redacted]";
5
+ const MIN_HEURISTIC_SECRET_CHARS = 8;
6
+ const EXPLICIT_CREDENTIAL_ENVIRONMENT_NAMES = new Set([
7
+ "ANTHROPIC_API_KEY",
8
+ "OLLAMA_API_KEY",
9
+ "OPENAI_API_KEY",
10
+ ]);
5
11
  const SENSITIVE_ENVIRONMENT_NAME = /(?:^|_)(?:API_?KEY|ACCESS_?KEY|PRIVATE_?KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|PASS|PWD|CREDENTIALS?|AUTH|JWT|COOKIE|PAT)(?:_|$)/i;
6
12
  const COMPACT_SENSITIVE_ENVIRONMENT_NAME = /^(?:PGPASSWORD)$/i;
7
13
  const SAFE_ENVIRONMENT_NAMES = new Set([
@@ -71,8 +77,12 @@ function sensitiveEnvironmentName(name) {
71
77
  function secrets(source) {
72
78
  const values = new Set([...credentialValues(), ...accountValues()]);
73
79
  for (const [name, value] of Object.entries(source)) {
74
- if (value !== undefined && value !== "" && sensitiveEnvironment(name, value))
80
+ if (value === undefined || value === "")
81
+ continue;
82
+ const explicit = EXPLICIT_CREDENTIAL_ENVIRONMENT_NAMES.has(name.toUpperCase());
83
+ if (explicit || (value.length >= MIN_HEURISTIC_SECRET_CHARS && sensitiveEnvironment(name, value))) {
75
84
  values.add(value);
85
+ }
76
86
  }
77
87
  return [...values].filter((value) => value !== "").sort((left, right) => right.length - left.length);
78
88
  }
@@ -8,6 +8,7 @@ const MAX_RESULTS = 500;
8
8
  const MAX_VISITED = 20_000;
9
9
  const MAX_FILE_BYTES = 1_000_000;
10
10
  const MAX_MATCH_LINE = 500;
11
+ const MAX_GLOB_CHARS = 512;
11
12
  const SKIP = new Set([".git", ".hg", ".svn", "node_modules"]);
12
13
  export const findFiles = {
13
14
  name: "find_files",
@@ -17,7 +18,10 @@ export const findFiles = {
17
18
  input: {
18
19
  type: "object",
19
20
  properties: {
20
- pattern: { type: "string", description: "Glob matched against workspace-relative paths." },
21
+ pattern: {
22
+ type: "string",
23
+ description: "Glob matched against workspace-relative paths. Maximum 512 characters.",
24
+ },
21
25
  path: { type: "string", description: "Directory to search, relative to the workspace root." },
22
26
  max_results: { type: "integer", description: "Maximum paths returned. Defaults to 100, caps at 500." },
23
27
  },
@@ -52,7 +56,10 @@ export const searchText = {
52
56
  properties: {
53
57
  query: { type: "string", description: "Literal text to find." },
54
58
  path: { type: "string", description: "Directory to search, relative to the workspace root." },
55
- pattern: { type: "string", description: "Optional file glob, for example **/*.ts." },
59
+ pattern: {
60
+ type: "string",
61
+ description: "Optional file glob, for example **/*.ts. Maximum 512 characters.",
62
+ },
56
63
  case_sensitive: { type: "boolean", description: "Defaults to false." },
57
64
  max_results: { type: "integer", description: "Maximum matching lines. Defaults to 100, caps at 500." },
58
65
  },
@@ -166,29 +173,107 @@ function resultLimit(args) {
166
173
  }
167
174
  function glob(pattern) {
168
175
  const normalized = pattern.replace(/\\/g, "/");
169
- let source = "";
170
- for (let index = 0; index < normalized.length; index++) {
171
- const char = normalized[index];
172
- if (char === "*" && normalized[index + 1] === "*") {
173
- if (normalized[index + 2] === "/") {
174
- source += "(?:.*/)?";
175
- index += 2;
176
+ if (normalized.length > MAX_GLOB_CHARS) {
177
+ throw new Error(`"pattern" must be at most ${MAX_GLOB_CHARS} characters`);
178
+ }
179
+ const tokens = tokenizeGlob(normalized.toLowerCase());
180
+ const basenameOnly = !normalized.includes("/");
181
+ return (relative) => {
182
+ const candidate = relative.replace(/\\/g, "/");
183
+ const target = (basenameOnly ? path.posix.basename(candidate) : candidate).toLowerCase();
184
+ return matchGlob(tokens, Array.from(target));
185
+ };
186
+ }
187
+ function tokenizeGlob(pattern) {
188
+ const chars = Array.from(pattern);
189
+ const tokens = [];
190
+ let index = 0;
191
+ while (index < chars.length) {
192
+ const char = chars[index];
193
+ if (char === "*") {
194
+ let end = index + 1;
195
+ while (chars[end] === "*")
196
+ end++;
197
+ if (end - index >= 2) {
198
+ if (chars[end] === "/") {
199
+ tokens.push({ kind: "globdir-start" }, { kind: "globdir-body" });
200
+ index = end + 1;
201
+ }
202
+ else {
203
+ tokens.push({ kind: "globstar" });
204
+ index = end;
205
+ }
176
206
  }
177
207
  else {
178
- source += ".*";
179
- index++;
208
+ tokens.push({ kind: "star" });
209
+ index = end;
180
210
  }
211
+ continue;
181
212
  }
182
- else if (char === "*")
183
- source += "[^/]*";
184
- else if (char === "?")
185
- source += "[^/]";
186
- else
187
- source += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
213
+ tokens.push(char === "?" ? { kind: "one" } : { kind: "literal", value: char });
214
+ index++;
188
215
  }
189
- const expression = new RegExp(`^${source}$`, "i");
190
- const basenameOnly = !normalized.includes("/");
191
- return (relative) => expression.test(basenameOnly ? path.posix.basename(relative) : relative);
216
+ return tokens;
217
+ }
218
+ /** Thompson-style wildcard matching: O(pattern × path), with no regex backtracking. */
219
+ function matchGlob(tokens, text) {
220
+ let states = epsilonClosure(new Set([0]), tokens);
221
+ for (const char of text) {
222
+ const next = new Set();
223
+ for (const state of states) {
224
+ const token = tokens[state];
225
+ if (token === undefined)
226
+ continue;
227
+ switch (token.kind) {
228
+ case "literal":
229
+ if (token.value === char)
230
+ next.add(state + 1);
231
+ break;
232
+ case "one":
233
+ if (char !== "/")
234
+ next.add(state + 1);
235
+ break;
236
+ case "star":
237
+ if (char !== "/")
238
+ next.add(state);
239
+ break;
240
+ case "globstar":
241
+ next.add(state);
242
+ break;
243
+ case "globdir-body":
244
+ next.add(state);
245
+ if (char === "/")
246
+ next.add(state + 1);
247
+ break;
248
+ case "globdir-start":
249
+ break;
250
+ }
251
+ }
252
+ states = epsilonClosure(next, tokens);
253
+ if (states.size === 0)
254
+ return false;
255
+ }
256
+ return epsilonClosure(states, tokens).has(tokens.length);
257
+ }
258
+ function epsilonClosure(seed, tokens) {
259
+ const states = new Set(seed);
260
+ const pending = [...seed];
261
+ while (pending.length > 0) {
262
+ const state = pending.pop();
263
+ const token = tokens[state];
264
+ const targets = token?.kind === "globdir-start"
265
+ ? [state + 1, state + 2]
266
+ : token?.kind === "star" || token?.kind === "globstar"
267
+ ? [state + 1]
268
+ : [];
269
+ for (const target of targets) {
270
+ if (states.has(target))
271
+ continue;
272
+ states.add(target);
273
+ pending.push(target);
274
+ }
275
+ }
276
+ return states;
192
277
  }
193
278
  function summary(count, limit, capped, one, many) {
194
279
  const noun = count === 1 ? one : many;
@@ -4,7 +4,9 @@ import { spawn } from "node:child_process";
4
4
  import { optionalInt, requireString } from "./args.js";
5
5
  import { credentialRedactor, redactCredentials, shellEnvironment } from "../credential-safety.js";
6
6
  const DEFAULT_TIMEOUT_MS = 120_000;
7
+ const MAX_TIMEOUT_MS = 2_147_483_647;
7
8
  const MAX_OUTPUT_CHARS = 30_000;
9
+ const PIPE_DRAIN_MS = 100;
8
10
  export const runCommand = {
9
11
  name: "run_command",
10
12
  description: "Run a shell command starting in the workspace root and return its combined stdout " +
@@ -24,6 +26,9 @@ export const runCommand = {
24
26
  const timeoutMs = optionalInt(args, "timeout_ms") ?? DEFAULT_TIMEOUT_MS;
25
27
  if (timeoutMs <= 0)
26
28
  throw new Error('"timeout_ms" must be a positive integer');
29
+ if (timeoutMs > MAX_TIMEOUT_MS) {
30
+ throw new Error(`"timeout_ms" must be at most ${MAX_TIMEOUT_MS}ms`);
31
+ }
27
32
  const result = await execute(command, ctx.root, timeoutMs, ctx.signal, ctx.onOutput);
28
33
  const output = redactCredentials(result.output);
29
34
  const summary = result.timedOut
@@ -53,6 +58,7 @@ function execute(command, cwd, timeoutMs, signal, onOutput) {
53
58
  let aborted;
54
59
  let settled = false;
55
60
  let forceTimer;
61
+ let drainTimer;
56
62
  const timer = setTimeout(() => {
57
63
  timedOut = true;
58
64
  stopTree(child.pid, false);
@@ -68,8 +74,20 @@ function execute(command, cwd, timeoutMs, signal, onOutput) {
68
74
  clearTimeout(timer);
69
75
  if (forceTimer !== undefined)
70
76
  clearTimeout(forceTimer);
77
+ if (drainTimer !== undefined)
78
+ clearTimeout(drainTimer);
71
79
  signal?.removeEventListener("abort", onAbort);
72
80
  };
81
+ const finish = (code) => {
82
+ if (settled)
83
+ return;
84
+ settled = true;
85
+ cleanup();
86
+ if (aborted !== undefined)
87
+ reject(aborted);
88
+ else
89
+ resolve({ output: output.value().trimEnd(), code, timedOut });
90
+ };
73
91
  child.stdout.setEncoding("utf8");
74
92
  child.stderr.setEncoding("utf8");
75
93
  child.stdout.on("data", output.append);
@@ -81,16 +99,17 @@ function execute(command, cwd, timeoutMs, signal, onOutput) {
81
99
  cleanup();
82
100
  reject(error);
83
101
  });
84
- child.on("close", (code) => {
85
- if (settled)
86
- return;
87
- settled = true;
88
- cleanup();
89
- if (aborted !== undefined)
90
- reject(aborted);
91
- else
92
- resolve({ output: output.value().trimEnd(), code, timedOut });
102
+ child.on("exit", (code) => {
103
+ // `close` normally follows once both pipes drain. A detached descendant
104
+ // can inherit those descriptors after the command itself has exited,
105
+ // though, so bound that final drain instead of hanging the tool on it.
106
+ drainTimer = setTimeout(() => {
107
+ child.stdout.destroy();
108
+ child.stderr.destroy();
109
+ finish(code);
110
+ }, PIPE_DRAIN_MS);
93
111
  });
112
+ child.on("close", finish);
94
113
  });
95
114
  }
96
115
  function capture(onOutput) {
package/dist/tui/app.js CHANGED
@@ -34,6 +34,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
34
34
  let closed;
35
35
  let frameTimer;
36
36
  let spinTimer;
37
+ let escapeTimer;
37
38
  let stopResize = () => { };
38
39
  let stopInput = () => { };
39
40
  // Timers outlive the teardown they were scheduled before. Painting after the
@@ -118,6 +119,8 @@ export async function runApp(session, transcriptRoot, environment = {}) {
118
119
  clearInterval(spinTimer);
119
120
  if (frameTimer !== undefined)
120
121
  clearTimeout(frameTimer);
122
+ if (escapeTimer !== undefined)
123
+ clearTimeout(escapeTimer);
121
124
  feedback.close();
122
125
  terminal.leave();
123
126
  closed?.();
@@ -204,12 +207,23 @@ export async function runApp(session, transcriptRoot, environment = {}) {
204
207
  draw();
205
208
  });
206
209
  stopInput = terminal.onInput((chunk) => {
207
- for (const key of keys.push(chunk))
210
+ if (escapeTimer !== undefined)
211
+ clearTimeout(escapeTimer);
212
+ for (const key of keys.push(chunk)) {
213
+ if (!live)
214
+ break;
208
215
  input.handle(key);
209
- setTimeout(() => {
216
+ }
217
+ if (!live)
218
+ return;
219
+ escapeTimer = setTimeout(() => {
220
+ escapeTimer = undefined;
221
+ if (!live)
222
+ return;
210
223
  for (const key of keys.flush())
211
224
  input.handle(key);
212
- render();
225
+ if (live)
226
+ render();
213
227
  }, ESCAPE_MS);
214
228
  render();
215
229
  });
package/dist/tui/keys.js CHANGED
@@ -12,6 +12,8 @@ const PASTE_END = "[201~";
12
12
  // one packs the coordinates into single bytes and simply stops being able to
13
13
  // say where the pointer is past column 223.
14
14
  const MOUSE = /^\[<(\d+);(\d+);(\d+)([Mm])/;
15
+ const CSI_SEQUENCE = /^\[[0-?]*[ -/]*[@-~]/;
16
+ const SS3_SEQUENCE = /^O[ -~]/;
15
17
  const BUTTONS = ["left", "middle", "right", "none"];
16
18
  // Both the normal and the application-cursor forms, because a terminal sends
17
19
  // either depending on the mode it thinks it is in.
@@ -59,6 +61,16 @@ export function decoder() {
59
61
  if (pasting) {
60
62
  const end = held.indexOf(ESC + PASTE_END);
61
63
  if (end === -1) {
64
+ const interrupt = firstInterrupt(held);
65
+ if (interrupt !== -1) {
66
+ // A missing bracketed-paste terminator must not trap the decoder
67
+ // forever. Ctrl+C/Ctrl+D are emergency exits: discard the partial
68
+ // paste, then let the normal control-key path handle the byte.
69
+ held = held.slice(interrupt);
70
+ pasted = "";
71
+ pasting = false;
72
+ continue;
73
+ }
62
74
  // Hold back a possible partial terminator rather than pasting it.
63
75
  const safe = held.length - PASTE_END.length - 1;
64
76
  if (safe > 0) {
@@ -96,6 +108,13 @@ export function decoder() {
96
108
  keys.push({ name: SEQUENCES[match], text: "", ctrl: false });
97
109
  continue;
98
110
  }
111
+ const unbound = completeTerminalSequence(rest);
112
+ if (unbound !== undefined) {
113
+ // Terminals have many optional keys and mode reports. An unbound but
114
+ // complete CSI/SS3 sequence is terminal protocol, never editor text.
115
+ held = rest.slice(unbound.length);
116
+ continue;
117
+ }
99
118
  if (!final && couldGrow(rest))
100
119
  break;
101
120
  held = held.slice(1);
@@ -139,6 +158,15 @@ export function decoder() {
139
158
  },
140
159
  };
141
160
  }
161
+ function firstInterrupt(text) {
162
+ const ctrlC = text.indexOf(String.fromCharCode(3));
163
+ const ctrlD = text.indexOf(String.fromCharCode(4));
164
+ if (ctrlC === -1)
165
+ return ctrlD;
166
+ if (ctrlD === -1)
167
+ return ctrlC;
168
+ return Math.min(ctrlC, ctrlD);
169
+ }
142
170
  function matchSequence(rest) {
143
171
  for (const seq of Object.keys(SEQUENCES)) {
144
172
  if (rest.startsWith(seq))
@@ -146,10 +174,15 @@ function matchSequence(rest) {
146
174
  }
147
175
  return undefined;
148
176
  }
177
+ function completeTerminalSequence(rest) {
178
+ return CSI_SEQUENCE.exec(rest)?.[0] ?? SS3_SEQUENCE.exec(rest)?.[0];
179
+ }
149
180
  /** Whether `rest` is still a viable prefix of something we know. */
150
181
  function couldGrow(rest) {
151
182
  if (PASTE_START.startsWith(rest))
152
183
  return true;
184
+ if (/^\[[0-?]*[ -/]*$/.test(rest))
185
+ return true;
153
186
  // A mouse report has no fixed length, so it grows until its final letter.
154
187
  if (/^\[<?\d*;?\d*;?\d*$/.test(rest))
155
188
  return true;
@@ -37,6 +37,7 @@ const CURSOR_RESET = `${CSI}0 q`;
37
37
  const SYNC_BEGIN = `${CSI}?2026h`;
38
38
  const SYNC_END = `${CSI}?2026l`;
39
39
  let active = false;
40
+ let handlersRegistered = false;
40
41
  export function interactive() {
41
42
  return process.stdin.isTTY === true && process.stdout.isTTY === true;
42
43
  }
@@ -53,9 +54,7 @@ export function enter(reducedMotion = false) {
53
54
  if (active)
54
55
  return;
55
56
  active = true;
56
- process.on("exit", leave);
57
- process.on("SIGTERM", onFatalSignal);
58
- process.on("SIGHUP", onFatalSignal);
57
+ registerProcessHandlers();
59
58
  write(ALT_ON + WRAP_OFF + CURSOR_HIDE + (reducedMotion ? CURSOR_STEADY : CURSOR_BLOCK) + PASTE_ON + MOUSE_ON);
60
59
  process.stdin.setRawMode(true);
61
60
  process.stdin.setEncoding("utf8");
@@ -74,9 +73,24 @@ export function setReducedMotion(reducedMotion) {
74
73
  if (active)
75
74
  write(reducedMotion ? CURSOR_STEADY : CURSOR_BLOCK);
76
75
  }
77
- function onFatalSignal() {
76
+ function registerProcessHandlers() {
77
+ if (handlersRegistered)
78
+ return;
79
+ handlersRegistered = true;
80
+ process.on("exit", leave);
81
+ process.on("uncaughtExceptionMonitor", leave);
82
+ process.on("SIGTERM", onSigterm);
83
+ process.on("SIGHUP", onSighup);
84
+ }
85
+ function onSigterm() {
86
+ onFatalSignal(15);
87
+ }
88
+ function onSighup() {
89
+ onFatalSignal(1);
90
+ }
91
+ function onFatalSignal(number) {
78
92
  leave();
79
- process.exit(0);
93
+ process.exit(128 + number);
80
94
  }
81
95
  export function onResize(handler) {
82
96
  process.stdout.on("resize", handler);
package/dist/ui/width.js CHANGED
@@ -27,6 +27,43 @@ const WIDE = [
27
27
  [0x1fa70, 0x1faff],
28
28
  [0x20000, 0x3fffd],
29
29
  ];
30
+ /** Default emoji-presentation ranges below the main supplementary blocks. */
31
+ const EMOJI_WIDE = [
32
+ [0x231a, 0x231b],
33
+ [0x23e9, 0x23ec],
34
+ [0x23f0, 0x23f0],
35
+ [0x23f3, 0x23f3],
36
+ [0x25fd, 0x25fe],
37
+ [0x2614, 0x2615],
38
+ [0x2648, 0x2653],
39
+ [0x267f, 0x267f],
40
+ [0x2693, 0x2693],
41
+ [0x26a1, 0x26a1],
42
+ [0x26aa, 0x26ab],
43
+ [0x26bd, 0x26be],
44
+ [0x26c4, 0x26c5],
45
+ [0x26ce, 0x26ce],
46
+ [0x26d4, 0x26d4],
47
+ [0x26ea, 0x26ea],
48
+ [0x26f2, 0x26f3],
49
+ [0x26f5, 0x26f5],
50
+ [0x26fa, 0x26fa],
51
+ [0x26fd, 0x26fd],
52
+ [0x2705, 0x2705],
53
+ [0x270a, 0x270b],
54
+ [0x2728, 0x2728],
55
+ [0x274c, 0x274c],
56
+ [0x274e, 0x274e],
57
+ [0x2753, 0x2755],
58
+ [0x2757, 0x2757],
59
+ [0x2795, 0x2797],
60
+ [0x27b0, 0x27b0],
61
+ [0x27bf, 0x27bf],
62
+ [0x2b1b, 0x2b1c],
63
+ [0x2b50, 0x2b50],
64
+ [0x2b55, 0x2b55],
65
+ [0x1f1e6, 0x1f1ff],
66
+ ];
30
67
  /** Ranges that occupy no cell of their own: they attach to what precedes. */
31
68
  const ZERO = [
32
69
  [0x0300, 0x036f],
@@ -54,6 +91,7 @@ function inRanges(code, ranges) {
54
91
  }
55
92
  // The emoji presentation selector, built rather than typed: an invisible byte
56
93
  // in source is a byte nobody reviews.
94
+ const VS15 = String.fromCodePoint(0xfe0e);
57
95
  const VS16 = String.fromCodePoint(0xfe0f);
58
96
  // Grapheme segmentation is in the standard library, so a family emoji built
59
97
  // out of five code points and three joiners counts as the one thing the
@@ -80,9 +118,11 @@ export function charWidth(cluster) {
80
118
  return 0;
81
119
  if (inRanges(code, ZERO))
82
120
  return 0;
121
+ if (cluster.includes(VS15))
122
+ return inRanges(code, WIDE) ? 2 : 1;
83
123
  if (cluster.includes(VS16))
84
124
  return 2;
85
- return inRanges(code, WIDE) ? 2 : 1;
125
+ return inRanges(code, WIDE) || inRanges(code, EMOJI_WIDE) ? 2 : 1;
86
126
  }
87
127
  export function textWidth(text) {
88
128
  let total = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giovannijecha/jecode",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "An owned coding agent with zero external runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {