Skip to content

feat: state CRUA tools + states management UI (#215)#216

Merged
ProfSynapse merged 7 commits into
mainfrom
feat/issue-215-archive-state-tool-and-ui
May 26, 2026
Merged

feat: state CRUA tools + states management UI (#215)#216
ProfSynapse merged 7 commits into
mainfrom
feat/issue-215-archive-state-tool-and-ui

Conversation

@ProfSynapse

Copy link
Copy Markdown
Owner

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 tags
  • memory archive-state [--restore] — soft-archive + restore, mirroring archive-workspace
  • No delete-state tool — by design, the AI cannot delete states. Deletion is UI-only.

UI surface (Settings → Workspaces → workspace detail):

  • New "States" section under the existing Projects section
  • List states with show-archived toggle
  • Edit modal (name + description)
  • Archive / Restore (reversible)
  • Delete with confirmation modal (humans can destroy; mirrors WorkspaceDeleteConfirmModal)

Bonus: latent bug fix

While tracing the service layer, found that 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). The storage layer is extended in this PR to fix it:

  • New 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

Any other caller of MemoryService.updateState now actually persists changes.

Test plan

  • npm test — 2756 / 2771 pass (15 pre-existing skips, 0 regressions)
  • npx tsc --noEmit — clean
  • npm run build — clean (esbuild + connector regen)
  • npx eslint on touched files — clean
  • Manual: create a state, archive it via UI, restore it
  • Manual: edit state name + description via UI
  • Manual: delete state via UI (confirm modal gates correctly)
  • Manual: call memory archive-state and memory update-state from MCP
  • Manual: confirm memory list-states --include-archived surfaces archived states

🤖 Generated with Claude Code

ProfSynapse and others added 7 commits May 26, 2026 15:28
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>
…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>
@ProfSynapse ProfSynapse force-pushed the feat/issue-215-archive-state-tool-and-ui branch from 19cfe12 to 52b2012 Compare May 26, 2026 19:29
@ProfSynapse ProfSynapse merged commit 61d1cb7 into main 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 26, 2026
Patch release covering #218 — surface isArchived from getStates for
tagged states (follow-up to #215 manual-test finding from PR #216).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

memoryManager: archiveState / deleteState not exposed in CLI (only createState/listStates/loadState)

1 participant