chore: checkpoint browser tooling and routing updates
This commit is contained in:
@@ -17,6 +17,17 @@ export interface ToolUseEvent {
|
||||
result?: ToolResult;
|
||||
}
|
||||
|
||||
export interface ToolInventorySnapshot {
|
||||
sessionId: string;
|
||||
agent: string;
|
||||
provider: string;
|
||||
skill: string;
|
||||
internalCount: number;
|
||||
exposedCount: number;
|
||||
internalBrowser: string[];
|
||||
exposedBrowser: string[];
|
||||
}
|
||||
|
||||
export interface NativeAgentConfig {
|
||||
modelClient: ModelClient | ModelRouter;
|
||||
systemPrompt: string;
|
||||
@@ -453,6 +464,35 @@ export class NativeAgent {
|
||||
return this._toolPolicyContext;
|
||||
}
|
||||
|
||||
getToolInventorySnapshot(): ToolInventorySnapshot {
|
||||
if (!this.toolRegistry) {
|
||||
return {
|
||||
sessionId: '-',
|
||||
agent: '-',
|
||||
provider: '-',
|
||||
skill: '-',
|
||||
internalCount: 0,
|
||||
exposedCount: 0,
|
||||
internalBrowser: [],
|
||||
exposedBrowser: [],
|
||||
};
|
||||
}
|
||||
|
||||
const internal = this.toolRegistry.filteredList(this._toolPolicyContext).map((tool) => tool.name);
|
||||
const exposed = this.toolRegistry.filteredToAnthropicFormat(this._toolPolicyContext).map((tool) => tool.name);
|
||||
const context = this._toolPolicyContext;
|
||||
return {
|
||||
sessionId: context?.sessionId ?? '-',
|
||||
agent: context?.agent ?? '-',
|
||||
provider: context?.provider ?? '-',
|
||||
skill: context?.skillName ?? '-',
|
||||
internalCount: internal.length,
|
||||
exposedCount: exposed.length,
|
||||
internalBrowser: internal.filter((name) => name.startsWith('browser.')),
|
||||
exposedBrowser: exposed.filter((name) => name.startsWith('browser_')),
|
||||
};
|
||||
}
|
||||
|
||||
setAttachmentCollector(collector: OutboundAttachmentCollector | undefined): void {
|
||||
this._attachmentCollector = collector;
|
||||
}
|
||||
|
||||
+11
-39
@@ -103,13 +103,6 @@ export function registerTuiCommand(program: Command): void {
|
||||
const { MinimalTui, startFullscreenTui } = await import('../frontends/tui/index.js');
|
||||
const { NativeAgent } = await import('../backends/index.js');
|
||||
const {
|
||||
ToolRegistry,
|
||||
ToolExecutor,
|
||||
ToolPolicy,
|
||||
allBuiltinTools,
|
||||
createWebSearchTools,
|
||||
createProcessTools,
|
||||
ProcessManager,
|
||||
createGmailTools,
|
||||
createGcalTools,
|
||||
createGdocsTools,
|
||||
@@ -119,6 +112,8 @@ export function registerTuiCommand(program: Command): void {
|
||||
createAgentDelegateTool,
|
||||
} = await import('../tools/index.js');
|
||||
const { HookEngine } = await import('../hooks/index.js');
|
||||
const { Lifecycle } = await import('../daemon/lifecycle.js');
|
||||
const { initTools } = await import('../daemon/tools.js');
|
||||
const { createModelRouter } = await import('../daemon/index.js');
|
||||
const { AgentConfigRegistry } = await import('../agents/index.js');
|
||||
|
||||
@@ -147,33 +142,8 @@ export function registerTuiCommand(program: Command): void {
|
||||
const systemPrompt = loadSystemPrompt();
|
||||
|
||||
const hookEngine = new HookEngine(config.hooks);
|
||||
const toolRegistry = new ToolRegistry();
|
||||
for (const tool of allBuiltinTools) {
|
||||
toolRegistry.register(tool);
|
||||
}
|
||||
|
||||
// Register web search tools if configured with credentials
|
||||
if (config.web_search.api_key || config.web_search.endpoint) {
|
||||
for (const tool of createWebSearchTools({
|
||||
provider: config.web_search.provider,
|
||||
apiKey: config.web_search.api_key,
|
||||
endpoint: config.web_search.endpoint,
|
||||
maxResults: config.web_search.max_results,
|
||||
})) {
|
||||
toolRegistry.register(tool);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize process manager and register process tools
|
||||
const processManager = new ProcessManager({
|
||||
maxConcurrent: config.process.max_concurrent,
|
||||
maxRuntimeMinutes: config.process.max_runtime_minutes,
|
||||
bufferSize: config.process.buffer_size,
|
||||
});
|
||||
|
||||
for (const tool of createProcessTools(processManager)) {
|
||||
toolRegistry.register(tool);
|
||||
}
|
||||
const lifecycle = new Lifecycle();
|
||||
const { toolRegistry, toolExecutor } = initTools({ config, lifecycle, hookEngine });
|
||||
|
||||
// Register Gmail tools if configured
|
||||
if (config.automation.gmail?.enabled) {
|
||||
@@ -233,10 +203,6 @@ export function registerTuiCommand(program: Command): void {
|
||||
}));
|
||||
}
|
||||
|
||||
toolRegistry.setPolicy(new ToolPolicy(config.tools));
|
||||
|
||||
const toolExecutor = new ToolExecutor(toolRegistry, hookEngine);
|
||||
|
||||
const session = sessionManager.getSession('tui', 'local');
|
||||
const modelProviderConfigs = buildProviderConfigMap(config);
|
||||
|
||||
@@ -262,7 +228,7 @@ export function registerTuiCommand(program: Command): void {
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
processManager.shutdown();
|
||||
void lifecycle.shutdown();
|
||||
sessionStore.close();
|
||||
};
|
||||
|
||||
@@ -292,6 +258,9 @@ export function registerTuiCommand(program: Command): void {
|
||||
model: config.models.default.model,
|
||||
agent,
|
||||
hookEngine,
|
||||
pairingManager,
|
||||
localProviders: config.models.local_providers,
|
||||
currentLocalProvider: config.models.local?.provider,
|
||||
modelProviderConfigs,
|
||||
contextThresholdPct: config.compaction.threshold_pct,
|
||||
onTransfer: transferSessionToTarget,
|
||||
@@ -331,6 +300,9 @@ export function registerTuiCommand(program: Command): void {
|
||||
model: config.models.default.model,
|
||||
agent,
|
||||
hookEngine,
|
||||
pairingManager,
|
||||
localProviders: config.models.local_providers,
|
||||
currentLocalProvider: config.models.local?.provider,
|
||||
modelProviderConfigs,
|
||||
contextThresholdPct: config.compaction.threshold_pct,
|
||||
onTransfer: transferSessionToTarget,
|
||||
|
||||
+15
-13
@@ -253,6 +253,20 @@ export function createMessageRouter(deps: {
|
||||
} as AgentDelegateDeps));
|
||||
}
|
||||
|
||||
const toolPolicyContext = {
|
||||
agent: effectiveTier,
|
||||
provider: effectiveProvider,
|
||||
sessionId: session.id,
|
||||
channel,
|
||||
sender: senderId,
|
||||
tier: effectiveTier,
|
||||
autonomyLevel: deps.config.agents.autonomy_level ?? 'standard',
|
||||
skillName: activeSkillName,
|
||||
skillPermissions: activeSkill?.manifest.permissions,
|
||||
allowedSecretScopes: activeSkill?.manifest.permissions?.secrets,
|
||||
executionEnvironment,
|
||||
};
|
||||
|
||||
const orchestrator = new AgentOrchestrator({
|
||||
modelRouter: deps.modelRouter,
|
||||
systemPrompt: effectiveSystemPrompt,
|
||||
@@ -283,19 +297,7 @@ export function createMessageRouter(deps: {
|
||||
memoryAutoExtract: deps.config.memory?.auto_extract,
|
||||
memoryInjectionStrategy: deps.config.memory?.injection_strategy,
|
||||
memoryMaxInjectionTokens: deps.config.memory?.max_injection_tokens,
|
||||
toolPolicyContext: {
|
||||
agent: effectiveTier,
|
||||
provider: effectiveProvider,
|
||||
sessionId: session.id,
|
||||
channel,
|
||||
sender: senderId,
|
||||
tier: effectiveTier,
|
||||
autonomyLevel: deps.config.agents.autonomy_level ?? 'standard',
|
||||
skillName: activeSkillName,
|
||||
skillPermissions: activeSkill?.manifest.permissions,
|
||||
allowedSecretScopes: activeSkill?.manifest.permissions?.secrets,
|
||||
executionEnvironment,
|
||||
},
|
||||
toolPolicyContext,
|
||||
attachmentCollector: collector,
|
||||
});
|
||||
// Resolve the lazy orchestrator reference for agent.delegate
|
||||
|
||||
+3
-1
@@ -66,7 +66,7 @@ export function initTools(deps: ToolsDeps): ToolsResult {
|
||||
}
|
||||
|
||||
// Initialize browser manager and register browser tools (if enabled)
|
||||
const browserToolNames = ['browser.navigate', 'browser.screenshot', 'browser.click', 'browser.type', 'browser.content', 'browser.eval'];
|
||||
const browserToolNames = ['browser.navigate', 'browser.screenshot', 'browser.click', 'browser.type', 'browser.content', 'browser.eval', 'browser.evaluate'];
|
||||
let browserManager: BrowserManager | undefined;
|
||||
if (config.browser?.enabled) {
|
||||
const manager = new BrowserManager({
|
||||
@@ -108,6 +108,8 @@ export function initTools(deps: ToolsDeps): ToolsResult {
|
||||
const availableBrowserTools = browserToolNames.filter((name) => allowed.has(name));
|
||||
if (availableBrowserTools.length === 0) {
|
||||
console.log('Browser tools are registered but blocked by tool policy (use tools.profile=coding/full or tools.allow).');
|
||||
} else {
|
||||
console.log(`Browser tools available after policy: ${availableBrowserTools.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -55,7 +55,8 @@ describe('Browser tools', () => {
|
||||
expect(names).toContain('browser.type');
|
||||
expect(names).toContain('browser.content');
|
||||
expect(names).toContain('browser.eval');
|
||||
expect(names).toHaveLength(6);
|
||||
expect(names).toContain('browser.evaluate');
|
||||
expect(names).toHaveLength(7);
|
||||
});
|
||||
|
||||
it('browser.navigate navigates to URL', async () => {
|
||||
@@ -147,6 +148,13 @@ describe('Browser tools', () => {
|
||||
expect(result.output).toBe('hello world');
|
||||
});
|
||||
|
||||
it('browser.evaluate aliases browser.eval behavior', async () => {
|
||||
const tool = getTool('browser.evaluate');
|
||||
const result = await tool.execute({ expression: '1 + 1' });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain('42');
|
||||
});
|
||||
|
||||
it('handles navigation errors gracefully', async () => {
|
||||
mockGoto.mockRejectedValueOnce(new Error('Navigation failed'));
|
||||
const tool = getTool('browser.navigate');
|
||||
|
||||
@@ -64,6 +64,7 @@ export function createBrowserTools(manager: BrowserManager): Tool[] {
|
||||
createBrowserTypeTool(manager),
|
||||
createBrowserContentTool(manager),
|
||||
createBrowserEvalTool(manager),
|
||||
createBrowserEvaluateTool(manager),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -294,9 +295,25 @@ function createBrowserContentTool(manager: BrowserManager): Tool {
|
||||
}
|
||||
|
||||
function createBrowserEvalTool(manager: BrowserManager): Tool {
|
||||
return createBrowserEvalLikeTool(
|
||||
manager,
|
||||
'browser.eval',
|
||||
'Evaluate JavaScript in the browser page context. Returns the result as a string.',
|
||||
);
|
||||
}
|
||||
|
||||
function createBrowserEvaluateTool(manager: BrowserManager): Tool {
|
||||
return createBrowserEvalLikeTool(
|
||||
manager,
|
||||
'browser.evaluate',
|
||||
'Alias of browser.eval for compatibility. Evaluates JavaScript in the browser page context.',
|
||||
);
|
||||
}
|
||||
|
||||
function createBrowserEvalLikeTool(manager: BrowserManager, name: 'browser.eval' | 'browser.evaluate', description: string): Tool {
|
||||
return {
|
||||
name: 'browser.eval',
|
||||
description: 'Evaluate JavaScript in the browser page context. Returns the result as a string.',
|
||||
name,
|
||||
description,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
+2
-1
@@ -98,6 +98,7 @@ const PROFILE_TOOLS: Record<ToolProfile, Set<string>> = {
|
||||
'browser.type',
|
||||
'browser.content',
|
||||
'browser.eval',
|
||||
'browser.evaluate',
|
||||
'agent.delegate',
|
||||
'agents.list',
|
||||
]),
|
||||
@@ -110,7 +111,7 @@ const PROFILE_TOOLS: Record<ToolProfile, Set<string>> = {
|
||||
export const TOOL_GROUPS: Record<string, string[]> = {
|
||||
'group:fs': ['file.read', 'file.write', 'file.edit', 'file.patch', 'file.list'],
|
||||
'group:runtime': ['shell.exec', 'process.start', 'process.output', 'process.status', 'process.kill', 'process.list', 'screen.capture', 'camera.capture'],
|
||||
'group:web': ['web.fetch', 'web.search', 'browser.navigate', 'browser.screenshot', 'browser.click', 'browser.type', 'browser.content', 'browser.eval'],
|
||||
'group:web': ['web.fetch', 'web.search', 'browser.navigate', 'browser.screenshot', 'browser.click', 'browser.type', 'browser.content', 'browser.eval', 'browser.evaluate'],
|
||||
'group:memory': ['memory.read', 'memory.write', 'memory.search'],
|
||||
'group:gmail': ['gmail.list', 'gmail.search', 'gmail.read'],
|
||||
'group:gcal': ['calendar.today', 'calendar.list', 'calendar.search'],
|
||||
|
||||
Reference in New Issue
Block a user