dsh-codinput 0.3.1 → 0.3.5

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.
@@ -17,51 +17,21 @@
17
17
  * locale 缺席)一律不渲染,绝不半组装。
18
18
  */
19
19
 
20
- import { createElement, useCallback, useMemo, useSyncExternalStore, Component } from 'react';
20
+ import { createElement, useMemo, useSyncExternalStore } from 'react';
21
21
  import type { HostObservable, SlotEntryLike } from '../host-types';
22
- import { stash } from '../services';
23
- import { bindForeign } from '../i18n';
24
22
  import { diagError } from '../diag';
23
+ import { EntryBoundary, firstComponentEntry, translatorFor, useSlotEntries } from './slotEntry';
25
24
 
26
25
  const ACTIVITY_SLOT = 'conversation.input.activity';
27
26
  const VOICE_NS_FALLBACK = 'voice-input';
28
27
 
29
- type Translator = (key: string, params?: Record<string, string | number>) => string;
30
28
  type ReadinessSelector = (value: unknown) => unknown;
31
29
  type ReadinessHook = (selector: ReadinessSelector) => unknown;
32
30
 
33
- const EMPTY_ENTRIES: readonly unknown[] = [];
34
-
35
31
  /** observableHook 同款缓存轴:钩子身份按 readiness store 稳定,不逐渲染重订。 */
36
32
  const readinessHookCache = new WeakMap<object, ReadinessHook>();
37
33
  /** 官方 renderer 的 inject 缓存轴:条目 × 会话绑定。 */
38
34
  const injectCache = new WeakMap<object, Map<string, Record<string, unknown> | null>>();
39
- const translatorCache = new Map<string, Translator | null>();
40
-
41
- /** 槽位条目快照:entries 引用在变更之间稳定(官方契约),直接作 uSES 源。 */
42
- function useSlotEntries(key: string): readonly unknown[] {
43
- const slots = stash.slots;
44
- const subscribe = useCallback(
45
- (fn: () => void) => {
46
- if (typeof slots?.subscribe !== 'function') return () => {};
47
- try {
48
- return slots.subscribe(key, fn) ?? (() => {});
49
- } catch {
50
- return () => {};
51
- }
52
- },
53
- [slots, key],
54
- );
55
- const getSnapshot = useCallback(() => {
56
- if (typeof slots?.entries !== 'function') return EMPTY_ENTRIES;
57
- try {
58
- return slots.entries(key) ?? EMPTY_ENTRIES;
59
- } catch {
60
- return EMPTY_ENTRIES;
61
- }
62
- }, [slots, key]);
63
- return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
64
- }
65
35
 
66
36
  /** 调条目 inject 工厂取 props 面(session 槽位以 sessionId 调用,官方同参)。 */
67
37
  function resolveInjected(entry: SlotEntryLike, sessionId: string): Record<string, unknown> | null {
@@ -84,13 +54,6 @@ function resolveInjected(entry: SlotEntryLike, sessionId: string): Record<string
84
54
  return perSession.get(sessionId) ?? null;
85
55
  }
86
56
 
87
- /** 官方 renderer 的 locale 座位:locale.bind(entry.locale),按 ns 缓存。 */
88
- function translatorFor(entry: SlotEntryLike): Translator | null {
89
- const ns = entry.locale ?? VOICE_NS_FALLBACK;
90
- if (!translatorCache.has(ns)) translatorCache.set(ns, bindForeign(ns));
91
- return translatorCache.get(ns) ?? null;
92
- }
93
-
94
57
  /** hooks.speechReadiness(快照 store)→ use<Name> 选择器钩子(observableHook 同款)。 */
95
58
  function readinessHookFor(source: unknown): ReadinessHook | null {
96
59
  const face = source as HostObservable<unknown> | undefined;
@@ -108,20 +71,6 @@ function readinessHookFor(source: unknown): ReadinessHook | null {
108
71
  return hook;
109
72
  }
110
73
 
111
- /** 条目崩溃只丢语音钮,不连坐 Codinput 的 bar 条目(bar 被废黜 = 全部输入消失)。 */
112
- class VoiceBoundary extends Component<{ children?: React.ReactNode }, { failed: boolean }> {
113
- state = { failed: false };
114
- static getDerivedStateFromError(): { failed: boolean } {
115
- return { failed: true };
116
- }
117
- componentDidCatch(error: unknown): void {
118
- diagError('voice.entry', error);
119
- }
120
- render(): React.ReactNode {
121
- return this.state.failed ? null : this.props.children;
122
- }
123
- }
124
-
125
74
  export interface VoiceActivityProps {
126
75
  sessionId: string | undefined;
127
76
  /** 官方输入机动作面(bar 槽位 standard props 里的 inputActions 原样透传)。 */
@@ -137,19 +86,13 @@ export function VoiceActivity(props: VoiceActivityProps): JSX.Element | null {
137
86
  const entries = useSlotEntries(ACTIVITY_SLOT);
138
87
 
139
88
  // single 槽位赢家 = 优先级最低者 = entries 首个(register 即按优先级排序)。
140
- const entry = useMemo((): SlotEntryLike | null => {
141
- for (const candidate of entries) {
142
- const record = candidate as SlotEntryLike | null | undefined;
143
- if (record && typeof record === 'object' && typeof record.component === 'function') return record;
144
- }
145
- return null;
146
- }, [entries]);
89
+ const entry = useMemo(() => firstComponentEntry(entries), [entries]);
147
90
 
148
91
  const injected = useMemo(
149
92
  () => (entry && sessionId ? resolveInjected(entry, sessionId) : null),
150
93
  [entry, sessionId],
151
94
  );
152
- const translator = useMemo(() => (entry ? translatorFor(entry) : null), [entry]);
95
+ const translator = useMemo(() => (entry ? translatorFor(entry, VOICE_NS_FALLBACK) : null), [entry]);
153
96
  const readinessSource = (injected?.['hooks'] as { speechReadiness?: unknown } | undefined)?.speechReadiness;
154
97
  const useReadiness = useMemo(() => readinessHookFor(readinessSource), [readinessSource]);
155
98
 
@@ -158,7 +101,7 @@ export function VoiceActivity(props: VoiceActivityProps): JSX.Element | null {
158
101
  // hooks 子表已折成 use* 钩子(bindInjectSources 同款),其余 props 原样下传。
159
102
  const { hooks: _hooks, ...actions } = injected;
160
103
  void _hooks;
161
- return createElement(VoiceBoundary, {
104
+ return createElement(EntryBoundary, {
162
105
  children: createElement(entry.component as React.ComponentType<Record<string, unknown>>, {
163
106
  sessionId,
164
107
  inputActions,
@@ -0,0 +1,205 @@
1
+ /**
2
+ * 槽位条目渲染的共享件:检视任意槽位的条目快照(检测即兼容)、按条目 locale
3
+ * 绑定官方词典、快照 store 的选择器钩子合成、条目崩溃兜底边界,以及宿主 kit
4
+ * 减去 owner props 的 standard 面(座位镜像 InputSlotSeat 与语音/模型标签
5
+ * 共用同一套合成公式,抽到这里避免多份漂移)。
6
+ */
7
+
8
+ import { useCallback, useSyncExternalStore, Component } from 'react';
9
+ import type { HostObservable, SlotEntryLike } from '../host-types';
10
+ import { bindForeign } from '../i18n';
11
+ import { diagError } from '../diag';
12
+ import { stash } from '../services';
13
+
14
+ export type Translator = (key: string, params?: Record<string, string | number>) => string;
15
+
16
+ const EMPTY_ENTRIES: readonly unknown[] = [];
17
+
18
+ /** 槽位条目快照:entries 引用在变更之间稳定(官方契约),直接作 uSES 源。 */
19
+ export function useSlotEntries(key: string): readonly unknown[] {
20
+ // 服务缺席时退化为恒空快照;slots 就绪后 deps 变化自动重订。
21
+ const slots = stash.slots;
22
+ const subscribe = useCallback(
23
+ (fn: () => void) => {
24
+ if (typeof slots?.subscribe !== 'function') return () => {};
25
+ try {
26
+ return slots.subscribe(key, fn) ?? (() => {});
27
+ } catch {
28
+ return () => {};
29
+ }
30
+ },
31
+ [slots, key],
32
+ );
33
+ const getSnapshot = useCallback(() => {
34
+ if (typeof slots?.entries !== 'function') return EMPTY_ENTRIES;
35
+ try {
36
+ return slots.entries(key) ?? EMPTY_ENTRIES;
37
+ } catch {
38
+ // 槽位键未声明(如 web 0.1.x 没有 model.label 子槽位)按无条目处理。
39
+ return EMPTY_ENTRIES;
40
+ }
41
+ }, [slots, key]);
42
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
43
+ }
44
+
45
+ /** single 槽位赢家 = entries 首个带组件的条目(register 即按优先级排序)。 */
46
+ export function firstComponentEntry(entries: readonly unknown[]): SlotEntryLike | null {
47
+ for (const candidate of entries) {
48
+ const record = candidate as SlotEntryLike | null | undefined;
49
+ if (record && typeof record === 'object' && typeof record.component === 'function') return record;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ /**
55
+ * 列表槽位行合成:复刻官方 renderer 的 list 分支——
56
+ * entries 已由 SlotCore 按(priority↑, order↑)排序,同 id 首个占位(去重),
57
+ * 剩余行再按 order 升序稳定排序(官方 `[...rows].sort(a.order - b.order)`)。
58
+ * 无组件的条目跳过(官方 deadCell 是废黜兜底,插件侧无法复现其内部状态)。
59
+ */
60
+ export function listRows(entries: readonly unknown[]): SlotEntryLike[] {
61
+ const byId = new Map<unknown, SlotEntryLike>();
62
+ for (const candidate of entries) {
63
+ const record = candidate as (SlotEntryLike & { options?: { id?: unknown } }) | null | undefined;
64
+ if (!record || typeof record !== 'object' || typeof record.component !== 'function') continue;
65
+ const id = (record as { options?: { id?: unknown } }).options?.id;
66
+ if (byId.has(id)) continue;
67
+ byId.set(id, record);
68
+ }
69
+ const rows = [...byId.values()];
70
+ rows.sort((a, b) => orderOf(a) - orderOf(b));
71
+ return rows;
72
+ }
73
+
74
+ function orderOf(entry: SlotEntryLike): number {
75
+ const options = (entry as { options?: { order?: number } }).options;
76
+ return options?.order ?? 0;
77
+ }
78
+
79
+ /** 条目 options.id(React key 用;官方 renderer 同轴)。 */
80
+ export function entryId(entry: SlotEntryLike, index: number): string {
81
+ const options = (entry as { options?: { id?: string } }).options;
82
+ return options?.id ?? `#${index}`;
83
+ }
84
+
85
+ export type SnapshotSelectorHook = <S>(sel: (s: any) => S, eq?: (a: S, b: S) => boolean) => S;
86
+
87
+ /** 快照 store → use<Name> 选择器钩子(官方 observableHook 同款,按源缓存)。 */
88
+ const snapshotHookCache = new WeakMap<object, SnapshotSelectorHook>();
89
+ export function snapshotHookFor(source: unknown): SnapshotSelectorHook | null {
90
+ const face = source as HostObservable<unknown> | undefined;
91
+ if (!face || typeof face.subscribe !== 'function' || typeof face.getSnapshot !== 'function') return null;
92
+ let hook = snapshotHookCache.get(face);
93
+ if (!hook) {
94
+ hook = <S,>(sel: (s: unknown) => S): S =>
95
+ useSyncExternalStore(
96
+ (fn: () => void) => face.subscribe(fn) ?? (() => {}),
97
+ () => sel(face.getSnapshot()),
98
+ () => sel(face.getSnapshot()),
99
+ );
100
+ snapshotHookCache.set(face, hook);
101
+ }
102
+ return hook;
103
+ }
104
+
105
+ /** 官方 standardHookPropName:name → use<Name>(首字母大写)。 */
106
+ export function standardHookPropName(name: string): string {
107
+ if (!name) return name;
108
+ return `use${name.charAt(0).toUpperCase()}${name.slice(1)}`;
109
+ }
110
+
111
+ /**
112
+ * 条目自带 inject 的 props 面(官方 runInject + bindInjectSources 的插件侧
113
+ * 复刻):以 sessionId 调 inject,把 hooks 子表折成 use<Name> 钩子、其余
114
+ * 原样下传;keyedHooks 需要官方 keyed resolver 面,插件侧不可复现 → 跳过
115
+ * (现网条目无键控钩子)。结果按(条目 × 会话)缓存——官方同款缓存轴。
116
+ */
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;
120
+ let perSession = entryInjectCache.get(entry);
121
+ if (!perSession) {
122
+ perSession = new Map();
123
+ entryInjectCache.set(entry, perSession);
124
+ }
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;
140
+ }
141
+ } catch (error) {
142
+ diagError('slot.inject', error);
143
+ bound = null;
144
+ }
145
+ perSession.set(sessionId, bound);
146
+ }
147
+ return perSession.get(sessionId) ?? null;
148
+ }
149
+
150
+ /** 官方 renderer 的 locale 座位:locale.bind(entry.locale),按 ns 缓存。 */
151
+ export function translatorFor(entry: SlotEntryLike, fallbackNs?: string): Translator | null {
152
+ const ns = entry.locale ?? fallbackNs;
153
+ if (!ns) return null;
154
+ return bindForeign(ns);
155
+ }
156
+
157
+ /** 条目崩溃只丢本座位,不连坐 Codinput 的 bar 条目(被废黜 = 输入面消失)。 */
158
+ export class EntryBoundary extends Component<
159
+ { children?: React.ReactNode; fallback?: React.ReactNode },
160
+ { failed: boolean }
161
+ > {
162
+ state = { failed: false };
163
+ static getDerivedStateFromError(): { failed: boolean } {
164
+ return { failed: true };
165
+ }
166
+ componentDidCatch(error: unknown): void {
167
+ diagError('slot.entry', error);
168
+ }
169
+ render(): React.ReactNode {
170
+ return this.state.failed ? this.props.fallback ?? null : this.props.children;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * 宿主 kit 减去 owner props 的原样面:供槽位条目按官方契约消费
176
+ * (sessionId + 选择器 hook + inputActions 等)。bar 条目的 kit 里 owner
177
+ * 专属字段(variant/blocked/disabled/placeholder/…)不该下发给子槽位条目;
178
+ * 侧栏 body 再额外剔除注入的 useTabInfo。
179
+ */
180
+ export function kitFromBarProps(
181
+ props: Record<string, unknown>,
182
+ extraOmit: readonly string[] = [],
183
+ ): Record<string, unknown> {
184
+ const {
185
+ variant: _variant,
186
+ blocked: _blocked,
187
+ disabled: _disabled,
188
+ placeholder: _placeholder,
189
+ workspacePickerOpen: _workspacePickerOpen,
190
+ onRequestWorkspace: _onRequestWorkspace,
191
+ accessory: _accessory,
192
+ hero: _hero,
193
+ ...kit
194
+ } = props;
195
+ void _variant;
196
+ void _blocked;
197
+ void _disabled;
198
+ void _placeholder;
199
+ void _workspacePickerOpen;
200
+ void _onRequestWorkspace;
201
+ void _accessory;
202
+ void _hero;
203
+ for (const key of extraOmit) delete kit[key];
204
+ return kit;
205
+ }
@@ -345,6 +345,8 @@ export interface SidebarTabInfo {
345
345
  readonly tab: {
346
346
  readonly visible: boolean;
347
347
  readonly id?: string;
348
+ /** 打开时捕获的标签名(dockkit tab 记录;chip 标题组件显示它)。 */
349
+ readonly title?: string;
348
350
  /** tab 域动作面(occurrence held):close() 关闭本标签。 */
349
351
  readonly actions?: { readonly close?: () => void };
350
352
  };
@@ -7,7 +7,7 @@ interface IconProps {
7
7
  className?: string;
8
8
  }
9
9
 
10
- function svg(paths: JSX.Element, size: number | undefined, className: string | undefined): JSX.Element {
10
+ function svg(paths: JSX.Element, size: number | undefined, className: string | undefined, stroke = 'currentColor'): JSX.Element {
11
11
  return (
12
12
  <svg
13
13
  xmlns="http://www.w3.org/2000/svg"
@@ -15,7 +15,7 @@ function svg(paths: JSX.Element, size: number | undefined, className: string | u
15
15
  height={size ?? 16}
16
16
  viewBox="0 0 24 24"
17
17
  fill="none"
18
- stroke="currentColor"
18
+ stroke={stroke}
19
19
  strokeWidth={2}
20
20
  strokeLinecap="round"
21
21
  strokeLinejoin="round"
@@ -47,6 +47,15 @@ export function IconBrand({ size, className }: IconProps): JSX.Element {
47
47
  return svg(BRAND_MARK, size, className);
48
48
  }
49
49
 
50
+ /**
51
+ * 品牌彩色标:同一机型标固定鲸鱼蓝(assets/icon.svg 同源 #3964FE),不随
52
+ * currentColor——侧栏标签 chip 与指南页条目用(官方在该两处画的是彩色
53
+ * artwork 同位:FileTypeIcon 文件夹黄、GuideArtworkFiles 彩绘)。
54
+ */
55
+ export function IconBrandLogo({ size, className }: IconProps): JSX.Element {
56
+ return svg(BRAND_MARK, size, className, '#3964FE');
57
+ }
58
+
50
59
  export function IconEdit({ size, className }: IconProps): JSX.Element {
51
60
  return svg(
52
61
  <>
@@ -17,7 +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 { sidebarBodyRegistration, sidebarTabDefinition } from './sidebar';
20
+ import { sidebarBodyRegistration, sidebarTabDefinition, sidebarTitleRegistration } from './sidebar';
21
21
  import { settingsRegistration } from './settings';
22
22
  import { registerCodinputCommand } from './command';
23
23
 
@@ -135,6 +135,11 @@ export function apply(ctx: ClientContext): void {
135
135
  tryRegister(() => {
136
136
  ctx.slots.register(body.options, body.component);
137
137
  }, 'sidebar.body');
138
+ // chip 标题(品牌标 + 标签名):官方 FilesTitle 同构的 keyed 注册。
139
+ const title = sidebarTitleRegistration();
140
+ tryRegister(() => {
141
+ ctx.slots.register(title.options, title.component);
142
+ }, 'sidebar.title');
138
143
  });
139
144
  });
140
145
 
@@ -19,6 +19,8 @@ export interface DirectorySnapshot {
19
19
  failures: readonly { id: string; name: string; message: string }[];
20
20
  status: 'idle' | 'loading' | 'ready' | 'selecting' | 'error';
21
21
  error: string | null;
22
+ /** 官方 syncInputs 附带:当前强度折算成的显示名(0.2.x 座位触发器在模型无 reasoning 元数据时显示它)。 */
23
+ retainedEffort?: string;
22
24
  }
23
25
 
24
26
  export interface SessionDirectory {
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * 侧边模式:官方 Sidebar(dockkit)标签页承载,入口与标签芯片同名
3
- * 「Codinput」。两段式注册——sidebarRightTabs.register(类型定义)+
4
- * sidebar.right.pane.tab(keyed,key = 定义 id)。停靠、分栏、全屏、
5
- * 浮动由官方 dockkit 承担;侧栏与主输入读写同一台输入机,草稿天然同源。
3
+ * 「Codinput」。三段式注册——sidebarRightTabs.register(类型定义,guide
4
+ * 条目带品牌标)+ sidebar.right.pane.tab(keyed,key = 定义 id,body)+
5
+ * sidebar.right.pane.tab.title(keyed 同 key,chip 标题:品牌标 + tab.title,
6
+ * 官方 FilesTitle 同构——不注册时 chip 退化为打开时捕获的纯文本)。
7
+ * 停靠、分栏、全屏、浮动由官方 dockkit 承担;侧栏与主输入读写同一台
8
+ * 输入机,草稿天然同源。
6
9
  *
7
10
  * 唯一输入组件约定:body **挂载**期间经 sideinput 总线声明承载本会话
8
11
  * 输入(形态 = side),主输入遮蔽条目随之隐藏——收起侧栏不让位(可见性
@@ -18,6 +21,8 @@ import { claimSidebarInput, subscribeSidebarTabClose, useSidebarCarryOwner, type
18
21
  import { loadPrefs, savePrefs, subscribePrefs } from './prefs';
19
22
  import { diagError, diagWindow } from './diag';
20
23
  import { CodinputSurface } from './components/CodinputSurface';
24
+ import { kitFromBarProps } from './components/slotEntry';
25
+ import { IconBrandLogo } from './icons';
21
26
  import { t, useT } from './i18n';
22
27
 
23
28
  export const SIDEBAR_TAB_ID = 'dsh-codinput';
@@ -39,6 +44,9 @@ export function sidebarTabDefinition(): Record<string, unknown> {
39
44
  order: 0,
40
45
  title: () => 'Codinput',
41
46
  description: () => t('side.guideDesc'),
47
+ // 指南页条目图标:官方 EntryBox 渲染 <Icon size={22|26}/>,缺席
48
+ // 退灰色立方体(placeholderInk)——给品牌彩色标。
49
+ icon: IconBrandLogo,
42
50
  },
43
51
  ],
44
52
  };
@@ -56,6 +64,32 @@ export function sidebarBodyRegistration(): { options: SlotRegisterOptions; compo
56
64
  };
57
65
  }
58
66
 
67
+ /**
68
+ * 第三段:标签 chip 标题(keyed 同 key)。官方同位组件(FilesTitle/
69
+ * GuideTitle)= 彩色图标 + useTabInfo().tab.title;title 座位声明了
70
+ * tabInfo 注入 hook,宿主按 use<Name> 约定下发。缺席时退回静态名。
71
+ */
72
+ export function sidebarTitleRegistration(): { options: SlotRegisterOptions; component: unknown } {
73
+ return {
74
+ options: {
75
+ name: 'sidebar.right.pane.tab.title',
76
+ key: SIDEBAR_TAB_ID,
77
+ registrant: 'dsh-codinput',
78
+ },
79
+ component: SidebarCodinputTitle,
80
+ };
81
+ }
82
+
83
+ export function SidebarCodinputTitle(props: { useTabInfo?: () => SidebarTabInfo }): JSX.Element {
84
+ const title = props.useTabInfo?.().tab.title ?? 'Codinput';
85
+ return (
86
+ <>
87
+ <IconBrandLogo size={16} />
88
+ {title}
89
+ </>
90
+ );
91
+ }
92
+
59
93
  /**
60
94
  * session 槽位标准 props 直达官方输入机。useTabInfo 是槽位声明的注入
61
95
  * hook(宿主按 use<Name> 约定随 props 下发,缺席说明宿主形状漂移——
@@ -65,6 +99,9 @@ export function sidebarBodyRegistration(): { options: SlotRegisterOptions; compo
65
99
  export function SidebarCodinputBody(props: SessionStandard & { useTabInfo?: () => SidebarTabInfo }): JSX.Element {
66
100
  const { sessionId, useInput, inputActions, useProjection, useSession, useTabInfo } = props;
67
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']);
68
105
  // 会话运行中:侧栏形态的主按钮同样变停止(官方同款)。
69
106
  const running = useSession ? useSession((s: unknown) => (s as { running?: boolean } | undefined)?.running) === true : false;
70
107
  useT();
@@ -165,6 +202,7 @@ export function SidebarCodinputBody(props: SessionStandard & { useTabInfo?: () =
165
202
  state={state}
166
203
  actions={inputActions}
167
204
  useProjection={useProjection}
205
+ inputStandard={inputStandard}
168
206
  running={running}
169
207
  side={true}
170
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)===== */
@@ -102,10 +106,13 @@ const CSS = `
102
106
  .dci-add:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-solid, color-mix(in srgb, currentColor 14%, transparent)); }
103
107
  .dci-add:disabled { opacity: 0.5; cursor: default; }
104
108
 
105
- /* 下拉触发(官方 PermissionSelect / ModelSelection .trigger)*/
109
+ /* 下拉触发(官方 PermissionSelect / ModelSelect .trigger)。radius 官方走
110
+ * --dsw-radius-sm,无 token 的旧宿主(0.1.5-rc.3)回退原 24px 视觉;
111
+ * 模型触发器官方 font-weight 400(权限 500),经 [data-model] 区分。*/
106
112
  .dci-trigger { min-width: 0; max-width: 220px; height: 28px; color: var(--dsw-alias-label-secondary, currentColor); cursor: pointer;
107
- background: transparent; border: none; border-radius: 24px; outline: none; align-items: center; gap: 4px;
113
+ background: transparent; border: none; border-radius: var(--dsw-radius-sm, 24px); outline: none; align-items: center; gap: 4px;
108
114
  padding: 0 4px 0 8px; font-size: 13px; font-weight: 500; line-height: 20px; display: inline-flex; }
115
+ .dci-trigger[data-model] { font-weight: 400; }
109
116
  .dci-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, currentColor 8%, transparent)); }
110
117
  .dci-trigger:focus-visible { box-shadow: 0 0 0 2px var(--dsw-alias-border-l3, transparent); }
111
118
  .dci-trigger:disabled { color: var(--dsw-alias-label-dimmed, currentColor); cursor: default; opacity: 0.5; }
@@ -123,14 +130,19 @@ const CSS = `
123
130
  .dci-send:hover:not(:disabled) { background: var(--dsw-alias-button-info-hover, color-mix(in srgb, currentColor 70%, transparent)); }
124
131
  .dci-send:disabled { opacity: 0.4; cursor: default; }
125
132
 
126
- /* ===== 菜单(官方 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)} 据此出条。 ===== */
127
137
  .dci-menu { position: fixed; z-index: 1100; box-sizing: border-box; padding: 4px; display: flex; flex-direction: column;
128
- 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));
129
141
  background: var(--dsw-menu-surface-fill, var(--dsw-specific-menu, rgba(28, 28, 32, 0.98)));
130
142
  backdrop-filter: var(--dsw-menu-backdrop-filter, none); color: var(--dsw-alias-label-primary, #ececf1);
131
143
  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1, transparent);
132
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; }
133
- .dci-viewport { display: flex; flex-direction: column; min-height: 0; }
145
+ .dci-viewport { display: flex; flex-direction: column; min-height: 0; overflow-y: auto; }
134
146
  .dci-menuLabel { padding: 8px 10px; font-size: 12px; line-height: 16px; color: var(--dsw-alias-label-tertiary, currentColor);
135
147
  user-select: none; text-align: left; }
136
148
  .dci-item { display: flex; align-items: center; gap: 8px; width: 100%; min-height: 40px; padding: 8px 10px; border: none;
@@ -207,7 +219,7 @@ const CSS = `
207
219
  .dci-search { width: calc(100% - 8px); margin: 2px 4px 4px; padding: 5px 9px; font-size: 13px;
208
220
  border-radius: 8px; border: 1px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 18%, transparent));
209
221
  background: transparent; color: inherit; outline: none; }
210
- .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; }
211
223
 
212
224
  /* ===== 模型/推理等级 cell 行(官方 ModelSelection .cell)===== */
213
225
  .dci-cell { box-sizing: border-box; width: auto; min-width: 100%; height: 40px; color: var(--dsw-alias-label-primary, inherit);
@@ -222,7 +234,7 @@ const CSS = `
222
234
  /* ===== 完全权限确认(官方 RiskConfirmation 语义)===== */
223
235
  .dci-confirm-warning { display: flex; align-items: flex-start; gap: 10px; color: var(--dsw-alias-label-secondary, currentColor);
224
236
  font-size: 14px; line-height: 22px; padding: 6px 8px 0; }
225
- .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); }
226
238
  .dci-confirm-warning p { margin: 0; }
227
239
  .dci-confirm-ack { display: flex; align-items: flex-start; gap: 10px; margin: 14px 8px 0; cursor: pointer;
228
240
  color: var(--dsw-alias-label-primary, inherit); font-size: 14px; line-height: 22px; }
@@ -234,7 +246,7 @@ const CSS = `
234
246
  .dci-btn { border: 1px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 22%, transparent)); border-radius: 10px;
235
247
  background: transparent; color: inherit; padding: 6px 14px; cursor: pointer; font-size: 14px; line-height: 20px; }
236
248
  .dci-btn[data-primary="true"] { background: var(--dsw-alias-button-primary-fill, color-mix(in srgb, currentColor 85%, transparent));
237
- color: #fff; border-color: transparent; }
249
+ color: var(--dsw-alias-label-primary-foreground, #fff); border-color: transparent; }
238
250
  .dci-btn:disabled { opacity: 0.4; cursor: default; }
239
251
 
240
252
  /* ===== 草稿附件轨(官方 ComposerAttachments/AttachmentRail/FileCard 逐值复刻)=====
@@ -510,7 +522,7 @@ button.dci-attach-body { color: inherit; font: inherit; text-align: left; backgr
510
522
  .dci-confirm-warning { display: flex; align-items: flex-start; gap: 10px; color: var(--dsw-alias-label-secondary, inherit);
511
523
  font-size: 14px; line-height: 22px; }
512
524
  .dci-confirm-warning p { margin: 0; }
513
- .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); }
514
526
  .dci-confirm-ack { display: flex; align-items: flex-start; gap: 10px; margin-top: 20px; cursor: pointer;
515
527
  color: var(--dsw-alias-label-primary, inherit); font-size: 14px; line-height: 22px; }
516
528
  .dci-confirm-ack input { flex: none; width: 16px; height: 16px; margin: 3px 0 0; cursor: pointer;