@giovannijecha/jecode 0.7.2 → 0.7.4

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 (43) hide show
  1. package/README.md +87 -23
  2. package/dist/atomic.js +14 -0
  3. package/dist/batch.js +26 -19
  4. package/dist/cli-info.js +1 -1
  5. package/dist/config.js +3 -2
  6. package/dist/context/budget.js +37 -0
  7. package/dist/context/compactor.js +9 -2
  8. package/dist/context/estimate.js +31 -0
  9. package/dist/context/policy.js +24 -14
  10. package/dist/controller-request.js +27 -4
  11. package/dist/controller.js +6 -4
  12. package/dist/conversation.js +48 -31
  13. package/dist/oauth-http.js +2 -1
  14. package/dist/openai-oauth-callback.js +2 -1
  15. package/dist/providers/anthropic.js +1 -1
  16. package/dist/providers/http.js +8 -5
  17. package/dist/providers/ollama.js +1 -1
  18. package/dist/providers/openai-codex.js +1 -3
  19. package/dist/providers/openai.js +1 -1
  20. package/dist/providers/sse.js +117 -40
  21. package/dist/providers/stream-limits.js +14 -2
  22. package/dist/sessions/store.js +2 -1
  23. package/dist/text-boundary.js +47 -0
  24. package/dist/timeline.js +2 -1
  25. package/dist/tools/fs.js +85 -38
  26. package/dist/tools/search.js +106 -44
  27. package/dist/tools/shell.js +14 -6
  28. package/dist/tools/text-boundary.js +7 -25
  29. package/dist/tui/app-state.js +0 -1
  30. package/dist/tui/app-workflows.js +40 -30
  31. package/dist/tui/app.js +12 -14
  32. package/dist/tui/blocks.js +0 -2
  33. package/dist/tui/components/messages.js +2 -5
  34. package/dist/tui/components/tool.js +11 -10
  35. package/dist/tui/session-view.js +2 -1
  36. package/dist/tui/transcript-view.js +178 -106
  37. package/dist/tui/turn.js +29 -8
  38. package/dist/tui/view.js +8 -3
  39. package/dist/ui/diff.js +51 -16
  40. package/dist/ui/render.js +16 -24
  41. package/dist/ui/width.js +31 -22
  42. package/dist/usage.js +5 -1
  43. package/package.json +2 -1
package/dist/ui/diff.js CHANGED
@@ -8,24 +8,37 @@
8
8
  * Above this many cells the table stops being worth building.
9
9
  *
10
10
  * The diff is quadratic in the two line counts, and it is drawn between two
11
- * keystrokes. Past the ceiling the honest answer is the coarse one — all of
12
- * the old, then all of the new — rather than a frame the user waits for.
11
+ * keystrokes. Common edges are removed before this limit is applied, so a
12
+ * local edit in a large file stays local. If the changed middle itself crosses
13
+ * the ceiling, the honest answer is the coarse one rather than a frame the
14
+ * user waits for.
13
15
  */
14
16
  const CEILING = 250_000;
15
17
  export function diff(before, after) {
16
18
  const a = lines(before);
17
19
  const b = lines(after);
18
- if (a.length * b.length > CEILING) {
19
- return [...a.map(del), ...b.map(add)];
20
+ const prefix = commonPrefix(a, b);
21
+ const suffix = commonSuffix(a, b, prefix);
22
+ const aEnd = a.length - suffix;
23
+ const bEnd = b.length - suffix;
24
+ const middleA = a.slice(prefix, aEnd);
25
+ const middleB = b.slice(prefix, bEnd);
26
+ const rows = a.slice(0, prefix).map(keep);
27
+ if (middleA.length * middleB.length > CEILING) {
28
+ for (const line of middleA)
29
+ rows.push(del(line));
30
+ for (const line of middleB)
31
+ rows.push(add(line));
32
+ appendSuffix(rows, a, aEnd);
33
+ return rows;
20
34
  }
21
- const table = common(a, b);
22
- const rows = [];
35
+ const table = common(middleA, middleB);
23
36
  let i = 0;
24
37
  let j = 0;
25
- const width = b.length + 1;
26
- while (i < a.length && j < b.length) {
27
- if (a[i] === b[j]) {
28
- rows.push({ kind: "keep", text: a[i] });
38
+ const width = middleB.length + 1;
39
+ while (i < middleA.length && j < middleB.length) {
40
+ if (middleA[i] === middleB[j]) {
41
+ rows.push(keep(middleA[i]));
29
42
  i++;
30
43
  j++;
31
44
  continue;
@@ -33,20 +46,39 @@ export function diff(before, after) {
33
46
  // Deletions first on a tie, so a replaced line reads old-then-new — the
34
47
  // order the eye expects, and the order every other diff prints.
35
48
  if (table[(i + 1) * width + j] >= table[i * width + j + 1]) {
36
- rows.push(del(a[i]));
49
+ rows.push(del(middleA[i]));
37
50
  i++;
38
51
  }
39
52
  else {
40
- rows.push(add(b[j]));
53
+ rows.push(add(middleB[j]));
41
54
  j++;
42
55
  }
43
56
  }
44
- while (i < a.length)
45
- rows.push(del(a[i++]));
46
- while (j < b.length)
47
- rows.push(add(b[j++]));
57
+ while (i < middleA.length)
58
+ rows.push(del(middleA[i++]));
59
+ while (j < middleB.length)
60
+ rows.push(add(middleB[j++]));
61
+ appendSuffix(rows, a, aEnd);
48
62
  return rows;
49
63
  }
64
+ function commonPrefix(a, b) {
65
+ const limit = Math.min(a.length, b.length);
66
+ let length = 0;
67
+ while (length < limit && a[length] === b[length])
68
+ length++;
69
+ return length;
70
+ }
71
+ function commonSuffix(a, b, prefix) {
72
+ const limit = Math.min(a.length, b.length) - prefix;
73
+ let length = 0;
74
+ while (length < limit && a[a.length - length - 1] === b[b.length - length - 1])
75
+ length++;
76
+ return length;
77
+ }
78
+ function appendSuffix(rows, source, start) {
79
+ for (let index = start; index < source.length; index++)
80
+ rows.push(keep(source[index]));
81
+ }
50
82
  /**
51
83
  * Drop the unchanged stretches, keeping `context` rows either side.
52
84
  *
@@ -109,3 +141,6 @@ function add(text) {
109
141
  function del(text) {
110
142
  return { kind: "del", text };
111
143
  }
144
+ function keep(text) {
145
+ return { kind: "keep", text };
146
+ }
package/dist/ui/render.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Composing a terminal row out of styled segments. Pure: every function here
2
2
  // returns a string or rows of segments, none of them write anywhere.
3
3
  import { terminalText } from "./terminal-text.js";
4
- import { elide, textWidth, wrapText } from "./width.js";
4
+ import { elide, splitByCells, textWidth, wrapText } from "./width.js";
5
5
  // Built rather than written literally: a raw escape byte in the source is
6
6
  // invisible in a diff and in a code review, which is how they survive.
7
7
  const ESC = String.fromCharCode(27);
@@ -166,13 +166,13 @@ export function flow(segs, max, continuation = []) {
166
166
  let used = 0;
167
167
  let pending;
168
168
  const room = () => Math.max(1, max - (rows.length === 0 ? 0 : lead));
169
- const add = (tok) => {
169
+ const add = (tok, width = textWidth(tok.text)) => {
170
170
  const last = current[current.length - 1];
171
171
  if (last !== undefined && sameStyle(last, tok.seg))
172
172
  last.text += tok.text;
173
173
  else
174
174
  current.push({ ...tok.seg, text: tok.text });
175
- used += textWidth(tok.text);
175
+ used += width;
176
176
  };
177
177
  const flush = () => {
178
178
  rows.push(rows.length === 0 ? current : [...continuation.map(copy), ...current]);
@@ -191,39 +191,31 @@ export function flow(segs, max, continuation = []) {
191
191
  continue;
192
192
  }
193
193
  const space = pending === undefined ? 0 : textWidth(pending.text);
194
- let word = tok.text;
195
- if (used + space + textWidth(word) > room() && current.length > 0)
194
+ const wordWidth = textWidth(tok.text);
195
+ if (used + space + wordWidth > room() && current.length > 0)
196
196
  flush();
197
197
  else if (pending !== undefined) {
198
- add(pending);
198
+ add(pending, space);
199
199
  pending = undefined;
200
200
  }
201
201
  // A word wider than any row is spent across rows: with autowrap off, what
202
202
  // overflows is not ugly, it is gone.
203
- while (textWidth(word) > room() - used) {
204
- const head = clipTo(word, room() - used);
205
- if (head === "")
206
- break;
207
- add({ ...tok, text: head });
208
- word = word.slice(head.length);
209
- flush();
203
+ if (wordWidth > room() - used) {
204
+ const chunks = splitByCells(tok.text, room() - used, Math.max(1, max - lead));
205
+ for (const [index, chunk] of chunks.entries()) {
206
+ add({ ...tok, text: chunk.text }, chunk.width);
207
+ if (index + 1 < chunks.length)
208
+ flush();
209
+ }
210
+ continue;
210
211
  }
211
- if (word !== "")
212
- add({ ...tok, text: word });
212
+ if (tok.text !== "")
213
+ add(tok, wordWidth);
213
214
  }
214
215
  if (current.length > 0 || rows.length === 0)
215
216
  flush();
216
217
  return rows;
217
218
  }
218
- function clipTo(text, cols) {
219
- let out = "";
220
- for (const char of text) {
221
- if (textWidth(out + char) > cols)
222
- break;
223
- out += char;
224
- }
225
- return out;
226
- }
227
219
  function copy(seg) {
228
220
  return { ...seg };
229
221
  }
package/dist/ui/width.js CHANGED
@@ -6,6 +6,8 @@
6
6
  // cell at all. Every alignment in the UI depends on this file being right:
7
7
  // the right-hand column, a ground band, the cursor. Nothing measures with
8
8
  // `.length`.
9
+ import { graphemes, segmentGraphemes } from "../text-boundary.js";
10
+ export { graphemes } from "../text-boundary.js";
9
11
  /** Ranges the terminal draws two cells wide (East Asian Wide and Fullwidth). */
10
12
  const WIDE = [
11
13
  [0x1100, 0x115f],
@@ -93,16 +95,6 @@ function inRanges(code, ranges) {
93
95
  // in source is a byte nobody reviews.
94
96
  const VS15 = String.fromCodePoint(0xfe0e);
95
97
  const VS16 = String.fromCodePoint(0xfe0f);
96
- // Grapheme segmentation is in the standard library, so a family emoji built
97
- // out of five code points and three joiners counts as the one thing the
98
- // terminal actually draws.
99
- const SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
100
- export function graphemes(text) {
101
- const out = [];
102
- for (const { segment } of SEGMENTER.segment(text))
103
- out.push(segment);
104
- return out;
105
- }
106
98
  /**
107
99
  * Cells taken by one grapheme cluster.
108
100
  *
@@ -126,17 +118,38 @@ export function charWidth(cluster) {
126
118
  }
127
119
  export function textWidth(text) {
128
120
  let total = 0;
129
- for (const cluster of SEGMENTER.segment(text))
121
+ for (const cluster of segmentGraphemes(text))
130
122
  total += charWidth(cluster.segment);
131
123
  return total;
132
124
  }
125
+ /** Split text into grapheme-safe chunks without rescanning any suffix. */
126
+ export function splitByCells(text, firstCols, followingCols = firstCols) {
127
+ if (text === "")
128
+ return [{ text, width: 0 }];
129
+ const chunks = [];
130
+ let start = 0;
131
+ let used = 0;
132
+ let room = Math.max(1, firstCols);
133
+ for (const { index, segment } of segmentGraphemes(text)) {
134
+ const width = charWidth(segment);
135
+ if (index > start && used + width > room) {
136
+ chunks.push({ text: text.slice(start, index), width: used });
137
+ start = index;
138
+ used = 0;
139
+ room = Math.max(1, followingCols);
140
+ }
141
+ used += width;
142
+ }
143
+ chunks.push({ text: text.slice(start), width: used });
144
+ return chunks;
145
+ }
133
146
  /** The longest prefix of `text` that fits in `cols` cells. */
134
147
  export function clip(text, cols) {
135
148
  if (cols <= 0)
136
149
  return "";
137
150
  let out = "";
138
151
  let used = 0;
139
- for (const { segment } of SEGMENTER.segment(text)) {
152
+ for (const { segment } of segmentGraphemes(text)) {
140
153
  const w = charWidth(segment);
141
154
  if (used + w > cols)
142
155
  break;
@@ -214,17 +227,13 @@ export function wrapText(text, max, continuation = "") {
214
227
  flush();
215
228
  if (w > room()) {
216
229
  // Too long for any row: spend whole rows on it until it fits.
217
- let rest = word;
218
- while (textWidth(rest) > room()) {
219
- const head = clip(rest, room() - (line === "" ? 0 : width + 1));
220
- if (head === "")
221
- break;
222
- line = line === "" ? head : `${line} ${head}`;
223
- rest = rest.slice(head.length);
224
- flush();
230
+ const chunks = splitByCells(word, room(), Math.max(1, max - lead));
231
+ for (const [index, chunk] of chunks.entries()) {
232
+ line = chunk.text;
233
+ width = chunk.width;
234
+ if (index + 1 < chunks.length)
235
+ flush();
225
236
  }
226
- line = rest;
227
- width = textWidth(rest);
228
237
  continue;
229
238
  }
230
239
  line = line === "" ? word : `${line} ${word}`;
package/dist/usage.js CHANGED
@@ -12,9 +12,12 @@ export function emptyUsage() {
12
12
  }
13
13
  export function recordUsage(total, next) {
14
14
  total.requests += 1;
15
- total.lastInputTokens = next.inputTokens;
16
15
  addUsage(total, next);
17
16
  }
17
+ /** Replace context pressure without inventing provider-reported usage. */
18
+ export function recordRequestInput(total, inputTokens) {
19
+ total.lastInputTokens = inputTokens;
20
+ }
18
21
  /** Account for an internal request without replacing the main context signal. */
19
22
  export function recordAuxiliaryUsage(total, next) {
20
23
  total.requests += 1;
@@ -32,6 +35,7 @@ export function usageFromHistory(messages) {
32
35
  for (const message of messages) {
33
36
  if (message.role === "assistant" && message.usage !== undefined) {
34
37
  recordUsage(total, message.usage);
38
+ recordRequestInput(total, message.usage.inputTokens);
35
39
  }
36
40
  }
37
41
  return total;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giovannijecha/jecode",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "An owned coding agent with zero external runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -45,6 +45,7 @@
45
45
  "start": "node src/main.ts",
46
46
  "tui:lab": "node dev/tui-lab.ts",
47
47
  "bench:transcript": "node dev/benchmark-transcript.ts",
48
+ "bench:search": "node dev/benchmark-search.ts",
48
49
  "typecheck": "tsc --noEmit",
49
50
  "test": "npm run build:release && node --test",
50
51
  "coverage": "npm run build:release && node --test --experimental-test-coverage --test-coverage-include=\"src/**/*.ts\" --test-coverage-lines=80 --test-coverage-branches=75 --test-coverage-functions=75",