···2222 * Initialize command registration listeners
2323 * Extensions publish cmd:register to add commands, cmd:unregister to remove
2424 */
2525-const initCommandRegistry = () => {
2626- // Listen for command registrations from extensions
2525+const initCommandRegistry = async () => {
2626+ // Query already-registered commands from main process
2727+ // This catches commands registered before cmd started (race condition fix)
2828+ const existingCommands = await api.commands.getAll();
2929+ console.log('[cmd] Loaded', existingCommands.length, 'existing commands from registry');
3030+ existingCommands.forEach(cmd => {
3131+ dynamicCommands.set(cmd.name, cmd);
3232+ });
3333+3434+ // Listen for command registrations from extensions (for live updates)
2735 api.subscribe('cmd:register', (msg) => {
2836 console.log('[cmd] cmd:register received:', msg.name);
2937 dynamicCommands.set(msg.name, {
···103111 }, { global: true });
104112};
105113106106-const init = () => {
114114+const init = async () => {
107115 console.log('init');
108116109117 const prefs = () => store.get(storageKeys.PREFS);
110118111119 // Initialize command registry before shortcuts so extensions can register
112112- initCommandRegistry();
120120+ await initCommandRegistry();
113121114122 initShortcut(prefs());
115123};
+5-22
app/features.js
···11/**
22 * Features Collection
33 *
44- * This module provides feature schemas for the Settings UI.
44+ * This module provides core feature schemas for the Settings UI.
55 *
66- * Architecture note (Jan 2025):
66+ * Architecture (Jan 2025):
77 * - cmd and scripts are CORE features (run in peek://app context)
88- * - peeks, slides, groups are EXTENSIONS (run in isolated peek://ext contexts)
99- *
1010- * Extensions are now loaded by the main process ExtensionManager, not by
1111- * importing them here. However, this module still exports extension schemas
1212- * for the Settings UI to render settings forms.
1313- *
1414- * TODO: Settings UI should load extension schemas from manifest.json instead,
1515- * then app/peeks/ and app/slides/ can be deleted.
88+ * - Extensions (peeks, slides, groups) run in isolated peek://ext contexts
99+ * and their schemas are loaded dynamically from manifest.json
1610 */
17111818-// Core features
1212+// Core features only
1913import cmd from './cmd/index.js';
2014import scripts from './scripts/index.js';
2121-2222-// Extension schemas (for Settings UI only - extensions run in isolated processes)
2323-import peeks from './peeks/index.js';
2424-import slides from './slides/index.js';
2525-import groups from './groups/index.js';
26152716const fc = {};
28172929-// Core features
3018fc[cmd.id] = cmd;
3119fc[scripts.id] = scripts;
3232-3333-// Extension schemas for Settings UI
3434-fc[peeks.id] = peeks;
3535-fc[slides.id] = slides;
3636-fc[groups.id] = groups;
37203821export default fc;
-131
app/peeks/config.js
···11-const id = 'ef3bd271-d408-421f-9338-47b615571e43';
22-33-const labels = {
44- name: 'Peeks',
55- prefs: {
66- keyPrefix: 'Peek shortcut prefix',
77- }
88-};
99-1010-const prefsSchema = {
1111- "$schema": "https://json-schema.org/draft/2020-12/schema",
1212- "$id": "peek.peeks.prefs.schema.json",
1313- "title": "Peeks preferences",
1414- "description": "Peeks user preferences",
1515- "type": "object",
1616- "properties": {
1717- "shortcutKeyPrefix": {
1818- "description": "Global OS hotkey prefix to trigger peeks - will be followed by 0-9",
1919- "type": "string",
2020- "default": "Option+"
2121- },
2222- },
2323- "required": [ "shortcutKeyPrefix"]
2424-};
2525-2626-const itemSchema = {
2727- "$schema": "https://json-schema.org/draft/2020-12/schema",
2828- "$id": "peek.peeks.peek.schema.json",
2929- "title": "Peek - page peek",
3030- "description": "Peek page peek",
3131- "type": "object",
3232- "properties": {
3333- "keyNum": {
3434- "description": "Number on keyboard to open this peek, 0-9",
3535- "type": "integer",
3636- "minimum": 0,
3737- "maximum": 9,
3838- "default": 0
3939- },
4040- "title": {
4141- "description": "Name of the peek - user defined label",
4242- "type": "string",
4343- "default": "New Peek"
4444- },
4545- "address": {
4646- "description": "URL to load",
4747- "type": "string",
4848- "default": "https://example.com"
4949- },
5050- "persistState": {
5151- "description": "Whether to persist local state or load page into empty container - defaults to false",
5252- "type": "boolean",
5353- "default": false
5454- },
5555- "keepLive": {
5656- "description": "Whether to keep page alive in background or load fresh when triggered - defaults to false",
5757- "type": "boolean",
5858- "default": false
5959- },
6060- "allowSound": {
6161- "description": "Whether to allow the page to emit sound or not (eg for background music player peeks - defaults to false",
6262- "type": "boolean",
6363- "default": false
6464- },
6565- "height": {
6666- "description": "User-defined height of peek page",
6767- "type": "integer",
6868- "default": 600
6969- },
7070- "width": {
7171- "description": "User-defined width of peek page",
7272- "type": "integer",
7373- "default": 800
7474- },
7575- "enabled": {
7676- "description": "Whether this peek is enabled or not.",
7777- "type": "boolean",
7878- "default": false
7979- },
8080- },
8181- "required": [ "keyNum", "title", "address", "persistState", "keepLive", "allowSound",
8282- "height", "width", "enabled" ]
8383-};
8484-8585-const listSchema = {
8686- type: 'array',
8787- items: { "$ref": "#/$defs/peek" }
8888-};
8989-9090-// TODO: schemaize 0-9 constraints for peeks
9191-const schemas = {
9292- prefs: prefsSchema,
9393- item: itemSchema,
9494- items: listSchema
9595-};
9696-9797-const storageKeys = {
9898- PREFS: 'prefs',
9999- ITEMS: 'items',
100100-};
101101-102102-const defaults = {
103103- prefs: {
104104- shortcutKeyPrefix: 'Option+'
105105- },
106106- items: Array.from(Array(10)),
107107-};
108108-109109-for (var i = 0; i != 10; i++) {
110110- const address = i == 0 ? 'https://example.com/' : '';
111111- const enabled = i == 0 ? true : false;
112112- defaults.items[i] = {
113113- keyNum: i,
114114- title: `Peek key ${i}`,
115115- address: address,
116116- persistState: false,
117117- keepLive: false,
118118- allowSound: false,
119119- height: 600,
120120- width: 800,
121121- enabled: enabled,
122122- };
123123-}
124124-125125-export {
126126- id,
127127- labels,
128128- schemas,
129129- storageKeys,
130130- defaults
131131-};
-124
app/peeks/index.js
···11-import { id, labels, schemas, storageKeys, defaults } from './config.js';
22-import { openStore } from "../utils.js";
33-import windows from "../windows.js";
44-import api from '../api.js';
55-66-console.log('background', labels.name);
77-88-const debug = api.debug;
99-const clear = false;
1010-1111-const store = openStore(id, defaults, clear /* clear storage */);
1212-1313-// Track registered shortcuts for cleanup
1414-let registeredShortcuts = [];
1515-1616-const executeItem = (item) => {
1717- console.log('executeItem:peek', item);
1818- const height = item.height || 600;
1919- const width = item.width || 800;
2020-2121- const params = {
2222- // browserwindow
2323- height,
2424- width,
2525-2626- // peek
2727- feature: labels.name,
2828- keepLive: item.keepLive || false,
2929- persistState: item.persistState || false,
3030-3131- // Create a unique key for this peek using its address
3232- key: `peek:${item.address}`,
3333-3434- // tracking (handled automatically by windows API)
3535- trackingSource: 'peek',
3636- trackingSourceId: item.keyNum ? `peek_${item.keyNum}` : 'peek',
3737- title: item.title || ''
3838- };
3939-4040- windows.openModalWindow(item.address, params)
4141- .then(result => {
4242- console.log('Peek window opened:', result);
4343- })
4444- .catch(error => {
4545- console.error('Failed to open peek window:', error);
4646- });
4747-};
4848-4949-const initItems = (prefs, items) => {
5050- const cmdPrefix = prefs.shortcutKeyPrefix;
5151- console.log('initItems', items);
5252-5353- items.forEach(item => {
5454- if (item.enabled == true && item.address.length > 0) {
5555- const shortcut = `${cmdPrefix}${item.keyNum}`;
5656-5757- api.shortcuts.register(shortcut, () => {
5858- executeItem(item);
5959- }, { global: true });
6060-6161- registeredShortcuts.push(shortcut);
6262- }
6363- });
6464-};
6565-6666-/**
6767- * Unregister all shortcuts and clean up
6868- */
6969-const uninit = () => {
7070- console.log('peeks uninit - unregistering', registeredShortcuts.length, 'shortcuts');
7171-7272- registeredShortcuts.forEach(shortcut => {
7373- api.shortcuts.unregister(shortcut, { global: true });
7474- });
7575-7676- registeredShortcuts = [];
7777-};
7878-7979-/**
8080- * Reinitialize peeks (called when settings change)
8181- *
8282- * TODO: This is inefficient - reinitializes all peeks when any single
8383- * property changes. A better approach would be to diff the old and new
8484- * settings and only update the shortcuts that actually changed.
8585- */
8686-const reinit = () => {
8787- console.log('peeks reinit');
8888- uninit();
8989-9090- const prefs = store.get(storageKeys.PREFS);
9191- const items = store.get(storageKeys.ITEMS);
9292-9393- if (items && items.length > 0) {
9494- initItems(prefs, items);
9595- }
9696-};
9797-9898-const init = () => {
9999- console.log('peeks init');
100100-101101- const prefs = () => store.get(storageKeys.PREFS);
102102- const items = () => store.get(storageKeys.ITEMS);
103103-104104- // Initialize peeks
105105- if (items().length > 0) {
106106- initItems(prefs(), items());
107107- }
108108-109109- // Listen for settings changes to hot-reload
110110- api.subscribe('peeks:settings-changed', () => {
111111- console.log('peeks settings changed, reinitializing');
112112- reinit();
113113- });
114114-};
115115-116116-export default {
117117- defaults,
118118- id,
119119- init,
120120- uninit,
121121- labels,
122122- schemas,
123123- storageKeys
124124-}
+76-4
app/settings/settings.js
···912912};
913913914914// Initialize
915915-const init = () => {
915915+const init = async () => {
916916 const sidebarNav = document.getElementById('sidebarNav');
917917 const contentArea = document.getElementById('settingsContent');
918918···931931 // Track feature nav items and sections for dynamic updates
932932 const featureElements = new Map();
933933934934- // Add feature sections (only show enabled ones, but create all for hot-reload)
934934+ // Add CORE feature sections (cmd, scripts - features that run in peek://app context)
935935 for (const i in fc) {
936936 const feature = fc[i];
937937 const name = feature.labels.name;
···956956 featureElements.set(name.toLowerCase(), { navItem, section });
957957 }
958958959959- // Listen for feature toggle events to update sidebar
959959+ // Listen for feature toggle events to update sidebar (core features only)
960960 api.subscribe('core:feature:toggle', (msg) => {
961961 const featureName = msg.featureId?.toLowerCase();
962962 const elements = featureElements.get(featureName);
···972972 }
973973 });
974974975975- // Add Extensions section
975975+ // Add Extensions management section (must be created before loading extension settings)
976976 const extNav = document.createElement('a');
977977 extNav.className = 'nav-item';
978978 extNav.textContent = 'Extensions';
···996996 });
997997998998 contentArea.appendChild(extSection);
999999+10001000+ // Add EXTENSION settings sections (peeks, slides, groups - run in isolated peek://ext contexts)
10011001+ // Load schemas dynamically from extension manifests
10021002+ // Track which extensions we've already added to avoid duplicates
10031003+ const addedExtensions = new Set();
10041004+10051005+ const loadExtensionSettings = async () => {
10061006+ try {
10071007+ const result = await api.extensions.list();
10081008+ if (result.success && result.data) {
10091009+ for (const ext of result.data) {
10101010+ // Only show extensions that have schemas defined
10111011+ if (!ext.manifest?.schemas) continue;
10121012+10131013+ // Skip if already added
10141014+ const extName = ext.manifest.name.toLowerCase();
10151015+ if (addedExtensions.has(extName)) continue;
10161016+ addedExtensions.add(extName);
10171017+10181018+ // Construct feature-like object from manifest
10191019+ const feature = {
10201020+ id: ext.manifest.id,
10211021+ labels: { name: ext.manifest.name },
10221022+ schemas: ext.manifest.schemas,
10231023+ storageKeys: ext.manifest.storageKeys || { PREFS: 'prefs', ITEMS: 'items' },
10241024+ defaults: ext.manifest.defaults || {}
10251025+ };
10261026+10271027+ const name = feature.labels.name;
10281028+ const sectionId = name.toLowerCase().replace(/\s+/g, '-');
10291029+10301030+ // Add nav item (insert before Extensions nav item)
10311031+ const navItem = document.createElement('a');
10321032+ navItem.className = 'nav-item';
10331033+ navItem.textContent = name;
10341034+ navItem.dataset.section = sectionId;
10351035+ navItem.dataset.featureName = name;
10361036+ navItem.dataset.isExtension = 'true';
10371037+ navItem.addEventListener('click', () => showSection(sectionId));
10381038+10391039+ // Insert before the Extensions section in nav
10401040+ const extNavItem = sidebarNav.querySelector('[data-section="extensions"]');
10411041+ if (extNavItem) {
10421042+ sidebarNav.insertBefore(navItem, extNavItem);
10431043+ } else {
10441044+ sidebarNav.appendChild(navItem);
10451045+ }
10461046+10471047+ // Add section (insert before extensions section)
10481048+ const section = createSection(sectionId, name, () => renderFeatureSettings(feature));
10491049+ const extSectionEl = document.getElementById('section-extensions');
10501050+ if (extSectionEl) {
10511051+ contentArea.insertBefore(section, extSectionEl);
10521052+ } else {
10531053+ contentArea.appendChild(section);
10541054+ }
10551055+10561056+ featureElements.set(name.toLowerCase(), { navItem, section });
10571057+ }
10581058+ }
10591059+ } catch (err) {
10601060+ console.error('[settings] Failed to load extension schemas:', err);
10611061+ }
10621062+ };
10631063+10641064+ // Load extensions that are already running
10651065+ await loadExtensionSettings();
10661066+10671067+ // Listen for all extensions loaded event to catch any we missed
10681068+ api.subscribe('ext:all-loaded', () => {
10691069+ loadExtensionSettings();
10701070+ }, api.scopes.GLOBAL);
999107110001072 // Add Datastore link
10011073 const datastoreNav = document.createElement('a');