dsh-codinput 0.3.5 → 0.3.7

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.
@@ -5,7 +5,7 @@
5
5
  * 共用同一套合成公式,抽到这里避免多份漂移)。
6
6
  */
7
7
 
8
- import { useCallback, useSyncExternalStore, Component } from 'react';
8
+ import { createElement, useCallback, useSyncExternalStore, Component } from 'react';
9
9
  import type { HostObservable, SlotEntryLike } from '../host-types';
10
10
  import { bindForeign } from '../i18n';
11
11
  import { diagError } from '../diag';
@@ -42,11 +42,33 @@ export function useSlotEntries(key: string): readonly unknown[] {
42
42
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
43
43
  }
44
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
+
45
67
  /** single 槽位赢家 = entries 首个带组件的条目(register 即按优先级排序)。 */
46
68
  export function firstComponentEntry(entries: readonly unknown[]): SlotEntryLike | null {
47
69
  for (const candidate of entries) {
48
70
  const record = candidate as SlotEntryLike | null | undefined;
49
- if (record && typeof record === 'object' && typeof record.component === 'function') return record;
71
+ if (record && typeof record === 'object' && isRenderableComponent(record.component)) return record;
50
72
  }
51
73
  return null;
52
74
  }
@@ -61,7 +83,7 @@ export function listRows(entries: readonly unknown[]): SlotEntryLike[] {
61
83
  const byId = new Map<unknown, SlotEntryLike>();
62
84
  for (const candidate of entries) {
63
85
  const record = candidate as (SlotEntryLike & { options?: { id?: unknown } }) | null | undefined;
64
- if (!record || typeof record !== 'object' || typeof record.component !== 'function') continue;
86
+ if (!record || typeof record !== 'object' || !isRenderableComponent(record.component)) continue;
65
87
  const id = (record as { options?: { id?: unknown } }).options?.id;
66
88
  if (byId.has(id)) continue;
67
89
  byId.set(id, record);
@@ -82,6 +104,76 @@ export function entryId(entry: SlotEntryLike, index: number): string {
82
104
  return options?.id ?? `#${index}`;
83
105
  }
84
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
+
85
177
  export type SnapshotSelectorHook = <S>(sel: (s: any) => S, eq?: (a: S, b: S) => boolean) => S;
86
178
 
87
179
  /** 快照 store → use<Name> 选择器钩子(官方 observableHook 同款,按源缓存)。 */
@@ -113,38 +205,60 @@ export function standardHookPropName(name: string): string {
113
205
  * 复刻):以 sessionId 调 inject,把 hooks 子表折成 use<Name> 钩子、其余
114
206
  * 原样下传;keyedHooks 需要官方 keyed resolver 面,插件侧不可复现 → 跳过
115
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 下发给一个必然崩溃的组件。
116
215
  */
117
- const entryInjectCache = new WeakMap<object, Map<string, Record<string, unknown> | null>>();
118
- export function entryInjectProps(entry: SlotEntryLike, sessionId: string): Record<string, unknown> | null {
119
- if (typeof entry.inject !== 'function') return null;
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 {
120
223
  let perSession = entryInjectCache.get(entry);
121
224
  if (!perSession) {
122
225
  perSession = new Map();
123
226
  entryInjectCache.set(entry, perSession);
124
227
  }
125
- if (!perSession.has(sessionId)) {
126
- let bound: Record<string, unknown> | null = null;
127
- try {
128
- const face = entry.inject(sessionId);
129
- if (face && typeof face === 'object') {
130
- const { hooks: _hooks, keyedHooks: _keyed, ...rest } = face as Record<string, unknown> & {
131
- hooks?: Record<string, unknown>;
132
- keyedHooks?: Record<string, unknown>;
133
- };
134
- void _keyed;
135
- for (const [name, source] of Object.entries(_hooks ?? {})) {
136
- const hook = snapshotHookFor(source);
137
- if (hook) rest[standardHookPropName(name)] = hook;
138
- }
139
- bound = rest;
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;
140
242
  }
141
- } catch (error) {
142
- diagError('slot.inject', error);
143
- bound = null;
243
+ bound = { props: rest, failed: false };
144
244
  }
145
- perSession.set(sessionId, bound);
245
+ } catch (error) {
246
+ diagError('slot.inject', error);
247
+ bound = { props: null, failed: true };
146
248
  }
147
- return perSession.get(sessionId) ?? null;
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;
148
262
  }
149
263
 
150
264
  /** 官方 renderer 的 locale 座位:locale.bind(entry.locale),按 ns 缓存。 */
@@ -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 ----------
@@ -92,6 +92,14 @@ const ZH: Dict = {
92
92
  'perm.preset.danger-full-access': '完全权限',
93
93
  'set.enabled': '启用接管',
94
94
  'set.enabledDesc': '关闭即恢复官方输入框,草稿不丢;/codinput 同样可切换',
95
+ 'set.userMarkdown': '用户消息 Markdown 渲染',
96
+ 'set.userMarkdownDesc': '右侧自己发出的消息按 Markdown 排版(标题/列表/代码块等);关闭恢复官方纯文本',
97
+ 'umsg.copy': '复制',
98
+ 'umsg.copied': '已复制',
99
+ 'umsg.referenceSummary': '引用:{labels}',
100
+ 'umsg.clock.md': '{m}月{d}日',
101
+ 'umsg.clock.ymd': '{y}年{m}月{d}日',
102
+ 'umsg.extraBlock': '额外内容块',
95
103
  'set.lineNumbers': '显示行号',
96
104
  'set.lineNumbersDesc': '当前行与行号高亮常开',
97
105
  'set.font': '编辑器字体',
@@ -196,6 +204,14 @@ const EN: Dict = {
196
204
  'perm.preset.danger-full-access': 'Full access',
197
205
  'set.enabled': 'Take over composer',
198
206
  'set.enabledDesc': 'Turning it off restores the official composer; the draft is kept. /codinput toggles it too.',
207
+ 'set.userMarkdown': 'Markdown for user messages',
208
+ 'set.userMarkdownDesc': 'Render your own (right-side) messages as Markdown — headings, lists, code blocks; off restores the official plain text',
209
+ 'umsg.copy': 'Copy',
210
+ 'umsg.copied': 'Copied',
211
+ 'umsg.referenceSummary': 'Referenced: {labels}',
212
+ 'umsg.clock.md': '{m}/{d}',
213
+ 'umsg.clock.ymd': '{y}/{m}/{d}',
214
+ 'umsg.extraBlock': 'Extra content block',
199
215
  'set.lineNumbers': 'Show line numbers',
200
216
  'set.lineNumbersDesc': 'The current line and its gutter number stay highlighted',
201
217
  'set.font': 'Editor font',
@@ -17,6 +17,7 @@ import { diagError, diagWindow, runGuarded } from './diag';
17
17
  import { loadPrefs, subscribePrefs } from './prefs';
18
18
  import { isAnySidebarInputCarried, subscribeSidebarInput } from './sideinput';
19
19
  import { barRegistration, CodinputBar } from './components/CodinputBar';
20
+ import { userMarkdownRegistrations } from './components/UserMessageMarkdown';
20
21
  import { sidebarBodyRegistration, sidebarTabDefinition, sidebarTitleRegistration } from './sidebar';
21
22
  import { settingsRegistration } from './settings';
22
23
  import { registerCodinputCommand } from './command';
@@ -121,6 +122,46 @@ export function apply(ctx: ClientContext): void {
121
122
  subscribeSidebarInput(reconcileBar);
122
123
  });
123
124
 
125
+ // ---- 用户消息 Markdown:conversation.chat.node 键控座 user/steering 遮蔽 ----
126
+ // 独立于接管开关,自带 prefs.userMarkdown;关闭即卸载条目把消息渲染还座
127
+ // 官方(同 key 低 priority 者胜,动态包守卫强制分配负秩)。
128
+ runGuarded('md.register', () => {
129
+ const mdSeats = userMarkdownRegistrations();
130
+ const mdMounted = new Map<string, () => void>();
131
+ const mdWanted = (): boolean => loadPrefs().userMarkdown;
132
+ const mountMdSeat = (seat: (typeof mdSeats)[number]): void => {
133
+ const key = seat.options.key ?? '';
134
+ if (mdMounted.has(key) || !mdWanted()) return;
135
+ mdMounted.set(key, ctx.slots.register(seat.options, seat.component));
136
+ diagWindow().__dshCodinputShell[`md.${key}`] = 'mounted';
137
+ };
138
+ const unmountMdSeat = (key: string): void => {
139
+ const disposer = mdMounted.get(key);
140
+ if (!disposer) return;
141
+ mdMounted.delete(key);
142
+ try {
143
+ disposer();
144
+ diagWindow().__dshCodinputShell[`md.${key}`] = 'unmounted';
145
+ } catch (error) {
146
+ diagError('md.dispose', error);
147
+ }
148
+ };
149
+ const reconcileMd = (): void => {
150
+ for (const seat of mdSeats) {
151
+ const key = seat.options.key ?? '';
152
+ if (mdWanted()) {
153
+ tryRegister(() => mountMdSeat(seat), `md.${key}`);
154
+ } else {
155
+ unmountMdSeat(key);
156
+ }
157
+ }
158
+ };
159
+ reconcileMd();
160
+ runGuarded('md.prefs', () => {
161
+ subscribePrefs(reconcileMd);
162
+ });
163
+ });
164
+
124
165
  // ---- 侧边模式:sidebarRightTabs 两段式注册(dockkit 承担停靠/浮动) ----
125
166
  runGuarded('sidebar.register', () => {
126
167
  ctx.inject(['slots', 'sidebarRightTabs', 'sidebarRight'], (scope: Record<string, unknown>) => {
@@ -17,6 +17,8 @@ export type CodinputMode = 'normal' | 'float' | 'side';
17
17
  export interface CodinputPrefs {
18
18
  /** 启用接管:关闭即恢复官方输入框(卸载遮蔽条目)。 */
19
19
  enabled: boolean;
20
+ /** 用户消息 Markdown 渲染(键控座 conversation.chat.node 的 user/steering 遮蔽)。 */
21
+ userMarkdown: boolean;
20
22
  /** 显示行号(当前行与行号高亮常开)。 */
21
23
  lineNumbers: boolean;
22
24
  /** 编辑器字体(默认 JetBrains Mono)。 */
@@ -43,6 +45,7 @@ export interface CodinputPrefs {
43
45
 
44
46
  export const DEFAULT_PREFS: CodinputPrefs = {
45
47
  enabled: true,
48
+ userMarkdown: true,
46
49
  lineNumbers: true,
47
50
  fontFamily: "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
48
51
  sendHotkey: 'mod+enter',
@@ -176,6 +176,13 @@ export function CodinputSettingsSection(): JSX.Element {
176
176
  <Row title={t("set.enabled")} desc={t("set.enabledDesc")}>
177
177
  <Switch on={prefs.enabled} label={t("set.enabled")} onToggle={() => update({ enabled: !prefs.enabled })} />
178
178
  </Row>
179
+ <Row title={t("set.userMarkdown")} desc={t("set.userMarkdownDesc")}>
180
+ <Switch
181
+ on={prefs.userMarkdown}
182
+ label={t("set.userMarkdown")}
183
+ onToggle={() => update({ userMarkdown: !prefs.userMarkdown })}
184
+ />
185
+ </Row>
179
186
  <Row title={t("set.lineNumbers")} desc={t("set.lineNumbersDesc")}>
180
187
  <Switch on={prefs.lineNumbers} label={t("set.lineNumbers")} onToggle={() => update({ lineNumbers: !prefs.lineNumbers })} />
181
188
  </Row>
@@ -537,6 +537,124 @@ button.dci-attach-body { color: inherit; font: inherit; text-align: left; backgr
537
537
  .dci-btn-primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover, currentColor); }
538
538
  .dci-btn-cancel { min-width: 72px; }
539
539
  .dci-btn-confirm { min-width: 136px; }
540
+
541
+ /* ===== 用户消息 Markdown(chat.node 键控座遮蔽条目)=====
542
+ 气泡/操作行/附件行取值逐条移植官方 MessageItem.module.css 与
543
+ MessageIconActions.module.css;Markdown 排版逐条移植官方
544
+ MarkdownText.module.css;引用 chip 逐条移植官方 user-text.module.css。
545
+ 官方走 --dsw-* token 的取值这里同样走 token(换肤全盘跟随)。 */
546
+ .dci-umsg-row { flex-direction: column; align-items: flex-end; gap: 6px; display: flex; }
547
+ .dci-umsg-stack { min-width: 0; max-width: min(calc(var(--dsh-chat-content-width, 748px) * 0.702), 82%);
548
+ flex-direction: column; align-items: flex-end; gap: 8px; display: flex; }
549
+ .dci-umsg-bubble { background: var(--dsw-specific-bubble); border-radius: var(--dsw-radius-xl); max-width: 100%;
550
+ font-size: var(--dsh-content-font-size, 14px); line-height: calc(22px + var(--dsh-content-font-delta, 0px));
551
+ color: var(--dsw-alias-label-primary); white-space: pre-wrap; word-break: break-word; padding: 10px 16px; }
552
+ .dci-umsg-refsum { color: var(--dsw-alias-label-tertiary);
553
+ font-size: var(--dsh-content-font-size-secondary, 13px); line-height: calc(18px + var(--dsh-content-font-delta-secondary, 0px)); }
554
+ .dci-umsg-attrow { flex-wrap: wrap; justify-content: flex-end; gap: 8px; max-width: 100%; display: flex; }
555
+ .dci-umsg-file { border: 0.5px solid var(--dsw-alias-border-l2, #0000001f); border-radius: var(--dsw-radius-xl);
556
+ background: var(--dsw-specific-input-major, transparent); box-sizing: border-box; flex: 0 0 240px;
557
+ align-items: center; gap: 10px; width: 240px; min-height: 64px; padding: 8px 12px; display: inline-flex; }
558
+ .dci-umsg-fileglyph { flex: none; width: 28px; height: 28px; }
559
+ .dci-umsg-filecontent { flex-direction: column; flex: 1; min-width: 0; display: flex; }
560
+ .dci-umsg-filename { white-space: nowrap; text-overflow: ellipsis; color: var(--dsw-alias-label-primary);
561
+ font-size: 14px; font-weight: 500; line-height: 22px; overflow: hidden; }
562
+ .dci-umsg-filemeta { white-space: nowrap; text-overflow: ellipsis; color: var(--dsw-alias-label-tertiary, #00000073);
563
+ font-size: 12px; line-height: 15px; overflow: hidden; }
564
+ .dci-umsg-actions { height: calc(28px + var(--dsh-content-font-delta, 0px)); align-items: center; gap: 8px; display: flex; }
565
+ .dci-umsg-time { font-size: var(--dsh-content-font-size-secondary, 13px);
566
+ line-height: calc(24px + var(--dsh-content-font-delta, 0px)); color: var(--dsw-alias-label-tertiary); white-space: nowrap; padding-right: 12px; }
567
+ .dci-umsg-action { width: calc(28px + var(--dsh-content-font-delta, 0px)); height: calc(28px + var(--dsh-content-font-delta, 0px));
568
+ border-radius: var(--dsw-radius-sm); color: var(--dsw-alias-label-tertiary); cursor: pointer; background: 0 0;
569
+ border: none; justify-content: center; align-items: center; padding: 6px; display: inline-flex; }
570
+ .dci-umsg-action svg { width: calc(15px + var(--dsh-content-font-delta, 0px)); height: calc(15px + var(--dsh-content-font-delta, 0px)); }
571
+ .dci-umsg-action:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-secondary); }
572
+ /* 悬停显形(官方同款 data 属性面:chat 根的 data-actions-reveal 与 flowItem 的
573
+ data-chat-flow-kind 都在宿主 DOM 上,这里只换目标类)。 */
574
+ @media (hover: hover) {
575
+ [data-actions-reveal="hover"] .dci-umsg-actions,
576
+ :is([data-chat-flow-kind="user"], [data-chat-flow-kind="steering"]):has(~ :is([data-chat-flow-kind="user"], [data-chat-flow-kind="steering"])) .dci-umsg-actions { opacity: 0; transition: opacity 80ms; }
577
+ [data-actions-reveal="hover"]:hover .dci-umsg-actions,
578
+ [data-actions-reveal="hover"]:focus-within .dci-umsg-actions,
579
+ :is([data-chat-flow-kind="user"], [data-chat-flow-kind="steering"]):hover .dci-umsg-actions,
580
+ :is([data-chat-flow-kind="user"], [data-chat-flow-kind="steering"]):focus-within .dci-umsg-actions { opacity: 1; }
581
+ }
582
+ .dci-umsg-extra { margin: 8px 0 0; padding: 8px; border: 0.5px solid var(--dsw-alias-border-l2);
583
+ border-radius: var(--dsw-radius-sm); font-family: var(--ds-font-family-code); font-size: 12px; line-height: 18px;
584
+ overflow: auto; max-height: 200px; white-space: pre; }
585
+
586
+ /* Markdown 排版(官方 MarkdownText.module.css 逐条移植,.markdown → 本根)。 */
587
+ .dci-umsg-md { min-width: 0; overflow-wrap: anywhere; font: var(--dsw-font-markdown-base);
588
+ color: var(--dsw-alias-label-primary); white-space: normal; }
589
+ .dci-umsg-md strong { font-weight: 600; }
590
+ .dci-umsg-md h1 { font: var(--dsw-font-markdown-h1); margin: 32px 0 16px; }
591
+ .dci-umsg-md h2 { font: var(--dsw-font-markdown-h2); margin: 32px 0 16px; }
592
+ .dci-umsg-md h3 { font: var(--dsw-font-markdown-h3); margin: 32px 0 16px; }
593
+ .dci-umsg-md h4 { font: var(--dsw-font-markdown-h4); margin: 16px 0; }
594
+ .dci-umsg-md :where(h5, h6) { font: var(--dsw-font-markdown-base-strong); margin: 16px 0; }
595
+ .dci-umsg-md :where(h1, h2, h3, h4, h5, h6) strong { font-weight: inherit; }
596
+ .dci-umsg-md p { margin: 16px 0; }
597
+ .dci-umsg-md :where(h4, h5, h6) + :where(ul, ol) { margin-top: 8px; }
598
+ .dci-umsg-md :where(h4, h5, h6):has(+ :where(ul, ol)) { margin-bottom: 8px; }
599
+ .dci-umsg-md a { color: var(--dsw-alias-link); font-weight: 500;
600
+ transition: box-shadow var(--ds-transition-duration) var(--ds-ease-in-out); position: relative; text-decoration: none;
601
+ border-left: 3px solid rgb(255 255 255 / 0); border-right: 3px solid rgb(255 255 255 / 0);
602
+ border-top: 2px solid rgb(255 255 255 / 0); border-bottom: 2px solid rgb(255 255 255 / 0);
603
+ margin-left: -3px; margin-right: -3px; }
604
+ .dci-umsg-md a:hover, .dci-umsg-md a:focus { outline: none; text-decoration: underline dotted var(--dsw-alias-link); text-underline-offset: 3px; }
605
+ .dci-umsg-md a:focus-visible { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
606
+ .dci-umsg-md :where(ul, ol) { margin: 16px 0; padding-left: 18px; }
607
+ .dci-umsg-md li:not(:first-child) { margin-top: 6px; }
608
+ .dci-umsg-md li > :where(ul, ol) { margin-top: 4px; }
609
+ .dci-umsg-md li::marker { line-height: 24px; color: var(--dsw-alias-label-secondary); }
610
+ .dci-umsg-md :where(ul, ol) ol { list-style-position: inside; padding-left: 0; }
611
+ .dci-umsg-md :where(ul, ol) ol li p { display: inline; }
612
+ .dci-umsg-md li > p { margin: 8px 0; }
613
+ .dci-umsg-md li > *:first-child { margin-top: 0; }
614
+ .dci-umsg-md hr { display: block; border: none; height: 0.5px; margin: 32px 0; background: var(--dsw-alias-border-l2); }
615
+ .dci-umsg-md blockquote { border-left: 2px solid var(--dsw-alias-label-caption); margin: 16px 0 0; padding-left: 14px; }
616
+ /* 围栏代码块:官方 CodeBlock.module.css 的 .block :where(pre) 逐值——底色/圆角/
617
+ 字体走官方 token(换肤跟随),气泡内靠底色与气泡面区分。语言横幅仍不做
618
+ (组件头注释的已知边界)。 */
619
+ .dci-umsg-md pre { margin: 16px 0; padding: 16px; font: var(--dsw-font-markdown-code-block, var(--ds-font-family-code));
620
+ background: var(--dsw-alias-markdown-code-block, color-mix(in srgb, currentColor 6%, transparent));
621
+ color: var(--dsw-alias-label-primary); white-space: pre-wrap; word-break: break-all; overflow-x: auto;
622
+ border-radius: var(--dsw-radius-lg); }
623
+ .dci-umsg-md pre code { font: inherit; background: none; padding: 0; }
624
+ .dci-umsg-md :not(pre) > code { display: inline-flex; align-items: center; box-sizing: border-box;
625
+ font: var(--dsw-font-markdown-code); font-family: var(--ds-font-family-code); font-size: 0.875em !important;
626
+ /* 底色与围栏代码块同一 token(--dsw-alias-markdown-code-block,产品决策 2026-10-29:
627
+ 两种代码面同色)。官方 --dsw-alias-markdown-inline-code 对页面底设计(深色
628
+ rgb(41,41,41)),进用户气泡(深色 rgb(44,44,46))只差 3/255 不可辨,故不取它。 */
629
+ background-color: var(--dsw-alias-markdown-code-block, color-mix(in srgb, currentColor 6%, transparent));
630
+ border: 0.5px solid var(--dsw-alias-border-l1);
631
+ border-radius: var(--dsw-radius-sm); padding: 0 5px; }
632
+ .dci-umsg-md :where(h1, h2, h3, h4, h5, h6) code { font: inherit; font-family: var(--ds-font-family-code); }
633
+ .dci-umsg-md input[type='checkbox'] { margin: 0 8px 0 0; accent-color: var(--dsw-alias-label-secondary); }
634
+ .dci-umsg-md img { display: block; width: auto; max-width: 100%; height: auto; margin: 0;
635
+ border-radius: var(--dsw-radius-md); background: var(--dsw-alias-bg-base); object-fit: contain; }
636
+ .dci-umsg-tscroll { max-width: 100%; overflow-x: auto; overscroll-behavior-x: contain; }
637
+ .dci-umsg-tscroll table { border-collapse: collapse; width: max-content; max-width: max-content; }
638
+ .dci-umsg-tscroll th { text-align: start; padding: 10px 16px; border-bottom: 0.5px solid var(--dsw-alias-border-l3);
639
+ border-top: none; font: var(--dsw-font-markdown-table-head); max-width: min(30vw, 320px); min-width: 100px; }
640
+ .dci-umsg-tscroll td { padding: 10px 16px; border-bottom: 0.5px solid var(--dsw-alias-border-l2);
641
+ font: var(--dsw-font-markdown-table); max-width: min(30vw, 320px); min-width: 100px; }
642
+ .dci-umsg-tscroll th:first-child, .dci-umsg-tscroll td:first-child { padding-left: 0; }
643
+ .dci-umsg-tscroll td:last-child { padding-right: 0; }
644
+ .dci-umsg-tscroll table code { font-size: 11px; }
645
+ .dci-umsg-md > :first-child, .dci-umsg-md p:first-child { margin-top: 0 !important; }
646
+ .dci-umsg-md > :last-child, .dci-umsg-md p:last-child { margin-bottom: 0 !important; }
647
+
648
+ /* 引用 chip(官方 user-text.module.css 逐条移植):inline 走消费方基线,
649
+ 可点 chip 用官方 fileMention 的链接语言。 */
650
+ .dci-umsg-chip { display: inline; max-width: 100%; margin: 0 2px; color: var(--dsw-alias-state-business-primary);
651
+ font-weight: 500; white-space: inherit; overflow-wrap: anywhere; text-align: inherit; }
652
+ button.dci-umsg-chip { margin: 0; padding: 0; border: none; background: none; font: inherit;
653
+ color: var(--dsw-alias-link); text-decoration: none; cursor: pointer; }
654
+ button.dci-umsg-chip:hover, button.dci-umsg-chip:focus { outline: none; text-decoration: underline dotted var(--dsw-alias-link); text-underline-offset: 3px; }
655
+ button.dci-umsg-chip:focus-visible { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
656
+ .dci-umsg-chip[data-chip="slash"] { font-family: var(--dsw-font-markdown-code-font-family); }
657
+ .dci-umsg-chipicon { width: 1em; height: 1em; margin-right: 4px; vertical-align: -0.125em; }
540
658
  `;
541
659
 
542
660
  export function injectStyles(): void {