Add BRAIN_DISABLED flag and fix Ollama tool call formatting

Features:
  - Add BRAIN_DISABLED feature flag to hide all Brain functionality
  - When enabled, hides Brain banner, status indicator, menu, and commands
  - Flag location: src/constants/brain.ts

  Fixes:
  - Fix Ollama 400 error by properly formatting tool_calls in messages
  - Update OllamaMessage type to include tool_calls field
  - Fix Brain menu keyboard not working (add missing modes to isMenuOpen)

  UI Changes:
  - Remove "^Tab toggle mode" hint from status bar
  - Remove "ctrl+t to hide todos" hint from status bar

  Files modified:
  - src/constants/brain.ts (add BRAIN_DISABLED flag)
  - src/types/ollama.ts (add tool_calls to OllamaMessage)
  - src/providers/ollama/chat.ts (format tool_calls in messages)
  - src/tui-solid/components/header.tsx (hide Brain UI when disabled)
  - src/tui-solid/components/status-bar.tsx (remove hints)
  - src/tui-solid/components/command-menu.tsx (filter brain command)
  - src/tui-solid/components/input-area.tsx (fix isMenuOpen modes)
  - src/tui-solid/routes/session.tsx (skip brain menu when disabled)
  - src/services/brain.ts (early return when disabled)
  - src/services/chat-tui/initialize.ts (skip brain init when disabled)
This commit is contained in:
2026-02-02 13:25:38 -05:00
parent 2eadda584a
commit c839fc4d68
114 changed files with 17243 additions and 273 deletions

View File

@@ -0,0 +1,289 @@
/**
* Agent definition loader service
* Loads agent definitions from markdown files with YAML frontmatter
*/
import { readFile, readdir } from "node:fs/promises";
import { join, basename, extname } from "node:path";
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import type {
AgentDefinition,
AgentFrontmatter,
AgentDefinitionFile,
AgentRegistry,
AgentLoadResult,
AgentTier,
AgentColor,
} from "@src/types/agent-definition";
import { DEFAULT_AGENT_DEFINITION, AGENT_DEFINITION_SCHEMA } from "@src/types/agent-definition";
import { AGENT_DEFINITION, AGENT_DEFINITION_PATHS, AGENT_MESSAGES } from "@src/constants/agent-definition";
const parseFrontmatter = (content: string): { frontmatter: Record<string, unknown>; body: string } | null => {
const delimiter = AGENT_DEFINITION.FRONTMATTER_DELIMITER;
const lines = content.split("\n");
if (lines[0]?.trim() !== delimiter) {
return null;
}
const endIndex = lines.findIndex((line, index) => index > 0 && line.trim() === delimiter);
if (endIndex === -1) {
return null;
}
const frontmatterLines = lines.slice(1, endIndex);
const body = lines.slice(endIndex + 1).join("\n").trim();
// Simple YAML parser for frontmatter
const frontmatter: Record<string, unknown> = {};
let currentKey = "";
let currentArray: string[] | null = null;
frontmatterLines.forEach((line) => {
const trimmed = line.trim();
if (trimmed.startsWith("- ") && currentArray !== null) {
currentArray.push(trimmed.slice(2));
return;
}
if (currentArray !== null) {
frontmatter[currentKey] = currentArray;
currentArray = null;
}
const colonIndex = trimmed.indexOf(":");
if (colonIndex === -1) return;
const key = trimmed.slice(0, colonIndex).trim();
const value = trimmed.slice(colonIndex + 1).trim();
if (value === "") {
currentKey = key;
currentArray = [];
} else if (value.startsWith("[") && value.endsWith("]")) {
frontmatter[key] = value
.slice(1, -1)
.split(",")
.map((s) => s.trim().replace(/^["']|["']$/g, ""));
} else if (value === "true") {
frontmatter[key] = true;
} else if (value === "false") {
frontmatter[key] = false;
} else if (!isNaN(Number(value))) {
frontmatter[key] = Number(value);
} else {
frontmatter[key] = value.replace(/^["']|["']$/g, "");
}
});
if (currentArray !== null) {
frontmatter[currentKey] = currentArray;
}
return { frontmatter, body };
};
const validateFrontmatter = (frontmatter: Record<string, unknown>): AgentFrontmatter | null => {
const { required } = AGENT_DEFINITION_SCHEMA;
for (const field of required) {
if (!(field in frontmatter)) {
return null;
}
}
const name = frontmatter.name;
const description = frontmatter.description;
const tools = frontmatter.tools;
if (typeof name !== "string" || typeof description !== "string" || !Array.isArray(tools)) {
return null;
}
return {
name,
description,
tools: tools as ReadonlyArray<string>,
tier: (frontmatter.tier as AgentTier) || DEFAULT_AGENT_DEFINITION.tier,
color: (frontmatter.color as AgentColor) || DEFAULT_AGENT_DEFINITION.color,
maxTurns: (frontmatter.maxTurns as number) || DEFAULT_AGENT_DEFINITION.maxTurns,
triggerPhrases: (frontmatter.triggerPhrases as ReadonlyArray<string>) || [],
capabilities: (frontmatter.capabilities as ReadonlyArray<string>) || [],
allowedPaths: frontmatter.allowedPaths as ReadonlyArray<string> | undefined,
deniedPaths: frontmatter.deniedPaths as ReadonlyArray<string> | undefined,
};
};
const frontmatterToDefinition = (frontmatter: AgentFrontmatter, content: string): AgentDefinition => ({
name: frontmatter.name,
description: frontmatter.description,
tools: frontmatter.tools,
tier: frontmatter.tier || (DEFAULT_AGENT_DEFINITION.tier as AgentTier),
color: frontmatter.color || (DEFAULT_AGENT_DEFINITION.color as AgentColor),
maxTurns: frontmatter.maxTurns || DEFAULT_AGENT_DEFINITION.maxTurns,
systemPrompt: content || undefined,
triggerPhrases: frontmatter.triggerPhrases || [],
capabilities: frontmatter.capabilities || [],
permissions: {
allowedPaths: frontmatter.allowedPaths,
deniedPaths: frontmatter.deniedPaths,
},
});
export const loadAgentDefinitionFile = async (filePath: string): Promise<AgentLoadResult> => {
try {
const content = await readFile(filePath, "utf-8");
const parsed = parseFrontmatter(content);
if (!parsed) {
return { success: false, error: AGENT_MESSAGES.INVALID_FRONTMATTER, filePath };
}
const frontmatter = validateFrontmatter(parsed.frontmatter);
if (!frontmatter) {
return { success: false, error: AGENT_MESSAGES.MISSING_REQUIRED, filePath };
}
const agent = frontmatterToDefinition(frontmatter, parsed.body);
return { success: true, agent, filePath };
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return { success: false, error: message, filePath };
}
};
export const loadAgentDefinitionsFromDirectory = async (
directoryPath: string
): Promise<ReadonlyArray<AgentLoadResult>> => {
const resolvedPath = directoryPath.replace("~", homedir());
if (!existsSync(resolvedPath)) {
return [];
}
try {
const files = await readdir(resolvedPath);
const mdFiles = files.filter(
(file) => extname(file) === AGENT_DEFINITION.FILE_EXTENSION
);
const results = await Promise.all(
mdFiles.map((file) => loadAgentDefinitionFile(join(resolvedPath, file)))
);
return results;
} catch {
return [];
}
};
export const loadAllAgentDefinitions = async (
projectPath: string
): Promise<AgentRegistry> => {
const agents = new Map<string, AgentDefinition>();
const byTrigger = new Map<string, string>();
const byCapability = new Map<string, string[]>();
// Load from all paths in priority order (project > global > builtin)
const paths = [
join(projectPath, AGENT_DEFINITION_PATHS.PROJECT),
AGENT_DEFINITION_PATHS.GLOBAL,
];
for (const path of paths) {
const results = await loadAgentDefinitionsFromDirectory(path);
results.forEach((result) => {
if (result.success && result.agent) {
const { agent } = result;
// Don't override if already loaded (project takes precedence)
if (!agents.has(agent.name)) {
agents.set(agent.name, agent);
// Index by trigger phrases
agent.triggerPhrases?.forEach((phrase) => {
byTrigger.set(phrase.toLowerCase(), agent.name);
});
// Index by capabilities
agent.capabilities?.forEach((capability) => {
const existing = byCapability.get(capability) || [];
byCapability.set(capability, [...existing, agent.name]);
});
}
}
});
}
return { agents, byTrigger, byCapability };
};
export const findAgentByTrigger = (
registry: AgentRegistry,
text: string
): AgentDefinition | undefined => {
const normalized = text.toLowerCase();
for (const [phrase, agentName] of registry.byTrigger) {
if (normalized.includes(phrase)) {
return registry.agents.get(agentName);
}
}
return undefined;
};
export const findAgentsByCapability = (
registry: AgentRegistry,
capability: string
): ReadonlyArray<AgentDefinition> => {
const agentNames = registry.byCapability.get(capability) || [];
return agentNames
.map((name) => registry.agents.get(name))
.filter((a): a is AgentDefinition => a !== undefined);
};
export const getAgentByName = (
registry: AgentRegistry,
name: string
): AgentDefinition | undefined => registry.agents.get(name);
export const listAllAgents = (registry: AgentRegistry): ReadonlyArray<AgentDefinition> =>
Array.from(registry.agents.values());
export const createAgentDefinitionContent = (agent: AgentDefinition): string => {
const frontmatter = [
"---",
`name: ${agent.name}`,
`description: ${agent.description}`,
`tools: [${agent.tools.join(", ")}]`,
`tier: ${agent.tier}`,
`color: ${agent.color}`,
];
if (agent.maxTurns) {
frontmatter.push(`maxTurns: ${agent.maxTurns}`);
}
if (agent.triggerPhrases && agent.triggerPhrases.length > 0) {
frontmatter.push("triggerPhrases:");
agent.triggerPhrases.forEach((phrase) => frontmatter.push(` - ${phrase}`));
}
if (agent.capabilities && agent.capabilities.length > 0) {
frontmatter.push("capabilities:");
agent.capabilities.forEach((cap) => frontmatter.push(` - ${cap}`));
}
frontmatter.push("---");
const content = agent.systemPrompt || `# ${agent.name}\n\n${agent.description}`;
return `${frontmatter.join("\n")}\n\n${content}`;
};