chore: checkpoint browser tooling and routing updates

This commit is contained in:
William Valentin
2026-02-17 15:18:37 -08:00
parent 0a4cfda787
commit 9a2f1e2bb2
15 changed files with 499 additions and 67 deletions
+277 -6
View File
@@ -13,6 +13,9 @@ import type { ModelConfig, ModelProvider } from '../../../config/schema.js';
import { MODEL_PROVIDERS } from '../../../config/schema.js';
import { createClientFromConfig } from '../../../daemon/index.js';
import { estimateMessageTokens, getContextWindow } from '../../../context/tokens.js';
import type { PairingManager } from '../../../channels/pairing.js';
import { loginGitHub, loginOpenAI } from '../../../auth/index.js';
import { OllamaClient, LlamaCppClient } from '../../../models/index.js';
/** Format a tool name like "gmail.list" -> "Gmail: List" */
function formatToolName(name: string): string {
@@ -49,6 +52,9 @@ export interface AppProps {
model: string;
agent?: NativeAgent;
hookEngine?: HookEngine;
pairingManager?: PairingManager;
localProviders?: Record<string, ModelConfig>;
currentLocalProvider?: string;
modelProviderConfigs?: Partial<Record<ModelProvider, ModelConfig>>;
contextThresholdPct?: number;
onTransfer?: (target: string) => string | void;
@@ -63,6 +69,9 @@ export function App({
model,
agent,
hookEngine,
pairingManager,
localProviders,
currentLocalProvider,
modelProviderConfigs,
contextThresholdPct,
onTransfer,
@@ -196,6 +205,55 @@ export function App({
}
});
const pushAssistantMessage = useCallback((content: string) => {
setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content })]);
}, [session]);
const getAvailableBackends = useCallback((): string[] => {
const backends: string[] = [];
if (currentLocalProvider) {
backends.push(currentLocalProvider);
}
if (localProviders) {
backends.push(...Object.keys(localProviders));
}
return [...new Set(backends)];
}, [currentLocalProvider, localProviders]);
const createLocalClient = useCallback((cfg: ModelConfig): ModelClient | null => {
if (cfg.provider === 'ollama') {
return new OllamaClient({
model: cfg.model,
host: cfg.endpoint,
});
}
if (cfg.provider === 'llamacpp') {
return new LlamaCppClient({
endpoint: cfg.endpoint ?? 'http://localhost:8080',
model: cfg.model,
authToken: cfg.auth_token,
});
}
return null;
}, []);
const parseDurationToMs = useCallback((value: string): number | null => {
const m = value.match(/^(\d+)([smhd])$/i);
if (!m) {
return null;
}
const n = Number.parseInt(m[1], 10);
if (!Number.isFinite(n) || n <= 0) {
return null;
}
const unit = m[2].toLowerCase();
if (unit === 's') {return n * 1000;}
if (unit === 'm') {return n * 60_000;}
if (unit === 'h') {return n * 3_600_000;}
if (unit === 'd') {return n * 86_400_000;}
return null;
}, []);
const handleSubmit = useCallback(async (value: string) => {
if (confirmation) {
return;
@@ -273,7 +331,12 @@ export function App({
case 'verbose': {
const next = !verbose;
setVerbose(next);
setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: `Verbose mode: ${next ? 'on' : 'off'}` })]);
let content = `Verbose mode: ${next ? 'on' : 'off'}`;
if (next && agent) {
const snapshot = agent.getToolInventorySnapshot();
content += `\n[Agent] tool-inventory session=${snapshot.sessionId} agent=${snapshot.agent} provider=${snapshot.provider} skill=${snapshot.skill} internal=${snapshot.internalCount} exposed=${snapshot.exposedCount} internal_browser=[${snapshot.internalBrowser.join(', ') || 'none'}] exposed_browser=[${snapshot.exposedBrowser.join(', ') || 'none'}]`;
}
setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content })]);
return;
}
@@ -480,12 +543,213 @@ export function App({
return;
}
case 'backend':
case 'login':
case 'pair':
case 'elevate':
setMessages(prev => [...prev, session.addMessage({ role: 'assistant', content: `/${command.type} is not supported in fullscreen mode.` })]);
case 'backend': {
if (!modelRouter) {
pushAssistantMessage('Backend switching not available.');
return;
}
if (!command.provider) {
const current = modelRouter.getLocalProviderName() ?? currentLocalProvider ?? 'unknown';
const available = getAvailableBackends();
pushAssistantMessage(`Current local backend: ${current}\nAvailable: ${available.join(', ')}`);
return;
}
const providerConfig = localProviders?.[command.provider];
if (!providerConfig) {
const available = getAvailableBackends();
pushAssistantMessage(`Backend '${command.provider}' not configured.\nAvailable: ${available.join(', ')}`);
return;
}
const client = createLocalClient(providerConfig);
if (!client) {
pushAssistantMessage(`Unsupported backend provider '${providerConfig.provider}'.`);
return;
}
modelRouter.setLocalClient(client, command.provider);
modelRouter.setTier('local');
if (agent) {
agent.setModelTier('local');
}
setCurrentModel(modelRouter.getLabel('local'));
pushAssistantMessage(`Switched backend to ${command.provider}`);
return;
}
case 'login': {
const provider = (command.provider ?? '').trim().toLowerCase();
if (!provider) {
pushAssistantMessage('Usage: /login <provider>\nSupported: github, openai, anthropic, zai');
return;
}
if (provider === 'github') {
pushAssistantMessage('Starting GitHub OAuth device login...');
try {
await loginGitHub((userCode, verificationUri) => {
pushAssistantMessage(`GitHub login required:\nCode: ${userCode}\nURL: ${verificationUri}`);
});
pushAssistantMessage('GitHub login complete. Token stored in ~/.config/flynn/auth.json');
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
pushAssistantMessage(`GitHub login failed: ${msg}`);
}
return;
}
if (provider === 'openai') {
pushAssistantMessage('Starting OpenAI OAuth device login...');
try {
await loginOpenAI((userCode, verificationUri) => {
pushAssistantMessage(`OpenAI login required:\nCode: ${userCode}\nURL: ${verificationUri}`);
});
pushAssistantMessage('OpenAI login complete. Credentials stored in ~/.config/flynn/auth.json');
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
pushAssistantMessage(`OpenAI login failed: ${msg}`);
}
return;
}
if (provider === 'anthropic' || provider === 'zai' || provider === 'zhipuai') {
pushAssistantMessage(
`/${command.type} ${provider} requires key entry, which fullscreen mode does not mask.\nUse minimal mode (pnpm tui) for interactive key setup.`,
);
return;
}
pushAssistantMessage(`Unknown login provider: ${provider}. Supported: github, openai, anthropic, zai`);
return;
}
case 'pair': {
if (!pairingManager) {
pushAssistantMessage('Pairing not enabled. Set pairing.enabled: true in config.');
return;
}
if (command.action === 'generate') {
const code = pairingManager.generateCode(command.args);
const pending = pairingManager.listPendingCodes().find(p => p.code === code);
const expiresIn = pending ? Math.round((pending.expiresAt - Date.now()) / 1000) : '?';
pushAssistantMessage(`Pairing code: ${code}\nExpires in ${expiresIn}s${command.args ? ` (label: ${command.args})` : ''}`);
return;
}
if (command.action === 'revoke') {
const args = (command.args ?? '').trim();
const parts = args.split(/\s+/);
if (parts.length < 2) {
pushAssistantMessage('Usage: /pair revoke <channel> <senderId>');
return;
}
const [channel, senderId] = parts;
const revoked = pairingManager.revokeApproval(channel, senderId);
pushAssistantMessage(revoked ? `Revoked approval for ${channel}:${senderId}` : `No approval found for ${channel}:${senderId}`);
return;
}
const pending = pairingManager.listPendingCodes();
const approved = pairingManager.listApproved();
if (pending.length === 0 && approved.length === 0) {
pushAssistantMessage('No pending codes or approved senders.');
return;
}
const lines: string[] = [];
if (pending.length > 0) {
lines.push('Pending codes:');
for (const p of pending) {
const ttl = Math.max(0, Math.round((p.expiresAt - Date.now()) / 1000));
lines.push(` ${p.code} expires in ${ttl}s${p.label ? ` (label: ${p.label})` : ''}`);
}
}
if (approved.length > 0) {
lines.push('Approved senders:');
for (const a of approved) {
const date = new Date(a.approvedAt).toISOString().slice(0, 16).replace('T', ' ');
lines.push(` ${a.channel}:${a.senderId} since ${date} (code: ${a.codeUsed})`);
}
}
pushAssistantMessage(lines.join('\n'));
return;
}
case 'elevate': {
const untilRaw = session.getConfig('elevation.until_ms');
const reason = session.getConfig('elevation.reason') ?? '';
const id = session.getConfig('elevation.id') ?? '';
const showStatus = () => {
if (!untilRaw || !id) {
pushAssistantMessage('Elevated mode: off');
return;
}
const untilMs = Number.parseInt(untilRaw, 10);
if (!Number.isFinite(untilMs) || untilMs <= Date.now()) {
session.deleteConfig('elevation.until_ms');
session.deleteConfig('elevation.reason');
session.deleteConfig('elevation.id');
pushAssistantMessage('Elevated mode: off');
return;
}
const remainingSec = Math.ceil((untilMs - Date.now()) / 1000);
pushAssistantMessage(`Elevated mode: on (${remainingSec}s remaining)${reason ? ` - ${reason}` : ''}`);
};
const raw = (command.args ?? '').trim();
if (!raw) {
showStatus();
return;
}
const parts = raw.split(/\s+/);
const hasYes = parts.includes('--yes') || parts.includes('--confirm');
const filtered = parts.filter((p) => p !== '--yes' && p !== '--confirm');
if (filtered.length === 0) {
pushAssistantMessage('Usage: /elevate <duration> <reason...> --yes | /elevate off --yes');
return;
}
if (filtered[0] === 'off') {
if (!hasYes) {
pushAssistantMessage('Refusing to disable elevation without explicit confirmation. Use: /elevate off --yes');
return;
}
session.deleteConfig('elevation.until_ms');
session.deleteConfig('elevation.reason');
session.deleteConfig('elevation.id');
pushAssistantMessage('Elevated mode: off');
return;
}
if (!hasYes) {
pushAssistantMessage('Refusing to enable elevation without explicit confirmation. Use: /elevate <duration> <reason...> --yes');
return;
}
const ttlMs = parseDurationToMs(filtered[0]);
if (!ttlMs) {
pushAssistantMessage('Invalid duration. Use one of: 30s, 10m, 1h, 1d');
return;
}
const reasonText = filtered.slice(1).join(' ').trim();
const untilMs = Date.now() + ttlMs;
const newId = `${untilMs}`;
session.setConfig('elevation.until_ms', String(untilMs));
session.setConfig('elevation.id', newId);
if (reasonText) {
session.setConfig('elevation.reason', reasonText);
} else {
session.deleteConfig('elevation.reason');
}
pushAssistantMessage(`Elevated mode: on until ${new Date(untilMs).toISOString()}`);
return;
}
case 'message':
break;
@@ -585,6 +849,13 @@ export function App({
messages.length,
tokenUsage.inputTokens,
tokenUsage.outputTokens,
pushAssistantMessage,
getAvailableBackends,
createLocalClient,
parseDurationToMs,
localProviders,
currentLocalProvider,
pairingManager,
modelProviderConfigs,
onTransfer,
]);
+7
View File
@@ -7,6 +7,7 @@ import type { ModelRouter } from '../../models/router.js';
import type { NativeAgent } from '../../backends/native/agent.js';
import type { HookEngine } from '../../hooks/index.js';
import type { ModelConfig, ModelProvider } from '../../config/index.js';
import type { PairingManager } from '../../channels/pairing.js';
export interface FullscreenTuiConfig {
session: ManagedSession;
@@ -16,6 +17,9 @@ export interface FullscreenTuiConfig {
model: string;
agent?: NativeAgent;
hookEngine?: HookEngine;
pairingManager?: PairingManager;
localProviders?: Record<string, ModelConfig>;
currentLocalProvider?: string;
modelProviderConfigs?: Partial<Record<ModelProvider, ModelConfig>>;
contextThresholdPct?: number;
onTransfer?: (target: string) => string | void;
@@ -41,6 +45,9 @@ export async function startFullscreenTui(config: FullscreenTuiConfig): Promise<v
model: config.model,
agent: config.agent,
hookEngine: config.hookEngine,
pairingManager: config.pairingManager,
localProviders: config.localProviders,
currentLocalProvider: config.currentLocalProvider,
modelProviderConfigs: config.modelProviderConfigs,
contextThresholdPct: config.contextThresholdPct,
onTransfer: config.onTransfer,
+8 -1
View File
@@ -468,7 +468,14 @@ export class MinimalTui {
private handleVerboseCommand(): void {
this.verbose = !this.verbose;
console.log(`${colors.gray}Verbose mode:${colors.reset} ${this.verbose ? 'on' : 'off'}\n`);
console.log(`${colors.gray}Verbose mode:${colors.reset} ${this.verbose ? 'on' : 'off'}`);
if (this.verbose && this.config.agent) {
const snapshot = this.config.agent.getToolInventorySnapshot();
console.log(
`[Agent] tool-inventory session=${snapshot.sessionId} agent=${snapshot.agent} provider=${snapshot.provider} skill=${snapshot.skill} internal=${snapshot.internalCount} exposed=${snapshot.exposedCount} internal_browser=[${snapshot.internalBrowser.join(', ') || 'none'}] exposed_browser=[${snapshot.exposedBrowser.join(', ') || 'none'}]`,
);
}
console.log('');
}
private handleQueueCommand(action?: 'show' | 'set' | 'reset', args?: string): void {