From 0c3f2704193e48eb7f2f409e053346427574bac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 25 Jul 2026 11:50:48 +0000 Subject: [PATCH] =?UTF-8?q?chat:=20rename=20claude=5Fsessions=20=E2=86=92?= =?UTF-8?q?=20general=5Fchat=5Fsessions;=20drop=20dead=20chat=5Fsessions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/servers/api/auth/signin.ts | 5 -- src/servers/api/chat/chat.ts | 8 +-- src/servers/api/chat/claude-sessions.ts | 49 +++++++++++++++---- src/servers/api/chat/websocket.ts | 10 ++-- src/servers/data-path.ts | 13 +---- src/servers/sidecar/claude/claude-manager.ts | 13 ++--- .../src/apps/ChatHistory/ChatDetailPanel.tsx | 12 +++-- .../src/apps/ChatHistory/PwdSelector.tsx | 10 ++-- .../src/apps/ChatHistory/SessionList.tsx | 36 +++++++++++--- src/workspaces/state/src/useClaudeSessions.ts | 11 ++++- 10 files changed, 107 insertions(+), 60 deletions(-) diff --git a/src/servers/api/auth/signin.ts b/src/servers/api/auth/signin.ts index 8cfb5c5b..be0aadb7 100755 --- a/src/servers/api/auth/signin.ts +++ b/src/servers/api/auth/signin.ts @@ -1,9 +1,6 @@ import type { Handler } from 'hono'; -import { mkdir } from 'node:fs/promises'; -import { join } from 'node:path'; import { getUserByEmail, getPasskeysByUserIdAndOrigin } from 'officerdb'; import { sign } from '@@/jwt'; -import { getClaudeDir } from '@@/data-path'; import argon2 from 'argon2'; import * as errors from '@@/custom-errors'; import { isLockdown, noteBlocked } from './panic'; @@ -33,8 +30,6 @@ export const signinHandler: Handler = async function (ctx) { const { id, name, username, role } = dbUser; - mkdir(join(getClaudeDir(email), 'archived'), { recursive: true }).catch(() => {}); - const tokenUser = { id, email, name, username, role, passkeys: passkeys.length }; if (passkeys.length > 0 && !origin.startsWith('chrome-extension://') && !TEST_USERS.includes(dbUser.id)) { diff --git a/src/servers/api/chat/chat.ts b/src/servers/api/chat/chat.ts index 6b6373ae..a04d5575 100644 --- a/src/servers/api/chat/chat.ts +++ b/src/servers/api/chat/chat.ts @@ -2,7 +2,7 @@ import type { Context } from 'hono'; import { createRouter } from '../../create-router'; import { getUserSettings } from 'officerdb'; import { - getClaudeSessionsCwd, + getGeneralChatSessionsCwd, listClaudePwds, listClaudeSessions, loadClaudeSession, @@ -24,14 +24,14 @@ 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 -// claude_sessions dir. Claude groups sessions by cwd, so this selects which project group we read. +// 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() || getClaudeSessionsCwd(email); +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: getClaudeSessionsCwd(email) }); + return ctx.json({ pwds: listClaudePwds(email), default: getGeneralChatSessionsCwd(email) }); }); // GET /chat/sessions[?cwd=] — conversations for a working directory, merged across both harnesses diff --git a/src/servers/api/chat/claude-sessions.ts b/src/servers/api/chat/claude-sessions.ts index d08ef905..7da93523 100644 --- a/src/servers/api/chat/claude-sessions.ts +++ b/src/servers/api/chat/claude-sessions.ts @@ -1,4 +1,15 @@ -import { readdirSync, readFileSync, existsSync, statSync, mkdirSync, rmSync, appendFileSync, openSync, readSync, closeSync } from 'node:fs'; +import { + readdirSync, + readFileSync, + existsSync, + statSync, + mkdirSync, + rmSync, + appendFileSync, + openSync, + readSync, + closeSync, +} from 'node:fs'; import { join } from 'node:path'; import { DATA_PATH } from '../../data-path'; @@ -13,11 +24,11 @@ import { DATA_PATH } from '../../data-path'; const claudeHome = (email: string): string => process.env.HOME_DIR ?? join(DATA_PATH, email, 'home'); /** Dedicated working directory for /chat sessions, so they form their own Claude "project" group. */ -export const getClaudeSessionsCwd = (email: string): string => join(DATA_PATH, email, 'claude_sessions'); +export const getGeneralChatSessionsCwd = (email: string): string => join(DATA_PATH, email, 'general_chat_sessions'); /** Same, but create the directory if it doesn't exist (call before spawning a /chat session). */ -export const ensureClaudeSessionsCwd = (email: string): string => { - const dir = getClaudeSessionsCwd(email); +export const ensureGeneralChatSessionsCwd = (email: string): string => { + const dir = getGeneralChatSessionsCwd(email); mkdirSync(dir, { recursive: true }); return dir; }; @@ -44,7 +55,11 @@ function entryText(message: unknown): string { if (typeof content === 'string') return content; if (Array.isArray(content)) { return content - .map((block) => (block && typeof block === 'object' && (block as { type?: string }).type === 'text' ? (block as { text?: string }).text ?? '' : '')) + .map((block) => + block && typeof block === 'object' && (block as { type?: string }).type === 'text' + ? ((block as { text?: string }).text ?? '') + : '', + ) .join('') .trim(); } @@ -116,7 +131,14 @@ function summarizeTranscript(filePath: string, id: string): ClaudeSessionSummary export type ClaudeChatMessage = | { role: 'user'; text: string } | { role: 'assistant'; id: string; text: string } - | { role: 'tool'; toolName: string; toolInput: Record; toolCallId: string; output?: string; isError?: boolean }; + | { + role: 'tool'; + toolName: string; + toolInput: Record; + toolCallId: string; + output?: string; + isError?: boolean; + }; type ContentBlock = | { type: 'text'; text?: string } @@ -128,7 +150,11 @@ function blockText(content: unknown): string { if (typeof content === 'string') return content; if (Array.isArray(content)) { return content - .map((b) => (b && typeof b === 'object' && (b as { type?: string }).type === 'text' ? (b as { text?: string }).text ?? '' : '')) + .map((b) => + b && typeof b === 'object' && (b as { type?: string }).type === 'text' + ? ((b as { text?: string }).text ?? '') + : '', + ) .join(''); } return ''; @@ -185,7 +211,12 @@ export function loadClaudeSession(email: string, cwd: string, sessionId: string) if (block.type === 'text' && block.text?.trim()) { messages.push({ role: 'assistant', id: `${sessionId}-${messages.length}`, text: block.text }); } else if (block.type === 'tool_use') { - const tool = { role: 'tool' as const, toolName: block.name, toolInput: block.input ?? {}, toolCallId: block.id }; + const tool = { + role: 'tool' as const, + toolName: block.name, + toolInput: block.input ?? {}, + toolCallId: block.id, + }; messages.push(tool); toolById.set(block.id, tool); } @@ -257,7 +288,7 @@ export type ClaudePwd = { cwd: string; sessionCount: number; updatedAt: string; /** All working directories that have Claude sessions, plus the default /chat dir. Newest first. */ export function listClaudePwds(email: string): ClaudePwd[] { const projectsDir = claudeProjectsDir(email); - const defaultCwd = getClaudeSessionsCwd(email); + const defaultCwd = getGeneralChatSessionsCwd(email); const byCwd = new Map(); if (existsSync(projectsDir)) { diff --git a/src/servers/api/chat/websocket.ts b/src/servers/api/chat/websocket.ts index 790136ea..0e4e04f6 100644 --- a/src/servers/api/chat/websocket.ts +++ b/src/servers/api/chat/websocket.ts @@ -4,7 +4,7 @@ import type { ClientMessage, ServerMessage, Message, ChatEvent } from './types'; import { sessionManager } from './session-manager'; import { sendClaudeCodeStreaming } from '@@/channels/send-claude-code'; import { sendOpenCodeStreaming } from '@@/channels/send-opencode'; -import { ensureClaudeSessionsCwd, getClaudeSessionsCwd } from './claude-sessions'; +import { ensureGeneralChatSessionsCwd, getGeneralChatSessionsCwd } from './claude-sessions'; import * as sidecar from '@@/sidecar-registry'; import { join } from 'path'; import { getHomeDirForRole, getEmailAccountsDir } from '../../../servers/data-path'; @@ -77,7 +77,7 @@ async function resolveEmailCwd(userId: number, ownerEmail: string, accountEmail? } // 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. +// pwd or the default general_chat_sessions dir; everything else (browser/project/dashboard) → the given cwd. async function resolveChatCwd( msg: { context?: string; contextId?: string; cwd?: string }, email: string, @@ -86,7 +86,7 @@ async function resolveChatCwd( ): Promise { 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 msg.cwd?.trim() ? resolveCwd(email, role, msg.cwd) : ensureGeneralChatSessionsCwd(email); return resolveCwd(email, role, msg.cwd); } @@ -460,8 +460,8 @@ async function handleOpenCodeChat( const onEvent = createEventHandler(sessionId, model, cwd); // The dir the OpenCode model should treat as its cwd (via the Officer system prompt). For the general - // /chat, the resolved cwd is just the claude_sessions grouping placeholder, so use the user's home. - const workingDir = cwd === getClaudeSessionsCwd(email) ? getHomeDirForRole(email, ws.data.role) : cwd; + // /chat, the resolved cwd is just the general_chat_sessions grouping placeholder, so use the user's home. + const workingDir = cwd === getGeneralChatSessionsCwd(email) ? getHomeDirForRole(email, ws.data.role) : cwd; try { const handle = await sendOpenCodeStreaming({ diff --git a/src/servers/data-path.ts b/src/servers/data-path.ts index 9733c7ca..5a55afc4 100644 --- a/src/servers/data-path.ts +++ b/src/servers/data-path.ts @@ -25,16 +25,6 @@ export const AGENT_CONFIG_DIR = join(homedir(), '.pi', 'agent'); export const SEED_PATH = resolve(import.meta.dir, '../../seed'); -export const getUserSessionsDir = (email: string) => join(DATA_PATH, email, 'chat_sessions'); - -export const getClaudeDir = (email: string) => join(DATA_PATH, email, 'chat_sessions', 'claude'); - -export const getSessionDir = (email: string, sessionId: string) => - join(DATA_PATH, email, 'chat_sessions', 'claude', sessionId); - -export const getArchivedSessionDir = (email: string, sessionId: string) => - join(DATA_PATH, email, 'chat_sessions', 'claude', 'archived', sessionId); - export const getHomeDir = (email: string) => join(DATA_PATH, email, 'home'); export const getHomeDirForRole = (email: string, role: string | null): string => @@ -53,7 +43,8 @@ export const getAttachmentsDir = (email: string, sessionId: string) => join(DATA export const getEmailAccountsDir = (ownerEmail: string) => join(DATA_PATH, ownerEmail, 'email_accounts'); export const getEmailDbPath = (ownerEmail: string, accountEmail: string) => join(getEmailAccountsDir(ownerEmail), accountEmail, 'emails.db'); -export const getEmailAttachmentCacheDir = (ownerEmail: string) => join(getEmailAccountsDir(ownerEmail), 'attachment_cache'); +export const getEmailAttachmentCacheDir = (ownerEmail: string) => + join(getEmailAccountsDir(ownerEmail), 'attachment_cache'); /** Derive a valid Linux username from a display username or email. */ export const toShellUsername = (username: string, email: string): string => { diff --git a/src/servers/sidecar/claude/claude-manager.ts b/src/servers/sidecar/claude/claude-manager.ts index 638ef659..7d4bd929 100644 --- a/src/servers/sidecar/claude/claude-manager.ts +++ b/src/servers/sidecar/claude/claude-manager.ts @@ -55,14 +55,7 @@ export async function spawnClaude(params: ClaudeSpawnParams): Promise('chat:active-cwd', null); const cwd = activeCwd ? { path: activeCwd } : locationState?.cwd; diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/PwdSelector.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/PwdSelector.tsx index 6f99af7f..135a6e2a 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/PwdSelector.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/PwdSelector.tsx @@ -4,7 +4,7 @@ import { useChatPwds } from 'state/useClaudeSessions'; import { DirPickerModal } from './DirPickerModal'; type PwdSelectorProps = { - value: string | null; // null = the default claude_sessions dir + value: string | null; // null = the default general_chat_sessions dir onChange: (cwd: string | null) => void; }; @@ -19,7 +19,7 @@ export const PwdSelector = ({ value, onChange }: PwdSelectorProps) => { const [custom, setCustom] = useState(''); const isDefaultActive = value === null || value === defaultCwd; - const label = isDefaultActive ? 'claude_sessions' : basename(value!); + const label = isDefaultActive ? 'general_chat_sessions' : basename(value!); const pick = (cwd: string | null) => { onChange(cwd); @@ -35,7 +35,7 @@ export const PwdSelector = ({ value, onChange }: PwdSelectorProps) => {
); diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx index ee474a5a..bb9074f8 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx @@ -11,7 +11,7 @@ import { PwdSelector } from './PwdSelector'; // Clicking a session loads its transcript and continues the real Claude session via --resume. export const SessionList = () => { const navigate = useNavigate(); - // The working directory the list operates on (null = the default claude_sessions dir). + // The working directory the list operates on (null = the default general_chat_sessions dir). const [activeCwd, setActiveCwd] = usePanelChannel('chat:active-cwd', null); const { sessions, isLoading, refetch, loadSession, deleteSession, renameSession } = useClaudeSessions(activeCwd); const [selected, setSelected] = usePanelChannel('chat:selected-session', null); @@ -125,10 +125,20 @@ export const SessionList = () => { onBlur={commitRename} className="flex-1 min-w-0 bg-transparent border-b border-duck-teal/40 text-sm outline-none" /> - -
@@ -145,7 +155,9 @@ export const SessionList = () => { )}
-
{session.title}
+
+ {session.title} +
{new Date(session.updatedAt).toLocaleDateString(undefined, { @@ -157,7 +169,9 @@ export const SessionList = () => { · {session.harness === 'opencode' ? ( - OpenCode + + OpenCode + ) : ( {session.messageCount} msg{session.messageCount === 1 ? '' : 's'} @@ -170,10 +184,18 @@ export const SessionList = () => { {isConfirming ? (
Delete? - -
diff --git a/src/workspaces/state/src/useClaudeSessions.ts b/src/workspaces/state/src/useClaudeSessions.ts index 87d91edd..8f83b8c5 100644 --- a/src/workspaces/state/src/useClaudeSessions.ts +++ b/src/workspaces/state/src/useClaudeSessions.ts @@ -19,13 +19,20 @@ export type ClaudeSessionSummary = { export type ClaudeSessionMessage = | { role: 'user'; text: string } | { role: 'assistant'; id: string; text: string } - | { role: 'tool'; toolName: string; toolInput: Record; toolCallId: string; output?: string; isError?: boolean }; + | { + role: 'tool'; + toolName: string; + toolInput: Record; + toolCallId: string; + output?: string; + isError?: boolean; + }; export type ClaudeSessionDetail = { id: string; model: string; cwd: string; messages: ClaudeSessionMessage[] }; export type ClaudePwd = { cwd: string; sessionCount: number; updatedAt: string; isDefault: boolean }; -// Query string for the selected working directory (null/undefined = the default claude_sessions dir). +// Query string for the selected working directory (null/undefined = the default general_chat_sessions dir). const cwdQuery = (cwd?: string | null) => (cwd ? `?cwd=${encodeURIComponent(cwd)}` : ''); /** The default /chat dir plus every directory that already has Claude sessions. */