@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.
- package/README.md +87 -23
- package/dist/atomic.js +14 -0
- package/dist/batch.js +26 -19
- package/dist/cli-info.js +1 -1
- package/dist/config.js +3 -2
- package/dist/context/budget.js +37 -0
- package/dist/context/compactor.js +9 -2
- package/dist/context/estimate.js +31 -0
- package/dist/context/policy.js +24 -14
- package/dist/controller-request.js +27 -4
- package/dist/controller.js +6 -4
- package/dist/conversation.js +48 -31
- package/dist/oauth-http.js +2 -1
- package/dist/openai-oauth-callback.js +2 -1
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers/http.js +8 -5
- package/dist/providers/ollama.js +1 -1
- package/dist/providers/openai-codex.js +1 -3
- package/dist/providers/openai.js +1 -1
- package/dist/providers/sse.js +117 -40
- package/dist/providers/stream-limits.js +14 -2
- package/dist/sessions/store.js +2 -1
- package/dist/text-boundary.js +47 -0
- package/dist/timeline.js +2 -1
- package/dist/tools/fs.js +85 -38
- package/dist/tools/search.js +106 -44
- package/dist/tools/shell.js +14 -6
- package/dist/tools/text-boundary.js +7 -25
- package/dist/tui/app-state.js +0 -1
- package/dist/tui/app-workflows.js +40 -30
- package/dist/tui/app.js +12 -14
- package/dist/tui/blocks.js +0 -2
- package/dist/tui/components/messages.js +2 -5
- package/dist/tui/components/tool.js +11 -10
- package/dist/tui/session-view.js +2 -1
- package/dist/tui/transcript-view.js +178 -106
- package/dist/tui/turn.js +29 -8
- package/dist/tui/view.js +8 -3
- package/dist/ui/diff.js +51 -16
- package/dist/ui/render.js +16 -24
- package/dist/ui/width.js +31 -22
- package/dist/usage.js +5 -1
- 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.
|
|
12
|
-
*
|
|
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
|
-
|
|
19
|
-
|
|
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(
|
|
22
|
-
const rows = [];
|
|
35
|
+
const table = common(middleA, middleB);
|
|
23
36
|
let i = 0;
|
|
24
37
|
let j = 0;
|
|
25
|
-
const width =
|
|
26
|
-
while (i <
|
|
27
|
-
if (
|
|
28
|
-
rows.push(
|
|
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(
|
|
49
|
+
rows.push(del(middleA[i]));
|
|
37
50
|
i++;
|
|
38
51
|
}
|
|
39
52
|
else {
|
|
40
|
-
rows.push(add(
|
|
53
|
+
rows.push(add(middleB[j]));
|
|
41
54
|
j++;
|
|
42
55
|
}
|
|
43
56
|
}
|
|
44
|
-
while (i <
|
|
45
|
-
rows.push(del(
|
|
46
|
-
while (j <
|
|
47
|
-
rows.push(add(
|
|
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 +=
|
|
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
|
-
|
|
195
|
-
if (used + space +
|
|
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
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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 (
|
|
212
|
-
add(
|
|
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
|
|
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
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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.
|
|
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",
|