dsh-fallback-router 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +162 -2
  3. package/README.zh-CN.md +170 -0
  4. package/bundle/cordis.patch.yml +19 -0
  5. package/dist/authorized-route.d.ts +68 -0
  6. package/dist/automatch.d.ts +98 -0
  7. package/dist/chains.d.ts +186 -0
  8. package/dist/client/ConversationFallbackSwitch.d.ts +91 -0
  9. package/dist/client/FallbacksCard.d.ts +36 -0
  10. package/dist/client/GeneralFallbacksRow.d.ts +19 -0
  11. package/dist/client/ModelsPanel.d.ts +64 -0
  12. package/dist/client/SubagentRoleBadge.d.ts +53 -0
  13. package/dist/client/fallbacks-store.d.ts +631 -0
  14. package/dist/client/index.d.ts +64 -0
  15. package/dist/client/index.js +3420 -0
  16. package/dist/client/locales.d.ts +454 -0
  17. package/dist/client/switch-guard.d.ts +17 -0
  18. package/dist/client/use-snapshot.d.ts +11 -0
  19. package/dist/commands.d.ts +380 -0
  20. package/dist/config.d.ts +239 -0
  21. package/dist/cooldown.d.ts +57 -0
  22. package/dist/events.d.ts +52 -0
  23. package/dist/gateway.d.ts +359 -0
  24. package/dist/index.d.ts +195 -0
  25. package/dist/index.js +4587 -0
  26. package/dist/models.d.ts +306 -0
  27. package/dist/override.d.ts +39 -0
  28. package/dist/presets.d.ts +20 -0
  29. package/dist/recovery.d.ts +92 -0
  30. package/dist/role-notice.d.ts +184 -0
  31. package/dist/role-persona.d.ts +71 -0
  32. package/dist/role-projection-key.d.ts +41 -0
  33. package/dist/role-projection.d.ts +152 -0
  34. package/dist/role-resolution.d.ts +105 -0
  35. package/dist/roles.d.ts +90 -0
  36. package/dist/route-allowlist.d.ts +43 -0
  37. package/dist/schema.d.ts +46 -0
  38. package/dist/seeds.d.ts +237 -0
  39. package/dist/selectors.d.ts +38 -0
  40. package/dist/state.d.ts +205 -0
  41. package/dist/subagent-policy.d.ts +123 -0
  42. package/dist/subagents-seam.d.ts +305 -0
  43. package/dist/time-slots.d.ts +147 -0
  44. package/dist/tui-settings.d.ts +145 -0
  45. package/dist/tui.d.ts +63 -0
  46. package/dist/virtual-adapter.d.ts +259 -0
  47. package/icon.svg +16 -0
  48. package/locale/en.json +6 -0
  49. package/locale/zh.json +6 -0
  50. package/package.json +138 -6
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Fallback chain resolution (spec §7.2; plan fallbacks-role-runtime Task 1).
3
+ *
4
+ * `resolveChain` returns the ordered candidate list for a failing
5
+ * (provider, model). Candidates come from the role's own chain plus
6
+ * `rootChain` appended as an inherit-root fallback — **append-not-replace**:
7
+ * `[...(roleDef.chain ?? []), ...(roleDef.fallback === 'none' ? [] :
8
+ * rootChain)]` (role entries first, rootChain entries after). The reserved
9
+ * built-in `inherit` id (INHERIT_ROLE_ID) resolves to `rootChain` silently —
10
+ * it is the legal no-rule-match role, not a typo'd id. A truly undeclared
11
+ * role id (defense) resolves to `rootChain` alone + one warn. Chain-key
12
+ * specificity lookup (`exact` / `provider/*` / role / `default` keys) is
13
+ * deleted — the two-block config model has no chain keys.
14
+ *
15
+ * Wildcard entries (`provider/*`) are resolved against the failing model —
16
+ * keep the model id, swap only the provider; the "target provider has no
17
+ * such model id" skip is judged by {@link resolveCandidate} (via
18
+ * `modelExists`) or by the caller's filter.
19
+ *
20
+ * The concatenation itself lives in ONE helper — {@link buildRoleEntries}
21
+ * (qc1 S-3 SSOT) — shared by {@link resolveChainViews} and
22
+ * {@link hasWildcardEntry}, so the wildcard probe is exact (qc2 F-003:
23
+ * `fallback: 'none'` excludes `rootChain`) and can never diverge from the
24
+ * resolution. Defensive warns (unknown role / undeclared rule target) go
25
+ * through an injected `warn` callback — the wiring passes the cordis
26
+ * `logger.warn` (qc2 F-002 / qc3 S-3), with `console.warn` as the
27
+ * direct-call default.
28
+ *
29
+ * Cooldown / failed-set / same-as-current filtering is caller-side;
30
+ * {@link createCandidateFilter} provides the ready-made predicate. A
31
+ * `CONTEXT_WINDOW_EXCEEDED` walk adds one more caller-side dimension to the
32
+ * same predicate — candidates whose disclosed context window is not larger
33
+ * than the failing model's cannot fit the request either
34
+ * ({@link fitsContextWindow}; unknown windows keep the candidate).
35
+ *
36
+ * @module dsh-fallback-router/chains
37
+ */
38
+ import { type FallbacksRole } from './config.ts';
39
+ import type { CooldownStore, StepFailureSet } from './cooldown.ts';
40
+ import { type Selector } from './selectors.ts';
41
+ /** The model that just failed — the anchor for chain resolution. */
42
+ export interface FailingModel {
43
+ provider: string;
44
+ model: string;
45
+ }
46
+ /**
47
+ * Resolve one chain entry against the failing model.
48
+ *
49
+ * - `provider/model` → that exact selector (not subject to `modelExists` —
50
+ * the user explicitly listed it).
51
+ * - `provider/*` → keeps the failing model id, swaps only the provider; when
52
+ * `modelExists` is given and the target provider has no such model id →
53
+ * `null` (skip).
54
+ * - Malformed entries → `null`: they "do not take effect"; the strict throw
55
+ * path is {@link parseSelector}, used by the Task 3 config-warning path.
56
+ */
57
+ export declare function resolveCandidate(entry: string, failing: FailingModel, modelExists?: (provider: string, model: string) => boolean): Selector | null;
58
+ /**
59
+ * Single-pass chain resolution for the decision path (T1 review Important
60
+ * #2). Walks the role's concatenated entries ONCE and returns the unfiltered
61
+ * view — `all` (every resolvable candidate, no filter and no existence
62
+ * probe: the caller's early-exit / annotation view) — plus the parallel
63
+ * `wildcard` provenance (`wildcard[i]` = candidate `all[i]` came from a
64
+ * `provider/*` entry). The caller builds the existence probe from `all` and
65
+ * then derives the surviving view with {@link selectCandidates}, so the
66
+ * decision path never resolves the chain twice (and an unknown role warns
67
+ * at most once per decision).
68
+ *
69
+ * The reserved built-in `inherit` id (INHERIT_ROLE_ID) is legal — it
70
+ * resolves to `rootChain` silently (the no-rule-match role, not a typo'd
71
+ * id). Only a truly undeclared id (∉ declared ids ∪ {'inherit'}) warns once
72
+ * (defensive — never crashes) through `warn` (defaults to `console.warn`
73
+ * for direct-call compatibility; the decision path injects the cordis
74
+ * `logger.warn` — qc2 F-002 / qc3 S-3). Malformed entries never become
75
+ * candidates.
76
+ */
77
+ export declare function resolveChainViews(roles: readonly FallbacksRole[], rootChain: readonly string[], role: string, provider: string, model: string, warn?: (message: string) => void): {
78
+ all: Selector[];
79
+ wildcard: boolean[];
80
+ };
81
+ /**
82
+ * Derive the surviving candidates from a {@link resolveChainViews} pass:
83
+ * the caller filter plus the wildcard-only existence probe. `modelExists`
84
+ * applies to `provider/*`-origin candidates only (scoped by the parallel
85
+ * `wildcard` provenance — T2 review Important #1: exact entries are never
86
+ * existence-filtered), mirroring how {@link resolveCandidate} receives it.
87
+ * Order and duplicates are preserved.
88
+ */
89
+ export declare function selectCandidates(all: readonly Selector[], wildcard: readonly boolean[], filter?: (candidate: Selector) => boolean, modelExists?: (provider: string, model: string) => boolean): Selector[];
90
+ /**
91
+ * Ordered fallback candidates for the failing (provider, model).
92
+ *
93
+ * Candidates are the role's concatenated entries (spec §7.2, see
94
+ * {@link buildRoleEntries} — the single concatenation SSOT): the declared
95
+ * role's own `chain` followed by `rootChain` unless `fallback: 'none'`
96
+ * (append-not-replace — role entries first, rootChain as the tail). The
97
+ * reserved built-in `inherit` id resolves to `rootChain` silently; a truly
98
+ * undeclared/unknown `role` id resolves to `rootChain` alone and warns once
99
+ * (defensive — never crashes). `filter` optionally drops candidates — the
100
+ * caller owns cooldown/failed-set/same-model filtering (see
101
+ * {@link createCandidateFilter}). The `modelExists` existence probe is NOT
102
+ * applied per entry here: {@link resolveChainViews} returns the parallel
103
+ * `wildcard` provenance, and {@link selectCandidates} applies `modelExists`
104
+ * to `provider/*`-origin candidates ONLY (qc1 S-2 — exact entries are never
105
+ * existence-filtered, spec §2 clause 2 / T2 review Important #1
106
+ * decision-path contract). `warn` forwards to {@link resolveChainViews}
107
+ * (defaults to `console.warn`; the decision path injects `logger.warn`).
108
+ *
109
+ * Single-pass: delegates to {@link resolveChainViews} + {@link selectCandidates}
110
+ * (one concatenated-entry walk, warn at most once per call).
111
+ */
112
+ export declare function resolveChain(roles: readonly FallbacksRole[], rootChain: readonly string[], role: string, provider: string, model: string, filter?: (candidate: Selector) => boolean, modelExists?: (provider: string, model: string) => boolean, warn?: (message: string) => void): Selector[];
113
+ /**
114
+ * Whether any entry of the role's concatenated candidates is a wildcard
115
+ * (`provider/*`). F-002: {@link resolveChain} resolves wildcard entries to
116
+ * concrete models, so the wildcard provenance is invisible on the resolved
117
+ * candidate list — the decision path consults the same concatenation
118
+ * `resolveChain` walks to decide whether the catalog existence probe is
119
+ * needed at all.
120
+ *
121
+ * The probe walks {@link buildRoleEntries} — the SAME concatenation
122
+ * {@link resolveChainViews} resolves (qc1 S-3 single SSOT) — so it is
123
+ * exact, never an over-approximation (qc2 F-003 / qc3 S-1): for
124
+ * `fallback: 'none'` roles `rootChain` is excluded, and a wildcard that can
125
+ * never reach the candidate list never builds a catalog probe. No
126
+ * false-negative is possible: any wildcard the resolution could reach is on
127
+ * this concatenation by construction. Malformed entries never become
128
+ * candidates and are skipped.
129
+ */
130
+ export declare function hasWildcardEntry(roles: readonly FallbacksRole[], rootChain: readonly string[], role: string): boolean;
131
+ /** Inputs for {@link createCandidateFilter}. */
132
+ export interface CandidateFilterOptions {
133
+ /** The currently active (failing) model — skipped as "same as current". */
134
+ current: FailingModel;
135
+ /** Cooldown store: suppressed candidates are skipped (keyed `provider/model`). */
136
+ cooldown: Pick<CooldownStore, 'isSuppressed'>;
137
+ /** Failed-set for the current step: already-failed candidates are skipped. */
138
+ failed: Pick<StepFailureSet, 'has'>;
139
+ /** Optional existence probe: candidates the target provider lacks are skipped. */
140
+ modelExists?: (provider: string, model: string) => boolean;
141
+ /**
142
+ * The failing model's context window, when known — set only for a
143
+ * `CONTEXT_WINDOW_EXCEEDED` walk. Candidates whose own known window is not
144
+ * LARGER than this cannot fit the request either and are skipped.
145
+ */
146
+ minContextWindow?: number;
147
+ /** Context-window lookup for a candidate route; `undefined` = not disclosed. */
148
+ contextWindowOf?: (provider: string, model: string) => number | undefined;
149
+ }
150
+ /**
151
+ * The caller-side candidate filter (Task 3): a candidate is usable when it
152
+ * differs from the current model, is not cooldown-suppressed, has not failed
153
+ * in this step, can fit the request that overflowed (context-window walks
154
+ * only — see {@link fitsContextWindow}), and (when `modelExists` is given)
155
+ * exists on its provider.
156
+ */
157
+ export declare function createCandidateFilter(options: CandidateFilterOptions): (candidate: Selector) => boolean;
158
+ /**
159
+ * Why one considered candidate was excluded from the selection (spec §2
160
+ * 行为可见性). `context-window` is the `CONTEXT_WINDOW_EXCEEDED` walk's
161
+ * exclusion: the candidate's disclosed window is not larger than the failing
162
+ * model's, so the request that just overflowed cannot fit there either.
163
+ */
164
+ export type CandidateSkipReason = 'same-as-current' | 'cooldown' | 'step-failed' | 'context-window' | 'missing-id';
165
+ /** One entry of the ordered, per-candidate annotation (T3 review Minor 1). */
166
+ export interface AnnotatedCandidate {
167
+ candidate: Selector;
168
+ /** Why this candidate was excluded; `undefined` = it survived every exclusion. */
169
+ skip?: CandidateSkipReason;
170
+ }
171
+ /**
172
+ * Annotate the ordered "considered" candidate list with each candidate's skip
173
+ * reason (spec §2 行为可见性: the switch log must show the attempt order and
174
+ * why each candidate was skipped). The selection-relevant view is the
175
+ * `surviving` list the decision path resolved with filter + existence probe:
176
+ * a candidate absent from it failed one of the exclusions, and the concrete
177
+ * reason is derived from the same checks {@link createCandidateFilter}
178
+ * applies, in the same precedence. `missing-id` therefore only ever labels
179
+ * entries the existence probe dropped — exact entries are never
180
+ * existence-probed (T2 contract), so they stay in `surviving` and are
181
+ * reported as usable, never as missing-id.
182
+ *
183
+ * This is the diagnostic counterpart of {@link createCandidateFilter} — pure,
184
+ * order-preserving, and duplicate-preserving.
185
+ */
186
+ export declare function annotateCandidates(candidates: readonly Selector[], surviving: readonly Selector[], options: Pick<CandidateFilterOptions, 'current' | 'cooldown' | 'failed' | 'minContextWindow' | 'contextWindowOf'>): AnnotatedCandidate[];
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Conversation-level fallback-switch visibility (plan fallbacks-aux-seams,
3
+ * task 2, D1+D2 seam).
4
+ *
5
+ * Every `fallbacks/switch` session event becomes its own chat-transcript
6
+ * node (`fallbacks-switch`), rendered as a compact system-style line at the
7
+ * switch's event seq — the user sees the recovery happen in place
8
+ * (provider/model A → B, role · reason), instead of the event existing only
9
+ * in the raw `sessions.history` event feed (it is NOT a SurfaceEventType,
10
+ * so the `unknown-surface` fallback never picked it up and the transcript
11
+ * showed nothing).
12
+ *
13
+ * Contract notes (dsh-private, current homes, verified 2026-08-28):
14
+ * - D1 registry: `ConversationEventRegistry.register(definition)` — the
15
+ * client Context's `uiConversation.events` service seat
16
+ * (`ui-conversation conversation/event-registry.ts:17-25`); external
17
+ * registration precedent `ui-workflow-run/src/client/index.ts:22-26`
18
+ * (its inject list declares `uiConversation` for the same direct read).
19
+ * The engine feeds EVERY session event to each definition's `match`
20
+ * (`ui-conversation conversation/assembler.ts:406-414`) — non-surface
21
+ * plugin events included — and live events reach the engine through the
22
+ * assembly window pump (`ui-conversation conversation/assembly.ts:97-105`
23
+ * `assembler.append`).
24
+ * - D2 seat: `conversation.chat.node` is a keyed seat dispatched by
25
+ * `ChatConversationViewNode.kind` (`ui-chat contract/slots.ts:171-180`;
26
+ * `chat/ChatNodeSeat.tsx:212` renderSlot dispatch), externally registrable
27
+ * as `{ name, key, locale }` (precedents: ui-tool `tool-call`
28
+ * `apply.ts:28-31`, ui-goal `command-input` `index.ts:60-63`,
29
+ * ui-workflow-run `workflow-run`).
30
+ * - Purity: this file only type-imports `@deepseek-ai/dsh-client-ui-chat/client`
31
+ * and `@deepseek-ai/dsh-client-ui-conversation/client` (both erased at
32
+ * build); the renderer self-draws on `--dsw-alias-*` tokens. Render-only:
33
+ * the Definition is a pure view contribution — no message construction,
34
+ * no model-context injection (C4 excluded by scope).
35
+ */
36
+ import type { ReactNode } from 'react';
37
+ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
38
+ import type { ConversationNodeDefinition } from '@deepseek-ai/dsh-client-ui-conversation/client';
39
+ import type { FallbackSwitchReason } from '../events.ts';
40
+ /** Final chat payload of one decided fallback switch (snapshot of the event). */
41
+ export interface FallbacksSwitchChatData {
42
+ readonly seq: number;
43
+ readonly time: number;
44
+ readonly turn: number;
45
+ readonly step: number;
46
+ /** The model the request was using when the switch was decided. */
47
+ readonly from: {
48
+ readonly provider: string;
49
+ readonly model: string;
50
+ };
51
+ /** The chain candidate the switch moves to. */
52
+ readonly to: {
53
+ readonly provider: string;
54
+ readonly model: string;
55
+ };
56
+ /** The fallback-chain role the decision resolved for the agent. */
57
+ readonly role: string;
58
+ readonly reason: FallbackSwitchReason;
59
+ }
60
+ declare module '@deepseek-ai/dsh-client-ui-chat/client' {
61
+ interface ChatNodeDataMap {
62
+ /** One decided fallback provider/model switch, rendered at its event seq. */
63
+ 'fallbacks-switch': FallbacksSwitchChatData;
64
+ }
65
+ }
66
+ /**
67
+ * One switch event → one chat node. Each `fallbacks/switch` event is its own
68
+ * Context (id = event seq — the durable unique key), so every match is a
69
+ * `start`; `update` is a passthrough (no aggregation — D3's per-Turn
70
+ * counting is a separate, unselected seam).
71
+ */
72
+ export declare const fallbackSwitchDefinition: ConversationNodeDefinition<FallbacksSwitchChatData>;
73
+ /**
74
+ * Props delivered by the keyed chat-node seat: runtime share + the `fallbacks` locale seat.
75
+ */
76
+ export type ConversationFallbackSwitchProps = PropsRuntime<'conversation.chat.node', 'fallbacks-switch'> & PropsLocale<'fallbacks'>;
77
+ /**
78
+ * Render one fallback switch as a compact system-style transcript line.
79
+ *
80
+ * Geometry follows the upstream chat system rows (the compaction boundary
81
+ * notice: warning-toned title + separator + ellipsized summary —
82
+ * `chat/MessageItem .module.css:38-122`); every color resolves through a
83
+ * `--dsw-alias-*` token. A reason outside the current union renders raw (forward-compatible
84
+ * durable log, same rule as the card/general row summaries). A malformed or
85
+ * partial payload (version skew) degrades to the title-only line instead of
86
+ * throwing during interpolation — the transcript slot stays visible with the
87
+ * warning-toned "model downgraded" title (T1 copy) and no summary details.
88
+ * @param props - composed keyed seat props.
89
+ * @returns the switch line element tree.
90
+ */
91
+ export declare function ConversationFallbackSwitch({ node, t }: ConversationFallbackSwitchProps): ReactNode;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The `llm-router` plugin card on the web Plugins page.
3
+ *
4
+ * SCOPE (deliberate, post-merge): the card renders ONE thing — the 模型配置
5
+ * panel. The legacy fallbacks surface (the global 默认降级链 editor, the
6
+ * time-slot table, the subagent role list, the advanced scalars) is not
7
+ * editable here any more:
8
+ *
9
+ * - `rootChain` / `timeSlots` / `namedChains` are COMPILED from the model
10
+ * rows by `withCompiledRouter`, so hand-editing them would be overwritten
11
+ * on the next read;
12
+ * - `roles` / `cooldownMs` / `revertPolicy` / `presets` / `tz` keep whatever
13
+ * the composed config already carries — a save writes only `models` and
14
+ * `providers`, so nothing is dropped;
15
+ * - provider API keys and other credentials belong to the provider plugins
16
+ * (`llm-pi-ai`, …) and are never surfaced (let alone made editable) here.
17
+ *
18
+ * The card is a thin shell around {@link ModelsPanel}: it loads the store on
19
+ * mount, renders the channel/error notices, and hands the panel the write
20
+ * gate. All authoring logic lives in the panel.
21
+ */
22
+ import { type ReactNode } from 'react';
23
+ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
24
+ import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots';
25
+ import { FallbacksSettingsController, type FallbacksSettingsState } from './fallbacks-store.ts';
26
+ /** The card store (loaded on mount, refreshed on pushed invalidations). */
27
+ export interface FallbacksCardInjected {
28
+ controller: FallbacksSettingsController;
29
+ /** Bare snapshot source the renderer binds as `useSnapshot`. */
30
+ hooks: {
31
+ snapshot: import('@deepseek-ai/dsh-client-store').SnapshotStore<FallbacksSettingsState>;
32
+ };
33
+ }
34
+ /** Props delivered by the slot outlet: runtime share + locale seat + inject face. */
35
+ export type FallbacksCardProps = PropsRuntime<'plugins.bundle.config'> & PropsLocale<'fallbacks'> & InjectFace<FallbacksCardInjected>;
36
+ export declare function FallbacksCard({ controller, useSnapshot }: FallbacksCardProps): ReactNode;
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
3
+ import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
4
+ import type { FallbacksSettingsController, FallbacksSettingsState } from './fallbacks-store.ts';
5
+ /** Injected dependencies of {@link GeneralFallbacksRow} (slot `inject`). */
6
+ export interface GeneralFallbacksRowInjected {
7
+ /** The shared controller (loaded on first mount, refreshed on pushed invalidations). */
8
+ controller: FallbacksSettingsController;
9
+ /** uSES subscription hook bound to the store (inject face — advisor pattern). */
10
+ useSnapshot: SnapshotSelectorHook<FallbacksSettingsState>;
11
+ }
12
+ /** Props delivered by the slot outlet: runtime share + locale seat + inject face. */
13
+ export type GeneralFallbacksRowProps = PropsRuntime<'settings.general.item'> & PropsLocale<'fallbacks'> & GeneralFallbacksRowInjected;
14
+ /**
15
+ * Render the Fallbacks status row.
16
+ * @param props - composed slot props.
17
+ * @returns the row element tree.
18
+ */
19
+ export declare function GeneralFallbacksRow({ controller, useSnapshot, t }: GeneralFallbacksRowProps): ReactNode;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * 模型配置 (model profiles) panel — the ONLY authoring surface of
3
+ * dsh-fallback-router.
4
+ *
5
+ * Every row is a MODEL CONFIGURATION. Two shapes exist:
6
+ *
7
+ * - 单一模型 (`kind: 'route'`) — a `provider/model` route with its own time
8
+ * windows (时间区间, either a frozen preset like 梁高峰/梁低谷 or a
9
+ * hand-written window), its own fallback policy (官方兜底 / 循环兜底 /
10
+ * 不兜底), the extra hops it may try (追加兜底路由) and its own failure
11
+ * rules (错误码 + 正则 + 结构判定).
12
+ * - 自由组合 (`kind: 'combo'`) — no route of its own; every 时间区间 names an
13
+ * existing configuration (`使用配置`), so 梁高峰 can run config A while
14
+ * 梁低谷 runs config B without duplicating either one's settings.
15
+ *
16
+ * Every provider/model field is a dropdown fed by the host catalog
17
+ * (`llm/listConfigurableProviders` + `session/modelCatalog`); a value the
18
+ * catalog does not know is kept as an extra option so a hand-written route is
19
+ * never silently replaced, and an empty catalog degrades to a text input.
20
+ *
21
+ * The host compiles this list down to `rootChain` / `timeSlots` /
22
+ * `namedChains` (`withCompiledRouter`), so the chain-walking engine keeps
23
+ * working unchanged.
24
+ *
25
+ * The panel owns its own draft and its own Save: it writes ONLY `models` and
26
+ * `providers`. Everything the legacy card used to expose (the global chain,
27
+ * the time-slot table, the subagent roles, the advanced scalars) is
28
+ * deliberately NOT editable here — it is either compiled from these rows or
29
+ * left at whatever the config already carries.
30
+ *
31
+ * @module dsh-fallback-router/client/ModelsPanel
32
+ */
33
+ import { type ReactNode } from 'react';
34
+ import type { FallbacksConfig } from '../config.ts';
35
+ /** One model the host can dispatch to, as a dropdown shows it. */
36
+ export interface ModelsCatalogModel {
37
+ id: string;
38
+ name: string;
39
+ /** 推理等级 the model advertises (empty when it has none). */
40
+ efforts?: {
41
+ id: string;
42
+ name: string;
43
+ }[];
44
+ /** The level the model picks when the request names none. */
45
+ defaultEffort?: string;
46
+ }
47
+ /** One provider and its models, as the dropdowns show them. */
48
+ export interface ModelsCatalogProvider {
49
+ id: string;
50
+ name: string;
51
+ models: ModelsCatalogModel[];
52
+ }
53
+ /** The dropdown source, as the card passes it in. */
54
+ export type ModelsCatalog = readonly ModelsCatalogProvider[];
55
+ /** Props: the live config, the catalog behind the dropdowns, and the write path. */
56
+ export interface ModelsPanelProps {
57
+ config: FallbacksConfig;
58
+ writable: boolean;
59
+ saving: boolean;
60
+ onSave: (next: FallbacksConfig) => void;
61
+ /** Provider/model catalog for the dropdowns; omit to fall back to text inputs. */
62
+ catalog?: ModelsCatalog;
63
+ }
64
+ export declare function ModelsPanel({ config, writable, saving, onSave, catalog }: ModelsPanelProps): ReactNode;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Session-header subagent role/model badge (plan subagent-role-badge T3 +
3
+ * plan subagent-role-model): a compact read-only pill in
4
+ * `conversation.session.header.actions` showing the viewed CHILD session's
5
+ * dispatch role and the model route it runs on, hovering as
6
+ * `role → provider/model (request route)`.
7
+ *
8
+ * Contract notes (re-verified 2026-10-23 against the installed dsh 0.2.1-rc.2):
9
+ * - The slot is `kind: 'list'`, `scope: 'session'`, owner props an empty marker
10
+ * (`ConversationHeaderActionOwnerProps`, `children?: never`) — the header
11
+ * renders occupants with `renderSlot(..., {})`, so ALL data flows through the
12
+ * session standard kit.
13
+ * - Every `scope: 'session'` occupant receives the session standard kit, whose
14
+ * members this component uses are `sessionId`, `useSessions` (the shared
15
+ * Session store) and `useProjection` (the viewed session's projection cut).
16
+ * The compiled peer types do not carry that merge into this plugin's
17
+ * typecheck program (ui-session is deliberately not a plugin peer), so the
18
+ * seats are read STRUCTURALLY and guarded — a version-skewed host without them
19
+ * renders nothing, never throws.
20
+ * - The badge belongs to a CHILD session: `byId[sessionId].origin === 'subagent'`
21
+ * is the host's own child-session idiom (`dsh-client-ui-subagent`
22
+ * `SubagentCatalogAction`). Root rows omit `origin`, so a root session renders
23
+ * nothing even though its own `modelSelection` carries a route — the composer
24
+ * owns that control.
25
+ * - Role data: the host's `fallbacksSubagentRole` projection (Task 3b), folded
26
+ * server-side out of the child's own durable log — the ONE role source. The
27
+ * session kit binds the hook to the VIEWED session, so no `sessionId`
28
+ * plumbing, no effect, no state, and no polling. An absent key (host without
29
+ * the unit, or no notice row) omits the role half; `inherit` omits it too.
30
+ * - Route data: the host's EXISTING `modelSelection` projection, read as
31
+ * `next ?? lastUsed` — the route the child's next request will use, falling
32
+ * back to the one its latest recorded request ran on. The two halves are
33
+ * INDEPENDENT: either alone still renders, and both missing renders nothing.
34
+ * - Hook order is render-independent (Task 3b L2 review C-1): the seats are real
35
+ * hooks, so the three reads are unconditional and only their derived values
36
+ * are guarded — a conditional read would change the hook count on the very
37
+ * re-render that publishes a running child's role.
38
+ * - Render-only discipline (C4 pattern, same as `ConversationFallbackSwitch`):
39
+ * the badge contributes a view; no message construction, no model-context
40
+ * injection. Degrade-never-crash: a missing/foreign projection value renders
41
+ * `null` — the actions row collapses cleanly.
42
+ */
43
+ import type { ReactNode } from 'react';
44
+ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
45
+ /** Props delivered by the actions slot outlet: runtime share + locale seat. */
46
+ export type SubagentRoleBadgeProps = PropsRuntime<'conversation.session.header.actions'> & PropsLocale<'fallbacks'>;
47
+ /**
48
+ * Render the viewed SUBAGENT session's role and model.
49
+ * @param props - composed slot props (the session-kit `useProjection` /
50
+ * `useSessions` seats are read structurally — see the module docblock).
51
+ * @returns the badge element, or `null` when there is nothing to show.
52
+ */
53
+ export declare function SubagentRoleBadge(props: SubagentRoleBadgeProps): ReactNode;