a chat with no chosen directory runs in the caller's own home
The default was DATA_PATH/<email>/general_chat_sessions, a dedicated directory so /chat sessions formed their own Claude project group instead of cluttering the home. It is a sibling of the home, and confineUserTree makes every sibling the platform's at 0700 because the others are attachments and email_accounts. So it was unreachable for a member: the first live member turn started there and every Bash call failed on its own working directory before doing anything. A per-member copy inside each home fixed the symptom and left two rules to remember. The owner chose one rule instead — the account's own home, whoever they are — and accepted the trade knowingly: /chat sessions now share a project group with anything else run from that home, which was the reason the dedicated directory existed. Removed rather than left dangling: getGeneralChatSessionsCwd, ensureGeneralChatSessionsCwd, ensureMemberChatCwd, and general_chat_sessions from USER_DIRS so new accounts stop getting it. Existing directories are untouched and their transcripts stay where they are — Claude groups by cwd, so the owner's old /chat history remains under its own project slug rather than moving. The UI labels move with it: the default group now reads "home" rather than naming a directory that no longer has a role. ChatIdentity keeps carrying both email and home. The pairing was justified in the comment by general_chat_sessions being email-derived, which is now gone — but the distinction it encodes is real (the email says who, the home says where), so the comment explains that instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,6 @@ import * as errors from '@@/custom-errors';
|
|||||||
import * as sidecar from '@@/sidecar-registry';
|
import * as sidecar from '@@/sidecar-registry';
|
||||||
import { getUserSettings } from 'officerdb';
|
import { getUserSettings } from 'officerdb';
|
||||||
import {
|
import {
|
||||||
getGeneralChatSessionsCwd,
|
|
||||||
listClaudePwds,
|
listClaudePwds,
|
||||||
listClaudeSessions,
|
listClaudeSessions,
|
||||||
loadClaudeSession,
|
loadClaudeSession,
|
||||||
@@ -71,22 +70,22 @@ export const chatRouter = createRouter();
|
|||||||
// reviewer who had not written it. If you are reverting this, revert to a refusal — not to a narrower one.
|
// reviewer who had not written it. If you are reverting this, revert to a refusal — not to a narrower one.
|
||||||
|
|
||||||
// The working directory a request operates on: an explicit ?cwd= (a chosen pwd), else the default
|
// The working directory a request operates on: an explicit ?cwd= (a chosen pwd), else the default
|
||||||
// general_chat_sessions dir. Claude groups sessions by cwd, so this selects which project group we read.
|
// caller's own home. Claude groups sessions by cwd, so this selects which project group we read.
|
||||||
// OpenCode runs on one fixed serve, but each session records the directory its turn ran in, so cwd
|
// OpenCode runs on one fixed serve, but each session records the directory its turn ran in, so cwd
|
||||||
// selects there too.
|
// selects there too.
|
||||||
const cwdOf = (ctx: Context, email: string): string => ctx.req.query('cwd')?.trim() || getGeneralChatSessionsCwd(email);
|
const cwdOf = (ctx: Context, home: string): string => ctx.req.query('cwd')?.trim() || home;
|
||||||
|
|
||||||
// GET /chat/pwds — the default /chat dir plus every directory that already has Claude sessions.
|
// GET /chat/pwds — the default /chat dir plus every directory that already has Claude sessions.
|
||||||
chatRouter.get('/pwds', async (ctx) => {
|
chatRouter.get('/pwds', async (ctx) => {
|
||||||
const who = await chatIdentity(ctx.get('user'));
|
const who = await chatIdentity(ctx.get('user'));
|
||||||
return ctx.json({ pwds: listClaudePwds(who), default: getGeneralChatSessionsCwd(who.email) });
|
return ctx.json({ pwds: listClaudePwds(who), default: who.home });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /chat/sessions[?cwd=] — conversations for a working directory, merged across both harnesses
|
// GET /chat/sessions[?cwd=] — conversations for a working directory, merged across both harnesses
|
||||||
// (Claude transcripts + OpenCode's session store), newest first.
|
// (Claude transcripts + OpenCode's session store), newest first.
|
||||||
chatRouter.get('/sessions', async (ctx) => {
|
chatRouter.get('/sessions', async (ctx) => {
|
||||||
const who = await chatIdentity(ctx.get('user'));
|
const who = await chatIdentity(ctx.get('user'));
|
||||||
const cwd = cwdOf(ctx, who.email);
|
const cwd = cwdOf(ctx, who.home);
|
||||||
const claude = listClaudeSessions(who, cwd).map((s) => ({ ...s, harness: 'claude' as const }));
|
const claude = listClaudeSessions(who, cwd).map((s) => ({ ...s, harness: 'claude' as const }));
|
||||||
const opencode = await listOpenCodeSessions(cwd);
|
const opencode = await listOpenCodeSessions(cwd);
|
||||||
const sessions = [...claude, ...opencode].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
const sessions = [...claude, ...opencode].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
||||||
@@ -102,7 +101,7 @@ chatRouter.get('/sessions', async (ctx) => {
|
|||||||
chatRouter.get('/sessions/:id', async (ctx) => {
|
chatRouter.get('/sessions/:id', async (ctx) => {
|
||||||
const who = await chatIdentity(ctx.get('user'));
|
const who = await chatIdentity(ctx.get('user'));
|
||||||
const id = ctx.req.param('id');
|
const id = ctx.req.param('id');
|
||||||
const cwd = cwdOf(ctx, who.email);
|
const cwd = cwdOf(ctx, who.home);
|
||||||
// Fall back to a by-id scan when the (default) cwd doesn't hold it — a fresh /chat/<id> deep-link/refresh
|
// Fall back to a by-id scan when the (default) cwd doesn't hold it — a fresh /chat/<id> deep-link/refresh
|
||||||
// doesn't know the session's cwd. The returned detail carries the real cwd for the client to scope the UI.
|
// doesn't know the session's cwd. The returned detail carries the real cwd for the client to scope the UI.
|
||||||
const detail = isOpenCodeSessionId(id)
|
const detail = isOpenCodeSessionId(id)
|
||||||
@@ -212,7 +211,7 @@ chatRouter.get('/live', async (ctx) => {
|
|||||||
chatRouter.delete('/sessions/:id', async (ctx) => {
|
chatRouter.delete('/sessions/:id', async (ctx) => {
|
||||||
const who = await chatIdentity(ctx.get('user'));
|
const who = await chatIdentity(ctx.get('user'));
|
||||||
const id = ctx.req.param('id');
|
const id = ctx.req.param('id');
|
||||||
const cwd = cwdOf(ctx, who.email);
|
const cwd = cwdOf(ctx, who.home);
|
||||||
const ok = isOpenCodeSessionId(id) ? await deleteOpenCodeSession(id) : deleteClaudeSession(who, cwd, id);
|
const ok = isOpenCodeSessionId(id) ? await deleteOpenCodeSession(id) : deleteClaudeSession(who, cwd, id);
|
||||||
if (!ok) return ctx.text('Not found', 404);
|
if (!ok) return ctx.text('Not found', 404);
|
||||||
return ctx.json({ ok: true });
|
return ctx.json({ ok: true });
|
||||||
@@ -222,7 +221,7 @@ chatRouter.delete('/sessions/:id', async (ctx) => {
|
|||||||
chatRouter.patch('/sessions/:id/title', async (ctx) => {
|
chatRouter.patch('/sessions/:id/title', async (ctx) => {
|
||||||
const who = await chatIdentity(ctx.get('user'));
|
const who = await chatIdentity(ctx.get('user'));
|
||||||
const id = ctx.req.param('id');
|
const id = ctx.req.param('id');
|
||||||
const cwd = cwdOf(ctx, who.email);
|
const cwd = cwdOf(ctx, who.home);
|
||||||
const { title } = await ctx.req.json<{ title?: string }>();
|
const { title } = await ctx.req.json<{ title?: string }>();
|
||||||
if (!title?.trim()) return ctx.text('title is required', 400);
|
if (!title?.trim()) return ctx.text('title is required', 400);
|
||||||
const ok = isOpenCodeSessionId(id)
|
const ok = isOpenCodeSessionId(id)
|
||||||
|
|||||||
@@ -29,25 +29,20 @@ import { DATA_PATH } from '../../data-path';
|
|||||||
// that were a property rather than an assumption. A member reaching these functions would have been handed the
|
// that were a property rather than an assumption. A member reaching these functions would have been handed the
|
||||||
// owner's conversation list.
|
// owner's conversation list.
|
||||||
//
|
//
|
||||||
// So the home arrives resolved, from `resolveHomeDir(userId)`, and this file has no way to invent one. The
|
// So the home arrives resolved, from `resolveHomeDir(userId)`, and this file has no way to invent one.
|
||||||
// email is still passed where a path is genuinely email-derived — `general_chat_sessions` lives under
|
//
|
||||||
// DATA_PATH, not under a home — which is why both travel together rather than one standing in for the other.
|
// The email travels alongside it rather than being derived from it, because the two answer different
|
||||||
|
// questions: the email says WHO, the home says WHERE. They were briefly conflated in the other direction —
|
||||||
|
// `general_chat_sessions` was an email-derived path under DATA_PATH used as a chat's working directory, and
|
||||||
|
// because `confineUserTree` makes every sibling of a home the platform's at 0700, a member's turn started in
|
||||||
|
// a directory it could not enter. That default is now the caller's own home; the pairing survives because the
|
||||||
|
// distinction it encodes is real.
|
||||||
export type ChatIdentity = {
|
export type ChatIdentity = {
|
||||||
email: string;
|
email: string;
|
||||||
/** From `resolveHomeDir`. Never `getOwnerHomeDir`, which ignores its argument. */
|
/** From `resolveHomeDir`. Never `getOwnerHomeDir`, which ignores its argument. */
|
||||||
home: string;
|
home: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Dedicated working directory for /chat sessions, so they form their own Claude "project" group. */
|
|
||||||
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 ensureGeneralChatSessionsCwd = (email: string): string => {
|
|
||||||
const dir = getGeneralChatSessionsCwd(email);
|
|
||||||
mkdirSync(dir, { recursive: true });
|
|
||||||
return dir;
|
|
||||||
};
|
|
||||||
|
|
||||||
const claudeProjectsDir = (home: string): string => join(home, '.claude', 'projects');
|
const claudeProjectsDir = (home: string): string => join(home, '.claude', 'projects');
|
||||||
|
|
||||||
/** Claude's folder name for a working directory. */
|
/** Claude's folder name for a working directory. */
|
||||||
@@ -767,10 +762,10 @@ function firstCwd(filePath: string): string {
|
|||||||
|
|
||||||
export type ClaudePwd = { cwd: string; sessionCount: number; updatedAt: string; isDefault: boolean };
|
export type ClaudePwd = { cwd: string; sessionCount: number; updatedAt: string; isDefault: boolean };
|
||||||
|
|
||||||
/** All working directories that have Claude sessions, plus the default /chat dir. Newest first. */
|
/** All working directories that have Claude sessions, plus the caller's home. Newest first. */
|
||||||
export function listClaudePwds(who: ChatIdentity): ClaudePwd[] {
|
export function listClaudePwds(who: ChatIdentity): ClaudePwd[] {
|
||||||
const projectsDir = claudeProjectsDir(who.home);
|
const projectsDir = claudeProjectsDir(who.home);
|
||||||
const defaultCwd = getGeneralChatSessionsCwd(who.email);
|
const defaultCwd = who.home;
|
||||||
const byCwd = new Map<string, { count: number; updatedAt: string }>();
|
const byCwd = new Map<string, { count: number; updatedAt: string }>();
|
||||||
|
|
||||||
if (existsSync(projectsDir)) {
|
if (existsSync(projectsDir)) {
|
||||||
|
|||||||
@@ -13,13 +13,12 @@ import { sessionManager } from './session-manager';
|
|||||||
import { rememberOpenCodePrompt } from './opencode/state';
|
import { rememberOpenCodePrompt } from './opencode/state';
|
||||||
import { sendClaudeCodeStreaming } from '@@/channels/send-claude-code';
|
import { sendClaudeCodeStreaming } from '@@/channels/send-claude-code';
|
||||||
import { sendOpenCodeStreaming } from '@@/channels/send-opencode';
|
import { sendOpenCodeStreaming } from '@@/channels/send-opencode';
|
||||||
import { ensureGeneralChatSessionsCwd } from './claude-sessions';
|
|
||||||
import * as sidecar from '@@/sidecar-registry';
|
import * as sidecar from '@@/sidecar-registry';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { getOwnerHomeDir, getEmailAccountsDir } from '../../../servers/data-path';
|
import { getOwnerHomeDir, getEmailAccountsDir } from '../../../servers/data-path';
|
||||||
import { getUserSettings, getEmailAccounts, getChatEventsSince, appendChatEvent, getUserById } from 'officerdb';
|
import { getUserSettings, getEmailAccounts, getChatEventsSince, appendChatEvent, getUserById } from 'officerdb';
|
||||||
import { resolveHomeDir } from '@@/user-home';
|
import { resolveHomeDir } from '@@/user-home';
|
||||||
import { claudeLoginState, ensureMemberChatCwd } from '@@/os-user-claude';
|
import { claudeLoginState } from '@@/os-user-claude';
|
||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
|
|
||||||
@@ -132,21 +131,29 @@ 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
|
// The working directory a chat turn runs in, by context: email → the account dir; /chat → a chosen
|
||||||
// pwd or the default general_chat_sessions dir; everything else (browser/project/dashboard) → the given cwd.
|
// pwd or the caller's own home; everything else (browser/project/dashboard) → the given cwd.
|
||||||
async function resolveChatCwd(
|
async function resolveChatCwd(
|
||||||
msg: { context?: string; contextId?: string; cwd?: string },
|
msg: { context?: string; contextId?: string; cwd?: string },
|
||||||
email: string,
|
email: string,
|
||||||
userId: number,
|
userId: number,
|
||||||
home: string,
|
home: string,
|
||||||
member?: { osUser: string; home: string },
|
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (msg.context === 'email') return resolveEmailCwd(userId, email, msg.contextId);
|
if (msg.context === 'email') return resolveEmailCwd(userId, email, msg.contextId);
|
||||||
if (msg.context === 'chat') {
|
if (msg.context === 'chat') {
|
||||||
if (msg.cwd?.trim()) return resolveCwd(home, msg.cwd);
|
if (msg.cwd?.trim()) return resolveCwd(home, msg.cwd);
|
||||||
// The default chat directory. The owner's is a sibling of their home; a member's is INSIDE theirs,
|
// The caller's own home, for everyone.
|
||||||
// because that sibling is platform-owned at 0700 by design and a member cannot enter it — which is
|
//
|
||||||
// exactly how the first live member turn failed, with every Bash call dying on its own cwd.
|
// This used to be `DATA_PATH/<email>/general_chat_sessions`, a dedicated directory so /chat sessions
|
||||||
return member ? ensureMemberChatCwd(member) : ensureGeneralChatSessionsCwd(email);
|
// formed their own Claude project group and did not clutter the home. That is a sibling of the home,
|
||||||
|
// and `confineUserTree` makes every sibling the platform's at 0700 because the others are `attachments`
|
||||||
|
// and `email_accounts` — so it was unreachable for a member. The first live member turn ran there and
|
||||||
|
// every Bash call failed on its own working directory before doing anything.
|
||||||
|
//
|
||||||
|
// A per-member copy inside each home would have worked and would have left two rules to remember. The
|
||||||
|
// owner chose one: a chat with no chosen directory runs in the account's own home, whoever they are.
|
||||||
|
// The cost is that /chat sessions now share a project group with anything else run from that home,
|
||||||
|
// which was the reason the dedicated directory existed and is a trade the owner made knowingly.
|
||||||
|
return home;
|
||||||
}
|
}
|
||||||
return resolveCwd(home, msg.cwd);
|
return resolveCwd(home, msg.cwd);
|
||||||
}
|
}
|
||||||
@@ -402,7 +409,7 @@ async function handleClaudeCodeChat(
|
|||||||
|
|
||||||
const home = identity.kind === 'member' ? identity.run.home : getOwnerHomeDir(email);
|
const home = identity.kind === 'member' ? identity.run.home : getOwnerHomeDir(email);
|
||||||
|
|
||||||
const cwd = await resolveChatCwd(msg, email, userId, home, identity.kind === 'member' ? identity.run : undefined);
|
const cwd = await resolveChatCwd(msg, email, userId, home);
|
||||||
|
|
||||||
const groupSlug = msg.groupSlug || null;
|
const groupSlug = msg.groupSlug || null;
|
||||||
|
|
||||||
|
|||||||
@@ -52,16 +52,7 @@ export const getOwnerHomeDir = (email: string): string => process.env.HOME_DIR ?
|
|||||||
// Single-sourced here rather than in the script that used to own the list, because there are now two
|
// Single-sourced here rather than in the script that used to own the list, because there are now two
|
||||||
// callers — `scripts/provision-user-dirs.ts` and the owner's create-account handler — and a skeleton
|
// callers — `scripts/provision-user-dirs.ts` and the owner's create-account handler — and a skeleton
|
||||||
// that differs depending on how the account was made is a bug nobody would think to look for.
|
// that differs depending on how the account was made is a bug nobody would think to look for.
|
||||||
export const USER_DIRS = [
|
export const USER_DIRS = ['home', 'attachments', 'cache', 'dashboards', 'email_accounts', 'logs', 'sidecar'] as const;
|
||||||
'home',
|
|
||||||
'attachments',
|
|
||||||
'cache',
|
|
||||||
'dashboards',
|
|
||||||
'email_accounts',
|
|
||||||
'general_chat_sessions',
|
|
||||||
'logs',
|
|
||||||
'sidecar',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an account's root and its skeleton, closed by default.
|
* Create an account's root and its skeleton, closed by default.
|
||||||
|
|||||||
@@ -123,23 +123,6 @@ export async function provisionClaudeCli(params: { email: string; osUser: string
|
|||||||
return { ok: true, binPath, wrote: true };
|
return { ok: true, binPath, wrote: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A member's own chat-sessions directory, created as them, inside their home.
|
|
||||||
*
|
|
||||||
* The owner's equivalent lives at `DATA_PATH/<email>/general_chat_sessions` — a SIBLING of the home, which
|
|
||||||
* `confineUserTree` deliberately makes the platform's at 0700 because the other siblings are `attachments`
|
|
||||||
* and `email_accounts`. Correct for those, and fatal as a member's cwd: the first live member turn ran there
|
|
||||||
* and every Bash call failed, because the account could not enter its own working directory.
|
|
||||||
*
|
|
||||||
* So a member's goes inside their home instead. Created through `runAs` because the platform cannot mkdir
|
|
||||||
* into a 0700 home it does not own — and `mkdir -p` is idempotent, so this is safe to call per turn.
|
|
||||||
*/
|
|
||||||
export async function ensureMemberChatCwd(params: { osUser: string; home: string }): Promise<string> {
|
|
||||||
const dir = join(params.home, 'general_chat_sessions');
|
|
||||||
await asMember(params.osUser, ['mkdir', '-p', dir]);
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ClaudeLoginState = {
|
export type ClaudeLoginState = {
|
||||||
/** The binary is present and executable in their home. */
|
/** The binary is present and executable in their home. */
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ function NewChat(props: NewChatProps) {
|
|||||||
invalidateClaudeSessions();
|
invalidateClaudeSessions();
|
||||||
}, [invalidateClaudeSessions]);
|
}, [invalidateClaudeSessions]);
|
||||||
|
|
||||||
// context 'chat' tells the backend to run this session from the dedicated general_chat_sessions cwd, so
|
// context 'chat' tells the backend to run this session from the caller's own home, so
|
||||||
// its transcript lands in Claude's own store as an isolated project group (source of truth).
|
// its transcript lands in Claude's own store as an isolated project group (source of truth).
|
||||||
const chat = useChat(undefined, locationState?.model, {
|
const chat = useChat(undefined, locationState?.model, {
|
||||||
resumeSummary,
|
resumeSummary,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useChatPwds } from 'state/useClaudeSessions';
|
|||||||
import { DirPickerModal } from './DirPickerModal';
|
import { DirPickerModal } from './DirPickerModal';
|
||||||
|
|
||||||
type PwdSelectorProps = {
|
type PwdSelectorProps = {
|
||||||
value: string | null; // null = the default general_chat_sessions dir
|
value: string | null; // null = the caller's own home
|
||||||
onChange: (cwd: string | null) => void;
|
onChange: (cwd: string | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ export const PwdSelector = ({ value, onChange }: PwdSelectorProps) => {
|
|||||||
const [custom, setCustom] = useState('');
|
const [custom, setCustom] = useState('');
|
||||||
|
|
||||||
const isDefaultActive = value === null || value === defaultCwd;
|
const isDefaultActive = value === null || value === defaultCwd;
|
||||||
const label = isDefaultActive ? 'general_chat_sessions' : basename(value!);
|
const label = isDefaultActive ? 'home' : basename(value!);
|
||||||
|
|
||||||
const pick = (cwd: string | null) => {
|
const pick = (cwd: string | null) => {
|
||||||
onChange(cwd);
|
onChange(cwd);
|
||||||
@@ -58,9 +58,7 @@ export const PwdSelector = ({ value, onChange }: PwdSelectorProps) => {
|
|||||||
className={`flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs cursor-pointer hover:bg-muted ${selected ? 'text-duck-teal' : 'text-foreground/80'}`}
|
className={`flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs cursor-pointer hover:bg-muted ${selected ? 'text-duck-teal' : 'text-foreground/80'}`}
|
||||||
>
|
>
|
||||||
<Check className={`h-3.5 w-3.5 shrink-0 ${selected ? 'opacity-100' : 'opacity-0'}`} />
|
<Check className={`h-3.5 w-3.5 shrink-0 ${selected ? 'opacity-100' : 'opacity-0'}`} />
|
||||||
<span className="min-w-0 flex-1 truncate">
|
<span className="min-w-0 flex-1 truncate">{p.isDefault ? 'Default · home' : shorten(p.cwd)}</span>
|
||||||
{p.isDefault ? 'Default · general_chat_sessions' : shorten(p.cwd)}
|
|
||||||
</span>
|
|
||||||
{p.sessionCount > 0 && <span className="shrink-0 opacity-40">{p.sessionCount}</span>}
|
{p.sessionCount > 0 && <span className="shrink-0 opacity-40">{p.sessionCount}</span>}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const SessionList = () => {
|
|||||||
const [selected, setSelected] = useSelectedChatSession();
|
const [selected, setSelected] = useSelectedChatSession();
|
||||||
// A group path when we're on one; otherwise the open session's own directory, so /chat/<id> shows
|
// A group path when we're on one; otherwise the open session's own directory, so /chat/<id> shows
|
||||||
// that session among its neighbours instead of snapping the list back to the default group. Null =
|
// that session among its neighbours instead of snapping the list back to the default group. Null =
|
||||||
// the default general_chat_sessions dir.
|
// the caller's own home.
|
||||||
const activeCwd = cwdFromSplat(splat) ?? selected?.cwd ?? null;
|
const activeCwd = cwdFromSplat(splat) ?? selected?.cwd ?? null;
|
||||||
const { sessions, isLoading, error, refetch, deleteSession, renameSession } = useClaudeSessions(activeCwd);
|
const { sessions, isLoading, error, refetch, deleteSession, renameSession } = useClaudeSessions(activeCwd);
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const GROUP_SEGMENT = 'g';
|
|||||||
/** Absolute cwd → path suffix. Encoded per segment; React Router decodes it the same way. */
|
/** Absolute cwd → path suffix. Encoded per segment; React Router decodes it the same way. */
|
||||||
const encodeCwd = (cwd: string): string => cwd.split('/').filter(Boolean).map(encodeURIComponent).join('/');
|
const encodeCwd = (cwd: string): string => cwd.split('/').filter(Boolean).map(encodeURIComponent).join('/');
|
||||||
|
|
||||||
/** The list for a group. `null` = the default general_chat_sessions group. */
|
/** The list for a group. `null` = the caller's own home. */
|
||||||
export const chatListPath = (cwd: string | null | undefined): string =>
|
export const chatListPath = (cwd: string | null | undefined): string =>
|
||||||
cwd ? `/chat/${GROUP_SEGMENT}/${encodeCwd(cwd)}` : '/chat';
|
cwd ? `/chat/${GROUP_SEGMENT}/${encodeCwd(cwd)}` : '/chat';
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export type ClaudeSessionDetail = {
|
|||||||
|
|
||||||
export type ClaudePwd = { cwd: string; sessionCount: number; updatedAt: string; isDefault: boolean };
|
export type ClaudePwd = { cwd: string; sessionCount: number; updatedAt: string; isDefault: boolean };
|
||||||
|
|
||||||
// Query string for the selected working directory (null/undefined = the default general_chat_sessions dir).
|
// Query string for the selected working directory (null/undefined = the caller's own home).
|
||||||
const cwdQuery = (cwd?: string | null) => (cwd ? `?cwd=${encodeURIComponent(cwd)}` : '');
|
const cwdQuery = (cwd?: string | null) => (cwd ? `?cwd=${encodeURIComponent(cwd)}` : '');
|
||||||
|
|
||||||
/** The default /chat dir plus every directory that already has Claude sessions. */
|
/** The default /chat dir plus every directory that already has Claude sessions. */
|
||||||
|
|||||||
Reference in New Issue
Block a user