fix(settings): use the host's @deepseek-ai/schemastery so volatile prefs apply
CI / ci (pull_request) Canceled after 0s
CI / ci-windows (pull_request) Canceled after 0s
CI / plugin-mount (pull_request) Canceled after 0s

Every side-card switch was dead: settings.update returned OK and
cordis.patch.yml was written, yet the running plugin never saw the new value
and nothing reported an error.

Root cause: the plugin declared Config with the plain npm `schemastery`
(3.18.0), which has no volatile support. The host resolves plugin config via
`runtime.Config['~standard'].validate()` — i.e. the plugin's OWN schemastery —
so `meta.volatile` nodes were never wrapped in cosmokit volatile references.
`cordis-plugin-loader`'s `_commitVolatile()` bails out as soon as
`volatileEntries(fiber.config)` is empty, so every volatile-only edit was
silently dropped: no in-place update, no plugin restart.

Fix: import z from '@deepseek-ai/schemastery' (peer provided by the host at
mount; pinned 3.18.4 as a devDependency for the dev tree), matching the
repo's existing pnpm-workspace convention for @deepseek-ai/* peers.

Verified: probe reports tabsIsVolatile=true and revision climbing 0->6 across
six writes, each read-back matching the written value (previously the value
was frozen and revision stuck at 1).

Also in this change:
- apes tab ids lose their colon (apes:tasks -> apesTasks, ...): the host's
  settings projection dropped map keys containing a colon.
- sidebar prefs ns follows the profile entry id (`better-sidebar`).
- subagent rows degrade to summary rows when the root catalog never hydrates.
- docs/feedback-0.2.0-rc2-contract-gaps.md: four host contract gaps, gap 4
  documenting the silent volatile-write drop above.

Gates: typecheck 0, lint 0, build 0, test 131 files / 1385 passed.
This commit is contained in:
figmar
2026-10-13 22:27:19 +08:00
parent 043340eb02
commit aa8f417aa3
11 changed files with 320 additions and 141 deletions
+13 -13
View File
@@ -2,7 +2,7 @@
* apes:* tab registration tests (Phase 1/1b → C7: 5 tabs). The fork registers
* its information-layer tabs (tasks / agents / apps / messages / profile)
* through the same BetterSidebarService the upstream builtins use (dogfood).
* `apes:tasks` is back (C7 reversal of C6): the native DSH session tree focuses
* `apesTasks` is back (C7 reversal of C6): the native DSH session tree focuses
* ONE lineage, while this panel is a flat, whole-workspace list with a running
* badge and an idle filter — the overlap is complementary.
*
@@ -38,7 +38,7 @@ import {
tasksShowInactive,
} from '../src/client/apes/tabs.tsx'
const APES_TAB_IDS = ['apes:agents', 'apes:apps', 'apes:messages', 'apes:profile', 'apes:tasks']
const APES_TAB_IDS = ['apesAgents', 'apesApps', 'apesMessages', 'apesProfile', 'apesTasks']
function setup(): {
service: ReturnType<typeof createBetterSidebarService>
@@ -119,10 +119,10 @@ describe('apes:* tab registrations', () => {
try {
const orders = {
tasks: service.getTab(TASKS_TAB_ID)?.order,
agents: service.getTab('apes:agents')?.order,
apps: service.getTab('apes:apps')?.order,
messages: service.getTab('apes:messages')?.order,
profile: service.getTab('apes:profile')?.order,
agents: service.getTab('apesAgents')?.order,
apps: service.getTab('apesApps')?.order,
messages: service.getTab('apesMessages')?.order,
profile: service.getTab('apesProfile')?.order,
}
// C8: order 40-50 places apes tabs immediately after the native builtins
// (10-35), not in a far-away 900+ band — visual unity in the tab strip.
@@ -149,7 +149,7 @@ describe('apes:* tab registrations', () => {
})
})
describe('apes:tasks plugin toggles', () => {
describe('apesTasks plugin toggles', () => {
it('declares the showInactive switch under the tasks descriptor', () => {
const descriptor = apesTabs.find(d => d.id === TASKS_TAB_ID)
const toggles = descriptor?.settings?.pluginToggles ?? []
@@ -166,7 +166,7 @@ describe('apes:tasks plugin toggles', () => {
expect(tasksShowInactive(store)).toBe(true)
})
it('reads the boolean from pluginSettings[apes:tasks]', () => {
it('reads the boolean from pluginSettings[apesTasks]', () => {
const store = {
getPrefs: () => ({ pluginSettings: { [TASKS_TAB_ID]: { [TASKS_SHOW_INACTIVE_KEY]: false } } }),
} as unknown as TabComponentProps['store']
@@ -183,7 +183,7 @@ describe('apes:tasks plugin toggles', () => {
})
})
describe('apes:tasks row selection (PURE)', () => {
describe('apesTasks row selection (PURE)', () => {
it('ranks current first, then running, then idle', () => {
const list = snap('idle-b', [
row('idle-a'),
@@ -219,7 +219,7 @@ describe('apes:tasks row selection (PURE)', () => {
})
})
describe('apes:agents badge + topology count', () => {
describe('apesAgents badge + topology count', () => {
it('counts the subagents of the current session tree only', () => {
const list = snap('main', [
row('main'),
@@ -251,12 +251,12 @@ describe('apes badge contract', () => {
})
it('badges the agents tab with the subagent count, null when childless', () => {
expect(badgeOf('apes:agents', snap('main', [row('main'), row('c1', { origin: 'subagent', parentId: 'main' })]), 'main')).toBe(1)
expect(badgeOf('apes:agents', snap('main', [row('main')]), 'main')).toBeNull()
expect(badgeOf('apesAgents', snap('main', [row('main'), row('c1', { origin: 'subagent', parentId: 'main' })]), 'main')).toBe(1)
expect(badgeOf('apesAgents', snap('main', [row('main')]), 'main')).toBeNull()
})
it('never badges the placeholder/static tabs', () => {
for (const id of ['apes:apps', 'apes:messages', 'apes:profile']) {
for (const id of ['apesApps', 'apesMessages', 'apesProfile']) {
expect(apesTabs.find(d => d.id === id)?.badge, `${id} badge`).toBeUndefined()
}
})
+1 -1
View File
@@ -53,7 +53,7 @@ describe('dsh-apes-sidebar plugin export shape', () => {
it('registers the side card preferences schema with the documented defaults', async () => {
const { PrefsSchema, SIDEBAR_PREFS_NS, SIDEBAR_PREFS_DEFAULTS } = await import('../src/config.ts')
expect(SIDEBAR_PREFS_NS).toBe('dsh-apes-sidebar')
expect(SIDEBAR_PREFS_NS).toBe('better-sidebar')
const resolved = (PrefsSchema as unknown as {
(input: Record<string, unknown> | undefined): Record<string, unknown>
})(undefined)
+12 -12
View File
@@ -11,7 +11,7 @@ import { join, resolve as resolvePath } from 'node:path'
import { SettingsConflictError, type SettingsNamespace } from '@deepseek-ai/dsh-settings'
import { apply, mediaTypeForPath, wsCloseReasonOf } from '../src/index.ts'
import { SidebarError } from '../src/wire.ts'
import { SIDEBAR_PREFS_DEFAULTS } from '../src/prefs-shared.ts'
import { SIDEBAR_PREFS_DEFAULTS, SIDEBAR_PREFS_NS } from '../src/prefs-shared.ts'
import { encodeHtmlUrl } from '../src/html-route.ts'
import * as git from '../src/git.ts'
import { listDirectory } from '../src/fs-tree.ts'
@@ -839,11 +839,11 @@ describe('side card settings routes', () => {
// 0.2.0-rc.2: the plugin's Config schema is auto-registered by the loader.
// The mock pre-seeds the sidebar namespace so describe() returns it even
// when the plugin no longer calls register() explicitly.
if (!pre || !('dsh-apes-sidebar' in pre)) {
if (!pre || !(SIDEBAR_PREFS_NS in pre)) {
// Use SIDEBAR_PREFS_DEFAULTS as the resolved value for the sidebar ns:
// the real SettingsForms service applies the Config schema defaults
// when no stored value exists, which is exactly what this constant is.
namespaces.set('dsh-apes-sidebar', { schema: (input: unknown) => input ?? SIDEBAR_PREFS_DEFAULTS, value: undefined, revision: 0 })
namespaces.set(SIDEBAR_PREFS_NS, { schema: (input: unknown) => input ?? SIDEBAR_PREFS_DEFAULTS, value: undefined, revision: 0 })
}
for (const [ns, value] of Object.entries(pre ?? {})) {
namespaces.set(ns, { schema: (input: unknown) => input ?? {}, value, revision: 0 })
@@ -979,7 +979,7 @@ describe('side card settings routes', () => {
it('shell.get reflects the settings-page override with the quotes stripped', async () => {
const route = mountWithSettings(createFakeSettings({
'dsh-apes-sidebar': {
[SIDEBAR_PREFS_NS]: {
terminalShell: '"C:\\Program Files\\PowerShell\\7\\pwsh.exe"',
terminalShellArgs: '-NoLogo',
},
@@ -1184,7 +1184,7 @@ describe('agent terminal tool gating', () => {
const handlerRef: { current: ((ns: string) => void) | null } = { current: null }
const settings = {
describe: () => [{
ns: 'dsh-apes-sidebar',
ns: SIDEBAR_PREFS_NS,
value: { ...SIDEBAR_PREFS_DEFAULTS, agentTerminalTools: enabled },
applies: 'live' as const,
revision: 0,
@@ -1215,18 +1215,18 @@ describe('agent terminal tool gating', () => {
expect(live()).toBe(0)
// Flipping the setting on registers all eight tools.
enabled = true
handlerRef.current?.('dsh-apes-sidebar')
handlerRef.current?.(SIDEBAR_PREFS_NS)
expect(live()).toBe(8)
expect(disposed).toBe(0)
// Flipping it back off unregisters them (and releases any agent terminals).
enabled = false
handlerRef.current?.('dsh-apes-sidebar')
handlerRef.current?.(SIDEBAR_PREFS_NS)
expect(live()).toBe(0)
expect(disposed).toBe(8)
// And a redundant toggle registers them fresh (no double-registration per
// flip: the guard only skips when the tools are already live).
enabled = true
handlerRef.current?.('dsh-apes-sidebar')
handlerRef.current?.(SIDEBAR_PREFS_NS)
expect(live()).toBe(8)
expect(registered).toBe(16)
})
@@ -1241,7 +1241,7 @@ describe('agent sidebar-open tool gating', () => {
const handlerRef: { current: ((ns: string) => void) | null } = { current: null }
const settings = {
describe: () => [{
ns: 'dsh-apes-sidebar',
ns: SIDEBAR_PREFS_NS,
value: { ...SIDEBAR_PREFS_DEFAULTS, agentOpenTools: enabled },
applies: 'live' as const,
revision: 0,
@@ -1272,17 +1272,17 @@ describe('agent sidebar-open tool gating', () => {
expect(live()).toBe(0)
// Flipping the setting on registers the single sidebar_open tool.
enabled = true
handlerRef.current?.('dsh-apes-sidebar')
handlerRef.current?.(SIDEBAR_PREFS_NS)
expect(live()).toBe(1)
expect(disposed).toBe(0)
// Flipping it back off unregisters it (and drains the undelivered queue).
enabled = false
handlerRef.current?.('dsh-apes-sidebar')
handlerRef.current?.(SIDEBAR_PREFS_NS)
expect(live()).toBe(0)
expect(disposed).toBe(1)
// And a redundant toggle registers it fresh (no double-registration).
enabled = true
handlerRef.current?.('dsh-apes-sidebar')
handlerRef.current?.(SIDEBAR_PREFS_NS)
expect(live()).toBe(1)
expect(registered).toBe(2)
})