chat: rename the pi chat transport + model list to chat (Stage 4b/1)

Pure rename, no behavior change. Moves the misnamed "pi" chat harness into the
chat namespace:

- api/pi/{websocket,session-manager,types,logger,list-models} → api/chat/
- merge api/pi/rest.ts into api/chat/chat.ts (/pi/models → /chat/models,
  /pi/stt → /chat/stt); drop the piRestRouter mount
- PiEvent → ChatEvent, piWebsocket → chatWebsocket, listPiModels → listChatModels
- WS route /api/pi/chat/ws → /api/chat/ws, provider tag 'pi' → 'chat'
- frontend: useChat/useAudioRecording URLs, usePiModels→useModels /
  useVisiblePiModels→useVisibleModels / useEnabledPiModels→useEnabledModels,
  'PI_MODELS' query key → 'CHAT_MODELS', attachments provider 'pi-mono' → 'chat'

The /pi-mono provider/harness settings router is renamed separately (next commit).
Note: the WS route change requires the mobile app to point at /api/chat/ws.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 15:47:40 +00:00
co-authored by Claude Opus 4.8
parent 70555c8d71
commit 49df1c0b0c
31 changed files with 119 additions and 129 deletions
+4 -4
View File
@@ -1,8 +1,8 @@
/**
* @deprecated Legacy chat types - Use types from ./pi/types.ts instead
* @deprecated Legacy chat types - Use types from ./chat/types.ts instead
*
* This file is kept for backward compatibility with existing code.
* New code should import from ./pi/types.ts
* New code should import from ./chat/types.ts
*/
// Re-export new Pi types for compatibility
@@ -13,8 +13,8 @@ export type {
MessageCost,
SessionMeta,
ModelInfo,
PiEvent,
} from './pi/types';
ChatEvent,
} from './chat/types';
// Legacy types (kept for compatibility)
export type ImageData = { mediaType: string; data: string };
+43
View File
@@ -1,5 +1,6 @@
import type { Context } from 'hono';
import { createRouter } from '../../create-router';
import { getUserSettings } from 'officerdb';
import {
getClaudeSessionsCwd,
listClaudePwds,
@@ -8,6 +9,10 @@ import {
deleteClaudeSession,
renameClaudeSession,
} from './claude-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();
@@ -52,3 +57,41 @@ chatRouter.patch('/sessions/:id/title', async (ctx) => {
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' };
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);
}
});
@@ -11,6 +11,6 @@ export function invalidateModelCache(): void {
// No-op: the Claude model list is static. Kept for call-site compatibility.
}
export async function listPiModels(): Promise<ModelInfo[]> {
export async function listChatModels(): Promise<ModelInfo[]> {
return [...CLAUDE_CODE_MODELS];
}
@@ -120,7 +120,7 @@ export type ServerMessage =
type: 'stopped';
};
export type PiEvent =
export type ChatEvent =
| { type: 'text'; text: string }
| { type: 'delta'; text: string }
| {
@@ -1,9 +1,9 @@
import type { ServerWebSocket } from 'bun';
import { randomUUID } from 'crypto';
import type { ClientMessage, ServerMessage, Message, PiEvent } from './types';
import type { ClientMessage, ServerMessage, Message, ChatEvent } from './types';
import { sessionManager } from './session-manager';
import { sendClaudeCodeStreaming } from '@@/channels/send-claude-code';
import { ensureClaudeSessionsCwd } from '../chat/claude-sessions';
import { ensureClaudeSessionsCwd } from './claude-sessions';
import * as sidecar from '@@/sidecar-registry';
import { join } from 'path';
import { getHomeDirForRole } from '../../../servers/data-path';
@@ -113,7 +113,7 @@ export function close(ws: ServerWebSocket<WSData>): void {
}
function createEventHandler(sessionId: string, model: string, cwd: string) {
return async (event: PiEvent): Promise<void> => {
return async (event: ChatEvent): Promise<void> => {
const session = sessionManager.getSession(sessionId);
if (!session) return;
@@ -441,7 +441,7 @@ async function handleStop(ws: ServerWebSocket<WSData>): Promise<void> {
sendToClient(ws, { type: 'stopped' });
}
export const piWebsocket = {
export const chatWebsocket = {
open,
message,
close,
-51
View File
@@ -1,51 +0,0 @@
import type { Context } from 'hono';
import { createRouter } from '../../create-router';
import { readSttConfig } from '../server-settings/stt';
import { listPiModels } from './list-models';
import { logger } from './logger';
import { transcribeAudio } from '../stt/transcribe';
import { getUserSettings } from 'officerdb';
export const piRestRouter = createRouter();
/**
* GET /api/pi/models — Claude tiers only.
*/
piRestRouter.get('/pi/models', async (ctx: Context) => {
try {
const models = await listPiModels();
const providerNames: Record<string, string> = { 'claude-code': 'Claude Code' };
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 /api/pi/stt
*/
piRestRouter.post('/pi/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);
}
});
+2 -2
View File
@@ -1,8 +1,8 @@
import { join } from 'node:path';
import { createRouter } from '../../create-router';
import { PI_CONFIG_DIR } from '../../data-path';
import { invalidateModelCache } from '../pi/list-models';
import { logger } from '../pi/logger';
import { invalidateModelCache } from '../chat/list-models';
import { logger } from '../chat/logger';
import { readConfigValue, writeConfigValue } from 'officerdb';
export const piMonoRouter = createRouter();
+3 -3
View File
@@ -6,10 +6,10 @@ import { tmpdir } from 'node:os';
import { getUserSettings } from 'officerdb';
import { getTaskByDirName } from './task-files';
import { getHomeDirForRole, getHomeDir } from '../../data-path';
import { resolveBaseCwd } from '../pi/websocket';
import { resolveBaseCwd } from '../chat/websocket';
import { SANDBOX_HOME } from '../../sidecar/sandbox';
import { sendClaudeCodeStreaming } from '../../channels/send-claude-code';
import type { PiEvent, MessageCost } from '../pi/types';
import type { ChatEvent, MessageCost } from '../chat/types';
import * as jobManager from './pipeline-job-manager';
const DEFAULT_MODEL = 'claude-code';
@@ -117,7 +117,7 @@ async function runAgenticStep({ userId, email, username, role, taskDirName, prom
fn();
};
const onEvent = (event: PiEvent) => {
const onEvent = (event: ChatEvent) => {
if (abortSignal.aborted) return;
lastActivity = Date.now();