diff --git a/AGENTS.md b/AGENTS.md index a2baf0565a..75701a321b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,6 +85,7 @@ lib/ pi-types.ts local structural types for pi SDK objects rpc-manager.ts AgentSessionWrapper + registry + startRpcSession session-reader.ts SessionManager wrappers + path cache + buildSessionContext adapter + structured-ask.ts adapter layer turning question-asking tool calls into native forms subagent-settings.ts read/write ~/.pi/agent/agents/settings.json tool-presets.ts PRESET_NONE/READ_ONLY/DEFAULT/FULL + getPresetFromTools() tool-preset-preference.ts browser-persisted default for fresh sessions @@ -98,6 +99,8 @@ components/ ChatWindow.tsx chat composition + completion sound wrapper ChatInput.tsx input bar + model/thinking/tools/compact controls MessageView.tsx renders one message (user/assistant/toolCall/toolResult) + AskCard.tsx native form for a structured ask (see docs/adr/0004) + AskAnswerCard.tsx transcript card for a pending or answered question BranchNavigator.tsx in-session branch switcher ChatMinimap.tsx scroll minimap alongside the message list MarkdownBody.tsx markdown renderer @@ -132,6 +135,9 @@ hooks/ **Fix**: `send("fork")` captures `newSessionId`, then calls `this.destroy()` before returning. The next request for the original session reloads a clean AgentSession from the original file. +### Questions from tools are rendered natively (`lib/structured-ask.ts`) +Extensions ask questions through `ctx.ui.custom()`, which Pi Web otherwise streams as terminal text. A structured-ask adapter recognizes a known question tool (`ask_user`), attaches an `ask` field to the `extension_ui_request` event, and the browser renders `AskCard`. The answer comes back as `extension_ui_ask_response`, is checked against the question, then resolves the extension's promise. Unknown custom UIs keep the terminal panel. See `docs/adr/0004-structured-ask-adapter.md`. + ### Two kinds of branching — don't confuse them - **Fork** ("New session" on user message): creates a new independent `.jsonl` file. Shown as a child in the sidebar tree via `parentSession` header field. - **In-session branch** ("Edit from here" / BranchNavigator): calls `navigate_tree` within the same file. Multiple entries share the same `parentId`. Switching between them calls `/api/sessions/[id]/context?leafId=`. diff --git a/components/AppShell.tsx b/components/AppShell.tsx index d1c8bb99dd..53585ad97e 100644 --- a/components/AppShell.tsx +++ b/components/AppShell.tsx @@ -867,7 +867,8 @@ export function AppShell() { targetSession: selectedSession, title: translate("i18n.attentionNeeded"), body: request.method === "custom" - ? translate("i18n.extensionInputNeeded") + // A structured question carries its own text; other custom UIs do not. + ? request.ask?.question ?? translate("i18n.extensionInputNeeded") : request.title, tag: `pi-extension-ui:${request.id}`, }); diff --git a/components/AskAnswerCard.tsx b/components/AskAnswerCard.tsx new file mode 100644 index 0000000000..63941ad00a --- /dev/null +++ b/components/AskAnswerCard.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { MarkdownBody } from "./MarkdownBody"; +import { useI18n } from "@/hooks/useI18n"; +import type { StructuredAskRecord } from "@/lib/structured-ask"; + +/** + * Transcript view of a finished structured ask: the question, the options that + * were offered, and what the user answered. Keeps a decision visible in history + * instead of collapsing it into a generic tool call. + */ +export function AskAnswerCard({ record, pending }: { record: StructuredAskRecord; pending?: boolean }) { + const { t } = useI18n(); + const answer = record.answer; + const selections = answer?.kind === "selection" ? answer.selections : []; + const freeform = answer?.kind === "freeform" ? answer.text : null; + + const status = pending + ? t("chat.askPending") + : record.cancelled || !answer + ? t("chat.askCancelled") + : t("chat.askAnswered"); + + return ( +