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.
- package/LICENSE +21 -21
- package/README.md +122 -122
- package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
- package/dist/src/agent-harness/__tests__/mock-model.spec.js +48 -1
- package/dist/src/agent-harness/mock-model.d.ts +11 -0
- package/dist/src/agent-harness/mock-model.js +21 -0
- package/dist/src/cli/cost-forensics.js +12 -12
- package/dist/src/council/__tests__/clarification-prompt.test.js +51 -0
- package/dist/src/council/__tests__/clarifier-ready-gate.test.js +32 -0
- package/dist/src/council/__tests__/decisions-lock.test.js +17 -1
- package/dist/src/council/__tests__/oauth-reachable.test.d.ts +1 -0
- package/dist/src/council/__tests__/oauth-reachable.test.js +31 -0
- package/dist/src/council/__tests__/parse-outcome-fallback.test.js +11 -0
- package/dist/src/council/clarifier.js +9 -1
- package/dist/src/council/debate.js +5 -1
- package/dist/src/council/decisions-lock.js +3 -3
- package/dist/src/council/index.js +12 -5
- package/dist/src/council/leader.d.ts +0 -17
- package/dist/src/council/leader.js +22 -15
- package/dist/src/council/planner.js +1 -1
- package/dist/src/council/prompts.js +63 -57
- package/dist/src/council/types.d.ts +7 -0
- package/dist/src/ee/__tests__/ee-onboarding.test.d.ts +1 -0
- package/dist/src/ee/__tests__/ee-onboarding.test.js +32 -0
- package/dist/src/ee/artifact-cache.d.ts +56 -0
- package/dist/src/ee/artifact-cache.js +155 -0
- package/dist/src/ee/artifact-cache.test.d.ts +1 -0
- package/dist/src/ee/artifact-cache.test.js +69 -0
- package/dist/src/ee/auth.d.ts +9 -0
- package/dist/src/ee/auth.js +19 -0
- package/dist/src/ee/ee-onboarding.d.ts +5 -0
- package/dist/src/ee/ee-onboarding.js +76 -0
- package/dist/src/ee/search.js +7 -5
- package/dist/src/ee/search.test.d.ts +1 -0
- package/dist/src/ee/search.test.js +23 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/headless/output.js +6 -4
- package/dist/src/headless/output.test.js +4 -3
- package/dist/src/index.js +20 -1
- package/dist/src/mcp/__tests__/auto-setup.test.js +74 -0
- package/dist/src/mcp/__tests__/client-pool.spec.d.ts +1 -0
- package/dist/src/mcp/__tests__/client-pool.spec.js +98 -0
- package/dist/src/mcp/__tests__/parallel-build.spec.d.ts +1 -0
- package/dist/src/mcp/__tests__/parallel-build.spec.js +67 -0
- package/dist/src/mcp/__tests__/smart-filter.test.js +56 -0
- package/dist/src/mcp/auto-setup.js +56 -2
- package/dist/src/mcp/client-pool.d.ts +46 -0
- package/dist/src/mcp/client-pool.js +212 -0
- package/dist/src/mcp/oauth-callback.js +2 -2
- package/dist/src/mcp/parse-headers.test.js +14 -14
- package/dist/src/mcp/runtime.d.ts +28 -0
- package/dist/src/mcp/runtime.js +117 -51
- package/dist/src/mcp/self-verify-runner.d.ts +14 -0
- package/dist/src/mcp/self-verify-runner.js +38 -0
- package/dist/src/mcp/setup-guide-text.d.ts +9 -0
- package/dist/src/mcp/setup-guide-text.js +84 -0
- package/dist/src/mcp/smart-filter.js +49 -0
- package/dist/src/mcp/smoke.test.js +43 -43
- package/dist/src/mcp/tools-server.d.ts +7 -0
- package/dist/src/mcp/tools-server.js +19 -22
- package/dist/src/models/catalog.json +349 -349
- package/dist/src/ops/__tests__/doctor-ee-health.test.js +21 -0
- package/dist/src/ops/doctor.d.ts +3 -2
- package/dist/src/ops/doctor.js +47 -11
- package/dist/src/ops/doctor.test.js +4 -3
- package/dist/src/orchestrator/__tests__/mcp-capability-block.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/mcp-capability-block.test.js +39 -0
- package/dist/src/orchestrator/__tests__/project-stack.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/project-stack.test.js +65 -0
- package/dist/src/orchestrator/batch-turn-runner.js +7 -11
- package/dist/src/orchestrator/compaction.d.ts +2 -0
- package/dist/src/orchestrator/compaction.js +14 -1
- package/dist/src/orchestrator/compaction.test.js +25 -1
- package/dist/src/orchestrator/message-processor.js +72 -32
- package/dist/src/orchestrator/orchestrator.js +26 -0
- package/dist/src/orchestrator/prompts.d.ts +51 -0
- package/dist/src/orchestrator/prompts.js +257 -134
- package/dist/src/orchestrator/scope-ceiling.js +6 -1
- package/dist/src/orchestrator/scope-reminder.d.ts +12 -0
- package/dist/src/orchestrator/scope-reminder.js +16 -0
- package/dist/src/orchestrator/scope-reminder.test.js +22 -1
- package/dist/src/orchestrator/stream-runner.js +23 -15
- package/dist/src/orchestrator/subagent-compactor.d.ts +14 -5
- package/dist/src/orchestrator/subagent-compactor.js +30 -8
- package/dist/src/orchestrator/subagent-compactor.spec.js +18 -0
- package/dist/src/orchestrator/text-tool-call-detector.test.js +13 -13
- package/dist/src/pil/__tests__/clarity-gate.test.js +24 -215
- package/dist/src/pil/__tests__/config.test.js +1 -17
- package/dist/src/pil/__tests__/discovery.test.js +144 -11
- package/dist/src/pil/__tests__/layer1-intent-trace.test.js +7 -2
- package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
- package/dist/src/pil/__tests__/layer16-clarity.test.js +32 -116
- package/dist/src/pil/__tests__/layer4-gsd.test.js +37 -0
- package/dist/src/pil/__tests__/layer6-output.test.js +158 -18
- package/dist/src/pil/__tests__/llm-classify.test.js +49 -2
- package/dist/src/pil/__tests__/surface-compaction-artifacts.test.d.ts +1 -0
- package/dist/src/pil/__tests__/surface-compaction-artifacts.test.js +112 -0
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +2 -0
- package/dist/src/pil/agent-operating-contract.test.js +7 -2
- package/dist/src/pil/cheap-model-playbook.js +35 -35
- package/dist/src/pil/cheap-model-workbooks.js +16 -13
- package/dist/src/pil/clarity-gate.d.ts +21 -19
- package/dist/src/pil/clarity-gate.js +26 -153
- package/dist/src/pil/config.d.ts +9 -1
- package/dist/src/pil/config.js +15 -4
- package/dist/src/pil/discovery.js +211 -136
- package/dist/src/pil/layer1-intent.d.ts +12 -0
- package/dist/src/pil/layer1-intent.js +283 -38
- package/dist/src/pil/layer1-intent.test.js +210 -4
- package/dist/src/pil/layer16-clarity.d.ts +25 -11
- package/dist/src/pil/layer16-clarity.js +19 -306
- package/dist/src/pil/layer3-ee-injection.d.ts +19 -0
- package/dist/src/pil/layer3-ee-injection.js +96 -4
- package/dist/src/pil/layer4-gsd.js +18 -6
- package/dist/src/pil/layer6-output.d.ts +2 -0
- package/dist/src/pil/layer6-output.js +151 -25
- package/dist/src/pil/llm-classify.d.ts +26 -0
- package/dist/src/pil/llm-classify.js +34 -5
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +82 -76
- package/dist/src/pil/pipeline.js +15 -9
- package/dist/src/pil/schema.d.ts +8 -0
- package/dist/src/pil/schema.js +12 -1
- package/dist/src/pil/task-tier-map.js +4 -0
- package/dist/src/pil/types.d.ts +11 -1
- package/dist/src/product-loop/done-gate.js +3 -3
- package/dist/src/product-loop/loop-driver.js +18 -18
- package/dist/src/product-loop/progress-snapshot.js +4 -4
- package/dist/src/providers/auth/gemini-oauth.js +6 -15
- package/dist/src/providers/auth/grok-oauth.js +6 -15
- package/dist/src/providers/auth/openai-oauth.js +6 -15
- package/dist/src/providers/mcp-vision-bridge.js +48 -48
- package/dist/src/reporter/index.js +1 -1
- package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
- package/dist/src/scaffold/bb-quality-gate.js +5 -5
- package/dist/src/scaffold/continuation-prompt.js +60 -60
- package/dist/src/scaffold/init-new.js +453 -453
- package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
- package/dist/src/self-qa/agentic-loop.js +24 -19
- package/dist/src/self-qa/spec-emitter.js +26 -23
- package/dist/src/storage/__tests__/migrations.test.js +2 -2
- package/dist/src/storage/interaction-log.js +5 -5
- package/dist/src/storage/migrations.js +122 -122
- package/dist/src/storage/sessions.js +42 -42
- package/dist/src/storage/transcript.js +91 -84
- package/dist/src/storage/usage.js +14 -14
- package/dist/src/storage/workspaces.js +12 -12
- package/dist/src/tools/__tests__/native-tools.test.d.ts +1 -0
- package/dist/src/tools/__tests__/native-tools.test.js +53 -0
- package/dist/src/tools/git-safety.d.ts +61 -0
- package/dist/src/tools/git-safety.js +141 -0
- package/dist/src/tools/git-safety.test.d.ts +1 -0
- package/dist/src/tools/git-safety.test.js +111 -0
- package/dist/src/tools/native-tools.d.ts +31 -0
- package/dist/src/tools/native-tools.js +273 -0
- package/dist/src/tools/registry-ee-query.test.js +18 -1
- package/dist/src/tools/registry-git-safety.test.d.ts +7 -0
- package/dist/src/tools/registry-git-safety.test.js +92 -0
- package/dist/src/tools/registry.js +52 -6
- package/dist/src/ui/__tests__/markdown-render.test.d.ts +1 -0
- package/dist/src/ui/__tests__/markdown-render.test.js +48 -0
- package/dist/src/ui/app.js +0 -0
- package/dist/src/ui/components/message-view.js +4 -1
- package/dist/src/ui/components/structured-response-view.js +7 -3
- package/dist/src/ui/components/tool-group.js +7 -1
- package/dist/src/ui/markdown-render.d.ts +41 -0
- package/dist/src/ui/markdown-render.js +223 -0
- package/dist/src/ui/markdown.d.ts +10 -0
- package/dist/src/ui/markdown.js +12 -35
- package/dist/src/ui/slash/council-inspect.js +4 -4
- package/dist/src/ui/slash/export.js +4 -4
- package/dist/src/ui/utils/text.d.ts +8 -0
- package/dist/src/ui/utils/text.js +16 -0
- package/dist/src/ui/utils/text.test.d.ts +1 -0
- package/dist/src/ui/utils/text.test.js +23 -0
- package/dist/src/usage/ledger.js +48 -15
- package/dist/src/utils/__tests__/footprint-gitignore.test.d.ts +1 -0
- package/dist/src/utils/__tests__/footprint-gitignore.test.js +50 -0
- package/dist/src/utils/clipboard-image.js +23 -23
- package/dist/src/utils/open-url.d.ts +56 -0
- package/dist/src/utils/open-url.js +58 -0
- package/dist/src/utils/open-url.test.d.ts +1 -0
- package/dist/src/utils/open-url.test.js +86 -0
- package/dist/src/utils/settings.d.ts +12 -0
- package/dist/src/utils/settings.js +48 -0
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/utils/skills.js +3 -3
- package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
- package/dist/src/verify/environment.js +2 -1
- package/package.json +1 -1
- package/dist/src/pil/layer16-clarity.test.js +0 -31
- /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
|
-
//
|
|
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
|
package/dist/src/ui/app.js
CHANGED
|
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: [
|
|
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 (
|
|
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: [
|
|
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
|
-
|
|
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;
|
package/dist/src/ui/markdown.js
CHANGED
|
@@ -1,38 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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;
|