Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions app/src/client/pages/ChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,25 @@ export function ChatPage({
});

/**
* Push settings to the agent. With `connect`, it also opens the MCP
* connections a turn needs — connections are held only while work runs
* (an idle Durable Object with live MCP clients never hibernates).
* Push settings to the agent (fast — MCP connections open server-side in
* beforeTurn, not here).
*/
async function ensureSetup(connect = false) {
if (!connect && appliedRef.current === settingsHash) return;
async function ensureSetup() {
if (appliedRef.current === settingsHash) return;
appliedRef.current = settingsHash;
try {
await agent.ready;
await agent.call("setup", [settings, { connect }]);
await agent.call("setup", [settings]);
} catch (err) {
console.warn("PI setup failed", err);
appliedRef.current = null;
}
}

// Sends run strictly in click order: a slow settings push for one message
// must never let a later message overtake it on the wire.
const sendChain = useRef(Promise.resolve());

// Apply changed settings to a conversation that's already underway.
useEffect(() => {
if (startedRef.current || messages.length > 0) void ensureSetup();
Expand All @@ -109,17 +112,23 @@ export function ChatPage({
if (el) el.scrollTop = el.scrollHeight;
}, [messages, status]);

async function send(text: string) {
function send(text: string) {
startedRef.current = true;
if (isDraft) navigate(`/chat/${chatId}`, true);
upsertChat(identity.netid, {
id: chatId,
title: firstTitle(messages) ?? text.slice(0, 48),
at: Date.now(),
});
// Open this turn's MCP connections (released again when the turn ends).
await ensureSetup(true);
void sendMessage({ text });
sendChain.current = sendChain.current.then(async () => {
// The first message of a chat needs settings on the server; bound the
// wait so a slow push can never swallow a message silently.
await Promise.race([
ensureSetup(),
new Promise((resolve) => setTimeout(resolve, 4000)),
]);
void sendMessage({ text });
});
}

/** Copy history up to `endIndex` (exclusive) into a fresh chat. */
Expand Down Expand Up @@ -237,7 +246,7 @@ export function ChatPage({
}
onRegenerate={
m.role === "assistant" && m.id === lastAssistantId
? () => void ensureSetup(true).then(() => regenerate())
? () => void regenerate()
: undefined
}
/>
Expand Down
33 changes: 30 additions & 3 deletions app/src/server/pi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,10 @@ export class Pi extends Think<Env, PiState> {
const connectedIds = new Set(Object.keys(this.getMcpServers().servers));
for (const app of PI_APPS) {
if (!enabled.has(app.key) || connectedIds.has(app.key)) continue;
// Engine connections are only opened right before a turn (see
// releaseIdleMcp for why); Google is handled here for the consent flow.
if (app.key !== "gcal" && !opts.connect) continue;
// Engine connections open right before a turn (see releaseIdleMcp for
// why); in a plain settings push only the desk's Google consent flow
// needs any connection work.
if (!opts.connect && !(app.key === "gcal" && this.isDesk())) continue;
try {
if (app.key === "gcal") {
if (
Expand Down Expand Up @@ -253,6 +254,32 @@ export class Pi extends Think<Env, PiState> {
}
}

/**
* Open this turn's MCP connections on the server, so the client can fire
* a message instantly instead of awaiting a connect round-trip first
* (which both delayed the echo of sent messages and let quick successive
* sends overtake each other). Think assembles its automatic MCP toolset
* before this hook runs — while nothing is connected — so the freshly
* connected tools are returned here to be merged into the turn.
*/
override async beforeTurn(
ctx: Parameters<Think["beforeTurn"]>[0]
): Promise<ReturnType<Think["beforeTurn"]> extends infer R ? Awaited<R> : never> {
const inherited = await super.beforeTurn(ctx);
const settings = this.getConfig<PiSettings>();
if (!settings) return inherited ?? undefined;
try {
await this.setup(settings, { connect: true });
} catch (err) {
console.warn("beforeTurn connect failed", err);
}
const tools = this.mcp.getAITools();
return {
...(inherited ?? {}),
tools: { ...(inherited?.tools ?? {}), ...tools },
};
}

override async onStart(props?: Record<string, unknown>) {
await super.onStart(props);
await this.releaseIdleMcp();
Expand Down
Loading