@plannotator/pi-extension 0.28.45 → 0.28.47

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.
@@ -61,6 +61,10 @@ body[data-plannotator-pinpoint-cursor],
61
61
  body[data-plannotator-pinpoint-cursor] * {
62
62
  cursor: crosshair !important;
63
63
  }
64
+ /* Armed, body listens for clicks so a tap pins (setTapTarget): no grey flash over the whole page on each tap. */
65
+ body[data-plannotator-pinpoint-cursor] {
66
+ -webkit-tap-highlight-color: transparent;
67
+ }
64
68
  /* A plain click on a link with a real destination follows it while armed
65
69
  * (Cmd/Ctrl-click pins it), so such a link keeps the pointer that says so,
66
70
  * until the modifier is held. A link that goes nowhere (no href, "", "#",
@@ -509,14 +513,31 @@ export const BRIDGE_SCRIPT = `(function() {
509
513
  var annotateModeActive = true;
510
514
  // A native host decides link navigations itself (HtmlViewer hostNavigates).
511
515
  var hostNavigates = false;
516
+ // WebKit's touch model (iOS, and Playwright's WebKit on Linux) turns a tap
517
+ // into mouse events and a click only when the tapped element or an ancestor
518
+ // up to <body> listens for a click; listeners on document and window (all
519
+ // of the bridge's) do not count. Without this, a tap on plain text while
520
+ // armed sends no click and pins nothing. So while armed in pinpoint, body
521
+ // listens (a no-op), and its tap highlight is off: the pin draws its own box.
522
+ var tapTargetBody = null;
523
+ function noteTap() {}
524
+ function setTapTarget(on) {
525
+ var body = on ? document.body : null;
526
+ if (body === tapTargetBody) return;
527
+ if (tapTargetBody) tapTargetBody.removeEventListener('click', noteTap);
528
+ tapTargetBody = body;
529
+ if (body) body.addEventListener('click', noteTap);
530
+ }
512
531
  function updatePinpointCursor() {
513
532
  if (!document.body) return;
514
533
  if (annotateModeActive && currentInputMethod === 'pinpoint') {
515
534
  document.body.setAttribute('data-plannotator-pinpoint-cursor', '');
516
535
  if (!LIVE) document.body.setAttribute('data-plannotator-frame-inert', '');
536
+ setTapTarget(true);
517
537
  } else {
518
538
  document.body.removeAttribute('data-plannotator-pinpoint-cursor');
519
539
  document.body.removeAttribute('data-plannotator-frame-inert');
540
+ setTapTarget(false);
520
541
  }
521
542
  }
522
543
  var pinpointHover = null;
@@ -873,11 +894,21 @@ export const BRIDGE_SCRIPT = `(function() {
873
894
  else if (type === PREFIX + 'reveal-pin') {
874
895
  // A host whose own panel covers the page's lower part (a phone's comment
875
896
  // sheet) shrank the frame: bring the pinned draft back into view.
897
+ // clearBottom: the panel floats over the frame instead (the Inbox's card).
876
898
  if (pendingPinEl && pendingPinEl.isConnected) {
877
- try { pendingPinEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (ex) {}
899
+ var clearBottom = e.data.clearBottom;
900
+ if (typeof clearBottom === 'number' && isFinite(clearBottom) && clearBottom > 0) {
901
+ revealAbove(pendingPinEl, Math.min(clearBottom, window.innerHeight));
902
+ } else {
903
+ try { pendingPinEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (ex) {}
904
+ }
878
905
  }
879
906
  }
880
907
 
908
+ else if (type === PREFIX + 'set-touch-selection') {
909
+ setTouchSelection(e.data.active === true);
910
+ }
911
+
881
912
  else if (type === PREFIX + 'scroll-to') {
882
913
  // Selecting an annotation scrolls its first resolved target into view
883
914
  // and flashes the overlay focus highlight over EVERY rect of EVERY
@@ -4125,6 +4156,67 @@ export const BRIDGE_SCRIPT = `(function() {
4125
4156
  return true;
4126
4157
  }
4127
4158
 
4159
+ // The pinned element in the frame's top part, a host's panel over the last
4160
+ // covered pixels: its middle in the middle of what shows, or its top when it
4161
+ // is taller. Already in sight, nothing moves.
4162
+ function revealAbove(el, covered) {
4163
+ var shown = window.innerHeight - covered;
4164
+ var box = el.getBoundingClientRect();
4165
+ if (box.top >= 0 && box.bottom <= shown) return;
4166
+ var by = box.height > shown ? box.top : box.top + box.height / 2 - shown / 2;
4167
+ var root = document.scrollingElement;
4168
+ for (var node = el.parentElement; node && by; node = node.parentElement) {
4169
+ if (node !== root && !/auto|scroll/.test(getComputedStyle(node).overflowY)) continue;
4170
+ var before = node.scrollTop;
4171
+ node.scrollTop = before + by;
4172
+ by -= node.scrollTop - before;
4173
+ }
4174
+ }
4175
+
4176
+ // --- Touch: a selection made with a finger ---
4177
+ // A long press and its handles send no mouseup, so the capture-phase mouseup
4178
+ // listener above never sees them. Where the host asks for it (HtmlViewer
4179
+ // touchSelection) and the main pointer is a finger, the selection is read
4180
+ // from selectionchange once it rests, as the Markdown viewer does. A collapsed
4181
+ // selection is not taken: moving focus out of the frame can collapse it, and
4182
+ // a tap on the page already closes the draft through mouseup. What the bridge
4183
+ // reported already (a pin's own selection, a draft that did not change) is
4184
+ // not reported again. The query and the wait are utils/touchSelection.ts's
4185
+ // (this file is vendored whole and imports nothing; a test keeps them equal).
4186
+ var touchSelectionOn = false;
4187
+ var touchSelectionTimer = 0;
4188
+ function isReportedRange(range) {
4189
+ if (!pendingRange) return false;
4190
+ try {
4191
+ return pendingRange.compareBoundaryPoints(Range.START_TO_START, range) === 0
4192
+ && pendingRange.compareBoundaryPoints(Range.END_TO_END, range) === 0;
4193
+ } catch (ex) {
4194
+ return false;
4195
+ }
4196
+ }
4197
+ function takeTouchSelection() {
4198
+ var sel = window.getSelection();
4199
+ if (!sel || sel.isCollapsed || !sel.rangeCount) return;
4200
+ if (isReportedRange(sel.getRangeAt(0))) return;
4201
+ if (!(sel.toString() || '').trim()) return;
4202
+ // New words chosen over a pin: the selection is the draft now.
4203
+ clearMultiTargets();
4204
+ clearPendingPin();
4205
+ handleSelection();
4206
+ }
4207
+ function onTouchSelectionChange() {
4208
+ clearTimeout(touchSelectionTimer);
4209
+ touchSelectionTimer = setTimeout(takeTouchSelection, 400);
4210
+ }
4211
+ function setTouchSelection(active) {
4212
+ var on = active && window.matchMedia('(pointer: coarse)').matches;
4213
+ if (on === touchSelectionOn) return;
4214
+ touchSelectionOn = on;
4215
+ clearTimeout(touchSelectionTimer);
4216
+ if (on) document.addEventListener('selectionchange', onTouchSelectionChange);
4217
+ else document.removeEventListener('selectionchange', onTouchSelectionChange);
4218
+ }
4219
+
4128
4220
  // --- Touch: a tap on a link follows it, press-and-hold pins it ---
4129
4221
  // The owner's amendment to html-link-click (2026-10-20): on a phone there is
4130
4222
  // no Cmd/Ctrl, so while armed a TAP on a link follows it (the click handlers
@@ -397,6 +397,16 @@ export interface PlannotatorConfig {
397
397
  * `{ text_size }`.
398
398
  */
399
399
  inboxTextSize?: number;
400
+ /**
401
+ * How the Inbox list's rows look: the names of the row options that are on
402
+ * (core `INBOX_ROWS_OPTIONS`), every other one off. Unset: none written yet
403
+ * (a window adopts its browser's old cookies once). Written by the Inbox's ⋮
404
+ * menu and Settings → Rows through POST /api/inbox/settings `{ rows }`. A
405
+ * name this build does not know is dropped on read.
406
+ */
407
+ inboxRows?: string[];
408
+ /** How many times `inboxRows` was saved: windows take a look only when it is newer than the last they took. */
409
+ inboxRowsRev?: number;
400
410
  /**
401
411
  * The Inbox's onboarding tour, per machine (approval "inbox-onboarding-tour"):
402
412
  * when it was started, finished or skipped, and whether the first real thread
@@ -431,8 +441,8 @@ export interface PlannotatorConfig {
431
441
  * announcement (`POST /api/inbox/settings` `{ voice }`).
432
442
  */
433
443
  inboxVoice?: { on?: boolean; phonesOnly?: boolean; engine?: "codex" | "api" };
434
- /** The voice announcement: the list intro's rule (`dueAt`, or `shownAt` on a fresh install), claimed with `POST /api/inbox/voice/intro`. */
435
- inboxVoiceIntro?: { dueAt?: string; shownAt?: string };
444
+ /** The voice announcement: the list intro's rule (`dueAt`, or `shownAt` on a fresh install), claimed with `POST /api/inbox/voice/intro`. `skippedAt`: the check found no ChatGPT login, so it never asks again. */
445
+ inboxVoiceIntro?: { dueAt?: string; shownAt?: string; skippedAt?: string };
436
446
  /**
437
447
  * The Board (approval "inbox-board", 2026-10-26 amendment): something the
438
448
  * person turns on, off by default. On: the sidebar's Board row and page,
@@ -6,10 +6,10 @@
6
6
  import type { GuideViewerAssets } from "./guide-format.ts";
7
7
 
8
8
  export const GUIDE_VIEWER_MANIFEST: Omit<GuideViewerAssets, "baseUrl"> = {
9
- js: "viewer.D8s5nXA_.js",
10
- css: "viewer.CfyK-nt_.css",
11
- jsIntegrity: "sha384-ttJm9w/o3SyLUMGneTh8OaaifLy3MT7HZB9/kBHqJ0+45DYhxzmOuWUBAYhNK3vm",
12
- cssIntegrity: "sha384-Egde7gEF8IuWkM4XoUBzRwFHrsOZewlgRG9X1g2ZKsiL0y89Iy/NvYtlfPpadq8f",
9
+ js: "viewer.BfflE3Ru.js",
10
+ css: "viewer.b_pZ_mPX.css",
11
+ jsIntegrity: "sha384-1EH/kDTH8mGiMlSMNNZVhSX+/faRqfHb+0y/rlCoFbHG5hoxc3xf5njsncjsu95X",
12
+ cssIntegrity: "sha384-7tITytAS3P1UFEPudEv1zIuOiOvjfnyNmjJj+HGZZUqJUfCBi2PIIX5rNL5WlC9M",
13
13
  langs: {
14
14
  "astro": "chunks/astro.Ts5EKq2l.js",
15
15
  "c": "chunks/c.BIGW1oBm.js",
@@ -316,7 +316,7 @@ export const INBOX_SESSION_THREADS_CHANNEL = "Their decisions stay in these thre
316
316
 
317
317
  /** When to use the Inbox. Fixed text: the agent tool's lead and the Inbox's server instructions carry it whole. */
318
318
  export const INBOX_USE_RULE =
319
- "Use it for a decision only they can make (a :::question block; go on with other work), finished long work (a short summary, files in `attachments`), or when they ask."
319
+ "Use it for a decision only they can make (a :::question block; go on with other work), finished long work (a short summary with its pictures inline, files in `attachments`), or when they ask."
320
320
 
321
321
  /**
322
322
  * Separate work, separate threads (the owner, 2026-10-29: an agent given new
@@ -403,6 +403,20 @@ export const INBOX_AGENT_ACTION_LEADS: Readonly<Record<string, string>> = {
403
403
  card: "A Board card for work that outlasts this reply: create it, then move it and say where it is as the work moves.",
404
404
  }
405
405
 
406
+ /**
407
+ * Pictures sit where they are discussed (the owner, 2026-11-02: agents sent
408
+ * a message's screenshots and the pictures of its options as attachments at
409
+ * the bottom, so the person had to scroll there to see what each option
410
+ * was). Fixed text: the agent tool's `body` field, the MCP server's `body`
411
+ * field and `plannotator inbox --help` carry it whole.
412
+ */
413
+ export const INBOX_PICTURES_RULE =
414
+ 'Pictures go in the body where you write about them, `![alt](/abs/path.png)`, never only in `attachments`; do not also attach one the body shows. Visual options are picture choices, one picture each: `- [ ] A · Label — ![alt](path) what it means`; the label comes back.'
415
+
416
+ /** The `attachments` field's line on pictures (fixed text, on every copy of the field). */
417
+ export const INBOX_ATTACHMENTS_PICTURES_LINE =
418
+ 'Screenshots and pictures go in the body instead (`![alt](path)`); attach what the person should open or click through.'
419
+
406
420
  /**
407
421
  * The agent tool's own words for the fields whose Inbox description is long
408
422
  * (the MCP server's are written for a raw MCP client, which has no other
@@ -432,7 +446,9 @@ export const INBOX_AGENT_FIELD_DESCRIPTIONS: Readonly<Record<string, Readonly<Re
432
446
  '',
433
447
  'Your recommendation goes on the `Recommended:` line, never in a choice label. Keep the choices in their natural order; never move yours first. `:::question-multi` takes several choices, `:::question-text` free text. The person can always answer Other, add a note or skip, so never add an Other choice yourself. `Stopped: <why>` only when you cannot go on until it is answered (never `Stopped: no`); `Holds up: <work>` names work that waits while you go on. Ask only what you cannot decide alone, about 8 a round at most. Answers come back in the reply under "Answers to your questions".',
434
448
  '',
435
- 'Optional lines after the question; most questions need none of these: `Stakes: <what it affects>` (give one with every Stopped or Holds up), `Shows: ![alt](path)` or `Shows: [name](file)` (the one picture or file it is about), `Why: <why your recommendation>` on its own line, `Reversible: no — <why>` only when it cannot be undone. Visual options can be picture choices, `- [ ] Label — ![alt](path)`: the picture picks it, the label comes back.',
449
+ INBOX_PICTURES_RULE,
450
+ '',
451
+ 'Optional lines after the question; most questions need none of these: `Stakes: <what it affects>` (give one with every Stopped or Holds up), `Shows: ![alt](path)` or `Shows: [name](file)` (the one picture or file it is about), `Why: <why your recommendation>` on its own line, `Reversible: no — <why>` only when it cannot be undone.',
436
452
  '',
437
453
  'Only for a true blocker the person must go and do outside this chat, that you cannot do yourself (approve a deploy, sign in, touch a security key, merge a PR you cannot merge), write a `:::action` block: first line the order, then why. Each command the person must run goes in the why in its own fenced code block (```), one block per command in the order to run them, so each gets a copy button. Optional: `Where: <url>`, `Stopped:`, `Watching: <what you watch>`. Never for a decision or a question, for what you can do yourself, or as a checklist or status update; most messages need none. Done or Can\'t do it comes back under "Action items you asked for".',
438
454
  '',
@@ -444,7 +460,7 @@ export const INBOX_AGENT_FIELD_DESCRIPTIONS: Readonly<Record<string, Readonly<Re
444
460
  thread: "A short thread name, one line, to keep separate work apart: the same name in this project is the same open thread, across sessions. Without it, your messages join your session's open thread. Ignored with reply_to, except when you are asked to name a thread the person started: then it names that thread, once.",
445
461
  supersedes: 'Your own questions in this thread that are still open (read_thread state open or picked) and no longer apply: a newer message replaces them, or the reply settled them in words. "<message id>/<key>" for one, "<message id>" for all of that message\'s; send with reply_to into their thread. With one question in this message it replaces them, otherwise they read "No longer applies".',
446
462
  done: 'Your own open action items (`:::action`) in this thread that you saw done yourself, e.g. the deploy you watch is Ready: "<message id>/<key>" (key a-…); send with reply_to into their thread. The card says you saw it done; nothing is sent to the person.',
447
- attachments: 'Files to show the person beside the message, absolute or relative to project_path: a plan, a screenshot, an HTML mock, code, a video, any file. For something visual, a screenshot or image usually reads fastest; attach the HTML or source too when they should open or click through it. Link one from the body (`[the mock](mock.html)`) to open it beside the thread. Their comments on the files come back in the reply. Folders and .env files are refused.',
463
+ attachments: `Files to show the person beside the message, absolute or relative to project_path: a plan, an HTML mock, code, a video, any file. ${INBOX_ATTACHMENTS_PICTURES_LINE} Link one from the body (\`[the mock](mock.html)\`) to open it beside the thread. Their comments on the files come back in the reply. Folders and .env files are refused.`,
448
464
  },
449
465
  read_thread: {
450
466
  thread_id: 'A thread id. read_thread shows its open questions, the newest messages in full, and earlier ones one line each.',
package/inbox.ts CHANGED
@@ -186,6 +186,23 @@ function userText(message: { content?: unknown } | undefined): string {
186
186
  .join("");
187
187
  }
188
188
 
189
+ /**
190
+ * PLANNOTATOR_SESSION_PROJECT (apps/hook/server/inbox-mcp-shim.ts,
191
+ * `inboxSessionProjectFromEnv`): `pi:<session id>:<ctx.cwd>`, the folder this
192
+ * session's `plannotator_inbox` tool files under. Pi gives each bash call this
193
+ * process's environment plus PI_SESSION_ID, so a `cd sub && plannotator inbox
194
+ * call …` or `plannotator card …` lands in the tool's project too. Set before
195
+ * every bash call, so a new or resumed session's id is the one named. A stale
196
+ * ctx names nothing new.
197
+ */
198
+ export function exportPiSessionProject(env: Record<string, string | undefined>, ctx: Pick<ExtensionContext, "cwd" | "sessionManager">): void {
199
+ try {
200
+ env.PLANNOTATOR_SESSION_PROJECT = `${PI_INBOX_HOST}:${ctx.sessionManager.getSessionId()}:${ctx.cwd}`;
201
+ } catch {
202
+ // A stale ctx: the last value stands, and it names its own session.
203
+ }
204
+ }
205
+
189
206
  function registerPiInbox(pi: InboxPi, connection: InboxAgentConnection, options: PiInboxOptions & { reminder: string | null }): void {
190
207
  const spec = inboxAgentTool(connection.tools);
191
208
  const ohMyPi = detectPiHost(pi) === "oh-my-pi";
@@ -324,6 +341,11 @@ function registerPiInbox(pi: InboxPi, connection: InboxAgentConnection, options:
324
341
  },
325
342
  };
326
343
 
344
+ // Before each bash call: the session's project for the commands it runs (exportPiSessionProject).
345
+ pi.on("tool_call", (event: { toolName?: unknown } | undefined, callCtx: ExtensionContext) => {
346
+ if (event?.toolName === "bash") exportPiSessionProject(process.env, callCtx);
347
+ });
348
+
327
349
  pi.on("session_start", (_event, startCtx: ExtensionContext) => {
328
350
  ctx = startCtx;
329
351
  if (!activationSettled) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/pi-extension",
3
- "version": "0.28.45",
3
+ "version": "0.28.47",
4
4
  "type": "module",
5
5
  "description": "Plannotator Pi extension - interactive plan review with annotations, annotate agent messages, and review code/PRs",
6
6
  "author": "backnotprop",