···65656666Profile data stored in `{userData}/{PROFILE}/` directory.
67676868+## Window API
6969+7070+### Opening Windows
7171+7272+```javascript
7373+import windows from './windows.js';
7474+7575+// Modal window (closes on blur/ESC)
7676+windows.openModalWindow(url, options);
7777+7878+// Regular window
7979+windows.createWindow(url, options);
8080+```
8181+8282+### Window Options
8383+8484+| Option | Type | Description |
8585+|--------|------|-------------|
8686+| `width`, `height` | number | Window dimensions |
8787+| `x`, `y` | number | Window position |
8888+| `modal` | boolean | Frameless, closes on blur |
8989+| `key` | string | Unique ID for window reuse |
9090+| `keepLive` | boolean | Hide instead of close |
9191+| `escapeMode` | string | ESC behavior: `'close'`, `'navigate'`, `'auto'` |
9292+| `trackingSource` | string | Source for visit tracking |
9393+9494+### Escape Handling
9595+9696+Windows can control how ESC behaves using `escapeMode`:
9797+9898+- **`'close'`** (default): ESC immediately closes/hides the window
9999+- **`'navigate'`**: Renderer handles ESC first; closes only when renderer signals root state
100100+- **`'auto'`**: Transient windows (opened via hotkey from another app) close immediately; active windows use navigate behavior
101101+102102+#### Using escapeMode: 'navigate'
103103+104104+1. Open window with `escapeMode: 'navigate'`:
105105+```javascript
106106+windows.createWindow(url, { escapeMode: 'navigate' });
107107+```
108108+109109+2. Register escape handler in renderer:
110110+```javascript
111111+api.escape.onEscape(() => {
112112+ if (canNavigateBack) {
113113+ navigateBack();
114114+ return { handled: true }; // ESC was handled internally
115115+ }
116116+ return { handled: false }; // At root, let window close
117117+});
118118+```
119119+120120+See `notes/escape-navigation.md` for full design details.
121121+68122## App Icon Generation
6912370124The macOS app icon is generated from a source PNG using ImageMagick. The process applies rounded corners and adds padding to match macOS icon guidelines.
+13-5
TODO.md
···30303131- [ ] App cmds (eg Editor -> cmd to edit note X)
32323333-3433## v? Portability
35343635- [ ] Common background runtime
···5958- [x] Untagged -> default group
6059- [x] Cmd to open groups home
61606262-- [ ] What does it mean for a group to be active/not
6363-- [ ] Open new empty page (in a group or not)
6161+- [ ] Use escapeMode to open pages "in an active group" (maybe depends on page metadata api?)
6262+- [ ] Escape for navigating back up the group views, not closing window
6363+- [ ] Be able to open new empty page (in a group or not)
64646565Cmd
6666-- [ ] adaptive matching
6767-- [ ] frecency
6666+- [x] adaptive matching
6767+- [x] frecency
6868+6969+## v? Windowing model
7070+7171+- [ ] active vs transient modality
7272+- [ ] configurable escape behavior per-window
68736974## V.0.3 - Datastore
7575+7676+this needs a lot of work, but good enough for now.
7777+also, will be shaped as we move through the extensibility pieces.
70787179- [x] Datastore
7280
···3232 untaggedCount: 0
3333};
34343535-// Handle ESC - go back to groups view
3636-document.onkeydown = (evt) => {
3737- if (evt.key === 'Escape') {
3838- if (state.view === VIEW_ADDRESSES) {
3939- showGroups();
4040- }
3535+// Handle ESC - cooperative escape handling with window manager
3636+// Returns { handled: true } if we navigated internally
3737+// Returns { handled: false } if at root (groups list) and window should close
3838+api.escape.onEscape(() => {
3939+ if (state.view === VIEW_ADDRESSES) {
4040+ // Navigate back to groups list
4141+ showGroups();
4242+ return { handled: true };
4143 }
4242-};
4444+ // At root (groups list) - let window close
4545+ return { handled: false };
4646+});
43474448const init = async () => {
4549 debug && console.log('Groups init');
+65-17
index.js
···919919 try {
920920 await win.loadURL(url);
921921922922+ // Determine if this is a transient window (opened while no Peek window was focused)
923923+ // Used for escapeMode: 'auto' to decide between navigate and close behavior
924924+ const focusedWindow = BrowserWindow.getFocusedWindow();
925925+ const isTransient = !focusedWindow || focusedWindow.isDestroyed();
926926+922927 // Add to window manager with modal parameter
923928 const windowEntry = {
924929 id: win.id,
925930 source: msg.source,
926931 params: {
927932 ...options,
928928- address: url
933933+ address: url,
934934+ transient: isTransient
929935 }
930936 };
931937 console.log('Adding window to manager:', windowEntry.id, 'modal:', windowEntry.params.modal, 'keepLive:', windowEntry.params.keepLive);
···18101816 }
18111817};
1812181818191819+// Ask renderer to handle escape, returns Promise<{ handled: boolean }>
18201820+const askRendererToHandleEscape = (bw) => {
18211821+ return new Promise((resolve) => {
18221822+ const responseChannel = `escape-response-${bw.id}-${Date.now()}`;
18231823+18241824+ // Timeout after 100ms - if renderer doesn't respond, assume not handled
18251825+ const timeout = setTimeout(() => {
18261826+ ipcMain.removeAllListeners(responseChannel);
18271827+ resolve({ handled: false });
18281828+ }, 100);
18291829+18301830+ ipcMain.once(responseChannel, (event, response) => {
18311831+ clearTimeout(timeout);
18321832+ resolve(response || { handled: false });
18331833+ });
18341834+18351835+ bw.webContents.send('escape-pressed', { responseChannel });
18361836+ });
18371837+};
18381838+18131839// esc handler
18141814-// TODO: make user-configurable
18401840+// Supports escapeMode: 'close' (default), 'navigate', 'auto'
18151841const addEscHandler = bw => {
18161842 console.log('adding esc handler to window:', bw.id);
18171817- bw.webContents.on('before-input-event', (e, i) => {
18431843+ bw.webContents.on('before-input-event', async (e, i) => {
18181844 if (i.key == 'Escape' && i.type == 'keyUp') {
18191819- // Get window info for better logging
18451845+ // Get window info
18201846 const entry = windowManager.getWindow(bw.id);
18211821- const isSettingsWindow = entry && entry.params && entry.params.address === settingsAddress;
18221822-18231823- console.log('===== Escape key pressed =====');
18241824- console.log(`Window ID: ${bw.id}`);
18251825- console.log(`Is settings window: ${isSettingsWindow}`);
18261826-18271827- if (entry && entry.params) {
18281828- console.log(`Window address: ${entry.params.address}`);
18291829- console.log(`Modal: ${entry.params.modal}, KeepLive: ${entry.params.keepLive}`);
18471847+ const params = entry?.params || {};
18481848+ const escapeMode = params.escapeMode || 'close';
18491849+18501850+ console.log(`ESC pressed - window ${bw.id}, escapeMode: ${escapeMode}`);
18511851+18521852+ // For 'navigate' mode, ask renderer first
18531853+ if (escapeMode === 'navigate') {
18541854+ const response = await askRendererToHandleEscape(bw);
18551855+ console.log(`Renderer escape response:`, response);
18561856+18571857+ if (response.handled) {
18581858+ // Renderer handled the escape (internal navigation)
18591859+ console.log('Renderer handled escape, not closing');
18601860+ return;
18611861+ }
18301862 }
18311831-18321832- // Always trigger close/hide on Escape
18331833- console.log('Calling closeOrHideWindow...');
18631863+18641864+ // For 'auto' mode, check if transient (no focused window when opened)
18651865+ if (escapeMode === 'auto') {
18661866+ if (params.transient) {
18671867+ // Transient mode - close immediately
18681868+ console.log('Auto mode (transient) - closing');
18691869+ } else {
18701870+ // Active mode - ask renderer first
18711871+ const response = await askRendererToHandleEscape(bw);
18721872+ console.log(`Renderer escape response (auto/active):`, response);
18731873+18741874+ if (response.handled) {
18751875+ console.log('Renderer handled escape, not closing');
18761876+ return;
18771877+ }
18781878+ }
18791879+ }
18801880+18811881+ // Close or hide the window
18821882+ console.log('Closing/hiding window');
18341883 closeOrHideWindow(bw.id);
18351835- console.log('===== Escape handling complete =====');
18361884 }
18371885 });
18381886};
+44
notes/commands.md
···11+# Commands
22+33+The cmd feature provides a command palette for executing actions, opening URLs, and interacting with the app.
44+55+## Command Matching & Sorting
66+77+When the user types in the command bar, matching commands are sorted using a three-tier system:
88+99+### 1. Exact Match Priority
1010+1111+When the input contains parameters (e.g., `tag foo`), an exact command match is prioritized over prefix matches. This ensures `tag foo` executes the `tag` command, not `tags`.
1212+1313+### 2. Adaptive Scoring
1414+1515+The system learns from user behavior using asymptotic scoring:
1616+1717+```
1818+score = count / (count + k)
1919+```
2020+2121+Where:
2222+- `count` = number of times user selected this command for this typed prefix
2323+- `k` = dampening constant (currently 5)
2424+2525+This creates ever-strengthening reinforcement: the more you select a command for a given input, the higher it ranks. The asymptotic curve means early selections have high impact, then it plateaus.
2626+2727+Example: If you type "t" and select "tag" 10 times, the score is `10/(10+5) = 0.67`. After 100 selections: `100/(100+5) = 0.95`.
2828+2929+Adaptive feedback is stored in localStorage per typed-prefix → command pairs.
3030+3131+### 3. Match Count (Frecency Fallback)
3232+3333+If adaptive scores are equal, falls back to raw match count—how many times each command has been used overall.
3434+3535+## Implementation
3636+3737+- `findMatchingCommands(text)` in `panel.js` handles matching and sorting
3838+- `updateAdaptiveFeedback(typed, name)` records user selections
3939+- `getAdaptiveScore(typed, name)` retrieves learned scores
4040+4141+## Storage Keys
4242+4343+- `cmd:adaptiveFeedback` - typed prefix → command selection counts
4444+- `cmd:matchCounts` - overall command usage counts
+93
notes/escape-navigation.md
···11+# Escape Navigation & Window Modes
22+33+## Modes
44+55+### Active Mode
66+Peek is the focused app at the OS level. User is actively working within Peek.
77+88+**Escape behavior**: Navigate back through internal state before closing.
99+1010+Example flow in Groups:
1111+1. Groups list view
1212+2. → Click group → Group detail view
1313+3. → Click page → Page view
1414+4. → ESC → Back to group detail
1515+5. → ESC → Back to groups list
1616+6. → ESC → Close groups window
1717+1818+### Transient Mode
1919+Peek was invoked via global hotkey while another app was active. User wants quick access then return to previous context.
2020+2121+**Escape behavior**: Close window immediately, return focus to previous app.
2222+2323+## Detection
2424+2525+How to determine mode:
2626+- Track whether Peek was active before window opened
2727+- If invoked via global shortcut while Peek wasn't focused → transient
2828+- If opened from within an existing Peek window → active
2929+3030+## API Changes
3131+3232+Window open API could accept an `escapeMode` option:
3333+3434+```javascript
3535+api.window.open(url, {
3636+ escapeMode: 'navigate' | 'close' | 'auto'
3737+});
3838+```
3939+4040+- `navigate`: ESC navigates back, only closes when at root
4141+- `close`: ESC immediately closes window
4242+- `auto`: Determine based on active vs transient mode (default)
4343+4444+## Implementation
4545+4646+Cooperative model using IPC between main process and renderer:
4747+4848+### Main Process (index.js)
4949+5050+1. `addEscHandler()` intercepts ESC via `before-input-event`
5151+2. For `escapeMode: 'navigate'` or `'auto'` (active), sends IPC `escape-pressed` to renderer
5252+3. Waits 100ms for response via unique response channel
5353+4. If renderer returns `{ handled: true }` → do nothing (renderer navigated)
5454+5. If renderer returns `{ handled: false }` or timeout → close/hide window
5555+5656+### Preload API (preload.js)
5757+5858+```javascript
5959+api.escape.onEscape(callback)
6060+```
6161+6262+Register a callback that's invoked on ESC. Callback should return:
6363+- `{ handled: true }` - ESC was handled (internal navigation occurred)
6464+- `{ handled: false }` - At root state, window should close
6565+6666+### Renderer Usage (e.g., groups/home.js)
6767+6868+```javascript
6969+api.escape.onEscape(() => {
7070+ if (state.view === VIEW_ADDRESSES) {
7171+ showGroups();
7272+ return { handled: true };
7373+ }
7474+ return { handled: false };
7575+});
7676+```
7777+7878+### Transient Detection
7979+8080+When a window is opened, main process checks `BrowserWindow.getFocusedWindow()`:
8181+- If no focused window → `transient: true` stored in window params
8282+- For `escapeMode: 'auto'`, transient windows close immediately on ESC
8383+8484+## Future: Pinned Windows
8585+8686+Exception case: windows pinned to stay visible over all OS windows. These would:
8787+- Ignore transient mode
8888+- Have their own escape behavior (perhaps require explicit close action)
8989+9090+## Open Questions
9191+9292+1. How does this interact with browser-style back/forward within webviews?
9393+2. Should there be visual indication of mode (transient vs active)?
+18
preload.js
···284284 ipcRenderer.send('app-quit', { source: sourceAddress });
285285};
286286287287+// Escape handling API
288288+// For windows with escapeMode: 'navigate' or 'auto'
289289+// Callback should return { handled: true } if escape was handled internally
290290+// or { handled: false } to let the window close
291291+api.escape = {
292292+ onEscape: (callback) => {
293293+ ipcRenderer.on('escape-pressed', async (event, data) => {
294294+ try {
295295+ const result = await callback();
296296+ ipcRenderer.send(data.responseChannel, result || { handled: false });
297297+ } catch (err) {
298298+ console.error('Error in escape handler:', err);
299299+ ipcRenderer.send(data.responseChannel, { handled: false });
300300+ }
301301+ });
302302+ }
303303+};
304304+287305// unused
288306/*
289307api.sendToWindow = (windowId, msg) => {