You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
First useful version of deckd's layout editor ships — icon picker, action editor, color picker, grid positioning writable from a desktop browser tab that saves to YAML and triggers hot-reload. The editor is a chrome view in the existing client, reusing the session view-pinning protocol (select_view/clear_view) to suspend auto-switching during editing.
Integrated into the existing client, not a separate app. Desktop-first UX, mobile-tolerant.
YAML files remain source of truth; editor writes to disk, daemon hot-reloads via watchfiles.
Use /grilling and /domain-modeling for decision tickets; /prototype for UX exploration; /research for fact-finding.
Blocking prerequisite: ADR-0010 (grid reflow) must be implemented before the editor can ship — it deletes the grid: [x,y,w,h] field and replaces it with ordered-list reflow + size: [w,h] spans. The editor's data model depends on this landing first. Reference: ADR-0010, mockup.
Decisions so far
V1 scope lock — Widget kinds: button, meter, stats. Button exposes id/label/icon/color/size/action (macro deferred). Meter exposes source/min/max. Stats exposes metrics list. Layout-level: match, display_name, theme, icon, jogstrip. Action: all four primitives. New widget defaults: kind=button, no defaults for icon/color/label. Amended per ADR-0010: grid → size span, drag-to-reorder list, viewport-preview pane.
YAML round-trip strategy — ruamel.yaml comment-preserving round-trip (not canonical model_dump()); files stay human-owned. Daemon holds the CommentedMap as edit surface, Pydantic validates only. Save = full-snapshot from client reconciled onto a fresh disk re-read, widgets matched by id (reorder/edit/add/delete, comments ride along); maps recurse, other sequences atomic. Add Layout-level duplicate-id validator; atomic temp-write + os.replace, natural watchfiles reload. New dep: ruamel.yaml. Delivers the mechanism Advanced field round-trip #89 needs; new-widget id generation + drag gesture deferred to Editor interaction model #87.
Daemon save_layout / create_layout API — write path is an HTTP endpoint, not a WS message (WS stays read/push-only). Ships save only: PUT /layouts/{id}, idempotent full-snapshot replace on the existing authed aiohttp surface. URL {id} authoritative — a match[0] change is rejected (409) as a rename; other match tokens editable. Validation failures → 400 with sanitized structured Pydantic errors (loc/msg/type only, strip input/ctx per the Core diagnostic surface for AI-assisted debugging #70 never-leak-payloads rule). Success → 200 echoing the canonical re-read ({ok,layout}); handler does not awaitwatchfiles — request/response owns editor sync, WS layout push owns live-deck render. create (POST /layouts) deferred to New-layout creation flow #88.
Editor interaction model — v1 ships Variant A only: a full-screen edit chrome view (palette · live-grid canvas · properties panel) — the complete editor (add/edit/reorder/delete), desktop-first. Variant B (inline tap-to-inspect inspector) ruled out of scope — edit-existing only, additive post-v1. Repositioning is in v1 via A's canvas; new-widget id = button-<n> minted by the palette, editable, backstopped by the YAML round-trip strategy #85 uniqueness validator. Prototype: editor-interaction.html (branch prototype/87-editor-interaction).
Icon picker research — icon ref is {source,name} over two registries: lucide (~1,756 glyphs, bundled ~198 KB, kebab-case) and simple-icons (~3,450 brand logos, lazy 2.1 MB chunk, slug). No enumeration API today but both enumerable client-side. v1 picker = search-driven grid of rendered glyphs with source tabs (Lucide immediate, Brands lazy-loaded on tab open), virtualized results, selected-chip showing the source/name written to YAML. Mockup: icon-picker.html (branch prototype/86-icon-picker). Follow-ups (virtualization lib, build-time name index) → component-architecture.
New-layout creation flow — editor opens edit-what-you-see (pinned via select_view) + secondary picker of all layouts. Create via detect-and-offer (app resolved to default → prompt, prefill match from app_id→wm_class) or manual "New layout". Create-on-first-save: in-memory draft, POST /layouts fires on first save with content; id/filename = slugified match[0], 409 on collision, mirrors Daemon save_layout / create_layout API #84's PUT contract. Browser case: is_browser → offer "browser" (match:[firefox]) or "this site" (match:[title:*<title>*] seeded from live title, user trims). "No layout yet" prompt is editor-only in v1. Draft abandon = clear_view, no file.
Advanced field round-trip — models are extra="forbid", so no unknown fields — just declared fields the v1 UI won't render (macro, media config, meter/stats sensor config). Preservation = client opaque pass-through: client edit model carries unrendered fields verbatim and echoes them in the full-snapshot PUT (keeps Daemon save_layout / create_layout API #84 full-replace; deletion = omission). UX = pure silent round-trip (no warn/toggle/badge). Unsupported-kind widgets (media, mediabrowser only) render as opaque placeholders — reorder/delete only, not palette-insertable. blank is editor-supported (takes only size; palette-insertable gap primitive) alongside button/meter/stats.
Overflow toggle in v1 scope? — ships in v1: two-value control (shrink-to-fit/clip) writing Layout.overflow. Near-zero cost (validated Literal, already round-tripped per Advanced field round-trip #89) and ADR-0010 names the toggle as an editor component. Semantics (per reflow.ts): cells are always uniform squares — a button is never partially cut; clip drops whole trailing widgets that exceed window height, shrink-to-fit shrinks all cells uniformly (16px floor) so all fit.
Reconsider the overflow default — currently shrink-to-fit (layouts.py:317); whether the default should be clip (drop-extras) is a daemon/ADR-0010 decision surfaced by Overflow toggle in v1 scope? #95, separate from the editor toggle. Doesn't block the editor.
Out of scope
Macro editor
Media widget config editor
Multi-widget copy/paste
Undo system
Inline tap-to-inspect inspector (Variant B) — on-device quick-edit popover; edit-existing only, can't add/delete/reorder. Additive polish on top of the v1 full-screen editor; revisit post-v1.
Layout deletion — removing a layout file from the editor; separate flow from creation (New-layout creation flow #88), not in v1.
Passive live-deck "no layout" hint — surfacing "this app has no layout" on the operational deck (not just inside the editor); post-v1 additive (New-layout creation flow #88 Q5).
Destination
First useful version of deckd's layout editor ships — icon picker, action editor, color picker, grid positioning writable from a desktop browser tab that saves to YAML and triggers hot-reload. The editor is a chrome view in the existing client, reusing the session view-pinning protocol (
select_view/clear_view) to suspend auto-switching during editing.Tickets
Notes
watchfiles./grillingand/domain-modelingfor decision tickets;/prototypefor UX exploration;/researchfor fact-finding.grid: [x,y,w,h]field and replaces it with ordered-list reflow +size: [w,h]spans. The editor's data model depends on this landing first. Reference: ADR-0010, mockup.Decisions so far
grid→sizespan, drag-to-reorder list, viewport-preview pane.ruamel.yamlcomment-preserving round-trip (not canonicalmodel_dump()); files stay human-owned. Daemon holds theCommentedMapas edit surface, Pydantic validates only. Save = full-snapshot from client reconciled onto a fresh disk re-read, widgets matched byid(reorder/edit/add/delete, comments ride along); maps recurse, other sequences atomic. Add Layout-level duplicate-id validator; atomic temp-write +os.replace, naturalwatchfilesreload. New dep:ruamel.yaml. Delivers the mechanism Advanced field round-trip #89 needs; new-widget id generation + drag gesture deferred to Editor interaction model #87.PUT /layouts/{id}, idempotent full-snapshot replace on the existing authed aiohttp surface. URL{id}authoritative — amatch[0]change is rejected (409) as a rename; other match tokens editable. Validation failures →400with sanitized structured Pydantic errors (loc/msg/typeonly, stripinput/ctxper the Core diagnostic surface for AI-assisted debugging #70 never-leak-payloads rule). Success →200echoing the canonical re-read ({ok,layout}); handler does notawaitwatchfiles— request/response owns editor sync, WSlayoutpush owns live-deck render.create(POST /layouts) deferred to New-layout creation flow #88.editchrome view (palette · live-grid canvas · properties panel) — the complete editor (add/edit/reorder/delete), desktop-first. Variant B (inline tap-to-inspect inspector) ruled out of scope — edit-existing only, additive post-v1. Repositioning is in v1 via A's canvas; new-widget id =button-<n>minted by the palette, editable, backstopped by the YAML round-trip strategy #85 uniqueness validator. Prototype:editor-interaction.html(branchprototype/87-editor-interaction).{source,name}over two registries: lucide (~1,756 glyphs, bundled ~198 KB, kebab-case) and simple-icons (~3,450 brand logos, lazy 2.1 MB chunk, slug). No enumeration API today but both enumerable client-side. v1 picker = search-driven grid of rendered glyphs with source tabs (Lucide immediate, Brands lazy-loaded on tab open), virtualized results, selected-chip showing thesource/namewritten to YAML. Mockup:icon-picker.html(branchprototype/86-icon-picker). Follow-ups (virtualization lib, build-time name index) → component-architecture.select_view) + secondary picker of all layouts. Create via detect-and-offer (app resolved todefault→ prompt, prefillmatchfromapp_id→wm_class) or manual "New layout". Create-on-first-save: in-memory draft,POST /layoutsfires on first save with content; id/filename = slugifiedmatch[0],409on collision, mirrors Daemon save_layout / create_layout API #84'sPUTcontract. Browser case:is_browser→ offer "browser" (match:[firefox]) or "this site" (match:[title:*<title>*]seeded from live title, user trims). "No layout yet" prompt is editor-only in v1. Draft abandon =clear_view, no file.extra="forbid", so no unknown fields — just declared fields the v1 UI won't render (macro, media config, meter/stats sensor config). Preservation = client opaque pass-through: client edit model carries unrendered fields verbatim and echoes them in the full-snapshotPUT(keeps Daemon save_layout / create_layout API #84 full-replace; deletion = omission). UX = pure silent round-trip (no warn/toggle/badge). Unsupported-kind widgets (media,mediabrowseronly) render as opaque placeholders — reorder/delete only, not palette-insertable.blankis editor-supported (takes onlysize; palette-insertable gap primitive) alongsidebutton/meter/stats.shrink-to-fit/clip) writingLayout.overflow. Near-zero cost (validatedLiteral, already round-tripped per Advanced field round-trip #89) and ADR-0010 names the toggle as an editor component. Semantics (perreflow.ts): cells are always uniform squares — a button is never partially cut;clipdrops whole trailing widgets that exceed window height,shrink-to-fitshrinks all cells uniformly (16px floor) so all fit.Not yet specified
overflowdefault — currentlyshrink-to-fit(layouts.py:317); whether the default should beclip(drop-extras) is a daemon/ADR-0010 decision surfaced by Overflow toggle in v1 scope? #95, separate from the editor toggle. Doesn't block the editor.Out of scope