feat(tui): add /history command — M1-007 #297
@@ -13,7 +13,8 @@ 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, commandRegistry } from './commands/index.js';
|
import { executeHelp, executeStatus, executeHistory, commandRegistry } from './commands/index.js';
|
||||||
|
import { fetchConversationMessages } from './gateway-api.js';
|
||||||
|
|
||||||
export interface TuiAppProps {
|
export interface TuiAppProps {
|
||||||
gatewayUrl: string;
|
gatewayUrl: string;
|
||||||
@@ -133,6 +134,23 @@ export function TuiApp({
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'history':
|
||||||
|
case 'hist': {
|
||||||
|
void executeHistory({
|
||||||
|
conversationId: socket.conversationId,
|
||||||
|
gatewayUrl,
|
||||||
|
sessionCookie,
|
||||||
|
fetchMessages: fetchConversationMessages,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
socket.addSystemMessage(result);
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
socket.addSystemMessage(`Failed to fetch history: ${msg}`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
socket.addSystemMessage(`Local command not implemented: /${parsed.command}`);
|
socket.addSystemMessage(`Local command not implemented: /${parsed.command}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ export { commandRegistry, CommandRegistry } from './registry.js';
|
|||||||
export { executeHelp } from './local/help.js';
|
export { executeHelp } from './local/help.js';
|
||||||
export { executeStatus } from './local/status.js';
|
export { executeStatus } from './local/status.js';
|
||||||
export type { StatusContext } from './local/status.js';
|
export type { StatusContext } from './local/status.js';
|
||||||
|
export { executeHistory } from './local/history.js';
|
||||||
|
export type { HistoryContext } from './local/history.js';
|
||||||
|
|||||||
53
packages/cli/src/tui/commands/local/history.ts
Normal file
53
packages/cli/src/tui/commands/local/history.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { ConversationMessage } from '../../gateway-api.js';
|
||||||
|
|
||||||
|
const CONTEXT_WINDOW = 200_000;
|
||||||
|
const CHARS_PER_TOKEN = 4;
|
||||||
|
|
||||||
|
function estimateTokens(messages: ConversationMessage[]): number {
|
||||||
|
const totalChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);
|
||||||
|
return Math.round(totalChars / CHARS_PER_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HistoryContext {
|
||||||
|
conversationId: string | undefined;
|
||||||
|
conversationTitle?: string | null;
|
||||||
|
gatewayUrl: string;
|
||||||
|
sessionCookie: string | undefined;
|
||||||
|
fetchMessages: (
|
||||||
|
gatewayUrl: string,
|
||||||
|
sessionCookie: string,
|
||||||
|
conversationId: string,
|
||||||
|
) => Promise<ConversationMessage[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function executeHistory(ctx: HistoryContext): Promise<string> {
|
||||||
|
const { conversationId, conversationTitle, gatewayUrl, sessionCookie, fetchMessages } = ctx;
|
||||||
|
|
||||||
|
if (!conversationId) {
|
||||||
|
return 'No active conversation.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sessionCookie) {
|
||||||
|
return 'Not authenticated — cannot fetch conversation messages.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = await fetchMessages(gatewayUrl, sessionCookie, conversationId);
|
||||||
|
|
||||||
|
const userMessages = messages.filter((m) => m.role === 'user').length;
|
||||||
|
const assistantMessages = messages.filter((m) => m.role === 'assistant').length;
|
||||||
|
const totalMessages = messages.length;
|
||||||
|
|
||||||
|
const estimatedTokens = estimateTokens(messages);
|
||||||
|
const contextPercent = Math.round((estimatedTokens / CONTEXT_WINDOW) * 100);
|
||||||
|
|
||||||
|
const label = conversationTitle ?? conversationId;
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
`Conversation: ${label}`,
|
||||||
|
`Messages: ${totalMessages} (${userMessages} user, ${assistantMessages} assistant)`,
|
||||||
|
`Estimated tokens: ~${estimatedTokens.toLocaleString()}`,
|
||||||
|
`Context usage: ~${contextPercent}% of ${(CONTEXT_WINDOW / 1000).toFixed(0)}K`,
|
||||||
|
];
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
@@ -38,6 +38,15 @@ const LOCAL_COMMANDS: CommandDef[] = [
|
|||||||
available: true,
|
available: true,
|
||||||
scope: 'core',
|
scope: 'core',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'history',
|
||||||
|
description: 'Show conversation message count and context usage',
|
||||||
|
aliases: ['hist'],
|
||||||
|
args: undefined,
|
||||||
|
execution: 'local',
|
||||||
|
available: true,
|
||||||
|
scope: 'core',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'clear',
|
name: 'clear',
|
||||||
description: 'Clear the current conversation display',
|
description: 'Clear the current conversation display',
|
||||||
@@ -64,6 +73,7 @@ const ALIASES: Record<string, string> = {
|
|||||||
a: 'agent',
|
a: 'agent',
|
||||||
s: 'status',
|
s: 'status',
|
||||||
h: 'help',
|
h: 'help',
|
||||||
|
hist: 'history',
|
||||||
pref: 'preferences',
|
pref: 'preferences',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -361,6 +361,31 @@ export async function deleteMission(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Conversation Message types ──
|
||||||
|
|
||||||
|
export interface ConversationMessage {
|
||||||
|
id: string;
|
||||||
|
role: 'user' | 'assistant' | 'system' | 'tool';
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Conversation Message endpoints ──
|
||||||
|
|
||||||
|
export async function fetchConversationMessages(
|
||||||
|
gatewayUrl: string,
|
||||||
|
sessionCookie: string,
|
||||||
|
conversationId: string,
|
||||||
|
): Promise<ConversationMessage[]> {
|
||||||
|
const res = await fetch(
|
||||||
|
`${gatewayUrl}/api/conversations/${encodeURIComponent(conversationId)}/messages`,
|
||||||
|
{
|
||||||
|
headers: headers(sessionCookie, gatewayUrl),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return handleResponse<ConversationMessage[]>(res, 'Failed to fetch conversation messages');
|
||||||
|
}
|
||||||
|
|
||||||
// ── Mission Task endpoints ──
|
// ── Mission Task endpoints ──
|
||||||
|
|
||||||
export async function fetchMissionTasks(
|
export async function fetchMissionTasks(
|
||||||
|
|||||||
Reference in New Issue
Block a user