mu-harness 0.28.0 → 0.30.0

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.
@@ -41,17 +41,27 @@ export const createQuitCommand = (onQuit) => ({
41
41
  },
42
42
  });
43
43
  const estTokens = (chars) => Math.max(1, Math.round(chars / 4));
44
- const GRID_COLS = 24;
45
- const GRID_ROWS = 8;
44
+ const GRID_COLS = 20;
45
+ const GRID_ROWS = 10;
46
46
  const GRID_CELLS = GRID_COLS * GRID_ROWS;
47
- const BLOCK = '█';
48
- const FREE = '·';
49
- // ANSI SGR colours — mu's TUI text utils are ANSI-aware so these render in the terminal;
47
+ const GRID_WIDTH = GRID_COLS * 2 - 1; // glyphs joined by single spaces
48
+ const USED = ''; // a filled context cell (Claude Code's /context glyph)
49
+ const FREEG = '⛶'; // a free cell
50
+ const BUFFER_COLOR = '\x1b[93m'; // bright yellow — the compaction reserve
51
+ // ANSI SGR codes — mu's TUI text utils are ANSI-aware so these render in the terminal;
50
52
  // the companion strips them (plain text), keeping the labelled breakdown readable.
51
53
  const RESET = '\x1b[0m';
52
54
  const DIM = '\x1b[2m';
55
+ const BOLD = '\x1b[1m';
56
+ const ITALIC_DIM = '\x1b[2;3m';
53
57
  const paint = (s, color) => `${color}${s}${RESET}`;
54
- const fillColor = (pct) => (pct >= 80 ? '\x1b[31m' : pct >= 50 ? '\x1b[33m' : '\x1b[32m');
58
+ const fmtTok = (n) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`);
59
+ const pctStr = (n, w) => {
60
+ if (w <= 0)
61
+ return '';
62
+ const p = (n / w) * 100;
63
+ return p > 0 && p < 0.1 ? '<0.1%' : `${p.toFixed(1)}%`;
64
+ };
55
65
  /** Extract a `<tag>…</tag>` block (with tags), or '' when absent. */
56
66
  function tagBlock(s, tag) {
57
67
  const i = s.indexOf(`<${tag}>`);
@@ -111,42 +121,68 @@ export const createContextCommand = () => ({
111
121
  }
112
122
  return { n: estTokens(text.length), exact: false };
113
123
  };
114
- // label, text, ANSI colour — one per category, in render order.
124
+ // label, text, ANSI colour — one per category, in render order (descriptive labels à la Claude Code).
115
125
  const SPEC = [
116
- ['system', sys.agent, '\x1b[36m'], // cyan — the agent prompt
117
- ['context', sys.env, '\x1b[33m'], // yellow — the <env> block
118
- ['instructions', sys.instructions, '\x1b[34m'], // blue — AGENTS.md / CLAUDE.md
119
- ['memory', sys.memory, '\x1b[35m'], // magenta — MEMORY.md
120
- ['tools', `${toolSchemas}\n${sys.toolPrompts}`, '\x1b[31m'], // red — schemas + tool prompts
121
- ['you', byRole('user'), '\x1b[32m'], // green — your messages
122
- ['agent', byRole('assistant'), '\x1b[94m'], // bright blue — assistant replies
123
- ['tool-out', byRole('tool'), '\x1b[90m'], // grey — tool results
126
+ ['System prompt', sys.agent, '\x1b[36m'], // cyan — the agent prompt
127
+ ['Environment', sys.env, '\x1b[33m'], // yellow — the <env> block
128
+ ['Instructions', sys.instructions, '\x1b[34m'], // blue — AGENTS.md / CLAUDE.md
129
+ ['Memory', sys.memory, '\x1b[35m'], // magenta — MEMORY.md
130
+ ['Tools', `${toolSchemas}\n${sys.toolPrompts}`, '\x1b[31m'], // red — schemas + tool prompts
131
+ ['You', byRole('user'), '\x1b[32m'], // green — your messages
132
+ ['Agent', byRole('assistant'), '\x1b[94m'], // bright blue — assistant replies
133
+ ['Tool results', byRole('tool'), '\x1b[90m'], // grey — tool results
124
134
  ];
125
135
  const measured = await Promise.all(SPEC.map(([, text]) => measure(text)));
126
136
  const cats = SPEC.map(([label, , color], i) => ({ label, n: measured[i].n, color })).filter((c) => c.n > 0);
127
137
  const exact = measured.every((m) => m.exact);
128
138
  const total = cats.reduce((s, c) => s + c.n, 0);
129
139
  const window = (await ctx.session?.contextWindow?.()) ?? 0;
130
- const mark = (n) => (exact ? `${n}` : `~${n}`);
131
- const lines = [`context — tokens (${exact ? 'exact, model tokenizer' : 'estimated ≈ chars/4'}):`];
132
- for (const c of cats)
133
- lines.push(` ${paint(BLOCK, c.color)} ${c.label.padEnd(13)} ${mark(c.n)}`);
134
- const pctNum = window ? Math.round((total / window) * 100) : 0;
135
- const pct = window ? ` / ${window} ${paint(`(${pctNum}%)`, fillColor(pctNum))}` : '';
136
- lines.push(` ${' '.repeat(15)}── ${mark(total)}${pct}`);
140
+ const buffer = window > 0 ? Math.min(Math.round(window * 0.2), Math.max(0, window - total)) : 0;
141
+ const free = Math.max(0, window - total - buffer);
142
+ const mark = (n) => (exact ? fmtTok(n) : `~${fmtTok(n)}`);
143
+ const model = ctx.session?.model ?? '';
144
+ // Right-hand info column model, totals, then per-category usage (Claude Code's /context).
145
+ const info = [];
146
+ if (model) {
147
+ info.push(model.split('/').pop() ?? model);
148
+ info.push(paint(model, DIM));
149
+ }
150
+ if (window > 0)
151
+ info.push(paint(`${mark(total)}/${fmtTok(window)} tokens (${pctStr(total, window)})`, DIM));
152
+ info.push('');
153
+ info.push(paint('Estimated usage by category', ITALIC_DIM));
154
+ for (const c of cats) {
155
+ info.push(`${paint(USED, c.color)} ${c.label}: ${paint(`${mark(c.n)} tokens (${pctStr(c.n, window)})`, DIM)}`);
156
+ }
157
+ if (window > 0) {
158
+ info.push(`${paint(USED, BUFFER_COLOR)} Compaction buffer: ${paint(`${fmtTok(buffer)} tokens (${pctStr(buffer, window)})`, DIM)}`);
159
+ info.push(`${paint(FREEG, DIM)} Free space: ${paint(`${fmtTok(free)} (${pctStr(free, window)})`, DIM)}`);
160
+ }
161
+ const lines = [`${BOLD}Context Usage${RESET}`];
137
162
  if (window > 0) {
163
+ // Grid: category cells, then free, with the compaction buffer at the very end.
138
164
  const cellTokens = Math.max(1, window / GRID_CELLS);
165
+ const ratio = (n) => (n <= 0 ? 0 : Math.max(1, Math.round(n / cellTokens)));
139
166
  const cells = [];
140
- for (const c of cats) {
141
- for (let i = 0; i < Math.round(c.n / cellTokens) && cells.length < GRID_CELLS; i++)
142
- cells.push(paint(BLOCK, c.color));
143
- }
144
- while (cells.length < GRID_CELLS)
145
- cells.push(paint(FREE, DIM));
167
+ for (const c of cats)
168
+ for (let i = 0; i < ratio(c.n) && cells.length < GRID_CELLS; i++)
169
+ cells.push(paint(USED, c.color));
170
+ const bufCells = Math.min(ratio(buffer), Math.max(0, GRID_CELLS - cells.length));
171
+ const freeCells = Math.max(0, GRID_CELLS - cells.length - bufCells);
172
+ for (let i = 0; i < freeCells; i++)
173
+ cells.push(paint(FREEG, DIM));
174
+ for (let i = 0; i < bufCells; i++)
175
+ cells.push(paint(USED, BUFFER_COLOR));
146
176
  cells.length = GRID_CELLS;
147
- lines.push('');
148
- for (let r = 0; r < GRID_ROWS; r++)
149
- lines.push(` ${cells.slice(r * GRID_COLS, (r + 1) * GRID_COLS).join('')}`);
177
+ // Side-by-side: 20×10 grid (space-separated glyphs) on the left, info column on the right.
178
+ for (let r = 0; r < Math.max(GRID_ROWS, info.length); r++) {
179
+ const gridRow = r < GRID_ROWS ? cells.slice(r * GRID_COLS, (r + 1) * GRID_COLS).join(' ') : ' '.repeat(GRID_WIDTH);
180
+ lines.push(`${gridRow} ${info[r] ?? ''}`.trimEnd());
181
+ }
182
+ }
183
+ else {
184
+ for (const l of info)
185
+ lines.push(l.trimEnd());
150
186
  }
151
187
  return { ok: true, output: lines.join('\n') };
152
188
  },
@@ -143,6 +143,7 @@ export const createAgentSession = (config) => {
143
143
  };
144
144
  return {
145
145
  id,
146
+ model: config.model,
146
147
  tools,
147
148
  get messages() {
148
149
  return messages;
@@ -12,6 +12,7 @@ const onFirstMessage = (session, fire) => {
12
12
  get tools() {
13
13
  return session.tools;
14
14
  },
15
+ model: session.model,
15
16
  assembleRequest: session.assembleRequest?.bind(session),
16
17
  countTokens: session.countTokens?.bind(session),
17
18
  contextWindow: session.contextWindow?.bind(session),
@@ -10,6 +10,7 @@ export const persistTo = (store, session, persisted = 0) => {
10
10
  get tools() {
11
11
  return session.tools;
12
12
  },
13
+ model: session.model,
13
14
  assembleRequest: session.assembleRequest?.bind(session),
14
15
  countTokens: session.countTokens?.bind(session),
15
16
  contextWindow: session.contextWindow?.bind(session),
@@ -19,6 +19,7 @@ export interface AssembledRequest {
19
19
  }
20
20
  export interface AgentSession {
21
21
  readonly id: string;
22
+ readonly model?: string;
22
23
  readonly messages: readonly Message[];
23
24
  readonly tools: readonly Tool[];
24
25
  /** Assemble the request from the CURRENT in-memory session — what the next turn would send. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-harness",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "description": "Agent harness: createHarness wires mu-core into a host — XDG paths, model registry, plugins, disk-loaded agents & skills, sub-agents, sessions (JSONL + SQLite catalog), slash commands, permission/approval hooks, an optional scheduler, and a composable TUI chat app",
5
5
  "license": "MIT",
6
6
  "main": "./script/index.js",
@@ -23,8 +23,8 @@
23
23
  "@swc/wasm-typescript": "^1.15.0",
24
24
  "cli-highlight": "^2.1.11",
25
25
  "croner": "^9.0.0",
26
- "mu-core": "^0.28.0",
27
- "mu-tui": "^0.28.0",
26
+ "mu-core": "^0.30.0",
27
+ "mu-tui": "^0.30.0",
28
28
  "ws": "^8.18.0"
29
29
  },
30
30
  "_generatedBy": "dnt@dev",
@@ -48,17 +48,27 @@ const createQuitCommand = (onQuit) => ({
48
48
  });
49
49
  exports.createQuitCommand = createQuitCommand;
50
50
  const estTokens = (chars) => Math.max(1, Math.round(chars / 4));
51
- const GRID_COLS = 24;
52
- const GRID_ROWS = 8;
51
+ const GRID_COLS = 20;
52
+ const GRID_ROWS = 10;
53
53
  const GRID_CELLS = GRID_COLS * GRID_ROWS;
54
- const BLOCK = '█';
55
- const FREE = '·';
56
- // ANSI SGR colours — mu's TUI text utils are ANSI-aware so these render in the terminal;
54
+ const GRID_WIDTH = GRID_COLS * 2 - 1; // glyphs joined by single spaces
55
+ const USED = ''; // a filled context cell (Claude Code's /context glyph)
56
+ const FREEG = '⛶'; // a free cell
57
+ const BUFFER_COLOR = '\x1b[93m'; // bright yellow — the compaction reserve
58
+ // ANSI SGR codes — mu's TUI text utils are ANSI-aware so these render in the terminal;
57
59
  // the companion strips them (plain text), keeping the labelled breakdown readable.
58
60
  const RESET = '\x1b[0m';
59
61
  const DIM = '\x1b[2m';
62
+ const BOLD = '\x1b[1m';
63
+ const ITALIC_DIM = '\x1b[2;3m';
60
64
  const paint = (s, color) => `${color}${s}${RESET}`;
61
- const fillColor = (pct) => (pct >= 80 ? '\x1b[31m' : pct >= 50 ? '\x1b[33m' : '\x1b[32m');
65
+ const fmtTok = (n) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`);
66
+ const pctStr = (n, w) => {
67
+ if (w <= 0)
68
+ return '';
69
+ const p = (n / w) * 100;
70
+ return p > 0 && p < 0.1 ? '<0.1%' : `${p.toFixed(1)}%`;
71
+ };
62
72
  /** Extract a `<tag>…</tag>` block (with tags), or '' when absent. */
63
73
  function tagBlock(s, tag) {
64
74
  const i = s.indexOf(`<${tag}>`);
@@ -118,42 +128,68 @@ const createContextCommand = () => ({
118
128
  }
119
129
  return { n: estTokens(text.length), exact: false };
120
130
  };
121
- // label, text, ANSI colour — one per category, in render order.
131
+ // label, text, ANSI colour — one per category, in render order (descriptive labels à la Claude Code).
122
132
  const SPEC = [
123
- ['system', sys.agent, '\x1b[36m'], // cyan — the agent prompt
124
- ['context', sys.env, '\x1b[33m'], // yellow — the <env> block
125
- ['instructions', sys.instructions, '\x1b[34m'], // blue — AGENTS.md / CLAUDE.md
126
- ['memory', sys.memory, '\x1b[35m'], // magenta — MEMORY.md
127
- ['tools', `${toolSchemas}\n${sys.toolPrompts}`, '\x1b[31m'], // red — schemas + tool prompts
128
- ['you', byRole('user'), '\x1b[32m'], // green — your messages
129
- ['agent', byRole('assistant'), '\x1b[94m'], // bright blue — assistant replies
130
- ['tool-out', byRole('tool'), '\x1b[90m'], // grey — tool results
133
+ ['System prompt', sys.agent, '\x1b[36m'], // cyan — the agent prompt
134
+ ['Environment', sys.env, '\x1b[33m'], // yellow — the <env> block
135
+ ['Instructions', sys.instructions, '\x1b[34m'], // blue — AGENTS.md / CLAUDE.md
136
+ ['Memory', sys.memory, '\x1b[35m'], // magenta — MEMORY.md
137
+ ['Tools', `${toolSchemas}\n${sys.toolPrompts}`, '\x1b[31m'], // red — schemas + tool prompts
138
+ ['You', byRole('user'), '\x1b[32m'], // green — your messages
139
+ ['Agent', byRole('assistant'), '\x1b[94m'], // bright blue — assistant replies
140
+ ['Tool results', byRole('tool'), '\x1b[90m'], // grey — tool results
131
141
  ];
132
142
  const measured = await Promise.all(SPEC.map(([, text]) => measure(text)));
133
143
  const cats = SPEC.map(([label, , color], i) => ({ label, n: measured[i].n, color })).filter((c) => c.n > 0);
134
144
  const exact = measured.every((m) => m.exact);
135
145
  const total = cats.reduce((s, c) => s + c.n, 0);
136
146
  const window = (await ctx.session?.contextWindow?.()) ?? 0;
137
- const mark = (n) => (exact ? `${n}` : `~${n}`);
138
- const lines = [`context — tokens (${exact ? 'exact, model tokenizer' : 'estimated ≈ chars/4'}):`];
139
- for (const c of cats)
140
- lines.push(` ${paint(BLOCK, c.color)} ${c.label.padEnd(13)} ${mark(c.n)}`);
141
- const pctNum = window ? Math.round((total / window) * 100) : 0;
142
- const pct = window ? ` / ${window} ${paint(`(${pctNum}%)`, fillColor(pctNum))}` : '';
143
- lines.push(` ${' '.repeat(15)}── ${mark(total)}${pct}`);
147
+ const buffer = window > 0 ? Math.min(Math.round(window * 0.2), Math.max(0, window - total)) : 0;
148
+ const free = Math.max(0, window - total - buffer);
149
+ const mark = (n) => (exact ? fmtTok(n) : `~${fmtTok(n)}`);
150
+ const model = ctx.session?.model ?? '';
151
+ // Right-hand info column model, totals, then per-category usage (Claude Code's /context).
152
+ const info = [];
153
+ if (model) {
154
+ info.push(model.split('/').pop() ?? model);
155
+ info.push(paint(model, DIM));
156
+ }
157
+ if (window > 0)
158
+ info.push(paint(`${mark(total)}/${fmtTok(window)} tokens (${pctStr(total, window)})`, DIM));
159
+ info.push('');
160
+ info.push(paint('Estimated usage by category', ITALIC_DIM));
161
+ for (const c of cats) {
162
+ info.push(`${paint(USED, c.color)} ${c.label}: ${paint(`${mark(c.n)} tokens (${pctStr(c.n, window)})`, DIM)}`);
163
+ }
164
+ if (window > 0) {
165
+ info.push(`${paint(USED, BUFFER_COLOR)} Compaction buffer: ${paint(`${fmtTok(buffer)} tokens (${pctStr(buffer, window)})`, DIM)}`);
166
+ info.push(`${paint(FREEG, DIM)} Free space: ${paint(`${fmtTok(free)} (${pctStr(free, window)})`, DIM)}`);
167
+ }
168
+ const lines = [`${BOLD}Context Usage${RESET}`];
144
169
  if (window > 0) {
170
+ // Grid: category cells, then free, with the compaction buffer at the very end.
145
171
  const cellTokens = Math.max(1, window / GRID_CELLS);
172
+ const ratio = (n) => (n <= 0 ? 0 : Math.max(1, Math.round(n / cellTokens)));
146
173
  const cells = [];
147
- for (const c of cats) {
148
- for (let i = 0; i < Math.round(c.n / cellTokens) && cells.length < GRID_CELLS; i++)
149
- cells.push(paint(BLOCK, c.color));
150
- }
151
- while (cells.length < GRID_CELLS)
152
- cells.push(paint(FREE, DIM));
174
+ for (const c of cats)
175
+ for (let i = 0; i < ratio(c.n) && cells.length < GRID_CELLS; i++)
176
+ cells.push(paint(USED, c.color));
177
+ const bufCells = Math.min(ratio(buffer), Math.max(0, GRID_CELLS - cells.length));
178
+ const freeCells = Math.max(0, GRID_CELLS - cells.length - bufCells);
179
+ for (let i = 0; i < freeCells; i++)
180
+ cells.push(paint(FREEG, DIM));
181
+ for (let i = 0; i < bufCells; i++)
182
+ cells.push(paint(USED, BUFFER_COLOR));
153
183
  cells.length = GRID_CELLS;
154
- lines.push('');
155
- for (let r = 0; r < GRID_ROWS; r++)
156
- lines.push(` ${cells.slice(r * GRID_COLS, (r + 1) * GRID_COLS).join('')}`);
184
+ // Side-by-side: 20×10 grid (space-separated glyphs) on the left, info column on the right.
185
+ for (let r = 0; r < Math.max(GRID_ROWS, info.length); r++) {
186
+ const gridRow = r < GRID_ROWS ? cells.slice(r * GRID_COLS, (r + 1) * GRID_COLS).join(' ') : ' '.repeat(GRID_WIDTH);
187
+ lines.push(`${gridRow} ${info[r] ?? ''}`.trimEnd());
188
+ }
189
+ }
190
+ else {
191
+ for (const l of info)
192
+ lines.push(l.trimEnd());
157
193
  }
158
194
  return { ok: true, output: lines.join('\n') };
159
195
  },
@@ -146,6 +146,7 @@ const createAgentSession = (config) => {
146
146
  };
147
147
  return {
148
148
  id,
149
+ model: config.model,
149
150
  tools,
150
151
  get messages() {
151
152
  return messages;
@@ -15,6 +15,7 @@ const onFirstMessage = (session, fire) => {
15
15
  get tools() {
16
16
  return session.tools;
17
17
  },
18
+ model: session.model,
18
19
  assembleRequest: session.assembleRequest?.bind(session),
19
20
  countTokens: session.countTokens?.bind(session),
20
21
  contextWindow: session.contextWindow?.bind(session),
@@ -13,6 +13,7 @@ const persistTo = (store, session, persisted = 0) => {
13
13
  get tools() {
14
14
  return session.tools;
15
15
  },
16
+ model: session.model,
16
17
  assembleRequest: session.assembleRequest?.bind(session),
17
18
  countTokens: session.countTokens?.bind(session),
18
19
  contextWindow: session.contextWindow?.bind(session),
@@ -19,6 +19,7 @@ export interface AssembledRequest {
19
19
  }
20
20
  export interface AgentSession {
21
21
  readonly id: string;
22
+ readonly model?: string;
22
23
  readonly messages: readonly Message[];
23
24
  readonly tools: readonly Tool[];
24
25
  /** Assemble the request from the CURRENT in-memory session — what the next turn would send. */