diff --git a/web/adapter/pi-adapter.ts b/web/adapter/pi-adapter.ts index fb4c0bc9..c6b9b614 100644 --- a/web/adapter/pi-adapter.ts +++ b/web/adapter/pi-adapter.ts @@ -510,6 +510,18 @@ export class PiWebAdapter { return snapshot; } + async searchSessions(query: string, includeArchived = false, offset = 0, limit = 100) { + const normalized = query.trim().toLocaleLowerCase(); + if (!normalized || normalized.length > 200) return []; + const projection = await this.listSessionProjection(); + return projection.sessions + .filter((session) => includeArchived || session.archived !== true) + .filter((session) => [session.name, session.firstMessage, session.cwd] + .filter((value): value is string => typeof value === "string") + .join("\n").toLocaleLowerCase().includes(normalized)) + .slice(offset, offset + limit); + } + private fitSnapshot(snapshot: { currentSessionId?: string; workspaces: WebWorkspaceSummary[]; diff --git a/web/host/web-host.ts b/web/host/web-host.ts index 6203e647..52a5bdfb 100644 --- a/web/host/web-host.ts +++ b/web/host/web-host.ts @@ -557,6 +557,21 @@ export class WebHost { } if (url.pathname === "/api/models") return this.json(response, 200, { models: this.runtime.listModels() }); + if (url.pathname === "/api/search") { + const query = url.searchParams.get("q") ?? ""; + const includeArchived = url.searchParams.get("archived") === "true"; + const offset = Number(url.searchParams.get("offset") ?? "0"); + const limit = Number(url.searchParams.get("limit") ?? "100"); + if (query.length > 200 || !Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) + return this.json(response, 400, { error: "bounded search parameters required" }); + const sessions = await this.adapter.searchSessions(query, includeArchived, offset, limit); + return this.json(response, 200, { query, sessions, nextOffset: sessions.length === limit ? offset + limit : null }); + } + if (url.pathname === "/api/thinking") + return this.json(response, 200, { + sessionId: this.runtime.sessionManager.getSessionId(), + ...(this.runtime.getThinkingState?.() ?? { level: "unknown", available: [] }), + }); if (url.pathname === "/api/snapshot") { const cursor = this.sequence; const projection = await this.adapter.getSnapshot( diff --git a/web/runtime/pi-runtime.ts b/web/runtime/pi-runtime.ts index cf792d39..c29d0cdc 100644 --- a/web/runtime/pi-runtime.ts +++ b/web/runtime/pi-runtime.ts @@ -265,6 +265,11 @@ export class PiWebRuntime implements WebRuntimeController { return () => this.listeners.delete(listener); } + getThinkingState() { + const session = this.runtime.session; + return { level: session.thinkingLevel, available: session.getAvailableThinkingLevels() }; + } + async sendPrompt(content: string, options?: WebPromptOptions) { this.assertActive(); this.assertWorkspaceSelected(); diff --git a/web/runtime/types.ts b/web/runtime/types.ts index 2b66c3f0..f464e178 100644 --- a/web/runtime/types.ts +++ b/web/runtime/types.ts @@ -61,6 +61,7 @@ export interface WebRuntimeController { ): Promise; switchSession(sessionPath: string): Promise<{ cancelled: boolean }>; listModels(): WebModelSummary[]; + getThinkingState?(): { level: string; available: readonly string[] }; setModel( provider: string, modelId: string,