dsh-superinput 0.0.0-stage → 0.12.1
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 +405 -0
- package/README.md +49 -2
- package/client.js +7397 -0
- package/cordis.patch.yml +6 -0
- package/index.js +34 -0
- package/modules/compaction.js +413 -0
- package/modules/cost.js +1914 -0
- package/modules/enhancer.js +726 -0
- package/modules/followup.js +718 -0
- package/modules/hub.client.js +218 -0
- package/modules/hub.js +78 -0
- package/modules/picker.js +133 -0
- package/modules/prompts.client.js +1709 -0
- package/modules/prompts.js +370 -0
- package/modules/voice.js +17 -0
- package/package.json +60 -4
- package/scripts/build-suite.mjs +325 -0
- package/scripts/smoke.mjs +172 -0
- package/server.js +4482 -0
|
@@ -0,0 +1,1709 @@
|
|
|
1
|
+
/* dsh-superinput · prompts 模块客户端:模板库入口 + 面板。 */
|
|
2
|
+
function createPromptsClient(require) {
|
|
3
|
+
const React = require('react')
|
|
4
|
+
const { createPortal } = require('react-dom')
|
|
5
|
+
const h = React.createElement
|
|
6
|
+
|
|
7
|
+
const STYLE_ID = 'dsh-superinput-prompts-style'
|
|
8
|
+
const ENDPOINT = '/api/dsh-superinput/prompts'
|
|
9
|
+
|
|
10
|
+
/* 划选引用:官方 reference chip + Trae 规格。
|
|
11
|
+
* CHIP_MARK 是不可见分隔符(U+2063),作为标签前缀用于 CSS 精确选中
|
|
12
|
+
* 「我们插入的 chip」而不波及官方 @file chip;视觉与文本内容无影响。 */
|
|
13
|
+
const SELECTION_SOURCE = 'superinput-selection'
|
|
14
|
+
const SELECTION_RULE = 'dsh-superinput-selection'
|
|
15
|
+
const CHIP_MARK = '\u2063'
|
|
16
|
+
const CONTEXT_MAX = 8
|
|
17
|
+
const CONTEXT_TEXT_MAX = 4000
|
|
18
|
+
const CONTEXT_COMMENT_MAX = 600
|
|
19
|
+
const STORAGE_KEY = 'dsh-superinput-selection-items'
|
|
20
|
+
|
|
21
|
+
const CSS = `
|
|
22
|
+
.dsh-cp-btn {
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
border: none !important;
|
|
26
|
+
background: transparent !important;
|
|
27
|
+
box-shadow: none !important;
|
|
28
|
+
color: var(--dsw-alias-label-secondary, rgba(232, 238, 250, 0.72)) !important;
|
|
29
|
+
font-size: 12px;
|
|
30
|
+
line-height: 20px;
|
|
31
|
+
padding: 1px 8px;
|
|
32
|
+
border-radius: var(--dsw-radius-sm, 6px);
|
|
33
|
+
cursor: default;
|
|
34
|
+
user-select: none;
|
|
35
|
+
}
|
|
36
|
+
.dsh-cp-btn:hover { background: var(--dsw-alias-interactive-bg-hover) !important; color: var(--dsw-alias-label-primary) !important; }
|
|
37
|
+
.dsh-cp-panel {
|
|
38
|
+
position: fixed;
|
|
39
|
+
z-index: 1100;
|
|
40
|
+
box-sizing: border-box;
|
|
41
|
+
border: 0;
|
|
42
|
+
border-radius: var(--dsw-radius-lg, 12px);
|
|
43
|
+
background: var(--dsw-specific-menu, rgba(67, 69, 74, 0.45));
|
|
44
|
+
backdrop-filter: var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%));
|
|
45
|
+
-webkit-backdrop-filter: var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%));
|
|
46
|
+
width: min(380px, 100vw - 24px);
|
|
47
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.08));
|
|
48
|
+
box-shadow: var(--dsw-elevation-prominent, 0 0 0 0.5px var(--dsw-elevation-stroke-color), 0 3px 8px 0 rgba(0, 0, 0, 0.04), 0 0 20px 0 rgba(0, 0, 0, 0.05));
|
|
49
|
+
padding: 10px;
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
gap: 8px;
|
|
53
|
+
}
|
|
54
|
+
.dsh-cp-head { display: flex; gap: 6px; align-items: center; }
|
|
55
|
+
.dsh-cp-search {
|
|
56
|
+
flex: 1 1 auto; min-width: 0; box-sizing: border-box;
|
|
57
|
+
padding: 6px 10px; font-size: 12px; line-height: 18px;
|
|
58
|
+
color: var(--dsw-alias-label-primary);
|
|
59
|
+
background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.06));
|
|
60
|
+
border: 0.5px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12));
|
|
61
|
+
border-radius: var(--dsw-radius-sm, 6px);
|
|
62
|
+
outline: none;
|
|
63
|
+
}
|
|
64
|
+
.dsh-cp-search:focus { border-color: var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.2)); }
|
|
65
|
+
.dsh-cp-new {
|
|
66
|
+
flex: 0 0 auto; width: 26px; height: 26px;
|
|
67
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
68
|
+
font-size: 16px; line-height: 1;
|
|
69
|
+
color: var(--dsw-alias-label-secondary);
|
|
70
|
+
background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.06));
|
|
71
|
+
border: 0.5px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12));
|
|
72
|
+
border-radius: var(--dsw-radius-sm, 6px);
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
}
|
|
75
|
+
.dsh-cp-new:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
|
|
76
|
+
.dsh-cp-list { display: flex; flex-direction: column; gap: 2px; max-height: 320px; overflow-y: auto; }
|
|
77
|
+
.dsh-cp-group { font-size: 11px; line-height: 18px; color: var(--dsw-alias-label-tertiary); padding: 4px 6px 2px; }
|
|
78
|
+
.dsh-cp-row { display: flex; align-items: flex-start; gap: 6px; padding: 6px; border-radius: var(--dsw-radius-sm, 6px); }
|
|
79
|
+
.dsh-cp-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
80
|
+
.dsh-cp-row-main { flex: 1 1 auto; min-width: 0; cursor: pointer; display: flex; flex-direction: column; gap: 2px; }
|
|
81
|
+
.dsh-cp-title { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
82
|
+
.dsh-cp-uses { color: var(--dsw-alias-label-tertiary); font-size: 10px; margin-left: 4px; }
|
|
83
|
+
.dsh-cp-preview { font-size: 11px; line-height: 16px; color: var(--dsw-alias-label-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
84
|
+
.dsh-cp-vars { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
|
|
85
|
+
.dsh-cp-var { font-size: 10px; line-height: 14px; padding: 0 4px; border-radius: 3px; color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.08)); }
|
|
86
|
+
.dsh-cp-row-actions { flex: 0 0 auto; display: flex; gap: 2px; }
|
|
87
|
+
.dsh-cp-mini {
|
|
88
|
+
width: 20px; height: 20px; padding: 0;
|
|
89
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
90
|
+
font-size: 11px; line-height: 1;
|
|
91
|
+
color: var(--dsw-alias-label-tertiary);
|
|
92
|
+
background: transparent; border: none; border-radius: 4px; cursor: pointer;
|
|
93
|
+
}
|
|
94
|
+
.dsh-cp-mini:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.08)); }
|
|
95
|
+
.dsh-cp-mini.danger:hover { color: #f87171; }
|
|
96
|
+
.dsh-cp-mini.active { color: #3a83f7; }
|
|
97
|
+
.dsh-cp-empty { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); padding: 12px 6px; text-align: center; }
|
|
98
|
+
.dsh-cp-editor { display: flex; flex-direction: column; gap: 6px; }
|
|
99
|
+
.dsh-cp-input, .dsh-cp-textarea {
|
|
100
|
+
box-sizing: border-box; width: 100%;
|
|
101
|
+
padding: 6px 10px; font-size: 12px; line-height: 18px;
|
|
102
|
+
color: var(--dsw-alias-label-primary);
|
|
103
|
+
background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.06));
|
|
104
|
+
border: 0.5px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12));
|
|
105
|
+
border-radius: var(--dsw-radius-sm, 6px);
|
|
106
|
+
outline: none; font-family: inherit; resize: vertical;
|
|
107
|
+
}
|
|
108
|
+
.dsh-cp-textarea { min-height: 120px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
109
|
+
.dsh-cp-actions { display: flex; justify-content: flex-end; gap: 6px; }
|
|
110
|
+
.dsh-cp-action {
|
|
111
|
+
padding: 4px 12px; font-size: 12px; line-height: 18px;
|
|
112
|
+
color: var(--dsw-alias-label-secondary);
|
|
113
|
+
background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.06));
|
|
114
|
+
border: 0.5px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12));
|
|
115
|
+
border-radius: var(--dsw-radius-sm, 6px);
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
}
|
|
118
|
+
.dsh-cp-action:hover { color: var(--dsw-alias-label-primary); }
|
|
119
|
+
.dsh-cp-action.primary { color: #fff; background: #3a83f7; border-color: transparent; }
|
|
120
|
+
.dsh-cp-action.primary:hover { background: #4f8ef9; }
|
|
121
|
+
/* 四选项横条:完全官方样式——令牌与几何照抄官方 QuoteSelection 按钮
|
|
122
|
+
(.U4oBDW_action:floating-fill 底 / 999px 胶囊 / panel 阴影 / xs-13 字), */
|
|
123
|
+
.dsh-cp-selmenu { display: flex; flex-direction: row; align-items: center; gap: 2px; padding: 3px; border-radius: 999px;
|
|
124
|
+
background: var(--dsw-alias-button-floating-fill, rgba(116, 126, 148, .24));
|
|
125
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l3, rgba(255, 255, 255, .1));
|
|
126
|
+
box-shadow: var(--dsw-elevation-panel, 0 0 0 .5px var(--dsw-elevation-stroke-color), 0 2px 6px rgba(0, 0, 0, .12));
|
|
127
|
+
}
|
|
128
|
+
.dsh-cp-selitem { border: none; background: transparent; text-align: center; padding: 5px 10px; font-size: 13px; line-height: 18px;
|
|
129
|
+
color: var(--dsw-alias-label-primary); border-radius: 999px; cursor: pointer; white-space: nowrap;
|
|
130
|
+
font-family: var(--dsw-font-family, inherit); letter-spacing: -.02em; }
|
|
131
|
+
.dsh-cp-selitem:hover, .dsh-cp-selitem:focus-visible { background: var(--dsw-alias-button-floating-hover, rgba(116, 126, 148, .36)); }
|
|
132
|
+
/* 官方浮动「引用文本」pill 由横条里的同名功能取代(同一 insertQuote API),隐藏避免双浮层 */
|
|
133
|
+
[class*="U4oBDW_action"] { display: none !important; }
|
|
134
|
+
.dsh-cp-toast { padding: 5px 12px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-primary, #fff);
|
|
135
|
+
background: rgba(50, 50, 54, 0.95); border-radius: 8px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25); }
|
|
136
|
+
/* ---- 划选引用 chip:Trae comment-mention 规格(ai-modules-chat/index.css)----
|
|
137
|
+
规格:高 22px / 圆角 --dsw-radius-sm(8) / business 浅色底 + business 主色文字 /
|
|
138
|
+
12px 500 / max-width 240;左图标在 hover 时换成 ×(Trae remove-mention-icon
|
|
139
|
+
同义)。颜色全部走 dsh 设计令牌,明暗主题各自生效,不再写死 Trae 的灰/蓝。 */
|
|
140
|
+
[contenteditable] span[title^="${CHIP_MARK}"] {
|
|
141
|
+
box-sizing: border-box;
|
|
142
|
+
display: inline-flex;
|
|
143
|
+
align-items: baseline;
|
|
144
|
+
gap: 4px;
|
|
145
|
+
max-width: min(80%, 300px);
|
|
146
|
+
padding: 0;
|
|
147
|
+
border: none;
|
|
148
|
+
border-radius: 0;
|
|
149
|
+
background: none;
|
|
150
|
+
color: var(--dsw-alias-state-business-primary, #4176e6);
|
|
151
|
+
font-family: var(--dsw-font-family, inherit);
|
|
152
|
+
font-size: 14px;
|
|
153
|
+
line-height: 20px;
|
|
154
|
+
vertical-align: baseline;
|
|
155
|
+
cursor: pointer;
|
|
156
|
+
overflow: hidden;
|
|
157
|
+
transition: background .14s ease;
|
|
158
|
+
}
|
|
159
|
+
[contenteditable] span[title^="${CHIP_MARK}"]:hover {
|
|
160
|
+
color: var(--dsw-alias-state-business-primary, #4176e6);
|
|
161
|
+
}
|
|
162
|
+
/* 隐藏宿主默认前缀(@ / file 图标),换官方同款 ❝ 引用标记(hover 变 ×) */
|
|
163
|
+
[contenteditable] span[title^="${CHIP_MARK}"] > span:first-child { display: none; }
|
|
164
|
+
[contenteditable] span[title^="${CHIP_MARK}"]::before {
|
|
165
|
+
content: '';
|
|
166
|
+
flex: 0 0 14px;
|
|
167
|
+
width: 14px;
|
|
168
|
+
height: 14px;
|
|
169
|
+
align-self: center;
|
|
170
|
+
margin-right: 2px;
|
|
171
|
+
background: currentColor;
|
|
172
|
+
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M6.6 3.2C4.5 4.3 3 6.3 3 8.6c0 2.3 1.3 3.7 3.2 3.7 1.6 0 2.7-1 2.7-2.5 0-1.4-.9-2.3-2.2-2.3-.4 0-.7 0-1 .2.3-1 .9-1.8 1.7-2.4L6.6 3.2Zm7 0c-2.1 1.1-3.6 3.1-3.6 5.4 0 2.3 1.3 3.7 3.2 3.7 1.6 0 2.7-1 2.7-2.5 0-1.4-.9-2.3-2.2-2.3-.4 0-.7 0-1 .2.3-1 .9-1.8 1.7-2.4l-.8-1.9Z' fill='black'/></svg>") center / 14px 14px no-repeat;
|
|
173
|
+
mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M6.6 3.2C4.5 4.3 3 6.3 3 8.6c0 2.3 1.3 3.7 3.2 3.7 1.6 0 2.7-1 2.7-2.5 0-1.4-.9-2.3-2.2-2.3-.4 0-.7 0-1 .2.3-1 .9-1.8 1.7-2.4L6.6 3.2Zm7 0c-2.1 1.1-3.6 3.1-3.6 5.4 0 2.3 1.3 3.7 3.2 3.7 1.6 0 2.7-1 2.7-2.5 0-1.4-.9-2.3-2.2-2.3-.4 0-.7 0-1 .2.3-1 .9-1.8 1.7-2.4l-.8-1.9Z' fill='black'/></svg>") center / 14px 14px no-repeat;
|
|
174
|
+
}
|
|
175
|
+
[contenteditable] span[title^="${CHIP_MARK}"]:hover::before {
|
|
176
|
+
content: '×';
|
|
177
|
+
background: none;
|
|
178
|
+
-webkit-mask: none;
|
|
179
|
+
mask: none;
|
|
180
|
+
color: var(--dsw-alias-label-primary, currentColor);
|
|
181
|
+
font-size: 14px;
|
|
182
|
+
line-height: 14px;
|
|
183
|
+
text-align: center;
|
|
184
|
+
}
|
|
185
|
+
/* 消息正文里的同款 chip(user-text 规则节点;不在 contenteditable 里) */
|
|
186
|
+
.dsh-si-msgchip { box-sizing: border-box; display: inline-flex; align-items: baseline; gap: 4px; max-width: min(80%, 300px);
|
|
187
|
+
padding: 0; border: none; border-radius: 0; background: none;
|
|
188
|
+
color: var(--dsw-alias-state-business-primary, #4176e6);
|
|
189
|
+
font-family: var(--dsw-font-family, inherit); font-size: 14px; line-height: 20px;
|
|
190
|
+
vertical-align: baseline; overflow: hidden; user-select: none; }
|
|
191
|
+
.dsh-si-msgchip.interactive { cursor: pointer; }
|
|
192
|
+
.dsh-si-msgchip-icon { flex: 0 0 14px; width: 14px; height: 14px; margin-right: 2px; opacity: .8; background: currentColor;
|
|
193
|
+
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M6.6 3.2C4.5 4.3 3 6.3 3 8.6c0 2.3 1.3 3.7 3.2 3.7 1.6 0 2.7-1 2.7-2.5 0-1.4-.9-2.3-2.2-2.3-.4 0-.7 0-1 .2.3-1 .9-1.8 1.7-2.4L6.6 3.2Zm7 0c-2.1 1.1-3.6 3.1-3.6 5.4 0 2.3 1.3 3.7 3.2 3.7 1.6 0 2.7-1 2.7-2.5 0-1.4-.9-2.3-2.2-2.3-.4 0-.7 0-1 .2.3-1 .9-1.8 1.7-2.4l-.8-1.9Z' fill='black'/></svg>") center / 14px 14px no-repeat;
|
|
194
|
+
mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M6.6 3.2C4.5 4.3 3 6.3 3 8.6c0 2.3 1.3 3.7 3.2 3.7 1.6 0 2.7-1 2.7-2.5 0-1.4-.9-2.3-2.2-2.3-.4 0-.7 0-1 .2.3-1 .9-1.8 1.7-2.4L6.6 3.2Zm7 0c-2.1 1.1-3.6 3.1-3.6 5.4 0 2.3 1.3 3.7 3.2 3.7 1.6 0 2.7-1 2.7-2.5 0-1.4-.9-2.3-2.2-2.3-.4 0-.7 0-1 .2.3-1 .9-1.8 1.7-2.4l-.8-1.9Z' fill='black'/></svg>") center / 14px 14px no-repeat; }
|
|
195
|
+
.dsh-si-msgchip-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
196
|
+
/* 详情弹窗:dsh 原生 Dialog 观感(layer-1 表面 / 16 圆角 / 突出投影 / 标准按钮) */
|
|
197
|
+
.dsh-si-mask { position: fixed; inset: 0; z-index: 1200; display: flex; align-items: center; justify-content: center;
|
|
198
|
+
padding: 24px; background: var(--dsw-alias-bg-mask-1, rgba(0, 0, 0, .24)); }
|
|
199
|
+
.dsh-si-dialog { box-sizing: border-box; width: min(480px, 100%); max-height: min(600px, 82vh); display: flex; flex-direction: column; gap: 12px;
|
|
200
|
+
padding: 16px; border-radius: var(--dsw-radius-lg, 16px); color: var(--dsw-alias-label-primary, #e8eefa); overflow-y: auto;
|
|
201
|
+
font-family: var(--dsw-font-family, inherit);
|
|
202
|
+
background: var(--dsw-alias-bg-layer-1, #232324);
|
|
203
|
+
border: .5px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, .08));
|
|
204
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l4, rgba(255, 255, 255, .2));
|
|
205
|
+
box-shadow: var(--dsw-elevation-prominent, 0 0 0 .5px var(--dsw-elevation-stroke-color), 0 8px 28px rgba(0, 0, 0, .28)); }
|
|
206
|
+
.dsh-si-dialog-head { display: flex; align-items: center; gap: 8px; }
|
|
207
|
+
.dsh-si-dialog-title { flex: 1 1 auto; min-width: 0; font-size: 16px; font-weight: 500; line-height: 24px;
|
|
208
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
209
|
+
.dsh-si-dialog-x { flex: 0 0 auto; width: 24px; height: 24px; padding: 0; border: 0; border-radius: var(--dsw-radius-xs, 4px); cursor: pointer;
|
|
210
|
+
background: transparent; color: var(--dsw-alias-label-tertiary, #8a9096); font-size: 15px; line-height: 1; }
|
|
211
|
+
.dsh-si-dialog-x:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, .08)); }
|
|
212
|
+
.dsh-si-dialog-quote { box-sizing: border-box; flex: 1 1 auto; min-height: 40px; max-height: 240px; overflow: auto; padding: 10px 12px; border-radius: var(--dsw-radius-sm, 8px);
|
|
213
|
+
background: var(--dsw-alias-bg-module-platform, rgba(255, 255, 255, .06));
|
|
214
|
+
border-left: 2px solid var(--dsw-alias-state-business-primary, #3a83f7);
|
|
215
|
+
font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-secondary); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
216
|
+
.dsh-si-dialog-label { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary, #8a9096); }
|
|
217
|
+
.dsh-si-dialog-detail { font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-secondary); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
218
|
+
.dsh-si-dialog-comment { box-sizing: border-box; width: 100%; min-height: 64px; max-height: 160px; resize: vertical; padding: 8px 10px;
|
|
219
|
+
font-size: 14px; line-height: 22px; font-family: inherit; color: var(--dsw-alias-label-primary);
|
|
220
|
+
background: var(--dsw-alias-bg-layer-2, rgba(255, 255, 255, .06)); border: .5px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, .12));
|
|
221
|
+
border-radius: var(--dsw-radius-sm, 8px); outline: none; }
|
|
222
|
+
.dsh-si-dialog-comment:focus {
|
|
223
|
+
border-color: var(--dsw-alias-state-business-primary, #3a83f7);
|
|
224
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary, #3a83f7) 16%, transparent);
|
|
225
|
+
}
|
|
226
|
+
.dsh-si-dialog-foot { display: flex; align-items: center; gap: 8px; }
|
|
227
|
+
.dsh-si-dialog-spacer { flex: 1 1 auto; }
|
|
228
|
+
.dsh-si-dialog-btn { height: 30px; padding: 0 14px; font-size: 13px; font-weight: 500; line-height: 20px; cursor: pointer;
|
|
229
|
+
color: var(--dsw-alias-label-primary); background: var(--dsw-alias-button-elevated-fill, transparent);
|
|
230
|
+
border: .5px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, .12)); border-radius: var(--dsw-radius-sm, 8px); }
|
|
231
|
+
.dsh-si-dialog-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, .08)); }
|
|
232
|
+
.dsh-si-dialog-btn.ghost { background: transparent; border-color: transparent; color: var(--dsw-alias-label-secondary); }
|
|
233
|
+
.dsh-si-dialog-btn.ghost:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, .08)); }
|
|
234
|
+
.dsh-si-dialog-btn.primary { color: var(--dsw-alias-label-primary-foreground, #fff);
|
|
235
|
+
background: var(--dsw-alias-button-primary-fill, #3a83f7); border-color: transparent; }
|
|
236
|
+
.dsh-si-dialog-btn.primary:hover { background: var(--dsw-alias-button-primary-hover, #4f8ef9); }
|
|
237
|
+
.dsh-si-dialog-btn.danger { background: transparent; border-color: transparent; color: var(--dsw-alias-state-error-primary, #f87171); }
|
|
238
|
+
.dsh-si-dialog-btn.danger:hover { background: var(--dsw-alias-interactive-bg-hover-danger, rgba(248, 113, 113, .12)); }
|
|
239
|
+
/* 划选评论编辑器:照抄 Trae CommentEditorCard(solo-lite 7920 包源码)——
|
|
240
|
+
previewCommentCardContainer(菜单底/1px 中性描边/radius 8/双层阴影)内嵌
|
|
241
|
+
commentEditorCard(320px/padding 8/gap 4 列布局);无标题无预览无取消钮,
|
|
242
|
+
单行 = 透明自增 textarea(13px/20px,max-height 140)+ ✓ 主按钮;Enter 提交、
|
|
243
|
+
Shift+Enter 换行、Esc/点外取消。 */
|
|
244
|
+
.dsh-si-comment-editor { position: fixed; z-index: 1300; box-sizing: border-box; width: 320px; padding: 8px;
|
|
245
|
+
display: flex; flex-direction: column; gap: 4px;
|
|
246
|
+
border-radius: var(--dsw-radius-sm, 8px); color: var(--dsw-alias-label-primary);
|
|
247
|
+
font-family: var(--dsw-font-family, inherit); background: var(--dsw-specific-menu, #202124);
|
|
248
|
+
border: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, .1));
|
|
249
|
+
/* specific-menu 是半透明底:必须配官方毛玻璃,否则透出下层文字(官方弹卡成对规则) */
|
|
250
|
+
backdrop-filter: var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%));
|
|
251
|
+
-webkit-backdrop-filter: var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%));
|
|
252
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, .19), 0 .5px rgba(255, 255, 255, .06), inset 0 .5px rgba(255, 255, 255, .06);
|
|
253
|
+
overflow: hidden;
|
|
254
|
+
transition: width .25s cubic-bezier(.4, 0, .2, 1), height .25s cubic-bezier(.4, 0, .2, 1);
|
|
255
|
+
visibility: hidden; }
|
|
256
|
+
.dsh-si-comment-row { display: flex; align-items: center; gap: 8px; }
|
|
257
|
+
.dsh-si-comment-input { flex: 1; min-width: 0; box-sizing: border-box; background: none; border: none; outline: none; resize: none;
|
|
258
|
+
color: var(--dsw-alias-label-primary); font-family: var(--dsw-font-family, inherit);
|
|
259
|
+
font-size: 13px; font-weight: 400; line-height: 20px; max-height: 140px;
|
|
260
|
+
padding: 0 8px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-l2, rgba(255, 255, 255, .16)) transparent; }
|
|
261
|
+
.dsh-si-comment-input::placeholder { color: var(--dsw-alias-label-tertiary); }
|
|
262
|
+
.dsh-si-comment-submit { flex: 0 0 auto; width: 24px; height: 24px; padding: 0; display: inline-flex; align-items: center; justify-content: center;
|
|
263
|
+
border: none; border-radius: 6px; cursor: pointer;
|
|
264
|
+
background: var(--dsw-alias-bg-layer-2, rgba(255, 255, 255, .12)); color: var(--dsw-alias-label-primary); }
|
|
265
|
+
.dsh-si-comment-submit:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, .2)); }
|
|
266
|
+
.dsh-si-comment-submit:disabled { opacity: .5; cursor: default; }
|
|
267
|
+
/* 点击标签跳源时对源元素的闪烁高亮。 */
|
|
268
|
+
.dsh-cp-flash { outline: 2px solid var(--dsw-alias-state-business-primary, #3a83f7) !important; outline-offset: 1px; border-radius: 3px; transition: outline-color .3s ease; }
|
|
269
|
+
`
|
|
270
|
+
|
|
271
|
+
function ensureStyle() {
|
|
272
|
+
if (typeof document === 'undefined') return
|
|
273
|
+
if (document.getElementById(STYLE_ID)) return
|
|
274
|
+
const style = document.createElement('style')
|
|
275
|
+
style.id = STYLE_ID
|
|
276
|
+
style.textContent = CSS
|
|
277
|
+
document.head.appendChild(style)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function api(path, body) {
|
|
281
|
+
try {
|
|
282
|
+
const res = await fetch(`${ENDPOINT}${path}`, body
|
|
283
|
+
? { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }
|
|
284
|
+
: { cache: 'no-store' })
|
|
285
|
+
return res.ok ? res.json() : null
|
|
286
|
+
} catch { return null }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** 异常留痕:走 prompts /diag(0.4.0 起套件统一通道,落 superinput-debug.log)。 */
|
|
290
|
+
function report(message) {
|
|
291
|
+
try {
|
|
292
|
+
void fetch('/api/dsh-superinput/prompts/diag', {
|
|
293
|
+
method: 'POST',
|
|
294
|
+
headers: { 'content-type': 'application/json' },
|
|
295
|
+
body: JSON.stringify({ message: `prompts: ${String(message).slice(0, 300)}` }),
|
|
296
|
+
}).catch(() => {})
|
|
297
|
+
} catch { /* ignore */ }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* ------------------------------------------------------------------ */
|
|
301
|
+
/* 划选引用:输入框内联 chip(官方 reference source)+ /context 同步 */
|
|
302
|
+
/* ------------------------------------------------------------------ */
|
|
303
|
+
|
|
304
|
+
const selectionItems = new Map() // id -> item(发送后归档,供消息正文的 chip 渲染)
|
|
305
|
+
const selectionSyncTimers = new Map()
|
|
306
|
+
const selectionActions = new Map() // sessionId -> InputActions
|
|
307
|
+
const selectionDetailSubs = new Set()
|
|
308
|
+
let selectionDetailRequest = null
|
|
309
|
+
let selectionSessions = null // ClientSessions 服务(取每会话 binding.ctx 发输入事件)
|
|
310
|
+
let selectionIdSeq = 0
|
|
311
|
+
let selectionStorageTimer = null
|
|
312
|
+
|
|
313
|
+
const tokenOf = (id) => `[选段#${id}]`
|
|
314
|
+
const previewOf = (text, max = 220) => String(text ?? '').replace(/\s+/g, ' ').trim().slice(0, max)
|
|
315
|
+
|
|
316
|
+
/** 选区矩形兜底:菜单给的 rect 失效时退回锚点元素/默认位置。 */
|
|
317
|
+
const normalizeSelectionRect = (rect, anchorEl) => {
|
|
318
|
+
const finite = (n) => typeof n === 'number' && Number.isFinite(n)
|
|
319
|
+
if (rect && finite(rect.left) && finite(rect.top) && finite(rect.right) && finite(rect.bottom)) return rect
|
|
320
|
+
const el = anchorEl instanceof Element ? anchorEl : null
|
|
321
|
+
if (el) {
|
|
322
|
+
const r = el.getBoundingClientRect()
|
|
323
|
+
return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height }
|
|
324
|
+
}
|
|
325
|
+
return { left: 16, top: 80, right: 16, bottom: 80, width: 0, height: 0 }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** chip 标签:Trae 是 `N. 选段文本 · 评论`(评论内联在标签里)。
|
|
329
|
+
* 有评论时把选段压到 ~14 字(Trae 的 text-content 也只占 50%),
|
|
330
|
+
* 否则 240px 的 chip 会把评论整段挤没。 */
|
|
331
|
+
const chipTextOf = (seq, text, comment) => {
|
|
332
|
+
const quote = String(text ?? '').replace(/\s+/g, ' ').trim()
|
|
333
|
+
const note = String(comment ?? '').replace(/\s+/g, ' ').trim()
|
|
334
|
+
if (!note) return `${seq}. ${quote}`
|
|
335
|
+
const head = quote.length > 14 ? `${quote.slice(0, 14)}…` : quote
|
|
336
|
+
return `${seq}. ${head} · ${note.slice(0, 40)}`
|
|
337
|
+
}
|
|
338
|
+
const chipLabelOf = (item) => `${CHIP_MARK}${chipTextOf(item.seq, item.text, item.comment)}`
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* 某条 chip 在 detect 坐标里的跨度。occurrences 的 offset 是 clipboard
|
|
342
|
+
* 坐标(chip 展开成 clipboardText),detect 坐标里每条 chip 只占 1 个
|
|
343
|
+
* U+FFFC——把前面所有 chip 的 (length-1) 扣掉即得。
|
|
344
|
+
*/
|
|
345
|
+
function chipSpanOf(occurrences, ref, draftRev) {
|
|
346
|
+
const own = (occurrences ?? []).find((occ) => occ.source === SELECTION_SOURCE && occ.ref === ref)
|
|
347
|
+
if (!own) return null
|
|
348
|
+
let start = own.offset
|
|
349
|
+
for (const occ of occurrences) if (occ.offset < own.offset) start -= occ.length - 1
|
|
350
|
+
return { start, end: start + 1, draftRev }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** 当前草稿里真实存在的条目,按草稿里的先后顺序(Trae 的编号顺序)。 */
|
|
354
|
+
function liveSelectionItems(sessionId, occurrences) {
|
|
355
|
+
if (!sessionId) return []
|
|
356
|
+
const seen = new Set()
|
|
357
|
+
const live = []
|
|
358
|
+
for (const occ of occurrences ?? []) {
|
|
359
|
+
if (occ.source !== SELECTION_SOURCE || seen.has(occ.ref)) continue
|
|
360
|
+
seen.add(occ.ref)
|
|
361
|
+
const item = selectionItems.get(occ.ref)
|
|
362
|
+
if (item && item.sessionId === sessionId) live.push(item)
|
|
363
|
+
}
|
|
364
|
+
return live
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function postContext(sessionId, items) {
|
|
368
|
+
if (!sessionId) return
|
|
369
|
+
try {
|
|
370
|
+
await fetch(`${ENDPOINT}/context`, {
|
|
371
|
+
method: 'POST',
|
|
372
|
+
headers: { 'content-type': 'application/json' },
|
|
373
|
+
body: JSON.stringify({
|
|
374
|
+
sessionId,
|
|
375
|
+
items: items.map(({ text, comment }) => ({
|
|
376
|
+
text: String(text ?? '').slice(0, CONTEXT_TEXT_MAX),
|
|
377
|
+
comment: String(comment ?? '').slice(0, CONTEXT_COMMENT_MAX),
|
|
378
|
+
})),
|
|
379
|
+
}),
|
|
380
|
+
keepalive: true,
|
|
381
|
+
})
|
|
382
|
+
} catch { /* 网络失败时服务端保留上一份,不打断输入 */ }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** 服务端 /context 同步:150ms 防抖;发送意图用 immediate flush。 */
|
|
386
|
+
function syncSelectionContext(sessionId, items, immediate) {
|
|
387
|
+
if (!sessionId) return
|
|
388
|
+
const previous = selectionSyncTimers.get(sessionId)
|
|
389
|
+
if (previous) clearTimeout(previous)
|
|
390
|
+
const send = () => {
|
|
391
|
+
selectionSyncTimers.delete(sessionId)
|
|
392
|
+
void postContext(sessionId, items)
|
|
393
|
+
}
|
|
394
|
+
if (immediate) send()
|
|
395
|
+
else selectionSyncTimers.set(sessionId, setTimeout(send, 150))
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** 落 sessionStorage:消息正文的 chip 在刷新后仍有文本/评论可渲染。 */
|
|
399
|
+
function persistSelectionItems() {
|
|
400
|
+
if (typeof sessionStorage === 'undefined' || selectionStorageTimer) return
|
|
401
|
+
selectionStorageTimer = setTimeout(() => {
|
|
402
|
+
selectionStorageTimer = null
|
|
403
|
+
try {
|
|
404
|
+
const snapshot = [...selectionItems.values()].slice(-120).map((item) => ({
|
|
405
|
+
id: item.id,
|
|
406
|
+
sessionId: item.sessionId,
|
|
407
|
+
seq: item.seq,
|
|
408
|
+
text: item.text,
|
|
409
|
+
comment: item.comment,
|
|
410
|
+
sent: item.sent === true,
|
|
411
|
+
at: item.at,
|
|
412
|
+
}))
|
|
413
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot))
|
|
414
|
+
} catch { /* 配额/隐私模式:忽略 */ }
|
|
415
|
+
}, 400)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function loadSelectionItems() {
|
|
419
|
+
if (typeof sessionStorage === 'undefined') return
|
|
420
|
+
try {
|
|
421
|
+
const saved = JSON.parse(sessionStorage.getItem(STORAGE_KEY) ?? '[]')
|
|
422
|
+
if (!Array.isArray(saved)) return
|
|
423
|
+
for (const raw of saved) {
|
|
424
|
+
if (!raw?.id || !raw?.text) continue
|
|
425
|
+
const id = String(raw.id)
|
|
426
|
+
if (selectionItems.has(id)) continue
|
|
427
|
+
const sessionId = String(raw.sessionId ?? '')
|
|
428
|
+
const seq = Number(raw.seq) || 0
|
|
429
|
+
selectionItems.set(id, {
|
|
430
|
+
id, sessionId, seq,
|
|
431
|
+
text: String(raw.text),
|
|
432
|
+
comment: String(raw.comment ?? ''),
|
|
433
|
+
anchor: null,
|
|
434
|
+
sent: raw.sent === true,
|
|
435
|
+
archived: true, // 历史条目:chip 已不在草稿里,只用于消息正文展示
|
|
436
|
+
at: Number(raw.at) || Date.now(),
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
} catch { /* 存档损坏:忽略 */ }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** 走会话作用域的 slash/input-insert-reference 事件把 chip 插进官方编辑器。 */
|
|
443
|
+
function applySelectionInsert(sessionId, item, span) {
|
|
444
|
+
if (!span) return false
|
|
445
|
+
const binding = selectionSessions?.binding?.(sessionId)
|
|
446
|
+
const actx = binding?.ctx
|
|
447
|
+
if (!actx) return false
|
|
448
|
+
const request = {
|
|
449
|
+
reference: {
|
|
450
|
+
source: SELECTION_SOURCE,
|
|
451
|
+
ref: item.id,
|
|
452
|
+
label: chipLabelOf(item),
|
|
453
|
+
clipboardText: tokenOf(item.id),
|
|
454
|
+
},
|
|
455
|
+
span,
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
if (typeof actx.bail === 'function') {
|
|
459
|
+
try { return actx.bail(actx, 'slash/input-insert-reference', request) === true }
|
|
460
|
+
catch { /* 有些版本只接受 (name, payload) */ }
|
|
461
|
+
try { return actx.bail('slash/input-insert-reference', request) === true }
|
|
462
|
+
catch { return false }
|
|
463
|
+
}
|
|
464
|
+
if (typeof actx.emit === 'function') {
|
|
465
|
+
actx.emit(actx, 'slash/input-insert-reference', request)
|
|
466
|
+
return true
|
|
467
|
+
}
|
|
468
|
+
} catch { return false }
|
|
469
|
+
return false
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** 删除一条 chip:用 insertText('') 替换它的 detect 跨度(空文本即删除)。 */
|
|
473
|
+
function applySelectionRemove(sessionId, item, span) {
|
|
474
|
+
if (!span) return false
|
|
475
|
+
const actions = selectionActions.get(sessionId)
|
|
476
|
+
if (typeof actions?.insertText !== 'function') return false
|
|
477
|
+
try { return actions.insertText('', span) === true } catch { return false }
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function requestSelectionDetail(sessionId, id, focus) {
|
|
481
|
+
selectionDetailRequest = { sessionId, id, focus: focus === true }
|
|
482
|
+
for (const listener of selectionDetailSubs) {
|
|
483
|
+
try { listener() } catch { /* ignore */ }
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
loadSelectionItems()
|
|
488
|
+
|
|
489
|
+
/** 找出正文里的 {{变量}},去重保序。 */
|
|
490
|
+
function varsOf(text) {
|
|
491
|
+
const found = []
|
|
492
|
+
const re = /\{\{\s*([a-zA-Z_$][\w$]*)\s*\}\}/g
|
|
493
|
+
let match
|
|
494
|
+
while ((match = re.exec(String(text ?? '')))) {
|
|
495
|
+
if (!found.includes(match[1])) found.push(match[1])
|
|
496
|
+
}
|
|
497
|
+
return found
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** 内置动态变量:同步可取的自动填值;取不到的(如剪贴板)留空手填/异步预填。 */
|
|
501
|
+
function autoValue(name) {
|
|
502
|
+
const now = new Date()
|
|
503
|
+
if (name === '日期') return now.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })
|
|
504
|
+
if (name === '时间') return now.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
|
505
|
+
if (name === '星期') return now.toLocaleDateString('zh-CN', { weekday: 'long' })
|
|
506
|
+
return null
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** / 命令插入路径:内置动态变量自动取值,手填变量保留 {{名}} 原样由用户行内改。 */
|
|
510
|
+
function insertViaBridge(item) {
|
|
511
|
+
try {
|
|
512
|
+
void api('/use', { id: item.id })
|
|
513
|
+
let out = String(item.body ?? '')
|
|
514
|
+
for (const name of varsOf(out)) {
|
|
515
|
+
const auto = autoValue(name)
|
|
516
|
+
if (auto !== null) out = out.replace(new RegExp(`\\{\\{\\s*${name}\\s*\\}\\}`, 'g'), auto)
|
|
517
|
+
}
|
|
518
|
+
const set = window.__dshSuperinputSetDraft
|
|
519
|
+
if (!set) {
|
|
520
|
+
report('slash insert skipped: draft bridge missing')
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
// action 执行时草稿里还留着刚敲的命令行(/e2e),官方壳清 token 与我们的
|
|
524
|
+
// 写入有时序竞争——首行是无空格的 /xxx 就剥掉,两种时序下都只留正文。
|
|
525
|
+
const current = String(window.__dshSuperinputDraft?.() ?? '')
|
|
526
|
+
let base = current
|
|
527
|
+
const firstLine = current.split('\n')[0] ?? ''
|
|
528
|
+
if (current && firstLine.startsWith('/') && !firstLine.includes(' ')) {
|
|
529
|
+
base = current.slice(firstLine.length).replace(/^\n+/, '')
|
|
530
|
+
}
|
|
531
|
+
set(base ? `${base}\n${out}` : out)
|
|
532
|
+
} catch (error) {
|
|
533
|
+
report(`slash insert failed: ${error?.stack ?? error}`)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** 划选正文文字浮出「存」按钮 → 存为模板 / 存为片段(两个库的收集入口)。 */
|
|
538
|
+
function installSelectionSaver() {
|
|
539
|
+
if (typeof document === 'undefined') return
|
|
540
|
+
// 安装守卫:宿主重挂载时 apply 可能跑第二次,重复监听会造出多个浮层。
|
|
541
|
+
if (document.documentElement.dataset.dshSelectionSaver) return
|
|
542
|
+
document.documentElement.dataset.dshSelectionSaver = '1'
|
|
543
|
+
let host = null
|
|
544
|
+
let hideTimer = 0
|
|
545
|
+
|
|
546
|
+
const ensureHost = () => {
|
|
547
|
+
// 文档级单例:无论模块实例化几次,全页只留一个浮层容器。
|
|
548
|
+
const existing = document.querySelectorAll('[data-dsh-superinput-portal="selection"]')
|
|
549
|
+
for (let i = 1; i < existing.length; i += 1) existing[i].remove()
|
|
550
|
+
if (existing[0]?.isConnected) {
|
|
551
|
+
host = existing[0]
|
|
552
|
+
return host
|
|
553
|
+
}
|
|
554
|
+
host = document.createElement('div')
|
|
555
|
+
host.setAttribute('data-dsh-superinput-portal', 'selection')
|
|
556
|
+
host.style.cssText = 'position:fixed;z-index:1150;display:none;'
|
|
557
|
+
document.body.appendChild(host)
|
|
558
|
+
return host
|
|
559
|
+
}
|
|
560
|
+
const hide = () => {
|
|
561
|
+
if (hideTimer) { clearTimeout(hideTimer); hideTimer = 0 }
|
|
562
|
+
if (host) host.style.display = 'none'
|
|
563
|
+
}
|
|
564
|
+
const toast = (text) => {
|
|
565
|
+
const el = ensureHost()
|
|
566
|
+
const div = document.createElement('div')
|
|
567
|
+
div.className = 'dsh-cp-toast'
|
|
568
|
+
div.textContent = text
|
|
569
|
+
el.replaceChildren(div) // 原生 DOM——h() 产物是 React 对象,replaceChildren 会把它强转成 "[object Object]"
|
|
570
|
+
el.style.left = '50%'
|
|
571
|
+
el.style.top = '72px'
|
|
572
|
+
el.style.transform = 'translateX(-50%)'
|
|
573
|
+
el.style.display = 'block'
|
|
574
|
+
if (hideTimer) clearTimeout(hideTimer)
|
|
575
|
+
hideTimer = setTimeout(hide, 1800)
|
|
576
|
+
}
|
|
577
|
+
// 评论编辑器在 React 侧保存后要提示,复用这个文档级 toast。
|
|
578
|
+
window.__dshSuperinputToast = toast
|
|
579
|
+
|
|
580
|
+
/* 四选项横条(用户拍板恢复 0.8.x 形态):复制 / 引用文本(官方
|
|
581
|
+
* insertQuote 机制)/ 添加评论(Trae 评论卡 → chip)/ 存为模板。
|
|
582
|
+
* 官方自己的浮动「引用文本」pill 用 CSS 隐藏——横条里的「引用文本」
|
|
583
|
+
* 调同一个官方 API,避免双浮层。 */
|
|
584
|
+
let showTimer = 0
|
|
585
|
+
document.addEventListener('selectionchange', () => {
|
|
586
|
+
try {
|
|
587
|
+
if (showTimer) { clearTimeout(showTimer); showTimer = 0 }
|
|
588
|
+
const selection = window.getSelection?.()
|
|
589
|
+
const text = String(selection ?? '').trim()
|
|
590
|
+
if (!text || !selection.rangeCount) { hide(); return }
|
|
591
|
+
showTimer = setTimeout(() => {
|
|
592
|
+
try {
|
|
593
|
+
const sel = window.getSelection?.()
|
|
594
|
+
const current = String(sel ?? '').trim()
|
|
595
|
+
if (!current || !sel.rangeCount) { hide(); return }
|
|
596
|
+
const node = sel.anchorNode
|
|
597
|
+
const el = node instanceof Element ? node : node?.parentElement
|
|
598
|
+
// 只收对话正文:跳过输入区、我们自己的弹层,以及侧栏/设置等非对话区域
|
|
599
|
+
if (!el || el.closest('[data-dsh-superinput-portal]') || el.closest('[contenteditable="true"], textarea, input')) { hide(); return }
|
|
600
|
+
if (!el.closest('[data-chat-flow], [data-slot="conversation.chat.flow"]')) { hide(); return }
|
|
601
|
+
const rect = sel.getRangeAt(0).getBoundingClientRect()
|
|
602
|
+
if (!rect || (rect.width === 0 && rect.height === 0)) { hide(); return }
|
|
603
|
+
const container = ensureHost()
|
|
604
|
+
// 横排四项 ≈320px:左缘对齐选区,右侧防溢出
|
|
605
|
+
container.style.left = `${Math.min(Math.max(8, rect.left), Math.max(8, window.innerWidth - 330))}px`
|
|
606
|
+
container.style.top = `${Math.max(8, rect.top - 44)}px`
|
|
607
|
+
container.style.transform = ''
|
|
608
|
+
container.style.display = 'block'
|
|
609
|
+
const menu = document.createElement('div')
|
|
610
|
+
menu.className = 'dsh-cp-selmenu'
|
|
611
|
+
// 阻止默认聚焦/清选区:否则 mousedown 触发 selectionchange,菜单会在 click 前被隐藏
|
|
612
|
+
menu.addEventListener('mousedown', (e) => { e.stopPropagation(); e.preventDefault() })
|
|
613
|
+
const mkItem = (label, action) => {
|
|
614
|
+
const b = document.createElement('button')
|
|
615
|
+
b.className = 'dsh-cp-selitem'
|
|
616
|
+
b.textContent = label
|
|
617
|
+
b.addEventListener('click', () => { void action(current, el, rect) })
|
|
618
|
+
return b
|
|
619
|
+
}
|
|
620
|
+
menu.append(
|
|
621
|
+
mkItem('复制', copySel),
|
|
622
|
+
mkItem('引用文本', quoteToComposer),
|
|
623
|
+
mkItem('添加评论', (text2) => openComment(text2, el, rect)),
|
|
624
|
+
mkItem('存为模板', save))
|
|
625
|
+
container.replaceChildren(menu)
|
|
626
|
+
} catch { /* ignore */ }
|
|
627
|
+
}, 250)
|
|
628
|
+
} catch { /* ignore */ }
|
|
629
|
+
})
|
|
630
|
+
document.addEventListener('mousedown', (e) => {
|
|
631
|
+
if (host?.style.display === 'block' && !e.target.closest?.('[data-dsh-superinput-portal="selection"]')) hide()
|
|
632
|
+
}, true)
|
|
633
|
+
window.addEventListener('scroll', hide, true)
|
|
634
|
+
document.addEventListener('selectionchange', () => {
|
|
635
|
+
if (!String(window.getSelection?.() ?? '').trim()) hide()
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
const copySel = async (text) => {
|
|
639
|
+
hide()
|
|
640
|
+
if (!navigator.clipboard?.writeText) { toast('当前环境不支持复制'); return }
|
|
641
|
+
try {
|
|
642
|
+
await navigator.clipboard.writeText(text)
|
|
643
|
+
toast('已复制')
|
|
644
|
+
} catch {
|
|
645
|
+
toast('复制失败')
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** 官方「引用文本」同款:inputActions.insertQuote 插 Markdown blockquote。
|
|
650
|
+
* plain 模式限制下返回 false 时,退回草稿桥手写 `> ` 前缀(发送后官方
|
|
651
|
+
* quoteRule 一样渲染成引用样式)。 */
|
|
652
|
+
const quoteToComposer = (text) => {
|
|
653
|
+
hide()
|
|
654
|
+
const insert = window.__dshSuperinputInsertQuote
|
|
655
|
+
if (typeof insert === 'function' && insert(text) === true) { toast('已引用到输入框'); return }
|
|
656
|
+
try {
|
|
657
|
+
const set = window.__dshSuperinputSetDraft
|
|
658
|
+
if (!set) { toast('输入框桥未就绪,刷新后重试'); return }
|
|
659
|
+
const block = text.split('\n').map((line) => `> ${line}`).join('\n')
|
|
660
|
+
const current = String(window.__dshSuperinputDraft?.() ?? '')
|
|
661
|
+
set(current ? `${current}\n${block}` : block)
|
|
662
|
+
toast('已引用到输入框')
|
|
663
|
+
} catch {
|
|
664
|
+
toast('引用失败')
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Trae 的「添加评论」:在选区旁弹评论卡,保存后才插 chip(带评论)。
|
|
669
|
+
* rect 是选区矩形,React 侧用它把卡片锚在选区附近。 */
|
|
670
|
+
const openComment = (text, anchorEl, rect) => {
|
|
671
|
+
hide()
|
|
672
|
+
const open = window.__dshSuperinputCommentContext
|
|
673
|
+
if (typeof open !== 'function') { toast('评论编辑器未就绪,刷新后重试'); return }
|
|
674
|
+
let status = 'failed'
|
|
675
|
+
try {
|
|
676
|
+
status = open(text, anchorEl, {
|
|
677
|
+
left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom,
|
|
678
|
+
width: rect.width, height: rect.height,
|
|
679
|
+
})
|
|
680
|
+
} catch { status = 'failed' }
|
|
681
|
+
if (status === 'full') toast(`最多添加 ${CONTEXT_MAX} 段对话内容`)
|
|
682
|
+
else if (status === 'empty') toast('选段为空')
|
|
683
|
+
else if (status === 'failed') toast('打开评论编辑器失败')
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const save = async (text) => {
|
|
687
|
+
const title = text.replace(/\s+/g, ' ').trim().slice(0, 40)
|
|
688
|
+
hide()
|
|
689
|
+
try {
|
|
690
|
+
const res = await fetch('/api/dsh-superinput/prompts/save', {
|
|
691
|
+
method: 'POST',
|
|
692
|
+
headers: { 'content-type': 'application/json' },
|
|
693
|
+
body: JSON.stringify({ title, body: text }),
|
|
694
|
+
})
|
|
695
|
+
const data = res.ok ? await res.json() : null
|
|
696
|
+
toast(data?.ok ? '已存为模板' : '保存失败')
|
|
697
|
+
} catch {
|
|
698
|
+
toast('保存失败')
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/* Trae dragDropMention 等价:把对话里划选的文字拖进输入框,落成引用 chip。
|
|
703
|
+
* dragstart 时对选区打自定义 dataTransfer 标记 + stash 文本;dragover 放行
|
|
704
|
+
* composer;drop 拦截宿主的纯文本插入,走 __dshSuperinputDropContext 插 chip。 */
|
|
705
|
+
let dragSelection = null // { text, anchorEl }
|
|
706
|
+
document.addEventListener('dragstart', (event) => {
|
|
707
|
+
try {
|
|
708
|
+
const selection = window.getSelection?.()
|
|
709
|
+
const text = String(selection ?? '').trim()
|
|
710
|
+
const node = selection?.anchorNode
|
|
711
|
+
const el = node instanceof Element ? node : node?.parentElement
|
|
712
|
+
if (!text || !el?.closest?.('[data-chat-flow], [data-slot="conversation.chat.flow"]')
|
|
713
|
+
|| el.closest?.('[contenteditable="true"], textarea, input')) { dragSelection = null; return }
|
|
714
|
+
dragSelection = { text: text.slice(0, CONTEXT_TEXT_MAX), anchorEl: el }
|
|
715
|
+
try { event.dataTransfer?.setData('application/x-dsh-selection', '1') } catch { /* 标记失败只影响识别 */ }
|
|
716
|
+
} catch { dragSelection = null }
|
|
717
|
+
}, true)
|
|
718
|
+
document.addEventListener('dragover', (event) => {
|
|
719
|
+
try {
|
|
720
|
+
if (!dragSelection) return
|
|
721
|
+
const target = event.target
|
|
722
|
+
if (!(target instanceof Element) || !target.closest?.('[contenteditable="true"]')) return
|
|
723
|
+
event.preventDefault()
|
|
724
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'
|
|
725
|
+
} catch { /* ignore */ }
|
|
726
|
+
}, true)
|
|
727
|
+
document.addEventListener('drop', (event) => {
|
|
728
|
+
try {
|
|
729
|
+
if (!dragSelection) return
|
|
730
|
+
const target = event.target
|
|
731
|
+
if (!(target instanceof Element) || !target.closest?.('[contenteditable="true"]')) return
|
|
732
|
+
event.preventDefault()
|
|
733
|
+
event.stopPropagation()
|
|
734
|
+
const { text, anchorEl } = dragSelection
|
|
735
|
+
dragSelection = null
|
|
736
|
+
// 尽力把光标挪到落点:宿主 captureInsertion 读到哪就是哪。
|
|
737
|
+
try {
|
|
738
|
+
const range = typeof document.caretRangeFromPoint === 'function'
|
|
739
|
+
? document.caretRangeFromPoint(event.clientX, event.clientY)
|
|
740
|
+
: null
|
|
741
|
+
if (range) {
|
|
742
|
+
window.getSelection()?.removeAllRanges()
|
|
743
|
+
window.getSelection()?.addRange(range)
|
|
744
|
+
}
|
|
745
|
+
} catch { /* 定位失败就落原 caret */ }
|
|
746
|
+
const insert = window.__dshSuperinputDropContext ?? window.__dshSuperinputAddContext
|
|
747
|
+
if (typeof insert === 'function') {
|
|
748
|
+
const status = insert(text, anchorEl)
|
|
749
|
+
const toast = window.__dshSuperinputToast
|
|
750
|
+
if (typeof toast === 'function') {
|
|
751
|
+
if (status === 'added') toast('已添加到对话')
|
|
752
|
+
else if (status === 'dup') toast('该选段已在列表中')
|
|
753
|
+
else if (status === 'full') toast(`最多添加 ${CONTEXT_MAX} 段对话内容`)
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
} catch { /* ignore */ }
|
|
757
|
+
}, true)
|
|
758
|
+
document.addEventListener('dragend', () => { dragSelection = null }, true)
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function PromptsPanel({ anchor, onClose, onSyncSlash }) {
|
|
762
|
+
const [items, setItems] = React.useState(null)
|
|
763
|
+
const [query, setQuery] = React.useState('')
|
|
764
|
+
const [draft, setDraft] = React.useState(null)
|
|
765
|
+
const [fill, setFill] = React.useState(null)
|
|
766
|
+
const [flash, setFlash] = React.useState(null)
|
|
767
|
+
const inputRef = React.useRef(null)
|
|
768
|
+
|
|
769
|
+
const reload = React.useCallback(async () => {
|
|
770
|
+
const data = await api('/list')
|
|
771
|
+
if (data && Array.isArray(data.items)) setItems(data.items)
|
|
772
|
+
onSyncSlash?.() // 模板增删改后重新对齐 / 命令注册
|
|
773
|
+
}, [onSyncSlash])
|
|
774
|
+
React.useEffect(() => { void reload() }, [reload])
|
|
775
|
+
|
|
776
|
+
// 编辑/填变量中不响应 Esc / 点外,避免丢失刚输入的内容。
|
|
777
|
+
React.useEffect(() => {
|
|
778
|
+
const onKey = (event) => { if (event.key === 'Escape' && !draft && !fill) onClose() }
|
|
779
|
+
const onClick = (event) => {
|
|
780
|
+
if (draft || fill) return
|
|
781
|
+
if (!event.target.closest?.('.dsh-cp-panel') && !event.target.closest?.('.dsh-cp-btn')) onClose()
|
|
782
|
+
}
|
|
783
|
+
document.addEventListener('keydown', onKey)
|
|
784
|
+
document.addEventListener('mousedown', onClick, true)
|
|
785
|
+
return () => {
|
|
786
|
+
document.removeEventListener('keydown', onKey)
|
|
787
|
+
document.removeEventListener('mousedown', onClick, true)
|
|
788
|
+
}
|
|
789
|
+
}, [onClose, draft, fill])
|
|
790
|
+
|
|
791
|
+
React.useEffect(() => {
|
|
792
|
+
if (!flash) return undefined
|
|
793
|
+
const timer = setTimeout(() => setFlash(null), 3500)
|
|
794
|
+
return () => clearTimeout(timer)
|
|
795
|
+
}, [flash])
|
|
796
|
+
|
|
797
|
+
const [pos, setPos] = React.useState(null)
|
|
798
|
+
React.useEffect(() => {
|
|
799
|
+
if (!anchor) return
|
|
800
|
+
const rect = anchor.getBoundingClientRect()
|
|
801
|
+
const width = Math.min(380, window.innerWidth - 24)
|
|
802
|
+
setPos({
|
|
803
|
+
left: Math.max(8, Math.min(rect.left, window.innerWidth - width - 8)),
|
|
804
|
+
top: Math.max(8, rect.top - 8),
|
|
805
|
+
transform: 'translateY(-100%)',
|
|
806
|
+
})
|
|
807
|
+
}, [anchor])
|
|
808
|
+
|
|
809
|
+
const insert = (item, values) => {
|
|
810
|
+
try {
|
|
811
|
+
void api('/use', { id: item.id }) // 使用计数:列表按最近使用排序
|
|
812
|
+
let out = String(item.body ?? '')
|
|
813
|
+
for (const [name, value] of Object.entries(values ?? {})) {
|
|
814
|
+
out = out.replace(new RegExp(`\\{\\{\\s*${name}\\s*\\}\\}`, 'g'), value ?? '')
|
|
815
|
+
}
|
|
816
|
+
if (!window.__dshSuperinputSetDraft) {
|
|
817
|
+
setFlash('草稿桥未就绪,请关掉面板重开再试')
|
|
818
|
+
report('insert skipped: draft bridge missing')
|
|
819
|
+
return
|
|
820
|
+
}
|
|
821
|
+
const current = String(window.__dshSuperinputDraft?.() ?? '')
|
|
822
|
+
window.__dshSuperinputSetDraft(current ? `${current}\n${out}` : out)
|
|
823
|
+
onClose()
|
|
824
|
+
} catch (error) {
|
|
825
|
+
report(`insert failed: ${error?.stack ?? error}`)
|
|
826
|
+
setFlash(`插入失败:${String(error?.message ?? error)}`)
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const remove = async (id) => { await api('/remove', { id }); void reload() }
|
|
831
|
+
const pin = async (item) => { await api('/pin', { id: item.id, pinned: !item.pinned }); void reload() }
|
|
832
|
+
const toggleSlash = async (item) => { await api('/slash', { id: item.id, slash: !item.slash }); void reload() }
|
|
833
|
+
|
|
834
|
+
/** 导出:拉全量 JSON,走浏览器下载落到系统下载目录。 */
|
|
835
|
+
const doExport = async () => {
|
|
836
|
+
try {
|
|
837
|
+
const res = await fetch(`${ENDPOINT}/export`, { cache: 'no-store' })
|
|
838
|
+
if (!res.ok) throw new Error(`export ${res.status}`)
|
|
839
|
+
const blob = new Blob([await res.text()], { type: 'application/json' })
|
|
840
|
+
const url = URL.createObjectURL(blob)
|
|
841
|
+
const name = `dsh-prompts-${new Date().toISOString().slice(0, 10)}.json`
|
|
842
|
+
const a = document.createElement('a')
|
|
843
|
+
a.href = url
|
|
844
|
+
a.download = name
|
|
845
|
+
a.click()
|
|
846
|
+
setTimeout(() => URL.revokeObjectURL(url), 5000)
|
|
847
|
+
setFlash(`已导出到下载目录:${name}`)
|
|
848
|
+
} catch (error) {
|
|
849
|
+
report(`export failed: ${error?.stack ?? error}`)
|
|
850
|
+
setFlash(`导出失败:${String(error?.message ?? error)}`)
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/** 导入:临时 file input 走系统文件选择框,JSON 解析后交给 /import 按 id 合并。 */
|
|
855
|
+
const doImport = () => {
|
|
856
|
+
const input = document.createElement('input')
|
|
857
|
+
input.type = 'file'
|
|
858
|
+
input.accept = '.json,application/json'
|
|
859
|
+
input.onchange = async () => {
|
|
860
|
+
const file = input.files?.[0]
|
|
861
|
+
if (!file) return
|
|
862
|
+
try {
|
|
863
|
+
const payload = JSON.parse(await file.text())
|
|
864
|
+
const res = await api('/import', payload)
|
|
865
|
+
if (!res?.ok) throw new Error(res?.error ?? 'import rejected')
|
|
866
|
+
setFlash(`导入完成:新增 ${res.added ?? 0},更新 ${res.updated ?? 0}`)
|
|
867
|
+
void reload()
|
|
868
|
+
} catch (error) {
|
|
869
|
+
report(`import failed: ${error?.stack ?? error}`)
|
|
870
|
+
setFlash(`导入失败:${String(error?.message ?? error)}`)
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
input.click()
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const save = async () => {
|
|
877
|
+
if (!draft || !String(draft.body ?? '').trim()) return
|
|
878
|
+
await api('/save', {
|
|
879
|
+
id: draft.id,
|
|
880
|
+
title: draft.title,
|
|
881
|
+
body: draft.body,
|
|
882
|
+
tags: String(draft.tagsText ?? '').split(/[,,]/).map((tag) => tag.trim()).filter(Boolean),
|
|
883
|
+
})
|
|
884
|
+
setDraft(null)
|
|
885
|
+
void reload()
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function row(item) {
|
|
889
|
+
const vars = varsOf(item.body)
|
|
890
|
+
return h('div', { className: 'dsh-cp-row', key: item.id },
|
|
891
|
+
h('div', {
|
|
892
|
+
className: 'dsh-cp-row-main',
|
|
893
|
+
onClick: () => {
|
|
894
|
+
const vars = varsOf(item.body)
|
|
895
|
+
if (vars.length === 0) { insert(item); return }
|
|
896
|
+
// 内置动态变量(日期/时间/星期)自动取值;全部能自动取就一键直插。
|
|
897
|
+
const values = {}
|
|
898
|
+
let pending = false
|
|
899
|
+
for (const name of vars) {
|
|
900
|
+
const auto = autoValue(name)
|
|
901
|
+
if (auto !== null) values[name] = auto
|
|
902
|
+
else pending = true
|
|
903
|
+
}
|
|
904
|
+
if (!pending) { insert(item, values); return }
|
|
905
|
+
setFill({ item, names: vars, values })
|
|
906
|
+
// {{剪贴板}} 异步预填:读不到就留空手填。
|
|
907
|
+
if (vars.includes('剪贴板')) {
|
|
908
|
+
void navigator.clipboard?.readText?.()
|
|
909
|
+
.then((text) => {
|
|
910
|
+
if (typeof text === 'string' && text) {
|
|
911
|
+
setFill((f) => (f && f.names.includes('剪贴板') && f.values['剪贴板'] === undefined
|
|
912
|
+
? { ...f, values: { ...f.values, 剪贴板: text } }
|
|
913
|
+
: f))
|
|
914
|
+
}
|
|
915
|
+
})
|
|
916
|
+
.catch(() => {})
|
|
917
|
+
}
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
h('div', { className: 'dsh-cp-title' },
|
|
921
|
+
item.title,
|
|
922
|
+
item.uses > 0 ? h('span', { className: 'dsh-cp-uses' }, `${item.uses}次`) : null),
|
|
923
|
+
h('div', { className: 'dsh-cp-preview' }, String(item.body).split('\n')[0].slice(0, 100)),
|
|
924
|
+
vars.length > 0
|
|
925
|
+
? h('div', { className: 'dsh-cp-vars' }, vars.map((name) => h('span', { className: 'dsh-cp-var', key: name }, `{{${name}}}`)))
|
|
926
|
+
: null),
|
|
927
|
+
h('div', { className: 'dsh-cp-row-actions' },
|
|
928
|
+
h('button', { className: 'dsh-cp-mini', title: item.pinned ? '取消收藏' : '收藏', onClick: () => { void pin(item) } }, item.pinned ? '★' : '☆'),
|
|
929
|
+
h('button', {
|
|
930
|
+
className: `dsh-cp-mini${item.slash ? ' active' : ''}`,
|
|
931
|
+
title: item.slash ? '取消 / 命令注册' : '注册为 / 命令(输入 / 按标题搜到)',
|
|
932
|
+
onClick: () => { void toggleSlash(item) },
|
|
933
|
+
}, '/'),
|
|
934
|
+
h('button', {
|
|
935
|
+
className: 'dsh-cp-mini',
|
|
936
|
+
title: '编辑',
|
|
937
|
+
onClick: () => setDraft({ id: item.id, title: item.title, body: item.body, tagsText: (item.tags ?? []).join(', ') }),
|
|
938
|
+
}, '✎'),
|
|
939
|
+
h('button', { className: 'dsh-cp-mini danger', title: '删除', onClick: () => { void remove(item.id) } }, '×')))
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const q = String(query).trim().toLowerCase()
|
|
943
|
+
const all = items ?? []
|
|
944
|
+
const match = (item) => !q || String(item.title).toLowerCase().includes(q) || String(item.body).toLowerCase().includes(q)
|
|
945
|
+
// 组内按最近使用排序:没插过的退回创建时间,新模板仍在顶部
|
|
946
|
+
const byLastUsed = (a, b) => (b.usedAt ?? b.at ?? 0) - (a.usedAt ?? a.at ?? 0)
|
|
947
|
+
const pinned = all.filter((item) => item.pinned && match(item)).sort(byLastUsed)
|
|
948
|
+
const recent = all.filter((item) => !item.pinned && match(item)).sort(byLastUsed)
|
|
949
|
+
|
|
950
|
+
const editor = draft
|
|
951
|
+
? h('div', { className: 'dsh-cp-editor' },
|
|
952
|
+
h('input', {
|
|
953
|
+
className: 'dsh-cp-input',
|
|
954
|
+
placeholder: '标题(留空取正文前 40 字)',
|
|
955
|
+
value: draft.title,
|
|
956
|
+
onInput: (event) => setDraft({ ...draft, title: event.target.value }),
|
|
957
|
+
}),
|
|
958
|
+
h('textarea', {
|
|
959
|
+
className: 'dsh-cp-textarea',
|
|
960
|
+
placeholder: '模板正文,可用 {{变量}} 占位符',
|
|
961
|
+
value: draft.body,
|
|
962
|
+
rows: 8,
|
|
963
|
+
onInput: (event) => setDraft({ ...draft, body: event.target.value }),
|
|
964
|
+
}),
|
|
965
|
+
h('input', {
|
|
966
|
+
className: 'dsh-cp-input',
|
|
967
|
+
placeholder: '标签,逗号分隔',
|
|
968
|
+
value: draft.tagsText ?? '',
|
|
969
|
+
onInput: (event) => setDraft({ ...draft, tagsText: event.target.value }),
|
|
970
|
+
}),
|
|
971
|
+
h('div', { className: 'dsh-cp-actions' },
|
|
972
|
+
h('button', { className: 'dsh-cp-action', onClick: () => setDraft(null) }, '取消'),
|
|
973
|
+
h('button', { className: 'dsh-cp-action primary', onClick: () => { void save() } }, '保存')))
|
|
974
|
+
: null
|
|
975
|
+
|
|
976
|
+
const fillForm = fill
|
|
977
|
+
? h('div', { className: 'dsh-cp-editor' },
|
|
978
|
+
h('div', { className: 'dsh-cp-group' }, `填写「${fill.item.title || '模板'}」的变量`),
|
|
979
|
+
...fill.names.map((name) => h('input', {
|
|
980
|
+
key: name,
|
|
981
|
+
className: 'dsh-cp-input',
|
|
982
|
+
placeholder: `{{${name}}}`,
|
|
983
|
+
value: fill.values[name] ?? '',
|
|
984
|
+
onInput: (event) => setFill({ ...fill, values: { ...fill.values, [name]: event.target.value } }),
|
|
985
|
+
})),
|
|
986
|
+
h('div', { className: 'dsh-cp-actions' },
|
|
987
|
+
h('button', { className: 'dsh-cp-action', onClick: () => setFill(null) }, '取消'),
|
|
988
|
+
h('button', {
|
|
989
|
+
className: 'dsh-cp-action primary',
|
|
990
|
+
onClick: () => { const f = fill; setFill(null); insert(f.item, f.values) },
|
|
991
|
+
}, '插入')))
|
|
992
|
+
: null
|
|
993
|
+
|
|
994
|
+
// portal 容器:自建 div 挂 body——不再直接用 document.body(rc.5 环境里
|
|
995
|
+
// 出现过 React #200 "Target container is not a DOM element",自建元素
|
|
996
|
+
// 结构上必然是 Element,绕开该判定的任何环境怪癖)。
|
|
997
|
+
const portalHost = React.useRef(null)
|
|
998
|
+
if (!portalHost.current && typeof document !== 'undefined' && document.body) {
|
|
999
|
+
try {
|
|
1000
|
+
portalHost.current = document.createElement('div')
|
|
1001
|
+
portalHost.current.setAttribute('data-dsh-superinput-portal', 'prompts')
|
|
1002
|
+
document.body.appendChild(portalHost.current)
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
report(`portal host create failed: ${error?.stack ?? error}`)
|
|
1005
|
+
portalHost.current = null
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
React.useEffect(() => () => { portalHost.current?.remove() }, [])
|
|
1009
|
+
|
|
1010
|
+
return portalHost.current
|
|
1011
|
+
? createPortal(h('div', { className: 'dsh-cp-panel', style: pos ?? { display: 'none' } },
|
|
1012
|
+
h('div', { className: 'dsh-cp-head' },
|
|
1013
|
+
h('input', {
|
|
1014
|
+
ref: inputRef,
|
|
1015
|
+
className: 'dsh-cp-search',
|
|
1016
|
+
placeholder: '搜索模板…',
|
|
1017
|
+
value: query,
|
|
1018
|
+
onInput: (event) => setQuery(event.target.value),
|
|
1019
|
+
}),
|
|
1020
|
+
h('button', { className: 'dsh-cp-new', title: '导出全部模板(JSON)', onClick: () => { void doExport() } }, '↓'),
|
|
1021
|
+
h('button', { className: 'dsh-cp-new', title: '从 JSON 备份导入', onClick: () => doImport() }, '↑'),
|
|
1022
|
+
h('button', {
|
|
1023
|
+
className: 'dsh-cp-new',
|
|
1024
|
+
title: '新建模板',
|
|
1025
|
+
onClick: () => setDraft({ title: '', body: '', tagsText: '' }),
|
|
1026
|
+
}, '+')),
|
|
1027
|
+
editor,
|
|
1028
|
+
fillForm,
|
|
1029
|
+
(draft || fill) ? null : h('div', { className: 'dsh-cp-list' },
|
|
1030
|
+
!items
|
|
1031
|
+
? h('div', { className: 'dsh-cp-empty' }, '加载中…')
|
|
1032
|
+
: (pinned.length === 0 && recent.length === 0)
|
|
1033
|
+
? h('div', { className: 'dsh-cp-empty' }, q ? '没有匹配的模板' : '点右上角 + 新建第一个模板')
|
|
1034
|
+
: [
|
|
1035
|
+
pinned.length > 0 ? h('div', { className: 'dsh-cp-group', key: 'pg' }, '已收藏') : null,
|
|
1036
|
+
...pinned.map(row),
|
|
1037
|
+
pinned.length > 0 && recent.length > 0 ? h('div', { className: 'dsh-cp-group', key: 'rg' }, '全部') : null,
|
|
1038
|
+
...recent.map(row),
|
|
1039
|
+
]),
|
|
1040
|
+
flash ? h('div', { className: 'dsh-cp-empty' }, flash) : null), portalHost.current)
|
|
1041
|
+
: null
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/** 错误边界:面板渲染崩溃时留痕(走 prompts /diag)而不是无声消失。 */
|
|
1045
|
+
class PromptsBoundary extends React.Component {
|
|
1046
|
+
constructor(props) { super(props); this.state = { crashed: false } }
|
|
1047
|
+
static getDerivedStateFromError() { return { crashed: true } }
|
|
1048
|
+
componentDidCatch(error, info) {
|
|
1049
|
+
report(`boundary: ${String(error?.message ?? error).slice(0, 200)} | stack: ${String(error?.stack ?? '').slice(0, 400)}`)
|
|
1050
|
+
}
|
|
1051
|
+
render() { return this.state.crashed ? null : this.props.children }
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/** 详情弹窗:Trae 点 chip 打开的**只读** Dialog(引用全文 + 评论 + 跳转原文)。
|
|
1055
|
+
* 评论不在这个弹窗里编辑——Trae 的评论在选区旁的 previewCommentCard 输入;
|
|
1056
|
+
* 这里只展示结果(与 ai-modules-chat 的 mention-info-modal 同构)。 */
|
|
1057
|
+
function SelectionDetail({ title, text, comment, onNavigate, onClose }) {
|
|
1058
|
+
const maskRef = React.useRef(null)
|
|
1059
|
+
|
|
1060
|
+
/* 桌面壳(Electron)里 body 会变成 0×0 的 fixed 包含块:`inset: 0` 的遮罩
|
|
1061
|
+
* 缩在左下角,弹窗被裁掉。打开后量一次遮罩实际矩形,若没有铺满视口,
|
|
1062
|
+
* 就按测量偏移把遮罩校正回视口原点并铺满——不依赖宿主沿用哪种 portal。 */
|
|
1063
|
+
React.useEffect(() => {
|
|
1064
|
+
const mask = maskRef.current
|
|
1065
|
+
if (!mask || typeof window === 'undefined') return undefined
|
|
1066
|
+
let corrected = false
|
|
1067
|
+
const fix = () => {
|
|
1068
|
+
const vw = window.innerWidth
|
|
1069
|
+
const vh = window.innerHeight
|
|
1070
|
+
if (!corrected) {
|
|
1071
|
+
const rect = mask.getBoundingClientRect()
|
|
1072
|
+
if (rect.width >= vw * 0.8 && rect.height >= vh * 0.8) return
|
|
1073
|
+
corrected = true
|
|
1074
|
+
mask.style.position = 'fixed'
|
|
1075
|
+
mask.style.inset = 'auto'
|
|
1076
|
+
mask.style.left = `${-rect.left}px`
|
|
1077
|
+
mask.style.top = `${-rect.top}px`
|
|
1078
|
+
mask.style.display = 'flex'
|
|
1079
|
+
mask.style.alignItems = 'center'
|
|
1080
|
+
mask.style.justifyContent = 'center'
|
|
1081
|
+
report(`selection dialog: corrected mask ${Math.round(rect.width)}x${Math.round(rect.height)}@${Math.round(rect.left)},${Math.round(rect.top)} to viewport ${vw}x${vh}`)
|
|
1082
|
+
}
|
|
1083
|
+
mask.style.width = `${vw}px`
|
|
1084
|
+
mask.style.height = `${vh}px`
|
|
1085
|
+
mask.style.boxSizing = 'border-box'
|
|
1086
|
+
}
|
|
1087
|
+
fix()
|
|
1088
|
+
window.addEventListener('resize', fix)
|
|
1089
|
+
return () => window.removeEventListener('resize', fix)
|
|
1090
|
+
}, [])
|
|
1091
|
+
|
|
1092
|
+
return h('div', {
|
|
1093
|
+
ref: maskRef,
|
|
1094
|
+
className: 'dsh-si-mask',
|
|
1095
|
+
onMouseDown: (event) => { event.stopPropagation(); onClose?.() },
|
|
1096
|
+
},
|
|
1097
|
+
h('div', {
|
|
1098
|
+
className: 'dsh-si-dialog',
|
|
1099
|
+
onMouseDown: (event) => event.stopPropagation(),
|
|
1100
|
+
onClick: (event) => event.stopPropagation(),
|
|
1101
|
+
},
|
|
1102
|
+
h('div', { className: 'dsh-si-dialog-head' },
|
|
1103
|
+
h('span', { className: 'dsh-si-dialog-title' }, title),
|
|
1104
|
+
h('button', { className: 'dsh-si-dialog-x', 'aria-label': '关闭', onClick: () => onClose?.() }, '×')),
|
|
1105
|
+
h('div', { className: 'dsh-si-dialog-quote' }, text),
|
|
1106
|
+
comment ? h(React.Fragment, null,
|
|
1107
|
+
h('div', { className: 'dsh-si-dialog-label' }, '评论'),
|
|
1108
|
+
h('div', { className: 'dsh-si-dialog-detail' }, comment)) : null,
|
|
1109
|
+
h('div', { className: 'dsh-si-dialog-foot' },
|
|
1110
|
+
onNavigate ? h('button', { className: 'dsh-si-dialog-btn', onClick: onNavigate }, '跳转原文') : null,
|
|
1111
|
+
h('span', { className: 'dsh-si-dialog-spacer' }),
|
|
1112
|
+
h('button', { className: 'dsh-si-dialog-btn ghost', onClick: () => onClose?.() }, '关闭')))
|
|
1113
|
+
)
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/** 划选评论编辑器:照抄 Trae CommentEditorCard——无标题/无预览/无取消钮,
|
|
1117
|
+
* 单行 = 透明自增 textarea + ✓ 按钮;Enter 提交、Shift+Enter 换行、
|
|
1118
|
+
* Esc/点外取消;IME 组合期不提交。锚在选区旁,不遮罩。 */
|
|
1119
|
+
function SelectionCommentEditor({ draft, onSave, onCancel }) {
|
|
1120
|
+
const [value, setValue] = React.useState(draft.comment ?? '')
|
|
1121
|
+
const cardRef = React.useRef(null)
|
|
1122
|
+
const inputRef = React.useRef(null)
|
|
1123
|
+
const valueRef = React.useRef(value)
|
|
1124
|
+
React.useEffect(() => { valueRef.current = value }, [value])
|
|
1125
|
+
|
|
1126
|
+
// textarea 自增高(Trae 同款:height auto → scrollHeight,上限 140)
|
|
1127
|
+
React.useEffect(() => {
|
|
1128
|
+
const el = inputRef.current
|
|
1129
|
+
if (!el) return
|
|
1130
|
+
el.style.height = 'auto'
|
|
1131
|
+
el.style.height = `${Math.min(el.scrollHeight, 140)}px`
|
|
1132
|
+
}, [value])
|
|
1133
|
+
|
|
1134
|
+
React.useEffect(() => {
|
|
1135
|
+
const card = cardRef.current
|
|
1136
|
+
if (!card || typeof window === 'undefined') return
|
|
1137
|
+
const vw = window.innerWidth
|
|
1138
|
+
const vh = window.innerHeight
|
|
1139
|
+
const margin = 8
|
|
1140
|
+
const width = Math.min(320, vw - margin * 2)
|
|
1141
|
+
card.style.width = `${width}px`
|
|
1142
|
+
const height = card.getBoundingClientRect().height || 44
|
|
1143
|
+
const rect = draft.rect ?? {}
|
|
1144
|
+
let left = Math.min(Math.max(margin, (rect.left ?? 16) + 20), Math.max(margin, vw - width - margin))
|
|
1145
|
+
let top = (rect.bottom ?? 80) + 8
|
|
1146
|
+
if (top + height > vh - margin) top = Math.max(margin, (rect.top ?? 80) - height - 8)
|
|
1147
|
+
// 先放到期望位置;桌面壳若把 fixed 包含块变成 0×0,再按实际偏差校正。
|
|
1148
|
+
card.style.visibility = 'hidden'
|
|
1149
|
+
card.style.left = `${left}px`
|
|
1150
|
+
card.style.top = `${top}px`
|
|
1151
|
+
const actual = card.getBoundingClientRect()
|
|
1152
|
+
const dx = actual.left - left
|
|
1153
|
+
const dy = actual.top - top
|
|
1154
|
+
if (Math.abs(dx) > 1) card.style.left = `${left - dx}px`
|
|
1155
|
+
if (Math.abs(dy) > 1) card.style.top = `${top - dy}px`
|
|
1156
|
+
card.style.visibility = 'visible'
|
|
1157
|
+
const textarea = card.querySelector('textarea')
|
|
1158
|
+
if (textarea) {
|
|
1159
|
+
textarea.focus()
|
|
1160
|
+
textarea.setSelectionRange?.(textarea.value.length, textarea.value.length)
|
|
1161
|
+
}
|
|
1162
|
+
}, [draft])
|
|
1163
|
+
|
|
1164
|
+
React.useEffect(() => {
|
|
1165
|
+
const onDown = (event) => {
|
|
1166
|
+
if (cardRef.current?.contains(event.target)) return
|
|
1167
|
+
onCancel?.()
|
|
1168
|
+
}
|
|
1169
|
+
document.addEventListener('mousedown', onDown, true)
|
|
1170
|
+
return () => document.removeEventListener('mousedown', onDown, true)
|
|
1171
|
+
}, [onCancel])
|
|
1172
|
+
|
|
1173
|
+
const submit = () => {
|
|
1174
|
+
if (!value.trim()) return
|
|
1175
|
+
onSave?.(value)
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
return createPortal(h('div', {
|
|
1179
|
+
ref: cardRef,
|
|
1180
|
+
className: 'dsh-si-comment-editor',
|
|
1181
|
+
onMouseDown: (event) => event.stopPropagation(),
|
|
1182
|
+
onClick: (event) => event.stopPropagation(),
|
|
1183
|
+
},
|
|
1184
|
+
h('div', { className: 'dsh-si-comment-row' },
|
|
1185
|
+
h('textarea', {
|
|
1186
|
+
ref: inputRef,
|
|
1187
|
+
className: 'dsh-si-comment-input',
|
|
1188
|
+
rows: 1,
|
|
1189
|
+
value,
|
|
1190
|
+
placeholder: '输入你的评论...',
|
|
1191
|
+
'aria-label': '输入你的评论...',
|
|
1192
|
+
onInput: (event) => setValue(event.target.value),
|
|
1193
|
+
onKeyDown: (event) => {
|
|
1194
|
+
if (event.nativeEvent?.isComposing || event.nativeEvent?.keyCode === 229) return
|
|
1195
|
+
event.stopPropagation()
|
|
1196
|
+
if (event.key === 'Escape') { event.preventDefault(); onCancel?.() }
|
|
1197
|
+
else if (event.key === 'Enter' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
1198
|
+
event.preventDefault()
|
|
1199
|
+
submit()
|
|
1200
|
+
}
|
|
1201
|
+
},
|
|
1202
|
+
}),
|
|
1203
|
+
h('button', {
|
|
1204
|
+
className: 'dsh-si-comment-submit',
|
|
1205
|
+
title: draft.comment ? '保存' : '评论到对话',
|
|
1206
|
+
'aria-label': draft.comment ? '保存' : '评论到对话',
|
|
1207
|
+
disabled: !value.trim(),
|
|
1208
|
+
onClick: submit,
|
|
1209
|
+
},
|
|
1210
|
+
h('svg', { width: 14, height: 14, viewBox: '0 0 16 16', 'aria-hidden': true },
|
|
1211
|
+
h('path', {
|
|
1212
|
+
d: 'M13.5 4.5L6.5 11.5L2.5 7.5',
|
|
1213
|
+
fill: 'none', stroke: 'currentColor', 'stroke-width': '1.8',
|
|
1214
|
+
'stroke-linecap': 'round', 'stroke-linejoin': 'round',
|
|
1215
|
+
}))))
|
|
1216
|
+
), document.body)
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/** 划选引用桥:把「添加到对话」插成输入框内的官方 reference chip。
|
|
1220
|
+
* Trae 的引用是编辑器里的内联节点(conversation-selection-mention-symbol),
|
|
1221
|
+
* dsh 对外只开放 reference chip 这一条内联通道——这里注册自定义 source,
|
|
1222
|
+
* 用 slash/input-insert-reference 落 chip,并按 Trae 规格重着色。
|
|
1223
|
+
* 输入框上方不再有独立 pill 行。 */
|
|
1224
|
+
function SelectionContextBridge({ session, useInput, inputActions }) {
|
|
1225
|
+
const sessionId = session?.sessionId === undefined ? '' : String(session.sessionId)
|
|
1226
|
+
const draftRev = useInput((state) => state?.draftRev ?? 0)
|
|
1227
|
+
const occurrences = useInput((state) => state?.occurrences)
|
|
1228
|
+
const phase = useInput((state) => state?.phase)
|
|
1229
|
+
const snapshotRef = React.useRef({ draftRev, occurrences })
|
|
1230
|
+
const actionsRef = React.useRef(inputActions)
|
|
1231
|
+
const previousPhase = React.useRef(phase)
|
|
1232
|
+
const [, setVersion] = React.useState(0)
|
|
1233
|
+
const [dialog, setDialog] = React.useState(null)
|
|
1234
|
+
const [editor, setEditor] = React.useState(null)
|
|
1235
|
+
const bump = () => setVersion((value) => value + 1)
|
|
1236
|
+
|
|
1237
|
+
React.useEffect(() => {
|
|
1238
|
+
snapshotRef.current = { draftRev, occurrences }
|
|
1239
|
+
actionsRef.current = inputActions
|
|
1240
|
+
}, [draftRev, occurrences, inputActions])
|
|
1241
|
+
|
|
1242
|
+
React.useEffect(() => {
|
|
1243
|
+
if (!sessionId || !inputActions) return undefined
|
|
1244
|
+
selectionActions.set(sessionId, inputActions)
|
|
1245
|
+
return () => {
|
|
1246
|
+
if (selectionActions.get(sessionId) === inputActions) selectionActions.delete(sessionId)
|
|
1247
|
+
}
|
|
1248
|
+
}, [sessionId, inputActions])
|
|
1249
|
+
|
|
1250
|
+
/** 发送意图:把当前 chip 立即同步给服务端(pre-step 读取),并标记发送。 */
|
|
1251
|
+
const flushForSend = () => {
|
|
1252
|
+
if (!sessionId) return
|
|
1253
|
+
const live = liveSelectionItems(sessionId, snapshotRef.current.occurrences)
|
|
1254
|
+
if (live.length === 0) return
|
|
1255
|
+
syncSelectionContext(sessionId, live, true)
|
|
1256
|
+
for (const item of live) item.sent = true
|
|
1257
|
+
persistSelectionItems()
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/** 插入/更新一条选段 chip;comment === undefined 表示「不改已有评论」。 */
|
|
1261
|
+
const insertSelection = (text, anchorEl, comment) => {
|
|
1262
|
+
const clean = String(text ?? '').trim().slice(0, CONTEXT_TEXT_MAX)
|
|
1263
|
+
if (!clean) return { status: 'empty' }
|
|
1264
|
+
const live = liveSelectionItems(sessionId, snapshotRef.current.occurrences)
|
|
1265
|
+
const existing = live.find((entry) => entry.text === clean)
|
|
1266
|
+
if (existing) {
|
|
1267
|
+
if (comment !== undefined) {
|
|
1268
|
+
const next = String(comment ?? '').slice(0, CONTEXT_COMMENT_MAX).trim()
|
|
1269
|
+
if (next !== (existing.comment ?? '')) {
|
|
1270
|
+
existing.comment = next
|
|
1271
|
+
const span = chipSpanOf(snapshotRef.current.occurrences, existing.id, snapshotRef.current.draftRev)
|
|
1272
|
+
if (span) applySelectionInsert(sessionId, existing, span)
|
|
1273
|
+
bump()
|
|
1274
|
+
syncSelectionContext(sessionId, liveSelectionItems(sessionId, snapshotRef.current.occurrences), false)
|
|
1275
|
+
persistSelectionItems()
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return { status: 'dup', id: existing.id, item: existing }
|
|
1279
|
+
}
|
|
1280
|
+
if (live.length >= CONTEXT_MAX) return { status: 'full' }
|
|
1281
|
+
const actions = actionsRef.current
|
|
1282
|
+
if (typeof actions?.captureInsertion !== 'function') return { status: 'failed' }
|
|
1283
|
+
let span
|
|
1284
|
+
try {
|
|
1285
|
+
const insertion = actions.captureInsertion()
|
|
1286
|
+
// 折叠到选区末尾:只插入、不替换用户已选中的草稿文字。
|
|
1287
|
+
span = { start: insertion.end, end: insertion.end, draftRev: insertion.draftRev }
|
|
1288
|
+
} catch { return { status: 'failed' } }
|
|
1289
|
+
// 显示序号按当前输入框里的条目数重排(Trae 每个草稿从 1 开始)。
|
|
1290
|
+
const seq = live.reduce((max, entry) => Math.max(max, entry.seq ?? 0), 0) + 1
|
|
1291
|
+
const id = `${Date.now().toString(36)}${(++selectionIdSeq).toString(36)}`
|
|
1292
|
+
const item = {
|
|
1293
|
+
id, sessionId, seq,
|
|
1294
|
+
text: clean,
|
|
1295
|
+
comment: comment === undefined ? '' : String(comment ?? '').slice(0, CONTEXT_COMMENT_MAX).trim(),
|
|
1296
|
+
anchor: anchorEl instanceof Element ? new WeakRef(anchorEl) : null,
|
|
1297
|
+
sent: false,
|
|
1298
|
+
archived: false,
|
|
1299
|
+
at: Date.now(),
|
|
1300
|
+
}
|
|
1301
|
+
if (!applySelectionInsert(sessionId, item, span)) return { status: 'failed' }
|
|
1302
|
+
selectionItems.set(id, item)
|
|
1303
|
+
bump()
|
|
1304
|
+
syncSelectionContext(sessionId, [...live, item], false)
|
|
1305
|
+
persistSelectionItems()
|
|
1306
|
+
return { status: 'added', id, item }
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/** 删除一条选段:chip + 条目 + 服务端同步(hover × / 编辑器移除共用)。 */
|
|
1310
|
+
const dropSelectionItem = (item) => {
|
|
1311
|
+
if (!item) return
|
|
1312
|
+
const span = chipSpanOf(snapshotRef.current.occurrences, item.id, snapshotRef.current.draftRev)
|
|
1313
|
+
if (span) applySelectionRemove(item.sessionId, item, span)
|
|
1314
|
+
selectionItems.delete(item.id)
|
|
1315
|
+
bump()
|
|
1316
|
+
syncSelectionContext(item.sessionId, liveSelectionItems(item.sessionId, snapshotRef.current.occurrences), false)
|
|
1317
|
+
persistSelectionItems()
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
React.useEffect(() => {
|
|
1321
|
+
const was = previousPhase.current
|
|
1322
|
+
previousPhase.current = phase
|
|
1323
|
+
if (was !== 'submitting' && phase === 'submitting') flushForSend()
|
|
1324
|
+
}, [phase, sessionId])
|
|
1325
|
+
|
|
1326
|
+
// 发送按钮 / Enter 比 phase 更早、更可靠;window capture 先于宿主监听。
|
|
1327
|
+
React.useEffect(() => {
|
|
1328
|
+
if (!sessionId) return undefined
|
|
1329
|
+
const onClick = (event) => {
|
|
1330
|
+
const target = event.target
|
|
1331
|
+
if (target?.closest?.('button[aria-label*="发送消息"], button[aria-label*="Send"], button[aria-label*="send"]')) flushForSend()
|
|
1332
|
+
}
|
|
1333
|
+
const onKey = (event) => {
|
|
1334
|
+
if (event.key !== 'Enter' || event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) return
|
|
1335
|
+
if (!event.target?.closest?.('[contenteditable="true"]')) return
|
|
1336
|
+
flushForSend()
|
|
1337
|
+
}
|
|
1338
|
+
window.addEventListener('click', onClick, true)
|
|
1339
|
+
window.addEventListener('keydown', onKey, true)
|
|
1340
|
+
return () => {
|
|
1341
|
+
window.removeEventListener('click', onClick, true)
|
|
1342
|
+
window.removeEventListener('keydown', onKey, true)
|
|
1343
|
+
}
|
|
1344
|
+
}, [sessionId])
|
|
1345
|
+
|
|
1346
|
+
// 草稿里的 chip 被键盘删掉 / 发送后清空:以 occurrences 为真相修剪条目。
|
|
1347
|
+
React.useEffect(() => {
|
|
1348
|
+
if (!sessionId) return
|
|
1349
|
+
const refs = new Set()
|
|
1350
|
+
for (const occ of occurrences ?? []) if (occ.source === SELECTION_SOURCE) refs.add(occ.ref)
|
|
1351
|
+
let changed = false
|
|
1352
|
+
for (const [id, item] of selectionItems) {
|
|
1353
|
+
if (item.sessionId !== sessionId) continue
|
|
1354
|
+
if (item.archived) {
|
|
1355
|
+
// 草稿恢复出的历史 chip:重新激活,参与同步
|
|
1356
|
+
if (refs.has(id)) { item.archived = false; item.sent = false; changed = true }
|
|
1357
|
+
continue
|
|
1358
|
+
}
|
|
1359
|
+
if (refs.has(id)) continue
|
|
1360
|
+
if (item.sent) { item.archived = true; changed = true; continue }
|
|
1361
|
+
selectionItems.delete(id)
|
|
1362
|
+
changed = true
|
|
1363
|
+
}
|
|
1364
|
+
if (changed) bump()
|
|
1365
|
+
syncSelectionContext(sessionId, liveSelectionItems(sessionId, occurrences), false)
|
|
1366
|
+
persistSelectionItems()
|
|
1367
|
+
}, [occurrences, sessionId])
|
|
1368
|
+
|
|
1369
|
+
// 详情弹窗请求(chip 点击 → source.openReference;添加成功后也会主动打开)。
|
|
1370
|
+
React.useEffect(() => {
|
|
1371
|
+
if (!sessionId) return undefined
|
|
1372
|
+
const listener = () => {
|
|
1373
|
+
const request = selectionDetailRequest
|
|
1374
|
+
if (!request || request.sessionId !== sessionId) return
|
|
1375
|
+
selectionDetailRequest = null
|
|
1376
|
+
const item = selectionItems.get(request.id)
|
|
1377
|
+
if (!item) return
|
|
1378
|
+
setEditor(null)
|
|
1379
|
+
setDialog({ id: request.id })
|
|
1380
|
+
}
|
|
1381
|
+
selectionDetailSubs.add(listener)
|
|
1382
|
+
listener()
|
|
1383
|
+
return () => { selectionDetailSubs.delete(listener) }
|
|
1384
|
+
}, [sessionId])
|
|
1385
|
+
|
|
1386
|
+
// Trae 的两个入口(previewCommentCard):
|
|
1387
|
+
// - 添加到对话:先插 chip,再把选区旁的卡片原位切成评论编辑器(addThenComment);
|
|
1388
|
+
// - 评论到对话:先弹评论卡片,保存后才插 chip(带评论)。
|
|
1389
|
+
React.useEffect(() => {
|
|
1390
|
+
if (!sessionId) return undefined
|
|
1391
|
+
const add = (text, anchorEl, rect) => {
|
|
1392
|
+
const result = insertSelection(text, anchorEl, undefined)
|
|
1393
|
+
if (result.status === 'added' || result.status === 'dup') {
|
|
1394
|
+
const item = result.item ?? (result.id ? selectionItems.get(result.id) : null)
|
|
1395
|
+
if (item) {
|
|
1396
|
+
// Trae addThenComment:chips 已经进输入框,卡片原位切换为评论编辑。
|
|
1397
|
+
setDialog(null)
|
|
1398
|
+
setEditor({
|
|
1399
|
+
id: item.id,
|
|
1400
|
+
text: item.text,
|
|
1401
|
+
comment: item.comment ?? '',
|
|
1402
|
+
anchorEl: anchorEl instanceof Element ? anchorEl : null,
|
|
1403
|
+
rect: normalizeSelectionRect(rect, anchorEl),
|
|
1404
|
+
})
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
return result.status
|
|
1408
|
+
}
|
|
1409
|
+
const openComment = (text, anchorEl, rect) => {
|
|
1410
|
+
const clean = String(text ?? '').trim().slice(0, CONTEXT_TEXT_MAX)
|
|
1411
|
+
if (!clean) return 'empty'
|
|
1412
|
+
const live = liveSelectionItems(sessionId, snapshotRef.current.occurrences)
|
|
1413
|
+
const existing = live.find((entry) => entry.text === clean)
|
|
1414
|
+
if (!existing && live.length >= CONTEXT_MAX) return 'full'
|
|
1415
|
+
setDialog(null)
|
|
1416
|
+
setEditor({
|
|
1417
|
+
id: existing?.id ?? null,
|
|
1418
|
+
text: clean,
|
|
1419
|
+
comment: existing?.comment ?? '',
|
|
1420
|
+
anchorEl: anchorEl instanceof Element ? anchorEl : null,
|
|
1421
|
+
rect: normalizeSelectionRect(rect, anchorEl),
|
|
1422
|
+
})
|
|
1423
|
+
return 'opened'
|
|
1424
|
+
}
|
|
1425
|
+
window.__dshSuperinputAddContext = add
|
|
1426
|
+
window.__dshSuperinputCommentContext = openComment
|
|
1427
|
+
// Trae dragDropMention 同义:拖放进来的引用只插 chip,不弹评论卡
|
|
1428
|
+
//(拖拽是快速引用手势;要评论走划选菜单的「评论到对话」)。
|
|
1429
|
+
window.__dshSuperinputDropContext = (text, anchorEl) => insertSelection(text, anchorEl, undefined)
|
|
1430
|
+
return () => {
|
|
1431
|
+
if (window.__dshSuperinputAddContext === add) delete window.__dshSuperinputAddContext
|
|
1432
|
+
if (window.__dshSuperinputCommentContext === openComment) delete window.__dshSuperinputCommentContext
|
|
1433
|
+
if (window.__dshSuperinputDropContext === insertSelection) delete window.__dshSuperinputDropContext
|
|
1434
|
+
}
|
|
1435
|
+
}, [sessionId])
|
|
1436
|
+
|
|
1437
|
+
// chip 悬停时左侧图标变 ×(Trae remove-mention-icon);点左侧 20px 移除。
|
|
1438
|
+
React.useEffect(() => {
|
|
1439
|
+
if (!sessionId) return undefined
|
|
1440
|
+
const onClick = (event) => {
|
|
1441
|
+
const target = event.target
|
|
1442
|
+
const chip = target?.closest?.('span[title]')
|
|
1443
|
+
if (!chip || !chip.closest?.('[contenteditable="true"]')) return
|
|
1444
|
+
if (!String(chip.title || '').startsWith(CHIP_MARK)) return
|
|
1445
|
+
const rect = chip.getBoundingClientRect()
|
|
1446
|
+
if (event.clientX > rect.left + 20 || !chip.matches(':hover')) return
|
|
1447
|
+
const label = String(chip.title)
|
|
1448
|
+
const item = [...selectionItems.values()].find((entry) => entry.sessionId === sessionId && chipLabelOf(entry) === label)
|
|
1449
|
+
if (!item) return
|
|
1450
|
+
event.preventDefault()
|
|
1451
|
+
event.stopPropagation()
|
|
1452
|
+
dropSelectionItem(item)
|
|
1453
|
+
}
|
|
1454
|
+
document.addEventListener('click', onClick, true)
|
|
1455
|
+
return () => document.removeEventListener('click', onClick, true)
|
|
1456
|
+
}, [sessionId])
|
|
1457
|
+
|
|
1458
|
+
const navigateTo = (item) => {
|
|
1459
|
+
try {
|
|
1460
|
+
const el = item.anchor?.deref?.()
|
|
1461
|
+
if (el?.isConnected) {
|
|
1462
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
1463
|
+
el.classList.add('dsh-cp-flash')
|
|
1464
|
+
setTimeout(() => el.classList.remove('dsh-cp-flash'), 1600)
|
|
1465
|
+
}
|
|
1466
|
+
} catch { /* 锚点失效:静默降级 */ }
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/** 评论卡片保存:已有条目更新评论,新条目插 chip(带评论)。 */
|
|
1470
|
+
const saveEditorComment = (comment) => {
|
|
1471
|
+
const draft = editor
|
|
1472
|
+
if (!draft) return
|
|
1473
|
+
const next = String(comment ?? '').slice(0, CONTEXT_COMMENT_MAX).trim()
|
|
1474
|
+
const existing = draft.id ? selectionItems.get(draft.id) : null
|
|
1475
|
+
if (existing) {
|
|
1476
|
+
if (next !== (existing.comment ?? '')) {
|
|
1477
|
+
existing.comment = next
|
|
1478
|
+
const span = chipSpanOf(snapshotRef.current.occurrences, existing.id, snapshotRef.current.draftRev)
|
|
1479
|
+
if (span) applySelectionInsert(existing.sessionId, existing, span)
|
|
1480
|
+
bump()
|
|
1481
|
+
syncSelectionContext(existing.sessionId, liveSelectionItems(existing.sessionId, snapshotRef.current.occurrences), false)
|
|
1482
|
+
persistSelectionItems()
|
|
1483
|
+
window.__dshSuperinputToast?.('已保存评论')
|
|
1484
|
+
}
|
|
1485
|
+
} else {
|
|
1486
|
+
const result = insertSelection(draft.text, draft.anchorEl, next)
|
|
1487
|
+
if (result.status === 'added' || result.status === 'dup') window.__dshSuperinputToast?.('已添加到对话')
|
|
1488
|
+
else if (result.status === 'full') window.__dshSuperinputToast?.(`最多添加 ${CONTEXT_MAX} 段对话内容`)
|
|
1489
|
+
else if (result.status === 'failed') window.__dshSuperinputToast?.('保存失败')
|
|
1490
|
+
}
|
|
1491
|
+
setEditor(null)
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
const dialogItem = dialog ? selectionItems.get(dialog.id) : null
|
|
1495
|
+
const editorNode = editor ? h(SelectionCommentEditor, {
|
|
1496
|
+
key: `comment:${editor.id ?? 'new'}:${editor.text}`,
|
|
1497
|
+
draft: editor,
|
|
1498
|
+
onSave: saveEditorComment,
|
|
1499
|
+
onCancel: () => setEditor(null),
|
|
1500
|
+
}) : null
|
|
1501
|
+
const detailNode = dialogItem ? createPortal(h(SelectionDetail, {
|
|
1502
|
+
title: `${dialogItem.seq}. ${previewOf(dialogItem.text, 48)}`,
|
|
1503
|
+
text: dialogItem.text,
|
|
1504
|
+
comment: dialogItem.comment,
|
|
1505
|
+
onNavigate: dialogItem.anchor ? () => navigateTo(dialogItem) : null,
|
|
1506
|
+
onClose: () => setDialog(null),
|
|
1507
|
+
}), document.body) : null
|
|
1508
|
+
if (!editorNode && !detailNode) return null
|
|
1509
|
+
return h(React.Fragment, null, editorNode, detailNode)
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/** 消息正文的选段 chip:user-text 规则把 token 换成同款标签(Trae 消息里的
|
|
1513
|
+
* mention chip 同义)。刷新后条目由 sessionStorage 恢复。 */
|
|
1514
|
+
const selectionUserTextRule = {
|
|
1515
|
+
id: SELECTION_RULE,
|
|
1516
|
+
order: 5,
|
|
1517
|
+
match(text, from) {
|
|
1518
|
+
const re = /\[选段#([0-9a-z]+)\]/g
|
|
1519
|
+
re.lastIndex = from
|
|
1520
|
+
const found = re.exec(text)
|
|
1521
|
+
if (!found) return undefined
|
|
1522
|
+
const item = selectionItems.get(found[1])
|
|
1523
|
+
if (!item) return undefined
|
|
1524
|
+
return {
|
|
1525
|
+
id: SELECTION_RULE,
|
|
1526
|
+
start: found.index,
|
|
1527
|
+
end: found.index + found[0].length,
|
|
1528
|
+
data: { id: item.id, seq: item.seq, text: item.text, comment: item.comment },
|
|
1529
|
+
}
|
|
1530
|
+
},
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function SelectionMessageChip({ node, interactive }) {
|
|
1534
|
+
const data = node?.data ?? {}
|
|
1535
|
+
const [open, setOpen] = React.useState(false)
|
|
1536
|
+
const label = chipTextOf(data.seq, data.text, data.comment)
|
|
1537
|
+
return h(React.Fragment, null,
|
|
1538
|
+
h('span', {
|
|
1539
|
+
className: `dsh-si-msgchip${interactive ? ' interactive' : ''}`,
|
|
1540
|
+
title: data.text,
|
|
1541
|
+
onClick: interactive ? () => setOpen(true) : undefined,
|
|
1542
|
+
},
|
|
1543
|
+
h('span', { className: 'dsh-si-msgchip-icon', 'aria-hidden': true }),
|
|
1544
|
+
h('span', { className: 'dsh-si-msgchip-label' }, label)),
|
|
1545
|
+
open ? createPortal(h(SelectionDetail, {
|
|
1546
|
+
title: label,
|
|
1547
|
+
text: data.text,
|
|
1548
|
+
comment: data.comment,
|
|
1549
|
+
onClose: () => setOpen(false),
|
|
1550
|
+
}), document.body) : null)
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/** 草稿桥:划选「添加评论」、模板插入、/ 命令插入、横条「引用文本」共用。
|
|
1554
|
+
* 挂 conversation.input.dock——该槽位会注入 useInput / inputActions;
|
|
1555
|
+
* composer.dock 不注入这些 props。片段模块移除时桥被一并删掉,这里补回。 */
|
|
1556
|
+
function PromptsDraftBridge({ useInput, inputActions }) {
|
|
1557
|
+
const draft = useInput((state) => state?.draft)
|
|
1558
|
+
React.useEffect(() => {
|
|
1559
|
+
window.__dshSuperinputDraft = () => (typeof draft === 'string' ? draft : '')
|
|
1560
|
+
if (typeof inputActions?.setDraft === 'function') {
|
|
1561
|
+
window.__dshSuperinputSetDraft = (value) => {
|
|
1562
|
+
try { inputActions.setDraft(value) } catch { /* ignore */ }
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
// 官方「引用文本」同款 API(0.2.2-rc.1 起可用):plain 模式下插 blockquote
|
|
1566
|
+
window.__dshSuperinputInsertQuote = (text) => {
|
|
1567
|
+
try { return typeof inputActions?.insertQuote === 'function' && inputActions.insertQuote(text) === true } catch { return false }
|
|
1568
|
+
}
|
|
1569
|
+
}, [draft, inputActions])
|
|
1570
|
+
return null
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function PromptsButton({ onSyncSlash }) {
|
|
1574
|
+
const [open, setOpen] = React.useState(false)
|
|
1575
|
+
const ref = React.useRef(null)
|
|
1576
|
+
return h('span', { ref, style: { display: 'inline-flex' } },
|
|
1577
|
+
h('button', {
|
|
1578
|
+
className: 'dsh-cp-btn',
|
|
1579
|
+
title: 'Prompt(提示词模板)',
|
|
1580
|
+
onClick: () => setOpen((value) => !value),
|
|
1581
|
+
}, 'Prompt'),
|
|
1582
|
+
open ? h(PromptsPanel, { anchor: ref.current, onClose: () => setOpen(false), onSyncSlash }) : null)
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
return {
|
|
1586
|
+
inject: ['slots', 'sessions', 'uiConversation'],
|
|
1587
|
+
apply(ctx) {
|
|
1588
|
+
ensureStyle()
|
|
1589
|
+
installSelectionSaver()
|
|
1590
|
+
|
|
1591
|
+
/* / 斜杠命令同步:标记了 slash 的模板注册进官方 / 菜单(action 命令)。
|
|
1592
|
+
* commandUi 是官方 ui-commands 的客户端服务;旧宿主没有它时 inject 不解析,整块跳过。
|
|
1593
|
+
* 按模板 opt-in 而不是全量注册,避免几十条模板淹没 / 菜单;同名冲突 register 会
|
|
1594
|
+
* 抛错(官方约定 fail loud),这里 try/catch 吞掉并留痕,不影响其它模板。 */
|
|
1595
|
+
const slashDisposers = new Map() // id -> { dispose, fp }
|
|
1596
|
+
let commandUi = null
|
|
1597
|
+
const slashName = (item) => {
|
|
1598
|
+
const slug = String(item.title ?? '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24)
|
|
1599
|
+
return `tpl-${slug || String(item.id ?? '').slice(0, 8)}`
|
|
1600
|
+
}
|
|
1601
|
+
const slashFp = (item) => `${slashName(item)}|${item.title}`
|
|
1602
|
+
const syncSlash = async () => {
|
|
1603
|
+
if (!commandUi) return
|
|
1604
|
+
let items = []
|
|
1605
|
+
try {
|
|
1606
|
+
const data = await api('/list')
|
|
1607
|
+
items = Array.isArray(data?.items) ? data.items : []
|
|
1608
|
+
} catch { return }
|
|
1609
|
+
const wanted = new Map(items.filter((item) => item.slash === true).map((item) => [item.id, item]))
|
|
1610
|
+
for (const [id, entry] of slashDisposers) {
|
|
1611
|
+
const fresh = wanted.get(id)
|
|
1612
|
+
if (!fresh || entry.fp !== slashFp(fresh)) {
|
|
1613
|
+
entry.dispose?.()
|
|
1614
|
+
slashDisposers.delete(id)
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
for (const [id, item] of wanted) {
|
|
1618
|
+
if (slashDisposers.has(id)) continue
|
|
1619
|
+
try {
|
|
1620
|
+
const dispose = commandUi.register({
|
|
1621
|
+
name: slashName(item),
|
|
1622
|
+
label: () => item.title,
|
|
1623
|
+
description: () => '插入提示词模板',
|
|
1624
|
+
available: () => true,
|
|
1625
|
+
ui: { kind: 'action', run: () => insertViaBridge(item) },
|
|
1626
|
+
})
|
|
1627
|
+
slashDisposers.set(id, { dispose: typeof dispose === 'function' ? dispose : null, fp: slashFp(item) })
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
report(`slash register "${item.title}": ${error?.message ?? error}`)
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
ctx.inject(['commandUi'], (scope) => {
|
|
1634
|
+
commandUi = scope.get('commandUi')
|
|
1635
|
+
void syncSlash()
|
|
1636
|
+
})
|
|
1637
|
+
|
|
1638
|
+
ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
|
|
1639
|
+
name: 'conversation.composer.dock',
|
|
1640
|
+
id: 'dsh-superinput-prompts-btn',
|
|
1641
|
+
order: 4,
|
|
1642
|
+
}, (props) => h(PromptsBoundary, null, h(PromptsButton, { ...props, onSyncSlash: syncSlash }))))
|
|
1643
|
+
|
|
1644
|
+
/* 划选引用的官方 input source:codec 决定 chip 的粘贴文本与送模文本;
|
|
1645
|
+
* candidates 恒空——只是借 @ 触发器的注册位拿到一个合法的自定义
|
|
1646
|
+
* source(官方 chip 插入/序列化按 source 名路由),不往 @ 菜单加条目。 */
|
|
1647
|
+
const selectionSource = {
|
|
1648
|
+
trigger: '@',
|
|
1649
|
+
name: SELECTION_SOURCE,
|
|
1650
|
+
showGroupTitle: false,
|
|
1651
|
+
async candidates() { return [] },
|
|
1652
|
+
onPick() { return undefined },
|
|
1653
|
+
openReference(session, reference) {
|
|
1654
|
+
const targetSession = session?.sessionId === undefined ? '' : String(session.sessionId)
|
|
1655
|
+
const ref = String(reference?.ref ?? '')
|
|
1656
|
+
if (!targetSession || !selectionItems.has(ref)) return false
|
|
1657
|
+
requestSelectionDetail(targetSession, ref, false)
|
|
1658
|
+
return true
|
|
1659
|
+
},
|
|
1660
|
+
codec: {
|
|
1661
|
+
clipboardText: (ref) => tokenOf(ref),
|
|
1662
|
+
serialize: async (ref) => tokenOf(ref),
|
|
1663
|
+
},
|
|
1664
|
+
}
|
|
1665
|
+
ctx.inject(['inputTriggers'], (scope) => {
|
|
1666
|
+
try {
|
|
1667
|
+
const service = scope.get('inputTriggers')
|
|
1668
|
+
ctx.effect(() => service.registerSource(selectionSource), 'dsh-superinput: selection source')
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
report(`selection source register failed: ${error?.message ?? error}`)
|
|
1671
|
+
}
|
|
1672
|
+
})
|
|
1673
|
+
ctx.inject(['sessions'], (scope) => {
|
|
1674
|
+
selectionSessions = scope.get('sessions')
|
|
1675
|
+
})
|
|
1676
|
+
|
|
1677
|
+
// 消息正文里的同款 chip:user-text 规则 + 节点渲染槽位。
|
|
1678
|
+
ctx.inject(['uiConversation'], (scope) => {
|
|
1679
|
+
try {
|
|
1680
|
+
const ui = scope.get('uiConversation')
|
|
1681
|
+
if (typeof ui?.userText?.register === 'function') {
|
|
1682
|
+
ctx.effect(() => ui.userText.register(selectionUserTextRule), 'dsh-superinput: selection user-text rule')
|
|
1683
|
+
}
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
report(`selection user-text rule failed: ${error?.message ?? error}`)
|
|
1686
|
+
}
|
|
1687
|
+
})
|
|
1688
|
+
ctx.slots.inject('conversation.user.text.node', () => ctx.slots.register({
|
|
1689
|
+
name: 'conversation.user.text.node',
|
|
1690
|
+
key: SELECTION_RULE,
|
|
1691
|
+
priority: 20,
|
|
1692
|
+
}, SelectionMessageChip))
|
|
1693
|
+
|
|
1694
|
+
// 草稿桥挂 input.dock(该槽位注入 useInput / inputActions)
|
|
1695
|
+
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
|
|
1696
|
+
name: 'conversation.input.dock',
|
|
1697
|
+
id: 'dsh-superinput-prompts-bridge',
|
|
1698
|
+
order: 96,
|
|
1699
|
+
}, PromptsDraftBridge))
|
|
1700
|
+
|
|
1701
|
+
// 划选引用桥:chip 已内联在编辑器里,这个槽位只提供入口与详情弹窗。
|
|
1702
|
+
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
|
|
1703
|
+
name: 'conversation.input.dock',
|
|
1704
|
+
id: 'dsh-superinput-selection-context',
|
|
1705
|
+
order: 97,
|
|
1706
|
+
}, (props) => h(PromptsBoundary, null, h(SelectionContextBridge, props))))
|
|
1707
|
+
},
|
|
1708
|
+
}
|
|
1709
|
+
}
|