@moikapy/lich 0.7.0 → 0.8.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/CHANGELOG.md +75 -5
- package/README.md +38 -9
- package/dist/{chunk-QVJCIZIF.js → chunk-EDRUZF22.js} +763 -286
- package/dist/chunk-EDRUZF22.js.map +1 -0
- package/dist/chunk-PL6MKRKE.js +75 -0
- package/dist/chunk-PL6MKRKE.js.map +1 -0
- package/dist/{chunk-JYURFAGB.js → chunk-QOTECFCN.js} +2 -2
- package/dist/chunk-VKEOHUCB.js +86 -0
- package/dist/chunk-VKEOHUCB.js.map +1 -0
- package/dist/{chunk-SAEB3QL3.js → chunk-W6JXZBUE.js} +2 -2
- package/dist/cli.d.ts +19 -1
- package/dist/cli.js +53 -11
- package/dist/cli.js.map +1 -1
- package/dist/{gateway-5BG3YCZF.js → gateway-WU5G4ODO.js} +109 -49
- package/dist/gateway-WU5G4ODO.js.map +1 -0
- package/dist/index.d.ts +46 -3
- package/dist/index.js +3 -2
- package/dist/resolve-H22TOVB5.js +7 -0
- package/dist/resolve-H22TOVB5.js.map +1 -0
- package/dist/store-COOLBAHB.js +9 -0
- package/dist/store-COOLBAHB.js.map +1 -0
- package/dist/{tui-L6RABP2J.js → tui-4BP3TI7J.js} +224 -30
- package/dist/tui-4BP3TI7J.js.map +1 -0
- package/docs/architecture/agent-loop.md +28 -9
- package/docs/architecture/overview.md +10 -10
- package/docs/architecture/tools.md +30 -3
- package/docs/getting-started.md +3 -3
- package/docs/index.md +1 -1
- package/docs/user-guide/cli.md +13 -5
- package/docs/user-guide/games.md +3 -3
- package/docs/user-guide/gateway.md +70 -16
- package/docs/user-guide/library.md +6 -3
- package/docs/user-guide/redot.md +2 -2
- package/docs/user-guide/tui.md +3 -2
- package/package.json +1 -1
- package/dist/chunk-QVJCIZIF.js.map +0 -1
- package/dist/gateway-5BG3YCZF.js.map +0 -1
- package/dist/tui-L6RABP2J.js.map +0 -1
- /package/dist/{chunk-JYURFAGB.js.map → chunk-QOTECFCN.js.map} +0 -0
- /package/dist/{chunk-SAEB3QL3.js.map → chunk-W6JXZBUE.js.map} +0 -0
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
2
|
fill_template,
|
|
3
3
|
load_theme
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-QOTECFCN.js";
|
|
5
5
|
import {
|
|
6
6
|
LICH_VERSION
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-W6JXZBUE.js";
|
|
8
|
+
import {
|
|
9
|
+
resolve_session_path
|
|
10
|
+
} from "./chunk-PL6MKRKE.js";
|
|
8
11
|
import {
|
|
9
12
|
create_agent_with_plugins,
|
|
13
|
+
logger
|
|
14
|
+
} from "./chunk-EDRUZF22.js";
|
|
15
|
+
import {
|
|
16
|
+
open_session,
|
|
17
|
+
read_session_messages,
|
|
10
18
|
truncate_text
|
|
11
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-VKEOHUCB.js";
|
|
12
20
|
|
|
13
21
|
// src/tui.tsx
|
|
14
22
|
import { render } from "ink";
|
|
@@ -18,6 +26,9 @@ import { useCallback, useEffect as useEffect2, useRef, useState as useState3 } f
|
|
|
18
26
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
19
27
|
import { readdir, stat } from "fs/promises";
|
|
20
28
|
|
|
29
|
+
// src/tui/load_resume.ts
|
|
30
|
+
import path from "path";
|
|
31
|
+
|
|
21
32
|
// src/tui/state.ts
|
|
22
33
|
var INITIAL_UI_STATE = {
|
|
23
34
|
phase: "idle",
|
|
@@ -59,7 +70,7 @@ function apply_event(state, event) {
|
|
|
59
70
|
...state,
|
|
60
71
|
phase: "thinking",
|
|
61
72
|
active_tool: void 0,
|
|
62
|
-
last_error: event.result.ok === true ? state.last_error : event.result.error ?? "tool failed"
|
|
73
|
+
last_error: event.cancelled === true || event.result.ok === true ? state.last_error : event.result.error ?? "tool failed"
|
|
63
74
|
};
|
|
64
75
|
case "turn_end":
|
|
65
76
|
return { ...state, turns_used: event.turn };
|
|
@@ -97,9 +108,65 @@ function parse_command(raw_input) {
|
|
|
97
108
|
function tui_banner_text(theme, version, model, kind) {
|
|
98
109
|
return fill_template(theme.welcome, { version, model, kind });
|
|
99
110
|
}
|
|
111
|
+
function resume_banner_count(messages) {
|
|
112
|
+
if (messages === void 0) {
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
return messages.filter((message) => message.role !== "system").length;
|
|
116
|
+
}
|
|
117
|
+
function resume_banner_line(id, message_count) {
|
|
118
|
+
return `resumed ${id} (${message_count} messages)`;
|
|
119
|
+
}
|
|
100
120
|
function format_usage(total_tokens) {
|
|
101
121
|
return total_tokens.toLocaleString("en-US");
|
|
102
122
|
}
|
|
123
|
+
function assistant_tool_line(call) {
|
|
124
|
+
const args_json = JSON.stringify(call.args) ?? "{}";
|
|
125
|
+
return ` \u23BF ${truncate_text(args_json, TOOL_ARGS_PREVIEW_CHARS)}`;
|
|
126
|
+
}
|
|
127
|
+
function format_message_lines(message, theme) {
|
|
128
|
+
if (message.role === "user") {
|
|
129
|
+
return [`${theme.user_label} \u203A ${message.content}`];
|
|
130
|
+
}
|
|
131
|
+
if (message.role === "assistant") {
|
|
132
|
+
const lines = [`${theme.response_label} \u203A ${message.content}`];
|
|
133
|
+
for (const call of message.tool_calls ?? []) {
|
|
134
|
+
lines.push(assistant_tool_line(call));
|
|
135
|
+
}
|
|
136
|
+
return lines;
|
|
137
|
+
}
|
|
138
|
+
if (message.role === "tool") {
|
|
139
|
+
const flag = message.is_error === true ? "error" : "ok";
|
|
140
|
+
return [` \u23BF ${message.name}: ${flag} (${truncate_text(message.content, TOOL_OUTPUT_PREVIEW_CHARS)})`];
|
|
141
|
+
}
|
|
142
|
+
return [`\xB7 system: ${message.content}`];
|
|
143
|
+
}
|
|
144
|
+
function format_message_block(message, theme) {
|
|
145
|
+
if (message.role === "tool") {
|
|
146
|
+
const ok = message.is_error !== true;
|
|
147
|
+
return { role: ok === true ? "tool" : "error", lines: format_message_lines(message, theme) };
|
|
148
|
+
}
|
|
149
|
+
const roles = {
|
|
150
|
+
system: "meta",
|
|
151
|
+
user: "user",
|
|
152
|
+
assistant: "lich"
|
|
153
|
+
};
|
|
154
|
+
return { role: roles[message.role], lines: format_message_lines(message, theme) };
|
|
155
|
+
}
|
|
156
|
+
function split_history_blocks(messages, cap, theme) {
|
|
157
|
+
const visible = messages.filter((message) => message.role !== "system");
|
|
158
|
+
const start = Math.max(0, visible.length - cap);
|
|
159
|
+
return visible.slice(start).map((message) => format_message_block(message, theme));
|
|
160
|
+
}
|
|
161
|
+
function resume_session_view(id, messages, theme, cap = HISTORY_CAP) {
|
|
162
|
+
const banner_line = resume_banner_line(id, resume_banner_count(messages));
|
|
163
|
+
const notice = { role: "meta", lines: [`\xB7 ${banner_line}`] };
|
|
164
|
+
const history_cap = Math.max(0, cap - 1);
|
|
165
|
+
return { blocks: [notice, ...split_history_blocks(messages, history_cap, theme)], banner_line };
|
|
166
|
+
}
|
|
167
|
+
function resume_missing_args_block() {
|
|
168
|
+
return { role: "error", lines: ["\xB7 /resume requires <id|latest>"] };
|
|
169
|
+
}
|
|
103
170
|
function assistant_result_block(message, theme) {
|
|
104
171
|
if (message.content.length === 0) {
|
|
105
172
|
return void 0;
|
|
@@ -168,10 +235,26 @@ function session_list_block(entries, theme, cap = 10) {
|
|
|
168
235
|
return { role: "meta", lines };
|
|
169
236
|
}
|
|
170
237
|
var HELP_LINES = [
|
|
171
|
-
"commands: /help /model /usage /clear /sessions /exit (aliases: /quit /q)",
|
|
238
|
+
"commands: /help /model /usage /clear /sessions /resume /exit (aliases: /quit /q)",
|
|
172
239
|
"enter submits \xB7 backspace deletes \xB7 up/down recalls history \xB7 pasted newlines become spaces"
|
|
173
240
|
];
|
|
174
241
|
|
|
242
|
+
// src/tui/load_resume.ts
|
|
243
|
+
function run_error_text(error) {
|
|
244
|
+
return error instanceof Error ? error.message : String(error);
|
|
245
|
+
}
|
|
246
|
+
async function load_resume_view(session_dir, value, theme) {
|
|
247
|
+
try {
|
|
248
|
+
const transcript = await resolve_session_path(session_dir, value);
|
|
249
|
+
const messages = await read_session_messages(transcript);
|
|
250
|
+
const id = path.basename(transcript, ".jsonl");
|
|
251
|
+
const view = resume_session_view(id, messages, theme);
|
|
252
|
+
return { ok: true, id, messages, blocks: view.blocks, banner_line: view.banner_line };
|
|
253
|
+
} catch (error) {
|
|
254
|
+
return { ok: false, block: error_notice_block(run_error_text(error)) };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
175
258
|
// src/tui/message_view.tsx
|
|
176
259
|
import { useEffect, useState } from "react";
|
|
177
260
|
import { Box, Text } from "ink";
|
|
@@ -242,6 +325,9 @@ function CommandBar({ busy, on_submit }) {
|
|
|
242
325
|
const [recall_ring, set_recall_ring] = useState2([]);
|
|
243
326
|
const [recall_index, set_recall_index] = useState2(void 0);
|
|
244
327
|
const submit_buffer = () => {
|
|
328
|
+
if (busy === true) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
245
331
|
const text = buffer.trim();
|
|
246
332
|
set_buffer("");
|
|
247
333
|
set_recall_index(void 0);
|
|
@@ -254,6 +340,14 @@ function CommandBar({ busy, on_submit }) {
|
|
|
254
340
|
if (recall_ring.length === 0) {
|
|
255
341
|
return;
|
|
256
342
|
}
|
|
343
|
+
if (direction === -1 && recall_index === 0) {
|
|
344
|
+
set_recall_index(void 0);
|
|
345
|
+
set_buffer("");
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (direction === -1 && recall_index === void 0) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
257
351
|
const current = recall_index ?? -direction;
|
|
258
352
|
const next = Math.min(Math.max(current + direction, 0), recall_ring.length - 1);
|
|
259
353
|
set_recall_index(next);
|
|
@@ -265,11 +359,11 @@ function CommandBar({ busy, on_submit }) {
|
|
|
265
359
|
return;
|
|
266
360
|
}
|
|
267
361
|
if (key.upArrow === true) {
|
|
268
|
-
walk_recall(
|
|
362
|
+
walk_recall(1);
|
|
269
363
|
return;
|
|
270
364
|
}
|
|
271
365
|
if (key.downArrow === true) {
|
|
272
|
-
walk_recall(1);
|
|
366
|
+
walk_recall(-1);
|
|
273
367
|
return;
|
|
274
368
|
}
|
|
275
369
|
if (key.backspace === true || key.delete === true) {
|
|
@@ -295,6 +389,9 @@ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
|
295
389
|
var SESSION_LIST_CAP = 10;
|
|
296
390
|
function event_blocks(event, theme) {
|
|
297
391
|
if (event.type === "tool_call_end") {
|
|
392
|
+
if (event.cancelled === true) {
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
298
395
|
return [tool_result_block(event.call, event.result.ok === true, event.result.output)];
|
|
299
396
|
}
|
|
300
397
|
if (event.type === "compress_end") {
|
|
@@ -305,10 +402,10 @@ function event_blocks(event, theme) {
|
|
|
305
402
|
}
|
|
306
403
|
return [];
|
|
307
404
|
}
|
|
308
|
-
async function run_agent_turn(agent, history, input, on_event, on_done, signal) {
|
|
405
|
+
async function run_agent_turn(agent, history, input, on_event, on_done, signal, session) {
|
|
309
406
|
const stop_listening = agent.events.on(on_event);
|
|
310
407
|
try {
|
|
311
|
-
const result = await agent.run({ input, history, signal, label: "tui" });
|
|
408
|
+
const result = await agent.run({ input, history, signal, label: "tui", session });
|
|
312
409
|
on_done(result);
|
|
313
410
|
} finally {
|
|
314
411
|
stop_listening();
|
|
@@ -330,12 +427,15 @@ async function sessions_block(config, theme) {
|
|
|
330
427
|
return { role: "meta", lines: ["\xB7 no session files yet"] };
|
|
331
428
|
}
|
|
332
429
|
}
|
|
333
|
-
function
|
|
430
|
+
function run_error_text2(error) {
|
|
334
431
|
return error instanceof Error ? error.message : String(error);
|
|
335
432
|
}
|
|
336
|
-
function use_agent_run(agent, theme, add_blocks, set_state, set_blocks) {
|
|
337
|
-
const history_ref = useRef([]);
|
|
433
|
+
function use_agent_run(agent, theme, add_blocks, set_state, set_blocks, initial_history, session) {
|
|
434
|
+
const history_ref = useRef(initial_history ?? []);
|
|
338
435
|
const controller_ref = useRef(void 0);
|
|
436
|
+
const set_history = useCallback((messages) => {
|
|
437
|
+
history_ref.current = messages;
|
|
438
|
+
}, []);
|
|
339
439
|
const finish_run = useCallback((result) => {
|
|
340
440
|
history_ref.current = result.messages;
|
|
341
441
|
set_state((current) => apply_run_result(current, result));
|
|
@@ -351,8 +451,8 @@ function use_agent_run(agent, theme, add_blocks, set_state, set_blocks) {
|
|
|
351
451
|
set_state((current) => apply_event(current, event));
|
|
352
452
|
set_blocks((current) => [...current, ...event_blocks(event, theme)].slice(-HISTORY_CAP));
|
|
353
453
|
};
|
|
354
|
-
void run_agent_turn(agent, history_ref.current, text, on_event, finish_run, controller.signal).catch((error) => {
|
|
355
|
-
add_blocks([error_notice_block(
|
|
454
|
+
void run_agent_turn(agent, history_ref.current, text, on_event, finish_run, controller.signal, session).catch((error) => {
|
|
455
|
+
add_blocks([error_notice_block(run_error_text2(error))]);
|
|
356
456
|
set_state((current) => ({ ...current, phase: "idle" }));
|
|
357
457
|
}).finally(() => {
|
|
358
458
|
if (controller_ref.current === controller) {
|
|
@@ -360,12 +460,12 @@ function use_agent_run(agent, theme, add_blocks, set_state, set_blocks) {
|
|
|
360
460
|
}
|
|
361
461
|
});
|
|
362
462
|
},
|
|
363
|
-
[agent, add_blocks, finish_run, set_blocks, set_state, theme]
|
|
463
|
+
[agent, add_blocks, finish_run, session, set_blocks, set_state, theme]
|
|
364
464
|
);
|
|
365
465
|
useEffect2(() => () => controller_ref.current?.abort(), []);
|
|
366
|
-
return start_message_run;
|
|
466
|
+
return { start_message_run, set_history };
|
|
367
467
|
}
|
|
368
|
-
function use_slash_commands(agent, theme, add_blocks, set_blocks, total_tokens) {
|
|
468
|
+
function use_slash_commands(agent, theme, add_blocks, set_blocks, set_history, set_resume_line, begin_resume_load, end_resume_load, total_tokens) {
|
|
369
469
|
const handle = useCallback(
|
|
370
470
|
(parsed) => {
|
|
371
471
|
if (parsed.name === "exit" || parsed.name === "quit" || parsed.name === "q") {
|
|
@@ -382,27 +482,100 @@ function use_slash_commands(agent, theme, add_blocks, set_blocks, total_tokens)
|
|
|
382
482
|
set_blocks([]);
|
|
383
483
|
} else if (parsed.name === "sessions") {
|
|
384
484
|
void sessions_block(agent.config, theme).then((block) => add_blocks([block]));
|
|
485
|
+
} else if (parsed.name === "resume") {
|
|
486
|
+
if (parsed.args.length === 0) {
|
|
487
|
+
add_blocks([resume_missing_args_block()]);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
begin_resume_load();
|
|
491
|
+
void load_resume_view(agent.config.session_dir, parsed.args, theme).then((result) => {
|
|
492
|
+
if (result.ok === false) {
|
|
493
|
+
add_blocks([result.block]);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
set_history(result.messages);
|
|
497
|
+
set_blocks(result.blocks);
|
|
498
|
+
set_resume_line(result.banner_line);
|
|
499
|
+
}).finally(() => {
|
|
500
|
+
end_resume_load();
|
|
501
|
+
});
|
|
385
502
|
} else {
|
|
386
503
|
add_blocks([unknown_command_block(parsed.name)]);
|
|
387
504
|
}
|
|
388
505
|
},
|
|
389
|
-
[
|
|
506
|
+
[
|
|
507
|
+
agent,
|
|
508
|
+
add_blocks,
|
|
509
|
+
begin_resume_load,
|
|
510
|
+
end_resume_load,
|
|
511
|
+
set_blocks,
|
|
512
|
+
set_history,
|
|
513
|
+
set_resume_line,
|
|
514
|
+
theme,
|
|
515
|
+
total_tokens
|
|
516
|
+
]
|
|
390
517
|
);
|
|
391
518
|
return handle;
|
|
392
519
|
}
|
|
393
|
-
function
|
|
394
|
-
|
|
520
|
+
function initial_blocks(history, theme) {
|
|
521
|
+
if (history === void 0 || history.length === 0) {
|
|
522
|
+
return [];
|
|
523
|
+
}
|
|
524
|
+
return split_history_blocks(history, HISTORY_CAP, theme);
|
|
525
|
+
}
|
|
526
|
+
function initial_resume_line(resumed_id, history) {
|
|
527
|
+
if (resumed_id === void 0) {
|
|
528
|
+
return void 0;
|
|
529
|
+
}
|
|
530
|
+
return resume_banner_line(resumed_id, resume_banner_count(history));
|
|
531
|
+
}
|
|
532
|
+
function TuiApp({ agent, theme, session, initial_history, resumed_id }) {
|
|
533
|
+
const [blocks, set_blocks] = useState3(() => initial_blocks(initial_history, theme));
|
|
395
534
|
const [state, set_state] = useState3(INITIAL_UI_STATE);
|
|
535
|
+
const [resume_line, set_resume_line] = useState3(
|
|
536
|
+
() => initial_resume_line(resumed_id, initial_history)
|
|
537
|
+
);
|
|
538
|
+
const resume_loading_ref = useRef(false);
|
|
539
|
+
const [resume_loading, set_resume_loading] = useState3(false);
|
|
540
|
+
const begin_resume_load = useCallback(() => {
|
|
541
|
+
resume_loading_ref.current = true;
|
|
542
|
+
set_resume_loading(true);
|
|
543
|
+
}, []);
|
|
544
|
+
const end_resume_load = useCallback(() => {
|
|
545
|
+
resume_loading_ref.current = false;
|
|
546
|
+
set_resume_loading(false);
|
|
547
|
+
}, []);
|
|
396
548
|
const add_blocks = useCallback((added) => {
|
|
397
549
|
if (added.length === 0) {
|
|
398
550
|
return;
|
|
399
551
|
}
|
|
400
552
|
set_blocks((current) => [...current, ...added].slice(-HISTORY_CAP));
|
|
401
553
|
}, []);
|
|
402
|
-
const start_message_run = use_agent_run(
|
|
403
|
-
|
|
554
|
+
const { start_message_run, set_history } = use_agent_run(
|
|
555
|
+
agent,
|
|
556
|
+
theme,
|
|
557
|
+
add_blocks,
|
|
558
|
+
set_state,
|
|
559
|
+
set_blocks,
|
|
560
|
+
initial_history,
|
|
561
|
+
session
|
|
562
|
+
);
|
|
563
|
+
const handle_slash = use_slash_commands(
|
|
564
|
+
agent,
|
|
565
|
+
theme,
|
|
566
|
+
add_blocks,
|
|
567
|
+
set_blocks,
|
|
568
|
+
set_history,
|
|
569
|
+
set_resume_line,
|
|
570
|
+
begin_resume_load,
|
|
571
|
+
end_resume_load,
|
|
572
|
+
state.usage.total_tokens
|
|
573
|
+
);
|
|
404
574
|
const submit = useCallback(
|
|
405
575
|
(text) => {
|
|
576
|
+
if (resume_loading_ref.current === true) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
406
579
|
const parsed = parse_command(text);
|
|
407
580
|
if (parsed.kind === "message") {
|
|
408
581
|
if (parsed.text.length > 0) {
|
|
@@ -415,24 +588,45 @@ function TuiApp({ agent, theme }) {
|
|
|
415
588
|
[handle_slash, start_message_run]
|
|
416
589
|
);
|
|
417
590
|
const provider = agent.config.providers[0];
|
|
591
|
+
const banner = tui_banner_text(theme, LICH_VERSION, provider?.model ?? "unknown", provider?.kind ?? "unknown");
|
|
418
592
|
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", minHeight: 8, children: [
|
|
419
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children:
|
|
593
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: resume_line === void 0 ? banner : `${banner}
|
|
594
|
+
${resume_line}` }),
|
|
420
595
|
/* @__PURE__ */ jsx4(MessageView, { blocks, state }),
|
|
421
596
|
/* @__PURE__ */ jsx4(StatusBar, { state, model: provider?.model ?? "unknown", theme }),
|
|
422
|
-
/* @__PURE__ */ jsx4(CommandBar, { busy: state.phase !== "idle", on_submit: submit })
|
|
597
|
+
/* @__PURE__ */ jsx4(CommandBar, { busy: state.phase !== "idle" || resume_loading, on_submit: submit })
|
|
423
598
|
] });
|
|
424
599
|
}
|
|
425
600
|
|
|
426
601
|
// src/tui.tsx
|
|
427
602
|
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
428
|
-
async function run_tui(config) {
|
|
603
|
+
async function run_tui(config, options = {}) {
|
|
429
604
|
const agent = await create_agent_with_plugins(config);
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
605
|
+
try {
|
|
606
|
+
const theme = load_theme(config.theme);
|
|
607
|
+
const session = await open_session(config.session_dir, "tui").catch((error) => {
|
|
608
|
+
logger.warn("session persistence failed; continuing without transcript", error);
|
|
609
|
+
return void 0;
|
|
610
|
+
});
|
|
611
|
+
const instance = render(
|
|
612
|
+
/* @__PURE__ */ jsx5(
|
|
613
|
+
TuiApp,
|
|
614
|
+
{
|
|
615
|
+
agent,
|
|
616
|
+
theme,
|
|
617
|
+
session,
|
|
618
|
+
initial_history: options.initial_history,
|
|
619
|
+
resumed_id: options.resumed_id
|
|
620
|
+
}
|
|
621
|
+
)
|
|
622
|
+
);
|
|
623
|
+
await instance.waitUntilExit();
|
|
624
|
+
return 0;
|
|
625
|
+
} finally {
|
|
626
|
+
agent.close();
|
|
627
|
+
}
|
|
434
628
|
}
|
|
435
629
|
export {
|
|
436
630
|
run_tui
|
|
437
631
|
};
|
|
438
|
-
//# sourceMappingURL=tui-
|
|
632
|
+
//# sourceMappingURL=tui-4BP3TI7J.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tui.tsx","../src/tui/app.tsx","../src/tui/load_resume.ts","../src/tui/state.ts","../src/tui/message_view.tsx","../src/tui/status_bar.tsx","../src/tui/command_bar.tsx"],"sourcesContent":["/**\n * TUI entry: builds the agent from the parsed config and renders the ink app\n * until exit. The CLI dynamic-imports this module for `lich tui`.\n */\nimport { render } from \"ink\";\nimport { create_agent_with_plugins } from \"./agent/agent.js\";\nimport type { AgentConfig } from \"./agent/config.js\";\nimport type { Message } from \"./providers/types.js\";\nimport { open_session } from \"./session/store.js\";\nimport { TuiApp } from \"./tui/app.js\";\nimport { load_theme } from \"./util/theme.js\";\nimport { logger } from \"./util/log.js\";\n\nexport interface RunTuiOptions {\n readonly initial_history?: readonly Message[];\n readonly resumed_id?: string;\n}\n\nexport async function run_tui(config: AgentConfig, options: RunTuiOptions = {}): Promise<number> {\n const agent = await create_agent_with_plugins(config);\n try {\n const theme = load_theme(config.theme);\n const session = await open_session(config.session_dir, \"tui\").catch((error: unknown) => {\n logger.warn(\"session persistence failed; continuing without transcript\", error);\n return undefined;\n });\n const instance = render(\n <TuiApp\n agent={agent}\n theme={theme}\n session={session}\n initial_history={options.initial_history}\n resumed_id={options.resumed_id}\n />,\n );\n await instance.waitUntilExit();\n return 0;\n } finally {\n agent.close();\n }\n}\n","/**\n * Root ink component for the lich TUI: wires agent events into the UI state\n * machine, drives agent.run with history continuity, and lays out header,\n * transcript, status bar, and the command input row.\n */\nimport { useCallback, useEffect, useRef, useState, type Dispatch, type SetStateAction } from \"react\";\nimport { Box, Text } from \"ink\";\nimport type { Agent, AgentRunResult } from \"../agent/agent.js\";\nimport type { AgentEvent } from \"../agent/events.js\";\nimport type { AgentConfig } from \"../agent/config.js\";\nimport type { Message } from \"../providers/types.js\";\nimport type { SessionHandle } from \"../session/store.js\";\nimport { LICH_VERSION } from \"../index.js\";\nimport type { ThemeSpec } from \"../util/lore.js\";\nimport { readdir, stat } from \"node:fs/promises\";\nimport { load_resume_view } from \"./load_resume.js\";\nimport {\n apply_event,\n apply_run_result,\n compress_notice_block,\n error_notice_block,\n help_block,\n HISTORY_CAP,\n INITIAL_UI_STATE,\n model_label_block,\n parse_command,\n tui_banner_text,\n resume_banner_count,\n resume_banner_line,\n resume_missing_args_block,\n run_notice_blocks,\n session_list_block,\n split_history_blocks,\n tool_result_block,\n unknown_command_block,\n usage_notice_block,\n type HistoryBlock,\n type ParsedInput,\n type SessionEntryInfo,\n type UiState,\n} from \"./state.js\";\nimport { MessageView } from \"./message_view.js\";\nimport { StatusBar } from \"./status_bar.js\";\nimport { CommandBar } from \"./command_bar.js\";\n\nconst SESSION_LIST_CAP = 10;\n\ntype SlashInput = Extract<ParsedInput, { kind: \"slash\" }>;\ntype AddBlocks = (added: readonly HistoryBlock[]) => void;\ntype SetUiState = Dispatch<SetStateAction<UiState>>;\ntype SetBlocks = Dispatch<SetStateAction<readonly HistoryBlock[]>>;\ntype SetHistory = (messages: readonly Message[]) => void;\n\ninterface AgentRunControls {\n readonly start_message_run: (text: string) => void;\n readonly set_history: SetHistory;\n}\n\n/** Map one agent event to optional transcript blocks (tool rows, notices). */\nfunction event_blocks(event: AgentEvent, theme: ThemeSpec): readonly HistoryBlock[] {\n if (event.type === \"tool_call_end\") {\n if (event.cancelled === true) {\n return [];\n }\n return [tool_result_block(event.call, event.result.ok === true, event.result.output)];\n }\n if (event.type === \"compress_end\") {\n return [compress_notice_block(event.summary_chars, theme)];\n }\n if (event.type === \"error\") {\n return [error_notice_block(event.error instanceof Error ? event.error.message : String(event.error))];\n }\n return [];\n}\n\n/** One agent turn: subscribe to events, run, unsubscribe in finally. */\nasync function run_agent_turn(\n agent: Agent,\n history: readonly Message[],\n input: string,\n on_event: (event: AgentEvent) => void,\n on_done: (result: AgentRunResult) => void,\n signal: AbortSignal,\n session: SessionHandle | undefined,\n): Promise<void> {\n const stop_listening = agent.events.on(on_event);\n try {\n const result = await agent.run({ input, history, signal, label: \"tui\", session });\n on_done(result);\n } finally {\n stop_listening();\n }\n}\n\n/** Async /sessions listing as a meta block (never throws). */\nasync function sessions_block(config: AgentConfig, theme: ThemeSpec): Promise<HistoryBlock> {\n try {\n const dir_entries = await readdir(config.session_dir, { withFileTypes: true });\n const entries: SessionEntryInfo[] = [];\n for (const entry of dir_entries) {\n if (entry.isFile() === false || entry.name.endsWith(\".jsonl\") === false) {\n continue;\n }\n const info = await stat(`${config.session_dir}/${entry.name}`);\n entries.push({ name: entry.name, size_bytes: info.size, mtime_ms: info.mtimeMs });\n }\n return session_list_block(entries, theme, SESSION_LIST_CAP);\n } catch {\n return { role: \"meta\", lines: [\"· no session files yet\"] };\n }\n}\n\nfunction run_error_text(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Runs one message exchange; owns history continuity and abort wiring. */\nfunction use_agent_run(\n agent: Agent,\n theme: ThemeSpec,\n add_blocks: AddBlocks,\n set_state: SetUiState,\n set_blocks: SetBlocks,\n initial_history: readonly Message[] | undefined,\n session: SessionHandle | undefined,\n): AgentRunControls {\n const history_ref = useRef<readonly Message[]>(initial_history ?? []);\n const controller_ref = useRef<AbortController | undefined>(undefined);\n\n const set_history = useCallback<SetHistory>((messages: readonly Message[]): void => {\n history_ref.current = messages;\n }, []);\n\n const finish_run = useCallback((result: AgentRunResult): void => {\n history_ref.current = result.messages;\n set_state((current) => apply_run_result(current, result));\n add_blocks(run_notice_blocks(result, theme));\n }, [add_blocks, set_state, theme]);\n\n const start_message_run = useCallback(\n (text: string): void => {\n add_blocks([{ role: \"user\", lines: [`${theme.user_label} › ${text}`] }]);\n set_state((current) => ({ ...current, phase: \"thinking\", active_tool: undefined }));\n const controller = new AbortController();\n controller_ref.current = controller;\n const on_event = (event: AgentEvent): void => {\n set_state((current) => apply_event(current, event));\n set_blocks((current) => [...current, ...event_blocks(event, theme)].slice(-HISTORY_CAP));\n };\n void run_agent_turn(agent, history_ref.current, text, on_event, finish_run, controller.signal, session)\n .catch((error: unknown) => {\n add_blocks([error_notice_block(run_error_text(error))]);\n set_state((current) => ({ ...current, phase: \"idle\" }));\n })\n .finally(() => {\n if (controller_ref.current === controller) {\n controller_ref.current = undefined;\n }\n });\n },\n [agent, add_blocks, finish_run, session, set_blocks, set_state, theme],\n );\n\n useEffect(() => () => controller_ref.current?.abort(), []);\n\n return { start_message_run, set_history };\n}\n\n/** Slash-command dispatch: pure client-side actions, never hits the agent. */\nfunction use_slash_commands(\n agent: Agent,\n theme: ThemeSpec,\n add_blocks: AddBlocks,\n set_blocks: SetBlocks,\n set_history: SetHistory,\n set_resume_line: Dispatch<SetStateAction<string | undefined>>,\n begin_resume_load: () => void,\n end_resume_load: () => void,\n total_tokens: number,\n): (parsed: SlashInput) => void {\n const handle = useCallback(\n (parsed: SlashInput): void => {\n if (parsed.name === \"exit\" || parsed.name === \"quit\" || parsed.name === \"q\") {\n process.exit(0);\n return;\n }\n if (parsed.name === \"help\") {\n add_blocks([help_block()]);\n } else if (parsed.name === \"model\") {\n add_blocks([model_label_block(agent.config)]);\n } else if (parsed.name === \"usage\") {\n add_blocks([usage_notice_block(total_tokens)]);\n } else if (parsed.name === \"clear\") {\n set_blocks([]);\n } else if (parsed.name === \"sessions\") {\n void sessions_block(agent.config, theme).then((block) => add_blocks([block]));\n } else if (parsed.name === \"resume\") {\n if (parsed.args.length === 0) {\n add_blocks([resume_missing_args_block()]);\n return;\n }\n begin_resume_load();\n void load_resume_view(agent.config.session_dir, parsed.args, theme)\n .then((result) => {\n if (result.ok === false) {\n add_blocks([result.block]);\n return;\n }\n set_history(result.messages);\n set_blocks(result.blocks);\n set_resume_line(result.banner_line);\n })\n .finally(() => {\n end_resume_load();\n });\n } else {\n add_blocks([unknown_command_block(parsed.name)]);\n }\n },\n [\n agent,\n add_blocks,\n begin_resume_load,\n end_resume_load,\n set_blocks,\n set_history,\n set_resume_line,\n theme,\n total_tokens,\n ],\n );\n return handle;\n}\n\ninterface TuiAppProps {\n readonly agent: Agent;\n readonly theme: ThemeSpec;\n readonly session?: SessionHandle;\n readonly initial_history?: readonly Message[];\n readonly resumed_id?: string;\n}\n\nfunction initial_blocks(history: readonly Message[] | undefined, theme: ThemeSpec): readonly HistoryBlock[] {\n if (history === undefined || history.length === 0) {\n return [];\n }\n return split_history_blocks(history, HISTORY_CAP, theme);\n}\n\nfunction initial_resume_line(\n resumed_id: string | undefined,\n history: readonly Message[] | undefined,\n): string | undefined {\n if (resumed_id === undefined) {\n return undefined;\n }\n return resume_banner_line(resumed_id, resume_banner_count(history));\n}\n\nexport function TuiApp({ agent, theme, session, initial_history, resumed_id }: TuiAppProps): React.JSX.Element {\n const [blocks, set_blocks] = useState<readonly HistoryBlock[]>(() => initial_blocks(initial_history, theme));\n const [state, set_state] = useState(INITIAL_UI_STATE);\n const [resume_line, set_resume_line] = useState<string | undefined>(() =>\n initial_resume_line(resumed_id, initial_history),\n );\n const resume_loading_ref = useRef(false);\n const [resume_loading, set_resume_loading] = useState(false);\n\n const begin_resume_load = useCallback((): void => {\n resume_loading_ref.current = true;\n set_resume_loading(true);\n }, []);\n\n const end_resume_load = useCallback((): void => {\n resume_loading_ref.current = false;\n set_resume_loading(false);\n }, []);\n\n const add_blocks = useCallback<AddBlocks>((added: readonly HistoryBlock[]): void => {\n if (added.length === 0) {\n return;\n }\n set_blocks((current) => [...current, ...added].slice(-HISTORY_CAP));\n }, []);\n\n const { start_message_run, set_history } = use_agent_run(\n agent,\n theme,\n add_blocks,\n set_state,\n set_blocks,\n initial_history,\n session,\n );\n const handle_slash = use_slash_commands(\n agent,\n theme,\n add_blocks,\n set_blocks,\n set_history,\n set_resume_line,\n begin_resume_load,\n end_resume_load,\n state.usage.total_tokens,\n );\n\n const submit = useCallback(\n (text: string): void => {\n if (resume_loading_ref.current === true) {\n return;\n }\n const parsed = parse_command(text);\n if (parsed.kind === \"message\") {\n if (parsed.text.length > 0) {\n start_message_run(parsed.text);\n }\n return;\n }\n handle_slash(parsed);\n },\n [handle_slash, start_message_run],\n );\n\n const provider = agent.config.providers[0];\n const banner = tui_banner_text(theme, LICH_VERSION, provider?.model ?? \"unknown\", provider?.kind ?? \"unknown\");\n return (\n <Box flexDirection=\"column\" minHeight={8}>\n <Text dimColor>{resume_line === undefined ? banner : `${banner}\\n${resume_line}`}</Text>\n <MessageView blocks={blocks} state={state} />\n <StatusBar state={state} model={provider?.model ?? \"unknown\"} theme={theme} />\n <CommandBar busy={state.phase !== \"idle\" || resume_loading} on_submit={submit} />\n </Box>\n );\n}\n","/**\n * Resolve and read a session transcript for the TUI `/resume` command.\n */\nimport path from \"node:path\";\nimport type { Message } from \"../providers/types.js\";\nimport { resolve_session_path } from \"../session/resolve.js\";\nimport { read_session_messages } from \"../session/store.js\";\nimport type { ThemeSpec } from \"../util/lore.js\";\nimport { error_notice_block, resume_session_view, type HistoryBlock } from \"./state.js\";\n\nexport type LoadResumeResult =\n | { ok: true; id: string; messages: readonly Message[]; blocks: readonly HistoryBlock[]; banner_line: string }\n | { ok: false; block: HistoryBlock };\n\nfunction run_error_text(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Load a transcript for `/resume`; returns view state or an error notice. */\nexport async function load_resume_view(\n session_dir: string,\n value: string,\n theme: ThemeSpec,\n): Promise<LoadResumeResult> {\n try {\n const transcript = await resolve_session_path(session_dir, value);\n const messages = await read_session_messages(transcript);\n const id = path.basename(transcript, \".jsonl\");\n const view = resume_session_view(id, messages, theme);\n return { ok: true, id, messages, blocks: view.blocks, banner_line: view.banner_line };\n } catch (error) {\n return { ok: false, block: error_notice_block(run_error_text(error)) };\n }\n}\n","/**\n * Pure state logic for the ink TUI: UI-state transitions from agent events,\n * slash-command parsing, transcript block mapping, and display formatters.\n * No ink/react imports here — this module is unit-tested without a TTY.\n */\nimport type { AgentEvent } from \"../agent/events.js\";\nimport type { AgentRunResult } from \"../agent/agent.js\";\nimport type { AssistantMessage, Message, ToolCall, Usage } from \"../providers/types.js\";\nimport { safe_json_parse, truncate_text } from \"../util/json.js\";\nimport type { ThemeSpec } from \"../util/lore.js\";\nimport { fill_template } from \"../util/theme.js\";\n\nexport type UiPhase = \"idle\" | \"thinking\" | \"tool\";\n\nexport interface UiState {\n readonly phase: UiPhase;\n readonly turns_used: number;\n readonly usage: Usage;\n readonly session_path: string | undefined;\n readonly compress_count: number;\n readonly budget_exhausted: boolean;\n readonly last_error: string | undefined;\n readonly active_tool: ToolCall | undefined;\n}\n\nexport const INITIAL_UI_STATE: UiState = {\n phase: \"idle\",\n turns_used: 0,\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n session_path: undefined,\n compress_count: 0,\n budget_exhausted: false,\n last_error: undefined,\n active_tool: undefined,\n};\n\nexport const HISTORY_CAP = 50;\nconst TOOL_ARGS_PREVIEW_CHARS = 80;\nconst TOOL_OUTPUT_PREVIEW_CHARS = 120;\nconst ERROR_PREVIEW_CHARS = 300;\n\nfunction error_text(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n return String(error);\n}\n\n/** Reducer over UiState; one pure mapping per AgentEvent variant. */\nexport function apply_event(state: UiState, event: AgentEvent): UiState {\n switch (event.type) {\n case \"llm_start\":\n return { ...state, phase: \"thinking\", active_tool: undefined };\n case \"llm_end\":\n return {\n ...state,\n usage: {\n prompt_tokens: state.usage.prompt_tokens + event.result.usage.prompt_tokens,\n completion_tokens: state.usage.completion_tokens + event.result.usage.completion_tokens,\n total_tokens: state.usage.total_tokens + event.result.usage.total_tokens,\n },\n };\n case \"tool_call_start\":\n return { ...state, phase: \"tool\", active_tool: event.call };\n case \"tool_call_end\":\n return {\n ...state,\n phase: \"thinking\",\n active_tool: undefined,\n last_error:\n event.cancelled === true || event.result.ok === true\n ? state.last_error\n : (event.result.error ?? \"tool failed\"),\n };\n case \"turn_end\":\n return { ...state, turns_used: event.turn };\n case \"compress_start\":\n return { ...state, compress_count: state.compress_count + 1 };\n case \"budget_exhausted\":\n return { ...state, budget_exhausted: true };\n case \"error\":\n return { ...state, last_error: error_text(event.error) };\n default:\n return state;\n }\n}\n\n/** Fold an AgentRunResult back into UiState after the run promise resolves. */\nexport function apply_run_result(state: UiState, result: AgentRunResult): UiState {\n return {\n ...state,\n phase: \"idle\",\n turns_used: result.outcome.turns_used,\n session_path: result.session_path ?? state.session_path,\n last_error: result.outcome.stopped_reason === \"aborted\" ? \"run aborted\" : state.last_error,\n };\n}\n\nexport type ParsedInput = { kind: \"slash\"; name: string; args: string } | { kind: \"message\"; text: string };\n\n/** Split trimmed input into slash command vs plain message (empty input = message). */\nexport function parse_command(raw_input: string): ParsedInput {\n const text = raw_input.trim();\n if (text.startsWith(\"/\") === false) {\n return { kind: \"message\", text };\n }\n const body = text.slice(1);\n const space_index = body.indexOf(\" \");\n if (space_index === -1) {\n return { kind: \"slash\", name: body, args: \"\" };\n }\n return { kind: \"slash\", name: body.slice(0, space_index), args: body.slice(space_index + 1).trim() };\n}\n\n/** Dim header: the theme welcome string, the one tagline placement. */\nexport function tui_banner_text(theme: ThemeSpec, version: string, model: string, kind: string): string {\n return fill_template(theme.welcome, { version, model, kind });\n}\n\n/** Count messages shown in the resume banner (matches split_history_blocks visibility). */\nexport function resume_banner_count(messages: readonly Message[] | undefined): number {\n if (messages === undefined) {\n return 0;\n }\n return messages.filter((message) => message.role !== \"system\").length;\n}\n\n/** Second banner line when a session transcript was loaded via `--resume` or `/resume`. */\nexport function resume_banner_line(id: string, message_count: number): string {\n return `resumed ${id} (${message_count} messages)`;\n}\n\n/** 1234567 -> \"1,234,567\" (US grouping, matching the status bar style). */\nexport function format_usage(total_tokens: number): string {\n return total_tokens.toLocaleString(\"en-US\");\n}\n\nexport type BlockRole = \"user\" | \"lich\" | \"tool\" | \"meta\" | \"error\";\n\nexport interface HistoryBlock {\n readonly role: BlockRole;\n readonly lines: readonly string[];\n}\n\nfunction assistant_tool_line(call: ToolCall): string {\n const args_json = JSON.stringify(call.args) ?? \"{}\";\n return ` \\u23bf ${truncate_text(args_json, TOOL_ARGS_PREVIEW_CHARS)}`;\n}\n\nfunction format_message_lines(message: Message, theme: ThemeSpec): string[] {\n if (message.role === \"user\") {\n return [`${theme.user_label} \\u203a ${message.content}`];\n }\n if (message.role === \"assistant\") {\n const lines = [`${theme.response_label} \\u203a ${message.content}`];\n for (const call of message.tool_calls ?? []) {\n lines.push(assistant_tool_line(call));\n }\n return lines;\n }\n if (message.role === \"tool\") {\n const flag = message.is_error === true ? \"error\" : \"ok\";\n return [` \\u23bf ${message.name}: ${flag} (${truncate_text(message.content, TOOL_OUTPUT_PREVIEW_CHARS)})`];\n }\n return [`\\u00b7 system: ${message.content}`];\n}\n\n/** Map one transcript Message to display lines with its role tag. */\nexport function format_message_block(message: Message, theme: ThemeSpec): HistoryBlock {\n if (message.role === \"tool\") {\n const ok = message.is_error !== true;\n return { role: ok === true ? \"tool\" : \"error\", lines: format_message_lines(message, theme) };\n }\n const roles: Record<Exclude<Message[\"role\"], \"tool\">, BlockRole> = {\n system: \"meta\",\n user: \"user\",\n assistant: \"lich\",\n };\n return { role: roles[message.role], lines: format_message_lines(message, theme) };\n}\n\n/** Keep the newest `cap` non-system messages as renderable blocks. */\nexport function split_history_blocks(messages: readonly Message[], cap: number, theme: ThemeSpec): HistoryBlock[] {\n const visible = messages.filter((message) => message.role !== \"system\");\n const start = Math.max(0, visible.length - cap);\n return visible.slice(start).map((message) => format_message_block(message, theme));\n}\n\n/** Meta notice + transcript blocks after a successful in-TUI `/resume`. */\nexport function resume_session_view(\n id: string,\n messages: readonly Message[],\n theme: ThemeSpec,\n cap: number = HISTORY_CAP,\n): { blocks: readonly HistoryBlock[]; banner_line: string } {\n const banner_line = resume_banner_line(id, resume_banner_count(messages));\n const notice: HistoryBlock = { role: \"meta\", lines: [`\\u00b7 ${banner_line}`] };\n const history_cap = Math.max(0, cap - 1);\n return { blocks: [notice, ...split_history_blocks(messages, history_cap, theme)], banner_line };\n}\n\n/** Missing `/resume` argument notice. */\nexport function resume_missing_args_block(): HistoryBlock {\n return { role: \"error\", lines: [\"\\u00b7 /resume requires <id|latest>\"] };\n}\n\nfunction assistant_result_block(message: AssistantMessage, theme: ThemeSpec): HistoryBlock | undefined {\n if (message.content.length === 0) {\n return undefined;\n }\n return { role: \"lich\", lines: [`${theme.response_label} \\u203a ${message.content}`] };\n}\n\n/** Post-run meta blocks: compression notices, budget, errors, final answer. */\nexport function run_notice_blocks(result: AgentRunResult, theme: ThemeSpec): HistoryBlock[] {\n const blocks: HistoryBlock[] = [];\n if (result.outcome.stopped_reason === \"budget\") {\n blocks.push({ role: \"error\", lines: [`\\u00b7 ${fill_template(theme.notices.budget_exhausted, {})}`] });\n }\n const final_block = result.outcome.final === undefined ? undefined : assistant_result_block(result.outcome.final, theme);\n if (final_block !== undefined) {\n blocks.push(final_block);\n }\n return blocks;\n}\n\nfunction truncate_tool_preview(result_content: string, ok: boolean): string {\n return ` \\u23bf ${ok === true ? \"ok\" : \"error\"} (${truncate_text(result_content, TOOL_OUTPUT_PREVIEW_CHARS)})`;\n}\n\n/** One finalized live tool row; falls back to the transcript copy when absent. */\nexport function tool_result_block(call: ToolCall, ok: boolean, result_content: string): HistoryBlock {\n const args_json = JSON.stringify(call.args) ?? \"{}\";\n const lines = [\n `\\u23fa ${call.name}(${truncate_text(args_json, TOOL_ARGS_PREVIEW_CHARS)})`,\n truncate_tool_preview(result_content, ok),\n ];\n return { role: ok === true ? \"tool\" : \"error\", lines };\n}\n\nexport function parse_tool_message_content(content: string): { ok: boolean; output: string } {\n const parsed = safe_json_parse<{ ok?: unknown; output?: unknown }>(content);\n if (parsed !== undefined && typeof parsed.ok === \"boolean\" && typeof parsed.output === \"string\") {\n return { ok: parsed.ok, output: parsed.output };\n }\n return { ok: true, output: content };\n}\n\nexport function compress_notice_block(summary_chars: number, theme: ThemeSpec): HistoryBlock {\n const line = fill_template(theme.notices.compressed, { chars: summary_chars });\n return { role: \"meta\", lines: [`\\u00b7 ${line}`] };\n}\n\nexport function error_notice_block(message: string): HistoryBlock {\n return { role: \"error\", lines: [`\\u00b7 error: ${truncate_text(message, ERROR_PREVIEW_CHARS)}`] };\n}\n\nexport function tool_args_preview(args: Record<string, unknown>): string {\n const args_json = JSON.stringify(args) ?? \"{}\";\n return truncate_text(args_json, TOOL_ARGS_PREVIEW_CHARS);\n}\n\nexport function help_block(): HistoryBlock {\n return { role: \"meta\", lines: [...HELP_LINES] };\n}\n\nexport function model_label_block(config: { providers: readonly { model: string; kind: string }[] }): HistoryBlock {\n const provider = config.providers[0];\n return {\n role: \"meta\",\n lines: [`\\u00b7 model: ${provider?.model ?? \"unknown\"} \\u00b7 provider: ${provider?.kind ?? \"unknown\"}`],\n };\n}\n\nexport function usage_notice_block(total_tokens: number): HistoryBlock {\n return { role: \"meta\", lines: [`\\u00b7 tokens used this session: ${format_usage(total_tokens)}`] };\n}\n\nexport function unknown_command_block(name: string): HistoryBlock {\n return { role: \"error\", lines: [`\\u00b7 unknown command: /${name} (try /help)`] };\n}\n\nexport interface SessionEntryInfo {\n readonly name: string;\n readonly size_bytes: number;\n readonly mtime_ms: number;\n}\n\n/** Newest-first session listing, capped at `cap` entries. */\nexport function session_list_block(entries: readonly SessionEntryInfo[], theme: ThemeSpec, cap: number = 10): HistoryBlock {\n const sorted = [...entries].sort((a, b) => b.mtime_ms - a.mtime_ms).slice(0, cap);\n if (sorted.length === 0) {\n return { role: \"meta\", lines: [\"\\u00b7 no session files yet\"] };\n }\n const label = fill_template(theme.notices.sessions, { count: sorted.length });\n const lines: string[] = [`\\u00b7 ${label}`];\n for (const entry of sorted) {\n lines.push(` ${entry.name} (${format_usage(entry.size_bytes)} bytes)`);\n }\n return { role: \"meta\", lines };\n}\n\nexport const SLASH_COMMAND_NAMES: readonly string[] = [\n \"exit\",\n \"quit\",\n \"q\",\n \"help\",\n \"model\",\n \"usage\",\n \"clear\",\n \"sessions\",\n \"resume\",\n];\n\nexport const HELP_LINES: readonly string[] = [\n \"commands: /help /model /usage /clear /sessions /resume /exit (aliases: /quit /q)\",\n \"enter submits \\u00b7 backspace deletes \\u00b7 up/down recalls history \\u00b7 pasted newlines become spaces\",\n];","/**\n * Transcript rendering: maps HistoryBlock descriptors to ink elements with\n * role-based colors, and shows an animated braille spinner while thinking.\n * Blocks are pre-capped by the app, so a plain flex column is sufficient.\n */\nimport { useEffect, useState } from \"react\";\nimport { Box, Text } from \"ink\";\nimport { tool_args_preview, type HistoryBlock, type UiState } from \"./state.js\";\n\nconst SPINNER_FRAMES: readonly string[] = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\nconst SPINNER_INTERVAL_MS = 80;\n\n/** Braille spinner frames on an 80ms interval; clears on unmount. */\nfunction use_spinner(): string {\n const [frame, set_frame] = useState(SPINNER_FRAMES[0] ?? \"⠋\");\n useEffect(() => {\n const timer = setInterval(() => {\n const next = SPINNER_FRAMES[(SPINNER_FRAMES.indexOf(frame) + 1) % SPINNER_FRAMES.length];\n set_frame(next ?? \"⠋\");\n }, SPINNER_INTERVAL_MS);\n return () => {\n clearInterval(timer);\n };\n }, [frame]);\n return frame;\n}\n\nfunction ThinkingLine(): React.JSX.Element {\n const frame = use_spinner();\n return <Text dimColor>{`${frame} thinking…`}</Text>;\n}\n\nconst ROLE_COLORS: Record<HistoryBlock[\"role\"], string | undefined> = {\n user: \"white\",\n lich: \"green\",\n tool: \"cyan\",\n meta: undefined,\n error: \"red\",\n};\n\nfunction BlockLines({ block }: { block: HistoryBlock }): React.JSX.Element {\n const color = ROLE_COLORS[block.role];\n return (\n <>\n {block.lines.map((line, index) => (\n <Text key={index} color={color} dimColor={color === undefined}>{line}</Text>\n ))}\n </>\n );\n}\n\ninterface MessageViewProps {\n readonly blocks: readonly HistoryBlock[];\n readonly state: UiState;\n}\n\n/** Transcript column plus the live phase line (spinner / running tool row). */\nexport function MessageView({ blocks, state }: MessageViewProps): React.JSX.Element {\n return (\n <Box flexDirection=\"column\" flexGrow={1}>\n {blocks.map((block, index) => (\n <Box key={index} flexDirection=\"column\">\n <BlockLines block={block} />\n </Box>\n ))}\n {state.phase === \"thinking\" ? <ThinkingLine /> : null}\n {state.phase === \"tool\" && state.active_tool !== undefined ? (\n <Text color=\"cyan\">{`⏺ ${state.active_tool.name}(${tool_args_preview(state.active_tool.args)})`}</Text>\n ) : null}\n </Box>\n );\n}","/**\n * Bottom status line: model, turns, token totals, phase tag, compression\n * count, and the session path once the agent has persisted a transcript.\n */\nimport { Box, Text } from \"ink\";\nimport type { ThemeSpec } from \"../util/lore.js\";\nimport { format_usage, type UiState } from \"./state.js\";\n\ninterface StatusBarProps {\n readonly state: UiState;\n readonly model: string;\n readonly theme: ThemeSpec;\n}\n\nexport function StatusBar({ state, model, theme }: StatusBarProps): React.JSX.Element {\n const phase = theme.phase_labels[state.phase];\n return (\n <Box>\n <Text dimColor>\n {`model ${model} · turns ${state.turns_used} · tokens ${format_usage(state.usage.total_tokens)} · [${phase}]`}\n {state.compress_count > 0 ? ` · compressed ${state.compress_count}` : \"\"}\n {state.session_path !== undefined ? ` · ${state.session_path}` : \"\"}\n </Text>\n {state.budget_exhausted ? <Text color=\"red\">{` · ${theme.notices.budget_exhausted}`}</Text> : null}\n </Box>\n );\n}","/**\n * Input row: printable characters accumulate in a buffer, Enter submits,\n * Backspace/Delete edits, Up/Down walk a 20-entry recall ring, and pasted\n * newlines collapse to spaces. Ctrl+C is left to ink's default handling.\n */\nimport { useState } from \"react\";\nimport { Box, Text, useInput } from \"ink\";\n\nconst INPUT_HISTORY_CAP = 20;\n\ninterface CommandBarProps {\n readonly busy: boolean;\n readonly on_submit: (text: string) => void;\n}\n\n/** Push onto a capped ring (newest first) without mutating the source. */\nfunction push_history(ring: readonly string[], entry: string): readonly string[] {\n return [entry, ...ring.filter((item) => item !== entry)].slice(0, INPUT_HISTORY_CAP);\n}\n\nexport function CommandBar({ busy, on_submit }: CommandBarProps): React.JSX.Element {\n const [buffer, set_buffer] = useState(\"\");\n const [recall_ring, set_recall_ring] = useState<readonly string[]>([]);\n const [recall_index, set_recall_index] = useState<number | undefined>(undefined);\n\n const submit_buffer = (): void => {\n if (busy === true) {\n return;\n }\n const text = buffer.trim();\n set_buffer(\"\");\n set_recall_index(undefined);\n if (text.length > 0) {\n set_recall_ring((current) => push_history(current, text));\n on_submit(text);\n }\n };\n\n /** Newest-first ring: Up walks toward older (higher index), Down toward newer. */\n const walk_recall = (direction: 1 | -1): void => {\n if (recall_ring.length === 0) {\n return;\n }\n if (direction === -1 && recall_index === 0) {\n set_recall_index(undefined);\n set_buffer(\"\");\n return;\n }\n if (direction === -1 && recall_index === undefined) {\n return;\n }\n const current = recall_index ?? -direction;\n const next = Math.min(Math.max(current + direction, 0), recall_ring.length - 1);\n set_recall_index(next);\n set_buffer(recall_ring[next] ?? \"\");\n };\n\n useInput((input, key) => {\n if (key.return === true) {\n submit_buffer();\n return;\n }\n if (key.upArrow === true) {\n walk_recall(1);\n return;\n }\n if (key.downArrow === true) {\n walk_recall(-1);\n return;\n }\n if (key.backspace === true || key.delete === true) {\n set_buffer((current) => current.slice(0, -1));\n return;\n }\n if (key.ctrl === true || key.escape === true || key.tab === true || key.meta === true) {\n return;\n }\n if (input.length > 0) {\n set_buffer((current) => current + input.replaceAll(\"\\n\", \" \").replaceAll(\"\\r\", \" \"));\n }\n });\n\n return (\n <Box>\n <Text dimColor>{busy ? \" … \" : \"› \"}</Text>\n <Text>{buffer}</Text>\n <Text dimColor>▌</Text>\n </Box>\n );\n}\n"],"mappings":"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","names":["useEffect","useState","Box","Text","Box","Text","jsx","jsxs","Box","Text","useState","Box","Text","jsx","jsxs","jsx","jsxs","run_error_text","useEffect","useState","Box","Text","jsx"]}
|
|
@@ -95,7 +95,7 @@ swallowed; handlers may unsubscribe mid-emit (the emitter iterates a snapshot).
|
|
|
95
95
|
| `llm_start` | `{ turn }` | Just before the chat call | Exactly one per LLM call |
|
|
96
96
|
| `llm_end` | `{ turn, result }` | Chat call resolved | Pairs with `llm_start`; never fires if the call throws |
|
|
97
97
|
| `tool_call_start` | `{ turn, call }` | Before each tool executes | After `llm_end`, sequential per call |
|
|
98
|
-
| `tool_call_end` | `{ turn, call, result }` | After that tool resolves |
|
|
98
|
+
| `tool_call_end` | `{ turn, call, result, cancelled? }` | After that tool resolves, or when abort skips a pending call | Normally pairs with `tool_call_start`; cancelled skips emit `cancelled: true` with no start |
|
|
99
99
|
| `final` | `{ message, result }` | A turn produced no tool calls | At most once per run; only on a real final |
|
|
100
100
|
| `budget_exhausted` | `{ turns_used }` | Loop exits without a final | Follows the last `tool_call_end` |
|
|
101
101
|
| `turn_end` | `{ turn }` | Last event of a turn | After `final` **or** after `budget_exhausted` |
|
|
@@ -185,18 +185,37 @@ coarse - see the trade-off note in [overview](./overview.md#design-trade-offs).
|
|
|
185
185
|
"message": { "role": "assistant", "content": "done" } }
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
188
|
+
`history_size` on `run_start` is the length of `options.history` at seed
|
|
189
|
+
time (pre-run seed count), not the final message count after the run.
|
|
190
|
+
|
|
191
|
+
What gets persisted while a run is active (`src/session/recorder.ts`): a
|
|
192
|
+
`run_start` meta record at seed time, the seeded system/user (and prior
|
|
193
|
+
history when the handle is owned or first used), then event-driven appends —
|
|
194
|
+
`llm_end` → assistant message, `tool_call_end` → tool message (via exported
|
|
195
|
+
`format_tool_result_content` / `tool_message_from_result`, including
|
|
196
|
+
abort-cancelled tools), `budget_exhausted` meta, and a `compress_end` meta
|
|
197
|
+
marker. A `run_end` meta record (`stopped_reason`, `usage`) closes every
|
|
198
|
+
completed run. `usage` is the run's `usage_total`. Everything is best-effort:
|
|
199
|
+
any error logs a warning and never fails the run; `session_path` is the
|
|
200
|
+
handle path when recording started.
|
|
201
|
+
|
|
202
|
+
Callers may pass `AgentRunOptions.session` to reuse a `SessionHandle`. The TUI
|
|
203
|
+
opens one handle per launch so N turns share one transcript file (one session
|
|
204
|
+
id). One-shot, chat, and gateway omit the option and keep per-run files.
|
|
195
205
|
|
|
196
206
|
`read_session_messages(path)` parses a file back into `Message[]`: per line it
|
|
197
207
|
JSON-parses leniently, accepts only records with a `kind: "message"`-shaped
|
|
198
208
|
`message` whose `role` is one of the four known roles, and silently skips
|
|
199
|
-
everything else. Missing files parse to an empty array.
|
|
209
|
+
everything else. Missing files parse to an empty array. A trailing user
|
|
210
|
+
message is dropped so a provider throw or abort-before-turn does not leave
|
|
211
|
+
two consecutive user turns on resume. On resume, raw pre-compress messages
|
|
212
|
+
are replayed; compression simply re-runs on a later turn.
|
|
213
|
+
|
|
214
|
+
**Resume (Phase 1 + 2):** `lich --resume <id|latest>` resolves a path with
|
|
215
|
+
`src/session/resolve.ts` (`latest` = newest `.jsonl` by mtime; otherwise exact
|
|
216
|
+
`<id>.jsonl` or a unique filename-prefix match), then loads messages via
|
|
217
|
+
`read_session_messages` into the TUI history. New turns append into a fresh
|
|
218
|
+
per-launch handle (seeded with that history on first write).
|
|
200
219
|
|
|
201
220
|
## Error propagation
|
|
202
221
|
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
Lich is a small TypeScript AI agent harness: it drives a chat model in a
|
|
4
4
|
think-act-observe loop, lets the model call tools, compresses history when the
|
|
5
5
|
context budget demands it, and persists transcripts. The published npm package
|
|
6
|
-
is 0.
|
|
7
|
-
`package.json` is still 0.6.0, so `lich --version` prints `0.6.0`. It runs on
|
|
6
|
+
is 0.7.0, including editor MCP (`mcp_servers`, `lich mcp`). It runs on
|
|
8
7
|
Node >= 20 and on Bun, is ESM with NodeNext resolution, and its runtime
|
|
9
8
|
dependencies are `zod` (config validation), `ink`, and `react` (the TUI).
|
|
10
9
|
Everything else is Node/Bun built-ins.
|
|
@@ -101,7 +100,7 @@ Walkthrough of a single `Agent.run({ input })` call
|
|
|
101
100
|
are connected (`initialize`, `notifications/initialized`, then `tools/list`)
|
|
102
101
|
and registered as `mcp_<server>_<tool>`. An empty `tools_enabled` never
|
|
103
102
|
connects. Disabled servers are skipped. A connect failure logs a warning
|
|
104
|
-
and the run continues. This client
|
|
103
|
+
and the run continues. This client ships in 0.7.0.
|
|
105
104
|
2. **Usage collector attached.** `run()` subscribes a `collect_usage` handler
|
|
106
105
|
on `agent.events`; every `llm_end` event adds the call's token usage into a
|
|
107
106
|
per-run `Usage` total. The subscription is removed in a `finally` block.
|
|
@@ -125,13 +124,14 @@ Walkthrough of a single `Agent.run({ input })` call
|
|
|
125
124
|
the final assistant message (or the last one seen), the last `ChatResult`
|
|
126
125
|
on a real final, `turns_used`, and a `stopped_reason` of `final`, `budget`,
|
|
127
126
|
or `aborted`.
|
|
128
|
-
8. **Session persist.**
|
|
129
|
-
|
|
130
|
-
`
|
|
131
|
-
`run_end`
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
127
|
+
8. **Session persist.** While the loop runs, a session recorder appends
|
|
128
|
+
`run_start`, seeded messages, then event-driven records (`llm_end` /
|
|
129
|
+
`tool_call_end` messages, `budget_exhausted`, `compress_end`), and finally
|
|
130
|
+
`run_end` (`stopped_reason`, `usage`) to a JSONL file under `session_dir`
|
|
131
|
+
(default `<work_dir>/.lich/sessions`). The TUI passes one shared
|
|
132
|
+
`SessionHandle` per launch; other modes open a fresh file per run.
|
|
133
|
+
Persistence is best-effort: failures are logged and never fail the run;
|
|
134
|
+
`session_path` is the handle path when recording started.
|
|
135
135
|
9. **Return.** `AgentRunResult` bundles the outcome, the full transcript
|
|
136
136
|
(prior history plus the new exchange), the collected `usage_total`, and the
|
|
137
137
|
session path.
|