chat session context scoping, fix sandboxed pi config mounts, dedupe zai/opencode providers
- Add context/contextId fields to session types, storage, and index - Filter sessions by context (project, workspace, or default chat) - Pass context through websocket, REST API, and frontend hooks - Mount ~/.pi/agent into sandboxed containers for auth + writable sessions - Remove PI_CODING_AGENT_DIR override so container pi uses ~/.pi/agent - Deduplicate zai/opencode models (same service, show as opencode) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,11 +10,17 @@ type ChatSessionSelection = {
|
||||
};
|
||||
|
||||
export const ChatPanelWrapper = () => {
|
||||
const { cwd, root } = useWorkspace();
|
||||
const { workspaceId, cwd, root } = useWorkspace();
|
||||
const scoped = cwd !== '~';
|
||||
const hostRoot = root === '~' || root === 'officer.dev';
|
||||
const sandboxed = !hostRoot;
|
||||
|
||||
const chatContext = workspaceId?.startsWith('proj-layout-')
|
||||
? { context: 'project' as const, contextId: workspaceId.replace('proj-layout-', '') }
|
||||
: workspaceId && !workspaceId.startsWith('screens/')
|
||||
? { context: 'workspace' as const, contextId: workspaceId }
|
||||
: {};
|
||||
|
||||
const [selection] = usePanelChannel<ChatSessionSelection | null>('chat:panel-session', null);
|
||||
const [, setActiveSession] = usePanelChannel<string | null>('chat:active-session', null);
|
||||
|
||||
@@ -23,7 +29,7 @@ export const ChatPanelWrapper = () => {
|
||||
const sessionId = selection?.sessionId ?? undefined;
|
||||
const model = selection?.model ?? undefined;
|
||||
|
||||
const chat = usePiChat(sessionId, model, { replaceUrl: false, projectScoped: scoped });
|
||||
const chat = usePiChat(sessionId, model, { replaceUrl: false, projectScoped: scoped, ...chatContext });
|
||||
|
||||
useEffect(() => {
|
||||
setActiveSession(chat.sessionId);
|
||||
@@ -39,6 +45,7 @@ export const ChatPanelWrapper = () => {
|
||||
cwd={cwdParam}
|
||||
sandboxed={sandboxed}
|
||||
replaceUrl={false}
|
||||
{...chatContext}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,8 @@ type EmbeddableChatProps = {
|
||||
replaceUrl?: boolean;
|
||||
autoSend?: boolean;
|
||||
chat?: UsePiChatType;
|
||||
context?: string;
|
||||
contextId?: string;
|
||||
};
|
||||
|
||||
export const EmbeddableChat = ({ className, ...params }: EmbeddableChatProps) => {
|
||||
|
||||
@@ -21,12 +21,14 @@ type UseEmbeddableChatParams = {
|
||||
replaceUrl?: boolean;
|
||||
autoSend?: boolean;
|
||||
chat?: UsePiChatType;
|
||||
context?: string;
|
||||
contextId?: string;
|
||||
};
|
||||
|
||||
export function useEmbeddableChat(params: UseEmbeddableChatParams) {
|
||||
const { initialMessage, defaultInput = '', promptPrefix, cwd, sandboxed, autoSend = false, chat: externalChat } = params;
|
||||
|
||||
const internalChat = usePiChat(params.sessionId, params.initialModel, { replaceUrl: params.replaceUrl ?? false });
|
||||
const internalChat = usePiChat(params.sessionId, params.initialModel, { replaceUrl: params.replaceUrl ?? false, context: params.context, contextId: params.contextId });
|
||||
const chat = externalChat ?? internalChat;
|
||||
|
||||
const {
|
||||
|
||||
@@ -28,6 +28,8 @@ export type SessionEntry = {
|
||||
messageCount: number;
|
||||
cost: MessageCost;
|
||||
groupSlug?: string | null;
|
||||
context?: string;
|
||||
contextId?: string;
|
||||
};
|
||||
|
||||
export type GroupEntry = {
|
||||
@@ -62,7 +64,7 @@ export type TaskInfo = {
|
||||
};
|
||||
|
||||
export type ServerMessage =
|
||||
| { type: 'session:init'; sessionId: string; model: string; cwd: string }
|
||||
| { type: 'session:init'; sessionId: string; model: string; cwd: string; context?: string; contextId?: string }
|
||||
| { type: 'assistant:text'; text: string }
|
||||
| { type: 'assistant:delta'; text: string }
|
||||
| { type: 'tool:start'; toolCallId: string; toolName: string; toolInput: Record<string, unknown> }
|
||||
|
||||
@@ -5,6 +5,7 @@ import { usePanelChannel } from 'hooks/usePanelChannel';
|
||||
import { useChatSessions } from 'state/useChatSessions';
|
||||
import { useChatGroups } from 'state/useChatGroups';
|
||||
import { getProviderDisplayName } from 'state/useModels';
|
||||
import { useWorkspace } from '../../components/Workspace';
|
||||
import type { SelectedSession } from './ChatDetailPanel';
|
||||
import { CreateGroupDialog } from './CreateGroupDialog';
|
||||
import { GroupContextMenu } from './GroupContextMenu';
|
||||
@@ -12,7 +13,14 @@ import { SessionContextMenu } from './SessionContextMenu';
|
||||
|
||||
export const SessionList = () => {
|
||||
const navigate = useNavigate();
|
||||
const { sessions, deleteSession } = useChatSessions();
|
||||
const { workspaceId } = useWorkspace();
|
||||
// Only scope sessions for actual project/workspace contexts, not screen layout IDs like 'screens/chat'
|
||||
const contextFilter = workspaceId?.startsWith('proj-layout-')
|
||||
? { context: 'project' as const, contextId: workspaceId.replace('proj-layout-', '') }
|
||||
: workspaceId && !workspaceId.startsWith('screens/')
|
||||
? { context: 'workspace' as const, contextId: workspaceId }
|
||||
: undefined;
|
||||
const { sessions, deleteSession } = useChatSessions(contextFilter);
|
||||
const { groups } = useChatGroups();
|
||||
const [selected, setSelected] = usePanelChannel<SelectedSession>('chat:selected-session', null);
|
||||
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
|
||||
|
||||
@@ -17,10 +17,12 @@ type UsePiChatOptions = {
|
||||
resourceChatDir?: string;
|
||||
taskInfo?: TaskInfo;
|
||||
projectScoped?: boolean;
|
||||
context?: string;
|
||||
contextId?: string;
|
||||
};
|
||||
|
||||
export function usePiChat(initialSessionId?: string, initialModel?: string | null, options?: UsePiChatOptions) {
|
||||
const { replaceUrl = true, storage, resourceChatDir, taskInfo, projectScoped } = options ?? {};
|
||||
const { replaceUrl = true, storage, resourceChatDir, taskInfo, projectScoped, context, contextId } = options ?? {};
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [streamingText, setStreamingText] = useState('');
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
@@ -54,7 +56,8 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
||||
const sessionIdRef = useRef<string | null>(initialSessionId ?? null);
|
||||
const saveTimerRef = useRef<number | null>(null);
|
||||
|
||||
const { getSession, saveMessages, invalidate: invalidateSessions } = useChatSessions();
|
||||
const sessionFilter = context ? { context, contextId } : undefined;
|
||||
const { getSession, saveMessages, invalidate: invalidateSessions } = useChatSessions(sessionFilter);
|
||||
|
||||
const token = localStorage.getItem('BEARER_TOKEN');
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
@@ -301,6 +304,8 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
||||
...(resourceChatDir ? { resourceChatDir } : {}),
|
||||
...(taskInfo ? { taskInfo } : {}),
|
||||
...(thinking ? { thinking } : {}),
|
||||
...(context ? { context } : {}),
|
||||
...(contextId ? { contextId } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,26 @@ import { useAuth } from 'hooks/useAuth';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
export function useChatSessions() {
|
||||
type ChatSessionsFilter = {
|
||||
context?: string;
|
||||
contextId?: string;
|
||||
};
|
||||
|
||||
export function useChatSessions(filter?: ChatSessionsFilter) {
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
const { data: sessions = [] } = useQuery<SessionEntry[]>({
|
||||
queryKey: ['PI_SESSIONS'],
|
||||
queryKey: ['PI_SESSIONS', filter?.context, filter?.contextId],
|
||||
enabled: isAuthenticated,
|
||||
queryFn: () => client.post<{ sessions: SessionEntry[] }>('/pi/sessions').then((r) => r.sessions),
|
||||
queryFn: () =>
|
||||
client
|
||||
.post<{ sessions: SessionEntry[] }>('/pi/sessions', {
|
||||
...(filter?.context ? { context: filter.context } : {}),
|
||||
...(filter?.contextId ? { contextId: filter.contextId } : {}),
|
||||
})
|
||||
.then((r) => r.sessions),
|
||||
});
|
||||
|
||||
function getSession(sessionId: string) {
|
||||
@@ -24,13 +35,13 @@ export function useChatSessions() {
|
||||
|
||||
async function renameSession(sessionId: string, title: string) {
|
||||
await client.patch(`/pi/sessions/${sessionId}`, { title });
|
||||
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS', filter?.context, filter?.contextId] });
|
||||
}
|
||||
|
||||
async function deleteSession(sessionId: string) {
|
||||
await client.delete(`/pi/sessions/${sessionId}`);
|
||||
queryClient.setQueryData<SessionEntry[]>(
|
||||
['PI_SESSIONS'],
|
||||
['PI_SESSIONS', filter?.context, filter?.contextId],
|
||||
(prev) => prev?.filter((s) => s.id !== sessionId) ?? [],
|
||||
);
|
||||
}
|
||||
@@ -40,7 +51,7 @@ export function useChatSessions() {
|
||||
}
|
||||
|
||||
function invalidate() {
|
||||
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS', filter?.context, filter?.contextId] });
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user