claude-code streaming chat, desktop remote viewer, new-automation route, tiktok task v4, misc fixes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+102
-11
@@ -4,6 +4,7 @@ import type { ClientMessage, ServerMessage, Message, PiEvent } from './types';
|
||||
import { sessionManager } from './session-manager';
|
||||
import * as storage from './storage';
|
||||
import * as piBridge from './pi-bridge';
|
||||
import { sendClaudeCodeStreaming } from '@@/channels/send-claude-code';
|
||||
import { join, resolve } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { getHomeDir } from '../../../servers/data-path';
|
||||
@@ -11,7 +12,7 @@ import { getUserSettings } from 'officerdb';
|
||||
import { logger } from './logger';
|
||||
|
||||
// Default model when no user preference is set
|
||||
const DEFAULT_MODEL = 'opencode/big-pickle';
|
||||
const DEFAULT_MODEL = 'claude-code';
|
||||
|
||||
async function getUserDefaultModel(userId: number): Promise<string | null> {
|
||||
try {
|
||||
@@ -262,14 +263,18 @@ async function handleChat(
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Model selected for chat', {
|
||||
sessionId,
|
||||
model,
|
||||
logger.info('Model selected for chat', {
|
||||
sessionId,
|
||||
model,
|
||||
modelSource,
|
||||
clientModel: msg.model || null,
|
||||
userDefault,
|
||||
});
|
||||
|
||||
|
||||
if (model === 'claude-code') {
|
||||
return handleClaudeCodeChat(ws, sessionId, model, msg);
|
||||
}
|
||||
|
||||
const homeDir = getHomeDir(email);
|
||||
const sandboxed = msg.sandboxed ?? false;
|
||||
const cwd = sandboxed
|
||||
@@ -351,6 +356,86 @@ async function handleChat(
|
||||
piBridge.sendPrompt(session.piProcess, msg.prompt, requestId);
|
||||
}
|
||||
|
||||
async function handleClaudeCodeChat(
|
||||
ws: ServerWebSocket<WSData>,
|
||||
sessionId: string,
|
||||
model: string,
|
||||
msg: { prompt: string; displayText?: string; groupSlug?: string; context?: string; contextId?: string; cwd?: string; cwdRoot?: string; sandboxed?: boolean },
|
||||
): Promise<void> {
|
||||
const { email, username, userId } = ws.data;
|
||||
const homeDir = getHomeDir(email);
|
||||
const sandboxed = msg.sandboxed ?? false;
|
||||
|
||||
// Claude Code always operates on the user's data home (not OS home).
|
||||
// Resolve cwd relative to data directory, then remap for container if sandboxed.
|
||||
const dataCwd = resolveSandboxedCwd(email, msg.cwdRoot, msg.cwd);
|
||||
let cwd: string;
|
||||
if (sandboxed) {
|
||||
const containerHome = `/home/${username}`;
|
||||
cwd = dataCwd.startsWith(homeDir)
|
||||
? `${containerHome}${dataCwd.slice(homeDir.length)}`
|
||||
: containerHome;
|
||||
} else {
|
||||
cwd = dataCwd;
|
||||
}
|
||||
|
||||
const groupSlug = msg.groupSlug || null;
|
||||
|
||||
const session = sessionManager.getOrCreate(sessionId, email, cwd, model, groupSlug, msg.context, msg.contextId);
|
||||
session.sandboxed = sandboxed;
|
||||
session.userId = userId;
|
||||
sessionManager.attachWs(sessionId, ws);
|
||||
wsToSessionMap.set(ws as any, sessionId);
|
||||
|
||||
sendToClient(ws, { type: 'session:init', sessionId, model, cwd, context: session.meta.context, contextId: session.meta.contextId });
|
||||
|
||||
// Add user message to session
|
||||
const userMsg: Message = {
|
||||
id: randomUUID(),
|
||||
timestamp: Date.now(),
|
||||
role: 'user',
|
||||
text: msg.prompt,
|
||||
};
|
||||
session.messages.push(userMsg);
|
||||
session.meta.messageCount += 1;
|
||||
session.meta.updatedAt = Date.now();
|
||||
|
||||
if (!session.meta.title) {
|
||||
session.meta.title = (msg.displayText ?? msg.prompt).slice(0, 100);
|
||||
}
|
||||
|
||||
session.isGenerating = true;
|
||||
|
||||
const onEvent = createEventHandler(sessionId, model, cwd, homeDir);
|
||||
|
||||
try {
|
||||
const handle = await sendClaudeCodeStreaming({
|
||||
userId,
|
||||
email,
|
||||
username,
|
||||
prompt: msg.prompt,
|
||||
sessionKey: sessionId,
|
||||
cwd,
|
||||
sandboxed,
|
||||
onEvent,
|
||||
});
|
||||
|
||||
// Store proc as piProcess so handleStop can kill it
|
||||
session.piProcess = handle.proc;
|
||||
|
||||
// Null out when process exits so next message spawns a new one
|
||||
handle.proc.exited.then(() => {
|
||||
if (session.piProcess === handle.proc) {
|
||||
session.piProcess = null;
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('Failed to start Claude Code streaming', { sessionId, error: String(err) });
|
||||
sendToClient(ws, { type: 'error', message: 'Failed to start Claude Code' });
|
||||
session.isGenerating = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResume(
|
||||
ws: ServerWebSocket<WSData>,
|
||||
msg: { sessionId: string; cwd?: string; cwdRoot?: string }
|
||||
@@ -444,21 +529,27 @@ async function handleResume(
|
||||
|
||||
async function handleStop(ws: ServerWebSocket<WSData>): Promise<void> {
|
||||
const sessionId = wsToSessionMap.get(ws);
|
||||
|
||||
|
||||
if (sessionId) {
|
||||
const session = sessionManager.getSession(sessionId);
|
||||
|
||||
|
||||
if (session?.piProcess) {
|
||||
try {
|
||||
piBridge.abort(session.piProcess, randomUUID());
|
||||
logger.info('Sent abort to Pi process', { sessionId });
|
||||
if (session.model === 'claude-code') {
|
||||
// Claude Code: kill the docker exec process directly
|
||||
session.piProcess.kill();
|
||||
logger.info('Killed Claude Code process', { sessionId });
|
||||
} else {
|
||||
piBridge.abort(session.piProcess, randomUUID());
|
||||
logger.info('Sent abort to Pi process', { sessionId });
|
||||
}
|
||||
session.isGenerating = false;
|
||||
} catch (err) {
|
||||
logger.error('Failed to abort Pi process', { sessionId, error: String(err) });
|
||||
logger.error('Failed to stop process', { sessionId, error: String(err) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sendToClient(ws, { type: 'stopped' });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user