dsh-codinput 0.1.0
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 +30 -0
- package/LICENSE +21 -0
- package/NOTICE +18 -0
- package/README.en.md +167 -0
- package/README.md +167 -0
- package/TESTING.md +95 -0
- package/assets/en/preview-ball-detail.png +0 -0
- package/assets/en/preview-float.png +0 -0
- package/assets/en/preview-normal.png +0 -0
- package/assets/en/preview-side.png +0 -0
- package/assets/en/preview-split.png +0 -0
- package/assets/icon.svg +1 -0
- package/assets/preview-ball-detail.png +0 -0
- package/assets/preview-float.png +0 -0
- package/assets/preview-normal.png +0 -0
- package/assets/preview-side.png +0 -0
- package/assets/preview-split.png +0 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +544 -0
- package/lib/index.js +5 -0
- package/package.json +79 -0
- package/src/client/attachments.ts +43 -0
- package/src/client/cm.ts +207 -0
- package/src/client/command.ts +78 -0
- package/src/client/components/AttachmentRail.tsx +241 -0
- package/src/client/components/CodinputBar.tsx +357 -0
- package/src/client/components/CodinputSurface.tsx +375 -0
- package/src/client/components/EditorPane.tsx +214 -0
- package/src/client/components/ExpandBall.tsx +122 -0
- package/src/client/components/MenuSurface.tsx +72 -0
- package/src/client/components/ModelMenu.tsx +234 -0
- package/src/client/components/PermissionMenu.tsx +257 -0
- package/src/client/components/PopupSelectView.tsx +140 -0
- package/src/client/components/SlashMenu.tsx +255 -0
- package/src/client/components/StatsRow.tsx +188 -0
- package/src/client/components/Toolbar.tsx +239 -0
- package/src/client/components/popup-filter.ts +13 -0
- package/src/client/components/useSnapshot.ts +18 -0
- package/src/client/diag.ts +58 -0
- package/src/client/host-types.ts +328 -0
- package/src/client/hotkey.ts +82 -0
- package/src/client/i18n.ts +312 -0
- package/src/client/icons.tsx +296 -0
- package/src/client/index.tsx +178 -0
- package/src/client/modeldir.ts +42 -0
- package/src/client/permission.ts +123 -0
- package/src/client/prefs.ts +123 -0
- package/src/client/services.ts +78 -0
- package/src/client/settings.tsx +236 -0
- package/src/client/sidebar.tsx +114 -0
- package/src/client/sideinput.ts +171 -0
- package/src/client/stats.ts +166 -0
- package/src/client/styles.ts +489 -0
- package/src/client/triggers.ts +84 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具行:结构与样式逐条对齐官方 InputBar 工具行——
|
|
3
|
+
* 左侧 .dci-tools:指令(plus)、添加附件(paperclip)、权限(shield 触发器);
|
|
4
|
+
* 右侧 .dci-trailing:模型与思考强度(触发器)、发送(.primary 圆钮)。
|
|
5
|
+
* 按钮沿用官方 keepFocus 语义(mousedown 不夺走编辑器焦点)。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useEffect, useRef, useState } from 'react';
|
|
9
|
+
import type { DraftAttachmentId, InputActions, InputState, InputTriggerController, UseProjection } from '../host-types';
|
|
10
|
+
import { openCommandMenu } from '../triggers';
|
|
11
|
+
import { stash } from '../services';
|
|
12
|
+
import { addFilesToDraft } from '../attachments';
|
|
13
|
+
import { t, useT } from '../i18n';
|
|
14
|
+
import { IconPaperclipOfficial, IconPlusOfficial, IconSendOfficial } from '../icons';
|
|
15
|
+
import { ModelMenu } from './ModelMenu';
|
|
16
|
+
import { PermissionMenu } from './PermissionMenu';
|
|
17
|
+
import { useSnapshot } from './useSnapshot';
|
|
18
|
+
|
|
19
|
+
export interface ToolbarProps {
|
|
20
|
+
sessionId: string | undefined;
|
|
21
|
+
state: InputState | undefined;
|
|
22
|
+
actions: InputActions | undefined;
|
|
23
|
+
triggers: InputTriggerController | undefined;
|
|
24
|
+
/** caret 偏移(指令按钮以当前 caret 构造 synthetic hit)。 */
|
|
25
|
+
caretRef: { current: number | null };
|
|
26
|
+
useProjection: UseProjection | undefined;
|
|
27
|
+
/** owner disabled(权限触发器随之锁住,对齐官方 locked)。 */
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
onNotify?(level: 'info' | 'error', text: string): void;
|
|
30
|
+
/** 主发送手势(与编辑器发送键同一策略)。 */
|
|
31
|
+
onSend(): void;
|
|
32
|
+
sendDisabled?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 官方 keepFocus:mousedown 阻止默认,避免编辑器失焦。 */
|
|
36
|
+
function keepFocus(event: React.MouseEvent): void {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function Toolbar(props: ToolbarProps): JSX.Element {
|
|
41
|
+
const { sessionId, state, actions, triggers, caretRef, useProjection, disabled, onNotify } = props;
|
|
42
|
+
useT();
|
|
43
|
+
const fileRef = useRef<HTMLInputElement | null>(null);
|
|
44
|
+
|
|
45
|
+
const attachments: readonly DraftAttachmentId[] = state?.attachmentIds ?? [];
|
|
46
|
+
|
|
47
|
+
const onCommandClick = (): void => {
|
|
48
|
+
if (!triggers) return;
|
|
49
|
+
openCommandMenu(triggers, undefined, caretRef.current ?? 0);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const onAttach = (files: FileList | null): void => {
|
|
53
|
+
addFilesToDraft(sessionId, actions, files, onNotify);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="dci-row">
|
|
58
|
+
<div className="dci-tools">
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
className="dci-add"
|
|
62
|
+
aria-label={t("tool.command")}
|
|
63
|
+
aria-haspopup="listbox"
|
|
64
|
+
title={t("tool.command")}
|
|
65
|
+
disabled={!triggers}
|
|
66
|
+
onMouseDown={keepFocus}
|
|
67
|
+
onClick={onCommandClick}
|
|
68
|
+
>
|
|
69
|
+
<IconPlusOfficial size={14} />
|
|
70
|
+
</button>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
className="dci-add"
|
|
74
|
+
aria-label={t("tool.attach")}
|
|
75
|
+
title={t("tool.attach")}
|
|
76
|
+
onMouseDown={keepFocus}
|
|
77
|
+
onClick={() => fileRef.current?.click()}
|
|
78
|
+
>
|
|
79
|
+
<IconPaperclipOfficial size={14} />
|
|
80
|
+
</button>
|
|
81
|
+
<input
|
|
82
|
+
ref={fileRef}
|
|
83
|
+
type="file"
|
|
84
|
+
multiple
|
|
85
|
+
hidden
|
|
86
|
+
onChange={(e) => {
|
|
87
|
+
onAttach(e.target.files);
|
|
88
|
+
e.target.value = '';
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
<PermissionMenu
|
|
92
|
+
sessionId={sessionId}
|
|
93
|
+
actions={actions}
|
|
94
|
+
currentDraft={state?.draft ?? ''}
|
|
95
|
+
useProjection={useProjection}
|
|
96
|
+
disabled={disabled}
|
|
97
|
+
onNotify={onNotify}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
<div className="dci-trailing">
|
|
101
|
+
<ModelMenu
|
|
102
|
+
sessionId={sessionId}
|
|
103
|
+
onNotify={onNotify}
|
|
104
|
+
/>
|
|
105
|
+
<ContextGauge useProjection={useProjection} />
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
className="dci-send"
|
|
109
|
+
aria-label={t("tool.send")}
|
|
110
|
+
title={t("tool.send")}
|
|
111
|
+
disabled={props.sendDisabled}
|
|
112
|
+
onMouseDown={keepFocus}
|
|
113
|
+
onClick={() => props.onSend()}
|
|
114
|
+
>
|
|
115
|
+
<IconSendOfficial size={16} />
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const GAUGE_CIRCUMFERENCE = 2 * Math.PI * 5.5;
|
|
123
|
+
|
|
124
|
+
interface PressureSnapshot {
|
|
125
|
+
contextWindow?: number;
|
|
126
|
+
pressureTokens?: number;
|
|
127
|
+
projectedTokens?: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface BreakdownSnapshot {
|
|
131
|
+
messageTokens?: number;
|
|
132
|
+
systemTokens?: number;
|
|
133
|
+
toolsTokens?: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function shortTokens(n: number): string {
|
|
137
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;
|
|
138
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}K`;
|
|
139
|
+
return `${n}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 上下文占用小圈(官方 JObwrW 复刻):双环仪表 + 点击展开分段面板。 */
|
|
143
|
+
function ContextGauge(props: { useProjection: UseProjection | undefined }): JSX.Element | null {
|
|
144
|
+
const { useProjection } = props;
|
|
145
|
+
const [open, setOpen] = useState(false);
|
|
146
|
+
const rootRef = useRef<HTMLSpanElement | null>(null);
|
|
147
|
+
const pressure = useProjection
|
|
148
|
+
? (useProjection('contextPressure') as PressureSnapshot | undefined)
|
|
149
|
+
: undefined;
|
|
150
|
+
const breakdown = useProjection
|
|
151
|
+
? (useProjection('contextBreakdown') as BreakdownSnapshot | undefined)
|
|
152
|
+
: undefined;
|
|
153
|
+
// 点外面关闭。
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (!open) return;
|
|
156
|
+
const onDown = (event: PointerEvent): void => {
|
|
157
|
+
if (rootRef.current && event.target instanceof Node && rootRef.current.contains(event.target)) return;
|
|
158
|
+
setOpen(false);
|
|
159
|
+
};
|
|
160
|
+
window.addEventListener('pointerdown', onDown, true);
|
|
161
|
+
return () => window.removeEventListener('pointerdown', onDown, true);
|
|
162
|
+
}, [open]);
|
|
163
|
+
|
|
164
|
+
if (!pressure || !pressure.contextWindow) return null;
|
|
165
|
+
const windowTokens = pressure.contextWindow;
|
|
166
|
+
const pct = Math.min(100, Math.max(0, ((pressure.pressureTokens ?? 0) / windowTokens) * 100));
|
|
167
|
+
const dash = (pct / 100) * GAUGE_CIRCUMFERENCE;
|
|
168
|
+
const segments: readonly [string, number][] = [
|
|
169
|
+
['dci-gauge-colorSystem', breakdown?.systemTokens ?? 0],
|
|
170
|
+
['dci-gauge-colorTools', breakdown?.toolsTokens ?? 0],
|
|
171
|
+
['dci-gauge-colorMessages', breakdown?.messageTokens ?? 0],
|
|
172
|
+
];
|
|
173
|
+
return (
|
|
174
|
+
<span className="dci-gauge-root" ref={rootRef}>
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
className="dci-gauge-trigger"
|
|
178
|
+
aria-label={t("tool.context", { percent: Math.round(pct) })}
|
|
179
|
+
aria-haspopup="dialog"
|
|
180
|
+
aria-expanded={open}
|
|
181
|
+
title={t("tool.context", { percent: Math.round(pct) })}
|
|
182
|
+
onClick={() => setOpen(!open)}
|
|
183
|
+
>
|
|
184
|
+
<svg viewBox="0 0 14 14" width="14" height="14" aria-hidden="true">
|
|
185
|
+
<circle className="dci-gauge-track" cx="7" cy="7" r="5.5" />
|
|
186
|
+
<circle
|
|
187
|
+
className="dci-gauge-fill"
|
|
188
|
+
cx="7" cy="7" r="5.5"
|
|
189
|
+
strokeDasharray={`${dash} ${GAUGE_CIRCUMFERENCE}`}
|
|
190
|
+
transform="rotate(-90 7 7)"
|
|
191
|
+
/>
|
|
192
|
+
</svg>
|
|
193
|
+
</button>
|
|
194
|
+
{open ? (
|
|
195
|
+
<div className="dci-gauge-panel" role="dialog" aria-label={t("tool.contextTitle")}>
|
|
196
|
+
<div className="dci-gauge-header">
|
|
197
|
+
<span className="dci-gauge-headline">{t("tool.contextTitle")}</span>
|
|
198
|
+
<span className="dci-gauge-percent">{Math.round(pct)}%</span>
|
|
199
|
+
<span className="dci-gauge-figures">
|
|
200
|
+
~{shortTokens(pressure.pressureTokens ?? 0)} / {shortTokens(windowTokens)}
|
|
201
|
+
</span>
|
|
202
|
+
</div>
|
|
203
|
+
<div className="dci-gauge-bar">
|
|
204
|
+
{segments.map(([cls, tokens]) => (
|
|
205
|
+
<div
|
|
206
|
+
key={cls}
|
|
207
|
+
className={`dci-gauge-segment ${cls}`}
|
|
208
|
+
style={{ width: `${(tokens / windowTokens) * 100}%` }}
|
|
209
|
+
/>
|
|
210
|
+
))}
|
|
211
|
+
</div>
|
|
212
|
+
<dl className="dci-gauge-rows">
|
|
213
|
+
<div className="dci-gauge-row">
|
|
214
|
+
<dt>
|
|
215
|
+
<span className={`dci-gauge-swatch dci-gauge-colorSystem`} aria-hidden="true" />
|
|
216
|
+
{t("stats.prompt")}
|
|
217
|
+
</dt>
|
|
218
|
+
<dd>~{shortTokens(breakdown?.systemTokens ?? 0)}</dd>
|
|
219
|
+
</div>
|
|
220
|
+
<div className="dci-gauge-row">
|
|
221
|
+
<dt>
|
|
222
|
+
<span className={`dci-gauge-swatch dci-gauge-colorTools`} aria-hidden="true" />
|
|
223
|
+
{t("stats.tools")}
|
|
224
|
+
</dt>
|
|
225
|
+
<dd>~{shortTokens(breakdown?.toolsTokens ?? 0)}</dd>
|
|
226
|
+
</div>
|
|
227
|
+
<div className="dci-gauge-row">
|
|
228
|
+
<dt>
|
|
229
|
+
<span className={`dci-gauge-swatch dci-gauge-colorMessages`} aria-hidden="true" />
|
|
230
|
+
{t("stats.messages")}
|
|
231
|
+
</dt>
|
|
232
|
+
<dd>~{shortTokens(breakdown?.messageTokens ?? 0)}</dd>
|
|
233
|
+
</div>
|
|
234
|
+
</dl>
|
|
235
|
+
</div>
|
|
236
|
+
) : null}
|
|
237
|
+
</span>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** popupSelect 本地过滤:与官方 filterOptions 同语义(label/detail 大小写不敏感子串)。 */
|
|
2
|
+
|
|
3
|
+
import type { SelectOption } from '../host-types';
|
|
4
|
+
|
|
5
|
+
export function filterOptions(options: readonly SelectOption[], search: string): readonly SelectOption[] {
|
|
6
|
+
const q = search.trim().toLowerCase();
|
|
7
|
+
if (q === '') return options;
|
|
8
|
+
return options.filter((option) => {
|
|
9
|
+
const label = typeof option.label === 'string' ? option.label.toLowerCase() : '';
|
|
10
|
+
const detail = typeof option.detail === 'string' ? option.detail.toLowerCase() : '';
|
|
11
|
+
return label.includes(q) || detail.includes(q);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** React 桥:HostObservable(getSnapshot + subscribe)→ useSyncExternalStore。 */
|
|
2
|
+
|
|
3
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
4
|
+
import type { HostObservable } from '../host-types';
|
|
5
|
+
|
|
6
|
+
export function useSnapshot<T>(source: HostObservable<T> | undefined): T | undefined {
|
|
7
|
+
const subscribe = useCallback(
|
|
8
|
+
(fn: () => void) => (source ? source.subscribe(fn) : () => {}),
|
|
9
|
+
[source],
|
|
10
|
+
);
|
|
11
|
+
const getSnapshot = useCallback(() => source?.getSnapshot(), [source]);
|
|
12
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useSnapshotOf<T>(source: HostObservable<T> | undefined, selector: (value: T) => unknown): unknown {
|
|
16
|
+
const value = useSnapshot(source);
|
|
17
|
+
return value === undefined ? undefined : selector(value);
|
|
18
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** 诊断口约定:window.__dshCodinput*(见 docs/idea.md 附录)。 */
|
|
2
|
+
|
|
3
|
+
import type { InputState, InputTriggerController, ComposerKeyboard } from './host-types';
|
|
4
|
+
|
|
5
|
+
interface DiagWindow {
|
|
6
|
+
__dshCodinputDebug: unknown[];
|
|
7
|
+
__dshCodinputErrs: unknown[];
|
|
8
|
+
__dshCodinputSessionId: string | null;
|
|
9
|
+
__dshCodinputTriggers: Record<string, InputTriggerController | undefined>;
|
|
10
|
+
__dshCodinputKeyboard: Record<string, ComposerKeyboard | undefined>;
|
|
11
|
+
__dshCodinputShell: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare global {
|
|
15
|
+
interface Window {
|
|
16
|
+
__dshCodinputDebug?: unknown[];
|
|
17
|
+
__dshCodinputErrs?: unknown[];
|
|
18
|
+
__dshCodinputSessionId?: string | null;
|
|
19
|
+
__dshCodinputTriggers?: Record<string, InputTriggerController | undefined>;
|
|
20
|
+
__dshCodinputKeyboard?: Record<string, ComposerKeyboard | undefined>;
|
|
21
|
+
__dshCodinputShell?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function diagWindow(): DiagWindow {
|
|
26
|
+
const w = window as Window & Partial<DiagWindow>;
|
|
27
|
+
w.__dshCodinputDebug ??= [];
|
|
28
|
+
w.__dshCodinputErrs ??= [];
|
|
29
|
+
w.__dshCodinputTriggers ??= {};
|
|
30
|
+
w.__dshCodinputKeyboard ??= {};
|
|
31
|
+
w.__dshCodinputShell ??= {};
|
|
32
|
+
return w as DiagWindow;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function diagDebug(...parts: unknown[]): void {
|
|
36
|
+
const w = diagWindow();
|
|
37
|
+
w.__dshCodinputDebug.push(parts.length === 1 ? parts[0] : parts);
|
|
38
|
+
if (w.__dshCodinputDebug.length > 400) w.__dshCodinputDebug.splice(0, w.__dshCodinputDebug.length - 400);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function diagError(scope: string, error: unknown): void {
|
|
42
|
+
const w = diagWindow();
|
|
43
|
+
w.__dshCodinputErrs.push({ scope, error: error instanceof Error ? error.stack ?? error.message : String(error) });
|
|
44
|
+
if (w.__dshCodinputErrs.length > 200) w.__dshCodinputErrs.splice(0, w.__dshCodinputErrs.length - 200);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function diagSession(id: string | null): void {
|
|
48
|
+
diagWindow().__dshCodinputSessionId = id;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Safe: 对外暴露的只读探测面。 */
|
|
52
|
+
export function runGuarded(scope: string, fn: () => void): void {
|
|
53
|
+
try {
|
|
54
|
+
fn();
|
|
55
|
+
} catch (error) {
|
|
56
|
+
diagError(scope, error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主服务面的本地结构类型。
|
|
3
|
+
*
|
|
4
|
+
* 插件对宿主零运行时依赖(模块表只认平台种子词与包工厂),这里的类型按
|
|
5
|
+
* @deepseek-ai 0.1.5-rc.1 公开契约(.d.ts)抄录我们实际消费的最小切面;
|
|
6
|
+
* 运行时全部为鸭子类型,形状漂移只影响功能降级,不会崩插件。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ---------- cordis ----------
|
|
10
|
+
|
|
11
|
+
export interface HostObservable<T> {
|
|
12
|
+
getSnapshot(): T;
|
|
13
|
+
subscribe(fn: () => void): () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ClientContext {
|
|
17
|
+
/** 属性读取要求 fiber inject 声明;只通过 ctx.inject(...) 的 scope 消费。 */
|
|
18
|
+
inject(services: readonly string[], fn: (scope: any) => void): void;
|
|
19
|
+
effect(fn: () => (() => void) | void, label?: string): void;
|
|
20
|
+
plugin(impl: unknown, config?: unknown): void;
|
|
21
|
+
get(name: string): unknown;
|
|
22
|
+
/** 客户端模块 inject 声明后可读(['slots', 'conversation'])。 */
|
|
23
|
+
slots: SlotsService;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---------- slots ----------
|
|
27
|
+
|
|
28
|
+
export interface SlotRegisterOptions {
|
|
29
|
+
name: string;
|
|
30
|
+
key?: string;
|
|
31
|
+
id?: string;
|
|
32
|
+
order?: number;
|
|
33
|
+
label?: string | (() => string);
|
|
34
|
+
/** single/keyed/list:遮蔽秩,升序、最低者渲染。 */
|
|
35
|
+
priority?: number;
|
|
36
|
+
children?: Record<string, unknown>;
|
|
37
|
+
inject?: (...args: unknown[]) => Record<string, unknown>;
|
|
38
|
+
store?: unknown;
|
|
39
|
+
locale?: string;
|
|
40
|
+
registrant?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SlotsService {
|
|
44
|
+
register(options: SlotRegisterOptions, component: unknown): () => void;
|
|
45
|
+
/** 锚定槽位:当该槽被声明时运行工厂;工厂 yield 掉的可清理返回值统一登记生命周期。 */
|
|
46
|
+
inject(slotName: string, factory: () => Generator<unknown> | (() => unknown)): void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------- input machine ----------
|
|
50
|
+
|
|
51
|
+
export type DraftAttachmentId = string;
|
|
52
|
+
export type InputPhase = 'plain' | 'adjudicating' | 'claimed' | 'submitting';
|
|
53
|
+
|
|
54
|
+
export interface InputState {
|
|
55
|
+
readonly draft: string;
|
|
56
|
+
readonly attachmentIds: readonly DraftAttachmentId[];
|
|
57
|
+
readonly draftRev: number;
|
|
58
|
+
readonly phase: InputPhase;
|
|
59
|
+
readonly claim?: { readonly token: string; readonly hint?: string; readonly attachments?: boolean };
|
|
60
|
+
readonly queue: readonly unknown[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface InputActions {
|
|
64
|
+
setDraft(text: string): void;
|
|
65
|
+
addAttachments(ids: readonly DraftAttachmentId[]): boolean;
|
|
66
|
+
removeAttachment(id: DraftAttachmentId): void;
|
|
67
|
+
pruneAttachments(ids: readonly DraftAttachmentId[]): void;
|
|
68
|
+
submit(): void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface EditSelection {
|
|
72
|
+
readonly start: number;
|
|
73
|
+
readonly end: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ComposerKeyboard {
|
|
77
|
+
readonly snapshot: InputState;
|
|
78
|
+
submit(mode: string): void;
|
|
79
|
+
steerQueue(): void;
|
|
80
|
+
paste(text: string): void;
|
|
81
|
+
caretSpan(): EditSelection;
|
|
82
|
+
arbitrate(key: string, composing: boolean): 'consumed' | 'pick-highlighted' | 'pass';
|
|
83
|
+
space(): boolean;
|
|
84
|
+
dismissPopup(): void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------- slash trigger pipeline ----------
|
|
88
|
+
|
|
89
|
+
export interface TokenSpan {
|
|
90
|
+
readonly start: number;
|
|
91
|
+
readonly end: number;
|
|
92
|
+
readonly draftRev: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface TriggerHit {
|
|
96
|
+
readonly trigger: '/' | '@';
|
|
97
|
+
readonly query: string;
|
|
98
|
+
readonly quoted: boolean;
|
|
99
|
+
readonly position: 'leading' | 'inline';
|
|
100
|
+
readonly span: TokenSpan;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface InputTriggerCandidate {
|
|
104
|
+
readonly name: string;
|
|
105
|
+
/** 官方本地化名(内置命令带,如「压缩/目标」);官方主名渲染 label ?? name,label 与 name 不同时另出别名。 */
|
|
106
|
+
readonly label?: string;
|
|
107
|
+
readonly description?: string;
|
|
108
|
+
/** 官方 MenuView 语义:字符串 = 引用类 kind(file/folder/session);组件 = 命令 glyph(渲染 <icon size={16}/>)。 */
|
|
109
|
+
readonly icon?: string | React.ComponentType<{ size?: number }>;
|
|
110
|
+
readonly hint?: string;
|
|
111
|
+
readonly section?: string;
|
|
112
|
+
readonly value?: string;
|
|
113
|
+
readonly drill?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface InputTriggerCrumb {
|
|
117
|
+
readonly label: string;
|
|
118
|
+
readonly value: string;
|
|
119
|
+
readonly current?: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface MenuState {
|
|
123
|
+
readonly open: boolean;
|
|
124
|
+
readonly hit: TriggerHit | null;
|
|
125
|
+
readonly generation: number;
|
|
126
|
+
readonly groups: readonly {
|
|
127
|
+
readonly source: string;
|
|
128
|
+
readonly showGroupTitle?: boolean;
|
|
129
|
+
readonly status: 'pending' | 'ready';
|
|
130
|
+
readonly items: readonly InputTriggerCandidate[];
|
|
131
|
+
}[];
|
|
132
|
+
readonly highlight: { readonly source: string; readonly index: number } | null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** ctx.conversation.input(InputHub)逐 id 服务面。 */
|
|
136
|
+
export interface InputTriggerController {
|
|
137
|
+
readonly menu: HostObservable<MenuState>;
|
|
138
|
+
readonly launcher: HostObservable<string | null>;
|
|
139
|
+
readonly headers: HostObservable<ReadonlyMap<string, readonly InputTriggerCrumb[]>>;
|
|
140
|
+
readonly lexicon: HostObservable<ReadonlyMap<string, readonly string[]>>;
|
|
141
|
+
track(draft: string, caret: number, guard: { readonly tier: 'plain' | 'claimed' | 'frozen' }, draftRev: number): void;
|
|
142
|
+
arbitrate(key: string, composing: boolean): 'consumed' | 'pick-highlighted' | 'pass';
|
|
143
|
+
onSpace(): boolean;
|
|
144
|
+
toggleSource(source: string, hit: TriggerHit): void;
|
|
145
|
+
pick(source: string, index: number, action?: 'pick' | 'drill'): void;
|
|
146
|
+
pickCrumb(source: string, index: number): void;
|
|
147
|
+
hover(source: string, index: number): void;
|
|
148
|
+
dismiss(): void;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------- conversation / sessions ----------
|
|
152
|
+
|
|
153
|
+
export interface SessionInput {
|
|
154
|
+
setDraft(text: string): void;
|
|
155
|
+
addAttachments(ids: readonly DraftAttachmentId[]): boolean;
|
|
156
|
+
removeAttachment(id: DraftAttachmentId): void;
|
|
157
|
+
pruneAttachments(ids: readonly DraftAttachmentId[]): void;
|
|
158
|
+
submit(): void;
|
|
159
|
+
notify(level: 'info' | 'error', text: string): void;
|
|
160
|
+
readonly state: HostObservable<InputState>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface ComposerAttachment {
|
|
164
|
+
kind: 'image' | 'file';
|
|
165
|
+
id: DraftAttachmentId;
|
|
166
|
+
file: File;
|
|
167
|
+
previewUrl?: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** ctx.conversation(IConversation)公开面里我们消费的切面。 */
|
|
171
|
+
export interface ConversationFace {
|
|
172
|
+
readonly input: { for(actx: unknown): SessionInput; shell(id: string): SessionInput & Record<string, unknown>; keyboard(id: string): ComposerKeyboard; inputTriggers(id: string): InputTriggerController | undefined };
|
|
173
|
+
createDrafts(sessionId: string, files: readonly File[]): readonly ComposerAttachment[];
|
|
174
|
+
releaseDraftAttachment(id: DraftAttachmentId): void;
|
|
175
|
+
releaseDraftAttachments(attachments: readonly ComposerAttachment[]): void;
|
|
176
|
+
resolveDraftAttachments(ids: readonly DraftAttachmentId[]): readonly ComposerAttachment[];
|
|
177
|
+
retryFileUpload(sessionId: string, id: DraftAttachmentId): void;
|
|
178
|
+
readonly fileUploads: HostObservable<Record<string, unknown>>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface SessionBinding {
|
|
182
|
+
readonly session: {
|
|
183
|
+
command(line: string): Promise<{ ok: boolean; value?: { matched?: boolean } }>;
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** ctx.sessions(ISessions)公开面里我们消费的切面。 */
|
|
188
|
+
export interface SessionsFace {
|
|
189
|
+
binding(id: string): SessionBinding | undefined;
|
|
190
|
+
scope(id: string): unknown;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------- 右侧栏控制器(ctx.sidebarRight / ISidebarRight) ----------
|
|
194
|
+
|
|
195
|
+
/** 一个已打开标签的记录(dockkit TabRecord 我们消费的切面)。 */
|
|
196
|
+
export interface TabRecordLike {
|
|
197
|
+
readonly id: string;
|
|
198
|
+
readonly kind: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 右侧栏公开控制器。侧栏收起后要「唤回侧栏里的 Codinput」(展开 + 激活
|
|
203
|
+
* 该标签)只能经它——`openTabIn` 一次完成展开与聚焦(store 的
|
|
204
|
+
* openContent 内建 planSetExpanded(state, true));`toggleExpanded` 作为
|
|
205
|
+
* 退路(openTabIn 缺失时)。全部按鸭子类型调用。
|
|
206
|
+
*/
|
|
207
|
+
export interface SidebarRightFace {
|
|
208
|
+
isExpanded?(): boolean;
|
|
209
|
+
toggleExpanded?(): void;
|
|
210
|
+
openTab?(kind: string): void;
|
|
211
|
+
openTabIn?(sessionId: string, kind: string): void;
|
|
212
|
+
tabsIn?(sessionId: string): readonly TabRecordLike[];
|
|
213
|
+
focus?(tabId: string): void;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---------- commandUi popupSelect ----------
|
|
217
|
+
|
|
218
|
+
export interface SelectOption {
|
|
219
|
+
readonly value?: unknown;
|
|
220
|
+
readonly label: string;
|
|
221
|
+
readonly detail?: string;
|
|
222
|
+
readonly [k: string]: unknown;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface PopupState {
|
|
226
|
+
readonly open: boolean;
|
|
227
|
+
readonly command: string | null;
|
|
228
|
+
readonly status: 'pending' | 'ready' | 'failed';
|
|
229
|
+
readonly options: readonly SelectOption[];
|
|
230
|
+
readonly search: string;
|
|
231
|
+
readonly active: number;
|
|
232
|
+
readonly submitting: boolean;
|
|
233
|
+
readonly confirming: SelectOption | null;
|
|
234
|
+
readonly acknowledged: boolean;
|
|
235
|
+
readonly error: string | null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface PopupSelectController {
|
|
239
|
+
readonly state: HostObservable<PopupState>;
|
|
240
|
+
retry(): void;
|
|
241
|
+
setSearch(search: string): void;
|
|
242
|
+
move(dir: 1 | -1): void;
|
|
243
|
+
highlight(index: number): void;
|
|
244
|
+
select(index: number): Promise<void>;
|
|
245
|
+
acknowledge(acknowledged: boolean): void;
|
|
246
|
+
cancelConfirmation(): void;
|
|
247
|
+
confirm(): Promise<void>;
|
|
248
|
+
dismiss(opts?: { readonly focusComposer?: boolean }): void;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface CommandUiFace {
|
|
252
|
+
popupFor(actx: unknown): PopupSelectController;
|
|
253
|
+
bindComposerFocus(id: string, focus: () => void): () => void;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------- model directory(web 外壳层服务,形状经防御归一化) ----------
|
|
257
|
+
|
|
258
|
+
export interface ModelReasoningEffort { readonly id: string; readonly name: string; readonly description?: string }
|
|
259
|
+
export interface ModelCatalogModel {
|
|
260
|
+
readonly id: string;
|
|
261
|
+
readonly name: string;
|
|
262
|
+
readonly description?: string;
|
|
263
|
+
readonly reasoning?: { readonly efforts: readonly ModelReasoningEffort[]; readonly defaultEffort?: string };
|
|
264
|
+
}
|
|
265
|
+
export interface ModelProviderGroup { readonly id: string; readonly name: string; readonly models: readonly ModelCatalogModel[] }
|
|
266
|
+
export interface ModelCatalog {
|
|
267
|
+
readonly default?: { readonly provider: string; readonly model: string; readonly reasoningEffort?: string };
|
|
268
|
+
readonly groups: readonly ModelProviderGroup[];
|
|
269
|
+
}
|
|
270
|
+
export interface ModelSelection { readonly provider: string; readonly model: string; readonly reasoningEffort?: string }
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 共享模型目录:与官方 /model 弹层同一状态源。服务形状不在本地包内
|
|
274
|
+
* (属 web 外壳层),读取按 ObservableSnapshot 兼容、写入按鸭子序探测。
|
|
275
|
+
*/
|
|
276
|
+
export interface ModelDirectoriesFace {
|
|
277
|
+
getSnapshot?(): unknown;
|
|
278
|
+
subscribe?(fn: () => void): () => void;
|
|
279
|
+
[key: string]: unknown;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---------- standard kit / settings / sidebar ----------
|
|
283
|
+
|
|
284
|
+
export type SelectorHook<T> = <S>(sel: (s: T) => S) => S;
|
|
285
|
+
|
|
286
|
+
/** 草稿镜像宿主内建投影:{ lastUsed, next },next 为下一次请求将用的选择。 */
|
|
287
|
+
export interface ModelSelectionProjection {
|
|
288
|
+
readonly lastUsed: ModelSelection | null;
|
|
289
|
+
readonly next: ModelSelection | null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export interface UseProjection {
|
|
293
|
+
(key: string): unknown;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** session-maybe 槽位标准 props(我们消费的切面)。 */
|
|
297
|
+
export interface SessionMaybeStandard {
|
|
298
|
+
useSession: <S>(sel: (s: any) => S) => S | undefined;
|
|
299
|
+
sessionId: string | undefined;
|
|
300
|
+
useInput: <S>(sel: (s: InputState) => S) => S | undefined;
|
|
301
|
+
inputActions: InputActions | undefined;
|
|
302
|
+
useConversation?: <S>(sel: (s: any) => S) => S | undefined;
|
|
303
|
+
useProjection: UseProjection;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** session 槽位标准 props(侧栏 body)。 */
|
|
307
|
+
export interface SessionStandard {
|
|
308
|
+
useSession: <S>(sel: (s: any) => S) => S;
|
|
309
|
+
sessionId: string;
|
|
310
|
+
useInput: <S>(sel: (s: InputState) => S) => S;
|
|
311
|
+
inputActions: InputActions;
|
|
312
|
+
useProjection: UseProjection;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface SettingsSectionProps {
|
|
316
|
+
close: () => void;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* sidebar.right.pane.tab 注入的 tabInfo hook(官方 useTabInfo)消费切面。
|
|
321
|
+
* `tab.visible` 是官方的标签可见判定:浮动恒可见;停靠需侧栏展开且为
|
|
322
|
+
* 激活标签(title 槽位才含未激活标签)。收起的侧栏是 CSS 隐藏、body
|
|
323
|
+
* 仍挂载,因此「主输入是否让位」必须读 visible 而非 body 挂载。
|
|
324
|
+
*/
|
|
325
|
+
export interface SidebarTabInfo {
|
|
326
|
+
readonly sidebar: { readonly expanded: boolean; readonly fullscreen: boolean };
|
|
327
|
+
readonly tab: { readonly visible: boolean };
|
|
328
|
+
}
|