@evandu/super-pi 0.1.39 → 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,8 +8,8 @@
8
8
  <link rel="alternate icon" href="/favicon.ico" />
9
9
  <link rel="apple-touch-icon" href="/favicon.svg" />
10
10
  <title>Super Pi</title>
11
- <script type="module" crossorigin src="/assets/index-DSJuo0Bi.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-DX1yjXR8.css">
11
+ <script type="module" crossorigin src="/assets/index-Dxm2Vtis.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-DuqJLo6R.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
@@ -1,7 +1,7 @@
1
1
  import type { Store } from "../store.js";
2
2
  import type { AgentRuntime } from "../runtime.js";
3
3
  import type { SubagentRunRecorder } from "../subagent-runs.js";
4
- import type { Conversation, Message, MessageImage, PiQueueSnapshot, Workspace } from "../types.js";
4
+ import type { Conversation, Message, MessageImage, PiQueueSnapshot, QuestionAnswer, Workspace } from "../types.js";
5
5
  import type { EventBroadcaster } from "../server-context.js";
6
6
  import type { ExtensionUiService } from "./extension-ui-service.js";
7
7
  import type { WorkflowEngine } from "../workflow-engine.js";
@@ -56,20 +56,36 @@ export declare class RunService {
56
56
  private readonly detached;
57
57
  constructor(store: Store, runtime: AgentRuntime, subagents: SubagentRunRecorder, broadcaster: EventBroadcaster, extensionUiService: ExtensionUiService, agentsDir: string | string[], workflows: WorkflowEngine);
58
58
  setAgentsDir(dir: string | string[]): void;
59
+ /**
60
+ * RECORDS WHAT THE PERSON CHOSE, ON THE `question` CALL THAT ASKED.
61
+ *
62
+ * The call is addressed by its index in the message's `toolCalls`, which is the same addressing every
63
+ * other tool update uses - and the client can only get that index from the row it is already rendering.
64
+ */
59
65
  answerQuestion(command: {
60
66
  conversationId: string;
61
67
  messageId: string;
62
- questionId: string;
63
- selected?: string[];
64
- extraNote?: string;
68
+ callIndex: number;
69
+ answers?: QuestionAnswer[];
65
70
  cancelled?: boolean;
66
71
  }): Promise<void>;
67
- askQuestion(conversationId: string, question: {
68
- question: string;
69
- options: string[];
70
- multiple?: boolean;
71
- extraNote?: boolean;
72
- }): Promise<unknown>;
72
+ /**
73
+ * THE `question` TOOL BLOCKS HERE, ON THE CALL THAT ASKED IT.
74
+ *
75
+ * It used to APPEND A BLOCK TO THE MESSAGE and poll the store for an answer, which meant the question
76
+ * had no position in the answer at all: the card rendered after everything the model said, so the text
77
+ * streamed in response to the answer appeared ABOVE the card it was answering. The call is already on
78
+ * screen, already positioned by its `at`, already persisted, already reloads - so the pending wait is
79
+ * registered against it and nothing else is created.
80
+ *
81
+ * A DISMISSAL FAILS THE CALL. It used to resolve with `{cancelled:true}` and let the run continue, so
82
+ * closing a card handed the model a turn it would answer as though it had been told something. OpenCode
83
+ * is unambiguous about this and is the reference here: rejecting a question fails the deferred, the tool
84
+ * dies and the run is interrupted (`packages/core/src/question.ts:128-141`). Nothing is invented to fill
85
+ * the gap - the same rule the Extension UI path already follows, where a cancelled method is never
86
+ * backfilled with `options[0]`.
87
+ */
88
+ askQuestion(conversationId: string): Promise<unknown>;
73
89
  /**
74
90
  * Resolves the conversation and workspace synchronously for a run.
75
91
  */
@@ -74,30 +74,71 @@ export class RunService {
74
74
  setAgentsDir(dir) {
75
75
  this.agentsDir = dir;
76
76
  }
77
+ /**
78
+ * RECORDS WHAT THE PERSON CHOSE, ON THE `question` CALL THAT ASKED.
79
+ *
80
+ * The call is addressed by its index in the message's `toolCalls`, which is the same addressing every
81
+ * other tool update uses - and the client can only get that index from the row it is already rendering.
82
+ */
77
83
  async answerQuestion(command) {
78
- const blocks = await this.store.answerQuestion(command.conversationId, command.messageId, command.questionId, command);
79
- this.broadcaster.broadcast({ type: "message.questions", conversationId: command.conversationId, messageId: command.messageId, questionBlocks: blocks });
84
+ const existing = this.store.snapshot().conversations.find((item) => item.id === command.conversationId)?.messages.find((item) => item.id === command.messageId)?.toolCalls?.[command.callIndex]?.question;
85
+ if (!existing)
86
+ throw new Error("这个工具调用不是一个问题");
87
+ const calls = await this.store.updateToolCall(command.conversationId, command.messageId, command.callIndex, {
88
+ question: {
89
+ items: existing.items,
90
+ status: command.cancelled ? "cancelled" : "answered",
91
+ ...(command.cancelled ? {} : { answers: command.answers ?? [] }),
92
+ },
93
+ });
94
+ this.broadcaster.broadcast({ type: "message.tools", conversationId: command.conversationId, messageId: command.messageId, toolCalls: calls });
80
95
  }
81
- async askQuestion(conversationId, question) {
96
+ /**
97
+ * THE `question` TOOL BLOCKS HERE, ON THE CALL THAT ASKED IT.
98
+ *
99
+ * It used to APPEND A BLOCK TO THE MESSAGE and poll the store for an answer, which meant the question
100
+ * had no position in the answer at all: the card rendered after everything the model said, so the text
101
+ * streamed in response to the answer appeared ABOVE the card it was answering. The call is already on
102
+ * screen, already positioned by its `at`, already persisted, already reloads - so the pending wait is
103
+ * registered against it and nothing else is created.
104
+ *
105
+ * A DISMISSAL FAILS THE CALL. It used to resolve with `{cancelled:true}` and let the run continue, so
106
+ * closing a card handed the model a turn it would answer as though it had been told something. OpenCode
107
+ * is unambiguous about this and is the reference here: rejecting a question fails the deferred, the tool
108
+ * dies and the run is interrupted (`packages/core/src/question.ts:128-141`). Nothing is invented to fill
109
+ * the gap - the same rule the Extension UI path already follows, where a cancelled method is never
110
+ * backfilled with `options[0]`.
111
+ */
112
+ async askQuestion(conversationId) {
82
113
  const conversation = this.store.snapshot().conversations.find((item) => item.id === conversationId);
83
114
  const message = conversation?.messages.slice().reverse().find((item) => item.role === "assistant");
84
115
  if (!message)
85
116
  throw new Error("No active assistant message");
86
- const block = await this.store.appendQuestion(conversationId, message.id, { question: question.question, options: question.options, multiple: question.multiple === true });
87
- this.broadcaster.broadcast({ type: "message.questions", conversationId, messageId: message.id, questionBlocks: [...(message.questionBlocks ?? []), block] });
117
+ // THE CALL, NOT THE MESSAGE. The newest `question` call still marked pending is the one the model is
118
+ // blocked inside; anything else would attach an answer to a question nobody is waiting on.
119
+ let index = -1;
120
+ for (let i = (message.toolCalls?.length ?? 0) - 1; i >= 0; i -= 1) {
121
+ const call = message.toolCalls?.[i];
122
+ if (call?.label === "question" && call.question?.status === "pending") {
123
+ index = i;
124
+ break;
125
+ }
126
+ }
127
+ if (index < 0)
128
+ throw new Error("这个会话里没有正在等待回答的 question 工具调用");
129
+ const messageId = message.id;
88
130
  return new Promise((resolve, reject) => {
89
- const timer = setInterval(() => {
90
- const current = this.store.snapshot().conversations.find((item) => item.id === conversationId)?.messages.find((item) => item.id === message.id)?.questionBlocks?.find((item) => item.id === block.id);
91
- if (current?.status === "answered") {
92
- clearInterval(timer);
93
- resolve({ selected: current.selected ?? [], extraNote: current.extraNote ?? "" });
94
- }
95
- else if (current?.status === "cancelled") {
96
- clearInterval(timer);
97
- resolve({ cancelled: true });
98
- }
131
+ const finish = (settle) => { clearInterval(poll); clearTimeout(deadline); settle(); };
132
+ const poll = setInterval(() => {
133
+ const current = this.store.snapshot().conversations.find((item) => item.id === conversationId)?.messages.find((item) => item.id === messageId)?.toolCalls?.[index]?.question;
134
+ if (current?.status === "answered")
135
+ finish(() => resolve({ answers: current.answers ?? [] }));
136
+ // CANCELLED IS NOT AN ANSWER. Rejecting is what makes the tool call fail, which is what stops the
137
+ // run; resolving here would let the model carry on from a question the person refused to answer.
138
+ else if (current?.status === "cancelled")
139
+ finish(() => reject(new Error("用户没有回答这个问题")));
99
140
  }, 250);
100
- setTimeout(() => { clearInterval(timer); reject(new Error("Question timed out")); }, 30 * 60 * 1000);
141
+ const deadline = setTimeout(() => finish(() => reject(new Error("等待回答超时"))), 30 * 60 * 1000);
101
142
  });
102
143
  }
103
144
  /**
@@ -317,13 +358,13 @@ export class RunService {
317
358
  // that never settles and one that belongs to a different call.
318
359
  if (call.id)
319
360
  rowByToolCallId.set(call.id, row);
320
- enqueueTools("a tool call", () => this.store.updateToolCall(conversation.id, assistant.id, row, { label: call.name, detail: call.detail, args: call.args, live: "", preparing: false }));
361
+ enqueueTools("a tool call", () => this.store.updateToolCall(conversation.id, assistant.id, row, { label: call.name, detail: call.detail, args: call.args, live: "", preparing: false, ...(call.question ? { question: call.question } : {}) }));
321
362
  return;
322
363
  }
323
364
  const row = rows++;
324
365
  if (call.id)
325
366
  rowByToolCallId.set(call.id, row);
326
- enqueueTools("a tool call", () => this.store.appendToolCall(conversation.id, assistant.id, { label: call.name, detail: call.detail, args: call.args, at }));
367
+ enqueueTools("a tool call", () => this.store.appendToolCall(conversation.id, assistant.id, { label: call.name, detail: call.detail, args: call.args, at, ...(call.question ? { question: call.question } : {}) }));
327
368
  };
328
369
  const onToolUpdate = (call) => {
329
370
  const index = call.id ? rowByToolCallId.get(call.id) : undefined;
@@ -396,7 +396,7 @@ export function createHttpServer(options) {
396
396
  const body = JSON.parse(raw);
397
397
  if (!body.conversationId || !body.question || !Array.isArray(body.options))
398
398
  throw new Error("invalid question");
399
- const result = await runService.askQuestion(body.conversationId, { question: body.question, options: body.options, multiple: body.multiple });
399
+ const result = await runService.askQuestion(body.conversationId);
400
400
  response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(result));
401
401
  }
402
402
  catch (error) {
@@ -9,7 +9,7 @@ import { resolvePiCommand, shellQuote } from "./pi-command.js";
9
9
  import { TASK_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_ROSTER_ENV, TASK_TOOL_NAME, TODO_ENDPOINT_ENV, TODO_TOOL_NAME, WORKFLOW_ENDPOINT_ENV, WORKFLOW_TOOL_NAME, rosterPayload, } from "./task-tool.js";
10
10
  import { BLOCKING_UI_METHODS, EXTENSION_UI_METHODS, } from "./types.js";
11
11
  import { loadConversationImages } from "./upload.js";
12
- import { describeToolCall, describeToolCallArgs, describeToolCallInProgress, extractToolResultText } from "./tool-calls.js";
12
+ import { describeToolCall, describeToolCallArgs, describeToolCallInProgress, extractToolResultText, readQuestionCall } from "./tool-calls.js";
13
13
  /** Deterministic offline runtime used by tests and local UI development. */
14
14
  export class WelcomeRuntime {
15
15
  async *answer({ prompt, signal }) {
@@ -956,8 +956,12 @@ export class PiRpcRuntime {
956
956
  // word, because an `edit`'s arguments routinely hold a whole file's replacement text. See
957
957
  // tool-calls.ts for the argument-vs-transcript line.
958
958
  const name = record.toolName ?? "unknown";
959
+ // A `question` CALL is read here, at `tool_execution_start`, rather than assembled somewhere
960
+ // else when the answer arrives - so the card on screen is the tool call the model is actually
961
+ // blocked in, with the same identity, the same `at`, and the same lifecycle. `args` stays the
962
+ // display reduction; this is the one call whose arguments are also the transcript's data.
959
963
  try {
960
- onToolCall?.({ id: record.toolCallId ?? "", name, detail: describeToolCall(name, record.args), args: describeToolCallArgs(record.args) });
964
+ onToolCall?.({ id: record.toolCallId ?? "", name, detail: describeToolCall(name, record.args), args: describeToolCallArgs(record.args), ...(() => { const question = readQuestionCall(name, record.args); return question ? { question } : {}; })() });
961
965
  }
962
966
  catch (error) {
963
967
  console.warn(`PiRpcRuntime: the tool call handler threw: ${error instanceof Error ? error.message : "unknown error"}`);
@@ -1,7 +1,7 @@
1
1
  import type { AppStateRepository } from "./persistence/app-state-repository.js";
2
2
  import { defaultJsonStatePath } from "./persistence/json-state-repository.js";
3
3
  import type { WorkflowRun } from "./workflow.js";
4
- import type { AppState, Conversation, Message, MessageImage, ModelSelection, QueuedMessage, SubagentRun, SubagentRunUpdate, TodoItem, ToolCallArg, ToolCallBlock, ToolResultSummary, Workspace } from "./types.js";
4
+ import type { AppState, Conversation, Message, MessageImage, ModelSelection, QueuedMessage, SubagentRun, SubagentRunUpdate, TodoItem, ToolCallQuestion, ToolCallArg, ToolCallBlock, ToolResultSummary, Workspace } from "./types.js";
5
5
  /** Mints the id a forked conversation will have, BEFORE its Pi transcript exists.
6
6
  *
7
7
  * Exported because the ordering demands it and no other arrangement provides it. Pi's copied file's header
@@ -232,12 +232,6 @@ export declare class Store {
232
232
  * instead of as a field it does not understand.
233
233
  */
234
234
  appendToolCall(conversationId: string, messageId: string, call: ToolCallBlock): Promise<ToolCallBlock[]>;
235
- appendQuestion(conversationId: string, messageId: string, question: Omit<import("./types.js").QuestionBlock, "id" | "status">): Promise<import("./types.js").QuestionBlock>;
236
- answerQuestion(conversationId: string, messageId: string, questionId: string, answer: {
237
- selected?: string[];
238
- extraNote?: string;
239
- cancelled?: boolean;
240
- }): Promise<import("./types.js").QuestionBlock[]>;
241
235
  /**
242
236
  * Fills in the outcome of the call at `index`, or returns the array UNCHANGED when there is no such
243
237
  * index.
@@ -275,6 +269,7 @@ export declare class Store {
275
269
  args?: ToolCallArg[];
276
270
  live?: string;
277
271
  preparing?: boolean;
272
+ question?: ToolCallQuestion;
278
273
  }): Promise<ToolCallBlock[]>;
279
274
  setStatus(conversationId: string, status: Conversation["status"]): Promise<void>;
280
275
  /** The plan a revert would carry out, without carrying it out. The server reads this to stop children first. */
@@ -899,37 +899,6 @@ export class Store {
899
899
  await this.persist();
900
900
  return structuredClone(calls);
901
901
  }
902
- async appendQuestion(conversationId, messageId, question) {
903
- const message = this.requireConversation(conversationId).messages.find((item) => item.id === messageId);
904
- if (!message)
905
- throw new Error("Assistant message not found");
906
- // `at` is WHERE IN THE ANSWER THE QUESTION WAS ASKED - the same character offset a tool call carries,
907
- // and for the same reason. Without it a question is a list item with no position, so it renders after
908
- // the whole message; and because the question BLOCKS, everything the model says after the answer
909
- // lands in the text above it. Measured on a real turn: the model said nothing, asked, waited, and on
910
- // being answered streamed 「已经调用 question 工具问完,停下来等你回答」 - which rendered ABOVE the card
911
- // and read as though it had been said before the question was ever asked.
912
- const block = { ...structuredClone(question), id: id("question"), status: "pending", at: message.content.length };
913
- (message.questionBlocks ??= []).push(block);
914
- await this.persistTransaction({ kind: "message.replace", conversationId, message: structuredClone(message), updatedAt: now() });
915
- return structuredClone(block);
916
- }
917
- async answerQuestion(conversationId, messageId, questionId, answer) {
918
- const conversation = this.requireConversation(conversationId);
919
- const message = conversation.messages.find((item) => item.id === messageId);
920
- if (!message?.questionBlocks)
921
- throw new Error("Question not found");
922
- const block = message.questionBlocks.find((item) => item.id === questionId);
923
- if (!block || block.status !== "pending")
924
- throw new Error("Question is no longer pending");
925
- block.status = answer.cancelled ? "cancelled" : "answered";
926
- if (!answer.cancelled) {
927
- block.selected = [...(answer.selected ?? [])];
928
- block.extraNote = answer.extraNote ?? "";
929
- }
930
- await this.persistTransaction({ kind: "message.replace", conversationId, message: structuredClone(message), updatedAt: now() });
931
- return structuredClone(message.questionBlocks);
932
- }
933
902
  /**
934
903
  * Fills in the outcome of the call at `index`, or returns the array UNCHANGED when there is no such
935
904
  * index.
@@ -1010,6 +979,9 @@ export class Store {
1010
979
  }
1011
980
  if (patch.live !== undefined)
1012
981
  call.live = patch.live;
982
+ // The question rides on the call rather than beside it, so this is the only write it needs.
983
+ if (patch.question !== undefined)
984
+ call.question = patch.question;
1013
985
  conversation.updatedAt = now();
1014
986
  await this.persist();
1015
987
  return structuredClone(calls);
@@ -1,4 +1,4 @@
1
- import type { ToolCallArg, ToolResultSummary } from "./types.js";
1
+ import type { ToolCallArg, ToolCallQuestion, ToolResultSummary } from "./types.js";
2
2
  /**
3
3
  * Turning one Pi tool call into something a human reads, and nothing more.
4
4
  *
@@ -104,3 +104,18 @@ export declare function describeToolCallInProgress(name: string, argumentsSoFar:
104
104
  * no parameter section rather than an empty one.
105
105
  */
106
106
  export declare function describeToolCallArgs(args: unknown): ToolCallArg[] | undefined;
107
+ /**
108
+ * READS A `question` CALL'S ARGUMENTS INTO THE SHAPE THE TRANSCRIPT RENDERS, OR NOTHING.
109
+ *
110
+ * Both shapes are accepted because the two references differ and both are in the wild: OpenCode sends
111
+ * `questions: [{ question, header, options: [{label, description}], multiple }]`
112
+ * (`packages/core/src/tool/question.ts:25`) and this project's own extension sends
113
+ * `{ question, options: string[], multiple?, extraNote? }`. A question with no options, or options that
114
+ * are not a list of strings, is not a question - so it returns nothing and the call renders as the
115
+ * ordinary tool call it is, rather than as a card with nothing in it.
116
+ *
117
+ * Ids are MINTED HERE when the caller did not send one, and that is what makes the answer pair by
118
+ * identity rather than by position: a person answering question 2 must not be recorded as answering
119
+ * question 1 because one of them was dropped.
120
+ */
121
+ export declare function readQuestionCall(name: string, args: unknown): ToolCallQuestion | undefined;
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  /**
2
3
  * Turning one Pi tool call into something a human reads, and nothing more.
3
4
  *
@@ -252,3 +253,51 @@ export function describeToolCallArgs(args) {
252
253
  }
253
254
  return listed.length ? listed : undefined;
254
255
  }
256
+ /**
257
+ * READS A `question` CALL'S ARGUMENTS INTO THE SHAPE THE TRANSCRIPT RENDERS, OR NOTHING.
258
+ *
259
+ * Both shapes are accepted because the two references differ and both are in the wild: OpenCode sends
260
+ * `questions: [{ question, header, options: [{label, description}], multiple }]`
261
+ * (`packages/core/src/tool/question.ts:25`) and this project's own extension sends
262
+ * `{ question, options: string[], multiple?, extraNote? }`. A question with no options, or options that
263
+ * are not a list of strings, is not a question - so it returns nothing and the call renders as the
264
+ * ordinary tool call it is, rather than as a card with nothing in it.
265
+ *
266
+ * Ids are MINTED HERE when the caller did not send one, and that is what makes the answer pair by
267
+ * identity rather than by position: a person answering question 2 must not be recorded as answering
268
+ * question 1 because one of them was dropped.
269
+ */
270
+ export function readQuestionCall(name, args) {
271
+ if (name !== "question")
272
+ return undefined;
273
+ const raw = args;
274
+ if (!raw || typeof raw !== "object")
275
+ return undefined;
276
+ const source = Array.isArray(raw.questions) ? raw.questions : [raw];
277
+ const items = [];
278
+ for (const entry of source) {
279
+ if (!entry || typeof entry !== "object")
280
+ return undefined;
281
+ const item = entry;
282
+ if (typeof item.question !== "string" || !item.question.trim())
283
+ return undefined;
284
+ if (!Array.isArray(item.options))
285
+ return undefined;
286
+ const options = item.options.map((option) => (typeof option === "string" ? option : option?.label)).filter((option) => typeof option === "string" && option.trim().length > 0);
287
+ // NO OPTIONS IS NOT A QUESTION. A `question` call with an empty list would render a card the person
288
+ // cannot answer, and the tool would then block for thirty minutes on it.
289
+ if (!options.length)
290
+ return undefined;
291
+ items.push({
292
+ id: typeof item.id === "string" && item.id ? item.id : randomUUID(),
293
+ ...(typeof item.header === "string" && item.header ? { header: item.header } : {}),
294
+ question: item.question,
295
+ options,
296
+ multiple: item.multiple === true,
297
+ // Defaults ON, matching the extension's own schema: a question that asked for an optional note and
298
+ // did not get one should show the field, not silently drop it.
299
+ extraNote: item.extraNote !== false,
300
+ });
301
+ }
302
+ return items.length ? { items, status: "pending" } : undefined;
303
+ }
@@ -55,15 +55,36 @@ export type MessageImage = {
55
55
  width?: number;
56
56
  height?: number;
57
57
  };
58
- export type QuestionBlock = {
58
+ /** ONE QUESTION AS THE TOOL ASKED IT, before any answer exists.
59
+ * Mirrors OpenCode's `QuestionV2.Prompt`: a stable id so the answer pairs by identity rather than by
60
+ * position, a short header for a dock, and options with their own text. */
61
+ export type QuestionItem = {
59
62
  id: string;
63
+ header?: string;
60
64
  question: string;
61
65
  options: string[];
62
66
  multiple: boolean;
67
+ extraNote: boolean;
68
+ };
69
+ /** WHAT THE PERSON CHOSE, paired to a `QuestionItem` by id. */
70
+ export type QuestionAnswer = {
71
+ id: string;
72
+ selected: string[];
73
+ note?: string;
74
+ };
75
+ /**
76
+ * A QUESTION IS A TOOL CALL, NOT A SECOND KIND OF BLOCK.
77
+ *
78
+ * It used to be a `questionBlocks` array hung off the message, which meant the question had no place
79
+ * in the answer: it rendered after everything the model said, so the text streamed in response to the
80
+ * answer appeared ABOVE the card it was answering. The tool-call row already carries an `at` offset, is
81
+ * already persisted, already reloads, and already expands - so the question lives there too, and the
82
+ * ordering, the persistence and the replay all come from the mechanism that was already correct.
83
+ */
84
+ export type ToolCallQuestion = {
85
+ items: QuestionItem[];
63
86
  status: "pending" | "answered" | "cancelled";
64
- selected?: string[];
65
- extraNote?: string;
66
- at?: number;
87
+ answers?: QuestionAnswer[];
67
88
  };
68
89
  export type Message = {
69
90
  id: string;
@@ -72,7 +93,6 @@ export type Message = {
72
93
  createdAt: string;
73
94
  piEntryId?: string;
74
95
  toolCalls?: ToolCallBlock[];
75
- questionBlocks?: QuestionBlock[];
76
96
  thinking?: string;
77
97
  images?: MessageImage[];
78
98
  agentIdentity?: {
@@ -148,6 +168,7 @@ export type ToolCallBlock = {
148
168
  * replacement text, and without this the screen is blank until the last token arrives. */
149
169
  preparing?: boolean;
150
170
  at?: number;
171
+ question?: ToolCallQuestion;
151
172
  };
152
173
  /**
153
174
  * ONE TOOL CALL'S OUTPUT, reduced to the part a person reads and bounded so it cannot become the
@@ -178,6 +199,7 @@ export type ToolCallNotice = {
178
199
  args?: ToolCallArg[];
179
200
  status?: "ok" | "error";
180
201
  durationMs?: number;
202
+ question?: ToolCallQuestion;
181
203
  result?: ToolResultSummary;
182
204
  live?: string;
183
205
  preparing?: boolean;
@@ -692,13 +714,21 @@ export type ExtensionUiAnswer = {
692
714
  export type ClientCommand = ClientCommandBody & {
693
715
  operationRequestId?: string;
694
716
  };
695
- export type ClientCommandBody = {
717
+ export type ClientCommandBody =
718
+ /**
719
+ * THE ANSWER TO A `question` CALL, ADDRESSED BY THE CALL'S ROW.
720
+ *
721
+ * It used to name a `questionId` on a block hung off the message. It now names `callIndex` - the same
722
+ * index every other tool update uses - because the question IS that call, and the row being rendered is
723
+ * the only place the browser can learn the index from. Answers pair to questions by `id`, not position,
724
+ * so a person answering the second of two is not recorded as answering the first.
725
+ */
726
+ {
696
727
  type: "question.answer";
697
728
  conversationId: string;
698
729
  messageId: string;
699
- questionId: string;
700
- selected?: string[];
701
- extraNote?: string;
730
+ callIndex: number;
731
+ answers?: QuestionAnswer[];
702
732
  cancelled?: boolean;
703
733
  }
704
734
  /**
@@ -1051,11 +1081,6 @@ export type ServerEvent = {
1051
1081
  conversationId: string;
1052
1082
  messageId: string;
1053
1083
  toolCalls: ToolCallBlock[];
1054
- } | {
1055
- type: "message.questions";
1056
- conversationId: string;
1057
- messageId: string;
1058
- questionBlocks: QuestionBlock[];
1059
1084
  } | {
1060
1085
  type: "error";
1061
1086
  message: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",