@toddzheng024/dscode-bundle 0.7.11 → 0.7.13
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.
- package/THIRD_PARTY_NOTICES.md +3 -3
- package/cordis.patch.yml +1 -1
- package/package.json +5 -4
- package/plugins/i18n/messages.mjs +6 -6
- package/plugins/session-metrics/view.mjs +16 -5
- package/plugins/tui-tools/index.mjs +14 -1
- package/vendor/tui/lib/app.mjs +6674 -0
- package/vendor/tui/lib/approval.mjs +122 -0
- package/vendor/tui/lib/attachments.mjs +218 -0
- package/vendor/tui/lib/authorization-panel.mjs +242 -0
- package/vendor/tui/lib/authorization.mjs +111 -0
- package/vendor/tui/lib/commands.mjs +123 -0
- package/vendor/tui/lib/dscode/chat.mjs +64 -0
- package/vendor/tui/lib/dscode/flags.mjs +32 -0
- package/vendor/tui/lib/dscode/model-search.mjs +68 -0
- package/vendor/tui/lib/dscode/paste.mjs +87 -0
- package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
- package/vendor/tui/lib/dscode/welcome.mjs +100 -0
- package/vendor/tui/lib/editor-keys.mjs +346 -0
- package/vendor/tui/lib/editor.mjs +50 -0
- package/vendor/tui/lib/fork.mjs +24 -0
- package/vendor/tui/lib/git-workflow.mjs +278 -0
- package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
- package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
- package/vendor/tui/lib/index.mjs +2198 -0
- package/vendor/tui/lib/input-split.mjs +170 -0
- package/vendor/tui/lib/internals.mjs +90 -0
- package/vendor/tui/lib/invariant.mjs +22 -0
- package/vendor/tui/lib/kernel-panels.mjs +1265 -0
- package/vendor/tui/lib/keyboard.mjs +283 -0
- package/vendor/tui/lib/language-panel.mjs +41 -0
- package/vendor/tui/lib/locales/en.mjs +495 -0
- package/vendor/tui/lib/locales/zh.mjs +495 -0
- package/vendor/tui/lib/mentions.mjs +150 -0
- package/vendor/tui/lib/model-capabilities.mjs +245 -0
- package/vendor/tui/lib/models.mjs +215 -0
- package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
- package/vendor/tui/lib/permissions.mjs +45 -0
- package/vendor/tui/lib/plugin-inventory.mjs +27 -0
- package/vendor/tui/lib/presets.mjs +46 -0
- package/vendor/tui/lib/provider-settings.mjs +600 -0
- package/vendor/tui/lib/questions.mjs +116 -0
- package/vendor/tui/lib/rainbow.mjs +181 -0
- package/vendor/tui/lib/render/animations.mjs +665 -0
- package/vendor/tui/lib/render/editor.mjs +437 -0
- package/vendor/tui/lib/render/export.mjs +119 -0
- package/vendor/tui/lib/render/fuzzy.mjs +79 -0
- package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
- package/vendor/tui/lib/render/inspector.mjs +93 -0
- package/vendor/tui/lib/render/lines.mjs +538 -0
- package/vendor/tui/lib/render/markdown.mjs +592 -0
- package/vendor/tui/lib/render/projection.mjs +1862 -0
- package/vendor/tui/lib/render/status.mjs +591 -0
- package/vendor/tui/lib/render/text.mjs +157 -0
- package/vendor/tui/lib/render/tool-detail.mjs +187 -0
- package/vendor/tui/lib/render/tool-preview.mjs +79 -0
- package/vendor/tui/lib/render/usage.mjs +336 -0
- package/vendor/tui/lib/render/width.mjs +191 -0
- package/vendor/tui/lib/session-directory.mjs +329 -0
- package/vendor/tui/lib/session-query.mjs +184 -0
- package/vendor/tui/lib/session-switch.mjs +51 -0
- package/vendor/tui/lib/settings-file.mjs +75 -0
- package/vendor/tui/lib/skills.mjs +119 -0
- package/vendor/tui/lib/startup.mjs +111 -0
- package/vendor/tui/lib/store.mjs +129 -0
- package/vendor/tui/lib/subagents.mjs +200 -0
- package/vendor/tui/lib/terminal-title.mjs +186 -0
- package/vendor/tui/lib/theme-panel.mjs +60 -0
- package/vendor/tui/lib/theme.mjs +379 -0
- package/vendor/tui/lib/update-panel.mjs +239 -0
- package/vendor/tui/lib/update.mjs +101 -0
- package/vendor/tui/lib/version.mjs +105 -0
- package/vendor/tui/lib/whale-glyph.mjs +20 -0
- package/vendor/tui/LICENSE +0 -21
- package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
- package/vendor/tui/dscode-email/cli.mjs +0 -32
- package/vendor/tui/dscode-email/contacts.mjs +0 -36
- package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
- package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
- package/vendor/tui/dscode-email/gmail.mjs +0 -194
- package/vendor/tui/dscode-email/imap.mjs +0 -136
- package/vendor/tui/dscode-email/inbox.d.mts +0 -25
- package/vendor/tui/dscode-email/inbox.mjs +0 -76
- package/vendor/tui/dscode-email/smtp.mjs +0 -69
- package/vendor/tui/dscode-email/store.mjs +0 -32
- package/vendor/tui/dscode-providers/catalog.mjs +0 -136
- package/vendor/tui/dscode-providers/effort.mjs +0 -35
- package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
- package/vendor/tui/index.mjs +0 -44483
- package/vendor/tui/invariant.mjs +0 -21
- package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
- package/vendor/tui/session-query.mjs +0 -150
- package/vendor/tui/startup.mjs +0 -109
- package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
- package/vendor/tui/types/app.d.ts +0 -348
- package/vendor/tui/types/approval.d.ts +0 -59
- package/vendor/tui/types/attachments.d.ts +0 -52
- package/vendor/tui/types/authorization-panel.d.ts +0 -22
- package/vendor/tui/types/authorization.d.ts +0 -36
- package/vendor/tui/types/commands.d.ts +0 -52
- package/vendor/tui/types/editor-keys.d.ts +0 -105
- package/vendor/tui/types/editor.d.ts +0 -6
- package/vendor/tui/types/fork.d.ts +0 -8
- package/vendor/tui/types/git-workflow.d.ts +0 -121
- package/vendor/tui/types/index.d.ts +0 -223
- package/vendor/tui/types/input-split.d.ts +0 -54
- package/vendor/tui/types/internals.d.ts +0 -26
- package/vendor/tui/types/invariant.d.ts +0 -15
- package/vendor/tui/types/kernel-panels.d.ts +0 -245
- package/vendor/tui/types/keyboard.d.ts +0 -80
- package/vendor/tui/types/language-panel.d.ts +0 -12
- package/vendor/tui/types/locales/en.d.ts +0 -450
- package/vendor/tui/types/locales/zh.d.ts +0 -9
- package/vendor/tui/types/mentions.d.ts +0 -85
- package/vendor/tui/types/model-capabilities.d.ts +0 -82
- package/vendor/tui/types/models.d.ts +0 -133
- package/vendor/tui/types/permissions.d.ts +0 -27
- package/vendor/tui/types/plugin-inventory.d.ts +0 -11
- package/vendor/tui/types/presets.d.ts +0 -22
- package/vendor/tui/types/provider-settings.d.ts +0 -239
- package/vendor/tui/types/questions.d.ts +0 -54
- package/vendor/tui/types/rainbow.d.ts +0 -69
- package/vendor/tui/types/render/animations.d.ts +0 -307
- package/vendor/tui/types/render/editor.d.ts +0 -163
- package/vendor/tui/types/render/export.d.ts +0 -9
- package/vendor/tui/types/render/fuzzy.d.ts +0 -21
- package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
- package/vendor/tui/types/render/inspector.d.ts +0 -62
- package/vendor/tui/types/render/lines.d.ts +0 -86
- package/vendor/tui/types/render/markdown.d.ts +0 -29
- package/vendor/tui/types/render/projection.d.ts +0 -587
- package/vendor/tui/types/render/status.d.ts +0 -196
- package/vendor/tui/types/render/text.d.ts +0 -64
- package/vendor/tui/types/render/tool-detail.d.ts +0 -94
- package/vendor/tui/types/render/tool-preview.d.ts +0 -28
- package/vendor/tui/types/render/usage.d.ts +0 -113
- package/vendor/tui/types/render/width.d.ts +0 -29
- package/vendor/tui/types/session-directory.d.ts +0 -190
- package/vendor/tui/types/session-query.d.ts +0 -92
- package/vendor/tui/types/session-switch.d.ts +0 -17
- package/vendor/tui/types/settings-file.d.ts +0 -41
- package/vendor/tui/types/skills.d.ts +0 -47
- package/vendor/tui/types/startup.d.ts +0 -65
- package/vendor/tui/types/store.d.ts +0 -58
- package/vendor/tui/types/subagents.d.ts +0 -70
- package/vendor/tui/types/terminal-title.d.ts +0 -66
- package/vendor/tui/types/theme-panel.d.ts +0 -24
- package/vendor/tui/types/theme.d.ts +0 -434
- package/vendor/tui/types/update-panel.d.ts +0 -49
- package/vendor/tui/types/update.d.ts +0 -75
- package/vendor/tui/types/version.d.ts +0 -19
- package/vendor/tui/types/whale-glyph.d.ts +0 -6
- /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
- /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/index.mjs +0 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace @mention support: file and directory candidates from the
|
|
3
|
+
* `fileReferences` service (dsh-file-reference-local), session candidates
|
|
4
|
+
* from the opt-in `sessionReferenceResolver` service, and submission
|
|
5
|
+
* preparation through its `prepare()` API. Picked session mentions land as
|
|
6
|
+
* canonical `@[label](dsh-session:…)` tokens; on submit the text is parsed
|
|
7
|
+
* back into readable `@label` text plus structured references, snapshots are
|
|
8
|
+
* injected via `agent.inject()` before the readable message wakes the driver
|
|
9
|
+
* (`followup`, which queues the next turn whether or not one is running) —
|
|
10
|
+
* exactly the upstream README's wiring. Steering the current turn is a
|
|
11
|
+
* deliberate user action through the `/queue` panel, never an implicit
|
|
12
|
+
* consequence of submitting while busy.
|
|
13
|
+
*
|
|
14
|
+
* File discovery lives entirely in the Harness service (per-agent bounded
|
|
15
|
+
* index, `@dir/` listing, symlink guards, tool/result invalidation); this
|
|
16
|
+
* module only maps candidates to menu rows and never re-implements scanning.
|
|
17
|
+
* The service is agent-scoped (the agent supplies the session cwd and the
|
|
18
|
+
* cache key), so before the first session creates an agent the SAME official
|
|
19
|
+
* search class runs against the launch cwd — @ file completion works on a
|
|
20
|
+
* bare launch, model- and session-independent, and the agent-scoped service
|
|
21
|
+
* takes over once a session exists.
|
|
22
|
+
*
|
|
23
|
+
* @module @deepseek-ai/dsh-code/mentions
|
|
24
|
+
*/
|
|
25
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
26
|
+
import { DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES, DEFAULT_FILE_SEARCH_MAX_ENTRIES, DEFAULT_FILE_SEARCH_MAX_RESULTS, WorkspaceFileSearch, } from '@deepseek-ai/dsh-file-reference-local';
|
|
27
|
+
import { formatSessionReferenceMention, parseSessionReferenceText, } from '@deepseek-ai/dsh-session-reference';
|
|
28
|
+
/** Menu cap on file rows; the service owns ranking and default rows. */
|
|
29
|
+
const MAX_FILE_ROWS = 20;
|
|
30
|
+
/** Whether a mention token is already navigating a filesystem path. */
|
|
31
|
+
export function isPathLikeMentionQuery(query) {
|
|
32
|
+
return /[\\/]/u.test(query);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Create the mention API for one agent's workspace. A missing
|
|
36
|
+
* `fileReferences` service (with an agent present) or `sessionReferenceResolver`
|
|
37
|
+
* degrades that half to empty rows; `prepare` passes text through untouched
|
|
38
|
+
* without references. An undefined agent (a bare launch before any session
|
|
39
|
+
* exists) runs the official WorkspaceFileSearch over the launch cwd — the
|
|
40
|
+
* same class the mounted service uses per agent — so `@` file completion
|
|
41
|
+
* works from the first keystroke; session references wait for the session.
|
|
42
|
+
*
|
|
43
|
+
* `candidates` never reaches for `this` — the runner hands it to the input
|
|
44
|
+
* editor as a detached callback, and a `this`-bound method would throw on
|
|
45
|
+
* every `@` key.
|
|
46
|
+
* @param ctx - context carrying the optional `fileReferences` and
|
|
47
|
+
* `sessionReferenceResolver` services.
|
|
48
|
+
* @param agent - the session owner; excluded from its own session candidates.
|
|
49
|
+
* @param cwd - launch working directory; bounds the pre-session search.
|
|
50
|
+
*/
|
|
51
|
+
export function createMentions(ctx, agent, cwd) {
|
|
52
|
+
const resolver = ctx.get('sessionReferenceResolver');
|
|
53
|
+
const fileReferences = ctx.get('fileReferences');
|
|
54
|
+
const sessionCapable = agent !== undefined && resolver !== undefined;
|
|
55
|
+
// Pre-session fallback: one lazily built search over the launch cwd with
|
|
56
|
+
// the official defaults — pure in-memory index, no handles to release.
|
|
57
|
+
// The mounted service invalidates its per-agent index after every tool
|
|
58
|
+
// result; nothing drives that for this bare instance, so a time window
|
|
59
|
+
// refreshes it instead — without it, files created or deleted before the
|
|
60
|
+
// first session never appear in (or never leave) the fuzzy @ menu.
|
|
61
|
+
const PRE_SESSION_INDEX_TTL_MS = 30_000;
|
|
62
|
+
let preSessionSearch;
|
|
63
|
+
let preSessionIndexedAt = 0;
|
|
64
|
+
const preSessionFiles = (query, signal) => {
|
|
65
|
+
if (preSessionSearch === undefined || Date.now() - preSessionIndexedAt > PRE_SESSION_INDEX_TTL_MS) {
|
|
66
|
+
preSessionSearch?.invalidate();
|
|
67
|
+
preSessionSearch = new WorkspaceFileSearch(cwd, {
|
|
68
|
+
maxResults: DEFAULT_FILE_SEARCH_MAX_RESULTS,
|
|
69
|
+
maxEntries: DEFAULT_FILE_SEARCH_MAX_ENTRIES,
|
|
70
|
+
excludedDirectories: [...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES],
|
|
71
|
+
});
|
|
72
|
+
preSessionIndexedAt = Date.now();
|
|
73
|
+
}
|
|
74
|
+
return preSessionSearch.list(query, signal ?? new AbortController().signal);
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
async candidates(query, signal) {
|
|
78
|
+
const needle = query.trim();
|
|
79
|
+
// A failed discovery half is remembered, never masked: when NOTHING came
|
|
80
|
+
// back the rejection tells the menu "search unavailable" instead of a
|
|
81
|
+
// silent empty list that reads as "no matches". A half that still
|
|
82
|
+
// returned rows shows them — partial results beat an error wall.
|
|
83
|
+
let fileFailure;
|
|
84
|
+
let sessionFailure;
|
|
85
|
+
const [files, sessions] = await Promise.all([
|
|
86
|
+
agent !== undefined && fileReferences !== undefined
|
|
87
|
+
? fileReferences
|
|
88
|
+
.list(agent, needle, signal ?? new AbortController().signal)
|
|
89
|
+
.catch((error) => {
|
|
90
|
+
fileFailure = error;
|
|
91
|
+
return [];
|
|
92
|
+
})
|
|
93
|
+
: agent === undefined
|
|
94
|
+
? preSessionFiles(needle, signal).catch((error) => {
|
|
95
|
+
fileFailure = error;
|
|
96
|
+
return [];
|
|
97
|
+
})
|
|
98
|
+
: Promise.resolve([]),
|
|
99
|
+
sessionCapable && needle !== '' && !isPathLikeMentionQuery(needle) && agent !== undefined
|
|
100
|
+
? resolver.listCandidates(agent, needle, 10, signal).catch((error) => {
|
|
101
|
+
sessionFailure = error;
|
|
102
|
+
return [];
|
|
103
|
+
})
|
|
104
|
+
: Promise.resolve([]),
|
|
105
|
+
]);
|
|
106
|
+
// The service owns ranking (and the bare-@ default rows); the menu caps
|
|
107
|
+
// file rows and always places sessions after files — `@` is a file
|
|
108
|
+
// mention first (Codex's semantics), session references second.
|
|
109
|
+
const fileRows = files.slice(0, MAX_FILE_ROWS).map(candidate => ({
|
|
110
|
+
label: candidate.path,
|
|
111
|
+
description: candidate.kind === 'directory' ? 'Folder' : 'File',
|
|
112
|
+
kind: candidate.kind,
|
|
113
|
+
...candidate.kind === 'file'
|
|
114
|
+
? { path: isAbsolute(candidate.path) ? candidate.path : resolve(cwd, candidate.path) }
|
|
115
|
+
: {},
|
|
116
|
+
}));
|
|
117
|
+
const sessionRows = sessions.map(candidate => ({
|
|
118
|
+
label: formatSessionReferenceMention(candidate),
|
|
119
|
+
description: `Session · ${candidate.cwd ?? '(no cwd)'}`,
|
|
120
|
+
kind: 'session',
|
|
121
|
+
}));
|
|
122
|
+
const rows = [...fileRows, ...sessionRows];
|
|
123
|
+
if (rows.length === 0 && (fileFailure !== undefined || sessionFailure !== undefined)) {
|
|
124
|
+
throw fileFailure instanceof Error
|
|
125
|
+
? fileFailure
|
|
126
|
+
: sessionFailure instanceof Error
|
|
127
|
+
? sessionFailure
|
|
128
|
+
: new Error(String(fileFailure ?? sessionFailure));
|
|
129
|
+
}
|
|
130
|
+
return rows;
|
|
131
|
+
},
|
|
132
|
+
parse(text) {
|
|
133
|
+
return parseSessionReferenceText(text);
|
|
134
|
+
},
|
|
135
|
+
async prepare(parsed, signal) {
|
|
136
|
+
if (parsed.references.length === 0 || resolver === undefined || agent === undefined) {
|
|
137
|
+
return { text: parsed.text, references: parsed.references };
|
|
138
|
+
}
|
|
139
|
+
const prepared = await resolver.prepare(agent, [{ type: 'text', text: parsed.text }], parsed.references, signal);
|
|
140
|
+
return {
|
|
141
|
+
text: prepared.content.filter(block => block.type === 'text').map(block => block.text).join(''),
|
|
142
|
+
references: parsed.references,
|
|
143
|
+
additionalContext: prepared.additionalContext,
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
sessionMention(candidate) {
|
|
147
|
+
return formatSessionReferenceMention({ sessionId: candidate.sessionId, label: candidate.label });
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-id reasoning-capability inheritance for hand-declared pi-ai routes:
|
|
3
|
+
* the model catalog inherits capabilities by route key, not by model id, so a
|
|
4
|
+
* relay route listing `gpt-5.5` reads nothing from the installed `openai`
|
|
5
|
+
* catalog entry and materializes as `reasoning: false` until its settings
|
|
6
|
+
* entry declares `reasoningEfforts`. This adapter closes that gap without
|
|
7
|
+
* touching upstream: whenever a pi-ai profile's model entry carries no
|
|
8
|
+
* declaration and its live row advertises no efforts, but the same model id
|
|
9
|
+
* is declared (in a sibling settings entry) or advertised (on another route)
|
|
10
|
+
* elsewhere, the declaration is materialized into settings — verbatim from a
|
|
11
|
+
* sibling declaration when one exists, otherwise as an identity level map
|
|
12
|
+
* (`off` maps to null, every other level to its own name), which is the
|
|
13
|
+
* correct wire spelling for OpenAI-compatible relays. Writes ride the same
|
|
14
|
+
* `settings.mutate` path as the provider panel, so the upstream
|
|
15
|
+
* `assertServiceable` gate still rejects anything invalid atomically.
|
|
16
|
+
*
|
|
17
|
+
* @module @deepseek-ai/dsh-tui/model-capabilities
|
|
18
|
+
*/
|
|
19
|
+
import { loadModelDirectory } from './models.mjs';
|
|
20
|
+
/** The settings namespace the llm-pi-ai plugin owns (identity via `settingsNamespace`). */
|
|
21
|
+
const PI_AI_SETTINGS_NS = 'llm-pi-ai';
|
|
22
|
+
/** The effort level that means "send no reasoning parameter"; its wire value is always null. */
|
|
23
|
+
const OFF_LEVEL = 'off';
|
|
24
|
+
/** Single-line a misbehaving error for the notice channel. */
|
|
25
|
+
function singleLine(message) {
|
|
26
|
+
const spaced = [...message].map(ch => { const code = ch.charCodeAt(0); return code < 32 || code === 127 ? ' ' : ch; }).join('');
|
|
27
|
+
return spaced.split(' ').filter(part => part !== '').join(' ');
|
|
28
|
+
}
|
|
29
|
+
/** Human text for a rejection value. */
|
|
30
|
+
function messageOf(error) {
|
|
31
|
+
return error instanceof Error ? error.message : String(error);
|
|
32
|
+
}
|
|
33
|
+
/** Read the value at a path through plain objects; undefined when any segment misses. */
|
|
34
|
+
function getPath(value, path) {
|
|
35
|
+
let current = value;
|
|
36
|
+
for (const segment of path) {
|
|
37
|
+
if (typeof current !== 'object' || current === null)
|
|
38
|
+
return undefined;
|
|
39
|
+
current = current[segment];
|
|
40
|
+
}
|
|
41
|
+
return current;
|
|
42
|
+
}
|
|
43
|
+
/** Whether a raw settings value is a usable `reasoningEfforts` dict (non-empty, non-false). */
|
|
44
|
+
function isDeclaredEfforts(value) {
|
|
45
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The identity level map for an advertised effort list: `off` sends no
|
|
49
|
+
* parameter, every other level keeps its own name — the wire spelling
|
|
50
|
+
* OpenAI-compatible relays accept as-is.
|
|
51
|
+
*/
|
|
52
|
+
function identityEfforts(levels) {
|
|
53
|
+
const dict = {};
|
|
54
|
+
for (const level of levels)
|
|
55
|
+
dict[level] = level === OFF_LEVEL ? null : level;
|
|
56
|
+
return dict;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Plan the reasoning declarations to materialize. A model entry inherits
|
|
60
|
+
* when it declares nothing (`reasoningEfforts` absent — a dict or `false` is
|
|
61
|
+
* an explicit choice and is never touched) and its live row advertises no
|
|
62
|
+
* efforts; the donor is the first sibling settings declaration for the same
|
|
63
|
+
* id, copied verbatim so dialect wire spellings survive, otherwise the first
|
|
64
|
+
* other-route row advertising efforts for that id, mapped by identity.
|
|
65
|
+
* Entries never lose fields and keep their key order; a provider appears in
|
|
66
|
+
* the result only when at least one entry changes.
|
|
67
|
+
* @param input - the live model rows and the raw pi-ai profiles from settings.
|
|
68
|
+
* @returns one plan per provider with at least one inheritance.
|
|
69
|
+
*/
|
|
70
|
+
export function planCapabilitySync(input) {
|
|
71
|
+
// Sibling declarations, first profile order wins: { id -> { dict, provider } }.
|
|
72
|
+
const declared = new Map();
|
|
73
|
+
for (const [provider, profile] of input.profiles) {
|
|
74
|
+
for (const entry of profile.models) {
|
|
75
|
+
const efforts = entry.reasoningEfforts;
|
|
76
|
+
if (!isDeclaredEfforts(efforts))
|
|
77
|
+
continue;
|
|
78
|
+
if (!declared.has(String(entry.id)))
|
|
79
|
+
declared.set(String(entry.id), { dict: efforts, provider });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Advertised donors, first row order wins: { id -> { levels, provider } }.
|
|
83
|
+
// A row whose only level is `off` advertises nothing usable and is skipped.
|
|
84
|
+
const advertised = new Map();
|
|
85
|
+
for (const row of input.rows) {
|
|
86
|
+
const levels = row.reasoning?.efforts.map(effort => effort.id) ?? [];
|
|
87
|
+
if (levels.filter(level => level !== OFF_LEVEL).length === 0)
|
|
88
|
+
continue;
|
|
89
|
+
if (!advertised.has(row.model))
|
|
90
|
+
advertised.set(row.model, { levels, provider: row.provider });
|
|
91
|
+
}
|
|
92
|
+
const plans = [];
|
|
93
|
+
for (const [provider, profile] of input.profiles) {
|
|
94
|
+
const inherited = [];
|
|
95
|
+
const sources = [];
|
|
96
|
+
const models = profile.models.map((entry) => {
|
|
97
|
+
if (entry.reasoningEfforts !== undefined)
|
|
98
|
+
return entry;
|
|
99
|
+
const id = String(entry.id);
|
|
100
|
+
const sibling = declared.get(id);
|
|
101
|
+
if (sibling !== undefined && sibling.provider !== provider) {
|
|
102
|
+
inherited.push(id);
|
|
103
|
+
if (!sources.includes(`${sibling.provider}/${id}`))
|
|
104
|
+
sources.push(`${sibling.provider}/${id}`);
|
|
105
|
+
return { ...entry, reasoningEfforts: sibling.dict };
|
|
106
|
+
}
|
|
107
|
+
const donor = advertised.get(id);
|
|
108
|
+
if (donor !== undefined && donor.provider !== provider) {
|
|
109
|
+
inherited.push(id);
|
|
110
|
+
if (!sources.includes(`${donor.provider}/${id}`))
|
|
111
|
+
sources.push(`${donor.provider}/${id}`);
|
|
112
|
+
return { ...entry, reasoningEfforts: identityEfforts(donor.levels) };
|
|
113
|
+
}
|
|
114
|
+
return entry;
|
|
115
|
+
});
|
|
116
|
+
if (inherited.length > 0) {
|
|
117
|
+
plans.push({
|
|
118
|
+
provider,
|
|
119
|
+
settingsNs: profile.settingsNs,
|
|
120
|
+
settingsPath: profile.settingsPath,
|
|
121
|
+
models,
|
|
122
|
+
sourceFingerprint: JSON.stringify(profile.models),
|
|
123
|
+
sourceRevision: profile.revision,
|
|
124
|
+
inherited,
|
|
125
|
+
sources,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return plans;
|
|
130
|
+
}
|
|
131
|
+
/* ------------------------------------------------------------------ *
|
|
132
|
+
* Application.
|
|
133
|
+
* ------------------------------------------------------------------ */
|
|
134
|
+
/** Provider -> the source snapshot and document revision its last write landed against. */
|
|
135
|
+
const lastApplied = new Map();
|
|
136
|
+
/** Fresh revision of one namespace, or undefined when it does not resolve. */
|
|
137
|
+
function revisionOf(settings, ns) {
|
|
138
|
+
try {
|
|
139
|
+
for (const descriptor of settings.describe({ redactSecrets: true })) {
|
|
140
|
+
if (descriptor.ns === ns)
|
|
141
|
+
return descriptor.revision;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Materialize same-id reasoning declarations once per provider. Reads the
|
|
151
|
+
* configurable directory, the redacted settings document, and the live model
|
|
152
|
+
* rows; plans; then writes each provider's merged models array through
|
|
153
|
+
* `settings.mutate` under a fresh revision (writes bump the section
|
|
154
|
+
* revision, so per-plan revisions are re-read). Every failure converges to a
|
|
155
|
+
* single-line notice — the caller's promise never rejects and the session
|
|
156
|
+
* keeps running on the previous configuration.
|
|
157
|
+
* @param ctx - context carrying the `llm` and `settings` services (optional).
|
|
158
|
+
* @param notify - the app bridge's notice sink, when one is live.
|
|
159
|
+
*/
|
|
160
|
+
export async function syncModelCapabilities(ctx, notify) {
|
|
161
|
+
try {
|
|
162
|
+
const llm = ctx.get('llm');
|
|
163
|
+
if (llm?.listConfigurableProviders === undefined)
|
|
164
|
+
return;
|
|
165
|
+
const settings = ctx.get('settings');
|
|
166
|
+
if (settings === undefined)
|
|
167
|
+
return;
|
|
168
|
+
let entries;
|
|
169
|
+
try {
|
|
170
|
+
entries = llm.listConfigurableProviders();
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const routed = entries.filter(entry => entry.settingsNs === PI_AI_SETTINGS_NS);
|
|
176
|
+
if (routed.length === 0)
|
|
177
|
+
return;
|
|
178
|
+
let descriptors;
|
|
179
|
+
try {
|
|
180
|
+
descriptors = settings.describe({ redactSecrets: true });
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const namespaces = new Map(descriptors.map(descriptor => [descriptor.ns, descriptor]));
|
|
186
|
+
const profiles = new Map();
|
|
187
|
+
for (const entry of routed) {
|
|
188
|
+
const namespace = namespaces.get(entry.settingsNs);
|
|
189
|
+
if (namespace === undefined)
|
|
190
|
+
continue;
|
|
191
|
+
const profile = entry.settingsPath.length === 0
|
|
192
|
+
? namespace.value
|
|
193
|
+
: getPath(namespace.value, entry.settingsPath);
|
|
194
|
+
const models = Array.isArray(profile?.models)
|
|
195
|
+
? profile.models
|
|
196
|
+
: undefined;
|
|
197
|
+
// An absent models list is a dormant or unconfigured route: nothing to
|
|
198
|
+
// inherit into (and the empty allow-list there is deliberate upstream).
|
|
199
|
+
if (models === undefined)
|
|
200
|
+
continue;
|
|
201
|
+
profiles.set(entry.provider, {
|
|
202
|
+
settingsNs: entry.settingsNs,
|
|
203
|
+
settingsPath: entry.settingsPath,
|
|
204
|
+
revision: namespace.revision,
|
|
205
|
+
models,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (profiles.size === 0)
|
|
209
|
+
return;
|
|
210
|
+
const directory = await loadModelDirectory(ctx);
|
|
211
|
+
for (const plan of planCapabilitySync({ rows: directory.rows, profiles })) {
|
|
212
|
+
// Skip only a provable echo of our own write: the same source
|
|
213
|
+
// snapshot at the same document revision we already wrote against
|
|
214
|
+
// (our write re-triggers the document event before the re-read
|
|
215
|
+
// catches up). An external edit that removes a materialized
|
|
216
|
+
// declaration bumps the revision, so identical source content
|
|
217
|
+
// re-plans and re-writes instead of being skipped forever.
|
|
218
|
+
const applied = lastApplied.get(plan.provider);
|
|
219
|
+
if (applied !== undefined && applied.fingerprint === plan.sourceFingerprint && applied.revision === plan.sourceRevision)
|
|
220
|
+
continue;
|
|
221
|
+
// Writes bump the section revision; re-read per plan so the second
|
|
222
|
+
// provider's optimistic revision is not already stale.
|
|
223
|
+
const revision = revisionOf(settings, plan.settingsNs);
|
|
224
|
+
if (revision === undefined)
|
|
225
|
+
continue;
|
|
226
|
+
try {
|
|
227
|
+
await settings.mutate(plan.settingsNs, [{ op: 'set', path: [...plan.settingsPath, 'models'], value: plan.models }], revision);
|
|
228
|
+
lastApplied.set(plan.provider, { fingerprint: plan.sourceFingerprint, revision: plan.sourceRevision });
|
|
229
|
+
const shown = plan.sources.slice(0, 3).join(', ');
|
|
230
|
+
const more = plan.sources.length > 3 ? `, +${plan.sources.length - 3}` : '';
|
|
231
|
+
notify?.(`inherited reasoning levels for ${plan.inherited.length} model${plan.inherited.length === 1 ? '' : 's'} on ${plan.provider} (from ${shown}${more})`, 'info');
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
notify?.(`capability inheritance failed on ${plan.provider}: ${singleLine(messageOf(error))}`, 'warning');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// A background sync must never surface as an unhandled rejection.
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** Test seam: forget the applied-write fingerprints. */
|
|
243
|
+
export function resetCapabilitySyncState() {
|
|
244
|
+
lastApplied.clear();
|
|
245
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model directory for the `/model` panel: the in-process equivalent of the
|
|
3
|
+
* web host's model catalog (`buildModelCatalog`), reading the advisory
|
|
4
|
+
* `ctx.llm` registry directly. Catalog membership is advisory — a route
|
|
5
|
+
* serving a model it stopped advertising stays usable — so selection never
|
|
6
|
+
* fails on catalog absence alone.
|
|
7
|
+
*
|
|
8
|
+
* @module @deepseek-ai/dsh-tui/models
|
|
9
|
+
*/
|
|
10
|
+
import { ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
|
|
11
|
+
/** Map an adapter's reasoning capability onto the panel's plain-id shape. */
|
|
12
|
+
export function mapReasoning(reasoning) {
|
|
13
|
+
return {
|
|
14
|
+
efforts: reasoning.efforts.map(effort => ({
|
|
15
|
+
id: effort.id,
|
|
16
|
+
name: effort.name,
|
|
17
|
+
...effort.description === undefined ? {} : { description: effort.description },
|
|
18
|
+
})),
|
|
19
|
+
...reasoning.defaultEffort === undefined ? {} : { defaultEffort: reasoning.defaultEffort },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the effective model selection for one live session, in the
|
|
24
|
+
* documented precedence: the in-process explicit pick, then the session's
|
|
25
|
+
* last `request/header` config, then the deployment default.
|
|
26
|
+
* @param picked - the explicit selection made in this process, when any.
|
|
27
|
+
* @param logged - the last logged request header config, when any.
|
|
28
|
+
* @param defaults - the deployment default selection.
|
|
29
|
+
* @returns the effective selection, carrying a reasoning effort when one is in force.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveEffectiveSelection(picked, logged, defaults) {
|
|
32
|
+
if (picked !== undefined)
|
|
33
|
+
return picked;
|
|
34
|
+
if (logged !== undefined) {
|
|
35
|
+
return {
|
|
36
|
+
provider: logged.provider,
|
|
37
|
+
model: logged.model,
|
|
38
|
+
...logged.reasoningEffort === undefined
|
|
39
|
+
? {}
|
|
40
|
+
: { reasoningEffort: ReasoningEffortId(logged.reasoningEffort) },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return defaults;
|
|
44
|
+
}
|
|
45
|
+
/** Structural equality of two selections (upstream `sameSelection`). */
|
|
46
|
+
function sameModelSelection(left, right) {
|
|
47
|
+
return left.provider === right.provider
|
|
48
|
+
&& left.model === right.model
|
|
49
|
+
&& left.reasoningEffort === right.reasoningEffort;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The session log's unconsumed explicit model selection (the resume layer of
|
|
53
|
+
* the documented precedence). `model/selection` events are log-only facts the
|
|
54
|
+
* web host's session-controller appends when its user picks a model for the
|
|
55
|
+
* NEXT request; a request header assembling exactly that selection retires
|
|
56
|
+
* it, while a header for different values (another route forced the request)
|
|
57
|
+
* leaves it armed. Seeding the resumed pick from here restores a selection
|
|
58
|
+
* that was made but never sent — the request header alone would answer with
|
|
59
|
+
* the older model.
|
|
60
|
+
* @param events - the full persisted event list of the resumed session.
|
|
61
|
+
* @returns the still-pending selection, when one exists.
|
|
62
|
+
*/
|
|
63
|
+
export function pendingModelSelection(events) {
|
|
64
|
+
let pending;
|
|
65
|
+
for (const event of events) {
|
|
66
|
+
// The event map merge lives in @deepseek-ai/dsh-api-session-controller,
|
|
67
|
+
// which this bundle does not depend on; the string guard keeps the fold
|
|
68
|
+
// decoupled while matching the typed upstream projection exactly.
|
|
69
|
+
if (event.type === 'model/selection') {
|
|
70
|
+
const data = event.data;
|
|
71
|
+
if (typeof data.provider === 'string' && typeof data.model === 'string') {
|
|
72
|
+
const selection = {
|
|
73
|
+
provider: data.provider,
|
|
74
|
+
model: data.model,
|
|
75
|
+
...data.reasoningEffort === undefined ? {} : { reasoningEffort: data.reasoningEffort },
|
|
76
|
+
};
|
|
77
|
+
if (pending === undefined || !sameModelSelection(pending, selection))
|
|
78
|
+
pending = selection;
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (event.type === 'request/header') {
|
|
83
|
+
const config = event.data.header.config;
|
|
84
|
+
if (pending !== undefined && sameModelSelection(pending, {
|
|
85
|
+
provider: config.provider,
|
|
86
|
+
model: config.model,
|
|
87
|
+
...config.reasoningEffort === undefined ? {} : { reasoningEffort: config.reasoningEffort },
|
|
88
|
+
})) {
|
|
89
|
+
pending = undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return pending;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Build the selection one `/model` pick applies, rejecting an effort the
|
|
97
|
+
* row does not advertise. The row is the picker's source of truth, so a
|
|
98
|
+
* stale directory cannot smuggle an unsupported effort into the next step
|
|
99
|
+
* (the request pipeline would reject it before network I/O regardless). The
|
|
100
|
+
* empty string is the picker's "provider default" sentinel — an explicit
|
|
101
|
+
* choice to leave the effort to the model's own default, exactly like an
|
|
102
|
+
* absent effort.
|
|
103
|
+
* @param row - the picked model row.
|
|
104
|
+
* @param effortId - the chosen advertised effort, '' or undefined for the model default.
|
|
105
|
+
* @returns the selection the runner records for the next assembled step.
|
|
106
|
+
*/
|
|
107
|
+
export function buildModelSelection(row, effortId) {
|
|
108
|
+
const selected = effortId === undefined || effortId === '' ? undefined : effortId;
|
|
109
|
+
if (selected !== undefined
|
|
110
|
+
&& (row.reasoning === undefined || !row.reasoning.efforts.some(effort => effort.id === selected))) {
|
|
111
|
+
throw new Error(`model ${row.provider}/${row.model} does not support reasoning effort "${selected}"`);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
provider: row.provider,
|
|
115
|
+
model: row.model,
|
|
116
|
+
...selected === undefined ? {} : { reasoningEffort: ReasoningEffortId(selected) },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** Display label for one applied selection: `provider/model` or `provider/model@effort`. */
|
|
120
|
+
export function modelSelectionLabel(selection) {
|
|
121
|
+
return selection.reasoningEffort === undefined
|
|
122
|
+
? `${selection.provider}/${selection.model}`
|
|
123
|
+
: `${selection.provider}/${selection.model}@${selection.reasoningEffort}`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Apply one model selection onto a resolved request config — the exact
|
|
127
|
+
* semantics of the kernel's `installModelSelection` request listener,
|
|
128
|
+
* extracted so the TUI can mirror it for subagent-origin requests: children
|
|
129
|
+
* spawned by the subagent tool inherit the parent's CREATE-TIME AgentOptions,
|
|
130
|
+
* which a mid-session /model switch never touches, so delegated work would
|
|
131
|
+
* otherwise keep running on the launch-time route. An absent effort strips
|
|
132
|
+
* any inherited effort (restoring the selected model's provider default),
|
|
133
|
+
* matching the kernel listener field-for-field.
|
|
134
|
+
* @param resolved - the config the inner chain produced.
|
|
135
|
+
* @param selection - the selection to enforce.
|
|
136
|
+
* @returns the overridden config.
|
|
137
|
+
*/
|
|
138
|
+
export function applyModelSelectionToConfig(resolved, selection) {
|
|
139
|
+
const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = resolved;
|
|
140
|
+
return {
|
|
141
|
+
...withoutInheritedEffort,
|
|
142
|
+
provider: selection.provider,
|
|
143
|
+
model: selection.model,
|
|
144
|
+
...selection.reasoningEffort === undefined
|
|
145
|
+
? {}
|
|
146
|
+
: { reasoningEffort: selection.reasoningEffort },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Load the selectable model directory from the live `ctx.llm` registry.
|
|
151
|
+
* Providers are listed synchronously; each provider's models are discovered
|
|
152
|
+
* with a bounded parallel fan-out whose failures degrade to that provider
|
|
153
|
+
* contributing no rows (mirrors the web catalog's per-provider failures).
|
|
154
|
+
* Each row's reasoning levels are resolved per exact model like the web
|
|
155
|
+
* catalog (`buildModelCatalog`); a single model's capability lookup failure
|
|
156
|
+
* degrades to that row having no effort picker rather than hiding the model,
|
|
157
|
+
* and the failure rides `reasoningFailures` so the caller can tell "no
|
|
158
|
+
* advertised reasoning" from "capability lookup failed".
|
|
159
|
+
* @param ctx - context carrying the `llm` service.
|
|
160
|
+
* @returns the resolved directory; empty rows when `llm` is unavailable.
|
|
161
|
+
*/
|
|
162
|
+
export async function loadModelDirectory(ctx) {
|
|
163
|
+
const llm = ctx.get('llm');
|
|
164
|
+
if (llm === undefined)
|
|
165
|
+
return { rows: [], failures: [], reasoningFailures: [] };
|
|
166
|
+
// Call resolveModelInfo AS A METHOD (llm.resolveModelInfo(...)): destructured
|
|
167
|
+
// off the service it loses `this` — this.registration() then throws on the
|
|
168
|
+
// first provider, the catch swallows it, and every row lands in
|
|
169
|
+
// reasoningFailures ("capability lookup failed") even though the adapter
|
|
170
|
+
// itself never fails.
|
|
171
|
+
const llmResolve = llm;
|
|
172
|
+
const providers = llm.listProviders();
|
|
173
|
+
const listed = await Promise.all(providers.map(async (provider) => {
|
|
174
|
+
try {
|
|
175
|
+
const models = await llm.listModels(provider.id);
|
|
176
|
+
const reasoningFailures = [];
|
|
177
|
+
const rows = await Promise.all(models.map(async (model) => {
|
|
178
|
+
const row = {
|
|
179
|
+
provider: provider.id,
|
|
180
|
+
providerName: provider.name,
|
|
181
|
+
model: model.id,
|
|
182
|
+
modelName: model.name,
|
|
183
|
+
...model.inputModalities === undefined ? {} : { inputModalities: [...model.inputModalities] },
|
|
184
|
+
};
|
|
185
|
+
if (llmResolve.resolveModelInfo === undefined)
|
|
186
|
+
return row;
|
|
187
|
+
try {
|
|
188
|
+
const resolved = await llmResolve.resolveModelInfo(provider.id, model.id);
|
|
189
|
+
return {
|
|
190
|
+
...row,
|
|
191
|
+
...resolved.inputModalities === undefined ? {} : { inputModalities: [...resolved.inputModalities] },
|
|
192
|
+
...resolved.reasoning === undefined ? {} : { reasoning: mapReasoning(resolved.reasoning) },
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
reasoningFailures.push(`${provider.id}/${model.id}`);
|
|
197
|
+
return row;
|
|
198
|
+
}
|
|
199
|
+
}));
|
|
200
|
+
return {
|
|
201
|
+
provider: provider.id,
|
|
202
|
+
providerName: provider.name,
|
|
203
|
+
models: rows,
|
|
204
|
+
reasoningFailures,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return { provider: provider.id, providerName: provider.name, models: [], failed: true };
|
|
209
|
+
}
|
|
210
|
+
}));
|
|
211
|
+
const rows = listed.flatMap(entry => entry.models);
|
|
212
|
+
const failures = listed.filter(entry => 'failed' in entry && entry.failed === true).map(entry => entry.provider);
|
|
213
|
+
const reasoningFailures = listed.flatMap(entry => 'failed' in entry ? [] : entry.reasoningFailures);
|
|
214
|
+
return { rows, failures, reasoningFailures };
|
|
215
|
+
}
|
|
@@ -10,12 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @module @deepseek-ai/dsh-tui/panel-accent
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
readonly border: RgbTriple;
|
|
17
|
-
readonly title: RgbTriple;
|
|
18
|
-
}
|
|
13
|
+
import { surfaceAccent } from './theme.mjs';
|
|
14
|
+
/** First-seen slot per panel id; stable for the process lifetime. */
|
|
15
|
+
const panelSlots = new Map();
|
|
19
16
|
/**
|
|
20
17
|
* The accent pair for one panel: in prismatic both entries are the panel's
|
|
21
18
|
* ring color (border and title match, per the design); in every other theme
|
|
@@ -25,4 +22,11 @@ export interface PanelAccent {
|
|
|
25
22
|
* @param title - the title color used outside prismatic (defaults to border).
|
|
26
23
|
* @returns the resolved accent pair for this panel.
|
|
27
24
|
*/
|
|
28
|
-
export
|
|
25
|
+
export function panelAccent(id, border, title = border) {
|
|
26
|
+
let slot = panelSlots.get(id);
|
|
27
|
+
if (slot === undefined) {
|
|
28
|
+
slot = panelSlots.size;
|
|
29
|
+
panelSlots.set(id, slot);
|
|
30
|
+
}
|
|
31
|
+
return { border: surfaceAccent(slot, border), title: surfaceAccent(slot, title) };
|
|
32
|
+
}
|