@fonlan/dsh-model-parameters 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.
@@ -0,0 +1,83 @@
1
+ import { type ModelParametersConfig } from '../shared/config.js';
2
+ /** Structural logger subset satisfied by cordis' LoggerService. */
3
+ export interface CatalogLogger {
4
+ info(format: any, ...param: any[]): void;
5
+ warn(format: any, ...param: any[]): void;
6
+ }
7
+ /** One flattened models.dev entry. */
8
+ export interface CatalogEntry {
9
+ /** models.dev provider id that carries this model. */
10
+ provider: string;
11
+ /** Full model id as models.dev keys it (may carry a vendor/ prefix). */
12
+ id: string;
13
+ /** Display name. */
14
+ name?: string;
15
+ /** Context window in tokens, when known. */
16
+ context?: number;
17
+ /** Output limit in tokens, when known. */
18
+ output?: number;
19
+ /** Whether the model reasons at all, when stated. */
20
+ reasoning?: boolean;
21
+ /** Offered reasoning effort levels (e.g. ["low","high","max"]). */
22
+ effortValues?: string[];
23
+ /** Accepted request modalities, raw from models.dev (may include pdf/audio/video). */
24
+ input?: string[];
25
+ }
26
+ export interface CatalogSnapshot {
27
+ /** Flattened entries in stable provider order. */
28
+ entries: CatalogEntry[];
29
+ /** Number of models.dev providers covered. */
30
+ providers: number;
31
+ /** Epoch ms the cache was last fetched (0 = never fetched). */
32
+ fetchedAt: number;
33
+ }
34
+ export declare class CatalogManager {
35
+ private readonly logger;
36
+ private snapshot;
37
+ private loaded;
38
+ private inflight;
39
+ private readonly dir;
40
+ private readonly cachePath;
41
+ constructor(logger: CatalogLogger, dataDir?: string);
42
+ /** The current snapshot (empty until the first load/fetch completes). */
43
+ snapshotOf(): CatalogSnapshot;
44
+ /**
45
+ * Load the disk cache once, then fetch when stale (or `force`). A failed
46
+ * fetch keeps the last good cache; with no cache at all the snapshot stays
47
+ * empty and the caller simply fills nothing. Concurrent callers share one
48
+ * in-flight refresh.
49
+ * @returns true when the snapshot may have changed.
50
+ */
51
+ ensureFresh(config: Pick<ModelParametersConfig, 'ttlDays'>, force?: boolean): Promise<boolean>;
52
+ /** Force a fetch now; never throws (falls back to cache or keeps empty). */
53
+ refresh(ttlDays: number): Promise<boolean>;
54
+ /**
55
+ * Resolve one dsh model id to the best catalog entry for its provider.
56
+ * @param modelId - the model id stored in the dsh provider profile.
57
+ * @param dshProvider - the dsh provider route id.
58
+ * @param config - plugin config (providerMap + officialProviders).
59
+ */
60
+ resolve(modelId: string, dshProvider: string, config: Pick<ModelParametersConfig, 'providerMap' | 'officialProviders'>): CatalogEntry | undefined;
61
+ /**
62
+ * All entries whose stripped id matches, plus any whose full id matches
63
+ * exactly (a dsh model id may already carry a vendor/ prefix that models.dev
64
+ * keys verbatim).
65
+ */
66
+ private candidatesFor;
67
+ private loadCache;
68
+ private fetchAndPersist;
69
+ private buildSnapshot;
70
+ }
71
+ /**
72
+ * Build the dsh `reasoningEfforts` identity dict from models.dev effort
73
+ * values, filtered to the levels dsh accepts. Returns undefined when nothing
74
+ * valid is offered (or the model does not state reasoning).
75
+ */
76
+ export declare function identityReasoningEfforts(entry: CatalogEntry): Record<string, string> | undefined;
77
+ /**
78
+ * The dsh-writable input modality list for one entry: models.dev values
79
+ * intersected with what pi-ai accepts. Returns undefined when nothing is
80
+ * accepted (writing an empty list is semantically "keep defaults", so absent
81
+ * is the honest fill).
82
+ */
83
+ export declare function acceptedInputModalities(entry: CatalogEntry): ('text' | 'image')[] | undefined;
@@ -0,0 +1,3 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { ModelParametersService } from './service.js';
3
+ export declare function registerApiRoutes(ctx: Context, service: ModelParametersService): () => void;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The model-parameters service: owns the plugin settings namespace, watches
3
+ * the `llm-pi-ai` namespace for provider model writes, reconciles missing
4
+ * fields from the cached models.dev catalog, and tracks a fill report.
5
+ *
6
+ * Trigger model:
7
+ * - any committed `llm-pi-ai` settings change (settings UI apply, manual
8
+ * settings.yaml edit published by the file provider, model-router-style
9
+ * external writes) runs a reconcile;
10
+ * - plugin start runs a backfill reconcile over the existing configuration.
11
+ * Reconcile is fill-only: it never overwrites a present field, and it writes
12
+ * nothing when nothing is missing — so its own writes cannot re-trigger a
13
+ * second patch (the next event sees the fields present and produces no ops).
14
+ */
15
+ import type { Context } from '@deepseek-ai/cordis';
16
+ import { type ModelParametersConfig } from '../shared/config.js';
17
+ import { type CatalogSnapshot } from './catalog.js';
18
+ /** One reconcile run's outcome, surfaced to the settings page. */
19
+ export interface FillReport {
20
+ at: number;
21
+ /** Number of individual field fills applied. */
22
+ filled: number;
23
+ /** Number of providers whose models array was rewritten. */
24
+ touched: number;
25
+ /** Model ids (provider/model) that matched nothing in the catalog. */
26
+ unmatched: string[];
27
+ }
28
+ export declare class ModelParametersService {
29
+ private readonly ctx;
30
+ /**
31
+ * The settings service value captured from the inject callback. The plugin
32
+ * itself declares `inject: []` (settings is optional — headless profiles
33
+ * have no settings service), so `this.ctx.settings` is never resolvable:
34
+ * the context proxy only walks ancestor fibers, and the provider lives on
35
+ * a sibling fiber. The injected child ctx itself is only valid while its
36
+ * fiber is active — the provider can be reloaded (and every dependent
37
+ * fiber torn down) while an async reconcile is in flight — so all reads
38
+ * and writes go through the captured service value, which is a plain
39
+ * object and never walks the fiber chain.
40
+ */
41
+ private settings;
42
+ private scope;
43
+ private lastReport;
44
+ private readonly catalog;
45
+ private reconciling;
46
+ constructor(ctx: Context);
47
+ start(): void;
48
+ stop(): void;
49
+ config(): ModelParametersConfig;
50
+ catalogSnapshot(): CatalogSnapshot;
51
+ report(): FillReport | null;
52
+ /** Force a catalog refresh, then reconcile and report. */
53
+ refreshAndReconcile(): Promise<void>;
54
+ /** Update plugin config toggles from the settings page. */
55
+ updateConfig(patch: Partial<ModelParametersConfig>): Promise<void>;
56
+ /**
57
+ * Fill missing fields on every provider model from the catalog. Serialized:
58
+ * concurrent triggers (event + backfill) coalesce into one run. Fill-only
59
+ * guarantees termination: after a write the fields are present, so the next
60
+ * event produces no ops.
61
+ */
62
+ reconcile(): Promise<void>;
63
+ private runReconcile;
64
+ /** Fill one model entry (fill-only), returning the same object when untouched. */
65
+ private fillModel;
66
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Plugin configuration and shared constants for @fonlan/dsh-model-parameters.
3
+ *
4
+ * The plugin owns the `model-parameters` settings namespace (stored in
5
+ * settings.yaml) and OBSERVES the `llm-pi-ai` namespace: whenever provider
6
+ * models are written there with missing capability fields (which is the norm
7
+ * for a freshly synced gateway), it patches the missing fields from the cached
8
+ * models.dev catalog.
9
+ */
10
+ import z from '@deepseek-ai/schemastery';
11
+ /** This plugin's own settings namespace. */
12
+ export declare const PLUGIN_NS = "model-parameters";
13
+ /** The namespace whose models this plugin completes: pi-ai provider profiles. */
14
+ export declare const TARGET_NS = "llm-pi-ai";
15
+ /** models.dev catalog endpoint. */
16
+ export declare const CATALOG_URL = "https://models.dev/api.json";
17
+ /** Default provider ids treated as first-party vendors (tie-break for id collisions). */
18
+ export declare const DEFAULT_OFFICIAL_PROVIDERS: readonly string[];
19
+ /**
20
+ * dsh pi-ai only accepts these request modalities; models.dev may also list
21
+ * pdf/audio/video, which must be filtered out before writing (schema rejects
22
+ * unknown modality values).
23
+ */
24
+ export declare const ACCEPTED_INPUT_MODALITIES: readonly string[];
25
+ /**
26
+ * Valid `reasoningEfforts` keys in pi-ai, in escalation order. models.dev
27
+ * effort values like `default`/`none`/empty are filtered out; only values in
28
+ * this set are written.
29
+ */
30
+ export declare const REASONING_LEVELS: readonly string[];
31
+ /** Plugin configuration shape. */
32
+ export interface ModelParametersConfig {
33
+ /** Master switch: when off, no fills happen (listener is inert). */
34
+ enabled: boolean;
35
+ /** How many days a cached models.dev catalog is considered fresh. */
36
+ ttlDays: number;
37
+ /** Fill display name from the catalog when the entry lacks one. */
38
+ fillName: boolean;
39
+ /** Fill contextWindow when the entry lacks one. */
40
+ fillContext: boolean;
41
+ /** Fill maxTokens when the entry lacks one. */
42
+ fillMaxTokens: boolean;
43
+ /** Fill reasoningEfforts (identity dict over offered effort levels) when absent. */
44
+ fillReasoning: boolean;
45
+ /** Fill input modalities (filtered to text/image) when absent. */
46
+ fillInput: boolean;
47
+ /** dsh provider id -> models.dev provider id overrides for matching. */
48
+ providerMap: Record<string, string>;
49
+ /** models.dev provider ids treated as first-party vendors for tie-breaking. */
50
+ officialProviders: string[];
51
+ }
52
+ export declare const Config: z<ModelParametersConfig>;
53
+ export declare const DEFAULT_CONFIG: ModelParametersConfig;
package/package.json ADDED
@@ -0,0 +1,98 @@
1
+ {
2
+ "name": "@fonlan/dsh-model-parameters",
3
+ "version": "0.1.0",
4
+ "description": "DSH plugin: when a provider's model sync returns only ids, automatically fill display name, context window, max output tokens, reasoning efforts, and input modalities from the models.dev catalog (cached, TTL-refreshed), and surface a settings page with toggles, refresh, and a fill report.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/fonlan/dsh-model-parameters.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/fonlan/dsh-model-parameters/issues"
12
+ },
13
+ "homepage": "https://github.com/fonlan/dsh-model-parameters",
14
+ "keywords": [
15
+ "dsh",
16
+ "deepseek-harness",
17
+ "plugin",
18
+ "model",
19
+ "models.dev",
20
+ "catalog",
21
+ "provider"
22
+ ],
23
+ "type": "module",
24
+ "main": "lib/index.js",
25
+ "types": "lib/types/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./lib/types/index.d.ts",
29
+ "default": "./lib/index.js"
30
+ },
31
+ "./client": {
32
+ "types": "./lib/types/client/index.d.ts",
33
+ "default": "./lib/client.js"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "files": [
38
+ "lib",
39
+ "src",
40
+ "cordis.patch.yml",
41
+ "README.md",
42
+ "README.en.md",
43
+ "LICENSE"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "scripts": {
49
+ "build": "rm -rf lib && tsc -p tsconfig.build.json && tsdown",
50
+ "prepare": "npm run build",
51
+ "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.client.json",
52
+ "watch": "tsdown --watch"
53
+ },
54
+ "dsh": {
55
+ "bundle": {
56
+ "patch": "./cordis.patch.yml"
57
+ },
58
+ "client": {
59
+ "platform": "web",
60
+ "inject": [
61
+ "@deepseek-ai/dsh-client-runtime",
62
+ "@deepseek-ai/dsh-client-locale",
63
+ "@deepseek-ai/dsh-client-ui-slots",
64
+ "@deepseek-ai/dsh-client-ui-settings"
65
+ ]
66
+ }
67
+ },
68
+ "peerDependencies": {
69
+ "@deepseek-ai/cordis": "^4.0.1",
70
+ "@deepseek-ai/schemastery": "^3.18.1",
71
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
72
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
73
+ "@deepseek-ai/dsh-llm-pi-ai": "^0.1.0-rc.6",
74
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
75
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
76
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
77
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
78
+ "react": "^18.2.0",
79
+ "react-dom": "^18.2.0"
80
+ },
81
+ "devDependencies": {
82
+ "@types/node": "^22.0.0",
83
+ "@types/react": "~18.3.1",
84
+ "@types/react-dom": "~18.3.0",
85
+ "tsdown": "^0.22.14",
86
+ "typescript": "^5.9.0",
87
+ "@deepseek-ai/cordis": "^4.0.1",
88
+ "@deepseek-ai/schemastery": "^3.18.1",
89
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
90
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
91
+ "@deepseek-ai/dsh-llm-pi-ai": "^0.1.0-rc.6",
92
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
93
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
94
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
95
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6"
96
+ },
97
+ "packageManager": "pnpm@11.21.0"
98
+ }
@@ -0,0 +1,59 @@
1
+ /** Client transport for the model-parameters settings API. */
2
+ import type { ModelParametersConfig } from '../shared/config.js'
3
+
4
+ export interface CatalogView {
5
+ entries: number
6
+ providers: number
7
+ fetchedAt: number
8
+ fresh: boolean
9
+ ttlDays: number
10
+ }
11
+
12
+ export interface FillReportView {
13
+ at: number
14
+ filled: number
15
+ touched: number
16
+ unmatched: string[]
17
+ }
18
+
19
+ export interface PluginState {
20
+ config: ModelParametersConfig
21
+ catalog: CatalogView
22
+ report: FillReportView | null
23
+ }
24
+
25
+ export class ModelParametersApiError extends Error {
26
+ constructor(readonly code: string, message: string) {
27
+ super(message)
28
+ }
29
+ }
30
+
31
+ async function call<T>(method: string, payload: Record<string, unknown> = {}): Promise<T> {
32
+ let response: Response
33
+ try {
34
+ response = await fetch(`/plugins/@fonlan/dsh-model-parameters/api/${method}`, {
35
+ method: 'POST',
36
+ headers: { 'content-type': 'application/json' },
37
+ body: JSON.stringify(payload),
38
+ })
39
+ } catch (error) {
40
+ throw new ModelParametersApiError('network', error instanceof Error ? error.message : String(error))
41
+ }
42
+ const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null
43
+ = await response.json().catch(() => null)
44
+ if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) {
45
+ throw new ModelParametersApiError(
46
+ parsed?.error?.code ?? 'http',
47
+ parsed?.error?.message ?? `HTTP ${response.status}`,
48
+ )
49
+ }
50
+ return parsed.value as T
51
+ }
52
+
53
+ export const api = {
54
+ state: () => call<PluginState>('state'),
55
+ refresh: () => call<PluginState>('refresh'),
56
+ reconcile: () => call<PluginState>('reconcile'),
57
+ setConfig: (patch: Partial<ModelParametersConfig>) =>
58
+ call<PluginState>('set-config', { patch }),
59
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @fonlan/dsh-model-parameters client half: the Model Parameters settings
3
+ * page, registered into the settings section slot.
4
+ */
5
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
6
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
7
+ import type {} from '@deepseek-ai/dsh-client-ui-slots'
8
+ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
9
+ import { LOCALE_NS, zh, en } from './locales'
10
+ import { makeSettingsSection } from './settings-section'
11
+
12
+ /** Services required before mounting (provided by the client runtime). */
13
+ export const inject = ['slots', 'locale']
14
+
15
+ /** Client plugin body. */
16
+ export function apply(ctx: ClientContext): void {
17
+ const t = ctx.locale.bind(LOCALE_NS)
18
+ ctx.effect(() => {
19
+ const off = ctx.locale.register(LOCALE_NS, { zh, en })
20
+ return () => off()
21
+ }, 'model-parameters: dictionaries')
22
+
23
+ const SettingsSection = makeSettingsSection(ctx)
24
+ ctx.slots.inject('settings.section', () =>
25
+ ctx.slots.register({
26
+ name: 'settings.section',
27
+ id: 'model-parameters',
28
+ order: 320,
29
+ label: () => t('settingsTitle'),
30
+ locale: LOCALE_NS,
31
+ }, SettingsSection as never),
32
+ )
33
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Locale dictionaries for @fonlan/dsh-model-parameters settings page.
3
+ * zh is the key source of truth; en must carry the exact same key set.
4
+ */
5
+ export const LOCALE_NS = 'model-parameters'
6
+
7
+ export const zh = {
8
+ settingsTitle: '模型参数补全',
9
+ description: '同步模型时自动从 models.dev 目录补全缺失的显示名称、最大上下文、最大输出、推理强度与输入模态。',
10
+ master: '启用自动补全',
11
+ masterDesc: '关闭后不再自动补全任何字段(已补全的值保留)。',
12
+ fields: '补全字段',
13
+ fillName: '显示名称',
14
+ fillNameDesc: '模型条目缺 name 时写入目录中的显示名称。',
15
+ fillContext: '最大上下文',
16
+ fillContextDesc: '缺 contextWindow 时写入目录的上下文上限。',
17
+ fillMaxTokens: '最大输出',
18
+ fillMaxTokensDesc: '缺 maxTokens 时写入目录的输出上限。',
19
+ fillReasoning: '推理强度',
20
+ fillReasoningDesc: '缺 reasoningEfforts 时按目录 effort 档位写入 identity 字典(如 {high:"high", max:"max"})。',
21
+ fillInput: '输入模态',
22
+ fillInputDesc: '缺 input 时写入目录中的 text/image 模态。',
23
+ ttl: '目录更新周期(天)',
24
+ ttlDesc: '缓存的 models.dev 目录超过该天数后,在下次补全时自动刷新。',
25
+ providerMap: 'Provider 映射',
26
+ providerMapDesc: 'dsh provider → models.dev provider 的匹配覆盖(可选)。',
27
+ addMapping: '添加映射',
28
+ removeMapping: '移除',
29
+ catalog: '模型目录',
30
+ catalogEntries: '目录条目',
31
+ catalogProviders: '覆盖 Provider',
32
+ catalogFetched: '上次更新',
33
+ catalogFresh: '新鲜',
34
+ catalogStale: '已过期',
35
+ catalogNever: '尚未获取',
36
+ refresh: '立即更新目录并补全',
37
+ refreshBusy: '更新中…',
38
+ reconcileNow: '立即补全一次',
39
+ report: '最近一次补全',
40
+ reportNever: '尚未执行',
41
+ reportFilled: '补全字段',
42
+ reportTouched: '涉及 Provider',
43
+ reportUnmatched: '未匹配模型',
44
+ reportEmpty: '全部匹配,无未匹配项',
45
+ reportAt: '时间',
46
+ error: '加载失败',
47
+ saving: '保存中…',
48
+ officialProviders: '官方厂商优先级',
49
+ officialProvidersDesc: '同一模型 id 在多处出现时,优先采用这些 models.dev provider 的条目。',
50
+ } as const
51
+
52
+ export const en: Record<keyof typeof zh, string> = {
53
+ settingsTitle: 'Model Parameters',
54
+ description: 'Auto-fill missing display name, context window, max output, reasoning efforts and input modalities from the models.dev catalog when syncing models.',
55
+ master: 'Enable auto-fill',
56
+ masterDesc: 'When off, no fields are filled (already-filled values are kept).',
57
+ fields: 'Fields to fill',
58
+ fillName: 'Display name',
59
+ fillNameDesc: 'Write the catalog display name when a model entry lacks name.',
60
+ fillContext: 'Context window',
61
+ fillContextDesc: 'Write the catalog context limit when contextWindow is missing.',
62
+ fillMaxTokens: 'Max output',
63
+ fillMaxTokensDesc: 'Write the catalog output limit when maxTokens is missing.',
64
+ fillReasoning: 'Reasoning efforts',
65
+ fillReasoningDesc: 'Write an identity reasoningEfforts dict (e.g. {high:"high", max:"max"}) from catalog effort levels when missing.',
66
+ fillInput: 'Input modalities',
67
+ fillInputDesc: 'Write text/image modalities from the catalog when input is missing.',
68
+ ttl: 'Catalog refresh interval (days)',
69
+ ttlDesc: 'The cached models.dev catalog is refreshed on the next fill once older than this many days.',
70
+ providerMap: 'Provider mapping',
71
+ providerMapDesc: 'Optional dsh provider → models.dev provider matching overrides.',
72
+ addMapping: 'Add mapping',
73
+ removeMapping: 'Remove',
74
+ catalog: 'Model catalog',
75
+ catalogEntries: 'Catalog entries',
76
+ catalogProviders: 'Providers covered',
77
+ catalogFetched: 'Last updated',
78
+ catalogFresh: 'fresh',
79
+ catalogStale: 'stale',
80
+ catalogNever: 'not fetched yet',
81
+ refresh: 'Update catalog & fill now',
82
+ refreshBusy: 'Updating…',
83
+ reconcileNow: 'Fill now',
84
+ report: 'Last fill',
85
+ reportNever: 'never run',
86
+ reportFilled: 'fields filled',
87
+ reportTouched: 'providers touched',
88
+ reportUnmatched: 'unmatched models',
89
+ reportEmpty: 'all matched, nothing unmatched',
90
+ reportAt: 'time',
91
+ error: 'Failed to load',
92
+ saving: 'Saving…',
93
+ officialProviders: 'Official provider priority',
94
+ officialProvidersDesc: 'When one model id appears under several providers, prefer entries from these models.dev providers.',
95
+ }
@@ -0,0 +1,172 @@
1
+ .mp-root {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 14px;
5
+ max-width: 720px;
6
+ }
7
+
8
+ .mp-desc {
9
+ margin: 0;
10
+ opacity: 0.85;
11
+ font-size: 13px;
12
+ line-height: 1.5;
13
+ }
14
+
15
+ .mp-card {
16
+ border: 1px solid var(--dsh-border-color, rgba(128, 128, 128, 0.25));
17
+ border-radius: 10px;
18
+ padding: 12px 14px;
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: 10px;
22
+ background: var(--dsh-surface-color, transparent);
23
+ }
24
+
25
+ .mp-card-title {
26
+ margin: 0;
27
+ font-size: 13px;
28
+ font-weight: 600;
29
+ opacity: 0.9;
30
+ }
31
+
32
+ .mp-toggle {
33
+ display: flex;
34
+ align-items: flex-start;
35
+ gap: 10px;
36
+ cursor: pointer;
37
+ }
38
+
39
+ .mp-toggle input[type='checkbox'] {
40
+ margin-top: 3px;
41
+ accent-color: var(--dsh-accent-color, #4f8cff);
42
+ }
43
+
44
+ .mp-toggle-sub {
45
+ padding-left: 22px;
46
+ }
47
+
48
+ .mp-toggle-body {
49
+ display: flex;
50
+ flex-direction: column;
51
+ gap: 2px;
52
+ }
53
+
54
+ .mp-toggle-label {
55
+ font-size: 13px;
56
+ }
57
+
58
+ .mp-toggle-desc {
59
+ font-size: 12px;
60
+ opacity: 0.65;
61
+ }
62
+
63
+ .mp-meta {
64
+ display: flex;
65
+ flex-wrap: wrap;
66
+ gap: 6px 16px;
67
+ font-size: 13px;
68
+ }
69
+
70
+ .mp-ttl {
71
+ display: inline-flex;
72
+ align-items: center;
73
+ gap: 6px;
74
+ font-size: 13px;
75
+ }
76
+
77
+ .mp-ttl input {
78
+ width: 64px;
79
+ padding: 3px 6px;
80
+ border-radius: 6px;
81
+ border: 1px solid var(--dsh-border-color, rgba(128, 128, 128, 0.35));
82
+ background: var(--dsh-input-bg, transparent);
83
+ color: inherit;
84
+ }
85
+
86
+ .mp-actions {
87
+ display: flex;
88
+ gap: 8px;
89
+ flex-wrap: wrap;
90
+ }
91
+
92
+ .mp-button {
93
+ padding: 6px 12px;
94
+ border-radius: 8px;
95
+ border: 1px solid var(--dsh-border-color, rgba(128, 128, 128, 0.35));
96
+ background: var(--dsh-accent-color, #4f8cff);
97
+ color: #fff;
98
+ font-size: 13px;
99
+ cursor: pointer;
100
+ }
101
+
102
+ .mp-button:disabled {
103
+ opacity: 0.5;
104
+ cursor: default;
105
+ }
106
+
107
+ .mp-button-ghost {
108
+ background: transparent;
109
+ color: inherit;
110
+ }
111
+
112
+ .mp-map-row {
113
+ display: flex;
114
+ align-items: center;
115
+ gap: 6px;
116
+ }
117
+
118
+ .mp-map-row input {
119
+ flex: 1;
120
+ padding: 4px 8px;
121
+ border-radius: 6px;
122
+ border: 1px solid var(--dsh-border-color, rgba(128, 128, 128, 0.35));
123
+ background: var(--dsh-input-bg, transparent);
124
+ color: inherit;
125
+ font-size: 13px;
126
+ }
127
+
128
+ .mp-map-arrow {
129
+ opacity: 0.6;
130
+ }
131
+
132
+ .mp-report {
133
+ margin: 0;
134
+ padding-left: 18px;
135
+ font-size: 13px;
136
+ line-height: 1.7;
137
+ }
138
+
139
+ .mp-code {
140
+ background: var(--dsh-code-bg, rgba(128, 128, 128, 0.15));
141
+ border-radius: 4px;
142
+ padding: 1px 5px;
143
+ font-size: 12px;
144
+ word-break: break-all;
145
+ }
146
+
147
+ .mp-badge {
148
+ font-size: 11px;
149
+ padding: 1px 7px;
150
+ border-radius: 999px;
151
+ margin-left: 4px;
152
+ }
153
+
154
+ .mp-badge-ok {
155
+ background: rgba(52, 199, 89, 0.18);
156
+ color: #34c759;
157
+ }
158
+
159
+ .mp-badge-warn {
160
+ background: rgba(255, 149, 0, 0.18);
161
+ color: #ff9500;
162
+ }
163
+
164
+ .mp-muted {
165
+ font-size: 13px;
166
+ opacity: 0.6;
167
+ }
168
+
169
+ .mp-error {
170
+ font-size: 13px;
171
+ color: #ff3b30;
172
+ }