feat(tui): add /history command — M1-007 (#297)
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:
2026-03-21 20:41:27 +00:00
committed by jason.woltje
parent 1d14ddcfe7
commit 02ff3b3256
5 changed files with 109 additions and 1 deletions

View File

@@ -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}`);
}

View File

@@ -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';

View 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');
}

View File

@@ -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',
};

View File

@@ -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(