Bootstrap the Dashward repo per arch/UBUNTU-DASHBOARD-SPACE.md: - pnpm-workspaces monorepo (sdk, extension, container, widgets-builtin/*) - GNOME extension stub (metadata.json, src/*.ts placeholders for warden, guard, supervisor, entry UX, DBus service) - WebKit container stub (GJS main + page-side runtime + dashboard.html) - TypeScript widget SDK (defineWidget + types) - Builtin clock widget as the first SDK consumer example - DBus interface XML (proto/shell.iface.xml) and shared types - esbuild configs for extension and container; tsc for SDK - Design doc copied in at repo root for discoverability No functional logic yet -- all components are placeholders that compose in extension.ts so the build chain can be exercised. P1 (workspace warden) starts next. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
29 KiB
Ubuntu Dashboard Space — Design
Status: design. Goal: a macOS-Dashboard-style dedicated rightmost workspace on GNOME/Ubuntu that hosts user-defined widgets. Widgets are written against a TypeScript SDK and rendered as web components inside an embedded WebKit view; the GNOME Shell extension is the workspace warden and the IPC bridge, nothing heavier.
Codename: Dashward (decided 2026-05-22; see §19).
1. Goals & non-goals
Goals
- A workspace that is created by the extension at enable time, lives at the rightmost position, and is destroyed at disable time. It is not one of the user's existing workspaces, even temporarily.
- Regular windows cannot enter it — neither by overview drag, by
super+shift+arrow, bywmctrl/xdotool, nor by being newly mapped there. Attempts get bounced to the prior workspace immediately. - Widgets are web components in TS. A buggy widget cannot take down GNOME Shell.
- Hot-reload during widget development.
- Light/dark theme follows the system.
- Zero hand-edits to user dotfiles outside
~/.local/share/dashward/and the extension install dir.
Non-goals (v1)
- Cross-distro support beyond Ubuntu 25.04 / GNOME 48. KDE, XFCE, Cinnamon are out of scope until v1 lands and stabilizes.
- A widget marketplace, signing, or sandboxing beyond what WebKit's process model already provides. Widgets are trusted code at v1.
- macOS-style "ripple in" widget add animation. Edit mode is utilitarian.
- Multi-monitor span: dashboard appears on the primary monitor only; other monitors get the regular last-workspace view.
- Replacing the regular workspace switcher UX. The dashboard is reached by a dedicated gesture/shortcut, not by being workspace N+1 in the existing switcher.
2. Hard constraints (carried from chat)
| C# | Constraint | Resolution |
|---|---|---|
| C1 | Dashboard does not reuse any existing workspace | §5: append a new workspace at enable, mark it, remove at disable |
| C2 | Dashboard is always rightmost | §5: position invariant maintained on every workspace mutation |
| C3 | Regular windows cannot be dragged into dashboard | §6: layered defense on window-added, overview drop target, gesture handlers |
| C4 | Widgets in TypeScript | §9: SDK is a TS package; widgets are TS modules bundled to ESM |
| C5 | Avoid the GridSpaces lockout failure mode | §17: VM-only iteration loop; extension never touches gnome-extensions enable on host until VM-green |
3. Reality check (probed 2026-05-22 on ubuntu2504-test)
| Component | Version | Note |
|---|---|---|
| Ubuntu | 25.04 (Plucky) | |
| GNOME Shell | 48.0-1ubuntu1 | ESM extension format |
| Mutter | 48.1 (libmutter-16-0) |
API gen 16, Meta.WorkspaceManager stable |
| gjs | 1.82.1 | SpiderMonkey 128 era — ESM, top-level await OK |
| WebKit2GTK 4.1 (GTK3) | 2.50.4 + gir1.2-webkit2-4.1 pre-installed |
Chosen for the container — zero new GIR deps |
| WebKitGTK 6.0 (GTK4) | 2.50.4 lib present, GIR not installed | Migration target post-v1 |
| Session | Wayland (gdm Ubuntu on Wayland default) |
|
| Pre-existing ext | ding, tiling-assistant, ubuntu-appindicators, ubuntu-dock |
tiling-assistant is the relevant precedent for window manipulation |
Implication: we can build v1 with only what's already on disk — no new apt packages required. (Build chain is in dev only; not shipped onto the VM at runtime.)
4. Architecture
Three processes, two IPC channels:
┌─────────────────────────────────────────────────────────────────────────┐
│ gnome-shell (GJS, in-process) │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Dashward extension │ │
│ │ • WorkspaceWarden (create/destroy + position invariant) │ │
│ │ • WindowGuard (block window-added on dashboard ws) │ │
│ │ • EntryUX (gesture, shortcut, panel indicator) │ │
│ │ • ContainerSupervisor (spawn/respawn web container, pin window) │ │
│ │ • DBusService "top.hangmanlab.Dashward.Shell" │ │
│ └─────────┬────────────────────────────────────────────────────────┘ │
└────────────┼────────────────────────────────────────────────────────────┘
│ DBus session bus
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ dashward-container (GJS + WebKit2 4.1, separate process) │
│ • One GtkWindow, fullscreen, undecorated, app_id = container │
│ • WebKitWebView loads file://…/dashboard.html │
│ • DBus client: pushes shell events into the page via │
│ UserContentManager.evaluate_javascript() │
│ • UserContentManager script-message handlers receive page→shell calls │
└─────────────────────────────────────────────────────────────────────────┘
│ WebKit user-content-manager bridge
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ dashboard.html (WebKit content process, sandboxed by WebKit) │
│ • Runtime: dashboard-runtime.js (host shell, layout grid, edit mode) │
│ • SDK: @dashward/widget-sdk (TS, bundled in) │
│ • Widgets: each loaded into its own <iframe> for crash isolation │
└─────────────────────────────────────────────────────────────────────────┘
Why the container is its own process: GJS code crashing inside
gnome-shell kills the session. Container is forked under Gio.Subprocess;
when it crashes, supervisor sees the exit, hides the dashboard slot's
content, and respawns with backoff. Shell stays up.
Why widgets are in <iframe>s and not just <div>s: a widget that
loops, throws unhandled, or leaks memory only affects its iframe — the host
page and other widgets keep running. Iframes also give us a clean origin
boundary for widget asset paths.
5. Workspace lifecycle
Enable
- Snapshot user state:
org.gnome.mutter dynamic-workspaces→ save under~/.local/state/dashward/saved-settings.json.org.gnome.desktop.wm.preferences num-workspaces→ save.- Current workspace count
N(fromMeta.WorkspaceManager).
- Set
dynamic-workspaces = false(we need a fixed slot at the end that doesn't get GC'd when empty). - Set
num-workspaces = N + 1. - Take the new last workspace (index
N) as the dashboard slot. Tag it: stash itsMeta.Workspacereference underwm._dashSpaceWsand record the workspace's index inWorkspaceWarden.index. - Spawn the container; pin its window to
wm._dashSpaceWs(see §7). - Install signal handlers (§6).
Steady state — invariants
After every workspace mutation (workspace appended/removed/reordered) the warden re-checks:
WorkspaceWarden.wsstill exists inWorkspaceManager.get_workspaces(). If not (something forcibly removed it), recreate at the end. Log.- Its index equals
WorkspaceManager.get_n_workspaces() - 1. If not, move it to the end viareorder_workspace(ws, N-1).
Disable
- Kill container.
- Remove the dashboard workspace via
WorkspaceManager.remove_workspace(ws, current_time). - Restore
num-workspacesanddynamic-workspacesto snapshotted values. - Disconnect all signals; clear
wm._dashSpaceWs.
Edge cases
- User changes
num-workspacesexternally while extension is enabled: warden treats this as the new "N", appends dashboard at N (effectivelynum-workspaces = newN + 1). Saved setting is updated. - User logs out / shell restart (Wayland: only logout; X11: alt-F2
r):disable()is called by Shell. If we somehow miss it (crash), a best-effort cleanup runs on nextenable(): detect workspace tagged with our marker (Meta.Workspace.get_work_area_all_monitorsis not enough; we tag via a sticky window — see §7) and remove. - Other extensions also manage workspaces (e.g. tiling-assistant adjusts but doesn't add/remove; should be compatible. WorkspaceMatrix would conflict — flag in README as incompatible).
6. Window-defense rules
Layer 1: window-added bounce-back
ws.connectObject('window-added', (_, window) => {
if (window === containerWindow) return; // whitelist
if (window.is_override_redirect()) return; // tooltip, menu, etc.
const fallback = priorWorkspaceFor(window);
window.change_workspace(fallback);
}, this);
priorWorkspaceFor heuristic: the window's get_user_time() workspace if
known, else workspace N-1 (dashboard index minus 1), else workspace 0.
This single hook catches:
- new windows mapped to dashboard (
window-createdrace: even if the window initially picks dashboard via_NET_WM_DESKTOP,window-addedfires immediately when it joins); - programmatic moves via
wmctrl -r ... -t N,xdotool windowmove,Meta.Window.change_workspace; - keyboard
super+shift+→on the rightmost workspace.
Layer 2: overview drop refusal
In the activities overview, workspace thumbnails are drop targets via
WorkspaceThumbnail + _acceptDrop. We monkey-patch the dashboard
thumbnail's acceptDrop to return false (and the visual to show "no
drop" cursor). Cleaner than letting Layer 1 bounce it back after the visual
drop completed.
Layer 3: hide from regular switcher
The dashboard workspace renders specially in the overview: instead of the normal thumbnail, we draw a placeholder card with the Dashward icon and "Dashboard" label. This is purely cosmetic (Layers 1+2 already enforce); the goal is user clarity — they don't try to drag, because the slot visibly isn't a regular workspace.
Implementation: override WorkspaceThumbnail for the specific workspace
via WorkspacesView._workspaces[N-1] patching, or simpler: hide it from
the thumbnail strip entirely. The dashboard is accessed via gesture /
shortcut (§14), not by clicking its thumbnail. Default: hide from strip.
Layer 4: workspace switcher (Super+Page_Up/Down)
By default this cycles through 0..N. We want Super+Page_Down on the last
real workspace to not land on dashboard. Implementation: hook
Meta.KeyBindingAction.WORKSPACE_DOWN and clamp target to N-1. (User
still gets to dashboard via dedicated gesture/shortcut.)
7. Web container process
Window properties
- Wayland app_id:
top.hangmanlab.dashward.container(set viaGLib.set_prgname+Gtk.Window.set_default_icon_name— for Wayland app_id detection on the Shell side, we identify byMeta.Window.get_gtk_application_id()). - Title:
Dashward. - Decorations: none (
set_decorated(false)). - Fullscreen on the primary monitor.
- Type hint:
NORMAL(notDOCK, since dock windows skip workspaces). - Stickiness: NOT sticky; it must belong to the dashboard workspace only.
Lifetime managed by ContainerSupervisor
spawn() → wait for window-created signal → match by app_id →
move to dashboardWs (Layer 1 whitelist needed!) → record windowId →
WebKit ready → first eval: bootstrap dashboard-runtime.js
Crash recovery: restart with exponential backoff (1s, 2s, 5s, 10s, then
give up and surface error in panel indicator). The container's stderr
streams to ~/.local/state/dashward/container.log.
Visibility model
The container window always exists on the dashboard workspace; user "opens dashboard" = switching to that workspace; "closes" = switching away. No need to map/unmap the window per visit — Mutter culls it from rendering when its workspace isn't active. This is the same model the overview uses for inactive workspace windows.
Exception: on disable / shell-shutdown, container is gracefully asked to quit via DBus, then SIGTERM after 2s.
8. IPC layers
8.1 Shell ↔ Container (DBus)
Bus name: top.hangmanlab.Dashward.Shell (owned by the extension).
Object path: /top/hangmanlab/Dashward. Interface methods:
| Method | Direction | Purpose |
|---|---|---|
Ready() |
container → shell | container finished bootstrap; safe to enter edit mode |
RequestEnterDashboard() |
container → shell | "switch to dashboard workspace" (used by <a href> clicks etc.) |
RequestExitDashboard() |
container → shell | "switch to previous workspace" |
GetSystemInfo() → a{sv} |
container → shell | brightness, battery, network, theme |
WidgetIPC(widgetId, method, args) |
container → shell | namespaced widget-to-shell calls; routed by SDK |
Signals (shell → container):
| Signal | Payload | Purpose |
|---|---|---|
ThemeChanged |
s ("light"/"dark") |
system theme toggle |
EditModeRequested |
b (enter/exit) |
extension's panel button or keybind |
SystemInfoChanged |
a{sv} (partial) |
battery/network/etc updates |
8.2 Container GJS ↔ WebKit page (UserContentManager)
GJS-side container exposes script-message handlers:
ucm.register_script_message_handler('shellCall'); // page → GJS
ucm.register_script_message_handler('log'); // widget logs to GJS stderr
Page-side bridge:
window.__dashShell__ = {
call(method, args) {
return new Promise((resolve, reject) => {
const id = nextId();
pending.set(id, { resolve, reject });
webkit.messageHandlers.shellCall.postMessage({ id, method, args });
});
}
};
Reverse (GJS → page): webView.evaluate_javascript(...).
8.3 Page ↔ widget iframe
window.postMessage with the SDK's wrapper. SDK injects into each iframe a
preload script that exposes:
const dashboard = {
config: { get(): T, set(T): Promise<void>, onChange(cb) },
system: { onThemeChange(cb), getTheme(): 'light'|'dark', /* … */ },
lifecycle: { onMount(cb), onUnmount(cb), onVisibilityChange(cb) },
shell: { call(method, args): Promise<any> }, // gated by manifest permissions
};
9. Widget SDK
Package: @dashward/widget-sdk (TypeScript, ESM, distributed as both
source-for-bundle and as a CDN-style local file the runtime can load).
Authoring API
import { defineWidget } from '@dashward/widget-sdk';
interface ClockConfig { hour12: boolean; showSeconds: boolean }
export default defineWidget<ClockConfig>({
id: 'clock',
defaultConfig: { hour12: false, showSeconds: true },
mount(host, { config, system, lifecycle }) {
const root = host.attachShadow({ mode: 'open' });
root.innerHTML = `<div class="clock"></div>`;
const tick = () => { /* render */ };
const timer = setInterval(tick, 1000);
lifecycle.onUnmount(() => clearInterval(timer));
config.onChange(() => tick());
system.onThemeChange(t => root.host.dataset.theme = t);
},
configUI(host, { config }) {
// optional: render a settings panel for edit mode
},
});
Surfaces:
| API | Capability |
|---|---|
config.get/set/onChange |
persistent JSON config; auto-saved to ~/.local/share/dashward/widgets/<id>/config.json |
system.getTheme/onThemeChange |
follow Shell theme |
system.getBattery/getNetwork/getBrightness |
shell DBus relay (requires manifest permission) |
lifecycle.onMount/onUnmount/onVisibilityChange |
called by host when widget appears/disappears |
shell.call(method, args) |
escape hatch to namespaced shell DBus calls (requires permission) |
What's not exposed: file system, network sockets, arbitrary
Gio/Meta access. Widgets that need that file an upstream issue.
10. Widget manifest
widget.json at the widget root:
{
"id": "clock",
"name": "Clock",
"version": "0.1.0",
"entry": "dist/index.js",
"icon": "icon.svg",
"size": { "w": 2, "h": 2, "minW": 1, "minH": 1, "maxW": 4, "maxH": 4 },
"permissions": ["timer"],
"configSchema": { "hour12": "boolean", "showSeconds": "boolean" }
}
Permissions vocabulary v1: timer, battery, network, brightness,
notifications. Permission grant is currently implicit (manifest declares
it, runtime allows it); a user-visible grant UI is v2.
Widget directory layout:
widgets/clock/
├── widget.json
├── src/
│ └── index.ts
├── dist/
│ └── index.js # esbuild-bundled, ESM, references SDK as bare import
└── icon.svg
Install location at runtime:
~/.local/share/dashward/widgets/
├── clock/ # user's widgets (drop a dir here, runtime sees it)
├── weather/
└── …
Plus a built-in pack shipped with the extension at
extension/widgets-builtin/ (clock, calendar, system-stats, notes).
11. Layout & edit mode
Grid
- 12 columns wide; rows grow vertically as needed.
- Cell aspect ratio: 1:1 at the canonical size of
floor(viewportWidth / 12)px. So a widget withsize: { w: 3, h: 2 }occupies 3 cols × 2 rows = 3w × 2h grid cells, naturally responsive. - Widgets snap to grid; no free positioning in v1.
Storage
User layout in ~/.local/share/dashward/layout.json:
{
"widgets": [
{ "instanceId": "clock-1", "widgetId": "clock", "x": 0, "y": 0, "w": 2, "h": 2 },
{ "instanceId": "weather-1", "widgetId": "weather", "x": 2, "y": 0, "w": 3, "h": 2 }
]
}
Multiple instances of the same widget are allowed (hence instanceId vs
widgetId); each has its own config file scoped by instanceId.
Edit mode
Triggered by:
- Top-edge floating button (visible only when dashboard is active).
- Keyboard
ewhile on dashboard. - Long-press on any widget.
In edit mode: widgets show resize handles + delete X; a "+" tray slides
in from the right with available widgets; drag to add. Exit on Esc or
clicking outside.
Out of edit mode: widgets are passive; no drag/resize.
12. Theming
- Container reads
org.gnome.desktop.interface color-schemeat startup. - Shell subscribes to GSettings changes; pushes
ThemeChangedDBus signal to container; container doesdocument.documentElement.dataset. theme = 'dark' | 'light'. - SDK exposes
system.onThemeChange(cb); widgets style via CSS[data-theme="dark"]attribute selectors (no JS theming, CSS does it). - Background is a configurable color + optional wallpaper image (defaults to a desaturated copy of the user's desktop wallpaper).
13. Entry / exit UX
Three ways in:
| Path | Default binding | Implementation |
|---|---|---|
| Gesture | 4-finger swipe right past last workspace | hook Shell.WorkspacesView._swipeTracker; on end with target > N-1, jump to dashboard |
| Keyboard | Super+Grave |
Meta.KeyBinding via Shell.ActionMode.NORMAL |
| Panel button | left-click Dashward indicator | PanelMenu.Button with custom icon |
All three call the same EntryUX.openDashboard() which animates the
workspace switch via existing Shell APIs.
Exit: same gesture/shortcut/button toggles. Plus Esc while on dashboard
(but only if no widget has focus capturing it).
14. Persistence & permissions
Filesystem layout (all under XDG_* standards):
~/.local/share/dashward/
├── layout.json # grid layout
├── widgets/<instanceId>/config.json # per-instance config
└── widgets-installed/<widgetId>/ # user-installed widget bundles
~/.local/state/dashward/
├── saved-settings.json # gsettings snapshot for restore
├── container.log
└── extension.log
~/.cache/dashward/ # WebKit cache, regenerable
Nothing else outside the extension's install directory is touched.
Permissions today: declared in manifest, allowed by runtime. v2: a first-run prompt per widget; revocable from edit mode.
15. Directory layout (the repo)
Dashward/
├── extension/ # GNOME Shell extension
│ ├── metadata.json # GNOME-mandated, shell-version: ["48"]
│ ├── src/
│ │ ├── extension.ts # entry, exports default class
│ │ ├── workspace-warden.ts
│ │ ├── window-guard.ts
│ │ ├── container-supervisor.ts
│ │ ├── entry-ux.ts
│ │ ├── dbus-service.ts
│ │ └── prefs.ts # GNOME Extension prefs window
│ ├── dist/ # esbuild output, single extension.js
│ ├── widgets-builtin/ # ships with extension
│ │ ├── clock/
│ │ ├── calendar/
│ │ ├── system-stats/
│ │ └── notes/
│ └── tsconfig.json
│
├── container/ # WebKit kiosk process
│ ├── src/
│ │ ├── main.ts # GJS bootstrap
│ │ ├── window.ts # GtkWindow + WebKitWebView
│ │ ├── dbus-client.ts
│ │ └── bridge.ts # UserContentManager wiring
│ ├── runtime/ # injected into the page
│ │ ├── runtime.ts # grid host, edit mode, widget loader
│ │ ├── dashboard.html
│ │ └── styles.css
│ └── dist/
│
├── sdk/ # @dashward/widget-sdk
│ ├── src/
│ │ ├── index.ts
│ │ ├── define-widget.ts
│ │ ├── config.ts
│ │ ├── system.ts
│ │ └── types.ts
│ ├── package.json
│ └── tsconfig.json
│
├── widgets/ # example/3rd-party widgets dev tree
│ └── … # each is a self-contained TS project
│
├── proto/ # DBus xml + shared TS types
│ ├── shell.iface.xml
│ └── types.ts
│
├── scripts/
│ ├── dev-vm.sh # rsync to VM + reload extension
│ ├── package.sh # produces .shell-extension.zip
│ └── lint.sh
│
└── README.md
16. Build chain
Single root package.json + pnpm workspaces (sdk, extension,
container, widgets/*).
| Subproject | Bundler | Output |
|---|---|---|
sdk |
tsc | ESM, types, published as workspace dep |
extension |
esbuild | single dist/extension.js (GNOME loads one file) |
container |
esbuild | single dist/main.js (GJS-friendly ESM) |
| each widget | esbuild | dist/index.js, SDK marked external (loaded by runtime) |
GJS specifics (carried from your project_fabric_esm habit):
"module": "NodeNext"in tsconfig; explicit.jsimport suffixes; default-import any CJS dep.Gio/Metaetc. imported viagi://URLs.- Type stubs:
@girs/gnome-shell,@girs/meta-16,@girs/gtk-3.0,@girs/webkit2-4.1.
Dev cycle:
pnpm dev # watches src/, rebuilds extension+container+widgets
pnpm vm:sync # rsync dist → VM, restart shell (or just kill container)
Inside the VM, a small helper script (~/dashward-dev/reload.sh):
- Wayland: re-enable extension via
gnome-extensions disable && enable(a full Shell restart on Wayland requires logout — see §17). - Container-only changes:
pkill dashward-container; supervisor respawns.
17. Implementation phases
| Phase | Deliverable | Acceptance |
|---|---|---|
| P0 | Repo skeleton, build chain, hello-world extension that logs on enable | extension shows in gnome-extensions list on VM |
| P1 | WorkspaceWarden in isolation: appends/removes ws, position invariant | manual: enable → see N+1 workspaces; disable → back to N; mutations preserved |
| P2 | WindowGuard | manual: try wmctrl -r foo -t <dashboard>, super+shift+→, overview drag — all bounce |
| P3 | Container spawn + WebKit window + pin to dashboard | container window appears on dashboard ws only |
| P4 | DBus + WebKit bridge round-trip | dashboard.system.getTheme() returns shell-side value |
| P5 | Runtime: grid host, layout.json load/save, one builtin widget (clock) | clock renders, persists across enable cycles |
| P6 | SDK + 3 more builtins (calendar, system-stats, notes) | each widget works in isolation; iframe crash containment verified |
| P7 | Edit mode (add/remove/resize/rearrange) | layout edits persist; instances scoped configs |
| P8 | Entry UX (gesture, shortcut, panel button) | all three paths open dashboard |
| P9 | Polish: theming, animations, error surfaces | passes a manual checklist |
| P10 | Stretch: WebKit 6.0 migration, multi-monitor, widget marketplace stub | post-v1 |
Critical: P1, P2, P3 all run only on the VM (project_gridspaces_test_vm). The host's GNOME Shell never sees the extension until P9 and only via gnome-extensions install from a packaged zip — never via a ~/.local/share/gnome-shell/extensions/ symlink, which would survive logout and lock us out as in project_gridspaces_lockout.
18. Risks & open questions
Risks
| R# | Risk | Mitigation |
|---|---|---|
| R1 | Wayland shell restart requires logout — slow iteration | Container hot-reload covers 80%; for extension changes, accept the logout cost; consider a nested Wayland session (mutter --nested) for ultra-fast iteration in P5+ |
| R2 | dynamic-workspaces = false is intrusive — user may rely on dynamic behavior |
Restore on disable; document in README; consider an "advanced mode" later that keeps dynamic mode and races the GC, but not v1 |
| R3 | Overview shows the dashboard slot oddly when hidden from strip | Spend a P9 day on the cosmetic; if upstream WorkspacesView proves fragile across GNOME minor versions, fall back to "show the slot with a placeholder thumbnail" |
| R4 | WebKitGTK 4.1 (GTK3) is older — long-term deprecation risk | Migration path: swap container to GTK4 + WebKitGTK 6.0 (install gir1.2-webkit-6.0). API surface is similar; isolated to container/, won't touch SDK or widgets |
| R5 | Widget IPC permissions are honor-system in v1 | Acceptable for personal-use v1; v2 adds first-run prompt |
| R6 | tiling-assistant or other extensions interact with workspace count | Document incompatibility list in README; test against default Ubuntu 25.04 extension set on VM during P1 |
| R7 | A misbehaving extension load could crash shell on enable | Wrap entire enable() in try/catch; on first-throw, write error to ~/.local/state/dashward/extension.log and bail rather than partial-init |
Open questions
| Q# | Question | Status |
|---|---|---|
| Q1 | Project name | Decided 2026-05-22: Dashward |
| Q2 | Where does the repo live? | Decided 2026-05-22: local ~/dev/Dashward/; remote = https://git.hangman-lab.top/hzhang/Dashward.git (Gitea on server.t0) |
| Q3 | Should widgets be able to span the entire dashboard (full-bleed)? | Open — no in v1; revisit |
| Q4 | Multi-monitor: dashboard on primary only, or per-monitor? | Open — primary only in v1 |
| Q5 | Should Super+Grave clash check happen at install or runtime? |
Open — runtime check on enable(); if taken, fall back to no shortcut and warn |
| Q6 | Widget config schema validation (Zod? JSON Schema? none?) | Open — none in v1; SDK trusts the widget's TS types |
| Q7 | Do we ship a widget devtools page (live reload of widget under dev)? | Open — P10 stretch |
19. Naming, hosting & next actions
Codename: Dashward (decided 2026-05-22).
Local working tree: ~/dev/Dashward/. Not under HangmanLab — Dashward is a personal tool, orthogonal to the lab estate.
Remote: Gitea on server.t0 at https://git.hangman-lab.top/hzhang/Dashward.git. Created when the P0 skeleton is ready to push. Per project-server-topology t0 hosts Git + auth; per project-local-sim-env the local-sim hijack of *.hangman-lab.top is opt-in (set up by setup-sim.sh) — when the hijack is off (default state), git.hangman-lab.top resolves to prod, which is what we want for pushing Dashward.
Next concrete step:
- ✅ Q1, Q2 decided.
- Create the repo skeleton on disk (§15) — empty files, working
pnpm install, no logic. - Create the Gitea repo (via Gitea API or web UI) and
git push -u origin main. - P0 hello-world extension on the VM, prove the dev loop end-to-end.
- Begin P1.
See project-gridspaces-lockout and project-gridspaces-test-vm for the testing discipline this project inherits.