experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(lex): always show recent lexicons section in sidebar

+673 -10
+630
TAG_ACTIONS_DESIGN.md
··· 1 + # Tag Actions Extension — Design Document 2 + 3 + ## 1. Concept 4 + 5 + Tag Actions is a new desktop extension that lets users define custom behaviors triggered by tagging items. Each "tag action" binds a tag (or tag pair) to an action — when an item receives that tag, the action fires. This replaces hardcoded tag-based features (like a future "offline" tag triggering content fetch) with a generic, user-configurable system. 6 + 7 + **Examples of tag actions a user could create:** 8 + - Tag `offline` triggers: fetch and cache page content for offline reading 9 + - Tag `archive` triggers: submit URL to the Wayback Machine 10 + - Tag `read-later` shows: an eyeball icon on items in list views 11 + - Tag `important` shows: a star/flag icon on items 12 + - Tag `summarize` triggers: run a script that summarizes the page content 13 + 14 + --- 15 + 16 + ## 2. Extension Structure 17 + 18 + ### File Layout 19 + 20 + ``` 21 + extensions/tag-actions/ 22 + manifest.json # Extension metadata, commands, shortcuts 23 + background.html # Entry point (loads background.js as ES module) 24 + background.js # Core logic: event listeners, action execution 25 + home.html # Settings/management UI 26 + home.js # Settings UI logic 27 + home.css # Settings UI styles 28 + settings-schema.json # Schema for extension settings (tag action configs) 29 + ``` 30 + 31 + ### manifest.json 32 + 33 + ```json 34 + { 35 + "id": "tag-actions", 36 + "shortname": "tag-actions", 37 + "name": "Tag Actions", 38 + "description": "Define custom actions triggered by tags", 39 + "version": "1.0.0", 40 + "background": "background.html", 41 + "builtin": true, 42 + "settingsSchema": "./settings-schema.json", 43 + "commands": [ 44 + { 45 + "name": "open tag actions", 46 + "description": "Open the tag actions manager", 47 + "action": { 48 + "type": "window", 49 + "url": "peek://ext/tag-actions/home.html", 50 + "options": { 51 + "role": "workspace", 52 + "key": "tag-actions-home", 53 + "width": 800, 54 + "height": 600, 55 + "title": "Tag Actions" 56 + } 57 + } 58 + } 59 + ], 60 + "shortcuts": [ 61 + { 62 + "keys": "Option+Shift+T", 63 + "command": "open tag actions", 64 + "global": true 65 + } 66 + ] 67 + } 68 + ``` 69 + 70 + ### background.html 71 + 72 + Standard extension entry point pattern: 73 + 74 + ```html 75 + <!DOCTYPE html> 76 + <html> 77 + <head> 78 + <script type="module" src="./background.js"></script> 79 + </head> 80 + <body></body> 81 + </html> 82 + ``` 83 + 84 + --- 85 + 86 + ## 3. Data Model 87 + 88 + ### Storage Location 89 + 90 + Tag action configurations are stored in `extension_settings` via `api.settings` (the standard extension settings API). This is the same pattern used by scripts, sheets, and other extensions. 91 + 92 + Key: `actions` (JSON array of action configs) 93 + 94 + ### Tag Action Config Schema 95 + 96 + ```json 97 + { 98 + "actions": { 99 + "type": "array", 100 + "items": { 101 + "type": "object", 102 + "properties": { 103 + "id": { "type": "string", "description": "Unique ID (generated)" }, 104 + "name": { "type": "string", "description": "User-facing label" }, 105 + "enabled": { "type": "boolean", "default": true }, 106 + "triggerTag": { "type": "string", "description": "Tag name that triggers this action" }, 107 + "filterTag": { "type": "string", "description": "Optional second tag required (AND logic)" }, 108 + "triggerOn": { "type": "string", "enum": ["add", "remove", "both"], "default": "add" }, 109 + "actionType": { "type": "string", "enum": ["icon", "script", "publish", "open-url", "tag"] }, 110 + "actionConfig": { "type": "object", "description": "Action-type-specific configuration" }, 111 + "itemTypes": { "type": "array", "items": { "type": "string" }, "description": "Optional filter: only trigger for these item types (url, text, tagset, etc.)" }, 112 + "createdAt": { "type": "number" }, 113 + "updatedAt": { "type": "number" } 114 + } 115 + } 116 + } 117 + } 118 + ``` 119 + 120 + ### settings-schema.json 121 + 122 + ```json 123 + { 124 + "labels": { 125 + "prefs": { 126 + "autoExecute": "Auto-execute actions on tag add" 127 + } 128 + }, 129 + "prefs": { 130 + "type": "object", 131 + "properties": { 132 + "autoExecute": { 133 + "type": "boolean", 134 + "description": "Automatically execute actions when tags are added (vs. manual trigger only)", 135 + "default": true 136 + } 137 + } 138 + }, 139 + "storageKeys": { 140 + "PREFS": "prefs", 141 + "ACTIONS": "actions" 142 + }, 143 + "defaults": { 144 + "prefs": { 145 + "autoExecute": true 146 + }, 147 + "actions": [] 148 + } 149 + } 150 + ``` 151 + 152 + ### Example Stored Actions 153 + 154 + ```json 155 + [ 156 + { 157 + "id": "ta_1708000000_abc123", 158 + "name": "Offline Reading", 159 + "enabled": true, 160 + "triggerTag": "offline", 161 + "filterTag": null, 162 + "triggerOn": "add", 163 + "actionType": "publish", 164 + "actionConfig": { 165 + "topic": "content:fetch-offline", 166 + "includeItemData": true 167 + }, 168 + "itemTypes": ["url"], 169 + "createdAt": 1708000000000, 170 + "updatedAt": 1708000000000 171 + }, 172 + { 173 + "id": "ta_1708000001_def456", 174 + "name": "Show Read-Later Icon", 175 + "enabled": true, 176 + "triggerTag": "read-later", 177 + "filterTag": null, 178 + "triggerOn": "add", 179 + "actionType": "icon", 180 + "actionConfig": { 181 + "icon": "eye", 182 + "color": "#4a9eff", 183 + "tooltip": "Read later" 184 + }, 185 + "itemTypes": null, 186 + "createdAt": 1708000001000, 187 + "updatedAt": 1708000001000 188 + }, 189 + { 190 + "id": "ta_1708000002_ghi789", 191 + "name": "Archive to Wayback", 192 + "enabled": true, 193 + "triggerTag": "archive", 194 + "filterTag": null, 195 + "triggerOn": "add", 196 + "actionType": "open-url", 197 + "actionConfig": { 198 + "urlTemplate": "https://web.archive.org/save/{url}", 199 + "openInBackground": true 200 + }, 201 + "itemTypes": ["url"], 202 + "createdAt": 1708000002000, 203 + "updatedAt": 1708000002000 204 + } 205 + ] 206 + ``` 207 + 208 + --- 209 + 210 + ## 4. Settings UI (home.js) 211 + 212 + The settings UI follows the pattern established by groups (home.js) and tags (home.js) — a single-page app with list and detail views, using peek-components. 213 + 214 + ### UI Structure 215 + 216 + ``` 217 + +-------------------------------------------------------+ 218 + | Tag Actions [+ New Action] | 219 + +-------------------------------------------------------+ 220 + | Search actions... | 221 + +-------------------------------------------------------+ 222 + | | 223 + | [x] Offline Reading offline -> fetch content | 224 + | [x] Read-Later Icon read-later -> eye icon | 225 + | [ ] Archive to Wayback archive -> open URL | 226 + | [x] Run Summarizer summarize -> run script | 227 + | | 228 + +-------------------------------------------------------+ 229 + ``` 230 + 231 + ### Create/Edit Form 232 + 233 + When clicking an action or the [+ New Action] button: 234 + 235 + ``` 236 + +-------------------------------------------------------+ 237 + | < Back | 238 + +-------------------------------------------------------+ 239 + | Name: [ Offline Reading ] | 240 + | Enabled: [x] | 241 + | | 242 + | --- Trigger --- | 243 + | Tag: [ offline ] (autocomplete from tags) | 244 + | Also requires tag: [ ] (optional AND tag) | 245 + | Trigger on: ( ) Tag added ( ) Tag removed ( ) Both | 246 + | Item types: [x] URL [ ] Text [ ] Tagset [ ] Image | 247 + | | 248 + | --- Action --- | 249 + | Type: [v Publish Event ] | 250 + | | 251 + | (Type-specific config form appears here) | 252 + | Topic: [ content:fetch-offline ] | 253 + | Include item data: [x] | 254 + | | 255 + | [Delete Action] [Save Action] | 256 + +-------------------------------------------------------+ 257 + ``` 258 + 259 + ### Action Type Config Forms 260 + 261 + Each action type shows a different config sub-form: 262 + 263 + **Icon** — purely visual, no code execution: 264 + - Icon picker (predefined set: eye, star, flag, bookmark, download, archive, clock, check) 265 + - Color picker 266 + - Tooltip text 267 + 268 + **Publish** — fires a pubsub event (for other extensions to handle): 269 + - Topic name (string) 270 + - Include item data in payload (boolean) 271 + 272 + **Script** — runs an existing user script: 273 + - Script picker (dropdown of scripts from the scripts extension) 274 + 275 + **Open URL** — opens a URL template: 276 + - URL template with `{url}`, `{title}`, `{id}` placeholders 277 + - Open in background (boolean) 278 + 279 + **Tag** — adds another tag to the item: 280 + - Tag name to add (autocomplete) 281 + 282 + ### Implementation Notes 283 + 284 + - Use `api.datastore.getTagsByFrecency()` to populate tag autocomplete in the trigger field 285 + - Use peek-components (peek-card, peek-input, peek-button, peek-switch, peek-select) per DEVELOPMENT.md guidelines 286 + - Follow the escape handler pattern from groups/tags home.js (list view -> detail view -> back on ESC) 287 + - Persist view state in localStorage (following existing pattern) 288 + 289 + --- 290 + 291 + ## 5. Event Flow 292 + 293 + ### How Tag Additions Trigger Actions 294 + 295 + ``` 296 + User tags item (via cmd, tags UI, groups, sync, etc.) 297 + | 298 + v 299 + Backend IPC handler: datastore-tag-item 300 + | 301 + v 302 + Backend publishes: tag:item-added { tagId, tagName, itemId, itemType } 303 + | 304 + v 305 + tag-actions background.js receives event via api.subscribe('tag:item-added', ...) 306 + | 307 + v 308 + Looks up matching actions: actions.filter(a => a.triggerTag === msg.tagName && a.enabled) 309 + | 310 + v 311 + For each matching action: 312 + - Check itemTypes filter (if specified) 313 + - Check filterTag (if specified, verify item also has this second tag) 314 + - Execute action based on actionType 315 + ``` 316 + 317 + ### background.js Core Logic (Pseudocode) 318 + 319 + ```javascript 320 + // On init: 321 + api.subscribe('tag:item-added', async (msg) => { 322 + await handleTagEvent('add', msg); 323 + }, api.scopes.GLOBAL); 324 + 325 + api.subscribe('tag:item-removed', async (msg) => { 326 + await handleTagEvent('remove', msg); 327 + }, api.scopes.GLOBAL); 328 + 329 + async function handleTagEvent(eventType, msg) { 330 + const settings = await api.settings.get(); 331 + const actions = settings.data?.actions || []; 332 + 333 + for (const action of actions) { 334 + if (!action.enabled) continue; 335 + if (action.triggerTag !== msg.tagName) continue; 336 + if (action.triggerOn !== 'both' && action.triggerOn !== eventType) continue; 337 + 338 + // Item type filter 339 + if (action.itemTypes?.length > 0 && !action.itemTypes.includes(msg.itemType)) continue; 340 + 341 + // Second tag filter (AND logic) 342 + if (action.filterTag) { 343 + const itemTags = await api.datastore.getItemTags(msg.itemId); 344 + if (!itemTags.data?.some(t => t.name === action.filterTag)) continue; 345 + } 346 + 347 + await executeAction(action, msg); 348 + } 349 + } 350 + 351 + async function executeAction(action, msg) { 352 + switch (action.actionType) { 353 + case 'icon': 354 + // Icons are handled at render time by the item list, not here. 355 + // Publish an event so list UIs can refresh icon state. 356 + api.publish('tag-actions:icon-changed', { itemId: msg.itemId }, api.scopes.GLOBAL); 357 + break; 358 + 359 + case 'publish': 360 + api.publish(action.actionConfig.topic, { 361 + itemId: msg.itemId, 362 + tagName: msg.tagName, 363 + ...(action.actionConfig.includeItemData ? await getItemData(msg.itemId) : {}) 364 + }, api.scopes.GLOBAL); 365 + break; 366 + 367 + case 'script': 368 + api.publish('scripts:execute', { 369 + scriptId: action.actionConfig.scriptId, 370 + context: { itemId: msg.itemId, url: msg.itemUrl } 371 + }, api.scopes.GLOBAL); 372 + break; 373 + 374 + case 'open-url': { 375 + const item = await api.datastore.getItem(msg.itemId); 376 + const url = action.actionConfig.urlTemplate 377 + .replace('{url}', encodeURIComponent(item.data?.content || '')) 378 + .replace('{title}', encodeURIComponent(item.data?.title || '')) 379 + .replace('{id}', msg.itemId); 380 + api.window.open(url, { 381 + role: 'content', 382 + width: 800, 383 + height: 600 384 + }); 385 + break; 386 + } 387 + 388 + case 'tag': { 389 + const tagResult = await api.datastore.getOrCreateTag(action.actionConfig.addTagName); 390 + if (tagResult.success) { 391 + await api.datastore.tagItem(msg.itemId, tagResult.data.tag.id); 392 + } 393 + break; 394 + } 395 + } 396 + } 397 + ``` 398 + 399 + ### Event Ordering Guarantee 400 + 401 + The `tag:item-added` event is published synchronously within the IPC handler after the database write completes. The tag-actions extension receives this event in its background.js process. Actions execute asynchronously but are guaranteed to see the tag already persisted in the database. 402 + 403 + --- 404 + 405 + ## 6. Item List Integration (Icons/Badges) 406 + 407 + ### Problem 408 + 409 + The tags home.js and groups home.js render item cards. Tag Actions needs to show icons/badges on items that match `icon`-type tag actions. But those UIs are in separate extensions. 410 + 411 + ### Approach: Pubsub Query Protocol 412 + 413 + The tag-actions extension exposes a pubsub-based query API that any item list UI can use: 414 + 415 + ```javascript 416 + // In tag-actions background.js: 417 + api.subscribe('tag-actions:get-icons', async (msg) => { 418 + // msg.itemId or msg.itemIds (batch) 419 + // Returns matching icon configs for items based on their tags 420 + const icons = await computeIconsForItems(msg.itemIds); 421 + api.publish('tag-actions:icons-response', { icons }, api.scopes.GLOBAL); 422 + }, api.scopes.GLOBAL); 423 + ``` 424 + 425 + **How it works:** 426 + 427 + 1. Tag-actions maintains an in-memory index: `tagName -> [icon configs]` 428 + 2. When tags home.js renders a card, it can query `tag-actions:get-icons` with the item's tag list 429 + 3. The response contains icon configs: `{ itemId: [{ icon: 'eye', color: '#4a9eff', tooltip: 'Read later' }] }` 430 + 4. The calling UI renders small SVG icons in the card 431 + 432 + ### Alternative: CSS Custom Properties + Data Attributes 433 + 434 + A simpler approach that avoids cross-extension queries: 435 + 436 + 1. Tag-actions publishes `tag-actions:icon-rules` on init with the current icon action configs 437 + 2. Other UIs listen and store the rules locally 438 + 3. When rendering cards, check item tags against rules and render icons inline 439 + 440 + This is simpler and avoids async round-trips during render. 441 + 442 + ### Recommended Approach 443 + 444 + Use the CSS/data-attribute approach (alternative above) for v1. It avoids pubsub latency during card rendering and is simpler to implement. The icon rules are small (usually <10 entries) and rarely change. 445 + 446 + ```javascript 447 + // Tag-actions publishes rules on init and on change: 448 + api.publish('tag-actions:icon-rules', { 449 + rules: [ 450 + { tag: 'read-later', icon: 'eye', color: '#4a9eff', tooltip: 'Read later' }, 451 + { tag: 'important', icon: 'star', color: '#ffd700', tooltip: 'Important' } 452 + ] 453 + }, api.scopes.GLOBAL); 454 + 455 + // Tags home.js / groups home.js subscribe: 456 + api.subscribe('tag-actions:icon-rules', (msg) => { 457 + iconRules = msg.rules; 458 + // Re-render if currently visible 459 + }, api.scopes.GLOBAL); 460 + 461 + // During card rendering, check: 462 + const itemTags = state.itemTags.get(item.id) || []; 463 + const icons = iconRules.filter(rule => itemTags.some(t => t.name === rule.tag)); 464 + // Render small SVG icons next to the card title 465 + ``` 466 + 467 + --- 468 + 469 + ## 7. Action Types — Initial Set 470 + 471 + ### 7.1 Icon (Visual Only) 472 + 473 + **Purpose:** Show a visual indicator on items with the trigger tag. 474 + 475 + **Config:** 476 + - `icon`: One of a predefined set (`eye`, `star`, `flag`, `bookmark`, `download`, `archive`, `clock`, `check`, `heart`, `pin`) 477 + - `color`: Hex color string 478 + - `tooltip`: Hover text 479 + 480 + **Execution:** No runtime action. The icon rules are broadcast via pubsub and rendered by item list UIs. 481 + 482 + ### 7.2 Publish Event 483 + 484 + **Purpose:** Fire a pubsub event for other extensions to handle. This is the generic hook for arbitrary integrations. 485 + 486 + **Config:** 487 + - `topic`: Pubsub topic name (string) 488 + - `includeItemData`: Whether to include full item data in the payload (boolean) 489 + 490 + **Execution:** `api.publish(topic, payload, api.scopes.GLOBAL)` 491 + 492 + **Use cases:** 493 + - `content:fetch-offline` could be handled by a future offline extension 494 + - `reader:open` could trigger a reader mode view 495 + - Custom inter-extension communication 496 + 497 + ### 7.3 Run Script 498 + 499 + **Purpose:** Execute an existing user script from the scripts extension. 500 + 501 + **Config:** 502 + - `scriptId`: ID of the script to run 503 + 504 + **Execution:** Publishes `scripts:execute` with the script ID and item context. 505 + 506 + ### 7.4 Open URL 507 + 508 + **Purpose:** Open a URL template, substituting item data. 509 + 510 + **Config:** 511 + - `urlTemplate`: URL with `{url}`, `{title}`, `{id}` placeholders 512 + - `openInBackground`: Whether to focus the new window (boolean) 513 + 514 + **Execution:** Substitutes placeholders and calls `api.window.open()`. 515 + 516 + **Use cases:** 517 + - Archive to Wayback Machine: `https://web.archive.org/save/{url}` 518 + - Search for related content: `https://www.google.com/search?q={title}` 519 + - Open in a specific service: `https://getpocket.com/save?url={url}` 520 + 521 + ### 7.5 Add Tag 522 + 523 + **Purpose:** Automatically add another tag when the trigger tag is applied. 524 + 525 + **Config:** 526 + - `addTagName`: Name of the tag to add 527 + 528 + **Execution:** Calls `api.datastore.getOrCreateTag()` then `api.datastore.tagItem()`. 529 + 530 + **Use cases:** 531 + - Tag `work-project` auto-adds `work` parent tag 532 + - Tag `recipe` auto-adds `food` category tag 533 + 534 + **Guard:** Must check for circular triggers (tag A adds tag B which triggers an action that adds tag A). Implement a per-event execution set to prevent re-entry. 535 + 536 + --- 537 + 538 + ## 8. Migration Path 539 + 540 + ### Current Hardcoded Tag-Based Features 541 + 542 + Currently there are no fully-implemented tag-based action systems in the desktop app. The mobile app (`backend/tauri-mobile/src-tauri/src/lib.rs`) has some offline-related code, but desktop does not. This means: 543 + 544 + 1. **No migration needed for v1.** The tag-actions extension is purely additive. 545 + 2. **Future offline reading feature** should be built as a separate extension that listens for a `content:fetch-offline` pubsub event, rather than hardcoding the tag name. Then a default tag action can bridge `offline` tag -> `content:fetch-offline` event. 546 + 3. **Groups extension** already uses tag metadata (`isGroup: true`) to distinguish group-tags from regular tags. Tag-actions should not conflict — it operates on a different dimension (what happens when a tag is applied, not what the tag IS). 547 + 548 + ### Recommended Migration Strategy for Future Features 549 + 550 + When building new tag-triggered features: 551 + 552 + 1. Build the feature as a standalone extension that listens for a pubsub event 553 + 2. Create a default tag action (shipped as a preset, not hardcoded) that maps the tag to the event 554 + 3. Users can disable, reconfigure, or replace the default tag action 555 + 556 + This keeps features decoupled — the offline extension does not need to know about tags, and the tags system does not need to know about offline reading. 557 + 558 + ### Shipping Default Actions 559 + 560 + The extension should include a set of suggested/default actions that users can enable: 561 + 562 + ```javascript 563 + const PRESET_ACTIONS = [ 564 + { 565 + name: 'Read Later Icon', 566 + triggerTag: 'read-later', 567 + actionType: 'icon', 568 + actionConfig: { icon: 'eye', color: '#4a9eff', tooltip: 'Read later' } 569 + }, 570 + { 571 + name: 'Archive to Wayback Machine', 572 + triggerTag: 'archive', 573 + actionType: 'open-url', 574 + actionConfig: { urlTemplate: 'https://web.archive.org/save/{url}', openInBackground: true }, 575 + itemTypes: ['url'] 576 + } 577 + ]; 578 + ``` 579 + 580 + On first run (no stored actions), the home.js UI could show these as "suggested actions" the user can add with one click. 581 + 582 + --- 583 + 584 + ## 9. Open Questions 585 + 586 + ### For User Input 587 + 588 + 1. **Should icon-type actions be toggleable in item lists?** E.g., clicking the eye icon could remove the `read-later` tag (acting as a checkbox). Or should icons be purely visual? 589 + 590 + 2. **Should tag actions support conditions beyond tag matching?** E.g., "only for URLs matching pattern X" or "only for items created in the last 24 hours". This adds power but also complexity. 591 + 592 + 3. **Should there be a "confirm before executing" option for destructive actions?** E.g., archiving to Wayback Machine might warrant a confirmation dialog. 593 + 594 + 4. **What icon set to use?** Options: 595 + - Inline SVG from a predefined set (simplest, no dependencies) 596 + - Single emoji character (cross-platform but rendering varies) 597 + - Reference to an external icon library 598 + 599 + 5. **Should tag actions support "on schedule" triggers?** E.g., "every day, for items tagged `daily-review`, do X". This is a different trigger model (cron-based vs event-based). 600 + 601 + 6. **Priority/ordering** — if multiple actions match the same tag event, should users be able to control execution order? 602 + 603 + 7. **Keyboard shortcut for quick-tagging** — should tag actions expose a way to tag the current window's URL with a single keypress? E.g., Option+1 = tag with `read-later`, Option+2 = tag with `archive`. This could be a feature of the tag-actions extension using `api.shortcuts.register()`. 604 + 605 + ### Technical Decisions 606 + 607 + 8. **Circular trigger prevention** — The "add tag" action type can create chains. Proposed solution: maintain a per-event Set of already-triggered action IDs. If an action is already in the set, skip it. Reset the set when the original event handler returns. 608 + 609 + 9. **Error handling** — If an action fails (e.g., script errors, network timeout on URL open), should the extension: 610 + - Log silently (current scripts extension pattern) 611 + - Show a notification 612 + - Store error state on the action config (like feeds' `errorCount`) 613 + 614 + 10. **Batch operations** — Sync pulls can import many items at once, each potentially triggering tag actions. Should actions be debounced per tag (process once after all sync items are tagged) or fire for every individual item? 615 + 616 + --- 617 + 618 + ## 10. Summary of Patterns Used 619 + 620 + | Aspect | Pattern Source | Notes | 621 + |--------|---------------|-------| 622 + | Extension structure | All extensions | manifest.json + background.html + background.js | 623 + | Settings storage | scripts, sheets | `api.settings.get/set` with JSON in `extension_settings` | 624 + | Settings schema | editor, example | `settings-schema.json` with labels, prefs, defaults | 625 + | Settings UI | groups/home.js, tags/home.js | peek-components, ESC navigation, list/detail views | 626 + | Event listening | tags/home.js, groups/home.js | `api.subscribe('tag:item-added', ...)` | 627 + | Cross-extension query | scripts (execute) | Pubsub request/response pattern | 628 + | Noun registration | tags, feeds, scripts | `registerNoun()` for auto-generated commands | 629 + | Command registration | all extensions | `api.commands.register()` in init | 630 + | Window management | feeds, scripts | `api.window.open()` with role/key/dimensions |
+2 -2
backend/tauri-mobile/src-tauri/gen/apple/Peek/ShareViewController.swift
··· 2146 2146 // MARK: - UITextFieldDelegate 2147 2147 extension ShareViewController: UITextFieldDelegate { 2148 2148 func textFieldShouldReturn(_ textField: UITextField) -> Bool { 2149 - // Add tag when Return is pressed 2150 - addTagPressed() 2149 + // Add any pending tag text and dismiss the share UI 2150 + closePressed() 2151 2151 return true 2152 2152 } 2153 2153 }
+1 -1
backend/tauri-mobile/src-tauri/tauri.conf.json
··· 6 6 "build": { 7 7 "beforeBuildCommand": "npm run build", 8 8 "frontendDist": "../dist", 9 - "devUrl": "http://192.168.50.69:58598" 9 + "devUrl": "http://192.168.50.69:5188" 10 10 }, 11 11 "app": { 12 12 "windows": [
+7
extensions/lex/home.css
··· 407 407 color: var(--base08); 408 408 } 409 409 410 + .recent-empty { 411 + font-size: 11px; 412 + color: var(--base03); 413 + padding: 6px 16px 4px; 414 + font-style: italic; 415 + } 416 + 410 417 .sidebar-footer { 411 418 padding: 12px 16px; 412 419 border-top: 1px solid var(--base02);
+33 -7
extensions/lex/home.js
··· 169 169 if (Array.isArray(msg.nsids)) { 170 170 for (const nsid of msg.nsids) { 171 171 if (!state.recentLexicons.includes(nsid)) { 172 - state.recentLexicons.push(nsid); 172 + // Add new ones at front (most recent) 173 + state.recentLexicons.unshift(nsid); 173 174 } 174 175 } 175 - state.recentLexicons.sort(); 176 + if (state.recentLexicons.length > MAX_RECENT_LEXICONS) { 177 + state.recentLexicons = state.recentLexicons.slice(0, MAX_RECENT_LEXICONS); 178 + } 176 179 saveRecentLexicons(); 177 180 renderRecentLexicons(); 178 181 knownRecentNsids = [...state.recentLexicons]; ··· 1384 1387 // Switch to create-record panel 1385 1388 switchPanel('create-record'); 1386 1389 1390 + // Track as recently used 1391 + addRecentLexicon(nsid); 1392 + 1387 1393 // Fetch the lexicon schema 1388 1394 try { 1389 1395 const lexicon = await resolveLexicon(nsid); ··· 1735 1741 state.cpMode = 'form'; 1736 1742 state.cpSubmitting = false; 1737 1743 1744 + // Track as recently used 1745 + addRecentLexicon(nsid); 1746 + 1738 1747 pickerInput.style.display = 'none'; 1739 1748 pickerSelected.style.display = 'flex'; 1740 1749 const pickerFriendly = nsidToFriendlyName(nsid); ··· 1954 1963 recordsPaginationEl.style.display = 'none'; 1955 1964 1956 1965 switchPanel('records'); 1966 + 1967 + // Track as recently used 1968 + addRecentLexicon(nsid); 1957 1969 1958 1970 // Check cache first for instant display 1959 1971 const cached = getCachedRecords(nsid); ··· 2416 2428 // ============================================================================ 2417 2429 2418 2430 const RECENT_LEXICONS_KEY = 'recentLexicons'; 2431 + const MAX_RECENT_LEXICONS = 20; 2419 2432 2420 2433 async function loadRecentLexicons() { 2421 2434 try { ··· 2433 2446 } catch {} 2434 2447 } 2435 2448 2449 + /** 2450 + * Add or promote a lexicon to the front of the recent list (most recent first). 2451 + * Called when a lexicon is created, browsed, or viewed. 2452 + */ 2436 2453 function addRecentLexicon(nsid) { 2437 - if (state.recentLexicons.includes(nsid)) return; 2438 - state.recentLexicons.push(nsid); 2439 - state.recentLexicons.sort(); 2454 + // Remove if already present (we will re-add at front) 2455 + state.recentLexicons = state.recentLexicons.filter(n => n !== nsid); 2456 + // Add to front (most recent first) 2457 + state.recentLexicons.unshift(nsid); 2458 + // Cap the list 2459 + if (state.recentLexicons.length > MAX_RECENT_LEXICONS) { 2460 + state.recentLexicons = state.recentLexicons.slice(0, MAX_RECENT_LEXICONS); 2461 + } 2440 2462 saveRecentLexicons(); 2441 2463 renderRecentLexicons(); 2442 2464 onRecentLexiconsChanged(); ··· 2451 2473 2452 2474 function renderRecentLexicons() { 2453 2475 if (!recentLexiconsEl) return; 2476 + 2454 2477 if (state.recentLexicons.length === 0) { 2455 - recentLexiconsEl.innerHTML = ''; 2478 + recentLexiconsEl.innerHTML = ` 2479 + <div class="recent-label">Recent Lexicons</div> 2480 + <div class="recent-empty">No recent lexicons yet</div> 2481 + `; 2456 2482 return; 2457 2483 } 2458 2484 2459 2485 recentLexiconsEl.innerHTML = ` 2460 - <div class="recent-label">Recent</div> 2486 + <div class="recent-label">Recent Lexicons</div> 2461 2487 ${state.recentLexicons.map(nsid => { 2462 2488 const friendly = nsidToFriendlyName(nsid); 2463 2489 return `