feat(extension): P1 — WorkspaceWarden lifecycle + position invariant

Replace the workspace-warden.ts stub with a real implementation matching
design §5:

- On enable: snapshot org.gnome.mutter::dynamic-workspaces and
  org.gnome.desktop.wm.preferences::num-workspaces, persist to
  ~/.local/state/dashward/workspace-warden.json, then disable dynamic
  mode and append a new workspace as the dashboard slot. num-workspaces
  is updated to match the new count so external observers stay in sync.
- Position invariant: connect to workspace-manager's workspaces-reordered,
  workspace-added, and workspace-removed signals; whenever the dashboard
  is no longer last, reorder it back. If something external removes the
  dashboard, append a replacement.
- Defensive guards: external changes to num-workspaces or
  dynamic-workspaces are clamped back; a `suppressGuard` flag avoids
  feedback loops between our own writes and our own signal handlers.
- On disable: remove the dashboard workspace, restore both gsettings,
  delete the state file.

Supporting infrastructure:

- util/logger.ts: console.log/warn/error wrappers with [Dashward] prefix.
- util/state-store.ts: load/save/clear JSON state under XDG_STATE_HOME.
- types/globals.d.ts: minimal Shell.Global declaration covering
  workspace_manager / display / get_current_time().

Build chain fixes uncovered while wiring P1:

- Replace placeholder @girs/* versions in extension/ and container/
  package.json with real published versions from npm (gnome-shell@50.0.0,
  meta-16@16.0.0-4.0.0, etc.). Add the required @girs/gio/glib/gobject
  packages so resolution actually succeeds.
- Set tsconfig `types` arrays to include each girs `/ambient` entry so
  the `gi://*` and `resource:///*` module specifiers resolve.
- Add `override` modifiers on Extension.enable/disable (required under
  the base tsconfig's noImplicitOverride).
- Fix workspace iteration to use get_workspace_by_index in a loop
  instead of the non-existent get_workspaces() — Meta 16 doesn't expose
  the bulk getter.

Verified: `pnpm -r build` and `pnpm -r exec tsc --noEmit` are both clean.
Functional verification against a real GNOME session is pending P2 — the
extension cannot be loaded yet because we haven't packaged it for the
test VM.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
h z
2026-05-22 23:05:10 +01:00
parent 3bf3aa1989
commit 7b5539cf75
10 changed files with 1561 additions and 23 deletions

View File

@@ -5,6 +5,7 @@ import { WindowGuard } from './window-guard.js';
import { ContainerSupervisor } from './container-supervisor.js';
import { EntryUX } from './entry-ux.js';
import { DBusService } from './dbus-service.js';
import { log, error } from './util/logger.js';
export default class DashwardExtension extends Extension {
private warden?: WorkspaceWarden;
@@ -13,22 +14,35 @@ export default class DashwardExtension extends Extension {
private entry?: EntryUX;
private dbus?: DBusService;
enable(): void {
log('[Dashward] enable: P0 skeleton — components not wired yet');
this.warden = new WorkspaceWarden();
this.guard = new WindowGuard();
this.container = new ContainerSupervisor();
this.entry = new EntryUX();
this.dbus = new DBusService();
override enable(): void {
log(`enable: ${this.metadata.uuid} v${this.metadata.version}`);
try {
this.warden = new WorkspaceWarden();
} catch (e) {
error(`WorkspaceWarden init failed: ${String(e)}`);
// Don't proceed if the warden didn't come up — everything else
// depends on the dashboard workspace existing.
return;
}
// P2+ components are still placeholders; left commented until each
// phase implements them so we don't pretend to be functional.
// this.guard = new WindowGuard();
// this.container = new ContainerSupervisor();
// this.entry = new EntryUX();
// this.dbus = new DBusService();
}
disable(): void {
log('[Dashward] disable');
override disable(): void {
log('disable');
this.dbus?.dispose();
this.entry?.dispose();
this.container?.dispose();
this.guard?.dispose();
this.warden?.dispose();
this.dbus = undefined;
this.entry = undefined;
this.container = undefined;
@@ -36,5 +50,3 @@ export default class DashwardExtension extends Extension {
this.warden = undefined;
}
}
declare function log(msg: string): void;