@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +75 -5
  2. package/README.md +38 -9
  3. package/dist/{chunk-QVJCIZIF.js → chunk-EDRUZF22.js} +763 -286
  4. package/dist/chunk-EDRUZF22.js.map +1 -0
  5. package/dist/chunk-PL6MKRKE.js +75 -0
  6. package/dist/chunk-PL6MKRKE.js.map +1 -0
  7. package/dist/{chunk-JYURFAGB.js → chunk-QOTECFCN.js} +2 -2
  8. package/dist/chunk-VKEOHUCB.js +86 -0
  9. package/dist/chunk-VKEOHUCB.js.map +1 -0
  10. package/dist/{chunk-SAEB3QL3.js → chunk-W6JXZBUE.js} +2 -2
  11. package/dist/cli.d.ts +19 -1
  12. package/dist/cli.js +53 -11
  13. package/dist/cli.js.map +1 -1
  14. package/dist/{gateway-5BG3YCZF.js → gateway-WU5G4ODO.js} +109 -49
  15. package/dist/gateway-WU5G4ODO.js.map +1 -0
  16. package/dist/index.d.ts +46 -3
  17. package/dist/index.js +3 -2
  18. package/dist/resolve-H22TOVB5.js +7 -0
  19. package/dist/resolve-H22TOVB5.js.map +1 -0
  20. package/dist/store-COOLBAHB.js +9 -0
  21. package/dist/store-COOLBAHB.js.map +1 -0
  22. package/dist/{tui-L6RABP2J.js → tui-4BP3TI7J.js} +224 -30
  23. package/dist/tui-4BP3TI7J.js.map +1 -0
  24. package/docs/architecture/agent-loop.md +28 -9
  25. package/docs/architecture/overview.md +10 -10
  26. package/docs/architecture/tools.md +30 -3
  27. package/docs/getting-started.md +3 -3
  28. package/docs/index.md +1 -1
  29. package/docs/user-guide/cli.md +13 -5
  30. package/docs/user-guide/games.md +3 -3
  31. package/docs/user-guide/gateway.md +70 -16
  32. package/docs/user-guide/library.md +6 -3
  33. package/docs/user-guide/redot.md +2 -2
  34. package/docs/user-guide/tui.md +3 -2
  35. package/package.json +1 -1
  36. package/dist/chunk-QVJCIZIF.js.map +0 -1
  37. package/dist/gateway-5BG3YCZF.js.map +0 -1
  38. package/dist/tui-L6RABP2J.js.map +0 -1
  39. /package/dist/{chunk-JYURFAGB.js.map → chunk-QOTECFCN.js.map} +0 -0
  40. /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-JYURFAGB.js";
4
+ } from "./chunk-QOTECFCN.js";
5
5
  import {
6
6
  LICH_VERSION
7
- } from "./chunk-SAEB3QL3.js";
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-QVJCIZIF.js";
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(-1);
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 run_error_text(error) {
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(run_error_text(error))]);
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
- [agent, add_blocks, set_blocks, theme, total_tokens]
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 TuiApp({ agent, theme }) {
394
- const [blocks, set_blocks] = useState3([]);
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(agent, theme, add_blocks, set_state, set_blocks);
403
- const handle_slash = use_slash_commands(agent, theme, add_blocks, set_blocks, state.usage.total_tokens);
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: tui_banner_text(theme, LICH_VERSION, provider?.model ?? "unknown", provider?.kind ?? "unknown") }),
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
- const theme = load_theme(config.theme);
431
- const instance = render(/* @__PURE__ */ jsx5(TuiApp, { agent, theme }));
432
- await instance.waitUntilExit();
433
- return 0;
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-L6RABP2J.js.map
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 | Pairs with its `tool_call_start` |
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
- What gets persisted, per run (`Agent.persist_session`): a `run_start` meta
189
- record, one `message` record per outcome message (including the seeded system
190
- message and tool messages), a `budget_exhausted` meta record when the run
191
- stopped on budget, and a `run_end` meta record (`stopped_reason`, `usage`)
192
- on every completed run. `usage` is the run's `usage_total`. Everything is
193
- best-effort: any error logs a warning and returns `session_path: undefined`
194
- instead of failing the run.
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.6.0. This tree also includes unreleased editor MCP (changelog 0.7.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 is in this source only (0.7.0 unreleased).
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.** `persist_session()` appends one `meta` record
129
- (`run_start`), then one `message` record per outcome message, then a
130
- `budget_exhausted` meta record if the budget stopped the run, then a
131
- `run_end` meta record (`stopped_reason`, `usage`) for every completed
132
- run, to a JSONL file under `session_dir` (default
133
- `<work_dir>/.lich/sessions`). Persistence is best-effort: failures are
134
- logged and the run still succeeds with `session_path: undefined`.
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.