dsh-codinput 0.3.2 → 0.3.6

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.
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * 槽位条目渲染的共享件:检视任意槽位的条目快照(检测即兼容)、按条目 locale
3
- * 绑定官方词典、以及条目崩溃兜底边界。语音输入(VoiceActivity)与模型名
4
- * 标签(ModelLabel)共用同一套合成公式,抽到这里避免两份漂移。
3
+ * 绑定官方词典、快照 store 的选择器钩子合成、条目崩溃兜底边界,以及宿主 kit
4
+ * 减去 owner props 的 standard 面(座位镜像 InputSlotSeat 与语音/模型标签
5
+ * 共用同一套合成公式,抽到这里避免多份漂移)。
5
6
  */
6
7
 
7
- import { useCallback, useSyncExternalStore, Component } from 'react';
8
- import type { SlotEntryLike } from '../host-types';
8
+ import { createElement, useCallback, useSyncExternalStore, Component } from 'react';
9
+ import type { HostObservable, SlotEntryLike } from '../host-types';
9
10
  import { bindForeign } from '../i18n';
10
11
  import { diagError } from '../diag';
11
12
  import { stash } from '../services';
@@ -41,15 +42,225 @@ export function useSlotEntries(key: string): readonly unknown[] {
41
42
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
42
43
  }
43
44
 
45
+ /**
46
+ * React 可作为元素类型使用的「组件包装」符号(memo / forwardRef / lazy)。
47
+ * 只认这三类,不做「任意 symbol `$$typeof`」放行——element / context / 伪造
48
+ * 对象都不是可渲染组件类型:放行后 React 会在为子元素创建 Fiber 时抛 #130,
49
+ * 而该错误发生在 EntryBoundary 自身的工作单元内(边界只捕获后代错误),会
50
+ * 冒泡到宿主的 SlotErrorBoundary 并废黜 conversation.composer.bar(接管整体
51
+ * 退回官方输入框)。收紧后这类坏条目回到静默跳过,与 v0.3.5 行为一致。
52
+ */
53
+ const RENDERABLE_OBJECT_TYPES: readonly symbol[] = [
54
+ Symbol.for('react.memo'),
55
+ Symbol.for('react.forward_ref'),
56
+ Symbol.for('react.lazy'),
57
+ ];
58
+
59
+ /** 组件位是否为 React 可渲染类型(函数组件 / memo / forwardRef / lazy 包装)。 */
60
+ export function isRenderableComponent(value: unknown): boolean {
61
+ if (typeof value === 'function') return true;
62
+ if (value === null || typeof value !== 'object') return false;
63
+ const type = (value as { $$typeof?: unknown }).$$typeof;
64
+ return typeof type === 'symbol' && RENDERABLE_OBJECT_TYPES.includes(type);
65
+ }
66
+
44
67
  /** single 槽位赢家 = entries 首个带组件的条目(register 即按优先级排序)。 */
45
68
  export function firstComponentEntry(entries: readonly unknown[]): SlotEntryLike | null {
46
69
  for (const candidate of entries) {
47
70
  const record = candidate as SlotEntryLike | null | undefined;
48
- if (record && typeof record === 'object' && typeof record.component === 'function') return record;
71
+ if (record && typeof record === 'object' && isRenderableComponent(record.component)) return record;
49
72
  }
50
73
  return null;
51
74
  }
52
75
 
76
+ /**
77
+ * 列表槽位行合成:复刻官方 renderer 的 list 分支——
78
+ * entries 已由 SlotCore 按(priority↑, order↑)排序,同 id 首个占位(去重),
79
+ * 剩余行再按 order 升序稳定排序(官方 `[...rows].sort(a.order - b.order)`)。
80
+ * 无组件的条目跳过(官方 deadCell 是废黜兜底,插件侧无法复现其内部状态)。
81
+ */
82
+ export function listRows(entries: readonly unknown[]): SlotEntryLike[] {
83
+ const byId = new Map<unknown, SlotEntryLike>();
84
+ for (const candidate of entries) {
85
+ const record = candidate as (SlotEntryLike & { options?: { id?: unknown } }) | null | undefined;
86
+ if (!record || typeof record !== 'object' || !isRenderableComponent(record.component)) continue;
87
+ const id = (record as { options?: { id?: unknown } }).options?.id;
88
+ if (byId.has(id)) continue;
89
+ byId.set(id, record);
90
+ }
91
+ const rows = [...byId.values()];
92
+ rows.sort((a, b) => orderOf(a) - orderOf(b));
93
+ return rows;
94
+ }
95
+
96
+ function orderOf(entry: SlotEntryLike): number {
97
+ const options = (entry as { options?: { order?: number } }).options;
98
+ return options?.order ?? 0;
99
+ }
100
+
101
+ /** 条目 options.id(React key 用;官方 renderer 同轴)。 */
102
+ export function entryId(entry: SlotEntryLike, index: number): string {
103
+ const options = (entry as { options?: { id?: string } }).options;
104
+ return options?.id ?? `#${index}`;
105
+ }
106
+
107
+ /**
108
+ * 座位「第三方遮蔽者」判据(单值座让位用)。
109
+ *
110
+ * 宿主的动态包守卫(dsh-cordis-client-runner 的 `guardedSlots`)对非 chain
111
+ * 槽位的 register 做两件事:① **强制**用页内遮蔽秩覆盖插件自报的 priority
112
+ * (`--nextPriority`,负数且唯一——插件写 priority:-1 也会被覆盖成分配值);
113
+ * ② fiber 名取 `dyn/<pluginId>`,而条目 `registrant` = `options.registrant ??
114
+ * fiber.name`。出厂自带占用者(如 client-ui-model-selection 的 ModelSelect)
115
+ * 不经过该守卫:不带 priority(≡0),registrant 也没有 `dyn/` 前缀。
116
+ *
117
+ * 据此把「第三方遮蔽者」与「出厂占用者」分开:前者应当让位(镜像它),
118
+ * 后者继续由 Codinput 自绘控件代表(镜像出厂条目会与自绘控件重复)。
119
+ * 负 priority 是主判据(分配秩即遮蔽意图);`dyn/` 前缀是兜底(防旧宿主/
120
+ * 未来守卫变化下 priority 未落进条目 options 的形态)。
121
+ */
122
+ export function isShadowingOccupant(entry: SlotEntryLike): boolean {
123
+ const options = (entry as { options?: { priority?: number; registrant?: string } }).options;
124
+ const priority = options?.priority;
125
+ if (typeof priority === 'number' && priority < 0) return true;
126
+ const registrant = entry.registrant ?? options?.registrant;
127
+ return typeof registrant === 'string' && registrant.startsWith('dyn/');
128
+ }
129
+
130
+ /**
131
+ * 单条目渲染(座位镜像与单值座共用):官方 SessionEntry → renderEntry 的
132
+ * props 展开式 `{...standard(kit), sessionId, ...entryInjectProps, ...t,
133
+ * ...ownerProps}`——owner props 最后展开(如模型座的 `locked` 不被 kit 覆盖)。
134
+ * 逐条目 EntryBoundary 隔离:单个条目崩溃只丢本座位。
135
+ */
136
+ export function renderSlotEntry(
137
+ entry: SlotEntryLike,
138
+ key: string,
139
+ options: {
140
+ sessionId: string;
141
+ standard: Record<string, unknown> | undefined;
142
+ /** 官方 renderSlot 第二参数的 owner props(如模型座 { locked })。 */
143
+ ownerProps?: Record<string, unknown> | undefined;
144
+ /** 条目崩溃时的兜底渲染(单值座让位用:回退自绘控件;缺省 null)。 */
145
+ fallback?: React.ReactNode;
146
+ },
147
+ ): JSX.Element {
148
+ const { sessionId, standard, ownerProps, fallback } = options;
149
+ const Comp = entry.component as React.ComponentType<Record<string, unknown>>;
150
+ // 条目词典的 t(官方 localeSeat 同款);无 locale 或绑定失败 → 不下发,
151
+ // 组件按各自回退(如 enhancer 回退内置中文词典)。
152
+ let t: unknown;
153
+ try {
154
+ t = translatorFor(entry) ?? undefined;
155
+ } catch {
156
+ t = undefined;
157
+ }
158
+ let injected: Record<string, unknown> | null = null;
159
+ try {
160
+ injected = entryInjectProps(entry, sessionId);
161
+ } catch (error) {
162
+ diagError('slot.seat.inject', error);
163
+ }
164
+ return createElement(
165
+ EntryBoundary,
166
+ { key, fallback },
167
+ createElement(Comp, {
168
+ ...(standard ?? {}),
169
+ sessionId,
170
+ ...(injected ?? {}),
171
+ ...(t !== undefined ? { t } : {}),
172
+ ...(ownerProps ?? {}),
173
+ }),
174
+ );
175
+ }
176
+
177
+ export type SnapshotSelectorHook = <S>(sel: (s: any) => S, eq?: (a: S, b: S) => boolean) => S;
178
+
179
+ /** 快照 store → use<Name> 选择器钩子(官方 observableHook 同款,按源缓存)。 */
180
+ const snapshotHookCache = new WeakMap<object, SnapshotSelectorHook>();
181
+ export function snapshotHookFor(source: unknown): SnapshotSelectorHook | null {
182
+ const face = source as HostObservable<unknown> | undefined;
183
+ if (!face || typeof face.subscribe !== 'function' || typeof face.getSnapshot !== 'function') return null;
184
+ let hook = snapshotHookCache.get(face);
185
+ if (!hook) {
186
+ hook = <S,>(sel: (s: unknown) => S): S =>
187
+ useSyncExternalStore(
188
+ (fn: () => void) => face.subscribe(fn) ?? (() => {}),
189
+ () => sel(face.getSnapshot()),
190
+ () => sel(face.getSnapshot()),
191
+ );
192
+ snapshotHookCache.set(face, hook);
193
+ }
194
+ return hook;
195
+ }
196
+
197
+ /** 官方 standardHookPropName:name → use<Name>(首字母大写)。 */
198
+ export function standardHookPropName(name: string): string {
199
+ if (!name) return name;
200
+ return `use${name.charAt(0).toUpperCase()}${name.slice(1)}`;
201
+ }
202
+
203
+ /**
204
+ * 条目自带 inject 的 props 面(官方 runInject + bindInjectSources 的插件侧
205
+ * 复刻):以 sessionId 调 inject,把 hooks 子表折成 use<Name> 钩子、其余
206
+ * 原样下传;keyedHooks 需要官方 keyed resolver 面,插件侧不可复现 → 跳过
207
+ * (现网条目无键控钩子)。结果按(条目 × 会话)缓存——官方同款缓存轴。
208
+ *
209
+ * 为什么额外记「失败」:官方 renderer 是在**宿主自己的 fiber 上下文里**调
210
+ * 这个 inject 闭包;插件侧从自己的 fiber 调会丢掉条目所属插件的 ctx 绑定,
211
+ * 于是依赖宿主服务面的 inject 会抛 `cannot get property "remote.session"
212
+ * without inject`(实测 dsh-model-accordion 0.2.1 的 `directoryFor`)。这是
213
+ * 插件侧不可复现的边界——单值座镜像据此让位降级(回退自绘控件),而不是把
214
+ * 空 props 下发给一个必然崩溃的组件。
215
+ */
216
+ interface InjectBound {
217
+ props: Record<string, unknown> | null;
218
+ failed: boolean;
219
+ }
220
+ const entryInjectCache = new WeakMap<object, Map<string, InjectBound>>();
221
+
222
+ function boundInject(entry: SlotEntryLike, sessionId: string): InjectBound {
223
+ let perSession = entryInjectCache.get(entry);
224
+ if (!perSession) {
225
+ perSession = new Map();
226
+ entryInjectCache.set(entry, perSession);
227
+ }
228
+ const cached = perSession.get(sessionId);
229
+ if (cached) return cached;
230
+ let bound: InjectBound = { props: null, failed: false };
231
+ try {
232
+ const face = entry.inject?.(sessionId);
233
+ if (face && typeof face === 'object') {
234
+ const { hooks: _hooks, keyedHooks: _keyed, ...rest } = face as Record<string, unknown> & {
235
+ hooks?: Record<string, unknown>;
236
+ keyedHooks?: Record<string, unknown>;
237
+ };
238
+ void _keyed;
239
+ for (const [name, source] of Object.entries(_hooks ?? {})) {
240
+ const hook = snapshotHookFor(source);
241
+ if (hook) rest[standardHookPropName(name)] = hook;
242
+ }
243
+ bound = { props: rest, failed: false };
244
+ }
245
+ } catch (error) {
246
+ diagError('slot.inject', error);
247
+ bound = { props: null, failed: true };
248
+ }
249
+ perSession.set(sessionId, bound);
250
+ return bound;
251
+ }
252
+
253
+ export function entryInjectProps(entry: SlotEntryLike, sessionId: string): Record<string, unknown> | null {
254
+ if (typeof entry.inject !== 'function') return null;
255
+ return boundInject(entry, sessionId).props;
256
+ }
257
+
258
+ /** 该条目 inject 是否抛错(宿主 ctx 绑定面缺失的判据,见上)。 */
259
+ export function entryInjectFailed(entry: SlotEntryLike, sessionId: string): boolean {
260
+ if (typeof entry.inject !== 'function') return false;
261
+ return boundInject(entry, sessionId).failed;
262
+ }
263
+
53
264
  /** 官方 renderer 的 locale 座位:locale.bind(entry.locale),按 ns 缓存。 */
54
265
  export function translatorFor(entry: SlotEntryLike, fallbackNs?: string): Translator | null {
55
266
  const ns = entry.locale ?? fallbackNs;
@@ -73,3 +284,36 @@ export class EntryBoundary extends Component<
73
284
  return this.state.failed ? this.props.fallback ?? null : this.props.children;
74
285
  }
75
286
  }
287
+
288
+ /**
289
+ * 宿主 kit 减去 owner props 的原样面:供槽位条目按官方契约消费
290
+ * (sessionId + 选择器 hook + inputActions 等)。bar 条目的 kit 里 owner
291
+ * 专属字段(variant/blocked/disabled/placeholder/…)不该下发给子槽位条目;
292
+ * 侧栏 body 再额外剔除注入的 useTabInfo。
293
+ */
294
+ export function kitFromBarProps(
295
+ props: Record<string, unknown>,
296
+ extraOmit: readonly string[] = [],
297
+ ): Record<string, unknown> {
298
+ const {
299
+ variant: _variant,
300
+ blocked: _blocked,
301
+ disabled: _disabled,
302
+ placeholder: _placeholder,
303
+ workspacePickerOpen: _workspacePickerOpen,
304
+ onRequestWorkspace: _onRequestWorkspace,
305
+ accessory: _accessory,
306
+ hero: _hero,
307
+ ...kit
308
+ } = props;
309
+ void _variant;
310
+ void _blocked;
311
+ void _disabled;
312
+ void _placeholder;
313
+ void _workspacePickerOpen;
314
+ void _onRequestWorkspace;
315
+ void _accessory;
316
+ void _hero;
317
+ for (const key of extraOmit) delete kit[key];
318
+ return kit;
319
+ }
@@ -61,7 +61,15 @@ export interface SlotEntryLike {
61
61
  readonly inject?: (...args: unknown[]) => Record<string, unknown>;
62
62
  /** 条目词典命名空间(locale.bind(ns) 即官方传给组件的 t)。 */
63
63
  readonly locale?: string;
64
+ /** 归属(options.registrant ?? fiber.name;动态包为 dyn/<pluginId>)。 */
64
65
  readonly registrant?: string;
66
+ /** 注册选项切面:priority 负数 = 动态包被守卫分配的页内遮蔽秩(见 slotEntry)。 */
67
+ readonly options?: {
68
+ readonly id?: string;
69
+ readonly order?: number;
70
+ readonly priority?: number;
71
+ readonly registrant?: string;
72
+ };
65
73
  }
66
74
 
67
75
  // ---------- input machine ----------
@@ -21,6 +21,7 @@ import { claimSidebarInput, subscribeSidebarTabClose, useSidebarCarryOwner, type
21
21
  import { loadPrefs, savePrefs, subscribePrefs } from './prefs';
22
22
  import { diagError, diagWindow } from './diag';
23
23
  import { CodinputSurface } from './components/CodinputSurface';
24
+ import { kitFromBarProps } from './components/slotEntry';
24
25
  import { IconBrandLogo } from './icons';
25
26
  import { t, useT } from './i18n';
26
27
 
@@ -98,6 +99,9 @@ export function SidebarCodinputTitle(props: { useTabInfo?: () => SidebarTabInfo
98
99
  export function SidebarCodinputBody(props: SessionStandard & { useTabInfo?: () => SidebarTabInfo }): JSX.Element {
99
100
  const { sessionId, useInput, inputActions, useProjection, useSession, useTabInfo } = props;
100
101
  const state = useInput((s) => s);
102
+ // conversation.input.left/.right 第三方条目的 standard props 原样面(本条目
103
+ // kit 减去注入的 useTabInfo;见 components/InputSlotSeat)。
104
+ const inputStandard = kitFromBarProps(props as unknown as Record<string, unknown>, ['useTabInfo']);
101
105
  // 会话运行中:侧栏形态的主按钮同样变停止(官方同款)。
102
106
  const running = useSession ? useSession((s: unknown) => (s as { running?: boolean } | undefined)?.running) === true : false;
103
107
  useT();
@@ -198,6 +202,7 @@ export function SidebarCodinputBody(props: SessionStandard & { useTabInfo?: () =
198
202
  state={state}
199
203
  actions={inputActions}
200
204
  useProjection={useProjection}
205
+ inputStandard={inputStandard}
201
206
  running={running}
202
207
  side={true}
203
208
  />
@@ -3,6 +3,9 @@
3
3
  * 工具行/菜单/发送按钮的属性值逐条复刻官方 InputBar.module.css 与
4
4
  * PermissionSelect/ModelSelection 的触发器与菜单(含 --dsw-* 主题变量,
5
5
  * 随宿主主题自动切换);编辑器/设置等自有部分用 currentColor 适配。
6
+ * 主题/皮肤插件兼容约定:官方走 --dsw-* token 的取值这里也走 token(皮肤
7
+ * 改 token 即全盘跟随);官方写死的(发送钮前景 #fff、小圈 tools 紫)同样
8
+ * 写死保持逐值对齐——多跟一分就会在换肤下与官方脱队。
6
9
  */
7
10
 
8
11
  const STYLE_ID = 'dsh-codinput-styles';
@@ -77,7 +80,8 @@ const CSS = `
77
80
  .dci-preview blockquote { margin: 0.5em 0; padding: 0.1em 0.9em; border-left: 3px solid color-mix(in srgb, currentColor 22%, transparent); opacity: 0.85; }
78
81
  .dci-preview table { border-collapse: collapse; margin: 0.6em 0; }
79
82
  .dci-preview th, .dci-preview td { border: 1px solid color-mix(in srgb, currentColor 18%, transparent); padding: 4px 9px; }
80
- .dci-preview a { color: #4c8dff; } .dci-preview img { max-width: 100%; } .dci-preview hr { border: none;
83
+ /* 预览链接走官方 markdown 同款 alias-link token(官方 _markdown_ a 逐值核对)。 */
84
+ .dci-preview a { color: var(--dsw-alias-link, #4c8dff); } .dci-preview img { max-width: 100%; } .dci-preview hr { border: none;
81
85
  border-top: 1px solid color-mix(in srgb, currentColor 14%, transparent); }
82
86
 
83
87
  /* ===== 工具行(官方 InputBar .row / .tools / .trailing)===== */
@@ -126,14 +130,19 @@ const CSS = `
126
130
  .dci-send:hover:not(:disabled) { background: var(--dsw-alias-button-info-hover, color-mix(in srgb, currentColor 70%, transparent)); }
127
131
  .dci-send:disabled { opacity: 0.4; cursor: default; }
128
132
 
129
- /* ===== 菜单(官方 primitives Menu:r20 卡片 / item r10 / side-top 弹出)===== */
133
+ /* ===== 菜单(官方 primitives Menu / ModelSelect .menu 逐值复刻:r20 卡片 / item r10 / 上方弹出)
134
+ 卡片 overflow:hidden + max-height:min(360px,100vh-96px)(官方 .menu 同款),
135
+ 列表在 .dci-viewport 内滚;--dsh-scrollbar-thumb(l2) 由卡片注入(官方同款),
136
+ 宿主全局 ::-webkit-scrollbar-thumb{background:var(--dsh-scrollbar-thumb)} 据此出条。 ===== */
130
137
  .dci-menu { position: fixed; z-index: 1100; box-sizing: border-box; padding: 4px; display: flex; flex-direction: column;
131
- min-width: 218px; max-width: min(360px, 100vw - 24px); max-height: calc(100vh - 24px); overflow-y: auto;
138
+ min-width: 218px; max-width: min(360px, 100vw - 24px); max-height: min(360px, calc(100vh - 96px)); overflow: hidden;
139
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2, color-mix(in srgb, currentColor 18%, transparent));
140
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2, color-mix(in srgb, currentColor 26%, transparent));
132
141
  background: var(--dsw-menu-surface-fill, var(--dsw-specific-menu, rgba(28, 28, 32, 0.98)));
133
142
  backdrop-filter: var(--dsw-menu-backdrop-filter, none); color: var(--dsw-alias-label-primary, #ececf1);
134
143
  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1, transparent);
135
144
  box-shadow: var(--dsw-elevation-prominent, 0 10px 34px rgba(0, 0, 0, 0.35)); border: 0; border-radius: var(--dsw-radius-lg, 20px); font-size: 13px; }
136
- .dci-viewport { display: flex; flex-direction: column; min-height: 0; }
145
+ .dci-viewport { display: flex; flex-direction: column; min-height: 0; overflow-y: auto; }
137
146
  .dci-menuLabel { padding: 8px 10px; font-size: 12px; line-height: 16px; color: var(--dsw-alias-label-tertiary, currentColor);
138
147
  user-select: none; text-align: left; }
139
148
  .dci-item { display: flex; align-items: center; gap: 8px; width: 100%; min-height: 40px; padding: 8px 10px; border: none;
@@ -210,7 +219,7 @@ const CSS = `
210
219
  .dci-search { width: calc(100% - 8px); margin: 2px 4px 4px; padding: 5px 9px; font-size: 13px;
211
220
  border-radius: 8px; border: 1px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 18%, transparent));
212
221
  background: transparent; color: inherit; outline: none; }
213
- .dci-error { padding: 6px 10px; color: var(--dsw-alias-state-error-primary, #ff6b6b); font-size: 12px; }
222
+ .dci-error { padding: 6px 10px; color: var(--dsw-alias-state-error-primary, #d54941); font-size: 12px; }
214
223
 
215
224
  /* ===== 模型/推理等级 cell 行(官方 ModelSelection .cell)===== */
216
225
  .dci-cell { box-sizing: border-box; width: auto; min-width: 100%; height: 40px; color: var(--dsw-alias-label-primary, inherit);
@@ -225,7 +234,7 @@ const CSS = `
225
234
  /* ===== 完全权限确认(官方 RiskConfirmation 语义)===== */
226
235
  .dci-confirm-warning { display: flex; align-items: flex-start; gap: 10px; color: var(--dsw-alias-label-secondary, currentColor);
227
236
  font-size: 14px; line-height: 22px; padding: 6px 8px 0; }
228
- .dci-confirm-warning svg { flex: none; margin-top: 2px; color: var(--dsw-alias-state-error-primary, #ff6b6b); }
237
+ .dci-confirm-warning svg { flex: none; margin-top: 2px; color: var(--dsw-alias-state-error-primary, #d54941); }
229
238
  .dci-confirm-warning p { margin: 0; }
230
239
  .dci-confirm-ack { display: flex; align-items: flex-start; gap: 10px; margin: 14px 8px 0; cursor: pointer;
231
240
  color: var(--dsw-alias-label-primary, inherit); font-size: 14px; line-height: 22px; }
@@ -237,7 +246,7 @@ const CSS = `
237
246
  .dci-btn { border: 1px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 22%, transparent)); border-radius: 10px;
238
247
  background: transparent; color: inherit; padding: 6px 14px; cursor: pointer; font-size: 14px; line-height: 20px; }
239
248
  .dci-btn[data-primary="true"] { background: var(--dsw-alias-button-primary-fill, color-mix(in srgb, currentColor 85%, transparent));
240
- color: #fff; border-color: transparent; }
249
+ color: var(--dsw-alias-label-primary-foreground, #fff); border-color: transparent; }
241
250
  .dci-btn:disabled { opacity: 0.4; cursor: default; }
242
251
 
243
252
  /* ===== 草稿附件轨(官方 ComposerAttachments/AttachmentRail/FileCard 逐值复刻)=====
@@ -513,7 +522,7 @@ button.dci-attach-body { color: inherit; font: inherit; text-align: left; backgr
513
522
  .dci-confirm-warning { display: flex; align-items: flex-start; gap: 10px; color: var(--dsw-alias-label-secondary, inherit);
514
523
  font-size: 14px; line-height: 22px; }
515
524
  .dci-confirm-warning p { margin: 0; }
516
- .dci-confirm-warningIcon { flex: none; margin-top: 2px; color: var(--dsw-alias-state-error-primary, #e5484d); }
525
+ .dci-confirm-warningIcon { flex: none; margin-top: 2px; color: var(--dsw-alias-state-error-primary, #d54941); }
517
526
  .dci-confirm-ack { display: flex; align-items: flex-start; gap: 10px; margin-top: 20px; cursor: pointer;
518
527
  color: var(--dsw-alias-label-primary, inherit); font-size: 14px; line-height: 22px; }
519
528
  .dci-confirm-ack input { flex: none; width: 16px; height: 16px; margin: 3px 0 0; cursor: pointer;