atom-lite 0.6.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ All notable changes to this project are documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.8.0] - 2026-11-06
9
+
10
+ ### Added
11
+ - Configurable keyboard shortcuts via the `keybindings` config section.
12
+ - `/shortcuts` command to list active shortcuts.
13
+ - `/keybindings` command to open the keybindings config file.
14
+ - Input history on Up/Down using the editor's native history.
15
+
16
+ ### Fixed
17
+ - Arrow keys no longer break slash-command autocomplete navigation.
18
+
19
+ ## [0.7.0] - 2026-11-06
20
+
21
+ ### Added
22
+ - Clickable “↓ bottom” control above the status line to jump the transcript to the end.
23
+ - `/rename` command (replacing `/session`) for showing or renaming the current session.
24
+
25
+ ### Fixed
26
+ - Resuming a session via `/sessions` now rebuilds rich transcript components (Markdown, collapsible tool boxes with output/diff/error state) instead of flattening to raw text.
27
+ - Thinking blocks are finalized before tool calls, so later thinking steps render as separate blocks.
28
+ - Session JSONL storage trimmed: removed redundant `tool_call_started` events and duplicated top-level message fields.
29
+
30
+ ### Changed
31
+ - Session tool results now persist `isError` and `diff` for faithful resume rendering.
32
+
8
33
  ## [0.6.3] - 2026-11-06
9
34
 
10
35
  ### Added
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  </p>
6
6
 
7
7
  [![npm](https://img.shields.io/npm/v/atom-lite?logo=npm)](https://www.npmjs.com/package/atom-lite)
8
+ [![npm downloads](https://img.shields.io/npm/dt/atom-lite?logo=npm&label=downloads)](https://www.npmjs.com/package/atom-lite)
8
9
  [![CI](https://github.com/beast-ofcourse/atom-lite/actions/workflows/ci.yml/badge.svg)](https://github.com/beast-ofcourse/atom-lite/actions/workflows/ci.yml)
9
10
  [![Node.js](https://img.shields.io/badge/node-%3E%3D22.19-339933?logo=node.js&logoColor=white)](https://nodejs.org)
10
11
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org)
@@ -170,7 +171,7 @@ Type `/help` for the grouped list, or press `ctrl+p` for the fuzzy command palet
170
171
  | `/model [model]` | Show or switch the active model |
171
172
  | `/provider [profile]` | Show or switch the active provider |
172
173
  | `/effort [low\|medium\|high]` | Set reasoning effort |
173
- | `/session [rename <title>]` | Show or rename the current session |
174
+ | `/rename [rename <title>]` | Show or rename the current session |
174
175
  | `/sessions [all]` | Search and resume a session |
175
176
  | `/search <query>` | Search sessions by title or content |
176
177
  | `/export [path]` | Export the session transcript |
@@ -189,7 +190,7 @@ Type `/help` for the grouped list, or press `ctrl+p` for the fuzzy command palet
189
190
 
190
191
  ## Sessions
191
192
 
192
- Sessions are append-only JSONL event logs recording everything in one conversation. They are resumable across CLI restarts and grouped by workspace. The session index derives a title from the first user message unless `/session rename <title>` overrides it, and records the provider, update time, and message count. `/sessions` resumes a session in place: retained messages are replayed into the transcript and a `session_resumed` event is appended. `/search <query>` matches titles, message content, and workspace paths, ranking title hits first. `--list-sessions` and `--search-sessions` print the same index as stable tab-separated rows for scripts. Compaction summarizes older history to stay within the context budget while retaining the full transcript.
193
+ Sessions are append-only JSONL event logs recording everything in one conversation. They are resumable across CLI restarts and grouped by workspace. The session index derives a title from the first user message unless `/rename rename <title>` overrides it, and records the provider, update time, and message count. `/sessions` resumes a session in place: retained messages are replayed into the transcript and a `session_resumed` event is appended. `/search <query>` matches titles, message content, and workspace paths, ranking title hits first. `--list-sessions` and `--search-sessions` print the same index as stable tab-separated rows for scripts. Compaction summarizes older history to stay within the context budget while retaining the full transcript.
193
194
 
194
195
  ## Context management
195
196
 
@@ -61,6 +61,8 @@ export class TurnRecorder {
61
61
  name: result.name,
62
62
  content: result.content,
63
63
  images: result.images,
64
+ isError: result.isError,
65
+ diff: Array.isArray(result.diff) ? result.diff : undefined,
64
66
  });
65
67
  this.onEvent?.({
66
68
  type: "tool_result",
@@ -1 +1 @@
1
- {"version":3,"file":"turn-recorder.js","sourceRoot":"","sources":["../../src/agent/turn-recorder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"turn-recorder.js","sourceRoot":"","sources":["../../src/agent/turn-recorder.ts"],"names":[],"mappings":"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"}
package/dist/cli/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { loadConfig } from "../config/load.js";
2
2
  import { DEFAULT_SYSTEM_PROMPT } from "../agent/system-prompt.js";
3
- import { defaultAgentDir, userConfigDir } from "../config/paths.js";
3
+ import { defaultAgentDir, defaultUserConfigPath, userConfigDir } from "../config/paths.js";
4
4
  import { saveProviderSelection, saveTheme, } from "../config/write.js";
5
5
  import { resolveApiKey, saveApiKey } from "../config/credentials.js";
6
6
  import { join } from "node:path";
@@ -27,7 +27,7 @@ import { collectExtensionTools } from "../extensions/tool-adapter.js";
27
27
  import { denyProject, trustProject } from "../extensions/trust.js";
28
28
  import { ProjectTrustPrompt } from "./trust-prompt.js";
29
29
  import { formatSessionList, formatSessionSearch, searchSessions, sessionTitleFromEvents, } from "../sessions/search.js";
30
- import { providerSwitchNotice, sessionHistoryLines, sessionSelectionFromEvents, } from "../sessions/history.js";
30
+ import { providerSwitchNotice, sessionSelectionFromEvents, } from "../sessions/history.js";
31
31
  import { blockAtLine, createMouseCapture, handleMouseEvent, parseSgrMouse, shouldEnableMouse, } from "./mouse.js";
32
32
  import { readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool, grepTool, globTool, taskTool, } from "../tools/index.js";
33
33
  import { McpManager, formatMcpStatus, formatMcpDoctor, createMcpTools, } from "../mcp/index.js";
@@ -342,7 +342,7 @@ export async function run(args) {
342
342
  return;
343
343
  }
344
344
  // ---- Interactive TUI (first-class) ----
345
- const { TuiAltScreen, Text, Input, Editor, Container, Spacer, ScrollView, VStack, Loader, Markdown, Box, } = await import("@earendil-works/pi-tui");
345
+ const { TuiAltScreen, Text, Input, Editor, Container, Spacer, ScrollView, VStack, Loader, Markdown, Box, visibleWidth, } = await import("@earendil-works/pi-tui");
346
346
  const mouseEnabled = shouldEnableMouse({
347
347
  interactive,
348
348
  configured: config.ui.mouse,
@@ -487,18 +487,37 @@ export async function run(args) {
487
487
  }
488
488
  originalHandleInput(data);
489
489
  };
490
+ const jumpToBottomLabel = "↓ bottom";
491
+ const jumpToBottomView = {
492
+ render(width) {
493
+ const label = output.isFollowingEnd
494
+ ? theme.tokens.dim(jumpToBottomLabel)
495
+ : theme.tokens.accent(jumpToBottomLabel);
496
+ const pad = Math.max(0, width - visibleWidth(jumpToBottomLabel));
497
+ return [" ".repeat(pad) + label];
498
+ },
499
+ invalidate() { },
500
+ };
490
501
  const root = new VStack([]);
491
- // Keep fixed chrome (spacer, input, status, header, dropdown) from being
492
- // collapsed when the transcript grows; only the output region may shrink.
502
+ // Keep fixed chrome (spacer, input, jump-to-bottom, status, header, dropdown)
503
+ // from being collapsed when the transcript grows; only the output region may
504
+ // shrink.
493
505
  root.addChild(output, { grow: 1, shrink: 1, minSize: 0 });
494
506
  root.addChild(new Spacer(1), { grow: 0, shrink: 0, minSize: 1 });
495
507
  root.addChild(input, { grow: 0, shrink: 0 });
508
+ root.addChild(jumpToBottomView, { grow: 0, shrink: 0, minSize: 1 });
496
509
  root.addChild(statusBar, { grow: 0, shrink: 0 });
497
510
  // folder:branch info below the status line, dimmed
498
511
  root.addChild(header, { grow: 0, shrink: 0 });
499
512
  const dropdownContainer = new Container();
500
513
  root.addChild(dropdownContainer, { grow: 0, shrink: 0 });
501
514
  tui.setLayoutRoot(root);
515
+ function jumpToBottomRow() {
516
+ const width = terminal.columns;
517
+ const outputHeight = Math.max(1, output.viewportHeight || output.render(width).length);
518
+ const inputHeight = Math.max(1, input.render(width).length);
519
+ return outputHeight + 1 + inputHeight;
520
+ }
502
521
  tui.setFocus(input);
503
522
  if (!noColor) {
504
523
  try {
@@ -540,6 +559,8 @@ export async function run(args) {
540
559
  let showThinking = config.ui.showThinking;
541
560
  let expandTools = false;
542
561
  let uiModalOpen = false;
562
+ const inputHistory = [];
563
+ let historyIndex = -1;
543
564
  let closeActiveOverlay;
544
565
  const toolBoxes = [];
545
566
  /**
@@ -1320,6 +1341,34 @@ export async function run(args) {
1320
1341
  openEffortPicker,
1321
1342
  openThemePicker,
1322
1343
  openSessionsPicker,
1344
+ openKeybindings: () => {
1345
+ const path = defaultUserConfigPath();
1346
+ appendLine(`keybindings config: ${path}`);
1347
+ const editor = process.env.EDITOR || process.env.VISUAL;
1348
+ if (editor) {
1349
+ try {
1350
+ spawn(editor, [path], { stdio: "inherit" });
1351
+ }
1352
+ catch (error) {
1353
+ appendLine(`could not open editor: ${error instanceof Error ? error.message : String(error)}`);
1354
+ }
1355
+ }
1356
+ else if (process.platform === "linux") {
1357
+ try {
1358
+ spawn("xdg-open", [path], { detached: true, stdio: "ignore" }).unref();
1359
+ }
1360
+ catch {
1361
+ // Best effort.
1362
+ }
1363
+ }
1364
+ tui.requestRender();
1365
+ },
1366
+ shortcutsText: () => {
1367
+ const kb = config.keybindings ?? {};
1368
+ return Object.entries(kb)
1369
+ .map(([action, key]) => `${action}: ${key ?? "(disabled)"}`)
1370
+ .join("\n");
1371
+ },
1323
1372
  searchSessions: searchSessionsInStore,
1324
1373
  renameSession,
1325
1374
  listExtensions: () => {
@@ -1366,11 +1415,77 @@ export async function run(args) {
1366
1415
  function restoreTranscript(next) {
1367
1416
  streamText.finalize();
1368
1417
  transcript.reset();
1418
+ // Drop stale references so toggles only affect the resumed transcript.
1419
+ toolBoxes.length = 0;
1369
1420
  banner.setTheme(theme);
1370
1421
  transcript.add(banner);
1371
1422
  transcript.add(new Spacer(1));
1372
- for (const line of sessionHistoryLines(next.messages)) {
1373
- transcript.addLine(line);
1423
+ const messages = Array.isArray(next?.messages) ? next.messages : [];
1424
+ // Index tool results by their call id so each restored ToolBox can carry
1425
+ // its output and render as the same collapsible block used live.
1426
+ const resultsByCallId = new Map();
1427
+ for (const message of messages) {
1428
+ if (message?.role === "tool" && message.toolCallId) {
1429
+ resultsByCallId.set(String(message.toolCallId), message);
1430
+ }
1431
+ }
1432
+ const renderedToolCallIds = new Set();
1433
+ const textOf = (content) => {
1434
+ if (typeof content === "string")
1435
+ return content;
1436
+ if (Array.isArray(content)) {
1437
+ return content
1438
+ .map((part) => part?.type === "text" ? String(part.text ?? "") : "")
1439
+ .join("");
1440
+ }
1441
+ return "";
1442
+ };
1443
+ for (const message of messages) {
1444
+ if (!message || typeof message !== "object")
1445
+ continue;
1446
+ if (message.role === "system")
1447
+ continue;
1448
+ if (message.role === "user") {
1449
+ const text = textOf(message.content).trim();
1450
+ if (text) {
1451
+ transcript.add(new Text(theme.tokens.user(`> ${text}`)));
1452
+ }
1453
+ continue;
1454
+ }
1455
+ if (message.role === "assistant") {
1456
+ const text = textOf(message.content).trim();
1457
+ if (text) {
1458
+ transcript.add(new Markdown(text, 0, 0, markdownThemeFor(theme)));
1459
+ }
1460
+ for (const call of message.toolCalls ?? []) {
1461
+ const box = new ToolBox({
1462
+ id: String(call?.id ?? ""),
1463
+ name: String(call?.name ?? ""),
1464
+ args: call?.arguments ?? {},
1465
+ });
1466
+ box.setExpanded(expandTools);
1467
+ const result = resultsByCallId.get(String(call?.id ?? ""));
1468
+ if (result) {
1469
+ box.setResult(String(result.content ?? ""), {
1470
+ isError: result.isError === true,
1471
+ diff: Array.isArray(result.diff) ? result.diff : undefined,
1472
+ });
1473
+ renderedToolCallIds.add(String(call?.id ?? ""));
1474
+ }
1475
+ transcript.add(box);
1476
+ toolBoxes.push(box);
1477
+ }
1478
+ continue;
1479
+ }
1480
+ if (message.role === "tool") {
1481
+ if (renderedToolCallIds.has(String(message.toolCallId ?? "")))
1482
+ continue;
1483
+ const text = String(message.content ?? "").trim();
1484
+ if (text) {
1485
+ transcript.add(new Text(theme.tokens.text(`[${message.name}] ${text}`)));
1486
+ }
1487
+ continue;
1488
+ }
1374
1489
  }
1375
1490
  tui.requestRender();
1376
1491
  }
@@ -1702,6 +1817,13 @@ export async function run(args) {
1702
1817
  return undefined;
1703
1818
  if (!mouseEnabled)
1704
1819
  return { consume: true };
1820
+ if (event.type === "press" &&
1821
+ event.button === "left" &&
1822
+ event.y === jumpToBottomRow()) {
1823
+ output.scrollToEnd();
1824
+ tui.requestRender();
1825
+ return { consume: true };
1826
+ }
1705
1827
  handleMouseEvent(event, {
1706
1828
  isModalOpen: () => uiModalOpen,
1707
1829
  scrollBy: (lines) => output.scrollBy(lines),
@@ -1715,6 +1837,45 @@ export async function run(args) {
1715
1837
  // Releases and repeats must not toggle modal shortcuts twice.
1716
1838
  if (isKeyRelease(data) || isKeyRepeat(data))
1717
1839
  return undefined;
1840
+ const keybindings = (config.keybindings ?? {});
1841
+ const matchesBinding = (action) => {
1842
+ const key = keybindings[action];
1843
+ return typeof key === "string" && matchesKey(data, key);
1844
+ };
1845
+ if (!uiModalOpen) {
1846
+ if (matchesBinding("jumpBottom")) {
1847
+ output.scrollToEnd();
1848
+ tui.requestRender();
1849
+ return { consume: true };
1850
+ }
1851
+ if (matchesBinding("newSession")) {
1852
+ session.messages.length = 0;
1853
+ content.clear();
1854
+ appendLine("started new session");
1855
+ tui.requestRender();
1856
+ return { consume: true };
1857
+ }
1858
+ if (matchesBinding("searchSessions")) {
1859
+ openSessionsPicker("workspace");
1860
+ return { consume: true };
1861
+ }
1862
+ if (matchesBinding("scrollUp")) {
1863
+ output.scrollBy(-5);
1864
+ tui.requestRender();
1865
+ return { consume: true };
1866
+ }
1867
+ if (matchesBinding("scrollDown")) {
1868
+ output.scrollBy(5);
1869
+ tui.requestRender();
1870
+ return { consume: true };
1871
+ }
1872
+ if (matchesBinding("toggleThinking")) {
1873
+ showThinking = !showThinking;
1874
+ appendLine(`thinking display: ${showThinking ? "shown" : "hidden"}`);
1875
+ tui.requestRender();
1876
+ return { consume: true };
1877
+ }
1878
+ }
1718
1879
  // Ctrl+P opens the command palette unless a modal owns the keyboard.
1719
1880
  if (matchesKey(data, Key.ctrl("p")) && !uiModalOpen) {
1720
1881
  openPalette();
@@ -1818,6 +1979,11 @@ export async function run(args) {
1818
1979
  updateStatus();
1819
1980
  }
1820
1981
  else if (e.type === "tool_call") {
1982
+ if (thinkingText) {
1983
+ transcript.invalidate();
1984
+ thinkingText = null;
1985
+ thinkingBuffer = "";
1986
+ }
1821
1987
  finalizeText();
1822
1988
  activity.setPhase("working", e.name);
1823
1989
  const box = new ToolBox({ id: e.id, name: e.name, args: e.args });
@@ -1845,6 +2011,11 @@ export async function run(args) {
1845
2011
  }
1846
2012
  }
1847
2013
  else if (e.type === "final") {
2014
+ if (thinkingText) {
2015
+ transcript.invalidate();
2016
+ thinkingText = null;
2017
+ thinkingBuffer = "";
2018
+ }
1848
2019
  finalizeText();
1849
2020
  activity.stop();
1850
2021
  }
@@ -2063,6 +2234,14 @@ export async function run(args) {
2063
2234
  input.onSubmit = async (value) => {
2064
2235
  const trimmed = value.trim();
2065
2236
  input.setText("");
2237
+ if (trimmed) {
2238
+ try {
2239
+ input.addToHistory?.(trimmed);
2240
+ }
2241
+ catch {
2242
+ // Best effort; history is non-critical.
2243
+ }
2244
+ }
2066
2245
  if (!trimmed) {
2067
2246
  tui.requestRender();
2068
2247
  return;