lazyclaw 5.4.0 → 5.4.2
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/cli.mjs +5 -12
- package/package.json +1 -1
- package/tui/editor.mjs +25 -9
- package/tui/repl.mjs +22 -14
package/cli.mjs
CHANGED
|
@@ -2702,19 +2702,12 @@ async function cmdChat(flags = {}) {
|
|
|
2702
2702
|
try { process.stdout.write(chunk); } catch { /* swallow */ }
|
|
2703
2703
|
});
|
|
2704
2704
|
};
|
|
2705
|
-
// v5.4
|
|
2706
|
-
// pre-print
|
|
2707
|
-
//
|
|
2708
|
-
//
|
|
2709
|
-
// only inside the alt canvas and disappear on unmount. When alt is
|
|
2710
|
-
// disabled (non-TTY pipelines, LAZYCLAW_NO_ALT), keep the legacy
|
|
2711
|
-
// in-tree splash so the pre-existing visual is unchanged.
|
|
2712
|
-
const _altWillMount = !!process.stdout.isTTY && !process.env.LAZYCLAW_NO_ALT;
|
|
2713
|
-
if (_altWillMount) {
|
|
2714
|
-
try { process.stdout.write(renderSplashToString(splashProps) + '\n'); } catch { /* swallow */ }
|
|
2715
|
-
}
|
|
2705
|
+
// v5.4.1: splash renders INSIDE the alt-buffer (not pre-printed to
|
|
2706
|
+
// primary). The v5.4.0 pre-print made the screen go blank during
|
|
2707
|
+
// chat because alt-buffer cleared it on enter. Splash lives in the
|
|
2708
|
+
// Static scrollback now regardless of alt-buffer state.
|
|
2716
2709
|
const ink = render(/* @__PURE__ */ React.createElement(ReplApp, {
|
|
2717
|
-
splashProps
|
|
2710
|
+
splashProps,
|
|
2718
2711
|
statusInfo: { provider: activeProvName, model: activeModel },
|
|
2719
2712
|
runTurn: _inkRunTurn,
|
|
2720
2713
|
onSlashCommand: _inkSlashHandler,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lazyclaw",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.2",
|
|
4
4
|
"description": "Lazy, elegant terminal CLI for chatting with Claude / OpenAI / Gemini / Ollama, orchestrating multi-step LLM workflows, and running multi-agent Slack teams with cross-task memory. Banner-on-launch, slash-command ghost autocomplete, persistent sessions, local HTTP gateway.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/tui/editor.mjs
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// `width: '100%'` so Ink's wrap-ansi (already string-width aware) gets
|
|
29
29
|
// the full terminal budget — fixes the perceived right-edge truncation
|
|
30
30
|
// on long Korean buffers.
|
|
31
|
-
import React, { useState, useEffect } from 'react';
|
|
31
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
32
32
|
import { Box, Text, useInput } from 'ink';
|
|
33
33
|
import stringWidth from 'string-width';
|
|
34
34
|
import { theme } from './theme.mjs';
|
|
@@ -150,15 +150,31 @@ export function Editor({
|
|
|
150
150
|
const [state, setState] = useState(() => makeEditorState({ history }));
|
|
151
151
|
const slashOpen = Array.isArray(slashSuggestions) && slashSuggestions.length > 0;
|
|
152
152
|
|
|
153
|
+
// v5.4.2: keep a synchronous mirror of the editor state so back-to-back
|
|
154
|
+
// keystrokes don't lose characters to React's stale-closure problem.
|
|
155
|
+
// Korean / Japanese IME commits each completed syllable as a separate
|
|
156
|
+
// stdin event; if two events fire inside one React frame the second
|
|
157
|
+
// useInput call captures the pre-first-event `state` and overwrites
|
|
158
|
+
// the first event's setState payload — leaving the first character
|
|
159
|
+
// missing from `buffer`. Reading + writing through the ref means every
|
|
160
|
+
// applyKey() sees the latest buffer regardless of render timing.
|
|
161
|
+
const stateRef = useRef(state);
|
|
162
|
+
useEffect(() => { stateRef.current = state; }, [state]);
|
|
163
|
+
const commit = (next) => {
|
|
164
|
+
stateRef.current = next;
|
|
165
|
+
setState(next);
|
|
166
|
+
};
|
|
167
|
+
|
|
153
168
|
useInput((input, key) => {
|
|
169
|
+
const current = stateRef.current;
|
|
154
170
|
// ─── Slash-popup keyboard contract (highest priority when open) ──
|
|
155
171
|
if (slashOpen) {
|
|
156
172
|
// Esc: clear the buffer and dismiss the popup. The host's onEscape
|
|
157
173
|
// is NOT called in this branch — Esc here is a popup gesture, not
|
|
158
174
|
// a turn-abort. (Outside of popup mode Esc still aborts streaming.)
|
|
159
175
|
if (key.escape) {
|
|
160
|
-
const cleared = { ...
|
|
161
|
-
|
|
176
|
+
const cleared = { ...current, buffer: '', cursor: 0, lastSubmit: null, lastWasPaste: false };
|
|
177
|
+
commit(cleared);
|
|
162
178
|
if (onBufferChange) {
|
|
163
179
|
try { onBufferChange(''); } catch {}
|
|
164
180
|
}
|
|
@@ -184,15 +200,15 @@ export function Editor({
|
|
|
184
200
|
if (key.tab || key.return) {
|
|
185
201
|
const safeIdx = Math.max(0, Math.min(slashSuggestions.length - 1, slashSelectedIndex || 0));
|
|
186
202
|
const picked = slashSuggestions[safeIdx];
|
|
187
|
-
const bufTrim =
|
|
188
|
-
const alreadyExact = !!picked && (
|
|
203
|
+
const bufTrim = current.buffer.replace(/\s+$/, '');
|
|
204
|
+
const alreadyExact = !!picked && (current.buffer === picked.cmd || bufTrim === picked.cmd);
|
|
189
205
|
if (alreadyExact) {
|
|
190
206
|
if (key.tab) return; // no completion to make
|
|
191
207
|
// key.return on exact match → fall through to applyKey/submit.
|
|
192
208
|
} else {
|
|
193
209
|
if (picked) {
|
|
194
|
-
const next = fillSlashCommand(
|
|
195
|
-
|
|
210
|
+
const next = fillSlashCommand(current, picked.cmd);
|
|
211
|
+
commit(next);
|
|
196
212
|
if (onBufferChange) {
|
|
197
213
|
try { onBufferChange(next.buffer); } catch {}
|
|
198
214
|
}
|
|
@@ -206,8 +222,8 @@ export function Editor({
|
|
|
206
222
|
// Esc: forward to host (ReplApp uses this to abort an in-flight turn).
|
|
207
223
|
// Do not mutate the buffer — the user may want to keep typing.
|
|
208
224
|
if (key.escape) { if (onEscape) onEscape(); return; }
|
|
209
|
-
const next = applyKey(
|
|
210
|
-
|
|
225
|
+
const next = applyKey(current, { input, key });
|
|
226
|
+
commit(next);
|
|
211
227
|
if (onBufferChange) {
|
|
212
228
|
try { onBufferChange(next.buffer); } catch { /* observer is best-effort */ }
|
|
213
229
|
}
|
package/tui/repl.mjs
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
// (spec §5.8) AND a sticky-bottom chat layout (v5.3).
|
|
3
3
|
//
|
|
4
4
|
// Layout (top → bottom inside the outer column):
|
|
5
|
-
// 1.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// as new content appends
|
|
9
|
-
//
|
|
5
|
+
// 1. Scrollback — splash item + per-turn user/assistant blocks.
|
|
6
|
+
// Non-alt path uses <Static items={scrollback}/>: Ink writes each
|
|
7
|
+
// item ONCE to terminal scrollback so the splash + history scroll
|
|
8
|
+
// away naturally as new content appends (the v5.3 contract).
|
|
9
|
+
// Alt-buffer path renders the same items as regular flex children
|
|
10
|
+
// instead — Static's "write above the live frame" mechanism is
|
|
11
|
+
// invisible inside the DEC 1049 alt canvas (the live frame
|
|
12
|
+
// immediately overwrites that area), so v5.4.1 splashes vanished.
|
|
13
|
+
// Flex children re-render each frame; <Splash/> output is stable.
|
|
10
14
|
// 2. Live region — partial assistant stream (state.liveAssistant) and
|
|
11
15
|
// optional <SlashHints/> while the input buffer starts with '/'.
|
|
12
16
|
// This Box re-renders on every chunk; the rest of the tree does not.
|
|
@@ -366,19 +370,23 @@ export function ReplApp({ splashProps, runTurn, runTurnFactory, slashCommands, o
|
|
|
366
370
|
React.createElement(
|
|
367
371
|
Box,
|
|
368
372
|
{ flexDirection: 'column', height: outerHeight },
|
|
369
|
-
// 1) Scrollback
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
// the
|
|
373
|
-
//
|
|
373
|
+
// 1) Scrollback.
|
|
374
|
+
// Alt-buffer path: render items as regular flex children, NOT via
|
|
375
|
+
// <Static/>. Ink's <Static/> writes once to stdout above the live
|
|
376
|
+
// frame — in the DEC 1049 alt canvas that area is immediately
|
|
377
|
+
// overwritten by the next live frame, so the splash + history
|
|
378
|
+
// end up invisible (v5.4.1 regression). Trade-off: items
|
|
379
|
+
// re-render each frame; <Splash/> output is stable so this is
|
|
380
|
+
// visually identical to the Static version.
|
|
381
|
+
// Non-alt path: keeps <Static/> — the legacy v5.3 contract
|
|
382
|
+
// (splash scrolls away naturally on the primary buffer) AND the
|
|
383
|
+
// structural snapshot in tests/v53-repl-layout.test.mjs.
|
|
374
384
|
altEnabled
|
|
375
385
|
? React.createElement(
|
|
376
386
|
Box,
|
|
377
387
|
{ flexDirection: 'column', flexGrow: 1, flexShrink: 1, overflow: 'hidden' },
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
{ items: state.scrollback },
|
|
381
|
-
(item) => React.createElement(ScrollbackItem, { key: item.id, item })
|
|
388
|
+
state.scrollback.map((item) =>
|
|
389
|
+
React.createElement(ScrollbackItem, { key: item.id, item })
|
|
382
390
|
),
|
|
383
391
|
// Live region — partial assistant stream (inside the scroll
|
|
384
392
|
// region so it grows naturally above the status bar).
|