feat: flat advisor settings card, localized plugin meta, drop the config-level enabled switch - #98
Merged
Merged
Conversation
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
commented
Sep 27, 2026
btspoony
left a comment
Member
Author
There was a problem hiding this comment.
⚠️ 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 }), keepingdisabledReasonfrom an incomplete pair; the contract atsrc/commands.ts:292-297says 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 ontoadvisor/getSession(src/index.ts:830-831). - Impact: on the default fresh-install state (pairless global) after
/advisor off,/advisor statusrenders "Reason: provider and model are missing — configure both…" under a deliberately-disabled session and the web session snapshot reportsenabled: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 theAdvisorSessionStatus.disabledReasoncontract 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— anystatus !== 'ready'returnsnull("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 withawait this.load()(src/client/advisor-store.ts:665); window focus +settings/document-updated+llm/adapters-updatedall re-runload()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-loadnullstays.
🔵 gateway.test.ts contract header ③ contradicts the tolerance test beneath it
- Evidence:
tests/gateway.test.ts:18-20— header ③ says the removedenabledkey "is rejected the same way"; the same file tests and pins the opposite ("tolerates the legacyenabledkey 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 — …", whichsrc/config.ts:227no longer emits ("provider and model are missing — …");src/commands.ts:430-431documentsAdvisorComposedConfig.enabledas 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 enabledremoved; row toggle is the switch; legacyenabled: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-base700ba7b) 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_KEYSis exactly{enabled}; strict unknown-key rejection preserved and pinned (src/config.ts,tests/config.test.ts,tests/settings.test.ts); gatewaysettolerates-then-strips (src/gateway.ts:278-313, tested attests/gateway.test.ts:359-377); noconfigEnabledleftovers (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.tsis an import-free string constant (bundle purity holds); package.json exports/files/icon consistent. - CI on this PR:
validatepass (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-807assertssource: { 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.tsemission 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
enabledis exempted. - config-level
enableddead-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.…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).
Merged
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
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
enabledswitch — the plugin-row toggle on the Plugins page is the master switch now.Changes
locale/en.json+locale/zh.json(meta.title= Advisor / 顾问, localized descriptions) and a hand-drawnicon.svgin the official gradient-tile style;package.jsongainsicon,exports["./locale/*.json"], and matchingfilesentries. The bundle detail page (title/description/icon) and the component row localize automatically via the host'sreadPluginMeta.enabledkey (breaking): dropped from the Loader schema, the config contract, the per-session override seed, the TUI/settingssection, and the web card. The S4 gate now keys on the pair alone: provider + model present → enabled. Legacyenabled: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.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.Testing
pnpm typecheckexit 0 (main / client / spec tsconfigs)pnpm build+ bundle contract greps: nodsh-client-ui-primitivesrequire, flat class map + default-prompt text present,plugins.bundle.configseat pinned./locale/*.json+./icon.svgexports/files