@alexleekt/pi-ask-user-glimpse 0.3.1 → 0.4.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,73 @@
2
2
 
3
3
  All notable changes to `@alexleekt/pi-ask-user-glimpse` are documented in this file.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ### Added
8
+ - **Agent preamble capture** — When the agent writes an introductory message before calling `ask_user`, that text is now automatically captured and prepended to the context panel. The extension finds the most recent assistant journal entry, extracts its text content, and appends it to the dialog's left panel (separated by a horizontal rule from any explicit `context` provided by the agent). This ensures the user sees the full reasoning that led to the question, not just the question itself.
9
+
10
+ ### Fixed
11
+ - **Markdown in question header** — The `question` field is now rendered through `marked` so inline markdown (bold `**`, italic `*`, code `` ` ``, links) displays correctly instead of showing raw escape characters. The HTML is sanitized with the same defense-in-depth sanitizer used by the context panel. Extracted shared `sanitizeHtml()`, `renderMarkdown()`, and `renderMarkdownInline()` to `webview/src/util/markdown.ts`.
12
+
13
+ ## [0.4.1] — 2026-05-20
14
+
15
+ ### Security
16
+ - **Comprehensive HTML sanitization** — `ContextPanel.sanitizeHtml()` now blocks `img`, `iframe`, `object`, `embed`, `form`, `input`, `style`, `link`, `svg`, `math`, `meta`, `base`, `noscript`, `template`, `portal`, `frame`, `frameset` tags, plus `javascript:` and `data:` URLs in `href`/`src`/`action` attributes.
17
+ - **XSS-safe search highlighting** — `highlightMatch()` in new `webview/src/util/html.ts` escapes both display text and query strings before wrapping matches in `<mark>`. Replaces raw `.replace()` in `SingleSelect` and `MultiSelect` that was vulnerable to search query injection.
18
+
19
+ ### Changed
20
+ - **Prominent question header** — Removed sparkle icon and "Ask User" branding. The header now shows the full non-truncated question text in `text-base` font, wrapping naturally.
21
+ - **50/50 panel split** — Default context/options panel width changed from 40/60 to 50/50.
22
+ - **Invisible splitter track** — Removed grey divider bar. Only a centered grip handle is visible (`w-1` by default, `w-1.5` on drag). Handle sits exactly at the panel boundary.
23
+ - **Instant drag feedback** — Removed CSS transition from panel width so resize is immediate, not animated.
24
+ - **Double-click to collapse** — Double-clicking the splitter toggles the context panel between 50% width and fully collapsed.
25
+ - **Click-when-collapsed to expand** — If the context panel is collapsed, clicking the splitter expands it back to 50% without starting a drag.
26
+ - **Hover-only scrollbars** — Scrollbars are hidden by default and appear as thin 6px tracks on hover (macOS overlay style). Applied to the context panel.
27
+ - **Theme persistence across all entry points** — Extracted shared helpers `enrichWithThemeSettings()`, `createThemeSaver()`, and `runAskUserWithTheme()` in `index.ts`. All three entry points (`ask_user` tool, `/ask`, `/ask-debug`) now share identical theme read/save behavior.
28
+ - **Type-safe theme settings** — `getThemeSettings()` now validates stored strings against `ThemeMode`/`AnimationLevel` union types before returning.
29
+ - **Refactored constants** — Extracted `STOPWORDS` (~200 words) and `PROTECTED_ABBREVIATIONS` to `constants/stopwords.ts` and `constants/abbreviations.ts`.
30
+ - **Fully controlled AdditionalComments** — Removed half-controlled anti-pattern. Component now requires both `value` and `onChange` props.
31
+ - **Command rename** — `/ask-last` → `/ask` (shorter, more intuitive).
32
+
33
+ ### Fixed
34
+ - **Mermaid rendering errors** — Added explicit `mermaid.initialize()` with `startOnLoad: false`. Defers `mermaid.run()` to `requestAnimationFrame` so the DOM is fully committed first. Errors now log via `console.warn` instead of being silently swallowed.
35
+ - **Stale closure in keydown handlers** — SingleSelect and MultiSelect now use a `stateRef` pattern: all mutable state is snapshotted into a ref, and the global keydown listener has stable dependencies (`useCallback` for handlers).
36
+ - **Short questions dropped** — `extractQuestions()` length threshold lowered from 10 to 3 characters so legitimate questions like "Why?" are not silently discarded.
37
+ - **ARIA on splitter** — Added `role="separator"`, `aria-orientation="vertical"`, `aria-valuenow/min/max`.
38
+ - **Option ref mutation** — Removed direct `optionRefs.current = []` mutation; uses `requestAnimationFrame` for focus timing instead.
39
+ - **Consistent sendCancelled references** — All cancel buttons now pass `sendCancelled` directly instead of arrow wrappers.
40
+
41
+ ### Added
42
+ - **`npm run test:with-context`** — New script that opens a WebView with the context panel, splitter, and Mermaid diagrams for visual testing.
43
+ - **`webview/src/util/html.ts`** — Shared HTML utilities: `escapeHtml()` and `highlightMatch()`.
44
+
45
+ ## [0.4.0] — 2026-05-20
46
+
47
+ ### Added
48
+ - **Branded header bar** — A thin branded bar at the top of every dialog with a sparkle icon + "Ask User" label on the left, and a settings cog + keyboard-shortcuts help on the right.
49
+ - **Theme toggle (dark / light / system)** — Settings dropdown lets users switch between dark mode, light mode, or following the OS preference. Choice is persisted in the webview's localStorage.
50
+ - **Animation level toggle (none / minimal / all)** — Settings dropdown also controls animation intensity. "None" disables all transitions; "minimal" keeps only essential ones; "all" enables full polish.
51
+ - **Consistent Cmd+Enter submit** — All four dialog types (single-select, multi-select, questionnaire, freeform) now support Cmd+Enter (macOS) / Ctrl+Enter (other) to submit the answer. Footer hints updated accordingly.
52
+ - **Window titles with session name** — Titles now read "Pi · {sessionName} · {question}" when a session name is set, making it easier to identify which conversation a dialog belongs to.
53
+ - **Character counter** — Freeform textareas and questionnaire freeform fields show a live `0/2000` or `0/1000` counter. Turns red when approaching the limit.
54
+ - **Required field badge** — In questionnaire mode, when `allowSkip: false`, unanswered questions show a red "Required" badge and a subtle red border until answered.
55
+ - **Search highlight** — When filtering options via the search box, matching text in option titles and descriptions is highlighted with a yellow background.
56
+ - **Quick-select all/none** — Multi-select dialogs show "Select all" and "Select none" links above the option list (when not actively searching).
57
+ - **Keyboard shortcuts legend** — A `?` button in the header bar opens a modal showing all available keyboard shortcuts.
58
+
59
+ ### Changed
60
+ - **CSS dark mode strategy** — Switched from `prefers-color-scheme` media query to Tailwind's `darkMode: 'class'` strategy, enabling explicit theme toggling independent of OS setting.
61
+ - **Theme metadata in results** — The webview sends back the active theme and animation level with every result, so the extension can persist preferences across sessions.
62
+
63
+ ### Fixed
64
+ - **White screen on /ask-debug** — The Glimpse native webview (WKWebView) blocks `localStorage` access with `SecurityError: The operation is insecure.` because `loadHTMLString(baseURL: nil)` gives the page no origin. Removed all `localStorage` usage from the webview entirely. Theme and animation state now flows through the payload: the extension reads stored settings from Pi journal entries, passes them into the webview via `AskUserPayload`, and the webview sends back the user's choices via the result's `__theme`/`__animationLevel` fields. The extension then persists them back into journal entries.
65
+ - **Top-level ErrorBoundary** — Wrapped the entire React app in an `ErrorBoundary` so future render crashes show a readable error message instead of an empty white screen.
66
+
67
+ ## [0.3.2] — 2026-05-20
68
+
69
+ ### Fixed
70
+ - **Recover all fixes lost during cherry-pick** — When cherry-picking the mermaid commit onto main, the jj colocated working copy was silently reset, discarding every other fix. This release restores: empty submit, shared icons/components, additional comments, auto-split logic, questionnaire focus fix, and updated prompt guidelines.
71
+
5
72
  ## [0.3.1] — 2026-05-20
6
73
 
7
74
  ### Fixed
package/CONTRIBUTING.md CHANGED
@@ -20,8 +20,9 @@ npm run check # dry-run npm pack
20
20
  ## Testing
21
21
 
22
22
  ```bash
23
- npm run validate # checks dist exists, placeholder present, binary found
24
- npm run validate:gui # same + opens actual WebView for visual check
23
+ npm run validate # checks dist exists, placeholder present, binary found
24
+ npm run validate:gui # same + opens actual WebView for visual check
25
+ npm run test:with-context # opens WebView with context panel + resizable splitter
25
26
  npx tsx scripts/smoke-test.ts # opens WebView for 2s
26
27
  npx tsx scripts/visual-qa.ts # cycles through all 5 scenarios
27
28
  ```
@@ -33,8 +34,9 @@ npx tsx scripts/visual-qa.ts # cycles through all 5 scenarios
33
34
  - **Console output:** Use `[pi-ask-user-glimpse]` prefix for all `console.warn`/`console.error`
34
35
  - **Peer deps:** Only list `@earendil-works/pi-coding-agent` and `@earendil-works/pi-ai` in `peerDependencies`
35
36
 
36
- ## Security Note
37
+ ## Security Notes
37
38
 
39
+ ### HTML escaping in payload injection
38
40
  If you modify payload injection or test scripts, ensure HTML escaping matches production:
39
41
  ```ts
40
42
  JSON.stringify(payload)
@@ -43,9 +45,16 @@ JSON.stringify(payload)
43
45
  .replace(/&/g, "\\u0026")
44
46
  ```
45
47
 
48
+ ### XSS prevention in search highlighting
49
+ `highlightMatch()` in `webview/src/util/html.ts` must escape both display text and search query before producing HTML. Never pass raw user input into `dangerouslySetInnerHTML`.
50
+
51
+ ### ContextPanel sanitization
52
+ `sanitizeHtml()` blocks dangerous tags (`script`, `img`, `iframe`, `object`, `embed`, `form`, `svg`, etc.) and strips `javascript:` / `data:` URLs. Audit the sanitizer when adding new rich content support.
53
+
46
54
  ## Before Submitting
47
55
 
48
56
  - [ ] `npm run build` passes
49
57
  - [ ] `npx tsc --noEmit` passes
50
- - [ ] `npm run check` shows the expected files
58
+ - [ ] `npm run check` shows the expected files (including `constants/`)
51
59
  - [ ] `npm run validate` passes
60
+ - [ ] `npm run test:with-context` passes
package/README.md CHANGED
@@ -30,10 +30,14 @@ The agent gets a clean selection back. You get a decision made in seconds, not m
30
30
 
31
31
  ## Features
32
32
 
33
- - **Single-select** — searchable option list with inline descriptions
34
- - **Multi-select** — checkbox-style selection with submit/cancel
35
- - **Freeform** — textarea input for open-ended responses
36
- - **Questionnaire** — cards in a vertical list for structured questions, each with its own options
33
+ - **Single-select** — searchable option list with inline descriptions and search highlight
34
+ - **Multi-select** — checkbox-style selection with quick-select all/none links
35
+ - **Freeform** — textarea input with live character counter and platform-aware keyboard shortcuts
36
+ - **Questionnaire** — cards in a vertical list for structured questions, with required-field badges and per-question character counters
37
+ - **Theme toggle** — dark / light / system mode switcher in the dialog header
38
+ - **Animation levels** — none / minimal / all, controlling transition intensity across the UI
39
+ - **Keyboard shortcuts legend** — press `?` in the header bar to see all available shortcuts
40
+ - **Prominent question header** — full non-truncated question text in the header bar, with settings cog and keyboard-shortcuts help
37
41
  - **Native WebView** — renders in a real window (macOS WKWebView / Linux GTK4 / Windows WebView2)
38
42
  - **Terminal fallback** — gracefully degrades to TUI prompts when glimpseui is unavailable
39
43
 
@@ -67,7 +71,7 @@ Ask the user to pick exactly one option:
67
71
  }
68
72
  ```
69
73
 
70
- The dialog shows a full-width question header and a two-panel layout. When `context` is provided, the left panel renders it as markdown for reference while the right panel shows the searchable option list. Option descriptions appear inline below each title. The "Custom" button under the search box lets the user submit a freeform answer.
74
+ The dialog shows the full question in the header bar, and a two-panel layout when `context` is provided. The left panel renders context as markdown (with Mermaid diagram support) while the right panel shows the searchable option list. The panels are resizable via a drag handle on the boundary — double-click the handle to collapse the context panel. Option descriptions appear inline below each title, and matching text is highlighted when searching. The "Custom" button lets the user submit a freeform answer. Use ⌘+Enter (macOS) or Ctrl+Enter to submit.
71
75
 
72
76
  ### Multi Select
73
77
 
@@ -88,7 +92,7 @@ Ask the user to pick multiple options:
88
92
  }
89
93
  ```
90
94
 
91
- Each option has a checkbox. A "Clear all" link resets selections. Submit is disabled until at least one item is selected.
95
+ Each option has a checkbox. "Select all" and "Select none" links appear above the list (when not searching). A "Clear all" link resets selections. Submit is disabled until at least one item is selected. Use ⌘+Enter (macOS) or Ctrl+Enter to submit.
92
96
 
93
97
  ### Freeform
94
98
 
@@ -102,7 +106,7 @@ Ask an open-ended question with no predefined options:
102
106
  }
103
107
  ```
104
108
 
105
- Shows a full-height textarea with platform-aware keyboard hints (⌘+Enter on macOS, Ctrl+Enter elsewhere). Submit is disabled until text is entered.
109
+ Shows a full-height textarea with a live character counter and platform-aware keyboard hints (⌘+Enter on macOS, Ctrl+Enter elsewhere). Submit is disabled until text is entered.
106
110
 
107
111
  ### Questionnaire
108
112
 
@@ -140,7 +144,7 @@ Ask multiple structured questions in one dialog:
140
144
  }
141
145
  ```
142
146
 
143
- Each question is shown as a card with a progress bar at the top. Questions with `options` render as single-select (radio) or multi-select (checkbox) depending on `allowMultiple`. Questions without `options` render as a textarea. The dialog auto-scrolls to the first unanswered question on open. The comment button shows "Edit comment" when text exists. Submit is disabled until all questions have a non-empty answer, unless `allowSkip: true` is set.
147
+ Each question is shown as a card with a progress bar at the top. Questions with `options` render as single-select (radio) or multi-select (checkbox) depending on `allowMultiple`. Questions without `options` render as a textarea with a character counter. The dialog auto-scrolls to the first unanswered question on open. When `allowSkip: false`, unanswered questions show a red "Required" badge. The comment button shows "Edit comment" when text exists. Submit is disabled until all questions have a non-empty answer, unless `allowSkip: true` is set. Use ⌘+Enter (macOS) or Ctrl+Enter to submit.
144
148
 
145
149
  ### Parameters
146
150
 
@@ -223,12 +227,12 @@ The setting is persisted in the session and survives restarts.
223
227
 
224
228
  The injected mandate is ~100 tokens. It is only appended when detection triggers, so normal conversations pay nothing extra.
225
229
 
226
- ## Slash Command: `/ask-last`
230
+ ## Slash Command: `/ask`
227
231
 
228
232
  When the assistant writes a question as free-form text (bypassing `ask_user`), use this command to retroactively open the rich dialog:
229
233
 
230
234
  ```
231
- /ask-last
235
+ /ask
232
236
  ```
233
237
 
234
238
  ### How it works
@@ -256,22 +260,26 @@ Options: `single-select`, `multi-select`, `freeform`, `questionnaire`. The resul
256
260
 
257
261
  ## Window Behavior
258
262
 
259
- - **Title bar** — shows a condensed version of the question text (up to 3 content words)
263
+ - **Title bar** — reads "Pi · {sessionName} · {question}" (session name is included when set)
260
264
  - **Centered dialog** — normal stacking, not floating
261
265
  - **Size** — 1200×900 by default
266
+ - **Context panel** — 50/50 split by default; drag the handle to resize, double-click to collapse
267
+ - **Scrollbars** — hidden by default, appear on hover (macOS-style overlay)
262
268
  - **Cursor follow** — off by default; enable with `followCursor: true`
263
- - **Dark mode** — automatically follows the system `prefers-color-scheme` setting
269
+ - **Dark mode** — togglable via the settings cog: dark, light, or system (follows OS preference)
270
+ - **Theme persistence** — theme and animation choices survive across dialogs and session restarts
264
271
 
265
272
  ## Architecture
266
273
 
267
274
  ```
268
275
  index.ts → Pi extension entrypoint (tool + command registration)
276
+ constants/ → STOPWORDS, PROTECTED_ABBREVIATIONS
269
277
  tool/ask-user.ts → constructs payload, injects into HTML, calls glimpseui.prompt()
270
278
  tool/response-formatter.ts → normalizes WebView response for Pi
271
- webview/src/components/ → SingleSelect, MultiSelect, Questionnaire, Freeform, ContextPanel, ErrorBoundary
272
279
  fallback/terminal-prompt.ts → readline fallback when WebView unavailable
273
280
  webview/ → Vite + React + Tailwind app
274
- src/components/ → SingleSelect, MultiSelect, Questionnaire, Freeform
281
+ src/components/ → SingleSelect, MultiSelect, Questionnaire, Freeform, ContextPanel, ErrorBoundary, HeaderBar, ShortcutsModal, AdditionalComments
282
+ src/util/ → settings.tsx (theme/animation context), glimpse.ts (host bridge), platform.ts (modKey), html.ts (escapeHtml + highlightMatch)
275
283
  dist/index.html → single-file bundle (inlined JS + CSS)
276
284
  ```
277
285
 
@@ -0,0 +1,6 @@
1
+ /** Protected abbreviations for sentence splitting. */
2
+ export const PROTECTED_ABBREVIATIONS = new Set([
3
+ "etc", "vs", "fig", "dr", "mr", "mrs", "ms", "prof", "jr", "sr",
4
+ "inc", "ltd", "corp", "co", "llc", "al", "et", "vol", "vols",
5
+ "pg", "pp", "ch", "chap", "sec", "secs",
6
+ ]);
@@ -0,0 +1,42 @@
1
+ /** ~200 common English stopwords for title extraction. */
2
+ export const STOPWORDS = new Set([
3
+ "a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
4
+ "have", "has", "had", "do", "does", "did", "will", "would", "could",
5
+ "should", "may", "might", "must", "shall", "can", "need", "ought",
6
+ "used", "to", "of", "in", "for", "on", "with", "at", "by", "from",
7
+ "as", "into", "through", "during", "before", "after", "above",
8
+ "below", "between", "under", "again", "further", "then", "once",
9
+ "here", "there", "when", "where", "why", "how", "all", "each",
10
+ "few", "more", "most", "other", "some", "such", "no", "nor", "not",
11
+ "only", "own", "same", "so", "than", "too", "very", "just", "and",
12
+ "but", "if", "or", "because", "until", "while", "which", "what",
13
+ "who", "whom", "this", "that", "these", "those", "am", "it", "its",
14
+ "we", "our", "you", "your", "they", "their", "them", "he", "him",
15
+ "his", "she", "her", "i", "me", "my", "mine", "us", "any", "both",
16
+ "either", "neither", "one", "two", "first", "last", "another",
17
+ "every", "many", "much", "several", "let", "new", "use", "using",
18
+ "make", "made", "get", "got", "go", "going", "want", "wanted", "like",
19
+ "liked", "know", "knew", "known", "think", "thought", "see", "saw",
20
+ "seen", "come", "came", "give", "gave", "given", "take", "took",
21
+ "taken", "find", "found", "say", "said", "tell", "told", "ask",
22
+ "asked", "work", "worked", "seem", "seemed", "feel", "felt", "try",
23
+ "tried", "leave", "left", "call", "called", "good", "well", "better",
24
+ "best", "bad", "worse", "worst", "old", "long", "great", "little",
25
+ "right", "left", "big", "high", "different", "important", "same",
26
+ "able", "next", "early", "young", "public", "free", "real", "easy",
27
+ "clear", "recent", "local", "social", "full", "small", "large",
28
+ "possible", "particular", "available", "special", "certain", "personal",
29
+ "open", "general", "enough", "probably", "actually", "especially",
30
+ "finally", "usually", "perhaps", "almost", "simply", "quickly",
31
+ "recently", "already", "eventually", "suddenly", "certainly",
32
+ "definitely", "absolutely", "completely", "totally", "entirely",
33
+ "exactly", "specifically", "particularly", "especially", "mainly",
34
+ "mostly", "partly", "fully", "nearly", "quite", "rather", "pretty",
35
+ "fairly", "really", "even", "still", "yet", "ever", "never", "always",
36
+ "sometimes", "often", "usually", "frequently", "rarely", "generally",
37
+ "typically", "normally", "largely", "potentially", "theoretically",
38
+ "practically", "basically", "essentially", "fundamentally",
39
+ "primarily", "chiefly", "principally", "partially", "half", "quarter",
40
+ "double", "single", "multiple", "various", "hundred", "thousand",
41
+ "million", "billion",
42
+ ]);