@giovannijecha/jecode 0.4.0 → 0.5.0
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 +41 -8
- package/dist/batch.js +22 -2
- package/dist/cli-info.js +3 -0
- package/dist/commands.js +3 -2
- package/dist/config.js +2 -0
- package/dist/controller.js +3 -0
- package/dist/conversation.js +208 -0
- package/dist/launch.js +19 -0
- package/dist/providers/ollama.js +8 -3
- package/dist/sessions/codec.js +344 -0
- package/dist/sessions/lease.js +76 -0
- package/dist/sessions/runtime.js +73 -0
- package/dist/sessions/store.js +368 -0
- package/dist/start.js +67 -4
- package/dist/transcript-types.js +6 -0
- package/dist/tui/activity.js +3 -0
- package/dist/tui/app-input.js +9 -6
- package/dist/tui/app-workflows.js +27 -3
- package/dist/tui/app.js +71 -19
- package/dist/tui/components/messages.js +0 -14
- package/dist/tui/components/status.js +4 -1
- package/dist/tui/components/tool.js +19 -2
- package/dist/tui/feedback.js +3 -0
- package/dist/tui/resume.js +24 -0
- package/dist/tui/turn.js +2 -3
- package/dist/ui/diff.js +2 -0
- package/dist/ui/render.js +2 -0
- package/dist/usage.js +9 -0
- package/package.json +1 -1
package/dist/tui/app.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Everything mutable about a session lives in `state` here. The view is a pure
|
|
4
4
|
// function of it, so the only job of a key handler is to change state and ask
|
|
5
5
|
// for a repaint — never to draw.
|
|
6
|
-
import { begin } from "./activity.js";
|
|
6
|
+
import { activityStatus, begin } from "./activity.js";
|
|
7
7
|
import * as overlay from "./overlay.js";
|
|
8
8
|
import { options as completionOptions } from "./complete.js";
|
|
9
9
|
import { decoder } from "./keys.js";
|
|
@@ -19,6 +19,7 @@ import { appState } from "./app-state.js";
|
|
|
19
19
|
import { appInput } from "./app-input.js";
|
|
20
20
|
import { appWorkflows } from "./app-workflows.js";
|
|
21
21
|
import { sessionPermissions } from "../permissions.js";
|
|
22
|
+
import { resumePicker } from "./resume.js";
|
|
22
23
|
const FRAME_MS = 16;
|
|
23
24
|
const SPIN_MS = 80;
|
|
24
25
|
/** How long a lone escape waits to prove it is not the start of a sequence. */
|
|
@@ -30,6 +31,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
30
31
|
const transcript = transcriptRenderer();
|
|
31
32
|
const workspace = await workspaceLabel(session.config.root);
|
|
32
33
|
const state = appState();
|
|
34
|
+
state.blocks.push(...session.conversation.transcript);
|
|
33
35
|
const permissions = sessionPermissions(session.tools, session.config.autoApprove);
|
|
34
36
|
let closed;
|
|
35
37
|
let frameTimer;
|
|
@@ -43,23 +45,28 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
43
45
|
const done = new Promise((resolve) => {
|
|
44
46
|
closed = resolve;
|
|
45
47
|
});
|
|
46
|
-
const view = () =>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
48
|
+
const view = () => {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
return {
|
|
51
|
+
blocks: state.blocks,
|
|
52
|
+
editor: state.editor,
|
|
53
|
+
scroll: state.scroll,
|
|
54
|
+
unseen: state.unseen,
|
|
55
|
+
pal: session.palette,
|
|
56
|
+
footer: footerInfo(session, workspace),
|
|
57
|
+
status: state.activity === undefined
|
|
58
|
+
? undefined
|
|
59
|
+
: activityStatus(state.activity, state.status ?? state.activity.label, now),
|
|
60
|
+
feedback: state.feedback,
|
|
61
|
+
readiness: turnBlocker(session),
|
|
62
|
+
spin: state.spin,
|
|
63
|
+
reducedMotion: session.config.reducedMotion,
|
|
64
|
+
now,
|
|
65
|
+
modal: overlay.shown(state.open),
|
|
66
|
+
menu: completionOptions(state.completing),
|
|
67
|
+
menuIndex: state.completing?.index,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
63
70
|
const draw = () => {
|
|
64
71
|
frameTimer = undefined;
|
|
65
72
|
if (!live)
|
|
@@ -201,6 +208,44 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
201
208
|
invalidate: () => paint.invalidate(),
|
|
202
209
|
transcriptChanged: (block) => transcript.invalidate(block),
|
|
203
210
|
});
|
|
211
|
+
const resumeAtLaunch = session.resume === undefined
|
|
212
|
+
? undefined
|
|
213
|
+
: openResumedSession(session.resume);
|
|
214
|
+
async function openResumedSession(launch) {
|
|
215
|
+
while (live) {
|
|
216
|
+
const index = await new Promise((resolve) => {
|
|
217
|
+
state.open = { picker: resumePicker(launch.candidates, session.palette), settle: resolve };
|
|
218
|
+
render();
|
|
219
|
+
});
|
|
220
|
+
if (index === undefined) {
|
|
221
|
+
quit();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const candidate = launch.candidates[index];
|
|
225
|
+
if (candidate === undefined)
|
|
226
|
+
continue;
|
|
227
|
+
const activity = startActivity("command", "Opening session");
|
|
228
|
+
if (activity === undefined)
|
|
229
|
+
return;
|
|
230
|
+
try {
|
|
231
|
+
await launch.open(candidate.id);
|
|
232
|
+
state.blocks.splice(0, state.blocks.length, ...session.conversation.transcript);
|
|
233
|
+
state.past.length = 0;
|
|
234
|
+
state.scroll = 0;
|
|
235
|
+
state.follow = true;
|
|
236
|
+
state.unseen = 0;
|
|
237
|
+
state.lastMaxScroll = 0;
|
|
238
|
+
transcript.invalidate();
|
|
239
|
+
paint.invalidate();
|
|
240
|
+
finishActivity(activity);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
feedback.show({ text: error.message, tone: "error", timeoutMs: 6_000 });
|
|
245
|
+
finishActivity(activity);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
204
249
|
terminal.enter(session.config.reducedMotion);
|
|
205
250
|
stopResize = terminal.onResize(() => {
|
|
206
251
|
paint.invalidate();
|
|
@@ -228,5 +273,12 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
228
273
|
render();
|
|
229
274
|
});
|
|
230
275
|
draw();
|
|
231
|
-
|
|
276
|
+
try {
|
|
277
|
+
await resumeAtLaunch;
|
|
278
|
+
if (live)
|
|
279
|
+
await done;
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
await session.persistence?.close();
|
|
283
|
+
}
|
|
232
284
|
}
|
|
@@ -31,22 +31,8 @@ export function renderReasoning(block, width, pal) {
|
|
|
31
31
|
: { text: block.text, truncated: false };
|
|
32
32
|
const content = markdown(source.text, inner, pal, inner);
|
|
33
33
|
const visible = expanded ? content : content.slice(-REASONING_PREVIEW_ROWS);
|
|
34
|
-
const action = expanded
|
|
35
|
-
? "ctrl+o compact"
|
|
36
|
-
: block.live === true && block.expanded === true
|
|
37
|
-
? "full when done"
|
|
38
|
-
: source.truncated || content.length > REASONING_PREVIEW_ROWS
|
|
39
|
-
? "ctrl+o full"
|
|
40
|
-
: undefined;
|
|
41
34
|
return [
|
|
42
35
|
"",
|
|
43
|
-
row(width, [
|
|
44
|
-
{
|
|
45
|
-
text: block.live === true ? "thinking" : "thought",
|
|
46
|
-
fg: pal.ink.bright,
|
|
47
|
-
bold: true,
|
|
48
|
-
},
|
|
49
|
-
], action === undefined ? [] : [{ text: action, fg: pal.ink.dim }], undefined, PAD),
|
|
50
36
|
...visible.map((line) => row(width, line.segs.map((seg) => ({ ...seg, fg: pal.ink.dim, italic: true })), [], undefined, PAD)),
|
|
51
37
|
];
|
|
52
38
|
}
|
|
@@ -6,7 +6,10 @@ export function renderStatus(info, pal) {
|
|
|
6
6
|
if (urgent !== undefined)
|
|
7
7
|
return withUnseen(feedbackSegments(urgent, pal), info.unseen, pal);
|
|
8
8
|
if (info.status !== undefined) {
|
|
9
|
-
return withUnseen([
|
|
9
|
+
return withUnseen([
|
|
10
|
+
{ text: info.status, fg: pal.ink.muted },
|
|
11
|
+
{ text: " · esc to interrupt", fg: pal.ink.dim, optional: true },
|
|
12
|
+
], info.unseen, pal);
|
|
10
13
|
}
|
|
11
14
|
if (info.feedback !== undefined) {
|
|
12
15
|
return withUnseen(feedbackSegments(info.feedback, pal), info.unseen, pal);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { hasColor, row } from "../../ui/render.js";
|
|
3
3
|
const OUTPUT_ROWS = 8;
|
|
4
4
|
const LIVE_OUTPUT_ROWS = 6;
|
|
5
|
+
const DIFF_ROWS = 15;
|
|
5
6
|
export function renderTool(block, width, pal, context = {}) {
|
|
6
7
|
const shown = visibleDetails(block);
|
|
7
8
|
const right = liveLabel(block, context);
|
|
@@ -31,8 +32,24 @@ function visibleDetails(block) {
|
|
|
31
32
|
return [{ kind: "gap", text: note }, ...all.slice(-limit)];
|
|
32
33
|
}
|
|
33
34
|
// The compact transcript is an audit of what changed, not a code excerpt.
|
|
34
|
-
//
|
|
35
|
-
|
|
35
|
+
// One shared budget applies to writes and edits. Keep both ends so a large
|
|
36
|
+
// replacement cannot show only deletions while hiding all new content.
|
|
37
|
+
// Context, omitted changes, and gap rows remain in semantic state for the
|
|
38
|
+
// explicit full view.
|
|
39
|
+
const changed = all.filter((detail) => detail.kind === "add" || detail.kind === "del");
|
|
40
|
+
if (changed.length <= DIFF_ROWS)
|
|
41
|
+
return changed;
|
|
42
|
+
const leading = Math.ceil(DIFF_ROWS / 2);
|
|
43
|
+
const trailing = DIFF_ROWS - leading;
|
|
44
|
+
const hidden = changed.length - DIFF_ROWS;
|
|
45
|
+
return [
|
|
46
|
+
...changed.slice(0, leading),
|
|
47
|
+
{
|
|
48
|
+
kind: "gap",
|
|
49
|
+
text: `… ${hidden} more changed ${hidden === 1 ? "line" : "lines"} · ctrl+o expand`,
|
|
50
|
+
},
|
|
51
|
+
...changed.slice(-trailing),
|
|
52
|
+
];
|
|
36
53
|
}
|
|
37
54
|
function renderDetail(detail, tone, width, pal) {
|
|
38
55
|
const lead = { text: " " };
|
package/dist/tui/feedback.js
CHANGED
|
@@ -50,6 +50,9 @@ export function commandFeedback(block) {
|
|
|
50
50
|
}
|
|
51
51
|
/** Explain why a model turn cannot start, without exposing configuration internals. */
|
|
52
52
|
export function turnBlocker(session) {
|
|
53
|
+
if (session.persistence?.failure !== undefined) {
|
|
54
|
+
return { text: "session could not be saved · /new to retry", tone: "error" };
|
|
55
|
+
}
|
|
53
56
|
const blocked = session.provider.blocked();
|
|
54
57
|
const auth = session.provider.auth;
|
|
55
58
|
const expected = auth.kind === "api-key"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { heading } from "./picker.js";
|
|
2
|
+
export function resumePicker(candidates, palette) {
|
|
3
|
+
return {
|
|
4
|
+
title: heading("resume", "saved conversations", palette),
|
|
5
|
+
searchable: true,
|
|
6
|
+
visible: 8,
|
|
7
|
+
options: candidates.map((candidate) => ({
|
|
8
|
+
label: candidate.preview,
|
|
9
|
+
hint: stamp(candidate.updatedAt),
|
|
10
|
+
value: `${candidate.turns} ${candidate.turns === 1 ? "turn" : "turns"}`,
|
|
11
|
+
})),
|
|
12
|
+
index: 0,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function stamp(value) {
|
|
16
|
+
const date = new Date(value);
|
|
17
|
+
if (!Number.isFinite(date.getTime()))
|
|
18
|
+
return value.replace("T", " ").slice(0, 16);
|
|
19
|
+
const two = (part) => String(part).padStart(2, "0");
|
|
20
|
+
return [
|
|
21
|
+
`${date.getFullYear()}-${two(date.getMonth() + 1)}-${two(date.getDate())}`,
|
|
22
|
+
`${two(date.getHours())}:${two(date.getMinutes())}`,
|
|
23
|
+
].join(" ");
|
|
24
|
+
}
|
package/dist/tui/turn.js
CHANGED
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
// neither has to know how the other is built.
|
|
6
6
|
import { condense, diff } from "../ui/diff.js";
|
|
7
7
|
import { promptFor } from "./approve.js";
|
|
8
|
-
// Semantic activity labels
|
|
9
|
-
//
|
|
10
|
-
// the live work in the transcript itself.
|
|
8
|
+
// Semantic activity labels feed the footer's compact state and timer while
|
|
9
|
+
// reasoning and tools keep the detailed work visible in the transcript.
|
|
11
10
|
const WAITING = "Waiting";
|
|
12
11
|
const THINKING = "Thinking";
|
|
13
12
|
const WRITING = "Writing";
|
package/dist/ui/diff.js
CHANGED
|
@@ -96,6 +96,8 @@ function common(a, b) {
|
|
|
96
96
|
// A trailing newline is a property of the file, not a line of it: counting it
|
|
97
97
|
// as one would report every append as touching two lines instead of one.
|
|
98
98
|
function lines(text) {
|
|
99
|
+
if (text === "")
|
|
100
|
+
return [];
|
|
99
101
|
const split = text.split("\n");
|
|
100
102
|
if (split.length > 1 && split[split.length - 1] === "")
|
|
101
103
|
split.pop();
|
package/dist/ui/render.js
CHANGED
package/dist/usage.js
CHANGED
|
@@ -19,6 +19,15 @@ export function recordUsage(total, next) {
|
|
|
19
19
|
total.cacheWriteInputTokens += next.cacheWriteInputTokens;
|
|
20
20
|
total.reasoningTokens += next.reasoningTokens;
|
|
21
21
|
}
|
|
22
|
+
export function usageFromHistory(messages) {
|
|
23
|
+
const total = emptyUsage();
|
|
24
|
+
for (const message of messages) {
|
|
25
|
+
if (message.role === "assistant" && message.usage !== undefined) {
|
|
26
|
+
recordUsage(total, message.usage);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return total;
|
|
30
|
+
}
|
|
22
31
|
export function formatTokens(value) {
|
|
23
32
|
if (value < 1_000)
|
|
24
33
|
return String(value);
|