Files
codetyper.cli/src/services/prompt-builder.ts
Carlos Gutierrez c839fc4d68 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)
2026-02-02 13:25:38 -05:00

279 lines
6.7 KiB
TypeScript

/**
* Prompt Builder Service
*
* Builds and manages system prompts based on interaction mode.
* Handles mode switching and context injection.
*/
import { buildAgenticPrompt } from "@prompts/system/agent";
import { buildAskPrompt } from "@prompts/system/ask";
import { buildCodeReviewPrompt } from "@prompts/system/code-review";
import { buildSystemPromptWithRules } from "@services/rules-service";
import { projectConfig } from "@services/project-config";
import { getProjectContextForAskMode } from "@services/context-gathering";
import type { InteractionMode } from "@/types/tui";
export interface PromptContext {
workingDir: string;
isGitRepo: boolean;
platform: string;
today: string;
model?: string;
gitBranch?: string;
gitStatus?: string;
recentCommits?: string[];
projectContext?: string;
prContext?: string;
}
export interface PromptBuilderState {
currentMode: InteractionMode;
basePrompt: string;
fullPrompt: string;
context: PromptContext;
}
const MODE_PROMPT_BUILDERS: Record<
InteractionMode,
(context: PromptContext) => string
> = {
agent: (ctx) =>
buildAgenticPrompt({
workingDir: ctx.workingDir,
isGitRepo: ctx.isGitRepo,
platform: ctx.platform,
today: ctx.today,
model: ctx.model,
gitBranch: ctx.gitBranch,
gitStatus: ctx.gitStatus,
recentCommits: ctx.recentCommits,
}),
ask: (ctx) => {
const projectContext =
ctx.projectContext ?? getProjectContextForAskMode(ctx.workingDir);
return buildAskPrompt({
workingDir: ctx.workingDir,
isGitRepo: ctx.isGitRepo,
platform: ctx.platform,
today: ctx.today,
model: ctx.model,
projectContext,
});
},
"code-review": (ctx) =>
buildCodeReviewPrompt({
workingDir: ctx.workingDir,
isGitRepo: ctx.isGitRepo,
platform: ctx.platform,
today: ctx.today,
model: ctx.model,
prContext: ctx.prContext,
}),
};
/**
* Execute git command asynchronously
*/
const execGitCommand = (args: string[]): Promise<string> => {
return new Promise((resolve, reject) => {
const { spawn } = require("child_process");
const proc = spawn("git", args, { cwd: process.cwd() });
let stdout = "";
let stderr = "";
proc.stdout.on("data", (data: Buffer) => {
stdout += data.toString();
});
proc.stderr.on("data", (data: Buffer) => {
stderr += data.toString();
});
proc.on("close", (code: number) => {
if (code === 0) {
resolve(stdout.trim());
} else {
reject(new Error(stderr || `git exited with code ${code}`));
}
});
proc.on("error", reject);
});
};
/**
* Get git context for prompt building (async, non-blocking)
*/
export const getGitContext = async (): Promise<{
isGitRepo: boolean;
branch?: string;
status?: string;
recentCommits?: string[];
}> => {
try {
// Run all git commands in parallel for faster execution
const [branch, status, commits] = await Promise.all([
execGitCommand(["branch", "--show-current"]),
execGitCommand(["status", "--short"]).then((s) => s || "(clean)"),
execGitCommand(["log", "--oneline", "-5"]).then((s) =>
s.split("\n").filter(Boolean),
),
]);
return { isGitRepo: true, branch, status, recentCommits: commits };
} catch {
return { isGitRepo: false };
}
};
/**
* Build base context for all modes
*/
export const buildBaseContext = async (
model?: string,
): Promise<PromptContext> => {
const gitContext = await getGitContext();
return {
workingDir: process.cwd(),
isGitRepo: gitContext.isGitRepo,
platform: process.platform,
today: new Date().toISOString().split("T")[0],
model,
gitBranch: gitContext.branch,
gitStatus: gitContext.status,
recentCommits: gitContext.recentCommits,
};
};
/**
* Build the base prompt for a specific mode
*/
export const buildModePrompt = (
mode: InteractionMode,
context: PromptContext,
): string => {
const builder = MODE_PROMPT_BUILDERS[mode];
return builder(context);
};
/**
* Build complete system prompt with rules and learnings
*/
export const buildCompletePrompt = async (
mode: InteractionMode,
context: PromptContext,
appendPrompt?: string,
): Promise<{ prompt: string; rulesPaths: string[] }> => {
const basePrompt = buildModePrompt(mode, context);
const { prompt: promptWithRules, rulesPaths } =
await buildSystemPromptWithRules(basePrompt, context.workingDir);
let finalPrompt = promptWithRules;
const learningsContext = await projectConfig.buildLearningsContext();
if (learningsContext) {
finalPrompt = finalPrompt + "\n\n" + learningsContext;
}
if (appendPrompt) {
finalPrompt = finalPrompt + "\n\n" + appendPrompt;
}
return { prompt: finalPrompt, rulesPaths };
};
/**
* Create a prompt builder instance for managing prompts across mode changes
*/
export const createPromptBuilder = (initialModel?: string) => {
let state: PromptBuilderState | null = null;
const initialize = async (
mode: InteractionMode,
appendPrompt?: string,
): Promise<string> => {
const context = await buildBaseContext(initialModel);
const { prompt } = await buildCompletePrompt(mode, context, appendPrompt);
state = {
currentMode: mode,
basePrompt: buildModePrompt(mode, context),
fullPrompt: prompt,
context,
};
return prompt;
};
const switchMode = async (
newMode: InteractionMode,
appendPrompt?: string,
): Promise<string> => {
if (!state) {
return initialize(newMode, appendPrompt);
}
if (state.currentMode === newMode) {
return state.fullPrompt;
}
const { prompt } = await buildCompletePrompt(
newMode,
state.context,
appendPrompt,
);
state = {
currentMode: newMode,
basePrompt: buildModePrompt(newMode, state.context),
fullPrompt: prompt,
context: state.context,
};
return prompt;
};
const getCurrentPrompt = (): string | null => state?.fullPrompt ?? null;
const getCurrentMode = (): InteractionMode | null =>
state?.currentMode ?? null;
const updateContext = async (
updates: Partial<PromptContext>,
appendPrompt?: string,
): Promise<string> => {
if (!state) {
throw new Error("Prompt builder not initialized");
}
const newContext = { ...state.context, ...updates };
const { prompt } = await buildCompletePrompt(
state.currentMode,
newContext,
appendPrompt,
);
state = {
...state,
context: newContext,
fullPrompt: prompt,
};
return prompt;
};
return {
initialize,
switchMode,
getCurrentPrompt,
getCurrentMode,
updateContext,
};
};
export type PromptBuilder = ReturnType<typeof createPromptBuilder>;