@nanhara/hara 0.101.0 → 0.101.1
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 +10 -0
- package/dist/tui/App.js +30 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ All notable changes to `@nanhara/hara`.
|
|
|
5
5
|
> Versioning (pre-1.0, SemVer-style): the **minor** (middle) number bumps for a **new feature**; the
|
|
6
6
|
> **patch** (last) number bumps for **optimizations/fixes of existing features**.
|
|
7
7
|
|
|
8
|
+
## 0.101.1 — the input box stops running to the top
|
|
9
|
+
|
|
10
|
+
- **Live-region overflow guard.** A long streaming answer (or a big diff) used to grow the live region
|
|
11
|
+
past the terminal height — at which point ink's in-place repaint breaks and the input box "runs to
|
|
12
|
+
the top of the screen". Live blocks now render a bounded tail window (sized to your terminal, elided
|
|
13
|
+
lines counted in a dim header); the FULL text lands in scrollback the moment the block finalizes,
|
|
14
|
+
and ctrl+t shows it live. Same treatment reasoning got in 0.99.2, now for answers and diffs — the
|
|
15
|
+
dynamic region can no longer outgrow the viewport, which is the invariant that kept codex stable
|
|
16
|
+
(line-level commits) and pushed Claude Code to a fullscreen ScrollBox.
|
|
17
|
+
|
|
8
18
|
## 0.101.0 — startup update check
|
|
9
19
|
|
|
10
20
|
- **`hara` tells you when it's out of date.** On launch (interactive TTY only), a one-line notice —
|
package/dist/tui/App.js
CHANGED
|
@@ -43,12 +43,33 @@ const MODE_SELECTOR_MS = 2500;
|
|
|
43
43
|
// Memoized: a live block only re-renders when its own `item` (a fresh object when its text grows) or
|
|
44
44
|
// `open` changes. So a spinner tick or an unrelated flush doesn't re-run `renderMarkdown` for every
|
|
45
45
|
// live block, and non-tail live blocks stay put while only the streaming tail grows.
|
|
46
|
-
|
|
46
|
+
/** Tail-window a LIVE block's rendered lines so the dynamic region can never outgrow the terminal.
|
|
47
|
+
* ink repaints the whole dynamic region in place; once it's taller than the viewport the erase math
|
|
48
|
+
* breaks and the input box "runs to the top of the screen". Bounding the live view fixes the class:
|
|
49
|
+
* earlier lines are elided with a dim counter, and the FULL text lands in scrollback the moment the
|
|
50
|
+
* block finalizes (nothing is lost — ctrl+t shows it live too). `maxRows` comes from the terminal. */
|
|
51
|
+
function tailWindow(rendered, maxRows) {
|
|
52
|
+
const lines = rendered.replace(/\n+$/, "").split("\n");
|
|
53
|
+
if (lines.length <= maxRows)
|
|
54
|
+
return { header: null, body: rendered };
|
|
55
|
+
return {
|
|
56
|
+
header: `… +${lines.length - maxRows} earlier lines — full text lands above when this block finishes · ctrl+t to view now`,
|
|
57
|
+
body: lines.slice(-maxRows).join("\n"),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const Block = memo(function Block({ item, open, liveRows }) {
|
|
61
|
+
// Live streaming blocks get a bounded tail view (liveRows set); committed <Static> blocks render full.
|
|
62
|
+
const windowed = (rendered) => {
|
|
63
|
+
if (!liveRows)
|
|
64
|
+
return _jsx(Text, { children: rendered });
|
|
65
|
+
const w = tailWindow(rendered, liveRows);
|
|
66
|
+
return (_jsxs(Box, { flexDirection: "column", children: [w.header ? _jsx(Text, { dimColor: true, children: w.header }) : null, _jsx(Text, { children: w.body })] }));
|
|
67
|
+
};
|
|
47
68
|
switch (item.kind) {
|
|
48
69
|
case "user":
|
|
49
70
|
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(Text, { children: item.text })] }));
|
|
50
71
|
case "assistant":
|
|
51
|
-
return
|
|
72
|
+
return windowed(renderMarkdown(item.text)); // headers/bold/inline-code/bullets + verbatim fences
|
|
52
73
|
case "reasoning": {
|
|
53
74
|
// A streaming reasoning block lives in the dynamic region ABOVE the input box, and the instant the
|
|
54
75
|
// model stops thinking it FOLDS to a single "✻ thought · N lines" notice in scrollback. If we streamed
|
|
@@ -66,7 +87,7 @@ const Block = memo(function Block({ item, open }) {
|
|
|
66
87
|
case "tool":
|
|
67
88
|
return _jsx(Text, { dimColor: true, children: " " + item.text });
|
|
68
89
|
case "diff":
|
|
69
|
-
return
|
|
90
|
+
return windowed(item.text); // a big diff must not blow the live region either
|
|
70
91
|
case "notice":
|
|
71
92
|
return _jsx(Text, { dimColor: true, children: item.text });
|
|
72
93
|
}
|
|
@@ -301,6 +322,11 @@ const TodoPanel = memo(function TodoPanel({ todos }) {
|
|
|
301
322
|
});
|
|
302
323
|
export function App({ initialStatus, model, cwd, header, onSubmit, cycleApproval, onClipboardImage, vim, visionNotice }) {
|
|
303
324
|
const { exit } = useApp();
|
|
325
|
+
const { stdout: termOut } = useStdout();
|
|
326
|
+
// Live tail budget: terminal rows minus the rest of the dynamic chrome (todo panel ≤10, status slot,
|
|
327
|
+
// input box + footer, margins). Keeps the WHOLE dynamic region under the viewport — the invariant that
|
|
328
|
+
// stops ink's repaint from "running to the top" when a long answer/diff streams. Floor 8 for tiny panes.
|
|
329
|
+
const liveRows = Math.max(8, (termOut?.rows ?? 30) - 20);
|
|
304
330
|
const [history, setHistory] = useState([]);
|
|
305
331
|
const [current, setCurrent] = useState([]);
|
|
306
332
|
const [working, setWorking] = useState(false);
|
|
@@ -587,5 +613,5 @@ export function App({ initialStatus, model, cwd, header, onSubmit, cycleApproval
|
|
|
587
613
|
});
|
|
588
614
|
if (showTranscript)
|
|
589
615
|
return _jsx(Transcript, { items: [...history, ...current], onClose: () => setShowTranscript(false) });
|
|
590
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: header ? [{ id: -1, kind: "notice", text: "" }, ...history] : history, children: (item) => (item.id === -1 ? _jsx(HeaderCard, { ...header }, "hdr") : _jsx(Block, { item: item }, item.id)) }), current.map((item) => (_jsx(Block, { item: item, open: reasoningOpen }, item.id))), _jsx(TodoPanel, { todos: todos }), prompt && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", children: ` ${stripAnsi(prompt.title)}` }), prompt.options.map((o, i) => (_jsx(Text, { color: i === promptSel ? "cyan" : undefined, bold: i === promptSel, children: (i === promptSel ? " ❯ " : " ") + `${i + 1}. ` + o.label }, i))), _jsx(Text, { dimColor: true, children: ` ↑↓ or 1–${prompt.options.length} to choose · Enter · Esc cancels` })] })), askText && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", children: ` ? ${stripAnsi(askText.title)}` }), _jsx(Text, { dimColor: true, children: " type your answer below · Enter to send · Esc cancels" })] })), pool.length > 0 && !prompt && !askText && (_jsx(Box, { flexDirection: "column", children: pool.map((l, i) => (_jsx(Text, { color: accent(), children: ` › ${l.length > 72 ? l.slice(0, 72) + "…" : l}` }, i))) })), modeSelector ? _jsx(ModeLine, { approval: status.approval }) : _jsx(StatusRow, { working: working, todos: todos, queued: pool.length }), _jsx(InputBox, { status: status, cwd: cwd, model: model, route: header?.routeHost, isActive: !prompt, vim: vim, onSubmit: handleSubmit, onClipboardImage: onClipboardImage })] }));
|
|
616
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: header ? [{ id: -1, kind: "notice", text: "" }, ...history] : history, children: (item) => (item.id === -1 ? _jsx(HeaderCard, { ...header }, "hdr") : _jsx(Block, { item: item }, item.id)) }), current.map((item) => (_jsx(Block, { item: item, open: reasoningOpen, liveRows: liveRows }, item.id))), _jsx(TodoPanel, { todos: todos }), prompt && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", children: ` ${stripAnsi(prompt.title)}` }), prompt.options.map((o, i) => (_jsx(Text, { color: i === promptSel ? "cyan" : undefined, bold: i === promptSel, children: (i === promptSel ? " ❯ " : " ") + `${i + 1}. ` + o.label }, i))), _jsx(Text, { dimColor: true, children: ` ↑↓ or 1–${prompt.options.length} to choose · Enter · Esc cancels` })] })), askText && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", children: ` ? ${stripAnsi(askText.title)}` }), _jsx(Text, { dimColor: true, children: " type your answer below · Enter to send · Esc cancels" })] })), pool.length > 0 && !prompt && !askText && (_jsx(Box, { flexDirection: "column", children: pool.map((l, i) => (_jsx(Text, { color: accent(), children: ` › ${l.length > 72 ? l.slice(0, 72) + "…" : l}` }, i))) })), modeSelector ? _jsx(ModeLine, { approval: status.approval }) : _jsx(StatusRow, { working: working, todos: todos, queued: pool.length }), _jsx(InputBox, { status: status, cwd: cwd, model: model, route: header?.routeHost, isActive: !prompt, vim: vim, onSubmit: handleSubmit, onClipboardImage: onClipboardImage })] }));
|
|
591
617
|
}
|