@hncode/hncode 0.5.1 → 0.5.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/bin/install.cjs +24 -44
- package/bin/preinstall.cjs +0 -14
- package/package.json +2 -2
- package/src/agent-task.js +12 -1
- package/src/agent.js +279 -48
- package/src/cache.js +175 -130
- package/src/cell-buffer.js +5 -0
- package/src/clipboard.js +111 -8
- package/src/context-bar.js +46 -5
- package/src/doctor.js +7 -2
- package/src/git.js +35 -4
- package/src/index.js +29 -5
- package/src/keybindings.js +25 -3
- package/src/llm.js +60 -6
- package/src/mcp.js +419 -384
- package/src/permissions.js +29 -7
- package/src/plugin.js +95 -10
- package/src/registry-browser.js +21 -6
- package/src/remote.js +307 -289
- package/src/session-core.js +203 -0
- package/src/session-host.js +213 -0
- package/src/session-view.js +110 -0
- package/src/settings-schema.js +12 -9
- package/src/shell-safety.js +610 -473
- package/src/swarm-progress.js +348 -293
- package/src/tasks-browser.js +4 -1
- package/src/team/constants.js +20 -0
- package/src/team/context.js +89 -0
- package/src/team/mailbox.js +216 -0
- package/src/team/permission.js +121 -0
- package/src/team/protocol.js +167 -0
- package/src/team/runner.js +462 -0
- package/src/team/spawn.js +305 -0
- package/src/team/store.js +172 -0
- package/src/team/tasks.js +177 -0
- package/src/team/team.js +346 -0
- package/src/team-panel.js +624 -0
- package/src/term-caps.js +457 -410
- package/src/term.js +162 -19
- package/src/tool-result.js +70 -7
- package/src/tools/agent-swarm.js +114 -11
- package/src/tools/agent.js +464 -386
- package/src/tools/bash.js +53 -14
- package/src/tools/diagnostics.js +13 -0
- package/src/tools/edit.js +428 -50
- package/src/tools/git.js +15 -3
- package/src/tools/glob.js +5 -3
- package/src/tools/grep.js +19 -5
- package/src/tools/ignore.js +17 -3
- package/src/tools/index.js +19 -3
- package/src/tools/read.js +2 -1
- package/src/tools/shell.js +145 -134
- package/src/tools/tasks.js +214 -141
- package/src/tools/team.js +411 -0
- package/src/tools/utils.js +12 -3
- package/src/tools/web-search.js +53 -40
- package/src/tui.js +7496 -5366
- package/src/update.js +320 -115
- package/src/web-daemon.js +27 -4
- package/src/web-live.js +250 -0
package/src/tools/bash.js
CHANGED
|
@@ -97,17 +97,32 @@ function decodeChunks(chunks) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Kill a child.
|
|
102
|
+
*
|
|
103
|
+
* `tree` is OFF for an interactive run, and that is not a style choice: a non-detached interactive child
|
|
104
|
+
* lives INSIDE our process tree, so `taskkill /t` walks up and kills the parent too — the TUI would die
|
|
105
|
+
* while stopping a background command. Both halves are pinned by `test/task-input.test.mjs`.
|
|
106
|
+
*
|
|
107
|
+
* A detached background job has no such risk: it was started as its own tree root, so `/t` reaches its
|
|
108
|
+
* children and nothing above it.
|
|
109
|
+
*/
|
|
110
|
+
function killTree(child, opts = {}) {
|
|
101
111
|
try {
|
|
102
112
|
const exit = child.exitCode !== null || child.signalCode !== null;
|
|
103
113
|
if (exit || child.killed) return;
|
|
104
114
|
if (process.platform === 'win32') {
|
|
105
|
-
|
|
115
|
+
const args = ['/pid', String(child.pid), '/f'];
|
|
116
|
+
if (opts.tree !== false) args.splice(2, 0, '/t');
|
|
117
|
+
try { cp.spawnSync('taskkill', args, { stdio: 'ignore' }); } catch {}
|
|
106
118
|
} else {
|
|
107
119
|
child.kill('SIGTERM');
|
|
108
120
|
}
|
|
109
121
|
} catch {}
|
|
110
|
-
|
|
122
|
+
// The SIGKILL follow-up must not keep the process alive on its own: unref it,
|
|
123
|
+
// so a child that exits politely still lets the process quit on time.
|
|
124
|
+
const followUp = setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 1000);
|
|
125
|
+
if (followUp.unref) followUp.unref();
|
|
111
126
|
}
|
|
112
127
|
|
|
113
128
|
export const spec = {
|
|
@@ -121,6 +136,7 @@ export const spec = {
|
|
|
121
136
|
timeout: { type: 'integer', minimum: 1, description: 'Seconds before the command is killed. Foreground default 60s/max 300s; background default 600s/max 86400s.' },
|
|
122
137
|
cwd: { type: 'string', description: 'Working directory (default: workspace/project root).' },
|
|
123
138
|
run_in_background: { type: 'boolean', default: false, description: 'Run detached and return a task id instead of waiting.' },
|
|
139
|
+
interactive: { type: 'boolean', default: false, description: 'With run_in_background: keep stdin open so TaskInput can write to it — for a command that prompts.' },
|
|
124
140
|
disable_timeout: { type: 'boolean', default: false, description: 'Disable the timeout (background only).' },
|
|
125
141
|
},
|
|
126
142
|
required: ['command'],
|
|
@@ -139,17 +155,24 @@ export const spec = {
|
|
|
139
155
|
const def = bg ? DEFAULT_BACKGROUND_TIMEOUT_S : DEFAULT_TIMEOUT_S;
|
|
140
156
|
const timeoutMs = args.disable_timeout ? undefined : Math.min(args.timeout ?? def, cap) * 1000;
|
|
141
157
|
const command = args.command;
|
|
142
|
-
// stdin is 'ignore' (NUL / /dev/null): leaving it as an open pipe made any
|
|
143
|
-
//
|
|
158
|
+
// stdin is 'ignore' by default (NUL / /dev/null): leaving it as an open pipe made any command that
|
|
159
|
+
// reads stdin (`cat`, `npm init`, …) block until the timeout.
|
|
144
160
|
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
161
|
+
// `interactive` OPTS INTO A PIPE, and only for a background run: a foreground command has nobody to
|
|
162
|
+
// type at it, while a background one can be fed through TaskInput. The pipe is closed by that tool
|
|
163
|
+
// when it is done, which is what keeps `cat` and friends from hanging.
|
|
164
|
+
if (args.interactive && !args.run_in_background) {
|
|
165
|
+
return 'Error: `interactive` requires `run_in_background: true` — a foreground run has no way to receive input.';
|
|
166
|
+
}
|
|
149
167
|
let shell;
|
|
150
168
|
try { shell = shellSpawnArgs(command); } catch (e) { return `Error: ${e.message}`; }
|
|
169
|
+
// Check the turn's signal BEFORE spawning. The check below ran only after the
|
|
170
|
+
// child existed: an abort landing in that window returned "Interrupted by user"
|
|
171
|
+
// while leaving the child RUNNING with open pipes until the timeout timer fired,
|
|
172
|
+
// and Esc could not stop it.
|
|
173
|
+
if (ctx.signal && ctx.signal.aborted) return 'Interrupted by user';
|
|
151
174
|
const child = cp.spawn(shell.bin, shell.args, {
|
|
152
|
-
cwd, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env },
|
|
175
|
+
cwd, stdio: [args.interactive ? 'pipe' : 'ignore', 'pipe', 'pipe'], env: { ...process.env },
|
|
153
176
|
});
|
|
154
177
|
|
|
155
178
|
const chunks = [];
|
|
@@ -298,17 +321,33 @@ function spawnBackground(args, cwd, ctx) {
|
|
|
298
321
|
// task's output file instead, so the process survives and TaskOutput can
|
|
299
322
|
// still read it. (In the TUI the parent rarely exits, but headless -p mode
|
|
300
323
|
// does, and a background job should outlive the prompt.)
|
|
301
|
-
|
|
302
|
-
|
|
324
|
+
//
|
|
325
|
+
// AN INTERACTIVE RUN CANNOT BE DETACHED. Measured on Windows: `detached: true` with piped stdout
|
|
326
|
+
// returns exit 0 and NO output, while the same command not detached returns its output normally — the
|
|
327
|
+
// detached process gets no working pipe back to us. Interactive needs that pipe in both directions, so
|
|
328
|
+
// it stays attached. The trade: it will not outlive the parent, which is the same lifetime its stdin
|
|
329
|
+
// already has.
|
|
330
|
+
let shell;
|
|
331
|
+
try { shell = shellSpawnArgs(command, { interactive: args.interactive }); } catch (e) { return `Error: ${e.message}`; }
|
|
303
332
|
const child = cp.spawn(shell.bin, shell.args, {
|
|
304
|
-
cwd, detached:
|
|
333
|
+
cwd, detached: !args.interactive, stdio: [args.interactive ? 'pipe' : 'ignore', 'pipe', 'pipe'], env: { ...process.env },
|
|
305
334
|
});
|
|
306
335
|
const task = createTask(ctx, 'process', {
|
|
307
336
|
description: args.description || args.command,
|
|
308
337
|
command: args.command,
|
|
309
338
|
pid: child.pid,
|
|
310
|
-
detached:
|
|
339
|
+
detached: !args.interactive,
|
|
311
340
|
});
|
|
341
|
+
// The stdin pipe, when the caller asked for one. Kept on the TASK so `TaskInput` can reach it from a
|
|
342
|
+
// task id alone — a tool receives an id, not a handle. Only `interactive` gets here, so a plain
|
|
343
|
+
// background job is unaffected and cannot be fed input by accident.
|
|
344
|
+
if (args.interactive && child.stdin) {
|
|
345
|
+
task.stdin = child.stdin;
|
|
346
|
+
task.interactive = true;
|
|
347
|
+
// A closed pipe is normal at the end of a run; writing afterwards would throw EPIPE.
|
|
348
|
+
child.stdin.on('error', () => { task.stdin = null; });
|
|
349
|
+
child.on('close', () => { task.stdin = null; });
|
|
350
|
+
}
|
|
312
351
|
defineOutput(task);
|
|
313
352
|
const push = (d) => appendTaskOutput(task, d);
|
|
314
353
|
if (child.stdout) child.stdout.on('data', push);
|
package/src/tools/diagnostics.js
CHANGED
|
@@ -54,6 +54,19 @@ export async function stopLspClients() {
|
|
|
54
54
|
for (const c of all) { try { await c.stop(); } catch { /* going away anyway */ } }
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Synchronous best-effort kill for the `exit` handler, which cannot await: a
|
|
59
|
+
* polite shutdown needs a round-trip the process no longer has time for, so the
|
|
60
|
+
* child is killed outright. rust-analyzer/pyright each hold hundreds of MB —
|
|
61
|
+
* leaving them running after hncode quits is the leak this closes.
|
|
62
|
+
*/
|
|
63
|
+
export function killLspClientsSync() {
|
|
64
|
+
const all = [...clients.values()];
|
|
65
|
+
clients.clear();
|
|
66
|
+
for (const c of all) { try { c._child && c._child.kill(); } catch { /* already gone */ }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
57
70
|
/** For tests and for the TUI: which servers are live right now. */
|
|
58
71
|
export function liveLspClients() {
|
|
59
72
|
return [...clients.entries()].map(([k, c]) => ({ key: k, serverId: c.serverId, root: c.root }));
|