@hncode/hncode 0.5.0 β 0.5.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/bin/install.cjs +24 -44
- package/bin/preinstall.cjs +0 -14
- package/package.json +1 -1
- package/src/agent-task.js +12 -1
- package/src/agent.js +115 -35
- package/src/cell-buffer.js +5 -0
- package/src/clipboard.js +42 -6
- package/src/colors.js +478 -387
- package/src/config.js +38 -11
- package/src/context-bar.js +46 -5
- package/src/doctor.js +214 -11
- package/src/git.js +35 -4
- package/src/llm.js +6 -0
- package/src/memory-profile.js +247 -0
- package/src/permissions.js +29 -7
- package/src/registry-browser.js +21 -6
- package/src/row-cache.js +147 -142
- package/src/session-core.js +203 -0
- package/src/session-host.js +213 -0
- package/src/session-hub.js +1 -0
- package/src/session-view.js +110 -0
- package/src/settings-schema.js +304 -0
- package/src/shell-safety.js +95 -6
- 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 +139 -0
- package/src/team/runner.js +347 -0
- package/src/team/spawn.js +247 -0
- package/src/team/store.js +145 -0
- package/src/team/tasks.js +164 -0
- package/src/team/team.js +311 -0
- package/src/team-panel.js +226 -0
- package/src/term.js +27 -0
- package/src/tool-result.js +279 -236
- package/src/tools/agent-swarm.js +114 -11
- package/src/tools/agent.js +53 -3
- package/src/tools/edit.js +591 -436
- package/src/tools/git.js +15 -3
- package/src/tools/index.js +2 -0
- package/src/tools/team.js +398 -0
- package/src/tools/web-search.js +88 -75
- package/src/tui.js +6187 -4780
- package/src/update.js +285 -115
- package/src/web-daemon.js +27 -4
- package/src/web-live.js +250 -0
- package/src/web.js +3 -1
package/bin/install.cjs
CHANGED
|
@@ -30,35 +30,27 @@ for (const dir of dirs) {
|
|
|
30
30
|
|
|
31
31
|
// Check if config file exists
|
|
32
32
|
if (!fs.existsSync(configFile)) {
|
|
33
|
-
const defaultConfig = `# hncode
|
|
34
|
-
#
|
|
33
|
+
const defaultConfig = `# hncode configuration
|
|
34
|
+
#
|
|
35
|
+
# Nothing here is required: every value has a default, and the same keys can be set with
|
|
36
|
+
# HNCODE_* environment variables or from /settings once hncode is running.
|
|
37
|
+
#
|
|
38
|
+
# Model and provider are deliberately left out. Naming one here starts hncode on a model
|
|
39
|
+
# you may have no key for, and the first turn fails against that provider instead of
|
|
40
|
+
# asking you to choose. Set up a provider with /provider, then pick a model with /model.
|
|
35
41
|
|
|
36
|
-
# Default model (change this to your preferred model)
|
|
37
|
-
default_model = "gpt-4o-mini"
|
|
38
|
-
|
|
39
|
-
# Optional: Add providers directly here
|
|
40
|
-
# Uncomment and configure as needed:
|
|
41
|
-
#
|
|
42
42
|
# [providers.openai]
|
|
43
43
|
# base_url = "https://api.openai.com/v1"
|
|
44
|
-
# api_key
|
|
44
|
+
# api_key = "sk-..." # or set HNCODE_API_KEY
|
|
45
45
|
# protocol = "openai"
|
|
46
|
-
|
|
47
|
-
# Or use environment variables:
|
|
48
|
-
# HNCODE_API_KEY="your-key"
|
|
49
|
-
# HNCODE_PROVIDER="openai"
|
|
50
|
-
# HNCODE_MODEL="gpt-4o-mini"
|
|
51
|
-
|
|
52
46
|
`;
|
|
53
47
|
|
|
54
48
|
fs.writeFileSync(configFile, defaultConfig);
|
|
55
49
|
console.log(`\nβ
Created default configuration at: ${configFile}`);
|
|
56
50
|
console.log('\nπ‘ Next steps:');
|
|
57
|
-
console.log(' 1.
|
|
58
|
-
console.log(' 2.
|
|
59
|
-
console.log('
|
|
60
|
-
console.log(' export HNCODE_PROVIDER="openai"');
|
|
61
|
-
console.log(' 3. Run "hncode" to start!');
|
|
51
|
+
console.log(' 1. Start hncode and run /provider to add an API key');
|
|
52
|
+
console.log(' 2. Then /model to pick one');
|
|
53
|
+
console.log(' Both write to the config above, so there is nothing to edit by hand.');
|
|
62
54
|
} else {
|
|
63
55
|
console.log(`\nβΉοΈ Configuration already exists at: ${configFile}`);
|
|
64
56
|
console.log(' Run "hncode init" to regenerate or edit manually.');
|
|
@@ -66,30 +58,18 @@ default_model = "gpt-4o-mini"
|
|
|
66
58
|
|
|
67
59
|
console.log('\n⨠hncode is ready to use!');
|
|
68
60
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
console.log(' . "$env:LOCALAPPDATA\\npm\\npm.cmd"');
|
|
82
|
-
console.log(' Or simply close and reopen your terminal');
|
|
83
|
-
}
|
|
84
|
-
} else {
|
|
85
|
-
// Unix-like systems
|
|
86
|
-
console.log('βΉοΈ Please run one of these to refresh environment:');
|
|
87
|
-
console.log(' source ~/.zshrc # For Zsh');
|
|
88
|
-
console.log(' source ~/.bashrc # For Bash');
|
|
89
|
-
console.log(' Or simply close and reopen your terminal');
|
|
90
|
-
}
|
|
91
|
-
} catch (err) {
|
|
92
|
-
console.log('βΉοΈ Note: You may need to restart your terminal');
|
|
61
|
+
// Tell the user how to pick up the new command.
|
|
62
|
+
//
|
|
63
|
+
// This used to run PowerShell to prepend the npm global path, and print "Environment
|
|
64
|
+
// refreshed automatically". That cannot work: `execSync` starts a CHILD shell, so the
|
|
65
|
+
// `$env:Path` it sets dies with that process. The claim is gone; the note below is true.
|
|
66
|
+
if (os.platform() === 'win32') {
|
|
67
|
+
console.log('βΉοΈ If "hncode" is not found, open a new terminal (the PATH is updated at');
|
|
68
|
+
console.log(' login), or run this in the current one:');
|
|
69
|
+
console.log(' $env:Path = [System.Environment]::GetEnvironmentVariable("Path","User") + ";" + $env:Path');
|
|
70
|
+
} else {
|
|
71
|
+
console.log('βΉοΈ If "hncode" is not found, open a new terminal, or re-read your shell');
|
|
72
|
+
console.log(' configuration: source ~/.zshrc (or ~/.bashrc)');
|
|
93
73
|
}
|
|
94
74
|
|
|
95
75
|
console.log('');
|
package/bin/preinstall.cjs
CHANGED
|
@@ -25,18 +25,4 @@ if (major < 20) {
|
|
|
25
25
|
const platform = os.platform();
|
|
26
26
|
console.log(`β Platform: ${platform}`);
|
|
27
27
|
|
|
28
|
-
// Check if already installed globally
|
|
29
|
-
try {
|
|
30
|
-
const { execSync } = require('child_process');
|
|
31
|
-
const npm = execSync('npm list -g hncode 2>&1').toString();
|
|
32
|
-
if (npm.includes('hncode')) {
|
|
33
|
-
console.log('βΉοΈ hncode is already installed globally');
|
|
34
|
-
console.log(' Run "npm uninstall -g hncode" to reinstall.\n');
|
|
35
|
-
} else {
|
|
36
|
-
console.log('β Not installed globally yet\n');
|
|
37
|
-
}
|
|
38
|
-
} catch (e) {
|
|
39
|
-
console.log('β Not installed globally yet\n');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
28
|
console.log('β
Environment check passed!\n');
|
package/package.json
CHANGED
package/src/agent-task.js
CHANGED
|
@@ -76,7 +76,10 @@ function store(ctx) {
|
|
|
76
76
|
*/
|
|
77
77
|
export function createTask(ctx, kind, options = {}) {
|
|
78
78
|
const tasks = store(ctx);
|
|
79
|
-
|
|
79
|
+
// A teammate is its own kind rather than falling through to 'question': the id prefix is
|
|
80
|
+
// what a reader of a task list uses to tell at a glance what kind of work it is.
|
|
81
|
+
const PREFIX = { process: 'bash', agent: 'agent', teammate: 'mate' };
|
|
82
|
+
const prefix = PREFIX[kind] || 'question';
|
|
80
83
|
const task = {
|
|
81
84
|
taskId: newTaskId(prefix),
|
|
82
85
|
kind,
|
|
@@ -101,10 +104,18 @@ export function createTask(ctx, kind, options = {}) {
|
|
|
101
104
|
task.subagentType = options.subagentType;
|
|
102
105
|
task.model = options.model;
|
|
103
106
|
task.thinkingEffort = options.thinkingEffort;
|
|
107
|
+
} else if (kind === 'teammate') {
|
|
108
|
+
task.teamName = options.teamName;
|
|
109
|
+
task.memberName = options.memberName;
|
|
110
|
+
task.agentId = options.agentId;
|
|
104
111
|
} else if (kind === 'question') {
|
|
105
112
|
task.questionCount = options.questionCount || 0;
|
|
106
113
|
task.toolCallId = options.toolCallId;
|
|
107
114
|
}
|
|
115
|
+
// WHICH TASK THIS ONE BELONGS TO. A swarm's subagents are tasks of their own so /tasks can show
|
|
116
|
+
// what each is doing, and this is what groups them under the swarm that launched them. Absent
|
|
117
|
+
// for a top-level task, which is the common case.
|
|
118
|
+
if (options.parentTaskId) task.parentTaskId = String(options.parentTaskId);
|
|
108
119
|
// Every task exposes its buffered output through `output`. The chunks are
|
|
109
120
|
// stored raw and decoded lazily:
|
|
110
121
|
// * a process appends Buffer chunks -> decode as UTF-8
|
package/src/agent.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// model answers without tool calls (or a step/interrupt condition is hit).
|
|
4
4
|
|
|
5
5
|
import { LLM, setToolsList } from './llm.js';
|
|
6
|
-
import { tools, getTool, llmTools } from './tools/index.js';
|
|
6
|
+
import { tools, combinedTools, getTool, llmTools } from './tools/index.js';
|
|
7
7
|
import { DELEGATION_TOOLS } from './subagent-types.js';
|
|
8
8
|
import { estimateMessagesTokens, estimateRequestOverhead, estimateTokens, messageTokens, TokenLedger, forgetMessageTokens } from './term.js';
|
|
9
9
|
import { runHooks, runPatch, runPatchSync } from './plugin.js';
|
|
@@ -140,9 +140,10 @@ function hashKey(s) {
|
|
|
140
140
|
h = (h * 0x01000193) >>> 0;
|
|
141
141
|
}
|
|
142
142
|
return h.toString(36) + '-' + str.length.toString(36);
|
|
143
|
-
return h.toString(36) + '-' + str.length.toString(36);
|
|
144
143
|
}
|
|
145
144
|
|
|
145
|
+
// Render what an Edit actually changed, for the MODEL's tool result.
|
|
146
|
+
|
|
146
147
|
// Render what an Edit actually changed, for the MODEL's tool result.
|
|
147
148
|
//
|
|
148
149
|
// The UI has always received the edit through `editDiff` (see the tool_result
|
|
@@ -166,10 +167,18 @@ export function formatEditDiffForModel(diff, maxLines = 400) {
|
|
|
166
167
|
const out = [];
|
|
167
168
|
out.push('');
|
|
168
169
|
out.push('--- diff ---');
|
|
170
|
+
// Unchanged lines above and below, unmarked, so the change can be LOCATED. Without them this
|
|
171
|
+
// was a quoted block with no indication of where in the file it landed β and identical-looking
|
|
172
|
+
// code appears in many places, so neither the model nor the user could confirm the edit hit
|
|
173
|
+
// the intended one rather than a same-text block further down.
|
|
174
|
+
const before = (diff.context && Array.isArray(diff.context.before)) ? diff.context.before : [];
|
|
175
|
+
const after = (diff.context && Array.isArray(diff.context.after)) ? diff.context.after : [];
|
|
176
|
+
for (const c of before) out.push(' ' + String(c.n).padStart(4) + ' ' + c.text);
|
|
169
177
|
let n = startLine;
|
|
170
178
|
for (const l of oldLines) out.push(' ' + String(n++).padStart(4) + ' - ' + l);
|
|
171
179
|
n = startLine;
|
|
172
180
|
for (const l of newLines) out.push(' ' + String(n++).padStart(4) + ' + ' + l);
|
|
181
|
+
for (const c of after) out.push(' ' + String(c.n).padStart(4) + ' ' + c.text);
|
|
173
182
|
// A whole-file edit (a Write-sized `new`) must not be echoed back in full.
|
|
174
183
|
if (out.length > maxLines + 3) {
|
|
175
184
|
const hidden = out.length - (maxLines + 3);
|
|
@@ -205,6 +214,37 @@ function toolResultLimitsEnabled() {
|
|
|
205
214
|
return !/^(0|off|false|no)$/i.test(String(process.env.HNCODE_TOOL_RESULT_LIMITS || ''));
|
|
206
215
|
}
|
|
207
216
|
|
|
217
|
+
/**
|
|
218
|
+
* A tool's return value, as the STRING the model can be sent.
|
|
219
|
+
*
|
|
220
|
+
* Every tool in `src/tools/` returns a plain string except the team tools, which return
|
|
221
|
+
* `{ content }` / `{ error }`. That object went into the `tool` message verbatim, and a provider
|
|
222
|
+
* that expects a string rejects the NEXT request with `missing type field in content item at
|
|
223
|
+
* index 0` β so a teammate's first `SendMessage` worked and then the conversation died, one
|
|
224
|
+
* message in. The symptom was a team that says hello and then never answers again.
|
|
225
|
+
*
|
|
226
|
+
* Returns `null` for the one shape that must stay an OBJECT: `ReadMediaFile` returns
|
|
227
|
+
* `{ text, media: [...] }`, and the media parts travel as structured blocks the caller builds
|
|
228
|
+
* from the object. Stringifying that would attach no image at all, so the caller keeps it.
|
|
229
|
+
*
|
|
230
|
+
* Normalised here, at the single point every tool result passes through, rather than in the
|
|
231
|
+
* team tools: a second tool family making the same choice cannot reintroduce the bug.
|
|
232
|
+
*/
|
|
233
|
+
function toolResultText(value) {
|
|
234
|
+
if (typeof value === 'string') return value;
|
|
235
|
+
if (value == null) return '';
|
|
236
|
+
if (typeof value === 'object') {
|
|
237
|
+
// Leave a media result alone β the caller needs the parts, not a sentence about them.
|
|
238
|
+
if (Array.isArray(value.media)) return null;
|
|
239
|
+
if (typeof value.content === 'string') return value.content;
|
|
240
|
+
if (typeof value.error === 'string') return value.error;
|
|
241
|
+
if (typeof value.text === 'string') return value.text;
|
|
242
|
+
try { return JSON.stringify(value); } catch { return String(value); }
|
|
243
|
+
}
|
|
244
|
+
return String(value);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
208
248
|
|
|
209
249
|
export const SYSTEM_PROMPT = `You are hncode, a coding agent in the user's terminal on ${process.platform}.
|
|
210
250
|
You work inside a workspace directory using the tools provided.
|
|
@@ -280,9 +320,9 @@ export class Agent {
|
|
|
280
320
|
// even if the filter would have allowed them. The catalog already excludes
|
|
281
321
|
// them from every profile; this is the hard backstop that also covers the
|
|
282
322
|
// `null` (= everything) case, so a subagent can never spawn another agent.
|
|
283
|
-
|
|
284
|
-
?
|
|
285
|
-
:
|
|
323
|
+
const filtered = (Array.isArray(cfg.toolFilter) && cfg.toolFilter.length
|
|
324
|
+
? combinedTools().filter((t) => cfg.toolFilter.includes(t.name))
|
|
325
|
+
: combinedTools()
|
|
286
326
|
)
|
|
287
327
|
.filter((t) => !(cfg.noDelegation && DELEGATION_TOOLS.includes(t.name)))
|
|
288
328
|
// AgentSwarm is gated behind swarm mode, like Agent is gated behind the user
|
|
@@ -420,9 +460,10 @@ export class Agent {
|
|
|
420
460
|
const used = this.usedTokens();
|
|
421
461
|
const maxSize = Math.max(1, Math.floor((used || (maxCtx || 512000) * 0.85) * keepRatio));
|
|
422
462
|
const msgs = this.messages || [];
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
463
|
+
// How far the LEADING system block runs. A cut before `floor` would land inside it,
|
|
464
|
+
// splitting the assembled system prompt across the drop boundary. The block itself is
|
|
465
|
+
// carried over by the trim (renamed `leadingSystem` there), because a cut can only
|
|
466
|
+
// ever land at or after its end.
|
|
426
467
|
let floor = 0;
|
|
427
468
|
while (floor < msgs.length && msgs[floor] && msgs[floor].role === 'system') floor++;
|
|
428
469
|
let recent = 1;
|
|
@@ -435,13 +476,66 @@ export class Agent {
|
|
|
435
476
|
// call. It is also the memoised form, so repeating this walk after a large tool
|
|
436
477
|
// result is a WeakMap hit rather than a re-measure.
|
|
437
478
|
size += messageTokens(msgs[split], null);
|
|
438
|
-
|
|
479
|
+
// The guard the leading system block needs, and the reverse of the `split > floor`
|
|
480
|
+
// this used to test. A cut at `split` drops `[0..split-1]`, so `split >= floor`
|
|
481
|
+
// keeps the cut at or after the block's end and never inside it β which is what
|
|
482
|
+
// the block's comment above asks for, and what `split > floor` inverted.
|
|
483
|
+
if (split >= floor && canSplitAfter(msgs, split - 1)) bestSplit = split;
|
|
439
484
|
if (size >= maxSize && bestSplit !== undefined) break;
|
|
440
485
|
}
|
|
441
486
|
return bestSplit ?? 0;
|
|
442
487
|
}
|
|
443
488
|
|
|
489
|
+
/**
|
|
490
|
+
* Trim the history at a legal cut and append the summary + continuation note.
|
|
491
|
+
*
|
|
492
|
+
* Extracted from `run()` so it can be tested against the REAL implementation. A test that
|
|
493
|
+
* re-implemented these four lines was green against a duplicated system prompt β the copy
|
|
494
|
+
* carried its own guard, which is the failure mode of testing a restatement.
|
|
495
|
+
*
|
|
496
|
+
* Returns `{ split, dropped }`.
|
|
497
|
+
*
|
|
498
|
+
* Shape: `[ ...lead, ...kept, summary, continuation ]`.
|
|
499
|
+
*
|
|
500
|
+
* The summary is a USER message wrapped in a `<system-reminder>` (kimi's shape), not a
|
|
501
|
+
* `system` one. Two reasons:
|
|
502
|
+
* * the TUI rebuilds the system prompt from scratch each turn and SKIPS every `system`
|
|
503
|
+
* entry in the stored history, so a system summary was dropped before the model saw it;
|
|
504
|
+
* * the turn-end save filters `system` out of the session, so it was not persisted either
|
|
505
|
+
* β history trimmed AND summary lost at both ends, which made "compaction" pure data loss.
|
|
506
|
+
* The continuation note goes LAST so the model's next move is "keep going" rather than
|
|
507
|
+
* "answer the summary".
|
|
508
|
+
*/
|
|
509
|
+
applyCompactionSplit(maxCtx, summary) {
|
|
510
|
+
const split = this.computeCompactSplit(maxCtx);
|
|
511
|
+
const keep = this.messages.slice(split);
|
|
512
|
+
const dropped = this.messages.length - keep.length;
|
|
513
|
+
// The LEADING system block is carried over explicitly. It sits at the head of the array
|
|
514
|
+
// and the cut drops everything before `split`, so it would otherwise be taken by every
|
|
515
|
+
// compaction β leaving the request with no instructions, which is the loss the `floor`
|
|
516
|
+
// guard in computeCompactSplit exists to prevent.
|
|
517
|
+
let leadingSystem = 0;
|
|
518
|
+
while (leadingSystem < this.messages.length
|
|
519
|
+
&& this.messages[leadingSystem] && this.messages[leadingSystem].role === 'system') leadingSystem++;
|
|
520
|
+
// Only what the cut actually REMOVED. `keep` is `slice(split)`, so at `split === 0` it
|
|
521
|
+
// still contains the whole array including the block β prepending the block then sends the
|
|
522
|
+
// system prompt twice, and `llm.js` concatenates every system message into the request, so
|
|
523
|
+
// it would stay doubled in every compacted request from then on.
|
|
524
|
+
const lead = split > 0 ? this.messages.slice(0, leadingSystem) : [];
|
|
525
|
+
const summaryText = summary
|
|
526
|
+
? `${COMPACTION_SUMMARY_PREFIX}\n${summary}`
|
|
527
|
+
: `${COMPACTION_SUMMARY_PREFIX}\n(no summary available)`;
|
|
528
|
+
this.messages = [
|
|
529
|
+
...lead,
|
|
530
|
+
...keep,
|
|
531
|
+
{ role: 'user', _harness: true, content: wrapSystemReminder(summaryText) },
|
|
532
|
+
{ role: 'user', _harness: true, content: wrapSystemReminder(COMPACTION_CONTINUATION_TEXT) },
|
|
533
|
+
];
|
|
534
|
+
return { split, dropped };
|
|
535
|
+
}
|
|
536
|
+
|
|
444
537
|
async run() {
|
|
538
|
+
// Clear any interrupt flag left from a previous (interrupted) turn. This runs at
|
|
445
539
|
// Clear any interrupt flag left from a previous (interrupted) turn. This runs at
|
|
446
540
|
// the start of EVERY turn, so a stalled Esc/Ctrl-C from an earlier compaction can
|
|
447
541
|
// never suppress auto-compaction on the next message. (The constructor also
|
|
@@ -655,29 +749,7 @@ export class Agent {
|
|
|
655
749
|
break;
|
|
656
750
|
}
|
|
657
751
|
// NOW trim, at a split point the boundary rule allows.
|
|
658
|
-
const split = this.
|
|
659
|
-
const keep = this.messages.slice(split);
|
|
660
|
-
const droppedCount = this.messages.length - keep.length;
|
|
661
|
-
// Shape: [ ...kept, summary, continuation ].
|
|
662
|
-
//
|
|
663
|
-
// The summary is a USER message wrapped in a <system-reminder> (kimi's
|
|
664
|
-
// shape), not a `system` message. Two reasons:
|
|
665
|
-
// * the TUI rebuilds the system prompt from scratch each turn and SKIPS
|
|
666
|
-
// every `system` entry in the stored history, so a system summary was
|
|
667
|
-
// dropped before the model ever saw it;
|
|
668
|
-
// * the turn-end save filters `system` out of the session, so it was not
|
|
669
|
-
// persisted either β history trimmed AND summary lost at both ends,
|
|
670
|
-
// which made "compaction" pure data loss.
|
|
671
|
-
// The continuation note goes LAST so the model's next move is "keep going"
|
|
672
|
-
// rather than "answer the summary".
|
|
673
|
-
const summaryText = summary
|
|
674
|
-
? `${COMPACTION_SUMMARY_PREFIX}\n${summary}`
|
|
675
|
-
: `${COMPACTION_SUMMARY_PREFIX}\n(no summary available)`;
|
|
676
|
-
this.messages = [
|
|
677
|
-
...keep,
|
|
678
|
-
{ role: 'user', _harness: true, content: wrapSystemReminder(summaryText) },
|
|
679
|
-
{ role: 'user', _harness: true, content: wrapSystemReminder(COMPACTION_CONTINUATION_TEXT) },
|
|
680
|
-
];
|
|
752
|
+
const { split, dropped: droppedCount } = this.applyCompactionSplit(maxCtx, summary);
|
|
681
753
|
// The array was REPLACED, so every count the ledger held describes messages
|
|
682
754
|
// that no longer exist. `sync` would notice on its own (the first/last
|
|
683
755
|
// identities no longer match), but resetting here states the intent at the
|
|
@@ -702,8 +774,10 @@ export class Agent {
|
|
|
702
774
|
// first, so it stays recoverable.
|
|
703
775
|
//
|
|
704
776
|
// Trigger: the request reached `trimThreshold` (50% by default) of the window.
|
|
705
|
-
// Target: keep `trimKeepRatio` (30% by default) of the tool-result TEXT,
|
|
706
|
-
//
|
|
777
|
+
// Target: keep `trimKeepRatio` (30% by default) of the tool-result TEXT, newest
|
|
778
|
+
// first. A different denominator from the trigger on purpose: the trigger asks
|
|
779
|
+
// about the window, this asks about the removable output. Skipped while a
|
|
780
|
+
// compaction just ran (that already shrank
|
|
707
781
|
// things) and above the compaction trigger (compaction is the right tool by
|
|
708
782
|
// then, and the two must not fight over the same history).
|
|
709
783
|
const trimThreshold = Number.isFinite(this.cfg.trimThreshold) ? this.cfg.trimThreshold : DEFAULT_TRIM_THRESHOLD;
|
|
@@ -929,7 +1003,13 @@ export class Agent {
|
|
|
929
1003
|
let _args = tc.args; let _skip = false;
|
|
930
1004
|
try { const out = await runPatch('toolDispatch', { name: tc.name, args: tc.args, ctx: this.ctx }, (c) => c); if (out) { _args = out.args !== undefined ? out.args : tc.args; if (out.skip) _skip = true; } } catch (e) { console.error('[hncode-plugin] toolDispatch error:', e.message); }
|
|
931
1005
|
if (_skip) { result = '[skipped by plugin: ' + tc.name + ']'; ok = true; }
|
|
932
|
-
else
|
|
1006
|
+
else {
|
|
1007
|
+
const raw = await tool.execute(_args, this.ctx);
|
|
1008
|
+
// `null` means the tool returned a structured MEDIA result: keep the object so the
|
|
1009
|
+
// image blocks can be built from it. Only a plain value is stringified.
|
|
1010
|
+
const text = toolResultText(raw);
|
|
1011
|
+
result = text === null ? raw : text;
|
|
1012
|
+
}
|
|
933
1013
|
} catch (err) {
|
|
934
1014
|
result = `Error running ${tc.name}: ${err.message}`;
|
|
935
1015
|
ok = false;
|
package/src/cell-buffer.js
CHANGED
|
@@ -126,6 +126,11 @@ export function renderCells(cells) {
|
|
|
126
126
|
}
|
|
127
127
|
out += c.ch;
|
|
128
128
|
}
|
|
129
|
+
// Close the LAST run too. Without this the final style stayed open past the end of the
|
|
130
|
+
// row, and a caller that concatenates rows β `drawFrameBuffer` does, once per line β
|
|
131
|
+
// carried it into the next one: a card drawn over a dimmed or highlighted row tinted
|
|
132
|
+
// every line below it, and the row's own trailing SGR was simply lost.
|
|
133
|
+
if (cur !== null && cur !== '') out += RESET;
|
|
129
134
|
return out;
|
|
130
135
|
}
|
|
131
136
|
|
package/src/clipboard.js
CHANGED
|
@@ -57,16 +57,34 @@ const ONESHOT_MS = 20000;
|
|
|
57
57
|
|
|
58
58
|
function win() { return process.platform === 'win32'; }
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Put text on the clipboard.
|
|
62
|
+
*
|
|
63
|
+
* WINDOWS: `Set-Clipboard` through PowerShell, NOT `clip.exe`.
|
|
64
|
+
*
|
|
65
|
+
* `clip.exe` takes UTF-16LE and needed a byte order mark to be read correctly β without it
|
|
66
|
+
* non-ASCII arrived as mojibake (`δΈζζ΅θ―` came back as `-NεKmθ«`, measured). But the mark is then
|
|
67
|
+
* part of the clipboard text: every copy began with a zero-width U+FEFF that showed up as a stray
|
|
68
|
+
* invisible character wherever it was pasted. Measured over six samples, `Set-Clipboard` was better
|
|
69
|
+
* on every one:
|
|
70
|
+
*
|
|
71
|
+
* copied clip.exe + BOM Set-Clipboard
|
|
72
|
+
* hello U+FEFF hello hello
|
|
73
|
+
* δΈζζ΅θ― U+FEFF δΈζζ΅θ― δΈζζ΅θ―
|
|
74
|
+
* line1\nline2 U+FEFF line1 CRLF line2 line1\nline2 (line endings untouched)
|
|
75
|
+
* emoji π ok U+FEFF emoji π ok emoji π ok
|
|
76
|
+
*
|
|
77
|
+
* It also needs no escaping: PowerShell receives a .NET string rather than a byte stream, so there
|
|
78
|
+
* is no console parser to satisfy β which is what the BOM and the quoting were working around.
|
|
79
|
+
*
|
|
80
|
+
* `clip.exe` stays as a fallback for a machine with no PowerShell at all, where a copy carrying a
|
|
81
|
+
* stray BOM beats no copy.
|
|
82
|
+
*/
|
|
61
83
|
export function copyText(text) {
|
|
62
84
|
const s = String(text == null ? '' : text);
|
|
63
85
|
if (!s) return false;
|
|
64
86
|
if (win()) {
|
|
65
|
-
|
|
66
|
-
// and newlines; plain UTF-8 input turned non-ASCII into mojibake.) Prefix the
|
|
67
|
-
// UTF-16 byte order mark (FF FE) so clip.exe is never left to guess the endian
|
|
68
|
-
// or treat input as ANSI β without it, non-ASCII on some Windows versions /
|
|
69
|
-
// code pages came out mojibake.
|
|
87
|
+
if (setClipboardText(s)) return true;
|
|
70
88
|
cp.execSync('clip', { input: Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(s, 'utf16le')]), windowsHide: true, timeout: 5000 });
|
|
71
89
|
return true;
|
|
72
90
|
}
|
|
@@ -75,6 +93,24 @@ export function copyText(text) {
|
|
|
75
93
|
return true;
|
|
76
94
|
}
|
|
77
95
|
|
|
96
|
+
/** `Set-Clipboard` via PowerShell. False when no PowerShell can be run. */
|
|
97
|
+
function setClipboardText(s) {
|
|
98
|
+
// The text travels as base64 so nothing in it can end the script or be re-parsed. A quoted
|
|
99
|
+
// PowerShell literal would need every `'` doubled, and a newline or a backtick in the text would
|
|
100
|
+
// otherwise change what the script means.
|
|
101
|
+
const b64 = Buffer.from(s, 'utf16le').toString('base64');
|
|
102
|
+
const full = 'Add-Type -AssemblyName System.Windows.Forms; '
|
|
103
|
+
+ '[System.Windows.Forms.Clipboard]::SetText('
|
|
104
|
+
+ `[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String('${b64}')))`;
|
|
105
|
+
for (const exe of [_psExe, 'powershell']) {
|
|
106
|
+
try {
|
|
107
|
+
cp.execFileSync(exe, ['-NoProfile', '-NonInteractive', '-Command', full], { timeout: 10000, windowsHide: true });
|
|
108
|
+
return true;
|
|
109
|
+
} catch { /* try the next exe */ }
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
78
114
|
// Which PowerShell the helper runs. `pwsh` (7) starts far faster than the built-in
|
|
79
115
|
// `powershell` (5.1) β measured 1.9s vs 11.5s for the same image probe β and the image
|
|
80
116
|
// path needs the WinForms/Drawing assemblies. This is only the PREFERRED name: it is
|