oira666_pi-watcher 0.0.3 → 0.0.5

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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A persistent side-agent conversation for your running [Pi](https://pi.dev) agent.
4
4
 
5
- **Package:** `oira666_pi-watcher` · **Version:** `0.0.3` · **License:** MIT
5
+ **Package:** `oira666_pi-watcher` · **Version:** `0.0.5` · **License:** MIT
6
6
 
7
7
  ## Install
8
8
 
@@ -23,18 +23,19 @@ pi -e ./src/index.ts
23
23
 
24
24
  | Command | Action |
25
25
  | --- | --- |
26
+ | **Alt+W** | Toggle the Watcher panel without sending a prompt or stopping either agent. |
26
27
  | `/watcher` | Create/resume the side agent and show its conversation. **Sends no prompt.** |
27
28
  | `/watcher Why is it retrying?` | Open and send exactly that question. |
28
29
  | `/watcher model` | Open the side agent's model picker. Does not prompt either agent. |
29
30
 
30
- These are human-facing TUI commands, not tools exposed to the main agent. They work while the main agent is running, including during compaction.
31
+ These are human-facing TUI controls, not tools exposed to the main agent. They work while the main agent is running, including during compaction.
31
32
 
32
33
  The panel opens immediately while the side process initializes its inherited extensions and MCPs in the background. You can type straight away; a submitted question waits for initialization and is sent once ready. `/stop` cancels a question waiting for startup. Opening without a question still sends no prompt. Saved history and the selected model appear when initialization finishes; reopening an already initialized side agent reuses it.
33
34
 
34
35
  Inside the overlay:
35
36
 
36
37
  - Type a question and press **Enter**. Use **Shift+Enter** for a newline.
37
- - **Esc** returns to the main view without stopping either agent. Run `/watcher` again to return; conversation, draft, and scroll position are retained.
38
+ - **Alt+W** closes the panel from any Watcher view. Press it again to reopen. **Esc** also returns from chat to the main view without stopping either agent. Conversation, draft, and scroll position are retained.
38
39
  - `/model` opens a searchable provider/model picker. Its selection affects **only the side agent** and is persisted in that side conversation. You can change models while the side agent is running, just like in Pi; switching does not abort the current reply.
39
40
  - `/stop` cancels only the side agent and clears its queued messages.
40
41
  - `/copy` copies to the system clipboard, because terminal mouse selection spans the full terminal width. Messages are numbered in the transcript (`You #n`, `Watcher #n`):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-watcher",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "A persistent side-agent conversation for observing and steering your running Pi agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -269,6 +269,20 @@ export default function watcher(pi: ExtensionAPI): void {
269
269
  pi.on("session_compact", () => { compacting = false; mainStatus(); });
270
270
  pi.on("session_compact_failed", () => { compacting = false; mainStatus(); });
271
271
 
272
+ pi.registerShortcut("alt+w", {
273
+ description: "Toggle the Watcher panel",
274
+ handler: async (ctx) => {
275
+ if (ctx.mode !== "tui" || !ctx.hasUI) return;
276
+ try {
277
+ if (side && isCurrent(side) && side.store.state.open) {
278
+ side.overlay?.abort();
279
+ return;
280
+ }
281
+ await show(await ensure(ctx));
282
+ } catch (error) { ctx.ui.notify(`Watcher: ${errorText(error)}`, "error"); }
283
+ },
284
+ });
285
+
272
286
  pi.registerCommand("watcher", {
273
287
  description: "Open the side agent; /watcher <question> to ask; /watcher model to choose its model",
274
288
  handler: async (args, ctx) => {
package/src/ui-copy.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { stripTerminalSequences } from "@earendil-works/pi-tui";
7
7
  import { messageNumbers } from "./ui-render.ts";
8
- import type { WatcherMessage } from "./ui-types.ts";
8
+ import type { WatcherLiveState, WatcherMessage } from "./ui-types.ts";
9
9
 
10
10
  export const COPY_USAGE = "/copy [me] [n|all] — e.g. /copy (last Watcher reply), /copy 3 (Watcher #3), /copy me (your last), /copy me 2, /copy all";
11
11
 
@@ -19,7 +19,12 @@ function clean(text: string): string {
19
19
  }
20
20
 
21
21
  /** Resolve `/copy` arguments against the stored conversation. */
22
- export function selectForCopy(messages: ReadonlyArray<WatcherMessage>, args: string): CopySelection {
22
+ export function selectForCopy(messages: ReadonlyArray<WatcherMessage>, args: string, live?: WatcherLiveState): CopySelection {
23
+ // Copy a snapshot of the visible reply without committing it to history.
24
+ if (live?.text) {
25
+ const prefix = live.truncated ? "… (earlier output not retained)\n" : "";
26
+ messages = [...messages, { role: "assistant", text: prefix + live.text }];
27
+ }
23
28
  const words = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
24
29
  let role: "user" | "assistant" = "assistant";
25
30
  let which: "last" | "all" | number = "last";
package/src/ui.ts CHANGED
@@ -113,6 +113,7 @@ export class WatcherOverlay implements Component, Focusable {
113
113
  private mode: Mode = "chat";
114
114
  private editor: Editor;
115
115
  private approvalEditor?: Editor;
116
+ private commandEditor?: Editor;
116
117
  private approvalId?: string;
117
118
  private approvalSel = 1; // 0 send, 1 edit (default, no accidental send), 2 cancel
118
119
 
@@ -181,9 +182,10 @@ export class WatcherOverlay implements Component, Focusable {
181
182
  }
182
183
 
183
184
  private syncEditorFocus(): void {
184
- const chat = this.mode === "chat" && !this.currentApproval();
185
+ const chat = !this.commandEditor && this.mode === "chat" && !this.currentApproval();
185
186
  this.editor.focused = this._focused && chat;
186
- if (this.approvalEditor) this.approvalEditor.focused = this._focused && this.mode === "approval-edit";
187
+ if (this.approvalEditor) this.approvalEditor.focused = this._focused && !this.commandEditor && this.mode === "approval-edit";
188
+ if (this.commandEditor) this.commandEditor.focused = this._focused;
187
189
  }
188
190
 
189
191
  // ---- lifecycle -------------------------------------------------------------------------
@@ -195,6 +197,7 @@ export class WatcherOverlay implements Component, Focusable {
195
197
  this.renderer.invalidate();
196
198
  this.editor.invalidate();
197
199
  this.approvalEditor?.invalidate();
200
+ this.commandEditor?.invalidate();
198
201
  this.pickerList?.invalidate();
199
202
  }
200
203
 
@@ -310,11 +313,19 @@ export class WatcherOverlay implements Component, Focusable {
310
313
 
311
314
  handleInput(data: string): void {
312
315
  if (this.closed) return;
316
+ // Focused overlays own input, so the main editor's shortcut cannot close us.
317
+ if (matchesKey(data, "alt+w")) {
318
+ this.close("closed");
319
+ return;
320
+ }
313
321
  if (this.notice) {
314
322
  this.notice = undefined;
315
323
  this.requestRender();
316
324
  }
317
- if (this.mode === "model") {
325
+ if (this.commandEditor) {
326
+ if (this.isClose(data)) this.commandEditor = undefined;
327
+ else this.commandEditor.handleInput(data);
328
+ } else if (this.mode === "model") {
318
329
  this.handlePickerInput(data);
319
330
  } else if (this.mode === "approval-edit") {
320
331
  this.handleApprovalEditInput(data);
@@ -349,10 +360,7 @@ export class WatcherOverlay implements Component, Focusable {
349
360
  const cmd = /^\/(model|stop|help|copy)(?:\s+([\s\S]*))?$/.exec(text);
350
361
  if (cmd) {
351
362
  this.clearEditor();
352
- if (cmd[1] === "model") this.openPicker((cmd[2] ?? "").trim());
353
- else if (cmd[1] === "stop") this.doStop();
354
- else if (cmd[1] === "copy") this.doCopy(cmd[2] ?? "");
355
- else this.setNotice(`Enter send · Shift+Enter newline · PgUp/PgDn scroll · /model [filter] · /stop · ${COPY_USAGE} · Esc close (main agent untouched)`);
363
+ this.runLocalCommand(cmd[1], cmd[2] ?? "");
356
364
  return;
357
365
  }
358
366
  if (this.sideBusy()) {
@@ -385,8 +393,29 @@ export class WatcherOverlay implements Component, Focusable {
385
393
  this.store.setDraft("");
386
394
  }
387
395
 
396
+ private runLocalCommand(command: string, args: string): void {
397
+ if (command === "model") this.openPicker(args.trim());
398
+ else if (command === "stop") this.doStop();
399
+ else if (command === "copy") this.doCopy(args);
400
+ else this.setNotice(`Enter send · Shift+Enter newline · PgUp/PgDn scroll · /model [filter] · /stop · ${COPY_USAGE} · Alt+W / Esc close (main agent untouched)`);
401
+ }
402
+
403
+ /** Allow local commands while an approval owns the normal input controls. */
404
+ private openCommandEditor(): void {
405
+ const editor = new Editor(this.tui, this.editorTheme(), { paddingX: 0 });
406
+ editor.setText("/");
407
+ editor.onSubmit = (raw) => {
408
+ const cmd = /^\/(model|stop|help|copy)(?:\s+([\s\S]*))?$/.exec(raw.trim());
409
+ if (cmd) {
410
+ this.commandEditor = undefined;
411
+ this.runLocalCommand(cmd[1], cmd[2] ?? "");
412
+ } else editor.setText(raw);
413
+ };
414
+ this.commandEditor = editor;
415
+ }
416
+
388
417
  private doCopy(args: string): void {
389
- const sel = selectForCopy(this.store.state.messages, args);
418
+ const sel = selectForCopy(this.store.state.messages, args, this.store.state.live);
390
419
  if (!sel.ok) {
391
420
  this.setNotice(sel.error, "warn");
392
421
  return;
@@ -549,6 +578,10 @@ export class WatcherOverlay implements Component, Focusable {
549
578
  }
550
579
 
551
580
  private handleApprovalInput(data: string): void {
581
+ if (data === "/" || decodeKittyPrintable(data) === "/") {
582
+ this.openCommandEditor();
583
+ return;
584
+ }
552
585
  const a = this.currentApproval();
553
586
  if (!a) return;
554
587
  if (this.isClose(data)) {
@@ -712,6 +745,7 @@ export class WatcherOverlay implements Component, Focusable {
712
745
  else if (inner >= 76) txt = "Enter send · ⇧Enter newline · PgUp/PgDn scroll · /model · /copy · /stop · Esc close";
713
746
  else if (inner >= 50) txt = "Enter send · PgUp/PgDn · /model · /copy · /stop · Esc";
714
747
  else txt = "Enter send · Esc close";
748
+ txt = `Alt+W close · ${txt}`;
715
749
  return th.fg("dim", ` ${txt}`);
716
750
  }
717
751
 
@@ -729,7 +763,9 @@ export class WatcherOverlay implements Component, Focusable {
729
763
  const approval = this.currentApproval();
730
764
  const editorCap = Math.max(3, Math.min(8, Math.floor(H * 0.35)));
731
765
 
732
- if (this.mode === "model") {
766
+ if (this.commandEditor) {
767
+ for (const l of this.capEditorLines(this.commandEditor.render(inner), editorCap)) lines.push(frame(l));
768
+ } else if (this.mode === "model") {
733
769
  lines.push(frame(` ${th.fg("accent", th.bold("Select side model"))} ${th.fg("dim", "(independent of main)")}`));
734
770
  lines.push(frame(` ${th.fg("muted", "filter:")} ${this.pickerFilter}${th.fg("dim", "▏")}`));
735
771
  if (this.pickerLoading) lines.push(frame(th.fg("dim", " loading models…")));