Skip to content

feat: flat advisor settings card, localized plugin meta, drop the config-level enabled switch - #98

Merged
btspoony merged 9 commits into
mainfrom
feat/web-config-flat-i18n
Sep 27, 2026
Merged

btspoony merged 9 commits into
mainfrom
feat/web-config-flat-i18n

Conversation

@btspoony

Copy link
Copy Markdown
Member

Summary

Aligns the Advisor settings surface with the current dsh web Plugins page (flat form, no collapsible card), localizes the plugin detail title/description and adds a plugin icon, and retires the config-level enabled switch — the plugin-row toggle on the Plugins page is the master switch now.

Changes

  • Localized plugin meta + icon: package-root locale/en.json + locale/zh.json (meta.title = Advisor / 顾问, localized descriptions) and a hand-drawn icon.svg in the official gradient-tile style; package.json gains icon, exports["./locale/*.json"], and matching files entries. The bundle detail page (title/description/icon) and the component row localize automatically via the host's readPluginMeta.
  • Remove the config-level enabled key (breaking): dropped from the Loader schema, the config contract, the per-session override seed, the TUI /settings section, and the web card. The S4 gate now keys on the pair alone: provider + model present → enabled. Legacy enabled: lines in existing profiles are accepted and silently ignored (exempted from unknown-key rejection, stripped from snapshots and writes — never re-persisted); every other unknown key is still strictly rejected.
  • Flat settings card: the self-drawn collapsible chrome (header/chevron/unsaved pill) is gone; provider/model selects render unconditionally (previously hidden behind the unchecked "enable advisor" checkbox — the reported missing-model-selection bug), the textarea placeholder now shows the default advisor system prompt (imported from the same package's prompts.ts, single source of truth), and field styles follow the official flat settings-form language. The client no longer value-imports @deepseek-ai/dsh-client-ui-primitives (peer dependency dropped), so the bundle carries zero native-UI component dependencies.
  • Spec (§5.1 five persisted keys, S4 wording, revision record), README (en/zh), and docs synced.

Testing

  • pnpm typecheck exit 0 (main / client / spec tsconfigs)
  • Targeted vitest over all affected contract files: 444 passed; legacy-key follow-up re-verified over the 9 re-affected files: 230 passed
  • pnpm build + bundle contract greps: no dsh-client-ui-primitives require, flat class map + default-prompt text present, plugins.bundle.config seat pinned
  • Node-resolver smoke for ./locale/*.json + ./icon.svg exports/files
  • Visual check on a live host (127.0.0.1:3080) after reinstall — pending

Add the locale/en.json + locale/zh.json meta pair and the plugin icon, and
declare both through package.json (exports ./locale/*.json, top-level icon,
files locale + icon.svg). The host plugin manager resolves the per-UI-language
title/description for the plugin detail page and the component row from these
files; the icon ships the official-gradient plugin mark.
The plugin-row enable/disable toggle is the master switch (2026-09-26 user
ruling): a config-level `enabled` key is redundant with it and is removed
across the whole chain, strictly and without a compatibility layer.

Host side:
- Config schema / CONFIG_KEYS / AdvisorConfig drop `enabled`; the S4 gate now
  keys purely on the pair — provider AND model non-empty -> enabled:true,
  otherwise disabled-with-reason (ResolvedAdvisorConfig.enabled stays as the
  post-gate flag). A stored profile still carrying `enabled:` is rejected as
  an unknown config key (actionable disabledReason on the row).
- AdvisorSessionOverrides drops the configEnabled seed + setConfigEnabled:
  the effective session switch is `override ?? true` (a running row is on);
  only /advisor off suppresses a session.
- The wiring's safeEffective applies the session override AFTER the gate
  instead of baking it into the raw config; the onChange re-apply no longer
  mirrors a fallback switch.
- The TUI /settings section drops the enabled boolean field (four keys left)
  and its provider/model hints state the requirement unconditionally.

Client side:
- The store draft drops `enabled`; the client-form gate requires the pair
  unconditionally; setEnabled is gone; patchFor always-keys drop 'enabled';
  AdvisorConfigView.enabled stays as a read-only wire fact.

Tests updated to the new contract, including pins that a patch or entry
carrying `enabled` is rejected as an unknown key.
Rebuild the advisor card to the official settings-page language: the fields
tile directly under the page's plugin title/description — no collapsible box,
no header button, no chevron, no dirty pill, no cardOpen disclosure state.
The chrome mirrored the retired upstream PluginCard and existed to gate the
form behind the config-level `enabled` switch; both are gone.

- provider/model selects render unconditionally (the model select was
  previously hidden behind the enable checkbox); the KD-S4 gate blocks Save
  until the pair is complete, unconditionally.
- the system-prompt textarea's placeholder IS DEFAULT_ADVISOR_SYSTEM_PROMPT
  (same-package import from src/prompts.ts — one SSOT; the client tsconfig
  widens its rootDir to src/ to include it), and the leave-empty contract
  moves to a hint line under the field.
- the CSS module drops the chrome classes and aligns the field language with
  the official settings-form values (12px field padding, 0.5px hairline
  separators, 13px/500 labels, 34px inputs, dark solid Save, 16px footer
  padding), every color through a --dsw-alias-* token.
- locales: title/intro move to the locale/*.json plugin meta (T1);
  collapse/expand/unsaved/enabled keys die with the chrome;
  providerRequired/modelRequired are unconditional; the namespaceUnavailable
  example config drops `enabled`.
- package.json drops the @deepseek-ai/dsh-client-ui-primitives peer (the
  chevron icon value-import was the last one; build-client CLIENT_EXTERNALS
  stays as the frozen platform-table mirror).
- client-build test pins move from the chrome classes to the flat ones, add
  not-to-contain pins for the primitives require and the inlined reviewer
  prompt; the card spec asserts the flat layout (no checkbox, always-rendered
  selects, prompt placeholder, unchanged disabled semantics).
- .mstar/specs/advisor-plugin.md: S4 + §5.1 schema (six -> five keys), §5.2
  gate wording (the pair is the on/off state), §5.3 resolution order
  (override ?? true), §7 verification entries, §8.6 KD-6 freeze sentence
  (five persisted keys), plus a 2026-09-26 revision record line in the
  header (user ruling: the row toggle replaces the config enabled key).
- README (en/zh) + docs/configuration.md: breaking-change note (the
  config-level enabled key is removed; the plugin-row toggle is the master
  switch; stored enabled: lines are rejected as unknown keys — no compat
  layer), five-key config examples, unconditional gate wording, flat card
  behavior; docs/install*.md TUI key list drops enabled; docs/verification.md
  contract descriptions + real-environment steps updated; docs/consumer-api.md
  type and override-resolution wording updated.
Follow-up to the 2026-09-26 removal: stored profiles still carrying
`enabled: false/true` must keep loading (2026-09-27 user ruling — the
stored line is simply ignored, not an error).

- resolveAdvisorConfig exempts `enabled` from the strict unknown-key
  rejection via the module-private LEGACY_KEYS (every other unknown key is
  still a hard reject — spec §5.2 unchanged in spirit); the gate keeps
  keying purely on the provider/model pair, so the dead value can neither
  enable nor disable anything.
- unwrapAdvisorConfig strips legacy keys from the snapshot before the
  rest-spread: `source()` and every downstream consumer never carry the
  dead key, and a snapshot-derived write can never re-persist it.
- the gateway `set` payload normalization drops `enabled` alongside the
  existing null filter, so the write channel cannot resurrect the removed
  switch; an enabled-only patch is a no-op under the existing empty-patch
  semantics.
- tests flipped to the tolerance contract (config resolves, gate
  unaffected, snapshot clean; the gateway accepts an enabled-bearing patch
  and persists only the real keys); spec revision record + README
  (en/zh) + configuration/verification docs updated: the stored line is
  silently ignored, no manual deletion needed.

@btspoony btspoony left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Verdict: needs fixes · Confidence 64%

Findings Count
🔴 must-fix 0
🟠 should-fix 2
🔵 nit 2
❓ unverified 0

📋 Review

What this PR does: Rebuilds the Advisor settings surface for the current dsh web Plugins page: flat settings card (collapsible chrome removed; provider/model selects always visible; textarea placeholder = the default advisor system prompt), localized plugin meta via package-root locale/{en,zh}.json + a hand-drawn icon.svg, and removal of the config-level enabled key — the plugin-row toggle is the master switch, with legacy enabled: lines silently tolerated and stripped (2026-09-27 ruling). The client bundle drops its @deepseek-ai/dsh-client-ui-primitives value dependency.

Findings

🟠 disabledReason leaks onto effectively-off sessions' status and wire snapshot

  • Evidence: src/index.ts:285 — the override-off branch spreads the pair-gate resolution verbatim ({ ...resolved, enabled: false }), keeping disabledReason from an incomplete pair; the contract at src/commands.ts:292-297 says the reason is present iff the session is effectively enabled but the gate blocks; the renderer prints it whenever present (src/commands.ts:324) and the B2 snapshot carries it onto advisor/getSession (src/index.ts:830-831).
  • Impact: on the default fresh-install state (pairless global) after /advisor off, /advisor status renders "Reason: provider and model are missing — configure both…" under a deliberately-disabled session and the web session snapshot reports enabled:false + disabledReason — framing a user-off session as gate-blocked. Pre-diff code was reason-free for off sessions.
  • Merge class: should-fix
  • Confidence: HIGH
  • Fix sketch: strip the reason on the override-off branch ({ ...resolved, enabled: false, disabledReason: undefined }) — or, if the reason is intended UX for off sessions, update the AdvisorSessionStatus.disabledReason contract doc in the same PR; one of the two must move.

🟠 Healthy form unmounts entirely during background refreshes and the post-apply reload

  • Evidence: src/client/advisor-card.tsx:177-182 — any status !== 'ready' returns null ("the flat card has no header chrome to hold the space"); load() synchronously flips status to 'loading' (src/client/advisor-store.ts:342); apply() ends with await this.load() (src/client/advisor-store.ts:665); window focus + settings/document-updated + llm/adapters-updated all re-run load() on a visible card (src/client/index.ts:179-189).
  • Impact: the flat form is always visible, so every background invalidation — including merely refocusing the browser window — unmounts the whole section (fields, staged-edit view, footer) to zero height and remounts it; right after Save, the saved feedback and the just-saved form blink out during the post-apply reload. Staged edits survive (the draft is not re-seeded), so this is visible instability + mid-edit focus loss, not data loss. The degraded branch explicitly holds its notice through the same window — the healthy form got no equivalent.
  • Merge class: should-fix — corrected from the seat's nit at synthesis: reachability (every window focus) plus the PR being the surface's own rebuild outweigh the render-only blast radius.
  • Confidence: HIGH
  • Fix sketch: render the last ready snapshot's form disabled while loading (or keep an empty-but-present section shell), mirroring the degraded latch; the first-load null stays.

🔵 gateway.test.ts contract header ③ contradicts the tolerance test beneath it

  • Evidence: tests/gateway.test.ts:18-20 — header ③ says the removed enabled key "is rejected the same way"; the same file tests and pins the opposite ("tolerates the legacy enabled key in a patch but never persists it", tests/gateway.test.ts:359-377).
  • Impact: the file's own contract header misdocuments the 2026-09-27 ruling; a future edit honoring it would re-introduce the stored-profile load rejection the ruling forbids.
  • Merge class: nit
  • Confidence: HIGH
  • Fix sketch: reword ③ to "tolerated and stripped from the write payload — accepted, never persisted".

🔵 Stale pre-PR resolver strings and doc wording survive in fixtures and a src docstring

  • Evidence: tests/commands.test.ts:170,359,431,508,511,941 — fixtures assert the retired reason string "enabled but provider and model are missing — …", which src/config.ts:227 no longer emits ("provider and model are missing — …"); src/commands.ts:430-431 documents AdvisorComposedConfig.enabled as a "Config-level composed switch", re-normalizing the switch this PR removes.
  • Impact: tests pass only because the controller is faked; fixture text no longer matches any producible output, and the docstring contradicts the PR's core ruling.
  • Merge class: nit
  • Confidence: HIGH
  • Fix sketch: refresh fixtures to the current strings; reword the doc to "post-gate pair flag — not the per-session override".

Linked-issue AC

Originating spec: .mstar/specs/advisor-plugin.md (revised in this PR) plus the PR's four stated goals, scored against the diff:

  • Localized meta + icon (locale/{en,zh}.json, exports/files/icon wiring) — met
  • enabled removed; row toggle is the switch; legacy enabled: lines silently ignored, other unknown keys still rejected — met
  • Flat card, provider/model selects unconditional, placeholder = default system prompt — met (the refresh-window defect above is a finding, not a missed AC)
  • Zero native-UI value imports (ui-primitives peer dropped, negative bundle pin) — met
  • Spec §5.1 five persisted keys / §5.2 pair gate / §5.3 override ?? true — met

✅ Verified

  • Pinned diff pack (origin/main...pr-98, merge-base 700ba7b) read first; two domain seats (host-config-contract, client-bundle-ui) collected and reviewed with the in-domain security lens; every finding citation re-opened at synthesis.
  • LEGACY_KEYS is exactly {enabled}; strict unknown-key rejection preserved and pinned (src/config.ts, tests/config.test.ts, tests/settings.test.ts); gateway set tolerates-then-strips (src/gateway.ts:278-313, tested at tests/gateway.test.ts:359-377); no configEnabled leftovers (grep clean).
  • Card class-map parity 17↔17 with no dead refs; disabled terms match the stated contract; en/zh locale mirror type-enforced; removed keys have zero residual references; src/prompts.ts is an import-free string constant (bundle purity holds); package.json exports/files/icon consistent.
  • CI on this PR: validate pass (run 36294037803). Builds/tests not re-run locally (read-only review; CI owns the suite).
  • ❓ cross-domain lead, out of PR scope (file unchanged by this diff): tests/integration.test.ts:805-807 asserts source: { kind: 'advisor' } in the delivery domain.

🗑️ Considered & rejected

  • build-client.mjs declaration-path doc inaccuracy: rejected — the main claim holds; the extra lib/prompts.d.ts emission is inert and identical to the main build's.
  • Number-input Number() clamp in the card handler: rejected — handler unchanged by this PR; pre-existing surface.
  • Strictness regression from the legacy tolerance: rejected — by design per the 2026-09-27 ruling; spec §5.1/§5.2 revised; only enabled is exempted.
  • config-level enabled dead-code leftovers: rejected — grep clean across src/ and tests/.

🛠️ Plan to fix

Expand fix plan
1. src/index.ts (safeEffective, override-off branch): return `{ ...resolved, enabled: false, disabledReason: undefined }`; add a test pinning that `/advisor status` with a pairless global after `/advisor off` shows no Reason line, and that advisor/getSession omits disabledReason. [finding 1]
2. src/client/advisor-card.tsx: keep the section mounted while `status === 'loading'` after a first ready render — render the last ready form with fields disabled (or an empty-but-present shell); extend tests/advisor-card.spec.tsx: a visible card does not unmount across a refresh, and the post-apply reload keeps the saved notice visible. [finding 2]
3. tests/gateway.test.ts header ③: reword to "tolerated and stripped from the write payload — accepted, never persisted (2026-09-27 ruling)". [finding 3]
4. tests/commands.test.ts: replace the six retired reason strings with the strings `src/config.ts` emits; reword the `AdvisorComposedConfig.enabled` doc (src/commands.ts:430) to "post-gate pair flag — not the per-session override". [finding 4]
Verification: pnpm typecheck; targeted vitest over config/gateway/commands/settings-live/advisor-card specs; `node scripts/build-client.mjs` + the client-build contract test.

Comment thread src/index.ts Outdated
Comment thread src/client/advisor-card.tsx Outdated
Comment thread tests/gateway.test.ts
…rough refreshes

Review fix wave (PR #98, items 1+2):

- safeEffective's per-session off branch now strips disabledReason instead
  of carrying it: a session turned off by its own /advisor off override
  reports plain disabled (`Advisor: disabled`, no `Reason:`) — the S4
  reason describes the persisted pair, which the off session is not asking
  about. The global readback (safeResolved, /advisor config + the web
  get readback) keeps the reason untouched.
- the flat card HOLDS its mounted form through a healthy card's background
  refresh window (status 'loading' after a settled advisorPresent): the
  store retains the last settled providers/draft/applyState until the ready
  update, so the fields render disabled instead of unmounting — a post-apply
  reload no longer blinks the saved notice away or drops focus. The FIRST
  load (loading with nothing settled) still renders nothing.
- test pins: settings-live (off + pairless status without `Reason:`, the
  config readback keeps it), gateway-session (getSession snapshot for an
  off session carries no disabledReason key — land a pair first so the
  override actually writes, then clear it with null), advisor-card spec
  (form stays mounted + disabled across a held refresh; saved notice stays
  visible through the post-apply reload window).
…rings

Review fix wave (PR #98, items 3+4):

- the gateway test contract header ③ now states the 2026-09-27 ruling
  (the removed `enabled` key is tolerated and stripped from the write
  payload — accepted, never persisted) instead of the superseded
  rejection wording.
- commands.test.ts: the six disabledReason fixture strings still carried
  the pre-removal gate wording (`enabled but provider and model are
  missing — …`); synced to the current resolver copy.
- AdvisorComposedConfig.enabled's doc line corrected to the post-gate
  pair flag (comment-only source change).
@btspoony
btspoony merged commit 5b24ad1 into main Sep 27, 2026
1 check passed
@btspoony
btspoony deleted the feat/web-config-flat-i18n branch September 27, 2026 06:28
@btspoony btspoony mentioned this pull request Sep 27, 2026
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.

1 participant