experiments in a post-browser web
10
fork

Configure Feed

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

feat(tile-manifest): extend TileCapabilities with context and dialogs

+130
+130
backend/electron/tile-manifest.ts
··· 83 83 } 84 84 85 85 /** 86 + * Context capability: access to the context-entry key/value store. 87 + * 88 + * Strict `tile:context:*` handlers gate each operation on the shape of 89 + * this grant: 90 + * - `read` — key allowlist for `get` / `history`. If omitted, every 91 + * key is readable (backward-compatible "unrestricted" mode). 92 + * - `write` — key allowlist for `set`. If omitted, every key is 93 + * writable. 94 + * - `modes` — gates the `setMode` / `watchMode` future helpers and 95 + * any write to the well-known `mode` key. 96 + * - `queryWindows` — gates `windowsWithValue` / `windowsInSpace`. 97 + * 98 + * Like ShortcutsCapability, the capability may also be declared as 99 + * `true` in a manifest — treated as an empty object (unrestricted, 100 + * no window-query privileges). 101 + */ 102 + export interface ContextCapability { 103 + /** Keys the tile can read; omitted = all keys */ 104 + read?: string[]; 105 + /** Keys the tile can write; omitted = all keys */ 106 + write?: string[]; 107 + /** Allow mode management (setMode/watchMode + writing the `mode` key) */ 108 + modes?: boolean; 109 + /** Allow windowsInSpace / windowsWithValue queries */ 110 + queryWindows?: boolean; 111 + } 112 + 113 + /** 114 + * Dialogs capability: permission to show native save/open file dialogs. 115 + * 116 + * May be expressed in the manifest as: 117 + * - `true` — tile may invoke any supported dialog type 118 + * - `{ types: [...] }` — only the listed dialog types may be invoked 119 + * 120 + * Supported types (current surface): `'save'`, `'open'`. The strict 121 + * IPC handlers reject any invocation whose type is not in the list. 122 + * When `types` is omitted (or the capability is `true`), no per-type 123 + * filtering is applied. 124 + */ 125 + export type DialogType = 'save' | 'open'; 126 + 127 + export interface DialogsCapability { 128 + /** If present, only these dialog types can be shown */ 129 + types?: DialogType[]; 130 + } 131 + 132 + /** 86 133 * All capabilities a tile can request 87 134 */ 88 135 export interface TileCapabilities { ··· 101 148 theme?: boolean; 102 149 /** Can read/write user settings */ 103 150 settings?: boolean; 151 + /** Can read/write application context (mode, spaces, user keys) */ 152 + context?: boolean | ContextCapability; 153 + /** Can invoke native file dialogs (save/open) */ 154 + dialogs?: boolean | DialogsCapability; 104 155 } 105 156 106 157 // ─── Tile Definition Types ─────────────────────────────────────────── ··· 423 474 errors.push({ path: 'capabilities.shortcuts', message: 'shortcuts must be boolean or { keys: string[] }' }); 424 475 } 425 476 } 477 + 478 + // Validate context capability shape — must be boolean or 479 + // { read?: string[]; write?: string[]; modes?: boolean; queryWindows?: boolean } 480 + if (caps.context !== undefined) { 481 + const cc = caps.context; 482 + if (typeof cc === 'boolean') { 483 + // valid 484 + } else if (cc && typeof cc === 'object') { 485 + const ccObj = cc as Record<string, unknown>; 486 + for (const listKey of ['read', 'write'] as const) { 487 + const v = ccObj[listKey]; 488 + if (v !== undefined) { 489 + if (!Array.isArray(v)) { 490 + errors.push({ path: `capabilities.context.${listKey}`, message: `${listKey} must be an array of strings` }); 491 + } else { 492 + for (let i = 0; i < v.length; i++) { 493 + const k = v[i]; 494 + if (typeof k !== 'string' || k.length === 0) { 495 + errors.push({ path: `capabilities.context.${listKey}[${i}]`, message: `${listKey} entries must be non-empty strings` }); 496 + } 497 + } 498 + } 499 + } 500 + } 501 + for (const flagKey of ['modes', 'queryWindows'] as const) { 502 + if (ccObj[flagKey] !== undefined && typeof ccObj[flagKey] !== 'boolean') { 503 + errors.push({ path: `capabilities.context.${flagKey}`, message: `${flagKey} must be a boolean` }); 504 + } 505 + } 506 + } else { 507 + errors.push({ path: 'capabilities.context', message: 'context must be boolean or { read?, write?, modes?, queryWindows? }' }); 508 + } 509 + } 510 + 511 + // Validate dialogs capability shape — must be boolean or { types?: ('save'|'open')[] } 512 + if (caps.dialogs !== undefined) { 513 + const dc = caps.dialogs; 514 + if (typeof dc === 'boolean') { 515 + // valid 516 + } else if (dc && typeof dc === 'object') { 517 + const dcObj = dc as Record<string, unknown>; 518 + if (dcObj.types !== undefined) { 519 + if (!Array.isArray(dcObj.types)) { 520 + errors.push({ path: 'capabilities.dialogs.types', message: 'types must be an array of dialog type strings' }); 521 + } else { 522 + const valid = new Set(['save', 'open']); 523 + for (let i = 0; i < dcObj.types.length; i++) { 524 + const t = dcObj.types[i]; 525 + if (typeof t !== 'string' || !valid.has(t)) { 526 + errors.push({ path: `capabilities.dialogs.types[${i}]`, message: 'types entries must be one of: save, open' }); 527 + } 528 + } 529 + } 530 + } 531 + } else { 532 + errors.push({ path: 'capabilities.dialogs', message: 'dialogs must be boolean or { types: string[] }' }); 533 + } 534 + } 426 535 } 427 536 428 537 // Commands validation ··· 595 704 // Settings: grant as requested 596 705 if (requested.settings) { 597 706 granted.settings = true; 707 + } 708 + 709 + // Context: preserve object shape so strict IPC handlers can read the 710 + // read/write allowlists and modes/queryWindows flags. Manifests may 711 + // declare `context: true` OR an object form. 712 + if (requested.context) { 713 + if (requested.context === true) { 714 + granted.context = true; 715 + } else if (typeof requested.context === 'object') { 716 + granted.context = { ...requested.context }; 717 + } 718 + } 719 + 720 + // Dialogs: preserve object shape (types allowlist). Manifests may 721 + // declare `dialogs: true` OR `dialogs: { types: [...] }`. 722 + if (requested.dialogs) { 723 + if (requested.dialogs === true) { 724 + granted.dialogs = true; 725 + } else if (typeof requested.dialogs === 'object') { 726 + granted.dialogs = { ...requested.dialogs }; 727 + } 598 728 } 599 729 600 730 return { tileId, capabilities: granted, denied };