@nanhara/hara 0.120.0 → 0.121.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 +39 -0
- package/dist/agent/failover.js +1 -1
- package/dist/agent/loop.js +24 -2
- package/dist/config.js +31 -5
- package/dist/context/mentions.js +17 -7
- package/dist/desk.js +64 -0
- package/dist/feedback.js +2 -7
- package/dist/fs-read.js +157 -0
- package/dist/fs-write.js +105 -0
- package/dist/index.js +103 -3
- package/dist/security/secrets.js +75 -0
- package/dist/serve/server.js +5 -5
- package/dist/serve/sessions.js +5 -1
- package/dist/session/store.js +58 -16
- package/dist/tools/builtin.js +74 -15
- package/dist/tools/edit.js +10 -2
- package/dist/tools/patch.js +45 -15
- package/dist/tools/registry.js +20 -6
- package/dist/tools/result-limit.js +37 -0
- package/dist/tools/web.js +227 -41
- package/dist/tui/App.js +1 -1
- package/dist/tui/InputBox.js +62 -4
- package/dist/tui/input-history.js +167 -0
- package/dist/tui/run.js +26 -23
- package/dist/undo.js +6 -4
- package/package.json +6 -1
package/dist/tui/InputBox.js
CHANGED
|
@@ -14,10 +14,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
14
14
|
// of leaning on ink's soft-wrap of a single <Text>, which mis-aligns wrapped rows against the
|
|
15
15
|
// "› " prompt gutter and reflows unpredictably as you type.
|
|
16
16
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
17
|
-
import { memo, useMemo, useState } from "react";
|
|
17
|
+
import { memo, useMemo, useRef, useState } from "react";
|
|
18
18
|
import { fileCandidates } from "../context/mentions.js";
|
|
19
19
|
import { imagePathFromPaste } from "../images.js";
|
|
20
20
|
import { vimNormal } from "./vim.js";
|
|
21
|
+
import { ComposerHistory, moveCursorLine, nextGraphemeIndex, previousGraphemeIndex, previousWordIndex, } from "./input-history.js";
|
|
21
22
|
export const MODES = ["suggest", "auto-edit", "full-auto", "plan"];
|
|
22
23
|
export const nextMode = (m) => MODES[(MODES.indexOf(m) + 1) % MODES.length];
|
|
23
24
|
const tok = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`);
|
|
@@ -312,15 +313,30 @@ export function InputBox({ status, cwd, model, route, width, onSubmit, onClipboa
|
|
|
312
313
|
const [mode, setMode] = useState("insert"); // vim only
|
|
313
314
|
const [pending, setPending] = useState(""); // vim operator-pending (d/c/g)
|
|
314
315
|
const [register, setRegister] = useState(""); // vim yank/delete register
|
|
316
|
+
const historyRef = useRef(null);
|
|
317
|
+
if (!historyRef.current)
|
|
318
|
+
historyRef.current = new ComposerHistory();
|
|
319
|
+
const inputHistory = historyRef.current;
|
|
315
320
|
const set = (v, c) => {
|
|
321
|
+
inputHistory.abandonNavigation();
|
|
316
322
|
setValue(v);
|
|
317
323
|
setCursor(c);
|
|
318
324
|
setSel(0);
|
|
319
325
|
setDismissed(false);
|
|
320
326
|
};
|
|
327
|
+
const snapshot = () => ({ value, attachments: images, pastes });
|
|
328
|
+
const restore = (draft) => {
|
|
329
|
+
setValue(draft.value);
|
|
330
|
+
setCursor(draft.value.length);
|
|
331
|
+
setImages(draft.attachments);
|
|
332
|
+
setPastes(draft.pastes);
|
|
333
|
+
setSel(0);
|
|
334
|
+
setDismissed(false);
|
|
335
|
+
};
|
|
321
336
|
// Attach an image: drop a highlighted `[Image #N]` token inline at the cursor and track the file
|
|
322
337
|
// (codex / Claude-Code style). Backspace over the token removes both.
|
|
323
338
|
const addImage = (img) => {
|
|
339
|
+
inputHistory.abandonNavigation();
|
|
324
340
|
const tok = `[Image #${images.length + 1}]`;
|
|
325
341
|
const before = value.slice(0, cursor);
|
|
326
342
|
const ins = (before && !/\s$/.test(before) ? " " : "") + tok + " ";
|
|
@@ -334,6 +350,7 @@ export function InputBox({ status, cwd, model, route, width, onSubmit, onClipboa
|
|
|
334
350
|
// the box: typing stays smooth (the VALUE stays short), the box stays small, and a multi-line paste
|
|
335
351
|
// can no longer fire the newline-submit path mid-paste. Expanded back to the full text on submit.
|
|
336
352
|
const addPaste = (text) => {
|
|
353
|
+
inputHistory.abandonNavigation();
|
|
337
354
|
const lines = text.split("\n").length;
|
|
338
355
|
const tok = `[Paste #${pastes.length + 1} +${lines} lines]`;
|
|
339
356
|
const before = value.slice(0, cursor);
|
|
@@ -348,6 +365,7 @@ export function InputBox({ status, cwd, model, route, width, onSubmit, onClipboa
|
|
|
348
365
|
const submit = (text) => {
|
|
349
366
|
if (!text.trim() && images.length === 0)
|
|
350
367
|
return; // nothing to send
|
|
368
|
+
inputHistory.record(snapshot());
|
|
351
369
|
onSubmit?.(expandPastes(text), images.length ? images : undefined);
|
|
352
370
|
set("", 0);
|
|
353
371
|
setImages([]);
|
|
@@ -365,6 +383,7 @@ export function InputBox({ status, cwd, model, route, width, onSubmit, onClipboa
|
|
|
365
383
|
const complete = (cand) => {
|
|
366
384
|
if (!mention)
|
|
367
385
|
return;
|
|
386
|
+
inputHistory.abandonNavigation();
|
|
368
387
|
const before = value.slice(0, mention.start); // includes the leading '@'
|
|
369
388
|
const after = value.slice(cursor);
|
|
370
389
|
const insert = cand.endsWith("/") ? cand : cand + " "; // dirs keep drilling; files end the mention
|
|
@@ -406,6 +425,8 @@ export function InputBox({ status, cwd, model, route, width, onSubmit, onClipboa
|
|
|
406
425
|
return setCursor((c) => Math.max(0, c - 1));
|
|
407
426
|
if (input && !key.ctrl && !key.meta) {
|
|
408
427
|
const st = vimNormal({ value, cursor, mode, pending, register }, input);
|
|
428
|
+
if (st.value !== value)
|
|
429
|
+
inputHistory.abandonNavigation();
|
|
409
430
|
setValue(st.value);
|
|
410
431
|
setCursor(st.cursor);
|
|
411
432
|
setMode(st.mode);
|
|
@@ -416,20 +437,54 @@ export function InputBox({ status, cwd, model, route, width, onSubmit, onClipboa
|
|
|
416
437
|
}
|
|
417
438
|
return;
|
|
418
439
|
}
|
|
440
|
+
if (key.upArrow) {
|
|
441
|
+
const onFirstLine = cursor === 0 || value.lastIndexOf("\n", cursor - 1) < 0;
|
|
442
|
+
if (onFirstLine) {
|
|
443
|
+
const draft = inputHistory.older(snapshot());
|
|
444
|
+
if (draft)
|
|
445
|
+
restore(draft);
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
setCursor(moveCursorLine(value, cursor, -1));
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (key.downArrow) {
|
|
453
|
+
const onLastLine = value.indexOf("\n", cursor) < 0;
|
|
454
|
+
if (onLastLine) {
|
|
455
|
+
const draft = inputHistory.newer();
|
|
456
|
+
if (draft)
|
|
457
|
+
restore(draft);
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
setCursor(moveCursorLine(value, cursor, 1));
|
|
461
|
+
}
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
419
464
|
if (key.return) {
|
|
465
|
+
if (key.shift || key.meta) {
|
|
466
|
+
set(value.slice(0, cursor) + "\n" + value.slice(cursor), cursor + 1);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
420
469
|
submit(value);
|
|
421
470
|
return;
|
|
422
471
|
}
|
|
423
472
|
if (key.leftArrow)
|
|
424
|
-
return setCursor((c) =>
|
|
473
|
+
return setCursor((c) => previousGraphemeIndex(value, c));
|
|
425
474
|
if (key.rightArrow)
|
|
426
|
-
return setCursor((c) =>
|
|
475
|
+
return setCursor((c) => nextGraphemeIndex(value, c));
|
|
427
476
|
if (key.ctrl && input === "a")
|
|
428
477
|
return setCursor(0);
|
|
429
478
|
if (key.ctrl && input === "e")
|
|
430
479
|
return setCursor(value.length);
|
|
431
480
|
if (key.ctrl && input === "u")
|
|
432
481
|
return set(value.slice(cursor), 0);
|
|
482
|
+
if (key.ctrl && input === "w") {
|
|
483
|
+
const start = previousWordIndex(value, cursor);
|
|
484
|
+
return set(value.slice(0, start) + value.slice(cursor), start);
|
|
485
|
+
}
|
|
486
|
+
if (key.ctrl && input === "k")
|
|
487
|
+
return set(value.slice(0, cursor), cursor);
|
|
433
488
|
if (key.ctrl && input === "v") {
|
|
434
489
|
// paste a screenshot / image from the OS clipboard
|
|
435
490
|
const img = onClipboardImage?.();
|
|
@@ -442,6 +497,7 @@ export function InputBox({ status, cwd, model, route, width, onSubmit, onClipboa
|
|
|
442
497
|
const head = value.slice(0, cursor);
|
|
443
498
|
const pm = /\[Paste #(\d+) \+\d+ lines\]\s?$/.exec(head); // paste token deletes whole + renumbers
|
|
444
499
|
if (pm) {
|
|
500
|
+
inputHistory.abandonNavigation();
|
|
445
501
|
const n = Number(pm[1]);
|
|
446
502
|
const kept = head.slice(0, pm.index) + value.slice(cursor);
|
|
447
503
|
const renumbered = kept.replace(/\[Paste #(\d+)( \+\d+ lines\])/g, (m2, d, tail) => (Number(d) > n ? `[Paste #${Number(d) - 1}${tail}` : m2));
|
|
@@ -454,6 +510,7 @@ export function InputBox({ status, cwd, model, route, width, onSubmit, onClipboa
|
|
|
454
510
|
}
|
|
455
511
|
const tm = /\[Image #(\d+)\]\s?$/.exec(head); // backspacing over an attachment token removes it whole
|
|
456
512
|
if (tm) {
|
|
513
|
+
inputHistory.abandonNavigation();
|
|
457
514
|
const n = Number(tm[1]);
|
|
458
515
|
const kept = head.slice(0, tm.index) + value.slice(cursor);
|
|
459
516
|
const renumbered = kept.replace(/\[Image #(\d+)\]/g, (m2, d) => (Number(d) > n ? `[Image #${Number(d) - 1}]` : m2));
|
|
@@ -464,7 +521,8 @@ export function InputBox({ status, cwd, model, route, width, onSubmit, onClipboa
|
|
|
464
521
|
setDismissed(false);
|
|
465
522
|
return;
|
|
466
523
|
}
|
|
467
|
-
|
|
524
|
+
const previous = previousGraphemeIndex(value, cursor);
|
|
525
|
+
set(value.slice(0, previous) + value.slice(cursor), previous);
|
|
468
526
|
}
|
|
469
527
|
return;
|
|
470
528
|
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Pure composer state: bounded shell-style history plus Unicode-safe cursor helpers. Keeping this out
|
|
2
|
+
// of the Ink component makes navigation deterministic and avoids copying a growing history on each key.
|
|
3
|
+
const cloneDraft = (draft) => ({
|
|
4
|
+
value: draft.value,
|
|
5
|
+
attachments: [...draft.attachments],
|
|
6
|
+
pastes: [...draft.pastes],
|
|
7
|
+
});
|
|
8
|
+
const draftChars = (draft) => draft.value.length + draft.pastes.reduce((sum, paste) => sum + paste.length, 0) + draft.attachments.length * 128;
|
|
9
|
+
/** Bounded in-process history. The draft that existed before Up is restored after navigating Down. */
|
|
10
|
+
export class ComposerHistory {
|
|
11
|
+
maxEntries;
|
|
12
|
+
maxChars;
|
|
13
|
+
entries = [];
|
|
14
|
+
cursor = null;
|
|
15
|
+
scratch = null;
|
|
16
|
+
totalChars = 0;
|
|
17
|
+
constructor(maxEntries = 100, maxChars = 2_000_000) {
|
|
18
|
+
this.maxEntries = maxEntries;
|
|
19
|
+
this.maxChars = maxChars;
|
|
20
|
+
}
|
|
21
|
+
get browsing() {
|
|
22
|
+
return this.cursor !== null;
|
|
23
|
+
}
|
|
24
|
+
get length() {
|
|
25
|
+
return this.entries.length;
|
|
26
|
+
}
|
|
27
|
+
record(draft) {
|
|
28
|
+
this.abandonNavigation();
|
|
29
|
+
const chars = draftChars(draft);
|
|
30
|
+
// A giant paste is already retained by the conversation history. Do not duplicate an entry larger
|
|
31
|
+
// than the entire composer-history budget just to support Up-arrow recall.
|
|
32
|
+
if (chars > this.maxChars || this.maxEntries <= 0)
|
|
33
|
+
return;
|
|
34
|
+
this.entries.push({ draft: cloneDraft(draft), chars });
|
|
35
|
+
this.totalChars += chars;
|
|
36
|
+
while (this.entries.length > this.maxEntries || this.totalChars > this.maxChars) {
|
|
37
|
+
this.totalChars -= this.entries.shift().chars;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
older(current) {
|
|
41
|
+
if (!this.entries.length)
|
|
42
|
+
return null;
|
|
43
|
+
if (this.cursor === null) {
|
|
44
|
+
this.scratch = cloneDraft(current);
|
|
45
|
+
this.cursor = this.entries.length - 1;
|
|
46
|
+
}
|
|
47
|
+
else if (this.cursor > 0) {
|
|
48
|
+
this.cursor--;
|
|
49
|
+
}
|
|
50
|
+
return cloneDraft(this.entries[this.cursor].draft);
|
|
51
|
+
}
|
|
52
|
+
newer() {
|
|
53
|
+
if (this.cursor === null)
|
|
54
|
+
return null;
|
|
55
|
+
if (this.cursor < this.entries.length - 1) {
|
|
56
|
+
this.cursor++;
|
|
57
|
+
return cloneDraft(this.entries[this.cursor].draft);
|
|
58
|
+
}
|
|
59
|
+
const draft = this.scratch ?? { value: "", attachments: [], pastes: [] };
|
|
60
|
+
this.cursor = null;
|
|
61
|
+
this.scratch = null;
|
|
62
|
+
return cloneDraft(draft);
|
|
63
|
+
}
|
|
64
|
+
abandonNavigation() {
|
|
65
|
+
this.cursor = null;
|
|
66
|
+
this.scratch = null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
let segmenter;
|
|
70
|
+
function graphemeSegmenter() {
|
|
71
|
+
// Constructing Intl.Segmenter loads ICU data. Defer that cost until the first actual cursor edit so
|
|
72
|
+
// lightweight commands such as `hara --version` do not pay for TUI Unicode support.
|
|
73
|
+
if (segmenter === undefined)
|
|
74
|
+
segmenter = typeof Intl.Segmenter === "function" ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) : null;
|
|
75
|
+
return segmenter;
|
|
76
|
+
}
|
|
77
|
+
/** Previous user-perceived character boundary (keeps emoji ZWJ sequences/combining marks intact). */
|
|
78
|
+
export function previousGraphemeIndex(value, cursor) {
|
|
79
|
+
const at = Math.max(0, Math.min(value.length, cursor));
|
|
80
|
+
if (at === 0)
|
|
81
|
+
return 0;
|
|
82
|
+
const segments = graphemeSegmenter();
|
|
83
|
+
if (!segments) {
|
|
84
|
+
const low = value.charCodeAt(at - 1);
|
|
85
|
+
const paired = low >= 0xdc00 && low <= 0xdfff && at > 1 && value.charCodeAt(at - 2) >= 0xd800 && value.charCodeAt(at - 2) <= 0xdbff;
|
|
86
|
+
return Math.max(0, at - (paired ? 2 : 1));
|
|
87
|
+
}
|
|
88
|
+
let previous = 0;
|
|
89
|
+
for (const part of segments.segment(value)) {
|
|
90
|
+
if (part.index >= at)
|
|
91
|
+
break;
|
|
92
|
+
previous = part.index;
|
|
93
|
+
}
|
|
94
|
+
return previous;
|
|
95
|
+
}
|
|
96
|
+
/** Next user-perceived character boundary. */
|
|
97
|
+
export function nextGraphemeIndex(value, cursor) {
|
|
98
|
+
const at = Math.max(0, Math.min(value.length, cursor));
|
|
99
|
+
if (at >= value.length)
|
|
100
|
+
return value.length;
|
|
101
|
+
const segments = graphemeSegmenter();
|
|
102
|
+
if (!segments) {
|
|
103
|
+
const cp = value.codePointAt(at);
|
|
104
|
+
return Math.min(value.length, at + (cp !== undefined && cp > 0xffff ? 2 : 1));
|
|
105
|
+
}
|
|
106
|
+
for (const part of segments.segment(value)) {
|
|
107
|
+
const end = part.index + part.segment.length;
|
|
108
|
+
if (end > at)
|
|
109
|
+
return end;
|
|
110
|
+
}
|
|
111
|
+
return value.length;
|
|
112
|
+
}
|
|
113
|
+
/** Start of the previous shell-style word, used by Ctrl+W. */
|
|
114
|
+
export function previousWordIndex(value, cursor) {
|
|
115
|
+
let at = Math.max(0, Math.min(value.length, cursor));
|
|
116
|
+
while (at > 0) {
|
|
117
|
+
const previous = previousGraphemeIndex(value, at);
|
|
118
|
+
if (!/\s/u.test(value.slice(previous, at)))
|
|
119
|
+
break;
|
|
120
|
+
at = previous;
|
|
121
|
+
}
|
|
122
|
+
while (at > 0) {
|
|
123
|
+
const previous = previousGraphemeIndex(value, at);
|
|
124
|
+
if (/\s/u.test(value.slice(previous, at)))
|
|
125
|
+
break;
|
|
126
|
+
at = previous;
|
|
127
|
+
}
|
|
128
|
+
return at;
|
|
129
|
+
}
|
|
130
|
+
function graphemeFloor(value, cursor) {
|
|
131
|
+
const at = Math.max(0, Math.min(value.length, cursor));
|
|
132
|
+
if (at === value.length)
|
|
133
|
+
return at;
|
|
134
|
+
const segments = graphemeSegmenter();
|
|
135
|
+
if (!segments)
|
|
136
|
+
return at > 0 && /[\uDC00-\uDFFF]/.test(value[at]) ? at - 1 : at;
|
|
137
|
+
let floor = 0;
|
|
138
|
+
for (const part of segments.segment(value)) {
|
|
139
|
+
if (part.index > at)
|
|
140
|
+
break;
|
|
141
|
+
floor = part.index;
|
|
142
|
+
if (part.index + part.segment.length === at)
|
|
143
|
+
return at;
|
|
144
|
+
}
|
|
145
|
+
return floor;
|
|
146
|
+
}
|
|
147
|
+
/** Move between logical pasted/typed lines while retaining the closest possible column. */
|
|
148
|
+
export function moveCursorLine(value, cursor, direction) {
|
|
149
|
+
const at = Math.max(0, Math.min(value.length, cursor));
|
|
150
|
+
const lineStart = (at === 0 ? -1 : value.lastIndexOf("\n", at - 1)) + 1;
|
|
151
|
+
const lineEndAt = value.indexOf("\n", at);
|
|
152
|
+
const lineEnd = lineEndAt < 0 ? value.length : lineEndAt;
|
|
153
|
+
const column = Math.min(at - lineStart, lineEnd - lineStart);
|
|
154
|
+
if (direction < 0) {
|
|
155
|
+
if (lineStart === 0)
|
|
156
|
+
return at;
|
|
157
|
+
const previousEnd = lineStart - 1;
|
|
158
|
+
const previousStart = value.lastIndexOf("\n", Math.max(0, previousEnd - 1)) + 1;
|
|
159
|
+
return graphemeFloor(value, previousStart + Math.min(column, previousEnd - previousStart));
|
|
160
|
+
}
|
|
161
|
+
if (lineEnd === value.length)
|
|
162
|
+
return at;
|
|
163
|
+
const nextStart = lineEnd + 1;
|
|
164
|
+
const nextEndAt = value.indexOf("\n", nextStart);
|
|
165
|
+
const nextEnd = nextEndAt < 0 ? value.length : nextEndAt;
|
|
166
|
+
return graphemeFloor(value, nextStart + Math.min(column, nextEnd - nextStart));
|
|
167
|
+
}
|
package/dist/tui/run.js
CHANGED
|
@@ -4,36 +4,39 @@
|
|
|
4
4
|
import { render, Box, Text, useApp, useInput } from "ink";
|
|
5
5
|
import { createElement } from "react";
|
|
6
6
|
import { App } from "./App.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// resize events during a window drag collapses to one clear.
|
|
15
|
-
const out = process.stdout;
|
|
16
|
-
let pending = false;
|
|
7
|
+
/** Ink 6.8 clears before repainting when a terminal gets narrower, but not when it gets wider. Install a
|
|
8
|
+
* complementary WIDTH-only clear ahead of Ink's own resize listener so Ink's normal layout pass redraws
|
|
9
|
+
* the frame immediately afterwards. Never clear on a rows-only resize: `instance.clear()` erases the
|
|
10
|
+
* dynamic region without scheduling a React render, which made an idle input box disappear when a user
|
|
11
|
+
* dragged only the top/bottom edge of the terminal window. Exported for a small ordering regression test. */
|
|
12
|
+
export function installResizeRepaint(out, instance) {
|
|
13
|
+
let lastColumns = out.columns;
|
|
17
14
|
const onResize = () => {
|
|
18
|
-
|
|
15
|
+
const columns = out.columns;
|
|
16
|
+
if (!columns || columns === lastColumns)
|
|
19
17
|
return;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/* best-effort — never let a repaint fix crash the session */
|
|
28
|
-
}
|
|
29
|
-
});
|
|
18
|
+
lastColumns = columns;
|
|
19
|
+
try {
|
|
20
|
+
instance.clear();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
/* best-effort — never let a repaint fix crash the session */
|
|
24
|
+
}
|
|
30
25
|
};
|
|
31
|
-
|
|
26
|
+
// Ink registered its listener inside render() already. Prepending is essential: clearing AFTER Ink's
|
|
27
|
+
// repaint is precisely what leaves the prompt blank until some unrelated state update happens.
|
|
28
|
+
out.prependListener("resize", onResize);
|
|
29
|
+
return () => out.off("resize", onResize);
|
|
30
|
+
}
|
|
31
|
+
export async function runTui(props) {
|
|
32
|
+
const instance = render(createElement(App, props));
|
|
33
|
+
const out = process.stdout;
|
|
34
|
+
const removeResizeRepaint = installResizeRepaint(out, instance);
|
|
32
35
|
try {
|
|
33
36
|
await instance.waitUntilExit();
|
|
34
37
|
}
|
|
35
38
|
finally {
|
|
36
|
-
|
|
39
|
+
removeResizeRepaint();
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
// A tiny ink yes/no prompt for pre-TUI confirms (e.g. the first-run "create AGENTS.md?" offer).
|
package/dist/undo.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// In-session undo stack for file changes. Each edit tool records the prior state of the files it
|
|
2
2
|
// touched; `/undo` pops the last group and restores it. Process-scoped (one REPL session).
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { unlink } from "node:fs/promises";
|
|
4
|
+
import { atomicWriteText } from "./fs-write.js";
|
|
5
|
+
import { invalidateFileCandidates } from "./context/mentions.js";
|
|
5
6
|
const stack = [];
|
|
6
7
|
const MAX = 50;
|
|
7
8
|
/** Record a group of file changes (one tool call = one undo step). */
|
|
@@ -27,8 +28,7 @@ export async function undoLast() {
|
|
|
27
28
|
await unlink(s.absPath).catch(() => { }); // was newly created → remove
|
|
28
29
|
}
|
|
29
30
|
else {
|
|
30
|
-
await
|
|
31
|
-
await writeFile(s.absPath, s.before, "utf8");
|
|
31
|
+
await atomicWriteText(s.absPath, s.before);
|
|
32
32
|
}
|
|
33
33
|
files.push(s.path);
|
|
34
34
|
}
|
|
@@ -36,5 +36,7 @@ export async function undoLast() {
|
|
|
36
36
|
/* skip a file we can't restore */
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
if (files.length)
|
|
40
|
+
invalidateFileCandidates();
|
|
39
41
|
return { files };
|
|
40
42
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanhara/hara",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.121.1",
|
|
4
4
|
"description": "hara — a coding agent CLI that runs like an engineering org.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hara": "dist/index.js"
|
|
@@ -70,5 +70,10 @@
|
|
|
70
70
|
"optionalDependencies": {
|
|
71
71
|
"@zvec/zvec": "^0.5.0",
|
|
72
72
|
"qrcode-terminal": "^0.12.0"
|
|
73
|
+
},
|
|
74
|
+
"overrides": {
|
|
75
|
+
"@larksuiteoapi/node-sdk": {
|
|
76
|
+
"axios": "1.18.1"
|
|
77
|
+
}
|
|
73
78
|
}
|
|
74
79
|
}
|