Terminal-based AI coding agent with interactive TUI for autonomous code generation.
Features: - Interactive TUI with React/Ink - Autonomous agent with tool calls (bash, read, write, edit, glob, grep) - Permission system with pattern-based rules - Session management with auto-compaction - Dual providers: GitHub Copilot and Ollama - MCP server integration - Todo panel and theme system - Streaming responses - GitHub-compatible project context
This commit is contained in:
36
src/commands/chat-tui.ts
Normal file
36
src/commands/chat-tui.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* TUI-based Chat Command for CodeTyper CLI (Presentation Layer)
|
||||
*
|
||||
* This file is the main entry point for the chat TUI.
|
||||
* It assembles callbacks and re-exports the execute function.
|
||||
* All business logic is delegated to chat-tui-service.ts
|
||||
*/
|
||||
|
||||
import type { ChatServiceCallbacks } from "@services/chat-tui-service.ts";
|
||||
import { onModeChange } from "@commands/components/callbacks/on-mode-change.ts";
|
||||
import { onLog } from "@commands/components/callbacks/on-log.ts";
|
||||
import { onToolCall } from "@commands/components/callbacks/on-tool-call.ts";
|
||||
import { onToolResult } from "@commands/components/callbacks/on-tool-result.ts";
|
||||
import { onPermissionRequest } from "@commands/components/callbacks/on-permission-request.ts";
|
||||
import { onLearningDetected } from "@commands/components/callbacks/on-learning-detected.ts";
|
||||
import executeCommand from "@commands/components/execute/index.ts";
|
||||
|
||||
export const createCallbacks = (): ChatServiceCallbacks => ({
|
||||
onModeChange,
|
||||
onLog,
|
||||
onToolCall,
|
||||
onToolResult,
|
||||
onPermissionRequest,
|
||||
onLearningDetected,
|
||||
});
|
||||
|
||||
export const execute = executeCommand;
|
||||
|
||||
export {
|
||||
onModeChange,
|
||||
onLog,
|
||||
onToolCall,
|
||||
onToolResult,
|
||||
onPermissionRequest,
|
||||
onLearningDetected,
|
||||
};
|
||||
8
src/commands/chat.ts
Normal file
8
src/commands/chat.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Interactive chat mode for CodeTyper CLI
|
||||
*
|
||||
* This file re-exports the modular chat implementation.
|
||||
*/
|
||||
|
||||
export { execute, createInitialState } from "@commands/components/chat/index";
|
||||
export type { ChatState } from "@commands/components/chat/index";
|
||||
23
src/commands/components/callbacks/on-learning-detected.ts
Normal file
23
src/commands/components/callbacks/on-learning-detected.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { appStore } from "@tui/index.ts";
|
||||
import type { LearningResponse } from "@tui/types.ts";
|
||||
import type { LearningCandidate } from "@services/learning-service.ts";
|
||||
|
||||
export const onLearningDetected = async (
|
||||
candidate: LearningCandidate,
|
||||
): Promise<LearningResponse> => {
|
||||
return new Promise((resolve) => {
|
||||
appStore.setMode("learning_prompt");
|
||||
appStore.setLearningPrompt({
|
||||
id: uuidv4(),
|
||||
content: candidate.content,
|
||||
context: candidate.context,
|
||||
category: candidate.category,
|
||||
resolve: (response: LearningResponse) => {
|
||||
appStore.setLearningPrompt(null);
|
||||
appStore.setMode("idle");
|
||||
resolve(response);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
14
src/commands/components/callbacks/on-log.ts
Normal file
14
src/commands/components/callbacks/on-log.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { appStore } from "@tui/index.ts";
|
||||
import type { LogType } from "@/types/log";
|
||||
|
||||
export const onLog = (
|
||||
type: string,
|
||||
content: string,
|
||||
metadata?: Record<string, unknown>,
|
||||
): void => {
|
||||
appStore.addLog({
|
||||
type: type as LogType,
|
||||
content,
|
||||
metadata,
|
||||
});
|
||||
};
|
||||
5
src/commands/components/callbacks/on-mode-change.ts
Normal file
5
src/commands/components/callbacks/on-mode-change.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { appStore } from "@tui/index.ts";
|
||||
|
||||
export const onModeChange = (mode: string): void => {
|
||||
appStore.setMode(mode as Parameters<typeof appStore.setMode>[0]);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
interface PermissionResponse {
|
||||
allowed: boolean;
|
||||
}
|
||||
|
||||
export const onPermissionRequest = async (): Promise<PermissionResponse> => {
|
||||
return { allowed: false };
|
||||
};
|
||||
25
src/commands/components/callbacks/on-tool-call.ts
Normal file
25
src/commands/components/callbacks/on-tool-call.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { appStore } from "@tui/index.ts";
|
||||
import { isQuietTool } from "@utils/tools.ts";
|
||||
import type { ToolCallParams } from "@interfaces/ToolCallParams.ts";
|
||||
|
||||
export const onToolCall = (call: ToolCallParams): void => {
|
||||
appStore.setCurrentToolCall({
|
||||
id: call.id,
|
||||
name: call.name,
|
||||
description: call.description,
|
||||
status: "running",
|
||||
});
|
||||
|
||||
const isQuiet = isQuietTool(call.name, call.args);
|
||||
|
||||
appStore.addLog({
|
||||
type: "tool",
|
||||
content: call.description,
|
||||
metadata: {
|
||||
toolName: call.name,
|
||||
toolStatus: "running",
|
||||
toolDescription: call.description,
|
||||
quiet: isQuiet,
|
||||
},
|
||||
});
|
||||
};
|
||||
47
src/commands/components/callbacks/on-tool-result.ts
Normal file
47
src/commands/components/callbacks/on-tool-result.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { appStore } from "@tui/index.ts";
|
||||
import {
|
||||
truncateOutput,
|
||||
detectDiffContent,
|
||||
} from "@services/chat-tui-service.ts";
|
||||
import { getThinkingMessage } from "@constants/status-messages.ts";
|
||||
|
||||
export const onToolResult = (
|
||||
success: boolean,
|
||||
title: string,
|
||||
output?: string,
|
||||
error?: string,
|
||||
): void => {
|
||||
appStore.updateToolCall({
|
||||
status: success ? "success" : "error",
|
||||
result: success ? output : undefined,
|
||||
error: error,
|
||||
});
|
||||
|
||||
const state = appStore.getState();
|
||||
const logEntry = state.logs.find(
|
||||
(log) => log.type === "tool" && log.metadata?.toolStatus === "running",
|
||||
);
|
||||
|
||||
if (logEntry) {
|
||||
const diffData = output ? detectDiffContent(output) : undefined;
|
||||
const displayContent = diffData?.isDiff
|
||||
? output
|
||||
: output
|
||||
? truncateOutput(output)
|
||||
: "";
|
||||
|
||||
appStore.updateLog(logEntry.id, {
|
||||
content: success
|
||||
? `${title}${displayContent ? "\n" + displayContent : ""}`
|
||||
: `${title}: ${error}`,
|
||||
metadata: {
|
||||
...logEntry.metadata,
|
||||
toolStatus: success ? "success" : "error",
|
||||
toolDescription: title,
|
||||
diffData: diffData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
appStore.setThinkingMessage(getThinkingMessage());
|
||||
};
|
||||
35
src/commands/components/chat/agents/show-agents.ts
Normal file
35
src/commands/components/chat/agents/show-agents.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Show available agents command
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { agentLoader } from "@services/agent-loader";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
|
||||
export const showAgents = async (state: ChatState): Promise<void> => {
|
||||
const agents = await agentLoader.getAvailableAgents(process.cwd());
|
||||
const currentAgent = state.currentAgent ?? "coder";
|
||||
|
||||
console.log("\n" + chalk.bold.underline("Available Agents") + "\n");
|
||||
|
||||
for (const agent of agents) {
|
||||
const isCurrent = agent.id === currentAgent;
|
||||
const marker = isCurrent ? chalk.cyan("→") : " ";
|
||||
const nameStyle = isCurrent ? chalk.cyan.bold : chalk.white;
|
||||
|
||||
console.log(`${marker} ${nameStyle(agent.name)}`);
|
||||
|
||||
if (agent.description) {
|
||||
console.log(` ${chalk.gray(agent.description)}`);
|
||||
}
|
||||
|
||||
if (agent.model) {
|
||||
console.log(` ${chalk.gray(`Model: ${agent.model}`)}`);
|
||||
}
|
||||
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log(chalk.gray("Use /agent <name> to switch agents"));
|
||||
console.log();
|
||||
};
|
||||
60
src/commands/components/chat/agents/switch-agent.ts
Normal file
60
src/commands/components/chat/agents/switch-agent.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Switch agent command
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { errorMessage, infoMessage, warningMessage } from "@utils/terminal";
|
||||
import { agentLoader } from "@services/agent-loader";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
|
||||
export const switchAgent = async (
|
||||
agentName: string,
|
||||
state: ChatState,
|
||||
): Promise<void> => {
|
||||
if (!agentName.trim()) {
|
||||
warningMessage("Usage: /agent <name>");
|
||||
infoMessage("Use /agents to see available agents");
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedName = agentName.toLowerCase().trim();
|
||||
const agents = await agentLoader.getAvailableAgents(process.cwd());
|
||||
|
||||
// Find agent by id or partial name match
|
||||
const agent = agents.find(
|
||||
(a) =>
|
||||
a.id === normalizedName ||
|
||||
a.name.toLowerCase() === normalizedName ||
|
||||
a.id.includes(normalizedName) ||
|
||||
a.name.toLowerCase().includes(normalizedName),
|
||||
);
|
||||
|
||||
if (!agent) {
|
||||
errorMessage(`Agent not found: ${agentName}`);
|
||||
infoMessage("Use /agents to see available agents");
|
||||
return;
|
||||
}
|
||||
|
||||
state.currentAgent = agent.id;
|
||||
|
||||
// Update system prompt with agent prompt
|
||||
if (agent.prompt) {
|
||||
// Prepend agent prompt to system prompt
|
||||
const basePrompt = state.systemPrompt;
|
||||
state.systemPrompt = `${agent.prompt}\n\n${basePrompt}`;
|
||||
|
||||
// Update the system message in messages array
|
||||
if (state.messages.length > 0 && state.messages[0].role === "system") {
|
||||
state.messages[0].content = state.systemPrompt;
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(chalk.green(`✓ Switched to agent: ${chalk.bold(agent.name)}`));
|
||||
|
||||
if (agent.description) {
|
||||
console.log(chalk.gray(` ${agent.description}`));
|
||||
}
|
||||
|
||||
console.log();
|
||||
};
|
||||
12
src/commands/components/chat/cleanup.ts
Normal file
12
src/commands/components/chat/cleanup.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import chalk from "chalk";
|
||||
import type { ChatState } from "./state.ts";
|
||||
|
||||
export const createCleanup = (state: ChatState) => (): void => {
|
||||
state.isRunning = false;
|
||||
if (state.inputEditor) {
|
||||
state.inputEditor.stop();
|
||||
state.inputEditor = null;
|
||||
}
|
||||
console.log("\n" + chalk.cyan("Goodbye!"));
|
||||
process.exit(0);
|
||||
};
|
||||
111
src/commands/components/chat/commands/commandsRegistry.ts
Normal file
111
src/commands/components/chat/commands/commandsRegistry.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { saveSession } from "@services/session";
|
||||
import { showHelp } from "@commands/components/chat/commands/show-help";
|
||||
import { clearConversation } from "@commands/components/chat/history/clear-conversation";
|
||||
import { showContextFiles } from "@commands/components/chat/context/show-context-files";
|
||||
import { removeFile } from "@commands/components/chat/context/remove-file";
|
||||
import { showContext } from "@commands/components/chat/history/show-context";
|
||||
import { compactHistory } from "@commands/components/chat/history/compact-history";
|
||||
import { showHistory } from "@commands/components/chat/history/show-history";
|
||||
import { showModels } from "@commands/components/chat/models/show-models";
|
||||
import { showProviders } from "@commands/components/chat/models/show-providers";
|
||||
import { switchProvider } from "@commands/components/chat/models/switch-provider";
|
||||
import { switchModel } from "@commands/components/chat/models/switch-model";
|
||||
import { showSessionInfo } from "@commands/components/chat/session/show-session-info";
|
||||
import { listSessions } from "@commands/components/chat/session/list-sessions";
|
||||
import { showUsage } from "@commands/components/chat/usage/show-usage";
|
||||
import { showAgents } from "@commands/components/chat/agents/show-agents";
|
||||
import { switchAgent } from "@commands/components/chat/agents/switch-agent";
|
||||
import { handleMCP } from "@commands/components/chat/mcp/handle-mcp";
|
||||
import { CommandContext } from "@interfaces/commandContext";
|
||||
import type { CommandHandler } from "@/types/commandHandler";
|
||||
import { successMessage } from "@utils/terminal";
|
||||
|
||||
const COMMAND_REGISTRY: Map<string, CommandHandler> = new Map<
|
||||
string,
|
||||
CommandHandler
|
||||
>([
|
||||
["help", () => showHelp()],
|
||||
["h", () => showHelp()],
|
||||
["clear", (ctx: CommandContext) => clearConversation(ctx.state)],
|
||||
["c", (ctx: CommandContext) => clearConversation(ctx.state)],
|
||||
["files", (ctx: CommandContext) => showContextFiles(ctx.state.contextFiles)],
|
||||
["f", (ctx: CommandContext) => showContextFiles(ctx.state.contextFiles)],
|
||||
["exit", (ctx: CommandContext) => ctx.cleanup()],
|
||||
["quit", (ctx: CommandContext) => ctx.cleanup()],
|
||||
["q", (ctx: CommandContext) => ctx.cleanup()],
|
||||
[
|
||||
"save",
|
||||
async () => {
|
||||
await saveSession();
|
||||
successMessage("Session saved");
|
||||
},
|
||||
],
|
||||
[
|
||||
"s",
|
||||
async () => {
|
||||
await saveSession();
|
||||
successMessage("Session saved");
|
||||
},
|
||||
],
|
||||
[
|
||||
"models",
|
||||
async (ctx: CommandContext) =>
|
||||
showModels(ctx.state.currentProvider, ctx.state.currentModel),
|
||||
],
|
||||
[
|
||||
"m",
|
||||
async (ctx: CommandContext) =>
|
||||
showModels(ctx.state.currentProvider, ctx.state.currentModel),
|
||||
],
|
||||
["providers", async () => showProviders()],
|
||||
["p", async () => showProviders()],
|
||||
[
|
||||
"provider",
|
||||
async (ctx: CommandContext) =>
|
||||
switchProvider(ctx.args.join(" "), ctx.state),
|
||||
],
|
||||
[
|
||||
"model",
|
||||
async (ctx: CommandContext) => switchModel(ctx.args.join(" "), ctx.state),
|
||||
],
|
||||
["context", (ctx: CommandContext) => showContext(ctx.state)],
|
||||
["compact", (ctx: CommandContext) => compactHistory(ctx.state)],
|
||||
["history", (ctx: CommandContext) => showHistory(ctx.state)],
|
||||
[
|
||||
"remove",
|
||||
(ctx: CommandContext) =>
|
||||
removeFile(ctx.args.join(" "), ctx.state.contextFiles),
|
||||
],
|
||||
[
|
||||
"rm",
|
||||
(ctx: CommandContext) =>
|
||||
removeFile(ctx.args.join(" "), ctx.state.contextFiles),
|
||||
],
|
||||
["session", async () => showSessionInfo()],
|
||||
["sessions", async () => listSessions()],
|
||||
["usage", async (ctx: CommandContext) => showUsage(ctx.state)],
|
||||
["u", async (ctx: CommandContext) => showUsage(ctx.state)],
|
||||
[
|
||||
"agent",
|
||||
async (ctx: CommandContext) => {
|
||||
if (ctx.args.length === 0) {
|
||||
await showAgents(ctx.state);
|
||||
} else {
|
||||
await switchAgent(ctx.args.join(" "), ctx.state);
|
||||
}
|
||||
},
|
||||
],
|
||||
[
|
||||
"a",
|
||||
async (ctx: CommandContext) => {
|
||||
if (ctx.args.length === 0) {
|
||||
await showAgents(ctx.state);
|
||||
} else {
|
||||
await switchAgent(ctx.args.join(" "), ctx.state);
|
||||
}
|
||||
},
|
||||
],
|
||||
["mcp", async (ctx: CommandContext) => handleMCP(ctx.args)],
|
||||
]);
|
||||
|
||||
export default COMMAND_REGISTRY;
|
||||
28
src/commands/components/chat/commands/handle-command.ts
Normal file
28
src/commands/components/chat/commands/handle-command.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { warningMessage, infoMessage } from "@utils/terminal";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
import COMMAND_REGISTRY from "@commands/components/chat/commands/commandsRegistry";
|
||||
|
||||
const isValidCommand = (cmd: string): boolean => {
|
||||
return COMMAND_REGISTRY.has(cmd);
|
||||
};
|
||||
|
||||
export const handleCommand = async (
|
||||
command: string,
|
||||
state: ChatState,
|
||||
cleanup: () => void,
|
||||
): Promise<void> => {
|
||||
const parts = command.slice(1).split(/\s+/);
|
||||
const cmd = parts[0].toLowerCase();
|
||||
const args = parts.slice(1);
|
||||
|
||||
if (!isValidCommand(cmd)) {
|
||||
warningMessage(`Unknown command: /${cmd}`);
|
||||
infoMessage("Type /help for available commands");
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = COMMAND_REGISTRY.get(cmd);
|
||||
if (handler) {
|
||||
await handler({ state, args, cleanup });
|
||||
}
|
||||
};
|
||||
25
src/commands/components/chat/commands/show-help.ts
Normal file
25
src/commands/components/chat/commands/show-help.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import chalk from "chalk";
|
||||
import { HELP_COMMANDS } from "@constants/help-commands.ts";
|
||||
|
||||
export const showHelp = (): void => {
|
||||
console.log("\n" + chalk.bold.underline("Commands") + "\n");
|
||||
|
||||
for (const [cmd, desc] of HELP_COMMANDS) {
|
||||
console.log(` ${chalk.yellow(cmd.padEnd(20))} ${desc}`);
|
||||
}
|
||||
|
||||
console.log("\n" + chalk.bold.underline("File References") + "\n");
|
||||
console.log(` ${chalk.yellow("@<file>")} Add a file to context`);
|
||||
console.log(
|
||||
` ${chalk.yellow('@"file with spaces"')} Add file with spaces in name`,
|
||||
);
|
||||
console.log(
|
||||
` ${chalk.yellow("@src/*.ts")} Add files matching glob pattern`,
|
||||
);
|
||||
|
||||
console.log("\n" + chalk.bold.underline("Examples") + "\n");
|
||||
console.log(" @src/app.ts explain this code");
|
||||
console.log(" @src/utils.ts @src/types.ts refactor these files");
|
||||
console.log(" /model gpt-4o");
|
||||
console.log(" /provider copilot\n");
|
||||
};
|
||||
35
src/commands/components/chat/context/add-context-file.ts
Normal file
35
src/commands/components/chat/context/add-context-file.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { resolve } from "path";
|
||||
import { existsSync } from "fs";
|
||||
import fg from "fast-glob";
|
||||
import { errorMessage, warningMessage } from "@utils/terminal";
|
||||
import { loadFile } from "@commands/components/chat/context/load-file";
|
||||
import { IGNORE_FOLDERS } from "@constants/paths";
|
||||
|
||||
export const addContextFile = async (
|
||||
pattern: string,
|
||||
contextFiles: Map<string, string>,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const paths = await fg(pattern, {
|
||||
cwd: process.cwd(),
|
||||
absolute: true,
|
||||
ignore: IGNORE_FOLDERS,
|
||||
});
|
||||
|
||||
if (paths.length === 0) {
|
||||
const absolutePath = resolve(process.cwd(), pattern);
|
||||
if (existsSync(absolutePath)) {
|
||||
await loadFile(absolutePath, contextFiles);
|
||||
} else {
|
||||
warningMessage(`File not found: ${pattern}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const filePath of paths) {
|
||||
await loadFile(filePath, contextFiles);
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage(`Failed to add file: ${error}`);
|
||||
}
|
||||
};
|
||||
32
src/commands/components/chat/context/load-file.ts
Normal file
32
src/commands/components/chat/context/load-file.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { readFile, stat } from "fs/promises";
|
||||
import { basename } from "path";
|
||||
import { warningMessage, successMessage, errorMessage } from "@utils/terminal";
|
||||
import { addContextFile } from "@services/session";
|
||||
|
||||
export const loadFile = async (
|
||||
filePath: string,
|
||||
contextFiles: Map<string, string>,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const stats = await stat(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
warningMessage(`Skipping directory: ${filePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (stats.size > 100 * 1024) {
|
||||
warningMessage(`File too large (>100KB): ${basename(filePath)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
contextFiles.set(filePath, content);
|
||||
successMessage(
|
||||
`Added: ${basename(filePath)} (${content.split("\n").length} lines)`,
|
||||
);
|
||||
|
||||
await addContextFile(filePath);
|
||||
} catch (error) {
|
||||
errorMessage(`Failed to read file: ${error}`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { FILE_REFERENCE_PATTERN } from "@constants/patterns";
|
||||
import { addContextFile } from "@commands/components/chat/context/add-context-file";
|
||||
|
||||
export const processFileReferences = async (
|
||||
input: string,
|
||||
contextFiles: Map<string, string>,
|
||||
): Promise<string> => {
|
||||
const pattern = new RegExp(FILE_REFERENCE_PATTERN.source, "g");
|
||||
let match;
|
||||
const filesToAdd: string[] = [];
|
||||
|
||||
while ((match = pattern.exec(input)) !== null) {
|
||||
const filePath = match[1] || match[2] || match[3];
|
||||
filesToAdd.push(filePath);
|
||||
}
|
||||
|
||||
for (const filePath of filesToAdd) {
|
||||
await addContextFile(filePath, contextFiles);
|
||||
}
|
||||
|
||||
const textOnly = input.replace(pattern, "").trim();
|
||||
if (!textOnly && filesToAdd.length > 0) {
|
||||
return `Analyze the files I've added to the context.`;
|
||||
}
|
||||
|
||||
return input;
|
||||
};
|
||||
22
src/commands/components/chat/context/remove-file.ts
Normal file
22
src/commands/components/chat/context/remove-file.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { basename } from "path";
|
||||
import { warningMessage, successMessage } from "@utils/terminal";
|
||||
|
||||
export const removeFile = (
|
||||
filename: string,
|
||||
contextFiles: Map<string, string>,
|
||||
): void => {
|
||||
if (!filename) {
|
||||
warningMessage("Please specify a file to remove");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [path] of contextFiles) {
|
||||
if (path.includes(filename) || basename(path) === filename) {
|
||||
contextFiles.delete(path);
|
||||
successMessage(`Removed: ${basename(path)}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
warningMessage(`File not found in context: ${filename}`);
|
||||
};
|
||||
32
src/commands/components/chat/context/show-context-files.ts
Normal file
32
src/commands/components/chat/context/show-context-files.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import chalk from "chalk";
|
||||
import { basename } from "path";
|
||||
import { getCurrentSession } from "@services/session";
|
||||
import { infoMessage, filePath } from "@utils/terminal";
|
||||
|
||||
export const showContextFiles = (contextFiles: Map<string, string>): void => {
|
||||
const session = getCurrentSession();
|
||||
const files = session?.contextFiles || [];
|
||||
|
||||
if (files.length === 0 && contextFiles.size === 0) {
|
||||
infoMessage("No context files loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("\n" + chalk.bold("Context Files:"));
|
||||
|
||||
if (contextFiles.size > 0) {
|
||||
console.log(chalk.gray(" Pending (will be included in next message):"));
|
||||
for (const [path] of contextFiles) {
|
||||
console.log(` - ${filePath(basename(path))}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
console.log(chalk.gray(" In session:"));
|
||||
files.forEach((file, index) => {
|
||||
console.log(` ${index + 1}. ${filePath(file)}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log();
|
||||
};
|
||||
10
src/commands/components/chat/history/clear-conversation.ts
Normal file
10
src/commands/components/chat/history/clear-conversation.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { clearMessages } from "@services/session";
|
||||
import { successMessage } from "@utils/terminal";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
|
||||
export const clearConversation = (state: ChatState): void => {
|
||||
state.messages = [{ role: "system", content: state.systemPrompt }];
|
||||
state.contextFiles.clear();
|
||||
clearMessages();
|
||||
successMessage("Conversation cleared");
|
||||
};
|
||||
15
src/commands/components/chat/history/compact-history.ts
Normal file
15
src/commands/components/chat/history/compact-history.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { successMessage, infoMessage } from "@utils/terminal";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
|
||||
export const compactHistory = (state: ChatState): void => {
|
||||
if (state.messages.length <= 11) {
|
||||
infoMessage("History is already compact");
|
||||
return;
|
||||
}
|
||||
|
||||
const systemPrompt = state.messages[0];
|
||||
const recentMessages = state.messages.slice(-10);
|
||||
state.messages = [systemPrompt, ...recentMessages];
|
||||
|
||||
successMessage(`Compacted to ${state.messages.length - 1} messages`);
|
||||
};
|
||||
18
src/commands/components/chat/history/show-context.ts
Normal file
18
src/commands/components/chat/history/show-context.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import chalk from "chalk";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
|
||||
export const showContext = (state: ChatState): void => {
|
||||
const messageCount = state.messages.length - 1;
|
||||
const totalChars = state.messages.reduce(
|
||||
(acc, m) => acc + m.content.length,
|
||||
0,
|
||||
);
|
||||
const estimatedTokens = Math.round(totalChars / 4);
|
||||
|
||||
console.log("\n" + chalk.bold("Context Information:"));
|
||||
console.log(` Messages: ${messageCount}`);
|
||||
console.log(` Characters: ${totalChars.toLocaleString()}`);
|
||||
console.log(` Estimated tokens: ~${estimatedTokens.toLocaleString()}`);
|
||||
console.log(` Pending files: ${state.contextFiles.size}`);
|
||||
console.log();
|
||||
};
|
||||
18
src/commands/components/chat/history/show-history.ts
Normal file
18
src/commands/components/chat/history/show-history.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import chalk from "chalk";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
|
||||
export const showHistory = (state: ChatState): void => {
|
||||
console.log("\n" + chalk.bold("Conversation History:") + "\n");
|
||||
|
||||
for (let i = 1; i < state.messages.length; i++) {
|
||||
const msg = state.messages[i];
|
||||
const role =
|
||||
msg.role === "user" ? chalk.cyan("You") : chalk.green("Assistant");
|
||||
const preview = msg.content.slice(0, 100).replace(/\n/g, " ");
|
||||
console.log(
|
||||
` ${i}. ${role}: ${preview}${msg.content.length > 100 ? "..." : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log();
|
||||
};
|
||||
224
src/commands/components/chat/index.ts
Normal file
224
src/commands/components/chat/index.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import chalk from "chalk";
|
||||
import { infoMessage, errorMessage, warningMessage } from "@utils/terminal";
|
||||
import {
|
||||
createSession,
|
||||
loadSession,
|
||||
getMostRecentSession,
|
||||
findSession,
|
||||
setWorkingDirectory,
|
||||
} from "@services/session";
|
||||
import { getConfig } from "@services/config";
|
||||
import type { Provider as ProviderName, ChatSession } from "@/types/index";
|
||||
import { getProvider, getProviderStatus } from "@providers/index.ts";
|
||||
import {
|
||||
printWelcome,
|
||||
formatTipLine,
|
||||
Style,
|
||||
Theme,
|
||||
createInputEditor,
|
||||
} from "@ui/index";
|
||||
import {
|
||||
DEFAULT_SYSTEM_PROMPT,
|
||||
buildSystemPromptWithRules,
|
||||
} from "@prompts/index.ts";
|
||||
import type { ChatOptions } from "@interfaces/ChatOptions.ts";
|
||||
|
||||
import { createInitialState, type ChatState } from "./state.ts";
|
||||
import { restoreMessagesFromSession } from "./session/restore-messages.ts";
|
||||
import { addContextFile } from "./context/add-context-file.ts";
|
||||
import { handleCommand } from "./commands/handle-command.ts";
|
||||
import { handleInput } from "./messages/handle-input.ts";
|
||||
import { executePrintMode } from "./print-mode.ts";
|
||||
import { createCleanup } from "./cleanup.ts";
|
||||
|
||||
export const execute = async (options: ChatOptions): Promise<void> => {
|
||||
const config = await getConfig();
|
||||
const state = createInitialState(
|
||||
(options.provider || config.get("provider")) as ProviderName,
|
||||
);
|
||||
|
||||
state.verbose = options.verbose || false;
|
||||
state.autoApprove = options.autoApprove || false;
|
||||
state.currentModel = options.model || config.get("model") || "auto";
|
||||
|
||||
const status = await getProviderStatus(state.currentProvider);
|
||||
if (!status.valid) {
|
||||
errorMessage(`Provider ${state.currentProvider} is not configured.`);
|
||||
infoMessage(`Run: codetyper login ${state.currentProvider}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options.systemPrompt) {
|
||||
state.systemPrompt = options.systemPrompt;
|
||||
} else {
|
||||
const { prompt: promptWithRules, rulesPaths } =
|
||||
await buildSystemPromptWithRules(DEFAULT_SYSTEM_PROMPT, process.cwd());
|
||||
state.systemPrompt = promptWithRules;
|
||||
|
||||
if (rulesPaths.length > 0 && state.verbose) {
|
||||
infoMessage(`Loaded ${rulesPaths.length} rule file(s):`);
|
||||
for (const rulePath of rulesPaths) {
|
||||
infoMessage(` - ${rulePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.appendSystemPrompt) {
|
||||
state.systemPrompt =
|
||||
state.systemPrompt + "\n\n" + options.appendSystemPrompt;
|
||||
}
|
||||
}
|
||||
|
||||
let session: ChatSession;
|
||||
|
||||
if (options.continueSession) {
|
||||
const recent = await getMostRecentSession(process.cwd());
|
||||
if (recent) {
|
||||
session = recent;
|
||||
await loadSession(session.id);
|
||||
state.messages = restoreMessagesFromSession(session, state.systemPrompt);
|
||||
if (state.verbose) {
|
||||
infoMessage(`Continuing session: ${session.id}`);
|
||||
}
|
||||
} else {
|
||||
warningMessage(
|
||||
"No previous session found in this directory. Starting new session.",
|
||||
);
|
||||
session = await createSession("coder");
|
||||
}
|
||||
} else if (options.resumeSession) {
|
||||
const found = await findSession(options.resumeSession);
|
||||
if (found) {
|
||||
session = found;
|
||||
await loadSession(session.id);
|
||||
state.messages = restoreMessagesFromSession(session, state.systemPrompt);
|
||||
if (state.verbose) {
|
||||
infoMessage(`Resumed session: ${session.id}`);
|
||||
}
|
||||
} else {
|
||||
errorMessage(`Session not found: ${options.resumeSession}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
session = await createSession("coder");
|
||||
await setWorkingDirectory(process.cwd());
|
||||
}
|
||||
|
||||
if (state.messages.length === 0) {
|
||||
state.messages = [{ role: "system", content: state.systemPrompt }];
|
||||
}
|
||||
|
||||
if (options.files && options.files.length > 0) {
|
||||
for (const file of options.files) {
|
||||
await addContextFile(file, state.contextFiles);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.printMode && options.initialPrompt) {
|
||||
await executePrintMode(options.initialPrompt, state);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasInitialPrompt =
|
||||
options.initialPrompt && options.initialPrompt.trim().length > 0;
|
||||
|
||||
const provider = getProvider(state.currentProvider);
|
||||
const model = state.currentModel || "auto";
|
||||
|
||||
printWelcome("0.1.0", provider.displayName, model);
|
||||
|
||||
console.log(
|
||||
Theme.textMuted +
|
||||
" Session: " +
|
||||
Style.RESET +
|
||||
chalk.gray(session.id.slice(0, 16) + "..."),
|
||||
);
|
||||
console.log("");
|
||||
console.log(
|
||||
Theme.textMuted +
|
||||
" Commands: " +
|
||||
Style.RESET +
|
||||
chalk.cyan("@file") +
|
||||
" " +
|
||||
chalk.cyan("/help") +
|
||||
" " +
|
||||
chalk.cyan("/clear") +
|
||||
" " +
|
||||
chalk.cyan("/exit"),
|
||||
);
|
||||
console.log(
|
||||
Theme.textMuted +
|
||||
" Input: " +
|
||||
Style.RESET +
|
||||
chalk.cyan("Enter") +
|
||||
" to send, " +
|
||||
chalk.cyan("Alt+Enter") +
|
||||
" for newline",
|
||||
);
|
||||
console.log("");
|
||||
console.log(" " + formatTipLine());
|
||||
console.log("");
|
||||
|
||||
state.inputEditor = createInputEditor({
|
||||
prompt: "\x1b[36m> \x1b[0m",
|
||||
continuationPrompt: "\x1b[90m│ \x1b[0m",
|
||||
});
|
||||
|
||||
state.isRunning = true;
|
||||
|
||||
const cleanup = createCleanup(state);
|
||||
|
||||
const commandHandler = async (command: string, st: ChatState) => {
|
||||
await handleCommand(command, st, cleanup);
|
||||
};
|
||||
|
||||
state.inputEditor.on("submit", async (input: string) => {
|
||||
if (state.isProcessing) return;
|
||||
|
||||
state.isProcessing = true;
|
||||
state.inputEditor?.lock();
|
||||
|
||||
try {
|
||||
await handleInput(input, state, commandHandler);
|
||||
} catch (error) {
|
||||
errorMessage(`Error: ${error}`);
|
||||
}
|
||||
|
||||
state.isProcessing = false;
|
||||
if (state.isRunning && state.inputEditor) {
|
||||
state.inputEditor.unlock();
|
||||
}
|
||||
});
|
||||
|
||||
state.inputEditor.on("interrupt", () => {
|
||||
if (state.isProcessing) {
|
||||
console.log("\n" + chalk.yellow("Interrupted"));
|
||||
state.isProcessing = false;
|
||||
state.inputEditor?.unlock();
|
||||
} else {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
state.inputEditor.on("close", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
state.inputEditor.start();
|
||||
|
||||
if (hasInitialPrompt) {
|
||||
state.isProcessing = true;
|
||||
state.inputEditor.lock();
|
||||
console.log(chalk.cyan("> ") + options.initialPrompt);
|
||||
try {
|
||||
await handleInput(options.initialPrompt!, state, commandHandler);
|
||||
} catch (error) {
|
||||
errorMessage(`Error: ${error}`);
|
||||
}
|
||||
state.isProcessing = false;
|
||||
if (state.isRunning && state.inputEditor) {
|
||||
state.inputEditor.unlock();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { createInitialState, type ChatState } from "./state.ts";
|
||||
143
src/commands/components/chat/mcp/handle-mcp.ts
Normal file
143
src/commands/components/chat/mcp/handle-mcp.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Handle MCP commands in chat
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
initializeMCP,
|
||||
connectServer,
|
||||
disconnectServer,
|
||||
connectAllServers,
|
||||
disconnectAllServers,
|
||||
getAllTools,
|
||||
} from "@services/mcp/index";
|
||||
import { showMCPStatus } from "@commands/components/chat/mcp/show-mcp-status";
|
||||
import { appStore } from "@tui-solid/context/app";
|
||||
|
||||
/**
|
||||
* Handle MCP subcommands
|
||||
*/
|
||||
export const handleMCP = async (args: string[]): Promise<void> => {
|
||||
const subcommand = args[0] || "status";
|
||||
|
||||
const handlers: Record<string, (args: string[]) => Promise<void>> = {
|
||||
status: handleStatus,
|
||||
connect: handleConnect,
|
||||
disconnect: handleDisconnect,
|
||||
tools: handleTools,
|
||||
add: handleAdd,
|
||||
};
|
||||
|
||||
const handler = handlers[subcommand];
|
||||
if (!handler) {
|
||||
console.log(chalk.yellow(`Unknown MCP command: ${subcommand}`));
|
||||
console.log(
|
||||
chalk.gray("Available: status, connect, disconnect, tools, add"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await handler(args.slice(1));
|
||||
};
|
||||
|
||||
/**
|
||||
* Show MCP status
|
||||
*/
|
||||
const handleStatus = async (_args: string[]): Promise<void> => {
|
||||
await showMCPStatus();
|
||||
};
|
||||
|
||||
/**
|
||||
* Connect to MCP servers
|
||||
*/
|
||||
const handleConnect = async (args: string[]): Promise<void> => {
|
||||
await initializeMCP();
|
||||
|
||||
const name = args[0];
|
||||
|
||||
if (name) {
|
||||
try {
|
||||
console.log(chalk.gray(`Connecting to ${name}...`));
|
||||
const instance = await connectServer(name);
|
||||
console.log(chalk.green(`✓ Connected to ${name}`));
|
||||
console.log(chalk.gray(` Tools: ${instance.tools.length}`));
|
||||
} catch (err) {
|
||||
console.log(chalk.red(`✗ Failed to connect: ${err}`));
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.gray("Connecting to all servers..."));
|
||||
const results = await connectAllServers();
|
||||
|
||||
for (const [serverName, instance] of results) {
|
||||
if (instance.state === "connected") {
|
||||
console.log(
|
||||
chalk.green(`✓ ${serverName}: ${instance.tools.length} tools`),
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
chalk.red(`✗ ${serverName}: ${instance.error || "Failed"}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
};
|
||||
|
||||
/**
|
||||
* Disconnect from MCP servers
|
||||
*/
|
||||
const handleDisconnect = async (args: string[]): Promise<void> => {
|
||||
const name = args[0];
|
||||
|
||||
if (name) {
|
||||
await disconnectServer(name);
|
||||
console.log(chalk.green(`✓ Disconnected from ${name}`));
|
||||
} else {
|
||||
await disconnectAllServers();
|
||||
console.log(chalk.green("✓ Disconnected from all servers"));
|
||||
}
|
||||
console.log();
|
||||
};
|
||||
|
||||
/**
|
||||
* List available MCP tools
|
||||
*/
|
||||
const handleTools = async (_args: string[]): Promise<void> => {
|
||||
await connectAllServers();
|
||||
const tools = getAllTools();
|
||||
|
||||
if (tools.length === 0) {
|
||||
console.log(chalk.yellow("\nNo tools available."));
|
||||
console.log(chalk.gray("Connect to MCP servers first with /mcp connect"));
|
||||
console.log();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold("\nMCP Tools\n"));
|
||||
|
||||
// Group by server
|
||||
const byServer = new Map<string, typeof tools>();
|
||||
for (const item of tools) {
|
||||
const existing = byServer.get(item.server) || [];
|
||||
existing.push(item);
|
||||
byServer.set(item.server, existing);
|
||||
}
|
||||
|
||||
for (const [server, serverTools] of byServer) {
|
||||
console.log(chalk.cyan(`${server}:`));
|
||||
for (const { tool } of serverTools) {
|
||||
console.log(` ${chalk.white(tool.name)}`);
|
||||
if (tool.description) {
|
||||
console.log(` ${chalk.gray(tool.description)}`);
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the MCP add form
|
||||
*/
|
||||
const handleAdd = async (_args: string[]): Promise<void> => {
|
||||
appStore.setMode("mcp_add");
|
||||
};
|
||||
6
src/commands/components/chat/mcp/index.ts
Normal file
6
src/commands/components/chat/mcp/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* MCP chat commands
|
||||
*/
|
||||
|
||||
export { showMCPStatus } from "@commands/components/chat/mcp/show-mcp-status";
|
||||
export { handleMCP } from "@commands/components/chat/mcp/handle-mcp";
|
||||
76
src/commands/components/chat/mcp/show-mcp-status.ts
Normal file
76
src/commands/components/chat/mcp/show-mcp-status.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Show MCP server status in chat
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
initializeMCP,
|
||||
getServerInstances,
|
||||
getAllTools,
|
||||
isMCPAvailable,
|
||||
} from "@services/mcp/index";
|
||||
|
||||
/**
|
||||
* Display MCP server status
|
||||
*/
|
||||
export const showMCPStatus = async (): Promise<void> => {
|
||||
await initializeMCP();
|
||||
|
||||
const hasServers = await isMCPAvailable();
|
||||
if (!hasServers) {
|
||||
console.log(chalk.yellow("\nNo MCP servers configured."));
|
||||
console.log(chalk.gray("Add a server with: codetyper mcp add <name>"));
|
||||
console.log();
|
||||
return;
|
||||
}
|
||||
|
||||
const instances = getServerInstances();
|
||||
const tools = getAllTools();
|
||||
|
||||
console.log(chalk.bold("\nMCP Status\n"));
|
||||
|
||||
// Server status
|
||||
console.log(chalk.cyan("Servers:"));
|
||||
for (const [name, instance] of instances) {
|
||||
const stateColors: Record<string, (s: string) => string> = {
|
||||
connected: chalk.green,
|
||||
connecting: chalk.yellow,
|
||||
disconnected: chalk.gray,
|
||||
error: chalk.red,
|
||||
};
|
||||
|
||||
const colorFn = stateColors[instance.state] || chalk.white;
|
||||
const status = colorFn(instance.state);
|
||||
const toolCount =
|
||||
instance.state === "connected" ? ` (${instance.tools.length} tools)` : "";
|
||||
|
||||
console.log(` ${chalk.white(name)}: ${status}${chalk.gray(toolCount)}`);
|
||||
|
||||
if (instance.error) {
|
||||
console.log(` ${chalk.red(instance.error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Tool summary
|
||||
if (tools.length > 0) {
|
||||
console.log();
|
||||
console.log(chalk.cyan(`Available Tools: ${chalk.white(tools.length)}`));
|
||||
|
||||
// Group by server
|
||||
const byServer = new Map<string, string[]>();
|
||||
for (const { server, tool } of tools) {
|
||||
const existing = byServer.get(server) || [];
|
||||
existing.push(tool.name);
|
||||
byServer.set(server, existing);
|
||||
}
|
||||
|
||||
for (const [server, toolNames] of byServer) {
|
||||
console.log(` ${chalk.gray(server)}: ${toolNames.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(chalk.gray("Use /mcp connect to connect servers"));
|
||||
console.log(chalk.gray("Use /mcp tools for detailed tool info"));
|
||||
console.log();
|
||||
};
|
||||
17
src/commands/components/chat/messages/handle-input.ts
Normal file
17
src/commands/components/chat/messages/handle-input.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { processFileReferences } from "@commands/components/chat/context/process-file-references";
|
||||
import { sendMessage } from "@commands/components/chat/messages/send-message";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
|
||||
export const handleInput = async (
|
||||
input: string,
|
||||
state: ChatState,
|
||||
handleCommand: (command: string, state: ChatState) => Promise<void>,
|
||||
): Promise<void> => {
|
||||
if (input.startsWith("/")) {
|
||||
await handleCommand(input, state);
|
||||
return;
|
||||
}
|
||||
|
||||
const processedInput = await processFileReferences(input, state.contextFiles);
|
||||
await sendMessage(processedInput, state);
|
||||
};
|
||||
228
src/commands/components/chat/messages/send-message.ts
Normal file
228
src/commands/components/chat/messages/send-message.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import chalk from "chalk";
|
||||
import { basename, extname } from "path";
|
||||
import { addMessage } from "@services/session";
|
||||
import { initializePermissions } from "@services/permissions";
|
||||
import { createAgent } from "@services/agent";
|
||||
import { infoMessage, errorMessage, warningMessage } from "@utils/terminal";
|
||||
import { getThinkingMessage } from "@constants/status-messages";
|
||||
import {
|
||||
detectDebuggingRequest,
|
||||
buildDebuggingContext,
|
||||
getDebuggingPrompt,
|
||||
} from "@services/debugging-service";
|
||||
import {
|
||||
detectCodeReviewRequest,
|
||||
buildCodeReviewContext,
|
||||
getCodeReviewPrompt,
|
||||
} from "@services/code-review-service";
|
||||
import {
|
||||
detectRefactoringRequest,
|
||||
buildRefactoringContext,
|
||||
getRefactoringPrompt,
|
||||
} from "@services/refactoring-service";
|
||||
import {
|
||||
detectMemoryCommand,
|
||||
processMemoryCommand,
|
||||
buildRelevantMemoryPrompt,
|
||||
} from "@services/memory-service";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
|
||||
export const sendMessage = async (
|
||||
content: string,
|
||||
state: ChatState,
|
||||
): Promise<void> => {
|
||||
let userMessage = content;
|
||||
|
||||
if (state.contextFiles.size > 0) {
|
||||
const contextParts: string[] = [];
|
||||
for (const [path, fileContent] of state.contextFiles) {
|
||||
const ext = extname(path).slice(1) || "txt";
|
||||
contextParts.push(
|
||||
`File: ${basename(path)}\n\`\`\`${ext}\n${fileContent}\n\`\`\``,
|
||||
);
|
||||
}
|
||||
userMessage = contextParts.join("\n\n") + "\n\n" + content;
|
||||
state.contextFiles.clear();
|
||||
}
|
||||
|
||||
// Detect debugging requests and enhance message with context
|
||||
const debugContext = detectDebuggingRequest(userMessage);
|
||||
if (debugContext.isDebugging) {
|
||||
const debugPrompt = getDebuggingPrompt();
|
||||
const contextInfo = buildDebuggingContext(debugContext);
|
||||
|
||||
// Inject debugging system message before user message if not already present
|
||||
const hasDebuggingPrompt = state.messages.some(
|
||||
(msg) => msg.role === "system" && msg.content.includes("debugging mode"),
|
||||
);
|
||||
|
||||
if (!hasDebuggingPrompt) {
|
||||
state.messages.push({ role: "system", content: debugPrompt });
|
||||
}
|
||||
|
||||
// Append debug context to user message if extracted
|
||||
if (contextInfo) {
|
||||
userMessage = userMessage + "\n\n" + contextInfo;
|
||||
}
|
||||
|
||||
if (state.verbose) {
|
||||
infoMessage(`Debugging mode activated: ${debugContext.debugType}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect code review requests and enhance message with context
|
||||
const reviewContext = detectCodeReviewRequest(userMessage);
|
||||
if (reviewContext.isReview) {
|
||||
const reviewPrompt = getCodeReviewPrompt();
|
||||
const contextInfo = buildCodeReviewContext(reviewContext);
|
||||
|
||||
// Inject code review system message before user message if not already present
|
||||
const hasReviewPrompt = state.messages.some(
|
||||
(msg) =>
|
||||
msg.role === "system" && msg.content.includes("code review mode"),
|
||||
);
|
||||
|
||||
if (!hasReviewPrompt) {
|
||||
state.messages.push({ role: "system", content: reviewPrompt });
|
||||
}
|
||||
|
||||
// Append review context to user message if extracted
|
||||
if (contextInfo) {
|
||||
userMessage = userMessage + "\n\n" + contextInfo;
|
||||
}
|
||||
|
||||
if (state.verbose) {
|
||||
infoMessage(`Code review mode activated: ${reviewContext.reviewType}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect refactoring requests and enhance message with context
|
||||
const refactorContext = detectRefactoringRequest(userMessage);
|
||||
if (refactorContext.isRefactoring) {
|
||||
const refactorPrompt = getRefactoringPrompt();
|
||||
const contextInfo = buildRefactoringContext(refactorContext);
|
||||
|
||||
// Inject refactoring system message before user message if not already present
|
||||
const hasRefactoringPrompt = state.messages.some(
|
||||
(msg) =>
|
||||
msg.role === "system" && msg.content.includes("refactoring mode"),
|
||||
);
|
||||
|
||||
if (!hasRefactoringPrompt) {
|
||||
state.messages.push({ role: "system", content: refactorPrompt });
|
||||
}
|
||||
|
||||
// Append refactoring context to user message if extracted
|
||||
if (contextInfo) {
|
||||
userMessage = userMessage + "\n\n" + contextInfo;
|
||||
}
|
||||
|
||||
if (state.verbose) {
|
||||
infoMessage(
|
||||
`Refactoring mode activated: ${refactorContext.refactoringType}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect memory commands
|
||||
const memoryContext = detectMemoryCommand(userMessage);
|
||||
if (memoryContext.isMemoryCommand) {
|
||||
const result = await processMemoryCommand(memoryContext);
|
||||
console.log(chalk.cyan("\n[Memory System]"));
|
||||
console.log(
|
||||
result.success
|
||||
? chalk.green(result.message)
|
||||
: chalk.yellow(result.message),
|
||||
);
|
||||
|
||||
// For store/forget commands, still send to agent for confirmation response
|
||||
if (
|
||||
memoryContext.commandType === "list" ||
|
||||
memoryContext.commandType === "query"
|
||||
) {
|
||||
// Just display results, don't send to agent
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.verbose) {
|
||||
infoMessage(`Memory command: ${memoryContext.commandType}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-retrieve relevant memories for context
|
||||
const relevantMemoryPrompt = await buildRelevantMemoryPrompt(userMessage);
|
||||
if (relevantMemoryPrompt) {
|
||||
userMessage = userMessage + "\n\n" + relevantMemoryPrompt;
|
||||
if (state.verbose) {
|
||||
infoMessage("Relevant memories retrieved");
|
||||
}
|
||||
}
|
||||
|
||||
state.messages.push({ role: "user", content: userMessage });
|
||||
await addMessage("user", content);
|
||||
|
||||
await initializePermissions();
|
||||
|
||||
const agent = createAgent(process.cwd(), {
|
||||
provider: state.currentProvider,
|
||||
model: state.currentModel,
|
||||
verbose: state.verbose,
|
||||
autoApprove: state.autoApprove,
|
||||
onToolCall: (call) => {
|
||||
console.log(chalk.cyan(`\n[Tool: ${call.name}]`));
|
||||
if (state.verbose) {
|
||||
console.log(chalk.gray(JSON.stringify(call.arguments, null, 2)));
|
||||
}
|
||||
},
|
||||
onToolResult: (_callId, result) => {
|
||||
if (result.success) {
|
||||
console.log(chalk.green(`✓ ${result.title}`));
|
||||
} else {
|
||||
console.log(chalk.red(`✗ ${result.title}: ${result.error}`));
|
||||
}
|
||||
},
|
||||
onText: (_text) => {},
|
||||
});
|
||||
|
||||
process.stdout.write(chalk.gray(getThinkingMessage() + "\n"));
|
||||
|
||||
try {
|
||||
const result = await agent.run(state.messages);
|
||||
|
||||
if (result.finalResponse) {
|
||||
state.messages.push({
|
||||
role: "assistant",
|
||||
content: result.finalResponse,
|
||||
});
|
||||
await addMessage("assistant", result.finalResponse);
|
||||
|
||||
console.log(chalk.bold("\nAssistant:"));
|
||||
console.log(result.finalResponse);
|
||||
console.log();
|
||||
|
||||
if (result.toolCalls.length > 0) {
|
||||
const successful = result.toolCalls.filter(
|
||||
(tc) => tc.result.success,
|
||||
).length;
|
||||
infoMessage(
|
||||
chalk.gray(
|
||||
`Tools: ${successful}/${result.toolCalls.length} successful, ${result.iterations} iteration(s)`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (result.toolCalls.length > 0) {
|
||||
const successful = result.toolCalls.filter(
|
||||
(tc) => tc.result.success,
|
||||
).length;
|
||||
infoMessage(
|
||||
chalk.gray(
|
||||
`Completed: ${successful}/${result.toolCalls.length} tools successful`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
warningMessage("No response received");
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage(`Failed: ${error}`);
|
||||
}
|
||||
};
|
||||
31
src/commands/components/chat/models/show-models.ts
Normal file
31
src/commands/components/chat/models/show-models.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import chalk from "chalk";
|
||||
import type { Provider as ProviderName } from "@/types/index";
|
||||
import { getProvider } from "@providers/index.ts";
|
||||
|
||||
export const showModels = async (
|
||||
currentProvider: ProviderName,
|
||||
currentModel: string | undefined,
|
||||
): Promise<void> => {
|
||||
const provider = getProvider(currentProvider);
|
||||
const models = await provider.getModels();
|
||||
const activeModel = currentModel || "auto";
|
||||
const isAutoSelected = activeModel === "auto";
|
||||
|
||||
console.log(`\n${chalk.bold(provider.displayName + " Models")}\n`);
|
||||
|
||||
// Show "auto" option first
|
||||
const autoMarker = isAutoSelected ? chalk.cyan("→") : " ";
|
||||
console.log(
|
||||
`${autoMarker} ${chalk.cyan("auto")} - Let API choose the best model`,
|
||||
);
|
||||
|
||||
for (const model of models) {
|
||||
const isCurrent = model.id === activeModel;
|
||||
const marker = isCurrent ? chalk.cyan("→") : " ";
|
||||
|
||||
console.log(`${marker} ${chalk.cyan(model.id)} - ${model.name}`);
|
||||
}
|
||||
|
||||
console.log("\n" + chalk.gray("Use /model <name> to switch"));
|
||||
console.log();
|
||||
};
|
||||
7
src/commands/components/chat/models/show-providers.ts
Normal file
7
src/commands/components/chat/models/show-providers.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { getConfig } from "@services/config";
|
||||
import { displayProvidersStatus } from "@providers/index.ts";
|
||||
|
||||
export const showProviders = async (): Promise<void> => {
|
||||
const config = await getConfig();
|
||||
await displayProvidersStatus(config.get("provider"));
|
||||
};
|
||||
50
src/commands/components/chat/models/switch-model.ts
Normal file
50
src/commands/components/chat/models/switch-model.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
infoMessage,
|
||||
warningMessage,
|
||||
successMessage,
|
||||
errorMessage,
|
||||
} from "@utils/terminal";
|
||||
import { getConfig } from "@services/config";
|
||||
import { getProvider } from "@providers/index.ts";
|
||||
import { showModels } from "./show-models.ts";
|
||||
import type { ChatState } from "../state.ts";
|
||||
|
||||
export const switchModel = async (
|
||||
modelName: string,
|
||||
state: ChatState,
|
||||
): Promise<void> => {
|
||||
if (!modelName) {
|
||||
warningMessage("Please specify a model name");
|
||||
await showModels(state.currentProvider, state.currentModel);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle "auto" as a special case
|
||||
if (modelName.toLowerCase() === "auto") {
|
||||
state.currentModel = "auto";
|
||||
successMessage("Switched to model: auto (API will choose)");
|
||||
|
||||
const config = await getConfig();
|
||||
config.set("model", "auto");
|
||||
await config.save();
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = getProvider(state.currentProvider);
|
||||
const models = await provider.getModels();
|
||||
const model = models.find((m) => m.id === modelName || m.name === modelName);
|
||||
|
||||
if (!model) {
|
||||
errorMessage(`Model not found: ${modelName}`);
|
||||
infoMessage("Use /models to see available models, or use 'auto'");
|
||||
return;
|
||||
}
|
||||
|
||||
state.currentModel = model.id;
|
||||
successMessage(`Switched to model: ${model.name}`);
|
||||
|
||||
// Persist model selection to config
|
||||
const config = await getConfig();
|
||||
config.set("model", model.id);
|
||||
await config.save();
|
||||
};
|
||||
51
src/commands/components/chat/models/switch-provider.ts
Normal file
51
src/commands/components/chat/models/switch-provider.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { Provider as ProviderName } from "@/types/index";
|
||||
import {
|
||||
errorMessage,
|
||||
warningMessage,
|
||||
infoMessage,
|
||||
successMessage,
|
||||
} from "@utils/terminal";
|
||||
import { getConfig } from "@services/config";
|
||||
import {
|
||||
getProvider,
|
||||
getProviderStatus,
|
||||
getDefaultModel,
|
||||
} from "@providers/index.ts";
|
||||
import type { ChatState } from "../state.ts";
|
||||
|
||||
export const switchProvider = async (
|
||||
providerName: string,
|
||||
state: ChatState,
|
||||
): Promise<void> => {
|
||||
if (!providerName) {
|
||||
warningMessage("Please specify a provider: copilot, or ollama");
|
||||
return;
|
||||
}
|
||||
|
||||
const validProviders = ["copilot", "ollama"];
|
||||
if (!validProviders.includes(providerName)) {
|
||||
errorMessage(`Invalid provider: ${providerName}`);
|
||||
infoMessage("Valid providers: " + validProviders.join(", "));
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await getProviderStatus(providerName as ProviderName);
|
||||
if (!status.valid) {
|
||||
errorMessage(`Provider ${providerName} is not configured`);
|
||||
infoMessage(`Run: codetyper login ${providerName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
state.currentProvider = providerName as ProviderName;
|
||||
state.currentModel = undefined;
|
||||
|
||||
const config = await getConfig();
|
||||
config.set("provider", providerName as ProviderName);
|
||||
await config.save();
|
||||
|
||||
const provider = getProvider(state.currentProvider);
|
||||
const model = getDefaultModel(state.currentProvider);
|
||||
|
||||
successMessage(`Switched to ${provider.displayName}`);
|
||||
infoMessage(`Using model: ${model}`);
|
||||
};
|
||||
71
src/commands/components/chat/print-mode.ts
Normal file
71
src/commands/components/chat/print-mode.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import chalk from "chalk";
|
||||
import { basename, extname } from "path";
|
||||
import { initializePermissions } from "@services/permissions";
|
||||
import { createAgent } from "@services/agent";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
import { processFileReferences } from "@commands/components/chat/context/process-file-references";
|
||||
|
||||
export const executePrintMode = async (
|
||||
prompt: string,
|
||||
state: ChatState,
|
||||
): Promise<void> => {
|
||||
const processedPrompt = await processFileReferences(
|
||||
prompt,
|
||||
state.contextFiles,
|
||||
);
|
||||
|
||||
let userMessage = processedPrompt;
|
||||
if (state.contextFiles.size > 0) {
|
||||
const contextParts: string[] = [];
|
||||
for (const [path, fileContent] of state.contextFiles) {
|
||||
const ext = extname(path).slice(1) || "txt";
|
||||
contextParts.push(
|
||||
`File: ${basename(path)}\n\`\`\`${ext}\n${fileContent}\n\`\`\``,
|
||||
);
|
||||
}
|
||||
userMessage = contextParts.join("\n\n") + "\n\n" + processedPrompt;
|
||||
}
|
||||
|
||||
state.messages.push({ role: "user", content: userMessage });
|
||||
|
||||
await initializePermissions();
|
||||
|
||||
const agent = createAgent(process.cwd(), {
|
||||
provider: state.currentProvider,
|
||||
model: state.currentModel,
|
||||
verbose: state.verbose,
|
||||
autoApprove: state.autoApprove,
|
||||
onToolCall: (call) => {
|
||||
console.error(chalk.cyan(`[Tool: ${call.name}]`));
|
||||
},
|
||||
onToolResult: (_callId, result) => {
|
||||
if (result.success) {
|
||||
console.error(chalk.green(`✓ ${result.title}`));
|
||||
} else {
|
||||
console.error(chalk.red(`✗ ${result.title}: ${result.error}`));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await agent.run(state.messages);
|
||||
|
||||
if (result.finalResponse) {
|
||||
console.log(result.finalResponse);
|
||||
}
|
||||
|
||||
if (state.verbose && result.toolCalls.length > 0) {
|
||||
const successful = result.toolCalls.filter(
|
||||
(tc) => tc.result.success,
|
||||
).length;
|
||||
console.error(
|
||||
chalk.gray(
|
||||
`[Tools: ${successful}/${result.toolCalls.length} successful, ${result.iterations} iteration(s)]`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${error}`));
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
38
src/commands/components/chat/session/list-sessions.ts
Normal file
38
src/commands/components/chat/session/list-sessions.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import chalk from "chalk";
|
||||
import { getSessionSummaries } from "@services/session";
|
||||
import { infoMessage } from "@utils/terminal";
|
||||
|
||||
export const listSessions = async (): Promise<void> => {
|
||||
const summaries = await getSessionSummaries();
|
||||
|
||||
if (summaries.length === 0) {
|
||||
infoMessage("No saved sessions");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("\n" + chalk.bold("Saved Sessions:") + "\n");
|
||||
|
||||
for (const session of summaries.slice(0, 10)) {
|
||||
const date = new Date(session.updatedAt).toLocaleDateString();
|
||||
const time = new Date(session.updatedAt).toLocaleTimeString();
|
||||
const preview = session.lastMessage
|
||||
? session.lastMessage.slice(0, 50).replace(/\n/g, " ")
|
||||
: "(no messages)";
|
||||
|
||||
console.log(` ${chalk.cyan(session.id.slice(0, 20))}...`);
|
||||
console.log(
|
||||
` ${chalk.gray(`${date} ${time}`)} - ${session.messageCount} messages`,
|
||||
);
|
||||
console.log(
|
||||
` ${chalk.gray(preview)}${preview.length >= 50 ? "..." : ""}`,
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (summaries.length > 10) {
|
||||
infoMessage(`... and ${summaries.length - 10} more sessions`);
|
||||
}
|
||||
|
||||
console.log(chalk.gray("Resume with: codetyper -r <session-id>"));
|
||||
console.log();
|
||||
};
|
||||
20
src/commands/components/chat/session/restore-messages.ts
Normal file
20
src/commands/components/chat/session/restore-messages.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { ChatSession } from "@/types/index";
|
||||
import type { Message } from "@providers/index.ts";
|
||||
|
||||
export const restoreMessagesFromSession = (
|
||||
session: ChatSession,
|
||||
systemPrompt: string,
|
||||
): Message[] => {
|
||||
const messages: Message[] = [{ role: "system", content: systemPrompt }];
|
||||
|
||||
for (const msg of session.messages) {
|
||||
if (msg.role !== "system") {
|
||||
messages.push({
|
||||
role: msg.role as "user" | "assistant",
|
||||
content: msg.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
};
|
||||
20
src/commands/components/chat/session/show-session-info.ts
Normal file
20
src/commands/components/chat/session/show-session-info.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import chalk from "chalk";
|
||||
import { getCurrentSession } from "@services/session";
|
||||
import { warningMessage } from "@utils/terminal";
|
||||
|
||||
export const showSessionInfo = async (): Promise<void> => {
|
||||
const session = getCurrentSession();
|
||||
if (!session) {
|
||||
warningMessage("No active session");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("\n" + chalk.bold("Session Information:"));
|
||||
console.log(` ID: ${chalk.cyan(session.id)}`);
|
||||
console.log(` Agent: ${session.agent}`);
|
||||
console.log(` Messages: ${session.messages.length}`);
|
||||
console.log(` Context files: ${session.contextFiles.length}`);
|
||||
console.log(` Created: ${new Date(session.createdAt).toLocaleString()}`);
|
||||
console.log(` Updated: ${new Date(session.updatedAt).toLocaleString()}`);
|
||||
console.log();
|
||||
};
|
||||
34
src/commands/components/chat/state.ts
Normal file
34
src/commands/components/chat/state.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Provider as ProviderName } from "@/types/index";
|
||||
import type { Message } from "@providers/index";
|
||||
import type { InputEditorInstance } from "@ui/index";
|
||||
import { DEFAULT_SYSTEM_PROMPT } from "@prompts/index";
|
||||
|
||||
export interface ChatState {
|
||||
inputEditor: InputEditorInstance | null;
|
||||
isRunning: boolean;
|
||||
isProcessing: boolean;
|
||||
currentProvider: ProviderName;
|
||||
currentModel: string | undefined;
|
||||
currentAgent: string | undefined;
|
||||
messages: Message[];
|
||||
contextFiles: Map<string, string>;
|
||||
systemPrompt: string;
|
||||
verbose: boolean;
|
||||
autoApprove: boolean;
|
||||
}
|
||||
|
||||
export const createInitialState = (
|
||||
provider: ProviderName = "copilot",
|
||||
): ChatState => ({
|
||||
inputEditor: null,
|
||||
isRunning: false,
|
||||
isProcessing: false,
|
||||
currentProvider: provider,
|
||||
currentModel: undefined,
|
||||
currentAgent: "coder",
|
||||
messages: [],
|
||||
contextFiles: new Map(),
|
||||
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
||||
verbose: false,
|
||||
autoApprove: false,
|
||||
});
|
||||
129
src/commands/components/chat/usage/show-usage.ts
Normal file
129
src/commands/components/chat/usage/show-usage.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Show usage statistics command
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { usageStore } from "@stores/usage-store";
|
||||
import { getUserInfo } from "@providers/copilot/credentials";
|
||||
import { getCopilotUsage } from "@providers/copilot/usage";
|
||||
import { getProvider } from "@providers/index";
|
||||
import { renderUsageBar, renderUnlimitedBar } from "@utils/progress-bar";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
import type { CopilotQuotaDetail } from "@/types/copilot-usage";
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
return num.toLocaleString();
|
||||
};
|
||||
|
||||
const formatDuration = (ms: number): string => {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m ${seconds % 60}s`;
|
||||
}
|
||||
return `${seconds}s`;
|
||||
};
|
||||
|
||||
const printQuotaBar = (
|
||||
name: string,
|
||||
quota: CopilotQuotaDetail | undefined,
|
||||
resetInfo?: string,
|
||||
): void => {
|
||||
if (!quota) {
|
||||
console.log(chalk.bold(name));
|
||||
console.log(chalk.gray("N/A"));
|
||||
console.log();
|
||||
return;
|
||||
}
|
||||
|
||||
if (quota.unlimited) {
|
||||
renderUnlimitedBar(name).forEach((line) => console.log(line));
|
||||
console.log();
|
||||
return;
|
||||
}
|
||||
|
||||
const used = quota.entitlement - quota.remaining;
|
||||
renderUsageBar(name, used, quota.entitlement, resetInfo).forEach((line) =>
|
||||
console.log(line),
|
||||
);
|
||||
console.log();
|
||||
};
|
||||
|
||||
export const showUsage = async (state: ChatState): Promise<void> => {
|
||||
const stats = usageStore.getStats();
|
||||
const provider = getProvider(state.currentProvider);
|
||||
const sessionDuration = Date.now() - stats.sessionStartTime;
|
||||
|
||||
console.log();
|
||||
|
||||
// User info and quota for Copilot
|
||||
if (state.currentProvider === "copilot") {
|
||||
const userInfo = await getUserInfo();
|
||||
const copilotUsage = await getCopilotUsage();
|
||||
|
||||
if (copilotUsage) {
|
||||
const resetDate = copilotUsage.quota_reset_date;
|
||||
|
||||
printQuotaBar(
|
||||
"Premium Requests",
|
||||
copilotUsage.quota_snapshots.premium_interactions,
|
||||
`Resets ${resetDate}`,
|
||||
);
|
||||
|
||||
printQuotaBar(
|
||||
"Chat",
|
||||
copilotUsage.quota_snapshots.chat,
|
||||
`Resets ${resetDate}`,
|
||||
);
|
||||
|
||||
printQuotaBar(
|
||||
"Completions",
|
||||
copilotUsage.quota_snapshots.completions,
|
||||
`Resets ${resetDate}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(chalk.bold("Account"));
|
||||
console.log(`${chalk.gray("Provider:")} ${provider.displayName}`);
|
||||
console.log(`${chalk.gray("Model:")} ${state.currentModel ?? "auto"}`);
|
||||
if (userInfo) {
|
||||
console.log(`${chalk.gray("User:")} ${userInfo.login}`);
|
||||
}
|
||||
if (copilotUsage) {
|
||||
console.log(`${chalk.gray("Plan:")} ${copilotUsage.copilot_plan}`);
|
||||
}
|
||||
console.log();
|
||||
} else {
|
||||
console.log(chalk.bold("Provider"));
|
||||
console.log(`${chalk.gray("Name:")} ${provider.displayName}`);
|
||||
console.log(`${chalk.gray("Model:")} ${state.currentModel ?? "auto"}`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Session stats with bar
|
||||
console.log(chalk.bold("Current Session"));
|
||||
renderUsageBar(
|
||||
"Tokens",
|
||||
stats.totalTokens,
|
||||
stats.totalTokens || 1,
|
||||
`${formatNumber(stats.promptTokens)} prompt + ${formatNumber(stats.completionTokens)} completion`,
|
||||
)
|
||||
.slice(1)
|
||||
.forEach((line) => console.log(line));
|
||||
console.log(`${chalk.gray("Requests:")} ${formatNumber(stats.requestCount)}`);
|
||||
console.log(`${chalk.gray("Duration:")} ${formatDuration(sessionDuration)}`);
|
||||
if (stats.requestCount > 0) {
|
||||
const avgTokensPerRequest = Math.round(
|
||||
stats.totalTokens / stats.requestCount,
|
||||
);
|
||||
console.log(
|
||||
`${chalk.gray("Avg tokens/request:")} ${formatNumber(avgTokensPerRequest)}`,
|
||||
);
|
||||
}
|
||||
console.log();
|
||||
};
|
||||
25
src/commands/components/dashboard/build-config.ts
Normal file
25
src/commands/components/dashboard/build-config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Dashboard Config Builder
|
||||
*/
|
||||
|
||||
import os from "os";
|
||||
import { getConfig } from "@services/config";
|
||||
import { DASHBOARD_TITLE } from "@constants/dashboard";
|
||||
import type { DashboardConfig } from "@/types/dashboard";
|
||||
|
||||
export const buildDashboardConfig = async (
|
||||
version: string,
|
||||
): Promise<DashboardConfig> => {
|
||||
const configMgr = await getConfig();
|
||||
const username = os.userInfo().username;
|
||||
const cwd = process.cwd();
|
||||
const provider = configMgr.get("provider") as string;
|
||||
|
||||
return {
|
||||
title: DASHBOARD_TITLE,
|
||||
version,
|
||||
user: username,
|
||||
cwd,
|
||||
provider,
|
||||
};
|
||||
};
|
||||
33
src/commands/components/dashboard/display.ts
Normal file
33
src/commands/components/dashboard/display.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Dashboard Display
|
||||
*
|
||||
* Renders and displays the main dashboard UI.
|
||||
*/
|
||||
|
||||
import { DASHBOARD_LAYOUT } from "@constants/dashboard";
|
||||
import { buildDashboardConfig } from "@commands/components/dashboard/build-config";
|
||||
import { renderHeader } from "@commands/components/dashboard/render-header";
|
||||
import { renderContent } from "@commands/components/dashboard/render-content";
|
||||
import { renderFooter } from "@commands/components/dashboard/render-footer";
|
||||
|
||||
const getTerminalWidth = (): number => {
|
||||
return process.stdout.columns || DASHBOARD_LAYOUT.DEFAULT_WIDTH;
|
||||
};
|
||||
|
||||
const renderDashboard = async (version: string): Promise<string> => {
|
||||
const config = await buildDashboardConfig(version);
|
||||
const width = getTerminalWidth();
|
||||
|
||||
const header = renderHeader(config, width);
|
||||
const content = renderContent(config, width, DASHBOARD_LAYOUT.CONTENT_HEIGHT);
|
||||
const footer = renderFooter(width);
|
||||
|
||||
return [header, content, footer].join("\n");
|
||||
};
|
||||
|
||||
export const displayDashboard = async (version: string): Promise<void> => {
|
||||
console.clear();
|
||||
const dashboard = await renderDashboard(version);
|
||||
console.log(dashboard);
|
||||
process.exit(0);
|
||||
};
|
||||
41
src/commands/components/dashboard/render-content.ts
Normal file
41
src/commands/components/dashboard/render-content.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Dashboard Content Renderer
|
||||
*/
|
||||
|
||||
import { DASHBOARD_LAYOUT, DASHBOARD_BORDER } from "@constants/dashboard";
|
||||
import { renderLeftContent } from "@commands/components/dashboard/render-left-content";
|
||||
import { renderRightContent } from "@commands/components/dashboard/render-right-content";
|
||||
import type { DashboardConfig } from "@/types/dashboard";
|
||||
|
||||
const padContent = (content: string[], height: number): string[] => {
|
||||
const padded = [...content];
|
||||
while (padded.length < height) {
|
||||
padded.push("");
|
||||
}
|
||||
return padded;
|
||||
};
|
||||
|
||||
export const renderContent = (
|
||||
config: DashboardConfig,
|
||||
width: number,
|
||||
height: number,
|
||||
): string => {
|
||||
const dividerPos = Math.floor(width * DASHBOARD_LAYOUT.LEFT_COLUMN_RATIO);
|
||||
const leftWidth = dividerPos - DASHBOARD_LAYOUT.PADDING;
|
||||
const rightWidth = width - dividerPos - DASHBOARD_LAYOUT.PADDING;
|
||||
|
||||
const leftContent = padContent(renderLeftContent(config), height);
|
||||
const rightContent = padContent(renderRightContent(), height);
|
||||
|
||||
const lines: string[] = [];
|
||||
|
||||
for (let i = 0; i < height; i++) {
|
||||
const left = (leftContent[i] || "").padEnd(leftWidth);
|
||||
const right = (rightContent[i] || "").padEnd(rightWidth);
|
||||
lines.push(
|
||||
`${DASHBOARD_BORDER.VERTICAL} ${left} ${DASHBOARD_BORDER.VERTICAL} ${right} ${DASHBOARD_BORDER.VERTICAL}`,
|
||||
);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
};
|
||||
31
src/commands/components/dashboard/render-footer.ts
Normal file
31
src/commands/components/dashboard/render-footer.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Dashboard Footer Renderer
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
DASHBOARD_BORDER,
|
||||
DASHBOARD_QUICK_COMMANDS,
|
||||
} from "@constants/dashboard";
|
||||
|
||||
export const renderFooter = (width: number): string => {
|
||||
const dashCount = Math.max(0, width - 2);
|
||||
const dashes = DASHBOARD_BORDER.HORIZONTAL.repeat(dashCount);
|
||||
const borderLine = `${DASHBOARD_BORDER.BOTTOM_LEFT}${dashes}${DASHBOARD_BORDER.BOTTOM_RIGHT}`;
|
||||
|
||||
const commandLines = DASHBOARD_QUICK_COMMANDS.map(
|
||||
({ command, description }) =>
|
||||
` ${chalk.cyan(command.padEnd(18))} ${description}`,
|
||||
);
|
||||
|
||||
const lines = [
|
||||
borderLine,
|
||||
"",
|
||||
chalk.dim("Quick Commands:"),
|
||||
...commandLines,
|
||||
"",
|
||||
chalk.dim("Press Ctrl+C to exit • Type 'codetyper chat' to begin"),
|
||||
];
|
||||
|
||||
return lines.join("\n");
|
||||
};
|
||||
18
src/commands/components/dashboard/render-header.ts
Normal file
18
src/commands/components/dashboard/render-header.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Dashboard Header Renderer
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { DASHBOARD_BORDER } from "@constants/dashboard";
|
||||
import type { DashboardConfig } from "@/types/dashboard";
|
||||
|
||||
export const renderHeader = (
|
||||
config: DashboardConfig,
|
||||
width: number,
|
||||
): string => {
|
||||
const title = ` ${config.title} ${config.version} `;
|
||||
const dashCount = Math.max(0, width - title.length - 2);
|
||||
const dashes = DASHBOARD_BORDER.HORIZONTAL.repeat(dashCount);
|
||||
|
||||
return `${DASHBOARD_BORDER.TOP_LEFT}${DASHBOARD_BORDER.HORIZONTAL}${DASHBOARD_BORDER.HORIZONTAL}${DASHBOARD_BORDER.HORIZONTAL} ${chalk.cyan.bold(title)}${dashes}${DASHBOARD_BORDER.TOP_RIGHT}`;
|
||||
};
|
||||
24
src/commands/components/dashboard/render-left-content.ts
Normal file
24
src/commands/components/dashboard/render-left-content.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Dashboard Left Content Renderer
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { DASHBOARD_LOGO } from "@constants/dashboard";
|
||||
import type { DashboardConfig } from "@/types/dashboard";
|
||||
|
||||
export const renderLeftContent = (config: DashboardConfig): string[] => {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push("");
|
||||
lines.push(chalk.green(`Welcome back ${config.user}!`));
|
||||
lines.push("");
|
||||
|
||||
const coloredLogo = DASHBOARD_LOGO.map((line) => chalk.cyan.bold(line));
|
||||
lines.push(...coloredLogo);
|
||||
|
||||
lines.push("");
|
||||
lines.push(chalk.cyan.bold(`${config.provider.toUpperCase()}`));
|
||||
lines.push(chalk.dim(`${config.user}@codetyper`));
|
||||
|
||||
return lines;
|
||||
};
|
||||
21
src/commands/components/dashboard/render-right-content.ts
Normal file
21
src/commands/components/dashboard/render-right-content.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Dashboard Right Content Renderer
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { DASHBOARD_COMMANDS } from "@constants/dashboard";
|
||||
|
||||
export const renderRightContent = (): string[] => {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(chalk.bold("Ready to code"));
|
||||
lines.push("");
|
||||
|
||||
for (const { command, description } of DASHBOARD_COMMANDS) {
|
||||
lines.push(chalk.cyan(command));
|
||||
lines.push(` ${description}`);
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
return lines;
|
||||
};
|
||||
55
src/commands/components/execute/execute-solid.tsx
Normal file
55
src/commands/components/execute/execute-solid.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { tui } from "@tui-solid/index";
|
||||
import { getProviderInfo } from "@services/chat-tui-service";
|
||||
import type { ChatServiceState } from "@services/chat-tui-service";
|
||||
import type { AgentConfig } from "@/types/agent-config";
|
||||
import type { PermissionScope, LearningScope } from "@/types/tui";
|
||||
|
||||
export interface RenderAppSolidProps {
|
||||
sessionId: string;
|
||||
handleSubmit: (message: string) => Promise<void>;
|
||||
handleCommand: (command: string) => Promise<void>;
|
||||
handleModelSelect: (model: string) => Promise<void>;
|
||||
handleAgentSelect: (agentId: string, agent: AgentConfig) => Promise<void>;
|
||||
handleThemeSelect: (theme: string) => void;
|
||||
handlePermissionResponse?: (
|
||||
allowed: boolean,
|
||||
scope?: PermissionScope,
|
||||
) => void;
|
||||
handleLearningResponse?: (
|
||||
save: boolean,
|
||||
scope?: LearningScope,
|
||||
editedContent?: string,
|
||||
) => void;
|
||||
handleExit: () => void;
|
||||
showBanner: boolean;
|
||||
state: ChatServiceState;
|
||||
plan?: {
|
||||
id: string;
|
||||
title: string;
|
||||
items: Array<{ id: string; text: string; completed: boolean }>;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export const renderAppSolid = async (
|
||||
props: RenderAppSolidProps,
|
||||
): Promise<void> => {
|
||||
const { displayName, model: defaultModel } = getProviderInfo(
|
||||
props.state.provider,
|
||||
);
|
||||
const currentModel = props.state.model ?? defaultModel;
|
||||
|
||||
await tui({
|
||||
sessionId: props.sessionId,
|
||||
provider: displayName,
|
||||
model: currentModel,
|
||||
onSubmit: props.handleSubmit,
|
||||
onCommand: props.handleCommand,
|
||||
onModelSelect: props.handleModelSelect,
|
||||
onThemeSelect: props.handleThemeSelect,
|
||||
onPermissionResponse: props.handlePermissionResponse ?? (() => {}),
|
||||
onLearningResponse: props.handleLearningResponse ?? (() => {}),
|
||||
plan: props.plan,
|
||||
});
|
||||
|
||||
props.handleExit();
|
||||
};
|
||||
104
src/commands/components/execute/execute.tsx
Normal file
104
src/commands/components/execute/execute.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { tui, appStore } from "@tui/index";
|
||||
import { getProviderInfo } from "@services/chat-tui-service";
|
||||
import { addServer, connectServer } from "@services/mcp/index";
|
||||
import type { ChatServiceState } from "@services/chat-tui-service";
|
||||
import type { AgentConfig } from "@/types/agent-config";
|
||||
import type { PermissionScope, LearningScope } from "@/types/tui";
|
||||
import type { ProviderModel } from "@/types/providers";
|
||||
import type { MCPAddFormData } from "@/types/mcp";
|
||||
|
||||
interface AgentOption {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface RenderAppProps {
|
||||
sessionId?: string;
|
||||
handleSubmit: (message: string) => Promise<void>;
|
||||
handleCommand: (command: string) => Promise<void>;
|
||||
handleModelSelect: (model: string) => Promise<void>;
|
||||
handleAgentSelect: (agentId: string, agent: AgentConfig) => Promise<void>;
|
||||
handleThemeSelect: (theme: string) => void;
|
||||
handleProviderSelect?: (providerId: string) => Promise<void>;
|
||||
handleCascadeToggle?: (enabled: boolean) => Promise<void>;
|
||||
handleMCPAdd?: (data: MCPAddFormData) => Promise<void>;
|
||||
handlePermissionResponse?: (
|
||||
allowed: boolean,
|
||||
scope?: PermissionScope,
|
||||
) => void;
|
||||
handleLearningResponse?: (
|
||||
save: boolean,
|
||||
scope?: LearningScope,
|
||||
editedContent?: string,
|
||||
) => void;
|
||||
handleExit: () => void;
|
||||
showBanner: boolean;
|
||||
state: ChatServiceState;
|
||||
availableModels?: ProviderModel[];
|
||||
agents?: AgentOption[];
|
||||
initialPrompt?: string;
|
||||
theme?: string;
|
||||
cascadeEnabled?: boolean;
|
||||
plan?: {
|
||||
id: string;
|
||||
title: string;
|
||||
items: Array<{ id: string; text: string; completed: boolean }>;
|
||||
} | null;
|
||||
}
|
||||
|
||||
const defaultHandleMCPAdd = async (data: MCPAddFormData): Promise<void> => {
|
||||
const serverArgs = data.args.trim()
|
||||
? data.args.trim().split(/\s+/)
|
||||
: undefined;
|
||||
|
||||
await addServer(
|
||||
data.name,
|
||||
{
|
||||
command: data.command,
|
||||
args: serverArgs,
|
||||
enabled: true,
|
||||
},
|
||||
data.isGlobal,
|
||||
);
|
||||
|
||||
await connectServer(data.name);
|
||||
};
|
||||
|
||||
export const renderApp = async (props: RenderAppProps): Promise<void> => {
|
||||
const { displayName, model: defaultModel } = getProviderInfo(
|
||||
props.state.provider,
|
||||
);
|
||||
const currentModel = props.state.model ?? defaultModel;
|
||||
|
||||
await tui({
|
||||
sessionId: props.sessionId,
|
||||
provider: displayName,
|
||||
model: currentModel,
|
||||
theme: props.theme,
|
||||
cascadeEnabled: props.cascadeEnabled,
|
||||
availableModels: props.availableModels,
|
||||
agents: props.agents,
|
||||
initialPrompt: props.initialPrompt,
|
||||
onSubmit: props.handleSubmit,
|
||||
onCommand: props.handleCommand,
|
||||
onModelSelect: props.handleModelSelect,
|
||||
onThemeSelect: props.handleThemeSelect,
|
||||
onProviderSelect: props.handleProviderSelect,
|
||||
onCascadeToggle: props.handleCascadeToggle,
|
||||
onAgentSelect: async (agentId: string) => {
|
||||
const agent = props.agents?.find((a) => a.id === agentId);
|
||||
if (agent) {
|
||||
await props.handleAgentSelect(agentId, agent as AgentConfig);
|
||||
}
|
||||
},
|
||||
onMCPAdd: props.handleMCPAdd ?? defaultHandleMCPAdd,
|
||||
onPermissionResponse: props.handlePermissionResponse ?? (() => {}),
|
||||
onLearningResponse: props.handleLearningResponse ?? (() => {}),
|
||||
plan: props.plan,
|
||||
});
|
||||
|
||||
props.handleExit();
|
||||
};
|
||||
|
||||
export { appStore };
|
||||
195
src/commands/components/execute/index.ts
Normal file
195
src/commands/components/execute/index.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { renderApp, appStore } from "@commands/components/execute/execute";
|
||||
import type { RenderAppProps } from "@commands/components/execute/execute";
|
||||
import {
|
||||
initializeChatService,
|
||||
loadModels,
|
||||
handleModelSelect as serviceHandleModelSelect,
|
||||
executePrintMode,
|
||||
setupPermissionHandler,
|
||||
cleanupPermissionHandler,
|
||||
executeCommand,
|
||||
handleMessage,
|
||||
} from "@services/chat-tui-service";
|
||||
import type { ChatServiceState } from "@services/chat-tui-service";
|
||||
import type { ChatTUIOptions } from "@interfaces/ChatTUIOptions";
|
||||
import type { AgentConfig } from "@/types/agent-config";
|
||||
import { getConfig } from "@services/config";
|
||||
import { getThinkingMessage } from "@constants/status-messages";
|
||||
import {
|
||||
enterFullscreen,
|
||||
registerExitHandlers,
|
||||
exitFullscreen,
|
||||
clearScreen,
|
||||
} from "@utils/terminal";
|
||||
import { createCallbacks } from "@commands/chat-tui";
|
||||
import { agentLoader } from "@services/agent-loader";
|
||||
|
||||
interface ExecuteContext {
|
||||
state: ChatServiceState | null;
|
||||
}
|
||||
|
||||
const createHandleExit = (): (() => void) => (): void => {
|
||||
cleanupPermissionHandler();
|
||||
exitFullscreen();
|
||||
clearScreen();
|
||||
console.log("Goodbye!");
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
const createHandleModelSelect =
|
||||
(ctx: ExecuteContext) =>
|
||||
async (model: string): Promise<void> => {
|
||||
if (!ctx.state) return;
|
||||
await serviceHandleModelSelect(ctx.state, model, createCallbacks());
|
||||
};
|
||||
|
||||
const createHandleAgentSelect =
|
||||
(ctx: ExecuteContext) =>
|
||||
async (agentId: string, agent: AgentConfig): Promise<void> => {
|
||||
if (!ctx.state) return;
|
||||
|
||||
(ctx.state as ChatServiceState & { currentAgent?: string }).currentAgent =
|
||||
agentId;
|
||||
|
||||
if (agent.prompt) {
|
||||
const basePrompt = ctx.state.systemPrompt;
|
||||
ctx.state.systemPrompt = `${agent.prompt}\n\n${basePrompt}`;
|
||||
|
||||
if (
|
||||
ctx.state.messages.length > 0 &&
|
||||
ctx.state.messages[0].role === "system"
|
||||
) {
|
||||
ctx.state.messages[0].content = ctx.state.systemPrompt;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createHandleThemeSelect =
|
||||
() =>
|
||||
(themeName: string): void => {
|
||||
getConfig().then((config) => {
|
||||
config.set("theme", themeName);
|
||||
config.save();
|
||||
});
|
||||
};
|
||||
|
||||
const createHandleProviderSelect =
|
||||
(ctx: ExecuteContext) =>
|
||||
async (providerId: string): Promise<void> => {
|
||||
if (!ctx.state) return;
|
||||
ctx.state.provider = providerId as "copilot" | "ollama";
|
||||
const config = await getConfig();
|
||||
config.set("provider", providerId as "copilot" | "ollama");
|
||||
await config.save();
|
||||
};
|
||||
|
||||
const createHandleCascadeToggle =
|
||||
() =>
|
||||
async (enabled: boolean): Promise<void> => {
|
||||
const config = await getConfig();
|
||||
config.set("cascadeEnabled", enabled);
|
||||
await config.save();
|
||||
};
|
||||
|
||||
const createHandleCommand =
|
||||
(ctx: ExecuteContext, handleExit: () => void) =>
|
||||
async (command: string): Promise<void> => {
|
||||
if (!ctx.state) return;
|
||||
|
||||
if (["exit", "quit", "q"].includes(command.toLowerCase())) {
|
||||
handleExit();
|
||||
return;
|
||||
}
|
||||
|
||||
await executeCommand(ctx.state, command, createCallbacks());
|
||||
};
|
||||
|
||||
const createHandleSubmit =
|
||||
(ctx: ExecuteContext, handleCommand: (command: string) => Promise<void>) =>
|
||||
async (message: string): Promise<void> => {
|
||||
if (!ctx.state) return;
|
||||
|
||||
if (message.startsWith("/")) {
|
||||
const [command] = message.slice(1).split(/\s+/);
|
||||
await handleCommand(command);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set initial thinking message (streaming will update this)
|
||||
appStore.setThinkingMessage(getThinkingMessage());
|
||||
|
||||
try {
|
||||
await handleMessage(ctx.state, message, createCallbacks());
|
||||
} finally {
|
||||
// Clean up any remaining state after message handling
|
||||
appStore.setThinkingMessage(null);
|
||||
appStore.setCurrentToolCall(null);
|
||||
appStore.setMode("idle");
|
||||
}
|
||||
};
|
||||
|
||||
const execute = async (options: ChatTUIOptions): Promise<void> => {
|
||||
const ctx: ExecuteContext = {
|
||||
state: null,
|
||||
};
|
||||
|
||||
const { state, session } = await initializeChatService(options);
|
||||
ctx.state = state;
|
||||
|
||||
if (options.printMode && options.initialPrompt) {
|
||||
await executePrintMode(state, options.initialPrompt);
|
||||
return;
|
||||
}
|
||||
|
||||
setupPermissionHandler();
|
||||
|
||||
const models = await loadModels(state.provider);
|
||||
const agents = await agentLoader.getAvailableAgents(process.cwd());
|
||||
const config = await getConfig();
|
||||
const savedTheme = config.get("theme");
|
||||
|
||||
// Register exit handlers to ensure terminal cleanup on abrupt termination
|
||||
registerExitHandlers();
|
||||
enterFullscreen();
|
||||
|
||||
const handleExit = createHandleExit();
|
||||
const handleModelSelectFn = createHandleModelSelect(ctx);
|
||||
const handleAgentSelectFn = createHandleAgentSelect(ctx);
|
||||
const handleThemeSelectFn = createHandleThemeSelect();
|
||||
const handleProviderSelectFn = createHandleProviderSelect(ctx);
|
||||
const handleCascadeToggleFn = createHandleCascadeToggle();
|
||||
const handleCommand = createHandleCommand(ctx, handleExit);
|
||||
const handleSubmit = createHandleSubmit(ctx, handleCommand);
|
||||
|
||||
// Only pass sessionId if resuming/continuing - otherwise show Home view first
|
||||
const isResuming = options.continueSession || options.resumeSession;
|
||||
|
||||
const savedCascadeEnabled = config.get("cascadeEnabled");
|
||||
|
||||
const renderProps: RenderAppProps = {
|
||||
sessionId: isResuming ? session.id : undefined,
|
||||
handleSubmit,
|
||||
handleCommand,
|
||||
handleModelSelect: handleModelSelectFn,
|
||||
handleAgentSelect: handleAgentSelectFn,
|
||||
handleThemeSelect: handleThemeSelectFn,
|
||||
handleProviderSelect: handleProviderSelectFn,
|
||||
handleCascadeToggle: handleCascadeToggleFn,
|
||||
handleExit,
|
||||
showBanner: true,
|
||||
state,
|
||||
availableModels: models,
|
||||
agents: agents.map((a) => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
description: a.description,
|
||||
})),
|
||||
initialPrompt: options.initialPrompt,
|
||||
theme: savedTheme,
|
||||
cascadeEnabled: savedCascadeEnabled ?? true,
|
||||
};
|
||||
|
||||
await renderApp(renderProps);
|
||||
};
|
||||
|
||||
export default execute;
|
||||
8
src/commands/dashboard.ts
Normal file
8
src/commands/dashboard.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Dashboard Command
|
||||
*
|
||||
* Re-exports the modular dashboard implementation.
|
||||
*/
|
||||
|
||||
export { displayDashboard } from "@commands/components/dashboard/display";
|
||||
export type { DashboardConfig } from "@/types/dashboard";
|
||||
25
src/commands/handlers.ts
Normal file
25
src/commands/handlers.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Command handlers - Route commands to appropriate implementations
|
||||
*/
|
||||
|
||||
import { errorMessage } from "@utils/terminal";
|
||||
import { COMMAND_REGISTRY, isValidCommand } from "@commands/handlers/registry";
|
||||
import type { CommandOptions } from "@/types/index";
|
||||
|
||||
export const handleCommand = async (
|
||||
command: string,
|
||||
options: CommandOptions,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (!isValidCommand(command)) {
|
||||
errorMessage(`Unknown command: ${command}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const handler = COMMAND_REGISTRY[command];
|
||||
await handler(options);
|
||||
} catch (error) {
|
||||
errorMessage(`Command failed: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
10
src/commands/handlers/chat.ts
Normal file
10
src/commands/handlers/chat.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Chat command handler
|
||||
*/
|
||||
|
||||
import { execute as executeChat } from "@commands/chat";
|
||||
import type { CommandOptions } from "@/types/index";
|
||||
|
||||
export const handleChat = async (options: CommandOptions): Promise<void> => {
|
||||
await executeChat(options);
|
||||
};
|
||||
123
src/commands/handlers/classify.ts
Normal file
123
src/commands/handlers/classify.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Classify command handler
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
succeedSpinner,
|
||||
startSpinner,
|
||||
errorMessage,
|
||||
failSpinner,
|
||||
headerMessage,
|
||||
} from "@utils/terminal";
|
||||
import {
|
||||
INTENT_KEYWORDS,
|
||||
CLASSIFICATION_CONFIDENCE,
|
||||
} from "@constants/handlers";
|
||||
import type {
|
||||
CommandOptions,
|
||||
IntentRequest,
|
||||
IntentResponse,
|
||||
} from "@/types/index";
|
||||
|
||||
const classifyIntent = async (
|
||||
request: IntentRequest,
|
||||
): Promise<IntentResponse> => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
const prompt = request.prompt.toLowerCase();
|
||||
let intent: IntentResponse["intent"] = "ask";
|
||||
let confidence: number = CLASSIFICATION_CONFIDENCE.DEFAULT;
|
||||
|
||||
const intentMatchers: Record<string, () => void> = {
|
||||
fix: () => {
|
||||
intent = "fix";
|
||||
confidence = CLASSIFICATION_CONFIDENCE.HIGH;
|
||||
},
|
||||
test: () => {
|
||||
intent = "test";
|
||||
confidence = CLASSIFICATION_CONFIDENCE.MEDIUM;
|
||||
},
|
||||
refactor: () => {
|
||||
intent = "refactor";
|
||||
confidence = CLASSIFICATION_CONFIDENCE.LOW;
|
||||
},
|
||||
code: () => {
|
||||
intent = "code";
|
||||
confidence = CLASSIFICATION_CONFIDENCE.DEFAULT;
|
||||
},
|
||||
document: () => {
|
||||
intent = "document";
|
||||
confidence = CLASSIFICATION_CONFIDENCE.HIGH;
|
||||
},
|
||||
};
|
||||
|
||||
for (const [intentKey, keywords] of Object.entries(INTENT_KEYWORDS)) {
|
||||
const hasMatch = keywords.some((keyword) => prompt.includes(keyword));
|
||||
if (hasMatch) {
|
||||
intentMatchers[intentKey]?.();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
intent,
|
||||
confidence,
|
||||
reasoning: `Based on keywords in the prompt, this appears to be a ${intent} request.`,
|
||||
needsClarification: confidence < CLASSIFICATION_CONFIDENCE.THRESHOLD,
|
||||
clarificationQuestions:
|
||||
confidence < CLASSIFICATION_CONFIDENCE.THRESHOLD
|
||||
? [
|
||||
"Which specific files should I focus on?",
|
||||
"What is the expected outcome?",
|
||||
]
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const handleClassify = async (
|
||||
options: CommandOptions,
|
||||
): Promise<void> => {
|
||||
const { prompt, context, files = [] } = options;
|
||||
|
||||
if (!prompt) {
|
||||
errorMessage("Prompt is required");
|
||||
return;
|
||||
}
|
||||
|
||||
headerMessage("Classifying Intent");
|
||||
console.log(chalk.bold("Prompt:") + ` ${prompt}`);
|
||||
if (context) {
|
||||
console.log(chalk.bold("Context:") + ` ${context}`);
|
||||
}
|
||||
if (files.length > 0) {
|
||||
console.log(chalk.bold("Files:") + ` ${files.join(", ")}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
startSpinner("Analyzing prompt...");
|
||||
|
||||
try {
|
||||
const result = await classifyIntent({ prompt, context, files });
|
||||
succeedSpinner("Analysis complete");
|
||||
|
||||
console.log();
|
||||
console.log(chalk.bold("Intent:") + ` ${chalk.cyan(result.intent)}`);
|
||||
console.log(
|
||||
chalk.bold("Confidence:") +
|
||||
` ${chalk.green((result.confidence * 100).toFixed(1) + "%")}`,
|
||||
);
|
||||
console.log(chalk.bold("Reasoning:") + ` ${result.reasoning}`);
|
||||
|
||||
if (result.needsClarification && result.clarificationQuestions) {
|
||||
console.log();
|
||||
console.log(chalk.yellow.bold("Clarification needed:"));
|
||||
result.clarificationQuestions.forEach((q, i) => {
|
||||
console.log(` ${i + 1}. ${q}`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
failSpinner("Classification failed");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
113
src/commands/handlers/config.ts
Normal file
113
src/commands/handlers/config.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Config command handler
|
||||
*/
|
||||
|
||||
import {
|
||||
errorMessage,
|
||||
filePath,
|
||||
successMessage,
|
||||
hightLigthedJson,
|
||||
headerMessage,
|
||||
infoMessage,
|
||||
} from "@utils/terminal";
|
||||
import { getConfig } from "@services/config";
|
||||
import {
|
||||
VALID_CONFIG_KEYS,
|
||||
VALID_PROVIDERS,
|
||||
CONFIG_VALIDATION,
|
||||
} from "@constants/handlers";
|
||||
import type { CommandOptions, Provider } from "@/types/index";
|
||||
import type { ConfigAction, ConfigKey } from "@/types/handlers";
|
||||
|
||||
type ConfigActionHandler = (key?: string, value?: string) => Promise<void>;
|
||||
|
||||
const showConfig = async (): Promise<void> => {
|
||||
const config = await getConfig();
|
||||
headerMessage("Configuration");
|
||||
const allConfig = config.getAll();
|
||||
hightLigthedJson(allConfig);
|
||||
};
|
||||
|
||||
const showPath = async (): Promise<void> => {
|
||||
const config = await getConfig();
|
||||
const configPath = config.getConfigPath();
|
||||
console.log(filePath(configPath));
|
||||
};
|
||||
|
||||
const setConfigValue = async (key?: string, value?: string): Promise<void> => {
|
||||
if (!key || value === undefined) {
|
||||
errorMessage("Key and value are required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!VALID_CONFIG_KEYS.includes(key as ConfigKey)) {
|
||||
errorMessage(`Invalid config key: ${key}`);
|
||||
infoMessage(`Valid keys: ${VALID_CONFIG_KEYS.join(", ")}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await getConfig();
|
||||
|
||||
const keySetters: Record<ConfigKey, () => boolean> = {
|
||||
provider: () => {
|
||||
if (!VALID_PROVIDERS.includes(value as Provider)) {
|
||||
errorMessage(`Invalid provider: ${value}`);
|
||||
infoMessage(`Valid providers: ${VALID_PROVIDERS.join(", ")}`);
|
||||
return false;
|
||||
}
|
||||
config.set("provider", value as Provider);
|
||||
return true;
|
||||
},
|
||||
model: () => {
|
||||
config.set("model", value);
|
||||
return true;
|
||||
},
|
||||
maxIterations: () => {
|
||||
const num = parseInt(value, 10);
|
||||
if (isNaN(num) || num < CONFIG_VALIDATION.MIN_ITERATIONS) {
|
||||
errorMessage("maxIterations must be a positive number");
|
||||
return false;
|
||||
}
|
||||
config.set("maxIterations", num);
|
||||
return true;
|
||||
},
|
||||
timeout: () => {
|
||||
const num = parseInt(value, 10);
|
||||
if (isNaN(num) || num < CONFIG_VALIDATION.MIN_TIMEOUT_MS) {
|
||||
errorMessage(
|
||||
`timeout must be at least ${CONFIG_VALIDATION.MIN_TIMEOUT_MS}ms`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
config.set("timeout", num);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
const setter = keySetters[key as ConfigKey];
|
||||
const success = setter();
|
||||
|
||||
if (success) {
|
||||
await config.save();
|
||||
successMessage(`Set ${key} = ${value}`);
|
||||
}
|
||||
};
|
||||
|
||||
const CONFIG_ACTION_HANDLERS: Record<ConfigAction, ConfigActionHandler> = {
|
||||
show: showConfig,
|
||||
path: showPath,
|
||||
set: setConfigValue,
|
||||
};
|
||||
|
||||
export const handleConfig = async (options: CommandOptions): Promise<void> => {
|
||||
const { action, key, value } = options;
|
||||
|
||||
const handler = CONFIG_ACTION_HANDLERS[action as ConfigAction];
|
||||
|
||||
if (!handler) {
|
||||
errorMessage(`Unknown config action: ${action}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await handler(key, value);
|
||||
};
|
||||
62
src/commands/handlers/plan.ts
Normal file
62
src/commands/handlers/plan.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Plan command handler
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
hightLigthedJson,
|
||||
filePath,
|
||||
errorMessage,
|
||||
failSpinner,
|
||||
headerMessage,
|
||||
startSpinner,
|
||||
succeedSpinner,
|
||||
successMessage,
|
||||
} from "@utils/terminal";
|
||||
import type { CommandOptions } from "@/types/index";
|
||||
|
||||
export const handlePlan = async (options: CommandOptions): Promise<void> => {
|
||||
const { intent, task, files = [], output } = options;
|
||||
|
||||
if (!task) {
|
||||
errorMessage("Task description is required");
|
||||
return;
|
||||
}
|
||||
|
||||
headerMessage("Generating Plan");
|
||||
console.log(chalk.bold("Intent:") + ` ${chalk.cyan(intent || "unknown")}`);
|
||||
console.log(chalk.bold("Task:") + ` ${task}`);
|
||||
if (files.length > 0) {
|
||||
console.log(chalk.bold("Files:") + ` ${files.join(", ")}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
startSpinner("Generating execution plan...");
|
||||
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
succeedSpinner("Plan generated");
|
||||
|
||||
const plan = {
|
||||
intent,
|
||||
task,
|
||||
files,
|
||||
steps: [
|
||||
{ id: "step_1", type: "read", description: "Analyze existing code" },
|
||||
{ id: "step_2", type: "edit", description: "Apply changes" },
|
||||
{ id: "step_3", type: "execute", description: "Run tests" },
|
||||
],
|
||||
};
|
||||
|
||||
if (output) {
|
||||
const fs = await import("fs/promises");
|
||||
await fs.writeFile(output, JSON.stringify(plan, null, 2));
|
||||
successMessage(`Plan saved to ${filePath(output)}`);
|
||||
} else {
|
||||
hightLigthedJson(plan);
|
||||
}
|
||||
} catch (error) {
|
||||
failSpinner("Plan generation failed");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
28
src/commands/handlers/registry.ts
Normal file
28
src/commands/handlers/registry.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Command handler registry - object-based routing
|
||||
*/
|
||||
|
||||
import { handleChat } from "@commands/handlers/chat";
|
||||
import { handleRun } from "@commands/handlers/run";
|
||||
import { handleClassify } from "@commands/handlers/classify";
|
||||
import { handlePlan } from "@commands/handlers/plan";
|
||||
import { handleValidate } from "@commands/handlers/validate";
|
||||
import { handleConfig } from "@commands/handlers/config";
|
||||
import { handleServe } from "@commands/handlers/serve";
|
||||
import type { CommandRegistry } from "@/types/handlers";
|
||||
|
||||
export const COMMAND_REGISTRY: CommandRegistry = {
|
||||
chat: handleChat,
|
||||
run: handleRun,
|
||||
classify: handleClassify,
|
||||
plan: handlePlan,
|
||||
validate: handleValidate,
|
||||
config: handleConfig,
|
||||
serve: handleServe,
|
||||
};
|
||||
|
||||
export const isValidCommand = (
|
||||
command: string,
|
||||
): command is keyof CommandRegistry => {
|
||||
return command in COMMAND_REGISTRY;
|
||||
};
|
||||
10
src/commands/handlers/run.ts
Normal file
10
src/commands/handlers/run.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Run command handler
|
||||
*/
|
||||
|
||||
import { execute } from "@commands/runner";
|
||||
import type { CommandOptions } from "@/types/index";
|
||||
|
||||
export const handleRun = async (options: CommandOptions): Promise<void> => {
|
||||
await execute(options);
|
||||
};
|
||||
15
src/commands/handlers/serve.ts
Normal file
15
src/commands/handlers/serve.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Serve command handler
|
||||
*/
|
||||
|
||||
import { boxMessage, warningMessage, infoMessage } from "@utils/terminal";
|
||||
import type { CommandOptions } from "@/types/index";
|
||||
import { SERVER_INFO } from "@constants/serve";
|
||||
|
||||
export const handleServe = async (_options: CommandOptions): Promise<void> => {
|
||||
boxMessage(SERVER_INFO, "Server Mode");
|
||||
warningMessage("Server mode not yet implemented");
|
||||
infoMessage(
|
||||
"This will integrate with the existing agent/main.py JSON-RPC server",
|
||||
);
|
||||
};
|
||||
78
src/commands/handlers/validate.ts
Normal file
78
src/commands/handlers/validate.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Validate command handler
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
failSpinner,
|
||||
warningMessage,
|
||||
successMessage,
|
||||
succeedSpinner,
|
||||
startSpinner,
|
||||
errorMessage,
|
||||
headerMessage,
|
||||
filePath,
|
||||
} from "@utils/terminal";
|
||||
import { getConfig } from "@services/config";
|
||||
import type { CommandOptions } from "@/types/index";
|
||||
|
||||
export const handleValidate = async (
|
||||
options: CommandOptions,
|
||||
): Promise<void> => {
|
||||
const { planFile } = options;
|
||||
|
||||
if (!planFile) {
|
||||
errorMessage("Plan file is required");
|
||||
return;
|
||||
}
|
||||
|
||||
headerMessage("Validating Plan");
|
||||
console.log(chalk.bold("Plan file:") + ` ${filePath(planFile)}`);
|
||||
console.log();
|
||||
|
||||
startSpinner("Validating plan...");
|
||||
|
||||
try {
|
||||
const fs = await import("fs/promises");
|
||||
const planData = await fs.readFile(planFile, "utf-8");
|
||||
const plan = JSON.parse(planData);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
const config = await getConfig();
|
||||
|
||||
const warnings: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
plan.files?.forEach((file: string) => {
|
||||
if (config.isProtectedPath(file)) {
|
||||
warnings.push(`Protected path: ${file}`);
|
||||
}
|
||||
});
|
||||
|
||||
succeedSpinner("Validation complete");
|
||||
|
||||
console.log();
|
||||
if (errors.length > 0) {
|
||||
console.log(chalk.red.bold("Errors:"));
|
||||
errors.forEach((err) => console.log(` - ${err}`));
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
console.log(chalk.yellow.bold("Warnings:"));
|
||||
warnings.forEach((warn) => console.log(` - ${warn}`));
|
||||
}
|
||||
|
||||
if (errors.length === 0 && warnings.length === 0) {
|
||||
successMessage("Plan is valid and safe to execute");
|
||||
} else if (errors.length > 0) {
|
||||
errorMessage("Plan has errors and cannot be executed");
|
||||
process.exit(1);
|
||||
} else {
|
||||
warningMessage("Plan has warnings - proceed with caution");
|
||||
}
|
||||
} catch (error) {
|
||||
failSpinner("Validation failed");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
319
src/commands/mcp.ts
Normal file
319
src/commands/mcp.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* MCP Command - Manage MCP servers
|
||||
*
|
||||
* Usage:
|
||||
* codetyper mcp list - List configured servers
|
||||
* codetyper mcp add <name> - Add a new server
|
||||
* codetyper mcp remove <name> - Remove a server
|
||||
* codetyper mcp connect [name] - Connect to server(s)
|
||||
* codetyper mcp disconnect [name] - Disconnect from server(s)
|
||||
* codetyper mcp status - Show connection status
|
||||
* codetyper mcp tools - List available tools
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { errorMessage, infoMessage, successMessage } from "@utils/terminal";
|
||||
import {
|
||||
initializeMCP,
|
||||
getMCPConfig,
|
||||
addServer,
|
||||
removeServer,
|
||||
connectServer,
|
||||
disconnectServer,
|
||||
connectAllServers,
|
||||
disconnectAllServers,
|
||||
getServerInstances,
|
||||
getAllTools,
|
||||
} from "@services/mcp/index";
|
||||
|
||||
/**
|
||||
* MCP command handler
|
||||
*/
|
||||
export const mcpCommand = async (args: string[]): Promise<void> => {
|
||||
const subcommand = args[0] || "status";
|
||||
|
||||
const handlers: Record<string, (args: string[]) => Promise<void>> = {
|
||||
list: handleList,
|
||||
add: handleAdd,
|
||||
remove: handleRemove,
|
||||
connect: handleConnect,
|
||||
disconnect: handleDisconnect,
|
||||
status: handleStatus,
|
||||
tools: handleTools,
|
||||
help: handleHelp,
|
||||
};
|
||||
|
||||
const handler = handlers[subcommand];
|
||||
if (!handler) {
|
||||
errorMessage(`Unknown subcommand: ${subcommand}`);
|
||||
await handleHelp([]);
|
||||
return;
|
||||
}
|
||||
|
||||
await handler(args.slice(1));
|
||||
};
|
||||
|
||||
/**
|
||||
* List configured servers
|
||||
*/
|
||||
const handleList = async (_args: string[]): Promise<void> => {
|
||||
await initializeMCP();
|
||||
const config = await getMCPConfig();
|
||||
|
||||
const servers = Object.entries(config.servers);
|
||||
if (servers.length === 0) {
|
||||
infoMessage("No MCP servers configured.");
|
||||
infoMessage("Add a server with: codetyper mcp add <name>");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold("\nConfigured MCP Servers:\n"));
|
||||
|
||||
for (const [name, server] of servers) {
|
||||
const enabled =
|
||||
server.enabled !== false ? chalk.green("✓") : chalk.gray("○");
|
||||
console.log(` ${enabled} ${chalk.cyan(name)}`);
|
||||
console.log(
|
||||
` Command: ${server.command} ${(server.args || []).join(" ")}`,
|
||||
);
|
||||
if (server.transport && server.transport !== "stdio") {
|
||||
console.log(` Transport: ${server.transport}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new server
|
||||
*/
|
||||
const handleAdd = async (args: string[]): Promise<void> => {
|
||||
const name = args[0];
|
||||
if (!name) {
|
||||
errorMessage("Server name required");
|
||||
infoMessage(
|
||||
"Usage: codetyper mcp add <name> --command <cmd> [--args <args>]",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse options
|
||||
let command = "";
|
||||
const serverArgs: string[] = [];
|
||||
let isGlobal = false;
|
||||
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === "--command" || arg === "-c") {
|
||||
command = args[++i] || "";
|
||||
} else if (arg === "--args" || arg === "-a") {
|
||||
// Collect remaining args
|
||||
while (args[i + 1] && !args[i + 1].startsWith("--")) {
|
||||
serverArgs.push(args[++i]);
|
||||
}
|
||||
} else if (arg === "--global" || arg === "-g") {
|
||||
isGlobal = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!command) {
|
||||
// Interactive mode - ask for command
|
||||
infoMessage("Adding MCP server interactively...");
|
||||
infoMessage("Example: npx @modelcontextprotocol/server-sqlite");
|
||||
|
||||
// For now, require command flag
|
||||
errorMessage("Command required. Use --command <cmd>");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await addServer(
|
||||
name,
|
||||
{
|
||||
command,
|
||||
args: serverArgs.length > 0 ? serverArgs : undefined,
|
||||
enabled: true,
|
||||
},
|
||||
isGlobal,
|
||||
);
|
||||
|
||||
successMessage(`Added MCP server: ${name}`);
|
||||
infoMessage(`Connect with: codetyper mcp connect ${name}`);
|
||||
} catch (err) {
|
||||
errorMessage(`Failed to add server: ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a server
|
||||
*/
|
||||
const handleRemove = async (args: string[]): Promise<void> => {
|
||||
const name = args[0];
|
||||
if (!name) {
|
||||
errorMessage("Server name required");
|
||||
return;
|
||||
}
|
||||
|
||||
const isGlobal = args.includes("--global") || args.includes("-g");
|
||||
|
||||
try {
|
||||
await removeServer(name, isGlobal);
|
||||
successMessage(`Removed MCP server: ${name}`);
|
||||
} catch (err) {
|
||||
errorMessage(`Failed to remove server: ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Connect to server(s)
|
||||
*/
|
||||
const handleConnect = async (args: string[]): Promise<void> => {
|
||||
const name = args[0];
|
||||
|
||||
if (name) {
|
||||
// Connect to specific server
|
||||
try {
|
||||
infoMessage(`Connecting to ${name}...`);
|
||||
const instance = await connectServer(name);
|
||||
successMessage(`Connected to ${name}`);
|
||||
console.log(` Tools: ${instance.tools.length}`);
|
||||
console.log(` Resources: ${instance.resources.length}`);
|
||||
} catch (err) {
|
||||
errorMessage(`Failed to connect: ${err}`);
|
||||
}
|
||||
} else {
|
||||
// Connect to all servers
|
||||
infoMessage("Connecting to all servers...");
|
||||
const results = await connectAllServers();
|
||||
|
||||
for (const [serverName, instance] of results) {
|
||||
if (instance.state === "connected") {
|
||||
successMessage(
|
||||
`${serverName}: Connected (${instance.tools.length} tools)`,
|
||||
);
|
||||
} else {
|
||||
errorMessage(`${serverName}: ${instance.error || "Failed"}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Disconnect from server(s)
|
||||
*/
|
||||
const handleDisconnect = async (args: string[]): Promise<void> => {
|
||||
const name = args[0];
|
||||
|
||||
if (name) {
|
||||
await disconnectServer(name);
|
||||
successMessage(`Disconnected from ${name}`);
|
||||
} else {
|
||||
await disconnectAllServers();
|
||||
successMessage("Disconnected from all servers");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Show connection status
|
||||
*/
|
||||
const handleStatus = async (_args: string[]): Promise<void> => {
|
||||
await initializeMCP();
|
||||
const instances = getServerInstances();
|
||||
|
||||
if (instances.size === 0) {
|
||||
infoMessage("No MCP servers configured.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold("\nMCP Server Status:\n"));
|
||||
|
||||
for (const [name, instance] of instances) {
|
||||
const stateColors: Record<string, (s: string) => string> = {
|
||||
connected: chalk.green,
|
||||
connecting: chalk.yellow,
|
||||
disconnected: chalk.gray,
|
||||
error: chalk.red,
|
||||
};
|
||||
|
||||
const colorFn = stateColors[instance.state] || chalk.white;
|
||||
const status = colorFn(instance.state.toUpperCase());
|
||||
|
||||
console.log(` ${chalk.cyan(name)}: ${status}`);
|
||||
|
||||
if (instance.state === "connected") {
|
||||
console.log(` Tools: ${instance.tools.length}`);
|
||||
console.log(` Resources: ${instance.resources.length}`);
|
||||
}
|
||||
|
||||
if (instance.error) {
|
||||
console.log(` Error: ${chalk.red(instance.error)}`);
|
||||
}
|
||||
|
||||
console.log();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* List available tools
|
||||
*/
|
||||
const handleTools = async (_args: string[]): Promise<void> => {
|
||||
await connectAllServers();
|
||||
const tools = getAllTools();
|
||||
|
||||
if (tools.length === 0) {
|
||||
infoMessage("No tools available. Connect to MCP servers first.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold("\nAvailable MCP Tools:\n"));
|
||||
|
||||
// Group by server
|
||||
const byServer = new Map<string, typeof tools>();
|
||||
for (const item of tools) {
|
||||
const existing = byServer.get(item.server) || [];
|
||||
existing.push(item);
|
||||
byServer.set(item.server, existing);
|
||||
}
|
||||
|
||||
for (const [server, serverTools] of byServer) {
|
||||
console.log(chalk.cyan(` ${server}:`));
|
||||
for (const { tool } of serverTools) {
|
||||
console.log(` - ${chalk.white(tool.name)}`);
|
||||
if (tool.description) {
|
||||
console.log(` ${chalk.gray(tool.description)}`);
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Show help
|
||||
*/
|
||||
const handleHelp = async (_args: string[]): Promise<void> => {
|
||||
console.log(`
|
||||
${chalk.bold("MCP (Model Context Protocol) Management")}
|
||||
|
||||
${chalk.cyan("Usage:")}
|
||||
codetyper mcp <command> [options]
|
||||
|
||||
${chalk.cyan("Commands:")}
|
||||
list List configured servers
|
||||
add <name> Add a new server
|
||||
--command, -c <cmd> Command to run
|
||||
--args, -a <args> Arguments for command
|
||||
--global, -g Add to global config
|
||||
remove <name> Remove a server
|
||||
--global, -g Remove from global config
|
||||
connect [name] Connect to server(s)
|
||||
disconnect [name] Disconnect from server(s)
|
||||
status Show connection status
|
||||
tools List available tools from connected servers
|
||||
|
||||
${chalk.cyan("Examples:")}
|
||||
codetyper mcp add sqlite -c npx -a @modelcontextprotocol/server-sqlite
|
||||
codetyper mcp connect sqlite
|
||||
codetyper mcp tools
|
||||
`);
|
||||
};
|
||||
|
||||
export default mcpCommand;
|
||||
10
src/commands/runner.ts
Normal file
10
src/commands/runner.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Autonomous task runner - executes agent tasks
|
||||
*/
|
||||
|
||||
export { execute } from "@commands/runner/execute";
|
||||
export { createPlan } from "@commands/runner/create-plan";
|
||||
export { executePlan } from "@commands/runner/execute-plan";
|
||||
export { displayPlan, getStepIcon } from "@commands/runner/display-plan";
|
||||
export { displayHeader } from "@commands/runner/display-header";
|
||||
export { delay } from "@commands/runner/utils";
|
||||
45
src/commands/runner/create-plan.ts
Normal file
45
src/commands/runner/create-plan.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Plan creation utilities
|
||||
*/
|
||||
|
||||
import {
|
||||
RUNNER_DELAYS,
|
||||
MOCK_STEPS,
|
||||
DEFAULT_FILE,
|
||||
ESTIMATED_TIME_PER_STEP,
|
||||
} from "@constants/runner";
|
||||
import { delay } from "@commands/runner/utils";
|
||||
import type { AgentType, ExecutionPlan, PlanStep } from "@/types/index";
|
||||
|
||||
export const createPlan = async (
|
||||
task: string,
|
||||
_agent: AgentType,
|
||||
files: string[],
|
||||
): Promise<ExecutionPlan> => {
|
||||
await delay(RUNNER_DELAYS.PLANNING);
|
||||
|
||||
const targetFile = files[0] ?? DEFAULT_FILE;
|
||||
|
||||
const steps: PlanStep[] = [
|
||||
{
|
||||
...MOCK_STEPS.READ,
|
||||
file: targetFile,
|
||||
},
|
||||
{
|
||||
...MOCK_STEPS.EDIT,
|
||||
file: targetFile,
|
||||
dependencies: [...MOCK_STEPS.EDIT.dependencies],
|
||||
},
|
||||
{
|
||||
...MOCK_STEPS.EXECUTE,
|
||||
dependencies: [...MOCK_STEPS.EXECUTE.dependencies],
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
steps,
|
||||
intent: "code",
|
||||
summary: task,
|
||||
estimatedTime: steps.length * ESTIMATED_TIME_PER_STEP,
|
||||
};
|
||||
};
|
||||
27
src/commands/runner/display-header.ts
Normal file
27
src/commands/runner/display-header.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Runner header display utilities
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { headerMessage, filePath } from "@utils/terminal";
|
||||
import type { RunnerOptions } from "@/types/runner";
|
||||
|
||||
export const displayHeader = (options: RunnerOptions): void => {
|
||||
const { task, agent, files, dryRun } = options;
|
||||
|
||||
headerMessage("Running Task");
|
||||
console.log(chalk.bold("Agent:") + ` ${chalk.cyan(agent)}`);
|
||||
console.log(chalk.bold("Task:") + ` ${task}`);
|
||||
|
||||
if (files.length > 0) {
|
||||
console.log(
|
||||
chalk.bold("Files:") + ` ${files.map((f) => filePath(f)).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.bold("Mode:") +
|
||||
` ${dryRun ? chalk.yellow("Dry Run") : chalk.green("Execute")}`,
|
||||
);
|
||||
console.log();
|
||||
};
|
||||
34
src/commands/runner/display-plan.ts
Normal file
34
src/commands/runner/display-plan.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Plan display utilities
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { filePath } from "@utils/terminal";
|
||||
import { STEP_ICONS, DEFAULT_STEP_ICON } from "@constants/runner";
|
||||
import type { ExecutionPlan, PlanStep } from "@/types/index";
|
||||
|
||||
export const getStepIcon = (type: PlanStep["type"]): string =>
|
||||
STEP_ICONS[type] ?? DEFAULT_STEP_ICON;
|
||||
|
||||
export const displayPlan = (plan: ExecutionPlan): void => {
|
||||
console.log("\n" + chalk.bold.underline("Execution Plan:"));
|
||||
console.log(chalk.gray(`${plan.summary}`));
|
||||
console.log();
|
||||
|
||||
plan.steps.forEach((step, index) => {
|
||||
const icon = getStepIcon(step.type);
|
||||
const deps = step.dependencies
|
||||
? chalk.gray(` (depends on: ${step.dependencies.join(", ")})`)
|
||||
: "";
|
||||
console.log(
|
||||
`${icon} ${chalk.bold(`Step ${index + 1}:`)} ${step.description}${deps}`,
|
||||
);
|
||||
if (step.file) {
|
||||
console.log(` ${filePath(step.file)}`);
|
||||
}
|
||||
if (step.tool) {
|
||||
console.log(` ${chalk.gray(`Tool: ${step.tool}`)}`);
|
||||
}
|
||||
});
|
||||
console.log();
|
||||
};
|
||||
35
src/commands/runner/execute-plan.ts
Normal file
35
src/commands/runner/execute-plan.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Plan execution utilities
|
||||
*/
|
||||
|
||||
import { failSpinner, succeedSpinner, startSpinner } from "@utils/terminal";
|
||||
import { RUNNER_DELAYS } from "@constants/runner";
|
||||
import { getStepIcon } from "@commands/runner/display-plan";
|
||||
import { delay } from "@commands/runner/utils";
|
||||
import type { ExecutionPlan, PlanStep } from "@/types/index";
|
||||
import type { StepContext } from "@/types/runner";
|
||||
|
||||
const executeStep = async (context: StepContext): Promise<void> => {
|
||||
const { step, current, total } = context;
|
||||
const icon = getStepIcon(step.type);
|
||||
const message = `${icon} Step ${current}/${total}: ${step.description}`;
|
||||
|
||||
startSpinner(message);
|
||||
|
||||
try {
|
||||
await delay(RUNNER_DELAYS.STEP_EXECUTION);
|
||||
succeedSpinner(message);
|
||||
} catch (error) {
|
||||
failSpinner(message);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const executePlan = async (plan: ExecutionPlan): Promise<void> => {
|
||||
const total = plan.steps.length;
|
||||
|
||||
for (let i = 0; i < plan.steps.length; i++) {
|
||||
const step: PlanStep = plan.steps[i];
|
||||
await executeStep({ step, current: i + 1, total });
|
||||
}
|
||||
};
|
||||
118
src/commands/runner/execute.ts
Normal file
118
src/commands/runner/execute.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Main runner execution function
|
||||
*/
|
||||
|
||||
import {
|
||||
askConfirm,
|
||||
failSpinner,
|
||||
successMessage,
|
||||
succeedSpinner,
|
||||
headerMessage,
|
||||
startSpinner,
|
||||
infoMessage,
|
||||
errorMessage,
|
||||
warningMessage,
|
||||
} from "@utils/terminal";
|
||||
import { RUNNER_DELAYS, RUNNER_MESSAGES } from "@constants/runner";
|
||||
import { displayHeader } from "@commands/runner/display-header";
|
||||
import { displayPlan } from "@commands/runner/display-plan";
|
||||
import { createPlan } from "@commands/runner/create-plan";
|
||||
import { executePlan } from "@commands/runner/execute-plan";
|
||||
import { delay } from "@commands/runner/utils";
|
||||
import type { CommandOptions, AgentType } from "@/types/index";
|
||||
import type { RunnerOptions } from "@/types/runner";
|
||||
|
||||
const parseOptions = (options: CommandOptions): RunnerOptions | null => {
|
||||
const {
|
||||
task,
|
||||
agent = "coder",
|
||||
files = [],
|
||||
dryRun = false,
|
||||
autoApprove = false,
|
||||
} = options;
|
||||
|
||||
if (!task) {
|
||||
errorMessage(RUNNER_MESSAGES.TASK_REQUIRED);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
task,
|
||||
agent: agent as AgentType,
|
||||
files,
|
||||
dryRun,
|
||||
autoApprove,
|
||||
};
|
||||
};
|
||||
|
||||
const runDiscoveryPhase = async (): Promise<void> => {
|
||||
startSpinner(RUNNER_MESSAGES.DISCOVERY_START);
|
||||
await delay(RUNNER_DELAYS.DISCOVERY);
|
||||
succeedSpinner(RUNNER_MESSAGES.DISCOVERY_COMPLETE);
|
||||
};
|
||||
|
||||
const runPlanningPhase = async (
|
||||
task: string,
|
||||
agent: AgentType,
|
||||
files: string[],
|
||||
) => {
|
||||
startSpinner(RUNNER_MESSAGES.PLANNING_START);
|
||||
const plan = await createPlan(task, agent, files);
|
||||
succeedSpinner(`Plan created with ${plan.steps.length} steps`);
|
||||
return plan;
|
||||
};
|
||||
|
||||
const confirmExecution = async (autoApprove: boolean): Promise<boolean> => {
|
||||
if (autoApprove) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const approved = await askConfirm(RUNNER_MESSAGES.CONFIRM_EXECUTE);
|
||||
|
||||
if (!approved) {
|
||||
warningMessage(RUNNER_MESSAGES.EXECUTION_CANCELLED);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const execute = async (options: CommandOptions): Promise<void> => {
|
||||
const runnerOptions = parseOptions(options);
|
||||
|
||||
if (!runnerOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { task, agent, files, dryRun, autoApprove } = runnerOptions;
|
||||
|
||||
displayHeader(runnerOptions);
|
||||
|
||||
try {
|
||||
await runDiscoveryPhase();
|
||||
|
||||
const plan = await runPlanningPhase(task, agent, files);
|
||||
|
||||
displayPlan(plan);
|
||||
|
||||
if (dryRun) {
|
||||
infoMessage(RUNNER_MESSAGES.DRY_RUN_INFO);
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldExecute = await confirmExecution(autoApprove);
|
||||
|
||||
if (!shouldExecute) {
|
||||
return;
|
||||
}
|
||||
|
||||
headerMessage("Executing Plan");
|
||||
await executePlan(plan);
|
||||
|
||||
successMessage(`\n${RUNNER_MESSAGES.TASK_COMPLETE}`);
|
||||
} catch (error) {
|
||||
failSpinner(RUNNER_MESSAGES.TASK_FAILED);
|
||||
errorMessage(`Error: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
6
src/commands/runner/utils.ts
Normal file
6
src/commands/runner/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Runner utility functions
|
||||
*/
|
||||
|
||||
export const delay = (ms: number): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
5
src/constants/agent.ts
Normal file
5
src/constants/agent.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Agent constants
|
||||
*/
|
||||
|
||||
export const MAX_ITERATIONS = 50;
|
||||
23
src/constants/auto-scroll.ts
Normal file
23
src/constants/auto-scroll.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Auto-Scroll Constants
|
||||
*
|
||||
* Constants for auto-scroll behavior in the TUI
|
||||
*/
|
||||
|
||||
/** Distance from bottom (in lines) to consider "at bottom" */
|
||||
export const BOTTOM_THRESHOLD = 3;
|
||||
|
||||
/** Settling time after operations complete (ms) */
|
||||
export const SETTLE_TIMEOUT_MS = 300;
|
||||
|
||||
/** Timeout for marking auto-scroll events (ms) */
|
||||
export const AUTO_SCROLL_MARK_TIMEOUT_MS = 250;
|
||||
|
||||
/** Default scroll lines per keyboard event */
|
||||
export const KEYBOARD_SCROLL_LINES = 3;
|
||||
|
||||
/** Default scroll lines per page event */
|
||||
export const PAGE_SCROLL_LINES = 10;
|
||||
|
||||
/** Mouse scroll lines per wheel event */
|
||||
export const MOUSE_SCROLL_LINES = 3;
|
||||
103
src/constants/banner.ts
Normal file
103
src/constants/banner.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Banner constants for CodeTyper CLI
|
||||
*/
|
||||
|
||||
// ASCII art for "codetyper" using block characters
|
||||
export const BANNER_LINES = [
|
||||
" __ __ ",
|
||||
" _______ _____/ /__ / /___ ______ ___ _____ ",
|
||||
" / ___/ / / / _ \\/ _ \\/ __/ / / / __ \\/ _ \\/ ___/ ",
|
||||
"/ /__/ /_/ / __/ __/ /_/ /_/ / /_/ / __/ / ",
|
||||
"\\___/\\____/\\___/\\___/\\__/\\__, / .___/\\___/_/ ",
|
||||
" /____/_/ ",
|
||||
] as const;
|
||||
|
||||
// Alternative minimal banner
|
||||
export const BANNER_MINIMAL = [
|
||||
"╭───────────────────────────────────────╮",
|
||||
"│ ▄▀▀ ▄▀▄ █▀▄ ██▀ ▀█▀ ▀▄▀ █▀▄ ██▀ █▀▄ │",
|
||||
"│ ▀▄▄ ▀▄▀ █▄▀ █▄▄ █ █ █▀ █▄▄ █▀▄ │",
|
||||
"╰───────────────────────────────────────╯",
|
||||
] as const;
|
||||
|
||||
// Block-style banner (similar to opencode)
|
||||
export const BANNER_BLOCKS = [
|
||||
"█▀▀ █▀█ █▀▄ █▀▀ ▀█▀ █▄█ █▀█ █▀▀ █▀█",
|
||||
"█ █ █ █ █ █▀▀ █ █ █▀▀ █▀▀ █▀▄",
|
||||
"▀▀▀ ▀▀▀ ▀▀ ▀▀▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀",
|
||||
] as const;
|
||||
|
||||
// Gradient colors for banner (cyan to blue)
|
||||
export const GRADIENT_COLORS = [
|
||||
"\x1b[96m", // Bright cyan
|
||||
"\x1b[36m", // Cyan
|
||||
"\x1b[94m", // Bright blue
|
||||
"\x1b[34m", // Blue
|
||||
"\x1b[95m", // Bright magenta
|
||||
"\x1b[35m", // Magenta
|
||||
] as const;
|
||||
|
||||
// Banner style to lines mapping
|
||||
export const BANNER_STYLE_MAP: Record<string, readonly string[]> = {
|
||||
default: BANNER_LINES,
|
||||
minimal: BANNER_MINIMAL,
|
||||
blocks: BANNER_BLOCKS,
|
||||
} as const;
|
||||
|
||||
// Large ASCII art banner
|
||||
export const BANNER = `
|
||||
,gggg, _,gggggg,_ ,gggggggggggg, ,ggggggg, ,ggggggggggggggg ,ggg, gg ,ggggggggggg, ,ggggggg, ,ggggggggggg,
|
||||
,88"""Y8b, ,d8P""d8P"Y8b, dP"""88""""""Y8b, ,dP"""""""Y8bdP""""""88"""""""dP""Y8a 88 dP"""88""""""Y8, ,dP"""""""Y8bdP"""88""""""Y8,
|
||||
d8" \`Y8,d8' Y8 "8b,dPYb, 88 \`8b, d8' a Y8Yb,_ 88 Yb, \`88 88 Yb, 88 \`8b d8' a Y8Yb, 88 \`8b
|
||||
d8' 8b d8d8' \`Ybaaad88P' \`" 88 \`8b 88 "Y8P' \`"" 88 \`" 88 88 \`" 88 ,8P 88 "Y8P' \`" 88 ,8P
|
||||
,8I "Y88P'8P \`"""Y8 88 Y8 \`8baaaa 88 88 88 88aaaad8P" \`8baaaa 88aaaad8P"
|
||||
I8' 8b d8 88 d8,d8P"""" 88 88 88 88""""" ,d8P"""" 88""""Yb,
|
||||
d8 Y8, ,8P 88 ,8Pd8" 88 88 ,88 88 d8" 88 "8b
|
||||
Y8, \`Y8, ,8P' 88 ,8P'Y8, gg, 88 Y8b,___,d888 88 Y8, 88 \`8i
|
||||
\`Yba,,_____, \`Y8b,,__,,d8P' 88______,dP' \`Yba,,_____, "Yb,,8P "Y88888P"88, 88 \`Yba,,_____, 88 Yb,
|
||||
\`"Y8888888 \`"Y8888P"' 888888888P" \`"Y8888888 "Y8P' ,ad8888 88 \`"Y8888888 88 Y8
|
||||
d8P" 88
|
||||
,d8' 88
|
||||
d8' 88
|
||||
88 88
|
||||
Y8,_ _,88
|
||||
"Y888P"
|
||||
`;
|
||||
|
||||
// Welcome message with help information
|
||||
export const WELCOME_MESSAGE = `
|
||||
🤖 CodeTyper AI Agent - Autonomous Code Generation Assistant
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Default Provider: GitHub Copilot (gpt-4)
|
||||
|
||||
Getting Started:
|
||||
codetyper chat Start interactive chat
|
||||
codetyper run "your task" Execute autonomous task
|
||||
codetyper classify "prompt" Analyze intent
|
||||
codetyper config show View configuration
|
||||
|
||||
Commands:
|
||||
chat Interactive REPL session
|
||||
run <task> Execute task autonomously
|
||||
classify <prompt> Classify user intent
|
||||
plan <intent> Generate execution plan
|
||||
validate <plan> Validate plan safety
|
||||
config Manage configuration
|
||||
serve Start JSON-RPC server
|
||||
|
||||
Options:
|
||||
--help, -h Show help
|
||||
--version, -V Show version
|
||||
|
||||
Chat Commands:
|
||||
/help Show help
|
||||
/models View available LLM providers
|
||||
/provider Switch LLM provider
|
||||
/files List context files
|
||||
/clear Clear conversation
|
||||
/exit Exit chat
|
||||
|
||||
💡 Tip: Use 'codetyper chat' then '/models' to see all available providers
|
||||
📖 Docs: Run 'codetyper --help <command>' for detailed information
|
||||
`;
|
||||
30
src/constants/bash.ts
Normal file
30
src/constants/bash.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Bash tool constants
|
||||
*/
|
||||
|
||||
export const BASH_DEFAULTS = {
|
||||
MAX_OUTPUT_LENGTH: 30000,
|
||||
TIMEOUT: 120000,
|
||||
KILL_DELAY: 1000,
|
||||
} as const;
|
||||
|
||||
export const BASH_SIGNALS = {
|
||||
TERMINATE: "SIGTERM",
|
||||
KILL: "SIGKILL",
|
||||
} as const;
|
||||
|
||||
export const BASH_MESSAGES = {
|
||||
PERMISSION_DENIED: "Permission denied by user",
|
||||
TIMED_OUT: (timeout: number) => `Command timed out after ${timeout}ms`,
|
||||
ABORTED: "Command aborted",
|
||||
EXIT_CODE: (code: number) => `Command exited with code ${code}`,
|
||||
TRUNCATED: "\n\n... (truncated)",
|
||||
} as const;
|
||||
|
||||
export const BASH_DESCRIPTION = `Execute a shell command. Use this tool to run commands like git, npm, mkdir, etc.
|
||||
|
||||
Guidelines:
|
||||
- Always provide a clear description of what the command does
|
||||
- Use absolute paths when possible
|
||||
- Be careful with destructive commands (rm, etc.)
|
||||
- Commands that modify the filesystem will require user approval`;
|
||||
20
src/constants/bashPatterns.ts
Normal file
20
src/constants/bashPatterns.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/** Quiet bash commands - read-only exploration operations */
|
||||
export const QUIET_BASH_PATTERNS = [
|
||||
/^ls\b/,
|
||||
/^cat\b/,
|
||||
/^head\b/,
|
||||
/^tail\b/,
|
||||
/^find\b/,
|
||||
/^grep\b/,
|
||||
/^rg\b/,
|
||||
/^fd\b/,
|
||||
/^tree\b/,
|
||||
/^pwd\b/,
|
||||
/^echo\b/,
|
||||
/^which\b/,
|
||||
/^file\b/,
|
||||
/^stat\b/,
|
||||
/^wc\b/,
|
||||
/^du\b/,
|
||||
/^df\b/,
|
||||
];
|
||||
106
src/constants/chat-service.ts
Normal file
106
src/constants/chat-service.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Chat service constants
|
||||
*/
|
||||
|
||||
export const CHAT_TRUNCATE_DEFAULTS = {
|
||||
MAX_LINES: 10,
|
||||
MAX_LENGTH: 500,
|
||||
} as const;
|
||||
|
||||
export const FILE_SIZE_LIMITS = {
|
||||
MAX_CONTEXT_FILE_SIZE: 100000,
|
||||
} as const;
|
||||
|
||||
export const DIFF_PATTERNS = [
|
||||
/@@\s*-\d+/m,
|
||||
/---\s+[ab]?\//m,
|
||||
/\+\+\+\s+[ab]?\//m,
|
||||
] as const;
|
||||
|
||||
export const GLOB_IGNORE_PATTERNS = [
|
||||
"**/node_modules/**",
|
||||
"**/.git/**",
|
||||
] as const;
|
||||
|
||||
export const CHAT_MESSAGES = {
|
||||
CONVERSATION_CLEARED: "Conversation cleared",
|
||||
SESSION_SAVED: "Session saved",
|
||||
LEARNING_SAVED: (scope: string) => `Learning saved (${scope})`,
|
||||
LEARNING_SKIPPED: "Learning skipped",
|
||||
NO_LEARNINGS:
|
||||
"No learnings saved yet. Use /remember to save learnings about your project.",
|
||||
NO_CONVERSATION:
|
||||
"No conversation to create learning from. Start a conversation first.",
|
||||
NO_LEARNINGS_DETECTED:
|
||||
'No learnings detected from the last exchange. Try being more explicit about preferences (e.g., "always use TypeScript", "prefer functional style").',
|
||||
UNKNOWN_COMMAND: (cmd: string) => `Unknown command: ${cmd}`,
|
||||
FILE_NOT_FOUND: (pattern: string) => `File not found: ${pattern}`,
|
||||
FILE_TOO_LARGE: (name: string, size: number) =>
|
||||
`File too large: ${name} (${Math.round(size / 1024)}KB)`,
|
||||
FILE_IS_BINARY: (name: string) => `Cannot add binary file: ${name}`,
|
||||
FILE_ADDED: (name: string) => `Added to context: ${name}`,
|
||||
FILE_ADD_FAILED: (error: unknown) => `Failed to add file: ${error}`,
|
||||
FILE_READ_FAILED: (error: unknown) => `Failed to read file: ${error}`,
|
||||
ANALYZE_FILES: "Analyze the files I've added to the context.",
|
||||
GITHUB_ISSUES_FOUND: (count: number, issues: string) =>
|
||||
`Found ${count} GitHub issue(s): ${issues}`,
|
||||
COMPACTION_STARTING: "Summarizing conversation history...",
|
||||
COMPACTION_CONTINUING: "Continuing with your request...",
|
||||
} as const;
|
||||
|
||||
export const AUTH_MESSAGES = {
|
||||
ALREADY_LOGGED_IN: "Already logged in. Use /logout first to re-authenticate.",
|
||||
AUTH_SUCCESS: "Successfully authenticated with GitHub Copilot!",
|
||||
AUTH_FAILED: (error: string) => `Authentication failed: ${error}`,
|
||||
AUTH_START_FAILED: (error: string) =>
|
||||
`Failed to start authentication: ${error}`,
|
||||
LOGGED_OUT:
|
||||
"Logged out from GitHub Copilot. Run /login to authenticate again.",
|
||||
NOT_LOGGED_IN: "Not logged in. Run /login to authenticate.",
|
||||
NO_LOGIN_REQUIRED: (provider: string) =>
|
||||
`Provider ${provider} doesn't require login.`,
|
||||
NO_LOGOUT_SUPPORT: (provider: string) =>
|
||||
`Provider ${provider} doesn't support logout.`,
|
||||
OLLAMA_NO_AUTH: "Ollama is a local provider - no authentication required.",
|
||||
COPILOT_AUTH_INSTRUCTIONS: (uri: string, code: string) =>
|
||||
`To authenticate with GitHub Copilot:\n\n1. Open: ${uri}\n2. Enter code: ${code}\n\nWaiting for authentication...`,
|
||||
LOGGED_IN_AS: (login: string, name?: string) =>
|
||||
`Logged in as: ${login}${name ? ` (${name})` : ""}`,
|
||||
} as const;
|
||||
|
||||
export const MODEL_MESSAGES = {
|
||||
MODEL_AUTO: "Model set to auto - the provider will choose the best model.",
|
||||
MODEL_CHANGED: (model: string) => `Model changed to: ${model}`,
|
||||
} as const;
|
||||
|
||||
// Re-export HELP_TEXT from prompts for backward compatibility
|
||||
export { HELP_TEXT } from "@prompts/ui/help";
|
||||
|
||||
export const LEARNING_CONFIDENCE_THRESHOLD = 0.7;
|
||||
export const MAX_LEARNINGS_DISPLAY = 20;
|
||||
|
||||
export type CommandName =
|
||||
| "help"
|
||||
| "h"
|
||||
| "clear"
|
||||
| "c"
|
||||
| "save"
|
||||
| "s"
|
||||
| "context"
|
||||
| "usage"
|
||||
| "u"
|
||||
| "model"
|
||||
| "models"
|
||||
| "agent"
|
||||
| "a"
|
||||
| "theme"
|
||||
| "mcp"
|
||||
| "mode"
|
||||
| "whoami"
|
||||
| "login"
|
||||
| "logout"
|
||||
| "provider"
|
||||
| "p"
|
||||
| "status"
|
||||
| "remember"
|
||||
| "learnings";
|
||||
85
src/constants/command-suggestion.ts
Normal file
85
src/constants/command-suggestion.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Command suggestion constants
|
||||
*/
|
||||
|
||||
import type { SuggestionPriority } from "@/types/command-suggestion";
|
||||
|
||||
export const PROJECT_FILES = {
|
||||
PACKAGE_JSON: "package.json",
|
||||
YARN_LOCK: "yarn.lock",
|
||||
PNPM_LOCK: "pnpm-lock.yaml",
|
||||
BUN_LOCK: "bun.lockb",
|
||||
CARGO_TOML: "Cargo.toml",
|
||||
GO_MOD: "go.mod",
|
||||
PYPROJECT: "pyproject.toml",
|
||||
REQUIREMENTS: "requirements.txt",
|
||||
MAKEFILE: "Makefile",
|
||||
DOCKERFILE: "Dockerfile",
|
||||
} as const;
|
||||
|
||||
export const PRIORITY_ORDER: Record<SuggestionPriority, number> = {
|
||||
high: 0,
|
||||
medium: 1,
|
||||
low: 2,
|
||||
};
|
||||
|
||||
export const PRIORITY_ICONS: Record<SuggestionPriority, string> = {
|
||||
high: "⚡",
|
||||
medium: "→",
|
||||
low: "·",
|
||||
};
|
||||
|
||||
export const FILE_PATTERNS = {
|
||||
PACKAGE_JSON: /package\.json$/,
|
||||
TSCONFIG: /tsconfig.*\.json$/,
|
||||
SOURCE_FILES: /\.(ts|tsx|js|jsx)$/,
|
||||
CARGO_TOML: /Cargo\.toml$/,
|
||||
GO_MOD: /go\.mod$/,
|
||||
PYTHON_DEPS: /requirements.*\.txt$|pyproject\.toml$/,
|
||||
DOCKER: /Dockerfile$|docker-compose.*\.ya?ml$/,
|
||||
MAKEFILE: /Makefile$/,
|
||||
MIGRATIONS: /migrations?\/.*\.(sql|ts|js)$/,
|
||||
ENV_EXAMPLE: /\.env\.example$|\.env\.sample$/,
|
||||
LINTER_CONFIG: /\.eslintrc|\.prettierrc|eslint\.config|prettier\.config/,
|
||||
TEST_FILE: /\.test\.|\.spec\.|__tests__/,
|
||||
} as const;
|
||||
|
||||
export const CONTENT_PATTERNS = {
|
||||
DEPENDENCIES: /\"dependencies\"/,
|
||||
DEV_DEPENDENCIES: /\"devDependencies\"/,
|
||||
PEER_DEPENDENCIES: /\"peerDependencies\"/,
|
||||
} as const;
|
||||
|
||||
export const SUGGESTION_MESSAGES = {
|
||||
INSTALL_DEPS: "Install dependencies",
|
||||
REBUILD_PROJECT: "Rebuild the project",
|
||||
RUN_TESTS: "Run tests",
|
||||
START_DEV: "Start development server",
|
||||
BUILD_RUST: "Build the Rust project",
|
||||
TIDY_GO: "Tidy Go modules",
|
||||
INSTALL_PYTHON_EDITABLE: "Install Python package in editable mode",
|
||||
INSTALL_PYTHON_DEPS: "Install Python dependencies",
|
||||
DOCKER_COMPOSE_BUILD: "Rebuild and start Docker containers",
|
||||
DOCKER_BUILD: "Rebuild Docker image",
|
||||
RUN_MAKE: "Run make",
|
||||
RUN_MIGRATE: "Run database migrations",
|
||||
CREATE_ENV: "Create local .env file",
|
||||
RUN_LINT: "Run linter to check for issues",
|
||||
} as const;
|
||||
|
||||
export const SUGGESTION_REASONS = {
|
||||
PACKAGE_JSON_MODIFIED: "package.json was modified",
|
||||
TSCONFIG_CHANGED: "TypeScript configuration changed",
|
||||
TEST_FILE_MODIFIED: "Test file was modified",
|
||||
SOURCE_FILE_MODIFIED: "Source file was modified",
|
||||
CARGO_MODIFIED: "Cargo.toml was modified",
|
||||
GO_MOD_MODIFIED: "go.mod was modified",
|
||||
PYTHON_DEPS_CHANGED: "Python dependencies changed",
|
||||
REQUIREMENTS_MODIFIED: "requirements.txt was modified",
|
||||
DOCKER_COMPOSE_CHANGED: "Docker Compose configuration changed",
|
||||
DOCKERFILE_MODIFIED: "Dockerfile was modified",
|
||||
MAKEFILE_MODIFIED: "Makefile was modified",
|
||||
MIGRATION_MODIFIED: "Migration file was added or modified",
|
||||
ENV_TEMPLATE_MODIFIED: "Environment template was modified",
|
||||
LINTER_CONFIG_CHANGED: "Linter configuration changed",
|
||||
} as const;
|
||||
114
src/constants/components.ts
Normal file
114
src/constants/components.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* UI component constants
|
||||
*/
|
||||
|
||||
// Box drawing characters
|
||||
export const BoxChars = {
|
||||
// Single line
|
||||
single: {
|
||||
topLeft: "┌",
|
||||
topRight: "┐",
|
||||
bottomLeft: "└",
|
||||
bottomRight: "┘",
|
||||
horizontal: "─",
|
||||
vertical: "│",
|
||||
leftT: "├",
|
||||
rightT: "┤",
|
||||
topT: "┬",
|
||||
bottomT: "┴",
|
||||
cross: "┼",
|
||||
},
|
||||
// Double line
|
||||
double: {
|
||||
topLeft: "╔",
|
||||
topRight: "╗",
|
||||
bottomLeft: "╚",
|
||||
bottomRight: "╝",
|
||||
horizontal: "═",
|
||||
vertical: "║",
|
||||
leftT: "╠",
|
||||
rightT: "╣",
|
||||
topT: "╦",
|
||||
bottomT: "╩",
|
||||
cross: "╬",
|
||||
},
|
||||
// Rounded
|
||||
rounded: {
|
||||
topLeft: "╭",
|
||||
topRight: "╮",
|
||||
bottomLeft: "╰",
|
||||
bottomRight: "╯",
|
||||
horizontal: "─",
|
||||
vertical: "│",
|
||||
leftT: "├",
|
||||
rightT: "┤",
|
||||
topT: "┬",
|
||||
bottomT: "┴",
|
||||
cross: "┼",
|
||||
},
|
||||
// Bold
|
||||
bold: {
|
||||
topLeft: "┏",
|
||||
topRight: "┓",
|
||||
bottomLeft: "┗",
|
||||
bottomRight: "┛",
|
||||
horizontal: "━",
|
||||
vertical: "┃",
|
||||
leftT: "┣",
|
||||
rightT: "┫",
|
||||
topT: "┳",
|
||||
bottomT: "┻",
|
||||
cross: "╋",
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Default box options
|
||||
export const BOX_DEFAULTS = {
|
||||
style: "rounded" as const,
|
||||
padding: 1,
|
||||
align: "left" as const,
|
||||
} as const;
|
||||
|
||||
// Tool icon mapping
|
||||
export const TOOL_ICONS = {
|
||||
bash: "bash",
|
||||
read: "read",
|
||||
write: "write",
|
||||
edit: "edit",
|
||||
default: "default",
|
||||
} as const;
|
||||
|
||||
// State color mapping
|
||||
export const STATE_COLORS = {
|
||||
pending: "DIM",
|
||||
running: "primary",
|
||||
success: "success",
|
||||
error: "error",
|
||||
} as const;
|
||||
|
||||
// Role configuration for message display
|
||||
export const ROLE_CONFIG = {
|
||||
user: { label: "You", colorKey: "primary" },
|
||||
assistant: { label: "CodeTyper", colorKey: "success" },
|
||||
system: { label: "System", colorKey: "textMuted" },
|
||||
tool: { label: "Tool", colorKey: "warning" },
|
||||
} as const;
|
||||
|
||||
// Status indicator configuration
|
||||
export const STATUS_INDICATORS = {
|
||||
success: { iconKey: "success", colorKey: "success" },
|
||||
error: { iconKey: "error", colorKey: "error" },
|
||||
warning: { iconKey: "warning", colorKey: "warning" },
|
||||
info: { iconKey: "info", colorKey: "info" },
|
||||
pending: { iconKey: "pending", colorKey: "textMuted" },
|
||||
running: { iconKey: "running", colorKey: "primary" },
|
||||
} as const;
|
||||
|
||||
// Tool call icon configuration
|
||||
export const TOOL_CALL_ICONS = {
|
||||
bash: { iconKey: "bash", colorKey: "warning" },
|
||||
read: { iconKey: "read", colorKey: "info" },
|
||||
write: { iconKey: "write", colorKey: "success" },
|
||||
edit: { iconKey: "edit", colorKey: "primary" },
|
||||
default: { iconKey: "gear", colorKey: "textMuted" },
|
||||
} as const;
|
||||
213
src/constants/copilot.ts
Normal file
213
src/constants/copilot.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import type { ProviderModel, ProviderName } from "@/types/providers";
|
||||
|
||||
// Provider identification
|
||||
export const COPILOT_PROVIDER_NAME: ProviderName = "copilot";
|
||||
export const COPILOT_DISPLAY_NAME = "GitHub Copilot";
|
||||
|
||||
// GitHub Copilot API endpoints
|
||||
export const COPILOT_AUTH_URL =
|
||||
"https://api.github.com/copilot_internal/v2/token";
|
||||
export const COPILOT_MODELS_URL = "https://api.githubcopilot.com/models";
|
||||
|
||||
// GitHub OAuth endpoints for device flow
|
||||
export const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
||||
export const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
|
||||
export const GITHUB_ACCESS_TOKEN_URL =
|
||||
"https://github.com/login/oauth/access_token";
|
||||
|
||||
// Cache and retry configuration
|
||||
export const COPILOT_MODELS_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
export const COPILOT_MAX_RETRIES = 3;
|
||||
export const COPILOT_INITIAL_RETRY_DELAY = 1000; // 1 second
|
||||
|
||||
// Default model
|
||||
export const COPILOT_DEFAULT_MODEL = "gpt-5-mini";
|
||||
|
||||
// Unlimited fallback model (used when quota is exceeded)
|
||||
export const COPILOT_UNLIMITED_MODEL = "gpt-4o";
|
||||
|
||||
// Copilot messages
|
||||
export const COPILOT_MESSAGES = {
|
||||
QUOTA_EXCEEDED_SWITCHING: (from: string, to: string) =>
|
||||
`Quota exceeded for ${from}. Switching to unlimited model: ${to}`,
|
||||
MODEL_SWITCHED: (from: string, to: string) =>
|
||||
`Model switched: ${from} → ${to} (quota exceeded)`,
|
||||
FORMAT_MULTIPLIER: (multiplier: number) =>
|
||||
multiplier === 0 ? "Unlimited" : `${multiplier}x`,
|
||||
} as const;
|
||||
|
||||
// Model cost multipliers from GitHub Copilot
|
||||
// 0x = Unlimited (no premium request usage)
|
||||
// Lower multiplier = cheaper, Higher multiplier = more expensive
|
||||
export const MODEL_COST_MULTIPLIERS: Record<string, number> = {
|
||||
// Unlimited models (0x)
|
||||
"gpt-4o": 0,
|
||||
"gpt-4o-mini": 0,
|
||||
"gpt-5-mini": 0,
|
||||
"grok-code-fast-1": 0,
|
||||
"raptor-mini": 0,
|
||||
|
||||
// Low cost models (0.33x)
|
||||
"claude-haiku-4.5": 0.33,
|
||||
"gemini-3-flash-preview": 0.33,
|
||||
"gpt-5.1-codex-mini-preview": 0.33,
|
||||
|
||||
// Standard cost models (1.0x)
|
||||
"claude-sonnet-4": 1.0,
|
||||
"claude-sonnet-4.5": 1.0,
|
||||
"gemini-2.5-pro": 1.0,
|
||||
"gemini-3-pro-preview": 1.0,
|
||||
"gpt-4.1": 1.0,
|
||||
"gpt-5": 1.0,
|
||||
"gpt-5-codex-preview": 1.0,
|
||||
"gpt-5.1": 1.0,
|
||||
"gpt-5.1-codex": 1.0,
|
||||
"gpt-5.1-codex-max": 1.0,
|
||||
"gpt-5.2": 1.0,
|
||||
"gpt-5.2-codex": 1.0,
|
||||
|
||||
// Premium models (3.0x)
|
||||
"claude-opus-4.5": 3.0,
|
||||
};
|
||||
|
||||
// Models that are unlimited (0x cost multiplier)
|
||||
export const UNLIMITED_MODELS = new Set([
|
||||
"gpt-4o",
|
||||
"gpt-4o-mini",
|
||||
"gpt-5-mini",
|
||||
"grok-code-fast-1",
|
||||
"raptor-mini",
|
||||
]);
|
||||
|
||||
// Model context sizes (input tokens, output tokens)
|
||||
export interface ModelContextSize {
|
||||
input: number;
|
||||
output: number;
|
||||
}
|
||||
|
||||
export const MODEL_CONTEXT_SIZES: Record<string, ModelContextSize> = {
|
||||
// Claude models
|
||||
"claude-haiku-4.5": { input: 128000, output: 16000 },
|
||||
"claude-opus-4.5": { input: 128000, output: 16000 },
|
||||
"claude-sonnet-4": { input: 128000, output: 16000 },
|
||||
"claude-sonnet-4.5": { input: 128000, output: 16000 },
|
||||
|
||||
// Gemini models
|
||||
"gemini-2.5-pro": { input: 109000, output: 64000 },
|
||||
"gemini-3-flash-preview": { input: 109000, output: 64000 },
|
||||
"gemini-3-pro-preview": { input: 109000, output: 64000 },
|
||||
|
||||
// GPT-4 models
|
||||
"gpt-4.1": { input: 111000, output: 16000 },
|
||||
"gpt-4o": { input: 64000, output: 4000 },
|
||||
|
||||
// GPT-5 models
|
||||
"gpt-5": { input: 128000, output: 128000 },
|
||||
"gpt-5-mini": { input: 128000, output: 64000 },
|
||||
"gpt-5-codex-preview": { input: 128000, output: 128000 },
|
||||
"gpt-5.1": { input: 128000, output: 64000 },
|
||||
"gpt-5.1-codex": { input: 128000, output: 128000 },
|
||||
"gpt-5.1-codex-max": { input: 128000, output: 128000 },
|
||||
"gpt-5.1-codex-mini-preview": { input: 128000, output: 128000 },
|
||||
"gpt-5.2": { input: 128000, output: 64000 },
|
||||
"gpt-5.2-codex": { input: 272000, output: 128000 },
|
||||
|
||||
// Other models
|
||||
"grok-code-fast-1": { input: 109000, output: 64000 },
|
||||
"raptor-mini": { input: 200000, output: 64000 },
|
||||
};
|
||||
|
||||
// Default context size for unknown models
|
||||
export const DEFAULT_CONTEXT_SIZE: ModelContextSize = {
|
||||
input: 128000,
|
||||
output: 16000,
|
||||
};
|
||||
|
||||
// Get context size for a model
|
||||
export const getModelContextSize = (modelId: string): ModelContextSize =>
|
||||
MODEL_CONTEXT_SIZES[modelId] ?? DEFAULT_CONTEXT_SIZE;
|
||||
|
||||
// Fallback models when API is unavailable
|
||||
export const COPILOT_FALLBACK_MODELS: ProviderModel[] = [
|
||||
{
|
||||
id: "gpt-4o",
|
||||
name: "GPT-4o",
|
||||
maxTokens: 4000,
|
||||
supportsTools: true,
|
||||
supportsStreaming: true,
|
||||
costMultiplier: 0,
|
||||
isUnlimited: true,
|
||||
},
|
||||
{
|
||||
id: "gpt-5-mini",
|
||||
name: "GPT-5 mini",
|
||||
maxTokens: 64000,
|
||||
supportsTools: true,
|
||||
supportsStreaming: true,
|
||||
costMultiplier: 0,
|
||||
isUnlimited: true,
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-4",
|
||||
name: "Claude Sonnet 4",
|
||||
maxTokens: 16000,
|
||||
supportsTools: true,
|
||||
supportsStreaming: true,
|
||||
costMultiplier: 1.0,
|
||||
isUnlimited: false,
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-4.5",
|
||||
name: "Claude Sonnet 4.5",
|
||||
maxTokens: 16000,
|
||||
supportsTools: true,
|
||||
supportsStreaming: true,
|
||||
costMultiplier: 1.0,
|
||||
isUnlimited: false,
|
||||
},
|
||||
{
|
||||
id: "claude-opus-4.5",
|
||||
name: "Claude Opus 4.5",
|
||||
maxTokens: 16000,
|
||||
supportsTools: true,
|
||||
supportsStreaming: true,
|
||||
costMultiplier: 3.0,
|
||||
isUnlimited: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-4.1",
|
||||
name: "GPT-4.1",
|
||||
maxTokens: 16000,
|
||||
supportsTools: true,
|
||||
supportsStreaming: true,
|
||||
costMultiplier: 1.0,
|
||||
isUnlimited: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5",
|
||||
name: "GPT-5",
|
||||
maxTokens: 128000,
|
||||
supportsTools: true,
|
||||
supportsStreaming: true,
|
||||
costMultiplier: 1.0,
|
||||
isUnlimited: false,
|
||||
},
|
||||
{
|
||||
id: "gemini-2.5-pro",
|
||||
name: "Gemini 2.5 Pro",
|
||||
maxTokens: 64000,
|
||||
supportsTools: true,
|
||||
supportsStreaming: true,
|
||||
costMultiplier: 1.0,
|
||||
isUnlimited: false,
|
||||
},
|
||||
{
|
||||
id: "grok-code-fast-1",
|
||||
name: "Grok Code Fast 1",
|
||||
maxTokens: 64000,
|
||||
supportsTools: true,
|
||||
supportsStreaming: true,
|
||||
costMultiplier: 0,
|
||||
isUnlimited: true,
|
||||
},
|
||||
];
|
||||
42
src/constants/dashboard.ts
Normal file
42
src/constants/dashboard.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Dashboard Constants
|
||||
*/
|
||||
|
||||
export const DASHBOARD_TITLE = "CodeTyper";
|
||||
|
||||
export const DASHBOARD_LAYOUT = {
|
||||
DEFAULT_WIDTH: 120,
|
||||
CONTENT_HEIGHT: 15,
|
||||
LEFT_COLUMN_RATIO: 0.35,
|
||||
PADDING: 3,
|
||||
} as const;
|
||||
|
||||
export const DASHBOARD_LOGO = [
|
||||
" ██████╗███████╗",
|
||||
" ██╔════╝██╔════╝",
|
||||
" ██║ ███████╗",
|
||||
" ██║ ╚════██║",
|
||||
" ╚██████╗███████║",
|
||||
" ╚═════╝╚══════╝",
|
||||
] as const;
|
||||
|
||||
export const DASHBOARD_COMMANDS = [
|
||||
{ command: "codetyper chat", description: "Start interactive chat" },
|
||||
{ command: "codetyper run <task>", description: "Execute autonomous task" },
|
||||
{ command: "/help", description: "Show all commands in chat" },
|
||||
] as const;
|
||||
|
||||
export const DASHBOARD_QUICK_COMMANDS = [
|
||||
{ command: "codetyper chat", description: "Start interactive chat" },
|
||||
{ command: "codetyper run", description: "Execute autonomous task" },
|
||||
{ command: "codetyper --help", description: "Show all commands" },
|
||||
] as const;
|
||||
|
||||
export const DASHBOARD_BORDER = {
|
||||
TOP_LEFT: "╭",
|
||||
TOP_RIGHT: "╮",
|
||||
BOTTOM_LEFT: "╰",
|
||||
BOTTOM_RIGHT: "╯",
|
||||
HORIZONTAL: "─",
|
||||
VERTICAL: "│",
|
||||
} as const;
|
||||
13
src/constants/diff.ts
Normal file
13
src/constants/diff.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Diff utility constants
|
||||
*/
|
||||
|
||||
// Default context lines for hunks
|
||||
export const DIFF_CONTEXT_LINES = 3;
|
||||
|
||||
// Line type prefixes
|
||||
export const LINE_PREFIXES = {
|
||||
add: "+",
|
||||
remove: "-",
|
||||
context: " ",
|
||||
} as const;
|
||||
25
src/constants/edit.ts
Normal file
25
src/constants/edit.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Edit tool constants
|
||||
*/
|
||||
|
||||
export const EDIT_MESSAGES = {
|
||||
NOT_FOUND:
|
||||
"Could not find the text to replace. Make sure old_string matches exactly.",
|
||||
MULTIPLE_OCCURRENCES: (count: number) =>
|
||||
`old_string appears ${count} times. Use replace_all=true or provide more context to make it unique.`,
|
||||
PERMISSION_DENIED: "Permission denied by user",
|
||||
} as const;
|
||||
|
||||
export const EDIT_TITLES = {
|
||||
FAILED: (path: string) => `Edit failed: ${path}`,
|
||||
CANCELLED: (path: string) => `Edit cancelled: ${path}`,
|
||||
SUCCESS: (path: string) => `Edited: ${path}`,
|
||||
EDITING: (name: string) => `Editing ${name}`,
|
||||
} as const;
|
||||
|
||||
export const EDIT_DESCRIPTION = `Edit a file by replacing specific text. The old_string must match exactly.
|
||||
|
||||
Guidelines:
|
||||
- old_string must be unique in the file (or use replace_all)
|
||||
- Preserve indentation exactly as it appears in the file
|
||||
- Requires user approval for edits`;
|
||||
30
src/constants/embeddings.ts
Normal file
30
src/constants/embeddings.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Embedding Constants
|
||||
*
|
||||
* Configuration for semantic learning retrieval
|
||||
*/
|
||||
|
||||
export const EMBEDDING_DEFAULTS = {
|
||||
MODEL: "nomic-embed-text",
|
||||
FALLBACK_MODEL: "all-minilm",
|
||||
DIMENSIONS: 768,
|
||||
} as const;
|
||||
|
||||
export const EMBEDDING_ENDPOINTS = {
|
||||
EMBED: "/api/embed",
|
||||
} as const;
|
||||
|
||||
export const EMBEDDING_TIMEOUTS = {
|
||||
EMBED: 30000,
|
||||
} as const;
|
||||
|
||||
export const EMBEDDING_SEARCH = {
|
||||
TOP_K: 10,
|
||||
MIN_SIMILARITY: 0.3,
|
||||
CACHE_TTL_MS: 300000, // 5 minutes
|
||||
} as const;
|
||||
|
||||
export const EMBEDDING_STORAGE = {
|
||||
INDEX_FILE: "embeddings.json",
|
||||
VERSION: 1,
|
||||
} as const;
|
||||
123
src/constants/file-picker.ts
Normal file
123
src/constants/file-picker.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* File Picker constants
|
||||
*/
|
||||
|
||||
export const IGNORED_PATTERNS = [
|
||||
// Version control
|
||||
".git",
|
||||
".svn",
|
||||
".hg",
|
||||
// AI/Code assistants
|
||||
".claude",
|
||||
".coder",
|
||||
".codetyper",
|
||||
".cursor",
|
||||
".copilot",
|
||||
".aider",
|
||||
// Build outputs / binaries
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
"bin",
|
||||
"obj",
|
||||
"target",
|
||||
".next",
|
||||
".nuxt",
|
||||
".output",
|
||||
"out",
|
||||
// Cache directories
|
||||
".cache",
|
||||
".turbo",
|
||||
".parcel-cache",
|
||||
".vite",
|
||||
// Test/Coverage
|
||||
"coverage",
|
||||
".nyc_output",
|
||||
// Python
|
||||
"__pycache__",
|
||||
".venv",
|
||||
"venv",
|
||||
".env",
|
||||
// OS files
|
||||
".DS_Store",
|
||||
"thumbs.db",
|
||||
// IDE/Editor
|
||||
".idea",
|
||||
".vscode",
|
||||
// Misc
|
||||
".terraform",
|
||||
".serverless",
|
||||
] as const;
|
||||
|
||||
export const BINARY_EXTENSIONS = [
|
||||
// Executables
|
||||
".exe",
|
||||
".dll",
|
||||
".so",
|
||||
".dylib",
|
||||
".bin",
|
||||
".app",
|
||||
// Images
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".bmp",
|
||||
".ico",
|
||||
".webp",
|
||||
".svg",
|
||||
".tiff",
|
||||
// Audio/Video
|
||||
".mp3",
|
||||
".mp4",
|
||||
".wav",
|
||||
".avi",
|
||||
".mov",
|
||||
".mkv",
|
||||
".flac",
|
||||
".ogg",
|
||||
// Archives
|
||||
".zip",
|
||||
".tar",
|
||||
".gz",
|
||||
".rar",
|
||||
".7z",
|
||||
".bz2",
|
||||
// Documents
|
||||
".pdf",
|
||||
".doc",
|
||||
".docx",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".ppt",
|
||||
".pptx",
|
||||
// Fonts
|
||||
".ttf",
|
||||
".otf",
|
||||
".woff",
|
||||
".woff2",
|
||||
".eot",
|
||||
// Database
|
||||
".db",
|
||||
".sqlite",
|
||||
".sqlite3",
|
||||
// Other binary
|
||||
".pyc",
|
||||
".pyo",
|
||||
".class",
|
||||
".o",
|
||||
".a",
|
||||
".lib",
|
||||
".node",
|
||||
".wasm",
|
||||
] as const;
|
||||
|
||||
export type BinaryExtension = (typeof BINARY_EXTENSIONS)[number];
|
||||
|
||||
export type IgnoredPattern = (typeof IGNORED_PATTERNS)[number];
|
||||
|
||||
export const FILE_PICKER_DEFAULTS = {
|
||||
MAX_DEPTH: 2,
|
||||
MAX_RESULTS: 15,
|
||||
INITIAL_DEPTH: 0,
|
||||
} as const;
|
||||
2
src/constants/files.ts
Normal file
2
src/constants/files.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// File-related constants
|
||||
// MAX_FILE_SIZE, etc.
|
||||
1
src/constants/general.ts
Normal file
1
src/constants/general.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const PROVIDER_NAME_COPILOT = "copilot";
|
||||
31
src/constants/github-issue.ts
Normal file
31
src/constants/github-issue.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* GitHub Issue constants
|
||||
*/
|
||||
|
||||
export const ISSUE_PATTERNS = [
|
||||
/\bissue\s*#?(\d+)\b/gi,
|
||||
/\bfix\s+#(\d+)\b/gi,
|
||||
/\bclose\s+#(\d+)\b/gi,
|
||||
/\bresolve\s+#(\d+)\b/gi,
|
||||
/(?<!\w)#(\d+)(?!\w)/g,
|
||||
] as const;
|
||||
|
||||
export const GITHUB_ISSUE_DEFAULTS = {
|
||||
MAX_ISSUE_NUMBER: 100000,
|
||||
MIN_ISSUE_NUMBER: 1,
|
||||
} as const;
|
||||
|
||||
export const GITHUB_ISSUE_MESSAGES = {
|
||||
CONTEXT_HEADER: "The user is referencing the following GitHub issue(s):",
|
||||
SECTION_SEPARATOR: "\n\n---\n\n",
|
||||
USER_REQUEST_PREFIX: "User request: ",
|
||||
UNKNOWN_AUTHOR: "unknown",
|
||||
} as const;
|
||||
|
||||
export const GH_CLI_COMMANDS = {
|
||||
GET_REMOTE_URL: "git remote get-url origin 2>/dev/null",
|
||||
VIEW_ISSUE: (issueNumber: number) =>
|
||||
`gh issue view ${issueNumber} --json number,title,state,body,author,labels,url 2>/dev/null`,
|
||||
} as const;
|
||||
|
||||
export const GITHUB_REMOTE_IDENTIFIER = "github.com";
|
||||
58
src/constants/glob.ts
Normal file
58
src/constants/glob.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Glob tool constants
|
||||
*/
|
||||
|
||||
export const GLOB_DEFAULTS = {
|
||||
DOT: false,
|
||||
ONLY_FILES: true,
|
||||
ONLY_DIRECTORIES: false,
|
||||
} as const;
|
||||
|
||||
export const GLOB_IGNORE_PATTERNS = [
|
||||
// Version control
|
||||
"**/.git/**",
|
||||
"**/.svn/**",
|
||||
"**/.hg/**",
|
||||
// AI/Code assistants
|
||||
"**/.claude/**",
|
||||
"**/.coder/**",
|
||||
"**/.codetyper/**",
|
||||
"**/.cursor/**",
|
||||
"**/.copilot/**",
|
||||
"**/.aider/**",
|
||||
// Build outputs / binaries
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/build/**",
|
||||
"**/bin/**",
|
||||
"**/obj/**",
|
||||
"**/target/**",
|
||||
"**/.next/**",
|
||||
"**/.nuxt/**",
|
||||
"**/.output/**",
|
||||
"**/out/**",
|
||||
// Cache directories
|
||||
"**/.cache/**",
|
||||
"**/.turbo/**",
|
||||
"**/.parcel-cache/**",
|
||||
"**/.vite/**",
|
||||
// Test/Coverage
|
||||
"**/coverage/**",
|
||||
"**/.nyc_output/**",
|
||||
// Python
|
||||
"**/__pycache__/**",
|
||||
"**/.venv/**",
|
||||
"**/venv/**",
|
||||
"**/.env/**",
|
||||
// IDE/Editor
|
||||
"**/.idea/**",
|
||||
"**/.vscode/**",
|
||||
// Misc
|
||||
"**/.terraform/**",
|
||||
"**/.serverless/**",
|
||||
] as const;
|
||||
|
||||
export const GLOB_MESSAGES = {
|
||||
FAILED: (error: unknown) => `Glob failed: ${error}`,
|
||||
LIST_FAILED: (error: unknown) => `List failed: ${error}`,
|
||||
} as const;
|
||||
28
src/constants/grep.ts
Normal file
28
src/constants/grep.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Grep tool constants
|
||||
*/
|
||||
|
||||
export const GREP_DEFAULTS = {
|
||||
MAX_RESULTS: 100,
|
||||
DEFAULT_PATTERN: "**/*",
|
||||
NO_MATCHES_EXIT_CODE: 1,
|
||||
} as const;
|
||||
|
||||
export const GREP_IGNORE_PATTERNS = [
|
||||
"**/node_modules/**",
|
||||
"**/.git/**",
|
||||
"**/dist/**",
|
||||
"**/build/**",
|
||||
"**/.next/**",
|
||||
] as const;
|
||||
|
||||
export const GREP_MESSAGES = {
|
||||
NO_MATCHES: "No matches found",
|
||||
SEARCH_FAILED: (error: unknown) => `Search failed: ${error}`,
|
||||
RIPGREP_FAILED: (message: string) => `ripgrep failed: ${message}`,
|
||||
} as const;
|
||||
|
||||
export const GREP_COMMANDS = {
|
||||
RIPGREP: (pattern: string, directory: string) =>
|
||||
`rg --line-number --no-heading "${pattern}" "${directory}"`,
|
||||
} as const;
|
||||
45
src/constants/handlers.ts
Normal file
45
src/constants/handlers.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Constants for command handlers
|
||||
*/
|
||||
|
||||
import type { ConfigKey, ConfigAction } from "@/types/handlers";
|
||||
import type { Provider } from "@/types/index";
|
||||
|
||||
export const VALID_CONFIG_KEYS: readonly ConfigKey[] = [
|
||||
"provider",
|
||||
"model",
|
||||
"maxIterations",
|
||||
"timeout",
|
||||
] as const;
|
||||
|
||||
export const VALID_PROVIDERS: readonly Provider[] = [
|
||||
"copilot",
|
||||
"ollama",
|
||||
] as const;
|
||||
|
||||
export const VALID_CONFIG_ACTIONS: readonly ConfigAction[] = [
|
||||
"show",
|
||||
"path",
|
||||
"set",
|
||||
] as const;
|
||||
|
||||
export const CONFIG_VALIDATION = {
|
||||
MIN_TIMEOUT_MS: 1000,
|
||||
MIN_ITERATIONS: 1,
|
||||
} as const;
|
||||
|
||||
export const INTENT_KEYWORDS = {
|
||||
fix: ["fix", "bug"],
|
||||
test: ["test", "spec"],
|
||||
refactor: ["refactor", "improve"],
|
||||
code: ["add", "implement"],
|
||||
document: ["document", "comment"],
|
||||
} as const;
|
||||
|
||||
export const CLASSIFICATION_CONFIDENCE = {
|
||||
HIGH: 0.9,
|
||||
MEDIUM: 0.85,
|
||||
DEFAULT: 0.8,
|
||||
LOW: 0.75,
|
||||
THRESHOLD: 0.7,
|
||||
} as const;
|
||||
20
src/constants/help-commands.ts
Normal file
20
src/constants/help-commands.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const HELP_COMMANDS: [string, string][] = [
|
||||
["/help, /h", "Show this help message"],
|
||||
["/clear, /c", "Clear conversation history"],
|
||||
["/files, /f", "List files in context"],
|
||||
["/remove <file>, /rm", "Remove file from context"],
|
||||
["/context", "Show current context size"],
|
||||
["/compact", "Compact conversation history"],
|
||||
["/history", "Show conversation history"],
|
||||
["/models, /m", "Show available models"],
|
||||
["/model <name>", "Switch to a different model"],
|
||||
["/providers, /p", "Show all providers status"],
|
||||
["/provider <name>", "Switch to a different provider"],
|
||||
["/agent, /a", "Select agent"],
|
||||
["/usage, /u", "Show token usage statistics"],
|
||||
["/mcp [cmd]", "MCP server status/connect/disconnect/tools"],
|
||||
["/session", "Show current session info"],
|
||||
["/sessions", "List all saved sessions"],
|
||||
["/save, /s", "Save current session"],
|
||||
["/exit, /quit, /q", "Exit chat"],
|
||||
];
|
||||
35
src/constants/home-screen.ts
Normal file
35
src/constants/home-screen.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Home Screen Constants
|
||||
* Constants for the welcome/home screen TUI layout
|
||||
*/
|
||||
|
||||
/** Layout constants for home screen */
|
||||
export const HOME_LAYOUT = {
|
||||
maxWidth: 75,
|
||||
topPadding: 3,
|
||||
bottomPadding: 2,
|
||||
horizontalPadding: 2,
|
||||
logoGap: 1,
|
||||
} as const;
|
||||
|
||||
/** Input placeholders shown in the prompt box */
|
||||
export const PLACEHOLDERS = [
|
||||
"Fix a TODO in the codebase",
|
||||
"What is the tech stack of this project?",
|
||||
"Fix broken tests",
|
||||
"Explain how this function works",
|
||||
"Refactor this code for readability",
|
||||
"Add error handling to this function",
|
||||
];
|
||||
|
||||
/** Keyboard hints displayed below the prompt box */
|
||||
export const KEYBOARD_HINTS = {
|
||||
agents: { key: "tab", label: "agents" },
|
||||
commands: { key: "ctrl+p", label: "commands" },
|
||||
} as const;
|
||||
|
||||
/** MCP status indicators */
|
||||
export const MCP_INDICATORS = {
|
||||
connected: "⊙",
|
||||
error: "⊙",
|
||||
} as const;
|
||||
4
src/constants/home.ts
Normal file
4
src/constants/home.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const HOME_VARS = {
|
||||
title: "Welcome to CodeTyper - Your AI Coding Assistant",
|
||||
subTitle: "Type a prompt below to start a new session",
|
||||
};
|
||||
30
src/constants/input-editor.ts
Normal file
30
src/constants/input-editor.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Input editor constants
|
||||
*/
|
||||
|
||||
// Default prompts
|
||||
export const INPUT_EDITOR_DEFAULTS = {
|
||||
prompt: "\x1b[36m> \x1b[0m",
|
||||
continuationPrompt: "\x1b[90m│ \x1b[0m",
|
||||
} as const;
|
||||
|
||||
// ANSI escape sequences
|
||||
export const ANSI = {
|
||||
hideCursor: "\x1b[?25l",
|
||||
showCursor: "\x1b[?25h",
|
||||
clearLine: "\x1b[2K",
|
||||
moveUp: (n: number) => `\x1b[${n}A`,
|
||||
moveDown: (n: number) => `\x1b[${n}B`,
|
||||
moveRight: (n: number) => `\x1b[${n}C`,
|
||||
carriageReturn: "\r",
|
||||
} as const;
|
||||
|
||||
// Special key sequences for Alt+Enter
|
||||
export const ALT_ENTER_SEQUENCES = ["\x1b\r", "\x1b\n"] as const;
|
||||
|
||||
// Pasted text styling
|
||||
export const PASTE_STYLE = {
|
||||
// Gray/dim style for pasted text placeholder
|
||||
start: "\x1b[90m",
|
||||
end: "\x1b[0m",
|
||||
} as const;
|
||||
96
src/constants/learning.ts
Normal file
96
src/constants/learning.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Learning Service constants
|
||||
*/
|
||||
|
||||
import type { LearningCategory } from "@/types/learning";
|
||||
|
||||
export const LEARNING_PATTERNS = [
|
||||
// User preferences
|
||||
/always use (\w+)/i,
|
||||
/prefer (\w+) over (\w+)/i,
|
||||
/use (\.\w+) files?/i,
|
||||
/don't use (\w+)/i,
|
||||
/never use (\w+)/i,
|
||||
/code in (\w+)/i,
|
||||
/write in (\w+)/i,
|
||||
|
||||
// Project structure
|
||||
/put .+ in (.+) directory/i,
|
||||
/files? should be in (.+)/i,
|
||||
/follow (.+) pattern/i,
|
||||
/use (.+) architecture/i,
|
||||
|
||||
// Coding style
|
||||
/use (.+) naming convention/i,
|
||||
/follow (.+) style/i,
|
||||
/indent with (\w+)/i,
|
||||
/use (single|double) quotes/i,
|
||||
|
||||
// Testing
|
||||
/use (.+) for testing/i,
|
||||
/tests? should (.+)/i,
|
||||
|
||||
// Dependencies
|
||||
/use (.+) library/i,
|
||||
/prefer (.+) package/i,
|
||||
] as const;
|
||||
|
||||
export const LEARNING_KEYWORDS = [
|
||||
"always",
|
||||
"never",
|
||||
"prefer",
|
||||
"convention",
|
||||
"standard",
|
||||
"pattern",
|
||||
"rule",
|
||||
"style",
|
||||
"remember",
|
||||
"important",
|
||||
"must",
|
||||
"should",
|
||||
] as const;
|
||||
|
||||
export const ACKNOWLEDGMENT_PATTERNS = [
|
||||
/i('ll| will) (use|follow|apply) (.+)/i,
|
||||
/using (.+) as (you|per your) (requested|preference)/i,
|
||||
/following (.+) (convention|pattern|style)/i,
|
||||
/noted.+ (will|going to) (.+)/i,
|
||||
] as const;
|
||||
|
||||
export const ACKNOWLEDGMENT_PHRASES = [
|
||||
"i understand",
|
||||
"got it",
|
||||
"noted",
|
||||
] as const;
|
||||
|
||||
export const LEARNING_DEFAULTS = {
|
||||
BASE_PATTERN_CONFIDENCE: 0.7,
|
||||
BASE_KEYWORD_CONFIDENCE: 0.5,
|
||||
KEYWORD_CONFIDENCE_INCREMENT: 0.1,
|
||||
ACKNOWLEDGMENT_CONFIDENCE: 0.8,
|
||||
CONFIDENCE_BOOST: 0.2,
|
||||
MAX_CONFIDENCE: 1.0,
|
||||
MIN_KEYWORDS_FOR_LEARNING: 2,
|
||||
MAX_CONTENT_LENGTH: 80,
|
||||
TRUNCATE_LENGTH: 77,
|
||||
MAX_SLICE_LENGTH: 100,
|
||||
} as const;
|
||||
|
||||
export const LEARNING_CONTEXTS = {
|
||||
USER_PREFERENCE: "User preference",
|
||||
CONVENTION_IDENTIFIED: "Convention identified",
|
||||
MULTIPLE_INDICATORS: "Multiple preference indicators",
|
||||
CONVENTION_CONFIRMED: "Convention confirmed by assistant",
|
||||
PREFERENCE_ACKNOWLEDGED: "Preference acknowledged by assistant",
|
||||
} as const;
|
||||
|
||||
export const CATEGORY_PATTERNS: Record<string, LearningCategory> = {
|
||||
prefer: "preference",
|
||||
use: "preference",
|
||||
directory: "architecture",
|
||||
architecture: "architecture",
|
||||
style: "style",
|
||||
naming: "style",
|
||||
indent: "style",
|
||||
test: "testing",
|
||||
};
|
||||
28
src/constants/login.ts
Normal file
28
src/constants/login.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Login flow constants and messages
|
||||
*/
|
||||
|
||||
export const LOGIN_MESSAGES = {
|
||||
COPILOT_ALREADY_CONFIGURED: "✓ Copilot is already configured and working!",
|
||||
COPILOT_STARTING_AUTH: "\nStarting GitHub device flow authentication...\n",
|
||||
COPILOT_AUTH_INSTRUCTIONS: "To authenticate with GitHub Copilot:\n",
|
||||
COPILOT_WAITING: "Waiting for authentication (press Ctrl+C to cancel)...\n",
|
||||
COPILOT_SUCCESS: "\n✓ GitHub Copilot authenticated successfully!",
|
||||
OLLAMA_SUCCESS: "\n✓ Connected to Ollama!",
|
||||
OLLAMA_NO_MODELS: "\nNo models found. Pull a model with: ollama pull <model>",
|
||||
AVAILABLE_MODELS: "\nAvailable models:",
|
||||
VALIDATION_FAILED: "\n✗ Validation failed:",
|
||||
AUTH_FAILED: "\n✗ Authentication failed:",
|
||||
CONNECTION_FAILED: "\n✗ Failed to connect:",
|
||||
UNKNOWN_PROVIDER: "Unknown provider:",
|
||||
} as const;
|
||||
|
||||
export const LOGIN_PROMPTS = {
|
||||
RECONFIGURE: "Do you want to re-authenticate?",
|
||||
OLLAMA_HOST: "Ollama host URL:",
|
||||
} as const;
|
||||
|
||||
export const AUTH_STEP_PREFIXES = {
|
||||
OPEN_URL: " 1. Open:",
|
||||
ENTER_CODE: " 2. Enter code:",
|
||||
} as const;
|
||||
41
src/constants/mouse-handler.ts
Normal file
41
src/constants/mouse-handler.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Mouse Handler Constants
|
||||
*
|
||||
* Constants for terminal mouse event handling
|
||||
*/
|
||||
|
||||
// Mouse event button codes for SGR encoding
|
||||
export const MOUSE_WHEEL_CODES = {
|
||||
UP: 64,
|
||||
DOWN: 65,
|
||||
} as const;
|
||||
|
||||
// Default scroll lines per wheel event
|
||||
export const MOUSE_SCROLL_LINES = 3;
|
||||
|
||||
// SGR mouse sequence pattern: \x1b[<Cb;Cx;Cy(M|m)
|
||||
export const SGR_MOUSE_REGEX = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
||||
|
||||
// X10 mouse sequence pattern: \x1b[M followed by 3 bytes
|
||||
export const X10_MOUSE_REGEX = /\x1b\[M[\x20-\xff]{3}/g;
|
||||
|
||||
// Partial/incomplete sequence patterns for cleanup
|
||||
export const PARTIAL_SGR_REGEX = /\x1b\[<[\d;]*$/;
|
||||
export const PARTIAL_X10_REGEX = /\x1b\[M.{0,2}$/;
|
||||
|
||||
// Mouse tracking escape sequences
|
||||
export const MOUSE_TRACKING_SEQUENCES = {
|
||||
ENABLE_BUTTON: "\x1b[?1000h",
|
||||
ENABLE_SGR: "\x1b[?1006h",
|
||||
DISABLE_SGR: "\x1b[?1006l",
|
||||
DISABLE_BUTTON: "\x1b[?1000l",
|
||||
} as const;
|
||||
|
||||
// Scroll direction type
|
||||
export type MouseScrollDirection = "up" | "down";
|
||||
|
||||
// Button code to scroll direction mapping
|
||||
export const MOUSE_BUTTON_TO_SCROLL: Record<number, MouseScrollDirection> = {
|
||||
[MOUSE_WHEEL_CODES.UP]: "up",
|
||||
[MOUSE_WHEEL_CODES.DOWN]: "down",
|
||||
} as const;
|
||||
38
src/constants/mouse-scroll.ts
Normal file
38
src/constants/mouse-scroll.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Mouse Scroll Constants
|
||||
*
|
||||
* Terminal escape sequences for mouse mode handling
|
||||
*/
|
||||
|
||||
// Mouse mode enable/disable escape sequences
|
||||
export const MOUSE_ESCAPE_SEQUENCES = {
|
||||
ENABLE: "\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h",
|
||||
DISABLE: "\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l",
|
||||
} as const;
|
||||
|
||||
// Mouse button codes
|
||||
export const MOUSE_BUTTON_CODES = {
|
||||
SGR_SCROLL_UP: 64,
|
||||
SGR_SCROLL_DOWN: 65,
|
||||
X10_SCROLL_UP: 96, // 32 + 64
|
||||
X10_SCROLL_DOWN: 97, // 32 + 65
|
||||
} as const;
|
||||
|
||||
// SGR mouse mode regex pattern
|
||||
export const SGR_MOUSE_PATTERN = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/;
|
||||
|
||||
// X10/Normal mouse mode prefix
|
||||
export const X10_MOUSE_PREFIX = "\x1b[M";
|
||||
export const X10_MIN_LENGTH = 6;
|
||||
export const X10_BUTTON_OFFSET = 3;
|
||||
|
||||
// Scroll direction type
|
||||
export type ScrollDirection = "up" | "down";
|
||||
|
||||
// Mouse button to scroll direction mapping
|
||||
export const MOUSE_BUTTON_TO_DIRECTION: Record<number, ScrollDirection> = {
|
||||
[MOUSE_BUTTON_CODES.SGR_SCROLL_UP]: "up",
|
||||
[MOUSE_BUTTON_CODES.SGR_SCROLL_DOWN]: "down",
|
||||
[MOUSE_BUTTON_CODES.X10_SCROLL_UP]: "up",
|
||||
[MOUSE_BUTTON_CODES.X10_SCROLL_DOWN]: "down",
|
||||
} as const;
|
||||
32
src/constants/ollama.ts
Normal file
32
src/constants/ollama.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Ollama provider constants
|
||||
*/
|
||||
|
||||
export const OLLAMA_PROVIDER_NAME = "ollama" as const;
|
||||
export const OLLAMA_DISPLAY_NAME = "Ollama (Local)";
|
||||
|
||||
export const OLLAMA_DEFAULTS = {
|
||||
BASE_URL: "http://localhost:11434",
|
||||
MODEL: "deepseek-coder:6.7b",
|
||||
} as const;
|
||||
|
||||
export const OLLAMA_ENDPOINTS = {
|
||||
TAGS: "/api/tags",
|
||||
CHAT: "/api/chat",
|
||||
PULL: "/api/pull",
|
||||
} as const;
|
||||
|
||||
export const OLLAMA_TIMEOUTS = {
|
||||
VALIDATION: 5000,
|
||||
CHAT: 120000,
|
||||
} as const;
|
||||
|
||||
export const OLLAMA_CHAT_OPTIONS = {
|
||||
DEFAULT_TEMPERATURE: 0.3,
|
||||
DEFAULT_MAX_TOKENS: 4096,
|
||||
} as const;
|
||||
|
||||
export const OLLAMA_ERRORS = {
|
||||
NOT_RUNNING: (baseUrl: string) =>
|
||||
`Ollama not running at ${baseUrl}. Start it with: ollama serve`,
|
||||
} as const;
|
||||
12
src/constants/paste.ts
Normal file
12
src/constants/paste.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Constants for paste virtual text feature
|
||||
*/
|
||||
|
||||
/** Minimum number of lines to trigger paste summary */
|
||||
export const PASTE_LINE_THRESHOLD = 3;
|
||||
|
||||
/** Minimum character count to trigger paste summary */
|
||||
export const PASTE_CHAR_THRESHOLD = 150;
|
||||
|
||||
/** Format for paste placeholder display */
|
||||
export const PASTE_PLACEHOLDER_FORMAT = "[Pasted ~{lineCount} lines]";
|
||||
96
src/constants/paths.ts
Normal file
96
src/constants/paths.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* XDG-compliant storage paths
|
||||
*
|
||||
* Follows the XDG Base Directory Specification:
|
||||
* - XDG_CONFIG_HOME: User configuration (~/.config)
|
||||
* - XDG_DATA_HOME: User data (~/.local/share)
|
||||
* - XDG_CACHE_HOME: Cache data (~/.cache)
|
||||
* - XDG_STATE_HOME: State data (~/.local/state)
|
||||
*
|
||||
* See: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
||||
*/
|
||||
|
||||
import { homedir } from "os";
|
||||
import { join } from "path";
|
||||
|
||||
const APP_NAME = "codetyper";
|
||||
|
||||
/**
|
||||
* XDG base directories with fallbacks
|
||||
*/
|
||||
export const XDG = {
|
||||
config: process.env.XDG_CONFIG_HOME || join(homedir(), ".config"),
|
||||
data: process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"),
|
||||
cache: process.env.XDG_CACHE_HOME || join(homedir(), ".cache"),
|
||||
state: process.env.XDG_STATE_HOME || join(homedir(), ".local", "state"),
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Application directories
|
||||
*/
|
||||
export const DIRS = {
|
||||
/** Configuration directory (~/.config/codetyper) */
|
||||
config: join(XDG.config, APP_NAME),
|
||||
|
||||
/** Data directory (~/.local/share/codetyper) */
|
||||
data: join(XDG.data, APP_NAME),
|
||||
|
||||
/** Cache directory (~/.cache/codetyper) */
|
||||
cache: join(XDG.cache, APP_NAME),
|
||||
|
||||
/** State directory (~/.local/state/codetyper) */
|
||||
state: join(XDG.state, APP_NAME),
|
||||
|
||||
/** Sessions directory (~/.local/share/codetyper/sessions) */
|
||||
sessions: join(XDG.data, APP_NAME, "sessions"),
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Application files
|
||||
*/
|
||||
export const FILES = {
|
||||
/** Main configuration file */
|
||||
config: join(DIRS.config, "config.json"),
|
||||
|
||||
/** Keybindings configuration */
|
||||
keybindings: join(DIRS.config, "keybindings.json"),
|
||||
|
||||
/** Provider credentials (stored in data, not config) */
|
||||
credentials: join(DIRS.data, "credentials.json"),
|
||||
|
||||
/** Command history */
|
||||
history: join(DIRS.data, "history.json"),
|
||||
|
||||
/** Models cache */
|
||||
modelsCache: join(DIRS.cache, "models.json"),
|
||||
|
||||
/** Frecency cache for file/command suggestions */
|
||||
frecency: join(DIRS.cache, "frecency.json"),
|
||||
|
||||
/** Key-value state storage */
|
||||
kvStore: join(DIRS.state, "kv.json"),
|
||||
|
||||
/** Global settings (permissions, etc.) */
|
||||
settings: join(DIRS.config, "settings.json"),
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Local project config directory name
|
||||
*/
|
||||
export const LOCAL_CONFIG_DIR = ".codetyper";
|
||||
|
||||
export const IGNORE_FOLDERS = [
|
||||
"**/node_modules/**",
|
||||
"**/.git/**",
|
||||
"**/.codetyper/**",
|
||||
"**/.vscode/**",
|
||||
"**/.idea/**",
|
||||
"**/__pycache__/**",
|
||||
"**/.DS_Store/**",
|
||||
"**/dist/**",
|
||||
"**/build/**",
|
||||
"**/out/**",
|
||||
"**/.next/**",
|
||||
"**/.nuxt/**",
|
||||
"**/venv/**",
|
||||
];
|
||||
1
src/constants/patterns.ts
Normal file
1
src/constants/patterns.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const FILE_REFERENCE_PATTERN = /@(?:"([^"]+)"|'([^']+)'|(\S+))/g;
|
||||
127
src/constants/provider-quality.ts
Normal file
127
src/constants/provider-quality.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import type { TaskType } from "@/types/provider-quality";
|
||||
|
||||
export const QUALITY_THRESHOLDS = {
|
||||
HIGH: 0.85,
|
||||
MEDIUM: 0.6,
|
||||
LOW: 0.4,
|
||||
INITIAL: 0.5,
|
||||
} as const;
|
||||
|
||||
export const SCORE_ADJUSTMENTS = {
|
||||
APPROVAL: 0.05,
|
||||
CORRECTION: -0.08,
|
||||
USER_REJECTION: -0.15,
|
||||
MINOR_ISSUE: -0.02,
|
||||
MAJOR_ISSUE: -0.05,
|
||||
} as const;
|
||||
|
||||
export const TASK_TYPE_PATTERNS: Record<TaskType, RegExp[]> = {
|
||||
code_generation: [
|
||||
/create\s+(a\s+)?function/i,
|
||||
/write\s+(a\s+)?code/i,
|
||||
/implement/i,
|
||||
/generate\s+(a\s+)?/i,
|
||||
/add\s+(a\s+)?(new\s+)?feature/i,
|
||||
/build\s+(a\s+)?/i,
|
||||
],
|
||||
refactoring: [
|
||||
/refactor/i,
|
||||
/clean\s*up/i,
|
||||
/restructure/i,
|
||||
/reorganize/i,
|
||||
/simplify/i,
|
||||
/improve\s+(the\s+)?code/i,
|
||||
],
|
||||
bug_fix: [
|
||||
/fix/i,
|
||||
/bug/i,
|
||||
/error/i,
|
||||
/issue/i,
|
||||
/not\s+working/i,
|
||||
/broken/i,
|
||||
/problem/i,
|
||||
/debug/i,
|
||||
],
|
||||
documentation: [
|
||||
/document/i,
|
||||
/comment/i,
|
||||
/readme/i,
|
||||
/explain\s+.*\s+code/i,
|
||||
/add\s+.*\s+docs/i,
|
||||
/jsdoc/i,
|
||||
/tsdoc/i,
|
||||
],
|
||||
testing: [
|
||||
/test/i,
|
||||
/spec/i,
|
||||
/unit\s+test/i,
|
||||
/integration/i,
|
||||
/coverage/i,
|
||||
/mock/i,
|
||||
],
|
||||
explanation: [
|
||||
/explain/i,
|
||||
/what\s+(does|is)/i,
|
||||
/how\s+(does|do)/i,
|
||||
/why/i,
|
||||
/understand/i,
|
||||
/clarify/i,
|
||||
],
|
||||
review: [
|
||||
/review/i,
|
||||
/check/i,
|
||||
/evaluate/i,
|
||||
/assess/i,
|
||||
/audit/i,
|
||||
/pr\s+review/i,
|
||||
],
|
||||
general: [],
|
||||
};
|
||||
|
||||
export const NEGATIVE_FEEDBACK_PATTERNS = [
|
||||
/fix\s+this/i,
|
||||
/that'?s?\s+(wrong|incorrect)/i,
|
||||
/not\s+(good|right|correct|working)/i,
|
||||
/doesn'?t?\s+work/i,
|
||||
/incorrect/i,
|
||||
/broken/i,
|
||||
/bad\s+(code|response)/i,
|
||||
/try\s+again/i,
|
||||
/redo/i,
|
||||
/wrong/i,
|
||||
];
|
||||
|
||||
export const POSITIVE_FEEDBACK_PATTERNS = [
|
||||
/thanks/i,
|
||||
/thank\s+you/i,
|
||||
/perfect/i,
|
||||
/great/i,
|
||||
/works/i,
|
||||
/good\s+(job|work)/i,
|
||||
/excellent/i,
|
||||
/awesome/i,
|
||||
/exactly/i,
|
||||
];
|
||||
|
||||
export const DEFAULT_QUALITY_SCORES: Record<TaskType, number> = {
|
||||
code_generation: QUALITY_THRESHOLDS.INITIAL,
|
||||
refactoring: QUALITY_THRESHOLDS.INITIAL,
|
||||
bug_fix: QUALITY_THRESHOLDS.INITIAL,
|
||||
documentation: QUALITY_THRESHOLDS.INITIAL,
|
||||
testing: QUALITY_THRESHOLDS.INITIAL,
|
||||
explanation: QUALITY_THRESHOLDS.INITIAL,
|
||||
review: QUALITY_THRESHOLDS.INITIAL,
|
||||
general: QUALITY_THRESHOLDS.INITIAL,
|
||||
};
|
||||
|
||||
export const PROVIDER_IDS = {
|
||||
OLLAMA: "ollama",
|
||||
COPILOT: "copilot",
|
||||
} as const;
|
||||
|
||||
export const CASCADE_CONFIG = {
|
||||
MIN_AUDIT_THRESHOLD: QUALITY_THRESHOLDS.HIGH,
|
||||
MAX_SKIP_THRESHOLD: QUALITY_THRESHOLDS.LOW,
|
||||
DECAY_RATE: 0.01,
|
||||
DECAY_INTERVAL_MS: 7 * 24 * 60 * 60 * 1000, // 1 week
|
||||
} as const;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user