experiments in a post-browser web
10
fork

Configure Feed

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

fix(settings): render per-feature Options accordion + per-item arrows

Surfaced 2026-04-26: features that declare `settingsSchema` in their
manifest (peeks, slides, etc.) had no expandable Options bit on their
Settings → Features card. Two layered bugs:

1. `app/settings/settings.js::refreshFeaturesList` constructed each
feature's `manifest` object without a `schemas` field. The registry
(`api.features.list`) returns a flat entry shape that doesn't include
the manifest's `settingsSchema` path. The accordion gate
`manifest.schemas && (manifest.schemas.prefs || manifest.schemas.item)`
was therefore always false. Fix: fetch each feature's settings-schema
JSON in parallel via the existing `api.features.settingsSchema(id)`
IPC and populate `manifest.{schemas,storageKeys,defaults}` from it.

2. Once the accordion rendered, per-item cards inside it (e.g. Peek key
0..9) had no expand/collapse arrow — `getComputedStyle` showed
`::before { content: 'none' }`. The rule
`.item-card.no-collapse .item-card-title::before { content: none }`
was a descendant selector, so it cascaded into nested per-item titles
inside the no-collapse outer feature card. Fix: scope to the outer
card's own header via `.item-card.no-collapse > .item-card-header
.item-card-title::before` — per-item cards live in `.item-card-body`,
so they're no longer matched.

Coverage: new `tests/desktop/feature-options.spec.ts` opens Settings,
expands the Peeks Options accordion, asserts a form-section renders and
the inner peek card title's `::before` content is `▶` or `▼`.

+150 -5
+5
CHANGELOG.md
··· 15 15 16 16 ## 2026-04-20 17 17 18 + Desktop - per-feature Settings options accordion 19 + - [x] Fix Settings → Features Options accordion never rendering: `refreshFeaturesList` built each feature's `manifest` object without a `schemas` field (the registry's flat entry shape doesn't carry it), so the accordion gate `manifest.schemas && (manifest.schemas.prefs || manifest.schemas.item)` was always false. Now fetches each feature's settings-schema JSON in parallel via `api.features.settingsSchema(id)` and populates `manifest.{schemas,storageKeys,defaults}` from it. 20 + - [x] Fix per-item card expand/collapse arrows missing inside the Options accordion (Peeks/Slides item cards rendered without ▶/▼). The `.item-card.no-collapse .item-card-title::before { content: none }` rule was a descendant selector — it cascaded into nested per-item card titles. Scoped to direct-child via `.item-card.no-collapse > .item-card-header .item-card-title::before`. 21 + - [x] New `tests/desktop/feature-options.spec.ts` — opens Settings → Features, expands Peeks Options accordion, asserts a `.form-section` renders + the inner peek card's `::before` content is `▶` or `▼`. 22 + 18 23 Desktop - tag-action toggles + cmd panel hashtag shortcut 19 24 - [x] Fix tag-action toggles never rendering on item cards in tags / groups / search / pagestream — add `tag-actions:*` (5 topics) to each consumer's `pubsub.topics` allowlist so the strict tile gate stops dropping the `tag-actions:get-all` round-trip 20 25 - [x] Fix cold-start race in `features/tag-actions/home.js`: subscribe-before-publish was reachable when consumer tiles published their first `tag-actions:get-all` before the resident tag-actions tile had finished its `loadSettings()` await; tag-actions now broadcasts `tag-actions:get-all:response` once at end of init so any pre-existing subscriber catches up without a re-request
+5 -2
app/settings/settings.css
··· 269 269 content: '\25B6 '; 270 270 } 271 271 272 - .item-card.no-collapse .item-card-title::before { 272 + /* Scope to the no-collapse card's OWN header so per-item cards rendered 273 + * inside the feature's Options accordion (which live in the body, not the 274 + * header) keep their expand arrows. */ 275 + .item-card.no-collapse > .item-card-header .item-card-title::before { 273 276 content: none; 274 277 } 275 278 276 - .item-card.no-collapse .item-card-header { 279 + .item-card.no-collapse > .item-card-header { 277 280 cursor: default; 278 281 margin-bottom: 8px; 279 282 }
+22 -1
app/settings/settings.js
··· 1783 1783 ...(builtinResult?.entries || []), 1784 1784 ]; 1785 1785 1786 - const allFeatures = allEntries.map(entry => { 1786 + // Fetch each feature's settings-schema JSON in parallel so the 1787 + // Options accordion can render. The registry's flat entry shape 1788 + // doesn't include `settingsSchema` — without this round-trip the 1789 + // accordion gate is always false and per-feature options never appear. 1790 + const schemaResults = await Promise.all( 1791 + allEntries.map(entry => 1792 + api.features.settingsSchema(entry.id) 1793 + .then(r => r?.success ? r.data : null) 1794 + .catch(() => null) 1795 + ) 1796 + ); 1797 + 1798 + const allFeatures = allEntries.map((entry, i) => { 1787 1799 const isBuiltin = entry.source?.type === 'builtin'; 1788 1800 const feature = features.find(f => f.name.toLowerCase() === entry.id); 1789 1801 // Builtins respect the local features-list "enabled" flag ··· 1792 1804 ? (feature ? feature.enabled !== false : true) 1793 1805 : !entry.disabled; 1794 1806 1807 + const schemaJson = schemaResults[i]; 1808 + 1795 1809 // The registry stores a flat entry — adapt to the manifest-shaped 1796 1810 // object the renderer below expects. 1797 1811 const manifest = { ··· 1802 1816 version: entry.source?.version || '', 1803 1817 builtin: isBuiltin, 1804 1818 author: entry.source?.publisherName || entry.source?.publisher || '', 1819 + // Schema fields surface the per-feature Options accordion below. 1820 + // `schemas` / `storageKeys` / `defaults` are pulled out of the 1821 + // settings-schema.json that the manifest's `settingsSchema` field 1822 + // points at (loaded server-side via api.features.settingsSchema). 1823 + schemas: schemaJson ? { prefs: schemaJson.prefs, item: schemaJson.item } : null, 1824 + storageKeys: schemaJson?.storageKeys || { PREFS: 'prefs', ITEMS: 'items' }, 1825 + defaults: schemaJson?.defaults || {}, 1805 1826 }; 1806 1827 1807 1828 return {
+7 -2
docs/feed.xml
··· 4 4 <title>Peek Changelog</title> 5 5 <link>https://tangled.org/burrito.space/peek</link> 6 6 <description>Recent changes to Peek</description> 7 - <lastBuildDate>Sun, 26 Apr 2026 11:07:34 GMT</lastBuildDate> 7 + <lastBuildDate>Mon, 27 Apr 2026 10:08:51 GMT</lastBuildDate> 8 8 <generator>changelog-to-rss.js</generator> 9 9 <atom:link href="https://tangled.org/burrito.space/peek/raw/main/docs/feed.xml" rel="self" type="application/rss+xml"/> 10 10 <item> ··· 12 12 <link>https://tangled.org/burrito.space/peek/blob/main/CHANGELOG.md#2026-04-20</link> 13 13 <guid isPermaLink="false">https://tangled.org/burrito.space/peek#2026-04-20</guid> 14 14 <pubDate>Mon, 20 Apr 2026 12:00:00 GMT</pubDate> 15 - <description>Desktop - tag-action toggles + cmd panel hashtag shortcut 15 + <description>Desktop - per-feature Settings options accordion 16 + - Fix Settings → Features Options accordion never rendering: refreshFeaturesList built each feature&apos;s manifest object without a schemas field (the registry&apos;s flat entry shape doesn&apos;t carry it), so the accordion gate manifest.schemas &amp;&amp; (manifest.schemas.prefs || manifest.schemas.item) was always false. Now fetches each feature&apos;s settings-schema JSON in parallel via api.features.settingsSchema(id) and populates manifest.{schemas,storageKeys,defaults} from it. 17 + - Fix per-item card expand/collapse arrows missing inside the Options accordion (Peeks/Slides item cards rendered without ▶/▼). The .item-card.no-collapse .item-card-title::before { content: none } rule was a descendant selector — it cascaded into nested per-item card titles. Scoped to direct-child via .item-card.no-collapse &gt; .item-card-header .item-card-title::before. 18 + - New tests/desktop/feature-options.spec.ts — opens Settings → Features, expands Peeks Options accordion, asserts a .form-section renders + the inner peek card&apos;s ::before content is ▶ or ▼. 19 + 20 + Desktop - tag-action toggles + cmd panel hashtag shortcut 16 21 - Fix tag-action toggles never rendering on item cards in tags / groups / search / pagestream — add tag-actions:* (5 topics) to each consumer&apos;s pubsub.topics allowlist so the strict tile gate stops dropping the tag-actions:get-all round-trip 17 22 - Fix cold-start race in features/tag-actions/home.js: subscribe-before-publish was reachable when consumer tiles published their first tag-actions:get-all before the resident tag-actions tile had finished its loadSettings() await; tag-actions now broadcasts tag-actions:get-all:response once at end of init so any pre-existing subscriber catches up without a re-request 18 23 - New tests/desktop/tag-actions-toggles.spec.ts — full pipeline (toggle renders + click swaps todo → done) on tags/home, gate round-trip on groups/pagestream, manifest assertion for search
+111
tests/desktop/feature-options.spec.ts
··· 1 + /** 2 + * Regression: per-feature Options accordion in Settings → Features. 3 + * 4 + * Bug surfaced 2026-04-26: features with `settingsSchema` declared in 5 + * their manifest (e.g. peeks) had no expandable Options bit. Cause — 6 + * `app/settings/settings.js::refreshFeaturesList` constructed each 7 + * feature's `manifest` object without a `schemas` field, then gated the 8 + * accordion on `manifest.schemas && (manifest.schemas.prefs || manifest.schemas.item)`, 9 + * so the gate was always false. Fix: fetch each feature's schema via 10 + * `api.features.settingsSchema(id)` and populate `manifest.schemas` 11 + * (+ storageKeys + defaults) from it. 12 + */ 13 + import { test, expect, DesktopApp } from '../fixtures/desktop-app'; 14 + import { Page } from '@playwright/test'; 15 + import { createPerDescribeApp } from '../helpers/test-app'; 16 + 17 + test.describe('Settings - per-feature Options @desktop', () => { 18 + let app: DesktopApp; 19 + let bgWindow: Page; 20 + 21 + test.beforeAll(async () => { 22 + ({ app, bgWindow } = await createPerDescribeApp('feature-options')); 23 + }); 24 + 25 + test.afterAll(async () => { 26 + if (app) await app.close(); 27 + }); 28 + 29 + test('peeks feature card shows Options accordion + expanding renders prefs', async () => { 30 + // Open settings 31 + await bgWindow.evaluate(async () => { 32 + return await (window as any).app.window.open('peek://app/settings/settings.html', { 33 + width: 900, height: 700, 34 + }); 35 + }); 36 + 37 + const settingsWindow = await app.getWindow('settings/settings.html', 10000); 38 + await settingsWindow.waitForLoadState('domcontentloaded'); 39 + await settingsWindow.waitForSelector('#section-features', { state: 'attached', timeout: 10000 }); 40 + 41 + // Switch to Features section 42 + await settingsWindow.evaluate(() => { 43 + const link = Array.from(document.querySelectorAll('.nav-item')) 44 + .find(a => (a as HTMLElement).textContent === 'Features') as HTMLElement | undefined; 45 + link?.click(); 46 + }); 47 + 48 + // Wait for features list to populate (async after section render). 49 + // We look for the peeks card title text inside the features list. 50 + await settingsWindow.waitForFunction(() => { 51 + const titles = Array.from(document.querySelectorAll('#section-features .item-card-title')); 52 + return titles.some(t => (t.textContent || '').trim().startsWith('Peeks')); 53 + }, { timeout: 15000 }); 54 + 55 + // Find the peeks card and assert it has an Options toggle. 56 + const hasOptionsToggle = await settingsWindow.evaluate(() => { 57 + const cards = Array.from(document.querySelectorAll('#section-features .item-card')); 58 + const peeksCard = cards.find(c => { 59 + const title = c.querySelector('.item-card-title'); 60 + return title && (title.textContent || '').trim().startsWith('Peeks'); 61 + }); 62 + if (!peeksCard) return null; 63 + const toggle = peeksCard.querySelector('.ext-options-toggle'); 64 + return !!toggle; 65 + }); 66 + expect(hasOptionsToggle).toBe(true); 67 + 68 + // Click the Options toggle and confirm the content panel becomes visible 69 + // and renders at least one form-section (the prefs section from the schema). 70 + await settingsWindow.evaluate(() => { 71 + const cards = Array.from(document.querySelectorAll('#section-features .item-card')); 72 + const peeksCard = cards.find(c => { 73 + const title = c.querySelector('.item-card-title'); 74 + return title && (title.textContent || '').trim().startsWith('Peeks'); 75 + }); 76 + const toggle = peeksCard?.querySelector('.ext-options-toggle') as HTMLElement | null; 77 + toggle?.click(); 78 + }); 79 + 80 + await settingsWindow.waitForFunction(() => { 81 + const cards = Array.from(document.querySelectorAll('#section-features .item-card')); 82 + const peeksCard = cards.find(c => { 83 + const title = c.querySelector('.item-card-title'); 84 + return title && (title.textContent || '').trim().startsWith('Peeks'); 85 + }); 86 + const content = peeksCard?.querySelector('.ext-options-content') as HTMLElement | null; 87 + if (!content || content.style.display === 'none') return false; 88 + // Schema-driven content has at least one form-section (prefs or items). 89 + return !!content.querySelector('.form-section'); 90 + }, { timeout: 5000 }); 91 + 92 + // Per-item peek cards inside the Options accordion must show an 93 + // expand/collapse arrow (▶/▼) via .item-card-title::before. Regression 94 + // protection: a descendant `.item-card.no-collapse .item-card-title::before 95 + // { content: none }` rule used to cascade into these nested titles and 96 + // strip the arrow. 97 + const arrow = await settingsWindow.evaluate(() => { 98 + const cards = Array.from(document.querySelectorAll('#section-features .item-card')); 99 + const peeksCard = cards.find(c => { 100 + const title = c.querySelector('.item-card-title'); 101 + return title && (title.textContent || '').trim().startsWith('Peeks'); 102 + }); 103 + const content = peeksCard?.querySelector('.ext-options-content') as HTMLElement | null; 104 + const innerCard = content?.querySelector('.item-card'); 105 + const title = innerCard?.querySelector('.item-card-title') as HTMLElement | null; 106 + return title ? getComputedStyle(title, '::before').content : null; 107 + }); 108 + // CSS escape `\25B6 ` parses the trailing space as terminator → "▶". 109 + expect(['"▶"', '"▼"']).toContain(arrow); 110 + }); 111 + });