feat: state CRUA tools + states management UI (#215)#216
Merged
Conversation
Closes the asymmetry between workspace and state CLI surfaces. Workspaces have full CRUA via createWorkspace/listWorkspaces/loadWorkspace/ updateWorkspace/archiveWorkspace; states had only createState/listStates/ loadState. This adds updateState (rename + description + tags) and archiveState (with --restore flag) mirroring the workspace pattern. Per design constraint: no deleteState tool. The AI cannot delete states. Deletion is reserved for the UI (humans only). While tracing the service layer, found and fixed a latent bug: MemoryService.updateState was silently no-op'ing on the hybrid (SQL/ IndexedDB) backend — production default since v5.8.12. It read workspace.sessions[].states[] which is empty on hybrid (states are first-class table rows). Storage layer extended to support state mutation: - StateUpdatedEvent (mirrors state_deleted shape) - IStorageAdapter.updateState - IStateRepository.updateState + UpdateStateData - StateRepository.updateState (UPDATE + event-folding in getStateData for replay correctness) - HybridStorageAdapter.updateState delegation - WorkspaceEventApplier.applyStateUpdated handler - WorkspaceService.updateState facade - WorkspaceStateService.updateState with dual-backend support - MemoryService.updateState rewritten to route through the adapter Archive stored at state.state.metadata.isArchived (nested) to match existing listStates filter at listStates.ts:106. Tests: 2756/2771 pass (15 pre-existing skips, 0 regressions). Build: clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a States section to the workspace detail view in Settings → Workspaces, mirroring the renderProjectsSection pattern. Users can: - List states for the workspace (with show-archived toggle) - Edit (rename + description) - Archive / Restore (reversible, calls updateState with toggled metadata.isArchived per backend contract) - Delete (permanent, gated behind StateDeleteConfirmModal mirroring WorkspaceDeleteConfirmModal) Delete is UI-only — humans can destroy states, the AI cannot (no deleteState CLI tool per #215 constraint). Files: - NEW: src/components/workspace/StatesSectionRenderer.ts — section renderer, edit modal, delete-confirmation modal - MOD: src/components/workspace/WorkspaceDetailRenderer.ts — renderStatesSection wiring, DetailCallbacks extension - MOD: src/settings/tabs/WorkspacesTab.ts — getStatesService composing MemoryService + HybridStorageAdapter - MOD: styles.css (+138 lines) — Obsidian CSS vars only, no hardcoded colors Obsidian-plugin compliance: all styles in styles.css, no innerHTML with dynamic content, safeRegisterDomEvent for DOM events, no top-level Node.js imports. Tests: 214/214 suites pass, 2756/2756 tests pass (0 regressions). Build: clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ption for tagged states (PR #216 B1)
…ts (PR #216 B2+B3) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dapter (PR #216 M1) The prior implementation read workspace.sessions[sessionId].states[stateId] and wrote back via updateWorkspace. On the hybrid backend (production default since v5.8.12) getWorkspace returns sessions:{} because states are first-class rows in the states table, not nested under workspace.sessions — so the workspace round-trip silently no-ops. Mirror the updateState fix shape: route through HybridStorageAdapter.deleteState via withDualBackend on the hybrid path, preserve the legacy read-modify-write pattern (with the 'Session not found' error contract) as the fallback. Caller signature unchanged so createState.ts:437 rollback path continues working. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(PR #216 M1) Defensive 1-case test that fails if MemoryService.deleteState ever reintroduces the broken workspace round-trip pattern. Mocks a ready storage adapter and asserts: - adapter.deleteState is called once with the stateId - workspaceService.getWorkspace / updateWorkspace are NOT called This guards against hand-edits regressing the M1 fix shape on the hybrid backend (production default since v5.8.12). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Build artifact regenerated after rebasing PR #216 on origin/main. Resolves the connectorContent.ts timestamp conflict from v5.9.5→v5.9.6 bump. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
19cfe12 to
52b2012
Compare
This was referenced May 26, 2026
ProfSynapse
added a commit
that referenced
this pull request
May 26, 2026
…es (#218) Fixes the SQLite-metadata fast-path in `MemoryService.getStates` that skipped JSONL content fetch for tagged states, causing isArchived to never surface to the UI list filter or AI-facing listStates tool. Drop-the-shortcut, ~1 LoC. Tool filter inherits fix automatically. Adds 3 regression tests. User manually verified in Obsidian. Follow-up tech debt: denormalize is_archived into SQLite metadata (~80-120 LoC, v13 migration) — to be filed as separate issue. Follow-up to manual-test finding from #216 / #215.
ProfSynapse
added a commit
that referenced
this pull request
May 27, 2026
…#220) Wave 3 PR1 — workspace tab foundation: shared `BoxedSection` + `ConfirmModal` primitives, `WorkspaceDetailRenderer` / `WorkspaceFormRenderer` / `StatesSectionRenderer` shell ports preserving the PR #216 v5.9.7 archive-visibility chain, ConfirmModal sweep across 3 destructive call sites, `.ws-section` styles family, breadcrumb fix. +2270 / -246 across 13 files; full suite 2863 passed / 17 skipped; build + lint clean. Peer-reviewed by architect + frontend-coder + test-engineer (3 parallel reviewers) — 0 Blocking, 8 Minor + 4 Future findings dispositioned and bundled into PR2 carry-forwards (Groups A/B/C in `docs/plans/workspace-tab-redesign-plan.md`). F-4 mobile-compat lint guard tracked separately as #221. Review synthesis: `docs/review/pr220-wave3-pr1-2026-05-27.md`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #215 — adds the missing state-management CLI surface and a parallel UI in Settings.
AI surface (CRUA, mirrors workspace tools):
memory update-state— rename / edit description / replace tagsmemory archive-state [--restore]— soft-archive + restore, mirroringarchive-workspacedelete-statetool — by design, the AI cannot delete states. Deletion is UI-only.UI surface (Settings → Workspaces → workspace detail):
WorkspaceDeleteConfirmModal)Bonus: latent bug fix
While tracing the service layer, found that
MemoryService.updateStatewas silently no-op'ing on the hybrid (SQL/IndexedDB) backend — production default since v5.8.12. It readworkspace.sessions[].states[]which is empty on hybrid (states are first-class table rows). The storage layer is extended in this PR to fix it:StateUpdatedEvent(mirrorsstate_deletedshape)IStorageAdapter.updateState+IStateRepository.updateState+UpdateStateDataStateRepository.updateState(UPDATE + event-folding ingetStateDatafor replay correctness)HybridStorageAdapter.updateStatedelegationWorkspaceEventApplier.applyStateUpdatedhandlerWorkspaceService.updateStatefacadeWorkspaceStateService.updateStatewith dual-backend supportMemoryService.updateStaterewritten to route through the adapterAny other caller of
MemoryService.updateStatenow actually persists changes.Test plan
npm test— 2756 / 2771 pass (15 pre-existing skips, 0 regressions)npx tsc --noEmit— cleannpm run build— clean (esbuild + connector regen)npx eslinton touched files — cleanmemory archive-stateandmemory update-statefrom MCPmemory list-states --include-archivedsurfaces archived states🤖 Generated with Claude Code