chat: run the /email chat from the email account's storage dir
The email chat's working directory now resolves to DATA_PATH/<owner>/email_accounts/<accountEmail> (created if missing), so the agent operates in the selected account's dir (emails.db, attachment_cache, …). - websocket.ts: new resolveChatCwd — context 'email' → the account dir (via a new resolveEmailCwd), 'chat' → the pwd/claude_sessions dir, else the given cwd. Both the Claude and OpenCode handlers use it. The account defaults to the owner's first enabled account for now; the account selector will pass it as contextId later. - OpenCode honors the cwd again: send-opencode passes it as the session `directory` (client.createSession(directory?)) for context-scoped chats; the general /chat still omits it and uses the fixed server's default project. Verified against the live server that directory-bound sessions create + list. No frontend change — the /email panel already sends context:'email'. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -105,9 +105,13 @@ class ServerConnection {
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
async createSession(title?: string): Promise<string> {
|
||||
// No `directory` — the session lives in the fixed server's own project (its cwd).
|
||||
const session = await this.postJson<{ id?: string }>('/session', title ? { title } : {});
|
||||
async createSession(directory?: string, title?: string): Promise<string> {
|
||||
// `directory` binds the session's working dir (e.g. an email account dir). Omit it for the general
|
||||
// /chat, so the session lives in the fixed server's own project (its cwd).
|
||||
const body: Record<string, unknown> = {};
|
||||
if (directory) body.directory = directory;
|
||||
if (title) body.title = title;
|
||||
const session = await this.postJson<{ id?: string }>('/session', body);
|
||||
if (!session.id) throw new Error('opencode POST /session returned no id');
|
||||
return session.id;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ import { sendOpenCodeStreaming } from '@@/channels/send-opencode';
|
||||
import { ensureClaudeSessionsCwd } from './claude-sessions';
|
||||
import * as sidecar from '@@/sidecar-registry';
|
||||
import { join } from 'path';
|
||||
import { getHomeDirForRole } from '../../../servers/data-path';
|
||||
import { getUserSettings } from 'officerdb';
|
||||
import { getHomeDirForRole, getEmailAccountsDir } from '../../../servers/data-path';
|
||||
import { getUserSettings, getEmailAccounts } from 'officerdb';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { logger } from './logger';
|
||||
|
||||
// Default model when no user preference is set
|
||||
@@ -56,6 +57,38 @@ export const resolveBaseCwd = (email: string, role: string, cwd?: string) => {
|
||||
return resolveCwd(email, role, cwd);
|
||||
};
|
||||
|
||||
// The email chat runs from the selected account's storage dir:
|
||||
// DATA_PATH/<owner>/email_accounts/<accountEmail>
|
||||
// `accountEmail` will come from the account selector (msg.contextId) later; for now default to the
|
||||
// owner's first enabled account. Falls back to the email_accounts root if there are no accounts.
|
||||
async function resolveEmailCwd(userId: number, ownerEmail: string, accountEmail?: string): Promise<string> {
|
||||
let account = accountEmail?.trim();
|
||||
if (!account) {
|
||||
try {
|
||||
const accounts = await getEmailAccounts(userId);
|
||||
account = (accounts.find((a) => a.enabled) ?? accounts[0])?.email;
|
||||
} catch (err) {
|
||||
logger.error('Failed to resolve email account for chat cwd', { userId, error: String(err) });
|
||||
}
|
||||
}
|
||||
const dir = account ? join(getEmailAccountsDir(ownerEmail), account) : getEmailAccountsDir(ownerEmail);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
// The working directory a chat turn runs in, by context: email → the account dir; /chat → a chosen
|
||||
// pwd or the default claude_sessions dir; everything else (browser/project/dashboard) → the given cwd.
|
||||
async function resolveChatCwd(
|
||||
msg: { context?: string; contextId?: string; cwd?: string },
|
||||
email: string,
|
||||
role: string,
|
||||
userId: number,
|
||||
): Promise<string> {
|
||||
if (msg.context === 'email') return resolveEmailCwd(userId, email, msg.contextId);
|
||||
if (msg.context === 'chat') return msg.cwd?.trim() ? resolveCwd(email, role, msg.cwd) : ensureClaudeSessionsCwd(email);
|
||||
return resolveCwd(email, role, msg.cwd);
|
||||
}
|
||||
|
||||
const wsToSessionMap = new WeakMap<any, string>();
|
||||
|
||||
// Per-connection heartbeat. Bun closes a WS idle for `idleTimeout` (60s), and its timer only resets
|
||||
@@ -309,15 +342,7 @@ async function handleClaudeCodeChat(
|
||||
): Promise<void> {
|
||||
const { email, username, userId } = ws.data;
|
||||
|
||||
// The standalone /chat route runs from a chosen working directory (the pwd selector) or, by default,
|
||||
// a dedicated `claude_sessions` dir — so transcripts form their own Claude "project" group per cwd.
|
||||
// Other contexts (email/project panels) keep their own cwd.
|
||||
const cwd =
|
||||
msg.context === 'chat'
|
||||
? msg.cwd?.trim()
|
||||
? resolveCwd(email, ws.data.role, msg.cwd)
|
||||
: ensureClaudeSessionsCwd(email)
|
||||
: resolveCwd(email, ws.data.role, msg.cwd);
|
||||
const cwd = await resolveChatCwd(msg, email, ws.data.role, userId);
|
||||
|
||||
const groupSlug = msg.groupSlug || null;
|
||||
|
||||
@@ -397,13 +422,11 @@ async function handleOpenCodeChat(
|
||||
): Promise<void> {
|
||||
const { email, username, userId } = ws.data;
|
||||
|
||||
// Same cwd resolution as the Claude path: /chat runs from a chosen pwd or the default dir.
|
||||
const cwd =
|
||||
msg.context === 'chat'
|
||||
? msg.cwd?.trim()
|
||||
? resolveCwd(email, ws.data.role, msg.cwd)
|
||||
: ensureClaudeSessionsCwd(email)
|
||||
: resolveCwd(email, ws.data.role, msg.cwd);
|
||||
const cwd = await resolveChatCwd(msg, email, ws.data.role, userId);
|
||||
|
||||
// OpenCode's working directory: a context-scoped dir (email account, project, …) for OpenCode to
|
||||
// operate in; the general /chat runs in the fixed server's default project (home), so pass none.
|
||||
const openCodeDir = msg.context && msg.context !== 'chat' ? cwd : undefined;
|
||||
|
||||
const groupSlug = msg.groupSlug || null;
|
||||
|
||||
@@ -446,7 +469,7 @@ async function handleOpenCodeChat(
|
||||
username,
|
||||
prompt: effectivePrompt,
|
||||
sessionKey: sessionId,
|
||||
cwd,
|
||||
cwd: openCodeDir,
|
||||
model,
|
||||
role: ws.data.role,
|
||||
resumeSessionId: msg.resumeSessionId,
|
||||
|
||||
Reference in New Issue
Block a user