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.
- package/CHANGELOG.md +75 -0
- package/README.en.md +9 -0
- package/README.md +13 -0
- package/TESTING.md +3 -0
- package/lib/client.js +47 -41
- 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 +49 -0
- package/src/client/components/MenuSurface.tsx +53 -23
- package/src/client/components/ModelSlot.tsx +83 -0
- package/src/client/components/PopupSelectView.tsx +3 -2
- package/src/client/components/Toolbar.tsx +32 -5
- package/src/client/components/slotEntry.tsx +249 -5
- package/src/client/host-types.ts +8 -0
- 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,49 @@
|
|
|
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 { useMemo } from 'react';
|
|
29
|
+
import { entryId, listRows, renderSlotEntry, useSlotEntries } from './slotEntry';
|
|
30
|
+
|
|
31
|
+
export interface InputSlotSeatProps {
|
|
32
|
+
/** 官方子槽位全名(如 conversation.input.left)。 */
|
|
33
|
+
slot: string;
|
|
34
|
+
sessionId: string | undefined;
|
|
35
|
+
/** 宿主下发给本插件条目的 standard props 原样面(见组件头注释)。 */
|
|
36
|
+
standard: Record<string, unknown> | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function InputSlotSeat(props: InputSlotSeatProps): JSX.Element | null {
|
|
40
|
+
const { slot, sessionId, standard } = props;
|
|
41
|
+
const entries = useSlotEntries(slot);
|
|
42
|
+
// 官方 list 合成:id 首占位去重 + order 升序(entries 快照引用稳定,直接 memo)。
|
|
43
|
+
const rows = useMemo(() => listRows(entries), [entries]);
|
|
44
|
+
|
|
45
|
+
// 官方同款门槛:无会话不渲染(input.left / input.right 都是 strict session 槽位)。
|
|
46
|
+
if (sessionId === undefined || rows.length === 0) return null;
|
|
47
|
+
|
|
48
|
+
return <>{rows.map((entry, index) => renderSlotEntry(entry, entryId(entry, index), { sessionId, standard }))}</>;
|
|
49
|
+
}
|
|
@@ -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>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型座单值镜像(issue #4):`conversation.input.model`。
|
|
3
|
+
*
|
|
4
|
+
* 座位事实(宿主 0.2.1-rc.2 取证):
|
|
5
|
+
* - `conversation.input.model` 是 `{kind:"single", scope:"session"}` 单值槽,
|
|
6
|
+
* 由官方 InputBar 条目(`conversation.composer.bar` 缺省条目)声明,唯一
|
|
7
|
+
* 渲染点在 InputBar 体内:`input.model → renderSlot(..., { locked })`;
|
|
8
|
+
* 官方占用者是 client-ui-model-selection 的 ModelSelect(替换风险标记
|
|
9
|
+
* `shadows-shipped-ui`,生态替换类插件按契约以 priority 遮蔽它)。
|
|
10
|
+
* - Codinput 以 priority 遮蔽整个 bar 后,该 renderSlot 调用点随官方条目一起
|
|
11
|
+
* 消失;但「遮蔽」不是 dispose——宿主条目仍在 ledger 上,座位声明仍在,
|
|
12
|
+
* 第三方对它的 `slots.inject` 照常触发、`register` 照常成功(单值槽低
|
|
13
|
+
* priority 无人竞争时静默赢)、`entries` 里它确实是赢家,只是没有任何
|
|
14
|
+
* renderSlot 会读它。表现为「插件装了、不报错、永不上屏」。
|
|
15
|
+
*
|
|
16
|
+
* 本组件的收敛口径(0.3.6 起):
|
|
17
|
+
* - **只镜像第三方遮蔽者**(宿主动态包:register 被强制分配负数遮蔽秩、
|
|
18
|
+
* fiber 名带 `dyn/` 前缀),见 `isShadowingOccupant`;
|
|
19
|
+
* - **出厂占用的 ModelSelect 不镜像**——那个座位一直由 Codinput 自绘的模型
|
|
20
|
+
* 芯片代表(模型名显示仍走官方 `conversation.input.model.label` 子座),
|
|
21
|
+
* 镜像出厂条目会与自绘芯片重复成两个模型选择器;
|
|
22
|
+
* - 有第三方遮蔽者时**让位**:镜像其组件、隐藏自绘芯片(同座只留一个控件,
|
|
23
|
+
* 与官方「一个单值座只画一个占用者」一致)。
|
|
24
|
+
*
|
|
25
|
+
* 官方门槛同款复刻:`sessionId === undefined` 时该座位不渲染(此时自绘芯片
|
|
26
|
+
* 保持现状)。owner prop `locked`(官方 `InputControlOwnerProps`:会话是否
|
|
27
|
+
* 拒绝交互)原样下发——生态替换类插件按契约消费它(如 reasoning-slider 的
|
|
28
|
+
* `props.locked === true` 置灰)。
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { useMemo } from 'react';
|
|
32
|
+
import type { ReactNode } from 'react';
|
|
33
|
+
import type { SlotEntryLike } from '../host-types';
|
|
34
|
+
import {
|
|
35
|
+
entryId,
|
|
36
|
+
entryInjectFailed,
|
|
37
|
+
firstComponentEntry,
|
|
38
|
+
isShadowingOccupant,
|
|
39
|
+
renderSlotEntry,
|
|
40
|
+
useSlotEntries,
|
|
41
|
+
} from './slotEntry';
|
|
42
|
+
|
|
43
|
+
const MODEL_SLOT = 'conversation.input.model';
|
|
44
|
+
|
|
45
|
+
export interface ModelSlotProps {
|
|
46
|
+
sessionId: string | undefined;
|
|
47
|
+
/** 宿主下发给本插件条目的 standard props 原样面(见 InputSlotSeat)。 */
|
|
48
|
+
standard: Record<string, unknown> | undefined;
|
|
49
|
+
/** 官方 owner prop locked(会话拒绝交互时第三方座条目应置灰)。 */
|
|
50
|
+
locked: boolean;
|
|
51
|
+
/** 无第三方遮蔽者时的自绘模型芯片(保持既有 UX)。 */
|
|
52
|
+
fallback: ReactNode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 模型座的第三方遮蔽者(无则 null):单值槽赢家且命中动态包判据。 */
|
|
56
|
+
export function useModelSeatOccupant(): SlotEntryLike | null {
|
|
57
|
+
const entries = useSlotEntries(MODEL_SLOT);
|
|
58
|
+
return useMemo(() => {
|
|
59
|
+
const winner = firstComponentEntry(entries);
|
|
60
|
+
return winner !== null && isShadowingOccupant(winner) ? winner : null;
|
|
61
|
+
}, [entries]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function ModelSlot(props: ModelSlotProps): JSX.Element | null {
|
|
65
|
+
const { sessionId, standard, locked, fallback } = props;
|
|
66
|
+
const occupant = useModelSeatOccupant();
|
|
67
|
+
|
|
68
|
+
// 官方门槛:无会话不画该座位;自绘芯片(fallback)保持既有行为。
|
|
69
|
+
if (sessionId === undefined || occupant === null) return <>{fallback}</>;
|
|
70
|
+
|
|
71
|
+
// 让位可行性:inject 面必须在宿主 fiber 上下文里才算数(见 slotEntry)。
|
|
72
|
+
// 若该条目的 inject 依赖宿主服务面(插件侧调用会抛 without inject),
|
|
73
|
+
// 让位降级——回退自绘芯片(它本就是官方模型选择的等价物),而不是留空。
|
|
74
|
+
if (entryInjectFailed(occupant, sessionId)) return <>{fallback}</>;
|
|
75
|
+
|
|
76
|
+
return renderSlotEntry(occupant, `model:${entryId(occupant, 0)}`, {
|
|
77
|
+
sessionId,
|
|
78
|
+
standard,
|
|
79
|
+
ownerProps: { locked },
|
|
80
|
+
// 组件本体崩溃同样回退自绘芯片(逐条目边界已在 renderSlotEntry 内)。
|
|
81
|
+
fallback,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -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)。
|
|
@@ -16,8 +18,11 @@ import { openCommandMenu } from '../triggers';
|
|
|
16
18
|
import { t, useT } from '../i18n';
|
|
17
19
|
import { IconPlusOfficial, IconSendOfficial, IconStopOfficial } from '../icons';
|
|
18
20
|
import { ModelMenu } from './ModelMenu';
|
|
21
|
+
import { ModelSlot } from './ModelSlot';
|
|
19
22
|
import { PermissionMenu } from './PermissionMenu';
|
|
20
23
|
import { VoiceActivity } from './VoiceActivity';
|
|
24
|
+
import { InputLeftSlot } from './InputLeftSlot';
|
|
25
|
+
import { InputRightSlot } from './InputRightSlot';
|
|
21
26
|
|
|
22
27
|
export interface ToolbarProps {
|
|
23
28
|
sessionId: string | undefined;
|
|
@@ -27,6 +32,13 @@ export interface ToolbarProps {
|
|
|
27
32
|
/** caret 偏移(指令按钮以当前 caret 构造 synthetic hit)。 */
|
|
28
33
|
caretRef: { current: number | null };
|
|
29
34
|
useProjection: UseProjection | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* conversation.input.left / .right 条目的 standard props 原样面(宿主下发
|
|
37
|
+
* 给本插件 bar/body 条目的 kit 减去 owner props),供两座第三方条目转发
|
|
38
|
+
* 用。它是条目 props 的一部分而非门控:缺席时条目仍按槽位条目渲染,只是
|
|
39
|
+
* 拿不到这组标准面(见 InputSlotSeat)。
|
|
40
|
+
*/
|
|
41
|
+
inputStandard?: Record<string, unknown> | undefined;
|
|
30
42
|
/** owner disabled(权限触发器随之锁住,对齐官方 locked)。 */
|
|
31
43
|
disabled?: boolean;
|
|
32
44
|
onNotify?(level: 'info' | 'error', text: string): void;
|
|
@@ -45,7 +57,7 @@ function keepFocus(event: React.MouseEvent): void {
|
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
export function Toolbar(props: ToolbarProps): JSX.Element {
|
|
48
|
-
const { sessionId, state, actions, triggers, caretRef, useProjection, disabled, onNotify } = props;
|
|
60
|
+
const { sessionId, state, actions, triggers, caretRef, useProjection, disabled, onNotify, inputStandard } = props;
|
|
49
61
|
useT();
|
|
50
62
|
|
|
51
63
|
const attachments: readonly DraftAttachmentId[] = state?.attachmentIds ?? [];
|
|
@@ -81,12 +93,27 @@ export function Toolbar(props: ToolbarProps): JSX.Element {
|
|
|
81
93
|
disabled={disabled}
|
|
82
94
|
onNotify={onNotify}
|
|
83
95
|
/>
|
|
96
|
+
{/* 官方左区同构:`.tools` 内 modes(permission→plan)之后是
|
|
97
|
+
renderSlot("conversation.input.left") 的末位。 */}
|
|
98
|
+
{state !== undefined && sessionId !== undefined ? (
|
|
99
|
+
<InputLeftSlot sessionId={sessionId} standard={inputStandard} />
|
|
100
|
+
) : null}
|
|
84
101
|
</div>
|
|
85
102
|
<div className="dci-trailing">
|
|
86
103
|
<div className="dci-standard-controls">
|
|
87
|
-
|
|
104
|
+
{/* 官方同款门槛:input === undefined || sessionId === undefined 时
|
|
105
|
+
整个 input.right 座位不渲染(renderSlot 调用点的三元条件)。 */}
|
|
106
|
+
{state !== undefined && sessionId !== undefined ? (
|
|
107
|
+
<InputRightSlot sessionId={sessionId} standard={inputStandard} />
|
|
108
|
+
) : null}
|
|
109
|
+
{/* 官方右区顺序:input.right → input.model。模型座单值槽的第三方
|
|
110
|
+
遮蔽者(如 reasoning-slider / dsh-model-accordion / effort-dial)
|
|
111
|
+
由 ModelSlot 镜像并让位;无遮蔽者时回退自绘模型芯片(issue #4)。 */}
|
|
112
|
+
<ModelSlot
|
|
88
113
|
sessionId={sessionId}
|
|
89
|
-
|
|
114
|
+
standard={inputStandard}
|
|
115
|
+
locked={disabled === true}
|
|
116
|
+
fallback={<ModelMenu sessionId={sessionId} onNotify={onNotify} />}
|
|
90
117
|
/>
|
|
91
118
|
</div>
|
|
92
119
|
<div className="dci-voice-seat">
|