Unify TUI slash commands and harden tool inventory responses
This commit is contained in:
@@ -520,6 +520,7 @@ pnpm tui:fs
|
|||||||
| `/login [provider]` | Authenticate with GitHub (OAuth device flow) |
|
| `/login [provider]` | Authenticate with GitHub (OAuth device flow) |
|
||||||
| `/reset` | Clear history |
|
| `/reset` | Clear history |
|
||||||
| `/status` | Show session info |
|
| `/status` | Show session info |
|
||||||
|
| `/tools` | Show authoritative runtime tool list for this session |
|
||||||
| `/research <task>` | Delegate a task to `agent_configs.research` |
|
| `/research <task>` | Delegate a task to `agent_configs.research` |
|
||||||
| `/council <task>` | Run dual D/P councils pipeline with bridge+meta merge |
|
| `/council <task>` | Run dual D/P councils pipeline with bridge+meta merge |
|
||||||
| `/compact` | Compact conversation context |
|
| `/compact` | Compact conversation context |
|
||||||
|
|||||||
@@ -3,6 +3,31 @@
|
|||||||
"updated_at": "2026-02-21",
|
"updated_at": "2026-02-21",
|
||||||
"description": "Tracks the status of all Flynn plans and implementation phases",
|
"description": "Tracks the status of all Flynn plans and implementation phases",
|
||||||
"plans": {
|
"plans": {
|
||||||
|
"slash-command-parity-and-authoritative-tools": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-21",
|
||||||
|
"updated": "2026-02-21",
|
||||||
|
"summary": "Unified slash command behavior across minimal and fullscreen TUI by adding `/tools`, `/research`, and `/council` command parity, wiring shared callbacks, and registering `council.run` in TUI startup when councils are enabled. Added deterministic capability-query handling so tool-inventory prompts return authoritative runtime tool lists instead of model-invented answers.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/frontends/tui/commands.ts",
|
||||||
|
"src/frontends/tui/commands.test.ts",
|
||||||
|
"src/frontends/tui/minimal.ts",
|
||||||
|
"src/frontends/tui/minimal.test.ts",
|
||||||
|
"src/frontends/tui/fullscreen.ts",
|
||||||
|
"src/frontends/tui/components/App.tsx",
|
||||||
|
"src/commands/types.ts",
|
||||||
|
"src/commands/builtin/index.ts",
|
||||||
|
"src/commands/builtin/index.test.ts",
|
||||||
|
"src/commands/index.ts",
|
||||||
|
"src/daemon/routing.ts",
|
||||||
|
"src/daemon/routing.test.ts",
|
||||||
|
"src/cli/tui.ts",
|
||||||
|
"src/backends/native/agent.ts",
|
||||||
|
"README.md",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm test:run src/commands/builtin/index.test.ts src/daemon/routing.test.ts src/frontends/tui/commands.test.ts src/frontends/tui/minimal.test.ts + pnpm typecheck passing"
|
||||||
|
},
|
||||||
"faster-inflight-cancel-propagation": {
|
"faster-inflight-cancel-propagation": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"date": "2026-02-19",
|
"date": "2026-02-19",
|
||||||
|
|||||||
@@ -567,6 +567,16 @@ export class NativeAgent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAvailableToolNames(): string[] {
|
||||||
|
if (!this.toolRegistry) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.toolRegistry
|
||||||
|
.filteredList(this._toolPolicyContext)
|
||||||
|
.map((tool) => tool.name)
|
||||||
|
.sort();
|
||||||
|
}
|
||||||
|
|
||||||
setAttachmentCollector(collector: OutboundAttachmentCollector | undefined): void {
|
setAttachmentCollector(collector: OutboundAttachmentCollector | undefined): void {
|
||||||
this._attachmentCollector = collector;
|
this._attachmentCollector = collector;
|
||||||
}
|
}
|
||||||
|
|||||||
+88
-15
@@ -1,5 +1,5 @@
|
|||||||
import type { Command } from 'commander';
|
import type { Command } from 'commander';
|
||||||
import type { Config, ModelConfig, ModelProvider } from '../config/index.js';
|
import type { Config, CouncilsConfig, ModelConfig, ModelProvider } from '../config/index.js';
|
||||||
import { loadConfigSafe, getConfigPath } from './shared.js';
|
import { loadConfigSafe, getConfigPath } from './shared.js';
|
||||||
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
@@ -110,12 +110,14 @@ export function registerTuiCommand(program: Command): void {
|
|||||||
createGtasksTools,
|
createGtasksTools,
|
||||||
createAgentsListTool,
|
createAgentsListTool,
|
||||||
createAgentDelegateTool,
|
createAgentDelegateTool,
|
||||||
|
createCouncilRunTool,
|
||||||
} = await import('../tools/index.js');
|
} = await import('../tools/index.js');
|
||||||
const { HookEngine } = await import('../hooks/index.js');
|
const { HookEngine } = await import('../hooks/index.js');
|
||||||
const { Lifecycle } = await import('../daemon/lifecycle.js');
|
const { Lifecycle } = await import('../daemon/lifecycle.js');
|
||||||
const { initTools } = await import('../daemon/tools.js');
|
const { initTools } = await import('../daemon/tools.js');
|
||||||
const { createModelRouter } = await import('../daemon/index.js');
|
const { createModelRouter } = await import('../daemon/index.js');
|
||||||
const { AgentConfigRegistry } = await import('../agents/index.js');
|
const { AgentConfigRegistry } = await import('../agents/index.js');
|
||||||
|
const { loadCouncilScaffoldSafe } = await import('../councils/scaffold.js');
|
||||||
|
|
||||||
const dataDir = process.env.FLYNN_DATA_DIR ?? resolve(homedir(), '.local/share/flynn');
|
const dataDir = process.env.FLYNN_DATA_DIR ?? resolve(homedir(), '.local/share/flynn');
|
||||||
mkdirSync(dataDir, { recursive: true });
|
mkdirSync(dataDir, { recursive: true });
|
||||||
@@ -182,25 +184,39 @@ export function registerTuiCommand(program: Command): void {
|
|||||||
|
|
||||||
const agentConfigRegistry = new AgentConfigRegistry();
|
const agentConfigRegistry = new AgentConfigRegistry();
|
||||||
agentConfigRegistry.loadFromConfig(config.agent_configs);
|
agentConfigRegistry.loadFromConfig(config.agent_configs);
|
||||||
|
const delegateRunner = {
|
||||||
|
async delegate(request: {
|
||||||
|
tier: 'fast' | 'default' | 'complex' | 'local';
|
||||||
|
systemPrompt: string;
|
||||||
|
message: string;
|
||||||
|
maxTokens?: number;
|
||||||
|
}) {
|
||||||
|
const response = await modelRouter.chat({
|
||||||
|
messages: [{ role: 'user', content: request.message }],
|
||||||
|
system: request.systemPrompt,
|
||||||
|
maxTokens: request.maxTokens,
|
||||||
|
}, request.tier);
|
||||||
|
return {
|
||||||
|
content: response.content,
|
||||||
|
usage: response.usage,
|
||||||
|
tier: request.tier,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
if (agentConfigRegistry.list().length > 0) {
|
if (agentConfigRegistry.list().length > 0) {
|
||||||
toolRegistry.register(createAgentsListTool(agentConfigRegistry));
|
toolRegistry.register(createAgentsListTool(agentConfigRegistry));
|
||||||
toolRegistry.register(createAgentDelegateTool({
|
toolRegistry.register(createAgentDelegateTool({
|
||||||
registry: agentConfigRegistry,
|
registry: agentConfigRegistry,
|
||||||
orchestrator: {
|
orchestrator: delegateRunner,
|
||||||
async delegate(request) {
|
|
||||||
const response = await modelRouter.chat({
|
|
||||||
messages: [{ role: 'user', content: request.message }],
|
|
||||||
system: request.systemPrompt,
|
|
||||||
maxTokens: request.maxTokens,
|
|
||||||
}, request.tier);
|
|
||||||
return {
|
|
||||||
content: response.content,
|
|
||||||
usage: response.usage,
|
|
||||||
tier: request.tier,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
if (config.councils?.enabled) {
|
||||||
|
toolRegistry.register(createCouncilRunTool({
|
||||||
|
registry: agentConfigRegistry,
|
||||||
|
orchestrator: delegateRunner,
|
||||||
|
config: config.councils as CouncilsConfig,
|
||||||
|
scaffold: loadCouncilScaffoldSafe(config.councils.scaffold_path),
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = sessionManager.getSession('tui', 'local');
|
const session = sessionManager.getSession('tui', 'local');
|
||||||
@@ -296,6 +312,54 @@ export function registerTuiCommand(program: Command): void {
|
|||||||
return `Unknown transfer target: ${target}. Supported targets: tui, telegram`;
|
return `Unknown transfer target: ${target}. Supported targets: tui, telegram`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listAvailableTools = (): string => {
|
||||||
|
const names = toolRegistry.filteredList().map((tool) => tool.name).sort();
|
||||||
|
return [
|
||||||
|
`Available tools (${names.length}):`,
|
||||||
|
...names.map((name) => `- ${name}`),
|
||||||
|
].join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
const delegateToResearchAgent = async (task: string): Promise<string> => {
|
||||||
|
const message = task.trim();
|
||||||
|
if (!message) {
|
||||||
|
return 'Usage: /research <question or task>';
|
||||||
|
}
|
||||||
|
const agentConfig = agentConfigRegistry.get('research');
|
||||||
|
if (!agentConfig) {
|
||||||
|
return 'Agent "research" not found. Configure agent_configs.research first.';
|
||||||
|
}
|
||||||
|
const tier = agentConfig.modelTier ?? 'default';
|
||||||
|
const systemPrompt = agentConfig.systemPrompt
|
||||||
|
?? 'You are a research sub-agent. Produce concise, source-grounded findings.';
|
||||||
|
const result = await delegateRunner.delegate({
|
||||||
|
tier,
|
||||||
|
systemPrompt,
|
||||||
|
message,
|
||||||
|
maxTokens: 4096,
|
||||||
|
});
|
||||||
|
return `[Agent: research | Tier: ${result.tier} | Tokens: ${result.usage.inputTokens}+${result.usage.outputTokens}]\n\n${result.content}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const runCouncilTask = async (task: string): Promise<string> => {
|
||||||
|
const message = task.trim();
|
||||||
|
if (!message) {
|
||||||
|
return 'Usage: /council <question or task>';
|
||||||
|
}
|
||||||
|
if (!config.councils?.enabled) {
|
||||||
|
return 'Councils are disabled. Set councils.enabled: true in config.';
|
||||||
|
}
|
||||||
|
const tool = toolRegistry.get('council.run');
|
||||||
|
if (!tool) {
|
||||||
|
return 'Council tool is not registered. Verify councils config and restart Flynn.';
|
||||||
|
}
|
||||||
|
const result = await tool.execute({ task: message });
|
||||||
|
if (!result.success) {
|
||||||
|
return `Council run failed: ${result.error ?? 'unknown error'}`;
|
||||||
|
}
|
||||||
|
return result.output;
|
||||||
|
};
|
||||||
|
|
||||||
if (opts.fullscreen) {
|
if (opts.fullscreen) {
|
||||||
await startFullscreenTui({
|
await startFullscreenTui({
|
||||||
session,
|
session,
|
||||||
@@ -311,6 +375,9 @@ export function registerTuiCommand(program: Command): void {
|
|||||||
modelProviderConfigs,
|
modelProviderConfigs,
|
||||||
contextThresholdPct: config.compaction.threshold_pct,
|
contextThresholdPct: config.compaction.threshold_pct,
|
||||||
onTransfer: transferSessionToTarget,
|
onTransfer: transferSessionToTarget,
|
||||||
|
onTools: listAvailableTools,
|
||||||
|
onResearch: delegateToResearchAgent,
|
||||||
|
onCouncil: runCouncilTask,
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
void cleanup();
|
void cleanup();
|
||||||
},
|
},
|
||||||
@@ -326,6 +393,9 @@ export function registerTuiCommand(program: Command): void {
|
|||||||
agent,
|
agent,
|
||||||
hookEngine,
|
hookEngine,
|
||||||
pairingManager,
|
pairingManager,
|
||||||
|
onTools: listAvailableTools,
|
||||||
|
onResearch: delegateToResearchAgent,
|
||||||
|
onCouncil: runCouncilTask,
|
||||||
localProviders: config.models.local_providers,
|
localProviders: config.models.local_providers,
|
||||||
modelProviderConfigs,
|
modelProviderConfigs,
|
||||||
contextThresholdPct: config.compaction.threshold_pct,
|
contextThresholdPct: config.compaction.threshold_pct,
|
||||||
@@ -357,6 +427,9 @@ export function registerTuiCommand(program: Command): void {
|
|||||||
modelProviderConfigs,
|
modelProviderConfigs,
|
||||||
contextThresholdPct: config.compaction.threshold_pct,
|
contextThresholdPct: config.compaction.threshold_pct,
|
||||||
onTransfer: transferSessionToTarget,
|
onTransfer: transferSessionToTarget,
|
||||||
|
onTools: listAvailableTools,
|
||||||
|
onResearch: delegateToResearchAgent,
|
||||||
|
onCouncil: runCouncilTask,
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
void cleanup();
|
void cleanup();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
|
||||||
import { createApproveCommand, createApprovalsCommand, createContextCommand, createCouncilCommand, createDenyCommand, createElevateCommand, createModelCommand, createQueueCommand, createResearchCommand, createSkillCommand, createStopCommand, createTransferCommand } from './index.js';
|
import { createApproveCommand, createApprovalsCommand, createContextCommand, createCouncilCommand, createDenyCommand, createElevateCommand, createModelCommand, createQueueCommand, createResearchCommand, createSkillCommand, createStopCommand, createToolsCommand, createTransferCommand } from './index.js';
|
||||||
|
|
||||||
describe('builtin /model command', () => {
|
describe('builtin /model command', () => {
|
||||||
it('passes through the full argument string', async () => {
|
it('passes through the full argument string', async () => {
|
||||||
@@ -198,6 +198,34 @@ describe('builtin /context command', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('builtin /tools command', () => {
|
||||||
|
it('returns tools text from services', async () => {
|
||||||
|
const cmd = createToolsCommand();
|
||||||
|
const getTools = vi.fn(() => 'Available tools (2):\n- file.read\n- web.fetch');
|
||||||
|
const result = await cmd.execute([], {
|
||||||
|
channel: 'test',
|
||||||
|
senderId: 'user',
|
||||||
|
sessionId: 's1',
|
||||||
|
rawInput: '/tools',
|
||||||
|
services: { getTools },
|
||||||
|
});
|
||||||
|
expect(getTools).toHaveBeenCalledOnce();
|
||||||
|
expect(result).toEqual({ handled: true, text: 'Available tools (2):\n- file.read\n- web.fetch' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns not-available when service is missing', async () => {
|
||||||
|
const cmd = createToolsCommand();
|
||||||
|
const result = await cmd.execute([], {
|
||||||
|
channel: 'test',
|
||||||
|
senderId: 'user',
|
||||||
|
sessionId: 's1',
|
||||||
|
rawInput: '/tools',
|
||||||
|
services: {},
|
||||||
|
});
|
||||||
|
expect(result).toEqual({ handled: true, text: 'Tools command is not available in this session.' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('builtin /transfer command', () => {
|
describe('builtin /transfer command', () => {
|
||||||
it('passes through the full target argument string', async () => {
|
it('passes through the full target argument string', async () => {
|
||||||
const cmd = createTransferCommand();
|
const cmd = createTransferCommand();
|
||||||
|
|||||||
@@ -48,6 +48,22 @@ export function createStatusCommand(): CommandDefinition {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createToolsCommand(): CommandDefinition {
|
||||||
|
return {
|
||||||
|
name: 'tools',
|
||||||
|
description: 'Show available tools in this session',
|
||||||
|
execute: async (_args, ctx) => {
|
||||||
|
if (!ctx.services?.getTools) {
|
||||||
|
return notAvailable('Tools command');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
text: await ctx.services.getTools(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function createUsageCommand(): CommandDefinition {
|
export function createUsageCommand(): CommandDefinition {
|
||||||
return {
|
return {
|
||||||
name: 'usage',
|
name: 'usage',
|
||||||
@@ -343,6 +359,7 @@ export function createSkillCommand(): CommandDefinition {
|
|||||||
export function registerBuiltinCommands(registry: CommandRegistry): void {
|
export function registerBuiltinCommands(registry: CommandRegistry): void {
|
||||||
registry.register(createHelpCommand(registry));
|
registry.register(createHelpCommand(registry));
|
||||||
registry.register(createStatusCommand());
|
registry.register(createStatusCommand());
|
||||||
|
registry.register(createToolsCommand());
|
||||||
registry.register(createUsageCommand());
|
registry.register(createUsageCommand());
|
||||||
registry.register(createContextCommand());
|
registry.register(createContextCommand());
|
||||||
registry.register(createResearchCommand());
|
registry.register(createResearchCommand());
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export type { CommandContext, CommandDefinition, CommandResult, CommandServices
|
|||||||
export {
|
export {
|
||||||
createHelpCommand,
|
createHelpCommand,
|
||||||
createStatusCommand,
|
createStatusCommand,
|
||||||
|
createToolsCommand,
|
||||||
createUsageCommand,
|
createUsageCommand,
|
||||||
createContextCommand,
|
createContextCommand,
|
||||||
createModelCommand,
|
createModelCommand,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface CommandDefinition {
|
|||||||
|
|
||||||
export interface CommandServices {
|
export interface CommandServices {
|
||||||
getStatus?: () => Promise<string> | string;
|
getStatus?: () => Promise<string> | string;
|
||||||
|
getTools?: () => Promise<string> | string;
|
||||||
getUsage?: () => Promise<string> | string;
|
getUsage?: () => Promise<string> | string;
|
||||||
getContext?: () => Promise<string> | string;
|
getContext?: () => Promise<string> | string;
|
||||||
getModel?: () => Promise<string> | string;
|
getModel?: () => Promise<string> | string;
|
||||||
|
|||||||
@@ -1062,6 +1062,78 @@ describe('daemon external backend integration', () => {
|
|||||||
expect(reply).toHaveBeenCalledWith(expect.objectContaining({ text: 'external backend response' }));
|
expect(reply).toHaveBeenCalledWith(expect.objectContaining({ text: 'external backend response' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('forces native processing for capability/tool inventory queries', async () => {
|
||||||
|
const processSpy = vi.spyOn(AgentOrchestrator.prototype, 'process')
|
||||||
|
.mockResolvedValue('Available tools (authoritative):\n- file.read');
|
||||||
|
const history: Array<{ role: 'user' | 'assistant'; content: string }> = [];
|
||||||
|
const session = {
|
||||||
|
id: 'telegram:external-tools-query',
|
||||||
|
addMessage: vi.fn((msg: { role: 'user' | 'assistant'; content: string }) => {
|
||||||
|
history.push(msg);
|
||||||
|
return msg;
|
||||||
|
}),
|
||||||
|
getHistory: vi.fn(() => [...history]),
|
||||||
|
clear: vi.fn(),
|
||||||
|
replaceHistory: vi.fn(),
|
||||||
|
getConfig: vi.fn(() => undefined),
|
||||||
|
setConfig: vi.fn(),
|
||||||
|
deleteConfig: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const externalBackend = {
|
||||||
|
name: 'codex',
|
||||||
|
process: vi.fn(async () => 'external backend response'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createMessageRouter({
|
||||||
|
sessionManager: {
|
||||||
|
getSession: vi.fn(() => session),
|
||||||
|
} as unknown as MessageRouterDeps['sessionManager'],
|
||||||
|
modelRouter: {
|
||||||
|
getAvailableTiers: () => ['fast', 'default', 'complex', 'local'],
|
||||||
|
getAllLabels: () => ({ fast: 'fast', default: 'default', complex: 'complex', local: 'local' }),
|
||||||
|
getLabel: (tier: string) => tier,
|
||||||
|
} as unknown as MessageRouterDeps['modelRouter'],
|
||||||
|
systemPrompt: 'test prompt',
|
||||||
|
toolRegistry: {
|
||||||
|
clone() { return this; },
|
||||||
|
register: vi.fn(),
|
||||||
|
} as unknown as MessageRouterDeps['toolRegistry'],
|
||||||
|
toolExecutor: {} as unknown as MessageRouterDeps['toolExecutor'],
|
||||||
|
config: {
|
||||||
|
agents: {
|
||||||
|
primary_tier: 'default',
|
||||||
|
delegation: {
|
||||||
|
compaction: 'fast',
|
||||||
|
memory_extraction: 'fast',
|
||||||
|
classification: 'fast',
|
||||||
|
tool_summarisation: 'fast',
|
||||||
|
complex_reasoning: 'complex',
|
||||||
|
},
|
||||||
|
max_delegation_depth: 3,
|
||||||
|
max_iterations: 10,
|
||||||
|
},
|
||||||
|
compaction: { enabled: false },
|
||||||
|
models: { default: { provider: 'anthropic', model: 'claude' } },
|
||||||
|
} as unknown as MessageRouterDeps['config'],
|
||||||
|
externalBackends: { codex: externalBackend } as unknown as MessageRouterDeps['externalBackends'],
|
||||||
|
defaultName: 'codex',
|
||||||
|
});
|
||||||
|
|
||||||
|
const reply = vi.fn(async (_message: OutboundMessage) => {});
|
||||||
|
await router.handler({
|
||||||
|
id: 'm-external-tools-query',
|
||||||
|
channel: 'telegram',
|
||||||
|
senderId: 'external-tools-query',
|
||||||
|
text: 'check your available tools',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
} as MessageRouterInput, reply);
|
||||||
|
|
||||||
|
expect(externalBackend.process).not.toHaveBeenCalled();
|
||||||
|
expect(processSpy).toHaveBeenCalled();
|
||||||
|
expect(reply).toHaveBeenCalledWith(expect.objectContaining({ text: 'Available tools (authoritative):\n- file.read' }));
|
||||||
|
});
|
||||||
|
|
||||||
it('falls back to native processing when external backend fails', async () => {
|
it('falls back to native processing when external backend fails', async () => {
|
||||||
const processSpy = vi.spyOn(AgentOrchestrator.prototype, 'process')
|
const processSpy = vi.spyOn(AgentOrchestrator.prototype, 'process')
|
||||||
.mockResolvedValue('native fallback response');
|
.mockResolvedValue('native fallback response');
|
||||||
|
|||||||
+36
-2
@@ -140,6 +140,24 @@ function parseResearchPrefix(text: string): string | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldForceNativeForCapabilityQuery(text: string): boolean {
|
||||||
|
const normalized = text.trim().toLowerCase();
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
normalized.includes('available tools')
|
||||||
|
|| normalized.includes('what tools')
|
||||||
|
|| normalized.includes('which tools')
|
||||||
|
|| normalized.includes('tool list')
|
||||||
|
|| normalized.includes('list tools')
|
||||||
|
|| normalized.includes('your tools')
|
||||||
|
|| normalized.includes('what can you do')
|
||||||
|
|| normalized.includes('can you do')
|
||||||
|
|| normalized.includes('capabilities')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isTtsEnabledForChannel(config: Config, channel: string): boolean {
|
function isTtsEnabledForChannel(config: Config, channel: string): boolean {
|
||||||
if (!config.tts?.enabled) {
|
if (!config.tts?.enabled) {
|
||||||
return false;
|
return false;
|
||||||
@@ -649,6 +667,21 @@ export function createMessageRouter(deps: {
|
|||||||
: 'native';
|
: 'native';
|
||||||
return `Flynn is running. Active model tier: ${agent.getModelTier()}. Backend: ${backend}`;
|
return `Flynn is running. Active model tier: ${agent.getModelTier()}. Backend: ${backend}`;
|
||||||
},
|
},
|
||||||
|
getTools: () => {
|
||||||
|
const names = new Set(deps.toolRegistry.list().map((tool: Tool) => tool.name));
|
||||||
|
names.add('media.send');
|
||||||
|
if (deps.agentConfigRegistry && deps.agentConfigRegistry.list().length > 0) {
|
||||||
|
names.add('agent.delegate');
|
||||||
|
if (deps.config.councils?.enabled) {
|
||||||
|
names.add('council.run');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sorted = [...names].sort();
|
||||||
|
return [
|
||||||
|
`Available tools (${sorted.length}):`,
|
||||||
|
...sorted.map((name) => `- ${name}`),
|
||||||
|
].join('\n');
|
||||||
|
},
|
||||||
getUsage: () => {
|
getUsage: () => {
|
||||||
const usage = agent.getUsage();
|
const usage = agent.getUsage();
|
||||||
const lines = [
|
const lines = [
|
||||||
@@ -1260,11 +1293,12 @@ export function createMessageRouter(deps: {
|
|||||||
// buildUserMessage() in the agent will create native audio content parts
|
// buildUserMessage() in the agent will create native audio content parts
|
||||||
|
|
||||||
const requestedBackend = agentConfig?.backend ?? deps.defaultName;
|
const requestedBackend = agentConfig?.backend ?? deps.defaultName;
|
||||||
|
const forceNativeForCapabilityQuery = shouldForceNativeForCapabilityQuery(messageText);
|
||||||
const sessionIdForAudit = `${msg.channel}:${msg.senderId}`;
|
const sessionIdForAudit = `${msg.channel}:${msg.senderId}`;
|
||||||
const selectedBackend = requestedBackend && requestedBackend !== 'native'
|
const selectedBackend = requestedBackend && requestedBackend !== 'native'
|
||||||
? deps.externalBackends?.[requestedBackend]
|
? deps.externalBackends?.[requestedBackend]
|
||||||
: undefined;
|
: undefined;
|
||||||
const selectedBackendForAudit: 'native' | ExternalBackendName = selectedBackend && requestedBackend
|
const selectedBackendForAudit: 'native' | ExternalBackendName = selectedBackend && requestedBackend && !forceNativeForCapabilityQuery
|
||||||
? requestedBackend
|
? requestedBackend
|
||||||
: 'native';
|
: 'native';
|
||||||
|
|
||||||
@@ -1280,7 +1314,7 @@ export function createMessageRouter(deps: {
|
|||||||
: 'native',
|
: 'native',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (selectedBackend && (!attachments || attachments.length === 0)) {
|
if (selectedBackend && (!attachments || attachments.length === 0) && !forceNativeForCapabilityQuery) {
|
||||||
try {
|
try {
|
||||||
const history = toExternalHistory(session.getHistory());
|
const history = toExternalHistory(session.getHistory());
|
||||||
session.addMessage({ role: 'user', content: messageText });
|
session.addMessage({ role: 'user', content: messageText });
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { parseCommand, getHelpText, getCommandCompletions, PROVIDER_NAMES } from './commands.js';
|
import { parseCommand, getHelpText, getCommandCompletions, isToolInventoryQuery, PROVIDER_NAMES } from './commands.js';
|
||||||
import { MODEL_PROVIDERS } from '../../config/index.js';
|
import { MODEL_PROVIDERS } from '../../config/index.js';
|
||||||
|
|
||||||
describe('parseCommand', () => {
|
describe('parseCommand', () => {
|
||||||
@@ -22,6 +22,20 @@ describe('parseCommand', () => {
|
|||||||
expect(parseCommand('/status')).toEqual({ type: 'status' });
|
expect(parseCommand('/status')).toEqual({ type: 'status' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('parses /tools command', () => {
|
||||||
|
expect(parseCommand('/tools')).toEqual({ type: 'tools' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses /research command', () => {
|
||||||
|
expect(parseCommand('/research compare k3s and k0s')).toEqual({ type: 'research', task: 'compare k3s and k0s' });
|
||||||
|
expect(parseCommand('/research')).toEqual({ type: 'research', task: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses /council command', () => {
|
||||||
|
expect(parseCommand('/council design backup plan')).toEqual({ type: 'council', task: 'design backup plan' });
|
||||||
|
expect(parseCommand('/council')).toEqual({ type: 'council', task: '' });
|
||||||
|
});
|
||||||
|
|
||||||
it('parses /fullscreen command', () => {
|
it('parses /fullscreen command', () => {
|
||||||
expect(parseCommand('/fullscreen')).toEqual({ type: 'fullscreen' });
|
expect(parseCommand('/fullscreen')).toEqual({ type: 'fullscreen' });
|
||||||
expect(parseCommand('/fs')).toEqual({ type: 'fullscreen' });
|
expect(parseCommand('/fs')).toEqual({ type: 'fullscreen' });
|
||||||
@@ -119,6 +133,9 @@ describe('getHelpText', () => {
|
|||||||
const help = getHelpText();
|
const help = getHelpText();
|
||||||
expect(help).toContain('/help');
|
expect(help).toContain('/help');
|
||||||
expect(help).toContain('/model');
|
expect(help).toContain('/model');
|
||||||
|
expect(help).toContain('/tools');
|
||||||
|
expect(help).toContain('/research');
|
||||||
|
expect(help).toContain('/council');
|
||||||
expect(help).toContain('/reset');
|
expect(help).toContain('/reset');
|
||||||
expect(help).toContain('/compact');
|
expect(help).toContain('/compact');
|
||||||
expect(help).toContain('/usage');
|
expect(help).toContain('/usage');
|
||||||
@@ -181,3 +198,16 @@ describe('getCommandCompletions', () => {
|
|||||||
expect(completions).toEqual(['/model fast xai']);
|
expect(completions).toEqual(['/model fast xai']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isToolInventoryQuery', () => {
|
||||||
|
it('detects common capability/tool-list prompts', () => {
|
||||||
|
expect(isToolInventoryQuery('Check out your new tools')).toBe(true);
|
||||||
|
expect(isToolInventoryQuery('what tools do you have?')).toBe(true);
|
||||||
|
expect(isToolInventoryQuery('show capabilities')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not match unrelated prompts', () => {
|
||||||
|
expect(isToolInventoryQuery('write a shell script')).toBe(false);
|
||||||
|
expect(isToolInventoryQuery('summarize this doc')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ export type Command =
|
|||||||
| { type: 'reset' }
|
| { type: 'reset' }
|
||||||
| { type: 'help' }
|
| { type: 'help' }
|
||||||
| { type: 'status' }
|
| { type: 'status' }
|
||||||
|
| { type: 'tools' }
|
||||||
|
| { type: 'research'; task: string }
|
||||||
|
| { type: 'council'; task: string }
|
||||||
| { type: 'fullscreen' }
|
| { type: 'fullscreen' }
|
||||||
| { type: 'compact' }
|
| { type: 'compact' }
|
||||||
| { type: 'usage' }
|
| { type: 'usage' }
|
||||||
@@ -17,6 +20,28 @@ export type Command =
|
|||||||
| { type: 'elevate'; args?: string }
|
| { type: 'elevate'; args?: string }
|
||||||
| { type: 'message'; content: string };
|
| { type: 'message'; content: string };
|
||||||
|
|
||||||
|
export function isToolInventoryQuery(input: string): boolean {
|
||||||
|
const normalized = input.trim().toLowerCase();
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasToolsWord = /\btools?\b/.test(normalized);
|
||||||
|
const hasInventoryIntent = /\b(check|show|list|what|which|available|new|have)\b/.test(normalized);
|
||||||
|
return (
|
||||||
|
normalized.includes('available tools')
|
||||||
|
|| normalized.includes('what tools')
|
||||||
|
|| normalized.includes('which tools')
|
||||||
|
|| normalized.includes('tool list')
|
||||||
|
|| normalized.includes('list tools')
|
||||||
|
|| normalized.includes('new tools')
|
||||||
|
|| normalized.includes('your tools')
|
||||||
|
|| normalized.includes('what can you do')
|
||||||
|
|| normalized.includes('can you do')
|
||||||
|
|| normalized.includes('capabilities')
|
||||||
|
|| (hasToolsWord && hasInventoryIntent)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function parseCommand(input: string): Command | null {
|
export function parseCommand(input: string): Command | null {
|
||||||
const trimmed = input.trim();
|
const trimmed = input.trim();
|
||||||
if (!trimmed) {return null;}
|
if (!trimmed) {return null;}
|
||||||
@@ -41,6 +66,29 @@ export function parseCommand(input: string): Command | null {
|
|||||||
return { type: 'status' };
|
return { type: 'status' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tools
|
||||||
|
if (trimmed === '/tools') {
|
||||||
|
return { type: 'tools' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Research
|
||||||
|
if (trimmed.startsWith('/research ')) {
|
||||||
|
const task = trimmed.slice('/research '.length).trim();
|
||||||
|
return { type: 'research', task };
|
||||||
|
}
|
||||||
|
if (trimmed === '/research') {
|
||||||
|
return { type: 'research', task: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Council
|
||||||
|
if (trimmed.startsWith('/council ')) {
|
||||||
|
const task = trimmed.slice('/council '.length).trim();
|
||||||
|
return { type: 'council', task };
|
||||||
|
}
|
||||||
|
if (trimmed === '/council') {
|
||||||
|
return { type: 'council', task: '' };
|
||||||
|
}
|
||||||
|
|
||||||
// Fullscreen
|
// Fullscreen
|
||||||
if (trimmed === '/fullscreen' || trimmed === '/fs') {
|
if (trimmed === '/fullscreen' || trimmed === '/fs') {
|
||||||
return { type: 'fullscreen' };
|
return { type: 'fullscreen' };
|
||||||
@@ -157,9 +205,12 @@ export function getHelpText(): string {
|
|||||||
return `
|
return `
|
||||||
Commands:
|
Commands:
|
||||||
/help, /? Show this help
|
/help, /? Show this help
|
||||||
|
/tools Show available tools in this session
|
||||||
/model [name] Show or switch model tier (local, default, fast, complex)
|
/model [name] Show or switch model tier (local, default, fast, complex)
|
||||||
/model <tier> <p/m> Change tier's provider/model (e.g. /model default anthropic/claude-sonnet-4)
|
/model <tier> <p/m> Change tier's provider/model (e.g. /model default anthropic/claude-sonnet-4)
|
||||||
/backend [provider] Show or switch local backend (ollama, llamacpp)
|
/backend [provider] Show or switch local backend (ollama, llamacpp)
|
||||||
|
/research <task> Delegate a task to the configured research agent
|
||||||
|
/council <task> Run the councils pipeline for a task
|
||||||
/login [provider] Authenticate with GitHub, OpenAI, Anthropic, or Z.AI
|
/login [provider] Authenticate with GitHub, OpenAI, Anthropic, or Z.AI
|
||||||
/pair List pending pairing codes and approved senders
|
/pair List pending pairing codes and approved senders
|
||||||
/pair generate [label] Generate a new DM pairing code
|
/pair generate [label] Generate a new DM pairing code
|
||||||
@@ -190,8 +241,11 @@ export type ModelAlias = 'local' | 'default' | 'fast' | 'complex' | 'opus' | 'so
|
|||||||
// List of all slash commands for autocompletion
|
// List of all slash commands for autocompletion
|
||||||
export const SLASH_COMMANDS = [
|
export const SLASH_COMMANDS = [
|
||||||
'/help',
|
'/help',
|
||||||
|
'/tools',
|
||||||
'/model',
|
'/model',
|
||||||
'/backend',
|
'/backend',
|
||||||
|
'/research',
|
||||||
|
'/council',
|
||||||
'/reset',
|
'/reset',
|
||||||
'/clear',
|
'/clear',
|
||||||
'/new',
|
'/new',
|
||||||
@@ -217,8 +271,11 @@ export const SLASH_COMMANDS = [
|
|||||||
// Command descriptions for tooltips
|
// Command descriptions for tooltips
|
||||||
export const COMMAND_TOOLTIPS: Record<string, string> = {
|
export const COMMAND_TOOLTIPS: Record<string, string> = {
|
||||||
'/help': 'Show available commands',
|
'/help': 'Show available commands',
|
||||||
|
'/tools': 'Show authoritative runtime tool list for this session',
|
||||||
'/model': 'Show or switch model (local, default, fast, complex)',
|
'/model': 'Show or switch model (local, default, fast, complex)',
|
||||||
'/backend': 'Show or switch local backend (ollama, llamacpp)',
|
'/backend': 'Show or switch local backend (ollama, llamacpp)',
|
||||||
|
'/research': 'Delegate a task to the configured research agent',
|
||||||
|
'/council': 'Run the councils pipeline for a task',
|
||||||
'/reset': 'Clear conversation history',
|
'/reset': 'Clear conversation history',
|
||||||
'/clear': 'Clear conversation history',
|
'/clear': 'Clear conversation history',
|
||||||
'/new': 'Start a new conversation',
|
'/new': 'Start a new conversation',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Box, Text, useApp, useInput } from 'ink';
|
|||||||
import { StatusBar } from './StatusBar.js';
|
import { StatusBar } from './StatusBar.js';
|
||||||
import { MessageList } from './MessageList.js';
|
import { MessageList } from './MessageList.js';
|
||||||
import { InputBar } from './InputBar.js';
|
import { InputBar } from './InputBar.js';
|
||||||
import { parseCommand, getHelpText, resolveModelAlias, getCommandCompletions } from '../commands.js';
|
import { parseCommand, getHelpText, resolveModelAlias, getCommandCompletions, isToolInventoryQuery } from '../commands.js';
|
||||||
import type { Message, ModelClient, TokenUsage } from '../../../models/types.js';
|
import type { Message, ModelClient, TokenUsage } from '../../../models/types.js';
|
||||||
import type { ModelRouter } from '../../../models/router.js';
|
import type { ModelRouter } from '../../../models/router.js';
|
||||||
import type { ManagedSession } from '../../../session/index.js';
|
import type { ManagedSession } from '../../../session/index.js';
|
||||||
@@ -59,6 +59,9 @@ export interface AppProps {
|
|||||||
modelProviderConfigs?: Partial<Record<ModelProvider, ModelConfig>>;
|
modelProviderConfigs?: Partial<Record<ModelProvider, ModelConfig>>;
|
||||||
contextThresholdPct?: number;
|
contextThresholdPct?: number;
|
||||||
onTransfer?: (target: string) => string | void;
|
onTransfer?: (target: string) => string | void;
|
||||||
|
onTools?: () => string;
|
||||||
|
onResearch?: (task: string) => Promise<string> | string;
|
||||||
|
onCouncil?: (task: string) => Promise<string> | string;
|
||||||
onExit?: () => void;
|
onExit?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +79,9 @@ export function App({
|
|||||||
modelProviderConfigs,
|
modelProviderConfigs,
|
||||||
contextThresholdPct,
|
contextThresholdPct,
|
||||||
onTransfer,
|
onTransfer,
|
||||||
|
onTools,
|
||||||
|
onResearch,
|
||||||
|
onCouncil,
|
||||||
onExit,
|
onExit,
|
||||||
}: AppProps): React.ReactElement {
|
}: AppProps): React.ReactElement {
|
||||||
const ensureTimestamp = useCallback((message: Message): Message => ({
|
const ensureTimestamp = useCallback((message: Message): Message => ({
|
||||||
@@ -603,6 +609,41 @@ export function App({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'tools': {
|
||||||
|
if (!onTools) {
|
||||||
|
pushAssistantMessage('Tools command is not available in this TUI mode.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pushAssistantMessage(onTools());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'research': {
|
||||||
|
if (!command.task.trim()) {
|
||||||
|
pushAssistantMessage('Usage: /research <question or task>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!onResearch) {
|
||||||
|
pushAssistantMessage('Research command is not available in this TUI mode.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pushAssistantMessage(await onResearch(command.task));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'council': {
|
||||||
|
if (!command.task.trim()) {
|
||||||
|
pushAssistantMessage('Usage: /council <question or task>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!onCouncil) {
|
||||||
|
pushAssistantMessage('Council command is not available in this TUI mode.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pushAssistantMessage(await onCouncil(command.task));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
case 'pair': {
|
case 'pair': {
|
||||||
if (!pairingManager) {
|
if (!pairingManager) {
|
||||||
pushAssistantMessage('Pairing not enabled. Set pairing.enabled: true in config.');
|
pushAssistantMessage('Pairing not enabled. Set pairing.enabled: true in config.');
|
||||||
@@ -695,6 +736,11 @@ export function App({
|
|||||||
}
|
}
|
||||||
setScrollOffset(0);
|
setScrollOffset(0);
|
||||||
|
|
||||||
|
if (onTools && isToolInventoryQuery(command.content)) {
|
||||||
|
pushAssistantMessage(onTools());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsStreaming(true);
|
setIsStreaming(true);
|
||||||
setStreamingContent('');
|
setStreamingContent('');
|
||||||
toolLinesRef.current = [];
|
toolLinesRef.current = [];
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ export interface FullscreenTuiConfig {
|
|||||||
modelProviderConfigs?: Partial<Record<ModelProvider, ModelConfig>>;
|
modelProviderConfigs?: Partial<Record<ModelProvider, ModelConfig>>;
|
||||||
contextThresholdPct?: number;
|
contextThresholdPct?: number;
|
||||||
onTransfer?: (target: string) => string | void;
|
onTransfer?: (target: string) => string | void;
|
||||||
|
onTools?: () => string;
|
||||||
|
onResearch?: (task: string) => Promise<string> | string;
|
||||||
|
onCouncil?: (task: string) => Promise<string> | string;
|
||||||
onExit?: () => void;
|
onExit?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +54,9 @@ export async function startFullscreenTui(config: FullscreenTuiConfig): Promise<v
|
|||||||
modelProviderConfigs: config.modelProviderConfigs,
|
modelProviderConfigs: config.modelProviderConfigs,
|
||||||
contextThresholdPct: config.contextThresholdPct,
|
contextThresholdPct: config.contextThresholdPct,
|
||||||
onTransfer: config.onTransfer,
|
onTransfer: config.onTransfer,
|
||||||
|
onTools: config.onTools,
|
||||||
|
onResearch: config.onResearch,
|
||||||
|
onCouncil: config.onCouncil,
|
||||||
onExit: config.onExit,
|
onExit: config.onExit,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -371,6 +371,32 @@ describe('MinimalTui backend command', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('prints tools output when /tools is invoked', async () => {
|
||||||
|
const mockSession = {
|
||||||
|
id: 'test',
|
||||||
|
getHistory: () => [],
|
||||||
|
addMessage: vi.fn(),
|
||||||
|
clear: vi.fn(),
|
||||||
|
replaceHistory: vi.fn(),
|
||||||
|
};
|
||||||
|
const onTools = vi.fn(() => 'Available tools (2):\n- file.read\n- council.run');
|
||||||
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
try {
|
||||||
|
const tui = new MinimalTui({
|
||||||
|
session: asSession(mockSession),
|
||||||
|
modelClient: asModelClient({}),
|
||||||
|
systemPrompt: 'test',
|
||||||
|
onTools,
|
||||||
|
});
|
||||||
|
|
||||||
|
await minimalTuiPrivates(tui).handleCommand({ type: 'tools' });
|
||||||
|
expect(onTools).toHaveBeenCalledOnce();
|
||||||
|
expect(logSpy).toHaveBeenCalledWith('Available tools (2):\n- file.read\n- council.run\n');
|
||||||
|
} finally {
|
||||||
|
logSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('only renders tool activity when verbose mode is enabled', () => {
|
it('only renders tool activity when verbose mode is enabled', () => {
|
||||||
const mockSession = {
|
const mockSession = {
|
||||||
id: 'test',
|
id: 'test',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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 } from '../../models/router.js';
|
import type { ModelRouter } from '../../models/router.js';
|
||||||
import type { NativeAgent, ToolUseEvent } from '../../backends/native/agent.js';
|
import type { NativeAgent, ToolUseEvent } from '../../backends/native/agent.js';
|
||||||
import { parseCommand, getHelpText, resolveModelAlias, getCommandCompletions, getCommandTooltip, type Command } from './commands.js';
|
import { parseCommand, getHelpText, resolveModelAlias, getCommandCompletions, getCommandTooltip, isToolInventoryQuery, type Command } from './commands.js';
|
||||||
import { renderMarkdown } from './markdown.js';
|
import { renderMarkdown } from './markdown.js';
|
||||||
import type { ModelConfig, ModelProvider } from '../../config/schema.js';
|
import type { ModelConfig, ModelProvider } from '../../config/schema.js';
|
||||||
import { MODEL_PROVIDERS } from '../../config/schema.js';
|
import { MODEL_PROVIDERS } from '../../config/schema.js';
|
||||||
@@ -68,6 +68,9 @@ export interface MinimalTuiConfig {
|
|||||||
agent?: NativeAgent;
|
agent?: NativeAgent;
|
||||||
onFullscreen?: () => void;
|
onFullscreen?: () => void;
|
||||||
onTransfer?: (target: string) => string | void;
|
onTransfer?: (target: string) => string | void;
|
||||||
|
onTools?: () => string;
|
||||||
|
onResearch?: (task: string) => Promise<string> | string;
|
||||||
|
onCouncil?: (task: string) => Promise<string> | string;
|
||||||
localProviders?: Record<string, ModelConfig>;
|
localProviders?: Record<string, ModelConfig>;
|
||||||
modelProviderConfigs?: Partial<Record<ModelProvider, ModelConfig>>;
|
modelProviderConfigs?: Partial<Record<ModelProvider, ModelConfig>>;
|
||||||
currentLocalProvider?: string;
|
currentLocalProvider?: string;
|
||||||
@@ -444,6 +447,18 @@ export class MinimalTui {
|
|||||||
this.printStatus();
|
this.printStatus();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'tools':
|
||||||
|
this.handleToolsCommand();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'research':
|
||||||
|
await this.handleResearchCommand(command.task);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'council':
|
||||||
|
await this.handleCouncilCommand(command.task);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'fullscreen':
|
case 'fullscreen':
|
||||||
this.config.onFullscreen?.();
|
this.config.onFullscreen?.();
|
||||||
break;
|
break;
|
||||||
@@ -520,6 +535,41 @@ export class MinimalTui {
|
|||||||
this.printStatus();
|
this.printStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleToolsCommand(): void {
|
||||||
|
if (!this.config.onTools) {
|
||||||
|
console.log(`${colors.gray}Tools command is not available in this TUI mode.${colors.reset}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const output = this.config.onTools();
|
||||||
|
console.log(`${output}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleResearchCommand(task: string): Promise<void> {
|
||||||
|
if (!task.trim()) {
|
||||||
|
console.log(`${colors.gray}Usage: /research <question or task>${colors.reset}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.config.onResearch) {
|
||||||
|
console.log(`${colors.gray}Research command is not available in this TUI mode.${colors.reset}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const output = await this.config.onResearch(task);
|
||||||
|
console.log(`${output}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleCouncilCommand(task: string): Promise<void> {
|
||||||
|
if (!task.trim()) {
|
||||||
|
console.log(`${colors.gray}Usage: /council <question or task>${colors.reset}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.config.onCouncil) {
|
||||||
|
console.log(`${colors.gray}Council command is not available in this TUI mode.${colors.reset}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const output = await this.config.onCouncil(task);
|
||||||
|
console.log(`${output}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
private handleContextCommand(): void {
|
private handleContextCommand(): void {
|
||||||
const history = this.config.session.getHistory();
|
const history = this.config.session.getHistory();
|
||||||
const estimated = estimateMessageTokens(history);
|
const estimated = estimateMessageTokens(history);
|
||||||
@@ -1255,6 +1305,13 @@ export class MinimalTui {
|
|||||||
this.startBusyIndicator();
|
this.startBusyIndicator();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (this.config.onTools && isToolInventoryQuery(content)) {
|
||||||
|
this.stopBusyIndicator();
|
||||||
|
console.log(this.config.onTools());
|
||||||
|
console.log();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Use agent if available (supports tool loop)
|
// Use agent if available (supports tool loop)
|
||||||
if (this.config.agent) {
|
if (this.config.agent) {
|
||||||
let cancelRequested = false;
|
let cancelRequested = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user