feat(tui): add /history command — M1-007 (#297)
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 #297.
This commit is contained in:
@@ -13,7 +13,8 @@ import { useViewport } from './hooks/use-viewport.js';
|
||||
import { useAppMode } from './hooks/use-app-mode.js';
|
||||
import { useConversations } from './hooks/use-conversations.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 {
|
||||
gatewayUrl: string;
|
||||
@@ -133,6 +134,23 @@ export function TuiApp({
|
||||
);
|
||||
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:
|
||||
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 { executeStatus } 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,
|
||||
scope: 'core',
|
||||
},
|
||||
{
|
||||
name: 'history',
|
||||
description: 'Show conversation message count and context usage',
|
||||
aliases: ['hist'],
|
||||
args: undefined,
|
||||
execution: 'local',
|
||||
available: true,
|
||||
scope: 'core',
|
||||
},
|
||||
{
|
||||
name: 'clear',
|
||||
description: 'Clear the current conversation display',
|
||||
@@ -64,6 +73,7 @@ const ALIASES: Record<string, string> = {
|
||||
a: 'agent',
|
||||
s: 'status',
|
||||
h: 'help',
|
||||
hist: 'history',
|
||||
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 ──
|
||||
|
||||
export async function fetchMissionTasks(
|
||||
|
||||
Reference in New Issue
Block a user