feat(tui): display tool execution status in minimal TUI
TUI now creates a NativeAgent with tool registry/executor and uses agent.process() for message handling. Tool calls display status lines showing tool name, args, and success/error result. Falls back to direct model client when agent is not configured.
This commit is contained in:
@@ -2,6 +2,7 @@ import * as readline from 'node:readline';
|
|||||||
import type { ManagedSession } from '../../session/index.js';
|
import type { ManagedSession } from '../../session/index.js';
|
||||||
import type { ModelClient, TokenUsage } from '../../models/types.js';
|
import type { ModelClient, TokenUsage } from '../../models/types.js';
|
||||||
import type { ModelRouter, ModelTier } from '../../models/router.js';
|
import type { ModelRouter, ModelTier } from '../../models/router.js';
|
||||||
|
import type { NativeAgent } from '../../backends/native/agent.js';
|
||||||
import { parseCommand, getHelpText, resolveModelAlias, getCommandCompletions, getCommandTooltip, type Command } from './commands.js';
|
import { parseCommand, getHelpText, resolveModelAlias, getCommandCompletions, getCommandTooltip, type Command } from './commands.js';
|
||||||
import { renderMarkdown } from './markdown.js';
|
import { renderMarkdown } from './markdown.js';
|
||||||
import type { ModelConfig } from '../../config/schema.js';
|
import type { ModelConfig } from '../../config/schema.js';
|
||||||
@@ -32,6 +33,7 @@ export interface MinimalTuiConfig {
|
|||||||
modelClient: ModelClient;
|
modelClient: ModelClient;
|
||||||
modelRouter?: ModelRouter;
|
modelRouter?: ModelRouter;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
|
agent?: NativeAgent;
|
||||||
onFullscreen?: () => void;
|
onFullscreen?: () => void;
|
||||||
onTransfer?: (target: string) => void;
|
onTransfer?: (target: string) => void;
|
||||||
localProviders?: Record<string, ModelConfig>;
|
localProviders?: Record<string, ModelConfig>;
|
||||||
@@ -285,12 +287,22 @@ export class MinimalTui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleMessage(content: string): Promise<void> {
|
private async handleMessage(content: string): Promise<void> {
|
||||||
this.config.session.addMessage({ role: 'user', content });
|
|
||||||
|
|
||||||
// Print Flynn label before response
|
// Print Flynn label before response
|
||||||
process.stdout.write(`\n${colors.orange}${colors.bold}Flynn:${colors.reset}\n`);
|
process.stdout.write(`\n${colors.orange}${colors.bold}Flynn:${colors.reset}\n`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Use agent if available (supports tool loop)
|
||||||
|
if (this.config.agent) {
|
||||||
|
const response = await this.config.agent.process(content);
|
||||||
|
const rendered = renderMarkdown(response);
|
||||||
|
console.log(rendered);
|
||||||
|
console.log();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: direct model client (no tool support)
|
||||||
|
this.config.session.addMessage({ role: 'user', content });
|
||||||
|
|
||||||
// Try streaming if available
|
// Try streaming if available
|
||||||
if (this.config.modelClient.chatStream) {
|
if (this.config.modelClient.chatStream) {
|
||||||
let fullContent = '';
|
let fullContent = '';
|
||||||
|
|||||||
+60
-6
@@ -2,17 +2,40 @@ import { loadConfig } from './config/index.js';
|
|||||||
import { SessionStore, SessionManager } from './session/index.js';
|
import { SessionStore, SessionManager } from './session/index.js';
|
||||||
import { AnthropicClient, OpenAIClient, OllamaClient, LlamaCppClient, ModelRouter } from './models/index.js';
|
import { AnthropicClient, OpenAIClient, OllamaClient, LlamaCppClient, ModelRouter } from './models/index.js';
|
||||||
import { MinimalTui, startFullscreenTui } from './frontends/tui/index.js';
|
import { MinimalTui, startFullscreenTui } from './frontends/tui/index.js';
|
||||||
|
import { NativeAgent } from './backends/index.js';
|
||||||
|
import { ToolRegistry, ToolExecutor, allBuiltinTools } from './tools/index.js';
|
||||||
|
import { HookEngine } from './hooks/index.js';
|
||||||
import type { Config } from './config/index.js';
|
import type { Config } from './config/index.js';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
import { existsSync, mkdirSync } from 'fs';
|
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
||||||
|
|
||||||
const CONFIG_PATH = process.env.FLYNN_CONFIG
|
const CONFIG_PATH = process.env.FLYNN_CONFIG
|
||||||
?? resolve(homedir(), '.config/flynn/config.yaml');
|
?? resolve(homedir(), '.config/flynn/config.yaml');
|
||||||
|
|
||||||
const SYSTEM_PROMPT = `You are Flynn, a helpful personal AI assistant. You are direct, concise, and helpful. You can help with a variety of tasks including answering questions, providing information, and having conversations.
|
// ANSI color codes for tool status display
|
||||||
|
const toolColors = {
|
||||||
|
reset: '\x1b[0m',
|
||||||
|
dim: '\x1b[2m',
|
||||||
|
cyan: '\x1b[36m',
|
||||||
|
green: '\x1b[32m',
|
||||||
|
red: '\x1b[31m',
|
||||||
|
};
|
||||||
|
|
||||||
Keep responses focused and avoid unnecessary verbosity. Use markdown formatting when it improves readability.`;
|
function loadSystemPrompt(): string {
|
||||||
|
const paths = [
|
||||||
|
resolve(process.cwd(), 'SOUL.md'),
|
||||||
|
resolve(import.meta.dirname, '../SOUL.md'),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const soulPath of paths) {
|
||||||
|
if (existsSync(soulPath)) {
|
||||||
|
return readFileSync(soulPath, 'utf-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'You are Flynn, a helpful personal AI assistant. Be direct, concise, and helpful. Use markdown when it improves readability.';
|
||||||
|
}
|
||||||
|
|
||||||
function createModelRouter(config: Config): ModelRouter {
|
function createModelRouter(config: Config): ModelRouter {
|
||||||
const models = config.models;
|
const models = config.models;
|
||||||
@@ -99,10 +122,40 @@ async function main() {
|
|||||||
const sessionStore = new SessionStore(resolve(dataDir, 'sessions.db'));
|
const sessionStore = new SessionStore(resolve(dataDir, 'sessions.db'));
|
||||||
const sessionManager = new SessionManager(sessionStore);
|
const sessionManager = new SessionManager(sessionStore);
|
||||||
const modelRouter = createModelRouter(config);
|
const modelRouter = createModelRouter(config);
|
||||||
|
const systemPrompt = loadSystemPrompt();
|
||||||
|
|
||||||
|
// Initialize tool registry and executor
|
||||||
|
const hookEngine = new HookEngine(config.hooks);
|
||||||
|
const toolRegistry = new ToolRegistry();
|
||||||
|
for (const tool of allBuiltinTools) {
|
||||||
|
toolRegistry.register(tool);
|
||||||
|
}
|
||||||
|
const toolExecutor = new ToolExecutor(toolRegistry, hookEngine);
|
||||||
|
|
||||||
// Get TUI session
|
// Get TUI session
|
||||||
const session = sessionManager.getSession('tui', 'local');
|
const session = sessionManager.getSession('tui', 'local');
|
||||||
|
|
||||||
|
// Create agent with tools and tool status display
|
||||||
|
const agent = new NativeAgent({
|
||||||
|
modelClient: modelRouter,
|
||||||
|
systemPrompt,
|
||||||
|
session,
|
||||||
|
toolRegistry,
|
||||||
|
toolExecutor,
|
||||||
|
onToolUse: (event) => {
|
||||||
|
if (event.type === 'start') {
|
||||||
|
const argsStr = event.args ? ` ${toolColors.dim}${JSON.stringify(event.args)}${toolColors.reset}` : '';
|
||||||
|
process.stdout.write(`${toolColors.cyan}> ${event.tool}${toolColors.reset}${argsStr}\n`);
|
||||||
|
} else if (event.type === 'end' && event.result) {
|
||||||
|
const icon = event.result.success ? `${toolColors.green}done` : `${toolColors.red}error`;
|
||||||
|
const detail = event.result.success
|
||||||
|
? `${toolColors.dim}(${event.result.output.split('\n').length} lines)${toolColors.reset}`
|
||||||
|
: `${toolColors.dim}${event.result.error ?? 'unknown error'}${toolColors.reset}`;
|
||||||
|
process.stdout.write(` ${icon}${toolColors.reset} ${detail}\n`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
sessionStore.close();
|
sessionStore.close();
|
||||||
};
|
};
|
||||||
@@ -118,7 +171,7 @@ async function main() {
|
|||||||
session,
|
session,
|
||||||
modelClient: modelRouter,
|
modelClient: modelRouter,
|
||||||
modelRouter,
|
modelRouter,
|
||||||
systemPrompt: SYSTEM_PROMPT,
|
systemPrompt: systemPrompt,
|
||||||
model: config.models.default.model,
|
model: config.models.default.model,
|
||||||
onExit: cleanup,
|
onExit: cleanup,
|
||||||
});
|
});
|
||||||
@@ -130,7 +183,8 @@ async function main() {
|
|||||||
session,
|
session,
|
||||||
modelClient: modelRouter,
|
modelClient: modelRouter,
|
||||||
modelRouter,
|
modelRouter,
|
||||||
systemPrompt: SYSTEM_PROMPT,
|
systemPrompt,
|
||||||
|
agent,
|
||||||
localProviders: config.models.local_providers,
|
localProviders: config.models.local_providers,
|
||||||
currentLocalProvider: config.models.local?.provider,
|
currentLocalProvider: config.models.local?.provider,
|
||||||
onTransfer: (target) => {
|
onTransfer: (target) => {
|
||||||
@@ -156,7 +210,7 @@ async function main() {
|
|||||||
session,
|
session,
|
||||||
modelClient: modelRouter,
|
modelClient: modelRouter,
|
||||||
modelRouter,
|
modelRouter,
|
||||||
systemPrompt: SYSTEM_PROMPT,
|
systemPrompt,
|
||||||
model: config.models.default.model,
|
model: config.models.default.model,
|
||||||
onExit: cleanup,
|
onExit: cleanup,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user