@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 +67 -0
- package/CONTRIBUTING.md +13 -4
- package/README.md +22 -14
- package/constants/abbreviations.ts +6 -0
- package/constants/stopwords.ts +42 -0
- package/dist/index.html +413 -413
- package/index.ts +171 -52
- package/package.json +4 -2
- package/shared/ask-user.ts +6 -0
- package/tool/ask-user.ts +39 -291
- package/tool/response-formatter.ts +13 -9
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
|
|
24
|
-
npm run validate:gui
|
|
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
|
|
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
|
|
35
|
-
- **Freeform** — textarea input
|
|
36
|
-
- **Questionnaire** — cards in a vertical list for structured questions,
|
|
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
|
|
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
|
|
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
|
|
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** —
|
|
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** —
|
|
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
|
+
]);
|