Fixing the prompts

This commit is contained in:
2026-01-31 19:20:36 -05:00
parent 0cd557a3fe
commit 8691b926fb
12 changed files with 1376 additions and 358 deletions

View File

@@ -325,7 +325,14 @@ export const runAgentLoopStream = async (
// Initialize
await initializePermissions();
await refreshMCPTools();
// Refresh MCP tools and log results
const mcpResult = await refreshMCPTools();
if (mcpResult.success && mcpResult.toolCount > 0) {
state.options.onText?.(`[Loaded ${mcpResult.toolCount} MCP tool(s)]\n`);
} else if (mcpResult.error) {
state.options.onWarning?.(`MCP tools unavailable: ${mcpResult.error}`);
}
const agentMessages: AgentMessage[] = [...messages];

View File

@@ -11,24 +11,24 @@ import {
} from "@services/session";
import { getConfig } from "@services/config";
import { initializePermissions } from "@services/permissions";
import { projectConfig } from "@services/project-config";
import { getProviderStatus } from "@providers/index";
import { appStore } from "@tui/index";
import { themeActions } from "@stores/theme-store";
import {
AGENTIC_SYSTEM_PROMPT,
buildAgenticPrompt,
buildSystemPromptWithRules,
} from "@prompts/index";
buildBaseContext,
buildCompletePrompt,
} from "@services/prompt-builder";
import { initSuggestionService } from "@services/command-suggestion-service";
import { addContextFile } from "@services/chat-tui/files";
import type { ProviderName, Message } from "@/types/providers";
import type { ChatSession } from "@/types/index";
import type { ChatTUIOptions } from "@interfaces/ChatTUIOptions";
import type { ChatServiceState } from "@/types/chat-service";
import type { InteractionMode } from "@/types/tui";
const createInitialState = async (
options: ChatTUIOptions,
initialMode: InteractionMode = "agent",
): Promise<ChatServiceState> => {
const config = await getConfig();
@@ -37,7 +37,8 @@ const createInitialState = async (
model: options.model || config.get("model") || undefined,
messages: [],
contextFiles: new Map(),
systemPrompt: AGENTIC_SYSTEM_PROMPT,
systemPrompt: "",
currentMode: initialMode,
verbose: options.verbose || false,
autoApprove: options.autoApprove || false,
};
@@ -52,26 +53,6 @@ const validateProvider = async (state: ChatServiceState): Promise<void> => {
}
};
const getGitContext = async (): Promise<{
isGitRepo: boolean;
branch?: string;
status?: string;
recentCommits?: string[];
}> => {
try {
const { execSync } = await import("child_process");
const branch = execSync("git branch --show-current", { encoding: "utf-8" }).trim();
const status = execSync("git status --short", { encoding: "utf-8" }).trim() || "(clean)";
const commits = execSync("git log --oneline -5", { encoding: "utf-8" })
.trim()
.split("\n")
.filter(Boolean);
return { isGitRepo: true, branch, status, recentCommits: commits };
} catch {
return { isGitRepo: false };
}
};
const buildSystemPrompt = async (
state: ChatServiceState,
options: ChatTUIOptions,
@@ -81,23 +62,14 @@ const buildSystemPrompt = async (
return;
}
// Build agentic prompt with environment context
const gitContext = await getGitContext();
const basePrompt = buildAgenticPrompt({
workingDir: process.cwd(),
isGitRepo: gitContext.isGitRepo,
platform: process.platform,
today: new Date().toISOString().split("T")[0],
model: state.model,
gitBranch: gitContext.branch,
gitStatus: gitContext.status,
recentCommits: gitContext.recentCommits,
});
const context = await buildBaseContext(state.model);
const { prompt, rulesPaths } = await buildCompletePrompt(
state.currentMode,
context,
options.appendSystemPrompt,
);
// Add project rules
const { prompt: promptWithRules, rulesPaths } =
await buildSystemPromptWithRules(basePrompt, process.cwd());
state.systemPrompt = promptWithRules;
state.systemPrompt = prompt;
if (rulesPaths.length > 0 && state.verbose) {
infoMessage(`Loaded ${rulesPaths.length} rule file(s):`);
@@ -105,19 +77,6 @@ const buildSystemPrompt = async (
infoMessage(` - ${rulePath}`);
}
}
const learningsContext = await projectConfig.buildLearningsContext();
if (learningsContext) {
state.systemPrompt = state.systemPrompt + "\n\n" + learningsContext;
if (state.verbose) {
infoMessage("Loaded project learnings");
}
}
if (options.appendSystemPrompt) {
state.systemPrompt =
state.systemPrompt + "\n\n" + options.appendSystemPrompt;
}
};
const restoreMessagesFromSession = (
@@ -188,10 +147,36 @@ const initializeTheme = async (): Promise<void> => {
}
};
/**
* Rebuild system prompt when interaction mode changes
* Updates both the state and the first message in the conversation
*/
export const rebuildSystemPromptForMode = async (
state: ChatServiceState,
newMode: InteractionMode,
appendPrompt?: string,
): Promise<void> => {
if (state.currentMode === newMode) {
return;
}
state.currentMode = newMode;
const context = await buildBaseContext(state.model);
const { prompt } = await buildCompletePrompt(newMode, context, appendPrompt);
state.systemPrompt = prompt;
if (state.messages.length > 0 && state.messages[0].role === "system") {
state.messages[0].content = prompt;
}
};
export const initializeChatService = async (
options: ChatTUIOptions,
): Promise<{ state: ChatServiceState; session: ChatSession }> => {
const state = await createInitialState(options);
const initialMode = appStore.getState().interactionMode;
const state = await createInitialState(options, initialMode);
await validateProvider(state);
await buildSystemPrompt(state, options);

View File

@@ -26,6 +26,7 @@ import {
} from "@services/chat-tui/files";
import { getToolDescription } from "@services/chat-tui/utils";
import { processLearningsFromExchange } from "@services/chat-tui/learnings";
import { rebuildSystemPromptForMode } from "@services/chat-tui/initialize";
import {
compactConversation,
createCompactionSummary,
@@ -61,6 +62,10 @@ import {
detectCommand,
executeDetectedCommand,
} from "@services/command-detection";
import {
detectSkillCommand,
executeSkill,
} from "@services/skill-service";
// Track last response for feedback learning
let lastResponseContext: {
@@ -276,6 +281,30 @@ export const handleMessage = async (
// Check for feedback on previous response
await checkUserFeedback(message, callbacks);
// Check for skill commands (e.g., /review, /commit)
const skillMatch = await detectSkillCommand(message);
if (skillMatch) {
addDebugLog("info", `Detected skill: /${skillMatch.skill.command}`);
callbacks.onLog(
"system",
`Running skill: ${skillMatch.skill.name}`,
);
// Execute the skill and get the expanded prompt
const { expandedPrompt } = executeSkill(skillMatch.skill, skillMatch.args);
// Show the original command
appStore.addLog({
type: "user",
content: message,
});
// Process the expanded prompt as the actual message
// Fall through to normal processing with the expanded prompt
message = expandedPrompt;
addDebugLog("info", `Expanded skill prompt: ${expandedPrompt.substring(0, 100)}...`);
}
// Detect explicit command requests and execute directly
const detected = detectCommand(message);
if (detected.detected && detected.command) {
@@ -324,11 +353,20 @@ export const handleMessage = async (
const { interactionMode, cascadeEnabled } = appStore.getState();
const isReadOnlyMode = interactionMode === "ask" || interactionMode === "code-review";
// Rebuild system prompt if mode has changed
if (state.currentMode !== interactionMode) {
await rebuildSystemPromptForMode(state, interactionMode);
callbacks.onLog(
"system",
`Switched to ${interactionMode} mode`,
);
}
if (isReadOnlyMode) {
const modeLabel = interactionMode === "ask" ? "Ask" : "Code Review";
callbacks.onLog(
"system",
`${modeLabel} mode: Read-only responses (Ctrl+Tab to switch modes)`,
`${modeLabel} mode: Read-only tools only (Ctrl+Tab to switch modes)`,
);
}

View File

@@ -0,0 +1,263 @@
/**
* Context Gathering Service
*
* Automatically gathers project context for ask mode.
* Provides codebase overview without requiring manual file references.
*/
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
import { join } from "path";
export interface ProjectContext {
projectType: string;
name?: string;
description?: string;
mainLanguage: string;
frameworks: string[];
structure: string;
keyFiles: string[];
dependencies?: string[];
}
interface ProjectConfig {
name?: string;
description?: string;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
scripts?: Record<string, string>;
}
const PROJECT_MARKERS: Record<string, { type: string; language: string }> = {
"package.json": { type: "Node.js", language: "JavaScript/TypeScript" },
"tsconfig.json": { type: "TypeScript", language: "TypeScript" },
"Cargo.toml": { type: "Rust", language: "Rust" },
"go.mod": { type: "Go", language: "Go" },
"pom.xml": { type: "Maven/Java", language: "Java" },
"build.gradle": { type: "Gradle/Java", language: "Java" },
"pyproject.toml": { type: "Python", language: "Python" },
"setup.py": { type: "Python", language: "Python" },
"requirements.txt": { type: "Python", language: "Python" },
"Gemfile": { type: "Ruby", language: "Ruby" },
"composer.json": { type: "PHP", language: "PHP" },
".csproj": { type: ".NET", language: "C#" },
};
const FRAMEWORK_MARKERS: Record<string, string[]> = {
react: ["react", "react-dom", "next", "gatsby"],
vue: ["vue", "nuxt"],
angular: ["@angular/core"],
svelte: ["svelte", "sveltekit"],
express: ["express"],
fastify: ["fastify"],
nestjs: ["@nestjs/core"],
django: ["django"],
flask: ["flask"],
rails: ["rails"],
spring: ["spring-boot"],
};
const IGNORED_DIRS = new Set([
"node_modules",
".git",
"dist",
"build",
".next",
".nuxt",
"target",
"__pycache__",
".venv",
"venv",
"vendor",
".idea",
".vscode",
"coverage",
]);
const detectProjectType = (workingDir: string): { type: string; language: string } => {
for (const [marker, info] of Object.entries(PROJECT_MARKERS)) {
if (existsSync(join(workingDir, marker))) {
return info;
}
}
return { type: "Unknown", language: "Unknown" };
};
const detectFrameworks = (deps: Record<string, string>): string[] => {
const frameworks: string[] = [];
for (const [framework, markers] of Object.entries(FRAMEWORK_MARKERS)) {
for (const marker of markers) {
if (deps[marker]) {
frameworks.push(framework);
break;
}
}
}
return frameworks;
};
const readPackageJson = (workingDir: string): ProjectConfig | null => {
const packagePath = join(workingDir, "package.json");
if (!existsSync(packagePath)) return null;
try {
const content = readFileSync(packagePath, "utf-8");
return JSON.parse(content) as ProjectConfig;
} catch {
return null;
}
};
const getDirectoryStructure = (
dir: string,
baseDir: string,
depth = 0,
maxDepth = 3,
): string[] => {
if (depth >= maxDepth) return [];
const entries: string[] = [];
try {
const items = readdirSync(dir);
for (const item of items) {
if (IGNORED_DIRS.has(item) || item.startsWith(".")) continue;
const fullPath = join(dir, item);
try {
const stat = statSync(fullPath);
const indent = " ".repeat(depth);
if (stat.isDirectory()) {
entries.push(`${indent}${item}/`);
const subEntries = getDirectoryStructure(fullPath, baseDir, depth + 1, maxDepth);
entries.push(...subEntries);
} else if (depth < 2) {
entries.push(`${indent}${item}`);
}
} catch {
// Skip inaccessible files
}
}
} catch {
// Skip inaccessible directories
}
return entries;
};
const getKeyFiles = (workingDir: string): string[] => {
const keyPatterns = [
"README.md",
"readme.md",
"README",
"package.json",
"tsconfig.json",
"Cargo.toml",
"go.mod",
"pyproject.toml",
".env.example",
"docker-compose.yml",
"Dockerfile",
"Makefile",
];
const found: string[] = [];
for (const pattern of keyPatterns) {
if (existsSync(join(workingDir, pattern))) {
found.push(pattern);
}
}
return found;
};
const getMainDependencies = (pkg: ProjectConfig): string[] => {
const allDeps = {
...pkg.dependencies,
...pkg.devDependencies,
};
const importantDeps = Object.keys(allDeps).filter(
(dep) =>
!dep.startsWith("@types/") &&
!dep.startsWith("eslint") &&
!dep.startsWith("prettier") &&
!dep.includes("lint"),
);
return importantDeps.slice(0, 15);
};
/**
* Gather comprehensive project context
*/
export const gatherProjectContext = (workingDir: string): ProjectContext => {
const { type, language } = detectProjectType(workingDir);
const pkg = readPackageJson(workingDir);
const structure = getDirectoryStructure(workingDir, workingDir);
const keyFiles = getKeyFiles(workingDir);
const frameworks = pkg
? detectFrameworks({ ...pkg.dependencies, ...pkg.devDependencies })
: [];
const dependencies = pkg ? getMainDependencies(pkg) : undefined;
return {
projectType: type,
name: pkg?.name,
description: pkg?.description,
mainLanguage: language,
frameworks,
structure: structure.slice(0, 50).join("\n"),
keyFiles,
dependencies,
};
};
/**
* Build a formatted context string for injection into prompts
*/
export const buildProjectContextString = (context: ProjectContext): string => {
const sections: string[] = [];
const header = context.name
? `Project: ${context.name}`
: `Project Type: ${context.projectType}`;
sections.push(header);
if (context.description) {
sections.push(`Description: ${context.description}`);
}
sections.push(`Language: ${context.mainLanguage}`);
if (context.frameworks.length > 0) {
sections.push(`Frameworks: ${context.frameworks.join(", ")}`);
}
if (context.keyFiles.length > 0) {
sections.push(`Key Files: ${context.keyFiles.join(", ")}`);
}
if (context.dependencies && context.dependencies.length > 0) {
sections.push(`Main Dependencies: ${context.dependencies.join(", ")}`);
}
if (context.structure) {
sections.push(`\nProject Structure:\n\`\`\`\n${context.structure}\n\`\`\``);
}
return sections.join("\n");
};
/**
* Get project context for ask mode prompts
*/
export const getProjectContextForAskMode = (workingDir: string): string => {
const context = gatherProjectContext(workingDir);
return buildProjectContextString(context);
};

View File

@@ -0,0 +1,249 @@
/**
* 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,
}),
};
/**
* Get git context for prompt building
*/
export const getGitContext = async (): Promise<{
isGitRepo: boolean;
branch?: string;
status?: string;
recentCommits?: string[];
}> => {
try {
const { execSync } = await import("child_process");
const branch = execSync("git branch --show-current", {
encoding: "utf-8",
}).trim();
const status =
execSync("git status --short", { encoding: "utf-8" }).trim() || "(clean)";
const commits = execSync("git log --oneline -5", { encoding: "utf-8" })
.trim()
.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>;

View File

@@ -0,0 +1,121 @@
/**
* Skill Service
*
* Handles skill detection and execution.
* Skills are custom commands that expand into prompts.
*/
import { getSkills } from "@services/project-config";
import type { SkillConfig } from "@/types/project-config";
export interface SkillMatch {
skill: SkillConfig;
args: string;
}
export interface SkillExecutionResult {
expandedPrompt: string;
skill: SkillConfig;
}
let skillsCache: SkillConfig[] | null = null;
/**
* Load and cache skills
*/
export const loadSkills = async (): Promise<SkillConfig[]> => {
if (skillsCache) return skillsCache;
skillsCache = await getSkills();
return skillsCache;
};
/**
* Refresh skills cache
*/
export const refreshSkills = async (): Promise<void> => {
skillsCache = await getSkills();
};
/**
* Detect if a message starts with a skill command
* Skills are invoked with /command syntax
*/
export const detectSkillCommand = async (
message: string,
): Promise<SkillMatch | null> => {
const trimmed = message.trim();
if (!trimmed.startsWith("/")) {
return null;
}
const skills = await loadSkills();
for (const skill of skills) {
const commandPattern = `/${skill.command}`;
if (
trimmed === commandPattern ||
trimmed.startsWith(`${commandPattern} `)
) {
const args = trimmed.slice(commandPattern.length).trim();
return { skill, args };
}
}
return null;
};
/**
* Execute a skill by expanding its prompt template
* Supports $ARGS placeholder for arguments
*/
export const executeSkill = (
skill: SkillConfig,
args: string,
): SkillExecutionResult => {
let expandedPrompt = skill.prompt;
// Replace $ARGS placeholder with actual arguments
expandedPrompt = expandedPrompt.replace(/\$ARGS/g, args);
// Replace $1, $2, etc. with positional arguments
const argParts = args.split(/\s+/).filter(Boolean);
for (let i = 0; i < argParts.length; i++) {
expandedPrompt = expandedPrompt.replace(
new RegExp(`\\$${i + 1}`, "g"),
argParts[i],
);
}
// Clean up any remaining unreplaced placeholders
expandedPrompt = expandedPrompt.replace(/\$\d+/g, "");
expandedPrompt = expandedPrompt.replace(/\$ARGS/g, "");
return {
expandedPrompt: expandedPrompt.trim(),
skill,
};
};
/**
* Get all available skill commands for display
*/
export const getAvailableSkills = async (): Promise<
Array<{ command: string; name: string; description: string }>
> => {
const skills = await loadSkills();
return skills.map((s) => ({
command: `/${s.command}`,
name: s.name,
description: s.description,
}));
};
/**
* Check if skills are available
*/
export const hasSkills = async (): Promise<boolean> => {
const skills = await loadSkills();
return skills.length > 0;
};