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:
2026-03-02 20:00:45 +00:00
co-authored by Claude Opus 4.6
parent 0068244356
commit 40a9768cb3
91 changed files with 17872 additions and 85 deletions
+102 -11
View File
@@ -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' });
}