Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4744b91
chore(backlog): mark item 249 as implementing
claude May 28, 2026
5e2f1e1
feat(261): Phase 1 — call-site + deletion-safety inventory
claude May 28, 2026
8d95eff
feat(261): Phase 2 schema — consolidate value types, SystemStatePrope…
claude May 28, 2026
fc2fcdf
test(261): golden fixtures + schema adherence + cross-language round-…
claude May 28, 2026
e9aa629
feat(261): shared SystemState helper + converters + unit tests (Phase…
claude May 28, 2026
25b3b1f
feat(261): dirty-tracking contract — view-state is exploration (FR-01…
claude May 28, 2026
1dda661
feat(261): VS Code host wiring — FC-based view-state load/save, sidec…
claude May 28, 2026
572e281
feat(261): web-shell host wiring + shared store-bridge (FR-009a/FR-007)
claude May 28, 2026
3b5dc58
feat(261): delete the .debrief-session sidecar (US2, Phase 6)
claude May 28, 2026
63cfbb6
test(261): web-shell self-describing round-trip E2E + real screenshots
claude May 28, 2026
9657b37
docs(261): evidence — test-summary, usage-example, round-trip proof, …
claude May 28, 2026
1eaddec
docs(261): ADR-034/035/036 + issues.md completion log
claude May 28, 2026
becabc8
feat(261): web-shell self-describing FC via reactive memo (no current…
claude May 28, 2026
8f39007
docs(261): add animated interaction.gif (T105) + reference it in the …
claude May 28, 2026
d335c81
style(261): ruff format test_system_state_adherence.py (CI ruff forma…
claude May 28, 2026
076b35f
chore(261): mark tasks.md — implementation, evidence, verify, PR comp…
claude May 28, 2026
bcc1255
test(web-shell): fix pre-existing tool-registry drift (unblocks CI Te…
claude May 29, 2026
0fea52f
test(web-shell): quarantine viewport-lock:103 (real-Chromium failure)…
claude May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ Description formats:

| ID | Category | Description | V | M | A | Total | Complexity | Status | Epic | Created | Updated |
|----|----------|-------------|---|---|---|-------|------------|--------|------|---------|---------|
| 274 | Bug | [Viewport lock holds under real Chromium — un-suppress `viewport-lock:103`](specs/274-viewport-lock-real-chromium/spec.md) — the spec-260 acceptance test `viewport-lock.spec.ts › locked map gestures (drag, scroll) leave viewport unchanged` was quarantined via `test.fixme` during PR #651 (spec 261) to keep CI green. It passes under the cloud `@sparticuz/chromium` headless build but **fails under CI's real Chromium**: a locked map still scroll-wheel-zooms (observed 10→12) and drag-pans, so #260's gesture-disable is not holding in that environment. Runs in two merge-gating jobs (`Web-Shell E2E`, `Test & Lint`→`web-shell-pw`). Fix = reproduce under real Chromium, fix the `MapView`/`viewportLocked` handler-disable so a locked map ignores wheel/drag, then flip `test.fixme(` back to `test(` and delete the `// SUPPRESSED (spec 274 …)` block (the acceptance gate). NOT caused by #261 (touches no viewport-lock plumbing; passes 4/4 under @sparticuz). (follow-up to #260; surfaced by PR #651) | 4 | 1 | 2 | 7 | Medium | proposed | | 2026-05-28 | 2026-05-28 |
| 273 | Feature | [Storyboard live Preview button + web-shell briefing-zip export parity](specs/273-storyboard-preview-button/spec.md) — two coordinated deliverables surfaced from the shared `StoryboardPanel` header so both hosts stay in lock-step. **(A) Preview button (both VS Code + web-shell):** one-click affordance that opens the `apps/briefing-renderer/` SPA in a **new browser tab**, pointed at a **URL for the current plot's `features.geojson`** (option (b) from triage — *not* a packed zip), so the author can verify look/playback during an iterative authoring loop. Requires the renderer to gain a **second, additive boot path**: today it only boots from JSON inlined into its HTML at export time (`boot.ts` reads `<script type="application/json">` slots; the `?story=` param in `App.tsx:55` is only a Playwright isolation hook). The deliberately air-gapped inlined path (used by #264) must stay untouched and still pass its tests. Per-host the "features.geojson URL" resolves differently (VS Code: localhost / webview-resource served to the external browser; web-shell: same-origin URL over the IndexedDB store from #236) — a key spec design point. **(B) Export-as-zip parity:** bring the VS Code-only `exportStoryboardAsBriefingZip` (#264) to **web-shell** (writes to the IndexedDB virtual filesystem, then offers a browser download); factor the packing logic to be shared across hosts. Reuses the shared playback engine (#217 + #258 + #263). Out of scope: PMTiles basemap (#272), MP4/GIF export (#265), durable shareable preview URLs. (follow-up to #264; builds on #263 / #258 / #236 / #174, all complete; [issue #645](https://github.com/debrief/debrief-future/issues/645)) | 4 | 4 | 3 | 11 | Medium | specified | E13 | 2026-05-26 | 2026-05-26 |
| 272 | Feature | [Air-gapped briefing zip — PMTiles / vector-tile basemap](specs/272-pmtiles-basemap/spec.md) — replace the per-Scene raster XYZ tile cache (typical 5–15 MB; capped near ~50 MB by the integer-zoom-only policy in #264 research.md R2) with a single-file PMTiles vector basemap. PMTiles adds a ~80 KB JS dependency and a new tile renderer in the SPA but eliminates the per-tile file proliferation, supports arbitrary zoom without bundling additional levels, and makes large-area briefings transportable as smaller artefacts (memory stick, email attachment). Considered and deferred during #264 research (R2): raster tiles work today with the existing Leaflet `TileLayer`; PMTiles is the natural next step if and when zip size proves a real problem in practice. Trigger: a briefing zip exceeds ~50 MB, OR analyst feedback that the raster-tile zip size is impeding transport. Estimate 5 dev-days. (follow-up to #264 `/speckit.review` decision 3 — captured as deferred-scope BL-C) | 3 | 4 | 3 | 10 | Medium | blocked — no offline vector-tile source (see spec) | | 2026-05-20 | 2026-05-26 |
| 271 | Feature | Overlap detection for time-range Scenes (warn, not block) — detect when two or more time-range Scenes have overlapping `[t_start, t_end]` windows within a single Storyboard and surface a non-blocking warning on the offending rows ("Overlaps with Scene B"). Authors can dismiss; the platform does not reorder, merge, or reject. Overlapping ranges are legitimate creative choices for some narratives (emphasis re-plays) but more often a sign of authoring drift; a passive warning helps analysts spot accidental overlaps without forcing a policy on intentional ones. Deferred from #263 (FR-SCO-003) — the MVP treats overlap detection as authoring discipline. Trigger: real-world analyst feedback that overlapping ranges are being authored accidentally. Estimate 1–2 dev-days. (follow-up to #263 review decision; depends on #263 shipping) | 2 | 1 | 4 | 7 | Low | approved | | 2026-05-19 | 2026-05-26 |
Expand All @@ -186,7 +187,7 @@ Description formats:
| 246 | Tech Debt | `@debrief/hooks` workspace package extraction — once a third app needs `useIsMobile` (or any other UI-agnostic hook currently exported from `@debrief/components`), lift the hook(s) into a small dependency-free `shared/hooks/` workspace package so consumers don't pay the full `@debrief/components` cost (Leaflet, Vega, MapView, FilterBar, FeatureList) just to read a matchMedia value. Today (2026-05-02) only `apps/web-shell` and `apps/backlog-navigator` (#244) consume the hook; tree-shake handles the bundle question for those two. Trigger: third consumer (e.g. spec-navigator going mobile, or `apps/loader`) adopting the hook, or any future framework-agnostic hook (Reduce-Motion / online-status / focus-visible) needing the same home. Estimate ~1 dev-day. (follow-up to #244 review §Issue 2 Option B) | 1 | 1 | 4 | 6 | Low | approved | | 2026-05-02 | 2026-05-05 |
| ~~247~~ | ~~Tech Debt~~ | ~~Backlog Navigator — lazy-load mobile component tree below the 1024px breakpoint — wrap the entire `src/components/mobile/*` subtree (CardList, ItemCard, BottomSheet, BottomSheetEditor, DescriptionEditorScreen, StickyPushBar, MobileFilterBar) in `React.lazy()` + `<Suspense>` so the desktop bundle never ships the mobile chunk and vice-versa. ~20 LoC + a Suspense fallback (a skeleton card list reusing `@debrief/components` skeletons). Trigger: when the desktop bundle nears the +30% cap committed in `scripts/bundle-baseline-244.json` (#244 review §Issue 4A) and there's nowhere else to cut without compromising the mobile experience. Today the bundle is provisionally within budget; this ticket is held for the moment it isn't. Estimate 0.5 dev-day. (follow-up to #244 review §Issue 4 Option B)~~ | ~~1~~ | ~~1~~ | ~~4~~ | ~~6~~ | ~~Low~~ | ~~complete~~ | | ~~2026-05-02~~ | ~~2026-05-07~~ |
| 248 | Enhancement | Backlog Navigator — iOS install-help banner — show a small in-app banner on iOS Safari (detected via UA + the absence of `beforeinstallprompt`) walking the user through "Tap the Share icon → Add to Home Screen" with an inline illustration. iOS doesn't expose `beforeinstallprompt`, so the standard `<UpdatePrompt>` install-affordance path is invisible to iPhone/iPad users; without this, US5 AS1 silently degrades on the largest mobile platform. Banner is dismissable + persists dismissal in `localStorage` so it doesn't nag on every visit. Trigger: when user feedback or telemetry shows iOS users not finding the install path, or before any push to make the navigator the recommended on-call interface for iOS users. Estimate 0.5–1 dev-day. (follow-up to #244 research R-13 deferral) | 2 | 2 | 3 | 7 | Low | proposed | | 2026-05-02 | 2026-05-02 |
| 249 | Tech Debt | [Unify `@debrief/session-state` (temporal / spatial / selection) with the in-plot `SystemState` Feature pattern](specs/261-session-state-systemstate/spec.md) — once #237 lands the first runtime consumer of `SystemState` (the `active_storyboard` variant), retroactively migrate the existing `temporal` / `spatial` / `selection` slices currently stored in the `.debrief-session` sidecar so they too live as `SystemState` features inside the plot's FeatureCollection. Today the LinkML schema (`shared/schemas/src/linkml/geojson.yaml`) already models all three variants of `SystemStateProperties` (per #215 schema work) but they're never produced by either host — the actual session state lives in a sidecar JSON file managed by `services/session-state/`. After #237 the precedent is set: state-bearing-but-non-spatial information lives inside the plot. Migrating the three existing slices unifies the model (one source of truth per piece of plot state), enables natural cross-host sync (any host opening the plot file gets the temporal viewport, the bbox, and the selection set), and lets the `.debrief-session` sidecar be retired (or scoped down to truly per-user / per-machine concerns). Significant scope: touches `services/session-state/`, both hosts' session lifecycle wiring, and adds three new schema fixtures. Should be sequenced after #237 ships and after the team confirms cross-analyst-shared-state semantics are desired for these slices too (might NOT be — temporal viewport and selection may want to stay per-user; see #251). (follow-up to #237; depends on #237 landing the SystemState pattern with at least one consumer; cross-references #215 schema work) | 5 | 3 | 3 | 11 | High | specified | | 2026-05-07 | 2026-05-19 |
| ~~249~~ | ~~Tech Debt~~ | ~~[Unify `@debrief/session-state` (temporal / spatial / selection) with the in-plot `SystemState` Feature pattern](specs/261-session-state-systemstate/spec.md) — once #237 lands the first runtime consumer of `SystemState` (the `active_storyboard` variant), retroactively migrate the existing `temporal` / `spatial` / `selection` slices currently stored in the `.debrief-session` sidecar so they too live as `SystemState` features inside the plot's FeatureCollection. Today the LinkML schema (`shared/schemas/src/linkml/geojson.yaml`) already models all three variants of `SystemStateProperties` (per #215 schema work) but they're never produced by either host — the actual session state lives in a sidecar JSON file managed by `services/session-state/`. After #237 the precedent is set: state-bearing-but-non-spatial information lives inside the plot. Migrating the three existing slices unifies the model (one source of truth per piece of plot state), enables natural cross-host sync (any host opening the plot file gets the temporal viewport, the bbox, and the selection set), and lets the `.debrief-session` sidecar be retired (or scoped down to truly per-user / per-machine concerns). Significant scope: touches `services/session-state/`, both hosts' session lifecycle wiring, and adds three new schema fixtures. Should be sequenced after #237 ships and after the team confirms cross-analyst-shared-state semantics are desired for these slices too (might NOT be — temporal viewport and selection may want to stay per-user; see #251). (follow-up to #237; depends on #237 landing the SystemState pattern with at least one consumer; cross-references #215 schema work)~~ | ~~5~~ | ~~3~~ | ~~3~~ | ~~11~~ | ~~High~~ | ~~complete~~ | | ~~2026-05-07~~ | ~~2026-05-28~~ |
| 250 | Feature | Web-shell `loadSession` / `saveSession` parity with VS Code — currently `apps/web-shell` has no equivalent of VS Code's `loadSession.ts` / `saveSession.ts` commands; the web-shell preserves no per-plot session state across reloads beyond what the plot file itself carries. After #236 (web-shell STAC writes) and #242 (saveSession through stac-writer) the infrastructure exists to land web-shell session-state persistence cleanly, but it has not yet been specified. Scope: choose a per-plot session-state storage location for the web-shell (the project's IndexedDB store from #236 is the natural fit), wire `loadSession` on plot-open, wire `saveSession` on plot-close / explicit user action, surface the existing #235 storyboard playback time-controller state at minimum, and decide which other slices are worth carrying (temporal viewport, selection set, layer visibility — same questions raised in #249). Today this gap is masked because the web-shell is primarily used as a preview / demo surface, but as it matures into a first-class analyst surface (#244 mobile parity, ongoing analyst feedback) the asymmetry with VS Code becomes user-visible. Estimate 5–8 dev-days; medium complexity. (follow-up to #236 / #242; partial overlap with #249 if that lands first) | - | - | - | - | Medium | proposed | | 2026-05-07 | 2026-05-07 |
| 251 | Feature | Per-user-within-shared-plot active-Storyboard view memory — #237 ships per-plot SHARED active-Storyboard selection (any analyst opening the plot lands on the most-recently-pinned Storyboard, regardless of who pinned it). For collaborative review and multi-analyst exercises this is the right semantic; but a future requirement may emerge for per-user-per-plot view memory ("remember *my* last Storyboard view of this plot, separately from the team's"). Implementing this requires a user-identity model the project does not have today — Debrief currently has no notion of "who am I" beyond the OS username (#221 has flagged the need for a configurable capture-actor setting). Scope (when picked up): (a) decide on a user-identity surface (configurable analyst code? OS-derived? per-org directory?); (b) add a per-user dimension to the `SystemState` `active_storyboard` variant — either a separate `state_type: active_storyboard_per_user` with a map field, or a sibling property carrying user attribution; (c) decide write-merge semantics when a per-user pin and a shared pin both exist for the same plot. Trigger: real analyst feedback that the shared semantic is wrong for their workflow, OR landing of #221 (configurable capture-actor) which would supply the user-identity primitive. Until then the per-plot SHARED semantic suffices. (follow-up to #237; cross-references #221) | - | - | - | - | High | proposed | | 2026-05-07 | 2026-05-07 |
| 252 | Tech Debt | Backlog Navigator — symmetric desktop-tree lazy split — once #247 has landed mobile-only `React.lazy()` boundaries, also wrap the desktop-only subtree (`ItemsTable`, desktop FilterBar, push dialog) in a complementary lazy boundary so a user opening the navigator on a phone never downloads desktop-only modules. Out of scope for #247 per review decision 1A (mobile-only scope). Trigger: mobile-side bundle measurement (e.g. via the same manifest-aware guard added in #247) showing mobile transfer is dominated by desktop-only code, OR a complaint from a mobile user about cold-load weight on a slow link. Estimate 0.5 dev-day. (follow-up to #247 review decision 1A — deferred D1) | 1 | 1 | 4 | 6 | Low | proposed | | 2026-05-07 | 2026-05-07 |
Expand Down
60 changes: 17 additions & 43 deletions apps/vscode/src/commands/openPlot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
*/

import * as vscode from 'vscode';
import { access, readFile } from 'fs/promises';
import { loadSession, createLogService, createSnapshotService, createTimeInstant, type ResultIdRegistry, type StacAssetForHydration } from '@debrief/session-state';
import { readFile } from 'fs/promises';
import { createLogService, createSnapshotService, createTimeInstant, type ResultIdRegistry, type StacAssetForHydration } from '@debrief/session-state';
import { hydrateStoreFromFeatures, SystemStateLoadError } from '../services/systemStateBridge';
import type { StacWriter } from '@debrief/stac-writer';
import type { ConfigService } from '../services/configService';
import type { StacService } from '../services/stacService';
Expand Down Expand Up @@ -65,29 +66,6 @@ interface OpenPlotArgs {
uri?: string;
}

/**
* Derive session file path from store path and item path.
* Converts item.json to item.debrief-session.
*/
function deriveSessionPath(storePath: string, itemPath: string): string {
const sessionItemPath = itemPath.replace(/\.json$/, '.debrief-session');
// Normalize path separators
const normalizedStorePath = storePath.replace(/\\/g, '/');
return `${normalizedStorePath}/${sessionItemPath}`;
}

/**
* Check if a file exists.
*/
async function fileExists(path: string): Promise<boolean> {
try {
await access(path);
return true;
} catch {
return false;
}
}

interface PlotQuickPickItem extends vscode.QuickPickItem {
uri: string;
storeId: string;
Expand Down Expand Up @@ -192,25 +170,21 @@ export function createOpenPlotCommand(
featureCollectionUri: plotUri,
});

// Check for existing session file and load it (Feature: 029 - T053-T055)
const sessionPath = deriveSessionPath(store.path, itemPath);
if (await fileExists(sessionPath)) {
const loadResult = await loadSession(session, sessionPath);
if (loadResult.success) {
// Session loaded successfully
void vscode.window.showInformationMessage(
`Restored session from ${sessionPath.split('/').pop()}`
);
} else if (loadResult.error?.includes('newer than supported')) {
// Future version - warn user (T055)
void vscode.window.showWarningMessage(
`Session file was created with a newer version of Debrief. Some settings may not be restored. ${loadResult.error}`
);
} else if (loadResult.error) {
// Other error - warn but continue
void vscode.window.showWarningMessage(
`Could not restore session: ${loadResult.error}`
// Feature 261 (FR-007): hydrate view-state (viewport, time window/playhead,
// selection) and per-feature visibility from the SystemState features inside
// the loaded features.geojson. The `.debrief-session` sidecar is gone — the
// plot is self-describing. Absence of a SystemState variant leaves the store
// at its defaults (FR-008). Malformed / duplicate / cross-field-invalid
// SystemState features fail loudly (strict-on-import, FR-011/FR-012).
try {
hydrateStoreFromFeatures(session.getState(), plotData.features);
} catch (err) {
if (err instanceof SystemStateLoadError) {
void vscode.window.showErrorMessage(
`Could not restore plot view-state: ${err.message}`,
);
} else {
throw err;
}
}

Expand Down
Loading
Loading