mu-harness 0.29.0 → 0.31.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.
@@ -44,13 +44,16 @@ const estTokens = (chars) => Math.max(1, Math.round(chars / 4));
44
44
  const GRID_COLS = 20;
45
45
  const GRID_ROWS = 10;
46
46
  const GRID_CELLS = GRID_COLS * GRID_ROWS;
47
- const BLOCK = '█';
48
- const FREE = '·';
49
- const BUFFER_COLOR = '\x1b[93m'; // bright yellow — the compaction reserve
50
- // ANSI SGR colours — mu's TUI text utils are ANSI-aware so these render in the terminal;
51
- // the companion strips them (plain text), keeping the labelled breakdown readable.
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
+ // Truecolor SGR — distinct, readable hues (mu's TUI utils + the companion both strip/parse
51
+ // these correctly). Secondary text uses a single grey so only the category glyphs carry colour.
52
52
  const RESET = '\x1b[0m';
53
- const DIM = '\x1b[2m';
53
+ const GREY = '\x1b[38;2;153;153;153m';
54
+ const BOLD = '\x1b[1m';
55
+ const ITALIC = '\x1b[3m';
56
+ const BUFFER_COLOR = '\x1b[38;2;255;193;7m'; // amber — the compaction reserve
54
57
  const paint = (s, color) => `${color}${s}${RESET}`;
55
58
  const fmtTok = (n) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`);
56
59
  const pctStr = (n, w) => {
@@ -59,7 +62,6 @@ const pctStr = (n, w) => {
59
62
  const p = (n / w) * 100;
60
63
  return p > 0 && p < 0.1 ? '<0.1%' : `${p.toFixed(1)}%`;
61
64
  };
62
- const fillColor = (pct) => (pct >= 80 ? '\x1b[31m' : pct >= 50 ? '\x1b[33m' : '\x1b[32m');
63
65
  /** Extract a `<tag>…</tag>` block (with tags), or '' when absent. */
64
66
  function tagBlock(s, tag) {
65
67
  const i = s.indexOf(`<${tag}>`);
@@ -119,16 +121,16 @@ export const createContextCommand = () => ({
119
121
  }
120
122
  return { n: estTokens(text.length), exact: false };
121
123
  };
122
- // label, text, ANSI colour one per category, in render order.
124
+ // label, text, truecolordistinct hues across the wheel, in render order.
123
125
  const SPEC = [
124
- ['system', sys.agent, '\x1b[36m'], // cyan — the agent prompt
125
- ['context', sys.env, '\x1b[33m'], // yellow — the <env> block
126
- ['instructions', sys.instructions, '\x1b[34m'], // blue — AGENTS.md / CLAUDE.md
127
- ['memory', sys.memory, '\x1b[35m'], // magenta — MEMORY.md
128
- ['tools', `${toolSchemas}\n${sys.toolPrompts}`, '\x1b[31m'], // red — schemas + tool prompts
129
- ['you', byRole('user'), '\x1b[32m'], // green — your messages
130
- ['agent', byRole('assistant'), '\x1b[94m'], // bright blue — assistant replies
131
- ['tool-out', byRole('tool'), '\x1b[90m'], // grey — tool results
126
+ ['System prompt', sys.agent, '\x1b[38;2;239;108;82m'], // coral — the agent prompt
127
+ ['Environment', sys.env, '\x1b[38;2;129;199;132m'], // green — the <env> block
128
+ ['Instructions', sys.instructions, '\x1b[38;2;100;181;246m'], // blue — AGENTS.md / CLAUDE.md
129
+ ['Memory', sys.memory, '\x1b[38;2;186;104;200m'], // purple — MEMORY.md
130
+ ['Tools', `${toolSchemas}\n${sys.toolPrompts}`, '\x1b[38;2;77;182;172m'], // teal — schemas + tool prompts
131
+ ['You', byRole('user'), '\x1b[38;2;121;134;203m'], // indigo — your messages
132
+ ['Agent', byRole('assistant'), '\x1b[38;2;240;98;146m'], // pink — assistant replies
133
+ ['Tool results', byRole('tool'), '\x1b[38;2;0;188;212m'], // cyan — tool results (distinct from amber buffer)
132
134
  ];
133
135
  const measured = await Promise.all(SPEC.map(([, text]) => measure(text)));
134
136
  const cats = SPEC.map(([label, , color], i) => ({ label, n: measured[i].n, color })).filter((c) => c.n > 0);
@@ -138,43 +140,49 @@ export const createContextCommand = () => ({
138
140
  const buffer = window > 0 ? Math.min(Math.round(window * 0.2), Math.max(0, window - total)) : 0;
139
141
  const free = Math.max(0, window - total - buffer);
140
142
  const mark = (n) => (exact ? fmtTok(n) : `~${fmtTok(n)}`);
141
- const pctNum = window ? Math.round((total / window) * 100) : 0;
142
- // Legend rows (categories, then buffer + free), Claude Code / oh-my-pi style.
143
- const rows = cats.map((c) => ({ marker: paint(BLOCK, c.color), label: c.label, n: c.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, GREY));
149
+ }
150
+ if (window > 0)
151
+ info.push(paint(`${mark(total)}/${fmtTok(window)} tokens (${pctStr(total, window)})`, GREY));
152
+ info.push('');
153
+ info.push(`${ITALIC}${GREY}Estimated usage by category${RESET}`);
154
+ for (const c of cats) {
155
+ info.push(`${paint(USED, c.color)} ${c.label}: ${paint(`${mark(c.n)} tokens (${pctStr(c.n, window)})`, GREY)}`);
156
+ }
144
157
  if (window > 0) {
145
- rows.push({ marker: paint(BLOCK, BUFFER_COLOR), label: 'buffer', n: buffer });
146
- rows.push({ marker: paint(FREE, DIM), label: 'free', n: free });
158
+ info.push(`${paint(USED, BUFFER_COLOR)} Compaction buffer: ${paint(`${fmtTok(buffer)} tokens (${pctStr(buffer, window)})`, GREY)}`);
159
+ info.push(`${paint(FREEG, GREY)} Free space: ${paint(`${fmtTok(free)} (${pctStr(free, window)})`, GREY)}`);
147
160
  }
148
- const legend = rows.map((r) => `${r.marker} ${r.label.padEnd(13)} ${mark(r.n).padStart(7)} ${pctStr(r.n, window).padStart(6)}`);
149
- const header = window
150
- ? `context · ${mark(total)} / ${fmtTok(window)} ${paint(`(${pctStr(total, window)})`, fillColor(pctNum))}`
151
- : `context · ${mark(total)} tokens (no model context window)`;
152
- const lines = [header, ''];
161
+ const lines = [`${BOLD}Context Usage${RESET}`];
153
162
  if (window > 0) {
154
- // Build the grid: category cells, then free, with the compaction buffer at the very end.
163
+ // Grid: category cells, then free, with the compaction buffer at the very end.
155
164
  const cellTokens = Math.max(1, window / GRID_CELLS);
156
165
  const ratio = (n) => (n <= 0 ? 0 : Math.max(1, Math.round(n / cellTokens)));
157
166
  const cells = [];
158
167
  for (const c of cats)
159
168
  for (let i = 0; i < ratio(c.n) && cells.length < GRID_CELLS; i++)
160
- cells.push(paint(BLOCK, c.color));
169
+ cells.push(paint(USED, c.color));
161
170
  const bufCells = Math.min(ratio(buffer), Math.max(0, GRID_CELLS - cells.length));
162
171
  const freeCells = Math.max(0, GRID_CELLS - cells.length - bufCells);
163
172
  for (let i = 0; i < freeCells; i++)
164
- cells.push(paint(FREE, DIM));
173
+ cells.push(paint(FREEG, GREY));
165
174
  for (let i = 0; i < bufCells; i++)
166
- cells.push(paint(BLOCK, BUFFER_COLOR));
175
+ cells.push(paint(USED, BUFFER_COLOR));
167
176
  cells.length = GRID_CELLS;
168
- // Side-by-side: 20×10 grid on the left, the legend on the right.
169
- const rowsCount = Math.max(GRID_ROWS, legend.length);
170
- for (let r = 0; r < rowsCount; r++) {
171
- const gridRow = r < GRID_ROWS ? cells.slice(r * GRID_COLS, (r + 1) * GRID_COLS).join('') : ' '.repeat(GRID_COLS);
172
- lines.push(` ${gridRow} ${legend[r] ?? ''}`.trimEnd());
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());
173
181
  }
174
182
  }
175
183
  else {
176
- for (const l of legend)
177
- lines.push(` ${l.trimEnd()}`);
184
+ for (const l of info)
185
+ lines.push(l.trimEnd());
178
186
  }
179
187
  return { ok: true, output: lines.join('\n') };
180
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.29.0",
3
+ "version": "0.31.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.29.0",
27
- "mu-tui": "^0.29.0",
26
+ "mu-core": "^0.31.0",
27
+ "mu-tui": "^0.31.0",
28
28
  "ws": "^8.18.0"
29
29
  },
30
30
  "_generatedBy": "dnt@dev",
@@ -51,13 +51,16 @@ const estTokens = (chars) => Math.max(1, Math.round(chars / 4));
51
51
  const GRID_COLS = 20;
52
52
  const GRID_ROWS = 10;
53
53
  const GRID_CELLS = GRID_COLS * GRID_ROWS;
54
- const BLOCK = '█';
55
- const FREE = '·';
56
- const BUFFER_COLOR = '\x1b[93m'; // bright yellow — the compaction reserve
57
- // ANSI SGR colours — mu's TUI text utils are ANSI-aware so these render in the terminal;
58
- // the companion strips them (plain text), keeping the labelled breakdown readable.
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
+ // Truecolor SGR — distinct, readable hues (mu's TUI utils + the companion both strip/parse
58
+ // these correctly). Secondary text uses a single grey so only the category glyphs carry colour.
59
59
  const RESET = '\x1b[0m';
60
- const DIM = '\x1b[2m';
60
+ const GREY = '\x1b[38;2;153;153;153m';
61
+ const BOLD = '\x1b[1m';
62
+ const ITALIC = '\x1b[3m';
63
+ const BUFFER_COLOR = '\x1b[38;2;255;193;7m'; // amber — the compaction reserve
61
64
  const paint = (s, color) => `${color}${s}${RESET}`;
62
65
  const fmtTok = (n) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`);
63
66
  const pctStr = (n, w) => {
@@ -66,7 +69,6 @@ const pctStr = (n, w) => {
66
69
  const p = (n / w) * 100;
67
70
  return p > 0 && p < 0.1 ? '<0.1%' : `${p.toFixed(1)}%`;
68
71
  };
69
- const fillColor = (pct) => (pct >= 80 ? '\x1b[31m' : pct >= 50 ? '\x1b[33m' : '\x1b[32m');
70
72
  /** Extract a `<tag>…</tag>` block (with tags), or '' when absent. */
71
73
  function tagBlock(s, tag) {
72
74
  const i = s.indexOf(`<${tag}>`);
@@ -126,16 +128,16 @@ const createContextCommand = () => ({
126
128
  }
127
129
  return { n: estTokens(text.length), exact: false };
128
130
  };
129
- // label, text, ANSI colour one per category, in render order.
131
+ // label, text, truecolordistinct hues across the wheel, in render order.
130
132
  const SPEC = [
131
- ['system', sys.agent, '\x1b[36m'], // cyan — the agent prompt
132
- ['context', sys.env, '\x1b[33m'], // yellow — the <env> block
133
- ['instructions', sys.instructions, '\x1b[34m'], // blue — AGENTS.md / CLAUDE.md
134
- ['memory', sys.memory, '\x1b[35m'], // magenta — MEMORY.md
135
- ['tools', `${toolSchemas}\n${sys.toolPrompts}`, '\x1b[31m'], // red — schemas + tool prompts
136
- ['you', byRole('user'), '\x1b[32m'], // green — your messages
137
- ['agent', byRole('assistant'), '\x1b[94m'], // bright blue — assistant replies
138
- ['tool-out', byRole('tool'), '\x1b[90m'], // grey — tool results
133
+ ['System prompt', sys.agent, '\x1b[38;2;239;108;82m'], // coral — the agent prompt
134
+ ['Environment', sys.env, '\x1b[38;2;129;199;132m'], // green — the <env> block
135
+ ['Instructions', sys.instructions, '\x1b[38;2;100;181;246m'], // blue — AGENTS.md / CLAUDE.md
136
+ ['Memory', sys.memory, '\x1b[38;2;186;104;200m'], // purple — MEMORY.md
137
+ ['Tools', `${toolSchemas}\n${sys.toolPrompts}`, '\x1b[38;2;77;182;172m'], // teal — schemas + tool prompts
138
+ ['You', byRole('user'), '\x1b[38;2;121;134;203m'], // indigo — your messages
139
+ ['Agent', byRole('assistant'), '\x1b[38;2;240;98;146m'], // pink — assistant replies
140
+ ['Tool results', byRole('tool'), '\x1b[38;2;0;188;212m'], // cyan — tool results (distinct from amber buffer)
139
141
  ];
140
142
  const measured = await Promise.all(SPEC.map(([, text]) => measure(text)));
141
143
  const cats = SPEC.map(([label, , color], i) => ({ label, n: measured[i].n, color })).filter((c) => c.n > 0);
@@ -145,43 +147,49 @@ const createContextCommand = () => ({
145
147
  const buffer = window > 0 ? Math.min(Math.round(window * 0.2), Math.max(0, window - total)) : 0;
146
148
  const free = Math.max(0, window - total - buffer);
147
149
  const mark = (n) => (exact ? fmtTok(n) : `~${fmtTok(n)}`);
148
- const pctNum = window ? Math.round((total / window) * 100) : 0;
149
- // Legend rows (categories, then buffer + free), Claude Code / oh-my-pi style.
150
- const rows = cats.map((c) => ({ marker: paint(BLOCK, c.color), label: c.label, n: c.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, GREY));
156
+ }
157
+ if (window > 0)
158
+ info.push(paint(`${mark(total)}/${fmtTok(window)} tokens (${pctStr(total, window)})`, GREY));
159
+ info.push('');
160
+ info.push(`${ITALIC}${GREY}Estimated usage by category${RESET}`);
161
+ for (const c of cats) {
162
+ info.push(`${paint(USED, c.color)} ${c.label}: ${paint(`${mark(c.n)} tokens (${pctStr(c.n, window)})`, GREY)}`);
163
+ }
151
164
  if (window > 0) {
152
- rows.push({ marker: paint(BLOCK, BUFFER_COLOR), label: 'buffer', n: buffer });
153
- rows.push({ marker: paint(FREE, DIM), label: 'free', n: free });
165
+ info.push(`${paint(USED, BUFFER_COLOR)} Compaction buffer: ${paint(`${fmtTok(buffer)} tokens (${pctStr(buffer, window)})`, GREY)}`);
166
+ info.push(`${paint(FREEG, GREY)} Free space: ${paint(`${fmtTok(free)} (${pctStr(free, window)})`, GREY)}`);
154
167
  }
155
- const legend = rows.map((r) => `${r.marker} ${r.label.padEnd(13)} ${mark(r.n).padStart(7)} ${pctStr(r.n, window).padStart(6)}`);
156
- const header = window
157
- ? `context · ${mark(total)} / ${fmtTok(window)} ${paint(`(${pctStr(total, window)})`, fillColor(pctNum))}`
158
- : `context · ${mark(total)} tokens (no model context window)`;
159
- const lines = [header, ''];
168
+ const lines = [`${BOLD}Context Usage${RESET}`];
160
169
  if (window > 0) {
161
- // Build the grid: category cells, then free, with the compaction buffer at the very end.
170
+ // Grid: category cells, then free, with the compaction buffer at the very end.
162
171
  const cellTokens = Math.max(1, window / GRID_CELLS);
163
172
  const ratio = (n) => (n <= 0 ? 0 : Math.max(1, Math.round(n / cellTokens)));
164
173
  const cells = [];
165
174
  for (const c of cats)
166
175
  for (let i = 0; i < ratio(c.n) && cells.length < GRID_CELLS; i++)
167
- cells.push(paint(BLOCK, c.color));
176
+ cells.push(paint(USED, c.color));
168
177
  const bufCells = Math.min(ratio(buffer), Math.max(0, GRID_CELLS - cells.length));
169
178
  const freeCells = Math.max(0, GRID_CELLS - cells.length - bufCells);
170
179
  for (let i = 0; i < freeCells; i++)
171
- cells.push(paint(FREE, DIM));
180
+ cells.push(paint(FREEG, GREY));
172
181
  for (let i = 0; i < bufCells; i++)
173
- cells.push(paint(BLOCK, BUFFER_COLOR));
182
+ cells.push(paint(USED, BUFFER_COLOR));
174
183
  cells.length = GRID_CELLS;
175
- // Side-by-side: 20×10 grid on the left, the legend on the right.
176
- const rowsCount = Math.max(GRID_ROWS, legend.length);
177
- for (let r = 0; r < rowsCount; r++) {
178
- const gridRow = r < GRID_ROWS ? cells.slice(r * GRID_COLS, (r + 1) * GRID_COLS).join('') : ' '.repeat(GRID_COLS);
179
- lines.push(` ${gridRow} ${legend[r] ?? ''}`.trimEnd());
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());
180
188
  }
181
189
  }
182
190
  else {
183
- for (const l of legend)
184
- lines.push(` ${l.trimEnd()}`);
191
+ for (const l of info)
192
+ lines.push(l.trimEnd());
185
193
  }
186
194
  return { ok: true, output: lines.join('\n') };
187
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. */