muonroi-cli 1.4.1 → 1.6.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 (194) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +122 -122
  3. package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
  4. package/dist/src/agent-harness/__tests__/mock-model.spec.js +48 -1
  5. package/dist/src/agent-harness/mock-model.d.ts +11 -0
  6. package/dist/src/agent-harness/mock-model.js +21 -0
  7. package/dist/src/cli/cost-forensics.js +12 -12
  8. package/dist/src/council/__tests__/clarification-prompt.test.js +51 -0
  9. package/dist/src/council/__tests__/clarifier-ready-gate.test.js +32 -0
  10. package/dist/src/council/__tests__/decisions-lock.test.js +17 -1
  11. package/dist/src/council/__tests__/oauth-reachable.test.d.ts +1 -0
  12. package/dist/src/council/__tests__/oauth-reachable.test.js +31 -0
  13. package/dist/src/council/__tests__/parse-outcome-fallback.test.js +11 -0
  14. package/dist/src/council/clarifier.js +9 -1
  15. package/dist/src/council/debate.js +5 -1
  16. package/dist/src/council/decisions-lock.js +3 -3
  17. package/dist/src/council/index.js +12 -5
  18. package/dist/src/council/leader.d.ts +0 -17
  19. package/dist/src/council/leader.js +22 -15
  20. package/dist/src/council/planner.js +1 -1
  21. package/dist/src/council/prompts.js +63 -57
  22. package/dist/src/council/types.d.ts +7 -0
  23. package/dist/src/ee/__tests__/ee-onboarding.test.d.ts +1 -0
  24. package/dist/src/ee/__tests__/ee-onboarding.test.js +32 -0
  25. package/dist/src/ee/artifact-cache.d.ts +56 -0
  26. package/dist/src/ee/artifact-cache.js +155 -0
  27. package/dist/src/ee/artifact-cache.test.d.ts +1 -0
  28. package/dist/src/ee/artifact-cache.test.js +69 -0
  29. package/dist/src/ee/auth.d.ts +9 -0
  30. package/dist/src/ee/auth.js +19 -0
  31. package/dist/src/ee/ee-onboarding.d.ts +5 -0
  32. package/dist/src/ee/ee-onboarding.js +76 -0
  33. package/dist/src/ee/search.js +7 -5
  34. package/dist/src/ee/search.test.d.ts +1 -0
  35. package/dist/src/ee/search.test.js +23 -0
  36. package/dist/src/generated/version.d.ts +1 -1
  37. package/dist/src/generated/version.js +1 -1
  38. package/dist/src/headless/output.js +6 -4
  39. package/dist/src/headless/output.test.js +4 -3
  40. package/dist/src/index.js +20 -1
  41. package/dist/src/mcp/__tests__/auto-setup.test.js +74 -0
  42. package/dist/src/mcp/__tests__/client-pool.spec.d.ts +1 -0
  43. package/dist/src/mcp/__tests__/client-pool.spec.js +98 -0
  44. package/dist/src/mcp/__tests__/parallel-build.spec.d.ts +1 -0
  45. package/dist/src/mcp/__tests__/parallel-build.spec.js +67 -0
  46. package/dist/src/mcp/__tests__/smart-filter.test.js +56 -0
  47. package/dist/src/mcp/auto-setup.js +56 -2
  48. package/dist/src/mcp/client-pool.d.ts +46 -0
  49. package/dist/src/mcp/client-pool.js +212 -0
  50. package/dist/src/mcp/oauth-callback.js +2 -2
  51. package/dist/src/mcp/parse-headers.test.js +14 -14
  52. package/dist/src/mcp/runtime.d.ts +28 -0
  53. package/dist/src/mcp/runtime.js +117 -51
  54. package/dist/src/mcp/self-verify-runner.d.ts +14 -0
  55. package/dist/src/mcp/self-verify-runner.js +38 -0
  56. package/dist/src/mcp/setup-guide-text.d.ts +9 -0
  57. package/dist/src/mcp/setup-guide-text.js +84 -0
  58. package/dist/src/mcp/smart-filter.js +49 -0
  59. package/dist/src/mcp/smoke.test.js +43 -43
  60. package/dist/src/mcp/tools-server.d.ts +7 -0
  61. package/dist/src/mcp/tools-server.js +19 -22
  62. package/dist/src/models/catalog.json +349 -349
  63. package/dist/src/ops/__tests__/doctor-ee-health.test.js +21 -0
  64. package/dist/src/ops/doctor.d.ts +3 -2
  65. package/dist/src/ops/doctor.js +47 -11
  66. package/dist/src/ops/doctor.test.js +4 -3
  67. package/dist/src/orchestrator/__tests__/mcp-capability-block.test.d.ts +1 -0
  68. package/dist/src/orchestrator/__tests__/mcp-capability-block.test.js +39 -0
  69. package/dist/src/orchestrator/__tests__/project-stack.test.d.ts +1 -0
  70. package/dist/src/orchestrator/__tests__/project-stack.test.js +65 -0
  71. package/dist/src/orchestrator/batch-turn-runner.js +7 -11
  72. package/dist/src/orchestrator/compaction.d.ts +2 -0
  73. package/dist/src/orchestrator/compaction.js +14 -1
  74. package/dist/src/orchestrator/compaction.test.js +25 -1
  75. package/dist/src/orchestrator/message-processor.js +72 -32
  76. package/dist/src/orchestrator/orchestrator.js +26 -0
  77. package/dist/src/orchestrator/prompts.d.ts +51 -0
  78. package/dist/src/orchestrator/prompts.js +257 -134
  79. package/dist/src/orchestrator/scope-ceiling.js +6 -1
  80. package/dist/src/orchestrator/scope-reminder.d.ts +12 -0
  81. package/dist/src/orchestrator/scope-reminder.js +16 -0
  82. package/dist/src/orchestrator/scope-reminder.test.js +22 -1
  83. package/dist/src/orchestrator/stream-runner.js +23 -15
  84. package/dist/src/orchestrator/subagent-compactor.d.ts +14 -5
  85. package/dist/src/orchestrator/subagent-compactor.js +30 -8
  86. package/dist/src/orchestrator/subagent-compactor.spec.js +18 -0
  87. package/dist/src/orchestrator/text-tool-call-detector.test.js +13 -13
  88. package/dist/src/pil/__tests__/clarity-gate.test.js +24 -215
  89. package/dist/src/pil/__tests__/config.test.js +1 -17
  90. package/dist/src/pil/__tests__/discovery.test.js +144 -11
  91. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +7 -2
  92. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  93. package/dist/src/pil/__tests__/layer16-clarity.test.js +32 -116
  94. package/dist/src/pil/__tests__/layer4-gsd.test.js +37 -0
  95. package/dist/src/pil/__tests__/layer6-output.test.js +158 -18
  96. package/dist/src/pil/__tests__/llm-classify.test.js +49 -2
  97. package/dist/src/pil/__tests__/surface-compaction-artifacts.test.d.ts +1 -0
  98. package/dist/src/pil/__tests__/surface-compaction-artifacts.test.js +112 -0
  99. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  100. package/dist/src/pil/agent-operating-contract.js +2 -0
  101. package/dist/src/pil/agent-operating-contract.test.js +7 -2
  102. package/dist/src/pil/cheap-model-playbook.js +35 -35
  103. package/dist/src/pil/cheap-model-workbooks.js +16 -13
  104. package/dist/src/pil/clarity-gate.d.ts +21 -19
  105. package/dist/src/pil/clarity-gate.js +26 -153
  106. package/dist/src/pil/config.d.ts +9 -1
  107. package/dist/src/pil/config.js +15 -4
  108. package/dist/src/pil/discovery.js +211 -136
  109. package/dist/src/pil/layer1-intent.d.ts +12 -0
  110. package/dist/src/pil/layer1-intent.js +283 -38
  111. package/dist/src/pil/layer1-intent.test.js +210 -4
  112. package/dist/src/pil/layer16-clarity.d.ts +25 -11
  113. package/dist/src/pil/layer16-clarity.js +19 -306
  114. package/dist/src/pil/layer3-ee-injection.d.ts +19 -0
  115. package/dist/src/pil/layer3-ee-injection.js +96 -4
  116. package/dist/src/pil/layer4-gsd.js +18 -6
  117. package/dist/src/pil/layer6-output.d.ts +2 -0
  118. package/dist/src/pil/layer6-output.js +151 -25
  119. package/dist/src/pil/llm-classify.d.ts +26 -0
  120. package/dist/src/pil/llm-classify.js +34 -5
  121. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  122. package/dist/src/pil/native-capabilities-workbook.js +82 -76
  123. package/dist/src/pil/pipeline.js +15 -9
  124. package/dist/src/pil/schema.d.ts +8 -0
  125. package/dist/src/pil/schema.js +12 -1
  126. package/dist/src/pil/task-tier-map.js +4 -0
  127. package/dist/src/pil/types.d.ts +11 -1
  128. package/dist/src/product-loop/done-gate.js +3 -3
  129. package/dist/src/product-loop/loop-driver.js +18 -18
  130. package/dist/src/product-loop/progress-snapshot.js +4 -4
  131. package/dist/src/providers/auth/gemini-oauth.js +6 -15
  132. package/dist/src/providers/auth/grok-oauth.js +6 -15
  133. package/dist/src/providers/auth/openai-oauth.js +6 -15
  134. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  135. package/dist/src/reporter/index.js +1 -1
  136. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  137. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  138. package/dist/src/scaffold/continuation-prompt.js +60 -60
  139. package/dist/src/scaffold/init-new.js +453 -453
  140. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  141. package/dist/src/self-qa/agentic-loop.js +24 -19
  142. package/dist/src/self-qa/spec-emitter.js +26 -23
  143. package/dist/src/storage/__tests__/migrations.test.js +2 -2
  144. package/dist/src/storage/interaction-log.js +5 -5
  145. package/dist/src/storage/migrations.js +122 -122
  146. package/dist/src/storage/sessions.js +42 -42
  147. package/dist/src/storage/transcript.js +91 -84
  148. package/dist/src/storage/usage.js +14 -14
  149. package/dist/src/storage/workspaces.js +12 -12
  150. package/dist/src/tools/__tests__/native-tools.test.d.ts +1 -0
  151. package/dist/src/tools/__tests__/native-tools.test.js +53 -0
  152. package/dist/src/tools/git-safety.d.ts +61 -0
  153. package/dist/src/tools/git-safety.js +141 -0
  154. package/dist/src/tools/git-safety.test.d.ts +1 -0
  155. package/dist/src/tools/git-safety.test.js +111 -0
  156. package/dist/src/tools/native-tools.d.ts +31 -0
  157. package/dist/src/tools/native-tools.js +273 -0
  158. package/dist/src/tools/registry-ee-query.test.js +18 -1
  159. package/dist/src/tools/registry-git-safety.test.d.ts +7 -0
  160. package/dist/src/tools/registry-git-safety.test.js +92 -0
  161. package/dist/src/tools/registry.js +52 -6
  162. package/dist/src/ui/__tests__/markdown-render.test.d.ts +1 -0
  163. package/dist/src/ui/__tests__/markdown-render.test.js +48 -0
  164. package/dist/src/ui/app.js +0 -0
  165. package/dist/src/ui/components/message-view.js +4 -1
  166. package/dist/src/ui/components/structured-response-view.js +7 -3
  167. package/dist/src/ui/components/tool-group.js +7 -1
  168. package/dist/src/ui/markdown-render.d.ts +41 -0
  169. package/dist/src/ui/markdown-render.js +223 -0
  170. package/dist/src/ui/markdown.d.ts +10 -0
  171. package/dist/src/ui/markdown.js +12 -35
  172. package/dist/src/ui/slash/council-inspect.js +4 -4
  173. package/dist/src/ui/slash/export.js +4 -4
  174. package/dist/src/ui/utils/text.d.ts +8 -0
  175. package/dist/src/ui/utils/text.js +16 -0
  176. package/dist/src/ui/utils/text.test.d.ts +1 -0
  177. package/dist/src/ui/utils/text.test.js +23 -0
  178. package/dist/src/usage/ledger.js +48 -15
  179. package/dist/src/utils/__tests__/footprint-gitignore.test.d.ts +1 -0
  180. package/dist/src/utils/__tests__/footprint-gitignore.test.js +50 -0
  181. package/dist/src/utils/clipboard-image.js +23 -23
  182. package/dist/src/utils/open-url.d.ts +56 -0
  183. package/dist/src/utils/open-url.js +58 -0
  184. package/dist/src/utils/open-url.test.d.ts +1 -0
  185. package/dist/src/utils/open-url.test.js +86 -0
  186. package/dist/src/utils/settings.d.ts +12 -0
  187. package/dist/src/utils/settings.js +48 -0
  188. package/dist/src/utils/side-question.js +2 -2
  189. package/dist/src/utils/skills.js +3 -3
  190. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  191. package/dist/src/verify/environment.js +2 -1
  192. package/package.json +1 -1
  193. package/dist/src/pil/layer16-clarity.test.js +0 -31
  194. /package/dist/src/{pil/layer16-clarity.test.d.ts → council/__tests__/clarification-prompt.test.d.ts} +0 -0
@@ -12,7 +12,9 @@ import { needsVisionProxy } from "../providers/vision-proxy.js";
12
12
  import { getBashRun, sliceBashOutput } from "./bash-output-cache.js";
13
13
  import { editFile, readFile, writeFile } from "./file.js";
14
14
  import { FileTracker } from "./file-tracker.js";
15
+ import { analyzeGitCommand, checkPushGate, pushBlockedMessage, recordCommandOutcome, stagingWarning, } from "./git-safety.js";
15
16
  import { executeGrep } from "./grep.js";
17
+ import { registerNativeMuonroiTools } from "./native-tools.js";
16
18
  import { VISION_TOOL_NAMES } from "./vision-gate.js";
17
19
  function getBashRepeatState() {
18
20
  if (!globalThis.__muonroiBashRepeatState) {
@@ -124,6 +126,15 @@ export function createBuiltinTools(bash, mode, opts) {
124
126
  // user turns / askcards no longer wipes it. See getBashRepeatState().
125
127
  const repeatState = getBashRepeatState();
126
128
  const repeatKey = resolveBashRepeatKey(opts?.sessionId);
129
+ // Git-safety state key. MUST be stable across createBuiltinTools() rebuilds
130
+ // within one process — otherwise a failed-test record made before a registry
131
+ // rebuild (askcard answer, sub-agent turn) would be invisible to the push
132
+ // gate after the rebuild. Unlike resolveBashRepeatKey's anon fallback (which
133
+ // intentionally generates a fresh key per instance to isolate repeat-reminder
134
+ // state), we want the gate to PERSIST: use the real sessionId when present,
135
+ // else a single process-stable key. Over-sharing here is the safe direction
136
+ // (it can only over-block a push, never wrongly allow one).
137
+ const gitSafetyKey = opts?.sessionId && opts.sessionId.length > 0 ? opts.sessionId : `__proc_default__:${process.pid}`;
127
138
  tools.bash = dynamicTool({
128
139
  description: "Execute a shell command. Output is automatically cached — every call returns a " +
129
140
  "run_id you can re-query via bash_output_get(run_id, mode=tail|head|grep|lines). " +
@@ -149,19 +160,36 @@ export function createBuiltinTools(bash, mode, opts) {
149
160
  if (typeof input.command !== "string" || input.command.trim() === "") {
150
161
  return 'ERROR: the `bash` tool requires a non-empty "command" string, but the call had empty arguments. Provide the shell command to run, e.g. {"command":"ls -la"}.';
151
162
  }
163
+ const cmd = typeof input.command === "string" ? input.command : "";
164
+ // Git safety (pre-execution). Block `git push` while a verification
165
+ // command failed this session and was not re-run green; warn on broad
166
+ // `git add -A` / `git commit -a` when sensitive paths exist. Applied to
167
+ // BOTH foreground and background paths. See git-safety.ts for the audit
168
+ // motivation (session 18285908637a). gitSafetyKey is STABLE per process
169
+ // (or the real sessionId) — unlike repeatKey, whose anon fallback changes
170
+ // on every registry rebuild and would silently drop the gate across turns.
171
+ const gitShape = analyzeGitCommand(cmd);
172
+ const stageWarn = gitShape.isBroadStage ? stagingWarning(bash.getCwd()) : "";
173
+ if (gitShape.isPush) {
174
+ const gate = checkPushGate(gitSafetyKey);
175
+ if (gate.blocked) {
176
+ return `${pushBlockedMessage(gate.failed)}${stageWarn}`;
177
+ }
178
+ }
152
179
  if (input.background) {
153
180
  const result = await bash.startBackground(input.command);
154
- return formatResult(result);
181
+ return `${formatResult(result)}${stageWarn}`;
155
182
  }
156
183
  // 3-3: compute canonical form BEFORE running so we can attach an
157
184
  // inline reminder if it matches the previous bash call.
158
- const cmd = typeof input.command === "string" ? input.command : "";
159
185
  const canonical = cmd ? canonicalizeBashCommand(cmd) : "";
160
186
  const entry = repeatState.get(repeatKey) ?? { lastCanonical: null, lastRunId: null };
161
187
  const repeatedIntent = canonical !== "" && canonical === entry.lastCanonical && entry.lastRunId !== null;
162
188
  const prevRunId = entry.lastRunId;
163
189
  const result = await bash.execute(input.command, input.timeout ?? 30000);
164
190
  const formatted = formatResult(result);
191
+ // Record verification outcome so a later `git push` can be gated on it.
192
+ recordCommandOutcome(gitSafetyKey, canonical, result.success);
165
193
  // Update last-canonical state AFTER we compared, so the current call's
166
194
  // runId becomes the comparison target for the next one. Session-scoped
167
195
  // map persists across createBuiltinTools() rebuilds (Phase 4R).
@@ -185,9 +213,9 @@ export function createBuiltinTools(bash, mode, opts) {
185
213
  const hint = chars >= 4_000
186
214
  ? ` — ${chars} chars cached; use bash_output_get(run_id, mode=tail|head|grep|lines) to re-query`
187
215
  : "";
188
- return `${formatted}\n\n[bash_run_id: ${result.bashRunId}${hint}]${reminder}`;
216
+ return `${formatted}\n\n[bash_run_id: ${result.bashRunId}${hint}]${reminder}${stageWarn}`;
189
217
  }
190
- return formatted;
218
+ return `${formatted}${stageWarn}`;
191
219
  },
192
220
  });
193
221
  // bash_output_get — re-query the cached full output of a previous bash run.
@@ -438,14 +466,25 @@ export function createBuiltinTools(bash, mode, opts) {
438
466
  }
439
467
  try {
440
468
  if (isToolArtifactQuery(query)) {
441
- // Artifact rehydration raw /api/search (exact-collection lookup).
469
+ // Local-first (anti-mù durability): the compactor records each elided
470
+ // output in-process by toolCallId. For an exact "tool-artifact id=X"
471
+ // lookup this is the authoritative full content for THIS session and
472
+ // works even when EE is down — the failure window long sessions hit.
473
+ const { findArtifactByQuery, findArtifactOnDisk } = await import("../ee/artifact-cache.js");
474
+ const mem = findArtifactByQuery(query);
475
+ const local = mem ?? (await findArtifactOnDisk(query));
476
+ if (local) {
477
+ const src = mem ? "in-session cache" : "local disk cache";
478
+ return truncateOutput(`[tool-artifact id=${local.toolCallId} tool=${local.toolName} — rehydrated from ${src}]\n${local.content}`);
479
+ }
480
+ // EE fallback (cross-session / post-restart) → raw /api/search exact lookup.
442
481
  const { searchEE } = await import("../ee/search.js");
443
482
  const resp = await searchEE(query, {
444
483
  ...(Array.isArray(input?.collections) ? { collections: input.collections } : {}),
445
484
  ...(typeof input?.limit === "number" ? { limit: input.limit } : {}),
446
485
  });
447
486
  if (resp === null) {
448
- return "[ee_unavailable] Experience Engine returned no response (server down, timeout, circuit open, or unconfigured). Proceed without EE recall — re-read the source directly if you need the elided content.";
487
+ return "[ee_unavailable] Experience Engine returned no response (server down, timeout, circuit open, or unconfigured) and the artifact is not in this session's local cache. Proceed without EE recall — re-read the source directly if you need the elided content.";
449
488
  }
450
489
  return truncateOutput(JSON.stringify(resp));
451
490
  }
@@ -473,6 +512,13 @@ export function createBuiltinTools(bash, mode, opts) {
473
512
  }
474
513
  },
475
514
  });
515
+ // Native muonroi-tools builtins — ee_health, ee_feedback, usage_forensics,
516
+ // lsp_query, setup_guide, selfverify_*. These run IN-PROCESS; the CLI no
517
+ // longer self-spawns itself as an MCP server to expose them to its own inner
518
+ // agent (that self-spawn cold-started 2-3.5s and overran the build deadline,
519
+ // and a seed-time bug once persisted a crashing vitest-worker command). The
520
+ // muonroi-tools MCP server stays only for EXTERNAL agents. See native-tools.ts.
521
+ registerNativeMuonroiTools(tools, { cwd: bash.getCwd() });
476
522
  }
477
523
  // Vision proxy tools — only for text-only models (DeepSeek, etc.)
478
524
  if (opts?.modelId && needsVisionProxy(opts.modelId)) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseInline } from "../markdown-render.js";
3
+ import { dark } from "../theme.js";
4
+ const t = dark;
5
+ const text = (segs) => segs.map((s) => s.text).join("");
6
+ describe("parseInline — marker concealment", () => {
7
+ it("strips bold markers and styles the inner text", () => {
8
+ const segs = parseInline("a **bold** b", t);
9
+ expect(text(segs)).toBe("a bold b");
10
+ const bold = segs.find((s) => s.text === "bold");
11
+ expect(bold?.bold).toBe(true);
12
+ expect(bold?.fg).toBe(t.mdBold);
13
+ });
14
+ it("strips italic markers (* and _)", () => {
15
+ expect(text(parseInline("an *italic* word", t))).toBe("an italic word");
16
+ expect(text(parseInline("an _italic_ word", t))).toBe("an italic word");
17
+ expect(parseInline("an *italic* word", t).find((s) => s.text === "italic")?.italic).toBe(true);
18
+ });
19
+ it("handles bold+italic ***x***", () => {
20
+ const seg = parseInline("***wow***", t).find((s) => s.text === "wow");
21
+ expect(seg?.bold).toBe(true);
22
+ expect(seg?.italic).toBe(true);
23
+ });
24
+ it("strips inline code backticks and colors it", () => {
25
+ const segs = parseInline("call `.catch(next)` here", t);
26
+ expect(text(segs)).toBe("call .catch(next) here");
27
+ expect(segs.find((s) => s.text === ".catch(next)")?.fg).toBe(t.mdCode);
28
+ });
29
+ it("renders link label only, dropping the url", () => {
30
+ const segs = parseInline("see [the docs](https://x/y) now", t);
31
+ expect(text(segs)).toBe("see the docs now");
32
+ expect(text(segs)).not.toContain("https://x/y");
33
+ expect(segs.find((s) => s.text === "the docs")?.underline).toBe(true);
34
+ });
35
+ it("strips strikethrough ~~x~~", () => {
36
+ expect(text(parseInline("~~gone~~", t))).toBe("gone");
37
+ });
38
+ it("leaves unterminated markers as literal text (streaming-safe)", () => {
39
+ expect(text(parseInline("a **partial answer", t))).toBe("a **partial answer");
40
+ expect(text(parseInline("trailing `code", t))).toBe("trailing `code");
41
+ });
42
+ it("never leaves ** ` ### markers in styled segments", () => {
43
+ const sample = "**A** and `b` and ***c*** and [d](http://e) and ~~f~~";
44
+ const out = text(parseInline(sample, t));
45
+ expect(out).not.toMatch(/\*\*|`|~~|\]\(/);
46
+ });
47
+ });
48
+ //# sourceMappingURL=markdown-render.test.js.map
Binary file
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/reac
2
2
  import { memo } from "react";
3
3
  import { Markdown } from "../markdown.js";
4
4
  import { PlanView } from "../plan.js";
5
- import { trunc } from "../utils/text.js";
5
+ import { stripStrayModelMacros, trunc } from "../utils/text.js";
6
6
  import { describeMcpFsTool, toolArgs, toolLabel, tryParseArg } from "../utils/tools.js";
7
7
  import { DiffView, ReadFilePreviewView } from "./diff-view.js";
8
8
  import { LspDiagnosticsView, LspResultView } from "./lsp-views.js";
@@ -29,6 +29,9 @@ const USER_MSG_COLLAPSED_LINES = 5;
29
29
  // cost. 8 fits comfortably on a short terminal and conveys the gist.
30
30
  const ASSISTANT_MSG_COLLAPSED_LINES = 8;
31
31
  export function AssistantMessageContent({ content, t, expanded, isFinal, }) {
32
+ // Strip stray model self-annotation macros (e.g. grok's trailing
33
+ // `\confidence{85}`) that leak into the answer — not instructed in the prompt.
34
+ content = stripStrayModelMacros(content);
32
35
  const lines = content.split("\n");
33
36
  const isLong = lines.length > ASSISTANT_MSG_COLLAPSED_LINES;
34
37
  // Phase 5 F7 — the FINAL assistant message in a turn IS the answer the
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
+ import { renderMarkdown } from "../markdown-render.js";
2
3
  export function StructuredResponseView({ t, sr, modeColor }) {
3
4
  const d = sr.data;
4
5
  switch (sr.taskType) {
@@ -27,17 +28,20 @@ export function StructuredResponseView({ t, sr, modeColor }) {
27
28
  }
28
29
  case "documentation": {
29
30
  const r = d;
30
- return (_jsxs("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: [_jsx("text", { children: r.content }), (r.examples ?? []).map((ex, i) => (_jsxs("box", { flexDirection: "column", marginTop: 1, children: [_jsx("text", { fg: t.textMuted, children: ex.description }), _jsx("text", { fg: t.mdCode, children: ex.code })] }, `de${i}`)))] }));
31
+ return (_jsxs("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: [r.content ? renderMarkdown(r.content, t) : null, (r.examples ?? []).map((ex, i) => (_jsxs("box", { flexDirection: "column", marginTop: 1, children: [_jsx("text", { fg: t.textMuted, children: ex.description }), _jsx("text", { fg: t.mdCode, children: ex.code })] }, `de${i}`)))] }));
31
32
  }
32
33
  case "generate": {
33
34
  const r = d;
34
35
  return (_jsxs("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: [r.explanation && _jsx("text", { fg: t.textMuted, children: r.explanation }), (r.files ?? []).map((f, i) => (_jsxs("box", { flexDirection: "column", marginTop: 1, children: [_jsxs("text", { children: [_jsx("span", { style: { fg: t.accent }, children: "── " }), _jsx("span", { style: { fg: t.accent }, children: f.path }), _jsx("span", { style: { fg: t.textMuted }, children: ` (${f.language})` }), _jsx("span", { style: { fg: t.accent }, children: " ──" })] }), _jsx("text", { fg: t.mdCodeBlockFg, children: f.content })] }, `gf${i}`)))] }));
35
36
  }
36
37
  case "general": {
38
+ // `reasoning` is the model's internal justification — deliberately NOT
39
+ // surfaced (it leaked as a "── reasoning:" tail and reads as process
40
+ // narration). The user-facing answer is `response`, rendered as markdown.
37
41
  const g = d;
38
42
  if (!g.response)
39
43
  return _jsx("text", { fg: t.textMuted, children: JSON.stringify(d, null, 2) });
40
- return (_jsxs("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: [_jsx("text", { fg: t.text, children: g.response }), g.reasoning ? (_jsxs("text", { fg: t.textMuted, children: [" ── reasoning: ", g.reasoning] })) : null] }));
44
+ return (_jsx("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: renderMarkdown(g.response, t) }));
41
45
  }
42
46
  default: {
43
47
  // Graceful fallback for taskTypes without a dedicated renderer (e.g. a new
@@ -50,7 +54,7 @@ export function StructuredResponseView({ t, sr, modeColor }) {
50
54
  (typeof obj.text === "string" && obj.text) ||
51
55
  null;
52
56
  if (primary) {
53
- return (_jsxs("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: [_jsx("text", { fg: t.text, children: primary }), _jsx("text", { fg: t.textMuted, children: ` ── (renderer missing for taskType: ${sr.taskType})` })] }));
57
+ return (_jsxs("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: [renderMarkdown(primary, t), _jsx("text", { fg: t.textMuted, children: ` ── (renderer missing for taskType: ${sr.taskType})` })] }));
54
58
  }
55
59
  return _jsx("text", { fg: t.text, children: JSON.stringify(d, null, 2) });
56
60
  }
@@ -15,6 +15,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
15
15
  import { Semantic } from "@muonroi/agent-harness-opentui";
16
16
  import { trunc } from "../utils/text.js";
17
17
  import { dominantVerb, toolLabel } from "../utils/tools.js";
18
+ import { DiffView } from "./diff-view.js";
19
+ // Tools whose result carries a FileDiff worth rendering inline under the item
20
+ // line. Mirrors the per-tool branch in message-view.tsx so grouped edits show
21
+ // the same +/- diff a non-grouped tool_result would.
22
+ const DIFF_TOOLS = new Set(["write_file", "edit_file"]);
18
23
  // Max items rendered inline while a group is active. Anything beyond gets a
19
24
  // "+N more (ctrl+e expand)" affordance — matches Claude Code's overflow line.
20
25
  const ACTIVE_INLINE_LIMIT = 8;
@@ -79,7 +84,8 @@ export function ToolGroupView({ entry, t, expanded, modeColor }) {
79
84
  return (_jsx(Semantic, { id: `tool-group-${g.id}`, role: "region", name: headerVerb, state: g.state, children: _jsxs("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { style: { fg: headerColor }, children: "● " }), _jsx("span", { style: { fg: t.text }, children: headerVerb }), _jsx("span", { style: { fg: t.textMuted }, children: ` (${stats})` })] }), showItems && visibleItems.length > 0 && (_jsxs("box", { paddingLeft: 2, flexDirection: "column", children: [overflow > 0 && (_jsx("text", { fg: t.textMuted, children: `… +${overflow} earlier ${overflow > 1 ? "tools" : "tool"} (ctrl+e to expand)` })), visibleItems.map((it) => {
80
85
  const label = trunc(toolLabel(it.toolCall), 90);
81
86
  const errSuffix = it.failed && it.result?.error ? ` — ${trunc(it.result.error.replace(/\s+/g, " "), 60)}` : "";
82
- return (_jsxs("text", { children: [_jsx("span", { style: { fg: itemColor(it, t) }, children: `${itemGlyph(it)} ` }), _jsx("span", { style: { fg: t.textMuted }, children: label }), errSuffix && _jsx("span", { style: { fg: t.diffRemovedFg }, children: errSuffix })] }, it.toolCall.id));
87
+ const diff = !it.failed && DIFF_TOOLS.has(it.toolCall.function.name) ? it.result?.diff : undefined;
88
+ return (_jsxs("box", { flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { style: { fg: itemColor(it, t) }, children: `${itemGlyph(it)} ` }), _jsx("span", { style: { fg: t.textMuted }, children: label }), errSuffix && _jsx("span", { style: { fg: t.diffRemovedFg }, children: errSuffix })] }), diff && _jsx(DiffView, { t: t, diff: diff })] }, it.toolCall.id));
83
89
  })] })), g.state === "done" && !expanded && total > 0 && (_jsx("box", { paddingLeft: 2, children: _jsx("text", { fg: t.textDim, children: "ctrl+e to expand" }) }))] }) }));
84
90
  }
85
91
  //# sourceMappingURL=tool-group.js.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * src/ui/markdown-render.tsx
3
+ *
4
+ * Self-contained markdown → styled-text renderer for the TUI.
5
+ *
6
+ * Why this exists: the bundled `@opentui/core` MarkdownRenderable (v0.1.107)
7
+ * does NOT support concealing syntax markers — the `conceal` prop passed to
8
+ * `<markdown>` is silently ignored, so `**bold**`, `### heading`, `` `code` ``
9
+ * and `- bullet` all render with their literal markers visible (verified via
10
+ * @opentui/react testRender). On top of that the tree-sitter wasm used for
11
+ * highlight often fails to load, leaving raw unstyled text. The result reads
12
+ * like machine output rather than a rendered answer.
13
+ *
14
+ * This renderer parses the common markdown constructs an LLM answer uses and
15
+ * emits opentui `<text>`/`<span>` nodes with theme colors and the markers
16
+ * stripped. It is intentionally pragmatic (not CommonMark-complete): headings,
17
+ * bold, italic, bold+italic, inline code, strikethrough, links, ordered and
18
+ * unordered lists, blockquotes, fenced code blocks, and horizontal rules.
19
+ * Unterminated inline markers degrade to literal text (streaming-safe).
20
+ */
21
+ import type { ReactNode } from "react";
22
+ import type { Theme } from "./theme.js";
23
+ export interface InlineSegment {
24
+ text: string;
25
+ fg?: string;
26
+ bold?: boolean;
27
+ italic?: boolean;
28
+ underline?: boolean;
29
+ strike?: boolean;
30
+ }
31
+ /**
32
+ * Parse a single line of inline markdown into styled segments, with markers
33
+ * removed. `base` carries the inherited style (used when recursing into the
34
+ * body of a bold/italic span so emphasis nests).
35
+ */
36
+ export declare function parseInline(line: string, t: Theme, base?: Partial<InlineSegment>): InlineSegment[];
37
+ /**
38
+ * Render markdown `content` into themed opentui nodes with all syntax markers
39
+ * concealed. Returns a column box; safe to embed inside any flex container.
40
+ */
41
+ export declare function renderMarkdown(content: string, t: Theme): ReactNode;
@@ -0,0 +1,223 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
+ import { detectLang, tokenize } from "./syntax-highlight.js";
3
+ // Inline markers, longest-first so `***` wins over `**`/`*` and `~~` is whole.
4
+ const INLINE = [
5
+ { open: "***", close: "***", bold: true, italic: true },
6
+ { open: "___", close: "___", bold: true, italic: true },
7
+ { open: "**", close: "**", bold: true },
8
+ { open: "__", close: "__", bold: true },
9
+ { open: "~~", close: "~~", strike: true },
10
+ { open: "*", close: "*", italic: true },
11
+ { open: "_", close: "_", italic: true },
12
+ ];
13
+ function pushText(out, text, base) {
14
+ if (!text)
15
+ return;
16
+ const last = out[out.length - 1];
17
+ // Merge adjacent plain segments to keep the span count low.
18
+ if (last && !last.fg && !last.bold && !last.italic && !last.underline && !last.strike && isPlain(base)) {
19
+ last.text += text;
20
+ return;
21
+ }
22
+ out.push({ text, ...base });
23
+ }
24
+ function isPlain(b) {
25
+ return !b.fg && !b.bold && !b.italic && !b.underline && !b.strike;
26
+ }
27
+ /**
28
+ * Parse a single line of inline markdown into styled segments, with markers
29
+ * removed. `base` carries the inherited style (used when recursing into the
30
+ * body of a bold/italic span so emphasis nests).
31
+ */
32
+ export function parseInline(line, t, base = {}) {
33
+ const out = [];
34
+ let i = 0;
35
+ let plainStart = 0;
36
+ const flushPlain = (end) => {
37
+ if (end > plainStart)
38
+ pushText(out, line.slice(plainStart, end), base);
39
+ };
40
+ while (i < line.length) {
41
+ const ch = line[i];
42
+ // Inline code: `code` — highest precedence, no nested formatting inside.
43
+ if (ch === "`") {
44
+ const end = line.indexOf("`", i + 1);
45
+ if (end > i) {
46
+ flushPlain(i);
47
+ out.push({ text: line.slice(i + 1, end), fg: t.mdCode });
48
+ i = end + 1;
49
+ plainStart = i;
50
+ continue;
51
+ }
52
+ }
53
+ // Link: [label](url) — show the label as an underlined link, drop the url.
54
+ if (ch === "[") {
55
+ const close = line.indexOf("]", i + 1);
56
+ if (close > i && line[close + 1] === "(") {
57
+ const urlEnd = line.indexOf(")", close + 2);
58
+ if (urlEnd > close) {
59
+ flushPlain(i);
60
+ const label = line.slice(i + 1, close);
61
+ for (const seg of parseInline(label, t, { ...base, fg: t.mdLinkText, underline: true }))
62
+ out.push(seg);
63
+ i = urlEnd + 1;
64
+ plainStart = i;
65
+ continue;
66
+ }
67
+ }
68
+ }
69
+ // Emphasis markers.
70
+ let matched = false;
71
+ for (const m of INLINE) {
72
+ if (!line.startsWith(m.open, i))
73
+ continue;
74
+ const close = line.indexOf(m.close, i + m.open.length);
75
+ if (close < 0)
76
+ continue; // unterminated → treat as literal
77
+ const inner = line.slice(i + m.open.length, close);
78
+ if (inner.length === 0)
79
+ continue;
80
+ flushPlain(i);
81
+ const isBold = "bold" in m && m.bold;
82
+ const isItalic = "italic" in m && m.italic;
83
+ const isStrike = "strike" in m && m.strike;
84
+ const childBase = {
85
+ ...base,
86
+ bold: base.bold || isBold || undefined,
87
+ italic: base.italic || isItalic || undefined,
88
+ strike: base.strike || isStrike || undefined,
89
+ fg: base.fg ?? (isBold ? t.mdBold : isItalic ? t.mdItalic : isStrike ? t.textMuted : undefined),
90
+ };
91
+ for (const seg of parseInline(inner, t, childBase))
92
+ out.push(seg);
93
+ i = close + m.close.length;
94
+ plainStart = i;
95
+ matched = true;
96
+ break;
97
+ }
98
+ if (matched)
99
+ continue;
100
+ i++;
101
+ }
102
+ flushPlain(line.length);
103
+ return out;
104
+ }
105
+ const FENCE = /^\s*(`{3,}|~{3,})\s*([\w+-]*)\s*$/;
106
+ const HEADING = /^(#{1,6})\s+(.*)$/;
107
+ const HR = /^\s*([-*_])\1{2,}\s*$/;
108
+ const BULLET = /^(\s*)[-*+]\s+(.*)$/;
109
+ const ORDERED = /^(\s*)(\d+)[.)]\s+(.*)$/;
110
+ const QUOTE = /^\s*>\s?(.*)$/;
111
+ function parseBlocks(content, t) {
112
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
113
+ const blocks = [];
114
+ let i = 0;
115
+ while (i < lines.length) {
116
+ const line = lines[i];
117
+ const fence = FENCE.exec(line);
118
+ if (fence) {
119
+ const marker = fence[1];
120
+ const lang = fence[2] ?? "";
121
+ const body = [];
122
+ i++;
123
+ while (i < lines.length && !new RegExp(`^\\s*${marker[0]}{${marker.length},}\\s*$`).test(lines[i])) {
124
+ body.push(lines[i]);
125
+ i++;
126
+ }
127
+ i++; // consume closing fence (or run off the end — streaming-safe)
128
+ blocks.push({ kind: "code", lang, lines: body });
129
+ continue;
130
+ }
131
+ if (line.trim() === "") {
132
+ blocks.push({ kind: "blank" });
133
+ i++;
134
+ continue;
135
+ }
136
+ if (HR.test(line)) {
137
+ blocks.push({ kind: "hr" });
138
+ i++;
139
+ continue;
140
+ }
141
+ const h = HEADING.exec(line);
142
+ if (h) {
143
+ blocks.push({ kind: "heading", level: h[1].length, segs: parseInline(h[2], t, { fg: t.mdHeading, bold: true }) });
144
+ i++;
145
+ continue;
146
+ }
147
+ const q = QUOTE.exec(line);
148
+ if (q) {
149
+ blocks.push({ kind: "quote", segs: parseInline(q[1], t, { fg: t.mdItalic, italic: true }) });
150
+ i++;
151
+ continue;
152
+ }
153
+ const b = BULLET.exec(line);
154
+ if (b) {
155
+ blocks.push({ kind: "bullet", indent: b[1].length, segs: parseInline(b[2], t) });
156
+ i++;
157
+ continue;
158
+ }
159
+ const o = ORDERED.exec(line);
160
+ if (o) {
161
+ blocks.push({ kind: "ordered", indent: o[1].length, marker: `${o[2]}.`, segs: parseInline(o[3], t) });
162
+ i++;
163
+ continue;
164
+ }
165
+ blocks.push({ kind: "para", segs: parseInline(line, t) });
166
+ i++;
167
+ }
168
+ return blocks;
169
+ }
170
+ function Spans({ segs }) {
171
+ // opentui sets bold/italic/underline via the <b>/<i>/<u> text-modifier
172
+ // elements, NOT via span style flags. Compose them around a colored <span>.
173
+ return (_jsx(_Fragment, { children: segs.map((s, i) => {
174
+ // biome-ignore lint/suspicious/noArrayIndexKey: segments are positional within a line
175
+ let node = (_jsx("span", { style: s.fg ? { fg: s.fg } : undefined, children: s.text }, `s${i}`));
176
+ if (s.underline)
177
+ node = _jsx("u", { children: node }, `u${i}`);
178
+ if (s.italic)
179
+ node = _jsx("i", { children: node }, `i${i}`);
180
+ if (s.bold)
181
+ node = _jsx("b", { children: node }, `b${i}`);
182
+ return node;
183
+ }) }));
184
+ }
185
+ /**
186
+ * Render markdown `content` into themed opentui nodes with all syntax markers
187
+ * concealed. Returns a column box; safe to embed inside any flex container.
188
+ */
189
+ export function renderMarkdown(content, t) {
190
+ const blocks = parseBlocks(content, t);
191
+ return (_jsx("box", { flexDirection: "column", flexShrink: 0, children: blocks.map((blk, idx) => {
192
+ // biome-ignore lint/suspicious/noArrayIndexKey: blocks are positional and re-rendered wholesale
193
+ const key = `b${idx}`;
194
+ switch (blk.kind) {
195
+ case "blank":
196
+ return _jsx("box", { height: 1 }, key);
197
+ case "hr":
198
+ return (_jsx("text", { fg: t.mdHr, children: "─".repeat(40) }, key));
199
+ case "heading":
200
+ return (_jsx("text", { marginTop: idx === 0 ? 0 : 1, children: _jsx(Spans, { segs: blk.segs }) }, key));
201
+ case "quote":
202
+ return (_jsxs("text", { children: [_jsx("span", { style: { fg: t.mdHr }, children: "▏ " }), _jsx(Spans, { segs: blk.segs })] }, key));
203
+ case "bullet":
204
+ return (_jsxs("text", { children: [_jsx("span", { style: { fg: t.text }, children: " ".repeat(blk.indent) }), _jsx("span", { style: { fg: t.mdListBullet }, children: "• " }), _jsx(Spans, { segs: blk.segs })] }, key));
205
+ case "ordered":
206
+ return (_jsxs("text", { children: [_jsx("span", { style: { fg: t.text }, children: " ".repeat(blk.indent) }), _jsx("span", { style: { fg: t.mdListBullet }, children: `${blk.marker} ` }), _jsx(Spans, { segs: blk.segs })] }, key));
207
+ case "code": {
208
+ const lang = detectLang(`x.${blk.lang || "txt"}`);
209
+ return (_jsx("box", { backgroundColor: t.mdCodeBlockBg, paddingLeft: 1, paddingRight: 1, flexDirection: "column", children: blk.lines.map((ln, j) => {
210
+ const toks = lang === "plain" ? [] : tokenize(ln, lang, t);
211
+ return (
212
+ // biome-ignore lint/suspicious/noArrayIndexKey: code lines are positional
213
+ _jsx("text", { children: toks.length === 0 ? (_jsx("span", { style: { fg: t.mdCodeBlockFg }, children: ln || " " })) : (toks.map((tok, k) => (
214
+ // biome-ignore lint/suspicious/noArrayIndexKey: token positions are stable per render
215
+ _jsx("span", { style: { fg: tok.fg }, children: tok.text }, `t${k}`)))) }, `c${j}`));
216
+ }) }, key));
217
+ }
218
+ default:
219
+ return (_jsx("text", { children: _jsx(Spans, { segs: blk.segs }) }, key));
220
+ }
221
+ }) }));
222
+ }
223
+ //# sourceMappingURL=markdown-render.js.map
@@ -1,4 +1,14 @@
1
1
  import type { Theme } from "./theme.js";
2
+ /**
3
+ * Render markdown to themed TUI text.
4
+ *
5
+ * Historically this delegated to opentui's `<markdown>` renderable with
6
+ * `conceal`, but the bundled @opentui/core (0.1.107) ignores `conceal` and
7
+ * leaves `**`/`###`/`` ` `` markers visible (and frequently fails to load the
8
+ * tree-sitter wasm for highlighting). We now render markdown ourselves via
9
+ * `renderMarkdown`, which strips markers and applies theme colors. See
10
+ * markdown-render.tsx for the construct coverage.
11
+ */
2
12
  export declare function Markdown({ content, t }: {
3
13
  content: string;
4
14
  t: Theme;
@@ -1,38 +1,15 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import { RGBA, SyntaxStyle } from "@opentui/core";
3
- import { useMemo } from "react";
4
- function buildSyntaxStyle(t) {
5
- return SyntaxStyle.fromStyles({
6
- default: { fg: RGBA.fromHex(t.text) },
7
- "markup.heading": { fg: RGBA.fromHex(t.mdHeading), bold: true },
8
- "markup.heading.1": { fg: RGBA.fromHex(t.mdHeading), bold: true },
9
- "markup.heading.2": { fg: RGBA.fromHex(t.mdHeading), bold: true },
10
- "markup.heading.3": { fg: RGBA.fromHex(t.mdHeading), bold: true },
11
- "markup.bold": { fg: RGBA.fromHex(t.mdBold), bold: true },
12
- "markup.italic": { fg: RGBA.fromHex(t.mdItalic), italic: true },
13
- "markup.raw": { fg: RGBA.fromHex(t.mdCode) },
14
- "markup.link": { fg: RGBA.fromHex(t.mdLink), underline: true },
15
- "markup.link.label": { fg: RGBA.fromHex(t.mdLinkText) },
16
- "markup.list": { fg: RGBA.fromHex(t.mdListBullet) },
17
- "markup.quote": { fg: RGBA.fromHex(t.mdItalic), italic: true },
18
- "markup.separator": { fg: RGBA.fromHex(t.mdHr) },
19
- code: { fg: RGBA.fromHex(t.mdCodeBlockFg), bg: RGBA.fromHex(t.mdCodeBlockBg) },
20
- });
21
- }
22
- const TABLE_OPTIONS = {
23
- widthMode: "full",
24
- columnFitter: "balanced",
25
- wrapMode: "word",
26
- cellPadding: 1,
27
- borders: true,
28
- outerBorder: true,
29
- borderStyle: "rounded",
30
- borderColor: "#333333",
31
- };
1
+ import { renderMarkdown } from "./markdown-render.js";
2
+ /**
3
+ * Render markdown to themed TUI text.
4
+ *
5
+ * Historically this delegated to opentui's `<markdown>` renderable with
6
+ * `conceal`, but the bundled @opentui/core (0.1.107) ignores `conceal` and
7
+ * leaves `**`/`###`/`` ` `` markers visible (and frequently fails to load the
8
+ * tree-sitter wasm for highlighting). We now render markdown ourselves via
9
+ * `renderMarkdown`, which strips markers and applies theme colors. See
10
+ * markdown-render.tsx for the construct coverage.
11
+ */
32
12
  export function Markdown({ content, t }) {
33
- const syntaxStyle = useMemo(() => buildSyntaxStyle(t), [t]);
34
- return (_jsx("markdown", { content: content, syntaxStyle: syntaxStyle, conceal: true,
35
- // @ts-expect-error MarkdownProps omits inherited Renderable.selectable; needed for TUI text selection
36
- selectable: true, tableOptions: TABLE_OPTIONS, flexShrink: 0 }));
13
+ return renderMarkdown(content, t);
37
14
  }
38
15
  //# sourceMappingURL=markdown.js.map
@@ -37,10 +37,10 @@ export const handleCouncilInspectSlash = async (args) => {
37
37
  }
38
38
  // Load all system messages for this session — parameterized query prevents SQL injection (T-17-04)
39
39
  const rows = db
40
- .prepare(`SELECT role, message_json, seq, created_at
41
- FROM messages
42
- WHERE session_id = ?
43
- AND role = 'system'
40
+ .prepare(`SELECT role, message_json, seq, created_at
41
+ FROM messages
42
+ WHERE session_id = ?
43
+ AND role = 'system'
44
44
  ORDER BY seq ASC`)
45
45
  .all(sessionId);
46
46
  if (rows.length === 0) {
@@ -25,10 +25,10 @@ function selectInteractionTimeline(sessionId) {
25
25
  try {
26
26
  const db = getDatabase();
27
27
  return db
28
- .prepare(`SELECT event_type, event_subtype, metadata_json, created_at
29
- FROM interaction_logs
30
- WHERE session_id = ?
31
- AND event_type IN ('ui_interaction', 'routing', 'council', 'ee_injection')
28
+ .prepare(`SELECT event_type, event_subtype, metadata_json, created_at
29
+ FROM interaction_logs
30
+ WHERE session_id = ?
31
+ AND event_type IN ('ui_interaction', 'routing', 'council', 'ee_injection')
32
32
  ORDER BY created_at ASC, id ASC`)
33
33
  .all(sessionId);
34
34
  }
@@ -4,3 +4,11 @@ export declare function truncateLine(s: string, n: number): string;
4
4
  export declare function truncateBlock(text: string, maxLines: number): string;
5
5
  export declare function compactTaskLabel(label: string): string;
6
6
  export declare function sanitizeContent(raw: string): string;
7
+ /**
8
+ * Strip stray model self-annotation macros that leak into the user-facing answer
9
+ * but are NOT instructed anywhere in the prompt. Currently: a trailing
10
+ * `\confidence{NN}` macro emitted intermittently by grok-build. Conservative —
11
+ * only the `\confidence{...}` form is removed, so legitimate LaTeX/code in an
12
+ * answer (e.g. `\frac{a}{b}`) is untouched. Fast-pathed: no work when absent.
13
+ */
14
+ export declare function stripStrayModelMacros(text: string): string;