monomind 2.0.3 → 2.1.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 (89) hide show
  1. package/package.json +9 -5
  2. package/packages/@monomind/cli/.claude/helpers/control-start.cjs +13 -20
  3. package/packages/@monomind/cli/.claude/helpers/event-logger.cjs +109 -6
  4. package/packages/@monomind/cli/.claude/helpers/graphify-freshen.cjs +19 -17
  5. package/packages/@monomind/cli/.claude/helpers/handlers/agent-start-handler.cjs +60 -28
  6. package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +75 -18
  7. package/packages/@monomind/cli/.claude/helpers/handlers/edit-handler.cjs +27 -9
  8. package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +16 -7
  9. package/packages/@monomind/cli/.claude/helpers/handlers/loops-status-handler.cjs +1 -1
  10. package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +4 -4
  11. package/packages/@monomind/cli/.claude/helpers/handlers/session-handler.cjs +40 -22
  12. package/packages/@monomind/cli/.claude/helpers/handlers/session-restore-handler.cjs +0 -12
  13. package/packages/@monomind/cli/.claude/helpers/handlers/task-handler.cjs +53 -22
  14. package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +54 -4
  15. package/packages/@monomind/cli/.claude/helpers/intelligence.cjs +94 -15
  16. package/packages/@monomind/cli/.claude/helpers/session.cjs +6 -1
  17. package/packages/@monomind/cli/.claude/helpers/statusline.cjs +10 -10
  18. package/packages/@monomind/cli/.claude/helpers/token-tracker.cjs +3 -3
  19. package/packages/@monomind/cli/.claude/helpers/utils/agent-registrations.cjs +41 -0
  20. package/packages/@monomind/cli/.claude/helpers/utils/fs-helpers.cjs +183 -0
  21. package/packages/@monomind/cli/.claude/helpers/utils/micro-agents.cjs +10 -6
  22. package/packages/@monomind/cli/.claude/helpers/utils/telemetry.cjs +35 -19
  23. package/packages/@monomind/cli/bin/cli.js +10 -1
  24. package/packages/@monomind/cli/dist/src/commands/agent-ops.js +9 -15
  25. package/packages/@monomind/cli/dist/src/commands/cleanup.js +20 -4
  26. package/packages/@monomind/cli/dist/src/commands/completions.js +66 -261
  27. package/packages/@monomind/cli/dist/src/commands/doctor.js +7 -2
  28. package/packages/@monomind/cli/dist/src/commands/hooks-workers.js +7 -0
  29. package/packages/@monomind/cli/dist/src/commands/index.js +3 -0
  30. package/packages/@monomind/cli/dist/src/commands/mcp.js +11 -6
  31. package/packages/@monomind/cli/dist/src/commands/memory-admin.js +12 -6
  32. package/packages/@monomind/cli/dist/src/commands/memory-transfer.js +6 -13
  33. package/packages/@monomind/cli/dist/src/commands/monograph.js +3 -3
  34. package/packages/@monomind/cli/dist/src/commands/org.d.ts +3 -1
  35. package/packages/@monomind/cli/dist/src/commands/org.js +306 -129
  36. package/packages/@monomind/cli/dist/src/commands/start.js +142 -37
  37. package/packages/@monomind/cli/dist/src/commands/tokens.js +21 -3
  38. package/packages/@monomind/cli/dist/src/config-adapter.js +37 -17
  39. package/packages/@monomind/cli/dist/src/index.js +64 -28
  40. package/packages/@monomind/cli/dist/src/init/claudemd-generator.js +44 -11
  41. package/packages/@monomind/cli/dist/src/init/executor.js +63 -49
  42. package/packages/@monomind/cli/dist/src/mcp-client.d.ts +8 -3
  43. package/packages/@monomind/cli/dist/src/mcp-client.js +37 -3
  44. package/packages/@monomind/cli/dist/src/mcp-server.js +8 -2
  45. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.d.ts +20 -0
  46. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.js +10 -5
  47. package/packages/@monomind/cli/dist/src/mcp-tools/github-tools.js +30 -8
  48. package/packages/@monomind/cli/dist/src/mcp-tools/graphify-tools.js +11 -2
  49. package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +51 -34
  50. package/packages/@monomind/cli/dist/src/mcp-tools/knowledge-tools.js +9 -1
  51. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-compat.js +56 -14
  52. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +65 -43
  53. package/packages/@monomind/cli/dist/src/mcp-tools/swarm-tools.js +6 -4
  54. package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +4 -4
  55. package/packages/@monomind/cli/dist/src/mcp-tools/types.d.ts +20 -0
  56. package/packages/@monomind/cli/dist/src/mcp-tools/types.js +36 -1
  57. package/packages/@monomind/cli/dist/src/memory/hnsw-operations.d.ts +13 -1
  58. package/packages/@monomind/cli/dist/src/memory/hnsw-operations.js +66 -14
  59. package/packages/@monomind/cli/dist/src/memory/intelligence.js +99 -3
  60. package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +7 -0
  61. package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +20 -0
  62. package/packages/@monomind/cli/dist/src/orgrt/broker.d.ts +24 -0
  63. package/packages/@monomind/cli/dist/src/orgrt/broker.js +68 -0
  64. package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +30 -0
  65. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +96 -7
  66. package/packages/@monomind/cli/dist/src/orgrt/forwarder.d.ts +10 -5
  67. package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +105 -8
  68. package/packages/@monomind/cli/dist/src/orgrt/live.html +56 -0
  69. package/packages/@monomind/cli/dist/src/orgrt/provider.js +1 -0
  70. package/packages/@monomind/cli/dist/src/orgrt/scheduler.d.ts +13 -0
  71. package/packages/@monomind/cli/dist/src/orgrt/scheduler.js +48 -0
  72. package/packages/@monomind/cli/dist/src/orgrt/server.d.ts +8 -0
  73. package/packages/@monomind/cli/dist/src/orgrt/server.js +72 -0
  74. package/packages/@monomind/cli/dist/src/orgrt/test-loop.d.ts +12 -0
  75. package/packages/@monomind/cli/dist/src/orgrt/test-loop.js +108 -0
  76. package/packages/@monomind/cli/dist/src/output.js +12 -5
  77. package/packages/@monomind/cli/dist/src/parser.d.ts +32 -0
  78. package/packages/@monomind/cli/dist/src/parser.js +130 -5
  79. package/packages/@monomind/cli/dist/src/routing/embed-worker.js +13 -1
  80. package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +31 -0
  81. package/packages/@monomind/cli/dist/src/services/config-file-manager.d.ts +21 -0
  82. package/packages/@monomind/cli/dist/src/services/config-file-manager.js +60 -7
  83. package/packages/@monomind/cli/dist/src/services/crash-reporter.js +2 -1
  84. package/packages/@monomind/cli/dist/src/ui/dashboard.html +40 -40
  85. package/packages/@monomind/cli/dist/src/ui/orgs.html +110 -11
  86. package/packages/@monomind/cli/dist/src/ui/server.mjs +304 -133
  87. package/packages/@monomind/cli/dist/src/utils/input-guards.d.ts +9 -0
  88. package/packages/@monomind/cli/dist/src/utils/input-guards.js +39 -5
  89. package/packages/@monomind/cli/package.json +6 -2
@@ -56,6 +56,35 @@ function runWorker(task) {
56
56
  reject(new Error('embed worker timed out'));
57
57
  }, WORKER_TIMEOUT_MS);
58
58
  timer.unref?.();
59
+ // Resolve as soon as the result marker is *seen*, rather than waiting for
60
+ // the child's `close` event. This is a robustness net: onnxruntime's
61
+ // native thread pool can keep the child's event loop alive after it has
62
+ // already written its result, so `close` may never fire (or only fires
63
+ // after a slow OS-level teardown). embed-worker.ts explicitly
64
+ // process.exit()s right after writing the marker, but resolving here too
65
+ // means a future regression in that exit call degrades to "resolves
66
+ // immediately, child lingers" instead of "silently discards a correct
67
+ // result after a 90s timeout".
68
+ const tryResolveFromMarker = () => {
69
+ if (settled)
70
+ return;
71
+ const markedLine = stdout
72
+ .split('\n')
73
+ .reverse()
74
+ .find((l) => l.startsWith(RESULT_MARKER));
75
+ if (!markedLine)
76
+ return;
77
+ try {
78
+ const parsed = JSON.parse(markedLine.slice(RESULT_MARKER.length));
79
+ settled = true;
80
+ clearTimeout(timer);
81
+ resolve(parsed);
82
+ }
83
+ catch {
84
+ // Marker line present but not yet valid JSON (unlikely — it's written
85
+ // in one shot) — fall through and let `close`/timeout handle it.
86
+ }
87
+ };
59
88
  child.stdout?.on('data', (d) => {
60
89
  stdout += d.toString();
61
90
  if (stdout.length > MAX_WORKER_OUTPUT) {
@@ -68,7 +97,9 @@ function runWorker(task) {
68
97
  }
69
98
  catch { /* already dead */ }
70
99
  reject(new Error('embed worker produced excessive output'));
100
+ return;
71
101
  }
102
+ tryResolveFromMarker();
72
103
  });
73
104
  child.stderr?.on('data', (d) => { if (stderr.length < MAX_WORKER_OUTPUT)
74
105
  stderr += d.toString(); });
@@ -2,11 +2,32 @@
2
2
  * Config File Manager
3
3
  * Shared JSON config file persistence with atomic writes and Zod validation
4
4
  */
5
+ /**
6
+ * Default config values.
7
+ *
8
+ * IMPORTANT: never hand this object (or a shallow `{ ...DEFAULT_CONFIG }`
9
+ * copy) out directly — its nested sections (`agents`, `swarm`, `memory`, ...)
10
+ * would be shared-by-reference with every caller, so one caller mutating
11
+ * `config.swarm.maxAgents` would silently corrupt this module-level constant
12
+ * for the rest of the process. Always hand out `cloneDefaultConfig()`.
13
+ */
14
+ declare const DEFAULT_CONFIG: Record<string, unknown>;
15
+ /** Exposed read-only for callers (e.g. config-adapter.ts) that need to
16
+ * agree with this module's defaults instead of hardcoding their own. */
17
+ export { DEFAULT_CONFIG };
5
18
  export declare class ConfigFileManager {
6
19
  private configPath;
7
20
  private config;
8
21
  /** Find config file in search paths starting from cwd */
9
22
  findConfig(cwd: string): string | null;
23
+ /**
24
+ * Load config from an EXACT file path (used by `--config <file>` / `-c`).
25
+ * Unlike load()/findConfig(), this never falls back to directory search —
26
+ * an explicitly-named config file that doesn't exist or fails to parse is
27
+ * an error, not a silent fallback to defaults or an unrelated file in the
28
+ * same directory.
29
+ */
30
+ loadExact(filePath: string): Record<string, unknown>;
10
31
  /** Load config from file, returns null if not found */
11
32
  load(cwd: string): Record<string, unknown> | null;
12
33
  /** Get the current config, loading if needed */
@@ -10,7 +10,15 @@ const CONFIG_FILENAMES = [
10
10
  'monomind.config.json',
11
11
  '.monomind/config.json',
12
12
  ];
13
- /** Default config values */
13
+ /**
14
+ * Default config values.
15
+ *
16
+ * IMPORTANT: never hand this object (or a shallow `{ ...DEFAULT_CONFIG }`
17
+ * copy) out directly — its nested sections (`agents`, `swarm`, `memory`, ...)
18
+ * would be shared-by-reference with every caller, so one caller mutating
19
+ * `config.swarm.maxAgents` would silently corrupt this module-level constant
20
+ * for the rest of the process. Always hand out `cloneDefaultConfig()`.
21
+ */
14
22
  const DEFAULT_CONFIG = {
15
23
  version: '3.5',
16
24
  agents: {
@@ -61,6 +69,20 @@ const DEFAULT_CONFIG = {
61
69
  },
62
70
  },
63
71
  };
72
+ /**
73
+ * Deep-clone the default config so every caller gets its own independent
74
+ * object graph. `{ ...DEFAULT_CONFIG }` is only a shallow copy — its nested
75
+ * sub-configs (`agents`, `swarm`, `memory`, ...) would remain the SAME
76
+ * object references as the module-level constant, so mutating a nested
77
+ * field on one caller's copy would corrupt the shared default for every
78
+ * subsequent caller in the process.
79
+ */
80
+ function cloneDefaultConfig() {
81
+ return structuredClone(DEFAULT_CONFIG);
82
+ }
83
+ /** Exposed read-only for callers (e.g. config-adapter.ts) that need to
84
+ * agree with this module's defaults instead of hardcoding their own. */
85
+ export { DEFAULT_CONFIG };
64
86
  export class ConfigFileManager {
65
87
  configPath = null;
66
88
  config = null;
@@ -83,6 +105,36 @@ export class ConfigFileManager {
83
105
  }
84
106
  return null;
85
107
  }
108
+ /**
109
+ * Load config from an EXACT file path (used by `--config <file>` / `-c`).
110
+ * Unlike load()/findConfig(), this never falls back to directory search —
111
+ * an explicitly-named config file that doesn't exist or fails to parse is
112
+ * an error, not a silent fallback to defaults or an unrelated file in the
113
+ * same directory.
114
+ */
115
+ loadExact(filePath) {
116
+ const resolved = path.resolve(filePath);
117
+ if (!fs.existsSync(resolved)) {
118
+ throw new Error(`Config file not found: ${resolved}`);
119
+ }
120
+ let content;
121
+ try {
122
+ content = fs.readFileSync(resolved, 'utf-8');
123
+ }
124
+ catch (err) {
125
+ throw new Error(`Failed to read config file ${resolved}: ${err instanceof Error ? err.message : String(err)}`);
126
+ }
127
+ let parsed;
128
+ try {
129
+ parsed = JSON.parse(content);
130
+ }
131
+ catch (err) {
132
+ throw new Error(`Failed to parse config file ${resolved}: ${err instanceof Error ? err.message : String(err)}`);
133
+ }
134
+ this.configPath = resolved;
135
+ this.config = sanitizeConfigObject(parsed);
136
+ return this.config;
137
+ }
86
138
  /** Load config from file, returns null if not found */
87
139
  load(cwd) {
88
140
  this.configPath = this.findConfig(cwd);
@@ -106,7 +158,7 @@ export class ConfigFileManager {
106
158
  if (this.config === null) {
107
159
  this.load(cwd);
108
160
  }
109
- return this.config ?? { ...DEFAULT_CONFIG };
161
+ return this.config ?? cloneDefaultConfig();
110
162
  }
111
163
  /** Get a nested config value by dot-separated key */
112
164
  get(cwd, key) {
@@ -136,7 +188,7 @@ export class ConfigFileManager {
136
188
  // cross-process atomic without an OS-level flock, but combined with the
137
189
  // O_EXCL atomic rename it prevents the most common credential-clobber
138
190
  // window where two CLIs interleave their getConfig→set→write cycles.
139
- let onDisk = { ...DEFAULT_CONFIG };
191
+ let onDisk = cloneDefaultConfig();
140
192
  if (fs.existsSync(targetPath)) {
141
193
  try {
142
194
  const parsed = JSON.parse(fs.readFileSync(targetPath, 'utf-8'));
@@ -155,7 +207,7 @@ export class ConfigFileManager {
155
207
  if (fs.existsSync(targetPath) && !force) {
156
208
  throw new Error(`Config file already exists: ${targetPath}. Use --force to overwrite.`);
157
209
  }
158
- const config = { ...DEFAULT_CONFIG, ...(overrides ? sanitizeConfigObject(overrides) : {}) };
210
+ const config = { ...cloneDefaultConfig(), ...(overrides ? sanitizeConfigObject(overrides) : {}) };
159
211
  this.writeAtomic(targetPath, config);
160
212
  this.config = config;
161
213
  this.configPath = targetPath;
@@ -164,8 +216,9 @@ export class ConfigFileManager {
164
216
  /** Reset config to defaults */
165
217
  reset(cwd) {
166
218
  const targetPath = this.configPath ?? path.resolve(cwd, CONFIG_FILENAMES[0]);
167
- this.writeAtomic(targetPath, DEFAULT_CONFIG);
168
- this.config = { ...DEFAULT_CONFIG };
219
+ const fresh = cloneDefaultConfig();
220
+ this.writeAtomic(targetPath, fresh);
221
+ this.config = fresh;
169
222
  this.configPath = targetPath;
170
223
  return targetPath;
171
224
  }
@@ -228,7 +281,7 @@ export class ConfigFileManager {
228
281
  }
229
282
  /** Get default config */
230
283
  getDefaults() {
231
- return { ...DEFAULT_CONFIG };
284
+ return cloneDefaultConfig();
232
285
  }
233
286
  /** Atomic write with restrictive 0o600 mode.
234
287
  * SECURITY: this config file may contain API keys (per `commands/providers.ts`).
@@ -84,7 +84,8 @@ export function redact(text) {
84
84
  /(?:secret|password|passwd|pwd)\s*[:=]\s*['"]?[^\s'"]{8,}['"]?/gi,
85
85
  /(?:token|bearer)\s*[:=]\s*['"]?[^\s'"]{10,}['"]?/gi,
86
86
  /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
87
- /sk-[a-zA-Z0-9]{20,}/g,
87
+ /sk-ant-[a-zA-Z0-9_-]{20,}/g,
88
+ /sk-[a-zA-Z0-9_-]{20,}/g,
88
89
  /ghp_[a-zA-Z0-9]{36}/g,
89
90
  /gho_[a-zA-Z0-9]{36}/g,
90
91
  /npm_[a-zA-Z0-9]{36}/g,
@@ -4448,9 +4448,9 @@ function appendChatViewEvent(ev, animate) {
4448
4448
  const atBottom = !animate || (feed.scrollHeight - feed.scrollTop - feed.clientHeight < 80);
4449
4449
  const ts = ev.ts ? new Date(ev.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
4450
4450
  if (ev.type === 'run:start') {
4451
- el = mkCVSys('▶ Run started' + (ev.goal ? ': ' + esc(ev.goal.slice(0,80)) : '') + (ev.bossRole ? ' · boss: ' + esc(ev.bossRole) : ''), ts);
4451
+ el = mkCVSys('▶ Run started' + (ev.goal ? ': ' + (ev.goal.slice(0,80)) : '') + (ev.bossRole ? ' · boss: ' + (ev.bossRole) : ''), ts);
4452
4452
  } else if (ev.type === 'run:cycle:complete') {
4453
- el = mkCVSys('◎ Cycle complete' + (ev.pending != null ? ' · ' + esc(String(ev.pending)) + ' tasks pending' : ''), ts);
4453
+ el = mkCVSys('◎ Cycle complete' + (ev.pending != null ? ' · ' + (String(ev.pending)) + ' tasks pending' : ''), ts);
4454
4454
  } else if (ev.type === 'intercom') {
4455
4455
  el = mkCVIntercom(ev.from, ev.to, ev.msg || '', ts);
4456
4456
  } else if (ev.type === 'org:comms') {
@@ -4459,15 +4459,15 @@ function appendChatViewEvent(ev, animate) {
4459
4459
  if (ev.to && ev.to !== 'all') el = mkCVIntercom(_cvCommsFrom, ev.to, ev.msg || '', ts);
4460
4460
  else el = mkCVAgent(_cvCommsFrom, ev.msg || '', ts, 'org:comms');
4461
4461
  } else if (ev.type === 'org:agent:online') {
4462
- el = mkCVSys('[' + esc(ev.org || '') + '] ' + esc(ev.role || ev.title || '?') + ' online' + (ev.title && ev.role && ev.title !== ev.role ? ' (' + esc(ev.title) + ')' : ''), ts);
4462
+ el = mkCVSys('[' + (ev.org || '') + '] ' + (ev.role || ev.title || '?') + ' online' + (ev.title && ev.role && ev.title !== ev.role ? ' (' + (ev.title) + ')' : ''), ts);
4463
4463
  } else if (ev.type === 'org:checkpoint') {
4464
- el = mkCVSys('[checkpoint] ' + esc(ev.summary || ev.progress || ev.msg || ''), ts);
4464
+ el = mkCVSys('[checkpoint] ' + (ev.summary || ev.progress || ev.msg || ''), ts);
4465
4465
  } else if (ev.type === 'org:start') {
4466
- el = mkCVSys('[org:start] ' + esc(ev.org || ''), ts);
4466
+ el = mkCVSys('[org:start] ' + (ev.org || ''), ts);
4467
4467
  } else if (ev.type === 'run:complete') {
4468
- el = mkCVSys('■ Run complete' + (ev.status ? ' [' + esc(ev.status) + ']' : ''), ts);
4468
+ el = mkCVSys('■ Run complete' + (ev.status ? ' [' + (ev.status) + ']' : ''), ts);
4469
4469
  } else if (ev.type === 'org:complete') {
4470
- el = mkCVSys('■ Org complete' + (ev.org ? ' — ' + esc(ev.org) : ''), ts);
4470
+ el = mkCVSys('■ Org complete' + (ev.org ? ' — ' + (ev.org) : ''), ts);
4471
4471
  } else if (ev.type === 'agent:result') {
4472
4472
  el = mkCVResult(ev.agent || ev.from || '?', ev.result || ev.msg || ev.message || ev.summary || '', ts);
4473
4473
  } else if (ev.type === 'agent:spawn') {
@@ -4487,36 +4487,36 @@ function appendChatViewEvent(ev, animate) {
4487
4487
  const _auAgent = ev.role || ev.agentType || '?';
4488
4488
  const _auCost = ev.cost_usd != null ? '$' + Number(ev.cost_usd).toFixed(4) : '';
4489
4489
  const _auTok = (ev.tokens_in || ev.tokens_out) ? ((ev.tokens_in||0)).toLocaleString() + '↑ ' + ((ev.tokens_out||0)).toLocaleString() + '↓' : '';
4490
- el = mkCVSys('◈ ' + esc(_auAgent) + (_auTok ? ' · ' + _auTok : '') + (_auCost ? ' · ' + _auCost : ''), ts);
4490
+ el = mkCVSys('◈ ' + (_auAgent) + (_auTok ? ' · ' + _auTok : '') + (_auCost ? ' · ' + _auCost : ''), ts);
4491
4491
  } else if (ev.type === 'agent:message') {
4492
4492
  el = mkCVAgent(ev.agent || ev.name || ev.role || '?', ev.msg || ev.message || ev.content || '', ts, ev.type);
4493
4493
  } else if (ev.type === 'session:start') {
4494
- el = mkCVSys('Session started' + (ev.prompt ? ': ' + esc(ev.prompt.slice(0,80)) : ''), ts);
4494
+ el = mkCVSys('Session started' + (ev.prompt ? ': ' + (ev.prompt.slice(0,80)) : ''), ts);
4495
4495
  } else if (ev.type === 'session:complete') {
4496
- el = mkCVSys('Session complete' + (ev.status ? ' — ' + esc(ev.status) : ''), ts);
4496
+ el = mkCVSys('Session complete' + (ev.status ? ' — ' + (ev.status) : ''), ts);
4497
4497
  } else if (ev.type === 'domain:dispatch') {
4498
- el = mkCVSys('→ ' + esc(ev.domain || '') + (ev.cmd ? ': ' + esc(ev.cmd.slice(0,80)) : ''), ts);
4498
+ el = mkCVSys('→ ' + (ev.domain || '') + (ev.cmd ? ': ' + (ev.cmd.slice(0,80)) : ''), ts);
4499
4499
  } else if (ev.type === 'domain:complete') {
4500
- el = mkCVSys('✓ ' + esc(ev.domain || '') + (ev.status ? ' [' + esc(ev.status) + ']' : ''), ts);
4500
+ el = mkCVSys('✓ ' + (ev.domain || '') + (ev.status ? ' [' + (ev.status) + ']' : ''), ts);
4501
4501
  } else if (ev.type === 'loop:start') {
4502
- el = mkCVSys('◎ Loop: ' + esc(ev.command || '') + (ev.repeat ? ' ×' + esc(String(ev.repeat)) : ''), ts);
4502
+ el = mkCVSys('◎ Loop: ' + (ev.command || '') + (ev.repeat ? ' ×' + (String(ev.repeat)) : ''), ts);
4503
4503
  if (currentView === 'loops') renderLoops();
4504
4504
  } else if (ev.type === 'loop:complete') {
4505
- el = mkCVSys('■ Loop done: ' + esc(ev.command || '') + (ev.ranReps ? ' (' + esc(String(ev.ranReps)) + ' runs)' : ''), ts);
4505
+ el = mkCVSys('■ Loop done: ' + (ev.command || '') + (ev.ranReps ? ' (' + (String(ev.ranReps)) + ' runs)' : ''), ts);
4506
4506
  if (currentView === 'loops') renderLoops();
4507
4507
  } else if (ev.type === 'loop:tick') {
4508
4508
  const _tickLbl = ev.rep != null
4509
- ? 'rep ' + esc(String(ev.rep)) + ' done' + (ev.wait != null ? ' → next in ' + esc(String(ev.wait)) + 's' : '')
4510
- : esc(ev.command || ev.loopId || ev.id || '');
4509
+ ? 'rep ' + (String(ev.rep)) + ' done' + (ev.wait != null ? ' → next in ' + (String(ev.wait)) + 's' : '')
4510
+ : (ev.command || ev.loopId || ev.id || '');
4511
4511
  el = mkCVSys('◷ ' + _tickLbl, ts);
4512
4512
  el.dataset.evType = 'loop:tick';
4513
4513
  if (feed.lastElementChild?.dataset?.evType === 'loop:tick') feed.lastElementChild.remove();
4514
4514
  if (currentView === 'loops') renderLoops();
4515
4515
  } else if (ev.type === 'loop:hil' || ev.type === 'loop:hil:waiting') {
4516
- el = mkCVSys('⚠ Loop HIL: ' + esc(ev.command || ev.loopId || ev.id || ''), ts);
4516
+ el = mkCVSys('⚠ Loop HIL: ' + (ev.command || ev.loopId || ev.id || ''), ts);
4517
4517
  if (currentView === 'loops') renderLoops();
4518
4518
  } else if (ev.type === 'loop:hil:resolved') {
4519
- el = mkCVSys('✓ Loop HIL resolved: ' + esc(ev.loopId || ev.command || ''), ts);
4519
+ el = mkCVSys('✓ Loop HIL resolved: ' + (ev.loopId || ev.command || ''), ts);
4520
4520
  if (currentView === 'loops') renderLoops();
4521
4521
  } else if (ev.type === 'file:write') {
4522
4522
  el = mkCVFileCard(ev.name || ev.path || '?', ev.path || '', ev.size || 0, ts);
@@ -4527,12 +4527,12 @@ function appendChatViewEvent(ev, animate) {
4527
4527
  } else if (ev.type === 'agent:browse') {
4528
4528
  el = mkCVTool('BROWSE', ev.payload || ev.url || '', '', 'oklch(65% 0.15 190)', ts);
4529
4529
  } else if (ev.type === 'org:agent:offline') {
4530
- el = mkCVSys('◌ ' + esc(ev.title || ev.role || '?') + ' offline' + (ev.org ? ' [' + esc(ev.org) + ']' : ''), ts);
4530
+ el = mkCVSys('◌ ' + (ev.title || ev.role || '?') + ' offline' + (ev.org ? ' [' + (ev.org) + ']' : ''), ts);
4531
4531
  } else if (ev.type === 'org:error') {
4532
- el = mkCVSys('⚠ Error: ' + esc(ev.msg || ev.error || ev.message || ''), ts);
4532
+ el = mkCVSys('⚠ Error: ' + (ev.msg || ev.error || ev.message || ''), ts);
4533
4533
  el.classList.add('cv-err');
4534
4534
  } else {
4535
- el = mkCVSys(esc(ev.type || 'event'), ts);
4535
+ el = mkCVSys((ev.type || 'event'), ts);
4536
4536
  }
4537
4537
  if (animate) el.classList.add('cv-new');
4538
4538
  feed.appendChild(el);
@@ -4671,10 +4671,10 @@ function mkCVTool(tag, cmd, output, color, ts) {
4671
4671
  return d;
4672
4672
  }
4673
4673
 
4674
- function mkCVSys(html, ts) {
4674
+ function mkCVSys(text, ts) {
4675
4675
  const d = document.createElement('div');
4676
4676
  d.className = 'cv-msg cv-sys';
4677
- d.innerHTML = `<div class="cv-bub"><span class="cv-etype">SYS</span><span class="cv-text">${html}</span><span class="cv-ts">${ts}</span></div>`;
4677
+ d.innerHTML = `<div class="cv-bub"><span class="cv-etype">SYS</span><span class="cv-text">${esc(text)}</span><span class="cv-ts">${ts}</span></div>`;
4678
4678
  return d;
4679
4679
  }
4680
4680
 
@@ -8014,18 +8014,18 @@ function _odtAppendEvent(ev, animate) {
8014
8014
  const ts = ev.ts ? new Date(ev.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
8015
8015
  let el;
8016
8016
  if (ev.type === 'run:start') {
8017
- el = mkCVSys('▶ Run started' + (ev.goal ? ': ' + esc(ev.goal.slice(0,80)) : '') + (ev.bossRole ? ' · boss: ' + esc(ev.bossRole) : ''), ts);
8017
+ el = mkCVSys('▶ Run started' + (ev.goal ? ': ' + (ev.goal.slice(0,80)) : '') + (ev.bossRole ? ' · boss: ' + (ev.bossRole) : ''), ts);
8018
8018
  } else if (ev.type === 'run:cycle:complete') {
8019
- el = mkCVSys('◎ Cycle complete' + (ev.pending != null ? ' · ' + esc(String(ev.pending)) + ' tasks pending' : ''), ts);
8019
+ el = mkCVSys('◎ Cycle complete' + (ev.pending != null ? ' · ' + (String(ev.pending)) + ' tasks pending' : ''), ts);
8020
8020
  } else if (ev.type === 'run:complete') {
8021
- el = mkCVSys('■ Run complete' + (ev.status ? ' [' + esc(ev.status) + ']' : ''), ts);
8021
+ el = mkCVSys('■ Run complete' + (ev.status ? ' [' + (ev.status) + ']' : ''), ts);
8022
8022
  } else if (ev.type === 'org:comms') {
8023
8023
  // Some older runs use ev.role instead of ev.from — normalise before rendering
8024
8024
  const _commsFrom = ev.from || ev.role || 'boss';
8025
8025
  if (ev.to && ev.to !== 'all') el = mkCVIntercom(_commsFrom, ev.to, ev.msg || '', ts);
8026
8026
  else el = mkCVAgent(_commsFrom, ev.msg || '', ts, 'org:comms');
8027
8027
  } else if (ev.type === 'org:agent:online') {
8028
- el = mkCVSys('◉ ' + esc(ev.title || ev.role || '?') + ' online', ts);
8028
+ el = mkCVSys('◉ ' + (ev.title || ev.role || '?') + ' online', ts);
8029
8029
  } else if (ev.type === 'org:checkpoint') {
8030
8030
  el = mkCVAgent('boss', ev.summary || ev.progress || ev.msg || '', ts, 'org:checkpoint');
8031
8031
  } else if (ev.type === 'org:start' || ev.type === 'org:complete') {
@@ -8041,32 +8041,32 @@ function _odtAppendEvent(ev, animate) {
8041
8041
  } else if (ev.type === 'agent:message') {
8042
8042
  el = mkCVAgent(ev.agent || ev.name || ev.role || '?', ev.msg || ev.message || ev.content || '', ts, ev.type);
8043
8043
  } else if (ev.type === 'session:start') {
8044
- el = mkCVSys('▶ ' + esc((ev.prompt || '').replace(/^running org:\s*/i,'').slice(0,80) || 'Session started'), ts);
8044
+ el = mkCVSys('▶ ' + ((ev.prompt || '').replace(/^running org:\s*/i,'').slice(0,80) || 'Session started'), ts);
8045
8045
  } else if (ev.type === 'session:complete') {
8046
- el = mkCVSys('■ Complete' + (ev.status ? ' — ' + esc(ev.status) : ''), ts);
8046
+ el = mkCVSys('■ Complete' + (ev.status ? ' — ' + (ev.status) : ''), ts);
8047
8047
  } else if (ev.type === 'domain:dispatch') {
8048
- el = mkCVSys('→ ' + esc(ev.domain || '') + (ev.cmd ? ': ' + esc(ev.cmd.slice(0,80)) : ''), ts);
8048
+ el = mkCVSys('→ ' + (ev.domain || '') + (ev.cmd ? ': ' + (ev.cmd.slice(0,80)) : ''), ts);
8049
8049
  } else if (ev.type === 'domain:complete') {
8050
- el = mkCVSys('✓ ' + esc(ev.domain || '') + (ev.status ? ' [' + esc(ev.status) + ']' : ''), ts);
8050
+ el = mkCVSys('✓ ' + (ev.domain || '') + (ev.status ? ' [' + (ev.status) + ']' : ''), ts);
8051
8051
  } else if (ev.type === 'loop:start') {
8052
- el = mkCVSys('Loop: ' + esc(ev.command || ''), ts);
8052
+ el = mkCVSys('Loop: ' + (ev.command || ''), ts);
8053
8053
  } else if (ev.type === 'loop:complete') {
8054
- el = mkCVSys('Loop done: ' + esc(ev.command || '') + (ev.ranReps ? ' (' + ev.ranReps + ' runs)' : ''), ts);
8054
+ el = mkCVSys('Loop done: ' + (ev.command || '') + (ev.ranReps ? ' (' + ev.ranReps + ' runs)' : ''), ts);
8055
8055
  } else if (ev.type === 'loop:tick') {
8056
8056
  const _trep = ev.rep ?? ev.fromRep;
8057
8057
  const _tnext = ev.nextRep ?? ev.toRep;
8058
8058
  const tickLabel = _trep != null
8059
- ? 'rep ' + esc(String(_trep)) + ' done' + (_tnext != null ? ' → rep ' + esc(String(_tnext)) + (ev.wait != null ? ' in ' + esc(String(ev.wait)) + 's' : '') : '')
8060
- : esc(ev.command || ev.loopId || '');
8059
+ ? 'rep ' + (String(_trep)) + ' done' + (_tnext != null ? ' → rep ' + (String(_tnext)) + (ev.wait != null ? ' in ' + (String(ev.wait)) + 's' : '') : '')
8060
+ : (ev.command || ev.loopId || '');
8061
8061
  el = mkCVSys('◷ ' + tickLabel, ts);
8062
8062
  el.dataset.evType = 'loop:tick';
8063
8063
  // Collapse: replace the previous loop:tick if it's the last element — prevents the
8064
8064
  // feed from flooding with one entry per rep in long-running loops (e.g. --repeat 9999).
8065
8065
  if (feed.lastElementChild?.dataset?.evType === 'loop:tick') feed.lastElementChild.remove();
8066
8066
  } else if (ev.type === 'loop:hil' || ev.type === 'loop:hil:waiting') {
8067
- el = mkCVSys('⚠ Loop HIL: ' + esc(ev.command || ev.loopId || ''), ts);
8067
+ el = mkCVSys('⚠ Loop HIL: ' + (ev.command || ev.loopId || ''), ts);
8068
8068
  } else if (ev.type === 'loop:hil:resolved') {
8069
- el = mkCVSys('✓ Loop HIL resolved: ' + esc(ev.loopId || ''), ts);
8069
+ el = mkCVSys('✓ Loop HIL resolved: ' + (ev.loopId || ''), ts);
8070
8070
  } else if (ev.type === 'agent:edit' || ev.type === 'agent:write') {
8071
8071
  el = mkCVTool('EDIT', ev.payload || ev.file || ev.path || '', '', 'oklch(65% 0.20 270)', ts);
8072
8072
  } else if (ev.type === 'agent:bash') {
@@ -8074,15 +8074,15 @@ function _odtAppendEvent(ev, animate) {
8074
8074
  } else if (ev.type === 'agent:browse') {
8075
8075
  el = mkCVTool('BROWSE', ev.payload || ev.url || '', '', 'oklch(65% 0.15 190)', ts);
8076
8076
  } else if (ev.type === 'org:error') {
8077
- el = mkCVSys('⚠ Error: ' + esc(ev.msg || ev.error || ev.message || ''), ts);
8077
+ el = mkCVSys('⚠ Error: ' + (ev.msg || ev.error || ev.message || ''), ts);
8078
8078
  el.classList.add('cv-err');
8079
8079
  } else if (ev.type === 'org:agent:offline') {
8080
- el = mkCVSys('◌ ' + esc(ev.title || ev.role || '?') + ' offline', ts);
8080
+ el = mkCVSys('◌ ' + (ev.title || ev.role || '?') + ' offline', ts);
8081
8081
  } else if (ev.type === 'user:message') {
8082
8082
  el = mkCVAgent('you', ev.text || ev.msg || ev.message || '', ts, 'user:message');
8083
8083
  el.classList.add('cv-user-msg');
8084
8084
  } else {
8085
- el = mkCVSys(esc(ev.type || 'event'), ts);
8085
+ el = mkCVSys((ev.type || 'event'), ts);
8086
8086
  }
8087
8087
  if (animate) el.classList.add('cv-new');
8088
8088
  feed.appendChild(el);
@@ -383,6 +383,37 @@ html, body {
383
383
  content: '—'; color: oklch(36% 0.008 186 / 0.5); flex-shrink: 0; margin-top: 1px;
384
384
  }
385
385
 
386
+ /* ── Files tab ──────────────────────────────────────────────────── */
387
+ #files-pane { padding: 20px; }
388
+ .files-grid {
389
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
390
+ gap: 10px;
391
+ }
392
+ .file-card {
393
+ background: var(--bg-panel);
394
+ border: 1px solid var(--border); border-radius: 3px;
395
+ padding: 10px 12px;
396
+ display: flex; align-items: center; gap: 10px;
397
+ transition: border-color 0.12s;
398
+ }
399
+ .file-card:hover { border-color: var(--teal-dim); }
400
+ .fc-icon { font-size: 18px; flex-shrink: 0; }
401
+ .fc-body { flex: 1; min-width: 0; }
402
+ .fc-name {
403
+ font-size: 12px; color: var(--text); font-weight: 500;
404
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
405
+ }
406
+ .fc-meta {
407
+ font-size: 9px; color: var(--dim); margin-top: 2px;
408
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
409
+ }
410
+ .fc-view {
411
+ flex-shrink: 0; background: #1a3a5a; color: #5d9fd9; border: 1px solid #2980b944;
412
+ border-radius: 4px; padding: 4px 10px; font-size: 10px; font-weight: 700; cursor: pointer;
413
+ }
414
+ .fc-view:hover { background: #204a70; }
415
+ .fc-binary { flex-shrink: 0; font-size: 9px; color: var(--dim); }
416
+
386
417
  /* ── Activity / event log ───────────────────────────────────────── */
387
418
  #activity-pane { padding: 20px; }
388
419
  #activity-log { display: flex; flex-direction: column; gap: 0; }
@@ -819,6 +850,7 @@ html, body {
819
850
  <button class="tab-btn" onclick="switchTab('roles')">ROLES</button>
820
851
  <button class="tab-btn" onclick="switchTab('activity')">ACTIVITY</button>
821
852
  <button class="tab-btn" onclick="switchTab('health')">HEALTH</button>
853
+ <button class="tab-btn" onclick="switchTab('files')">FILES</button>
822
854
  <button class="tab-btn" onclick="switchTab('chat')">CHAT</button>
823
855
  </div>
824
856
 
@@ -937,6 +969,14 @@ html, body {
937
969
  </div>
938
970
  </div>
939
971
 
972
+ <!-- Files tab: every asset any agent in this org has produced, across all runs -->
973
+ <div class="tab-pane" id="tab-files">
974
+ <div id="files-pane">
975
+ <div id="files-empty" class="act-empty" style="display:none">No files produced yet.</div>
976
+ <div id="files-grid" class="files-grid"></div>
977
+ </div>
978
+ </div>
979
+
940
980
  <!-- Chat tab -->
941
981
  <div class="tab-pane" id="tab-chat">
942
982
  <div id="chat-pane">
@@ -1081,9 +1121,61 @@ window.switchTab = function(tab) {
1081
1121
  const mainBody = document.getElementById('main-body');
1082
1122
  if (mainBody) mainBody.classList.toggle('chat-active', tab === 'chat');
1083
1123
  if (tab === 'chat') renderChatTab();
1124
+ else if (tab === 'files') renderFilesTab();
1084
1125
  else if (orgDetailData) renderTab(tab);
1085
1126
  };
1086
1127
 
1128
+ // ── Files tab: every asset produced by this org, deduped by path, latest first ──
1129
+ function collectOrgArtifacts() {
1130
+ const bySessions = chatSessions.filter(orgSessionMatch);
1131
+ const seen = new Map(); // path -> { art, ts, from }
1132
+ bySessions.forEach(s => (s.events || []).forEach(ev => {
1133
+ if (ev.type !== 'org:artifact') return;
1134
+ const art = ev.artifact || (ev.path ? { path: ev.path, label: ev.label, mimeType: ev.mimeType } : null);
1135
+ if (!art || !art.path) return;
1136
+ const prev = seen.get(art.path);
1137
+ if (!prev || (ev.ts || 0) >= prev.ts) seen.set(art.path, { art, ts: ev.ts || 0, from: ev.from || '' });
1138
+ }));
1139
+ return Array.from(seen.values()).sort((a, b) => b.ts - a.ts);
1140
+ }
1141
+
1142
+ function mkFileCard(entry) {
1143
+ const { art, ts, from } = entry;
1144
+ const el = document.createElement('div');
1145
+ el.className = 'file-card';
1146
+ const isText = (art.mimeType || '').startsWith('text/') || (art.mimeType || '') === 'application/json';
1147
+ const labelRaw = art.label || (art.path || 'file').split('/').pop();
1148
+ const timeStr = ts ? new Date(ts).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '';
1149
+ const meta = [from, art.path ? art.path.split('/').slice(-3).join('/') : null, timeStr].filter(Boolean).join(' · ');
1150
+ el.innerHTML = `
1151
+ <div class="fc-icon">📄</div>
1152
+ <div class="fc-body">
1153
+ <div class="fc-name">${esc(labelRaw)}</div>
1154
+ <div class="fc-meta">${esc(meta)}</div>
1155
+ </div>
1156
+ ${isText && art.path
1157
+ ? `<button class="fc-view" onclick="viewArtifact(${JSON.stringify(art.path)},${JSON.stringify(labelRaw)})">View</button>`
1158
+ : `<span class="fc-binary">Binary</span>`}
1159
+ `;
1160
+ return el;
1161
+ }
1162
+
1163
+ function paintFilesGrid() {
1164
+ const grid = document.getElementById('files-grid');
1165
+ const empty = document.getElementById('files-empty');
1166
+ if (!grid) return;
1167
+ const files = collectOrgArtifacts();
1168
+ grid.innerHTML = '';
1169
+ if (!files.length) { if (empty) empty.style.display = 'block'; return; }
1170
+ if (empty) empty.style.display = 'none';
1171
+ files.forEach(entry => grid.appendChild(mkFileCard(entry)));
1172
+ }
1173
+
1174
+ function renderFilesTab() {
1175
+ paintFilesGrid(); // paint immediately with whatever's cached
1176
+ loadChatSessions().then(() => { if (currentTab === 'files') paintFilesGrid(); });
1177
+ }
1178
+
1087
1179
  // ── Render all orgs in sidebar ────────────────────────────────────
1088
1180
  function renderSidebar() {
1089
1181
  const list = document.getElementById('sb-list');
@@ -1689,8 +1781,13 @@ function orgSessionMatch(s) {
1689
1781
  const orgLc = selectedOrg.toLowerCase();
1690
1782
  const prompt = (s.prompt || '').toLowerCase();
1691
1783
  if (prompt.includes('running org: ' + orgLc) || prompt.includes('org: ' + orgLc)) return true;
1692
- // Also check events for an org field
1693
- return (s.events || []).some(ev => (ev.org || '').toLowerCase() === orgLc);
1784
+ // Only genuine org-run events (org:comms, org:artifact, org:start, ...) count.
1785
+ // agent:bash / agent:edit / etc. also carry an `org` field (ambient "which
1786
+ // org was active while this Claude Code session ran" tagging) — matching on
1787
+ // those too would pull an unrelated dev session's entire tool-call history
1788
+ // into this org's Chat/Files tabs whenever that org happened to be running
1789
+ // at the same time.
1790
+ return (s.events || []).some(ev => (ev.type || '').startsWith('org:') && (ev.org || '').toLowerCase() === orgLc);
1694
1791
  }
1695
1792
 
1696
1793
  function populateChatSelect() {
@@ -1834,21 +1931,21 @@ function appendChatEvent(ev, animate) {
1834
1931
  } else if (ev.type === 'agent:spawn') {
1835
1932
  const col = dc(ev.domain);
1836
1933
  const name = ev.agentType || ev.from || ev.agent || 'agent';
1837
- el = mkAgent(name, `spawned: ${esc(ev.task||'task')}`, ts, col, 'spawn');
1934
+ el = mkAgent(name, `spawned: ${ev.task||'task'}`, ts, col, 'spawn');
1838
1935
  } else if (ev.type === 'agent:complete') {
1839
1936
  const name = ev.agentType || ev.role || ev.from || 'agent';
1840
1937
  const result = (ev.result || '').slice(0, 300);
1841
1938
  const cost = ev.cost_usd ? ` · $${parseFloat(ev.cost_usd).toFixed(4)}` : '';
1842
1939
  const tools = ev.toolCalls?.length ? ` · ${ev.toolCalls.slice(0,4).join(', ')}` : '';
1843
- el = mkAgent(name, `${esc(result) || 'done'}${esc(tools)}${esc(cost)}`, ts, dc(ev.domain), 'complete');
1940
+ el = mkAgent(name, `${result || 'done'}${tools}${cost}`, ts, dc(ev.domain), 'complete');
1844
1941
  } else if (ev.type === 'org:comms') {
1845
- el = mkIntercom(ev.from || 'boss', ev.to || 'agent', esc(ev.msg || ev.message || ''), ts);
1942
+ el = mkIntercom(ev.from || 'boss', ev.to || 'agent', ev.msg || ev.message || '', ts);
1846
1943
  } else if (ev.type === 'agent:message') {
1847
- el = mkAgent(ev.agent||'agent', esc(ev.text||''), ts, dc(ev.domain), null);
1944
+ el = mkAgent(ev.agent||'agent', ev.text||'', ts, dc(ev.domain), null);
1848
1945
  } else if (ev.type === 'intercom') {
1849
- el = mkIntercom(ev.from, ev.to, esc(ev.msg||''), ts);
1946
+ el = mkIntercom(ev.from, ev.to, ev.msg||'', ts);
1850
1947
  } else if (ev.type === 'user:message') {
1851
- el = mkUser(esc(ev.text||ev.msg||''), ts);
1948
+ el = mkUser(ev.text||ev.msg||'', ts);
1852
1949
  } else if (ev.type === 'loop:start') {
1853
1950
  el = mkSys(`🔁 Loop: ${esc(ev.command||'')} (${ev.repeat||'?'} runs)`, ts);
1854
1951
  } else if (ev.type === 'loop:complete') {
@@ -1876,13 +1973,13 @@ function mkAgent(name, text, ts, color, typeTag) {
1876
1973
  el.className = 'cmsg cagent';
1877
1974
  const tagStyle = color ? `style="border-color:${color}44;color:${color}"` : '';
1878
1975
  const ttag = typeTag ? `<span class="cevtype">${esc(typeTag)}</span>` : '';
1879
- el.innerHTML = `<div class="cmsg-meta"><span class="ctag" ${tagStyle}>${esc(name)}</span>${ttag}<span class="cts">${ts}</span></div><div class="cbubble">${text}</div>`;
1976
+ el.innerHTML = `<div class="cmsg-meta"><span class="ctag" ${tagStyle}>${esc(name)}</span>${ttag}<span class="cts">${ts}</span></div><div class="cbubble">${esc(text)}</div>`;
1880
1977
  return el;
1881
1978
  }
1882
1979
  function mkIntercom(from, to, text, ts) {
1883
1980
  const el = document.createElement('div');
1884
1981
  el.className = 'cmsg cic';
1885
- el.innerHTML = `<div class="cmsg-meta"><span class="ctag sender">${esc(from||'?')}</span><span class="cic-arrow">→</span><span class="ctag receiver">${esc(to||'?')}</span><span class="cevtype">intercom</span><span class="cts">${ts}</span></div><div class="cbubble">${text}</div>`;
1982
+ el.innerHTML = `<div class="cmsg-meta"><span class="ctag sender">${esc(from||'?')}</span><span class="cic-arrow">→</span><span class="ctag receiver">${esc(to||'?')}</span><span class="cevtype">intercom</span><span class="cts">${ts}</span></div><div class="cbubble">${esc(text)}</div>`;
1886
1983
  return el;
1887
1984
  }
1888
1985
  function mkArtifact(art, ts) {
@@ -1901,7 +1998,7 @@ function mkArtifact(art, ts) {
1901
1998
  function mkUser(text, ts) {
1902
1999
  const el = document.createElement('div');
1903
2000
  el.className = 'cmsg cuser';
1904
- el.innerHTML = `<div class="cmsg-meta" style="justify-content:flex-end"><span class="cts">${ts}</span><span class="ctag" style="border-color:oklch(68% 0.18 250 / 0.35);color:oklch(68% 0.14 250)">you</span></div><div class="cbubble">${text}</div>`;
2001
+ el.innerHTML = `<div class="cmsg-meta" style="justify-content:flex-end"><span class="cts">${ts}</span><span class="ctag" style="border-color:oklch(68% 0.18 250 / 0.35);color:oklch(68% 0.14 250)">you</span></div><div class="cbubble">${esc(text)}</div>`;
1905
2002
  return el;
1906
2003
  }
1907
2004
 
@@ -2042,6 +2139,8 @@ function handleMmEvent(ev) {
2042
2139
  appendChatEvent(ev, true);
2043
2140
  chatScrollFeed();
2044
2141
  }
2142
+ // Live-update the Files tab when a new asset lands while it's open
2143
+ if (ev.type === 'org:artifact' && currentTab === 'files') paintFilesGrid();
2045
2144
  }
2046
2145
  }
2047
2146