@toddzheng024/dscode-bundle 0.7.12 → 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.
Files changed (152) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -3
  2. package/cordis.patch.yml +1 -1
  3. package/package.json +5 -4
  4. package/plugins/session-metrics/view.mjs +10 -2
  5. package/vendor/tui/lib/app.mjs +6674 -0
  6. package/vendor/tui/lib/approval.mjs +122 -0
  7. package/vendor/tui/lib/attachments.mjs +218 -0
  8. package/vendor/tui/lib/authorization-panel.mjs +242 -0
  9. package/vendor/tui/lib/authorization.mjs +111 -0
  10. package/vendor/tui/lib/commands.mjs +123 -0
  11. package/vendor/tui/lib/dscode/chat.mjs +64 -0
  12. package/vendor/tui/lib/dscode/flags.mjs +32 -0
  13. package/vendor/tui/lib/dscode/model-search.mjs +68 -0
  14. package/vendor/tui/lib/dscode/paste.mjs +87 -0
  15. package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
  16. package/vendor/tui/lib/dscode/welcome.mjs +100 -0
  17. package/vendor/tui/lib/editor-keys.mjs +346 -0
  18. package/vendor/tui/lib/editor.mjs +50 -0
  19. package/vendor/tui/lib/fork.mjs +24 -0
  20. package/vendor/tui/lib/git-workflow.mjs +278 -0
  21. package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
  22. package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
  23. package/vendor/tui/lib/index.mjs +2198 -0
  24. package/vendor/tui/lib/input-split.mjs +170 -0
  25. package/vendor/tui/lib/internals.mjs +90 -0
  26. package/vendor/tui/lib/invariant.mjs +22 -0
  27. package/vendor/tui/lib/kernel-panels.mjs +1265 -0
  28. package/vendor/tui/lib/keyboard.mjs +283 -0
  29. package/vendor/tui/lib/language-panel.mjs +41 -0
  30. package/vendor/tui/lib/locales/en.mjs +495 -0
  31. package/vendor/tui/lib/locales/zh.mjs +495 -0
  32. package/vendor/tui/lib/mentions.mjs +150 -0
  33. package/vendor/tui/lib/model-capabilities.mjs +245 -0
  34. package/vendor/tui/lib/models.mjs +215 -0
  35. package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
  36. package/vendor/tui/lib/permissions.mjs +45 -0
  37. package/vendor/tui/lib/plugin-inventory.mjs +27 -0
  38. package/vendor/tui/lib/presets.mjs +46 -0
  39. package/vendor/tui/lib/provider-settings.mjs +600 -0
  40. package/vendor/tui/lib/questions.mjs +116 -0
  41. package/vendor/tui/lib/rainbow.mjs +181 -0
  42. package/vendor/tui/lib/render/animations.mjs +665 -0
  43. package/vendor/tui/lib/render/editor.mjs +437 -0
  44. package/vendor/tui/lib/render/export.mjs +119 -0
  45. package/vendor/tui/lib/render/fuzzy.mjs +79 -0
  46. package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
  47. package/vendor/tui/lib/render/inspector.mjs +93 -0
  48. package/vendor/tui/lib/render/lines.mjs +538 -0
  49. package/vendor/tui/lib/render/markdown.mjs +592 -0
  50. package/vendor/tui/lib/render/projection.mjs +1862 -0
  51. package/vendor/tui/lib/render/status.mjs +591 -0
  52. package/vendor/tui/lib/render/text.mjs +157 -0
  53. package/vendor/tui/lib/render/tool-detail.mjs +187 -0
  54. package/vendor/tui/lib/render/tool-preview.mjs +79 -0
  55. package/vendor/tui/lib/render/usage.mjs +336 -0
  56. package/vendor/tui/lib/render/width.mjs +191 -0
  57. package/vendor/tui/lib/session-directory.mjs +329 -0
  58. package/vendor/tui/lib/session-query.mjs +184 -0
  59. package/vendor/tui/lib/session-switch.mjs +51 -0
  60. package/vendor/tui/lib/settings-file.mjs +75 -0
  61. package/vendor/tui/lib/skills.mjs +119 -0
  62. package/vendor/tui/lib/startup.mjs +111 -0
  63. package/vendor/tui/lib/store.mjs +129 -0
  64. package/vendor/tui/lib/subagents.mjs +200 -0
  65. package/vendor/tui/lib/terminal-title.mjs +186 -0
  66. package/vendor/tui/lib/theme-panel.mjs +60 -0
  67. package/vendor/tui/lib/theme.mjs +379 -0
  68. package/vendor/tui/lib/update-panel.mjs +239 -0
  69. package/vendor/tui/lib/update.mjs +101 -0
  70. package/vendor/tui/lib/version.mjs +105 -0
  71. package/vendor/tui/lib/whale-glyph.mjs +20 -0
  72. package/vendor/tui/LICENSE +0 -21
  73. package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
  74. package/vendor/tui/dscode-email/cli.mjs +0 -32
  75. package/vendor/tui/dscode-email/contacts.mjs +0 -36
  76. package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
  77. package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
  78. package/vendor/tui/dscode-email/gmail.mjs +0 -194
  79. package/vendor/tui/dscode-email/imap.mjs +0 -136
  80. package/vendor/tui/dscode-email/inbox.d.mts +0 -25
  81. package/vendor/tui/dscode-email/inbox.mjs +0 -76
  82. package/vendor/tui/dscode-email/smtp.mjs +0 -69
  83. package/vendor/tui/dscode-email/store.mjs +0 -32
  84. package/vendor/tui/dscode-providers/catalog.mjs +0 -136
  85. package/vendor/tui/dscode-providers/effort.mjs +0 -35
  86. package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
  87. package/vendor/tui/index.mjs +0 -44441
  88. package/vendor/tui/invariant.mjs +0 -21
  89. package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
  90. package/vendor/tui/session-query.mjs +0 -150
  91. package/vendor/tui/startup.mjs +0 -109
  92. package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
  93. package/vendor/tui/types/app.d.ts +0 -348
  94. package/vendor/tui/types/approval.d.ts +0 -59
  95. package/vendor/tui/types/attachments.d.ts +0 -52
  96. package/vendor/tui/types/authorization-panel.d.ts +0 -22
  97. package/vendor/tui/types/authorization.d.ts +0 -36
  98. package/vendor/tui/types/commands.d.ts +0 -52
  99. package/vendor/tui/types/editor-keys.d.ts +0 -105
  100. package/vendor/tui/types/editor.d.ts +0 -6
  101. package/vendor/tui/types/fork.d.ts +0 -8
  102. package/vendor/tui/types/git-workflow.d.ts +0 -121
  103. package/vendor/tui/types/index.d.ts +0 -223
  104. package/vendor/tui/types/input-split.d.ts +0 -54
  105. package/vendor/tui/types/internals.d.ts +0 -26
  106. package/vendor/tui/types/invariant.d.ts +0 -15
  107. package/vendor/tui/types/kernel-panels.d.ts +0 -245
  108. package/vendor/tui/types/keyboard.d.ts +0 -80
  109. package/vendor/tui/types/language-panel.d.ts +0 -12
  110. package/vendor/tui/types/locales/en.d.ts +0 -450
  111. package/vendor/tui/types/locales/zh.d.ts +0 -9
  112. package/vendor/tui/types/mentions.d.ts +0 -85
  113. package/vendor/tui/types/model-capabilities.d.ts +0 -82
  114. package/vendor/tui/types/models.d.ts +0 -133
  115. package/vendor/tui/types/permissions.d.ts +0 -27
  116. package/vendor/tui/types/plugin-inventory.d.ts +0 -11
  117. package/vendor/tui/types/presets.d.ts +0 -22
  118. package/vendor/tui/types/provider-settings.d.ts +0 -239
  119. package/vendor/tui/types/questions.d.ts +0 -54
  120. package/vendor/tui/types/rainbow.d.ts +0 -69
  121. package/vendor/tui/types/render/animations.d.ts +0 -307
  122. package/vendor/tui/types/render/editor.d.ts +0 -163
  123. package/vendor/tui/types/render/export.d.ts +0 -9
  124. package/vendor/tui/types/render/fuzzy.d.ts +0 -21
  125. package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
  126. package/vendor/tui/types/render/inspector.d.ts +0 -62
  127. package/vendor/tui/types/render/lines.d.ts +0 -86
  128. package/vendor/tui/types/render/markdown.d.ts +0 -29
  129. package/vendor/tui/types/render/projection.d.ts +0 -587
  130. package/vendor/tui/types/render/status.d.ts +0 -196
  131. package/vendor/tui/types/render/text.d.ts +0 -64
  132. package/vendor/tui/types/render/tool-detail.d.ts +0 -94
  133. package/vendor/tui/types/render/tool-preview.d.ts +0 -28
  134. package/vendor/tui/types/render/usage.d.ts +0 -113
  135. package/vendor/tui/types/render/width.d.ts +0 -29
  136. package/vendor/tui/types/session-directory.d.ts +0 -190
  137. package/vendor/tui/types/session-query.d.ts +0 -92
  138. package/vendor/tui/types/session-switch.d.ts +0 -17
  139. package/vendor/tui/types/settings-file.d.ts +0 -41
  140. package/vendor/tui/types/skills.d.ts +0 -47
  141. package/vendor/tui/types/startup.d.ts +0 -65
  142. package/vendor/tui/types/store.d.ts +0 -58
  143. package/vendor/tui/types/subagents.d.ts +0 -70
  144. package/vendor/tui/types/terminal-title.d.ts +0 -66
  145. package/vendor/tui/types/theme-panel.d.ts +0 -24
  146. package/vendor/tui/types/theme.d.ts +0 -434
  147. package/vendor/tui/types/update-panel.d.ts +0 -49
  148. package/vendor/tui/types/update.d.ts +0 -75
  149. package/vendor/tui/types/version.d.ts +0 -19
  150. package/vendor/tui/types/whale-glyph.d.ts +0 -6
  151. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
  152. /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 { type RgbTriple } from './theme.ts';
14
- /** One panel's resolved accent pair (border and title share the ring color in prismatic). */
15
- export interface PanelAccent {
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 declare function panelAccent(id: string, border: RgbTriple, title?: RgbTriple): PanelAccent;
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
+ }