@moikapy/lich 0.7.1 → 0.9.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 +63 -0
  2. package/README.md +38 -19
  3. package/dist/{chunk-CVX7LZWC.js → chunk-4N3S6ACI.js} +2 -2
  4. package/dist/{chunk-WNFBIX4E.js → chunk-KOEZQUIX.js} +1312 -600
  5. package/dist/chunk-KOEZQUIX.js.map +1 -0
  6. package/dist/{chunk-PZNYVGD4.js → chunk-P2YEHNM4.js} +2 -2
  7. package/dist/chunk-P2YEHNM4.js.map +1 -0
  8. package/dist/chunk-PL6MKRKE.js +75 -0
  9. package/dist/chunk-PL6MKRKE.js.map +1 -0
  10. package/dist/chunk-TYRKVBWQ.js +104 -0
  11. package/dist/chunk-TYRKVBWQ.js.map +1 -0
  12. package/dist/cli.d.ts +19 -1
  13. package/dist/cli.js +125 -47
  14. package/dist/cli.js.map +1 -1
  15. package/dist/{gateway-44QTIJTJ.js → gateway-OSB7254F.js} +209 -46
  16. package/dist/gateway-OSB7254F.js.map +1 -0
  17. package/dist/index.d.ts +122 -62
  18. package/dist/index.js +12 -4
  19. package/dist/resolve-H22TOVB5.js +7 -0
  20. package/dist/resolve-H22TOVB5.js.map +1 -0
  21. package/dist/store-YYLEB7JG.js +9 -0
  22. package/dist/store-YYLEB7JG.js.map +1 -0
  23. package/dist/{tui-MEJGEILU.js → tui-XXIU4W7K.js} +216 -25
  24. package/dist/tui-XXIU4W7K.js.map +1 -0
  25. package/docs/architecture/agent-loop.md +28 -9
  26. package/docs/architecture/overview.md +10 -9
  27. package/docs/architecture/tools.md +3 -1
  28. package/docs/getting-started.md +6 -6
  29. package/docs/index.md +1 -1
  30. package/docs/user-guide/cli.md +17 -8
  31. package/docs/user-guide/games.md +5 -4
  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-PZNYVGD4.js.map +0 -1
  37. package/dist/chunk-WNFBIX4E.js.map +0 -1
  38. package/dist/gateway-44QTIJTJ.js.map +0 -1
  39. package/dist/tui-MEJGEILU.js.map +0 -1
  40. /package/dist/{chunk-CVX7LZWC.js.map → chunk-4N3S6ACI.js.map} +0 -0
@@ -1,14 +1,22 @@
1
1
  import {
2
2
  fill_template,
3
3
  load_theme
4
- } from "./chunk-CVX7LZWC.js";
4
+ } from "./chunk-4N3S6ACI.js";
5
5
  import {
6
6
  LICH_VERSION
7
- } from "./chunk-PZNYVGD4.js";
7
+ } from "./chunk-P2YEHNM4.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-KOEZQUIX.js";
15
+ import {
16
+ open_session,
17
+ read_session_messages,
10
18
  truncate_text
11
- } from "./chunk-WNFBIX4E.js";
19
+ } from "./chunk-TYRKVBWQ.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",
@@ -52,6 +63,18 @@ function apply_event(state, event) {
52
63
  total_tokens: state.usage.total_tokens + event.result.usage.total_tokens
53
64
  }
54
65
  };
66
+ case "compress_end":
67
+ if (event.usage === void 0) {
68
+ return state;
69
+ }
70
+ return {
71
+ ...state,
72
+ usage: {
73
+ prompt_tokens: state.usage.prompt_tokens + event.usage.prompt_tokens,
74
+ completion_tokens: state.usage.completion_tokens + event.usage.completion_tokens,
75
+ total_tokens: state.usage.total_tokens + event.usage.total_tokens
76
+ }
77
+ };
55
78
  case "tool_call_start":
56
79
  return { ...state, phase: "tool", active_tool: event.call };
57
80
  case "tool_call_end":
@@ -59,7 +82,7 @@ function apply_event(state, event) {
59
82
  ...state,
60
83
  phase: "thinking",
61
84
  active_tool: void 0,
62
- last_error: event.result.ok === true ? state.last_error : event.result.error ?? "tool failed"
85
+ last_error: event.cancelled === true || event.result.ok === true ? state.last_error : event.result.error ?? "tool failed"
63
86
  };
64
87
  case "turn_end":
65
88
  return { ...state, turns_used: event.turn };
@@ -97,9 +120,65 @@ function parse_command(raw_input) {
97
120
  function tui_banner_text(theme, version, model, kind) {
98
121
  return fill_template(theme.welcome, { version, model, kind });
99
122
  }
123
+ function resume_banner_count(messages) {
124
+ if (messages === void 0) {
125
+ return 0;
126
+ }
127
+ return messages.filter((message) => message.role !== "system").length;
128
+ }
129
+ function resume_banner_line(id, message_count) {
130
+ return `resumed ${id} (${message_count} messages)`;
131
+ }
100
132
  function format_usage(total_tokens) {
101
133
  return total_tokens.toLocaleString("en-US");
102
134
  }
135
+ function assistant_tool_line(call) {
136
+ const args_json = JSON.stringify(call.args) ?? "{}";
137
+ return ` \u23BF ${truncate_text(args_json, TOOL_ARGS_PREVIEW_CHARS)}`;
138
+ }
139
+ function format_message_lines(message, theme) {
140
+ if (message.role === "user") {
141
+ return [`${theme.user_label} \u203A ${message.content}`];
142
+ }
143
+ if (message.role === "assistant") {
144
+ const lines = [`${theme.response_label} \u203A ${message.content}`];
145
+ for (const call of message.tool_calls ?? []) {
146
+ lines.push(assistant_tool_line(call));
147
+ }
148
+ return lines;
149
+ }
150
+ if (message.role === "tool") {
151
+ const flag = message.is_error === true ? "error" : "ok";
152
+ return [` \u23BF ${message.name}: ${flag} (${truncate_text(message.content, TOOL_OUTPUT_PREVIEW_CHARS)})`];
153
+ }
154
+ return [`\xB7 system: ${message.content}`];
155
+ }
156
+ function format_message_block(message, theme) {
157
+ if (message.role === "tool") {
158
+ const ok = message.is_error !== true;
159
+ return { role: ok === true ? "tool" : "error", lines: format_message_lines(message, theme) };
160
+ }
161
+ const roles = {
162
+ system: "meta",
163
+ user: "user",
164
+ assistant: "lich"
165
+ };
166
+ return { role: roles[message.role], lines: format_message_lines(message, theme) };
167
+ }
168
+ function split_history_blocks(messages, cap, theme) {
169
+ const visible = messages.filter((message) => message.role !== "system");
170
+ const start = Math.max(0, visible.length - cap);
171
+ return visible.slice(start).map((message) => format_message_block(message, theme));
172
+ }
173
+ function resume_session_view(id, messages, theme, cap = HISTORY_CAP) {
174
+ const banner_line = resume_banner_line(id, resume_banner_count(messages));
175
+ const notice = { role: "meta", lines: [`\xB7 ${banner_line}`] };
176
+ const history_cap = Math.max(0, cap - 1);
177
+ return { blocks: [notice, ...split_history_blocks(messages, history_cap, theme)], banner_line };
178
+ }
179
+ function resume_missing_args_block() {
180
+ return { role: "error", lines: ["\xB7 /resume requires <id|latest>"] };
181
+ }
103
182
  function assistant_result_block(message, theme) {
104
183
  if (message.content.length === 0) {
105
184
  return void 0;
@@ -168,10 +247,26 @@ function session_list_block(entries, theme, cap = 10) {
168
247
  return { role: "meta", lines };
169
248
  }
170
249
  var HELP_LINES = [
171
- "commands: /help /model /usage /clear /sessions /exit (aliases: /quit /q)",
250
+ "commands: /help /model /usage /clear /sessions /resume /exit (aliases: /quit /q)",
172
251
  "enter submits \xB7 backspace deletes \xB7 up/down recalls history \xB7 pasted newlines become spaces"
173
252
  ];
174
253
 
254
+ // src/tui/load_resume.ts
255
+ function run_error_text(error) {
256
+ return error instanceof Error ? error.message : String(error);
257
+ }
258
+ async function load_resume_view(session_dir, value, theme) {
259
+ try {
260
+ const transcript = await resolve_session_path(session_dir, value);
261
+ const messages = await read_session_messages(transcript);
262
+ const id = path.basename(transcript, ".jsonl");
263
+ const view = resume_session_view(id, messages, theme);
264
+ return { ok: true, id, messages, blocks: view.blocks, banner_line: view.banner_line };
265
+ } catch (error) {
266
+ return { ok: false, block: error_notice_block(run_error_text(error)) };
267
+ }
268
+ }
269
+
175
270
  // src/tui/message_view.tsx
176
271
  import { useEffect, useState } from "react";
177
272
  import { Box, Text } from "ink";
@@ -306,6 +401,9 @@ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
306
401
  var SESSION_LIST_CAP = 10;
307
402
  function event_blocks(event, theme) {
308
403
  if (event.type === "tool_call_end") {
404
+ if (event.cancelled === true) {
405
+ return [];
406
+ }
309
407
  return [tool_result_block(event.call, event.result.ok === true, event.result.output)];
310
408
  }
311
409
  if (event.type === "compress_end") {
@@ -316,10 +414,10 @@ function event_blocks(event, theme) {
316
414
  }
317
415
  return [];
318
416
  }
319
- async function run_agent_turn(agent, history, input, on_event, on_done, signal) {
417
+ async function run_agent_turn(agent, history, input, on_event, on_done, signal, session) {
320
418
  const stop_listening = agent.events.on(on_event);
321
419
  try {
322
- const result = await agent.run({ input, history, signal, label: "tui" });
420
+ const result = await agent.run({ input, history, signal, label: "tui", session });
323
421
  on_done(result);
324
422
  } finally {
325
423
  stop_listening();
@@ -341,12 +439,15 @@ async function sessions_block(config, theme) {
341
439
  return { role: "meta", lines: ["\xB7 no session files yet"] };
342
440
  }
343
441
  }
344
- function run_error_text(error) {
442
+ function run_error_text2(error) {
345
443
  return error instanceof Error ? error.message : String(error);
346
444
  }
347
- function use_agent_run(agent, theme, add_blocks, set_state, set_blocks) {
348
- const history_ref = useRef([]);
445
+ function use_agent_run(agent, theme, add_blocks, set_state, set_blocks, initial_history, session) {
446
+ const history_ref = useRef(initial_history ?? []);
349
447
  const controller_ref = useRef(void 0);
448
+ const set_history = useCallback((messages) => {
449
+ history_ref.current = messages;
450
+ }, []);
350
451
  const finish_run = useCallback((result) => {
351
452
  history_ref.current = result.messages;
352
453
  set_state((current) => apply_run_result(current, result));
@@ -362,8 +463,8 @@ function use_agent_run(agent, theme, add_blocks, set_state, set_blocks) {
362
463
  set_state((current) => apply_event(current, event));
363
464
  set_blocks((current) => [...current, ...event_blocks(event, theme)].slice(-HISTORY_CAP));
364
465
  };
365
- void run_agent_turn(agent, history_ref.current, text, on_event, finish_run, controller.signal).catch((error) => {
366
- add_blocks([error_notice_block(run_error_text(error))]);
466
+ void run_agent_turn(agent, history_ref.current, text, on_event, finish_run, controller.signal, session).catch((error) => {
467
+ add_blocks([error_notice_block(run_error_text2(error))]);
367
468
  set_state((current) => ({ ...current, phase: "idle" }));
368
469
  }).finally(() => {
369
470
  if (controller_ref.current === controller) {
@@ -371,12 +472,12 @@ function use_agent_run(agent, theme, add_blocks, set_state, set_blocks) {
371
472
  }
372
473
  });
373
474
  },
374
- [agent, add_blocks, finish_run, set_blocks, set_state, theme]
475
+ [agent, add_blocks, finish_run, session, set_blocks, set_state, theme]
375
476
  );
376
477
  useEffect2(() => () => controller_ref.current?.abort(), []);
377
- return start_message_run;
478
+ return { start_message_run, set_history };
378
479
  }
379
- function use_slash_commands(agent, theme, add_blocks, set_blocks, total_tokens) {
480
+ function use_slash_commands(agent, theme, add_blocks, set_blocks, set_history, set_resume_line, begin_resume_load, end_resume_load, total_tokens) {
380
481
  const handle = useCallback(
381
482
  (parsed) => {
382
483
  if (parsed.name === "exit" || parsed.name === "quit" || parsed.name === "q") {
@@ -393,27 +494,100 @@ function use_slash_commands(agent, theme, add_blocks, set_blocks, total_tokens)
393
494
  set_blocks([]);
394
495
  } else if (parsed.name === "sessions") {
395
496
  void sessions_block(agent.config, theme).then((block) => add_blocks([block]));
497
+ } else if (parsed.name === "resume") {
498
+ if (parsed.args.length === 0) {
499
+ add_blocks([resume_missing_args_block()]);
500
+ return;
501
+ }
502
+ begin_resume_load();
503
+ void load_resume_view(agent.config.session_dir, parsed.args, theme).then((result) => {
504
+ if (result.ok === false) {
505
+ add_blocks([result.block]);
506
+ return;
507
+ }
508
+ set_history(result.messages);
509
+ set_blocks(result.blocks);
510
+ set_resume_line(result.banner_line);
511
+ }).finally(() => {
512
+ end_resume_load();
513
+ });
396
514
  } else {
397
515
  add_blocks([unknown_command_block(parsed.name)]);
398
516
  }
399
517
  },
400
- [agent, add_blocks, set_blocks, theme, total_tokens]
518
+ [
519
+ agent,
520
+ add_blocks,
521
+ begin_resume_load,
522
+ end_resume_load,
523
+ set_blocks,
524
+ set_history,
525
+ set_resume_line,
526
+ theme,
527
+ total_tokens
528
+ ]
401
529
  );
402
530
  return handle;
403
531
  }
404
- function TuiApp({ agent, theme }) {
405
- const [blocks, set_blocks] = useState3([]);
532
+ function initial_blocks(history, theme) {
533
+ if (history === void 0 || history.length === 0) {
534
+ return [];
535
+ }
536
+ return split_history_blocks(history, HISTORY_CAP, theme);
537
+ }
538
+ function initial_resume_line(resumed_id, history) {
539
+ if (resumed_id === void 0) {
540
+ return void 0;
541
+ }
542
+ return resume_banner_line(resumed_id, resume_banner_count(history));
543
+ }
544
+ function TuiApp({ agent, theme, session, initial_history, resumed_id }) {
545
+ const [blocks, set_blocks] = useState3(() => initial_blocks(initial_history, theme));
406
546
  const [state, set_state] = useState3(INITIAL_UI_STATE);
547
+ const [resume_line, set_resume_line] = useState3(
548
+ () => initial_resume_line(resumed_id, initial_history)
549
+ );
550
+ const resume_loading_ref = useRef(false);
551
+ const [resume_loading, set_resume_loading] = useState3(false);
552
+ const begin_resume_load = useCallback(() => {
553
+ resume_loading_ref.current = true;
554
+ set_resume_loading(true);
555
+ }, []);
556
+ const end_resume_load = useCallback(() => {
557
+ resume_loading_ref.current = false;
558
+ set_resume_loading(false);
559
+ }, []);
407
560
  const add_blocks = useCallback((added) => {
408
561
  if (added.length === 0) {
409
562
  return;
410
563
  }
411
564
  set_blocks((current) => [...current, ...added].slice(-HISTORY_CAP));
412
565
  }, []);
413
- const start_message_run = use_agent_run(agent, theme, add_blocks, set_state, set_blocks);
414
- const handle_slash = use_slash_commands(agent, theme, add_blocks, set_blocks, state.usage.total_tokens);
566
+ const { start_message_run, set_history } = use_agent_run(
567
+ agent,
568
+ theme,
569
+ add_blocks,
570
+ set_state,
571
+ set_blocks,
572
+ initial_history,
573
+ session
574
+ );
575
+ const handle_slash = use_slash_commands(
576
+ agent,
577
+ theme,
578
+ add_blocks,
579
+ set_blocks,
580
+ set_history,
581
+ set_resume_line,
582
+ begin_resume_load,
583
+ end_resume_load,
584
+ state.usage.total_tokens
585
+ );
415
586
  const submit = useCallback(
416
587
  (text) => {
588
+ if (resume_loading_ref.current === true) {
589
+ return;
590
+ }
417
591
  const parsed = parse_command(text);
418
592
  if (parsed.kind === "message") {
419
593
  if (parsed.text.length > 0) {
@@ -426,21 +600,38 @@ function TuiApp({ agent, theme }) {
426
600
  [handle_slash, start_message_run]
427
601
  );
428
602
  const provider = agent.config.providers[0];
603
+ const banner = tui_banner_text(theme, LICH_VERSION, provider?.model ?? "unknown", provider?.kind ?? "unknown");
429
604
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", minHeight: 8, children: [
430
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: tui_banner_text(theme, LICH_VERSION, provider?.model ?? "unknown", provider?.kind ?? "unknown") }),
605
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: resume_line === void 0 ? banner : `${banner}
606
+ ${resume_line}` }),
431
607
  /* @__PURE__ */ jsx4(MessageView, { blocks, state }),
432
608
  /* @__PURE__ */ jsx4(StatusBar, { state, model: provider?.model ?? "unknown", theme }),
433
- /* @__PURE__ */ jsx4(CommandBar, { busy: state.phase !== "idle", on_submit: submit })
609
+ /* @__PURE__ */ jsx4(CommandBar, { busy: state.phase !== "idle" || resume_loading, on_submit: submit })
434
610
  ] });
435
611
  }
436
612
 
437
613
  // src/tui.tsx
438
614
  import { jsx as jsx5 } from "react/jsx-runtime";
439
- async function run_tui(config) {
615
+ async function run_tui(config, options = {}) {
440
616
  const agent = await create_agent_with_plugins(config);
441
617
  try {
442
618
  const theme = load_theme(config.theme);
443
- const instance = render(/* @__PURE__ */ jsx5(TuiApp, { agent, theme }));
619
+ const session = await open_session(config.session_dir, "tui").catch((error) => {
620
+ logger.warn("session persistence failed; continuing without transcript", error);
621
+ return void 0;
622
+ });
623
+ const instance = render(
624
+ /* @__PURE__ */ jsx5(
625
+ TuiApp,
626
+ {
627
+ agent,
628
+ theme,
629
+ session,
630
+ initial_history: options.initial_history,
631
+ resumed_id: options.resumed_id
632
+ }
633
+ )
634
+ );
444
635
  await instance.waitUntilExit();
445
636
  return 0;
446
637
  } finally {
@@ -450,4 +641,4 @@ async function run_tui(config) {
450
641
  export {
451
642
  run_tui
452
643
  };
453
- //# sourceMappingURL=tui-MEJGEILU.js.map
644
+ //# sourceMappingURL=tui-XXIU4W7K.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 \"compress_end\":\n if (event.usage === undefined) {\n return state;\n }\n return {\n ...state,\n usage: {\n prompt_tokens: state.usage.prompt_tokens + event.usage.prompt_tokens,\n completion_tokens: state.usage.completion_tokens + event.usage.completion_tokens,\n total_tokens: state.usage.total_tokens + event.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":";;;;;;;;;;;;;;;;;;;;;AAIA,SAAS,cAAc;;;ACCvB,SAAS,aAAa,aAAAA,YAAW,QAAQ,YAAAC,iBAAoD;AAC7F,SAAS,OAAAC,MAAK,QAAAC,aAAY;AAQ1B,SAAS,SAAS,YAAY;;;ACX9B,OAAO,UAAU;;;ACsBV,IAAM,mBAA4B;AAAA,EACvC,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,OAAO,EAAE,eAAe,GAAG,mBAAmB,GAAG,cAAc,EAAE;AAAA,EACjE,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,kBAAkB;AAAA,EAClB,YAAY;AAAA,EACZ,aAAa;AACf;AAEO,IAAM,cAAc;AAC3B,IAAM,0BAA0B;AAChC,IAAM,4BAA4B;AAClC,IAAM,sBAAsB;AAE5B,SAAS,WAAW,OAAwB;AAC1C,MAAI,iBAAiB,OAAO;AAC1B,WAAO,MAAM;AAAA,EACf;AACA,SAAO,OAAO,KAAK;AACrB;AAGO,SAAS,YAAY,OAAgB,OAA4B;AACtE,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,OAAO,YAAY,aAAa,OAAU;AAAA,IAC/D,KAAK;AACH,aAAO;AAAA,QACL,GAAG;AAAA,QACH,OAAO;AAAA,UACL,eAAe,MAAM,MAAM,gBAAgB,MAAM,OAAO,MAAM;AAAA,UAC9D,mBAAmB,MAAM,MAAM,oBAAoB,MAAM,OAAO,MAAM;AAAA,UACtE,cAAc,MAAM,MAAM,eAAe,MAAM,OAAO,MAAM;AAAA,QAC9D;AAAA,MACF;AAAA,IACF,KAAK;AACH,UAAI,MAAM,UAAU,QAAW;AAC7B,eAAO;AAAA,MACT;AACA,aAAO;AAAA,QACL,GAAG;AAAA,QACH,OAAO;AAAA,UACL,eAAe,MAAM,MAAM,gBAAgB,MAAM,MAAM;AAAA,UACvD,mBAAmB,MAAM,MAAM,oBAAoB,MAAM,MAAM;AAAA,UAC/D,cAAc,MAAM,MAAM,eAAe,MAAM,MAAM;AAAA,QACvD;AAAA,MACF;AAAA,IACF,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,OAAO,QAAQ,aAAa,MAAM,KAAK;AAAA,IAC5D,KAAK;AACH,aAAO;AAAA,QACL,GAAG;AAAA,QACH,OAAO;AAAA,QACP,aAAa;AAAA,QACb,YACE,MAAM,cAAc,QAAQ,MAAM,OAAO,OAAO,OAC5C,MAAM,aACL,MAAM,OAAO,SAAS;AAAA,MAC/B;AAAA,IACF,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,YAAY,MAAM,KAAK;AAAA,IAC5C,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,gBAAgB,MAAM,iBAAiB,EAAE;AAAA,IAC9D,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,kBAAkB,KAAK;AAAA,IAC5C,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,YAAY,WAAW,MAAM,KAAK,EAAE;AAAA,IACzD;AACE,aAAO;AAAA,EACX;AACF;AAGO,SAAS,iBAAiB,OAAgB,QAAiC;AAChF,SAAO;AAAA,IACL,GAAG;AAAA,IACH,OAAO;AAAA,IACP,YAAY,OAAO,QAAQ;AAAA,IAC3B,cAAc,OAAO,gBAAgB,MAAM;AAAA,IAC3C,YAAY,OAAO,QAAQ,mBAAmB,YAAY,gBAAgB,MAAM;AAAA,EAClF;AACF;AAKO,SAAS,cAAc,WAAgC;AAC5D,QAAM,OAAO,UAAU,KAAK;AAC5B,MAAI,KAAK,WAAW,GAAG,MAAM,OAAO;AAClC,WAAO,EAAE,MAAM,WAAW,KAAK;AAAA,EACjC;AACA,QAAM,OAAO,KAAK,MAAM,CAAC;AACzB,QAAM,cAAc,KAAK,QAAQ,GAAG;AACpC,MAAI,gBAAgB,IAAI;AACtB,WAAO,EAAE,MAAM,SAAS,MAAM,MAAM,MAAM,GAAG;AAAA,EAC/C;AACA,SAAO,EAAE,MAAM,SAAS,MAAM,KAAK,MAAM,GAAG,WAAW,GAAG,MAAM,KAAK,MAAM,cAAc,CAAC,EAAE,KAAK,EAAE;AACrG;AAGO,SAAS,gBAAgB,OAAkB,SAAiB,OAAe,MAAsB;AACtG,SAAO,cAAc,MAAM,SAAS,EAAE,SAAS,OAAO,KAAK,CAAC;AAC9D;AAGO,SAAS,oBAAoB,UAAkD;AACpF,MAAI,aAAa,QAAW;AAC1B,WAAO;AAAA,EACT;AACA,SAAO,SAAS,OAAO,CAAC,YAAY,QAAQ,SAAS,QAAQ,EAAE;AACjE;AAGO,SAAS,mBAAmB,IAAY,eAA+B;AAC5E,SAAO,WAAW,EAAE,KAAK,aAAa;AACxC;AAGO,SAAS,aAAa,cAA8B;AACzD,SAAO,aAAa,eAAe,OAAO;AAC5C;AASA,SAAS,oBAAoB,MAAwB;AACnD,QAAM,YAAY,KAAK,UAAU,KAAK,IAAI,KAAK;AAC/C,SAAO,YAAY,cAAc,WAAW,uBAAuB,CAAC;AACtE;AAEA,SAAS,qBAAqB,SAAkB,OAA4B;AAC1E,MAAI,QAAQ,SAAS,QAAQ;AAC3B,WAAO,CAAC,GAAG,MAAM,UAAU,WAAW,QAAQ,OAAO,EAAE;AAAA,EACzD;AACA,MAAI,QAAQ,SAAS,aAAa;AAChC,UAAM,QAAQ,CAAC,GAAG,MAAM,cAAc,WAAW,QAAQ,OAAO,EAAE;AAClE,eAAW,QAAQ,QAAQ,cAAc,CAAC,GAAG;AAC3C,YAAM,KAAK,oBAAoB,IAAI,CAAC;AAAA,IACtC;AACA,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,SAAS,QAAQ;AAC3B,UAAM,OAAO,QAAQ,aAAa,OAAO,UAAU;AACnD,WAAO,CAAC,YAAY,QAAQ,IAAI,KAAK,IAAI,KAAK,cAAc,QAAQ,SAAS,yBAAyB,CAAC,GAAG;AAAA,EAC5G;AACA,SAAO,CAAC,gBAAkB,QAAQ,OAAO,EAAE;AAC7C;AAGO,SAAS,qBAAqB,SAAkB,OAAgC;AACrF,MAAI,QAAQ,SAAS,QAAQ;AAC3B,UAAM,KAAK,QAAQ,aAAa;AAChC,WAAO,EAAE,MAAM,OAAO,OAAO,SAAS,SAAS,OAAO,qBAAqB,SAAS,KAAK,EAAE;AAAA,EAC7F;AACA,QAAM,QAA6D;AAAA,IACjE,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,WAAW;AAAA,EACb;AACA,SAAO,EAAE,MAAM,MAAM,QAAQ,IAAI,GAAG,OAAO,qBAAqB,SAAS,KAAK,EAAE;AAClF;AAGO,SAAS,qBAAqB,UAA8B,KAAa,OAAkC;AAChH,QAAM,UAAU,SAAS,OAAO,CAAC,YAAY,QAAQ,SAAS,QAAQ;AACtE,QAAM,QAAQ,KAAK,IAAI,GAAG,QAAQ,SAAS,GAAG;AAC9C,SAAO,QAAQ,MAAM,KAAK,EAAE,IAAI,CAAC,YAAY,qBAAqB,SAAS,KAAK,CAAC;AACnF;AAGO,SAAS,oBACd,IACA,UACA,OACA,MAAc,aAC4C;AAC1D,QAAM,cAAc,mBAAmB,IAAI,oBAAoB,QAAQ,CAAC;AACxE,QAAM,SAAuB,EAAE,MAAM,QAAQ,OAAO,CAAC,QAAU,WAAW,EAAE,EAAE;AAC9E,QAAM,cAAc,KAAK,IAAI,GAAG,MAAM,CAAC;AACvC,SAAO,EAAE,QAAQ,CAAC,QAAQ,GAAG,qBAAqB,UAAU,aAAa,KAAK,CAAC,GAAG,YAAY;AAChG;AAGO,SAAS,4BAA0C;AACxD,SAAO,EAAE,MAAM,SAAS,OAAO,CAAC,mCAAqC,EAAE;AACzE;AAEA,SAAS,uBAAuB,SAA2B,OAA4C;AACrG,MAAI,QAAQ,QAAQ,WAAW,GAAG;AAChC,WAAO;AAAA,EACT;AACA,SAAO,EAAE,MAAM,QAAQ,OAAO,CAAC,GAAG,MAAM,cAAc,WAAW,QAAQ,OAAO,EAAE,EAAE;AACtF;AAGO,SAAS,kBAAkB,QAAwB,OAAkC;AAC1F,QAAM,SAAyB,CAAC;AAChC,MAAI,OAAO,QAAQ,mBAAmB,UAAU;AAC9C,WAAO,KAAK,EAAE,MAAM,SAAS,OAAO,CAAC,QAAU,cAAc,MAAM,QAAQ,kBAAkB,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AAAA,EACvG;AACA,QAAM,cAAc,OAAO,QAAQ,UAAU,SAAY,SAAY,uBAAuB,OAAO,QAAQ,OAAO,KAAK;AACvH,MAAI,gBAAgB,QAAW;AAC7B,WAAO,KAAK,WAAW;AAAA,EACzB;AACA,SAAO;AACT;AAEA,SAAS,sBAAsB,gBAAwB,IAAqB;AAC1E,SAAO,YAAY,OAAO,OAAO,OAAO,OAAO,KAAK,cAAc,gBAAgB,yBAAyB,CAAC;AAC9G;AAGO,SAAS,kBAAkB,MAAgB,IAAa,gBAAsC;AACnG,QAAM,YAAY,KAAK,UAAU,KAAK,IAAI,KAAK;AAC/C,QAAM,QAAQ;AAAA,IACZ,UAAU,KAAK,IAAI,IAAI,cAAc,WAAW,uBAAuB,CAAC;AAAA,IACxE,sBAAsB,gBAAgB,EAAE;AAAA,EAC1C;AACA,SAAO,EAAE,MAAM,OAAO,OAAO,SAAS,SAAS,MAAM;AACvD;AAUO,SAAS,sBAAsB,eAAuB,OAAgC;AAC3F,QAAM,OAAO,cAAc,MAAM,QAAQ,YAAY,EAAE,OAAO,cAAc,CAAC;AAC7E,SAAO,EAAE,MAAM,QAAQ,OAAO,CAAC,QAAU,IAAI,EAAE,EAAE;AACnD;AAEO,SAAS,mBAAmB,SAA+B;AAChE,SAAO,EAAE,MAAM,SAAS,OAAO,CAAC,eAAiB,cAAc,SAAS,mBAAmB,CAAC,EAAE,EAAE;AAClG;AAEO,SAAS,kBAAkB,MAAuC;AACvE,QAAM,YAAY,KAAK,UAAU,IAAI,KAAK;AAC1C,SAAO,cAAc,WAAW,uBAAuB;AACzD;AAEO,SAAS,aAA2B;AACzC,SAAO,EAAE,MAAM,QAAQ,OAAO,CAAC,GAAG,UAAU,EAAE;AAChD;AAEO,SAAS,kBAAkB,QAAiF;AACjH,QAAM,WAAW,OAAO,UAAU,CAAC;AACnC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO,CAAC,eAAiB,UAAU,SAAS,SAAS,mBAAqB,UAAU,QAAQ,SAAS,EAAE;AAAA,EACzG;AACF;AAEO,SAAS,mBAAmB,cAAoC;AACrE,SAAO,EAAE,MAAM,QAAQ,OAAO,CAAC,kCAAoC,aAAa,YAAY,CAAC,EAAE,EAAE;AACnG;AAEO,SAAS,sBAAsB,MAA4B;AAChE,SAAO,EAAE,MAAM,SAAS,OAAO,CAAC,0BAA4B,IAAI,cAAc,EAAE;AAClF;AASO,SAAS,mBAAmB,SAAsC,OAAkB,MAAc,IAAkB;AACzH,QAAM,SAAS,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,GAAG;AAChF,MAAI,OAAO,WAAW,GAAG;AACvB,WAAO,EAAE,MAAM,QAAQ,OAAO,CAAC,2BAA6B,EAAE;AAAA,EAChE;AACA,QAAM,QAAQ,cAAc,MAAM,QAAQ,UAAU,EAAE,OAAO,OAAO,OAAO,CAAC;AAC5E,QAAM,QAAkB,CAAC,QAAU,KAAK,EAAE;AAC1C,aAAW,SAAS,QAAQ;AAC1B,UAAM,KAAK,KAAK,MAAM,IAAI,KAAK,aAAa,MAAM,UAAU,CAAC,SAAS;AAAA,EACxE;AACA,SAAO,EAAE,MAAM,QAAQ,MAAM;AAC/B;AAcO,IAAM,aAAgC;AAAA,EAC3C;AAAA,EACA;AACF;;;AD3TA,SAAS,eAAe,OAAwB;AAC9C,SAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AAGA,eAAsB,iBACpB,aACA,OACA,OAC2B;AAC3B,MAAI;AACF,UAAM,aAAa,MAAM,qBAAqB,aAAa,KAAK;AAChE,UAAM,WAAW,MAAM,sBAAsB,UAAU;AACvD,UAAM,KAAK,KAAK,SAAS,YAAY,QAAQ;AAC7C,UAAM,OAAO,oBAAoB,IAAI,UAAU,KAAK;AACpD,WAAO,EAAE,IAAI,MAAM,IAAI,UAAU,QAAQ,KAAK,QAAQ,aAAa,KAAK,YAAY;AAAA,EACtF,SAAS,OAAO;AACd,WAAO,EAAE,IAAI,OAAO,OAAO,mBAAmB,eAAe,KAAK,CAAC,EAAE;AAAA,EACvE;AACF;;;AE5BA,SAAS,WAAW,gBAAgB;AACpC,SAAS,KAAK,YAAY;AAuBjB,SAcL,UAdK,KA8BL,YA9BK;AApBT,IAAM,iBAAoC,CAAC,UAAK,UAAK,UAAK,UAAK,UAAK,UAAK,UAAK,UAAK,UAAK,QAAG;AAC3F,IAAM,sBAAsB;AAG5B,SAAS,cAAsB;AAC7B,QAAM,CAAC,OAAO,SAAS,IAAI,SAAS,eAAe,CAAC,KAAK,QAAG;AAC5D,YAAU,MAAM;AACd,UAAM,QAAQ,YAAY,MAAM;AAC9B,YAAM,OAAO,gBAAgB,eAAe,QAAQ,KAAK,IAAI,KAAK,eAAe,MAAM;AACvF,gBAAU,QAAQ,QAAG;AAAA,IACvB,GAAG,mBAAmB;AACtB,WAAO,MAAM;AACX,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,KAAK,CAAC;AACV,SAAO;AACT;AAEA,SAAS,eAAkC;AACzC,QAAM,QAAQ,YAAY;AAC1B,SAAO,oBAAC,QAAK,UAAQ,MAAE,aAAG,KAAK,mBAAa;AAC9C;AAEA,IAAM,cAAgE;AAAA,EACpE,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AACT;AAEA,SAAS,WAAW,EAAE,MAAM,GAA+C;AACzE,QAAM,QAAQ,YAAY,MAAM,IAAI;AACpC,SACE,gCACG,gBAAM,MAAM,IAAI,CAAC,MAAM,UACtB,oBAAC,QAAiB,OAAc,UAAU,UAAU,QAAY,kBAArD,KAA0D,CACtE,GACH;AAEJ;AAQO,SAAS,YAAY,EAAE,QAAQ,MAAM,GAAwC;AAClF,SACE,qBAAC,OAAI,eAAc,UAAS,UAAU,GACnC;AAAA,WAAO,IAAI,CAAC,OAAO,UAClB,oBAAC,OAAgB,eAAc,UAC7B,8BAAC,cAAW,OAAc,KADlB,KAEV,CACD;AAAA,IACA,MAAM,UAAU,aAAa,oBAAC,gBAAa,IAAK;AAAA,IAChD,MAAM,UAAU,UAAU,MAAM,gBAAgB,SAC/C,oBAAC,QAAK,OAAM,QAAQ,oBAAK,MAAM,YAAY,IAAI,IAAI,kBAAkB,MAAM,YAAY,IAAI,CAAC,KAAI,IAC9F;AAAA,KACN;AAEJ;;;ACnEA,SAAS,OAAAC,MAAK,QAAAC,aAAY;AAcpB,SAK0B,OAAAC,MAL1B,QAAAC,aAAA;AAJC,SAAS,UAAU,EAAE,OAAO,OAAO,MAAM,GAAsC;AACpF,QAAM,QAAQ,MAAM,aAAa,MAAM,KAAK;AAC5C,SACE,gBAAAA,MAACC,MAAA,EACC;AAAA,oBAAAD,MAACE,OAAA,EAAK,UAAQ,MACX;AAAA,eAAS,KAAK,eAAY,MAAM,UAAU,gBAAa,aAAa,MAAM,MAAM,YAAY,CAAC,UAAO,KAAK;AAAA,MACzG,MAAM,iBAAiB,IAAI,oBAAiB,MAAM,cAAc,KAAK;AAAA,MACrE,MAAM,iBAAiB,SAAY,SAAM,MAAM,YAAY,KAAK;AAAA,OACnE;AAAA,IACC,MAAM,mBAAmB,gBAAAH,KAACG,OAAA,EAAK,OAAM,OAAO,mBAAM,MAAM,QAAQ,gBAAgB,IAAG,IAAU;AAAA,KAChG;AAEJ;;;ACrBA,SAAS,YAAAC,iBAAgB;AACzB,SAAS,OAAAC,MAAK,QAAAC,OAAM,gBAAgB;AA6EhC,SACE,OAAAC,MADF,QAAAC,aAAA;AA3EJ,IAAM,oBAAoB;AAQ1B,SAAS,aAAa,MAAyB,OAAkC;AAC/E,SAAO,CAAC,OAAO,GAAG,KAAK,OAAO,CAAC,SAAS,SAAS,KAAK,CAAC,EAAE,MAAM,GAAG,iBAAiB;AACrF;AAEO,SAAS,WAAW,EAAE,MAAM,UAAU,GAAuC;AAClF,QAAM,CAAC,QAAQ,UAAU,IAAIJ,UAAS,EAAE;AACxC,QAAM,CAAC,aAAa,eAAe,IAAIA,UAA4B,CAAC,CAAC;AACrE,QAAM,CAAC,cAAc,gBAAgB,IAAIA,UAA6B,MAAS;AAE/E,QAAM,gBAAgB,MAAY;AAChC,QAAI,SAAS,MAAM;AACjB;AAAA,IACF;AACA,UAAM,OAAO,OAAO,KAAK;AACzB,eAAW,EAAE;AACb,qBAAiB,MAAS;AAC1B,QAAI,KAAK,SAAS,GAAG;AACnB,sBAAgB,CAAC,YAAY,aAAa,SAAS,IAAI,CAAC;AACxD,gBAAU,IAAI;AAAA,IAChB;AAAA,EACF;AAGA,QAAM,cAAc,CAAC,cAA4B;AAC/C,QAAI,YAAY,WAAW,GAAG;AAC5B;AAAA,IACF;AACA,QAAI,cAAc,MAAM,iBAAiB,GAAG;AAC1C,uBAAiB,MAAS;AAC1B,iBAAW,EAAE;AACb;AAAA,IACF;AACA,QAAI,cAAc,MAAM,iBAAiB,QAAW;AAClD;AAAA,IACF;AACA,UAAM,UAAU,gBAAgB,CAAC;AACjC,UAAM,OAAO,KAAK,IAAI,KAAK,IAAI,UAAU,WAAW,CAAC,GAAG,YAAY,SAAS,CAAC;AAC9E,qBAAiB,IAAI;AACrB,eAAW,YAAY,IAAI,KAAK,EAAE;AAAA,EACpC;AAEA,WAAS,CAAC,OAAO,QAAQ;AACvB,QAAI,IAAI,WAAW,MAAM;AACvB,oBAAc;AACd;AAAA,IACF;AACA,QAAI,IAAI,YAAY,MAAM;AACxB,kBAAY,CAAC;AACb;AAAA,IACF;AACA,QAAI,IAAI,cAAc,MAAM;AAC1B,kBAAY,EAAE;AACd;AAAA,IACF;AACA,QAAI,IAAI,cAAc,QAAQ,IAAI,WAAW,MAAM;AACjD,iBAAW,CAAC,YAAY,QAAQ,MAAM,GAAG,EAAE,CAAC;AAC5C;AAAA,IACF;AACA,QAAI,IAAI,SAAS,QAAQ,IAAI,WAAW,QAAQ,IAAI,QAAQ,QAAQ,IAAI,SAAS,MAAM;AACrF;AAAA,IACF;AACA,QAAI,MAAM,SAAS,GAAG;AACpB,iBAAW,CAAC,YAAY,UAAU,MAAM,WAAW,MAAM,GAAG,EAAE,WAAW,MAAM,GAAG,CAAC;AAAA,IACrF;AAAA,EACF,CAAC;AAED,SACE,gBAAAI,MAACH,MAAA,EACC;AAAA,oBAAAE,KAACD,OAAA,EAAK,UAAQ,MAAE,iBAAO,cAAS,WAAK;AAAA,IACrC,gBAAAC,KAACD,OAAA,EAAM,kBAAO;AAAA,IACd,gBAAAC,KAACD,OAAA,EAAK,UAAQ,MAAC,oBAAC;AAAA,KAClB;AAEJ;;;AL6OI,SACE,OAAAG,MADF,QAAAC,aAAA;AAzRJ,IAAM,mBAAmB;AAczB,SAAS,aAAa,OAAmB,OAA2C;AAClF,MAAI,MAAM,SAAS,iBAAiB;AAClC,QAAI,MAAM,cAAc,MAAM;AAC5B,aAAO,CAAC;AAAA,IACV;AACA,WAAO,CAAC,kBAAkB,MAAM,MAAM,MAAM,OAAO,OAAO,MAAM,MAAM,OAAO,MAAM,CAAC;AAAA,EACtF;AACA,MAAI,MAAM,SAAS,gBAAgB;AACjC,WAAO,CAAC,sBAAsB,MAAM,eAAe,KAAK,CAAC;AAAA,EAC3D;AACA,MAAI,MAAM,SAAS,SAAS;AAC1B,WAAO,CAAC,mBAAmB,MAAM,iBAAiB,QAAQ,MAAM,MAAM,UAAU,OAAO,MAAM,KAAK,CAAC,CAAC;AAAA,EACtG;AACA,SAAO,CAAC;AACV;AAGA,eAAe,eACb,OACA,SACA,OACA,UACA,SACA,QACA,SACe;AACf,QAAM,iBAAiB,MAAM,OAAO,GAAG,QAAQ;AAC/C,MAAI;AACF,UAAM,SAAS,MAAM,MAAM,IAAI,EAAE,OAAO,SAAS,QAAQ,OAAO,OAAO,QAAQ,CAAC;AAChF,YAAQ,MAAM;AAAA,EAChB,UAAE;AACA,mBAAe;AAAA,EACjB;AACF;AAGA,eAAe,eAAe,QAAqB,OAAyC;AAC1F,MAAI;AACF,UAAM,cAAc,MAAM,QAAQ,OAAO,aAAa,EAAE,eAAe,KAAK,CAAC;AAC7E,UAAM,UAA8B,CAAC;AACrC,eAAW,SAAS,aAAa;AAC/B,UAAI,MAAM,OAAO,MAAM,SAAS,MAAM,KAAK,SAAS,QAAQ,MAAM,OAAO;AACvE;AAAA,MACF;AACA,YAAM,OAAO,MAAM,KAAK,GAAG,OAAO,WAAW,IAAI,MAAM,IAAI,EAAE;AAC7D,cAAQ,KAAK,EAAE,MAAM,MAAM,MAAM,YAAY,KAAK,MAAM,UAAU,KAAK,QAAQ,CAAC;AAAA,IAClF;AACA,WAAO,mBAAmB,SAAS,OAAO,gBAAgB;AAAA,EAC5D,QAAQ;AACN,WAAO,EAAE,MAAM,QAAQ,OAAO,CAAC,2BAAwB,EAAE;AAAA,EAC3D;AACF;AAEA,SAASC,gBAAe,OAAwB;AAC9C,SAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AAGA,SAAS,cACP,OACA,OACA,YACA,WACA,YACA,iBACA,SACkB;AAClB,QAAM,cAAc,OAA2B,mBAAmB,CAAC,CAAC;AACpE,QAAM,iBAAiB,OAAoC,MAAS;AAEpE,QAAM,cAAc,YAAwB,CAAC,aAAuC;AAClF,gBAAY,UAAU;AAAA,EACxB,GAAG,CAAC,CAAC;AAEL,QAAM,aAAa,YAAY,CAAC,WAAiC;AAC/D,gBAAY,UAAU,OAAO;AAC7B,cAAU,CAAC,YAAY,iBAAiB,SAAS,MAAM,CAAC;AACxD,eAAW,kBAAkB,QAAQ,KAAK,CAAC;AAAA,EAC7C,GAAG,CAAC,YAAY,WAAW,KAAK,CAAC;AAEjC,QAAM,oBAAoB;AAAA,IACxB,CAAC,SAAuB;AACtB,iBAAW,CAAC,EAAE,MAAM,QAAQ,OAAO,CAAC,GAAG,MAAM,UAAU,WAAM,IAAI,EAAE,EAAE,CAAC,CAAC;AACvE,gBAAU,CAAC,aAAa,EAAE,GAAG,SAAS,OAAO,YAAY,aAAa,OAAU,EAAE;AAClF,YAAM,aAAa,IAAI,gBAAgB;AACvC,qBAAe,UAAU;AACzB,YAAM,WAAW,CAAC,UAA4B;AAC5C,kBAAU,CAAC,YAAY,YAAY,SAAS,KAAK,CAAC;AAClD,mBAAW,CAAC,YAAY,CAAC,GAAG,SAAS,GAAG,aAAa,OAAO,KAAK,CAAC,EAAE,MAAM,CAAC,WAAW,CAAC;AAAA,MACzF;AACA,WAAK,eAAe,OAAO,YAAY,SAAS,MAAM,UAAU,YAAY,WAAW,QAAQ,OAAO,EACnG,MAAM,CAAC,UAAmB;AACzB,mBAAW,CAAC,mBAAmBA,gBAAe,KAAK,CAAC,CAAC,CAAC;AACtD,kBAAU,CAAC,aAAa,EAAE,GAAG,SAAS,OAAO,OAAO,EAAE;AAAA,MACxD,CAAC,EACA,QAAQ,MAAM;AACb,YAAI,eAAe,YAAY,YAAY;AACzC,yBAAe,UAAU;AAAA,QAC3B;AAAA,MACF,CAAC;AAAA,IACL;AAAA,IACA,CAAC,OAAO,YAAY,YAAY,SAAS,YAAY,WAAW,KAAK;AAAA,EACvE;AAEA,EAAAC,WAAU,MAAM,MAAM,eAAe,SAAS,MAAM,GAAG,CAAC,CAAC;AAEzD,SAAO,EAAE,mBAAmB,YAAY;AAC1C;AAGA,SAAS,mBACP,OACA,OACA,YACA,YACA,aACA,iBACA,mBACA,iBACA,cAC8B;AAC9B,QAAM,SAAS;AAAA,IACb,CAAC,WAA6B;AAC5B,UAAI,OAAO,SAAS,UAAU,OAAO,SAAS,UAAU,OAAO,SAAS,KAAK;AAC3E,gBAAQ,KAAK,CAAC;AACd;AAAA,MACF;AACA,UAAI,OAAO,SAAS,QAAQ;AAC1B,mBAAW,CAAC,WAAW,CAAC,CAAC;AAAA,MAC3B,WAAW,OAAO,SAAS,SAAS;AAClC,mBAAW,CAAC,kBAAkB,MAAM,MAAM,CAAC,CAAC;AAAA,MAC9C,WAAW,OAAO,SAAS,SAAS;AAClC,mBAAW,CAAC,mBAAmB,YAAY,CAAC,CAAC;AAAA,MAC/C,WAAW,OAAO,SAAS,SAAS;AAClC,mBAAW,CAAC,CAAC;AAAA,MACf,WAAW,OAAO,SAAS,YAAY;AACrC,aAAK,eAAe,MAAM,QAAQ,KAAK,EAAE,KAAK,CAAC,UAAU,WAAW,CAAC,KAAK,CAAC,CAAC;AAAA,MAC9E,WAAW,OAAO,SAAS,UAAU;AACnC,YAAI,OAAO,KAAK,WAAW,GAAG;AAC5B,qBAAW,CAAC,0BAA0B,CAAC,CAAC;AACxC;AAAA,QACF;AACA,0BAAkB;AAClB,aAAK,iBAAiB,MAAM,OAAO,aAAa,OAAO,MAAM,KAAK,EAC/D,KAAK,CAAC,WAAW;AAChB,cAAI,OAAO,OAAO,OAAO;AACvB,uBAAW,CAAC,OAAO,KAAK,CAAC;AACzB;AAAA,UACF;AACA,sBAAY,OAAO,QAAQ;AAC3B,qBAAW,OAAO,MAAM;AACxB,0BAAgB,OAAO,WAAW;AAAA,QACpC,CAAC,EACA,QAAQ,MAAM;AACb,0BAAgB;AAAA,QAClB,CAAC;AAAA,MACL,OAAO;AACL,mBAAW,CAAC,sBAAsB,OAAO,IAAI,CAAC,CAAC;AAAA,MACjD;AAAA,IACF;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAUA,SAAS,eAAe,SAAyC,OAA2C;AAC1G,MAAI,YAAY,UAAa,QAAQ,WAAW,GAAG;AACjD,WAAO,CAAC;AAAA,EACV;AACA,SAAO,qBAAqB,SAAS,aAAa,KAAK;AACzD;AAEA,SAAS,oBACP,YACA,SACoB;AACpB,MAAI,eAAe,QAAW;AAC5B,WAAO;AAAA,EACT;AACA,SAAO,mBAAmB,YAAY,oBAAoB,OAAO,CAAC;AACpE;AAEO,SAAS,OAAO,EAAE,OAAO,OAAO,SAAS,iBAAiB,WAAW,GAAmC;AAC7G,QAAM,CAAC,QAAQ,UAAU,IAAIC,UAAkC,MAAM,eAAe,iBAAiB,KAAK,CAAC;AAC3G,QAAM,CAAC,OAAO,SAAS,IAAIA,UAAS,gBAAgB;AACpD,QAAM,CAAC,aAAa,eAAe,IAAIA;AAAA,IAA6B,MAClE,oBAAoB,YAAY,eAAe;AAAA,EACjD;AACA,QAAM,qBAAqB,OAAO,KAAK;AACvC,QAAM,CAAC,gBAAgB,kBAAkB,IAAIA,UAAS,KAAK;AAE3D,QAAM,oBAAoB,YAAY,MAAY;AAChD,uBAAmB,UAAU;AAC7B,uBAAmB,IAAI;AAAA,EACzB,GAAG,CAAC,CAAC;AAEL,QAAM,kBAAkB,YAAY,MAAY;AAC9C,uBAAmB,UAAU;AAC7B,uBAAmB,KAAK;AAAA,EAC1B,GAAG,CAAC,CAAC;AAEL,QAAM,aAAa,YAAuB,CAAC,UAAyC;AAClF,QAAI,MAAM,WAAW,GAAG;AACtB;AAAA,IACF;AACA,eAAW,CAAC,YAAY,CAAC,GAAG,SAAS,GAAG,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC;AAAA,EACpE,GAAG,CAAC,CAAC;AAEL,QAAM,EAAE,mBAAmB,YAAY,IAAI;AAAA,IACzC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,QAAM,eAAe;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,MAAM;AAAA,EACd;AAEA,QAAM,SAAS;AAAA,IACb,CAAC,SAAuB;AACtB,UAAI,mBAAmB,YAAY,MAAM;AACvC;AAAA,MACF;AACA,YAAM,SAAS,cAAc,IAAI;AACjC,UAAI,OAAO,SAAS,WAAW;AAC7B,YAAI,OAAO,KAAK,SAAS,GAAG;AAC1B,4BAAkB,OAAO,IAAI;AAAA,QAC/B;AACA;AAAA,MACF;AACA,mBAAa,MAAM;AAAA,IACrB;AAAA,IACA,CAAC,cAAc,iBAAiB;AAAA,EAClC;AAEA,QAAM,WAAW,MAAM,OAAO,UAAU,CAAC;AACzC,QAAM,SAAS,gBAAgB,OAAO,cAAc,UAAU,SAAS,WAAW,UAAU,QAAQ,SAAS;AAC7G,SACE,gBAAAH,MAACI,MAAA,EAAI,eAAc,UAAS,WAAW,GACrC;AAAA,oBAAAL,KAACM,OAAA,EAAK,UAAQ,MAAE,0BAAgB,SAAY,SAAS,GAAG,MAAM;AAAA,EAAK,WAAW,IAAG;AAAA,IACjF,gBAAAN,KAAC,eAAY,QAAgB,OAAc;AAAA,IAC3C,gBAAAA,KAAC,aAAU,OAAc,OAAO,UAAU,SAAS,WAAW,OAAc;AAAA,IAC5E,gBAAAA,KAAC,cAAW,MAAM,MAAM,UAAU,UAAU,gBAAgB,WAAW,QAAQ;AAAA,KACjF;AAEJ;;;ADlTM,gBAAAO,YAAA;AATN,eAAsB,QAAQ,QAAqB,UAAyB,CAAC,GAAoB;AAC/F,QAAM,QAAQ,MAAM,0BAA0B,MAAM;AACpD,MAAI;AACF,UAAM,QAAQ,WAAW,OAAO,KAAK;AACrC,UAAM,UAAU,MAAM,aAAa,OAAO,aAAa,KAAK,EAAE,MAAM,CAAC,UAAmB;AACtF,aAAO,KAAK,6DAA6D,KAAK;AAC9E,aAAO;AAAA,IACT,CAAC;AACD,UAAM,WAAW;AAAA,MACf,gBAAAA;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA;AAAA,UACA,iBAAiB,QAAQ;AAAA,UACzB,YAAY,QAAQ;AAAA;AAAA,MACtB;AAAA,IACF;AACA,UAAM,SAAS,cAAc;AAC7B,WAAO;AAAA,EACT,UAAE;AACA,UAAM,MAAM;AAAA,EACd;AACF;","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,7 +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.0, including editor MCP (`mcp_servers`, `lich mcp`). It runs on
6
+ is 0.8.0, including editor MCP (`mcp_servers`, `lich mcp`). It runs on
7
7
  Node >= 20 and on Bun, is ESM with NodeNext resolution, and its runtime
8
8
  dependencies are `zod` (config validation), `ink`, and `react` (the TUI).
9
9
  Everything else is Node/Bun built-ins.
@@ -100,7 +100,7 @@ Walkthrough of a single `Agent.run({ input })` call
100
100
  are connected (`initialize`, `notifications/initialized`, then `tools/list`)
101
101
  and registered as `mcp_<server>_<tool>`. An empty `tools_enabled` never
102
102
  connects. Disabled servers are skipped. A connect failure logs a warning
103
- and the run continues. This client ships in 0.7.0.
103
+ and the run continues. This client has shipped since 0.7.0 (current package 0.8.0).
104
104
  2. **Usage collector attached.** `run()` subscribes a `collect_usage` handler
105
105
  on `agent.events`; every `llm_end` event adds the call's token usage into a
106
106
  per-run `Usage` total. The subscription is removed in a `finally` block.
@@ -124,13 +124,14 @@ Walkthrough of a single `Agent.run({ input })` call
124
124
  the final assistant message (or the last one seen), the last `ChatResult`
125
125
  on a real final, `turns_used`, and a `stopped_reason` of `final`, `budget`,
126
126
  or `aborted`.
127
- 8. **Session persist.** `persist_session()` appends one `meta` record
128
- (`run_start`), then one `message` record per outcome message, then a
129
- `budget_exhausted` meta record if the budget stopped the run, then a
130
- `run_end` meta record (`stopped_reason`, `usage`) for every completed
131
- run, to a JSONL file under `session_dir` (default
132
- `<work_dir>/.lich/sessions`). Persistence is best-effort: failures are
133
- 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.
134
135
  9. **Return.** `AgentRunResult` bundles the outcome, the full transcript
135
136
  (prior history plus the new exchange), the collected `usage_total`, and the
136
137
  session path.
@@ -42,7 +42,9 @@ confinement root), a process environment map (the agent injects
42
42
  from `Agent.run` so tools cancel on caller abort **or** the executor deadline
43
43
  (`tool.timeout_ms`, else
44
44
  `DEFAULT_TOOL_TIMEOUT_MS` = 30000). `terminal` sets 300000, `run_tests` sets
45
- 600000, and registered MCP tools set 120000.
45
+ 600000, and registered MCP tools set 120000. Note: `LICH_TERMINAL_TIMEOUT_MS`
46
+ is injected from config `terminal_timeout_ms` but the `terminal` tool ignores
47
+ it today — use the tool's `timeout_ms` argument (default 60000).
46
48
 
47
49
  **Parameter schemas.** `parameters` is a `JsonSchemaObject`
48
50
  (`src/util/json_schema.ts`) passed through verbatim into provider requests.
@@ -10,7 +10,7 @@
10
10
 
11
11
  ```sh
12
12
  npm install -g @moikapy/lich
13
- lich --version # -> 0.7.0 (reads package.json)
13
+ lich --version # -> 0.8.0 (reads package.json)
14
14
  ```
15
15
 
16
16
  ## Choose a configuration path
@@ -23,11 +23,11 @@ Lich needs exactly one thing before it runs: a model. You can provide it three w
23
23
  # ollama — no api key needed
24
24
  LICH_PROVIDER_KIND=ollama LICH_MODEL=llama3.2 lich "Reply with ok"
25
25
 
26
- # openai-compatible (api.openai.com/v1 by default)
27
- LICH_PROVIDER_KIND=openai_compat LICH_MODEL=gpt-4.1-mini lich "Reply with ok"
26
+ # openai-compatible (api.openai.com/v1 by default; use a model id your provider accepts)
27
+ LICH_PROVIDER_KIND=openai_compat LICH_MODEL=gpt-4o-mini lich "Reply with ok"
28
28
 
29
29
  # anthropic
30
- LICH_PROVIDER_KIND=anthropic LICH_MODEL=claude-sonnet-4 lich "Reply with ok"
30
+ LICH_PROVIDER_KIND=anthropic LICH_MODEL=claude-sonnet-4-20250514 lich "Reply with ok"
31
31
  ```
32
32
 
33
33
  Defaults per kind when `LICH_BASE_URL`/`LICH_API_KEY_ENV` are unset: `openai_compat` uses `https://api.openai.com/v1` and reads `OPENAI_API_KEY`; `anthropic` uses `https://api.anthropic.com` and reads `ANTHROPIC_API_KEY`; `ollama` uses `http://localhost:11434` and needs no key.
@@ -76,7 +76,7 @@ lich # TUI; first run on a TTY opens the setup wizard
76
76
  lich tui # same TUI, no wizard
77
77
  ```
78
78
 
79
- Type a message and press Enter. The transcript shows your line, live tool-call rows while the agent works, and the reply; the status bar at the bottom tracks turns, tokens, and the session file path. Slash commands: `/help`, `/model`, `/usage`, `/clear`, `/sessions`, `/exit`. Details in [the TUI guide](user-guide/tui.md).
79
+ Type a message and press Enter. The transcript shows your line, live tool-call rows while the agent works, and the reply; the status bar at the bottom tracks turns, tokens, and the session file path. Slash commands: `/help`, `/model`, `/usage`, `/clear`, `/sessions`, `/resume`, `/exit`. Details in [the TUI guide](user-guide/tui.md).
80
80
 
81
81
  ## Your first gateway webhook
82
82
 
@@ -153,7 +153,7 @@ To hack on Lich itself, run the CLI straight from a clone instead of the npm pac
153
153
  ```sh
154
154
  git clone https://github.com/Moikapy/lich.git && cd lich
155
155
  bun install
156
- bun src/cli.ts --version # -> 0.7.0 (package.json)
156
+ bun src/cli.ts --version # -> 0.8.0 (package.json)
157
157
  ```
158
158
 
159
159
  `bun src/cli.ts` accepts the same arguments as the installed `lich` binary, so every command on this page works unchanged.