@emptyvessel_003/dsh-prompt-shelf 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +63 -2
- package/assets/screenshot-main.png +0 -0
- package/cordis.patch.yml +4 -0
- package/lib/client.js +972 -0
- package/lib/index.js +68 -0
- package/package.json +55 -4
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EmptyVessel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,64 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 快捷指令(@emptyvessel_003/dsh-prompt-shelf)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
DSH 右侧栏的常用指令库:把你反复要发的提示词存成卡片,点一下带进下一条消息;打开「每轮注入」后,这张卡片在当前会话的每一轮自动附带。已经发出的用户消息也能一键存回库里,下次直接复用。
|
|
6
|
+
|
|
7
|
+
## 功能
|
|
8
|
+
|
|
9
|
+
- **指令库**:在侧栏新建、编辑、删除卡片,支持搜索、标签筛选、置顶、拖动排序。置顶卡片单独排在列表最前。
|
|
10
|
+
- **单次附带**:点卡片加入下一条消息。输入框下方显示当前附带了哪些,超过 3 张折叠为 +N。发完即清空,不影响其他消息。
|
|
11
|
+
- **每轮注入**:按会话开关。打开后,这张卡片在当前会话的每一轮自动追加到消息末尾。新会话默认全部关闭;删除会话时跟着清掉;分叉会话在分叉当下复制来源会话的清单。
|
|
12
|
+
- **存为指令**:每条已发送的用户消息,图标行里多一个「存为指令」按钮(在复制按钮前面)。点一下把这条消息的正文原样存成卡片,标题按轮次自动命名(如「第 12 轮」)。点击后按钮消失,刷新后同一消息会重新出现(不持久化点击状态)。
|
|
13
|
+
|
|
14
|
+
## 发送行为
|
|
15
|
+
|
|
16
|
+
卡片内容在发送前通过 DSH 的 InputActions 插入到草稿末尾,已有的结构化引用(@ 提及、附件占位)不受影响。之后走 DSH 原生的发送流程。DSH 接纳这条消息后,本次附带清空并回落到该会话的每轮注入清单;发送失败则保留原选区。
|
|
17
|
+
|
|
18
|
+
## 数据存储
|
|
19
|
+
|
|
20
|
+
所有数据保存在 `~/.dsh/dsh-prompt-shelf/data.json`:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"cards": [{ "id": "…", "title": "…", "content": "…", "tags": [], "pinned": false }],
|
|
25
|
+
"sessions": { "<sessionId>": ["<cardId>"] }
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
写入用临时文件 + fsync + rename 原子提交,不会留下半个文件。备份直接复制这个文件即可。单次附带选区只在页面内存里,不落盘。
|
|
30
|
+
|
|
31
|
+
## 安装
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
dsh plugin --profile web add @emptyvessel_003/dsh-prompt-shelf
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
本地开发:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
dsh plugin --profile web add link:/abs/path/to/dsh-prompt-shelf
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**重启说明**:改了 `lib/index.js`(宿主半)要重启 DSH 进程才生效;只改 `lib/client.js`(客户端半)刷新页面即可。
|
|
44
|
+
|
|
45
|
+
卸载:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
dsh plugin --profile web remove @emptyvessel_003/dsh-prompt-shelf
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
卸载不会删除 `~/.dsh/dsh-prompt-shelf/data.json`,需要清空时手动删除该目录。
|
|
52
|
+
|
|
53
|
+
## 版本要求
|
|
54
|
+
|
|
55
|
+
DSH `>=0.1.7-rc.2 <0.3.0`,仅 Web 平台。
|
|
56
|
+
|
|
57
|
+
## 已知限制
|
|
58
|
+
|
|
59
|
+
- DSH 没有为用户消息提供独立的动作行插槽(只有 `conversation.chat.assistant-actions` 是给助手消息的),因此本插件通过 `conversation.chat.node` 以更低优先级接管 user/steering 节点,再原样渲染宿主组件,只把按钮插进它自己渲染出的图标行。宿主改这部分实现时需要同步调整。
|
|
60
|
+
- 「存为指令」按钮点击后的隐藏状态只在当前页面内存里,刷新后会重新出现。
|
|
61
|
+
|
|
62
|
+
## 许可证
|
|
63
|
+
|
|
64
|
+
MIT,见 [LICENSE](./LICENSE)。
|
|
Binary file
|
package/cordis.patch.yml
ADDED
package/lib/client.js
ADDED
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: '@emptyvessel_003/dsh-prompt-shelf',
|
|
3
|
+
factory: function (require) {
|
|
4
|
+
var module = { exports: {} }
|
|
5
|
+
var exports = module.exports
|
|
6
|
+
var React = require('react')
|
|
7
|
+
var ReactDOM = require('react-dom')
|
|
8
|
+
var primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
9
|
+
var h = React.createElement
|
|
10
|
+
|
|
11
|
+
var Switch = primitives.Switch
|
|
12
|
+
var Button = primitives.Button
|
|
13
|
+
var Tag = primitives.Tag
|
|
14
|
+
var Input = primitives.Input
|
|
15
|
+
var Pill = primitives.Pill
|
|
16
|
+
var IconPlus = primitives.IconPlusOutlineRegular
|
|
17
|
+
var IconSearch = primitives.IconSearchOutlineRegular
|
|
18
|
+
var IconPin = primitives.IconPinOutlineRegular
|
|
19
|
+
var IconPinFill = primitives.IconPinFillRegular
|
|
20
|
+
var IconShelf = primitives.IconListPenOutlineRegular
|
|
21
|
+
|
|
22
|
+
var TAB_ID = 'prompt-shelf'
|
|
23
|
+
var CHANNEL = '/dsh-prompt-shelf'
|
|
24
|
+
var COMPOSER = '[data-composer-input]'
|
|
25
|
+
var slotsRef = null
|
|
26
|
+
var connection = null
|
|
27
|
+
var loaded = false
|
|
28
|
+
var storageError = ''
|
|
29
|
+
var writeQueue = Promise.resolve()
|
|
30
|
+
var book = { cards: [] }
|
|
31
|
+
var sessionBook = {}
|
|
32
|
+
var selections = Object.create(null)
|
|
33
|
+
|
|
34
|
+
async function storageCall(endpoint, payload) {
|
|
35
|
+
var result = await connection.rpc.call(CHANNEL, endpoint, payload)
|
|
36
|
+
if (result.ok !== true) throw new Error(result.error.message)
|
|
37
|
+
return result.value
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 唯一的状态出口:通知界面,然后把整份状态排进写队列。
|
|
41
|
+
function writeState() {
|
|
42
|
+
emit()
|
|
43
|
+
if (!loaded) return
|
|
44
|
+
var snapshot = JSON.parse(JSON.stringify({ cards: book.cards, sessions: sessionBook }))
|
|
45
|
+
writeQueue = writeQueue.catch(function () {}).then(async function () {
|
|
46
|
+
await storageCall('save', snapshot)
|
|
47
|
+
if (storageError !== '') {
|
|
48
|
+
storageError = ''
|
|
49
|
+
emit()
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
writeQueue.catch(function (error) {
|
|
53
|
+
storageError = '写入 ~/.dsh/dsh-prompt-shelf/data.json 失败:' + error.message
|
|
54
|
+
console.error('[prompt-shelf]', storageError)
|
|
55
|
+
emit()
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 目录里已经消失的会话,连着它的每轮注入清单一起删掉。
|
|
60
|
+
function pruneSessions(list) {
|
|
61
|
+
if (list.phase !== 'ready') return
|
|
62
|
+
var alive = list.ids.concat(Object.keys(list.byId))
|
|
63
|
+
var keys = Object.keys(sessionBook)
|
|
64
|
+
var changed = false
|
|
65
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
66
|
+
if (alive.indexOf(keys[i]) >= 0) continue
|
|
67
|
+
delete sessionBook[keys[i]]
|
|
68
|
+
delete selections[keys[i]]
|
|
69
|
+
changed = true
|
|
70
|
+
}
|
|
71
|
+
if (changed) writeState()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function loadStorage(source) {
|
|
75
|
+
connection = source
|
|
76
|
+
return storageCall('load', {}).then(function (stored) {
|
|
77
|
+
if (stored === null) stored = { cards: [], sessions: {} }
|
|
78
|
+
book = { cards: stored.cards }
|
|
79
|
+
sessionBook = stored.sessions
|
|
80
|
+
loaded = true
|
|
81
|
+
storageError = ''
|
|
82
|
+
emit()
|
|
83
|
+
}).catch(function (error) {
|
|
84
|
+
storageError = '无法读取 ~/.dsh/dsh-prompt-shelf/data.json:' + error.message
|
|
85
|
+
console.error('[prompt-shelf]', storageError)
|
|
86
|
+
emit()
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
var listeners = []
|
|
91
|
+
|
|
92
|
+
function emit() {
|
|
93
|
+
var copy = listeners.slice()
|
|
94
|
+
for (var i = 0; i < copy.length; i += 1) {
|
|
95
|
+
try { copy[i]() } catch (error) { console.error('[prompt-shelf]', error) }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function subscribe(listener) {
|
|
100
|
+
listeners.push(listener)
|
|
101
|
+
return function () {
|
|
102
|
+
var at = listeners.indexOf(listener)
|
|
103
|
+
if (at >= 0) listeners.splice(at, 1)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function everyOf(sid) {
|
|
108
|
+
var list = sessionBook[sid]
|
|
109
|
+
return Array.isArray(list) ? list : []
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function selectionOf(sid) {
|
|
113
|
+
if (!Object.prototype.hasOwnProperty.call(selections, sid)) selections[sid] = everyOf(sid).slice()
|
|
114
|
+
return selections[sid]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 可见选区只在内存里;发送成功后重置为该会话的每轮注入清单。
|
|
118
|
+
function attachedFor(sid) {
|
|
119
|
+
var selected = selectionOf(sid)
|
|
120
|
+
return book.cards.filter(function (card) { return selected.indexOf(card.id) >= 0 })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function suffixOf(list) {
|
|
124
|
+
var parts = []
|
|
125
|
+
for (var i = 0; i < list.length; i += 1) {
|
|
126
|
+
var body = list[i].content.trim()
|
|
127
|
+
if (body !== '') parts.push(body)
|
|
128
|
+
}
|
|
129
|
+
return parts.join('\n\n')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function cardById(id) {
|
|
133
|
+
for (var i = 0; i < book.cards.length; i += 1) if (book.cards[i].id === id) return book.cards[i]
|
|
134
|
+
return undefined
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function toggleInclude(sid, id) {
|
|
138
|
+
var selected = selectionOf(sid)
|
|
139
|
+
var at = selected.indexOf(id)
|
|
140
|
+
if (at >= 0) selected.splice(at, 1)
|
|
141
|
+
else selected.push(id)
|
|
142
|
+
emit()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function toggleEvery(sid, id, next) {
|
|
146
|
+
var list = sessionBook[sid]
|
|
147
|
+
if (list === undefined) {
|
|
148
|
+
list = []
|
|
149
|
+
sessionBook[sid] = list
|
|
150
|
+
}
|
|
151
|
+
var at = list.indexOf(id)
|
|
152
|
+
if (next && at < 0) list.push(id)
|
|
153
|
+
if (!next && at >= 0) list.splice(at, 1)
|
|
154
|
+
if (list.length === 0) delete sessionBook[sid]
|
|
155
|
+
var selected = selectionOf(sid)
|
|
156
|
+
var selectedAt = selected.indexOf(id)
|
|
157
|
+
if (next && selectedAt < 0) selected.push(id)
|
|
158
|
+
if (!next && selectedAt >= 0) selected.splice(selectedAt, 1)
|
|
159
|
+
writeState()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function refreshSelection(sid) {
|
|
163
|
+
selections[sid] = everyOf(sid).slice()
|
|
164
|
+
emit()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function createCard(fields) {
|
|
168
|
+
var card = {
|
|
169
|
+
id: 'c' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
170
|
+
title: fields.title.trim().slice(0, 80) || '未命名指令',
|
|
171
|
+
tags: normalizeTags(fields.tags),
|
|
172
|
+
content: fields.content,
|
|
173
|
+
pinned: false
|
|
174
|
+
}
|
|
175
|
+
book.cards.push(card)
|
|
176
|
+
writeState()
|
|
177
|
+
return card
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function updateCard(id, fields) {
|
|
181
|
+
var card = cardById(id)
|
|
182
|
+
if (card === undefined) return
|
|
183
|
+
card.title = fields.title.trim().slice(0, 80) || '未命名指令'
|
|
184
|
+
card.tags = normalizeTags(fields.tags)
|
|
185
|
+
card.content = fields.content
|
|
186
|
+
writeState()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 置顶只改标记,卡片的相对位置不动;显示时置顶组整体排在前面。
|
|
190
|
+
function setPinned(id, pinned) {
|
|
191
|
+
var card = cardById(id)
|
|
192
|
+
if (card === undefined) return
|
|
193
|
+
card.pinned = pinned
|
|
194
|
+
writeState()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
var END = '__end__'
|
|
198
|
+
|
|
199
|
+
// 置顶与不置顶是两个分区,只在同一区内部换位;拖到别区的卡上不生效。
|
|
200
|
+
function moveCard(dragId, overId, before) {
|
|
201
|
+
if (dragId === overId) return
|
|
202
|
+
var from = -1
|
|
203
|
+
for (var i = 0; i < book.cards.length; i += 1) if (book.cards[i].id === dragId) { from = i; break }
|
|
204
|
+
if (from < 0) return
|
|
205
|
+
var card = book.cards[from]
|
|
206
|
+
if (overId !== END) {
|
|
207
|
+
var target = cardById(overId)
|
|
208
|
+
if (target === undefined || (target.pinned === true) !== (card.pinned === true)) return
|
|
209
|
+
}
|
|
210
|
+
book.cards.splice(from, 1)
|
|
211
|
+
if (overId === END) {
|
|
212
|
+
// 列表末尾:不置顶卡落到最后,置顶卡落到置顶组末尾。
|
|
213
|
+
if (card.pinned !== true) book.cards.push(card)
|
|
214
|
+
else {
|
|
215
|
+
var lastPinned = -1
|
|
216
|
+
for (var k = 0; k < book.cards.length; k += 1) if (book.cards[k].pinned === true) lastPinned = k
|
|
217
|
+
book.cards.splice(lastPinned + 1, 0, card)
|
|
218
|
+
}
|
|
219
|
+
writeState()
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
var to = -1
|
|
223
|
+
for (var j = 0; j < book.cards.length; j += 1) if (book.cards[j].id === overId) { to = j; break }
|
|
224
|
+
if (to < 0) { book.cards.splice(from, 0, card); return }
|
|
225
|
+
book.cards.splice(before ? to : to + 1, 0, card)
|
|
226
|
+
writeState()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function removeCard(id) {
|
|
230
|
+
for (var i = 0; i < book.cards.length; i += 1) {
|
|
231
|
+
if (book.cards[i].id !== id) continue
|
|
232
|
+
book.cards.splice(i, 1)
|
|
233
|
+
break
|
|
234
|
+
}
|
|
235
|
+
for (var key in selections) {
|
|
236
|
+
var selected = selections[key]
|
|
237
|
+
var selectedAt = selected.indexOf(id)
|
|
238
|
+
if (selectedAt >= 0) selected.splice(selectedAt, 1)
|
|
239
|
+
}
|
|
240
|
+
var sessionIds = Object.keys(sessionBook)
|
|
241
|
+
for (var s = 0; s < sessionIds.length; s += 1) {
|
|
242
|
+
var list = sessionBook[sessionIds[s]]
|
|
243
|
+
var everyAt = list.indexOf(id)
|
|
244
|
+
if (everyAt >= 0) list.splice(everyAt, 1)
|
|
245
|
+
}
|
|
246
|
+
writeState()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function normalizeTags(value) {
|
|
250
|
+
var out = []
|
|
251
|
+
for (var i = 0; i < value.length; i += 1) {
|
|
252
|
+
var tag = value[i].trim().slice(0, 24)
|
|
253
|
+
if (tag !== '' && out.indexOf(tag) < 0) out.push(tag)
|
|
254
|
+
}
|
|
255
|
+
return out
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function allTags() {
|
|
259
|
+
var out = []
|
|
260
|
+
for (var i = 0; i < book.cards.length; i += 1) {
|
|
261
|
+
var tags = book.cards[i].tags
|
|
262
|
+
for (var j = 0; j < tags.length; j += 1) if (out.indexOf(tags[j]) < 0) out.push(tags[j])
|
|
263
|
+
}
|
|
264
|
+
return out
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function useStore() {
|
|
268
|
+
var state = React.useState(0)
|
|
269
|
+
var bump = state[1]
|
|
270
|
+
React.useEffect(function () {
|
|
271
|
+
return subscribe(function () { bump(function (n) { return n + 1 }) })
|
|
272
|
+
}, [])
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---- 发送时把附带指令并进消息 ----
|
|
276
|
+
// 发送入口在会话内挂载:键盘与发送按钮的事件只在当前会话存在期间监听。
|
|
277
|
+
var SEND_LABELS = ['发送消息', '排队发送', '插话发送', 'Send message', 'Queue message', 'Steer message']
|
|
278
|
+
|
|
279
|
+
// ---- 样式 ----
|
|
280
|
+
var STYLE_ID = 'dsh-prompt-shelf-style'
|
|
281
|
+
var CSS = [
|
|
282
|
+
'.ps-root{display:flex;flex-direction:column;height:100%;min-height:0;gap:12px;padding:12px;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);}',
|
|
283
|
+
'.ps-root,.ps-root *,.ps-root *::before,.ps-root *::after{box-sizing:border-box;}',
|
|
284
|
+
'.ps-toolbar{display:flex;align-items:center;gap:8px;flex:none;min-width:0;}',
|
|
285
|
+
'.ps-toolbar>span{flex:1;min-width:0;}',
|
|
286
|
+
'.ps-toolbar .ps-new{flex:none;height:32px;border-radius:var(--dsw-radius-md);}',
|
|
287
|
+
'.ps-filters{display:flex;flex-wrap:wrap;align-items:center;gap:6px;flex:none;max-height:88px;overflow-y:auto;}',
|
|
288
|
+
'.ps-list{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:8px;padding-right:2px;}',
|
|
289
|
+
'.ps-card{border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);padding:10px 12px;display:flex;flex-direction:column;gap:6px;min-width:0;cursor:pointer;background:transparent;transition:border-color .16s ease,background-color .16s ease,box-shadow .16s ease;}',
|
|
290
|
+
'.ps-card:hover{border-color:var(--dsw-alias-border-l4);}',
|
|
291
|
+
'.ps-card--on{border-color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 10%,transparent);}',
|
|
292
|
+
'.ps-card--on:hover{border-color:var(--dsw-alias-brand-primary);}',
|
|
293
|
+
'.ps-card--pulse{animation:ps-pop .2s ease;}',
|
|
294
|
+
'.ps-card--drag{opacity:.45;border-style:dashed;cursor:grabbing;}',
|
|
295
|
+
'.ps-drop{flex:none;height:3px;margin:3px 2px;border-radius:2px;background:var(--dsw-alias-brand-primary);}',
|
|
296
|
+
'.ps-tail{flex:none;height:32px;}',
|
|
297
|
+
'@keyframes ps-pop{0%{transform:scale(1)}45%{transform:scale(.991)}100%{transform:scale(1)}}',
|
|
298
|
+
'.ps-head{display:flex;align-items:center;gap:12px;min-width:0;}',
|
|
299
|
+
'.ps-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;}',
|
|
300
|
+
'.ps-title{font-size:13px;line-height:20px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
|
|
301
|
+
'.ps-sub{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
|
|
302
|
+
'.ps-tagrow{display:flex;flex-wrap:wrap;align-items:center;gap:6px;}',
|
|
303
|
+
'.ps-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-right:-10px;}',
|
|
304
|
+
'.ps-every{display:flex;align-items:center;gap:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;}',
|
|
305
|
+
'.ps-ops{display:flex;align-items:center;gap:2px;}',
|
|
306
|
+
'.ps-empty{margin:auto 0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:20px;text-align:center;padding:24px 8px;}',
|
|
307
|
+
'.ps-form{border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);padding:12px;display:flex;flex-direction:column;gap:10px;background:var(--dsw-alias-bg-layer-2);cursor:default;}',
|
|
308
|
+
'.ps-field{display:flex;flex-direction:column;gap:6px;min-width:0;}',
|
|
309
|
+
'.ps-label{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);}',
|
|
310
|
+
'.ps-textarea{width:100%;min-width:0;resize:vertical;min-height:100px;padding:8px 10px;border-radius:var(--dsw-radius-sm);border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:inherit;font-family:inherit;font-size:13px;line-height:20px;outline:none;}',
|
|
311
|
+
'.ps-textarea:focus{border-color:var(--dsw-alias-state-business-primary);}',
|
|
312
|
+
'.ps-textarea::placeholder{color:var(--dsw-alias-label-dimmed);}',
|
|
313
|
+
'.ps-form-foot{display:flex;justify-content:flex-end;gap:8px;}',
|
|
314
|
+
'.ps-suggest{display:flex;flex-wrap:wrap;gap:6px;}',
|
|
315
|
+
'.ps-bar{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:12px;line-height:18px;}',
|
|
316
|
+
'.ps-bar-label{display:inline-flex;align-items:center;gap:4px;height:24px;color:var(--dsw-alias-label-tertiary);}',
|
|
317
|
+
'.ps-chip{display:inline-flex;align-items:center;gap:2px;max-width:200px;flex:none;height:24px;padding:0 2px 0 10px;border-radius:999px;border:1px solid var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 10%,transparent);color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;}',
|
|
318
|
+
'.ps-chip>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}',
|
|
319
|
+
'.ps-chip>button{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border:none;background:transparent;color:inherit;border-radius:50%;cursor:pointer;font-size:13px;line-height:1;padding:0;opacity:.6;font-family:inherit;}',
|
|
320
|
+
'.ps-chip>button:hover{opacity:1;background:var(--dsw-alias-interactive-bg-hover);}',
|
|
321
|
+
'.ps-chip--more{padding:0 10px;border-color:var(--dsw-alias-border-l4);background:transparent;color:var(--dsw-alias-label-tertiary);font-size:12px;cursor:default;}',
|
|
322
|
+
'.ps-umsg{display:block;min-width:0;}',
|
|
323
|
+
'.ps-umsg-slot{display:contents;}',
|
|
324
|
+
'.ps-umsg-act{width:calc(28px + var(--dsh-content-font-delta,0px));height:calc(28px + var(--dsh-content-font-delta,0px));padding:6px;border:none;border-radius:var(--dsw-radius-sm);background:transparent;color:var(--dsw-alias-label-tertiary);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;}',
|
|
325
|
+
'.ps-umsg-act svg{width:calc(15px + var(--dsh-content-font-delta,0px));height:calc(15px + var(--dsh-content-font-delta,0px));}',
|
|
326
|
+
'.ps-umsg-act:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);}',
|
|
327
|
+
'.ps-umsg-act:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px;}',
|
|
328
|
+
'.ps-umsg-fallback{padding:8px 12px;border-radius:var(--dsw-radius-md);background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-label-primary);white-space:pre-wrap;}'
|
|
329
|
+
].join('')
|
|
330
|
+
|
|
331
|
+
function ensureStyle() {
|
|
332
|
+
if (document.getElementById(STYLE_ID) !== null) return
|
|
333
|
+
var el = document.createElement('style')
|
|
334
|
+
el.id = STYLE_ID
|
|
335
|
+
el.textContent = CSS
|
|
336
|
+
document.head.appendChild(el)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 输入状态只在发送那一刻被读取,放进不渲染的子组件里订阅,
|
|
340
|
+
// 附带条本身只为选区和存储变化重渲染。
|
|
341
|
+
function InputTap(props) {
|
|
342
|
+
props.live.current.input = props.useInput(function (state) { return state })
|
|
343
|
+
return null
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ---- 输入框下方的本次附带行 ----
|
|
347
|
+
function AttachBar(props) {
|
|
348
|
+
useStore()
|
|
349
|
+
var sid = props.sessionId
|
|
350
|
+
var pendingIds = props.useSession(function (state) { return state.pendingSubmissions.map(function (item) { return item.requestId }).join('\n') })
|
|
351
|
+
var promptError = props.useSession(function (state) { return state.promptError })
|
|
352
|
+
var chatIds = props.useChat(function (snapshot) {
|
|
353
|
+
var ids = []
|
|
354
|
+
for (var i = 0; i < snapshot.order.length; i += 1) {
|
|
355
|
+
var node = snapshot.nodes.get(snapshot.order[i])
|
|
356
|
+
if (node === undefined) continue
|
|
357
|
+
if (node.kind !== 'user' && node.kind !== 'steering') continue
|
|
358
|
+
var source = node.data.source
|
|
359
|
+
if (source.kind === 'user' && typeof source.rpcId === 'string') ids.push(source.rpcId)
|
|
360
|
+
}
|
|
361
|
+
return ids.join('\n')
|
|
362
|
+
})
|
|
363
|
+
var inboxIds = props.useProjection('inbox', function (inbox) {
|
|
364
|
+
if (inbox === undefined) return ''
|
|
365
|
+
var ids = []
|
|
366
|
+
var lists = [inbox['next-turn'] || [], inbox['next-step'] || []]
|
|
367
|
+
for (var i = 0; i < lists.length; i += 1) {
|
|
368
|
+
for (var j = 0; j < lists[i].length; j += 1) {
|
|
369
|
+
var source = lists[i][j].source
|
|
370
|
+
if (source.kind === 'user' && typeof source.rpcId === 'string') ids.push(source.rpcId)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return ids.join('\n')
|
|
374
|
+
})
|
|
375
|
+
var awaitingState = React.useState(null)
|
|
376
|
+
var awaiting = awaitingState[0]
|
|
377
|
+
var setAwaiting = awaitingState[1]
|
|
378
|
+
var live = React.useRef(null)
|
|
379
|
+
if (live.current === null) live.current = { sid: sid, inserted: false }
|
|
380
|
+
live.current.sid = sid
|
|
381
|
+
live.current.actions = props.inputActions || null
|
|
382
|
+
live.current.pendingIds = pendingIds
|
|
383
|
+
live.current.chatIds = chatIds
|
|
384
|
+
live.current.inboxIds = inboxIds
|
|
385
|
+
live.current.promptError = promptError
|
|
386
|
+
|
|
387
|
+
React.useEffect(function () {
|
|
388
|
+
if (awaiting === null) return
|
|
389
|
+
if (awaiting.sid !== sid) { setAwaiting(null); return }
|
|
390
|
+
var accepted = '\n' + chatIds + '\n' + inboxIds + '\n'
|
|
391
|
+
var candidate = awaiting.requestId
|
|
392
|
+
if (candidate === '') {
|
|
393
|
+
var pendingParts = pendingIds.split('\n')
|
|
394
|
+
for (var j = 0; j < pendingParts.length; j += 1) {
|
|
395
|
+
var pendingId = pendingParts[j]
|
|
396
|
+
if (pendingId !== '' && awaiting.baselinePending.indexOf('\n' + pendingId + '\n') < 0) { candidate = pendingId; break }
|
|
397
|
+
}
|
|
398
|
+
if (candidate === '') {
|
|
399
|
+
var acceptedParts = accepted.split('\n')
|
|
400
|
+
for (var n = 0; n < acceptedParts.length; n += 1) {
|
|
401
|
+
var acceptedId = acceptedParts[n]
|
|
402
|
+
if (acceptedId !== '' && awaiting.baselineAccepted.indexOf('\n' + acceptedId + '\n') < 0) { candidate = acceptedId; break }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (candidate !== '') {
|
|
406
|
+
if (accepted.indexOf('\n' + candidate + '\n') >= 0) {
|
|
407
|
+
refreshSelection(awaiting.sid)
|
|
408
|
+
setAwaiting(null)
|
|
409
|
+
} else setAwaiting({ ...awaiting, requestId: candidate })
|
|
410
|
+
}
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
if (accepted.indexOf('\n' + candidate + '\n') >= 0) {
|
|
414
|
+
refreshSelection(awaiting.sid)
|
|
415
|
+
setAwaiting(null)
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
if (pendingIds.split('\n').indexOf(candidate) < 0) { setAwaiting(null); return }
|
|
419
|
+
if (promptError !== null && promptError !== awaiting.promptError) setAwaiting(null)
|
|
420
|
+
}, [awaiting, sid, pendingIds, chatIds, inboxIds, promptError])
|
|
421
|
+
|
|
422
|
+
React.useEffect(function () {
|
|
423
|
+
function inComposer(node) {
|
|
424
|
+
return node instanceof Element && node.closest(COMPOSER) !== null
|
|
425
|
+
}
|
|
426
|
+
function overlayOpen() {
|
|
427
|
+
return document.querySelector('[data-trigger-menu], [data-conversation-composer-overlay]') !== null
|
|
428
|
+
}
|
|
429
|
+
function isSendButton(node) {
|
|
430
|
+
if (!(node instanceof Element)) return false
|
|
431
|
+
var button = node.closest('button')
|
|
432
|
+
return button !== null && button.closest('[data-composer-card]') !== null && SEND_LABELS.indexOf(button.getAttribute('aria-label') || '') >= 0
|
|
433
|
+
}
|
|
434
|
+
function appendIncoming() {
|
|
435
|
+
var current = live.current
|
|
436
|
+
if (!loaded || current.actions === null || current.inserted) return
|
|
437
|
+
var draft = current.input.draft
|
|
438
|
+
var attachmentIds = current.input.attachmentIds || []
|
|
439
|
+
if (draft.trim() === '' && attachmentIds.length === 0) return
|
|
440
|
+
// 只挡住同一次发送的重复事件;下一条消息不等接纳结果。
|
|
441
|
+
current.inserted = true
|
|
442
|
+
window.setTimeout(function () { current.inserted = false }, 0)
|
|
443
|
+
var suffix = suffixOf(attachedFor(current.sid))
|
|
444
|
+
if (suffix !== '' && !draft.endsWith(suffix)) {
|
|
445
|
+
var span = current.actions.captureInsertion()
|
|
446
|
+
if (span.draftRev !== current.input.draftRev) return
|
|
447
|
+
var end = draft.length
|
|
448
|
+
for (var i = 0; i < current.input.occurrences.length; i += 1) end -= current.input.occurrences[i].length - 1
|
|
449
|
+
var separator = draft.trim() === '' ? '' : draft.endsWith('\n\n') ? '' : draft.endsWith('\n') ? '\n' : '\n\n'
|
|
450
|
+
if (!current.actions.insertText(separator + suffix, { start: end, end: end, draftRev: span.draftRev })) return
|
|
451
|
+
}
|
|
452
|
+
var attempt = {
|
|
453
|
+
sid: current.sid,
|
|
454
|
+
baselinePending: '\n' + current.pendingIds + '\n',
|
|
455
|
+
baselineAccepted: '\n' + current.chatIds + '\n' + current.inboxIds + '\n',
|
|
456
|
+
promptError: current.promptError,
|
|
457
|
+
requestId: ''
|
|
458
|
+
}
|
|
459
|
+
setAwaiting(attempt)
|
|
460
|
+
}
|
|
461
|
+
function onKeyDown(event) {
|
|
462
|
+
if (event.key !== 'Enter' || event.isComposing || event.keyCode === 229) return
|
|
463
|
+
if (event.shiftKey || event.altKey) return
|
|
464
|
+
if (!inComposer(event.target) || overlayOpen()) return
|
|
465
|
+
appendIncoming()
|
|
466
|
+
}
|
|
467
|
+
function onClick(event) {
|
|
468
|
+
if (isSendButton(event.target)) appendIncoming()
|
|
469
|
+
}
|
|
470
|
+
window.addEventListener('keydown', onKeyDown, true)
|
|
471
|
+
window.addEventListener('click', onClick, true)
|
|
472
|
+
return function () {
|
|
473
|
+
window.removeEventListener('keydown', onKeyDown, true)
|
|
474
|
+
window.removeEventListener('click', onClick, true)
|
|
475
|
+
}
|
|
476
|
+
}, [])
|
|
477
|
+
|
|
478
|
+
if (!loaded) return null
|
|
479
|
+
var list = attachedFor(sid)
|
|
480
|
+
var shown = list.length > 3 ? list.slice(0, 3) : list
|
|
481
|
+
var hidden = list.length - shown.length
|
|
482
|
+
return h(React.Fragment, null,
|
|
483
|
+
h(InputTap, { live: live, useInput: props.useInput }),
|
|
484
|
+
list.length === 0 ? null : h('div', { className: 'ps-root', style: { height: 'auto', padding: 0 } },
|
|
485
|
+
h('div', { className: 'ps-bar' },
|
|
486
|
+
h('span', { className: 'ps-bar-label' }, h(IconShelf, { size: 14 }), '本次附带'),
|
|
487
|
+
shown.map(function (card) {
|
|
488
|
+
return h('span', { key: card.id, className: 'ps-chip', title: card.title },
|
|
489
|
+
h('span', null, card.title),
|
|
490
|
+
h('button', { type: 'button', 'aria-label': '本次不附带', onClick: function () { toggleInclude(sid, card.id) } }, '×')
|
|
491
|
+
)
|
|
492
|
+
}),
|
|
493
|
+
hidden === 0 ? null : h('span', { key: 'ps-more', className: 'ps-chip ps-chip--more' }, '+' + hidden)
|
|
494
|
+
))
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ---- 每条用户消息下方的保存入口 ----
|
|
499
|
+
// 消息文本只取正文文字块;纯附件消息没有可存的指令内容。
|
|
500
|
+
function messageText(node) {
|
|
501
|
+
var content = node.data.content
|
|
502
|
+
var parts = []
|
|
503
|
+
for (var i = 0; i < content.length; i += 1) {
|
|
504
|
+
var block = content[i]
|
|
505
|
+
if (block.type !== 'text') continue
|
|
506
|
+
var text = block.text.trim()
|
|
507
|
+
if (text !== '') parts.push(text)
|
|
508
|
+
}
|
|
509
|
+
return parts.join('\n')
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// 宿主自己的用户消息渲染器:接管 key 后要原样渲染它。
|
|
513
|
+
// 注册表的 entries 数组在两次变更之间是同一个引用,直接拿它当缓存键:
|
|
514
|
+
// 注册表一变(插件热重载、他人后注册、优先级变化)缓存自动失效,不需要失效逻辑。
|
|
515
|
+
var nativeRows = new WeakMap()
|
|
516
|
+
|
|
517
|
+
function subscribeChatNodes(listener) {
|
|
518
|
+
return slotsRef.subscribe('conversation.chat.node', listener)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function chatNodeEntries() {
|
|
522
|
+
return slotsRef.entries('conversation.chat.node')
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function nativeRowComponent(entries, kind) {
|
|
526
|
+
var known = nativeRows.get(entries)
|
|
527
|
+
if (known === undefined) {
|
|
528
|
+
known = Object.create(null)
|
|
529
|
+
nativeRows.set(entries, known)
|
|
530
|
+
}
|
|
531
|
+
if (known[kind] !== undefined) return known[kind]
|
|
532
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
533
|
+
var entry = entries[i]
|
|
534
|
+
if (entry.options.key !== kind) continue
|
|
535
|
+
if (entry.component === UserMessageRow) continue
|
|
536
|
+
known[kind] = entry.component
|
|
537
|
+
return entry.component
|
|
538
|
+
}
|
|
539
|
+
return null
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// 按钮放进宿主自己渲染的图标行(那一行带 data-clock),插在复制按钮之前,不额外占一行。
|
|
543
|
+
function UserMessageRow(props) {
|
|
544
|
+
var rootRef = React.useRef(null)
|
|
545
|
+
var slotRef = React.useRef(null)
|
|
546
|
+
var rowState = React.useState(null)
|
|
547
|
+
var row = rowState[0]
|
|
548
|
+
var setRow = rowState[1]
|
|
549
|
+
var doneState = React.useState(false)
|
|
550
|
+
var done = doneState[0]
|
|
551
|
+
var setDone = doneState[1]
|
|
552
|
+
// 只跟 loaded:存储读完才让保存按钮出现,其他状态变化不重渲染整条消息。
|
|
553
|
+
var readyState = React.useState(loaded)
|
|
554
|
+
var ready = readyState[0]
|
|
555
|
+
React.useEffect(function () {
|
|
556
|
+
return subscribe(function () { readyState[1](loaded) })
|
|
557
|
+
}, [])
|
|
558
|
+
var node = props.node
|
|
559
|
+
var text = messageText(node)
|
|
560
|
+
// 注册表一变就重渲染,宿主渲染器换了不用刷新页面。
|
|
561
|
+
var entries = React.useSyncExternalStore(subscribeChatNodes, chatNodeEntries)
|
|
562
|
+
var Native = nativeRowComponent(entries, node.kind)
|
|
563
|
+
|
|
564
|
+
React.useEffect(function () {
|
|
565
|
+
var root = rootRef.current
|
|
566
|
+
var actions = root === null ? null : root.querySelector('[data-clock="start"]')
|
|
567
|
+
if (actions === null) {
|
|
568
|
+
if (row !== null) setRow(null)
|
|
569
|
+
return
|
|
570
|
+
}
|
|
571
|
+
var slot = slotRef.current
|
|
572
|
+
if (slot === null) {
|
|
573
|
+
slot = document.createElement('span')
|
|
574
|
+
slot.className = 'ps-umsg-slot'
|
|
575
|
+
slotRef.current = slot
|
|
576
|
+
}
|
|
577
|
+
// 只看宿主行直属的子节点:插进去的槽自己也可能含按钮,不能再选到它。
|
|
578
|
+
var copy = null
|
|
579
|
+
for (var i = 0; i < actions.children.length; i += 1) {
|
|
580
|
+
var child = actions.children[i]
|
|
581
|
+
if (child === slot) continue
|
|
582
|
+
if (child.tagName === 'BUTTON' || child.querySelector('button') !== null) { copy = child; break }
|
|
583
|
+
}
|
|
584
|
+
if (slot.parentNode !== actions || (copy !== null && slot.nextSibling !== copy)) {
|
|
585
|
+
if (copy === null) actions.appendChild(slot)
|
|
586
|
+
else actions.insertBefore(slot, copy)
|
|
587
|
+
}
|
|
588
|
+
if (row !== slot) setRow(slot)
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
React.useEffect(function () {
|
|
592
|
+
return function () {
|
|
593
|
+
var slot = slotRef.current
|
|
594
|
+
if (slot !== null && slot.parentNode !== null) slot.parentNode.removeChild(slot)
|
|
595
|
+
slotRef.current = null
|
|
596
|
+
}
|
|
597
|
+
}, [])
|
|
598
|
+
|
|
599
|
+
if (Native === null) {
|
|
600
|
+
// 宿主渲染器不在时至少把消息文字留在页面上,不吞掉用户的话。
|
|
601
|
+
return text === '' ? null : h('div', { className: 'ps-umsg-fallback' }, text)
|
|
602
|
+
}
|
|
603
|
+
var turn = node.location.turn.turn
|
|
604
|
+
var save = text === '' || done || row === null || !ready ? null : ReactDOM.createPortal(
|
|
605
|
+
h(primitives.Tooltip, { label: '存为指令', side: 'bottom', children: h('button', {
|
|
606
|
+
type: 'button',
|
|
607
|
+
className: 'ps-umsg-act',
|
|
608
|
+
'aria-label': '存为指令',
|
|
609
|
+
onClick: function () {
|
|
610
|
+
createCard({ title: turn > 0 ? '第 ' + turn + ' 轮' : '', tags: [], content: text })
|
|
611
|
+
setDone(true)
|
|
612
|
+
}
|
|
613
|
+
}, h(IconPlus, { size: 15 })) }),
|
|
614
|
+
row
|
|
615
|
+
)
|
|
616
|
+
return h('div', { className: 'ps-umsg', ref: rootRef },
|
|
617
|
+
h(Native, {
|
|
618
|
+
node: node,
|
|
619
|
+
renderMessageImages: props.renderMessageImages,
|
|
620
|
+
openFile: props.openFile,
|
|
621
|
+
openSkill: props.openSkill,
|
|
622
|
+
t: props.t
|
|
623
|
+
}),
|
|
624
|
+
save
|
|
625
|
+
)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ---- 侧栏面板 ----
|
|
629
|
+
function Panel(props) {
|
|
630
|
+
useStore()
|
|
631
|
+
var sid = props.sessionId
|
|
632
|
+
var queryState = React.useState('')
|
|
633
|
+
var query = queryState[0]
|
|
634
|
+
var setQuery = queryState[1]
|
|
635
|
+
var tagState = React.useState('')
|
|
636
|
+
var tagFilter = tagState[0]
|
|
637
|
+
var setTagFilter = tagState[1]
|
|
638
|
+
var editState = React.useState(null)
|
|
639
|
+
var editing = editState[0]
|
|
640
|
+
var setEditing = editState[1]
|
|
641
|
+
var deleteState = React.useState('')
|
|
642
|
+
var deleting = deleteState[0]
|
|
643
|
+
var setDeleting = deleteState[1]
|
|
644
|
+
var pulseState = React.useState('')
|
|
645
|
+
var pulse = pulseState[0]
|
|
646
|
+
var setPulse = pulseState[1]
|
|
647
|
+
var dragState = React.useState({ id: '', over: '', before: true })
|
|
648
|
+
var drag = dragState[0]
|
|
649
|
+
var setDrag = dragState[1]
|
|
650
|
+
|
|
651
|
+
var tags = allTags()
|
|
652
|
+
var needle = query.trim().toLowerCase()
|
|
653
|
+
var matched = book.cards.filter(function (card) {
|
|
654
|
+
if (tagFilter !== '' && card.tags.indexOf(tagFilter) < 0) return false
|
|
655
|
+
if (needle === '') return true
|
|
656
|
+
var hay = (card.title + '\n' + card.content + '\n' + card.tags.join(' ')).toLowerCase()
|
|
657
|
+
return hay.indexOf(needle) >= 0
|
|
658
|
+
})
|
|
659
|
+
// 置顶是独立分区:置顶组永远在前,两组内部都保持数组顺序。
|
|
660
|
+
var rows = []
|
|
661
|
+
for (var pinAt = 0; pinAt < matched.length; pinAt += 1) if (matched[pinAt].pinned === true) rows.push(matched[pinAt])
|
|
662
|
+
for (var restAt = 0; restAt < matched.length; restAt += 1) if (matched[restAt].pinned !== true) rows.push(matched[restAt])
|
|
663
|
+
|
|
664
|
+
function flash(id) {
|
|
665
|
+
setPulse(id)
|
|
666
|
+
window.setTimeout(function () {
|
|
667
|
+
setPulse(function (current) { return current === id ? '' : current })
|
|
668
|
+
}, 210)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function startEdit(card) {
|
|
672
|
+
setDeleting('')
|
|
673
|
+
setEditing({
|
|
674
|
+
id: card === null ? '' : card.id,
|
|
675
|
+
title: card === null ? '' : card.title,
|
|
676
|
+
tags: card === null ? '' : card.tags.join(', '),
|
|
677
|
+
content: card === null ? '' : card.content
|
|
678
|
+
})
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function commit() {
|
|
682
|
+
if (editing === null) return
|
|
683
|
+
var fields = {
|
|
684
|
+
title: editing.title,
|
|
685
|
+
tags: editing.tags.split(/[,,、]+/),
|
|
686
|
+
content: editing.content
|
|
687
|
+
}
|
|
688
|
+
if (editing.id === '') createCard(fields)
|
|
689
|
+
else updateCard(editing.id, fields)
|
|
690
|
+
setEditing(null)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function editor() {
|
|
694
|
+
return h('div', { className: 'ps-form' },
|
|
695
|
+
h('div', { className: 'ps-field' },
|
|
696
|
+
h('label', { className: 'ps-label' }, '指令标题'),
|
|
697
|
+
h(Input, {
|
|
698
|
+
value: editing.title,
|
|
699
|
+
placeholder: '例如:先问清目标再动手',
|
|
700
|
+
autoFocus: true,
|
|
701
|
+
onChange: function (event) { setEditing({ id: editing.id, title: event.target.value, tags: editing.tags, content: editing.content }) }
|
|
702
|
+
})
|
|
703
|
+
),
|
|
704
|
+
h('div', { className: 'ps-field' },
|
|
705
|
+
h('label', { className: 'ps-label' }, '标签(可留空;已有标签点一下加入)'),
|
|
706
|
+
h(Input, {
|
|
707
|
+
value: editing.tags,
|
|
708
|
+
placeholder: '用逗号分隔,例如:写作, 代码',
|
|
709
|
+
onChange: function (event) { setEditing({ id: editing.id, title: editing.title, tags: event.target.value, content: editing.content }) }
|
|
710
|
+
}),
|
|
711
|
+
tags.length === 0 ? null : h('div', { className: 'ps-suggest' },
|
|
712
|
+
tags.map(function (tag) {
|
|
713
|
+
return h(Pill, {
|
|
714
|
+
key: tag,
|
|
715
|
+
onClick: function () {
|
|
716
|
+
var current = editing.tags.split(/[,,、]+/).map(function (item) { return item.trim() }).filter(Boolean)
|
|
717
|
+
if (current.indexOf(tag) >= 0) return
|
|
718
|
+
current.push(tag)
|
|
719
|
+
setEditing({ id: editing.id, title: editing.title, tags: current.join(', '), content: editing.content })
|
|
720
|
+
}
|
|
721
|
+
}, tag)
|
|
722
|
+
})
|
|
723
|
+
)
|
|
724
|
+
),
|
|
725
|
+
h('div', { className: 'ps-field' },
|
|
726
|
+
h('label', { className: 'ps-label' }, '指令内容(发送时并入消息)'),
|
|
727
|
+
h('textarea', {
|
|
728
|
+
className: 'ps-textarea',
|
|
729
|
+
value: editing.content,
|
|
730
|
+
placeholder: '这段文字会在发送时接在你的消息后面',
|
|
731
|
+
onChange: function (event) { setEditing({ id: editing.id, title: editing.title, tags: editing.tags, content: event.target.value }) }
|
|
732
|
+
})
|
|
733
|
+
),
|
|
734
|
+
h('div', { className: 'ps-form-foot' },
|
|
735
|
+
h(Button, { size: 'sm', variant: 'ghost', onClick: function () { setEditing(null) } }, '取消'),
|
|
736
|
+
h(Button, { size: 'sm', variant: 'primary', onClick: commit }, '保存')
|
|
737
|
+
)
|
|
738
|
+
)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function row(card) {
|
|
742
|
+
if (editing !== null && editing.id === card.id) return h(React.Fragment, { key: card.id }, editor())
|
|
743
|
+
var every = everyOf(sid).indexOf(card.id) >= 0
|
|
744
|
+
var on = selectionOf(sid).indexOf(card.id) >= 0
|
|
745
|
+
var classes = 'ps-card' + (on ? ' ps-card--on' : '')
|
|
746
|
+
+ (pulse === card.id ? ' ps-card--pulse' : '')
|
|
747
|
+
+ (drag.id === card.id ? ' ps-card--drag' : '')
|
|
748
|
+
return h('div', {
|
|
749
|
+
key: card.id,
|
|
750
|
+
className: classes,
|
|
751
|
+
draggable: true,
|
|
752
|
+
onClick: function (event) {
|
|
753
|
+
if (event.target.closest('button') !== null) return
|
|
754
|
+
if (drag.id !== '') return
|
|
755
|
+
toggleInclude(sid, card.id)
|
|
756
|
+
flash(card.id)
|
|
757
|
+
},
|
|
758
|
+
onDragStart: function (event) {
|
|
759
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
760
|
+
try { event.dataTransfer.setData('text/plain', card.id) } catch (error) {}
|
|
761
|
+
setDrag({ id: card.id, over: '', before: true })
|
|
762
|
+
},
|
|
763
|
+
onDragOver: function (event) {
|
|
764
|
+
if (drag.id === '' || drag.id === card.id) return
|
|
765
|
+
var dragged = cardById(drag.id)
|
|
766
|
+
if (dragged === undefined || (dragged.pinned === true) !== (card.pinned === true)) return
|
|
767
|
+
event.preventDefault()
|
|
768
|
+
var rect = event.currentTarget.getBoundingClientRect()
|
|
769
|
+
var before = event.clientY < rect.top + rect.height / 2
|
|
770
|
+
if (drag.over !== card.id || drag.before !== before) setDrag({ id: drag.id, over: card.id, before: before })
|
|
771
|
+
},
|
|
772
|
+
onDrop: function (event) {
|
|
773
|
+
event.preventDefault()
|
|
774
|
+
if (drag.id !== '' && drag.id !== card.id) moveCard(drag.id, card.id, drag.before)
|
|
775
|
+
setDrag({ id: '', over: '', before: true })
|
|
776
|
+
},
|
|
777
|
+
onDragEnd: function () { setDrag({ id: '', over: '', before: true }) }
|
|
778
|
+
},
|
|
779
|
+
h('div', { className: 'ps-head' },
|
|
780
|
+
h('div', { className: 'ps-body' },
|
|
781
|
+
h('div', { className: 'ps-title', title: card.title }, card.title),
|
|
782
|
+
h('div', { className: 'ps-sub', title: card.content }, card.content.replace(/\s+/g, ' ').trim() || '(空指令)')
|
|
783
|
+
)
|
|
784
|
+
),
|
|
785
|
+
card.tags.length === 0 ? null : h('div', { className: 'ps-tagrow' },
|
|
786
|
+
card.tags.map(function (tag) { return h(Tag, { key: tag, tone: 'outline' }, tag) })
|
|
787
|
+
),
|
|
788
|
+
h('div', { className: 'ps-foot' },
|
|
789
|
+
h('span', { className: 'ps-every' },
|
|
790
|
+
'每轮注入',
|
|
791
|
+
h(Switch, {
|
|
792
|
+
checked: every,
|
|
793
|
+
label: '每轮注入',
|
|
794
|
+
onChange: function (next) { toggleEvery(sid, card.id, next) }
|
|
795
|
+
})
|
|
796
|
+
),
|
|
797
|
+
h('div', { className: 'ps-ops' },
|
|
798
|
+
h(Button, {
|
|
799
|
+
size: 'sm', variant: 'ghost',
|
|
800
|
+
icon: card.pinned ? h(IconPinFill, { size: 16 }) : h(IconPin, { size: 16 }),
|
|
801
|
+
onClick: function () { setPinned(card.id, !card.pinned) }
|
|
802
|
+
}, card.pinned ? '取消置顶' : '置顶'),
|
|
803
|
+
h(Button, { size: 'sm', variant: 'ghost', onClick: function () { startEdit(card) } }, '编辑'),
|
|
804
|
+
deleting === card.id
|
|
805
|
+
? h(React.Fragment, null,
|
|
806
|
+
h(Button, { size: 'sm', variant: 'ghost', onClick: function () { setDeleting('') } }, '取消'),
|
|
807
|
+
h(Button, {
|
|
808
|
+
size: 'sm', variant: 'ghost',
|
|
809
|
+
style: { color: 'var(--dsw-alias-state-error-primary)' },
|
|
810
|
+
onClick: function () { setDeleting(''); removeCard(card.id) }
|
|
811
|
+
}, '确认删除')
|
|
812
|
+
)
|
|
813
|
+
: h(Button, { size: 'sm', variant: 'ghost', onClick: function () { setDeleting(card.id) } }, '删除')
|
|
814
|
+
)
|
|
815
|
+
)
|
|
816
|
+
)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function dropLine(key) {
|
|
820
|
+
return h('div', { key: key, className: 'ps-drop' })
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// 插入线就是松手后卡片所在的缝。
|
|
824
|
+
function listRows() {
|
|
825
|
+
var out = []
|
|
826
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
827
|
+
var card = rows[i]
|
|
828
|
+
var here = drag.id !== '' && drag.over === card.id
|
|
829
|
+
if (here && drag.before) out.push(dropLine('drop-' + card.id + '-before'))
|
|
830
|
+
out.push(row(card))
|
|
831
|
+
if (here && !drag.before) out.push(dropLine('drop-' + card.id + '-after'))
|
|
832
|
+
}
|
|
833
|
+
if (drag.id !== '' && drag.over === END) out.push(dropLine('drop-end'))
|
|
834
|
+
return out
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function tailZone() {
|
|
838
|
+
if (drag.id === '') return null
|
|
839
|
+
return h('div', {
|
|
840
|
+
key: 'ps-tail',
|
|
841
|
+
className: 'ps-tail',
|
|
842
|
+
onDragOver: function (event) {
|
|
843
|
+
event.preventDefault()
|
|
844
|
+
if (drag.over !== END) setDrag({ id: drag.id, over: END, before: false })
|
|
845
|
+
},
|
|
846
|
+
onDrop: function (event) {
|
|
847
|
+
event.preventDefault()
|
|
848
|
+
if (drag.id !== '') moveCard(drag.id, END, false)
|
|
849
|
+
setDrag({ id: '', over: '', before: true })
|
|
850
|
+
}
|
|
851
|
+
})
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (!loaded) return h('div', { className: 'ps-root' }, storageError || '正在从 DSH 存储读取指令…')
|
|
855
|
+
return h('div', { className: 'ps-root' },
|
|
856
|
+
storageError === '' ? null : h('div', { style: { color: 'var(--dsw-alias-state-error-primary)' } }, storageError),
|
|
857
|
+
h('div', { className: 'ps-toolbar' },
|
|
858
|
+
h(Input, {
|
|
859
|
+
icon: h(IconSearch, { size: 14 }),
|
|
860
|
+
value: query,
|
|
861
|
+
placeholder: '搜索标题、内容或标签',
|
|
862
|
+
onChange: function (event) { setQuery(event.target.value) }
|
|
863
|
+
}),
|
|
864
|
+
h(Button, {
|
|
865
|
+
size: 'sm', variant: 'outline', className: 'ps-new',
|
|
866
|
+
icon: h(IconPlus, { size: 16 }),
|
|
867
|
+
onClick: function () { startEdit(null) }
|
|
868
|
+
}, '新建')
|
|
869
|
+
),
|
|
870
|
+
tags.length === 0 ? null : h('div', { className: 'ps-filters' },
|
|
871
|
+
h(Pill, { key: '*', active: tagFilter === '', onClick: function () { setTagFilter('') } }, '全部'),
|
|
872
|
+
tags.map(function (tag) {
|
|
873
|
+
return h(Pill, {
|
|
874
|
+
key: tag,
|
|
875
|
+
active: tagFilter === tag,
|
|
876
|
+
onClick: function () { setTagFilter(tagFilter === tag ? '' : tag) }
|
|
877
|
+
}, tag)
|
|
878
|
+
})
|
|
879
|
+
),
|
|
880
|
+
h('div', { className: 'ps-list' },
|
|
881
|
+
editing !== null && editing.id === '' ? editor() : null,
|
|
882
|
+
rows.length === 0 && !(editing !== null && editing.id === '')
|
|
883
|
+
? h('div', { className: 'ps-empty' },
|
|
884
|
+
book.cards.length === 0
|
|
885
|
+
? '还没有指令。点右上角「新建」,或在已发送的消息下方点「存为指令」。'
|
|
886
|
+
: '没有匹配的指令。换个关键词或标签试试。')
|
|
887
|
+
: listRows(),
|
|
888
|
+
tailZone()
|
|
889
|
+
)
|
|
890
|
+
)
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// ---- 注册 ----
|
|
894
|
+
function apply(ctx) {
|
|
895
|
+
var slots = ctx.slots
|
|
896
|
+
ensureStyle()
|
|
897
|
+
loadStorage(ctx.connection).then(function () {
|
|
898
|
+
pruneSessions(ctx.sessions.list.getSnapshot())
|
|
899
|
+
})
|
|
900
|
+
ctx.effect(function () {
|
|
901
|
+
var list = ctx.sessions.list
|
|
902
|
+
return list.subscribe(function () { pruneSessions(list.getSnapshot()) })
|
|
903
|
+
}, 'dsh-prompt-shelf: prune sessions')
|
|
904
|
+
|
|
905
|
+
// 分叉当下复制来源会话的每轮注入清单,不在别处追踪分叉关系。
|
|
906
|
+
ctx.effect(function () {
|
|
907
|
+
var sessions = ctx.sessions
|
|
908
|
+
var fork = sessions.fork
|
|
909
|
+
var wrapper = function (opts) {
|
|
910
|
+
return fork.call(sessions, opts).then(function (childId) {
|
|
911
|
+
var source = everyOf(opts.sessionId)
|
|
912
|
+
if (source.length > 0) {
|
|
913
|
+
sessionBook[childId] = source.slice()
|
|
914
|
+
writeState()
|
|
915
|
+
}
|
|
916
|
+
return childId
|
|
917
|
+
})
|
|
918
|
+
}
|
|
919
|
+
sessions.fork = wrapper
|
|
920
|
+
return function () { if (sessions.fork === wrapper) sessions.fork = fork }
|
|
921
|
+
}, 'dsh-prompt-shelf: fork settings')
|
|
922
|
+
|
|
923
|
+
ctx.effect(function () {
|
|
924
|
+
return ctx.sidebarRightTabs.register({
|
|
925
|
+
id: TAB_ID,
|
|
926
|
+
kind: TAB_ID,
|
|
927
|
+
priority: 'extension',
|
|
928
|
+
title: function () { return '快捷指令' },
|
|
929
|
+
guide: [{
|
|
930
|
+
id: TAB_ID,
|
|
931
|
+
order: 40,
|
|
932
|
+
icon: IconShelf,
|
|
933
|
+
title: function () { return '快捷指令' },
|
|
934
|
+
description: function () { return '存取常用指令,按需加入本次消息或每轮自动注入' }
|
|
935
|
+
}]
|
|
936
|
+
})
|
|
937
|
+
}, 'dsh-prompt-shelf: tab type')
|
|
938
|
+
|
|
939
|
+
ctx.effect(function () {
|
|
940
|
+
return slots.inject('sidebar.right.pane.tab', function () {
|
|
941
|
+
return slots.register({ name: 'sidebar.right.pane.tab', key: TAB_ID }, Panel)
|
|
942
|
+
})
|
|
943
|
+
}, 'dsh-prompt-shelf: tab body')
|
|
944
|
+
|
|
945
|
+
ctx.effect(function () {
|
|
946
|
+
return slots.inject('conversation.composer.dock', function () {
|
|
947
|
+
return slots.register({ name: 'conversation.composer.dock', id: 'prompt-shelf-attach', order: 30, label: function () { return '本次附带' } }, AttachBar)
|
|
948
|
+
})
|
|
949
|
+
}, 'dsh-prompt-shelf: attach bar')
|
|
950
|
+
|
|
951
|
+
// 用户消息的键控渲染位:以更低优先级接管该 key,再原样渲染宿主组件,
|
|
952
|
+
// 只在它下面补一行保存按钮,原生气泡、图片、引用都保持宿主实现。
|
|
953
|
+
slotsRef = slots
|
|
954
|
+
function mountRow(kind) {
|
|
955
|
+
return slots.inject('conversation.chat.node', function () {
|
|
956
|
+
return slots.register({ name: 'conversation.chat.node', key: kind, priority: -1, locale: 'chat' }, UserMessageRow)
|
|
957
|
+
})
|
|
958
|
+
}
|
|
959
|
+
ctx.effect(function () {
|
|
960
|
+
var user = mountRow('user')
|
|
961
|
+
var steering = mountRow('steering')
|
|
962
|
+
return function () { user(); steering() }
|
|
963
|
+
}, 'dsh-prompt-shelf: message save')
|
|
964
|
+
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
exports.name = '@emptyvessel_003/dsh-prompt-shelf'
|
|
968
|
+
exports.inject = ['slots', 'sidebarRightTabs', 'sessions', 'connection']
|
|
969
|
+
exports.apply = apply
|
|
970
|
+
return module.exports
|
|
971
|
+
}
|
|
972
|
+
})
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rename, rm } from 'node:fs/promises'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
export const name = '@emptyvessel_003/dsh-prompt-shelf'
|
|
6
|
+
export const inject = ['connection', 'dshHomePath']
|
|
7
|
+
|
|
8
|
+
const CHANNEL = '/dsh-prompt-shelf'
|
|
9
|
+
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function validState(value) {
|
|
15
|
+
if (!isRecord(value) || !Array.isArray(value.cards) || !isRecord(value.sessions)) return false
|
|
16
|
+
for (const card of value.cards) {
|
|
17
|
+
if (!isRecord(card) || typeof card.id !== 'string' || typeof card.title !== 'string' || typeof card.content !== 'string') return false
|
|
18
|
+
if (card.tags !== undefined && (!Array.isArray(card.tags) || card.tags.some(tag => typeof tag !== 'string'))) return false
|
|
19
|
+
if (card.pinned !== undefined && typeof card.pinned !== 'boolean') return false
|
|
20
|
+
}
|
|
21
|
+
for (const cardIds of Object.values(value.sessions)) {
|
|
22
|
+
if (!Array.isArray(cardIds) || cardIds.some(id => typeof id !== 'string')) return false
|
|
23
|
+
}
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function apply(ctx) {
|
|
28
|
+
const file = ctx.dshHomePath('dsh-prompt-shelf', 'data.json')
|
|
29
|
+
|
|
30
|
+
ctx.effect(() => ctx.connection.rpc.handle(CHANNEL, async (endpoint, payload) => {
|
|
31
|
+
try {
|
|
32
|
+
if (endpoint === 'load') {
|
|
33
|
+
let text
|
|
34
|
+
try {
|
|
35
|
+
text = await readFile(file, 'utf8')
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error && error.code === 'ENOENT') return { ok: true, value: null }
|
|
38
|
+
throw error
|
|
39
|
+
}
|
|
40
|
+
const value = JSON.parse(text)
|
|
41
|
+
if (!validState(value)) throw new Error('指令架数据结构无效,已停止读取以保护原文件')
|
|
42
|
+
return { ok: true, value }
|
|
43
|
+
}
|
|
44
|
+
if (endpoint === 'save') {
|
|
45
|
+
if (!validState(payload)) return { ok: false, error: { code: 'invalid-state', message: '指令架数据结构无效' } }
|
|
46
|
+
await mkdir(dirname(file), { recursive: true })
|
|
47
|
+
const temporary = join(dirname(file), '.' + randomUUID() + '.tmp')
|
|
48
|
+
try {
|
|
49
|
+
const handle = await open(temporary, 'wx')
|
|
50
|
+
try {
|
|
51
|
+
await handle.writeFile(JSON.stringify(payload, null, 2) + '\n', 'utf8')
|
|
52
|
+
await handle.sync()
|
|
53
|
+
} finally {
|
|
54
|
+
await handle.close()
|
|
55
|
+
}
|
|
56
|
+
await rename(temporary, file)
|
|
57
|
+
} catch (error) {
|
|
58
|
+
await rm(temporary, { force: true })
|
|
59
|
+
throw error
|
|
60
|
+
}
|
|
61
|
+
return { ok: true, value: { saved: true } }
|
|
62
|
+
}
|
|
63
|
+
return { ok: false, error: { code: 'unknown-endpoint', message: 'unknown endpoint ' + String(endpoint) } }
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return { ok: false, error: { code: 'storage-error', message: error && error.message ? error.message : String(error) } }
|
|
66
|
+
}
|
|
67
|
+
}))
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emptyvessel_003/dsh-prompt-shelf",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DSH Web 插件:在右侧栏管理常用指令卡片,点一下带进下一条消息,按会话每轮自动注入,已发消息可一键存回。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"lib",
|
|
14
|
+
"cordis.patch.yml",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"CHANGELOG.md",
|
|
18
|
+
"assets"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"dsh",
|
|
22
|
+
"dsh-plugin",
|
|
23
|
+
"deepseek-harness",
|
|
24
|
+
"prompt",
|
|
25
|
+
"shortcut",
|
|
26
|
+
"prompt-library",
|
|
27
|
+
"right-sidebar"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20",
|
|
31
|
+
"dsh": ">=0.1.7-rc.2 <0.3.0"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"author": "EmptyVessel",
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/EmptyVessel-000/dsh-prompt-shelf.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/EmptyVessel-000/dsh-prompt-shelf/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/EmptyVessel-000/dsh-prompt-shelf#readme",
|
|
46
|
+
"dsh": {
|
|
47
|
+
"bundle": {
|
|
48
|
+
"patch": "./cordis.patch.yml"
|
|
49
|
+
},
|
|
50
|
+
"client": {
|
|
51
|
+
"platform": "web",
|
|
52
|
+
"inject": [
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-primitives"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|