dsh-codinput 0.3.2 → 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.
- package/CHANGELOG.md +56 -0
- package/README.en.md +9 -0
- package/README.md +9 -0
- package/lib/client.js +50 -44
- package/package.json +1 -1
- package/src/client/components/CodinputBar.tsx +6 -0
- package/src/client/components/CodinputSurface.tsx +7 -1
- package/src/client/components/InputLeftSlot.tsx +26 -0
- package/src/client/components/InputRightSlot.tsx +26 -0
- package/src/client/components/InputSlotSeat.tsx +80 -0
- package/src/client/components/MenuSurface.tsx +53 -23
- package/src/client/components/PopupSelectView.tsx +3 -2
- package/src/client/components/Toolbar.tsx +24 -3
- package/src/client/components/slotEntry.tsx +133 -3
- package/src/client/sidebar.tsx +5 -0
- package/src/client/styles.ts +17 -8
package/package.json
CHANGED
|
@@ -23,6 +23,7 @@ import { loadPrefs, savePrefs, subscribePrefs, type CodinputPrefs } from '../pre
|
|
|
23
23
|
import { useSidebarInputCarried } from '../sideinput';
|
|
24
24
|
import { CodinputBall } from './ExpandBall';
|
|
25
25
|
import { CodinputSurface } from './CodinputSurface';
|
|
26
|
+
import { kitFromBarProps } from './slotEntry';
|
|
26
27
|
|
|
27
28
|
/** ComposerBarOwnerProps 切面(官方 bar owner)。 */
|
|
28
29
|
interface BarOwner {
|
|
@@ -38,6 +39,10 @@ type BarProps = BarOwner & SessionMaybeStandard & { useChat?: <S>(sel: (s: unkno
|
|
|
38
39
|
export function CodinputBar(props: BarProps): JSX.Element | null {
|
|
39
40
|
const { variant, blocked, disabled, placeholder, sessionId, useInput, inputActions, useProjection, useChat, useSession } = props;
|
|
40
41
|
const state = useInput((s) => s);
|
|
42
|
+
// conversation.input.left/.right 第三方条目的 standard props 原样面:宿主给
|
|
43
|
+
// 本条目的 kit(standard 全集)减去 owner props——enhancer ✨/🎤 等按钮与
|
|
44
|
+
// 左座读数按官方契约消费(sessionId + useSession/useInput + inputActions + t)。
|
|
45
|
+
const inputStandard = kitFromBarProps(props as unknown as Record<string, unknown>);
|
|
41
46
|
// 官方 StatsPills 同款数据源:chat store 的 legacy 节点数组(选择器只取
|
|
42
47
|
// 数组引用,派生换算在 stats.ts 的 useSessionStats 里做)。
|
|
43
48
|
const chatNodes = useChat ? useChat((s: unknown) => (s as { legacy?: { nodes?: unknown } })?.legacy?.nodes) : undefined;
|
|
@@ -56,6 +61,7 @@ export function CodinputBar(props: BarProps): JSX.Element | null {
|
|
|
56
61
|
state={state}
|
|
57
62
|
actions={inputActions}
|
|
58
63
|
useProjection={useProjection}
|
|
64
|
+
inputStandard={inputStandard}
|
|
59
65
|
placeholder={placeholder}
|
|
60
66
|
disabled={disabled === true}
|
|
61
67
|
blocked={blocked !== undefined}
|
|
@@ -34,6 +34,11 @@ export interface CodinputSurfaceProps {
|
|
|
34
34
|
state: InputState | undefined;
|
|
35
35
|
actions: InputActions | undefined;
|
|
36
36
|
useProjection: UseProjection | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* conversation.input.left / .right 条目的 standard props 原样面(宿主
|
|
39
|
+
* kit − owner props),经 Toolbar 转给输入座位镜像转发渲染。
|
|
40
|
+
*/
|
|
41
|
+
inputStandard?: Record<string, unknown> | undefined;
|
|
37
42
|
/** 宿主 bar owner props 透传:占位文案与禁用。 */
|
|
38
43
|
placeholder?: string;
|
|
39
44
|
disabled?: boolean;
|
|
@@ -61,7 +66,7 @@ const DOMPurify = ((DOMPurifyModule as unknown as { default?: unknown }).default
|
|
|
61
66
|
DOMPurifyModule) as unknown as { sanitize(html: string): string };
|
|
62
67
|
|
|
63
68
|
export function CodinputSurface(props: CodinputSurfaceProps): JSX.Element {
|
|
64
|
-
const { sessionId, state, actions, useProjection, placeholder, disabled, blocked, running, side, hero, chatNodes, float, onDragZonePointerDown } = props;
|
|
69
|
+
const { sessionId, state, actions, useProjection, inputStandard, placeholder, disabled, blocked, running, side, hero, chatNodes, float, onDragZonePointerDown } = props;
|
|
65
70
|
// 语言切换时整棵表面重渲染(文案都走 i18n 的 t())。
|
|
66
71
|
useT();
|
|
67
72
|
|
|
@@ -401,6 +406,7 @@ export function CodinputSurface(props: CodinputSurfaceProps): JSX.Element {
|
|
|
401
406
|
onNotify={notifyFor(sessionId)}
|
|
402
407
|
onSend={sendGesture}
|
|
403
408
|
sendDisabled={sendDisabled}
|
|
409
|
+
inputStandard={inputStandard}
|
|
404
410
|
/>
|
|
405
411
|
|
|
406
412
|
{/* 数据行:悬浮形态收进卡片(工具行下方,随卡片圆角一体)。 */}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 左座薄壳(v0.3.5 新增;引擎见 InputSlotSeat)。
|
|
3
|
+
*
|
|
4
|
+
* 座位 = conversation.input.left:官方工具行左区 `.tools` 内
|
|
5
|
+
* [renderSlot(input.permission), renderSlot(input.plan), renderSlot(input.left)]
|
|
6
|
+
* 的末位——issue #1 里「上传视频」按钮、dsh-qoder-plugin 的额度读数等第三方
|
|
7
|
+
* 条目所在座位。遮蔽官方条目后该渲染点消失,本组件在自绘工具行左区同位置
|
|
8
|
+
* (`.dci-tools` 内、权限触发器之后)复刻官方 renderer 的 list 合成。
|
|
9
|
+
*
|
|
10
|
+
* 门槛与官方同款:`input === undefined || sessionId === undefined` 不渲染
|
|
11
|
+
* (Toolbar 判断 state,组件再兜底 sessionId);槽位未声明/无条目 → 零残留。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { InputSlotSeat } from './InputSlotSeat';
|
|
15
|
+
|
|
16
|
+
const LEFT_SLOT = 'conversation.input.left';
|
|
17
|
+
|
|
18
|
+
export interface InputLeftSlotProps {
|
|
19
|
+
sessionId: string | undefined;
|
|
20
|
+
/** 宿主下发给本插件条目的 standard props 原样面(见 InputSlotSeat)。 */
|
|
21
|
+
standard: Record<string, unknown> | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function InputLeftSlot(props: InputLeftSlotProps): JSX.Element | null {
|
|
25
|
+
return <InputSlotSeat slot={LEFT_SLOT} sessionId={props.sessionId} standard={props.standard} />;
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 右座薄壳(v0.3.4 引入;v0.3.5 起与左座共用 InputSlotSeat 通用引擎)。
|
|
3
|
+
*
|
|
4
|
+
* 座位 = conversation.input.right:官方工具行 `.standardControls` 内
|
|
5
|
+
* [renderSlot(input.right), renderSlot(input.model)] 的第一位——
|
|
6
|
+
* 如 dsh-prompt-enhancer 的 ✨ 提示词优化与 🎤 语音识别按钮、各渠道插件的
|
|
7
|
+
* 积分/余额读数。本组件在自绘工具行的同座位(`.dci-standard-controls` 内、
|
|
8
|
+
* 模型选择之前)把条目原样渲染回来。
|
|
9
|
+
*
|
|
10
|
+
* 门槛与官方同款:`input === undefined || sessionId === undefined` 不渲染
|
|
11
|
+
* (Toolbar 判断 state,组件再兜底 sessionId);槽位未声明/无条目 → 零残留。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { InputSlotSeat } from './InputSlotSeat';
|
|
15
|
+
|
|
16
|
+
const RIGHT_SLOT = 'conversation.input.right';
|
|
17
|
+
|
|
18
|
+
export interface InputRightSlotProps {
|
|
19
|
+
sessionId: string | undefined;
|
|
20
|
+
/** 宿主下发给本插件条目的 standard props 原样面(见 InputSlotSeat)。 */
|
|
21
|
+
standard: Record<string, unknown> | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function InputRightSlot(props: InputRightSlotProps): JSX.Element | null {
|
|
25
|
+
return <InputSlotSeat slot={RIGHT_SLOT} sessionId={props.sessionId} standard={props.standard} />;
|
|
26
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 座位镜像通用引擎:把注册在官方 InputBar 子槽位(conversation.input.left /
|
|
3
|
+
* conversation.input.right 等,均为 list/session)上的**条目组件本体**在自绘
|
|
4
|
+
* 工具行的对应座位重新渲染。
|
|
5
|
+
*
|
|
6
|
+
* 为什么需要这里:这些子槽位由官方 InputBar(conversation.composer.bar 的缺省
|
|
7
|
+
* 条目)声明并在其工具行内渲染——Codinput 以 priority -1 遮蔽官方条目后,槽位
|
|
8
|
+
* 声明仍在(宿主按注册周期记账),但渲染点随官方输入框一起消失,第三方按钮/
|
|
9
|
+
* 读数便无处挂载(issue #1)。本组件复刻官方 renderer 的 list 合成,座位名与
|
|
10
|
+
* standard props 面由调用方给(InputLeftSlot / InputRightSlot 两片薄壳)——
|
|
11
|
+
* 一套引擎、N 个座位。
|
|
12
|
+
*
|
|
13
|
+
* props 合成复刻官方 SessionEntry → renderEntry 的展开式:
|
|
14
|
+
* {...standard(kit), ...entryInjectProps(entry, sessionId), ...(t)}
|
|
15
|
+
* standard 走 bar/session 条目已下发的原样通道(CodinputBar 与
|
|
16
|
+
* SidebarCodinputBody 把宿主 kit props 减去 owner props 传下来)——第三方
|
|
17
|
+
* 条目按官方契约消费 sessionId + useSession/useInput 选择器 hook +
|
|
18
|
+
* inputActions(enhancer v3.4.x 实证),有的条目还从 inject 面拿自带 props
|
|
19
|
+
* (如 qoder 左座的 directory store 与 t);这里只做原样转发,绝不重新
|
|
20
|
+
* 物化宿主绑定。
|
|
21
|
+
*
|
|
22
|
+
* 检测即兼容:无条目 → 渲染 null;单条目崩溃被 EntryBoundary 隔离,不连坐
|
|
23
|
+
* 输入面;旧宿主(无该子槽位声明)槽位未声明 → entries 空数组,同样不渲染。
|
|
24
|
+
* 条目声明 store 的 useStore/actions 面插件侧不可复现(官方 host.storeOf
|
|
25
|
+
* 私有),现网无此类条目;如出现则降级为无 store props 渲染。
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { createElement, useMemo } from 'react';
|
|
29
|
+
import { diagError } from '../diag';
|
|
30
|
+
import { EntryBoundary, entryId, entryInjectProps, listRows, translatorFor, useSlotEntries } from './slotEntry';
|
|
31
|
+
|
|
32
|
+
export interface InputSlotSeatProps {
|
|
33
|
+
/** 官方子槽位全名(如 conversation.input.left)。 */
|
|
34
|
+
slot: string;
|
|
35
|
+
sessionId: string | undefined;
|
|
36
|
+
/** 宿主下发给本插件条目的 standard props 原样面(见组件头注释)。 */
|
|
37
|
+
standard: Record<string, unknown> | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function InputSlotSeat(props: InputSlotSeatProps): JSX.Element | null {
|
|
41
|
+
const { slot, sessionId, standard } = props;
|
|
42
|
+
const entries = useSlotEntries(slot);
|
|
43
|
+
// 官方 list 合成:id 首占位去重 + order 升序(entries 快照引用稳定,直接 memo)。
|
|
44
|
+
const rows = useMemo(() => listRows(entries), [entries]);
|
|
45
|
+
|
|
46
|
+
// 官方同款门槛:无会话不渲染(input.left / input.right 都是 strict session 槽位)。
|
|
47
|
+
if (sessionId === undefined || rows.length === 0) return null;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
{rows.map((entry, index) => {
|
|
52
|
+
const Comp = entry.component as React.ComponentType<Record<string, unknown>>;
|
|
53
|
+
// 条目词典的 t(官方 localeSeat 同款);无 locale 或绑定失败 → 不下发,
|
|
54
|
+
// 组件按各自回退(enhancer 回退内置中文词典)。
|
|
55
|
+
let t: unknown;
|
|
56
|
+
try {
|
|
57
|
+
t = translatorFor(entry) ?? undefined;
|
|
58
|
+
} catch {
|
|
59
|
+
t = undefined;
|
|
60
|
+
}
|
|
61
|
+
let injected: Record<string, unknown> | null = null;
|
|
62
|
+
try {
|
|
63
|
+
injected = entryInjectProps(entry, sessionId);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
diagError('slot.seat.inject', error);
|
|
66
|
+
}
|
|
67
|
+
return createElement(
|
|
68
|
+
EntryBoundary,
|
|
69
|
+
{ key: entryId(entry, index) },
|
|
70
|
+
createElement(Comp, {
|
|
71
|
+
...(standard ?? {}),
|
|
72
|
+
sessionId,
|
|
73
|
+
...(injected ?? {}),
|
|
74
|
+
...(t !== undefined ? { t } : {}),
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
</>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* 官方 Menu 容器复刻:fixed 定位、锚点上方弹出、r20 卡片、视口约束。
|
|
3
|
+
*
|
|
4
|
+
* 定位复刻官方 ModelSelect 的 place():
|
|
5
|
+
* y = anchor.top - 8 - h(side=top)/ anchor.bottom + 4(side=bottom),
|
|
6
|
+
* x/y 各夹进 [12, innerWidth - w - 12] / [12, innerHeight - h - 12]。
|
|
7
|
+
* place 随**内容尺寸每次变化**重跑(ResizeObserver 挂卡片本体)+ 页面
|
|
8
|
+
* scroll/resize 重跑——官方同款监听。旧实现只在打开瞬间定位一次,钻入
|
|
9
|
+
* 更长的子页(模型/推理强度列表)时卡片保持小菜单的 top 向下生长,
|
|
10
|
+
* 表现为「向下弹出」且列表伸出视口外够不着。
|
|
11
|
+
*
|
|
12
|
+
* 滚动复刻官方两级结构:卡片 overflow:hidden +
|
|
13
|
+
* max-height: min(360px, 100vh - 96px),列表在 .dci-viewport 内滚,
|
|
14
|
+
* 细滚动条由卡片注入的 --dsh-scrollbar-thumb(l2) 变量着色(官方
|
|
15
|
+
* ModelSelect .menu 同款)。
|
|
16
|
+
*/
|
|
2
17
|
|
|
3
|
-
import { useEffect, useLayoutEffect, useRef, type ReactNode } from 'react';
|
|
18
|
+
import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react';
|
|
4
19
|
|
|
5
20
|
export interface MenuSurfaceProps {
|
|
6
21
|
/** 触发器矩形(getBoundingClientRect 快照)。 */
|
|
@@ -10,31 +25,42 @@ export interface MenuSurfaceProps {
|
|
|
10
25
|
/** 右对齐锚点(trailing 控件用)。 */
|
|
11
26
|
alignEnd?: boolean;
|
|
12
27
|
minWidth?: number;
|
|
13
|
-
/** 弹出方向:top =
|
|
28
|
+
/** 弹出方向:top = 锚点上方(默认,官方 ModelSelect/Menu 同款),bottom = 锚点下方(设置区下拉)。 */
|
|
14
29
|
side?: 'top' | 'bottom';
|
|
15
30
|
}
|
|
16
31
|
|
|
32
|
+
/** 官方 place() 的视口留距。 */
|
|
33
|
+
const MARGIN = 12;
|
|
34
|
+
|
|
17
35
|
export function MenuSurface(props: MenuSurfaceProps): JSX.Element {
|
|
18
36
|
const { anchor, onClose, children, alignEnd, minWidth = 218, side = 'top' } = props;
|
|
19
37
|
const ref = useRef<HTMLDivElement | null>(null);
|
|
38
|
+
const [pos, setPos] = useState<{ left: number; top: number } | null>(null);
|
|
20
39
|
|
|
21
40
|
useLayoutEffect(() => {
|
|
22
41
|
const el = ref.current;
|
|
23
42
|
if (!el) return;
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
const place = (): void => {
|
|
44
|
+
// 尺寸取 offset*(官方同款):不受自身 top/left 变更反馈影响。
|
|
45
|
+
const w = el.offsetWidth;
|
|
46
|
+
const h = el.offsetHeight;
|
|
47
|
+
let x = alignEnd ? anchor.right - w : anchor.left;
|
|
48
|
+
let y = side === 'top' ? anchor.top - 8 - h : anchor.bottom + 4;
|
|
49
|
+
if (w > 0) x = Math.min(Math.max(x, MARGIN), window.innerWidth - w - MARGIN);
|
|
50
|
+
if (h > 0) y = Math.min(Math.max(y, MARGIN), window.innerHeight - h - MARGIN);
|
|
51
|
+
setPos((prev) => (prev && prev.left === x && prev.top === y ? prev : { left: x, top: y }));
|
|
52
|
+
};
|
|
53
|
+
place();
|
|
54
|
+
// 内容换页/目录热更/滚动/视口变化都重定位——官方 place 的三路监听。
|
|
55
|
+
window.addEventListener('scroll', place, true);
|
|
56
|
+
window.addEventListener('resize', place);
|
|
57
|
+
const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(place) : null;
|
|
58
|
+
observer?.observe(el);
|
|
59
|
+
return () => {
|
|
60
|
+
observer?.disconnect();
|
|
61
|
+
window.removeEventListener('scroll', place, true);
|
|
62
|
+
window.removeEventListener('resize', place);
|
|
63
|
+
};
|
|
38
64
|
}, [anchor, alignEnd, side]);
|
|
39
65
|
|
|
40
66
|
useEffect(() => {
|
|
@@ -46,16 +72,20 @@ export function MenuSurface(props: MenuSurfaceProps): JSX.Element {
|
|
|
46
72
|
return () => window.removeEventListener('pointerdown', onDown, true);
|
|
47
73
|
}, [onClose]);
|
|
48
74
|
|
|
49
|
-
|
|
75
|
+
// 未定位的首帧隐藏但仍参与布局(官方 unplaced card 同款)——place() 在
|
|
76
|
+
// layout effect 里同步测得尺寸后即落位,无闪烁。
|
|
50
77
|
return (
|
|
51
78
|
<div
|
|
52
79
|
ref={ref}
|
|
53
80
|
className="dci-menu"
|
|
54
|
-
style={
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
81
|
+
style={{
|
|
82
|
+
left: pos?.left ?? 0,
|
|
83
|
+
top: pos?.top ?? 0,
|
|
84
|
+
bottom: 'auto',
|
|
85
|
+
right: 'auto',
|
|
86
|
+
minWidth,
|
|
87
|
+
...(pos ? null : { visibility: 'hidden' }),
|
|
88
|
+
}}
|
|
59
89
|
>
|
|
60
90
|
<div className="dci-viewport">{children}</div>
|
|
61
91
|
</div>
|
|
@@ -114,7 +114,8 @@ export function PopupSelectView(props: PopupSelectViewProps): JSX.Element | null
|
|
|
114
114
|
</div>
|
|
115
115
|
</div>
|
|
116
116
|
) : (
|
|
117
|
-
|
|
117
|
+
/* 官方两级结构:卡片 overflow:hidden,列表在 .dci-viewport 内滚 */
|
|
118
|
+
<div className="dci-viewport">
|
|
118
119
|
{state.status === 'pending' ? <div className="dci-empty">{t('menu.loading')}</div> : null}
|
|
119
120
|
{state.status === 'ready' && filtered.length === 0 ? <div className="dci-empty">{t('menu.empty')}</div> : null}
|
|
120
121
|
{filtered.map((option, index) => (
|
|
@@ -132,7 +133,7 @@ export function PopupSelectView(props: PopupSelectViewProps): JSX.Element | null
|
|
|
132
133
|
</span>
|
|
133
134
|
</button>
|
|
134
135
|
))}
|
|
135
|
-
|
|
136
|
+
</div>
|
|
136
137
|
)}
|
|
137
138
|
{state.error && state.status === 'ready' ? <div className="dci-error">{state.error}</div> : null}
|
|
138
139
|
</div>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 工具行:结构与样式逐条对齐官方 InputBar 工具行——
|
|
3
|
-
* 左侧 .dci-tools:指令(plus)、权限(shield
|
|
3
|
+
* 左侧 .dci-tools:指令(plus)、权限(shield 触发器),其后是官方左区末位
|
|
4
|
+
* 的 conversation.input.left 座位(第三方条目镜像,见 InputLeftSlot);
|
|
4
5
|
* 右侧 .dci-trailing:模型与思考强度(触发器)、语音输入(官方 activity 槽位
|
|
5
|
-
* 条目,桌面版宿主才有)、发送(.primary
|
|
6
|
+
* 条目,桌面版宿主才有)、发送(.primary 圆钮);conversation.input.right 座位
|
|
7
|
+
* 在 .dci-standard-controls 内、模型之前(见 InputRightSlot)。
|
|
6
8
|
* 按钮沿用官方 keepFocus 语义(mousedown 不夺走编辑器焦点)。
|
|
7
9
|
* 附件无独立按钮(官方已并入 slash「添加 · 文件」),粘贴/拖拽路径保留。
|
|
8
10
|
* 0.1.7-rc.2 起官方上下文小圈不再在工具行(挪进卡片下方 dock 行,见 StatsRow)。
|
|
@@ -18,6 +20,8 @@ import { IconPlusOfficial, IconSendOfficial, IconStopOfficial } from '../icons';
|
|
|
18
20
|
import { ModelMenu } from './ModelMenu';
|
|
19
21
|
import { PermissionMenu } from './PermissionMenu';
|
|
20
22
|
import { VoiceActivity } from './VoiceActivity';
|
|
23
|
+
import { InputLeftSlot } from './InputLeftSlot';
|
|
24
|
+
import { InputRightSlot } from './InputRightSlot';
|
|
21
25
|
|
|
22
26
|
export interface ToolbarProps {
|
|
23
27
|
sessionId: string | undefined;
|
|
@@ -27,6 +31,13 @@ export interface ToolbarProps {
|
|
|
27
31
|
/** caret 偏移(指令按钮以当前 caret 构造 synthetic hit)。 */
|
|
28
32
|
caretRef: { current: number | null };
|
|
29
33
|
useProjection: UseProjection | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* conversation.input.left / .right 条目的 standard props 原样面(宿主下发
|
|
36
|
+
* 给本插件 bar/body 条目的 kit 减去 owner props),供两座第三方条目转发
|
|
37
|
+
* 用。它是条目 props 的一部分而非门控:缺席时条目仍按槽位条目渲染,只是
|
|
38
|
+
* 拿不到这组标准面(见 InputSlotSeat)。
|
|
39
|
+
*/
|
|
40
|
+
inputStandard?: Record<string, unknown> | undefined;
|
|
30
41
|
/** owner disabled(权限触发器随之锁住,对齐官方 locked)。 */
|
|
31
42
|
disabled?: boolean;
|
|
32
43
|
onNotify?(level: 'info' | 'error', text: string): void;
|
|
@@ -45,7 +56,7 @@ function keepFocus(event: React.MouseEvent): void {
|
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
export function Toolbar(props: ToolbarProps): JSX.Element {
|
|
48
|
-
const { sessionId, state, actions, triggers, caretRef, useProjection, disabled, onNotify } = props;
|
|
59
|
+
const { sessionId, state, actions, triggers, caretRef, useProjection, disabled, onNotify, inputStandard } = props;
|
|
49
60
|
useT();
|
|
50
61
|
|
|
51
62
|
const attachments: readonly DraftAttachmentId[] = state?.attachmentIds ?? [];
|
|
@@ -81,9 +92,19 @@ export function Toolbar(props: ToolbarProps): JSX.Element {
|
|
|
81
92
|
disabled={disabled}
|
|
82
93
|
onNotify={onNotify}
|
|
83
94
|
/>
|
|
95
|
+
{/* 官方左区同构:`.tools` 内 modes(permission→plan)之后是
|
|
96
|
+
renderSlot("conversation.input.left") 的末位。 */}
|
|
97
|
+
{state !== undefined && sessionId !== undefined ? (
|
|
98
|
+
<InputLeftSlot sessionId={sessionId} standard={inputStandard} />
|
|
99
|
+
) : null}
|
|
84
100
|
</div>
|
|
85
101
|
<div className="dci-trailing">
|
|
86
102
|
<div className="dci-standard-controls">
|
|
103
|
+
{/* 官方同款门槛:input === undefined || sessionId === undefined 时
|
|
104
|
+
整个 input.right 座位不渲染(renderSlot 调用点的三元条件)。 */}
|
|
105
|
+
{state !== undefined && sessionId !== undefined ? (
|
|
106
|
+
<InputRightSlot sessionId={sessionId} standard={inputStandard} />
|
|
107
|
+
) : null}
|
|
87
108
|
<ModelMenu
|
|
88
109
|
sessionId={sessionId}
|
|
89
110
|
onNotify={onNotify}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 槽位条目渲染的共享件:检视任意槽位的条目快照(检测即兼容)、按条目 locale
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 绑定官方词典、快照 store 的选择器钩子合成、条目崩溃兜底边界,以及宿主 kit
|
|
4
|
+
* 减去 owner props 的 standard 面(座位镜像 InputSlotSeat 与语音/模型标签
|
|
5
|
+
* 共用同一套合成公式,抽到这里避免多份漂移)。
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { useCallback, useSyncExternalStore, Component } from 'react';
|
|
8
|
-
import type { SlotEntryLike } from '../host-types';
|
|
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';
|
|
@@ -50,6 +51,102 @@ export function firstComponentEntry(entries: readonly unknown[]): SlotEntryLike
|
|
|
50
51
|
return null;
|
|
51
52
|
}
|
|
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
|
+
|
|
53
150
|
/** 官方 renderer 的 locale 座位:locale.bind(entry.locale),按 ns 缓存。 */
|
|
54
151
|
export function translatorFor(entry: SlotEntryLike, fallbackNs?: string): Translator | null {
|
|
55
152
|
const ns = entry.locale ?? fallbackNs;
|
|
@@ -73,3 +170,36 @@ export class EntryBoundary extends Component<
|
|
|
73
170
|
return this.state.failed ? this.props.fallback ?? null : this.props.children;
|
|
74
171
|
}
|
|
75
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
|
+
}
|
package/src/client/sidebar.tsx
CHANGED
|
@@ -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
|
/>
|
package/src/client/styles.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 -
|
|
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, #
|
|
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, #
|
|
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, #
|
|
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;
|