mu-harness 0.16.16 → 0.16.17

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.
@@ -53,6 +53,15 @@ export interface ChatHost {
53
53
  * (a splash). Cleared once the conversation starts.
54
54
  */
55
55
  banner?: string;
56
+ /**
57
+ * Lean input presentation: drop the surface-background input frame, the
58
+ * model/provider/agent footer inside the input, and the context readout on
59
+ * the status bar — leaving a bare prompt + editor. The {@link ChatHost.banner}
60
+ * splash is independent and still shown if set. Hosts that want the full
61
+ * information-rich input (surface frame, model · provider · @agent footer,
62
+ * token/context usage in the status line) leave this unset (the default).
63
+ */
64
+ minimal?: boolean;
56
65
  onExit(code: number): void;
57
66
  }
58
67
  export declare class ChatApp {
@@ -69,6 +78,7 @@ export declare class ChatApp {
69
78
  private session;
70
79
  private readonly features;
71
80
  private readonly banner;
81
+ private readonly minimal;
72
82
  private unsubscribe;
73
83
  private unsubscribeTheme;
74
84
  private unsubscribeSubAgents;
@@ -161,8 +171,8 @@ export declare class ChatApp {
161
171
  private startSpinner;
162
172
  private stopSpinner;
163
173
  private loadModels;
164
- /** Plain model id (+ provider) shown in the status bar. */
165
- private modelText;
174
+ /** Styled model id + provider + active agent, shown in the input container footer. */
175
+ private modelLabel;
166
176
  private updateSpeaker;
167
177
  private promptGlyph;
168
178
  private inputPanel;
@@ -77,13 +77,14 @@ export class ChatApp {
77
77
  session;
78
78
  features;
79
79
  banner;
80
+ minimal;
80
81
  unsubscribe;
81
82
  unsubscribeTheme;
82
83
  unsubscribeSubAgents;
83
84
  runUnsubs = new Set();
84
85
  activeRuns = new Set();
85
86
  mentionAc;
86
- status = { label: 'ready', busy: false, spinnerTick: 0, context: '', model: '' };
87
+ status = { label: 'ready', busy: false, spinnerTick: 0, context: '' };
87
88
  running = false;
88
89
  queue = [];
89
90
  pendingShell = [];
@@ -113,6 +114,7 @@ export class ChatApp {
113
114
  this.session = host.session;
114
115
  this.features = host.features ?? {};
115
116
  this.banner = host.banner;
117
+ this.minimal = host.minimal ?? false;
116
118
  this.transcript.thinkingVisible = host.initialThinking;
117
119
  this.history = host.history?.load() ?? [];
118
120
  this.historyIndex = this.history.length;
@@ -1051,15 +1053,24 @@ export class ChatApp {
1051
1053
  // backend may be unreachable; surfaced on first send
1052
1054
  }
1053
1055
  }
1054
- /** Plain model id (+ provider) shown in the status bar. */
1055
- modelText() {
1056
+ /** Styled model id + provider + active agent, shown in the input container footer. */
1057
+ modelLabel() {
1056
1058
  const ref = this.host.modelRef();
1057
1059
  const slash = ref.indexOf('/');
1058
1060
  const id = slash >= 0 ? ref.slice(slash + 1) : ref;
1059
1061
  const providerName = slash >= 0 ? ref.slice(0, slash) : '';
1060
1062
  const model = this.models.find((m) => m.id === id);
1061
1063
  const provider = model?.ownedBy ?? providerName;
1062
- return provider ? `${id} ${provider}` : id;
1064
+ const theme = this.theme();
1065
+ const bold = styleToAnsi({ fg: theme.colors.text, bold: true });
1066
+ const dim = styleToAnsi({ fg: theme.colors.textMuted });
1067
+ const head = provider ? `${bold}${id}${RESET} ${dim}${provider}${RESET}` : `${bold}${id}${RESET}`;
1068
+ const agent = this.host.agentRef();
1069
+ if (!agent)
1070
+ return head;
1071
+ const hex = asHexColor(this.host.agentColor());
1072
+ const agentSgr = hex ? styleToAnsi({ fg: hex, bold: true }) : dim;
1073
+ return `${head} ${dim}·${RESET} ${agentSgr}@${agent}${RESET}`;
1063
1074
  }
1064
1075
  updateSpeaker() {
1065
1076
  this.transcript.speaker = { name: this.host.agentRef(), color: asHexColor(this.host.agentColor()) };
@@ -1078,19 +1089,27 @@ export class ChatApp {
1078
1089
  }
1079
1090
  inputPanel() {
1080
1091
  const inner = this.approvalView() ?? this.editorInner();
1092
+ if (this.minimal)
1093
+ return box(inner, { padding: 0 });
1081
1094
  return box(inner, { background: this.theme().colors.surface, padding: 1 });
1082
1095
  }
1083
1096
  editorInner() {
1084
1097
  const prompt = this.promptGlyph();
1085
1098
  const editor = this.editor;
1086
1099
  const editorRows = editor.rows();
1100
+ const label = this.minimal ? '' : this.modelLabel();
1087
1101
  return {
1088
1102
  render: (s) => {
1089
1103
  if (s.width <= 0 || s.height <= 0)
1090
1104
  return;
1091
1105
  s.text(0, 0, prompt);
1092
- const rows = Math.min(editorRows, Math.max(1, s.height - 1));
1106
+ const reserve = label ? 2 : 1;
1107
+ const rows = Math.min(editorRows, Math.max(1, s.height - reserve));
1093
1108
  s.child(editor, { x: PROMPT_WIDTH, y: 0, width: Math.max(1, s.width - PROMPT_WIDTH), height: rows });
1109
+ if (label) {
1110
+ const labelRow = rows + 1;
1111
+ s.text(0, labelRow, visibleWidth(label) > s.width ? truncateToWidth(label, s.width) : label);
1112
+ }
1094
1113
  },
1095
1114
  };
1096
1115
  }
@@ -1150,7 +1169,7 @@ export class ChatApp {
1150
1169
  return children;
1151
1170
  }
1152
1171
  statusBar() {
1153
- this.status.model = this.modelText();
1172
+ this.status.minimal = this.minimal;
1154
1173
  return statusComponent(this.status, this.theme());
1155
1174
  }
1156
1175
  dock() {
@@ -1279,19 +1298,17 @@ export class ChatApp {
1279
1298
  const focused = this.focusedSub();
1280
1299
  const showBanner = this.banner !== undefined && this.transcript.entries.length === 0 && !focused;
1281
1300
  const spacer = { render: () => { } };
1282
- const inner = focused
1283
- ? this.subAgentView(focused)
1284
- : showBanner
1285
- // Splash: banner + a centered, width-limited minimal input; status pinned at the bottom.
1286
- ? column([
1287
- flex(spacer),
1288
- this.bannerBlock(),
1289
- this.centered(column(this.inputGroup()), SPLASH_INPUT_WIDTH),
1290
- flex(spacer),
1291
- this.statusBar(),
1292
- ])
1293
- // Conversation: transcript fills, input docked at the bottom.
1294
- : column([flex(this.scroll), this.dock()]);
1301
+ const inner = focused ? this.subAgentView(focused) : showBanner
1302
+ // Splash: banner + a centered, width-limited minimal input; status pinned at the bottom.
1303
+ ? column([
1304
+ flex(spacer),
1305
+ this.bannerBlock(),
1306
+ this.centered(column(this.inputGroup()), SPLASH_INPUT_WIDTH),
1307
+ flex(spacer),
1308
+ this.statusBar(),
1309
+ ])
1310
+ // Conversation: transcript fills, input docked at the bottom.
1311
+ : column([flex(this.scroll), this.dock()]);
1295
1312
  return {
1296
1313
  render: (s) => {
1297
1314
  s.fill({ x: 0, y: 0, width: s.width, height: s.height }, this.theme().colors.background);
@@ -8,8 +8,8 @@ export interface StatusState {
8
8
  busy: boolean;
9
9
  spinnerTick: number;
10
10
  context: string;
11
- /** Active model id (+ provider), shown persistently on the left. */
12
- model: string;
11
+ /** Lean mode: hide the context readout, leaving only a busy spinner. */
12
+ minimal?: boolean;
13
13
  }
14
14
  export declare function statusFromEvent(event: AgentSessionEvent): string | undefined;
15
15
  export declare function statusComponent(state: StatusState, theme: Theme): Component;
@@ -32,10 +32,8 @@ export function statusComponent(state, theme) {
32
32
  return;
33
33
  const muted = styleToAnsi(theme.styles.muted);
34
34
  const spinner = `${muted}${spinnerFrame(state.spinnerTick)}${RESET}`;
35
- const activity = state.busy ? (state.label ? `${spinner} ${muted}${state.label}${RESET}` : spinner) : '';
36
- const model = state.model ? `${muted}${state.model}${RESET}` : '';
37
- const left = [model, activity].filter(Boolean).join(`${muted} · ${RESET}`);
38
- const right = state.context ? `${muted}${state.context}${RESET}` : '';
35
+ const left = state.busy ? (state.label ? `${spinner} ${muted}${state.label}${RESET}` : spinner) : '';
36
+ const right = state.minimal ? '' : (state.context ? `${muted}${state.context}${RESET}` : '');
39
37
  if (!left && !right) {
40
38
  s.text(0, 0, '');
41
39
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-harness",
3
- "version": "0.16.16",
3
+ "version": "0.16.17",
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.16.16",
27
- "mu-tui": "^0.16.16"
26
+ "mu-core": "^0.16.17",
27
+ "mu-tui": "^0.16.17"
28
28
  },
29
29
  "_generatedBy": "dnt@dev",
30
30
  "types": "./esm/index.d.ts"
@@ -53,6 +53,15 @@ export interface ChatHost {
53
53
  * (a splash). Cleared once the conversation starts.
54
54
  */
55
55
  banner?: string;
56
+ /**
57
+ * Lean input presentation: drop the surface-background input frame, the
58
+ * model/provider/agent footer inside the input, and the context readout on
59
+ * the status bar — leaving a bare prompt + editor. The {@link ChatHost.banner}
60
+ * splash is independent and still shown if set. Hosts that want the full
61
+ * information-rich input (surface frame, model · provider · @agent footer,
62
+ * token/context usage in the status line) leave this unset (the default).
63
+ */
64
+ minimal?: boolean;
56
65
  onExit(code: number): void;
57
66
  }
58
67
  export declare class ChatApp {
@@ -69,6 +78,7 @@ export declare class ChatApp {
69
78
  private session;
70
79
  private readonly features;
71
80
  private readonly banner;
81
+ private readonly minimal;
72
82
  private unsubscribe;
73
83
  private unsubscribeTheme;
74
84
  private unsubscribeSubAgents;
@@ -161,8 +171,8 @@ export declare class ChatApp {
161
171
  private startSpinner;
162
172
  private stopSpinner;
163
173
  private loadModels;
164
- /** Plain model id (+ provider) shown in the status bar. */
165
- private modelText;
174
+ /** Styled model id + provider + active agent, shown in the input container footer. */
175
+ private modelLabel;
166
176
  private updateSpeaker;
167
177
  private promptGlyph;
168
178
  private inputPanel;
@@ -80,13 +80,14 @@ class ChatApp {
80
80
  session;
81
81
  features;
82
82
  banner;
83
+ minimal;
83
84
  unsubscribe;
84
85
  unsubscribeTheme;
85
86
  unsubscribeSubAgents;
86
87
  runUnsubs = new Set();
87
88
  activeRuns = new Set();
88
89
  mentionAc;
89
- status = { label: 'ready', busy: false, spinnerTick: 0, context: '', model: '' };
90
+ status = { label: 'ready', busy: false, spinnerTick: 0, context: '' };
90
91
  running = false;
91
92
  queue = [];
92
93
  pendingShell = [];
@@ -116,6 +117,7 @@ class ChatApp {
116
117
  this.session = host.session;
117
118
  this.features = host.features ?? {};
118
119
  this.banner = host.banner;
120
+ this.minimal = host.minimal ?? false;
119
121
  this.transcript.thinkingVisible = host.initialThinking;
120
122
  this.history = host.history?.load() ?? [];
121
123
  this.historyIndex = this.history.length;
@@ -1054,15 +1056,24 @@ class ChatApp {
1054
1056
  // backend may be unreachable; surfaced on first send
1055
1057
  }
1056
1058
  }
1057
- /** Plain model id (+ provider) shown in the status bar. */
1058
- modelText() {
1059
+ /** Styled model id + provider + active agent, shown in the input container footer. */
1060
+ modelLabel() {
1059
1061
  const ref = this.host.modelRef();
1060
1062
  const slash = ref.indexOf('/');
1061
1063
  const id = slash >= 0 ? ref.slice(slash + 1) : ref;
1062
1064
  const providerName = slash >= 0 ? ref.slice(0, slash) : '';
1063
1065
  const model = this.models.find((m) => m.id === id);
1064
1066
  const provider = model?.ownedBy ?? providerName;
1065
- return provider ? `${id} ${provider}` : id;
1067
+ const theme = this.theme();
1068
+ const bold = (0, theme_js_1.styleToAnsi)({ fg: theme.colors.text, bold: true });
1069
+ const dim = (0, theme_js_1.styleToAnsi)({ fg: theme.colors.textMuted });
1070
+ const head = provider ? `${bold}${id}${RESET} ${dim}${provider}${RESET}` : `${bold}${id}${RESET}`;
1071
+ const agent = this.host.agentRef();
1072
+ if (!agent)
1073
+ return head;
1074
+ const hex = (0, theme_js_1.asHexColor)(this.host.agentColor());
1075
+ const agentSgr = hex ? (0, theme_js_1.styleToAnsi)({ fg: hex, bold: true }) : dim;
1076
+ return `${head} ${dim}·${RESET} ${agentSgr}@${agent}${RESET}`;
1066
1077
  }
1067
1078
  updateSpeaker() {
1068
1079
  this.transcript.speaker = { name: this.host.agentRef(), color: (0, theme_js_1.asHexColor)(this.host.agentColor()) };
@@ -1081,19 +1092,27 @@ class ChatApp {
1081
1092
  }
1082
1093
  inputPanel() {
1083
1094
  const inner = this.approvalView() ?? this.editorInner();
1095
+ if (this.minimal)
1096
+ return (0, mu_tui_1.box)(inner, { padding: 0 });
1084
1097
  return (0, mu_tui_1.box)(inner, { background: this.theme().colors.surface, padding: 1 });
1085
1098
  }
1086
1099
  editorInner() {
1087
1100
  const prompt = this.promptGlyph();
1088
1101
  const editor = this.editor;
1089
1102
  const editorRows = editor.rows();
1103
+ const label = this.minimal ? '' : this.modelLabel();
1090
1104
  return {
1091
1105
  render: (s) => {
1092
1106
  if (s.width <= 0 || s.height <= 0)
1093
1107
  return;
1094
1108
  s.text(0, 0, prompt);
1095
- const rows = Math.min(editorRows, Math.max(1, s.height - 1));
1109
+ const reserve = label ? 2 : 1;
1110
+ const rows = Math.min(editorRows, Math.max(1, s.height - reserve));
1096
1111
  s.child(editor, { x: PROMPT_WIDTH, y: 0, width: Math.max(1, s.width - PROMPT_WIDTH), height: rows });
1112
+ if (label) {
1113
+ const labelRow = rows + 1;
1114
+ s.text(0, labelRow, (0, mu_tui_1.visibleWidth)(label) > s.width ? (0, mu_tui_1.truncateToWidth)(label, s.width) : label);
1115
+ }
1097
1116
  },
1098
1117
  };
1099
1118
  }
@@ -1153,7 +1172,7 @@ class ChatApp {
1153
1172
  return children;
1154
1173
  }
1155
1174
  statusBar() {
1156
- this.status.model = this.modelText();
1175
+ this.status.minimal = this.minimal;
1157
1176
  return (0, status_js_1.statusComponent)(this.status, this.theme());
1158
1177
  }
1159
1178
  dock() {
@@ -1282,19 +1301,17 @@ class ChatApp {
1282
1301
  const focused = this.focusedSub();
1283
1302
  const showBanner = this.banner !== undefined && this.transcript.entries.length === 0 && !focused;
1284
1303
  const spacer = { render: () => { } };
1285
- const inner = focused
1286
- ? this.subAgentView(focused)
1287
- : showBanner
1288
- // Splash: banner + a centered, width-limited minimal input; status pinned at the bottom.
1289
- ? (0, mu_tui_1.column)([
1290
- (0, mu_tui_1.flex)(spacer),
1291
- this.bannerBlock(),
1292
- this.centered((0, mu_tui_1.column)(this.inputGroup()), SPLASH_INPUT_WIDTH),
1293
- (0, mu_tui_1.flex)(spacer),
1294
- this.statusBar(),
1295
- ])
1296
- // Conversation: transcript fills, input docked at the bottom.
1297
- : (0, mu_tui_1.column)([(0, mu_tui_1.flex)(this.scroll), this.dock()]);
1304
+ const inner = focused ? this.subAgentView(focused) : showBanner
1305
+ // Splash: banner + a centered, width-limited minimal input; status pinned at the bottom.
1306
+ ? (0, mu_tui_1.column)([
1307
+ (0, mu_tui_1.flex)(spacer),
1308
+ this.bannerBlock(),
1309
+ this.centered((0, mu_tui_1.column)(this.inputGroup()), SPLASH_INPUT_WIDTH),
1310
+ (0, mu_tui_1.flex)(spacer),
1311
+ this.statusBar(),
1312
+ ])
1313
+ // Conversation: transcript fills, input docked at the bottom.
1314
+ : (0, mu_tui_1.column)([(0, mu_tui_1.flex)(this.scroll), this.dock()]);
1298
1315
  return {
1299
1316
  render: (s) => {
1300
1317
  s.fill({ x: 0, y: 0, width: s.width, height: s.height }, this.theme().colors.background);
@@ -8,8 +8,8 @@ export interface StatusState {
8
8
  busy: boolean;
9
9
  spinnerTick: number;
10
10
  context: string;
11
- /** Active model id (+ provider), shown persistently on the left. */
12
- model: string;
11
+ /** Lean mode: hide the context readout, leaving only a busy spinner. */
12
+ minimal?: boolean;
13
13
  }
14
14
  export declare function statusFromEvent(event: AgentSessionEvent): string | undefined;
15
15
  export declare function statusComponent(state: StatusState, theme: Theme): Component;
@@ -39,10 +39,8 @@ function statusComponent(state, theme) {
39
39
  return;
40
40
  const muted = (0, theme_js_1.styleToAnsi)(theme.styles.muted);
41
41
  const spinner = `${muted}${(0, exports.spinnerFrame)(state.spinnerTick)}${RESET}`;
42
- const activity = state.busy ? (state.label ? `${spinner} ${muted}${state.label}${RESET}` : spinner) : '';
43
- const model = state.model ? `${muted}${state.model}${RESET}` : '';
44
- const left = [model, activity].filter(Boolean).join(`${muted} · ${RESET}`);
45
- const right = state.context ? `${muted}${state.context}${RESET}` : '';
42
+ const left = state.busy ? (state.label ? `${spinner} ${muted}${state.label}${RESET}` : spinner) : '';
43
+ const right = state.minimal ? '' : (state.context ? `${muted}${state.context}${RESET}` : '');
46
44
  if (!left && !right) {
47
45
  s.text(0, 0, '');
48
46
  return;