feat(cli): TUI slash command parsing + local commands (P8-009) (#176)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #176.
This commit is contained in:
@@ -76,8 +76,8 @@
|
|||||||
| P7-021 | done | Phase 7 | Verify Phase 7 — feature-complete platform E2E | — | #132 W10 done |
|
| P7-021 | done | Phase 7 | Verify Phase 7 — feature-complete platform E2E | — | #132 W10 done |
|
||||||
| P8-005 | done | Phase 8 | CLI command architecture — DB schema + brain repo + gateway endpoints | #158 | |
|
| P8-005 | done | Phase 8 | CLI command architecture — DB schema + brain repo + gateway endpoints | #158 | |
|
||||||
| P8-006 | done | Phase 8 | CLI command architecture — agent, mission, prdy commands + TUI mods | #158 | |
|
| P8-006 | done | Phase 8 | CLI command architecture — agent, mission, prdy commands + TUI mods | #158 | |
|
||||||
| P8-007 | not-started | Phase 8 | DB migrations — preferences.mutable + teams + team_members + projects.teamId | — | #160 |
|
| P8-007 | done | Phase 8 | DB migrations — preferences.mutable + teams + team_members + projects.teamId | #175 | #160 |
|
||||||
| P8-008 | not-started | Phase 8 | @mosaic/types — CommandDef, CommandManifest, new socket events | — | #161 |
|
| P8-008 | done | Phase 8 | @mosaic/types — CommandDef, CommandManifest, new socket events | #174 | #161 |
|
||||||
| P8-009 | not-started | Phase 8 | TUI Phase 1 — slash command parsing, local commands, system message rendering, InputBar wiring | — | #162 |
|
| P8-009 | not-started | Phase 8 | TUI Phase 1 — slash command parsing, local commands, system message rendering, InputBar wiring | — | #162 |
|
||||||
| P8-010 | not-started | Phase 8 | Gateway Phase 2 — CommandRegistryService, CommandExecutorService, socket + REST commands | — | #163 |
|
| P8-010 | not-started | Phase 8 | Gateway Phase 2 — CommandRegistryService, CommandExecutorService, socket + REST commands | — | #163 |
|
||||||
| P8-011 | not-started | Phase 8 | Gateway Phase 3 — PreferencesService, /preferences REST, /system Valkey override, prompt injection | — | #164 |
|
| P8-011 | not-started | Phase 8 | Gateway Phase 3 — PreferencesService, /preferences REST, /system Valkey override, prompt injection | — | #164 |
|
||||||
|
|||||||
40
docs/scratchpads/p8-009-tui-slash-commands.md
Normal file
40
docs/scratchpads/p8-009-tui-slash-commands.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# P8-009: TUI Phase 1 — Slash Command Parsing
|
||||||
|
|
||||||
|
## Task Reference
|
||||||
|
|
||||||
|
- Issue: #162
|
||||||
|
- Branch: feat/p8-009-tui-slash-commands
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- New files: parse.ts, registry.ts, local/help.ts, local/status.ts, commands/index.ts
|
||||||
|
- Modified files: use-socket.ts, input-bar.tsx, message-list.tsx, app.tsx
|
||||||
|
|
||||||
|
## Key Observations
|
||||||
|
|
||||||
|
- CommandDef in @mosaic/types does NOT have `category` field — will omit from LOCAL_COMMANDS
|
||||||
|
- CommandDef.args is `CommandArgDef[] | undefined`, not `{ usage: string }` — help.ts args rendering needs adjustment
|
||||||
|
- Message role union currently: 'user' | 'assistant' | 'thinking' | 'tool' — adding 'system'
|
||||||
|
- InputBar currently takes `onSubmit: (value: string) => void` — need to add slash command interception
|
||||||
|
- app.tsx passes `onSubmit={socket.sendMessage}` directly — needs command-aware handler
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
- ASSUMPTION: `category` field not in CommandDef type — will skip category grouping in help output, or add it only to registry (not to CommandDef type)
|
||||||
|
- ASSUMPTION: For the `args` field display in help, will use `CommandArgDef.name` and `CommandArgDef.description`
|
||||||
|
- ASSUMPTION: `commands:manifest` event type may not be in ServerToClientEvents — will handle via socket.on with casting if needed
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- [ ] Create commands directory structure
|
||||||
|
- [ ] Implement parse.ts
|
||||||
|
- [ ] Implement registry.ts
|
||||||
|
- [ ] Implement local/help.ts
|
||||||
|
- [ ] Implement local/status.ts
|
||||||
|
- [ ] Implement commands/index.ts
|
||||||
|
- [ ] Modify use-socket.ts
|
||||||
|
- [ ] Modify input-bar.tsx
|
||||||
|
- [ ] Modify message-list.tsx
|
||||||
|
- [ ] Modify app.tsx
|
||||||
|
- [ ] Run quality gates
|
||||||
|
- [ ] Commit + Push + PR + CI
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { Box, useApp, useInput } from 'ink';
|
import { Box, useApp, useInput } from 'ink';
|
||||||
|
import type { ParsedCommand } from '@mosaic/types';
|
||||||
import { TopBar } from './components/top-bar.js';
|
import { TopBar } from './components/top-bar.js';
|
||||||
import { BottomBar } from './components/bottom-bar.js';
|
import { BottomBar } from './components/bottom-bar.js';
|
||||||
import { MessageList } from './components/message-list.js';
|
import { MessageList } from './components/message-list.js';
|
||||||
@@ -12,6 +13,7 @@ import { useViewport } from './hooks/use-viewport.js';
|
|||||||
import { useAppMode } from './hooks/use-app-mode.js';
|
import { useAppMode } from './hooks/use-app-mode.js';
|
||||||
import { useConversations } from './hooks/use-conversations.js';
|
import { useConversations } from './hooks/use-conversations.js';
|
||||||
import { useSearch } from './hooks/use-search.js';
|
import { useSearch } from './hooks/use-search.js';
|
||||||
|
import { executeHelp, executeStatus } from './commands/index.js';
|
||||||
|
|
||||||
export interface TuiAppProps {
|
export interface TuiAppProps {
|
||||||
gatewayUrl: string;
|
gatewayUrl: string;
|
||||||
@@ -71,6 +73,63 @@ export function TuiApp({
|
|||||||
|
|
||||||
const [sidebarSelectedIndex, setSidebarSelectedIndex] = useState(0);
|
const [sidebarSelectedIndex, setSidebarSelectedIndex] = useState(0);
|
||||||
|
|
||||||
|
const handleLocalCommand = useCallback(
|
||||||
|
(parsed: ParsedCommand) => {
|
||||||
|
switch (parsed.command) {
|
||||||
|
case 'help':
|
||||||
|
case 'h': {
|
||||||
|
const result = executeHelp(parsed);
|
||||||
|
socket.addSystemMessage(result);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'status':
|
||||||
|
case 's': {
|
||||||
|
const result = executeStatus(parsed, {
|
||||||
|
connected: socket.connected,
|
||||||
|
model: socket.modelName,
|
||||||
|
provider: socket.providerName,
|
||||||
|
sessionId: socket.conversationId ?? null,
|
||||||
|
tokenCount: socket.tokenUsage.total,
|
||||||
|
});
|
||||||
|
socket.addSystemMessage(result);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'clear':
|
||||||
|
socket.clearMessages();
|
||||||
|
break;
|
||||||
|
case 'stop':
|
||||||
|
// Currently no stop mechanism exposed — show feedback
|
||||||
|
socket.addSystemMessage('Stop is not available for the current session.');
|
||||||
|
break;
|
||||||
|
case 'cost': {
|
||||||
|
const u = socket.tokenUsage;
|
||||||
|
socket.addSystemMessage(
|
||||||
|
`Tokens — input: ${u.input}, output: ${u.output}, total: ${u.total}\nCost: $${u.cost.toFixed(6)}`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
socket.addSystemMessage(`Local command not implemented: /${parsed.command}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[socket],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleGatewayCommand = useCallback(
|
||||||
|
(parsed: ParsedCommand) => {
|
||||||
|
if (!socket.socketRef.current?.connected || !socket.conversationId) {
|
||||||
|
socket.addSystemMessage('Not connected to gateway. Command cannot be executed.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socket.socketRef.current.emit('command:execute', {
|
||||||
|
conversationId: socket.conversationId,
|
||||||
|
command: parsed.command,
|
||||||
|
args: parsed.args ?? undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[socket],
|
||||||
|
);
|
||||||
|
|
||||||
const handleSwitchConversation = useCallback(
|
const handleSwitchConversation = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
socket.switchConversation(id);
|
socket.switchConversation(id);
|
||||||
@@ -202,6 +261,9 @@ export function TuiApp({
|
|||||||
|
|
||||||
<InputBar
|
<InputBar
|
||||||
onSubmit={socket.sendMessage}
|
onSubmit={socket.sendMessage}
|
||||||
|
onSystemMessage={socket.addSystemMessage}
|
||||||
|
onLocalCommand={handleLocalCommand}
|
||||||
|
onGatewayCommand={handleGatewayCommand}
|
||||||
isStreaming={socket.isStreaming}
|
isStreaming={socket.isStreaming}
|
||||||
connected={socket.connected}
|
connected={socket.connected}
|
||||||
placeholder={inputPlaceholder}
|
placeholder={inputPlaceholder}
|
||||||
|
|||||||
5
packages/cli/src/tui/commands/index.ts
Normal file
5
packages/cli/src/tui/commands/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export { parseSlashCommand } from './parse.js';
|
||||||
|
export { commandRegistry, CommandRegistry } from './registry.js';
|
||||||
|
export { executeHelp } from './local/help.js';
|
||||||
|
export { executeStatus } from './local/status.js';
|
||||||
|
export type { StatusContext } from './local/status.js';
|
||||||
19
packages/cli/src/tui/commands/local/help.ts
Normal file
19
packages/cli/src/tui/commands/local/help.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { ParsedCommand } from '@mosaic/types';
|
||||||
|
import { commandRegistry } from '../registry.js';
|
||||||
|
|
||||||
|
export function executeHelp(_parsed: ParsedCommand): string {
|
||||||
|
const commands = commandRegistry.getAll();
|
||||||
|
const lines = ['Available commands:', ''];
|
||||||
|
|
||||||
|
for (const cmd of commands) {
|
||||||
|
const aliases =
|
||||||
|
cmd.aliases.length > 0 ? ` (${cmd.aliases.map((a) => `/${a}`).join(', ')})` : '';
|
||||||
|
const argsStr =
|
||||||
|
cmd.args && cmd.args.length > 0
|
||||||
|
? ' ' + cmd.args.map((a) => (a.optional ? `[${a.name}]` : `<${a.name}>`)).join(' ')
|
||||||
|
: '';
|
||||||
|
lines.push(` /${cmd.name}${argsStr}${aliases} — ${cmd.description}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n').trimEnd();
|
||||||
|
}
|
||||||
20
packages/cli/src/tui/commands/local/status.ts
Normal file
20
packages/cli/src/tui/commands/local/status.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { ParsedCommand } from '@mosaic/types';
|
||||||
|
|
||||||
|
export interface StatusContext {
|
||||||
|
connected: boolean;
|
||||||
|
model: string | null;
|
||||||
|
provider: string | null;
|
||||||
|
sessionId: string | null;
|
||||||
|
tokenCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function executeStatus(_parsed: ParsedCommand, ctx: StatusContext): string {
|
||||||
|
const lines = [
|
||||||
|
`Connection: ${ctx.connected ? 'connected' : 'disconnected'}`,
|
||||||
|
`Model: ${ctx.model ?? 'unknown'}`,
|
||||||
|
`Provider: ${ctx.provider ?? 'unknown'}`,
|
||||||
|
`Session: ${ctx.sessionId ?? 'none'}`,
|
||||||
|
`Tokens (session): ${ctx.tokenCount}`,
|
||||||
|
];
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
11
packages/cli/src/tui/commands/parse.ts
Normal file
11
packages/cli/src/tui/commands/parse.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { ParsedCommand } from '@mosaic/types';
|
||||||
|
|
||||||
|
export function parseSlashCommand(input: string): ParsedCommand | null {
|
||||||
|
const match = input.match(/^\/([a-z][a-z0-9:_-]*)\s*(.*)?$/i);
|
||||||
|
if (!match) return null;
|
||||||
|
return {
|
||||||
|
command: match[1]!,
|
||||||
|
args: match[2]?.trim() || null,
|
||||||
|
raw: input,
|
||||||
|
};
|
||||||
|
}
|
||||||
98
packages/cli/src/tui/commands/registry.ts
Normal file
98
packages/cli/src/tui/commands/registry.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import type { CommandDef, CommandManifest } from '@mosaic/types';
|
||||||
|
|
||||||
|
// Local-only commands (work even when gateway is disconnected)
|
||||||
|
const LOCAL_COMMANDS: CommandDef[] = [
|
||||||
|
{
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show available commands',
|
||||||
|
aliases: ['h'],
|
||||||
|
args: undefined,
|
||||||
|
execution: 'local',
|
||||||
|
available: true,
|
||||||
|
scope: 'core',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stop',
|
||||||
|
description: 'Cancel current streaming response',
|
||||||
|
aliases: [],
|
||||||
|
args: undefined,
|
||||||
|
execution: 'local',
|
||||||
|
available: true,
|
||||||
|
scope: 'core',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cost',
|
||||||
|
description: 'Show token usage and cost for current session',
|
||||||
|
aliases: [],
|
||||||
|
args: undefined,
|
||||||
|
execution: 'local',
|
||||||
|
available: true,
|
||||||
|
scope: 'core',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
description: 'Show connection and session status',
|
||||||
|
aliases: ['s'],
|
||||||
|
args: undefined,
|
||||||
|
execution: 'local',
|
||||||
|
available: true,
|
||||||
|
scope: 'core',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'clear',
|
||||||
|
description: 'Clear the current conversation display',
|
||||||
|
aliases: [],
|
||||||
|
args: undefined,
|
||||||
|
execution: 'local',
|
||||||
|
available: true,
|
||||||
|
scope: 'core',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ALIASES: Record<string, string> = {
|
||||||
|
n: 'new',
|
||||||
|
m: 'model',
|
||||||
|
t: 'thinking',
|
||||||
|
a: 'agent',
|
||||||
|
s: 'status',
|
||||||
|
h: 'help',
|
||||||
|
pref: 'preferences',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class CommandRegistry {
|
||||||
|
private gatewayManifest: CommandManifest | null = null;
|
||||||
|
|
||||||
|
updateManifest(manifest: CommandManifest): void {
|
||||||
|
this.gatewayManifest = manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveAlias(command: string): string {
|
||||||
|
return ALIASES[command] ?? command;
|
||||||
|
}
|
||||||
|
|
||||||
|
find(command: string): CommandDef | null {
|
||||||
|
const resolved = this.resolveAlias(command);
|
||||||
|
// Search local first, then gateway manifest
|
||||||
|
const local = LOCAL_COMMANDS.find((c) => c.name === resolved || c.aliases.includes(resolved));
|
||||||
|
if (local) return local;
|
||||||
|
if (this.gatewayManifest) {
|
||||||
|
return (
|
||||||
|
this.gatewayManifest.commands.find(
|
||||||
|
(c) => c.name === resolved || c.aliases.includes(resolved),
|
||||||
|
) ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(): CommandDef[] {
|
||||||
|
const gateway = this.gatewayManifest?.commands ?? [];
|
||||||
|
return [...LOCAL_COMMANDS, ...gateway];
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocalCommands(): CommandDef[] {
|
||||||
|
return LOCAL_COMMANDS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const commandRegistry = new CommandRegistry();
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import TextInput from 'ink-text-input';
|
import TextInput from 'ink-text-input';
|
||||||
|
import type { ParsedCommand } from '@mosaic/types';
|
||||||
|
import { parseSlashCommand, commandRegistry } from '../commands/index.js';
|
||||||
|
|
||||||
export interface InputBarProps {
|
export interface InputBarProps {
|
||||||
onSubmit: (value: string) => void;
|
onSubmit: (value: string) => void;
|
||||||
|
onSystemMessage?: (message: string) => void;
|
||||||
|
onLocalCommand?: (parsed: ParsedCommand) => void;
|
||||||
|
onGatewayCommand?: (parsed: ParsedCommand) => void;
|
||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -11,6 +16,9 @@ export interface InputBarProps {
|
|||||||
|
|
||||||
export function InputBar({
|
export function InputBar({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
onSystemMessage,
|
||||||
|
onLocalCommand,
|
||||||
|
onGatewayCommand,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
connected,
|
connected,
|
||||||
placeholder: placeholderOverride,
|
placeholder: placeholderOverride,
|
||||||
@@ -20,10 +28,39 @@ export function InputBar({
|
|||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
if (!value.trim() || isStreaming || !connected) return;
|
if (!value.trim() || isStreaming || !connected) return;
|
||||||
|
|
||||||
|
const trimmed = value.trim();
|
||||||
|
|
||||||
|
if (trimmed.startsWith('/')) {
|
||||||
|
const parsed = parseSlashCommand(trimmed);
|
||||||
|
if (!parsed) {
|
||||||
|
onSystemMessage?.(`Unknown command format: ${trimmed}`);
|
||||||
|
setInput('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const def = commandRegistry.find(parsed.command);
|
||||||
|
if (!def) {
|
||||||
|
onSystemMessage?.(
|
||||||
|
`Unknown command: /${parsed.command}. Type /help for available commands.`,
|
||||||
|
);
|
||||||
|
setInput('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (def.execution === 'local') {
|
||||||
|
onLocalCommand?.(parsed);
|
||||||
|
setInput('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Gateway-executed commands
|
||||||
|
onGatewayCommand?.(parsed);
|
||||||
|
setInput('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
onSubmit(value);
|
onSubmit(value);
|
||||||
setInput('');
|
setInput('');
|
||||||
},
|
},
|
||||||
[onSubmit, isStreaming, connected],
|
[onSubmit, onSystemMessage, onLocalCommand, onGatewayCommand, isStreaming, connected],
|
||||||
);
|
);
|
||||||
|
|
||||||
const placeholder =
|
const placeholder =
|
||||||
|
|||||||
@@ -24,6 +24,17 @@ function formatTime(date: Date): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SystemMessageBubble({ msg }: { msg: Message }) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="row" marginBottom={1} marginLeft={2}>
|
||||||
|
<Text dimColor>{'⚙ '}</Text>
|
||||||
|
<Text dimColor wrap="wrap">
|
||||||
|
{msg.content}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function MessageBubble({
|
function MessageBubble({
|
||||||
msg,
|
msg,
|
||||||
highlight,
|
highlight,
|
||||||
@@ -31,6 +42,10 @@ function MessageBubble({
|
|||||||
msg: Message;
|
msg: Message;
|
||||||
highlight?: 'match' | 'current' | undefined;
|
highlight?: 'match' | 'current' | undefined;
|
||||||
}) {
|
}) {
|
||||||
|
if (msg.role === 'system') {
|
||||||
|
return <SystemMessageBubble msg={msg} />;
|
||||||
|
}
|
||||||
|
|
||||||
const isUser = msg.role === 'user';
|
const isUser = msg.role === 'user';
|
||||||
const prefix = isUser ? '❯' : '◆';
|
const prefix = isUser ? '❯' : '◆';
|
||||||
const color = isUser ? 'green' : 'cyan';
|
const color = isUser ? 'green' : 'cyan';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { type MutableRefObject, useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { io, type Socket } from 'socket.io-client';
|
import { io, type Socket } from 'socket.io-client';
|
||||||
import type {
|
import type {
|
||||||
ServerToClientEvents,
|
ServerToClientEvents,
|
||||||
@@ -11,7 +11,9 @@ import type {
|
|||||||
ToolEndPayload,
|
ToolEndPayload,
|
||||||
SessionInfoPayload,
|
SessionInfoPayload,
|
||||||
ErrorPayload,
|
ErrorPayload,
|
||||||
|
CommandManifestPayload,
|
||||||
} from '@mosaic/types';
|
} from '@mosaic/types';
|
||||||
|
import { commandRegistry } from '../commands/index.js';
|
||||||
|
|
||||||
export interface ToolCall {
|
export interface ToolCall {
|
||||||
toolCallId: string;
|
toolCallId: string;
|
||||||
@@ -20,7 +22,7 @@ export interface ToolCall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
role: 'user' | 'assistant' | 'thinking' | 'tool';
|
role: 'user' | 'assistant' | 'thinking' | 'tool' | 'system';
|
||||||
content: string;
|
content: string;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
toolCalls?: ToolCall[];
|
toolCalls?: ToolCall[];
|
||||||
@@ -46,6 +48,8 @@ export interface UseSocketOptions {
|
|||||||
agentId?: string;
|
agentId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TypedSocket = Socket<ServerToClientEvents, ClientToServerEvents>;
|
||||||
|
|
||||||
export interface UseSocketReturn {
|
export interface UseSocketReturn {
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
connecting: boolean;
|
connecting: boolean;
|
||||||
@@ -61,14 +65,14 @@ export interface UseSocketReturn {
|
|||||||
thinkingLevel: string;
|
thinkingLevel: string;
|
||||||
availableThinkingLevels: string[];
|
availableThinkingLevels: string[];
|
||||||
sendMessage: (content: string) => void;
|
sendMessage: (content: string) => void;
|
||||||
|
addSystemMessage: (content: string) => void;
|
||||||
setThinkingLevel: (level: string) => void;
|
setThinkingLevel: (level: string) => void;
|
||||||
switchConversation: (id: string) => void;
|
switchConversation: (id: string) => void;
|
||||||
clearMessages: () => void;
|
clearMessages: () => void;
|
||||||
connectionError: string | null;
|
connectionError: string | null;
|
||||||
|
socketRef: MutableRefObject<TypedSocket | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TypedSocket = Socket<ServerToClientEvents, ClientToServerEvents>;
|
|
||||||
|
|
||||||
const EMPTY_USAGE: TokenUsage = {
|
const EMPTY_USAGE: TokenUsage = {
|
||||||
input: 0,
|
input: 0,
|
||||||
output: 0,
|
output: 0,
|
||||||
@@ -222,6 +226,10 @@ export function useSocket(opts: UseSocketOptions): UseSocketReturn {
|
|||||||
setIsStreaming(false);
|
setIsStreaming(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('commands:manifest', (data: CommandManifestPayload) => {
|
||||||
|
commandRegistry.updateManifest(data.manifest);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.disconnect();
|
socket.disconnect();
|
||||||
};
|
};
|
||||||
@@ -245,6 +253,10 @@ export function useSocket(opts: UseSocketOptions): UseSocketReturn {
|
|||||||
[conversationId, isStreaming],
|
[conversationId, isStreaming],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const addSystemMessage = useCallback((content: string) => {
|
||||||
|
setMessages((msgs) => [...msgs, { role: 'system', content, timestamp: new Date() }]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const setThinkingLevel = useCallback((level: string) => {
|
const setThinkingLevel = useCallback((level: string) => {
|
||||||
const cid = conversationIdRef.current;
|
const cid = conversationIdRef.current;
|
||||||
if (!socketRef.current?.connected || !cid) return;
|
if (!socketRef.current?.connected || !cid) return;
|
||||||
@@ -285,9 +297,11 @@ export function useSocket(opts: UseSocketOptions): UseSocketReturn {
|
|||||||
thinkingLevel,
|
thinkingLevel,
|
||||||
availableThinkingLevels,
|
availableThinkingLevels,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
addSystemMessage,
|
||||||
setThinkingLevel,
|
setThinkingLevel,
|
||||||
switchConversation,
|
switchConversation,
|
||||||
clearMessages,
|
clearMessages,
|
||||||
connectionError,
|
connectionError,
|
||||||
|
socketRef,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user