@giovannijecha/jecode 0.6.0 → 0.7.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 +17 -6
- package/dist/batch.js +5 -1
- package/dist/commands.js +38 -10
- package/dist/context/compactor.js +3 -1
- package/dist/context/manual.js +69 -0
- package/dist/settings-command.js +1 -1
- package/dist/timeline.js +90 -0
- package/dist/tui/app-state.js +1 -0
- package/dist/tui/app-workflows.js +30 -4
- package/dist/tui/app.js +14 -7
- package/dist/tui/components/misc.js +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
<a href="https://github.com/giovannijecha/jecode/releases">Releases</a>
|
|
26
26
|
</p>
|
|
27
27
|
|
|
28
|
-
> Jecode is an early 0.
|
|
28
|
+
> Jecode is an early 0.7.x release. The core loop is usable today; commands and
|
|
29
29
|
> terminal interactions may still evolve before 1.0.
|
|
30
30
|
|
|
31
31
|
## Why Jecode
|
|
@@ -201,6 +201,8 @@ Type **/** to open searchable command completion inside the composer.
|
|
|
201
201
|
| /models | Search models across every available provider and select one |
|
|
202
202
|
| /permissions | Change session tool access inline and review remembered approvals |
|
|
203
203
|
| /new | Close the current conversation, start clean, and reset tool permissions |
|
|
204
|
+
| /timeline | Navigate completed turns and select where the next branch starts |
|
|
205
|
+
| /compact | Compact the active model context immediately |
|
|
204
206
|
| /export | Save a timestamped Markdown transcript in the launch directory |
|
|
205
207
|
| /help | Open a temporary keyboard reference in the composer dock |
|
|
206
208
|
| /exit | Restore the terminal and exit |
|
|
@@ -264,10 +266,15 @@ files use owner-only modes on POSIX; Windows relies on the user-profile ACL.
|
|
|
264
266
|
`jecode resume` keeps the same durable session identity and advances that
|
|
265
267
|
session's conversation tree, so reopening and continuing a conversation does
|
|
266
268
|
not create duplicate picker entries. `/new` or a fresh launch starts another
|
|
267
|
-
logical session.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
logical session. **/timeline** shows the completed turns in that tree. Selecting
|
|
270
|
+
an earlier turn changes only the visible path; it creates and persists a branch
|
|
271
|
+
only when the next real message is sent. Cancelling the picker or exiting first
|
|
272
|
+
leaves the durable head untouched, and resume returns to the last branch with a
|
|
273
|
+
persisted turn. Historical tools are displayed but never executed. If a crash
|
|
274
|
+
left the newest turn inside a tool loop, the same session resumes from its
|
|
275
|
+
latest completed ancestor and the next turn becomes a branch because
|
|
276
|
+
provider-only continuation data is intentionally not stored. **/export** writes
|
|
277
|
+
only the currently selected path.
|
|
271
278
|
|
|
272
279
|
When the model-facing context approaches the selected model's usable capacity,
|
|
273
280
|
Jecode asks the provider for one bounded summary of its older prefix and keeps
|
|
@@ -281,7 +288,11 @@ session, so resume reuses it instead of summarizing the same prefix again. A
|
|
|
281
288
|
failed or cancelled optional summary leaves the original context intact; a
|
|
282
289
|
definite provider context-limit rejection may trigger one compacted retry.
|
|
283
290
|
Internal summary requests count toward provider usage but never appear in the
|
|
284
|
-
transcript or Markdown export.
|
|
291
|
+
transcript or Markdown export. **/compact** requests the same model-aware,
|
|
292
|
+
branch-local compaction immediately, even below the automatic trigger. Very
|
|
293
|
+
small contexts are left unchanged. After selecting a historical branch point,
|
|
294
|
+
send its first new message before compacting so shared history is never
|
|
295
|
+
rewritten.
|
|
285
296
|
|
|
286
297
|
Jecode has one interface theme: dark Steel. **NO_COLOR** is supported for
|
|
287
298
|
terminals and pipelines that disable colour.
|
package/dist/batch.js
CHANGED
|
@@ -7,6 +7,7 @@ import { stdin, stdout } from "node:process";
|
|
|
7
7
|
import { runTurn } from "./controller.js";
|
|
8
8
|
import { resolveContextPolicy } from "./context/capacity.js";
|
|
9
9
|
import { compactContext } from "./context/compactor.js";
|
|
10
|
+
import { compactSession } from "./context/manual.js";
|
|
10
11
|
import { isContextOverflow, shouldResolveContextPolicy } from "./context/policy.js";
|
|
11
12
|
import { handleCommand } from "./commands.js";
|
|
12
13
|
import { renderBatch } from "./batch-view.js";
|
|
@@ -28,7 +29,10 @@ export async function runBatch(session, environment = {}) {
|
|
|
28
29
|
if (line === "")
|
|
29
30
|
continue;
|
|
30
31
|
if (line.startsWith("/")) {
|
|
31
|
-
if ((await handleCommand(line, session, {
|
|
32
|
+
if ((await handleCommand(line, session, {
|
|
33
|
+
emit,
|
|
34
|
+
compact: () => compactSession(session),
|
|
35
|
+
})) === "exit")
|
|
32
36
|
break;
|
|
33
37
|
continue;
|
|
34
38
|
}
|
package/dist/commands.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// Slash-command registry and dispatcher.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// this file keeps discovery, dispatch, and local session operations.
|
|
3
|
+
// Model discovery and explicit context compaction can reach a provider, but
|
|
4
|
+
// slash commands never become user messages. This file keeps discovery,
|
|
5
|
+
// dispatch, and session operations outside the canonical transcript.
|
|
7
6
|
import { ConversationTree } from "./conversation.js";
|
|
8
7
|
import { modelsCommand } from "./model-command.js";
|
|
9
8
|
import { providersCommand } from "./provider-commands.js";
|
|
@@ -22,6 +21,8 @@ export const COMMANDS = [
|
|
|
22
21
|
{ name: "exit", blurb: "exit and restore the terminal" },
|
|
23
22
|
{ name: "new", blurb: "start clean and reset tool permissions" },
|
|
24
23
|
{ name: "export", blurb: "save this transcript as Markdown" },
|
|
24
|
+
{ name: "timeline", blurb: "navigate this conversation tree" },
|
|
25
|
+
{ name: "compact", blurb: "compact the active context now" },
|
|
25
26
|
{ name: "permissions", blurb: "manage session tool access" },
|
|
26
27
|
{ name: "settings", blurb: "change and save jecode defaults" },
|
|
27
28
|
{ name: "effort", blurb: "set the reasoning effort" },
|
|
@@ -51,6 +52,39 @@ export async function handleCommand(line, session, host) {
|
|
|
51
52
|
session.usage = emptyUsage();
|
|
52
53
|
host.emit({ kind: "notice", text: "new session", tone: "info" });
|
|
53
54
|
return "handled";
|
|
55
|
+
case "timeline": {
|
|
56
|
+
if (host.timeline === undefined) {
|
|
57
|
+
host.emit({ kind: "notice", text: "timeline needs the interactive screen", tone: "warn" });
|
|
58
|
+
return "handled";
|
|
59
|
+
}
|
|
60
|
+
const result = await host.timeline();
|
|
61
|
+
if (result === "selected") {
|
|
62
|
+
host.emit({
|
|
63
|
+
kind: "notice",
|
|
64
|
+
text: "branch point selected · send a message to continue",
|
|
65
|
+
tone: "info",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return "handled";
|
|
69
|
+
}
|
|
70
|
+
case "compact": {
|
|
71
|
+
if (host.compact === undefined) {
|
|
72
|
+
host.emit({ kind: "notice", text: "compact is unavailable here", tone: "warn" });
|
|
73
|
+
return "handled";
|
|
74
|
+
}
|
|
75
|
+
const result = await host.compact();
|
|
76
|
+
if (result === "compacted") {
|
|
77
|
+
host.emit({ kind: "notice", text: "context compacted", tone: "info" });
|
|
78
|
+
}
|
|
79
|
+
else if (result === "branch-pending") {
|
|
80
|
+
host.emit({
|
|
81
|
+
kind: "notice",
|
|
82
|
+
text: "send a message on this branch before compacting",
|
|
83
|
+
tone: "warn",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return "handled";
|
|
87
|
+
}
|
|
54
88
|
case "export":
|
|
55
89
|
if (host.exportTranscript === undefined) {
|
|
56
90
|
host.emit({ kind: "notice", text: "export needs the interactive screen", tone: "warn" });
|
|
@@ -84,9 +118,3 @@ export async function handleCommand(line, session, host) {
|
|
|
84
118
|
return "handled";
|
|
85
119
|
}
|
|
86
120
|
}
|
|
87
|
-
function chooser(host) {
|
|
88
|
-
if (host.choose === undefined) {
|
|
89
|
-
host.emit({ kind: "notice", text: "that command needs the screen", tone: "warn" });
|
|
90
|
-
}
|
|
91
|
-
return host.choose;
|
|
92
|
-
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Explicit compaction of the selected durable leaf.
|
|
2
|
+
//
|
|
3
|
+
// The canonical messages and transcript remain untouched. Only the active
|
|
4
|
+
// leaf receives a new branch-local context anchor, using the same provider
|
|
5
|
+
// policy and summarizer as automatic compaction.
|
|
6
|
+
import { recordAuxiliaryUsage } from "../usage.js";
|
|
7
|
+
import { resolveContextPolicy } from "./capacity.js";
|
|
8
|
+
import { compactContext } from "./compactor.js";
|
|
9
|
+
import { estimateTokens, planCompaction } from "./policy.js";
|
|
10
|
+
const MIN_PREFIX_TOKENS = 512;
|
|
11
|
+
export async function compactSession(session, options = {}) {
|
|
12
|
+
const active = session.conversation.activeNode;
|
|
13
|
+
if (active === undefined)
|
|
14
|
+
return "unchanged";
|
|
15
|
+
const context = session.conversation.contextHistory;
|
|
16
|
+
if (estimateTokens(context) < MIN_PREFIX_TOKENS)
|
|
17
|
+
return "unchanged";
|
|
18
|
+
if (session.conversation.nodes.some((node) => node.parentId === active.id)) {
|
|
19
|
+
throw new Error("continue this branch before compacting");
|
|
20
|
+
}
|
|
21
|
+
options.onStatus?.("Checking context");
|
|
22
|
+
const policy = await resolveContextPolicy({
|
|
23
|
+
provider: session.provider,
|
|
24
|
+
model: session.model,
|
|
25
|
+
compactionPercent: session.config.compactionPercent,
|
|
26
|
+
signal: options.signal,
|
|
27
|
+
onStatus: (status) => options.onStatus?.(status),
|
|
28
|
+
});
|
|
29
|
+
const coveredMessages = active.context?.throughNodeId === active.id
|
|
30
|
+
? active.context.messageCount
|
|
31
|
+
: 0;
|
|
32
|
+
const plan = planCompaction(context, active.messages, coveredMessages, session.usage.lastInputTokens, true, policy);
|
|
33
|
+
if (plan === undefined || estimateTokens(plan.prefix) < MIN_PREFIX_TOKENS) {
|
|
34
|
+
options.onStatus?.();
|
|
35
|
+
return "unchanged";
|
|
36
|
+
}
|
|
37
|
+
const result = await compactContext({
|
|
38
|
+
provider: session.provider,
|
|
39
|
+
model: session.model,
|
|
40
|
+
effort: session.config.effort,
|
|
41
|
+
context,
|
|
42
|
+
turn: active.messages,
|
|
43
|
+
nodeId: active.id,
|
|
44
|
+
coveredMessages,
|
|
45
|
+
lastInputTokens: session.usage.lastInputTokens,
|
|
46
|
+
signal: options.signal,
|
|
47
|
+
force: true,
|
|
48
|
+
failLoudly: true,
|
|
49
|
+
policy,
|
|
50
|
+
onBegin: () => options.onStatus?.("Compacting"),
|
|
51
|
+
onEnd: () => options.onStatus?.(),
|
|
52
|
+
});
|
|
53
|
+
if (result === undefined)
|
|
54
|
+
throw new Error("context could not be compacted");
|
|
55
|
+
if (result.usage !== undefined)
|
|
56
|
+
recordAuxiliaryUsage(session.usage, result.usage);
|
|
57
|
+
const next = session.conversation.commit({
|
|
58
|
+
nodeId: active.id,
|
|
59
|
+
parentId: active.parentId,
|
|
60
|
+
createdAt: active.createdAt,
|
|
61
|
+
identity: active.identity,
|
|
62
|
+
messages: active.messages,
|
|
63
|
+
blocks: active.blocks,
|
|
64
|
+
context: result.anchor,
|
|
65
|
+
}, active.settlement);
|
|
66
|
+
await session.persistence?.checkpoint(next);
|
|
67
|
+
session.conversation = next;
|
|
68
|
+
return "compacted";
|
|
69
|
+
}
|
package/dist/settings-command.js
CHANGED
|
@@ -202,7 +202,7 @@ async function compactionSetting(session, host) {
|
|
|
202
202
|
const label = "context compaction";
|
|
203
203
|
const field = {
|
|
204
204
|
title: heading(label, `${MIN_COMPACTION_PERCENT}-${MAX_COMPACTION_PERCENT} percent`, session.palette),
|
|
205
|
-
right: "enter save
|
|
205
|
+
right: "enter save · esc back",
|
|
206
206
|
editor: of(String(session.config.compactionPercent)),
|
|
207
207
|
secret: false,
|
|
208
208
|
note: "Compacts when model context reaches this percentage.",
|
package/dist/timeline.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// A compact, read-only projection of the durable conversation tree.
|
|
2
|
+
//
|
|
3
|
+
// Selecting a node changes only the in-memory path. The next real user turn
|
|
4
|
+
// is what persists a branch, so opening or cancelling this control plane can
|
|
5
|
+
// never create empty history.
|
|
6
|
+
import { usageFromHistory } from "./usage.js";
|
|
7
|
+
import { heading } from "./tui/picker.js";
|
|
8
|
+
export function timelinePicker(conversation, palette) {
|
|
9
|
+
const entries = timelineEntries(conversation);
|
|
10
|
+
const selectedId = conversation.latestCompleted()?.activeNodeId ?? 0;
|
|
11
|
+
const index = Math.max(0, entries.findIndex((entry) => entry.node.id === selectedId));
|
|
12
|
+
return Object.freeze({
|
|
13
|
+
picker: {
|
|
14
|
+
title: heading("timeline", "conversation tree", palette),
|
|
15
|
+
searchable: true,
|
|
16
|
+
query: "",
|
|
17
|
+
visible: 8,
|
|
18
|
+
options: entries.map((entry) => ({
|
|
19
|
+
label: `${entry.prefix}${preview(entry.node)}`,
|
|
20
|
+
hint: stamp(entry.node.createdAt),
|
|
21
|
+
...(entry.node.id === selectedId ? { value: "active" } : {}),
|
|
22
|
+
})),
|
|
23
|
+
index,
|
|
24
|
+
},
|
|
25
|
+
nodeIds: Object.freeze(entries.map((entry) => entry.node.id)),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function selectTimeline(session, choose) {
|
|
29
|
+
const timeline = timelinePicker(session.conversation, session.palette);
|
|
30
|
+
if (timeline.nodeIds.length === 0)
|
|
31
|
+
return false;
|
|
32
|
+
const index = await choose(timeline.picker);
|
|
33
|
+
const nodeId = index === undefined ? undefined : timeline.nodeIds[index];
|
|
34
|
+
if (nodeId === undefined || nodeId === session.conversation.activeNodeId)
|
|
35
|
+
return false;
|
|
36
|
+
session.conversation = session.conversation.select(nodeId);
|
|
37
|
+
session.usage = usageFromHistory(session.conversation.history);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
function timelineEntries(conversation) {
|
|
41
|
+
const completed = conversation.nodes.filter((node) => node.settlement === "completed");
|
|
42
|
+
const completedIds = new Set(completed.map((node) => node.id));
|
|
43
|
+
const children = new Map();
|
|
44
|
+
for (const node of completed) {
|
|
45
|
+
let parentId = node.parentId;
|
|
46
|
+
while (parentId !== 0 && !completedIds.has(parentId)) {
|
|
47
|
+
parentId = conversation.node(parentId)?.parentId ?? 0;
|
|
48
|
+
}
|
|
49
|
+
const siblings = children.get(parentId) ?? [];
|
|
50
|
+
siblings.push(node);
|
|
51
|
+
children.set(parentId, siblings);
|
|
52
|
+
}
|
|
53
|
+
const entries = [];
|
|
54
|
+
const visit = (parentId, lanes) => {
|
|
55
|
+
const siblings = children.get(parentId) ?? [];
|
|
56
|
+
for (let index = 0; index < siblings.length; index++) {
|
|
57
|
+
const node = siblings[index];
|
|
58
|
+
const forks = siblings.length > 1;
|
|
59
|
+
const last = index === siblings.length - 1;
|
|
60
|
+
const hidden = Math.max(0, lanes.length - 4);
|
|
61
|
+
const lanePrefix = `${hidden === 0 ? "" : "… "}${lanes.slice(hidden)
|
|
62
|
+
.map((closed) => closed ? " " : "│ ").join("")}`;
|
|
63
|
+
entries.push({
|
|
64
|
+
node,
|
|
65
|
+
prefix: `${lanePrefix}${forks ? (last ? "└─ " : "├─ ") : "• "}`,
|
|
66
|
+
});
|
|
67
|
+
visit(node.id, forks ? [...lanes, last] : lanes);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
visit(0, []);
|
|
71
|
+
return entries;
|
|
72
|
+
}
|
|
73
|
+
function preview(node) {
|
|
74
|
+
for (const message of node.messages) {
|
|
75
|
+
if (message.role !== "user")
|
|
76
|
+
continue;
|
|
77
|
+
const text = message.content.find((block) => block.kind === "text")?.text
|
|
78
|
+
.replace(/\s+/gu, " ").trim();
|
|
79
|
+
if (text !== undefined && text !== "")
|
|
80
|
+
return text.slice(0, 160);
|
|
81
|
+
}
|
|
82
|
+
return "Untitled turn";
|
|
83
|
+
}
|
|
84
|
+
function stamp(value) {
|
|
85
|
+
const date = new Date(value);
|
|
86
|
+
if (!Number.isFinite(date.getTime()))
|
|
87
|
+
return value.replace("T", " ").slice(0, 16);
|
|
88
|
+
const two = (part) => String(part).padStart(2, "0");
|
|
89
|
+
return `${two(date.getHours())}:${two(date.getMinutes())}`;
|
|
90
|
+
}
|
package/dist/tui/app-state.js
CHANGED
|
@@ -3,10 +3,12 @@ import { handleCommand } from "../commands.js";
|
|
|
3
3
|
import { runTurn } from "../controller.js";
|
|
4
4
|
import { resolveContextPolicy } from "../context/capacity.js";
|
|
5
5
|
import { compactContext } from "../context/compactor.js";
|
|
6
|
+
import { compactSession } from "../context/manual.js";
|
|
6
7
|
import { isContextOverflow, shouldResolveContextPolicy } from "../context/policy.js";
|
|
7
8
|
import { updateSettings } from "../settings.js";
|
|
8
9
|
import { saveTranscript } from "../transcript-export.js";
|
|
9
10
|
import { recordAuxiliaryUsage, recordUsage } from "../usage.js";
|
|
11
|
+
import { selectTimeline } from "../timeline.js";
|
|
10
12
|
import { answerAt } from "./approve.js";
|
|
11
13
|
import { cancel as cancelOpen } from "./overlay.js";
|
|
12
14
|
import { controllerOptions, turnFailure } from "./session-view.js";
|
|
@@ -14,6 +16,10 @@ import { transcribe } from "./turn.js";
|
|
|
14
16
|
const WAITING = "Waiting";
|
|
15
17
|
export function appWorkflows(options) {
|
|
16
18
|
const { session, state, permissions, feedback } = options;
|
|
19
|
+
const choose = (picker) => new Promise((resolve) => {
|
|
20
|
+
state.open = { picker, settle: resolve };
|
|
21
|
+
options.render();
|
|
22
|
+
});
|
|
17
23
|
async function command(text) {
|
|
18
24
|
const activity = options.startActivity("command", `Running ${text.split(/\s+/)[0]}`);
|
|
19
25
|
if (activity === undefined)
|
|
@@ -26,10 +32,7 @@ export function appWorkflows(options) {
|
|
|
26
32
|
state.open = { help: true, settle: resolve };
|
|
27
33
|
options.render();
|
|
28
34
|
}),
|
|
29
|
-
choose
|
|
30
|
-
state.open = { picker, settle: resolve };
|
|
31
|
-
options.render();
|
|
32
|
-
}),
|
|
35
|
+
choose,
|
|
33
36
|
dismiss: () => {
|
|
34
37
|
state.open = state.open === undefined ? undefined : cancelOpen(state.open);
|
|
35
38
|
options.render();
|
|
@@ -51,6 +54,7 @@ export function appWorkflows(options) {
|
|
|
51
54
|
state.follow = true;
|
|
52
55
|
state.unseen = 0;
|
|
53
56
|
state.lastMaxScroll = 0;
|
|
57
|
+
state.committedNodeId = 0;
|
|
54
58
|
},
|
|
55
59
|
permissions,
|
|
56
60
|
exportTranscript: () => saveTranscript(options.transcriptRoot, state.blocks),
|
|
@@ -58,6 +62,27 @@ export function appWorkflows(options) {
|
|
|
58
62
|
await updateSettings(patch);
|
|
59
63
|
},
|
|
60
64
|
refreshSettings: options.refreshSettings,
|
|
65
|
+
timeline: async () => {
|
|
66
|
+
const selected = await selectTimeline(session, choose);
|
|
67
|
+
if (!selected)
|
|
68
|
+
return "unchanged";
|
|
69
|
+
options.replaceTranscript();
|
|
70
|
+
return session.conversation.activeNodeId === state.committedNodeId
|
|
71
|
+
? "unchanged"
|
|
72
|
+
: "selected";
|
|
73
|
+
},
|
|
74
|
+
compact: async () => {
|
|
75
|
+
if (session.conversation.activeNodeId !== state.committedNodeId) {
|
|
76
|
+
return "branch-pending";
|
|
77
|
+
}
|
|
78
|
+
return compactSession(session, {
|
|
79
|
+
signal: activity.control.signal,
|
|
80
|
+
onStatus: (status) => {
|
|
81
|
+
state.status = status ?? activity.label;
|
|
82
|
+
options.render();
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
},
|
|
61
86
|
});
|
|
62
87
|
if (outcome === "exit")
|
|
63
88
|
state.closeWhenIdle = true;
|
|
@@ -128,6 +153,7 @@ export function appWorkflows(options) {
|
|
|
128
153
|
await session.persistence?.checkpoint(next);
|
|
129
154
|
session.conversation = next;
|
|
130
155
|
nodeId = next.activeNodeId;
|
|
156
|
+
state.committedNodeId = next.activeNodeId;
|
|
131
157
|
};
|
|
132
158
|
const compact = async (checkpoint, projected, reason, error) => {
|
|
133
159
|
if (reason === "overflow" && (error === undefined || !isContextOverflow(error))) {
|
package/dist/tui/app.js
CHANGED
|
@@ -32,6 +32,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
32
32
|
const workspace = await workspaceLabel(session.config.root);
|
|
33
33
|
const state = appState();
|
|
34
34
|
state.blocks.push(...session.conversation.transcript);
|
|
35
|
+
state.committedNodeId = session.conversation.activeNodeId;
|
|
35
36
|
const permissions = sessionPermissions(session.tools, session.config.autoApprove);
|
|
36
37
|
let closed;
|
|
37
38
|
let frameTimer;
|
|
@@ -116,6 +117,16 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
116
117
|
if (state.follow)
|
|
117
118
|
state.unseen = 0;
|
|
118
119
|
};
|
|
120
|
+
const replaceTranscript = () => {
|
|
121
|
+
state.blocks.splice(0, state.blocks.length, ...session.conversation.transcript);
|
|
122
|
+
state.scroll = 0;
|
|
123
|
+
state.follow = true;
|
|
124
|
+
state.unseen = 0;
|
|
125
|
+
state.lastMaxScroll = 0;
|
|
126
|
+
transcript.invalidate();
|
|
127
|
+
paint.invalidate();
|
|
128
|
+
render();
|
|
129
|
+
};
|
|
119
130
|
function quit() {
|
|
120
131
|
if (!live)
|
|
121
132
|
return;
|
|
@@ -187,6 +198,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
187
198
|
emit,
|
|
188
199
|
commandNotice,
|
|
189
200
|
render,
|
|
201
|
+
replaceTranscript,
|
|
190
202
|
refreshSettings: () => {
|
|
191
203
|
terminal.setReducedMotion(session.config.reducedMotion);
|
|
192
204
|
paint.invalidate();
|
|
@@ -229,14 +241,9 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
229
241
|
return;
|
|
230
242
|
try {
|
|
231
243
|
await launch.open(candidate.id);
|
|
232
|
-
|
|
244
|
+
replaceTranscript();
|
|
245
|
+
state.committedNodeId = session.conversation.activeNodeId;
|
|
233
246
|
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
247
|
finishActivity(activity);
|
|
241
248
|
return;
|
|
242
249
|
}
|
|
@@ -5,11 +5,13 @@ export function renderNotice(block, width, pal) {
|
|
|
5
5
|
warn: pal.ink.attention,
|
|
6
6
|
error: pal.ink.removed,
|
|
7
7
|
};
|
|
8
|
-
const mark = block.tone === "error" ? "× " : block.tone === "warn" ? "! " :
|
|
8
|
+
const mark = block.tone === "error" ? "× " : block.tone === "warn" ? "! " : undefined;
|
|
9
9
|
return [
|
|
10
10
|
"",
|
|
11
11
|
...wrap(block.text, Math.max(1, width - 3)).map((line, index) => row(width, [
|
|
12
|
-
|
|
12
|
+
...(mark === undefined
|
|
13
|
+
? []
|
|
14
|
+
: [{ text: index === 0 ? mark : " ", fg: fg[block.tone], bold: index === 0 }]),
|
|
13
15
|
{ text: line, fg: fg[block.tone] },
|
|
14
16
|
], [], undefined, 1)),
|
|
15
17
|
];
|