aiden-runtime 4.8.0 → 4.9.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.
Files changed (99) hide show
  1. package/README.md +88 -1
  2. package/dist/cli/v4/aidenCLI.js +35 -4
  3. package/dist/cli/v4/chatSession.js +43 -16
  4. package/dist/cli/v4/commands/daemon.js +47 -2
  5. package/dist/cli/v4/commands/daemonDoctor.js +212 -0
  6. package/dist/cli/v4/commands/daemonStatus.js +1 -1
  7. package/dist/cli/v4/commands/help.js +2 -0
  8. package/dist/cli/v4/commands/hooks.js +428 -0
  9. package/dist/cli/v4/commands/index.js +5 -1
  10. package/dist/cli/v4/commands/mcp.js +89 -1
  11. package/dist/cli/v4/commands/mcpClientInstall.js +359 -0
  12. package/dist/cli/v4/commands/memory.js +702 -0
  13. package/dist/cli/v4/commands/recovery.js +1 -1
  14. package/dist/cli/v4/commands/skin.js +7 -0
  15. package/dist/cli/v4/commands/theme.js +217 -0
  16. package/dist/cli/v4/commands/trigger.js +1 -1
  17. package/dist/cli/v4/commands/update.js +14 -2
  18. package/dist/cli/v4/design/tokens.js +52 -4
  19. package/dist/cli/v4/display.js +102 -46
  20. package/dist/cli/v4/pasteIntercept.js +214 -70
  21. package/dist/cli/v4/replyRenderer.js +145 -5
  22. package/dist/cli/v4/skinEngine.js +67 -0
  23. package/dist/core/v4/aidenAgent.js +45 -2
  24. package/dist/core/v4/daemon/api/runs.js +131 -0
  25. package/dist/core/v4/daemon/bootstrap.js +368 -13
  26. package/dist/core/v4/daemon/db/migrations.js +169 -0
  27. package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +128 -0
  28. package/dist/core/v4/daemon/incarnationStore.js +47 -0
  29. package/dist/core/v4/daemon/runs/attemptStore.js +67 -0
  30. package/dist/core/v4/daemon/runs/reclaim.js +88 -0
  31. package/dist/core/v4/daemon/runs/retryPolicy.js +73 -0
  32. package/dist/core/v4/daemon/runs/runWithRetry.js +80 -0
  33. package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +72 -0
  34. package/dist/core/v4/daemon/spans/spanHelpers.js +272 -0
  35. package/dist/core/v4/daemon/spans/spanStore.js +113 -0
  36. package/dist/core/v4/daemon/triggerBus.js +50 -19
  37. package/dist/core/v4/hooks/auditQuery.js +67 -0
  38. package/dist/core/v4/hooks/dispatcher.js +286 -0
  39. package/dist/core/v4/hooks/index.js +46 -0
  40. package/dist/core/v4/hooks/lifecycle.js +27 -0
  41. package/dist/core/v4/hooks/manifest.js +142 -0
  42. package/dist/core/v4/hooks/registry.js +149 -0
  43. package/dist/core/v4/hooks/runtime/subprocessRunner.js +188 -0
  44. package/dist/core/v4/hooks/toolHookGate.js +76 -0
  45. package/dist/core/v4/hooks/trust.js +14 -0
  46. package/dist/core/v4/identity/contextManager.js +83 -0
  47. package/dist/core/v4/identity/daemonId.js +85 -0
  48. package/dist/core/v4/identity/enforcement.js +103 -0
  49. package/dist/core/v4/identity/executionContext.js +153 -0
  50. package/dist/core/v4/identity/hookExecution.js +62 -0
  51. package/dist/core/v4/identity/httpContext.js +68 -0
  52. package/dist/core/v4/identity/ids.js +185 -0
  53. package/dist/core/v4/identity/index.js +60 -0
  54. package/dist/core/v4/identity/subprocessContext.js +98 -0
  55. package/dist/core/v4/identity/traceparent.js +114 -0
  56. package/dist/core/v4/logger/index.js +3 -1
  57. package/dist/core/v4/logger/logger.js +28 -1
  58. package/dist/core/v4/logger/redact.js +149 -0
  59. package/dist/core/v4/logger/sinks/fileSink.js +13 -0
  60. package/dist/core/v4/logger/sinks/stdSink.js +19 -1
  61. package/dist/core/v4/mcp/install/backup.js +78 -0
  62. package/dist/core/v4/mcp/install/clientPaths.js +90 -0
  63. package/dist/core/v4/mcp/install/clients.js +203 -0
  64. package/dist/core/v4/mcp/install/healthCheck.js +83 -0
  65. package/dist/core/v4/mcp/install/jsoncMerge.js +174 -0
  66. package/dist/core/v4/mcp/install/profiles.js +109 -0
  67. package/dist/core/v4/mcp/install/wslDetect.js +62 -0
  68. package/dist/core/v4/memory/namespaceRegistry.js +117 -0
  69. package/dist/core/v4/memory/projectRoot.js +76 -0
  70. package/dist/core/v4/memory/reviewer/index.js +162 -0
  71. package/dist/core/v4/memory/reviewer/pendingStore.js +136 -0
  72. package/dist/core/v4/memory/reviewer/prompt.js +105 -0
  73. package/dist/core/v4/memory/reviewer/skipRules.js +92 -0
  74. package/dist/core/v4/memoryManager.js +57 -10
  75. package/dist/core/v4/paths.js +2 -0
  76. package/dist/core/v4/promptBuilder.js +6 -0
  77. package/dist/core/v4/subagent/spawnSubAgent.js +20 -7
  78. package/dist/core/v4/theme/bundledThemes.js +106 -0
  79. package/dist/core/v4/theme/themeLoader.js +160 -0
  80. package/dist/core/v4/theme/themeRegistry.js +97 -0
  81. package/dist/core/v4/theme/themeWatcher.js +95 -0
  82. package/dist/core/v4/toolRegistry.js +71 -8
  83. package/dist/core/v4/update/executeInstall.js +10 -6
  84. package/dist/core/v4/update/installMethodDetect.js +7 -0
  85. package/dist/core/version.js +67 -2
  86. package/dist/moat/approvalEngine.js +4 -0
  87. package/dist/moat/memoryGuard.js +8 -1
  88. package/dist/providers/v4/anthropicAdapter.js +10 -4
  89. package/dist/tools/v4/backends/local.js +19 -2
  90. package/dist/tools/v4/sessions/recallSession.js +6 -1
  91. package/package.json +3 -3
  92. package/plugins/aiden-plugin-chatgpt-plus/index.js +10 -1
  93. package/themes/default.yaml +52 -0
  94. package/themes/dracula.yaml +32 -0
  95. package/themes/light.yaml +32 -0
  96. package/themes/monochrome.yaml +31 -0
  97. package/themes/tokyo-night.yaml +32 -0
  98. package/dist/core/pluginSystem.js +0 -121
  99. package/dist/tools/v4/ui/_uiSmokeTool.js +0 -60
@@ -35,7 +35,7 @@ function pad(value, width) {
35
35
  }
36
36
  exports.recovery = {
37
37
  name: 'recovery',
38
- description: 'Inspect recurring failure patterns + recoveries (v4.6 Phase 3b).',
38
+ description: 'Inspect recurring failure patterns + recoveries.',
39
39
  category: 'system',
40
40
  icon: '🩹',
41
41
  handler: async (ctx) => {
@@ -7,6 +7,13 @@ exports.skin = {
7
7
  category: 'system',
8
8
  icon: '🎨',
9
9
  handler: async (ctx) => {
10
+ // v4.9.0 Slice 1a — /skin is now an alias for the new /theme system.
11
+ // The legacy color skins (~/.aiden/skins/*.yaml, RGB-tuple format)
12
+ // continue to work alongside the new theme system; /skin still
13
+ // manages the SkinEngine palette. Theme tokens (panel chrome,
14
+ // status footer glyphs, shimmer) are controlled by /theme.
15
+ ctx.display.warn('/skin is deprecated in v4.9 — use /theme for full visual customisation. ' +
16
+ '/skin continues to work for legacy colour skins.');
10
17
  const engine = ctx.skin;
11
18
  if (!engine) {
12
19
  ctx.display.warn('Skin engine not wired in this context.');
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.theme = void 0;
7
+ /**
8
+ * Copyright (c) 2026 Shiva Deore (Taracod).
9
+ * Licensed under AGPL-3.0. See LICENSE for details.
10
+ *
11
+ * Aiden — local-first agent.
12
+ */
13
+ /**
14
+ * cli/v4/commands/theme.ts — v4.9.0 Slice 1a.
15
+ *
16
+ * `/theme` show current theme name + path + hint
17
+ * `/theme reload` force re-read of ~/.aiden/theme.yaml
18
+ * `/theme reset` delete ~/.aiden/theme.yaml, restore baseline
19
+ * `/theme edit` print ~/.aiden/theme.yaml path
20
+ *
21
+ * `/theme list` and `/theme set <name>` ship in Slice 1b alongside
22
+ * the remaining 4 bundled themes.
23
+ */
24
+ const node_fs_1 = require("node:fs");
25
+ const node_path_1 = __importDefault(require("node:path"));
26
+ const themeLoader_1 = require("../../../core/v4/theme/themeLoader");
27
+ const themeRegistry_1 = require("../../../core/v4/theme/themeRegistry");
28
+ const bundledThemes_1 = require("../../../core/v4/theme/bundledThemes");
29
+ function themeFilePath(ctx) {
30
+ const root = ctx.paths?.root;
31
+ if (!root)
32
+ return null;
33
+ return node_path_1.default.join(root, 'theme.yaml');
34
+ }
35
+ function userThemesDir(ctx) {
36
+ const root = ctx.paths?.root;
37
+ if (!root)
38
+ return null;
39
+ return node_path_1.default.join(root, 'themes');
40
+ }
41
+ function listUserThemes(dir) {
42
+ if (!dir || !(0, node_fs_1.existsSync)(dir))
43
+ return [];
44
+ try {
45
+ return (0, node_fs_1.readdirSync)(dir)
46
+ .filter((f) => f.endsWith('.yaml'))
47
+ .map((f) => {
48
+ const name = f.replace(/\.yaml$/, '');
49
+ let description = '';
50
+ try {
51
+ const yaml = (0, node_fs_1.readFileSync)(node_path_1.default.join(dir, f), 'utf8');
52
+ const { parsed } = (0, themeLoader_1.parseThemeYaml)(yaml);
53
+ description = parsed?.description ?? '';
54
+ }
55
+ catch { /* tolerate unreadable */ }
56
+ return { name, description };
57
+ });
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ }
63
+ exports.theme = {
64
+ name: 'theme',
65
+ description: 'Show, reload, reset, or open the user theme.yaml.',
66
+ category: 'system',
67
+ icon: '🎨',
68
+ handler: async (ctx) => {
69
+ const sub = ctx.rawArgs.trim();
70
+ const themePath = themeFilePath(ctx);
71
+ if (sub === '' || sub === 'show') {
72
+ const current = (0, themeRegistry_1.getCurrentName)();
73
+ const active = (0, themeRegistry_1.getActivePath)();
74
+ ctx.display.info(`Active theme: ${current}`);
75
+ if (active) {
76
+ ctx.display.info(`Source: ${active}`);
77
+ }
78
+ else if (themePath) {
79
+ ctx.display.info(`No user theme.yaml at ${themePath} — using bundled default.`);
80
+ }
81
+ ctx.display.info('Use /theme list, /theme set <name>, /theme reload, /theme reset, or /theme edit.');
82
+ return {};
83
+ }
84
+ if (sub === 'list') {
85
+ const current = (0, themeRegistry_1.getCurrentName)();
86
+ const bundled = (0, bundledThemes_1.listBundled)();
87
+ const userDir = userThemesDir(ctx);
88
+ const userList = listUserThemes(userDir);
89
+ ctx.display.info(`Active theme: ${current}`);
90
+ ctx.display.info('Available themes:');
91
+ const labelW = Math.max(...bundled.map((b) => b.name.length), ...userList.map((u) => u.name.length), 4);
92
+ for (const b of bundled) {
93
+ const marker = b.name === current ? '●' : '○';
94
+ const padded = b.name.padEnd(labelW);
95
+ ctx.display.info(` ${marker} ${padded} (bundled) ${b.description}`);
96
+ }
97
+ for (const u of userList) {
98
+ const marker = u.name === current ? '●' : '○';
99
+ const padded = u.name.padEnd(labelW);
100
+ ctx.display.info(` ${marker} ${padded} (user) ${u.description}`);
101
+ }
102
+ if (bundled.length === 0 && userList.length === 0) {
103
+ ctx.display.warn('No themes found. Reinstall aiden-runtime or use /theme edit to author one.');
104
+ }
105
+ return {};
106
+ }
107
+ if (sub.startsWith('set ') || sub === 'set') {
108
+ const name = sub.replace(/^set\s*/, '').trim();
109
+ if (!name) {
110
+ ctx.display.printError('Usage: /theme set <name>', `Try one of: ${bundledThemes_1.BUNDLED_NAMES.join(', ')} or /theme list for full options.`);
111
+ return {};
112
+ }
113
+ if (!themePath) {
114
+ ctx.display.warn('/theme set needs Aiden user-data paths — try in a real session.');
115
+ return {};
116
+ }
117
+ // Resolution order: bundled → user themes/ dir.
118
+ let yamlText = null;
119
+ let sourceLabel = '';
120
+ if ((0, bundledThemes_1.isBundled)(name)) {
121
+ yamlText = (0, bundledThemes_1.getYaml)(name);
122
+ sourceLabel = 'bundled';
123
+ }
124
+ if (!yamlText) {
125
+ const userDir = userThemesDir(ctx);
126
+ if (userDir) {
127
+ const userFile = node_path_1.default.join(userDir, `${name}.yaml`);
128
+ if ((0, node_fs_1.existsSync)(userFile)) {
129
+ try {
130
+ yamlText = (0, node_fs_1.readFileSync)(userFile, 'utf8');
131
+ sourceLabel = 'user';
132
+ }
133
+ catch { /* fall through to not-found */ }
134
+ }
135
+ }
136
+ }
137
+ if (!yamlText) {
138
+ ctx.display.printError(`Theme not found: "${name}"`, `Available bundled: ${bundledThemes_1.BUNDLED_NAMES.join(', ')}. Or create ~/.aiden/themes/${name}.yaml.`);
139
+ return {};
140
+ }
141
+ // Copy to ~/.aiden/theme.yaml and apply immediately (don't wait
142
+ // for the chokidar watcher debounce — slash-command-driven
143
+ // changes should feel instant).
144
+ try {
145
+ (0, node_fs_1.mkdirSync)(node_path_1.default.dirname(themePath), { recursive: true });
146
+ (0, node_fs_1.writeFileSync)(themePath, yamlText, 'utf8');
147
+ }
148
+ catch (err) {
149
+ ctx.display.printError(`Could not write ${themePath}: ${err.message}`, 'Check filesystem permissions and try again.');
150
+ return {};
151
+ }
152
+ const { parsed, warnings } = (0, themeLoader_1.parseThemeYaml)(yamlText);
153
+ for (const w of warnings)
154
+ ctx.display.warn(`theme: ${w}`);
155
+ if (parsed) {
156
+ (0, themeRegistry_1.applyTheme)(parsed, themePath);
157
+ ctx.display.success(`✓ Theme set to ${name} (${sourceLabel}). Run /theme reset to revert to default.`);
158
+ }
159
+ else {
160
+ ctx.display.printError(`Theme "${name}" parsed empty; current theme retained.`, 'Check the warnings above for the specific YAML / hex error.');
161
+ }
162
+ return {};
163
+ }
164
+ if (sub === 'reload') {
165
+ if (!themePath) {
166
+ ctx.display.warn('/theme reload needs Aiden user-data paths — try in a real session.');
167
+ return {};
168
+ }
169
+ if (!(0, node_fs_1.existsSync)(themePath)) {
170
+ ctx.display.warn(`No theme file at ${themePath}. Use /theme edit to create one.`);
171
+ return {};
172
+ }
173
+ const { parsed, warnings } = (0, themeLoader_1.loadThemeFile)(themePath);
174
+ for (const w of warnings)
175
+ ctx.display.warn(`theme: ${w}`);
176
+ if (parsed) {
177
+ (0, themeRegistry_1.applyTheme)(parsed, themePath);
178
+ ctx.display.success(`Theme reloaded: ${parsed.name}`);
179
+ }
180
+ else {
181
+ ctx.display.printError('Theme parse failed; current theme retained.', 'Check the warnings above for the specific YAML / hex error.');
182
+ }
183
+ return {};
184
+ }
185
+ if (sub === 'reset') {
186
+ if (!themePath) {
187
+ ctx.display.warn('/theme reset needs Aiden user-data paths — try in a real session.');
188
+ return {};
189
+ }
190
+ if ((0, node_fs_1.existsSync)(themePath)) {
191
+ try {
192
+ (0, node_fs_1.unlinkSync)(themePath);
193
+ }
194
+ catch (err) {
195
+ ctx.display.warn(`Could not delete ${themePath}: ${err.message}`);
196
+ return {};
197
+ }
198
+ }
199
+ (0, themeRegistry_1.resetToDefault)();
200
+ ctx.display.success('Theme reset to bundled default.');
201
+ return {};
202
+ }
203
+ if (sub === 'edit') {
204
+ if (!themePath) {
205
+ ctx.display.warn('/theme edit needs Aiden user-data paths — try in a real session.');
206
+ return {};
207
+ }
208
+ ctx.display.info(`Theme file: ${themePath}`);
209
+ if (!(0, node_fs_1.existsSync)(themePath)) {
210
+ ctx.display.info('File does not exist yet. Create it with your editor; Aiden hot-reloads on save.');
211
+ }
212
+ return {};
213
+ }
214
+ ctx.display.printError(`Unknown /theme subcommand: ${sub}`, 'Available: /theme | /theme list | /theme set <name> | /theme reload | /theme reset | /theme edit');
215
+ return {};
216
+ },
217
+ };
@@ -369,7 +369,7 @@ async function runAddEmail(db, argv, out, err) {
369
369
  out(`poll interval: ${spec.pollIntervalMs}ms\n`);
370
370
  out(`allow-senders: ${spec.allowedSenders.join(', ')}\n`);
371
371
  out(`⚠ Password stored in plaintext in daemon.db (chmod 600 on POSIX,\n`);
372
- out(` user-private on Windows). Encryption-at-rest is deferred to v4.6+.\n`);
372
+ out(` user-private on Windows). Encryption-at-rest is deferred to a future release.\n`);
373
373
  out(`Restart the daemon to activate the trigger.\n`);
374
374
  // Note: runAddEmail returns a Promise<number>, so the outer switch must
375
375
  // await it. (Already handled — runTriggerSubcommand is async.)
@@ -78,14 +78,26 @@ async function runInstall(ctx) {
78
78
  return;
79
79
  }
80
80
  ctx.display.write(`Installing aiden-runtime v${status.latest} (current: v${status.installed})…\n`);
81
- const result = await (0, executeInstall_1.executeInstall)();
81
+ // v4.8.1 Slice 2 — reuse the v4.8.0 sliding-block shimmer indicator
82
+ // so the user sees motion while npm install runs (typically 5–15s
83
+ // on a warm cache, longer on cold). The indicator paints to a TTY
84
+ // only — non-TTY callers (CI, pipes) see the static "Installing…"
85
+ // line above and the result row below, no shimmer.
86
+ const indicator = ctx.display.activityIndicator('updating');
87
+ let result;
88
+ try {
89
+ result = await (0, executeInstall_1.executeInstall)();
90
+ }
91
+ finally {
92
+ indicator.stop();
93
+ }
82
94
  if (result.success) {
83
95
  const v = result.installedVersion ?? status.latest;
84
96
  ctx.display.write(`\n ✓ aiden-runtime v${v} installed.\n`);
85
97
  ctx.display.dim('Restart Aiden to apply: type /quit then re-run `aiden`.');
86
98
  return;
87
99
  }
88
- ctx.display.warn(result.error ?? 'Install failed (no error message).');
100
+ ctx.display.write(`\n ✗ Update failed: ${result.error ?? 'no error message'}\n`);
89
101
  }
90
102
  // ── v4.5 update system — skip + auto subcommands ───────────────────────────
91
103
  async function runSkip(ctx) {
@@ -16,7 +16,8 @@
16
16
  * isVerbose() reads env at call time (Ollama-options precedent).
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.VERBOSE_MODE_ENV = exports.spacing = exports.glyphs = exports.colors = exports.TRAIL_PIPE = void 0;
19
+ exports.VERBOSE_MODE_ENV = exports.spacing = exports.BASELINE_GLYPHS = exports.BASELINE_COLORS = exports.glyphs = exports.colors = exports.TRAIL_PIPE = void 0;
20
+ exports._restoreBaselineForTokens = _restoreBaselineForTokens;
20
21
  exports.isVerbose = isVerbose;
21
22
  // Re-export so consumers can import the trail gutter from tokens
22
23
  // alongside the rest of the design system. The original constant
@@ -24,6 +25,20 @@ exports.isVerbose = isVerbose;
24
25
  // that imports from `display/toolTrail` directly.
25
26
  var toolTrail_1 = require("../display/toolTrail");
26
27
  Object.defineProperty(exports, "TRAIL_PIPE", { enumerable: true, get: function () { return toolTrail_1.TRAIL_PIPE; } });
28
+ /**
29
+ * v4.9.0 Slice 1a — `colors` and `glyphs` were previously exported with
30
+ * `as const` (frozen literal-typed object). They are now mutable
31
+ * singletons so the ThemeRegistry can swap values at runtime when a
32
+ * user theme is loaded from `~/.aiden/theme.yaml`. Every existing
33
+ * consumer (`import { colors, glyphs } from '../design/tokens'`)
34
+ * continues to work because property reads now reflect the LIVE
35
+ * current values.
36
+ *
37
+ * `BASELINE_COLORS` / `BASELINE_GLYPHS` are deep-frozen snapshots
38
+ * captured at module load. The registry's `resetToDefault()` restores
39
+ * from these. Consumers that need the unmodified default (e.g. tests)
40
+ * can import these directly.
41
+ */
27
42
  // ── Colors ────────────────────────────────────────────────────────────────
28
43
  /**
29
44
  * Hex color tokens. Mirrors skinEngine RGB tuples for existing kinds;
@@ -119,9 +134,9 @@ exports.glyphs = {
119
134
  dotOpen: '○',
120
135
  /** Status footer column separator. */
121
136
  sep: '│',
122
- /** Slice 7 turn counter prefix. Slice 9 hotfix: glyph dropped
123
- * entirely. The bare colored number (purple `metric_turn` kind)
124
- * carries the meaning, matching the timer pattern. */
137
+ /** Turn counter retired per v4.9.0 pre-ship UX feedback
138
+ * value-to-pixel ratio too low. Field kept (empty) so the
139
+ * token-table shape stays stable for downstream consumers. */
125
140
  turn: '',
126
141
  /** Slice 7 — session timer prefix. Slice 9 hotfix: `⌛` (U+231B
127
142
  * HOURGLASS WITH FLOWING SAND) restored. The font set Shiva
@@ -224,6 +239,39 @@ exports.glyphs = {
224
239
  track: '─',
225
240
  },
226
241
  };
242
+ /**
243
+ * Deep-frozen snapshots of the baseline values, captured at module
244
+ * load. The ThemeRegistry uses these to reset to defaults via
245
+ * `resetToDefault()`, and tests can import them as the source of
246
+ * truth even after a user theme has been applied to the mutable
247
+ * `colors` / `glyphs` exports above.
248
+ */
249
+ function deepFreeze(o) {
250
+ if (o === null || typeof o !== 'object')
251
+ return o;
252
+ for (const k of Object.keys(o)) {
253
+ deepFreeze(o[k]);
254
+ }
255
+ return Object.freeze(o);
256
+ }
257
+ function deepClone(o) {
258
+ if (o === null || typeof o !== 'object')
259
+ return o;
260
+ if (Array.isArray(o))
261
+ return o.map(deepClone);
262
+ const out = {};
263
+ for (const k of Object.keys(o)) {
264
+ out[k] = deepClone(o[k]);
265
+ }
266
+ return out;
267
+ }
268
+ exports.BASELINE_COLORS = deepFreeze(deepClone(exports.colors));
269
+ exports.BASELINE_GLYPHS = deepFreeze(deepClone(exports.glyphs));
270
+ /** Used by ThemeRegistry to restore baseline values into the live singletons. */
271
+ function _restoreBaselineForTokens() {
272
+ Object.assign(exports.colors, deepClone(exports.BASELINE_COLORS));
273
+ Object.assign(exports.glyphs, deepClone(exports.BASELINE_GLYPHS));
274
+ }
227
275
  // ── Spacing ───────────────────────────────────────────────────────────────
228
276
  /**
229
277
  * Integer spacing tokens. 0-indexed column counts; subagent depth
@@ -23,6 +23,8 @@ exports.iconForTool = iconForTool;
23
23
  exports.detectConfiguredChannels = detectConfiguredChannels;
24
24
  exports.summarizeConfiguredChannels = summarizeConfiguredChannels;
25
25
  exports.summarizeChannelState = summarizeChannelState;
26
+ exports.computeContextBarFill = computeContextBarFill;
27
+ exports.renderContextBar = renderContextBar;
26
28
  exports.voiceIndicator = voiceIndicator;
27
29
  exports.makeNoOpToolRowHandle = makeNoOpToolRowHandle;
28
30
  exports.previewToolArgs = previewToolArgs;
@@ -214,6 +216,22 @@ const AIDEN_BANNER = String.raw `
214
216
  ██║ ██║██║██████╔╝███████╗██║ ╚████║
215
217
  ╚═╝ ╚═╝╚═╝╚═════╝ ╚══════╝╚═╝ ╚═══╝
216
218
  `;
219
+ /**
220
+ * v4.9.0 pre-ship UI hotfix — pure context-bar helpers. Extracted
221
+ * for testability + to fix the "always empty" symptom. Scale:
222
+ * 0% → 0 cells, 1-19% → 1, 20-39% → 2, 40-59% → 3, 60-79% → 4,
223
+ * 80-100% → 5. `renderContextBar` returns the glyph array.
224
+ */
225
+ function computeContextBarFill(pct, barW = 5) {
226
+ if (pct <= 0)
227
+ return 0;
228
+ if (pct >= 100)
229
+ return barW;
230
+ return Math.min(barW, Math.floor(pct / 20) + 1);
231
+ }
232
+ function renderContextBar(filled, barW = 5) {
233
+ return Array.from({ length: barW }, (_, i) => i < filled ? tokens_1.glyphs.bar.filled : tokens_1.glyphs.bar.empty);
234
+ }
217
235
  class Display {
218
236
  constructor(opts = {}) {
219
237
  // ── Phase 16c: streaming surface ─────────────────────────────────────
@@ -605,23 +623,23 @@ class Display {
605
623
  statusFooter(args) {
606
624
  const sk = this.skin;
607
625
  const SEP = sk.applyColors(' │ ', 'muted');
608
- const tri = this.triangle();
609
- // v4.8.0 Slice 7 hotfix #2 per-metric accent palette.
610
- // Model: cyan (tool kind). Token counts: amber (warn). Bar/pct:
611
- // semantic tier. Turn: purple (metric_turn). Timer: teal (success).
612
- const provModel = `${tri} ${sk.applyColors(args.provider, 'muted')}` +
626
+ // v4.9.0 pre-ship UI hotfix — dropped the leading `▲` from
627
+ // provModel (prompt `▲` owns the marker; footer `▲` read as
628
+ // a duplicate orphan). Slice 7 per-metric palette preserved.
629
+ const provModel = `${sk.applyColors(args.provider, 'muted')}` +
613
630
  `${sk.applyColors(' · ', 'muted')}` +
614
631
  sk.applyColors(args.model, 'tool');
615
632
  const pct = args.ctxMax > 0
616
633
  ? Math.min(100, Math.round((args.ctxUsed / args.ctxMax) * 100))
617
634
  : 0;
618
- // 5-cell bar with single-space separators reads as discrete dots
619
- // rather than a continuous line. Total visible width ≈ 9 cells,
620
- // similar to the prior 10-cell solid bar so 80-col tier stays tight.
635
+ // v4.9.0 pre-ship UI hotfixbar math extracted to
636
+ // `computeContextBarFill` (1 cell per 20% bucket, ≥1 when any
637
+ // context used). Old `Math.round(pct/100 * barW)` floored to 0
638
+ // below ~10% so the bar stayed empty all session at typical use.
621
639
  const barW = 5;
622
- const filled = Math.round((pct / 100) * barW);
640
+ const filled = computeContextBarFill(pct, barW);
623
641
  const ctxKind = pct < 60 ? 'success' : pct < 85 ? 'warn' : 'error';
624
- const cells = Array.from({ length: barW }, (_, i) => i < filled ? tokens_1.glyphs.bar.filled : tokens_1.glyphs.bar.empty);
642
+ const cells = renderContextBar(filled, barW);
625
643
  const bar = sk.applyColors(cells.join(' '), ctxKind);
626
644
  const ctxRatio = sk.applyColors(`${formatCompactTokens(args.ctxUsed)}/${formatCompactTokens(args.ctxMax)}`, 'warn');
627
645
  const ctxPctText = sk.applyColors(`${pct}%`, ctxKind);
@@ -638,12 +656,10 @@ class Display {
638
656
  const stateDot = args.state
639
657
  ? sk.applyColors(tokens_1.glyphs.status.dot, this.stateKind(args.state))
640
658
  : '';
641
- // v4.8.0 Slice 9 hotfix — turn glyph dropped; bare colored number
642
- // matches the timer pattern. Color alone (purple metric_turn)
643
- // carries the semantic.
644
- const turnSeg = args.turnCount !== undefined
645
- ? sk.applyColors(String(args.turnCount), 'metric_turn')
646
- : '';
659
+ // v4.9.0 pre-ship UI: turn counter retired entirely value-to-pixel
660
+ // ratio too low. `args.turnCount` stays in the signature for caller
661
+ // back-compat; ignored here.
662
+ void args.turnCount;
647
663
  // v4.8.0 Slice 9 hotfix — ⌛ restored ahead of the bare elapsed
648
664
  // string. Wider font support than the retired ⏱. `sessionMs` arg
649
665
  // stays plumbed-but-unused for backward compat with the field name.
@@ -654,14 +670,17 @@ class Display {
654
670
  const ctxSegFull = `${ctxRatio} ${bar} ${ctxPctText}`;
655
671
  const ctxSegCompact = `${bar} ${ctxPctText}`;
656
672
  let segments;
657
- if (cols >= 120 && stateDot && turnSeg && sessionSeg) {
658
- segments = [provModel, ctxSegFull, turnSeg, sessionSeg, stateDot];
673
+ if (cols >= 120 && stateDot && sessionSeg) {
674
+ segments = [provModel, ctxSegFull, sessionSeg, stateDot];
659
675
  }
660
- else if (cols >= 100 && turnSeg) {
661
- segments = [provModel, ctxSegFull, turnSeg, elapsed];
676
+ else if (cols >= 100) {
677
+ // v4.8.1 Slice 2 hotfix — sessionSeg keeps the ⌛ identity glyph
678
+ // (single-cell, cheap) even at this tier. v4.9.0 pre-ship UI:
679
+ // turn counter retired; mid tier collapses to 2 separators.
680
+ segments = [provModel, ctxSegFull, sessionSeg || elapsed];
662
681
  }
663
682
  else {
664
- segments = [provModel, ctxSegCompact, elapsed];
683
+ segments = [provModel, ctxSegCompact, sessionSeg || elapsed];
665
684
  }
666
685
  return ` ${segments.join(SEP)}`;
667
686
  }
@@ -892,6 +911,13 @@ class Display {
892
911
  let stopped = false;
893
912
  let printed = false;
894
913
  let tickTimer = null;
914
+ // v4.8.1 Slice 2 hotfix #4 — true once the indicator has paused
915
+ // and resumed at least once (i.e. a tool row interrupted it). When
916
+ // false at stop() time, the indicator is still in its initial-paint
917
+ // row immediately below the leading blank, so stop()'s erase can
918
+ // safely consume BOTH rows. When true, the leading blank is far
919
+ // above and stop() erases only the current indicator row.
920
+ let movedFromInitial = false;
895
921
  // Tunable cadence. v4.1.4 Phase 3b' (Issue G): bumped from 400ms
896
922
  // to 250ms after visual smoke — 400ms felt sluggish, made the
897
923
  // indicator look static between seconds. 250ms gives ~4 dot
@@ -951,7 +977,12 @@ class Display {
951
977
  : '';
952
978
  // Shimmer prefix (or none, when opts.waveBar === false).
953
979
  const prefix = shimmerEnabled ? `${buildShimmer()} ` : '';
954
- return `${prefix}${verb}${dots.padEnd(3, ' ')}${elapsedStr}`;
980
+ // v4.8.1 Slice 2 hotfix #4 — 2-space leading indent so the
981
+ // indicator line aligns at col 2, matching `▎ Aiden`, the
982
+ // user-prompt ` ▲ `, the panel ` │ ` bar, and every other
983
+ // structured surface. Prior buildLine started at col 0 which
984
+ // read as misaligned against the rest of the v4.8 chrome.
985
+ return ` ${prefix}${verb}${dots.padEnd(3, ' ')}${elapsedStr}`;
955
986
  };
956
987
  // v4.1.5 Part 1a — Issue M (Windows ConPTY buffering fix).
957
988
  //
@@ -1014,12 +1045,16 @@ class Display {
1014
1045
  return;
1015
1046
  out.write(`${ANSI_UP_ERASE}\n`);
1016
1047
  };
1017
- // Initial paint — only on TTY. v4.8.0 Slice 11 — prepend a blank
1018
- // `\n` so the indicator gets one visible row of breathing space
1019
- // above it. Prior behaviour butted the indicator flush against
1020
- // the user-prompt row, which read as cramped. The trailing `\n`
1021
- // on the verb row sits the cursor below the indicator, ready
1022
- // for the first tick to walk back up.
1048
+ // Initial paint — only on TTY.
1049
+ //
1050
+ // v4.8.1 Slice 2 hotfix #4 leading `\n` restored to give one
1051
+ // blank row between the user-input row and the indicator (hotfix
1052
+ // #3 dropped the dim rule that previously provided that gap).
1053
+ // To keep the post-stop layout at "exactly one blank between
1054
+ // user input and ▎ Aiden", stop() now walks up TWO rows when
1055
+ // the indicator never moved (no pause/resume), consuming both
1056
+ // the indicator row AND the leading blank. The `movedFromInitial`
1057
+ // flag below tracks that state.
1023
1058
  if (isTty) {
1024
1059
  out.write(`\n${buildLine()}\n`);
1025
1060
  printed = true;
@@ -1031,6 +1066,12 @@ class Display {
1031
1066
  return;
1032
1067
  paused = true;
1033
1068
  stopTick();
1069
+ // v4.8.1 Slice 2 hotfix #4 — mark the indicator as "moved" so
1070
+ // a subsequent stop() does NOT walk up 2 rows. The leading
1071
+ // blank from initial paint is now far above the current row
1072
+ // and shouldn't be consumed; doing so would erase tool-row
1073
+ // content instead.
1074
+ movedFromInitial = true;
1034
1075
  eraseLine();
1035
1076
  // After erase the cursor is at column 0 of the indicator's
1036
1077
  // (now empty) line. Caller is expected to write its own
@@ -1071,7 +1112,20 @@ class Display {
1071
1112
  return;
1072
1113
  stopped = true;
1073
1114
  stopTick();
1074
- eraseLine();
1115
+ // v4.8.1 Slice 2 hotfix #4 — when the indicator never moved
1116
+ // (no pause/resume happened during the turn), walk up TWO
1117
+ // rows: erase the indicator row AND the leading blank above
1118
+ // it. The trailing `\n` then lands the cursor exactly one
1119
+ // row below the user-input echo, so the next writer
1120
+ // (agentHeader → ▎ Aiden) produces a clean single-blank gap.
1121
+ if (!printed || !isTty)
1122
+ return;
1123
+ if (movedFromInitial) {
1124
+ out.write(`${ANSI_UP_ERASE}\n`);
1125
+ }
1126
+ else {
1127
+ out.write(`${ANSI_UP_ERASE}${ANSI_UP_ERASE}\n`);
1128
+ }
1075
1129
  },
1076
1130
  isPaused: () => paused,
1077
1131
  isStopped: () => stopped,
@@ -2051,23 +2105,25 @@ class Display {
2051
2105
  this.out.write(this.uiTrailRow(`${ok ? '✓' : '✗'} ${framework}: ${parts.join(', ')}${dur}`, ok ? 'success' : 'error'));
2052
2106
  this.streamLastEndedNewline = true;
2053
2107
  }
2054
- renderUiApprovalRequest(args) {
2055
- const prompt = typeof args.prompt === 'string' ? args.prompt : '';
2056
- if (!prompt)
2057
- return;
2058
- const riskTier = typeof args.risk_tier === 'string' ? args.risk_tier : 'medium';
2059
- const reason = typeof args.reason === 'string' ? args.reason : '';
2060
- this.commitStreamChunk();
2061
- const kind = riskTier === 'low' ? 'success'
2062
- : (riskTier === 'high' || riskTier === 'critical') ? 'error' : 'warn';
2063
- const shortP = prompt.length > 160 ? prompt.slice(0, 159) + '…' : prompt;
2064
- let out = this.uiTrailRow(`⚠ Approval needed: ${shortP}`, kind);
2065
- if (reason) {
2066
- const shortR = reason.length > 200 ? reason.slice(0, 199) + '…' : reason;
2067
- out += this.uiTrailRow(` ${shortR}`, 'muted');
2068
- }
2069
- this.out.write(out);
2070
- this.streamLastEndedNewline = true;
2108
+ renderUiApprovalRequest(_args) {
2109
+ // v4.8.1 Slice 1 — silent no-op. The Phase 2.5 wiring fires both
2110
+ // `ui_approval_request` (this method) AND `callbacks.promptApproval`
2111
+ // (which paints the framed approval panel via `renderApprovalBox`)
2112
+ // for every single approval request. The intent was complementary
2113
+ // succinct event row above, structured kv panel below but in live
2114
+ // smoke the two surfaces stack as a visual duplicate ("Approval
2115
+ // needed: file_write {...}" event row + "│ tool / │ reason / │ args"
2116
+ // panel). The panel is the canonical, information-rich surface; this
2117
+ // event-row paint is redundant.
2118
+ //
2119
+ // Behavioural change is renderer-side only: `approvalEngine` still
2120
+ // fires `onUiEvent('ui_approval_request', ...)` so any future
2121
+ // telemetry / daemon-side run_events subscriber will still see the
2122
+ // event. Nothing paints to the chat surface from this method.
2123
+ //
2124
+ // The `_args` parameter is retained for the dispatch signature
2125
+ // contract (`renderUiEvent` calls it positionally) and for the day
2126
+ // we re-introduce a single-paint surface keyed off args.risk_tier.
2071
2127
  }
2072
2128
  renderUiToast(args) {
2073
2129
  const message = typeof args.message === 'string' ? args.message : '';