@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/cache.js
CHANGED
|
@@ -1,131 +1,176 @@
|
|
|
1
|
-
// Prompt caching — explicit cache breakpoints for long, stable prefixes.
|
|
2
|
-
//
|
|
3
|
-
// Why this is worth doing: an agent resends the whole conversation every turn,
|
|
4
|
-
// and the prefix (system prompt + tool definitions + early messages) is
|
|
5
|
-
// byte-identical across turns. Providers that support prompt caching bill those
|
|
6
|
-
// tokens at a fraction of the input price and skip the prefill compute. Without
|
|
7
|
-
// explicit breakpoints only the providers that cache automatically benefit.
|
|
8
|
-
//
|
|
9
|
-
// Protocol differences:
|
|
10
|
-
// * anthropic — explicit: mark up to N breakpoints with
|
|
11
|
-
// `cache_control: { type: "ephemeral" }`. Anthropic caches everything UP TO
|
|
12
|
-
// and INCLUDING a marked block, so the LAST breakpoint should sit at the end
|
|
13
|
-
// of the stable prefix. The minimum cacheable prefix is model-dependent
|
|
14
|
-
// (~1024 tokens); marking a shorter prefix is a no-op, not an error.
|
|
15
|
-
// * openai — automatic for prompts over ~1024 tokens on supporting models. A
|
|
16
|
-
// stable `prompt_cache_key` improves routing; keys must not vary per request.
|
|
17
|
-
// * everything else — no-op. Unknown providers are left untouched: sending an
|
|
18
|
-
// unsupported `cache_control` field can 400 some gateways.
|
|
19
|
-
//
|
|
20
|
-
// The cache is only useful when the prefix is STABLE, so the helpers here take
|
|
21
|
-
// care to mark the same positions every turn for the same conversation.
|
|
22
|
-
|
|
23
|
-
import { estimateTokens } from './term.js';
|
|
24
|
-
|
|
25
|
-
// Anthropic allows at most 4 cache breakpoints per request.
|
|
26
|
-
export const MAX_BREAKPOINTS = 4;
|
|
27
|
-
// Below this, a breakpoint is pointless: most providers will not cache a prefix
|
|
28
|
-
// this short, and the marker is wasted budget.
|
|
29
|
-
export const MIN_CACHEABLE_TOKENS = 1024;
|
|
30
|
-
|
|
31
|
-
export function supportsCaching(protocol) {
|
|
32
|
-
return protocol === 'anthropic' || protocol === 'openai' || protocol === 'responses';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Index (into `messages`) of the last message that belongs to the STABLE prefix.
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
// for
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
1
|
+
// Prompt caching — explicit cache breakpoints for long, stable prefixes.
|
|
2
|
+
//
|
|
3
|
+
// Why this is worth doing: an agent resends the whole conversation every turn,
|
|
4
|
+
// and the prefix (system prompt + tool definitions + early messages) is
|
|
5
|
+
// byte-identical across turns. Providers that support prompt caching bill those
|
|
6
|
+
// tokens at a fraction of the input price and skip the prefill compute. Without
|
|
7
|
+
// explicit breakpoints only the providers that cache automatically benefit.
|
|
8
|
+
//
|
|
9
|
+
// Protocol differences:
|
|
10
|
+
// * anthropic — explicit: mark up to N breakpoints with
|
|
11
|
+
// `cache_control: { type: "ephemeral" }`. Anthropic caches everything UP TO
|
|
12
|
+
// and INCLUDING a marked block, so the LAST breakpoint should sit at the end
|
|
13
|
+
// of the stable prefix. The minimum cacheable prefix is model-dependent
|
|
14
|
+
// (~1024 tokens); marking a shorter prefix is a no-op, not an error.
|
|
15
|
+
// * openai — automatic for prompts over ~1024 tokens on supporting models. A
|
|
16
|
+
// stable `prompt_cache_key` improves routing; keys must not vary per request.
|
|
17
|
+
// * everything else — no-op. Unknown providers are left untouched: sending an
|
|
18
|
+
// unsupported `cache_control` field can 400 some gateways.
|
|
19
|
+
//
|
|
20
|
+
// The cache is only useful when the prefix is STABLE, so the helpers here take
|
|
21
|
+
// care to mark the same positions every turn for the same conversation.
|
|
22
|
+
|
|
23
|
+
import { estimateTokens } from './term.js';
|
|
24
|
+
|
|
25
|
+
// Anthropic allows at most 4 cache breakpoints per request.
|
|
26
|
+
export const MAX_BREAKPOINTS = 4;
|
|
27
|
+
// Below this, a breakpoint is pointless: most providers will not cache a prefix
|
|
28
|
+
// this short, and the marker is wasted budget.
|
|
29
|
+
export const MIN_CACHEABLE_TOKENS = 1024;
|
|
30
|
+
|
|
31
|
+
export function supportsCaching(protocol) {
|
|
32
|
+
return protocol === 'anthropic' || protocol === 'openai' || protocol === 'responses';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Index (into `messages`) of the last message that belongs to the STABLE prefix.
|
|
36
|
+
// The prefix is only cacheable while it cannot change between turns. The system
|
|
37
|
+
// prompt and tool list are fixed for a session; the conversation then grows by
|
|
38
|
+
// APPENDING. So the prefix is "everything except the recent tail", and the tail
|
|
39
|
+
// must be excluded because it is what changes next turn.
|
|
40
|
+
//
|
|
41
|
+
// We keep the last `keepTail` messages out of the cached region: a tool result
|
|
42
|
+
// appended this turn is not part of the stable prefix next turn until it is
|
|
43
|
+
// itself superseded. Returns -1 when there is nothing worth marking.
|
|
44
|
+
export function stablePrefixEnd(messages, keepTail = 2) {
|
|
45
|
+
const n = Array.isArray(messages) ? messages.length : 0;
|
|
46
|
+
if (n === 0) return -1;
|
|
47
|
+
// Never mark the very last message: it is the newest content and is the most
|
|
48
|
+
// likely to be replaced/regenerated.
|
|
49
|
+
const end = Math.min(n - 1, n - 1 - Math.max(1, keepTail));
|
|
50
|
+
return end >= 0 ? end : -1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Attach Anthropic `cache_control` breakpoints to a request body IN PLACE-safe
|
|
54
|
+
// Total character length of the prefix's content. `.length` is O(1) per string,
|
|
55
|
+
// so this is O(#messages) — versus the full-string join plus the per-character
|
|
56
|
+
// token walk the decision below used to make on EVERY request (and on every
|
|
57
|
+
// retry attempt): ~1 MB of building and ~1M loop iterations on a 200k-token
|
|
58
|
+
// history.
|
|
59
|
+
function prefixContentLength(messages, end) {
|
|
60
|
+
let n = 0;
|
|
61
|
+
for (let i = 0; i <= end; i++) {
|
|
62
|
+
const c = messages[i] && messages[i].content;
|
|
63
|
+
if (typeof c === 'string') n += c.length;
|
|
64
|
+
else if (Array.isArray(c)) {
|
|
65
|
+
for (const b of c) if (b && typeof b.text === 'string') n += b.text.length;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return n;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Memo of the last decision, keyed by `end:totalLength`. Tool-result trimming
|
|
72
|
+
// rewrites content in place, which changes the length and so invalidates it.
|
|
73
|
+
let _prefixDecision = { key: '', cacheable: false };
|
|
74
|
+
|
|
75
|
+
// Attach Anthropic `cache_control` breakpoints to a request body IN PLACE-safe
|
|
76
|
+
// fashion (returns a new body). Breakpoints are placed:
|
|
77
|
+
// 1. on the LAST tool definition (caches the whole tool block),
|
|
78
|
+
// 2. on the system prompt (caches tools + system together),
|
|
79
|
+
// 3. on the last message of the stable prefix (caches the conversation head).
|
|
80
|
+
// Marking the tail first and working backwards keeps the total <= MAX_BREAKPOINTS.
|
|
81
|
+
//
|
|
82
|
+
// `cacheEnd` is the index of the last message that belongs to the stable prefix,
|
|
83
|
+
// counted by the CALLER. The agent loop knows it exactly — everything before that
|
|
84
|
+
// index was already sent in an earlier request of this turn — so passing it beats
|
|
85
|
+
// any guess made from the message count here.
|
|
86
|
+
export function withAnthropicCache(body, opts = {}) {
|
|
87
|
+
if (!body || typeof body !== 'object') return body;
|
|
88
|
+
const minTokens = opts.minTokens || MIN_CACHEABLE_TOKENS;
|
|
89
|
+
const out = { ...body };
|
|
90
|
+
const marker = { type: 'ephemeral' };
|
|
91
|
+
|
|
92
|
+
// Tools: marking the last one caches the entire tools array.
|
|
93
|
+
if (Array.isArray(out.tools) && out.tools.length) {
|
|
94
|
+
const tools = out.tools.slice();
|
|
95
|
+
tools[tools.length - 1] = { ...tools[tools.length - 1], cache_control: marker };
|
|
96
|
+
out.tools = tools;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// System: a plain string must become a text block to carry cache_control.
|
|
100
|
+
if (typeof out.system === 'string' && out.system.trim()) {
|
|
101
|
+
out.system = [{ type: 'text', text: out.system, cache_control: marker }];
|
|
102
|
+
} else if (Array.isArray(out.system) && out.system.length) {
|
|
103
|
+
const sys = out.system.slice();
|
|
104
|
+
const last = sys.length - 1;
|
|
105
|
+
sys[last] = { ...(typeof sys[last] === 'string' ? { type: 'text', text: sys[last] } : sys[last]), cache_control: marker };
|
|
106
|
+
out.system = sys;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Conversation head: mark the stable prefix's last message.
|
|
110
|
+
if (Array.isArray(out.messages) && out.messages.length) {
|
|
111
|
+
// The caller's boundary when it has one; otherwise fall back to counting off
|
|
112
|
+
// the tail. A caller's value ABOVE the last index means the array grew after it
|
|
113
|
+
// was taken, so it is clamped to the end rather than rejected — the prefix it
|
|
114
|
+
// describes is still the stable one, it just no longer reaches the tail.
|
|
115
|
+
const n = out.messages.length;
|
|
116
|
+
const end = Number.isInteger(opts.cacheEnd)
|
|
117
|
+
? Math.min(n - 1, opts.cacheEnd)
|
|
118
|
+
: stablePrefixEnd(out.messages, opts.keepTail);
|
|
119
|
+
// An EMPTY prefix is not worth marking. With `cacheEnd === -1` the caller is
|
|
120
|
+
// saying "nothing has been sent yet", so every message in this request is new.
|
|
121
|
+
if (end >= 0 && end < n) {
|
|
122
|
+
// Only worth a breakpoint once the prefix is long enough to cache. Memoized
|
|
123
|
+
// on the length fingerprint; a cold decision uses ~4 chars/token — exact for
|
|
124
|
+
// ASCII code and prose, conservative for CJK (whose real token count is
|
|
125
|
+
// HIGHER than chars/4, so a marked prefix is always truly cacheable). Being
|
|
126
|
+
// wrong on the other side is a harmless no-op: the provider ignores a
|
|
127
|
+
// breakpoint on a sub-minimum prefix.
|
|
128
|
+
const plen = prefixContentLength(out.messages, end);
|
|
129
|
+
const key = `${end}:${plen}`;
|
|
130
|
+
let cacheable;
|
|
131
|
+
if (_prefixDecision.key === key) cacheable = _prefixDecision.cacheable;
|
|
132
|
+
else {
|
|
133
|
+
cacheable = plen / 4 >= minTokens;
|
|
134
|
+
_prefixDecision.key = key;
|
|
135
|
+
_prefixDecision.cacheable = cacheable;
|
|
136
|
+
}
|
|
137
|
+
if (cacheable) {
|
|
138
|
+
const msgs = out.messages.slice();
|
|
139
|
+
const msg = { ...msgs[end] };
|
|
140
|
+
if (typeof msg.content === 'string') {
|
|
141
|
+
msg.content = [{ type: 'text', text: msg.content }];
|
|
142
|
+
}
|
|
143
|
+
if (Array.isArray(msg.content) && msg.content.length) {
|
|
144
|
+
const c = msg.content.slice();
|
|
145
|
+
const li = c.length - 1;
|
|
146
|
+
c[li] = { ...c[li], cache_control: marker };
|
|
147
|
+
msg.content = c;
|
|
148
|
+
}
|
|
149
|
+
msgs[end] = msg;
|
|
150
|
+
out.messages = msgs;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// OpenAI-compatible: most providers cache automatically. A STABLE
|
|
158
|
+
// `prompt_cache_key` helps routing on the providers that accept it, and must not
|
|
159
|
+
// change between turns of the same conversation or the cache thrashes.
|
|
160
|
+
// `seed` should therefore identify the SESSION, not the request.
|
|
161
|
+
export function withOpenAiCache(body, opts = {}) {
|
|
162
|
+
if (!body || typeof body !== 'object') return body;
|
|
163
|
+
const key = opts.sessionKey;
|
|
164
|
+
if (!key) return body;
|
|
165
|
+
return { ...body, prompt_cache_key: String(key) };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Apply the right caching strategy for `protocol`. Returns the body unchanged
|
|
169
|
+
// for protocols with no explicit support.
|
|
170
|
+
export function applyPromptCache(protocol, body, opts = {}) {
|
|
171
|
+
if (!opts.enabled) return body;
|
|
172
|
+
if (protocol === 'anthropic') return withAnthropicCache(body, opts);
|
|
173
|
+
// Responses API accepts the same stable `prompt_cache_key` as chat/completions.
|
|
174
|
+
if (protocol === 'openai' || protocol === 'responses') return withOpenAiCache(body, opts);
|
|
175
|
+
return body;
|
|
131
176
|
}
|
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
|
@@ -40,6 +40,8 @@ let helperBuf = ''; // stdout accumulated since the last sentinel
|
|
|
40
40
|
let helperQueue = []; // pending read() resolvers, ONE per sentinel written
|
|
41
41
|
let helperBusy = false; // one command in flight at a time
|
|
42
42
|
let helperReady = false; // the warm sequence has answered: the shell can take commands
|
|
43
|
+
let keepAliveTimer = null; // a scheduled respawn, after the helper died
|
|
44
|
+
let _shuttingDown = false; // set by the exit handler so a dead shell is not respawned during cleanup
|
|
43
45
|
|
|
44
46
|
/** How long a read will wait for the helper to finish warming. It is already coming
|
|
45
47
|
* up, and waiting for a ~10ms answer beats the multi-second cold fallback. */
|
|
@@ -57,16 +59,34 @@ const ONESHOT_MS = 20000;
|
|
|
57
59
|
|
|
58
60
|
function win() { return process.platform === 'win32'; }
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Put text on the clipboard.
|
|
64
|
+
*
|
|
65
|
+
* WINDOWS: `Set-Clipboard` through PowerShell, NOT `clip.exe`.
|
|
66
|
+
*
|
|
67
|
+
* `clip.exe` takes UTF-16LE and needed a byte order mark to be read correctly — without it
|
|
68
|
+
* non-ASCII arrived as mojibake (`中文测试` came back as `-N噀Km諎`, measured). But the mark is then
|
|
69
|
+
* part of the clipboard text: every copy began with a zero-width U+FEFF that showed up as a stray
|
|
70
|
+
* invisible character wherever it was pasted. Measured over six samples, `Set-Clipboard` was better
|
|
71
|
+
* on every one:
|
|
72
|
+
*
|
|
73
|
+
* copied clip.exe + BOM Set-Clipboard
|
|
74
|
+
* hello U+FEFF hello hello
|
|
75
|
+
* 中文测试 U+FEFF 中文测试 中文测试
|
|
76
|
+
* line1\nline2 U+FEFF line1 CRLF line2 line1\nline2 (line endings untouched)
|
|
77
|
+
* emoji 🎉 ok U+FEFF emoji 🎉 ok emoji 🎉 ok
|
|
78
|
+
*
|
|
79
|
+
* It also needs no escaping: PowerShell receives a .NET string rather than a byte stream, so there
|
|
80
|
+
* is no console parser to satisfy — which is what the BOM and the quoting were working around.
|
|
81
|
+
*
|
|
82
|
+
* `clip.exe` stays as a fallback for a machine with no PowerShell at all, where a copy carrying a
|
|
83
|
+
* stray BOM beats no copy.
|
|
84
|
+
*/
|
|
61
85
|
export function copyText(text) {
|
|
62
86
|
const s = String(text == null ? '' : text);
|
|
63
87
|
if (!s) return false;
|
|
64
88
|
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.
|
|
89
|
+
if (setClipboardText(s)) return true;
|
|
70
90
|
cp.execSync('clip', { input: Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(s, 'utf16le')]), windowsHide: true, timeout: 5000 });
|
|
71
91
|
return true;
|
|
72
92
|
}
|
|
@@ -75,6 +95,24 @@ export function copyText(text) {
|
|
|
75
95
|
return true;
|
|
76
96
|
}
|
|
77
97
|
|
|
98
|
+
/** `Set-Clipboard` via PowerShell. False when no PowerShell can be run. */
|
|
99
|
+
function setClipboardText(s) {
|
|
100
|
+
// The text travels as base64 so nothing in it can end the script or be re-parsed. A quoted
|
|
101
|
+
// PowerShell literal would need every `'` doubled, and a newline or a backtick in the text would
|
|
102
|
+
// otherwise change what the script means.
|
|
103
|
+
const b64 = Buffer.from(s, 'utf16le').toString('base64');
|
|
104
|
+
const full = 'Add-Type -AssemblyName System.Windows.Forms; '
|
|
105
|
+
+ '[System.Windows.Forms.Clipboard]::SetText('
|
|
106
|
+
+ `[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String('${b64}')))`;
|
|
107
|
+
for (const exe of [_psExe, 'powershell']) {
|
|
108
|
+
try {
|
|
109
|
+
cp.execFileSync(exe, ['-NoProfile', '-NonInteractive', '-Command', full], { timeout: 10000, windowsHide: true });
|
|
110
|
+
return true;
|
|
111
|
+
} catch { /* try the next exe */ }
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
78
116
|
// Which PowerShell the helper runs. `pwsh` (7) starts far faster than the built-in
|
|
79
117
|
// `powershell` (5.1) — measured 1.9s vs 11.5s for the same image probe — and the image
|
|
80
118
|
// path needs the WinForms/Drawing assemblies. This is only the PREFERRED name: it is
|
|
@@ -99,9 +137,24 @@ function runPowershell(args, opts) {
|
|
|
99
137
|
// ------------------------------------------------------- PowerShell helper
|
|
100
138
|
export function warmClipboard() {
|
|
101
139
|
if (!win() || helper) return;
|
|
140
|
+
_shuttingDown = false; // an explicit warm cancels a shutdown in progress
|
|
102
141
|
startHelper(_psExe);
|
|
103
142
|
}
|
|
104
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Stop the keep-alive respawn and let the helper die with the process.
|
|
146
|
+
*
|
|
147
|
+
* Called from the exit handler. Without it a helper that exits during shutdown would be respawned by
|
|
148
|
+
* `dropHelper`, leaving a PowerShell child running after the TUI is gone.
|
|
149
|
+
*/
|
|
150
|
+
export function stopClipboardHelper() {
|
|
151
|
+
_shuttingDown = true;
|
|
152
|
+
if (keepAliveTimer) { clearTimeout(keepAliveTimer); keepAliveTimer = null; }
|
|
153
|
+
const h = helper;
|
|
154
|
+
dropHelper();
|
|
155
|
+
if (h) { try { h.stdin.end(); } catch { /* already closed */ } }
|
|
156
|
+
}
|
|
157
|
+
|
|
105
158
|
/** Spawn the long-lived shell. `exe` is tried first; if that exe does not exist,
|
|
106
159
|
* the spawn reports ENOENT on its `error` event and this retries with 5.1. */
|
|
107
160
|
function startHelper(exe) {
|
|
@@ -153,6 +206,18 @@ function dropHelper() {
|
|
|
153
206
|
const waiting = helperQueue;
|
|
154
207
|
helperQueue = [];
|
|
155
208
|
for (const w of waiting) w(null);
|
|
209
|
+
// RESPAWN, unless this is a deliberate shutdown.
|
|
210
|
+
//
|
|
211
|
+
// The helper can die without anyone noticing — a shell that exits on its own, a resource hiccup — and
|
|
212
|
+
// nothing restarted it. The next paste then paid the cold path: a fresh spawn plus the warm-up
|
|
213
|
+
// sentinel, measured at 1.2 seconds, on a keypress that is supposed to be instant.
|
|
214
|
+
//
|
|
215
|
+
// A respawn is scheduled rather than immediate so a crash loop cannot spin: the shell gets a moment to
|
|
216
|
+
// be gone before a new one is asked for. `_shuttingDown` stops it after cleanup.
|
|
217
|
+
if (!_shuttingDown) {
|
|
218
|
+
if (keepAliveTimer) clearTimeout(keepAliveTimer);
|
|
219
|
+
keepAliveTimer = setTimeout(() => { if (!_shuttingDown) warmClipboard(); }, 200);
|
|
220
|
+
}
|
|
156
221
|
}
|
|
157
222
|
|
|
158
223
|
/** Resolve once the shell can take commands, or false when it never got there. */
|
|
@@ -242,6 +307,24 @@ export async function readText(timeoutMs = HELPER_CMD_MS) {
|
|
|
242
307
|
// in use here), so it silently failed everywhere and image paste never worked.
|
|
243
308
|
// `[Windows.Forms.Clipboard]::GetImage()` exists on Windows PowerShell 5.1 AND
|
|
244
309
|
// pwsh 7 — verified on 5.1 — so it is the portable way to read a copied image.
|
|
310
|
+
/**
|
|
311
|
+
* The TEXT-ONLY probe, run first.
|
|
312
|
+
*
|
|
313
|
+
* The full probe below checks three clipboard formats in order — files, text, image — so a plain text
|
|
314
|
+
* paste pays for two failed checks before the one that answers. Text is overwhelmingly the common case,
|
|
315
|
+
* and the two other checks are the slow ones: `Get-Clipboard -Format FileDropList` round-trips through
|
|
316
|
+
* the shell's COM clipboard, and `Clipboard::GetImage()` decodes an image.
|
|
317
|
+
*
|
|
318
|
+
* So the text check is split out and run alone. Only when it comes back EMPTY — no text on the clipboard,
|
|
319
|
+
* so it must be a file or an image — does the caller run the full probe. A text paste now costs one
|
|
320
|
+
* clipboard call instead of three.
|
|
321
|
+
*/
|
|
322
|
+
const CLIP_PROBE_TEXT = [
|
|
323
|
+
"$ErrorActionPreference='SilentlyContinue'",
|
|
324
|
+
"$t=Get-Clipboard -Raw",
|
|
325
|
+
"if($t){'T::'+[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($t))}",
|
|
326
|
+
].join('; ');
|
|
327
|
+
|
|
245
328
|
const CLIP_PROBE = [
|
|
246
329
|
"$ErrorActionPreference='SilentlyContinue'",
|
|
247
330
|
"$f=Get-Clipboard -Format FileDropList",
|
|
@@ -315,9 +398,29 @@ async function readViaHelper() {
|
|
|
315
398
|
await new Promise((r) => setTimeout(r, 25));
|
|
316
399
|
}
|
|
317
400
|
if (helperBusy || !helper) return null;
|
|
401
|
+
|
|
402
|
+
// TEXT FIRST. One clipboard call instead of the full probe's three, so the common paste answers in a
|
|
403
|
+
// third of the round-trips.
|
|
318
404
|
helperBusy = true;
|
|
319
|
-
|
|
320
|
-
|
|
405
|
+
let out;
|
|
406
|
+
try {
|
|
407
|
+
out = await helperRun(CLIP_PROBE_TEXT, HELPER_CMD_MS);
|
|
408
|
+
} finally {
|
|
409
|
+
helperBusy = false;
|
|
410
|
+
}
|
|
411
|
+
const parsed = out == null ? null : parseClipResult(out);
|
|
412
|
+
// Text found — done. An EMPTY answer means there is no text, so the clipboard holds a file or an
|
|
413
|
+
// image and the full probe is worth its cost.
|
|
414
|
+
if (parsed) return parsed;
|
|
415
|
+
|
|
416
|
+
helperBusy = true;
|
|
417
|
+
let out2;
|
|
418
|
+
try {
|
|
419
|
+
out2 = await helperRun(CLIP_PROBE, HELPER_CMD_MS);
|
|
420
|
+
} finally {
|
|
421
|
+
helperBusy = false;
|
|
422
|
+
}
|
|
423
|
+
return out2 == null ? null : parseClipResult(out2);
|
|
321
424
|
}
|
|
322
425
|
|
|
323
426
|
export function readClipboardContent() {
|
package/src/context-bar.js
CHANGED
|
@@ -228,10 +228,51 @@ export function formatReadout(used, window, freeCols) {
|
|
|
228
228
|
return '';
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
/**
|
|
231
|
+
/**
|
|
232
|
+
* A token count as a compact string: `1200` -> `1.2k`, `97000` -> `97k`, `3400000` -> `3.4M`.
|
|
233
|
+
*
|
|
234
|
+
* Deliberately the SAME shape `fmtTokensShort` produces for the working row, because both appear
|
|
235
|
+
* in one frame — the context bar and the live turn counter would otherwise disagree about the same
|
|
236
|
+
* quantity. Two rules:
|
|
237
|
+
*
|
|
238
|
+
* * one decimal while it carries information, dropped when it is a zero (`97000` is `97k`, not
|
|
239
|
+
* `97.0k` — the trailing `.0` is three characters saying nothing);
|
|
240
|
+
* * K/M/B/T suffixes, with only the kilo lower-case, so `3.4M` here and `3.4M` on the working row
|
|
241
|
+
* read as one vocabulary.
|
|
242
|
+
*
|
|
243
|
+
* The previous version kept `.0` and disagreed with the working row on units entirely: it divided
|
|
244
|
+
* by 1024 where the row divided by 1000, so a 97k context read `94.7k` beside a `97k` turn.
|
|
245
|
+
*/
|
|
232
246
|
export function compactTokens(n) {
|
|
233
|
-
const v = Number(n) || 0;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
247
|
+
const v = Math.max(0, Math.floor(Number(n) || 0));
|
|
248
|
+
const units = [
|
|
249
|
+
{ at: 1e12, suffix: 'T' },
|
|
250
|
+
{ at: 1e9, suffix: 'B' },
|
|
251
|
+
{ at: 1e6, suffix: 'M' },
|
|
252
|
+
{ at: 1e3, suffix: 'k' },
|
|
253
|
+
];
|
|
254
|
+
for (let i = 0; i < units.length; i++) {
|
|
255
|
+
const u = units[i];
|
|
256
|
+
if (v < u.at) continue;
|
|
257
|
+
// The identical scaling rule the working row and the /ctx readout use: one decimal below 100,
|
|
258
|
+
// whole numbers above. Three formatters render token counts into one frame, so a single rule is
|
|
259
|
+
// the only way `262k` cannot appear beside `262.1k` for the same quantity.
|
|
260
|
+
const shown = scaleUnit(v / u.at);
|
|
261
|
+
// Rounding can reach the next unit (`999999` is `1000k`, which is `1m`), and showing `1000k`
|
|
262
|
+
// makes the reader convert a unit themselves.
|
|
263
|
+
if (Number(shown) >= 1000 && i > 0) return `${scaleUnit(v / units[i - 1].at)}${units[i - 1].suffix}`;
|
|
264
|
+
return `${shown}${u.suffix}`;
|
|
265
|
+
}
|
|
266
|
+
return String(v);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* One unit's worth of a count: `1.2`, `97`, `262`.
|
|
271
|
+
*
|
|
272
|
+
* Duplicated from tui.js rather than imported, because `context-bar.js` must stay a leaf module the
|
|
273
|
+
* TUI can import without a cycle. The two are pinned together by test/context-bar.test.mjs and
|
|
274
|
+
* test/working-tokens.test.mjs, which assert the same table of values through both.
|
|
275
|
+
*/
|
|
276
|
+
function scaleUnit(v) {
|
|
277
|
+
return v < 100 ? v.toFixed(1).replace(/\.0$/, '') : String(Math.round(v));
|
|
237
278
|
}
|
package/src/doctor.js
CHANGED
|
@@ -306,8 +306,14 @@ function checkGit(workspace, cp) {
|
|
|
306
306
|
const branch = run(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
307
307
|
out.push(check('git', 'ok', `${branch || '(detached)'} in ${top}`));
|
|
308
308
|
|
|
309
|
+
// Because `run` pins cwd to the workspace, `config --get` reports the identity that a
|
|
310
|
+
// commit HERE would actually use — not one inherited from an unrelated directory.
|
|
311
|
+
// A value that IS set is reported too: only a row in the report distinguishes
|
|
312
|
+
// "configured" from "the check never ran", and silence reads as success.
|
|
309
313
|
for (const key of ['user.name', 'user.email']) {
|
|
310
|
-
|
|
314
|
+
const value = run(['config', '--get', key]);
|
|
315
|
+
if (value) out.push(check(`git ${key}`, 'ok', value));
|
|
316
|
+
else {
|
|
311
317
|
out.push(check(`git ${key}`, 'warn', 'not set',
|
|
312
318
|
`Every commit fails without it. Run: git config --global ${key} "…"`));
|
|
313
319
|
}
|
|
@@ -324,7 +330,6 @@ function checkGit(workspace, cp) {
|
|
|
324
330
|
}
|
|
325
331
|
}
|
|
326
332
|
return out;
|
|
327
|
-
return out;
|
|
328
333
|
}
|
|
329
334
|
|
|
330
335
|
/**
|