a screen can declare its own chat context
ChatPanelWrapper's own comment asked for this: "the general fix is a way for a
plugin to declare a chat context, which does not exist. Until it does, adding a
SECOND id here is the signal that it needs to." this is the general fix rather
than the second id.
a caller passes chatContext; the workspace-derived branch stays for the screens
the platform mounts, including the hardcoded 'email' one, which is not a caller
that could pass anything.
the listing needed the same. ChatHeader derives an absolute cwd by joining the
workspace's home-relative path onto the account home — right for a dashboard,
impossible for a plugin, whose directory the server builds and never sends. so
useClaudeSessions takes a context instead and lets the server resolve the same
path it resolved for the turn. it goes in the query key too, or two plugins
share one cached list.
ChatHeader is exported now, because a registry header is mounted with only
{ panelId } — binding a context means wrapping it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -33,7 +33,22 @@ export type ChatSessionSelection = {
|
|||||||
|
|
||||||
const title = (s: { title: string }) => s.title || '(untitled)';
|
const title = (s: { title: string }) => s.title || '(untitled)';
|
||||||
|
|
||||||
export const ChatHeader = ({ panelId }: { panelId?: string }) => {
|
/**
|
||||||
|
* `chatContext` is for a panel whose directory the CLIENT cannot name.
|
||||||
|
*
|
||||||
|
* A dashboard is rooted in a home-relative path the browser already knows, so the cwd derivation below
|
||||||
|
* works. A plugin's chats live in a sibling of the home that the server builds and never sends — so
|
||||||
|
* without this the header would list nothing at all, which is the failure the cwd comment right below
|
||||||
|
* describes for a different reason. Given a context, the whole derivation is skipped and the server
|
||||||
|
* resolves the same directory it resolved for the turn.
|
||||||
|
*/
|
||||||
|
export const ChatHeader = ({
|
||||||
|
panelId,
|
||||||
|
chatContext,
|
||||||
|
}: {
|
||||||
|
panelId?: string;
|
||||||
|
chatContext?: { context: string; contextId?: string } | null;
|
||||||
|
}) => {
|
||||||
const { t, tUi } = useTranslation();
|
const { t, tUi } = useTranslation();
|
||||||
const { workspace, cwd } = useWorkspace();
|
const { workspace, cwd } = useWorkspace();
|
||||||
|
|
||||||
@@ -52,7 +67,9 @@ export const ChatHeader = ({ panelId }: { panelId?: string }) => {
|
|||||||
const relative = cwdToPath(cwd);
|
const relative = cwdToPath(cwd);
|
||||||
const absoluteCwd = relative === '/' ? null : defaultCwd ? `${defaultCwd.replace(/\/$/, '')}${relative}` : null;
|
const absoluteCwd = relative === '/' ? null : defaultCwd ? `${defaultCwd.replace(/\/$/, '')}${relative}` : null;
|
||||||
|
|
||||||
const { sessions, renameSession, deleteSession } = useClaudeSessions(absoluteCwd);
|
const { sessions, renameSession, deleteSession } = useClaudeSessions(absoluteCwd, {
|
||||||
|
chatContext: chatContext ?? null,
|
||||||
|
});
|
||||||
const [sessionId, setSessionId] = usePanelSession(panelId, workspace);
|
const [sessionId, setSessionId] = usePanelSession(panelId, workspace);
|
||||||
|
|
||||||
const [renaming, setRenaming] = useState<string | null>(null);
|
const [renaming, setRenaming] = useState<string | null>(null);
|
||||||
|
|||||||
@@ -151,9 +151,22 @@ const ChatPanelInner = ({
|
|||||||
// `promptPrefix` is a system prompt only this app understands, so the screen that wants one passes it
|
// `promptPrefix` is a system prompt only this app understands, so the screen that wants one passes it
|
||||||
// here rather than through the workspace context — a screen supplies its own chat panel through
|
// here rather than through the workspace context — a screen supplies its own chat panel through
|
||||||
// `components`, which is what that prop exists for. The registry mounts this with no prefix.
|
// `components`, which is what that prop exists for. The registry mounts this with no prefix.
|
||||||
type ChatPanelWrapperProps = { panelId: string; promptPrefix?: string };
|
type ChatPanelWrapperProps = {
|
||||||
|
panelId: string;
|
||||||
|
promptPrefix?: string;
|
||||||
|
/**
|
||||||
|
* Declared by a screen that owns its own chat directory — currently a plugin's panel.
|
||||||
|
*
|
||||||
|
* This is the "general fix" the `chatContext` comment below asks for. It said a plugin declaring a chat
|
||||||
|
* context did not exist, and that a SECOND hardcoded screen id here would be the signal it was needed.
|
||||||
|
* Rather than add that second id, a caller can now say what it is, and the server resolves the directory
|
||||||
|
* from the same pair it uses for the turn. The `'email'` branch stays where it is: it is a screen the
|
||||||
|
* platform mounts, not a caller that could pass this.
|
||||||
|
*/
|
||||||
|
chatContext?: { context: string; contextId?: string };
|
||||||
|
};
|
||||||
|
|
||||||
export const ChatPanelWrapper = ({ panelId, promptPrefix }: ChatPanelWrapperProps) => {
|
export const ChatPanelWrapper = ({ panelId, promptPrefix, chatContext: declared }: ChatPanelWrapperProps) => {
|
||||||
const { workspace, cwd, root } = useWorkspace();
|
const { workspace, cwd, root } = useWorkspace();
|
||||||
const scoped = cwd !== '~';
|
const scoped = cwd !== '~';
|
||||||
|
|
||||||
@@ -171,8 +184,9 @@ export const ChatPanelWrapper = ({ panelId, promptPrefix }: ChatPanelWrapperProp
|
|||||||
//
|
//
|
||||||
// The general fix is a way for a plugin to declare a chat context, which does not exist. Until it does,
|
// The general fix is a way for a plugin to declare a chat context, which does not exist. Until it does,
|
||||||
// adding a SECOND id here is the signal that it needs to.
|
// adding a SECOND id here is the signal that it needs to.
|
||||||
const chatContext =
|
const chatContext = declared
|
||||||
workspace?.kind === 'screen' && workspace.id === 'email'
|
? declared
|
||||||
|
: workspace?.kind === 'screen' && workspace.id === 'email'
|
||||||
? { context: 'email' as const }
|
? { context: 'email' as const }
|
||||||
: workspace?.kind === 'dashboard'
|
: workspace?.kind === 'dashboard'
|
||||||
? { context: 'dashboard' as const, contextId: workspace.key }
|
? { context: 'dashboard' as const, contextId: workspace.key }
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export { AttachmentList } from './components/AttachmentList';
|
|||||||
export { AttachButton } from './components/AttachButton';
|
export { AttachButton } from './components/AttachButton';
|
||||||
export { WebpageDialog } from './components/WebpageDialog';
|
export { WebpageDialog } from './components/WebpageDialog';
|
||||||
export { EmbeddableChat, type UseEmbeddableChatType } from './EmbeddableChat';
|
export { EmbeddableChat, type UseEmbeddableChatType } from './EmbeddableChat';
|
||||||
|
// Exported for a plugin that mounts its own chat panel: the registry hands a header only `{ panelId }`,
|
||||||
|
// so binding a chat context means wrapping this rather than passing a prop through the registry.
|
||||||
|
export { ChatHeader } from './ChatHeader';
|
||||||
export { useChat, type UseChatType } from '../../hooks/useChat';
|
export { useChat, type UseChatType } from '../../hooks/useChat';
|
||||||
export { useSlashCommands } from './useSlashCommands';
|
export { useSlashCommands } from './useSlashCommands';
|
||||||
export { useAttachments, type UseAttachmentsType } from './useAttachments';
|
export { useAttachments, type UseAttachmentsType } from './useAttachments';
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export {
|
|||||||
InputArea,
|
InputArea,
|
||||||
ChatLauncher,
|
ChatLauncher,
|
||||||
ChatPanelWrapper,
|
ChatPanelWrapper,
|
||||||
|
ChatHeader,
|
||||||
AttachmentList,
|
AttachmentList,
|
||||||
AttachButton,
|
AttachButton,
|
||||||
WebpageDialog,
|
WebpageDialog,
|
||||||
|
|||||||
@@ -119,6 +119,18 @@ export type ClaudePwd = { cwd: string; sessionCount: number; updatedAt: string;
|
|||||||
// Query string for the selected working directory (null/undefined = the caller's own home).
|
// 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)}` : '');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A chat context, for a caller that cannot name its own directory.
|
||||||
|
*
|
||||||
|
* A plugin's chats run in `$DATA_PATH/<email>/plugins/<appName>/chats`, which the server builds and never
|
||||||
|
* sends to the browser — so the `?cwd=` form has nothing to put in it. Naming the context instead lets the
|
||||||
|
* server resolve the same path it resolves for the turn. See `cwdOf` in `api/chat/chat.ts`.
|
||||||
|
*/
|
||||||
|
export type ChatContext = { context: string; contextId?: string };
|
||||||
|
|
||||||
|
const contextQuery = (ctx: ChatContext) =>
|
||||||
|
`?context=${encodeURIComponent(ctx.context)}${ctx.contextId ? `&contextId=${encodeURIComponent(ctx.contextId)}` : ''}`;
|
||||||
|
|
||||||
/** The default /chat dir plus every directory that already has Claude sessions. */
|
/** The default /chat dir plus every directory that already has Claude sessions. */
|
||||||
export function useChatPwds() {
|
export function useChatPwds() {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
@@ -143,16 +155,25 @@ export function useChatPwds() {
|
|||||||
* ZERO requests while a node is collapsed — twenty directories on screen is otherwise twenty transcript
|
* ZERO requests while a node is collapsed — twenty directories on screen is otherwise twenty transcript
|
||||||
* scans for rows nobody has asked to see.
|
* scans for rows nobody has asked to see.
|
||||||
*/
|
*/
|
||||||
export function useClaudeSessions(cwd?: string | null, options?: { all?: boolean; enabled?: boolean }) {
|
export function useClaudeSessions(
|
||||||
|
cwd?: string | null,
|
||||||
|
options?: { all?: boolean; enabled?: boolean; chatContext?: ChatContext | null },
|
||||||
|
) {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
const all = options?.all === true;
|
const all = options?.all === true;
|
||||||
|
const chatContext = options?.chatContext ?? null;
|
||||||
// `all` ignores cwd entirely — sending both would imply the server intersects them, and it does not.
|
// `all` ignores cwd entirely — sending both would imply the server intersects them, and it does not.
|
||||||
const q = all ? '?all=1' : cwdQuery(cwd);
|
// A context likewise replaces cwd rather than joining it: the server resolves one directory from it.
|
||||||
|
const q = all ? '?all=1' : chatContext ? contextQuery(chatContext) : cwdQuery(cwd);
|
||||||
|
|
||||||
const { data, isLoading, error, refetch } = useQuery<{ sessions: ClaudeSessionSummary[] }>({
|
const { data, isLoading, error, refetch } = useQuery<{ sessions: ClaudeSessionSummary[] }>({
|
||||||
queryKey: [SESSIONS_KEY, all ? '__all__' : (cwd ?? 'default')],
|
// The context belongs in the key, or two panels on different plugins share one cached list.
|
||||||
|
queryKey: [
|
||||||
|
SESSIONS_KEY,
|
||||||
|
all ? '__all__' : chatContext ? `ctx:${chatContext.context}:${chatContext.contextId ?? ''}` : (cwd ?? 'default'),
|
||||||
|
],
|
||||||
enabled: isAuthenticated && options?.enabled !== false,
|
enabled: isAuthenticated && options?.enabled !== false,
|
||||||
queryFn: () => client.get<{ sessions: ClaudeSessionSummary[] }>(`/chat/sessions${q}`),
|
queryFn: () => client.get<{ sessions: ClaudeSessionSummary[] }>(`/chat/sessions${q}`),
|
||||||
staleTime: 30 * 1000,
|
staleTime: 30 * 1000,
|
||||||
|
|||||||
Reference in New Issue
Block a user