@hncode/hncode 0.4.2 → 0.5.1

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.
Files changed (65) hide show
  1. package/README.md +1 -15
  2. package/README_CN.md +1 -15
  3. package/bin/hncode +22 -9
  4. package/package.json +3 -2
  5. package/src/agent.js +149 -35
  6. package/src/bootstrap.js +56 -0
  7. package/src/btw-panel.js +224 -0
  8. package/src/cache.js +3 -2
  9. package/src/cell-buffer.js +173 -0
  10. package/src/ci.js +4 -1
  11. package/src/clipboard.js +202 -43
  12. package/src/colors.js +478 -242
  13. package/src/config.js +409 -40
  14. package/src/context-bar.js +237 -0
  15. package/src/doctor.js +518 -0
  16. package/src/experiments.js +122 -0
  17. package/src/file-history.js +112 -0
  18. package/src/file-tree.js +255 -0
  19. package/src/flush-tick.js +56 -0
  20. package/src/fork.js +44 -0
  21. package/src/fx.js +148 -0
  22. package/src/index.js +123 -11
  23. package/src/keybindings.js +229 -0
  24. package/src/llm.js +355 -34
  25. package/src/lsp.js +401 -0
  26. package/src/memory-profile.js +247 -0
  27. package/src/notebook.js +232 -0
  28. package/src/output-styles.js +183 -0
  29. package/src/patch.js +248 -0
  30. package/src/permissions.js +130 -1
  31. package/src/plan.js +3 -2
  32. package/src/plugin.js +611 -58
  33. package/src/prompt-presets.js +1 -1
  34. package/src/registry-browser.js +31 -13
  35. package/src/remote.js +20 -2
  36. package/src/rollout.js +151 -0
  37. package/src/row-cache.js +148 -0
  38. package/src/schedule.js +294 -0
  39. package/src/session-approvals.js +86 -0
  40. package/src/session-hub.js +1 -0
  41. package/src/session.js +178 -8
  42. package/src/settings-schema.js +301 -0
  43. package/src/shell-safety.js +473 -0
  44. package/src/side-thread.js +240 -0
  45. package/src/skills.js +27 -0
  46. package/src/stream-buffer.js +350 -0
  47. package/src/subagent-control.js +160 -0
  48. package/src/teleport.js +260 -0
  49. package/src/term-caps.js +411 -0
  50. package/src/term.js +283 -8
  51. package/src/tool-result.js +264 -236
  52. package/src/tools/agent-control.js +98 -0
  53. package/src/tools/agent-swarm.js +11 -2
  54. package/src/tools/agent.js +35 -1
  55. package/src/tools/diagnostics.js +175 -0
  56. package/src/tools/edit.js +25 -10
  57. package/src/tools/grep.js +62 -20
  58. package/src/tools/ignore.js +38 -14
  59. package/src/tools/index.js +10 -0
  60. package/src/tools/notebook.js +113 -0
  61. package/src/tools/read-media-file.js +10 -3
  62. package/src/tools/shell.js +34 -6
  63. package/src/tui.js +5637 -746
  64. package/src/web-public/app.js +7 -0
  65. package/src/web.js +3 -1
@@ -0,0 +1,237 @@
1
+ // The segmented context bar: used tokens by content type, then the remainder as one free
2
+ // segment whose right edge carries the usage readout.
3
+ //
4
+ // WHY A BAR AND NOT A NUMBER
5
+ // -------------------------
6
+ // `context: 12% (1.2k/97.7k)` says how full the window is and nothing about what filled
7
+ // it. The bar answers the question people actually have — "why is this getting full" — by
8
+ // making the composition readable: a session that grew from tool output looks different
9
+ // from one that grew from long answers, without reading a single figure.
10
+ //
11
+ // THE SHAPE OF THE ALLOCATION
12
+ // --------------------------
13
+ // Two passes, because plain rounding is wrong in both directions:
14
+ //
15
+ // 1. Every non-zero segment is guaranteed ONE column. Without this, a segment holding
16
+ // 0.3% of the window rounds to zero columns and disappears — and "thinking is a small
17
+ // share" is exactly the fact a user is looking for.
18
+ // 2. The remaining columns are shared proportionally by LARGEST REMAINDER. Rounding each
19
+ // share independently gives a total that is off by one or two, so the bar either falls
20
+ // short of the row or overruns it.
21
+ //
22
+ // THE THREE-STEP LABEL
23
+ // -------------------
24
+ // Each segment says as much about itself as fits, because the segments are often only a
25
+ // few columns wide and there are three different things it could say:
26
+ //
27
+ // 1. `name + value` when the segment is wide enough for both — `ast 4.4k`
28
+ // 2. the name alone when only that fits — `ast`
29
+ // 3. nothing at all when not even the abbreviation does
30
+ //
31
+ // Steps 2 and 3 are not dead ends: every segment's columns are returned as a hit span, so
32
+ // pointing at one prints its full name and value in the row below the bar. That is why step
33
+ // 3 is acceptable — the information stays one pointer away.
34
+
35
+ import { C } from './colors.js';
36
+
37
+ /** The segments, in the order they are drawn. Order is part of the reading: the session
38
+ * grows from left to right in the same sequence the context does. */
39
+ export const SEGMENTS = [
40
+ { key: 'system', name: 'system prompt', fill: C.ctxSystem },
41
+ { key: 'prompt', name: 'user messages', fill: C.ctxPrompt },
42
+ { key: 'assistant', name: 'assistant', fill: C.ctxAssistant },
43
+ { key: 'thinking', name: 'thinking', fill: C.ctxThinking },
44
+ { key: 'tools', name: 'tool results', fill: C.ctxTools },
45
+ ];
46
+
47
+ // `labelFor` closes over `compactTokens`, which is declared below — hoisted function
48
+ // declarations make that safe, and keeping the forms next to the metadata is what stops the
49
+ // on-bar label and the hover caption from drifting apart.
50
+ //
51
+ // There is no abbreviation: `ast` says nothing to somebody who does not already know what
52
+ // the bar is, and a full word that does not fit is handled by DROPPING it, not by
53
+ // shortening it into a private code. The three-step degradation in `paintBar` is what makes
54
+ // that affordable — a wide segment gets `assistant 4.4k`, a narrow one gets `assistant`, and
55
+ // a very narrow one gets nothing and relies on the hover caption instead.
56
+ for (const seg of SEGMENTS) {
57
+ seg.labelFor = (tokens) => {
58
+ const v = compactTokens(tokens);
59
+ return { name: seg.name, full: `${seg.name} ${v}`, value: v, title: seg.name };
60
+ };
61
+ }
62
+ /** The caption a hovered segment produces: `assistant 4.4k`. */
63
+ /** The caption a hovered segment produces: `assistant 4.4k`. */
64
+ export function segmentCaption(key, tokens) {
65
+ const seg = SEGMENTS.find((s) => s.key === key);
66
+ if (!seg) return '';
67
+ return `${seg.name} ${compactTokens(tokens)}`;
68
+ }
69
+
70
+ /**
71
+ * Give every non-zero segment at least one column, then share the rest proportionally.
72
+ *
73
+ * @param {readonly number[]} values one per segment, plus the free segment last.
74
+ * @param {number} width columns available.
75
+ * @returns {number[]} columns per entry, summing to at most `width`.
76
+ */
77
+ export function allocateBarColumns(values, width) {
78
+ const used = SEGMENTS
79
+ .map((_, i) => i)
80
+ .filter((i) => (values[i] || 0) > 0);
81
+ if (used.length === 0 || used.length >= width) return shareLargestRemainder(values, width);
82
+
83
+ const minimum = values.map(() => 0);
84
+ for (const i of used) minimum[i] = 1;
85
+ const rest = shareLargestRemainder(values, width - used.length);
86
+ return minimum.map((m, i) => m + (rest[i] || 0));
87
+ }
88
+
89
+ /**
90
+ * Proportional split by LARGEST REMAINDER: floor each share, then hand the columns left
91
+ * over to the biggest fractional parts, largest first. This is what makes the parts sum to
92
+ * exactly `width`; rounding each independently does not.
93
+ */
94
+ function shareLargestRemainder(values, width) {
95
+ const total = values.reduce((a, b) => a + (b || 0), 0);
96
+ if (!(total > 0) || !(width > 0)) return values.map(() => 0);
97
+
98
+ const exact = values.map((v) => ((v || 0) / total) * width);
99
+ const floors = exact.map((x) => Math.floor(x));
100
+ let left = width - floors.reduce((a, b) => a + b, 0);
101
+
102
+ const order = exact
103
+ .map((x, i) => ({ i, frac: x - Math.floor(x) }))
104
+ .sort((a, b) => b.frac - a.frac || a.i - b.i); // stable on index for equal fractions
105
+ for (let k = 0; left > 0; k++, left--) floors[order[k % order.length].i]++;
106
+ return floors;
107
+ }
108
+
109
+ /**
110
+ * The bar and its readout, filling exactly `width` columns together.
111
+ *
112
+ * THE READOUT GETS ITS SPACE FIRST, AND THE BAR IS WHAT GIVES UP. Laying it out the other
113
+ * way round — bar across the full row, readout painted on top of the free segment — means
114
+ * the readout disappears the moment the window fills, because a full window has no free
115
+ * segment at all. That is the one moment the figure matters most, and it was the one moment
116
+ * it could not be seen.
117
+ *
118
+ * So the row is split: the readout claims the columns it needs (one gap before it, so the
119
+ * figures do not read as part of a segment), and the bar takes whatever is left. The bar can
120
+ * therefore never reach the readout, at any fill level, and the readout is never truncated
121
+ * into nonsense like `11.7k/97`.
122
+ *
123
+ * @param {Record<string, number>} segments tokens per content type.
124
+ * @param {number} usedTokens total used, driving the readout.
125
+ * @param {number} window the context window, or 0 when unknown.
126
+ * @param {number} width columns to fill.
127
+ * @param {string} [readout] the caller's own wording, e.g. `context: 12% (11.7k/97.7k)`.
128
+ * Preferred over the derived one because it is what the status line already said; when the
129
+ * row is too narrow for it, the derived form takes over, and failing that just the
130
+ * percentage — a readout that is always SOMETHING beats one that is sometimes absent.
131
+ * @returns {{ text: string, hits: Array<{col0: number, col1: number, data: string}> }}
132
+ * `text` occupies exactly `width` columns; `hits` cover the bar's segments only.
133
+ */
134
+ export function renderContextBar(segments, usedTokens, window, width, readout) {
135
+ if (!(width > 0) || !(window > 0)) return { text: '', hits: [] };
136
+ // Widest form first, then the derived ones, so the row always carries SOMETHING. A
137
+ // half-printed `11.7k/97` is worse than a bare percentage, so each step is a complete
138
+ // alternative rather than a truncation.
139
+ const pct = `${((usedTokens / window) * 100).toFixed(1)}%`;
140
+ const figures = `${compactTokens(usedTokens)}/${compactTokens(window)}`;
141
+ const own = typeof readout === 'string' ? readout : '';
142
+ const forms = [...new Set([own, `${figures} ${pct}`, pct].filter(Boolean))];
143
+ const text = forms.find((f) => f.length + 1 <= width) || '';
144
+ const barWidth = Math.max(0, width - text.length - 1);
145
+
146
+ if (barWidth <= 0) {
147
+ // Too narrow for a bar AND a gap: the readout has the row to itself. Padded to exactly
148
+ // `width` because every row of the frame is, and a short one shifts everything below it.
149
+ return { text: text ? C.white + text.padEnd(width) : ' '.repeat(width), hits: [] };
150
+ }
151
+
152
+ const free = Math.max(0, window - usedTokens);
153
+ const values = [...SEGMENTS.map((s) => segments[s.key] || 0), free];
154
+ const cols = allocateBarColumns(values, barWidth);
155
+ return paintBar(cols, cols[cols.length - 1] || 0, '', values, text, width);
156
+ }
157
+
158
+ /**
159
+ * Paint the allocated columns, then the readout.
160
+ *
161
+ * Each segment carries as much of its own label as fits, measured against the segment's OWN
162
+ * width, so a label can never spill into its neighbour and shift every segment to the right
163
+ * of it.
164
+ *
165
+ * @param {number[]} cols columns per entry, the last being the free segment.
166
+ * @param {number} freeCols the free segment's width, already included in `cols`.
167
+ * @param {string} _unused kept for call-shape stability; the readout no longer lives inside
168
+ * the free segment, so nothing is painted into it here.
169
+ * @param {number[]} values tokens per segment, driving each label.
170
+ * @param {string} readout the text set after the bar, on the page background rather than on
171
+ * any fill — so it is legible at every fill level, including a completely full bar.
172
+ * @param {number} width the whole row, bar plus readout.
173
+ */
174
+ function paintBar(cols, freeCols, _unused, values, readout, width) {
175
+ let out = '';
176
+ let at = 0; // visible column the next segment starts at
177
+ const hits = [];
178
+
179
+ for (let i = 0; i < SEGMENTS.length; i++) {
180
+ const n = cols[i] || 0;
181
+ if (!n) continue;
182
+ const seg = SEGMENTS[i];
183
+ const label = seg.labelFor(values[i]);
184
+ // One blank column on EACH side of the label. The right one keeps two adjacent short
185
+ // labels from running together (`thinkingtool results`) and making the boundary between
186
+ // them invisible; the left one stops the text from sitting hard against the edge of its
187
+ // own fill, which reads as if the label belonged to the segment before it. The first
188
+ // segment's left column is the row's own margin, so both cost the same.
189
+ const room = n - 2;
190
+ const inner = room >= label.full.length
191
+ ? label.full
192
+ : (room >= label.name.length ? label.name : '');
193
+ // `room` columns of label, a blank on the left and one on the right: `n`.
194
+ out += seg.fill + (inner ? ' ' + C.white + inner.padEnd(room, ' ') + ' ' : ' '.repeat(n));
195
+ hits.push({ col0: at, col1: at + n - 1, data: seg.key });
196
+ at += n;
197
+ }
198
+ // The free segment needs SPACES, not just its fill colour: a background colour on its own
199
+ // paints nothing, so the "how much room is left" band vanished the moment the readout moved
200
+ // out of it. It was only ever visible because the readout used to sit on those cells.
201
+ if (freeCols) { out += C.ctxFree + ' '.repeat(freeCols); at += freeCols; }
202
+
203
+ // The readout goes AFTER the bar, on the page background, in the columns the caller
204
+ // reserved for it. `C.reset` first: the last thing the bar emitted was a fill, and a
205
+ // background colour behind the figures would be a band nobody asked for.
206
+ //
207
+ // `at` is the bar's visible width and the row is `width` wide, so the gap is whatever is
208
+ // left minus the figures — at least one column, which is what keeps `11.7k/97` from reading
209
+ // as a continuation of a segment's fill.
210
+ const gap = Math.max(1, width - at - readout.length);
211
+ out += C.reset + ' '.repeat(gap) + C.white + readout;
212
+
213
+ return { text: out + C.reset, hits };
214
+ }
215
+
216
+ /**
217
+ * The right-hand readout, stepped down as its segment narrows: the full form, then the
218
+ * percentage alone, then nothing. A half-printed `11.7k/97` is worse than no number.
219
+ */
220
+ export function formatReadout(used, window, freeCols) {
221
+ const pct = `${((used / window) * 100).toFixed(1)}%`;
222
+ const full = `${compactTokens(used)}/${compactTokens(window)} ${pct}`;
223
+ // The thresholds match the renderer's own condition exactly (`freeCols >= length`). They
224
+ // used to ask for one more, which is how the bar came up a column short: the readout was
225
+ // accepted HERE and then not written THERE.
226
+ if (freeCols >= full.length) return full;
227
+ if (freeCols >= pct.length) return pct;
228
+ return '';
229
+ }
230
+
231
+ /** 1200 -> `1.2k`, 97000 -> `97.0k`, 3_400_000 -> `3.4M`. */
232
+ export function compactTokens(n) {
233
+ const v = Number(n) || 0;
234
+ if (v >= 1e6) return `${(v / 1e6).toFixed(1)}M`;
235
+ if (v >= 1e3) return `${(v / 1e3).toFixed(1)}k`;
236
+ return String(Math.round(v));
237
+ }