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:
@@ -66,6 +66,7 @@ export async function listPiModels(): Promise<ModelInfo[]> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const models: ModelInfo[] = [];
|
const models: ModelInfo[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
for (let i = 1; i < lines.length; i++) {
|
for (let i = 1; i < lines.length; i++) {
|
||||||
const line = lines[i]!;
|
const line = lines[i]!;
|
||||||
@@ -78,11 +79,16 @@ export async function listPiModels(): Promise<ModelInfo[]> {
|
|||||||
const thinking = extractCol(line, 4);
|
const thinking = extractCol(line, 4);
|
||||||
const images = extractCol(line, 5);
|
const images = extractCol(line, 5);
|
||||||
|
|
||||||
const normalizedProvider = provider === 'zai' ? 'opencode' : provider;
|
// zai and opencode are the same service — prefer opencode, skip zai duplicates
|
||||||
|
const displayProvider = provider === 'zai' ? 'opencode' : provider;
|
||||||
|
const dedupeKey = `${displayProvider}/${model}`;
|
||||||
|
if (seen.has(dedupeKey)) continue;
|
||||||
|
seen.add(dedupeKey);
|
||||||
|
|
||||||
models.push({
|
models.push({
|
||||||
id: `${provider}/${model}`,
|
id: `${provider}/${model}`,
|
||||||
name: model,
|
name: model,
|
||||||
provider: normalizedProvider,
|
provider: displayProvider,
|
||||||
contextWindow: parseSize(context),
|
contextWindow: parseSize(context),
|
||||||
maxTokens: parseSize(maxOut),
|
maxTokens: parseSize(maxOut),
|
||||||
reasoning: thinking === 'yes',
|
reasoning: thinking === 'yes',
|
||||||
|
|||||||
@@ -241,7 +241,6 @@ export async function spawnPi(
|
|||||||
await ensureGoogleTokenFile(sandbox.userId, sandbox.email);
|
await ensureGoogleTokenFile(sandbox.userId, sandbox.email);
|
||||||
|
|
||||||
const envFlags = [
|
const envFlags = [
|
||||||
'-e', `PI_CODING_AGENT_DIR=/officer/pi-config`,
|
|
||||||
'-e', `HOME=${containerHome}`,
|
'-e', `HOME=${containerHome}`,
|
||||||
'-e', `OFFICER_USER_HOME=${containerHome}`,
|
'-e', `OFFICER_USER_HOME=${containerHome}`,
|
||||||
'-e', `OFFICER_USER_ROOT=/officer/user`,
|
'-e', `OFFICER_USER_ROOT=/officer/user`,
|
||||||
|
|||||||
@@ -51,9 +51,10 @@ piRestRouter.post('/pi/sessions', async (ctx: Context) => {
|
|||||||
const body = await ctx.req.json().catch(() => ({}));
|
const body = await ctx.req.json().catch(() => ({}));
|
||||||
const userHome = getHomeDir(user.email);
|
const userHome = getHomeDir(user.email);
|
||||||
const filterCwd = body.cwd ? resolveBaseCwd(user.email, body.cwdRoot, body.cwd) : null;
|
const filterCwd = body.cwd ? resolveBaseCwd(user.email, body.cwdRoot, body.cwd) : null;
|
||||||
|
const contextFilter = body.context ? { context: body.context as string, contextId: body.contextId as string | undefined } : undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let sessions = await storage.listUserSessions(userHome);
|
let sessions = await storage.listUserSessions(userHome, contextFilter);
|
||||||
if (filterCwd) {
|
if (filterCwd) {
|
||||||
sessions = sessions.filter((s) => s.cwd === filterCwd);
|
sessions = sessions.filter((s) => s.cwd === filterCwd);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ class SessionManager {
|
|||||||
email: string,
|
email: string,
|
||||||
cwd: string,
|
cwd: string,
|
||||||
model: string,
|
model: string,
|
||||||
groupSlug?: string | null
|
groupSlug?: string | null,
|
||||||
|
context?: string,
|
||||||
|
contextId?: string,
|
||||||
): UserSession {
|
): UserSession {
|
||||||
let session = this.sessions.get(sessionId);
|
let session = this.sessions.get(sessionId);
|
||||||
|
|
||||||
@@ -35,6 +37,8 @@ class SessionManager {
|
|||||||
model,
|
model,
|
||||||
cwd,
|
cwd,
|
||||||
groupSlug: groupSlug || null,
|
groupSlug: groupSlug || null,
|
||||||
|
context,
|
||||||
|
contextId,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
messageCount: 0,
|
messageCount: 0,
|
||||||
|
|||||||
@@ -251,6 +251,8 @@ export function messagesToJnlEntries(messages: Message[], meta: SessionMeta): Jn
|
|||||||
messageCount: meta.messageCount,
|
messageCount: meta.messageCount,
|
||||||
createdAt: meta.createdAt,
|
createdAt: meta.createdAt,
|
||||||
updatedAt: meta.updatedAt,
|
updatedAt: meta.updatedAt,
|
||||||
|
context: meta.context,
|
||||||
|
contextId: meta.contextId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
entries.push(infoEntry);
|
entries.push(infoEntry);
|
||||||
@@ -357,6 +359,8 @@ function indexEntryToMeta(sessionId: string, entry: SessionIndexEntry): SessionM
|
|||||||
messageCount: entry.messageCount,
|
messageCount: entry.messageCount,
|
||||||
cost: entry.cost,
|
cost: entry.cost,
|
||||||
groupSlug: entry.groupSlug ?? null,
|
groupSlug: entry.groupSlug ?? null,
|
||||||
|
context: entry.context,
|
||||||
|
contextId: entry.contextId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,6 +375,8 @@ function metaToIndexEntry(meta: SessionMeta, relFile: string): SessionIndexEntry
|
|||||||
messageCount: meta.messageCount,
|
messageCount: meta.messageCount,
|
||||||
cost: meta.cost,
|
cost: meta.cost,
|
||||||
groupSlug: meta.groupSlug ?? null,
|
groupSlug: meta.groupSlug ?? null,
|
||||||
|
context: meta.context,
|
||||||
|
contextId: meta.contextId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,14 +501,28 @@ export async function deleteSession(
|
|||||||
await saveIndex(baseCwd, index);
|
await saveIndex(baseCwd, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listUserSessions(baseCwd: string): Promise<SessionMeta[]> {
|
type SessionFilter = {
|
||||||
|
context?: string;
|
||||||
|
contextId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function listUserSessions(baseCwd: string, filter?: SessionFilter): Promise<SessionMeta[]> {
|
||||||
let index = await loadIndex(baseCwd);
|
let index = await loadIndex(baseCwd);
|
||||||
|
|
||||||
if (Object.keys(index).length === 0) {
|
if (Object.keys(index).length === 0) {
|
||||||
index = await rebuildIndex(baseCwd);
|
index = await rebuildIndex(baseCwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessions: SessionMeta[] = Object.entries(index).map(([id, entry]) => indexEntryToMeta(id, entry));
|
let sessions: SessionMeta[] = Object.entries(index).map(([id, entry]) => indexEntryToMeta(id, entry));
|
||||||
|
|
||||||
|
if (filter?.context) {
|
||||||
|
if (filter.context === 'chat') {
|
||||||
|
// 'chat' matches sessions with no context or context='chat'
|
||||||
|
sessions = sessions.filter((s) => !s.context || s.context === 'chat');
|
||||||
|
} else {
|
||||||
|
sessions = sessions.filter((s) => s.context === filter.context && s.contextId === filter.contextId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sessions.sort((a, b) => b.updatedAt - a.updatedAt);
|
sessions.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||||
return sessions;
|
return sessions;
|
||||||
@@ -748,6 +768,8 @@ export async function rebuildIndex(baseCwd: string): Promise<SessionIndex> {
|
|||||||
let createdAt = new Date(header.timestamp).getTime();
|
let createdAt = new Date(header.timestamp).getTime();
|
||||||
let updatedAt = createdAt;
|
let updatedAt = createdAt;
|
||||||
let groupSlug: string | null = null;
|
let groupSlug: string | null = null;
|
||||||
|
let context: string | undefined;
|
||||||
|
let contextId: string | undefined;
|
||||||
|
|
||||||
// Count message entries and find session_info
|
// Count message entries and find session_info
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@@ -763,6 +785,8 @@ export async function rebuildIndex(baseCwd: string): Promise<SessionIndex> {
|
|||||||
createdAt = info.officer.createdAt;
|
createdAt = info.officer.createdAt;
|
||||||
updatedAt = info.officer.updatedAt;
|
updatedAt = info.officer.updatedAt;
|
||||||
groupSlug = info.officer.groupSlug ?? null;
|
groupSlug = info.officer.groupSlug ?? null;
|
||||||
|
context = info.officer.context;
|
||||||
|
contextId = info.officer.contextId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -777,6 +801,8 @@ export async function rebuildIndex(baseCwd: string): Promise<SessionIndex> {
|
|||||||
messageCount,
|
messageCount,
|
||||||
cost,
|
cost,
|
||||||
groupSlug,
|
groupSlug,
|
||||||
|
context,
|
||||||
|
contextId,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export type SessionMeta = {
|
|||||||
messageCount: number;
|
messageCount: number;
|
||||||
cost: MessageCost;
|
cost: MessageCost;
|
||||||
groupSlug?: string | null;
|
groupSlug?: string | null;
|
||||||
|
context?: string;
|
||||||
|
contextId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GroupMeta = {
|
export type GroupMeta = {
|
||||||
@@ -53,6 +55,8 @@ export type ClientMessage =
|
|||||||
groupSlug?: string;
|
groupSlug?: string;
|
||||||
attachmentIds?: string[];
|
attachmentIds?: string[];
|
||||||
thinking?: ThinkingLevel;
|
thinking?: ThinkingLevel;
|
||||||
|
context?: string;
|
||||||
|
contextId?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "resume";
|
type: "resume";
|
||||||
@@ -70,6 +74,8 @@ export type ServerMessage =
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
model: string;
|
model: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
|
context?: string;
|
||||||
|
contextId?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "assistant:text";
|
type: "assistant:text";
|
||||||
@@ -230,6 +236,8 @@ export type JnlSessionInfoEntry = JnlEntryBase & {
|
|||||||
messageCount: number;
|
messageCount: number;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
|
context?: string;
|
||||||
|
contextId?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -247,6 +255,8 @@ export type SessionIndexEntry = {
|
|||||||
messageCount: number;
|
messageCount: number;
|
||||||
cost: MessageCost;
|
cost: MessageCost;
|
||||||
groupSlug?: string | null;
|
groupSlug?: string | null;
|
||||||
|
context?: string;
|
||||||
|
contextId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SessionIndex = Record<string, SessionIndexEntry>;
|
export type SessionIndex = Record<string, SessionIndexEntry>;
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ function createEventHandler(sessionId: string, model: string, cwd: string, stora
|
|||||||
|
|
||||||
async function handleChat(
|
async function handleChat(
|
||||||
ws: ServerWebSocket<WSData>,
|
ws: ServerWebSocket<WSData>,
|
||||||
msg: { prompt: string; sessionId?: string; model?: string; cwd?: string; cwdRoot?: string; sandboxed?: boolean; groupSlug?: string; attachmentIds?: string[]; thinking?: string }
|
msg: { prompt: string; sessionId?: string; model?: string; cwd?: string; cwdRoot?: string; sandboxed?: boolean; groupSlug?: string; attachmentIds?: string[]; thinking?: string; context?: string; contextId?: string }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { email, username, userId } = ws.data;
|
const { email, username, userId } = ws.data;
|
||||||
const sessionId = msg.sessionId || randomUUID();
|
const sessionId = msg.sessionId || randomUUID();
|
||||||
@@ -276,13 +276,13 @@ async function handleChat(
|
|||||||
? resolveSandboxedCwd(email, msg.cwdRoot, msg.cwd)
|
? resolveSandboxedCwd(email, msg.cwdRoot, msg.cwd)
|
||||||
: resolveHostCwd(msg.cwdRoot, msg.cwd);
|
: resolveHostCwd(msg.cwdRoot, msg.cwd);
|
||||||
const groupSlug = msg.groupSlug || null;
|
const groupSlug = msg.groupSlug || null;
|
||||||
const session = sessionManager.getOrCreate(sessionId, email, cwd, model, groupSlug);
|
const session = sessionManager.getOrCreate(sessionId, email, cwd, model, groupSlug, msg.context, msg.contextId);
|
||||||
session.sandboxed = sandboxed;
|
session.sandboxed = sandboxed;
|
||||||
session.userId = userId;
|
session.userId = userId;
|
||||||
sessionManager.attachWs(sessionId, ws);
|
sessionManager.attachWs(sessionId, ws);
|
||||||
wsToSessionMap.set(ws as any, sessionId);
|
wsToSessionMap.set(ws as any, sessionId);
|
||||||
|
|
||||||
sendToClient(ws, { type: 'session:init', sessionId, model, cwd });
|
sendToClient(ws, { type: 'session:init', sessionId, model, cwd, context: session.meta.context, contextId: session.meta.contextId });
|
||||||
if (!session.piProcess) {
|
if (!session.piProcess) {
|
||||||
try {
|
try {
|
||||||
const onEvent = createEventHandler(sessionId, model, cwd, homeDir);
|
const onEvent = createEventHandler(sessionId, model, cwd, homeDir);
|
||||||
@@ -353,7 +353,7 @@ async function handleResume(
|
|||||||
sessionManager.attachWs(sessionId, ws);
|
sessionManager.attachWs(sessionId, ws);
|
||||||
wsToSessionMap.set(ws as any, sessionId);
|
wsToSessionMap.set(ws as any, sessionId);
|
||||||
|
|
||||||
sendToClient(ws, { type: 'session:init', sessionId, model: session.model, cwd: session.cwd });
|
sendToClient(ws, { type: 'session:init', sessionId, model: session.model, cwd: session.cwd, context: session.meta.context, contextId: session.meta.contextId });
|
||||||
|
|
||||||
// Spawn fresh Pi process if needed
|
// Spawn fresh Pi process if needed
|
||||||
if (!session.piProcess) {
|
if (!session.piProcess) {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { fileURLToPath } from 'node:url';
|
|||||||
import { getHomeDir, getGlobalSkillsDir, getGlobalToolsDir, getGlobalExtensionsDir, getUserSkillsDir, getUserToolsDir, DATA_PATH, SERVER_CONFIG_DIR, PI_CONFIG_DIR } from '@@/data-path';
|
import { getHomeDir, getGlobalSkillsDir, getGlobalToolsDir, getGlobalExtensionsDir, getUserSkillsDir, getUserToolsDir, DATA_PATH, SERVER_CONFIG_DIR, PI_CONFIG_DIR } from '@@/data-path';
|
||||||
import { getUsers, getServerIntegration, getUserIntegration } from 'officerdb';
|
import { getUsers, getServerIntegration, getUserIntegration } from 'officerdb';
|
||||||
|
|
||||||
|
const ensureDir = (dir: string) => { if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); return dir; };
|
||||||
|
|
||||||
type WSData = { userId: number; email: string; username: string; role: string; sandboxed: boolean; sessionId?: string; cwd?: string; cols?: number; rows?: number };
|
type WSData = { userId: number; email: string; username: string; role: string; sandboxed: boolean; sessionId?: string; cwd?: string; cols?: number; rows?: number };
|
||||||
type ShellInfo = { command: string; args: string[]; name: string };
|
type ShellInfo = { command: string; args: string[]; name: string };
|
||||||
type BridgeSession = {
|
type BridgeSession = {
|
||||||
@@ -191,6 +193,8 @@ const startDockerSidecar = async (port: number, homeDir: string, userId: number,
|
|||||||
'-v', `${getUserSkillsDir(email)}:/officer/user/skills:ro`,
|
'-v', `${getUserSkillsDir(email)}:/officer/user/skills:ro`,
|
||||||
'-v', `${getUserToolsDir(email)}:/officer/user/tools:ro`,
|
'-v', `${getUserToolsDir(email)}:/officer/user/tools:ro`,
|
||||||
'-v', `${PI_CONFIG_DIR}:/officer/pi-config:ro`,
|
'-v', `${PI_CONFIG_DIR}:/officer/pi-config:ro`,
|
||||||
|
'-v', `${PI_CONFIG_DIR}:${containerHome}/.pi/agent`,
|
||||||
|
'-v', `${ensureDir(join(DATA_PATH, email, 'pi-sessions'))}:${containerHome}/.pi/agent/sessions`,
|
||||||
'-v', `${join(DATA_PATH, '.generated')}:/officer/generated:ro`,
|
'-v', `${join(DATA_PATH, '.generated')}:/officer/generated:ro`,
|
||||||
...googleMounts,
|
...googleMounts,
|
||||||
'-v', `${join(DATA_PATH, email, 'integrations')}:/officer/user/integrations:ro`,
|
'-v', `${join(DATA_PATH, email, 'integrations')}:/officer/user/integrations:ro`,
|
||||||
|
|||||||
@@ -10,11 +10,17 @@ type ChatSessionSelection = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ChatPanelWrapper = () => {
|
export const ChatPanelWrapper = () => {
|
||||||
const { cwd, root } = useWorkspace();
|
const { workspaceId, cwd, root } = useWorkspace();
|
||||||
const scoped = cwd !== '~';
|
const scoped = cwd !== '~';
|
||||||
const hostRoot = root === '~' || root === 'officer.dev';
|
const hostRoot = root === '~' || root === 'officer.dev';
|
||||||
const sandboxed = !hostRoot;
|
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 [selection] = usePanelChannel<ChatSessionSelection | null>('chat:panel-session', null);
|
||||||
const [, setActiveSession] = usePanelChannel<string | null>('chat:active-session', null);
|
const [, setActiveSession] = usePanelChannel<string | null>('chat:active-session', null);
|
||||||
|
|
||||||
@@ -23,7 +29,7 @@ export const ChatPanelWrapper = () => {
|
|||||||
const sessionId = selection?.sessionId ?? undefined;
|
const sessionId = selection?.sessionId ?? undefined;
|
||||||
const model = selection?.model ?? 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(() => {
|
useEffect(() => {
|
||||||
setActiveSession(chat.sessionId);
|
setActiveSession(chat.sessionId);
|
||||||
@@ -39,6 +45,7 @@ export const ChatPanelWrapper = () => {
|
|||||||
cwd={cwdParam}
|
cwd={cwdParam}
|
||||||
sandboxed={sandboxed}
|
sandboxed={sandboxed}
|
||||||
replaceUrl={false}
|
replaceUrl={false}
|
||||||
|
{...chatContext}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ type EmbeddableChatProps = {
|
|||||||
replaceUrl?: boolean;
|
replaceUrl?: boolean;
|
||||||
autoSend?: boolean;
|
autoSend?: boolean;
|
||||||
chat?: UsePiChatType;
|
chat?: UsePiChatType;
|
||||||
|
context?: string;
|
||||||
|
contextId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmbeddableChat = ({ className, ...params }: EmbeddableChatProps) => {
|
export const EmbeddableChat = ({ className, ...params }: EmbeddableChatProps) => {
|
||||||
|
|||||||
@@ -21,12 +21,14 @@ type UseEmbeddableChatParams = {
|
|||||||
replaceUrl?: boolean;
|
replaceUrl?: boolean;
|
||||||
autoSend?: boolean;
|
autoSend?: boolean;
|
||||||
chat?: UsePiChatType;
|
chat?: UsePiChatType;
|
||||||
|
context?: string;
|
||||||
|
contextId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useEmbeddableChat(params: UseEmbeddableChatParams) {
|
export function useEmbeddableChat(params: UseEmbeddableChatParams) {
|
||||||
const { initialMessage, defaultInput = '', promptPrefix, cwd, sandboxed, autoSend = false, chat: externalChat } = params;
|
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 chat = externalChat ?? internalChat;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export type SessionEntry = {
|
|||||||
messageCount: number;
|
messageCount: number;
|
||||||
cost: MessageCost;
|
cost: MessageCost;
|
||||||
groupSlug?: string | null;
|
groupSlug?: string | null;
|
||||||
|
context?: string;
|
||||||
|
contextId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GroupEntry = {
|
export type GroupEntry = {
|
||||||
@@ -62,7 +64,7 @@ export type TaskInfo = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ServerMessage =
|
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:text'; text: string }
|
||||||
| { type: 'assistant:delta'; text: string }
|
| { type: 'assistant:delta'; text: string }
|
||||||
| { type: 'tool:start'; toolCallId: string; toolName: string; toolInput: Record<string, unknown> }
|
| { 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 { useChatSessions } from 'state/useChatSessions';
|
||||||
import { useChatGroups } from 'state/useChatGroups';
|
import { useChatGroups } from 'state/useChatGroups';
|
||||||
import { getProviderDisplayName } from 'state/useModels';
|
import { getProviderDisplayName } from 'state/useModels';
|
||||||
|
import { useWorkspace } from '../../components/Workspace';
|
||||||
import type { SelectedSession } from './ChatDetailPanel';
|
import type { SelectedSession } from './ChatDetailPanel';
|
||||||
import { CreateGroupDialog } from './CreateGroupDialog';
|
import { CreateGroupDialog } from './CreateGroupDialog';
|
||||||
import { GroupContextMenu } from './GroupContextMenu';
|
import { GroupContextMenu } from './GroupContextMenu';
|
||||||
@@ -12,7 +13,14 @@ import { SessionContextMenu } from './SessionContextMenu';
|
|||||||
|
|
||||||
export const SessionList = () => {
|
export const SessionList = () => {
|
||||||
const navigate = useNavigate();
|
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 { groups } = useChatGroups();
|
||||||
const [selected, setSelected] = usePanelChannel<SelectedSession>('chat:selected-session', null);
|
const [selected, setSelected] = usePanelChannel<SelectedSession>('chat:selected-session', null);
|
||||||
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
|
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ type UsePiChatOptions = {
|
|||||||
resourceChatDir?: string;
|
resourceChatDir?: string;
|
||||||
taskInfo?: TaskInfo;
|
taskInfo?: TaskInfo;
|
||||||
projectScoped?: boolean;
|
projectScoped?: boolean;
|
||||||
|
context?: string;
|
||||||
|
contextId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function usePiChat(initialSessionId?: string, initialModel?: string | null, options?: UsePiChatOptions) {
|
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 [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
const [streamingText, setStreamingText] = useState('');
|
const [streamingText, setStreamingText] = useState('');
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
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 sessionIdRef = useRef<string | null>(initialSessionId ?? null);
|
||||||
const saveTimerRef = useRef<number | null>(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 token = localStorage.getItem('BEARER_TOKEN');
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
@@ -301,6 +304,8 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
|
|||||||
...(resourceChatDir ? { resourceChatDir } : {}),
|
...(resourceChatDir ? { resourceChatDir } : {}),
|
||||||
...(taskInfo ? { taskInfo } : {}),
|
...(taskInfo ? { taskInfo } : {}),
|
||||||
...(thinking ? { thinking } : {}),
|
...(thinking ? { thinking } : {}),
|
||||||
|
...(context ? { context } : {}),
|
||||||
|
...(contextId ? { contextId } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,26 @@ import { useAuth } from 'hooks/useAuth';
|
|||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
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 client = useClient();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
|
|
||||||
const { data: sessions = [] } = useQuery<SessionEntry[]>({
|
const { data: sessions = [] } = useQuery<SessionEntry[]>({
|
||||||
queryKey: ['PI_SESSIONS'],
|
queryKey: ['PI_SESSIONS', filter?.context, filter?.contextId],
|
||||||
enabled: isAuthenticated,
|
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) {
|
function getSession(sessionId: string) {
|
||||||
@@ -24,13 +35,13 @@ export function useChatSessions() {
|
|||||||
|
|
||||||
async function renameSession(sessionId: string, title: string) {
|
async function renameSession(sessionId: string, title: string) {
|
||||||
await client.patch(`/pi/sessions/${sessionId}`, { title });
|
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) {
|
async function deleteSession(sessionId: string) {
|
||||||
await client.delete(`/pi/sessions/${sessionId}`);
|
await client.delete(`/pi/sessions/${sessionId}`);
|
||||||
queryClient.setQueryData<SessionEntry[]>(
|
queryClient.setQueryData<SessionEntry[]>(
|
||||||
['PI_SESSIONS'],
|
['PI_SESSIONS', filter?.context, filter?.contextId],
|
||||||
(prev) => prev?.filter((s) => s.id !== sessionId) ?? [],
|
(prev) => prev?.filter((s) => s.id !== sessionId) ?? [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -40,7 +51,7 @@ export function useChatSessions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function invalidate() {
|
function invalidate() {
|
||||||
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS'] });
|
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS', filter?.context, filter?.contextId] });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user