@nanhara/hara 0.132.2 → 0.132.4
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 +25 -4
- package/dist/fs-walk.js +52 -2
- package/dist/tui/App.js +26 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,14 +5,35 @@ 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.132.
|
|
8
|
+
## 0.132.4 — 2026-07-22 — release-class Intel readiness handshakes
|
|
9
|
+
|
|
10
|
+
- Non-Git `@path` completion now skips the bounded Git inventory subprocess when no repository marker
|
|
11
|
+
exists. Slow or contended machines keep the full synchronous budget for the actual filesystem walk instead
|
|
12
|
+
of occasionally returning an empty completion list.
|
|
13
|
+
- TUI streaming and semantic-cancellation regressions now synchronize on concrete composer, render, provider,
|
|
14
|
+
and completion handshakes rather than fixed millisecond sleeps. Main-branch CI also runs the complete suite
|
|
15
|
+
on the same Intel macOS class used for native releases before any immutable version tag is created.
|
|
16
|
+
- This patch carries the complete 0.132 feature set into the native and Desktop release train. Upgrade with
|
|
17
|
+
`npm i -g @nanhara/hara@0.132.4`.
|
|
18
|
+
|
|
19
|
+
## 0.132.3 — 2026-07-22 — immediate prompt-key routing (npm published; native assets withheld)
|
|
20
|
+
|
|
21
|
+
- TUI approval/select input now reads the live visible prompt and selection through stable refs. A key pressed
|
|
22
|
+
immediately after the prompt paints can no longer hit the previous render's null-prompt closure and be
|
|
23
|
+
swallowed; rapid arrow-plus-Enter input also uses the current selection before the next paint.
|
|
24
|
+
- The confirmation regression waits for the visible prompt and then presses `y` without an arbitrary grace
|
|
25
|
+
delay, covering the Node 24 CI failure directly. Remaining process-tree fixtures also wait for valid PID
|
|
26
|
+
content rather than file creation alone. The npm package is valid, but native assembly was withheld after
|
|
27
|
+
the release-class Intel lane exposed additional completion/render fixture races; upgrade to 0.132.4 instead.
|
|
28
|
+
|
|
29
|
+
## 0.132.2 — 2026-07-22 — complete bounded native release gates (npm published; native assets withheld)
|
|
9
30
|
|
|
10
31
|
- The remaining approved-org process-tree and TUI confirmation tests now wait for concrete PID/render
|
|
11
32
|
readiness before starting their decisive assertion, and always clean up mounted Ink apps after a failure.
|
|
12
33
|
This closes the last Intel macOS release-runner races without weakening runtime timeouts or child cleanup.
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
34
|
+
- The npm package contains the observable gateway status, scoped web proxy, complete config redaction, and
|
|
35
|
+
user-managed organization connection features introduced in 0.132.0. Native release assembly was cancelled
|
|
36
|
+
after Node 24 CI exposed a visible-prompt input-handler race; upgrade to 0.132.3 instead.
|
|
16
37
|
|
|
17
38
|
## 0.132.1 — 2026-07-22 — bounded native release-gate waits (npm published; native assets withheld)
|
|
18
39
|
|
package/dist/fs-walk.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Shared filesystem walker — powers @file completion and the grep/glob tools.
|
|
2
2
|
// Walks a directory tree, skipping noise dirs, capped so huge trees stay responsive.
|
|
3
|
-
import { readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { lstatSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
4
4
|
import { opendir } from "node:fs/promises";
|
|
5
|
-
import { join, relative, sep } from "node:path";
|
|
5
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { execFile, execSync } from "node:child_process";
|
|
7
7
|
import { fuzzyRank } from "./fuzzy.js";
|
|
8
8
|
import { isSensitiveFilePath } from "./security/sensitive-files.js";
|
|
@@ -22,6 +22,37 @@ const DEFAULT_ASYNC_TIMEOUT_MS = 2_000;
|
|
|
22
22
|
// interruptible. Keep them on a much tighter wall budget; agent/tool paths use the async APIs below.
|
|
23
23
|
const DEFAULT_SYNC_TIMEOUT_MS = 250;
|
|
24
24
|
const DEFAULT_YIELD_EVERY = 128;
|
|
25
|
+
function gitMarkerOnPath(start) {
|
|
26
|
+
let dir = resolve(start);
|
|
27
|
+
for (let depth = 0; depth < 128; depth++) {
|
|
28
|
+
try {
|
|
29
|
+
lstatSync(join(dir, ".git"));
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
// An unreadable or otherwise suspicious marker must still take the Git path. Only a definite
|
|
34
|
+
// absence permits the filesystem fallback to skip spawning Git.
|
|
35
|
+
if (error?.code !== "ENOENT")
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
const parent = dirname(dir);
|
|
39
|
+
if (parent === dir)
|
|
40
|
+
return false;
|
|
41
|
+
dir = parent;
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
function gitMarkerAbove(start) {
|
|
46
|
+
if (gitMarkerOnPath(start))
|
|
47
|
+
return true;
|
|
48
|
+
// Preserve worktree semantics when callers reach a repository through a symlinked project path.
|
|
49
|
+
try {
|
|
50
|
+
return gitMarkerOnPath(realpathSync.native(start));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
25
56
|
function finiteLimit(value, fallback) {
|
|
26
57
|
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : fallback;
|
|
27
58
|
}
|
|
@@ -228,6 +259,12 @@ export function listProjectFiles(root, cap = 8000) {
|
|
|
228
259
|
if (recursiveRootContainsHome(root))
|
|
229
260
|
return [];
|
|
230
261
|
const startedAt = Date.now();
|
|
262
|
+
// On a non-repository directory, a contended `git ls-files` can consume the entire synchronous
|
|
263
|
+
// completion budget before the real filesystem walk starts. A bounded marker scan is sufficient to
|
|
264
|
+
// prove that Git cannot be authoritative here, leaving the budget for the user's actual files.
|
|
265
|
+
if (!gitMarkerAbove(root)) {
|
|
266
|
+
return walkFiles(root, cap, { timeoutMs: Math.max(0, DEFAULT_SYNC_TIMEOUT_MS - (Date.now() - startedAt)) });
|
|
267
|
+
}
|
|
231
268
|
try {
|
|
232
269
|
const out = execSync("git ls-files --cached --others --exclude-standard", {
|
|
233
270
|
cwd: root,
|
|
@@ -288,6 +325,19 @@ export async function listProjectFilesAsync(root, options = {}) {
|
|
|
288
325
|
if (cfg.maxEntries === 0)
|
|
289
326
|
return result([], 0, 0, "entry_limit");
|
|
290
327
|
const remaining = () => cfg.timeoutMs - (Date.now() - startedAt);
|
|
328
|
+
if (!gitMarkerAbove(root)) {
|
|
329
|
+
const timeoutMs = remaining();
|
|
330
|
+
if (timeoutMs <= 0)
|
|
331
|
+
return result([], 0, 0, "time_limit");
|
|
332
|
+
return walkFilesAsync(root, {
|
|
333
|
+
maxFiles: cfg.maxFiles,
|
|
334
|
+
maxDirectories: cfg.maxDirectories,
|
|
335
|
+
maxEntries: cfg.maxEntries,
|
|
336
|
+
timeoutMs,
|
|
337
|
+
signal: cfg.signal,
|
|
338
|
+
yieldEvery: cfg.yieldEvery,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
291
341
|
try {
|
|
292
342
|
if (remaining() <= 0)
|
|
293
343
|
return result([], 0, 0, "time_limit");
|
package/dist/tui/App.js
CHANGED
|
@@ -373,6 +373,13 @@ export function App({ initialStatus, model, cwd, header, onSubmit, agentSlashCom
|
|
|
373
373
|
const [status, setStatus] = useState({ ...initialStatus, agents: 0 });
|
|
374
374
|
const [prompt, setPrompt] = useState(null);
|
|
375
375
|
const [promptSel, setPromptSel] = useState(0);
|
|
376
|
+
// Ink updates useInput's callback after a render effect. Keep the visible prompt/selection in refs too,
|
|
377
|
+
// so the already-registered handler can consume a key pressed immediately after the prompt paints instead
|
|
378
|
+
// of routing it through the previous render's null prompt closure.
|
|
379
|
+
const promptRef = useRef(prompt);
|
|
380
|
+
promptRef.current = prompt;
|
|
381
|
+
const promptSelRef = useRef(promptSel);
|
|
382
|
+
promptSelRef.current = promptSel;
|
|
376
383
|
// Free-text question prompt (ask_user with no/declined options): re-enables the InputBox to capture one
|
|
377
384
|
// line, then resolves the awaiting tool with that text. Separate from `prompt` (the select-only path).
|
|
378
385
|
const [askText, setAskText] = useState(null);
|
|
@@ -598,6 +605,7 @@ export function App({ initialStatus, model, cwd, header, onSubmit, agentSlashCom
|
|
|
598
605
|
reject(promptAbortReason(signal));
|
|
599
606
|
};
|
|
600
607
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
608
|
+
promptSelRef.current = 0;
|
|
601
609
|
setPromptSel(0);
|
|
602
610
|
setPrompt({
|
|
603
611
|
token,
|
|
@@ -720,6 +728,7 @@ export function App({ initialStatus, model, cwd, header, onSubmit, agentSlashCom
|
|
|
720
728
|
});
|
|
721
729
|
}, [working, prompt, askText, handleSubmit]);
|
|
722
730
|
useInput((input, key) => {
|
|
731
|
+
const activePrompt = promptRef.current;
|
|
723
732
|
if (key.ctrl && input === "t")
|
|
724
733
|
return setShowTranscript((x) => !x); // open/close the full-transcript overlay
|
|
725
734
|
if (showTranscript)
|
|
@@ -736,28 +745,36 @@ export function App({ initialStatus, model, cwd, header, onSubmit, agentSlashCom
|
|
|
736
745
|
}
|
|
737
746
|
return;
|
|
738
747
|
}
|
|
739
|
-
if (
|
|
740
|
-
const opts =
|
|
748
|
+
if (activePrompt) {
|
|
749
|
+
const opts = activePrompt.options;
|
|
741
750
|
if (key.upArrow)
|
|
742
|
-
setPromptSel((s) =>
|
|
751
|
+
setPromptSel((s) => {
|
|
752
|
+
const next = (s - 1 + opts.length) % opts.length;
|
|
753
|
+
promptSelRef.current = next;
|
|
754
|
+
return next;
|
|
755
|
+
});
|
|
743
756
|
else if (key.downArrow)
|
|
744
|
-
setPromptSel((s) =>
|
|
757
|
+
setPromptSel((s) => {
|
|
758
|
+
const next = (s + 1) % opts.length;
|
|
759
|
+
promptSelRef.current = next;
|
|
760
|
+
return next;
|
|
761
|
+
});
|
|
745
762
|
else if (key.return) {
|
|
746
|
-
|
|
763
|
+
activePrompt.resolve(opts[Math.min(promptSelRef.current, opts.length - 1)].value);
|
|
747
764
|
}
|
|
748
765
|
else if (key.escape) {
|
|
749
|
-
if (
|
|
766
|
+
if (activePrompt.abortTurnOnEscape && working && ctrlRef.current)
|
|
750
767
|
abortCurrentTurn();
|
|
751
768
|
else
|
|
752
|
-
|
|
769
|
+
activePrompt.resolve(opts[opts.length - 1].value); // select-only prompt: last option = cancel/no
|
|
753
770
|
}
|
|
754
771
|
else if (/^[1-9]$/.test(input) && Number(input) <= opts.length) {
|
|
755
|
-
|
|
772
|
+
activePrompt.resolve(opts[Number(input) - 1].value); // type a number to pick directly
|
|
756
773
|
}
|
|
757
774
|
else if (input) {
|
|
758
775
|
const hit = opts.find((o) => o.key && o.key === input.toLowerCase());
|
|
759
776
|
if (hit) {
|
|
760
|
-
|
|
777
|
+
activePrompt.resolve(hit.value);
|
|
761
778
|
}
|
|
762
779
|
}
|
|
763
780
|
return;
|