@nanhara/hara 0.109.3 → 0.109.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 +10 -0
- package/dist/context/mentions.js +17 -0
- package/dist/index.js +10 -3
- 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.109.4 — a dropped file path is read, not "Unknown command"
|
|
9
|
+
|
|
10
|
+
- **Dragging/pasting a file into the prompt no longer errors.** A dropped file pastes as an absolute path
|
|
11
|
+
(`/Users/…/spec.md`, often with trailing text or `[Image #N]` tokens). Because it starts with `/`, hara
|
|
12
|
+
treated it as a slash command and replied `Unknown command /Users/…`. Now the command parser only fires
|
|
13
|
+
when the first token has **no embedded slash** (real commands — `/help`, `/design` — never contain one),
|
|
14
|
+
so a path falls through to the normal turn. And when a message *begins* with an existing absolute path,
|
|
15
|
+
that path is rewritten to an `@`-mention so its content is **read into the turn** — i.e. "interpret this
|
|
16
|
+
file" just works. Fixed in both the TUI and the readline REPL.
|
|
17
|
+
|
|
8
18
|
## 0.109.3 — an empty model response no longer looks like a hang
|
|
9
19
|
|
|
10
20
|
- **A turn that comes back with no text and no tool calls is no longer silently dropped.** The agent
|
package/dist/context/mentions.js
CHANGED
|
@@ -8,6 +8,23 @@ import { mediaTypeFor } from "../images.js";
|
|
|
8
8
|
const MAX_FILE = 50_000;
|
|
9
9
|
// @ at start-of-string or after whitespace; capture a path with no spaces/@ (avoids emails like a@b.com)
|
|
10
10
|
const MENTION_RE = /(?:^|\s)@([^\s@]+)/g;
|
|
11
|
+
/** A submitted line is a slash COMMAND only if it starts with '/' AND its first whitespace-delimited
|
|
12
|
+
* token has no *embedded* '/'. A dropped/pasted absolute file path (`/Users/…/doc.md`, possibly followed
|
|
13
|
+
* by text or `[Image #N]` tokens) has an embedded slash → it is NOT a command. Without this, dragging a
|
|
14
|
+
* file into the prompt errored with "Unknown command /Users/…". Exported pure for tests. */
|
|
15
|
+
export function isSlashCommand(line) {
|
|
16
|
+
return line.startsWith("/") && !line.slice(1).split(/\s+/)[0].includes("/");
|
|
17
|
+
}
|
|
18
|
+
/** If a message BEGINS with an absolute file path (a dragged/pasted file), rewrite that leading path into
|
|
19
|
+
* an `@`-mention so expandMentions inlines the file's content into the turn — the "read/interpret this
|
|
20
|
+
* file" the user meant. Space-free leading paths only (a mention can't contain spaces); a non-existent or
|
|
21
|
+
* spaced path is left untouched. `exists` is injected so this stays pure/testable. */
|
|
22
|
+
export function inlineLeadingPath(line, exists) {
|
|
23
|
+
if (!line.startsWith("/"))
|
|
24
|
+
return line;
|
|
25
|
+
const first = line.split(/\s+/)[0];
|
|
26
|
+
return exists(first) ? "@" + line : line;
|
|
27
|
+
}
|
|
11
28
|
/** Expand `@path` references **in place** — the file/dir content lands where it's referenced, not
|
|
12
29
|
* dumped at the bottom (so "compare @a.ts with @b.ts" reads in context). A repeated mention keeps
|
|
13
30
|
* the bare `@path` the second time (no double-inlining), and a non-readable ref is left untouched. */
|
package/dist/index.js
CHANGED
|
@@ -47,7 +47,7 @@ import { loadAgentsMd, hasAgentsMd, INIT_PROMPT, findProjectRoot } from "./conte
|
|
|
47
47
|
import { getEmbedder } from "./search/embed.js";
|
|
48
48
|
import { collectRepoChunks, collectDirChunks, buildIndex, indexPath, indexExists } from "./search/semindex.js";
|
|
49
49
|
import { searchHybrid } from "./search/hybrid.js";
|
|
50
|
-
import { expandMentions, fileCandidates } from "./context/mentions.js";
|
|
50
|
+
import { expandMentions, fileCandidates, isSlashCommand, inlineLeadingPath } from "./context/mentions.js";
|
|
51
51
|
import { newSessionId, shortId, resolveSessionId, saveSession, loadSession, listSessions, latestForCwd, titleFrom, slugify, } from "./session/store.js";
|
|
52
52
|
import { setSessionForceModel, isSessionForceModel, effectiveRoleModel } from "./session/session-model.js";
|
|
53
53
|
import { loadRoles, scaffoldRoles, subagentToolFilter } from "./org/roles.js";
|
|
@@ -2814,7 +2814,9 @@ program.action(async (opts) => {
|
|
|
2814
2814
|
onClipboardImage: readClipboardImage,
|
|
2815
2815
|
vim: cfg.vimMode,
|
|
2816
2816
|
onSubmit: async (line, h, images) => {
|
|
2817
|
-
|
|
2817
|
+
// A dropped/pasted file path (`/Users/…/doc.md`, maybe trailing text/images) starts with '/' but
|
|
2818
|
+
// is NOT a command — treat it as a file to read, not "Unknown command" (see isSlashCommand).
|
|
2819
|
+
if (isSlashCommand(line)) {
|
|
2818
2820
|
const [nm, ...rest] = line.slice(1).split(/\s+/);
|
|
2819
2821
|
const arg = rest.join(" ").trim();
|
|
2820
2822
|
if (nm === "exit" || nm === "quit") {
|
|
@@ -3074,6 +3076,9 @@ program.action(async (opts) => {
|
|
|
3074
3076
|
const near = nearest(nm, [...byName.keys()]);
|
|
3075
3077
|
return void h.sink.notice(`Unknown command /${nm}.${near.length ? " Did you mean " + near.map((n) => "/" + n).join(", ") + "?" : ""}`);
|
|
3076
3078
|
}
|
|
3079
|
+
// A message that begins with an absolute file path (the case skipped above) → inline it as an
|
|
3080
|
+
// @-mention so its content is read into the turn (drag-a-file-in → interpret it).
|
|
3081
|
+
line = inlineLeadingPath(line, existsSync);
|
|
3077
3082
|
const ui = { text: h.sink.assistantDelta, reasoning: h.sink.reasoningDelta, tool: h.sink.tool, diff: h.sink.diff, notice: h.sink.notice };
|
|
3078
3083
|
const appr = h.approval;
|
|
3079
3084
|
// Type-ahead steering: fold messages typed mid-turn into the next model call (codex-style) so a
|
|
@@ -3237,7 +3242,8 @@ program.action(async (opts) => {
|
|
|
3237
3242
|
bar.renderBottom(); // bottom border + modes/usage
|
|
3238
3243
|
if (!line)
|
|
3239
3244
|
continue;
|
|
3240
|
-
|
|
3245
|
+
// A dropped/pasted file path starts with '/' but isn't a command — read it, don't error (see TUI path).
|
|
3246
|
+
if (isSlashCommand(line)) {
|
|
3241
3247
|
const [name, ...rest] = line.slice(1).split(/\s+/);
|
|
3242
3248
|
const cmd = byName.get(name);
|
|
3243
3249
|
if (!cmd) {
|
|
@@ -3275,6 +3281,7 @@ program.action(async (opts) => {
|
|
|
3275
3281
|
break;
|
|
3276
3282
|
continue;
|
|
3277
3283
|
}
|
|
3284
|
+
line = inlineLeadingPath(line, existsSync); // leading dropped file path → @-mention so it's read in
|
|
3278
3285
|
const userContent = (recalledContext ? `${recalledContext}\n\n---\n\n` : "") + expandMentions(line, cwd);
|
|
3279
3286
|
recalledContext = "";
|
|
3280
3287
|
history.push({ role: "user", content: userContent });
|