dsh-codinput 0.1.1 → 0.2.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 +31 -0
- package/README.en.md +30 -26
- package/README.md +32 -28
- package/TESTING.md +10 -2
- package/assets/icon-white.svg +11 -0
- package/assets/icon.png +0 -0
- package/assets/icon.svg +11 -1
- 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/lib/client.js +57 -37
- package/package.json +1 -1
- package/src/client/attachments.ts +34 -0
- package/src/client/command.ts +52 -12
- package/src/client/components/AttachmentRail.tsx +20 -1
- package/src/client/components/CodinputBar.tsx +13 -35
- package/src/client/components/CodinputSurface.tsx +42 -8
- package/src/client/components/ImageLightbox.tsx +61 -0
- package/src/client/components/StatsRow.tsx +76 -43
- package/src/client/components/Toolbar.tsx +12 -36
- package/src/client/host-types.ts +6 -1
- package/src/client/i18n.ts +16 -2
- package/src/client/icons.tsx +33 -27
- package/src/client/sidebar.tsx +66 -7
- package/src/client/sideinput.ts +33 -7
- package/src/client/stats.ts +156 -53
- package/src/client/styles.ts +27 -7
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
* 点外关闭。 */
|
|
5
5
|
|
|
6
6
|
import { useEffect, useRef, useState } from 'react';
|
|
7
|
-
import { useSessionStats, formatTokens, type SessionStats } from '../stats';
|
|
7
|
+
import { useSessionStats, formatTokens, formatTokensPerSecond, formatCacheHitPercent, formatDuration, type SessionStats } from '../stats';
|
|
8
8
|
import type { UseProjection } from '../host-types';
|
|
9
9
|
import { t } from '../i18n';
|
|
10
10
|
|
|
11
11
|
export interface StatsRowProps {
|
|
12
12
|
useProjection: UseProjection | undefined;
|
|
13
13
|
sessionId: string | undefined;
|
|
14
|
-
/** chat
|
|
15
|
-
|
|
14
|
+
/** chat legacy 节点数组(sessionStats 投影缺席时按官方 deriveStats 派生)。 */
|
|
15
|
+
chatNodes?: unknown;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** 官方仪表盘 glyph(弧 + 指针 + 中点,stroke 1.25)。 */
|
|
@@ -67,17 +67,44 @@ function StatPanel(props: {
|
|
|
67
67
|
);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/** 官方 exactCount:千分位分组 + " tok"。 */
|
|
71
|
+
function exactCount(value: number | undefined): string {
|
|
72
|
+
const digits = String(value ?? 0);
|
|
73
|
+
const groups: string[] = [];
|
|
74
|
+
for (let end = digits.length; end > 0; end -= 3) groups.unshift(digits.slice(Math.max(0, end - 3), end));
|
|
75
|
+
return `${groups.join(',')} tok`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 官方 TimePill:轮/步 + · tok/s(decodeMs>0 才出),面板 = 模型/工具用时、TTFT 均值、TPS。 */
|
|
70
79
|
function Pill(props: { stats: SessionStats; open: boolean; onToggle(): void }): JSX.Element {
|
|
71
80
|
const { stats, open, onToggle } = props;
|
|
72
|
-
const roundsPart =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
const roundsPart = t('stats.turns', { turns: stats.turns ?? 0, steps: stats.steps ?? 0 });
|
|
82
|
+
const tps =
|
|
83
|
+
(stats.decodeMs ?? 0) > 0 ? formatTokensPerSecond((stats.decodeTokens ?? 0) / ((stats.decodeMs ?? 1) / 1000)) : null;
|
|
84
|
+
const hasTimings =
|
|
85
|
+
(stats.llmMs ?? 0) > 0 || (stats.toolMs ?? 0) > 0 || (stats.ttftSteps ?? 0) > 0 || (stats.decodeMs ?? 0) > 0;
|
|
86
|
+
const label = (
|
|
87
|
+
<span className="dci-stat-label">
|
|
88
|
+
{roundsPart}
|
|
89
|
+
{tps !== null ? (
|
|
90
|
+
<>
|
|
91
|
+
<span className="dci-stat-sep" aria-hidden="true">·</span>
|
|
92
|
+
{t('stats.tps', { tps })}
|
|
93
|
+
</>
|
|
94
|
+
) : null}
|
|
95
|
+
</span>
|
|
96
|
+
);
|
|
97
|
+
// 官方:无任何计时数据时胶囊不可点(无面板可看)。
|
|
98
|
+
if (!hasTimings) {
|
|
99
|
+
return (
|
|
100
|
+
<span className="dci-stat-anchor">
|
|
101
|
+
<span className="dci-stat-pill">
|
|
102
|
+
<GaugeIcon />
|
|
103
|
+
{label}
|
|
104
|
+
</span>
|
|
105
|
+
</span>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
81
108
|
return (
|
|
82
109
|
<span className="dci-stat-anchor">
|
|
83
110
|
<button
|
|
@@ -85,27 +112,26 @@ function Pill(props: { stats: SessionStats; open: boolean; onToggle(): void }):
|
|
|
85
112
|
className="dci-stat-pill"
|
|
86
113
|
aria-haspopup="dialog"
|
|
87
114
|
aria-expanded={open}
|
|
115
|
+
aria-label={tps !== null ? `${roundsPart} · ${t('stats.tps', { tps })}` : roundsPart}
|
|
88
116
|
onClick={onToggle}
|
|
89
117
|
>
|
|
90
118
|
<GaugeIcon />
|
|
91
|
-
|
|
92
|
-
{roundsPart}
|
|
93
|
-
{speedPart !== null ? (
|
|
94
|
-
<>
|
|
95
|
-
<span className="dci-stat-sep" aria-hidden="true">·</span>
|
|
96
|
-
{speedPart}
|
|
97
|
-
</>
|
|
98
|
-
) : null}
|
|
99
|
-
</span>
|
|
119
|
+
{label}
|
|
100
120
|
</button>
|
|
101
121
|
{open ? (
|
|
102
122
|
<StatPanel
|
|
103
123
|
label={t("stats.session")}
|
|
104
124
|
icon={<GaugeIcon />}
|
|
105
125
|
rows={[
|
|
106
|
-
[t('stats.modelTime'),
|
|
107
|
-
[t('stats.
|
|
108
|
-
[t('stats.
|
|
126
|
+
[t('stats.modelTime'), (stats.llmMs ?? 0) > 0 ? formatDuration(stats.llmMs!) : '—'],
|
|
127
|
+
[t('stats.toolTime'), (stats.toolMs ?? 0) > 0 ? formatDuration(stats.toolMs!) : '—'],
|
|
128
|
+
[t('stats.ttft'), (stats.ttftSteps ?? 0) > 0 ? formatDuration(stats.ttftMs! / stats.ttftSteps!) : '—'],
|
|
129
|
+
[
|
|
130
|
+
t('stats.tpsLabel'),
|
|
131
|
+
(stats.decodeMs ?? 0) > 0
|
|
132
|
+
? t('stats.tps', { tps: formatTokensPerSecond((stats.decodeTokens ?? 0) / ((stats.decodeMs ?? 1) / 1000)) })
|
|
133
|
+
: '—',
|
|
134
|
+
],
|
|
109
135
|
]}
|
|
110
136
|
/>
|
|
111
137
|
) : null}
|
|
@@ -113,10 +139,17 @@ function Pill(props: { stats: SessionStats; open: boolean; onToggle(): void }):
|
|
|
113
139
|
);
|
|
114
140
|
}
|
|
115
141
|
|
|
142
|
+
/** 官方 UsagePill:总量 · 缓存命中(缓存命中用官方百分比精度规则)。 */
|
|
116
143
|
function TokenPill(props: { stats: SessionStats; open: boolean; onToggle(): void }): JSX.Element | null {
|
|
117
144
|
const { stats, open, onToggle } = props;
|
|
118
145
|
if (stats.totalTokens === undefined) return null;
|
|
119
|
-
const
|
|
146
|
+
const total = stats.totalTokens;
|
|
147
|
+
const cacheHitText =
|
|
148
|
+
stats.cacheHitText ??
|
|
149
|
+
formatCacheHitPercent(
|
|
150
|
+
stats.cacheReadTokens ?? 0,
|
|
151
|
+
(stats.cacheReadTokens ?? 0) + (stats.cacheWriteTokens ?? 0) + (stats.uncachedInputTokens ?? 0),
|
|
152
|
+
);
|
|
120
153
|
return (
|
|
121
154
|
<span className="dci-stat-anchor">
|
|
122
155
|
<button
|
|
@@ -124,15 +157,20 @@ function TokenPill(props: { stats: SessionStats; open: boolean; onToggle(): void
|
|
|
124
157
|
className="dci-stat-pill"
|
|
125
158
|
aria-haspopup="dialog"
|
|
126
159
|
aria-expanded={open}
|
|
160
|
+
aria-label={
|
|
161
|
+
cacheHitText !== null
|
|
162
|
+
? `${formatTokens(total)} · ${t('stats.cacheHit', { percent: cacheHitText })}`
|
|
163
|
+
: formatTokens(total)
|
|
164
|
+
}
|
|
127
165
|
onClick={onToggle}
|
|
128
166
|
>
|
|
129
167
|
<DatabaseIcon />
|
|
130
168
|
<span className="dci-stat-label">
|
|
131
|
-
{formatTokens(
|
|
132
|
-
{
|
|
169
|
+
{formatTokens(total)}
|
|
170
|
+
{cacheHitText !== null ? (
|
|
133
171
|
<>
|
|
134
172
|
<span className="dci-stat-sep" aria-hidden="true">·</span>
|
|
135
|
-
|
|
173
|
+
{t('stats.cacheHit', { percent: cacheHitText })}
|
|
136
174
|
</>
|
|
137
175
|
) : null}
|
|
138
176
|
</span>
|
|
@@ -141,12 +179,15 @@ function TokenPill(props: { stats: SessionStats; open: boolean; onToggle(): void
|
|
|
141
179
|
<StatPanel
|
|
142
180
|
label={t("stats.usage")}
|
|
143
181
|
icon={<DatabaseIcon />}
|
|
144
|
-
titleValue={
|
|
182
|
+
titleValue={exactCount(total)}
|
|
145
183
|
rows={[
|
|
146
|
-
[t('stats.cacheHitLabel'),
|
|
147
|
-
[t('stats.uncached'),
|
|
148
|
-
[t('stats.cacheRead'),
|
|
149
|
-
|
|
184
|
+
[t('stats.cacheHitLabel'), cacheHitText !== null ? `${cacheHitText}%` : '—'],
|
|
185
|
+
[t('stats.uncached'), exactCount(stats.uncachedInputTokens)],
|
|
186
|
+
[t('stats.cacheRead'), exactCount(stats.cacheReadTokens)],
|
|
187
|
+
...(stats.cacheWriteTokens
|
|
188
|
+
? [[t('stats.cacheWrite'), exactCount(stats.cacheWriteTokens)] as [string, string]]
|
|
189
|
+
: []),
|
|
190
|
+
[t('stats.output'), exactCount(stats.outputTokens)],
|
|
150
191
|
]}
|
|
151
192
|
/>
|
|
152
193
|
) : null}
|
|
@@ -155,18 +196,10 @@ function TokenPill(props: { stats: SessionStats; open: boolean; onToggle(): void
|
|
|
155
196
|
}
|
|
156
197
|
|
|
157
198
|
export function StatsRow(props: StatsRowProps): JSX.Element | null {
|
|
158
|
-
const { useProjection, sessionId,
|
|
199
|
+
const { useProjection, sessionId, chatNodes } = props;
|
|
159
200
|
const [open, setOpen] = useState<'rounds' | 'tokens' | null>(null);
|
|
160
201
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
161
|
-
const
|
|
162
|
-
// chat 树计时优先(历史会话也有值),采样值兜底。
|
|
163
|
-
const stats = sampled
|
|
164
|
-
? {
|
|
165
|
-
...sampled,
|
|
166
|
-
...(chatTimings?.modelMs !== undefined ? { modelMs: chatTimings.modelMs } : {}),
|
|
167
|
-
...(chatTimings?.ttftMs !== undefined ? { ttftMs: chatTimings.ttftMs } : {}),
|
|
168
|
-
}
|
|
169
|
-
: sampled;
|
|
202
|
+
const stats = useSessionStats(useProjection, sessionId, chatNodes);
|
|
170
203
|
|
|
171
204
|
// 点外面关闭(面板内/胶囊上不关)。
|
|
172
205
|
useEffect(() => {
|
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 工具行:结构与样式逐条对齐官方 InputBar 工具行——
|
|
3
|
-
* 左侧 .dci-tools:指令(plus
|
|
3
|
+
* 左侧 .dci-tools:指令(plus)、权限(shield 触发器);
|
|
4
4
|
* 右侧 .dci-trailing:模型与思考强度(触发器)、发送(.primary 圆钮)。
|
|
5
5
|
* 按钮沿用官方 keepFocus 语义(mousedown 不夺走编辑器焦点)。
|
|
6
|
+
* 附件无独立按钮(官方已并入 slash「添加 · 文件」),粘贴/拖拽路径保留。
|
|
6
7
|
* 0.1.7-rc.2 起官方上下文小圈不再在工具行(挪进卡片下方 dock 行,见 StatsRow)。
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
import { useRef } from 'react';
|
|
10
10
|
import type { DraftAttachmentId, InputActions, InputState, InputTriggerController, UseProjection } from '../host-types';
|
|
11
11
|
import { openCommandMenu } from '../triggers';
|
|
12
|
-
import { stash } from '../services';
|
|
13
|
-
import { addFilesToDraft } from '../attachments';
|
|
14
12
|
import { t, useT } from '../i18n';
|
|
15
|
-
import {
|
|
13
|
+
import { IconPlusOfficial, IconSendOfficial, IconStopOfficial } from '../icons';
|
|
16
14
|
import { ModelMenu } from './ModelMenu';
|
|
17
15
|
import { PermissionMenu } from './PermissionMenu';
|
|
18
|
-
import { useSnapshot } from './useSnapshot';
|
|
19
16
|
|
|
20
17
|
export interface ToolbarProps {
|
|
21
18
|
sessionId: string | undefined;
|
|
@@ -31,6 +28,10 @@ export interface ToolbarProps {
|
|
|
31
28
|
/** 主发送手势(与编辑器发送键同一策略)。 */
|
|
32
29
|
onSend(): void;
|
|
33
30
|
sendDisabled?: boolean;
|
|
31
|
+
/** 会话运行中且草稿空(官方 primaryStops):主按钮变停止。 */
|
|
32
|
+
primaryStops?: boolean;
|
|
33
|
+
/** 停止生成(官方 stop:conversation.cancel)。 */
|
|
34
|
+
onStop?(): void;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/** 官方 keepFocus:mousedown 阻止默认,避免编辑器失焦。 */
|
|
@@ -41,7 +42,6 @@ function keepFocus(event: React.MouseEvent): void {
|
|
|
41
42
|
export function Toolbar(props: ToolbarProps): JSX.Element {
|
|
42
43
|
const { sessionId, state, actions, triggers, caretRef, useProjection, disabled, onNotify } = props;
|
|
43
44
|
useT();
|
|
44
|
-
const fileRef = useRef<HTMLInputElement | null>(null);
|
|
45
45
|
|
|
46
46
|
const attachments: readonly DraftAttachmentId[] = state?.attachmentIds ?? [];
|
|
47
47
|
|
|
@@ -50,10 +50,6 @@ export function Toolbar(props: ToolbarProps): JSX.Element {
|
|
|
50
50
|
openCommandMenu(triggers, undefined, caretRef.current ?? 0);
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
const onAttach = (files: FileList | null): void => {
|
|
54
|
-
addFilesToDraft(sessionId, actions, files, onNotify);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
53
|
return (
|
|
58
54
|
<div className="dci-row">
|
|
59
55
|
<div className="dci-tools">
|
|
@@ -69,26 +65,6 @@ export function Toolbar(props: ToolbarProps): JSX.Element {
|
|
|
69
65
|
>
|
|
70
66
|
<IconPlusOfficial size={14} />
|
|
71
67
|
</button>
|
|
72
|
-
<button
|
|
73
|
-
type="button"
|
|
74
|
-
className="dci-add"
|
|
75
|
-
aria-label={t("tool.attach")}
|
|
76
|
-
title={t("tool.attach")}
|
|
77
|
-
onMouseDown={keepFocus}
|
|
78
|
-
onClick={() => fileRef.current?.click()}
|
|
79
|
-
>
|
|
80
|
-
<IconPaperclipOfficial size={14} />
|
|
81
|
-
</button>
|
|
82
|
-
<input
|
|
83
|
-
ref={fileRef}
|
|
84
|
-
type="file"
|
|
85
|
-
multiple
|
|
86
|
-
hidden
|
|
87
|
-
onChange={(e) => {
|
|
88
|
-
onAttach(e.target.files);
|
|
89
|
-
e.target.value = '';
|
|
90
|
-
}}
|
|
91
|
-
/>
|
|
92
68
|
<PermissionMenu
|
|
93
69
|
sessionId={sessionId}
|
|
94
70
|
actions={actions}
|
|
@@ -106,13 +82,13 @@ export function Toolbar(props: ToolbarProps): JSX.Element {
|
|
|
106
82
|
<button
|
|
107
83
|
type="button"
|
|
108
84
|
className="dci-send"
|
|
109
|
-
aria-label={t(
|
|
110
|
-
title={t(
|
|
111
|
-
disabled={props.sendDisabled}
|
|
85
|
+
aria-label={props.primaryStops ? t('tool.stop') : t('tool.send')}
|
|
86
|
+
title={props.primaryStops ? t('tool.stop') : t('tool.send')}
|
|
87
|
+
disabled={props.primaryStops ? !props.onStop : props.sendDisabled}
|
|
112
88
|
onMouseDown={keepFocus}
|
|
113
|
-
onClick={() => props.onSend()}
|
|
89
|
+
onClick={() => (props.primaryStops ? props.onStop?.() : props.onSend())}
|
|
114
90
|
>
|
|
115
|
-
<IconSendOfficial size={16} />
|
|
91
|
+
{props.primaryStops ? <IconStopOfficial size={16} /> : <IconSendOfficial size={16} />}
|
|
116
92
|
</button>
|
|
117
93
|
</div>
|
|
118
94
|
</div>
|
package/src/client/host-types.ts
CHANGED
|
@@ -324,5 +324,10 @@ export interface SettingsSectionProps {
|
|
|
324
324
|
*/
|
|
325
325
|
export interface SidebarTabInfo {
|
|
326
326
|
readonly sidebar: { readonly expanded: boolean; readonly fullscreen: boolean };
|
|
327
|
-
readonly tab: {
|
|
327
|
+
readonly tab: {
|
|
328
|
+
readonly visible: boolean;
|
|
329
|
+
readonly id?: string;
|
|
330
|
+
/** tab 域动作面(occurrence held):close() 关闭本标签。 */
|
|
331
|
+
readonly actions?: { readonly close?: () => void };
|
|
332
|
+
};
|
|
328
333
|
}
|
package/src/client/i18n.ts
CHANGED
|
@@ -27,7 +27,6 @@ const ZH: Dict = {
|
|
|
27
27
|
'editor.placeholder': '描述你想要构建的内容,/ 调用指令,@ 文件或对话',
|
|
28
28
|
'preview.empty': '暂无可预览内容',
|
|
29
29
|
'tool.command': '指令(/)',
|
|
30
|
-
'tool.attach': '添加附件',
|
|
31
30
|
'tool.send': '发送',
|
|
32
31
|
'tool.model': '选择模型,当前 {name}',
|
|
33
32
|
'tool.modelPlain': '选择模型',
|
|
@@ -47,6 +46,7 @@ const ZH: Dict = {
|
|
|
47
46
|
'stats.turns': '{turns} 轮 {steps} 步',
|
|
48
47
|
'stats.tps': '{tps} tok/s',
|
|
49
48
|
'stats.modelTime': '模型用时',
|
|
49
|
+
'stats.toolTime': '工具调用用时',
|
|
50
50
|
'stats.ttft': '首 token 平均(TTFT)',
|
|
51
51
|
'stats.tpsLabel': '输出速度(TPS)',
|
|
52
52
|
'stats.totalTokens': '{tokens} tok',
|
|
@@ -110,6 +110,13 @@ const ZH: Dict = {
|
|
|
110
110
|
'ball.title': '展开 Codinput',
|
|
111
111
|
'side.notice': '本会话的输入面已在另一个分栏中打开(输入入口唯一)。',
|
|
112
112
|
'side.guideDesc': '代码编辑器风格的输入面板;开启期间主输入框由本面板接管(共用同一台输入机)',
|
|
113
|
+
'side.disabled': 'Codinput 接管已关闭:主输入在原输入位(官方输入框)。输入 /codinput 或到 设置 → Codinput 重新开启。',
|
|
114
|
+
'tool.attach': '文件',
|
|
115
|
+
'tool.stop': '停止',
|
|
116
|
+
'image.preview': '原图预览',
|
|
117
|
+
'image.closePreview': '关闭原图预览',
|
|
118
|
+
'image.openOriginal': '查看原图',
|
|
119
|
+
'image.openOriginalLabel': '{name},点击查看原图',
|
|
113
120
|
'cmd.enter': '进入 Codinput 输入接管',
|
|
114
121
|
'cmd.exit': '退出 Codinput,恢复官方输入框',
|
|
115
122
|
};
|
|
@@ -124,7 +131,6 @@ const EN: Dict = {
|
|
|
124
131
|
'editor.placeholder': 'Describe what you want to build — / for commands, @ for files or chats',
|
|
125
132
|
'preview.empty': 'Nothing to preview',
|
|
126
133
|
'tool.command': 'Commands (/)',
|
|
127
|
-
'tool.attach': 'Add attachment',
|
|
128
134
|
'tool.send': 'Send',
|
|
129
135
|
'tool.model': 'Select model, current {name}',
|
|
130
136
|
'tool.modelPlain': 'Select model',
|
|
@@ -144,6 +150,7 @@ const EN: Dict = {
|
|
|
144
150
|
'stats.turns': '{turns} turns · {steps} steps',
|
|
145
151
|
'stats.tps': '{tps} tok/s',
|
|
146
152
|
'stats.modelTime': 'Model time',
|
|
153
|
+
'stats.toolTime': 'Tool call time',
|
|
147
154
|
'stats.ttft': 'Avg first token (TTFT)',
|
|
148
155
|
'stats.tpsLabel': 'Output speed (TPS)',
|
|
149
156
|
'stats.totalTokens': '{tokens} tok',
|
|
@@ -207,6 +214,13 @@ const EN: Dict = {
|
|
|
207
214
|
'ball.title': 'Open Codinput',
|
|
208
215
|
'side.notice': "This session's input panel is already open in another split pane (only one input surface exists).",
|
|
209
216
|
'side.guideDesc': 'Code-editor style input panel. While enabled it takes over the composer (sharing the same input machine).',
|
|
217
|
+
'side.disabled': 'Codinput takeover is off: the composer is back at its original place (official input). Type /codinput or open Settings → Codinput to re-enable.',
|
|
218
|
+
'tool.attach': 'File',
|
|
219
|
+
'tool.stop': 'Stop',
|
|
220
|
+
'image.preview': 'Original image preview',
|
|
221
|
+
'image.closePreview': 'Close original image preview',
|
|
222
|
+
'image.openOriginal': 'View original',
|
|
223
|
+
'image.openOriginalLabel': '{name}, click to view the original',
|
|
210
224
|
'cmd.enter': 'Enable the Codinput composer takeover',
|
|
211
225
|
'cmd.exit': 'Disable Codinput and restore the official composer',
|
|
212
226
|
};
|
package/src/client/icons.tsx
CHANGED
|
@@ -27,18 +27,24 @@ function svg(paths: JSX.Element, size: number | undefined, className: string | u
|
|
|
27
27
|
);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* 品牌图形:DSH-Codinput 机型标(代码括号 + 航迹斜杠 + 纸飞机)。
|
|
32
|
+
* 大尺寸原稿见 assets/icon.svg / icon-white.svg;这里是 24 栅格的
|
|
33
|
+
* 小尺寸简化——发卡弯并线、钩脚并入单笔括号,斜杠上收让出机鼻。
|
|
34
|
+
*/
|
|
35
|
+
const BRAND_MARK = (
|
|
36
|
+
<>
|
|
37
|
+
<path d="M6.8 5.8 2.9 11l4 4.4" />
|
|
38
|
+
<path d="M17.2 5.8 21.1 11l-3.9 4.4" />
|
|
39
|
+
<path d="M10.7 9.5 12.5 3.9" />
|
|
40
|
+
<path d="M16.6 9.4 6.9 14l2.9 1.3.1 2.5q.1.8.7.3l1-2.3 2.2 1.4Z" />
|
|
41
|
+
<path d="M9.8 15.3 16.6 9.4" />
|
|
42
|
+
</>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
/** 品牌:DSH-Codinput 机型标(悬浮小球 / 侧栏唤回球)。 */
|
|
31
46
|
export function IconBrand({ size, className }: IconProps): JSX.Element {
|
|
32
|
-
return svg(
|
|
33
|
-
<>
|
|
34
|
-
<path d="M4 12.15V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2h-3.35" />
|
|
35
|
-
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
|
|
36
|
-
<path d="m5 16-3 3 3 3" />
|
|
37
|
-
<path d="m9 22 3-3-3-3" />
|
|
38
|
-
</>,
|
|
39
|
-
size,
|
|
40
|
-
className,
|
|
41
|
-
);
|
|
47
|
+
return svg(BRAND_MARK, size, className);
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
export function IconEdit({ size, className }: IconProps): JSX.Element {
|
|
@@ -167,7 +173,7 @@ export function IconPlusOfficial({ size }: { size?: number }): JSX.Element {
|
|
|
167
173
|
);
|
|
168
174
|
}
|
|
169
175
|
|
|
170
|
-
/** 官方 IconPaperclipOutline16
|
|
176
|
+
/** 官方 IconPaperclipOutline16(slash 添加·文件 补位条目的 glyph;官方为 fill 路径自带描边轮廓)。 */
|
|
171
177
|
export function IconPaperclipOfficial({ size }: { size?: number }): JSX.Element {
|
|
172
178
|
return (
|
|
173
179
|
<svg width={size ?? 14} height={size ?? 14} viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
@@ -272,25 +278,25 @@ export function IconChevronOfficial({ size }: { size?: number }): JSX.Element {
|
|
|
272
278
|
}
|
|
273
279
|
|
|
274
280
|
/**
|
|
275
|
-
* /codinput 命令图标(宿主命令菜单 glyph
|
|
276
|
-
* currentColor,与 primitives Icon*Outline16
|
|
281
|
+
* /codinput 命令图标(宿主命令菜单 glyph:品牌机型标,stroke
|
|
282
|
+
* currentColor,与 primitives Icon*Outline16 同尺度——接受 size,命令
|
|
277
283
|
* 菜单里按官方语义渲染为 <icon size={16}/>)。
|
|
278
284
|
*/
|
|
279
285
|
export function IconCodinputCommand16({ size = 16, className }: { size?: number; className?: string }): JSX.Element {
|
|
280
286
|
return (
|
|
281
|
-
<svg
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
287
|
+
<svg
|
|
288
|
+
width={size}
|
|
289
|
+
height={size}
|
|
290
|
+
viewBox="0 0 24 24"
|
|
291
|
+
fill="none"
|
|
292
|
+
stroke="currentColor"
|
|
293
|
+
strokeWidth={2}
|
|
294
|
+
strokeLinecap="round"
|
|
295
|
+
strokeLinejoin="round"
|
|
296
|
+
className={className}
|
|
297
|
+
aria-hidden="true"
|
|
298
|
+
>
|
|
299
|
+
{BRAND_MARK}
|
|
294
300
|
</svg>
|
|
295
301
|
);
|
|
296
302
|
}
|
package/src/client/sidebar.tsx
CHANGED
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 唯一输入组件约定:body **挂载**期间经 sideinput 总线声明承载本会话
|
|
8
8
|
* 输入(形态 = side),主输入遮蔽条目随之隐藏——收起侧栏不让位(可见性
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* 只用于判断「需不需要右上角小球唤出」)。声明以 prefs.enabled 为前提:
|
|
10
|
+
* 侧边模式下 /codinput 关闭接管即撤销声明、主输入回到官方输入框,标签
|
|
11
|
+
* 内只出提示;重新开启自动恢复。标签关闭(body 卸载)后撤销声明,主输入
|
|
12
|
+
* 原样恢复。
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
import { useEffect, useRef, useState } from 'react';
|
|
14
16
|
import type { SessionStandard, SidebarTabInfo, SlotRegisterOptions } from './host-types';
|
|
15
|
-
import { claimSidebarInput, useSidebarCarryOwner, type SidebarCarry } from './sideinput';
|
|
17
|
+
import { claimSidebarInput, subscribeSidebarTabClose, useSidebarCarryOwner, type SidebarCarry } from './sideinput';
|
|
18
|
+
import { loadPrefs, savePrefs, subscribePrefs } from './prefs';
|
|
19
|
+
import { diagError, diagWindow } from './diag';
|
|
16
20
|
import { CodinputSurface } from './components/CodinputSurface';
|
|
17
21
|
import { t, useT } from './i18n';
|
|
18
22
|
|
|
@@ -26,6 +30,10 @@ export function sidebarTabDefinition(): Record<string, unknown> {
|
|
|
26
30
|
kind: SIDEBAR_TAB_KIND,
|
|
27
31
|
priority: 'extension',
|
|
28
32
|
title: () => 'Codinput',
|
|
33
|
+
// 非激活也保持 body 挂载(官方浏览器标签同款开关):承载声明挂在 body
|
|
34
|
+
// 生命周期上,若切到同栏其他标签(如工作区文件)body 被 dockkit 卸载,
|
|
35
|
+
// 声明随之消失、主输入回来——「标签存在即唯一输入入口」就不成立了。
|
|
36
|
+
keepMounted: true,
|
|
29
37
|
guide: [
|
|
30
38
|
{
|
|
31
39
|
order: 0,
|
|
@@ -55,19 +63,62 @@ export function sidebarBodyRegistration(): { options: SlotRegisterOptions; compo
|
|
|
55
63
|
* useTabInfo 内部订阅 dockkit store,展开/激活/浮动变化都会重渲染。
|
|
56
64
|
*/
|
|
57
65
|
export function SidebarCodinputBody(props: SessionStandard & { useTabInfo?: () => SidebarTabInfo }): JSX.Element {
|
|
58
|
-
const { sessionId, useInput, inputActions, useProjection, useTabInfo } = props;
|
|
66
|
+
const { sessionId, useInput, inputActions, useProjection, useSession, useTabInfo } = props;
|
|
59
67
|
const state = useInput((s) => s);
|
|
68
|
+
// 会话运行中:侧栏形态的主按钮同样变停止(官方同款)。
|
|
69
|
+
const running = useSession ? useSession((s: unknown) => (s as { running?: boolean } | undefined)?.running) === true : false;
|
|
60
70
|
useT();
|
|
61
71
|
// 注入 hook 的有无逐渲染稳定(渲染器 useMemo 绑定),条件调用合规。
|
|
62
72
|
const info = useTabInfo ? useTabInfo() : undefined;
|
|
63
73
|
const visible = info?.tab.visible === true;
|
|
64
74
|
const expanded = info?.sidebar.expanded === true;
|
|
65
75
|
|
|
66
|
-
//
|
|
67
|
-
//
|
|
76
|
+
// /codinput 从侧栏退出 → 命令侧广播关闭请求,这里经 tab 域动作面的
|
|
77
|
+
// actions.close() 关掉自己的标签。关闭即卸载 body、撤销承载,主输入按
|
|
78
|
+
// 退出后的 prefs 恢复官方输入框。
|
|
79
|
+
const closeSelf = info?.tab.actions?.close;
|
|
80
|
+
const closeRef = useRef(closeSelf);
|
|
81
|
+
closeRef.current = closeSelf;
|
|
82
|
+
// 诊断:tabId/关闭面是否可得(宿主形状漂移时链路断在哪一目了然)。
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const shell = diagWindow().__dshCodinputShell;
|
|
85
|
+
shell['sidebar.tabId'] = info?.tab.id ?? 'none';
|
|
86
|
+
shell['sidebar.closeFace'] = closeSelf ? 'yes' : 'none';
|
|
87
|
+
}, [info, closeSelf]);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
return subscribeSidebarTabClose((requested) => {
|
|
90
|
+
if (requested !== sessionId || !closeRef.current) return;
|
|
91
|
+
try {
|
|
92
|
+
closeRef.current();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
diagError('sidebar.closeTab', error);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
98
|
+
}, [sessionId]);
|
|
99
|
+
|
|
100
|
+
// 接管开关是承载的前提:侧边模式下输入 /codinput(或设置里关闭)要能
|
|
101
|
+
// 退回官方输入框——body 撤销承载声明(主输入位随之恢复),标签里只出
|
|
102
|
+
// 提示;重新开启后自动恢复侧栏形态。
|
|
103
|
+
const [enabled, setEnabled] = useState(() => loadPrefs().enabled);
|
|
104
|
+
useEffect(() => subscribePrefs(() => setEnabled(loadPrefs().enabled)), []);
|
|
105
|
+
// 打开标签即启用:body 挂载(首次激活/重载后激活/关掉重开)时若接管
|
|
106
|
+
// 未开,直接开启——用户点开这个标签就是要用它。body 在 keepMounted 下
|
|
107
|
+
// 常驻,/codinput 退出后不会重挂,不会把退出顶回去。
|
|
108
|
+
const bootRef = useRef(false);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (bootRef.current) return;
|
|
111
|
+
bootRef.current = true;
|
|
112
|
+
if (!loadPrefs().enabled) savePrefs({ enabled: true });
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
// 承载声明挂在 body 生命周期上(挂载即承载,与可见性无关),但以
|
|
116
|
+
// enabled 为前提;可见性与侧栏展开态随后续渲染实时上报(小球据此
|
|
117
|
+
// 判断「已收起」)。
|
|
68
118
|
const carryRef = useRef<SidebarCarry | null>(null);
|
|
69
119
|
const [token, setToken] = useState(0);
|
|
70
120
|
useEffect(() => {
|
|
121
|
+
if (!enabled) return;
|
|
71
122
|
const carry = claimSidebarInput(sessionId, { visible, expanded });
|
|
72
123
|
carryRef.current = carry;
|
|
73
124
|
setToken(carry.token);
|
|
@@ -76,7 +127,7 @@ export function SidebarCodinputBody(props: SessionStandard & { useTabInfo?: () =
|
|
|
76
127
|
carry.release();
|
|
77
128
|
};
|
|
78
129
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
79
|
-
}, [sessionId]);
|
|
130
|
+
}, [sessionId, enabled]);
|
|
80
131
|
useEffect(() => {
|
|
81
132
|
carryRef.current?.set({ visible, expanded });
|
|
82
133
|
}, [visible, expanded]);
|
|
@@ -84,6 +135,13 @@ export function SidebarCodinputBody(props: SessionStandard & { useTabInfo?: () =
|
|
|
84
135
|
// 同一会话被复制出第二个 Codinput 标签(分栏)时,只有最早声明的 body
|
|
85
136
|
// 渲染编辑面——输入入口唯一,另一个 body 出提示而不是第二个编辑器。
|
|
86
137
|
const owner = useSidebarCarryOwner(sessionId, token);
|
|
138
|
+
if (!enabled) {
|
|
139
|
+
return (
|
|
140
|
+
<div className="dci-side-notice" role="note">
|
|
141
|
+
{t('side.disabled')}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
87
145
|
if (!owner) {
|
|
88
146
|
return (
|
|
89
147
|
<div className="dci-side-notice" role="note">
|
|
@@ -107,6 +165,7 @@ export function SidebarCodinputBody(props: SessionStandard & { useTabInfo?: () =
|
|
|
107
165
|
state={state}
|
|
108
166
|
actions={inputActions}
|
|
109
167
|
useProjection={useProjection}
|
|
168
|
+
running={running}
|
|
110
169
|
side={true}
|
|
111
170
|
/>
|
|
112
171
|
</div>
|