@giovannijecha/jecode 0.8.0 → 0.8.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.
- package/dist/context/budget.js +15 -3
- package/dist/controller-request.js +19 -11
- package/dist/controller.js +20 -4
- package/dist/openai-oauth.js +59 -15
- package/dist/providers/ollama.js +26 -10
- package/dist/sessions/store.js +20 -10
- package/dist/tui/blocks.js +2 -3
- package/dist/tui/components/messages.js +8 -11
- package/dist/tui/components/tool.js +11 -13
- package/dist/tui/transcript-grammar.js +7 -11
- package/dist/ui/inline.js +7 -7
- package/dist/ui/theme.js +18 -18
- package/package.json +1 -1
package/dist/context/budget.js
CHANGED
|
@@ -18,10 +18,17 @@ export function estimateRequestInputTokens(envelope) {
|
|
|
18
18
|
}
|
|
19
19
|
/** Clamp the configured output ceiling so the complete request remains usable. */
|
|
20
20
|
export function budgetRequest(envelope, configuredMaxOutputTokens, policy) {
|
|
21
|
-
|
|
22
|
-
throw new Error("max output tokens must be a positive safe integer");
|
|
23
|
-
}
|
|
21
|
+
requirePositiveInteger(configuredMaxOutputTokens, "max output tokens");
|
|
24
22
|
const inputTokens = estimateRequestInputTokens(envelope);
|
|
23
|
+
return finishBudget(inputTokens, configuredMaxOutputTokens, policy);
|
|
24
|
+
}
|
|
25
|
+
/** Reuse an exact estimate already computed for the same request envelope. */
|
|
26
|
+
export function budgetRequestFromInputTokens(inputTokens, configuredMaxOutputTokens, policy) {
|
|
27
|
+
requirePositiveInteger(inputTokens, "request input tokens");
|
|
28
|
+
requirePositiveInteger(configuredMaxOutputTokens, "max output tokens");
|
|
29
|
+
return finishBudget(inputTokens, configuredMaxOutputTokens, policy);
|
|
30
|
+
}
|
|
31
|
+
function finishBudget(inputTokens, configuredMaxOutputTokens, policy) {
|
|
25
32
|
const available = policy.requestLimitTokens - inputTokens;
|
|
26
33
|
const minimum = Math.min(configuredMaxOutputTokens, MIN_REQUEST_OUTPUT_TOKENS);
|
|
27
34
|
if (available < minimum) {
|
|
@@ -35,3 +42,8 @@ export function budgetRequest(envelope, configuredMaxOutputTokens, policy) {
|
|
|
35
42
|
limitTokens: policy.requestLimitTokens,
|
|
36
43
|
});
|
|
37
44
|
}
|
|
45
|
+
function requirePositiveInteger(value, label) {
|
|
46
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
47
|
+
throw new Error(`${label} must be a positive safe integer`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
// One streamed provider request with a single safe context-overflow recovery.
|
|
2
|
-
import {
|
|
2
|
+
import { budgetRequestFromInputTokens, estimateRequestInputTokens, } from "./context/budget.js";
|
|
3
3
|
import { isContextOverflow } from "./context/policy.js";
|
|
4
4
|
export async function requestAssistant(history, current, specs, options, events, signal) {
|
|
5
5
|
let policy = await options.contextPolicy();
|
|
6
6
|
const prepared = await prepareContext(history, current, specs, options, events, policy, "budget");
|
|
7
|
-
let context = prepared === undefined ? [...current] : clone(prepared);
|
|
7
|
+
let context = prepared.projected === undefined ? [...current] : clone(prepared.projected);
|
|
8
|
+
let inputTokens = prepared.inputTokens;
|
|
8
9
|
let recovered = false;
|
|
9
10
|
for (;;) {
|
|
10
|
-
const budget =
|
|
11
|
-
system: options.system,
|
|
12
|
-
messages: context,
|
|
13
|
-
tools: specs,
|
|
14
|
-
}, options.maxTokens, policy);
|
|
11
|
+
const budget = budgetRequestFromInputTokens(inputTokens, options.maxTokens, policy);
|
|
15
12
|
try {
|
|
16
13
|
const message = await options.provider.send({
|
|
17
14
|
model: options.model,
|
|
@@ -31,10 +28,11 @@ export async function requestAssistant(history, current, specs, options, events,
|
|
|
31
28
|
throw error;
|
|
32
29
|
if (isContextOverflow(error))
|
|
33
30
|
policy = await options.contextPolicy();
|
|
34
|
-
const
|
|
35
|
-
if (projected === undefined)
|
|
31
|
+
const next = await prepareContext(history, context, specs, options, events, policy, "overflow", error);
|
|
32
|
+
if (next.projected === undefined)
|
|
36
33
|
throw error;
|
|
37
|
-
context = clone(projected);
|
|
34
|
+
context = clone(next.projected);
|
|
35
|
+
inputTokens = next.inputTokens;
|
|
38
36
|
recovered = true;
|
|
39
37
|
}
|
|
40
38
|
}
|
|
@@ -45,12 +43,22 @@ async function prepareContext(history, context, specs, options, events, policy,
|
|
|
45
43
|
messages: context,
|
|
46
44
|
tools: specs,
|
|
47
45
|
});
|
|
48
|
-
|
|
46
|
+
const projected = await events.onContext?.(history, context, {
|
|
49
47
|
reason,
|
|
50
48
|
policy,
|
|
51
49
|
inputTokens,
|
|
52
50
|
...(error === undefined ? {} : { error }),
|
|
53
51
|
});
|
|
52
|
+
return {
|
|
53
|
+
projected,
|
|
54
|
+
inputTokens: projected === undefined
|
|
55
|
+
? inputTokens
|
|
56
|
+
: estimateRequestInputTokens({
|
|
57
|
+
system: options.system,
|
|
58
|
+
messages: projected,
|
|
59
|
+
tools: specs,
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
54
62
|
}
|
|
55
63
|
function clone(messages) {
|
|
56
64
|
return structuredClone([...messages]);
|
package/dist/controller.js
CHANGED
|
@@ -97,13 +97,29 @@ export async function runTurn(history, options, events, signal, modelHistory = h
|
|
|
97
97
|
events.onToolCall(call, preview);
|
|
98
98
|
prepared.push({ call, current, preview });
|
|
99
99
|
}
|
|
100
|
-
const
|
|
101
|
-
|
|
100
|
+
const settlements = await Promise.allSettled(prepared.map(({ call, current, preview }) => settle(call, current, calls.length, options, events, signal, preview)));
|
|
101
|
+
let batchFailure;
|
|
102
|
+
for (let offset = 0; offset < settlements.length; offset++) {
|
|
102
103
|
const call = prepared[offset]?.call;
|
|
103
|
-
const
|
|
104
|
+
const settlement = settlements[offset];
|
|
105
|
+
const run = settlement.status === "fulfilled"
|
|
106
|
+
? settlement.value
|
|
107
|
+
: refuse(call, signal?.aborted === true
|
|
108
|
+
? "interrupted before completion"
|
|
109
|
+
: "tool processing stopped before completion", signal?.aborted === true ? "interrupted" : "failed");
|
|
110
|
+
if (settlement.status === "rejected" && batchFailure === undefined) {
|
|
111
|
+
batchFailure = { error: settlement.reason };
|
|
112
|
+
}
|
|
104
113
|
results.push(run.result);
|
|
105
|
-
|
|
114
|
+
try {
|
|
115
|
+
events.onToolResult(call, run.result, run.summary);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
batchFailure ??= { error };
|
|
119
|
+
}
|
|
106
120
|
}
|
|
121
|
+
if (batchFailure !== undefined)
|
|
122
|
+
throw batchFailure.error;
|
|
107
123
|
}
|
|
108
124
|
}
|
|
109
125
|
catch (error) {
|
package/dist/openai-oauth.js
CHANGED
|
@@ -17,6 +17,8 @@ const DEVICE_POLL = `${AUTHORITY}/api/accounts/deviceauth/token`;
|
|
|
17
17
|
const DEVICE_VERIFY = `${AUTHORITY}/codex/device`;
|
|
18
18
|
const DEVICE_REDIRECT = `${AUTHORITY}/deviceauth/callback`;
|
|
19
19
|
const LOGIN_LIMIT_MS = 15 * 60_000;
|
|
20
|
+
/** RFC 8628 increases the polling interval by five seconds after `slow_down`. */
|
|
21
|
+
const SLOW_DOWN_INCREMENT_MS = 5_000;
|
|
20
22
|
export async function beginBrowserLogin() {
|
|
21
23
|
const verifier = randomBytes(64).toString("base64url");
|
|
22
24
|
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
@@ -121,23 +123,65 @@ async function exchange(code, signal) {
|
|
|
121
123
|
return openAITokenReply(response.value);
|
|
122
124
|
}
|
|
123
125
|
async function pollDevice(deviceAuthId, userCode, interval, signal) {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
126
|
+
const deadline = new AbortController();
|
|
127
|
+
const timer = setTimeout(() => {
|
|
128
|
+
deadline.abort(new Error("ChatGPT device sign-in timed out after 15 minutes"));
|
|
129
|
+
}, LOGIN_LIMIT_MS);
|
|
130
|
+
const combined = signal === undefined
|
|
131
|
+
? deadline.signal
|
|
132
|
+
: AbortSignal.any([signal, deadline.signal]);
|
|
133
|
+
let intervalMs = interval * 1_000;
|
|
134
|
+
try {
|
|
135
|
+
while (true) {
|
|
136
|
+
const response = await oauthRequest(DEVICE_POLL, {
|
|
137
|
+
contentType: "application/json",
|
|
138
|
+
value: { device_auth_id: deviceAuthId, user_code: userCode },
|
|
139
|
+
}, combined, [200, 400, 403, 404, 429]);
|
|
140
|
+
if (combined.aborted)
|
|
141
|
+
throw abortReason(combined);
|
|
142
|
+
if (response.status === 200) {
|
|
143
|
+
const value = record(response.value) ? response.value : {};
|
|
144
|
+
return {
|
|
145
|
+
authorizationCode: required(value["authorization_code"], "authorization code"),
|
|
146
|
+
verifier: required(value["code_verifier"], "code verifier"),
|
|
147
|
+
redirectUri: DEVICE_REDIRECT,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const errorCode = deviceErrorCode(response.value);
|
|
151
|
+
if (errorCode === "access_denied") {
|
|
152
|
+
throw new Error("ChatGPT device sign-in was denied");
|
|
153
|
+
}
|
|
154
|
+
if (errorCode === "expired_token") {
|
|
155
|
+
throw new Error("ChatGPT device sign-in code expired");
|
|
156
|
+
}
|
|
157
|
+
const pending = errorCode === "authorization_pending" ||
|
|
158
|
+
errorCode === "deviceauth_authorization_pending" ||
|
|
159
|
+
((response.status === 403 || response.status === 404) && errorCode === undefined);
|
|
160
|
+
const slowDown = errorCode === "slow_down" ||
|
|
161
|
+
(response.status === 429 && errorCode === undefined);
|
|
162
|
+
if (!pending && !slowDown) {
|
|
163
|
+
throw new Error(`ChatGPT device sign-in failed (${response.status})`);
|
|
164
|
+
}
|
|
165
|
+
if (slowDown)
|
|
166
|
+
intervalMs += SLOW_DOWN_INCREMENT_MS;
|
|
167
|
+
await sleep(intervalMs, combined);
|
|
137
168
|
}
|
|
138
|
-
await sleep(interval * 1_000, signal);
|
|
139
169
|
}
|
|
140
|
-
|
|
170
|
+
finally {
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function deviceErrorCode(value) {
|
|
175
|
+
if (!record(value))
|
|
176
|
+
return undefined;
|
|
177
|
+
const error = value["error"];
|
|
178
|
+
const nested = typeof error === "string"
|
|
179
|
+
? error
|
|
180
|
+
: record(error)
|
|
181
|
+
? optional(error["code"])
|
|
182
|
+
: undefined;
|
|
183
|
+
const code = nested ?? optional(value["code"]);
|
|
184
|
+
return code?.trim().toLowerCase();
|
|
141
185
|
}
|
|
142
186
|
function intervalSeconds(value) {
|
|
143
187
|
const parsed = typeof value === "string" ? Number(value.trim()) : value;
|
package/dist/providers/ollama.js
CHANGED
|
@@ -17,7 +17,8 @@ const KEY = "OLLAMA_API_KEY";
|
|
|
17
17
|
const OLLAMA_EFFORTS = ["low", "medium", "high"];
|
|
18
18
|
let configuredHost;
|
|
19
19
|
const CONTEXT_CACHE_MS = 30_000;
|
|
20
|
-
const
|
|
20
|
+
const runtimeContextByEndpoint = new Map();
|
|
21
|
+
const modelContextByEndpoint = new Map();
|
|
21
22
|
/** Set the endpoint selected for this process. Undefined restores key-aware inference. */
|
|
22
23
|
export function configureOllama(host) {
|
|
23
24
|
configuredHost = host === undefined ? undefined : parseOllamaEndpoint(host).baseUrl;
|
|
@@ -56,15 +57,16 @@ export const ollama = {
|
|
|
56
57
|
async contextWindow(model, signal, onStatus) {
|
|
57
58
|
const at = endpoint();
|
|
58
59
|
const cacheKey = `${at.baseUrl}\u0000${model}`;
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
return
|
|
62
|
-
const
|
|
60
|
+
const runtime = cachedContext(runtimeContextByEndpoint, cacheKey);
|
|
61
|
+
if (runtime !== undefined)
|
|
62
|
+
return runtime;
|
|
63
|
+
const modelCapacity = cachedContext(modelContextByEndpoint, cacheKey);
|
|
64
|
+
const observed = await nativeContextWindow(at, model, modelCapacity, signal, onStatus);
|
|
63
65
|
if (observed?.runtime === true) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
rememberContext(runtimeContextByEndpoint, cacheKey, observed.value);
|
|
67
|
+
}
|
|
68
|
+
else if (observed !== undefined && modelCapacity === undefined) {
|
|
69
|
+
rememberContext(modelContextByEndpoint, cacheKey, observed.value);
|
|
68
70
|
}
|
|
69
71
|
return observed?.value;
|
|
70
72
|
},
|
|
@@ -96,7 +98,7 @@ export const ollama = {
|
|
|
96
98
|
return fromWireReply(reply);
|
|
97
99
|
},
|
|
98
100
|
};
|
|
99
|
-
async function nativeContextWindow(at, model, signal, onStatus) {
|
|
101
|
+
async function nativeContextWindow(at, model, fallback, signal, onStatus) {
|
|
100
102
|
try {
|
|
101
103
|
const running = await getJson(`${at.baseUrl}/api/ps`, headers(at), signal, onStatus);
|
|
102
104
|
const allocated = runningContext(running, model);
|
|
@@ -106,6 +108,8 @@ async function nativeContextWindow(at, model, signal, onStatus) {
|
|
|
106
108
|
catch (error) {
|
|
107
109
|
throwIfAborted(signal, error);
|
|
108
110
|
}
|
|
111
|
+
if (fallback !== undefined)
|
|
112
|
+
return { value: fallback, runtime: false };
|
|
109
113
|
try {
|
|
110
114
|
const details = await postJson(`${at.baseUrl}/api/show`, headers(at), { model }, signal, onStatus);
|
|
111
115
|
const capacity = modelCapacity(details);
|
|
@@ -118,6 +122,18 @@ async function nativeContextWindow(at, model, signal, onStatus) {
|
|
|
118
122
|
return undefined;
|
|
119
123
|
}
|
|
120
124
|
}
|
|
125
|
+
function cachedContext(cache, key) {
|
|
126
|
+
const cached = cache.get(key);
|
|
127
|
+
if (cached === undefined)
|
|
128
|
+
return undefined;
|
|
129
|
+
if (cached.expiresAt > Date.now())
|
|
130
|
+
return cached.value;
|
|
131
|
+
cache.delete(key);
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
function rememberContext(cache, key, value) {
|
|
135
|
+
cache.set(key, { value, expiresAt: Date.now() + CONTEXT_CACHE_MS });
|
|
136
|
+
}
|
|
121
137
|
function runningContext(value, model) {
|
|
122
138
|
if (!record(value) || !Array.isArray(value["models"]))
|
|
123
139
|
return undefined;
|
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$/;
|
|
@@ -296,17 +297,26 @@ async function readNodes(directory) {
|
|
|
296
297
|
}
|
|
297
298
|
const stored = [];
|
|
298
299
|
const sequences = new Set();
|
|
299
|
-
for (let
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
300
|
+
for (let start = 0; start < names.length; start += NODE_READ_CONCURRENCY) {
|
|
301
|
+
const decoded = await Promise.all(names.slice(start, start + NODE_READ_CONCURRENCY)
|
|
302
|
+
.map(async (name, offset) => {
|
|
303
|
+
const id = Number(NODE_NAME.exec(name)?.[1]);
|
|
304
|
+
if (id !== start + offset + 1) {
|
|
305
|
+
throw new Error("session conversation nodes are not contiguous");
|
|
306
|
+
}
|
|
307
|
+
const entry = decodeNode(await readJson(path.join(directory, name), SESSION_FILE_LIMITS.nodeBytes));
|
|
308
|
+
if (entry.node.id !== id) {
|
|
309
|
+
throw new Error("session conversation node identity is invalid");
|
|
310
|
+
}
|
|
311
|
+
return entry;
|
|
312
|
+
}));
|
|
313
|
+
for (const entry of decoded) {
|
|
314
|
+
if (sequences.has(entry.sequence)) {
|
|
315
|
+
throw new Error("session conversation node identity is invalid");
|
|
316
|
+
}
|
|
317
|
+
sequences.add(entry.sequence);
|
|
318
|
+
stored.push(entry);
|
|
307
319
|
}
|
|
308
|
-
sequences.add(decoded.sequence);
|
|
309
|
-
stored.push(decoded);
|
|
310
320
|
}
|
|
311
321
|
return stored;
|
|
312
322
|
}
|
package/dist/tui/blocks.js
CHANGED
|
@@ -10,12 +10,11 @@ export function render(block, width, pal, context = {}) {
|
|
|
10
10
|
return renderAnswer(block, width, pal);
|
|
11
11
|
case "reasoning":
|
|
12
12
|
return renderReasoning(block, width, pal, {
|
|
13
|
-
continues: context.previous?.kind === "
|
|
13
|
+
continues: context.previous?.kind === "reasoning",
|
|
14
14
|
});
|
|
15
15
|
case "tool":
|
|
16
16
|
return renderTool(block, width, pal, {
|
|
17
|
-
continues: context.previous?.kind === "tool"
|
|
18
|
-
followsReasoning: context.previous?.kind === "reasoning",
|
|
17
|
+
continues: context.previous?.kind === "tool",
|
|
19
18
|
now: context.now,
|
|
20
19
|
motion: context.motion,
|
|
21
20
|
reducedMotion: context.reducedMotion,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { trailingText } from "../../text-boundary.js";
|
|
2
|
-
import { row } from "../../ui/render.js";
|
|
2
|
+
import { blank, row } from "../../ui/render.js";
|
|
3
3
|
import { markdown } from "../../ui/markdown.js";
|
|
4
4
|
import { transcriptLead, transcriptWidth } from "../transcript-grammar.js";
|
|
5
5
|
export const REASONING_PREVIEW_ROWS = 3;
|
|
@@ -10,37 +10,34 @@ export function renderUser(block, width, pal) {
|
|
|
10
10
|
const content = markdown(block.text, inner, pal, inner);
|
|
11
11
|
return [
|
|
12
12
|
"",
|
|
13
|
+
blank(width, pal.surface.subtle),
|
|
13
14
|
...content.map((line, index) => row(width, [
|
|
14
15
|
...transcriptLead(width, index === 0
|
|
15
16
|
? { text: "❯", fg: pal.accent, bold: true }
|
|
16
17
|
: undefined),
|
|
17
18
|
...line.segs,
|
|
18
|
-
])),
|
|
19
|
+
], [], pal.surface.subtle)),
|
|
20
|
+
blank(width, pal.surface.subtle),
|
|
19
21
|
];
|
|
20
22
|
}
|
|
21
23
|
export function renderAnswer(block, width, pal) {
|
|
22
|
-
const inner = transcriptWidth(width);
|
|
23
24
|
return [
|
|
24
25
|
"",
|
|
25
|
-
...markdown(block.text,
|
|
26
|
+
...markdown(block.text, width, pal, width).map((line) => row(width, line.segs)),
|
|
26
27
|
];
|
|
27
28
|
}
|
|
28
29
|
export function renderReasoning(block, width, pal, context = {}) {
|
|
29
|
-
const inner = transcriptWidth(width);
|
|
30
30
|
// Expanding a live stream is deferred until it is sealed. Re-parsing an
|
|
31
31
|
// ever-growing full thought on every token makes the whole TUI stall.
|
|
32
32
|
const expanded = block.expanded === true && block.live !== true;
|
|
33
33
|
const source = !expanded
|
|
34
|
-
? reasoningPreviewSource(block.text,
|
|
34
|
+
? reasoningPreviewSource(block.text, width)
|
|
35
35
|
: { text: block.text, truncated: false };
|
|
36
|
-
const content = markdown(source.text,
|
|
36
|
+
const content = markdown(source.text, width, pal, width);
|
|
37
37
|
const visible = expanded ? content : content.slice(-REASONING_PREVIEW_ROWS);
|
|
38
38
|
return [
|
|
39
39
|
...(context.continues === true ? [] : [""]),
|
|
40
|
-
...visible.map((line) => row(width,
|
|
41
|
-
...transcriptLead(width, { text: "│", fg: pal.rule }),
|
|
42
|
-
...line.segs.map((seg) => ({ ...seg, fg: pal.ink.dim, italic: true })),
|
|
43
|
-
])),
|
|
40
|
+
...visible.map((line) => row(width, line.segs.map((seg) => ({ ...seg, fg: pal.ink.dim, italic: true })))),
|
|
44
41
|
];
|
|
45
42
|
}
|
|
46
43
|
export function reasoningPreviewSource(text, width) {
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import { fitSegs, hasColor, plainLen, row } from "../../ui/render.js";
|
|
4
4
|
import { graphemeCeiling, graphemeFloor } from "../../text-boundary.js";
|
|
5
5
|
import { toolDuration } from "../../duration.js";
|
|
6
|
-
import { breathe, easeInOut, easeOut, interval, mix, TOOL_BIRTH_MS, TOOL_LEADER_MAX_MS, TOOL_ROW_ARRIVAL_MS,
|
|
7
|
-
import { transcriptLead
|
|
6
|
+
import { breathe, easeInOut, easeOut, interval, mix, TOOL_BIRTH_MS, TOOL_LEADER_MAX_MS, TOOL_ROW_ARRIVAL_MS, } from "../motion.js";
|
|
7
|
+
import { transcriptLead } from "../transcript-grammar.js";
|
|
8
8
|
const OUTPUT_ROWS = 8;
|
|
9
9
|
const LIVE_OUTPUT_ROWS = 6;
|
|
10
10
|
const DIFF_ROWS = 15;
|
|
@@ -15,7 +15,7 @@ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇",
|
|
|
15
15
|
export function renderTool(block, width, pal, context = {}) {
|
|
16
16
|
const now = context.now ?? Date.now();
|
|
17
17
|
const shown = visibleDetails(block);
|
|
18
|
-
const ink =
|
|
18
|
+
const ink = stateInk(block, pal, context, now);
|
|
19
19
|
const nameInk = birthInk(pal.ink.bright, pal.ink.dim, context, now);
|
|
20
20
|
const left = [
|
|
21
21
|
...transcriptLead(width, { text: stateGlyph(block, context, now), fg: ink, bold: true }),
|
|
@@ -25,9 +25,7 @@ export function renderTool(block, width, pal, context = {}) {
|
|
|
25
25
|
const right = resultSegments(block, pal, context, now);
|
|
26
26
|
const leader = movingLeader(width, left, right, pal, context, now);
|
|
27
27
|
return [
|
|
28
|
-
...(context.
|
|
29
|
-
? [row(width, transcriptMark(width, { text: "│", fg: pal.rule }))]
|
|
30
|
-
: context.continues === true ? [] : [""]),
|
|
28
|
+
...(context.continues === true ? [] : [""]),
|
|
31
29
|
row(width, leader === undefined ? left : [...left, leader], right),
|
|
32
30
|
...shown.map(({ detail, sourceIndex }) => renderDetail(detail, block.tone, width, pal, context.reducedMotion === true ? undefined : context.motion?.rowsAt[sourceIndex ?? -1], now)),
|
|
33
31
|
];
|
|
@@ -138,7 +136,7 @@ function stateGlyph(block, context, now) {
|
|
|
138
136
|
return SPINNER[Math.floor(now / SPINNER_MS) % SPINNER.length] ?? "○";
|
|
139
137
|
}
|
|
140
138
|
if (block.tone === "fail")
|
|
141
|
-
return "×";
|
|
139
|
+
return hasColor() ? "●" : "×";
|
|
142
140
|
if (block.tone === "deny")
|
|
143
141
|
return "○";
|
|
144
142
|
return hasColor() ? "●" : "✓";
|
|
@@ -148,7 +146,7 @@ function resultSegments(block, pal, context, now) {
|
|
|
148
146
|
const duration = liveDuration(block, now);
|
|
149
147
|
if (status === "" && duration === "")
|
|
150
148
|
return [];
|
|
151
|
-
const ink =
|
|
149
|
+
const ink = resultInk(block, pal, context, now);
|
|
152
150
|
return [
|
|
153
151
|
...(status === "" ? [] : [{ text: status, fg: ink }]),
|
|
154
152
|
...(duration === "" ? [] : [{ text: `${status === "" ? "" : " · "}${duration}`, fg: pal.ink.dim }]),
|
|
@@ -160,7 +158,7 @@ function liveDuration(block, now) {
|
|
|
160
158
|
}
|
|
161
159
|
return block.durationMs === undefined ? "" : toolDuration(block.durationMs);
|
|
162
160
|
}
|
|
163
|
-
function
|
|
161
|
+
function stateInk(block, pal, context, now) {
|
|
164
162
|
if (block.tone === "fail")
|
|
165
163
|
return pal.ink.removed;
|
|
166
164
|
if (block.tone === "deny")
|
|
@@ -170,10 +168,10 @@ function statusInk(block, pal, context, now) {
|
|
|
170
168
|
return pal.ink.attention;
|
|
171
169
|
return mix(pal.ink.attention, pal.accent, breathe(now));
|
|
172
170
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return
|
|
171
|
+
return pal.ink.added;
|
|
172
|
+
}
|
|
173
|
+
function resultInk(block, pal, context, now) {
|
|
174
|
+
return block.tone === "ok" ? pal.ink.muted : stateInk(block, pal, context, now);
|
|
177
175
|
}
|
|
178
176
|
function birthInk(final, initial, context, now) {
|
|
179
177
|
if (context.reducedMotion === true || context.motion === undefined)
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
// One semantic margin shared by
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export function transcriptGutter(width) {
|
|
6
|
-
return width < COMPACT_BELOW ? COMPACT_GUTTER : FULL_GUTTER;
|
|
1
|
+
// One semantic margin shared by the user cue and executable tool evidence.
|
|
2
|
+
const GUTTER = 2;
|
|
3
|
+
export function transcriptGutter(_width) {
|
|
4
|
+
return GUTTER;
|
|
7
5
|
}
|
|
8
6
|
export function transcriptWidth(width) {
|
|
9
7
|
return Math.max(8, width - transcriptGutter(width));
|
|
@@ -14,12 +12,10 @@ export function transcriptLead(width, mark) {
|
|
|
14
12
|
return [{ text: " ".repeat(transcriptGutter(width)) }];
|
|
15
13
|
return [
|
|
16
14
|
...transcriptMark(width, mark),
|
|
17
|
-
{ text: " "
|
|
15
|
+
{ text: " " },
|
|
18
16
|
];
|
|
19
17
|
}
|
|
20
18
|
/** Draw only the gutter mark, without trailing content-column whitespace. */
|
|
21
|
-
export function transcriptMark(
|
|
22
|
-
|
|
23
|
-
return [{ ...mark }];
|
|
24
|
-
return [{ text: " " }, { ...mark }];
|
|
19
|
+
export function transcriptMark(_width, mark) {
|
|
20
|
+
return [{ ...mark }];
|
|
25
21
|
}
|
package/dist/ui/inline.js
CHANGED
|
@@ -14,22 +14,22 @@ export function inline(text, base, pal, bold = false) {
|
|
|
14
14
|
const { ink } = pal;
|
|
15
15
|
const segs = [];
|
|
16
16
|
let last = 0;
|
|
17
|
-
INLINE.
|
|
18
|
-
for (let match =
|
|
17
|
+
const matcher = new RegExp(INLINE.source, INLINE.flags);
|
|
18
|
+
for (let match = matcher.exec(text); match !== null; match = matcher.exec(text)) {
|
|
19
19
|
if (match.index > last) {
|
|
20
20
|
segs.push({ text: text.slice(last, match.index), fg: base, bold: bold || undefined });
|
|
21
21
|
}
|
|
22
22
|
const [, mono, strong, strongAlt, emphasis, label] = match;
|
|
23
23
|
if (mono !== undefined)
|
|
24
|
-
segs.push({ text: mono, fg: pal.technical });
|
|
24
|
+
segs.push({ text: mono, fg: pal.technical, bold: bold || undefined });
|
|
25
25
|
else if (strong !== undefined)
|
|
26
|
-
segs.push(
|
|
26
|
+
segs.push(...inline(strong, ink.bright, pal, true));
|
|
27
27
|
else if (strongAlt !== undefined)
|
|
28
|
-
segs.push(
|
|
28
|
+
segs.push(...inline(strongAlt, ink.bright, pal, true));
|
|
29
29
|
else if (emphasis !== undefined)
|
|
30
|
-
segs.push(
|
|
30
|
+
segs.push(...inline(emphasis, ink.bright, pal, bold));
|
|
31
31
|
else if (label !== undefined)
|
|
32
|
-
segs.push({ text: label, fg: pal.technical });
|
|
32
|
+
segs.push({ text: label, fg: pal.technical, bold: bold || undefined });
|
|
33
33
|
last = match.index + match[0].length;
|
|
34
34
|
}
|
|
35
35
|
if (last < text.length) {
|
package/dist/ui/theme.js
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
// Jecode's fixed terminal identity. Components depend on semantic tokens, not
|
|
2
2
|
// literal colours, while the product exposes one deliberate dark Slate look.
|
|
3
|
-
// Jecode's fixed dark Slate baseline.
|
|
4
|
-
//
|
|
5
|
-
// live state
|
|
3
|
+
// Jecode's fixed dark Slate baseline. Clear text contrast and luminous
|
|
4
|
+
// structural colours keep long terminal transcripts readable without making
|
|
5
|
+
// live state decorative.
|
|
6
6
|
// Components depend on these roles rather than embedding presentation values.
|
|
7
7
|
export const STEEL = {
|
|
8
|
-
accent: [
|
|
9
|
-
technical: [
|
|
10
|
-
focus: [
|
|
11
|
-
rule: [
|
|
8
|
+
accent: [102, 155, 210],
|
|
9
|
+
technical: [78, 201, 232],
|
|
10
|
+
focus: [102, 155, 210],
|
|
11
|
+
rule: [53, 80, 110],
|
|
12
12
|
ink: {
|
|
13
|
-
fg: [
|
|
14
|
-
bright: [
|
|
15
|
-
muted: [
|
|
16
|
-
dim: [
|
|
17
|
-
attention: [
|
|
18
|
-
added: [
|
|
19
|
-
removed: [
|
|
13
|
+
fg: [220, 224, 229],
|
|
14
|
+
bright: [235, 239, 244],
|
|
15
|
+
muted: [156, 169, 183],
|
|
16
|
+
dim: [112, 124, 137],
|
|
17
|
+
attention: [230, 191, 95],
|
|
18
|
+
added: [134, 203, 146],
|
|
19
|
+
removed: [232, 112, 112],
|
|
20
20
|
},
|
|
21
21
|
surface: {
|
|
22
|
-
subtle: [
|
|
23
|
-
added: [
|
|
24
|
-
removed: [
|
|
25
|
-
attention: [
|
|
22
|
+
subtle: [31, 38, 47],
|
|
23
|
+
added: [22, 55, 34],
|
|
24
|
+
removed: [62, 24, 27],
|
|
25
|
+
attention: [62, 50, 19],
|
|
26
26
|
},
|
|
27
27
|
};
|