@giovannijecha/jecode 0.8.1 → 0.8.3
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 +19 -278
- package/dist/accounts.js +17 -13
- package/dist/batch.js +54 -6
- package/dist/context/budget.js +28 -4
- package/dist/context/compactor.js +5 -4
- package/dist/context/estimate.js +43 -1
- package/dist/context/manual.js +8 -4
- package/dist/context/policy.js +68 -18
- package/dist/controller-request.js +23 -15
- package/dist/controller.js +26 -5
- package/dist/conversation.js +94 -33
- package/dist/credential-safety.js +56 -9
- package/dist/credentials.js +32 -4
- package/dist/input-boundary.js +80 -0
- package/dist/main.js +4 -1
- package/dist/openai-oauth-callback.js +1 -1
- package/dist/openai-oauth.js +59 -15
- package/dist/process-shutdown.js +52 -0
- package/dist/providers/anthropic-stream.js +24 -20
- package/dist/providers/anthropic-wire.js +7 -2
- package/dist/providers/ollama-wire.js +7 -15
- package/dist/providers/ollama.js +27 -10
- package/dist/providers/openai-wire.js +2 -16
- package/dist/providers/tool-input.js +17 -0
- package/dist/sessions/codec.js +2 -1
- package/dist/sessions/lease.js +7 -0
- package/dist/sessions/runtime.js +11 -3
- package/dist/sessions/store.js +90 -31
- package/dist/settings.js +10 -5
- package/dist/start.js +12 -2
- package/dist/text-boundary.js +2 -0
- package/dist/tui/app-input.js +40 -5
- package/dist/tui/app-state.js +1 -0
- package/dist/tui/app-workflows.js +1 -0
- package/dist/tui/app.js +11 -3
- package/dist/tui/blocks.js +1 -3
- package/dist/tui/components/messages.js +9 -13
- package/dist/tui/components/tool.js +2 -4
- package/dist/tui/editor.js +2 -0
- package/dist/tui/keys.js +64 -5
- package/dist/tui/overlay.js +12 -4
- package/dist/tui/picker.js +2 -0
- package/dist/tui/screen.js +5 -17
- package/dist/tui/transcript-grammar.js +1 -1
- package/dist/ui/theme.js +18 -18
- package/dist/user-store.js +54 -0
- package/package.json +6 -3
- /package/{docs/assets/brand → assets}/jeco-256.png +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Translation between the normalized vocabulary and the OpenAI Responses wire
|
|
2
2
|
// shape: a flat `input` list where tool calls and their outputs are top-level
|
|
3
3
|
// items keyed by `call_id`, rather than blocks nested inside a message.
|
|
4
|
+
import { toolInputFromJson } from "./tool-input.js";
|
|
4
5
|
import { wireTokenCount } from "./wire-usage.js";
|
|
5
6
|
export function toWireTool(tool) {
|
|
6
7
|
return {
|
|
@@ -73,7 +74,7 @@ export function fromWireResponse(data, providerId = "openai") {
|
|
|
73
74
|
kind: "tool_call",
|
|
74
75
|
id: item.call_id,
|
|
75
76
|
name: item.name,
|
|
76
|
-
|
|
77
|
+
...toolInputFromJson(item.arguments),
|
|
77
78
|
});
|
|
78
79
|
}
|
|
79
80
|
else {
|
|
@@ -106,18 +107,3 @@ function normalizeUsage(data) {
|
|
|
106
107
|
reasoningTokens: wireTokenCount(usage.output_tokens_details?.reasoning_tokens),
|
|
107
108
|
};
|
|
108
109
|
}
|
|
109
|
-
// Arguments arrive as a JSON string and models vary in how they escape it, so
|
|
110
|
-
// this always goes through a real parse — never string matching.
|
|
111
|
-
function parseArguments(text) {
|
|
112
|
-
if (text === undefined || text === "")
|
|
113
|
-
return {};
|
|
114
|
-
try {
|
|
115
|
-
const parsed = JSON.parse(text);
|
|
116
|
-
return typeof parsed === "object" && parsed !== null
|
|
117
|
-
? parsed
|
|
118
|
-
: {};
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
return {};
|
|
122
|
-
}
|
|
123
|
-
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Provider-neutral validation for tool arguments crossing a wire boundary.
|
|
2
|
+
export function toolInputFromJson(text) {
|
|
3
|
+
if (text === undefined || text.trim() === "")
|
|
4
|
+
return { input: {} };
|
|
5
|
+
try {
|
|
6
|
+
return toolInputFromValue(JSON.parse(text));
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return { input: {}, inputError: "tool arguments were not valid JSON" };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function toolInputFromValue(value) {
|
|
13
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
14
|
+
return { input: value };
|
|
15
|
+
}
|
|
16
|
+
return { input: {}, inputError: "tool arguments must be a JSON object" };
|
|
17
|
+
}
|
package/dist/sessions/codec.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
// can become conversation or provider input.
|
|
4
4
|
import { Buffer } from "node:buffer";
|
|
5
5
|
import { CONTEXT_LIMITS } from "../context/projection.js";
|
|
6
|
+
import { MAX_TEXT_CODE_UNITS } from "../text-boundary.js";
|
|
6
7
|
export const SESSION_SCHEMA = 4;
|
|
7
8
|
export const SESSION_FILE_LIMITS = Object.freeze({
|
|
8
|
-
text:
|
|
9
|
+
text: MAX_TEXT_CODE_UNITS,
|
|
9
10
|
metadataBytes: 64 * 1_024,
|
|
10
11
|
nodeBytes: 20 * 1_024 * 1_024,
|
|
11
12
|
jsonDepth: 24,
|
package/dist/sessions/lease.js
CHANGED
|
@@ -9,6 +9,13 @@ export function sessionLease(id, file, token) {
|
|
|
9
9
|
let closed = false;
|
|
10
10
|
return Object.freeze({
|
|
11
11
|
id,
|
|
12
|
+
assertOwned: async () => {
|
|
13
|
+
if (closed)
|
|
14
|
+
throw new Error("session lease is closed");
|
|
15
|
+
const current = await readLease(file);
|
|
16
|
+
if (current !== token)
|
|
17
|
+
throw new Error("session lease is no longer owned by this process");
|
|
18
|
+
},
|
|
12
19
|
close: async () => {
|
|
13
20
|
if (closed)
|
|
14
21
|
return;
|
package/dist/sessions/runtime.js
CHANGED
|
@@ -8,11 +8,13 @@ export class SessionPersistence {
|
|
|
8
8
|
#store;
|
|
9
9
|
#sessionId;
|
|
10
10
|
#lease;
|
|
11
|
+
#snapshot;
|
|
11
12
|
#failure;
|
|
12
|
-
constructor(store, sessionId, lease) {
|
|
13
|
+
constructor(store, sessionId, lease, snapshot) {
|
|
13
14
|
this.#store = store;
|
|
14
15
|
this.#sessionId = sessionId;
|
|
15
16
|
this.#lease = lease;
|
|
17
|
+
this.#snapshot = snapshot;
|
|
16
18
|
}
|
|
17
19
|
static fresh(store) {
|
|
18
20
|
return new SessionPersistence(store, null);
|
|
@@ -26,7 +28,7 @@ export class SessionPersistence {
|
|
|
26
28
|
throw new Error("session has no resumable turn");
|
|
27
29
|
return Object.freeze({
|
|
28
30
|
conversation,
|
|
29
|
-
persistence: new SessionPersistence(store, id, lease),
|
|
31
|
+
persistence: new SessionPersistence(store, id, lease, snapshot),
|
|
30
32
|
});
|
|
31
33
|
}
|
|
32
34
|
catch (error) {
|
|
@@ -51,9 +53,14 @@ export class SessionPersistence {
|
|
|
51
53
|
const published = await this.#store.publish(conversation, true);
|
|
52
54
|
this.#lease = published.lease;
|
|
53
55
|
this.#sessionId = published.meta.id;
|
|
56
|
+
this.#snapshot = published;
|
|
54
57
|
return;
|
|
55
58
|
}
|
|
56
|
-
|
|
59
|
+
if (this.#lease === undefined || this.#snapshot === undefined) {
|
|
60
|
+
throw new Error("session persistence has no verified owner snapshot");
|
|
61
|
+
}
|
|
62
|
+
await this.#lease.assertOwned();
|
|
63
|
+
this.#snapshot = await this.#store.checkpoint(this.#snapshot, conversation);
|
|
57
64
|
}
|
|
58
65
|
catch (error) {
|
|
59
66
|
this.#failure = error;
|
|
@@ -64,6 +71,7 @@ export class SessionPersistence {
|
|
|
64
71
|
await this.#lease?.close();
|
|
65
72
|
this.#lease = undefined;
|
|
66
73
|
this.#sessionId = null;
|
|
74
|
+
this.#snapshot = undefined;
|
|
67
75
|
this.#failure = undefined;
|
|
68
76
|
}
|
|
69
77
|
async close() {
|
package/dist/sessions/store.js
CHANGED
|
@@ -16,6 +16,7 @@ const DIRECTORY_MODE = 0o700;
|
|
|
16
16
|
const FILE_MODE = 0o600;
|
|
17
17
|
const MAX_CATALOG_ENTRIES = 4_096;
|
|
18
18
|
const CATALOG_READ_CONCURRENCY = 8;
|
|
19
|
+
const NODE_READ_CONCURRENCY = 4;
|
|
19
20
|
const SESSION_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
|
|
20
21
|
const NODE_NAME = /^(\d{6})\.json$/;
|
|
21
22
|
const ATOMIC_NODE_TEMP = /^\.\d{6}\.json\.\d+\.[a-f0-9-]+\.tmp$/;
|
|
@@ -104,15 +105,18 @@ export class DurableSessionStore {
|
|
|
104
105
|
if (!replacesHead && !extendsTree) {
|
|
105
106
|
throw new Error("session checkpoint cannot be recovered safely");
|
|
106
107
|
}
|
|
107
|
-
head = {
|
|
108
|
+
head = Object.freeze({
|
|
108
109
|
version: SESSION_SCHEMA,
|
|
109
110
|
sequence: candidate.sequence,
|
|
110
111
|
nodeId: candidate.node.id,
|
|
111
112
|
parentId: candidate.node.parentId,
|
|
112
113
|
revision: candidate.node.revision,
|
|
113
114
|
updatedAt: candidate.updatedAt,
|
|
114
|
-
};
|
|
115
|
-
await atomicWrite(path.join(directory, "head.json"), encodeHead(head), {
|
|
115
|
+
});
|
|
116
|
+
await atomicWrite(path.join(directory, "head.json"), encodeHead(head), {
|
|
117
|
+
mode: FILE_MODE,
|
|
118
|
+
validate: async () => assertDirectory(directory),
|
|
119
|
+
});
|
|
116
120
|
}
|
|
117
121
|
const nodes = stored.map((entry) => entry.node);
|
|
118
122
|
const conversation = ConversationTree.restore(nodes, head.nodeId);
|
|
@@ -128,21 +132,21 @@ export class DurableSessionStore {
|
|
|
128
132
|
const token = claim === true ? leaseToken() : undefined;
|
|
129
133
|
const temporary = path.join(this.#bucket, `.${id}.${randomUUID()}.tmp`);
|
|
130
134
|
const target = this.#sessionDirectory(id);
|
|
131
|
-
const meta = {
|
|
135
|
+
const meta = Object.freeze({
|
|
132
136
|
version: SESSION_SCHEMA,
|
|
133
137
|
id,
|
|
134
138
|
workspaceRoot: this.workspaceRoot,
|
|
135
139
|
workspaceDigest: this.workspaceDigest,
|
|
136
140
|
createdAt: now,
|
|
137
|
-
};
|
|
138
|
-
const head = {
|
|
141
|
+
});
|
|
142
|
+
const head = Object.freeze({
|
|
139
143
|
version: SESSION_SCHEMA,
|
|
140
144
|
sequence: conversation.nodes.length,
|
|
141
145
|
nodeId: active.id,
|
|
142
146
|
parentId: active.parentId,
|
|
143
147
|
revision: active.revision,
|
|
144
148
|
updatedAt: now,
|
|
145
|
-
};
|
|
149
|
+
});
|
|
146
150
|
try {
|
|
147
151
|
await makePrivateDirectory(temporary);
|
|
148
152
|
const nodes = path.join(temporary, "nodes");
|
|
@@ -168,12 +172,26 @@ export class DurableSessionStore {
|
|
|
168
172
|
const lease = sessionLease(id, path.join(target, "active"), token);
|
|
169
173
|
return Object.freeze({ ...snapshot, lease });
|
|
170
174
|
}
|
|
171
|
-
async checkpoint(
|
|
172
|
-
|
|
175
|
+
async checkpoint(previous, conversation) {
|
|
176
|
+
assertSnapshot(previous, this.workspaceRoot, this.workspaceDigest);
|
|
177
|
+
const id = previous.meta.id;
|
|
178
|
+
const directory = this.#sessionDirectory(id);
|
|
179
|
+
await assertDirectory(directory);
|
|
180
|
+
const nodesDirectory = path.join(directory, "nodes");
|
|
181
|
+
const validateNodesDirectory = async () => {
|
|
182
|
+
await assertDirectory(directory);
|
|
183
|
+
await assertDirectory(nodesDirectory);
|
|
184
|
+
};
|
|
185
|
+
await validateNodesDirectory();
|
|
186
|
+
const currentHead = decodeHead(await readJson(path.join(directory, "head.json"), SESSION_FILE_LIMITS.metadataBytes));
|
|
187
|
+
if (!sameHead(currentHead, previous.head)) {
|
|
188
|
+
throw new Error("session head changed after its verified snapshot");
|
|
189
|
+
}
|
|
173
190
|
const active = conversation.activeNode;
|
|
174
191
|
if (active === undefined)
|
|
175
192
|
throw new Error("an empty conversation cannot be checkpointed");
|
|
176
|
-
if (
|
|
193
|
+
if (active === previous.conversation.activeNode &&
|
|
194
|
+
conversation.nodes === previous.conversation.nodes) {
|
|
177
195
|
return Object.freeze({ ...previous, conversation });
|
|
178
196
|
}
|
|
179
197
|
const replacesHead = active.id === previous.head.nodeId &&
|
|
@@ -186,18 +204,23 @@ export class DurableSessionStore {
|
|
|
186
204
|
throw new Error("session checkpoint does not extend its durable tree");
|
|
187
205
|
}
|
|
188
206
|
assertSharedNodes(previous.conversation, conversation, replacesHead ? active.id : undefined);
|
|
207
|
+
if (extendsTree) {
|
|
208
|
+
await assertMissingNode(path.join(nodesDirectory, nodeName(active.id)));
|
|
209
|
+
}
|
|
189
210
|
const now = new Date().toISOString();
|
|
190
|
-
const head = {
|
|
211
|
+
const head = Object.freeze({
|
|
191
212
|
version: SESSION_SCHEMA,
|
|
192
213
|
sequence: previous.head.sequence + 1,
|
|
193
214
|
nodeId: active.id,
|
|
194
215
|
parentId: active.parentId,
|
|
195
216
|
revision: active.revision,
|
|
196
217
|
updatedAt: now,
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
await atomicWrite(path.join(directory, "
|
|
200
|
-
|
|
218
|
+
});
|
|
219
|
+
await atomicWrite(path.join(nodesDirectory, nodeName(active.id)), encodeNode(active, head.sequence, now), { mode: FILE_MODE, validate: async () => validateNodesDirectory() });
|
|
220
|
+
await atomicWrite(path.join(directory, "head.json"), encodeHead(head), {
|
|
221
|
+
mode: FILE_MODE,
|
|
222
|
+
validate: async () => assertDirectory(directory),
|
|
223
|
+
});
|
|
201
224
|
return Object.freeze({ meta: previous.meta, head, conversation });
|
|
202
225
|
}
|
|
203
226
|
async claim(id) {
|
|
@@ -270,17 +293,44 @@ function assertSharedNodes(previous, next, replacedId) {
|
|
|
270
293
|
if (node.id === replacedId)
|
|
271
294
|
continue;
|
|
272
295
|
const candidate = next.node(node.id);
|
|
273
|
-
if (candidate
|
|
296
|
+
if (candidate !== node) {
|
|
274
297
|
throw new Error("session checkpoint rewrites prior conversation history");
|
|
275
298
|
}
|
|
276
299
|
}
|
|
277
300
|
}
|
|
278
|
-
function
|
|
279
|
-
|
|
280
|
-
|
|
301
|
+
function assertSnapshot(snapshot, workspaceRoot, workspaceDigest) {
|
|
302
|
+
encodeMeta(snapshot.meta);
|
|
303
|
+
encodeHead(snapshot.head);
|
|
304
|
+
assertSessionId(snapshot.meta.id);
|
|
305
|
+
if (snapshot.meta.workspaceDigest !== workspaceDigest ||
|
|
306
|
+
workspaceKey(snapshot.meta.workspaceRoot) !== workspaceKey(workspaceRoot))
|
|
307
|
+
throw new Error("session snapshot belongs to a different workspace");
|
|
308
|
+
const active = snapshot.conversation.activeNode;
|
|
309
|
+
if (active === undefined ||
|
|
310
|
+
active.id !== snapshot.head.nodeId ||
|
|
311
|
+
active.parentId !== snapshot.head.parentId ||
|
|
312
|
+
active.revision !== snapshot.head.revision ||
|
|
313
|
+
snapshot.head.sequence < snapshot.conversation.nodes.length)
|
|
314
|
+
throw new Error("session snapshot does not match its verified head");
|
|
281
315
|
}
|
|
282
|
-
function
|
|
283
|
-
return
|
|
316
|
+
function sameHead(left, right) {
|
|
317
|
+
return left.version === right.version &&
|
|
318
|
+
left.sequence === right.sequence &&
|
|
319
|
+
left.nodeId === right.nodeId &&
|
|
320
|
+
left.parentId === right.parentId &&
|
|
321
|
+
left.revision === right.revision &&
|
|
322
|
+
left.updatedAt === right.updatedAt;
|
|
323
|
+
}
|
|
324
|
+
async function assertMissingNode(file) {
|
|
325
|
+
try {
|
|
326
|
+
await lstat(file);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
if (error.code === "ENOENT")
|
|
330
|
+
return;
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
throw new Error("session has an incomplete node outside its verified snapshot");
|
|
284
334
|
}
|
|
285
335
|
async function readNodes(directory) {
|
|
286
336
|
await assertDirectory(directory);
|
|
@@ -296,17 +346,26 @@ async function readNodes(directory) {
|
|
|
296
346
|
}
|
|
297
347
|
const stored = [];
|
|
298
348
|
const sequences = new Set();
|
|
299
|
-
for (let
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
349
|
+
for (let start = 0; start < names.length; start += NODE_READ_CONCURRENCY) {
|
|
350
|
+
const decoded = await Promise.all(names.slice(start, start + NODE_READ_CONCURRENCY)
|
|
351
|
+
.map(async (name, offset) => {
|
|
352
|
+
const id = Number(NODE_NAME.exec(name)?.[1]);
|
|
353
|
+
if (id !== start + offset + 1) {
|
|
354
|
+
throw new Error("session conversation nodes are not contiguous");
|
|
355
|
+
}
|
|
356
|
+
const entry = decodeNode(await readJson(path.join(directory, name), SESSION_FILE_LIMITS.nodeBytes));
|
|
357
|
+
if (entry.node.id !== id) {
|
|
358
|
+
throw new Error("session conversation node identity is invalid");
|
|
359
|
+
}
|
|
360
|
+
return entry;
|
|
361
|
+
}));
|
|
362
|
+
for (const entry of decoded) {
|
|
363
|
+
if (sequences.has(entry.sequence)) {
|
|
364
|
+
throw new Error("session conversation node identity is invalid");
|
|
365
|
+
}
|
|
366
|
+
sequences.add(entry.sequence);
|
|
367
|
+
stored.push(entry);
|
|
307
368
|
}
|
|
308
|
-
sequences.add(decoded.sequence);
|
|
309
|
-
stored.push(decoded);
|
|
310
369
|
}
|
|
311
370
|
return stored;
|
|
312
371
|
}
|
package/dist/settings.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// Persistent, non-secret defaults for interactive and batch sessions.
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
2
|
import { chmod, mkdir } from "node:fs/promises";
|
|
4
3
|
import * as path from "node:path";
|
|
5
4
|
import { atomicWrite } from "./atomic.js";
|
|
@@ -9,6 +8,7 @@ import { providerNames } from "./providers/index.js";
|
|
|
9
8
|
import { parseOllamaEndpoint } from "./providers/ollama-endpoint.js";
|
|
10
9
|
import { withStoreLock } from "./store-lock.js";
|
|
11
10
|
import { userDataLabel, userDataPath } from "./user-data.js";
|
|
11
|
+
import { assertStoreText, readBoundedJsonSync, USER_STORE_LIMITS } from "./user-store.js";
|
|
12
12
|
export { EFFORTS } from "./effort.js";
|
|
13
13
|
let saved;
|
|
14
14
|
export function readSettings() {
|
|
@@ -24,7 +24,9 @@ export async function updateSettings(patch) {
|
|
|
24
24
|
await chmod(directory, 0o700);
|
|
25
25
|
return withStoreLock(file, async () => {
|
|
26
26
|
const next = normalize({ ...readStore(file), ...patch });
|
|
27
|
-
|
|
27
|
+
const text = `${JSON.stringify(next, null, 2)}\n`;
|
|
28
|
+
assertStoreText(text, USER_STORE_LIMITS.settingsBytes);
|
|
29
|
+
await atomicWrite(file, text, { mode: 0o600 });
|
|
28
30
|
saved = next;
|
|
29
31
|
return file;
|
|
30
32
|
});
|
|
@@ -41,7 +43,7 @@ export function reloadSettings() {
|
|
|
41
43
|
}
|
|
42
44
|
function readStore(file = settingsPath()) {
|
|
43
45
|
try {
|
|
44
|
-
return normalize(
|
|
46
|
+
return normalize(readBoundedJsonSync(file, USER_STORE_LIMITS.settingsBytes));
|
|
45
47
|
}
|
|
46
48
|
catch {
|
|
47
49
|
// Missing, unreadable, and malformed stores all fall back safely. A bad
|
|
@@ -73,14 +75,14 @@ function normalize(value) {
|
|
|
73
75
|
function modelsOf(value, providers) {
|
|
74
76
|
if (!record(value))
|
|
75
77
|
return undefined;
|
|
76
|
-
const models = Object.fromEntries(Object.entries(value).filter((entry) => providers.includes(entry[0]) &&
|
|
78
|
+
const models = Object.fromEntries(Object.entries(value).filter((entry) => providers.includes(entry[0]) && boundedNonempty(entry[1], USER_STORE_LIMITS.model)));
|
|
77
79
|
return Object.keys(models).length === 0 ? undefined : models;
|
|
78
80
|
}
|
|
79
81
|
function member(value, values) {
|
|
80
82
|
return typeof value === "string" && values.includes(value) ? value : undefined;
|
|
81
83
|
}
|
|
82
84
|
function endpoint(value) {
|
|
83
|
-
if (
|
|
85
|
+
if (!boundedNonempty(value, USER_STORE_LIMITS.endpoint))
|
|
84
86
|
return undefined;
|
|
85
87
|
try {
|
|
86
88
|
return parseOllamaEndpoint(value).baseUrl;
|
|
@@ -89,6 +91,9 @@ function endpoint(value) {
|
|
|
89
91
|
return undefined;
|
|
90
92
|
}
|
|
91
93
|
}
|
|
94
|
+
function boundedNonempty(value, max) {
|
|
95
|
+
return typeof value === "string" && value.length <= max && value.trim() !== "";
|
|
96
|
+
}
|
|
92
97
|
function positiveInteger(value) {
|
|
93
98
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
94
99
|
}
|
package/dist/start.js
CHANGED
|
@@ -76,14 +76,24 @@ export async function start(args = process.argv.slice(2), environment = {}) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
try {
|
|
79
|
-
|
|
79
|
+
if (environment.runInteractive === undefined) {
|
|
80
|
+
await runApp(session, transcriptRoot, { shutdownSignal: environment.signal });
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
await environment.runInteractive(session, transcriptRoot, environment.signal);
|
|
84
|
+
}
|
|
80
85
|
}
|
|
81
86
|
finally {
|
|
82
87
|
await session.persistence?.close();
|
|
83
88
|
}
|
|
84
89
|
}
|
|
85
90
|
else {
|
|
86
|
-
|
|
91
|
+
if (environment.runNonInteractive === undefined) {
|
|
92
|
+
await runBatch(session, { signal: environment.signal });
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
await environment.runNonInteractive(session, environment.signal);
|
|
96
|
+
}
|
|
87
97
|
}
|
|
88
98
|
}
|
|
89
99
|
function applyResumedSession(session, conversation, persistence) {
|
package/dist/text-boundary.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Shared grapheme boundaries for every projection of user-visible text.
|
|
2
2
|
const SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
3
|
+
/** Shared persisted-text and user-prompt boundary, measured like String.length. */
|
|
4
|
+
export const MAX_TEXT_CODE_UNITS = 1_048_576;
|
|
3
5
|
export function segmentGraphemes(text) {
|
|
4
6
|
return SEGMENTER.segment(text);
|
|
5
7
|
}
|
package/dist/tui/app-input.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Keyboard and pointer intent for the TUI shell.
|
|
2
|
+
import { PROMPT_LIMIT_MESSAGE, PromptLimitError } from "../input-boundary.js";
|
|
2
3
|
import { activate as activateCompletion, move as moveCompletion, selected as selectedCompletion, } from "./complete.js";
|
|
3
4
|
import * as edit from "./editor.js";
|
|
4
5
|
import { turnBlocker } from "./feedback.js";
|
|
@@ -21,6 +22,13 @@ export function appInput(options) {
|
|
|
21
22
|
}
|
|
22
23
|
if (state.feedback !== undefined)
|
|
23
24
|
feedback.dismiss();
|
|
25
|
+
if (key.name === "input_limit") {
|
|
26
|
+
if (state.open === undefined)
|
|
27
|
+
rejectPrompt();
|
|
28
|
+
else
|
|
29
|
+
showInputLimit();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
24
32
|
// Detail expansion remains available while an approval is open. A large
|
|
25
33
|
// diff may be compacted, but the user must be able to inspect it before
|
|
26
34
|
// answering the permission prompt.
|
|
@@ -33,6 +41,8 @@ export function appInput(options) {
|
|
|
33
41
|
if (state.open !== undefined) {
|
|
34
42
|
const outcome = overlay.handle(state.open, key);
|
|
35
43
|
state.open = outcome.open;
|
|
44
|
+
if (outcome.inputLimit === true)
|
|
45
|
+
showInputLimit();
|
|
36
46
|
if (outcome.abort === true)
|
|
37
47
|
state.activity?.control.abort(new Error("interrupted"));
|
|
38
48
|
if (outcome.quit === true)
|
|
@@ -61,8 +71,10 @@ export function appInput(options) {
|
|
|
61
71
|
case "enter": {
|
|
62
72
|
if (state.completing !== undefined) {
|
|
63
73
|
const completed = selectedCompletion(state.completing);
|
|
64
|
-
if (completed !== undefined)
|
|
74
|
+
if (completed !== undefined) {
|
|
65
75
|
state.editor = edit.of(completed);
|
|
76
|
+
state.promptRejected = false;
|
|
77
|
+
}
|
|
66
78
|
state.completing = undefined;
|
|
67
79
|
}
|
|
68
80
|
submit();
|
|
@@ -75,6 +87,7 @@ export function appInput(options) {
|
|
|
75
87
|
const completed = completion === undefined ? undefined : selectedCompletion(completion);
|
|
76
88
|
if (completed !== undefined) {
|
|
77
89
|
state.editor = edit.of(completed);
|
|
90
|
+
state.promptRejected = false;
|
|
78
91
|
state.completing = undefined;
|
|
79
92
|
}
|
|
80
93
|
return;
|
|
@@ -106,16 +119,26 @@ export function appInput(options) {
|
|
|
106
119
|
options.invalidate();
|
|
107
120
|
return;
|
|
108
121
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
try {
|
|
123
|
+
const edited = applyKey(state.editor, key);
|
|
124
|
+
if (edited !== undefined) {
|
|
125
|
+
if (edited.text !== state.editor.text)
|
|
126
|
+
state.promptRejected = false;
|
|
127
|
+
state.editor = edited;
|
|
128
|
+
state.completing = state.activity === undefined ? activateCompletion(edited.text) : undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (!(error instanceof PromptLimitError))
|
|
133
|
+
throw error;
|
|
134
|
+
rejectPrompt();
|
|
113
135
|
}
|
|
114
136
|
}
|
|
115
137
|
function recall(step) {
|
|
116
138
|
if (state.past.length === 0)
|
|
117
139
|
return;
|
|
118
140
|
state.completing = undefined;
|
|
141
|
+
state.promptRejected = false;
|
|
119
142
|
if (state.recall === -1) {
|
|
120
143
|
if (step > 0)
|
|
121
144
|
return;
|
|
@@ -132,6 +155,10 @@ export function appInput(options) {
|
|
|
132
155
|
state.editor = edit.of(state.past[state.recall]);
|
|
133
156
|
}
|
|
134
157
|
function submit() {
|
|
158
|
+
if (state.promptRejected) {
|
|
159
|
+
keep(PROMPT_LIMIT_MESSAGE);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
135
162
|
const text = state.editor.text.trim();
|
|
136
163
|
if (text === "")
|
|
137
164
|
return;
|
|
@@ -171,6 +198,7 @@ export function appInput(options) {
|
|
|
171
198
|
state.recall = -1;
|
|
172
199
|
state.draft = "";
|
|
173
200
|
state.completing = undefined;
|
|
201
|
+
state.promptRejected = false;
|
|
174
202
|
state.past.push(text);
|
|
175
203
|
state.scroll = 0;
|
|
176
204
|
state.follow = true;
|
|
@@ -179,5 +207,12 @@ export function appInput(options) {
|
|
|
179
207
|
function keep(text) {
|
|
180
208
|
feedback.show({ text: `${text} · prompt kept`, tone: "warn", timeoutMs: 4_000 });
|
|
181
209
|
}
|
|
210
|
+
function rejectPrompt() {
|
|
211
|
+
state.promptRejected = true;
|
|
212
|
+
keep(PROMPT_LIMIT_MESSAGE);
|
|
213
|
+
}
|
|
214
|
+
function showInputLimit() {
|
|
215
|
+
feedback.show({ text: PROMPT_LIMIT_MESSAGE, tone: "warn", timeoutMs: 4_000 });
|
|
216
|
+
}
|
|
182
217
|
return { handle };
|
|
183
218
|
}
|
package/dist/tui/app-state.js
CHANGED
|
@@ -212,6 +212,7 @@ export function appWorkflows(options) {
|
|
|
212
212
|
nodeId: nodeId ?? prospectiveNodeId,
|
|
213
213
|
coveredMessages: context?.messageCount ?? 0,
|
|
214
214
|
lastInputTokens: Math.max(session.usage.lastInputTokens, request.inputTokens),
|
|
215
|
+
estimatedInputTokens: request.inputTokens,
|
|
215
216
|
signal: activity.control.signal,
|
|
216
217
|
force,
|
|
217
218
|
policy: request.policy,
|
package/dist/tui/app.js
CHANGED
|
@@ -42,6 +42,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
42
42
|
let escapeTimer;
|
|
43
43
|
let stopResize = () => { };
|
|
44
44
|
let stopInput = () => { };
|
|
45
|
+
let stopShutdown = () => { };
|
|
45
46
|
let failure;
|
|
46
47
|
let activeWorkflow;
|
|
47
48
|
// Timers outlive the teardown they were scheduled before. Painting after the
|
|
@@ -158,6 +159,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
158
159
|
live = false;
|
|
159
160
|
safely(stopInput);
|
|
160
161
|
safely(stopResize);
|
|
162
|
+
safely(stopShutdown);
|
|
161
163
|
if (activityTimer !== undefined)
|
|
162
164
|
clearInterval(activityTimer);
|
|
163
165
|
if (frameTimer !== undefined)
|
|
@@ -194,15 +196,15 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
194
196
|
activeWorkflow = tracked;
|
|
195
197
|
return tracked;
|
|
196
198
|
}
|
|
197
|
-
function requestQuit() {
|
|
199
|
+
function requestQuit(reason = new Error("interrupted")) {
|
|
200
|
+
state.open = overlay.cancel(state.open);
|
|
198
201
|
const activity = state.activity;
|
|
199
202
|
if (activity === undefined) {
|
|
200
203
|
quit();
|
|
201
204
|
return;
|
|
202
205
|
}
|
|
203
206
|
state.closeWhenIdle = true;
|
|
204
|
-
|
|
205
|
-
activity.control.abort(new Error("interrupted"));
|
|
207
|
+
activity.control.abort(reason);
|
|
206
208
|
}
|
|
207
209
|
function startActivity(kind, label) {
|
|
208
210
|
if (state.activity !== undefined)
|
|
@@ -305,6 +307,12 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
305
307
|
}
|
|
306
308
|
}
|
|
307
309
|
try {
|
|
310
|
+
const shutdownSignal = environment.shutdownSignal;
|
|
311
|
+
const onShutdown = () => requestQuit(shutdownSignal?.reason);
|
|
312
|
+
if (shutdownSignal?.aborted === true)
|
|
313
|
+
return;
|
|
314
|
+
shutdownSignal?.addEventListener("abort", onShutdown, { once: true });
|
|
315
|
+
stopShutdown = () => shutdownSignal?.removeEventListener("abort", onShutdown);
|
|
308
316
|
terminal.enter(session.config.reducedMotion);
|
|
309
317
|
stopResize = terminal.onResize(() => guard(() => {
|
|
310
318
|
paint.invalidate();
|
package/dist/tui/blocks.js
CHANGED
|
@@ -11,12 +11,10 @@ export function render(block, width, pal, context = {}) {
|
|
|
11
11
|
case "reasoning":
|
|
12
12
|
return renderReasoning(block, width, pal, {
|
|
13
13
|
continues: context.previous?.kind === "reasoning",
|
|
14
|
-
followsTool: context.previous?.kind === "tool",
|
|
15
14
|
});
|
|
16
15
|
case "tool":
|
|
17
16
|
return renderTool(block, width, pal, {
|
|
18
|
-
continues: context.previous?.kind === "tool"
|
|
19
|
-
followsReasoning: context.previous?.kind === "reasoning",
|
|
17
|
+
continues: context.previous?.kind === "tool",
|
|
20
18
|
now: context.now,
|
|
21
19
|
motion: context.motion,
|
|
22
20
|
reducedMotion: context.reducedMotion,
|