diff --git a/web/adapter/pi-adapter.ts b/web/adapter/pi-adapter.ts index fb4c0bc9..c8794bb1 100644 --- a/web/adapter/pi-adapter.ts +++ b/web/adapter/pi-adapter.ts @@ -490,6 +490,7 @@ export class PiWebAdapter { status: this.runtime.isIdle() ? ("idle" as const) : ("running" as const), + trust: this.runtime.projectTrustState ?? "unknown", capabilities: webCapabilitySnapshot(this.runtime.sessionManager), }, truncation: { @@ -510,6 +511,22 @@ export class PiWebAdapter { return snapshot; } + async searchSessions(query: string, includeArchived = false) { + 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(0, 100); + } + private fitSnapshot(snapshot: { currentSessionId?: string; workspaces: WebWorkspaceSummary[]; diff --git a/web/host/web-host.ts b/web/host/web-host.ts index 6203e647..7cba2ad0 100644 --- a/web/host/web-host.ts +++ b/web/host/web-host.ts @@ -586,6 +586,19 @@ export class WebHost { error: "session is not in the current workspace", }); } + if (url.pathname === "/api/search") { + const query = url.searchParams.get("q") ?? ""; + const includeArchived = url.searchParams.get("archived") === "true"; + if (query.length > 200) { + return this.json(response, 400, { error: "search query must be at most 200 characters" }); + } + const sessions = await this.adapter.searchSessions(query, includeArchived); + return this.json(response, 200, { + query, + sessions, + truncated: sessions.length >= 100, + }); + } this.json(response, 404, { error: "not found" }); } diff --git a/web/protocol/types.ts b/web/protocol/types.ts index a7429b6e..90181b4e 100644 --- a/web/protocol/types.ts +++ b/web/protocol/types.ts @@ -113,6 +113,7 @@ export interface WebSnapshot { models: WebModelSummary[]; runtime: { status: "idle" | "running" | "unknown"; + trust: "trusted" | "untrusted" | "unknown"; capabilities: WebCapabilitySnapshot; }; truncation: WebSnapshotTruncation; diff --git a/web/runtime/pi-runtime.ts b/web/runtime/pi-runtime.ts index cf792d39..1ea69196 100644 --- a/web/runtime/pi-runtime.ts +++ b/web/runtime/pi-runtime.ts @@ -86,6 +86,17 @@ export class PiWebRuntime implements WebRuntimeController { private disposePromise?: Promise; private hasSelectedWorkspace: boolean; + get projectTrustState() { + if (!this.hasSelectedWorkspace) return "unknown" as const; + try { + return this.runtime.session.settingsManager.isProjectTrusted() + ? ("trusted" as const) + : ("untrusted" as const); + } catch { + return "unknown" as const; + } + } + private constructor( runtime: AgentSessionRuntime, webSessionDirectory: string, diff --git a/web/runtime/types.ts b/web/runtime/types.ts index 2b66c3f0..11f2f0d2 100644 --- a/web/runtime/types.ts +++ b/web/runtime/types.ts @@ -53,6 +53,7 @@ export interface WebRuntimeController { readonly workspaceSelected: boolean; readonly sessionDirectory: string; readonly sessionManager: SessionManager; + readonly projectTrustState?: "trusted" | "untrusted" | "unknown"; isIdle(): boolean; sendPrompt(content: string, options?: WebPromptOptions): Promise; newSession(