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:
William Valentin
2026-02-05 17:53:00 -08:00
parent df92a9d95f
commit 5088f7a6be
2 changed files with 74 additions and 8 deletions
+14 -2
View File
@@ -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
View File
@@ -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,
}); });