tachibot-mcp 2.18.0 → 2.19.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/CHANGELOG.md +23 -0
- package/dist/src/modes/auditor.js +2 -1
- package/dist/src/modes/challenger.js +6 -5
- package/dist/src/server.js +15 -16
- package/dist/src/tools/planner-tools.js +4 -2
- package/dist/src/utils/ansi-renderer.js +33 -14
- package/dist/src/utils/ansi-styles.js +21 -10
- package/dist/src/utils/format-constants.js +47 -9
- package/docs/superpowers/plans/2026-03-21-sparse-mode-ansi-bold-headers.md +252 -0
- package/docs/superpowers/plans/2026-03-21-sparse-mode-fixes.md +187 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to TachiBot MCP will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.19.0] - 2026-03-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Sparse render mode** (`RENDER_OUTPUT=sparse`) — lightweight output formatting with ~72 tokens overhead per response
|
|
12
|
+
- **ANSI model badges** — colored background badges for model name (provider color) + tool name (charcoal bg)
|
|
13
|
+
- **Pastel section headers** — emoji section headers (`🧠 HEADER ───`) rendered as teal bg + dark bold text badges
|
|
14
|
+
- **Color-coded verdicts** — `✅ pass` (sage green), `🫠 partial` (soft yellow), `💀 fail` (rose) with colored bg badges
|
|
15
|
+
- **Summary badge** — tool name displayed as bold charcoal badge next to model badge
|
|
16
|
+
- **`stripMarkdown` options** — `{ boldHeaders: true }` converts markdown/emoji headers to ANSI-styled badges
|
|
17
|
+
- **Empty input guard** on `stripMarkdown` — early return for empty/whitespace input
|
|
18
|
+
- **Strip markdown headers** — `##` prefixes and `───` decorators removed from output
|
|
19
|
+
- **8 unit tests** for `stripMarkdown` covering headers, bold, bullets, code blocks, HR, empty input
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **ANSI truncation corruption** — truncate raw content BEFORE applying ANSI badges (prevents mid-escape code corruption)
|
|
23
|
+
- **Summary badge without model** — tools returning null from `inferModelFromTool` (think, focus) still show tool name badge
|
|
24
|
+
- **Unused imports** — cleaned up 10+ unused imports/variables in server.ts
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- **Emoji palette** — analysis 🧠, insight 🔮, key 🗝, verdict 👩⚖️ (replaced 🔍🧿🪩🎯)
|
|
28
|
+
- **Auditor/Challenger** — use `EMOJI_PALETTE` constants instead of hardcoded emoji
|
|
29
|
+
- **Planner** — topological task ordering with T-ID preservation and Dependencies metadata
|
|
30
|
+
|
|
8
31
|
## [2.18.0] - 2026-03-21
|
|
9
32
|
|
|
10
33
|
### Added
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EMOJI_PALETTE } from '../utils/format-constants.js';
|
|
1
2
|
export class Auditor {
|
|
2
3
|
constructor() {
|
|
3
4
|
this.defaultModel = 'perplexity-sonar-pro';
|
|
@@ -328,7 +329,7 @@ export class Auditor {
|
|
|
328
329
|
// Critical assumptions
|
|
329
330
|
const criticalAssumptions = assumptions.filter(a => a.risk === 'high');
|
|
330
331
|
if (criticalAssumptions.length > 0) {
|
|
331
|
-
synthesis += `**Critical Assumptions**
|
|
332
|
+
synthesis += `**Critical Assumptions** ${EMOJI_PALETTE.bad}:\n`;
|
|
332
333
|
criticalAssumptions.forEach(assumption => {
|
|
333
334
|
synthesis += `- ${assumption.description}\n`;
|
|
334
335
|
synthesis += ` Validation: ${assumption.validationMethod}\n`;
|
|
@@ -20,6 +20,7 @@ import { getChallengerModels } from '../config/model-defaults.js';
|
|
|
20
20
|
import { createProgressStream } from '../utils/progress-stream.js';
|
|
21
21
|
import { smartAPIClient } from '../utils/smart-api-client.js';
|
|
22
22
|
import { getSmartTimeout } from '../config/timeout-config.js';
|
|
23
|
+
import { EMOJI_PALETTE } from '../utils/format-constants.js';
|
|
23
24
|
import { execSync } from 'child_process';
|
|
24
25
|
// import {
|
|
25
26
|
// renderTable,
|
|
@@ -755,7 +756,7 @@ Write concrete, specific analysis. Do NOT include brackets or placeholders.`;
|
|
|
755
756
|
lines.push('');
|
|
756
757
|
const challengesTableData = challenges.map((ch, i) => {
|
|
757
758
|
const claim = claims.find(c => c.id === ch.claimId);
|
|
758
|
-
const severityIcon = ch.severity === 'high' ?
|
|
759
|
+
const severityIcon = ch.severity === 'high' ? EMOJI_PALETTE.bad : ch.severity === 'medium' ? EMOJI_PALETTE.warn : EMOJI_PALETTE.good;
|
|
759
760
|
return {
|
|
760
761
|
'#': String(i + 1),
|
|
761
762
|
Severity: `${severityIcon} ${ch.severity}`,
|
|
@@ -768,7 +769,7 @@ Write concrete, specific analysis. Do NOT include brackets or placeholders.`;
|
|
|
768
769
|
// Show full challenges below
|
|
769
770
|
challenges.forEach((ch, i) => {
|
|
770
771
|
const claim = claims.find(c => c.id === ch.claimId);
|
|
771
|
-
const severityIcon = ch.severity === 'high' ?
|
|
772
|
+
const severityIcon = ch.severity === 'high' ? EMOJI_PALETTE.bad : ch.severity === 'medium' ? EMOJI_PALETTE.warn : EMOJI_PALETTE.good;
|
|
772
773
|
lines.push(`${severityIcon} **Challenge #${i + 1}** (${ch.severity} severity)`);
|
|
773
774
|
lines.push(`Original: "${claim?.text}"`);
|
|
774
775
|
lines.push('');
|
|
@@ -863,9 +864,9 @@ Write concrete, specific analysis. Do NOT include brackets or placeholders.`;
|
|
|
863
864
|
lines.push('');
|
|
864
865
|
lines.push(renderKeyValueTable({
|
|
865
866
|
'Total Challenges': String(challenges.length),
|
|
866
|
-
'
|
|
867
|
-
'
|
|
868
|
-
'
|
|
867
|
+
[EMOJI_PALETTE.bad + ' High Severity']: String(highSeverity),
|
|
868
|
+
[EMOJI_PALETTE.warn + ' Medium Severity']: String(mediumSeverity),
|
|
869
|
+
[EMOJI_PALETTE.good + ' Low Severity']: String(lowSeverity),
|
|
869
870
|
'Alternatives': String(alternatives.length),
|
|
870
871
|
'Groupthink Risk': groupthinkRisk,
|
|
871
872
|
}));
|
package/dist/src/server.js
CHANGED
|
@@ -25,7 +25,7 @@ const savedThemeVars = {
|
|
|
25
25
|
RENDER_OUTPUT: process.env.RENDER_OUTPUT,
|
|
26
26
|
TACHIBOT_THEME: process.env.TACHIBOT_THEME,
|
|
27
27
|
};
|
|
28
|
-
|
|
28
|
+
dotenvConfig({
|
|
29
29
|
path: envPath,
|
|
30
30
|
override: true // API keys from .env take priority
|
|
31
31
|
});
|
|
@@ -52,7 +52,7 @@ import { z } from "zod";
|
|
|
52
52
|
import { InstructionOrchestrator } from "./orchestrator-instructions.js";
|
|
53
53
|
import { validateToolInput, sanitizeForLogging } from "./utils/input-validator.js";
|
|
54
54
|
import { isToolEnabled, logToolConfiguration } from "./utils/tool-config.js";
|
|
55
|
-
import {
|
|
55
|
+
import { renderOutput } from "./utils/ansi-renderer.js";
|
|
56
56
|
import { getToolAnnotations } from "./utils/tool-annotations.js";
|
|
57
57
|
import { truncateSmart } from "./utils/stream-distill.js";
|
|
58
58
|
import { trackToolCall, inferModelFromTool, estimateTokens, isTrackingEnabled, getUsageSummary, getAllReposSummary, getStatsJson, resetStats } from "./utils/usage-tracker.js";
|
|
@@ -158,16 +158,17 @@ function safeAddTool(tool) {
|
|
|
158
158
|
// Silently ignore tracking errors
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
-
//
|
|
162
|
-
// doesn't render markdown in tool results (shows raw ** and ### as text)
|
|
161
|
+
// Apply render mode (sparse = badge + bold headers + stripped, etc.)
|
|
163
162
|
if (typeof result === 'string') {
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
163
|
+
const model = inferModelFromTool(tool.name) || undefined;
|
|
164
|
+
// Truncate raw content BEFORE ANSI rendering — prevents mid-escape corruption
|
|
165
|
+
let raw = result;
|
|
166
|
+
const MAX_RAW_CHARS = 24000;
|
|
167
|
+
if (raw.length > MAX_RAW_CHARS) {
|
|
168
|
+
raw = truncateSmart(raw, MAX_RAW_CHARS);
|
|
169
169
|
}
|
|
170
|
-
|
|
170
|
+
const rendered = renderOutput(raw, { model, summary: tool.name });
|
|
171
|
+
return { type: "text", text: rendered };
|
|
171
172
|
}
|
|
172
173
|
return result;
|
|
173
174
|
}
|
|
@@ -226,7 +227,7 @@ safeAddTool({
|
|
|
226
227
|
}),
|
|
227
228
|
execute: async (args, mcpContext) => {
|
|
228
229
|
const { log } = mcpContext;
|
|
229
|
-
let { query, mode = "simple", context, domain, tokenEfficient = false, rounds = 5, executeNow = true, models, temperature = 0.7, saveSession = true, maxTokensPerRound = 2000, pingPongStyle = "collaborative" } = args;
|
|
230
|
+
let { query, mode = "simple", context, domain, tokenEfficient = false, rounds = 5, executeNow: _executeNow = true, models, temperature = 0.7, saveSession: _saveSession = true, maxTokensPerRound: _maxTokensPerRound = 2000, pingPongStyle: _pingPongStyle = "collaborative" } = args;
|
|
230
231
|
// Validate and sanitize input
|
|
231
232
|
const queryValidation = validateToolInput(query);
|
|
232
233
|
if (!queryValidation.valid) {
|
|
@@ -332,8 +333,7 @@ TIPS FOR EFFECTIVE SYNTHESIS:
|
|
|
332
333
|
Ready to help synthesize your collective intelligence results!`;
|
|
333
334
|
default: // simple mode
|
|
334
335
|
// BigText header disabled - plain text only
|
|
335
|
-
|
|
336
|
-
const focusBadge = '';
|
|
336
|
+
// BigText header/badge removed — plain text only
|
|
337
337
|
return `Enhanced reasoning for: "${query}"
|
|
338
338
|
${context ? `Context: ${context}` : ''}
|
|
339
339
|
|
|
@@ -458,8 +458,7 @@ MemoryProvider: Pluggable memory (devlog, mem0, custom). Set TACHIBOT_MEMORY_PRO
|
|
|
458
458
|
});
|
|
459
459
|
// Build response with model output if available
|
|
460
460
|
// BigText header disabled - plain text only
|
|
461
|
-
|
|
462
|
-
const thinkHeader = '';
|
|
461
|
+
// BigText header/badge removed — plain text only
|
|
463
462
|
let response = '';
|
|
464
463
|
if (result.modelResponse) {
|
|
465
464
|
response += `## Model Response (${args.model}):\n\n${result.modelResponse}\n\n---\n\n`;
|
|
@@ -691,7 +690,7 @@ async function initializeServer() {
|
|
|
691
690
|
console.error("✅ Server.start() called successfully");
|
|
692
691
|
// Keep the process alive with a heartbeat
|
|
693
692
|
// This ensures the server doesn't exit prematurely
|
|
694
|
-
|
|
693
|
+
setInterval(() => {
|
|
695
694
|
// Heartbeat to keep process alive
|
|
696
695
|
// Log every 30 seconds to show we're still alive
|
|
697
696
|
const now = new Date().toISOString();
|
|
@@ -774,7 +774,8 @@ ${codeContext ? "\nNote: All analysis was performed on actual code provided." :
|
|
|
774
774
|
|
|
775
775
|
OUTPUT FORMAT — Each task must be bite-sized (2-5 min):
|
|
776
776
|
|
|
777
|
-
### Task
|
|
777
|
+
### Task [T-ID]: [Component Name]
|
|
778
|
+
**Dependencies:** Blocked by: T1,T3 | Blocks: T4 | Parallel: yes/no | Complexity: Low/Med/High
|
|
778
779
|
**Files:** Create: path/to/file | Modify: path/to/file:lines | Test: path/to/test
|
|
779
780
|
**Step 1:** Write the failing test (show test code)
|
|
780
781
|
**Step 2:** Run test to verify it fails (exact command + expected output)
|
|
@@ -789,7 +790,8 @@ REQUIREMENTS:
|
|
|
789
790
|
4. Exact commands with expected output
|
|
790
791
|
5. Checkpoints at 50%, 80%, and 100%
|
|
791
792
|
6. Address ALL pre-mortem failure causes from critique as mitigations
|
|
792
|
-
7. Order tasks simplest → hardest (
|
|
793
|
+
7. Order tasks topologically: a task MUST appear after all tasks that block it. Within independent groups, order simplest → hardest (using Complexity).
|
|
794
|
+
8. Preserve exact task IDs from TASK DECOMPOSITION (T1, T1.1, T2). Do NOT renumber. Each task MUST include the Dependencies line with Blocked by/Blocks/Parallel/Complexity fields.
|
|
793
795
|
|
|
794
796
|
**QUALITY ASSESSMENT (at the end):**
|
|
795
797
|
- Code Quality: X/10
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Uses the theme system from ansi-styles.ts and ink-markdown-renderer.tsx.
|
|
6
6
|
*
|
|
7
7
|
* Configuration:
|
|
8
|
+
* RENDER_OUTPUT=sparse - ANSI badge header for model/tool name + raw markdown body (~1.01x tokens)
|
|
8
9
|
* RENDER_OUTPUT=markdown - Raw markdown, no processing (default, ~1x tokens)
|
|
9
10
|
* RENDER_OUTPUT=ink - React Ink rendering with themes, gradients, tables (~12x tokens)
|
|
10
11
|
* RENDER_OUTPUT=ansi - Legacy marked-terminal rendering
|
|
@@ -20,7 +21,7 @@ import chalk from 'chalk';
|
|
|
20
21
|
chalk.level = 3;
|
|
21
22
|
// Force gradient-string colors
|
|
22
23
|
process.env.FORCE_COLOR = '3';
|
|
23
|
-
import { getTheme, renderModelBadge, toolResultHeader, dividers, } from './ansi-styles.js';
|
|
24
|
+
import { getTheme, renderModelBadge, renderSummaryBadge, toolResultHeader, dividers, } from './ansi-styles.js';
|
|
24
25
|
const renderGradientDivider = (width = 50, preset) => '-'.repeat(width);
|
|
25
26
|
const renderGradientModelName = (model) => `[${model.toUpperCase()}]`;
|
|
26
27
|
// import { renderMarkdownToAnsi as renderInkMarkdown } from './ink-markdown-renderer.js';
|
|
@@ -105,7 +106,7 @@ export function clearThemeCache() {
|
|
|
105
106
|
export function getRenderMode() {
|
|
106
107
|
if (!cachedRenderMode) {
|
|
107
108
|
const mode = process.env.RENDER_OUTPUT?.toLowerCase();
|
|
108
|
-
if (mode === 'ink' || mode === 'plain' || mode === 'ansi') {
|
|
109
|
+
if (mode === 'ink' || mode === 'plain' || mode === 'ansi' || mode === 'sparse') {
|
|
109
110
|
cachedRenderMode = mode;
|
|
110
111
|
}
|
|
111
112
|
else {
|
|
@@ -176,6 +177,18 @@ export function renderOutput(content, modelOrOptions) {
|
|
|
176
177
|
output += renderAnsi(content);
|
|
177
178
|
output += '\n' + simpleGradient(60) + '\n';
|
|
178
179
|
break;
|
|
180
|
+
case 'sparse': {
|
|
181
|
+
// Sparse mode: badge bar (model + summary) + bold headers + stripped text
|
|
182
|
+
const parts = [];
|
|
183
|
+
if (options.model)
|
|
184
|
+
parts.push(renderModelBadge(options.model));
|
|
185
|
+
if (options.summary)
|
|
186
|
+
parts.push(renderSummaryBadge(options.summary));
|
|
187
|
+
if (parts.length)
|
|
188
|
+
output += parts.join('') + '\n';
|
|
189
|
+
output += stripMarkdown(content, { boldHeaders: true });
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
179
192
|
case 'plain':
|
|
180
193
|
output += stripMarkdown(content);
|
|
181
194
|
break;
|
|
@@ -422,18 +435,10 @@ function renderAnsi(md) {
|
|
|
422
435
|
return md;
|
|
423
436
|
}
|
|
424
437
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
* Strip decorative markdown formatting but KEEP structural elements.
|
|
430
|
-
* Keeps: # headers, - bullets, 1. numbered lists, > blockquotes, | tables, indentation
|
|
431
|
-
* Strips: **bold**, *italic*, ~~strike~~, `inline code` backticks, [link](url), , ``` fences
|
|
432
|
-
*
|
|
433
|
-
* Code blocks are protected via placeholder extraction to avoid corrupting code content.
|
|
434
|
-
* Underscore italic is skipped to avoid mangling snake_case identifiers.
|
|
435
|
-
*/
|
|
436
|
-
export function stripMarkdown(md) {
|
|
438
|
+
export function stripMarkdown(md, options) {
|
|
439
|
+
if (!md || !md.trim())
|
|
440
|
+
return '';
|
|
441
|
+
const { boldHeaders = false } = options || {};
|
|
437
442
|
// 1. Extract code blocks to placeholders (protect from stripping)
|
|
438
443
|
const codeBlocks = [];
|
|
439
444
|
let text = md.replace(/```[\s\S]*?```/g, (match) => {
|
|
@@ -443,6 +448,20 @@ export function stripMarkdown(md) {
|
|
|
443
448
|
});
|
|
444
449
|
// 2. Strip decorative formatting on non-code text
|
|
445
450
|
text = text
|
|
451
|
+
// Markdown headers — strip # prefix (or bold if boldHeaders)
|
|
452
|
+
.replace(/^#{1,6}\s+(.+)$/gm, boldHeaders ? '\x1b[1m$1\x1b[0m' : '$1')
|
|
453
|
+
// Emoji section headers — e.g. "🔍 TYPE SAFETY ───" → soft teal bg, dark bold text
|
|
454
|
+
.replace(/^(.{1,2})\s+([A-Z][A-Z\s&]+?)\s*─+$/gm, boldHeaders ? '\x1b[48;5;73m\x1b[30m\x1b[1m $1 $2 \x1b[0m' : '$1 $2')
|
|
455
|
+
// Verdict lines — color-coded: green=pass, yellow=partial, red=fail
|
|
456
|
+
.replace(/^(✅|🫠|💀|🟢|🟡|🔴)\s*(pass|partial|fail)\b(.*)$/gmi, (_match, emoji, status, rest) => {
|
|
457
|
+
if (!boldHeaders)
|
|
458
|
+
return `${emoji} ${status}${rest}`;
|
|
459
|
+
const s = status.toLowerCase();
|
|
460
|
+
const bg = s === 'pass' ? '\x1b[48;5;151m' : s === 'partial' ? '\x1b[48;5;186m' : '\x1b[48;5;174m';
|
|
461
|
+
return `${bg}\x1b[30m\x1b[1m ${emoji} ${status} \x1b[0m${rest}`;
|
|
462
|
+
})
|
|
463
|
+
// Horizontal rules
|
|
464
|
+
.replace(/^[-*_]{3,}\s*$/gm, '')
|
|
446
465
|
// Normalize * bullets to - before italic strip (prevents stray * on "* Security:")
|
|
447
466
|
.replace(/^(\s{0,3})\*(\s+)/gm, '$1-$2')
|
|
448
467
|
// Images before links (avoid ![...] conflict)
|
|
@@ -409,18 +409,19 @@ const fgMap = {
|
|
|
409
409
|
};
|
|
410
410
|
// Unicode colored emoji for badges (ANSI-free, works everywhere)
|
|
411
411
|
const MODEL_EMOJI = {
|
|
412
|
-
grok: '
|
|
413
|
-
gemini: '
|
|
414
|
-
openai: '
|
|
415
|
-
perplexity: '
|
|
416
|
-
kimi: '
|
|
417
|
-
qwen: '
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
412
|
+
grok: '🔮',
|
|
413
|
+
gemini: '🌀',
|
|
414
|
+
openai: '🧠',
|
|
415
|
+
perplexity: '🔭',
|
|
416
|
+
kimi: '🐉',
|
|
417
|
+
qwen: '🐉',
|
|
418
|
+
minimax: '🤖',
|
|
419
|
+
focus: '🎯',
|
|
420
|
+
workflow: '🌊',
|
|
421
|
+
scout: '🔎',
|
|
421
422
|
verifier: '✅',
|
|
422
423
|
challenger: '⚔️',
|
|
423
|
-
think: '
|
|
424
|
+
think: '🫨',
|
|
424
425
|
};
|
|
425
426
|
/**
|
|
426
427
|
* Render a model badge with ANSI background color
|
|
@@ -446,6 +447,16 @@ export function renderModelBadge(model, theme) {
|
|
|
446
447
|
const label = badgeStyle.label || ` ${normalized} `;
|
|
447
448
|
return `${bg}${fg}${ANSI.bold}${label}${ANSI.reset}`;
|
|
448
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* Render a summary badge with dimmed bg + dark text (pairs with model badge)
|
|
452
|
+
* e.g. model badge: [bright magenta] grok [reset] + summary: [dim gray] search query [reset]
|
|
453
|
+
*/
|
|
454
|
+
export function renderSummaryBadge(summary) {
|
|
455
|
+
// Dimmed bg + bold sits next to the bright model badge
|
|
456
|
+
const bg = '\x1b[48;5;238m'; // dark gray bg (ansi256 color 238)
|
|
457
|
+
const fg = '\x1b[97m'; // bright white text
|
|
458
|
+
return `${bg}${fg}${ANSI.bold} ${summary} ${ANSI.reset}`;
|
|
459
|
+
}
|
|
449
460
|
/**
|
|
450
461
|
* Get model badge (alias for backward compatibility)
|
|
451
462
|
*/
|
|
@@ -19,29 +19,67 @@ LISTS: Hierarchy for structure:
|
|
|
19
19
|
|
|
20
20
|
CODE: \`\`\`lang blocks. Minimal inline comments.
|
|
21
21
|
|
|
22
|
-
SECTIONS:
|
|
22
|
+
SECTIONS: 🧠 HEADER ───
|
|
23
23
|
Blank line after. 2-4 sections max.
|
|
24
24
|
|
|
25
|
-
VERDICT:
|
|
25
|
+
VERDICT: ✅ pass 🫠 partial 💀 fail
|
|
26
26
|
End only. One line + reason.`;
|
|
27
27
|
export const EMOJI_PALETTE = {
|
|
28
28
|
// Section headers
|
|
29
|
-
analysis: '
|
|
30
|
-
verdict: '
|
|
31
|
-
insight: '
|
|
32
|
-
key: '
|
|
29
|
+
analysis: '🧠',
|
|
30
|
+
verdict: '👩⚖️',
|
|
31
|
+
insight: '🔮',
|
|
32
|
+
key: '🗝',
|
|
33
33
|
judge: '⚖️',
|
|
34
|
-
fix: '
|
|
34
|
+
fix: '🪛',
|
|
35
35
|
search: '🔎',
|
|
36
|
-
idea: '
|
|
37
|
-
warning: '
|
|
36
|
+
idea: '🫧',
|
|
37
|
+
warning: '🫠',
|
|
38
|
+
sources: '📎',
|
|
39
|
+
compare: '⚡',
|
|
40
|
+
code: '🦾',
|
|
41
|
+
brainstorm: '🌩️',
|
|
42
|
+
reason: '🧪',
|
|
43
|
+
plan: '📋',
|
|
44
|
+
debug: '🐛',
|
|
45
|
+
security: '🛡️',
|
|
46
|
+
performance: '🔥',
|
|
47
|
+
architecture: '🏗️',
|
|
48
|
+
consensus: '🤝',
|
|
49
|
+
conflict: '⚔️',
|
|
50
|
+
caveat: '📌',
|
|
51
|
+
thinking: '🫨',
|
|
52
|
+
decompose: '🪆',
|
|
53
|
+
workflow: '🌊',
|
|
54
|
+
summary: '🧾',
|
|
38
55
|
// Status indicators
|
|
39
56
|
good: '🟢',
|
|
40
57
|
warn: '🟡',
|
|
41
58
|
bad: '🔴',
|
|
42
59
|
info: '🔵',
|
|
60
|
+
pass: '✅',
|
|
61
|
+
fail: '💀',
|
|
62
|
+
partial: '🫠',
|
|
63
|
+
running: '⏳',
|
|
64
|
+
blocked: '🚫',
|
|
65
|
+
hot: '🔥',
|
|
66
|
+
cold: '🧊',
|
|
67
|
+
// Models
|
|
68
|
+
grok: '🔮',
|
|
69
|
+
gemini: '🌀',
|
|
70
|
+
openai: '🧠',
|
|
71
|
+
perplexity: '🔭',
|
|
72
|
+
kimi: '🐉',
|
|
73
|
+
qwen: '🐉',
|
|
74
|
+
minimax: '🤖',
|
|
43
75
|
// Actions
|
|
44
76
|
arrow: '→',
|
|
45
77
|
check: '✓',
|
|
46
78
|
cross: '✗',
|
|
79
|
+
link: '🔗',
|
|
80
|
+
pin: '📍',
|
|
81
|
+
time: '⏱️',
|
|
82
|
+
cost: '💰',
|
|
83
|
+
sparkle: '✨',
|
|
84
|
+
nuke: '💥',
|
|
47
85
|
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Sparse Mode: ANSI Bold Headers Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Convert `## headers` to ANSI bold text (instead of stripping them) in sparse render mode, giving readable section separation at ~3 tokens/header instead of 0 (stripped) or ~12 (colored badge).
|
|
6
|
+
|
|
7
|
+
**Architecture:** Modify `stripMarkdown()` in `ansi-renderer.ts` to accept an options parameter. When `boldHeaders: true`, headers get `\x1b[1m` ANSI bold wrapping instead of being stripped to plain text. The sparse case in `renderOutput()` calls `stripMarkdown` with this option enabled.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, raw ANSI escape codes (`\x1b[1m`, `\x1b[0m`)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Task 1: Add ANSI Bold Header Support to stripMarkdown
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
- Modify: `src/utils/ansi-renderer.ts:552-592` (stripMarkdown function)
|
|
17
|
+
- Test: `src/utils/__tests__/strip-markdown.test.ts`
|
|
18
|
+
|
|
19
|
+
- [ ] **Step 1: Write the failing test**
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/utils/__tests__/strip-markdown.test.ts
|
|
23
|
+
import { describe, it, expect } from 'vitest';
|
|
24
|
+
import { stripMarkdown } from '../ansi-renderer.js';
|
|
25
|
+
|
|
26
|
+
describe('stripMarkdown', () => {
|
|
27
|
+
it('strips headers to plain text by default', () => {
|
|
28
|
+
expect(stripMarkdown('## Analysis')).toBe('Analysis');
|
|
29
|
+
expect(stripMarkdown('### Summary')).toBe('Summary');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('converts headers to ANSI bold when boldHeaders is true', () => {
|
|
33
|
+
const result = stripMarkdown('## Analysis', { boldHeaders: true });
|
|
34
|
+
expect(result).toBe('\x1b[1mAnalysis\x1b[0m');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('handles h1-h6 with ANSI bold', () => {
|
|
38
|
+
const result = stripMarkdown('# Title\n## Section\n### Sub', { boldHeaders: true });
|
|
39
|
+
expect(result).toContain('\x1b[1mTitle\x1b[0m');
|
|
40
|
+
expect(result).toContain('\x1b[1mSection\x1b[0m');
|
|
41
|
+
expect(result).toContain('\x1b[1mSub\x1b[0m');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('strips bold/italic/strikethrough normally', () => {
|
|
45
|
+
expect(stripMarkdown('**bold** and *italic* and ~~strike~~')).toBe('bold and italic and strike');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('preserves bullets and indentation', () => {
|
|
49
|
+
const input = '- Item 1\n - Sub item\n - Deep';
|
|
50
|
+
expect(stripMarkdown(input)).toBe(input);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('preserves code blocks untouched', () => {
|
|
54
|
+
const input = '```js\nconst x = 1;\n```';
|
|
55
|
+
expect(stripMarkdown(input)).toContain('const x = 1;');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('strips horizontal rules', () => {
|
|
59
|
+
expect(stripMarkdown('---')).toBe('');
|
|
60
|
+
expect(stripMarkdown('***')).toBe('');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('adds blank line after bold header for readability', () => {
|
|
64
|
+
const result = stripMarkdown('## Analysis\nSome text', { boldHeaders: true });
|
|
65
|
+
expect(result).toBe('\x1b[1mAnalysis\x1b[0m\nSome text');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
71
|
+
|
|
72
|
+
Run: `npx vitest run src/utils/__tests__/strip-markdown.test.ts`
|
|
73
|
+
Expected: FAIL — `stripMarkdown` does not accept options parameter
|
|
74
|
+
|
|
75
|
+
- [ ] **Step 3: Add options parameter to stripMarkdown**
|
|
76
|
+
|
|
77
|
+
In `src/utils/ansi-renderer.ts`, change the `stripMarkdown` signature and header regex:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
export interface StripMarkdownOptions {
|
|
81
|
+
/** Convert ## headers to ANSI bold instead of stripping # prefix */
|
|
82
|
+
boldHeaders?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function stripMarkdown(md: string, options?: StripMarkdownOptions): string {
|
|
86
|
+
const { boldHeaders = false } = options || {};
|
|
87
|
+
|
|
88
|
+
// ... existing code block extraction ...
|
|
89
|
+
|
|
90
|
+
// 2. Strip decorative formatting on non-code text
|
|
91
|
+
text = text
|
|
92
|
+
// Headers — ANSI bold or strip # prefix
|
|
93
|
+
.replace(/^#{1,6}\s+(.+)$/gm, boldHeaders ? '\x1b[1m$1\x1b[0m' : '$1')
|
|
94
|
+
// ... rest of existing regexes unchanged ...
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
98
|
+
|
|
99
|
+
Run: `npx vitest run src/utils/__tests__/strip-markdown.test.ts`
|
|
100
|
+
Expected: ALL PASS
|
|
101
|
+
|
|
102
|
+
- [ ] **Step 5: Commit**
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
git add src/utils/__tests__/strip-markdown.test.ts src/utils/ansi-renderer.ts
|
|
106
|
+
git commit -m "feat(sparse): convert headers to ANSI bold in stripMarkdown"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### Task 2: Wire Sparse Mode to Use Bold Headers
|
|
112
|
+
|
|
113
|
+
**Files:**
|
|
114
|
+
- Modify: `src/utils/ansi-renderer.ts:240-253` (sparse case in renderOutput)
|
|
115
|
+
|
|
116
|
+
- [ ] **Step 1: Update sparse case to pass boldHeaders option**
|
|
117
|
+
|
|
118
|
+
In the `renderOutput` function's sparse case:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
case 'sparse': {
|
|
122
|
+
// Sparse mode: ANSI badge header + clean text with bold section headers
|
|
123
|
+
if (options.model) {
|
|
124
|
+
const badge = renderModelBadge(options.model);
|
|
125
|
+
if (options.summary) {
|
|
126
|
+
output = badge + renderSummaryBadge(options.summary, options.model) + '\n';
|
|
127
|
+
} else {
|
|
128
|
+
output = badge + '\n';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
output += stripMarkdown(content, { boldHeaders: true });
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- [ ] **Step 2: Verify plain mode still strips headers (no bold)**
|
|
137
|
+
|
|
138
|
+
The `plain` case should remain unchanged:
|
|
139
|
+
```typescript
|
|
140
|
+
case 'plain':
|
|
141
|
+
output += stripMarkdown(content); // no options = headers stripped flat
|
|
142
|
+
break;
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- [ ] **Step 3: Build and test end-to-end**
|
|
146
|
+
|
|
147
|
+
Run: `npm run build && node -e "
|
|
148
|
+
import { renderOutput } from './dist/src/utils/ansi-renderer.js';
|
|
149
|
+
const md = '## Analysis\n\n**Critical bug** in server.ts:\n- Missing null check\n\n### Summary\nLooks good.';
|
|
150
|
+
console.log('--- sparse ---');
|
|
151
|
+
console.log(renderOutput(md, { model: 'gemini', summary: 'gemini_analyze_code', mode: 'sparse' }));
|
|
152
|
+
console.log('--- plain ---');
|
|
153
|
+
console.log(renderOutput(md, { model: 'gemini', mode: 'plain' }));
|
|
154
|
+
"`
|
|
155
|
+
|
|
156
|
+
Expected:
|
|
157
|
+
- sparse: badge + `\x1b[1mAnalysis\x1b[0m` (bold) + stripped body
|
|
158
|
+
- plain: `Analysis` (no bold) + stripped body
|
|
159
|
+
|
|
160
|
+
- [ ] **Step 4: Commit**
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
git add src/utils/ansi-renderer.ts
|
|
164
|
+
git commit -m "feat(sparse): wire bold headers in sparse render mode"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### Task 3: Fix TS Unused Import Warnings in server.ts
|
|
170
|
+
|
|
171
|
+
**Files:**
|
|
172
|
+
- Modify: `src/server.ts:34,63,67,71,73,78,84,339,342`
|
|
173
|
+
|
|
174
|
+
- [ ] **Step 1: Remove unused imports and variables**
|
|
175
|
+
|
|
176
|
+
Lines to fix (from TS diagnostics):
|
|
177
|
+
- Line 34: `envResult` — prefix with `void` or remove assignment: `dotenvConfig({ path: envPath, override: true });`
|
|
178
|
+
- Line 63: `stripMarkdown` — already removed in this session
|
|
179
|
+
- Line 67: `getUpdateStatus` — remove from import
|
|
180
|
+
- Line 71: `TechnicalDomain` — remove import
|
|
181
|
+
- Line 73: `getUnifiedAITools` — remove from import
|
|
182
|
+
- Line 78: `createFocusDeepPlan`, `generateFocusDeepVisualization` — remove from import
|
|
183
|
+
- Line 84: `getAllOpenRouterTools` — remove from import
|
|
184
|
+
- Line 339: `executeNow` — remove or prefix unused destructure
|
|
185
|
+
- Line 342: `saveSession` — remove or prefix unused destructure
|
|
186
|
+
|
|
187
|
+
- [ ] **Step 2: Build to verify no errors**
|
|
188
|
+
|
|
189
|
+
Run: `npm run build`
|
|
190
|
+
Expected: Clean build, no warnings
|
|
191
|
+
|
|
192
|
+
- [ ] **Step 3: Run existing tests**
|
|
193
|
+
|
|
194
|
+
Run: `npm test`
|
|
195
|
+
Expected: ALL PASS
|
|
196
|
+
|
|
197
|
+
- [ ] **Step 4: Commit**
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
git add src/server.ts
|
|
201
|
+
git commit -m "chore: remove unused imports in server.ts"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### Task 4: Set RENDER_OUTPUT=sparse as Default
|
|
207
|
+
|
|
208
|
+
**Files:**
|
|
209
|
+
- Modify: `.env` (add `RENDER_OUTPUT=sparse`)
|
|
210
|
+
- Modify: `src/utils/ansi-renderer.ts:113` (optional: change default from `markdown` to `sparse`)
|
|
211
|
+
|
|
212
|
+
- [ ] **Step 1: Add to .env**
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
echo 'RENDER_OUTPUT=sparse' >> .env
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
- [ ] **Step 2: Decide on code default**
|
|
219
|
+
|
|
220
|
+
Option A: Keep `markdown` as code default, override in `.env` only (safer, no behavior change for other users)
|
|
221
|
+
Option B: Change code default to `sparse` (all users get badges by default)
|
|
222
|
+
|
|
223
|
+
Recommend: Option A — set in `.env`, keep code default as `markdown`.
|
|
224
|
+
|
|
225
|
+
- [ ] **Step 3: Restart MCP server and test with a real tool call**
|
|
226
|
+
|
|
227
|
+
Call any tachibot tool (e.g., `gemini_analyze_code`) and verify:
|
|
228
|
+
- Colored badge appears at top
|
|
229
|
+
- Summary badge next to it
|
|
230
|
+
- Headers are bold
|
|
231
|
+
- Body is clean (no `**`, `##`, `~~`)
|
|
232
|
+
|
|
233
|
+
- [ ] **Step 4: Commit**
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
git add .env
|
|
237
|
+
git commit -m "feat: enable sparse render mode in .env"
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
### Token Cost Summary
|
|
243
|
+
|
|
244
|
+
| Element | Tokens per instance | Instances/response | Total |
|
|
245
|
+
|---------|-------------------:|-------------------:|------:|
|
|
246
|
+
| Model badge (ANSI) | ~12 | 1 | 12 |
|
|
247
|
+
| Summary badge (ANSI) | ~8 | 1 | 8 |
|
|
248
|
+
| Bold header (ANSI) | ~3-4 | 2-4 | 8-16 |
|
|
249
|
+
| Stripped `**`/`##`/`~~` | -2 each | 10-15 | -20 to -30 |
|
|
250
|
+
| **Net per response** | | | **~0 to +6** |
|
|
251
|
+
|
|
252
|
+
Sparse mode with bold headers is token-neutral vs raw markdown. You get visual readability for free.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Sparse Mode Fixes — Multi-Model Jury Findings
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Fix critical bugs and edge cases found by 4-model jury (Gemini, GPT, Qwen, background Gemini) in sparse render mode.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Three targeted fixes to existing functions — no new files, no architectural changes. Truncation order swap in server.ts, input guards in stripMarkdown, cleanup of renderSummaryBadge.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, raw ANSI escape codes
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Task 1: Truncate Raw Content BEFORE Rendering (Critical)
|
|
14
|
+
|
|
15
|
+
**Why:** All 4 models flagged this. `truncateSmart` runs after `renderOutput`, which adds ANSI escape codes. If truncation lands mid-escape (`\x1b[1`), it corrupts terminal output and bleeds colors. Truncating the raw text first, then applying ANSI badges, guarantees clean output.
|
|
16
|
+
|
|
17
|
+
**Files:**
|
|
18
|
+
- Modify: `src/server.ts:262-272`
|
|
19
|
+
- Test: Manual — build and verify with long content
|
|
20
|
+
|
|
21
|
+
- [ ] **Step 1: Swap truncation order in server.ts**
|
|
22
|
+
|
|
23
|
+
Change from:
|
|
24
|
+
```typescript
|
|
25
|
+
const model = inferModelFromTool(tool.name) || undefined;
|
|
26
|
+
let rendered = renderOutput(result, { model, summary: tool.name });
|
|
27
|
+
const MAX_RESPONSE_CHARS = 25000;
|
|
28
|
+
if (rendered.length > MAX_RESPONSE_CHARS) {
|
|
29
|
+
rendered = truncateSmart(rendered, MAX_RESPONSE_CHARS);
|
|
30
|
+
}
|
|
31
|
+
return { type: "text" as const, text: rendered };
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
To:
|
|
35
|
+
```typescript
|
|
36
|
+
const model = inferModelFromTool(tool.name) || undefined;
|
|
37
|
+
// Truncate raw content BEFORE adding ANSI — prevents mid-escape corruption
|
|
38
|
+
let raw = result;
|
|
39
|
+
const MAX_RAW_CHARS = 24000;
|
|
40
|
+
if (raw.length > MAX_RAW_CHARS) {
|
|
41
|
+
raw = truncateSmart(raw, MAX_RAW_CHARS);
|
|
42
|
+
}
|
|
43
|
+
const rendered = renderOutput(raw, { model, summary: tool.name });
|
|
44
|
+
return { type: "text" as const, text: rendered };
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Note: Use 24K (not 25K) to leave ~1K headroom for badge ANSI codes.
|
|
48
|
+
|
|
49
|
+
- [ ] **Step 2: Build and verify**
|
|
50
|
+
|
|
51
|
+
Run: `npm run build`
|
|
52
|
+
|
|
53
|
+
- [ ] **Step 3: Commit**
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
git add src/server.ts
|
|
57
|
+
git commit -m "fix: truncate raw content before ANSI rendering to prevent escape corruption"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### Task 2: Show Summary Badge Even Without Model
|
|
63
|
+
|
|
64
|
+
**Why:** GPT flagged that tools returning `null` from `inferModelFromTool` (e.g., `think`, `focus`, `nextThought`) lose their summary badge entirely. The tool name should still appear.
|
|
65
|
+
|
|
66
|
+
**Files:**
|
|
67
|
+
- Modify: `src/utils/ansi-renderer.ts:240-253` (sparse case)
|
|
68
|
+
|
|
69
|
+
- [ ] **Step 1: Update sparse case to show summary independently**
|
|
70
|
+
|
|
71
|
+
Change from:
|
|
72
|
+
```typescript
|
|
73
|
+
case 'sparse': {
|
|
74
|
+
if (options.model) {
|
|
75
|
+
const badge = renderModelBadge(options.model);
|
|
76
|
+
if (options.summary) {
|
|
77
|
+
output = badge + renderSummaryBadge(options.summary, options.model) + '\n';
|
|
78
|
+
} else {
|
|
79
|
+
output = badge + '\n';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
output += stripMarkdown(content, { boldHeaders: true });
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
To:
|
|
88
|
+
```typescript
|
|
89
|
+
case 'sparse': {
|
|
90
|
+
const parts: string[] = [];
|
|
91
|
+
if (options.model) {
|
|
92
|
+
parts.push(renderModelBadge(options.model));
|
|
93
|
+
}
|
|
94
|
+
if (options.summary) {
|
|
95
|
+
parts.push(renderSummaryBadge(options.summary));
|
|
96
|
+
}
|
|
97
|
+
if (parts.length) output += parts.join('') + '\n';
|
|
98
|
+
output += stripMarkdown(content, { boldHeaders: true });
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- [ ] **Step 2: Remove unused `model` param from renderSummaryBadge**
|
|
104
|
+
|
|
105
|
+
In `src/utils/ansi-styles.ts`, change:
|
|
106
|
+
```typescript
|
|
107
|
+
export function renderSummaryBadge(summary: string, model?: string): string {
|
|
108
|
+
```
|
|
109
|
+
To:
|
|
110
|
+
```typescript
|
|
111
|
+
export function renderSummaryBadge(summary: string): string {
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- [ ] **Step 3: Update import in ansi-renderer.ts if needed**
|
|
115
|
+
|
|
116
|
+
Verify the call site no longer passes `model` to `renderSummaryBadge`.
|
|
117
|
+
|
|
118
|
+
- [ ] **Step 4: Build and test**
|
|
119
|
+
|
|
120
|
+
Run: `npm run build && node -e "
|
|
121
|
+
import { renderOutput } from './dist/src/utils/ansi-renderer.js';
|
|
122
|
+
// Test: no model, has summary
|
|
123
|
+
console.log(renderOutput('Hello', { summary: 'think', mode: 'sparse' }));
|
|
124
|
+
// Test: model + summary
|
|
125
|
+
console.log(renderOutput('Hello', { model: 'grok', summary: 'grok_search', mode: 'sparse' }));
|
|
126
|
+
// Test: no model, no summary
|
|
127
|
+
console.log(renderOutput('Hello', { mode: 'sparse' }));
|
|
128
|
+
"`
|
|
129
|
+
|
|
130
|
+
- [ ] **Step 5: Commit**
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
git add src/utils/ansi-renderer.ts src/utils/ansi-styles.ts
|
|
134
|
+
git commit -m "fix(sparse): show summary badge even when model is null"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### Task 3: Add Input Guards to stripMarkdown
|
|
140
|
+
|
|
141
|
+
**Why:** GPT and Qwen flagged that empty/null input and missing early return can cause unnecessary regex processing.
|
|
142
|
+
|
|
143
|
+
**Files:**
|
|
144
|
+
- Modify: `src/utils/ansi-renderer.ts:557` (stripMarkdown function body)
|
|
145
|
+
- Modify: `src/utils/__tests__/strip-markdown.test.ts` (add empty input test)
|
|
146
|
+
|
|
147
|
+
- [ ] **Step 1: Add test for empty input**
|
|
148
|
+
|
|
149
|
+
Add to the test file:
|
|
150
|
+
```typescript
|
|
151
|
+
it('returns empty string for empty/null input', () => {
|
|
152
|
+
expect(stripMarkdown('')).toBe('');
|
|
153
|
+
expect(stripMarkdown(' ')).toBe('');
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- [ ] **Step 2: Add early return to stripMarkdown**
|
|
158
|
+
|
|
159
|
+
Add at the start of the function body (after destructuring options):
|
|
160
|
+
```typescript
|
|
161
|
+
if (!md || !md.trim()) return '';
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- [ ] **Step 3: Run tests**
|
|
165
|
+
|
|
166
|
+
Run: `npx vitest run src/utils/__tests__/strip-markdown.test.ts`
|
|
167
|
+
|
|
168
|
+
- [ ] **Step 4: Commit**
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
git add src/utils/ansi-renderer.ts src/utils/__tests__/strip-markdown.test.ts
|
|
172
|
+
git commit -m "fix: add empty input guard to stripMarkdown"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### Summary of What We're NOT Fixing
|
|
178
|
+
|
|
179
|
+
| Finding | Decision | Reason |
|
|
180
|
+
|---------|----------|--------|
|
|
181
|
+
| Replace regex with markdown parser | Skip | Overkill — model output is ~95% simple markdown |
|
|
182
|
+
| `@@CODE` placeholder collision | Skip | Probability near zero in real model output |
|
|
183
|
+
| Code block with nested backticks | Skip | Models rarely output nested fences |
|
|
184
|
+
| Link with `)` in URL | Skip | Models rarely output such URLs |
|
|
185
|
+
| Windows `\r\n` line endings | Skip | MCP runs on Unix; models output `\n` |
|
|
186
|
+
| ANSI enters LLM context | Accept | 1M context window; ~1.2x overhead acceptable |
|
|
187
|
+
| Decouple UI from LLM payload | Skip | MCP architecture doesn't support split rendering |
|
package/package.json
CHANGED