import type { Context } from 'hono'; import { getPluginChatsDir, getBotsDir, resolveEmailAccountDir } from '@@/data-path'; // One answer to "which directory is this chat about", for the two routers that need it. // // It lived in chat.ts as a private helper until 2026-08-27, when Command HQ grew a button that starts a // commander from a plugin chat. That commander watches the directory the chat runs in — and the whole // reason this function exists is that the CLIENT cannot name that directory. Two copies of the mapping // would be two chances for a commander to end up watching somewhere its chat has never been. /** * Which directory a listing is about. * * `?context=plugin&contextId=` resolves server-side, and it exists because the client CANNOT * name this one: a plugin's chats run in `$DATA_PATH//plugins//chats`, built by * `resolveChatCwd` and deliberately never sent to the browser. Without this, a plugin can start chats in * its own directory and then has no way to ask for them back. * * The plain `?cwd=` form stays for `/chat`, where the user picks a directory and knows what they picked. * It is not a hole — `listClaudeSessions` reads that identity's own `~/.claude/projects`, so naming * another directory shows you your own conversations elsewhere, never anybody else's. */ /** * The same mapping, from values rather than from a request. * * Command HQ takes the context in a JSON BODY — it is creating a commander, not listing anything — and * the request-reading version silently fell through to `who.home` for it, because there was nothing in * the query string. A commander watching the wrong directory is not an error anywhere: it just answers * questions about the wrong thing forever. */ export const chatContextDir = ( context: string | undefined, contextId: string | undefined, who: { home: string; email: string }, ): string => { const id = contextId?.trim(); if (context === 'plugin' && id) return getPluginChatsDir(who.email, id); if (context === 'bots') return getBotsDir(who.email); if (context === 'email') return resolveEmailAccountDir(who.email, id); return who.home; }; export const resolveChatContextDir = (ctx: Context, who: { home: string; email: string }): string => { const context = ctx.req.query('context'); const contextId = ctx.req.query('contextId')?.trim(); if (context === 'plugin' && contextId) return getPluginChatsDir(who.email, contextId); // Every bot shares one directory, so this needs no id — the context IS the address. if (context === 'bots') return getBotsDir(who.email); // An email chat runs in the MAILBOX's directory, beside `emails.db`. `contextId` is the account picked on // screen, and is optional: without one the resolver takes the most recently synced account. // // This branch was missing, and the failure was quiet in the way that costs the most time — the context // fell through to `who.home`, so the picker above an email composer listed the home's conversations. A // message sent into the mailbox then could not be found in the list directly above it. Same resolver as // the socket, so the turn and the list cannot disagree about where the chat lives. if (context === 'email') return resolveEmailAccountDir(who.email, contextId); return ctx.req.query('cwd')?.trim() || who.home; };