@pi-unipi/btw 3.0.0-alpha.35 → 3.0.0-alpha.36

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.
Files changed (2) hide show
  1. package/extensions/btw.ts +66 -17
  2. package/package.json +2 -2
package/extensions/btw.ts CHANGED
@@ -147,10 +147,17 @@ export function buildSeedEntries(ctx: {
147
147
  // ─── Page history (session-memory only, never persisted) ───────────────────
148
148
 
149
149
  export interface BtwPage {
150
+ /** Stable id (`btw-<pid>-<n>`), shared by the TUI panel and the app bridge
151
+ * (the bridge's `btw_delta`/`btw_end` `id` and `btw_list` page ids). */
152
+ id: string;
150
153
  question: string;
151
154
  /** One line per tool call: "… Reading <p>" while running → "✓ <desc>" */
152
155
  toolLines: string[];
153
156
  answer: string;
157
+ /** Text streamed so far (the final `answer` lands at the end). */
158
+ partial: string;
159
+ /** Model reasoning streamed so far (kept for `btw_list` replays). */
160
+ thinking: string;
154
161
  error?: string;
155
162
  aborted?: boolean;
156
163
  done: boolean;
@@ -159,11 +166,44 @@ export interface BtwPage {
159
166
  }
160
167
 
161
168
  const pages: BtwPage[] = [];
169
+ let pageSeq = 0;
162
170
 
163
- /** Read-only snapshot of recent pages (question, answer, error) for a
164
- * `btw_list{}` reply — never mutate the result. */
165
- export function listPages(): Array<{ question: string; answer: string; error?: string }> {
166
- return pages.map((p) => ({ question: p.question, answer: p.answer, error: p.error }));
171
+ /** A fresh page with a process-unique id. */
172
+ export function newPage(question: string): BtwPage {
173
+ return { id: `btw-${process.pid}-${++pageSeq}`, question, toolLines: [], answer: "", partial: "", thinking: "", done: false };
174
+ }
175
+
176
+ /** One page as the app bridge's `btw_list` sends it. `question`/`answer`/
177
+ * `error` keep their original meaning (older apps read only those); `id`,
178
+ * `done`/`running` and `toolLines` let the app reconcile its own pages.
179
+ * A running page's `answer` is the text streamed so far. */
180
+ export interface BtwListPage {
181
+ id: string;
182
+ question: string;
183
+ answer: string;
184
+ error?: string;
185
+ done: boolean;
186
+ running: boolean;
187
+ toolLines?: string[];
188
+ /** Running pages only: the tail of the model's reasoning so far. */
189
+ thinking?: string;
190
+ }
191
+
192
+ /** Tail kept for a running page's `thinking` in `btw_list`. */
193
+ const LIST_THINKING_TAIL = 2000;
194
+
195
+ /** Read-only snapshot of recent pages for a `btw_list{}` reply — never mutate the result. */
196
+ export function listPages(): BtwListPage[] {
197
+ return pages.map((p) => ({
198
+ id: p.id,
199
+ question: p.question,
200
+ answer: p.done ? p.answer : p.partial,
201
+ ...(p.error ? { error: p.error } : {}),
202
+ done: p.done,
203
+ running: !p.done,
204
+ ...(p.toolLines.length ? { toolLines: [...p.toolLines] } : {}),
205
+ ...(!p.done && p.thinking ? { thinking: p.thinking.slice(-LIST_THINKING_TAIL) } : {}),
206
+ }));
167
207
  }
168
208
 
169
209
  /** ↑/↓ paging reducer — clamp the target index into [0, count-1]. */
@@ -229,7 +269,9 @@ function runQuestion(
229
269
  page: BtwPage,
230
270
  thinkingLevel: AiThinkingLevel,
231
271
  onUpdate: () => void,
232
- onDelta?: (kind: BtwDeltaKind, text: string) => void,
272
+ /** `index` (tool deltas only): the `page.toolLines` slot this line fills —
273
+ * a tool's "done" line replaces its own "running" line. */
274
+ onDelta?: (kind: BtwDeltaKind, text: string, index?: number) => void,
233
275
  ): BtwRun {
234
276
  let session: AgentSession | null = null;
235
277
  let unsubscribe: (() => void) | null = null;
@@ -270,7 +312,7 @@ function runQuestion(
270
312
  const line = formatToolLine(e.toolName, e.args, ctx.cwd, "running");
271
313
  page.toolLines.push(line);
272
314
  onUpdate();
273
- onDelta?.("tool", line);
315
+ onDelta?.("tool", line, page.toolLines.length - 1);
274
316
  } else if (event.type === "tool_execution_end") {
275
317
  const e = event as { toolCallId: string; toolName: string; isError: boolean };
276
318
  const start = pendingStarts.get(e.toolCallId);
@@ -280,11 +322,17 @@ function runQuestion(
280
322
  else page.toolLines.push(line);
281
323
  pendingStarts.delete(e.toolCallId);
282
324
  onUpdate();
283
- onDelta?.("tool", line);
325
+ onDelta?.("tool", line, idx ?? page.toolLines.length - 1);
284
326
  } else if (event.type === "message_update") {
285
327
  const e = event as { assistantMessageEvent?: { type?: string; delta?: string } };
286
- if (e.assistantMessageEvent?.type === "text_delta" && e.assistantMessageEvent.delta) onDelta?.("text", e.assistantMessageEvent.delta);
287
- else if (e.assistantMessageEvent?.type === "thinking_delta" && e.assistantMessageEvent.delta) onDelta?.("thinking", e.assistantMessageEvent.delta);
328
+ if (e.assistantMessageEvent?.type === "text_delta" && e.assistantMessageEvent.delta) {
329
+ page.partial += e.assistantMessageEvent.delta;
330
+ onDelta?.("text", e.assistantMessageEvent.delta);
331
+ }
332
+ else if (e.assistantMessageEvent?.type === "thinking_delta" && e.assistantMessageEvent.delta) {
333
+ page.thinking += e.assistantMessageEvent.delta;
334
+ onDelta?.("thinking", e.assistantMessageEvent.delta);
335
+ }
288
336
  }
289
337
  });
290
338
 
@@ -481,7 +529,7 @@ class BtwPanel implements Component {
481
529
 
482
530
  /** One streamed update from `BtwApi.ask` (mirrors the bridge's `btw_delta`/`btw_end`). */
483
531
  export type BtwEvent =
484
- | { type: "delta"; kind: BtwDeltaKind; text: string }
532
+ | { type: "delta"; kind: BtwDeltaKind; text: string; index?: number }
485
533
  | { type: "end"; answer: string; error?: string; usage?: { input: number; output: number; totalTokens: number } };
486
534
 
487
535
  export interface BtwApi {
@@ -489,8 +537,8 @@ export interface BtwApi {
489
537
  * Only one question runs at a time (shared with the TUI panel): a second call while
490
538
  * one is in flight ends immediately with an error. */
491
539
  ask(cctx: ExtensionCommandContext, question: string, onEvent: (event: BtwEvent) => void): { id: string; finished: Promise<void> };
492
- /** Recent pages (question, answer, error) for this pi session — shared with the TUI panel. */
493
- list(): Array<{ question: string; answer: string; error?: string }>;
540
+ /** Recent pages for this pi session — shared with the TUI panel. */
541
+ list(): BtwListPage[];
494
542
  }
495
543
 
496
544
  const BTW_API_KEY = Symbol.for("unipi.btw.api");
@@ -502,12 +550,13 @@ function publishUiFreeApi(pi: ExtensionAPI, getActiveRun: () => BtwRun | null, s
502
550
  let idSeq = 0;
503
551
  const api: BtwApi = {
504
552
  ask(cctx, question, onEvent) {
505
- const id = `api-${process.pid}-${++idSeq}`;
506
553
  if (getActiveRun()) {
554
+ const id = `btw-${process.pid}-busy-${++idSeq}`;
507
555
  const finished = Promise.resolve().then(() => onEvent({ type: "end", answer: "", error: "btw is already answering another question" }));
508
556
  return { id, finished };
509
557
  }
510
- const page: BtwPage = { question, toolLines: [], answer: "", done: false };
558
+ const page = newPage(question);
559
+ const id = page.id;
511
560
  pages.push(page);
512
561
  const run = runQuestion(
513
562
  cctx,
@@ -515,7 +564,7 @@ function publishUiFreeApi(pi: ExtensionAPI, getActiveRun: () => BtwRun | null, s
515
564
  page,
516
565
  pi.getThinkingLevel() as AiThinkingLevel,
517
566
  () => {},
518
- (kind, text) => onEvent({ type: "delta", kind, text }),
567
+ (kind, text, index) => onEvent(index === undefined ? { type: "delta", kind, text } : { type: "delta", kind, text, index }),
519
568
  );
520
569
  setActiveRun(run);
521
570
  const finished = run.finished.then(() => {
@@ -545,7 +594,7 @@ export default function btwExtension(pi: ExtensionAPI): void {
545
594
 
546
595
  function startRun(ctx: ExtensionCommandContext, question: string): void {
547
596
  if (activeRun) return; // never two answers at once
548
- const page: BtwPage = { question, toolLines: [], answer: "", done: false };
597
+ const page = newPage(question);
549
598
  const index = pages.length;
550
599
  pages.push(page);
551
600
  const run = runQuestion(ctx, question, page, pi.getThinkingLevel() as AiThinkingLevel, () => panelRef?.tick());
@@ -589,7 +638,7 @@ export default function btwExtension(pi: ExtensionAPI): void {
589
638
  }
590
639
 
591
640
  async function headlessAsk(ctx: ExtensionCommandContext, question: string): Promise<void> {
592
- const page: BtwPage = { question, toolLines: [], answer: "", done: false };
641
+ const page = newPage(question);
593
642
  pages.push(page);
594
643
  notify(ctx, `btw: ${question}`, "info");
595
644
  const run = runQuestion(ctx, question, page, pi.getThinkingLevel() as AiThinkingLevel, () => {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/btw",
3
- "version": "3.0.0-alpha.35",
3
+ "version": "3.0.0-alpha.36",
4
4
  "description": "A pi extension for parallel side conversations with /unipi:btw — part of the Unipi suite",
5
5
  "type": "module",
6
6
  "main": "extensions/btw.ts",
@@ -37,7 +37,7 @@
37
37
  "@earendil-works/pi-tui": "^0.87.1"
38
38
  },
39
39
  "dependencies": {
40
- "@pi-unipi/core": "3.0.0-alpha.35"
40
+ "@pi-unipi/core": "3.0.0-alpha.36"
41
41
  },
42
42
  "pi": {
43
43
  "extensions": [