Files
platform/src/servers/api/chat/chat.ts
T
pastilhasandClaude Opus 4.8 0c3f270419 chat: rename claude_sessions → general_chat_sessions; drop dead chat_sessions
The default /chat working directory is used by both the Claude and OpenCode harnesses
now, so its Claude-specific name was misleading.

- Rename the dir + accessors: getClaudeSessionsCwd → getGeneralChatSessionsCwd,
  ensureClaudeSessionsCwd → ensureGeneralChatSessionsCwd, path segment claude_sessions
  → general_chat_sessions (data-path on disk + code + UI labels/comments). No history
  migration — the old Claude transcript slug is orphaned (intentionally).

- Remove the vestigial chat_sessions dir (leftover from the retired session store):
  it only ever held empty claude/archived/ dirs, recreated by a signin hook. Drop that
  hook (+ its dead imports) and the 4 unused data-path accessors (getUserSessionsDir,
  getClaudeDir, getSessionDir, getArchivedSessionDir), and delete the dir.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:50:48 +00:00

119 lines
4.9 KiB
TypeScript

import type { Context } from 'hono';
import { createRouter } from '../../create-router';
import { getUserSettings } from 'officerdb';
import {
getGeneralChatSessionsCwd,
listClaudePwds,
listClaudeSessions,
loadClaudeSession,
deleteClaudeSession,
renameClaudeSession,
} from './claude-sessions';
import {
listOpenCodeSessions,
loadOpenCodeSession,
deleteOpenCodeSession,
renameOpenCodeSession,
isOpenCodeSessionId,
} from './opencode-sessions';
import { listChatModels } from './list-models';
import { logger } from './logger';
import { readSttConfig } from '../server-settings/stt';
import { transcribeAudio } from '../stt/transcribe';
export const chatRouter = createRouter();
// The working directory a request operates on: an explicit ?cwd= (a chosen pwd), else the default
// general_chat_sessions dir. Claude groups sessions by cwd, so this selects which project group we read.
// (OpenCode sessions all live in the one fixed server and ignore cwd.)
const cwdOf = (ctx: Context, email: string): string => ctx.req.query('cwd')?.trim() || getGeneralChatSessionsCwd(email);
// GET /chat/pwds — the default /chat dir plus every directory that already has Claude sessions.
chatRouter.get('/pwds', (ctx) => {
const email = ctx.get('user').email;
return ctx.json({ pwds: listClaudePwds(email), default: getGeneralChatSessionsCwd(email) });
});
// GET /chat/sessions[?cwd=] — conversations for a working directory, merged across both harnesses
// (Claude transcripts + OpenCode's session store), newest first.
chatRouter.get('/sessions', async (ctx) => {
const email = ctx.get('user').email;
const cwd = cwdOf(ctx, email);
const claude = listClaudeSessions(email, cwd).map((s) => ({ ...s, harness: 'claude' as const }));
const opencode = await listOpenCodeSessions(cwd);
const sessions = [...claude, ...opencode].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
return ctx.json({ sessions });
});
// GET /chat/sessions/:id[?cwd=] — one conversation's transcript. Routes by harness (ses_ = OpenCode).
chatRouter.get('/sessions/:id', async (ctx) => {
const email = ctx.get('user').email;
const id = ctx.req.param('id');
const cwd = cwdOf(ctx, email);
const detail = isOpenCodeSessionId(id) ? await loadOpenCodeSession(id) : loadClaudeSession(email, cwd, id);
if (!detail) return ctx.text('Not found', 404);
return ctx.json(detail);
});
// DELETE /chat/sessions/:id[?cwd=] — remove a conversation from the owning harness's store.
chatRouter.delete('/sessions/:id', async (ctx) => {
const email = ctx.get('user').email;
const id = ctx.req.param('id');
const cwd = cwdOf(ctx, email);
const ok = isOpenCodeSessionId(id) ? await deleteOpenCodeSession(id) : deleteClaudeSession(email, cwd, id);
if (!ok) return ctx.text('Not found', 404);
return ctx.json({ ok: true });
});
// PATCH /chat/sessions/:id/title[?cwd=] — rename in the owning harness's store.
chatRouter.patch('/sessions/:id/title', async (ctx) => {
const email = ctx.get('user').email;
const id = ctx.req.param('id');
const cwd = cwdOf(ctx, email);
const { title } = await ctx.req.json<{ title?: string }>();
if (!title?.trim()) return ctx.text('title is required', 400);
const ok = isOpenCodeSessionId(id)
? await renameOpenCodeSession(id, title.trim())
: renameClaudeSession(email, cwd, id, title.trim());
if (!ok) return ctx.text('Not found', 404);
return ctx.json({ ok: true });
});
// GET /chat/models — Claude tiers only (the runner is the `claude` CLI).
chatRouter.get('/models', async (ctx: Context) => {
try {
const models = await listChatModels();
const providerNames: Record<string, string> = { 'claude-code': 'Claude Code', opencode: 'OpenCode Zen' };
return ctx.json({ models, providerNames, hostHome: process.env.HOME ?? '' });
} catch (err) {
logger.error('Failed to list models', { error: String(err) });
return ctx.json({ models: [], providerNames: {} });
}
});
// POST /chat/stt — proxy an audio clip to the configured Whisper server.
chatRouter.post('/stt', async (ctx: Context) => {
const sttConfig = await readSttConfig();
if (!sttConfig?.url) {
return ctx.json({ error: 'Whisper not configured — set it up in Settings → Speech to Text' }, 400);
}
const user = ctx.get('user');
const body = await ctx.req.parseBody();
const file = body['file'];
if (!file || !(file instanceof File)) {
return ctx.json({ error: 'file is required' }, 400);
}
const settings = (await getUserSettings(user.id)) as { languages?: { spoken?: string[] } };
const spokenLanguages = settings.languages?.spoken ?? [];
try {
const result = await transcribeAudio({ file, whisperUrl: sttConfig.url, spokenLanguages });
return ctx.json(result);
} catch (err) {
logger.error('STT proxy failed', { error: String(err) });
return ctx.json({ error: err instanceof Error ? err.message : 'Failed to reach Whisper server' }, 502);
}
});