From f671ea5ab56d26dc957f5a793420d09e9d3f024f Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 5 Feb 2026 00:41:17 -0800 Subject: [PATCH] feat: add fullscreen TUI mode with Ink React components --- package.json | 1 + src/frontends/tui/fullscreen.ts | 27 +++++++++++ src/frontends/tui/index.ts | 7 +++ src/tui.ts | 80 ++++++++++++++++++++++----------- 4 files changed, 89 insertions(+), 26 deletions(-) create mode 100644 src/frontends/tui/fullscreen.ts diff --git a/package.json b/package.json index 31ac74b..5b8b107 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "dev": "tsx watch src/index.ts", "start": "node dist/index.js", "tui": "tsx src/tui.ts", + "tui:fs": "tsx src/tui.ts --fullscreen", "tui:dev": "tsx watch src/tui.ts", "test": "vitest", "test:run": "vitest run", diff --git a/src/frontends/tui/fullscreen.ts b/src/frontends/tui/fullscreen.ts new file mode 100644 index 0000000..2f9d623 --- /dev/null +++ b/src/frontends/tui/fullscreen.ts @@ -0,0 +1,27 @@ +import React from 'react'; +import { render } from 'ink'; +import { App } from './components/index.js'; +import type { ManagedSession } from '../../session/index.js'; +import type { ModelClient } from '../../models/types.js'; + +export interface FullscreenTuiConfig { + session: ManagedSession; + modelClient: ModelClient; + systemPrompt: string; + model: string; + onExit?: () => void; +} + +export async function startFullscreenTui(config: FullscreenTuiConfig): Promise { + const { waitUntilExit } = render( + React.createElement(App, { + session: config.session, + modelClient: config.modelClient, + systemPrompt: config.systemPrompt, + model: config.model, + onExit: config.onExit, + }) + ); + + await waitUntilExit(); +} diff --git a/src/frontends/tui/index.ts b/src/frontends/tui/index.ts index 6687693..e3879f5 100644 --- a/src/frontends/tui/index.ts +++ b/src/frontends/tui/index.ts @@ -5,3 +5,10 @@ export { type TuiCommand, type MinimalTuiConfig, } from './minimal.js'; + +export { + startFullscreenTui, + type FullscreenTuiConfig, +} from './fullscreen.js'; + +export { App, StatusBar, MessageList, InputBar } from './components/index.js'; diff --git a/src/tui.ts b/src/tui.ts index 38109e0..67540d0 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -1,7 +1,7 @@ import { loadConfig } from './config/index.js'; import { SessionStore, SessionManager } from './session/index.js'; import { AnthropicClient, OpenAIClient, OllamaClient, ModelRouter } from './models/index.js'; -import { MinimalTui } from './frontends/tui/index.js'; +import { MinimalTui, startFullscreenTui } from './frontends/tui/index.js'; import type { Config } from './config/index.js'; import { resolve } from 'path'; import { homedir } from 'os'; @@ -61,6 +61,9 @@ function createModelRouter(config: Config): ModelRouter { } async function main() { + const args = process.argv.slice(2); + const fullscreenMode = args.includes('--fullscreen') || args.includes('-f'); + console.log('Flynn TUI starting...'); if (!existsSync(CONFIG_PATH)) { @@ -83,36 +86,61 @@ async function main() { // Get TUI session const session = sessionManager.getSession('tui', 'local'); - // Create and start minimal TUI - const tui = new MinimalTui({ - session, - modelClient: modelRouter, - systemPrompt: SYSTEM_PROMPT, - onTransfer: (target) => { - if (target === 'telegram') { - const telegramUserId = String(config.telegram.allowed_chat_ids[0]); - sessionManager.transferSession('tui', 'local', 'telegram', telegramUserId); - console.log(`Session transferred to Telegram (${telegramUserId})\n`); - } else { - console.log(`Unknown transfer target: ${target}\n`); - } - }, - onFullscreen: () => { - console.log('Fullscreen mode not yet implemented.\n'); - }, - }); - - // Handle shutdown - process.on('SIGINT', () => { - tui.stop(); + const cleanup = () => { sessionStore.close(); + }; + + process.on('SIGINT', () => { + cleanup(); process.exit(0); }); - await tui.start(); + if (fullscreenMode) { + // Start fullscreen Ink UI + await startFullscreenTui({ + session, + modelClient: modelRouter, + systemPrompt: SYSTEM_PROMPT, + model: config.models.default.model, + onExit: cleanup, + }); + } else { + // Start minimal readline UI + const tui = new MinimalTui({ + session, + modelClient: modelRouter, + systemPrompt: SYSTEM_PROMPT, + onTransfer: (target) => { + if (target === 'telegram') { + const telegramUserId = String(config.telegram.allowed_chat_ids[0]); + sessionManager.transferSession('tui', 'local', 'telegram', telegramUserId); + console.log(`Session transferred to Telegram (${telegramUserId})\n`); + } else { + console.log(`Unknown transfer target: ${target}\n`); + } + }, + onFullscreen: async () => { + tui.stop(); + console.clear(); + await startFullscreenTui({ + session, + modelClient: modelRouter, + systemPrompt: SYSTEM_PROMPT, + model: config.models.default.model, + onExit: () => { + // Return to minimal mode would require re-init + // For now, just exit + cleanup(); + process.exit(0); + }, + }); + }, + }); - // Cleanup - sessionStore.close(); + await tui.start(); + } + + cleanup(); } main().catch((error) => {