From 02ff3b325637dacb2c32accf4efbd49ef7b50786 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sat, 21 Mar 2026 20:41:27 +0000 Subject: [PATCH] =?UTF-8?q?feat(tui):=20add=20/history=20command=20?= =?UTF-8?q?=E2=80=94=20M1-007=20(#297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- packages/cli/src/tui/app.tsx | 20 ++++++- packages/cli/src/tui/commands/index.ts | 2 + .../cli/src/tui/commands/local/history.ts | 53 +++++++++++++++++++ packages/cli/src/tui/commands/registry.ts | 10 ++++ packages/cli/src/tui/gateway-api.ts | 25 +++++++++ 5 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 packages/cli/src/tui/commands/local/history.ts diff --git a/packages/cli/src/tui/app.tsx b/packages/cli/src/tui/app.tsx index 0004660..2c556a8 100644 --- a/packages/cli/src/tui/app.tsx +++ b/packages/cli/src/tui/app.tsx @@ -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}`); } diff --git a/packages/cli/src/tui/commands/index.ts b/packages/cli/src/tui/commands/index.ts index 17c6d2d..5373f01 100644 --- a/packages/cli/src/tui/commands/index.ts +++ b/packages/cli/src/tui/commands/index.ts @@ -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'; diff --git a/packages/cli/src/tui/commands/local/history.ts b/packages/cli/src/tui/commands/local/history.ts new file mode 100644 index 0000000..43eec72 --- /dev/null +++ b/packages/cli/src/tui/commands/local/history.ts @@ -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; +} + +export async function executeHistory(ctx: HistoryContext): Promise { + 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'); +} diff --git a/packages/cli/src/tui/commands/registry.ts b/packages/cli/src/tui/commands/registry.ts index eb28437..623a2ca 100644 --- a/packages/cli/src/tui/commands/registry.ts +++ b/packages/cli/src/tui/commands/registry.ts @@ -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 = { a: 'agent', s: 'status', h: 'help', + hist: 'history', pref: 'preferences', }; diff --git a/packages/cli/src/tui/gateway-api.ts b/packages/cli/src/tui/gateway-api.ts index f41913d..34147ab 100644 --- a/packages/cli/src/tui/gateway-api.ts +++ b/packages/cli/src/tui/gateway-api.ts @@ -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 { + const res = await fetch( + `${gatewayUrl}/api/conversations/${encodeURIComponent(conversationId)}/messages`, + { + headers: headers(sessionCookie, gatewayUrl), + }, + ); + return handleResponse(res, 'Failed to fetch conversation messages'); +} + // ── Mission Task endpoints ── export async function fetchMissionTasks(