dsh-agent-persona 0.1.7 → 0.1.9
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 +79 -0
- package/README.md +19 -11
- package/README_EN.md +22 -12
- package/lib/client.js +95 -21
- package/lib/endpoints.d.ts +1 -1
- package/lib/endpoints.js +1 -0
- package/lib/index.d.ts +34 -11
- package/lib/index.js +490 -115
- package/lib/remote.js +1 -0
- package/lib/typert.js +17 -0
- package/package.json +11 -7
- package/scripts/generate-remote.mjs +179 -0
- package/src/client.ts +3353 -0
- package/src/endpoints.ts +34 -0
- package/src/index.ts +2305 -0
- package/src/remote.ts +59 -0
- package/src/typert.ts +293 -0
- package/tsconfig.client.json +13 -0
- package/tsconfig.json +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,85 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.1.9] - 2026-10-23
|
|
8
|
+
|
|
9
|
+
版本历史从「只能看和恢复」补成「能管理」,并且不再记录重复的正文。
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **可以删除单个版本。** 选中一条,点「删除此版本」(连点两次确认)。删掉中间一条后,它后面的版本会
|
|
14
|
+
按新的前一条重新算 diff,链不会断:剩下的每一条仍然能逐条打开、恢复。删掉最后一条时这条人设的历史
|
|
15
|
+
记录一起清掉。
|
|
16
|
+
- **正文哈希去重。** 每条版本原本就存了 sha256 前 16 位,现在保存时先比对:如果这次要存的内容和已有
|
|
17
|
+
某条版本完全相同(撤销、恢复之后再保存、导入同一份正文都会遇到),就不新增记录,只提示「正文与第 N
|
|
18
|
+
个版本相同,没有新增版本」。列表行和对比窗口头部都显示这个哈希。
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **`savePersona({ id, name })` 仍会清空正文。** 0.1.8 声称修好了部分更新,但入口把整个 store 当成
|
|
23
|
+
「已有值」传下去,store 顶层没有 name/text 这些字段,于是没传的正文按默认值覆盖。现在按 id 取到那条
|
|
24
|
+
人设再合并:只改名,正文、范围、启用状态都不动。
|
|
25
|
+
|
|
26
|
+
### Tests
|
|
27
|
+
|
|
28
|
+
- 新增去重、删除(删中间/删首条/删最后一条/越界)与哈希格式的断言;测试总数 254。
|
|
29
|
+
- 删除后的链式重放、重算后的 `+n/−m` 统计、以及被删版本之后的 diff 都在测试里逐条验证。
|
|
30
|
+
|
|
31
|
+
## [0.1.8] - 2026-10-23
|
|
32
|
+
|
|
33
|
+
这一版几乎全部来自一次逐行读代码的审查:不改功能,修的是「它看起来在做 A,实际在做 B」的地方。
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **人设文件的守卫改成按能力判定,不再按工具名。** 原来维护两张工具名白名单,名单外的工具(`str_replace_editor`、
|
|
38
|
+
`terminal_send`,以及任何后来装上的插件工具)无论参数指向哪里都放行;`apply_patch` 的补丁正文更是完全看不见。
|
|
39
|
+
现在问的是「这次调用会不会写到受保护的文件」:参数里的路径(解析 `~` 与相对路径)、shell 行的重定向目标与写入型
|
|
40
|
+
命令、补丁正文里的 `*** Update File:` 目标,命中主文件、历史文件及其 `.bak-*`/`.tmp-*` 副本就拒。
|
|
41
|
+
- **不再误杀只读的重定向。** `jq . <人设文件> > /tmp/c.json` 以前因为同一行里既有路径又有 `>` 而被拒;现在看的是
|
|
42
|
+
`>` 后面那个目标,写到别处就放行,写到人设文件才拒(`2>/dev/null` 也不再算写入)。
|
|
43
|
+
- **`prefix` 规则不再把隔壁项目算进来。** `raw.startsWith(value)` 让 `/tmp/ws` 匹配到 `/tmp/ws-legacy`;现在两侧
|
|
44
|
+
`resolve` 后按分隔符比较,Windows 上按大小写不敏感比,`exact` 也开始认 `~/`。
|
|
45
|
+
- **正则规则不再能卡死事件循环。** 长度上限 500 字符,编译缓存上限 64 条(原来既无上限也无淘汰)。
|
|
46
|
+
- **局部更新不再毁字段。** `savePersona({ id, name })` 以前会把正文清空、把停用的人设重新启用(缺字段一律取默认值);
|
|
47
|
+
现在每个字段都是「传了才覆盖」。
|
|
48
|
+
- **保存失败不再留下脏内存。** 所有写操作先克隆当前 store 再改,持久化成功后才替换内存与时间戳;此前 `commit`
|
|
49
|
+
不刷新 `storeStamp`,写进去的内容下一次 reload 就被自己丢掉。
|
|
50
|
+
- **版本历史坏掉不再让保存永久失败。** 记录前先校验链能否重放到当前正文,不能就重新以当前正文为基准(自愈),
|
|
51
|
+
而不是抛「版本历史损坏」;历史文件损坏时先复制成 `.bak-<时间戳>` 再重建,不再直接覆盖。
|
|
52
|
+
- **`{"personas": []}` 不再静默吞掉全部版本。** 数组被当成对象接受,版本写到数组属性上,`JSON.stringify` 一序列化就没了。
|
|
53
|
+
- **删除人设会清掉它的历史。** 之前 `personas.history.json` 只增不减。
|
|
54
|
+
- **`duplicatePersona` 的副本保留注入方式。** 复制 `replace` 人设会退化成 `append`。
|
|
55
|
+
- **备份文件名不再同秒覆盖。** 时间戳加到毫秒。
|
|
56
|
+
- **`clipMarkdown` 不再劈开代理对。** 按 UTF-16 长度切片时可能停在半个 emoji 上,现在退一位。
|
|
57
|
+
- **RPC 失败只回 `error.message`。** 以前把 `error.stack` 整段回给浏览器。
|
|
58
|
+
- **配置里的 `tuneProvider` / `tuneModel` 进了 schema。** 代码一直在读它们,README 一直在写它们,只有校验层不认。
|
|
59
|
+
- **设置页保存不再吞掉在途输入。** 保存返回时用服务端数据覆盖草稿,保存期间继续敲的字会消失;现在只有草稿没被改过
|
|
60
|
+
才覆盖,并用 `saveSeq` 丢弃过期响应。
|
|
61
|
+
- **恢复版本、切换开关不再与在途保存打架。** 恢复前取消自动保存定时器并推进 `saveSeq`;版本 diff 请求带令牌,
|
|
62
|
+
过期响应不会覆盖新选择。
|
|
63
|
+
- **关掉设置页不会再丢最后一次编辑。** 卸载时若草稿是脏的且自动保存开着,补一次保存。
|
|
64
|
+
- **`Cmd/Ctrl+S` 不再能重入。** 保存中按快捷键会并发发两个请求。
|
|
65
|
+
- **toast 不再常驻。** `onDone` 每次渲染都是新函数,宿主 `Toast` 的 `useEffect` 依赖它 → 定时器被不断续命。
|
|
66
|
+
- **自动保存不再每次都弹两句话。** 去掉了先报的「正在自动保存…」,只留结果。
|
|
67
|
+
- **偏好同步只发改动的那一项。** 以前发整份 prefs,两个标签页会互相覆盖。
|
|
68
|
+
- **设置页刷新后偏好与存储一致。** `view.prefs` 现在也回填到界面状态,不只是写 localStorage。
|
|
69
|
+
- **导入失败后重新拉取。** replace 模式中途失败会留下删了一半的列表。
|
|
70
|
+
- **开关的无障碍语义。** `role="switch"` + `aria-checked` + `aria-describedby`(说明不再塞进 `aria-label` 变成一句话)。
|
|
71
|
+
- **`injectStyles` 缓存不再指向已移除的节点。**
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- `files` 补上 `src`、`scripts/generate-remote.mjs` 与两份 tsconfig:以前 `npm ci` 之后 `prepare` 会用构建产物
|
|
76
|
+
覆盖源码,仓库里「生成产物漂移」的校验因此永远是绿的,而消费者从一个 tarball 里 rebuild 会直接失败。
|
|
77
|
+
- CI 增加矩阵(Node 22 / 24)、`git diff --exit-code` 漂移门,以及一个把 tarball 装进空目录再 import 的冒烟 job。
|
|
78
|
+
- `release.yml` 的手动触发默认 `dry_run: true`(以前默认直接发布)。
|
|
79
|
+
- peer 依赖加上上界 `<0.3.0`:`>=` 表示「任何未来版本都能用」,这不成立。
|
|
80
|
+
|
|
81
|
+
### Known
|
|
82
|
+
|
|
83
|
+
- `exports["./client"]` 仍然没有 `types`:`lib/client.js` 是 esbuild 打成的一整个 IIFE,没有对应的 `.d.ts` 产物。
|
|
84
|
+
补它需要单独的类型入口,留给下一版。
|
|
85
|
+
|
|
7
86
|
## [0.1.7] - 2026-10-23
|
|
8
87
|
|
|
9
88
|
### Fixed
|
package/README.md
CHANGED
|
@@ -111,6 +111,10 @@ dsh plugin --profile web remove dsh-agent-persona # 卸载
|
|
|
111
111
|
- **只记正文**:名称、启用状态、适用范围不参与版本,改这些不会产生一条记录。
|
|
112
112
|
- **每条都是 diff**:相等行只记行数,只有真正改动的行才存原文,所以一百条版本也远小于一百份正文。
|
|
113
113
|
- **恢复**:选中任意一条点「恢复到此版本」写回正文;恢复动作本身也记一条,恢复错了还能再回滚。
|
|
114
|
+
- **删除**:选中任意一条点「删除此版本」(连点两次确认)就把这条记录删掉;它后面的版本会重新计算
|
|
115
|
+
diff,仍然可以逐条打开,链不会断。
|
|
116
|
+
- **同一份正文只存一条**:每条版本带正文的 sha256 前 16 位,保存时若和已有版本的内容哈希相同,就不
|
|
117
|
+
再新增记录(撤销、恢复后再保存、导入同样的正文都属于这种情况),列表和对比窗口里都能看到哈希。
|
|
114
118
|
- **链式重放**:第一条版本作为基准快照,之后逐条重放;超过 200 条时把最旧的几条折进基准,
|
|
115
119
|
保留下来的版本仍可逐条打开。每条记录带内容摘要,历史文件被手改坏时重放会报错,不会给出别的正文。
|
|
116
120
|
- 关掉开关只是停止记录,已有历史仍可查看与恢复。
|
|
@@ -145,7 +149,8 @@ dsh plugin --profile web remove dsh-agent-persona # 卸载
|
|
|
145
149
|
| 编辑后自动保存 | 关 | 停手约 1 秒自动保存编辑区内容 | 立即;切换到人设规则行(工作区/会话)时**不**自动保存,避免存下半截选择 |
|
|
146
150
|
| 在对话页显示「人设」标签 | 开 | 是否注册对话页那个视图 | 立即(运行时注册/注销) |
|
|
147
151
|
| 注册到 dsh-better-sidebar | 开 | 是否把「人设」页面注册进 dsh-better-sidebar 的右侧栏 | 立即;**没装该插件时这个开关不显示** |
|
|
148
|
-
| 禁止模型改写人设文件 | 开 |
|
|
152
|
+
| 禁止模型改写人设文件 | 开 | 按**能力**判定:任何会写入/删除人设存储(含它的历史文件与备份副本)的工具调用都拒绝,读取、`grep`、把路径写进笔记都不受影响;关掉后不拦 | 立即;对**下一次工具调用**生效 |
|
|
153
|
+
| 保存人设版本历史 | 关 | 每次保存记一条可重放的 diff(最多 200 条,可查看/对比/恢复) | 立即;关掉后不再记新版本,已有历史仍可查看 |
|
|
149
154
|
|
|
150
155
|
偏好存在人设文件的 `prefs` 字段里(不是浏览器),所以它和数据的可见范围一致;浏览器里只留一份镜像,
|
|
151
156
|
供"加载时决定要不要注册视图"用。
|
|
@@ -179,8 +184,9 @@ dsh plugin --profile web remove dsh-agent-persona # 卸载
|
|
|
179
184
|
|
|
180
185
|
```
|
|
181
186
|
~/.dsh/dsh-agent-persona/
|
|
182
|
-
├── personas.json
|
|
183
|
-
|
|
187
|
+
├── personas.json # 人设数据 + 偏好,权限 600,原子写入
|
|
188
|
+
├── personas.history.json # 版本历史(开关打开后才出现)
|
|
189
|
+
└── state.json # 加载心跳,每次加载时重写
|
|
184
190
|
```
|
|
185
191
|
|
|
186
192
|
正文里可以写 `{{model}}` 和 `{{cwd}}`,渲染时替换成当前的值;其他 `{{...}}` 会被去掉(未注册的变量会让
|
|
@@ -218,16 +224,18 @@ AI 只产出建议稿,点「采用」或「追加」才会进编辑框,再
|
|
|
218
224
|
## 几句实话
|
|
219
225
|
|
|
220
226
|
- **人设是行为约束,不是安全边界**。它约束模型怎么说,不阻止模型用工具。
|
|
221
|
-
-
|
|
222
|
-
|
|
223
|
-
|
|
227
|
+
- 人设文件的防篡改是**能力层面**的判定:插件注册了全局 `tools.guard`,它不看工具叫什么名字,而是看这次调用会不会写到
|
|
228
|
+
受保护的文件 —— 参数里的路径(解析 `~` 与相对路径)、shell 命令里的重定向目标、写入型命令(删、移、原地改、解释器
|
|
229
|
+
`-c`),以及补丁正文里的 `*** Update File:` 目标。受保护的是主文件 `personas.json`、它的历史文件和它们的备份/临时副本。
|
|
230
|
+
- 它仍然只是行为约束,绕过面写在明处:本机 `danger-full-access` 下改插件代码、改 profile 补丁文件都能绕过;脚本里
|
|
231
|
+
运行时拼出来的路径静态看不出;`$VAR` 展开看不出;别名/函数包装也看不出。要更结实得在本机层面做:
|
|
224
232
|
`chflags uchg <人设文件>`(自己改之前先 `chflags nouchg`),或者把 DSH 跑在受限沙箱里、让人设文件属于别的用户。
|
|
225
|
-
-
|
|
233
|
+
- 判定**按单次工具调用**生效:同一条命令里既读又写,写的那部分照样被拒,只读的部分不会被连坐。
|
|
226
234
|
- 多处同时匹配时**只取第一条**,不拼接;规则是线性扫的,几十条没问题,几百条就该重新设计了。
|
|
227
235
|
- 一个 profile 里只装一次。同一个进程挂两个同 id 的行会让组合起不来。
|
|
228
|
-
- **测试的边界**:仓库里的测试是**单元测试 + 源码审计**(
|
|
229
|
-
|
|
230
|
-
|
|
236
|
+
- **测试的边界**:仓库里的测试是**单元测试 + 源码审计**(236 条宿主断言 + 187 条客户端断言 + 生成产物漂移校验;
|
|
237
|
+
这个数字由测试自己统计,不会随文件改动而失真),它们保证逻辑与代码形状,**不驱动浏览器** —— 界面行为
|
|
238
|
+
(视图渲染、开关、侧边栏卡片)目前靠人工核对。
|
|
231
239
|
|
|
232
240
|
## 开发
|
|
233
241
|
|
|
@@ -246,7 +254,7 @@ docs/ 架构、开发笔记、设计决策
|
|
|
246
254
|
npm install
|
|
247
255
|
npm run build # generate:remote → tsc → esbuild(产出 lib/)
|
|
248
256
|
npm run typecheck # 两个 tsconfig 都不报错
|
|
249
|
-
npm test #
|
|
257
|
+
npm test # 236 + 187 条断言 + 生成产物漂移校验
|
|
250
258
|
dsh --profile web --dump-config | grep -c 'id: agent-persona' # 组合自检,应输出 1
|
|
251
259
|
```
|
|
252
260
|
|
package/README_EN.md
CHANGED
|
@@ -121,6 +121,11 @@ Turn on **Save persona version history** in Settings and every save records the
|
|
|
121
121
|
- **Text only**: name, enabled flag and targets are not versioned.
|
|
122
122
|
- **Diffs, not snapshots**: equal runs are stored as line counts; only changed lines keep their text.
|
|
123
123
|
- **Restore**: pick any version and restore it; the restore itself is recorded, so it can be undone too.
|
|
124
|
+
- **Delete**: pick a version and press Delete (twice: the first press arms it). The versions after it are
|
|
125
|
+
re-diffed against its predecessor, so the chain stays replayable.
|
|
126
|
+
- **One row per text**: each record carries the first 16 hex characters of its text's sha256. A save whose
|
|
127
|
+
text hashes to a version that is already stored adds nothing — undo, restore-then-save and importing the
|
|
128
|
+
same body all land on the existing row. The hash is shown in the row and in the diff header.
|
|
124
129
|
- **Chained replay**: the first text is the base snapshot, later versions replay on top of it; past 200
|
|
125
130
|
versions the oldest are folded into the base. A content hash per record makes a hand-edited history fail
|
|
126
131
|
loudly instead of restoring different text.
|
|
@@ -194,8 +199,9 @@ error.
|
|
|
194
199
|
|
|
195
200
|
```
|
|
196
201
|
~/.dsh/dsh-agent-persona/
|
|
197
|
-
├── personas.json
|
|
198
|
-
|
|
202
|
+
├── personas.json # personas + preferences, mode 600, written atomically
|
|
203
|
+
├── personas.history.json # version history (only while the switch is on)
|
|
204
|
+
└── state.json # load heartbeat, rewritten on every load
|
|
199
205
|
```
|
|
200
206
|
|
|
201
207
|
The body may use `{{model}}` and `{{cwd}}`; other `{{...}}` are de-braced (an unregistered variable would make the
|
|
@@ -229,18 +235,22 @@ Agent 人设 page (`保存在 …`), so it is never a mystery which file you are
|
|
|
229
235
|
|
|
230
236
|
- **A persona is a behavioural constraint, not a security boundary.** It shapes what the model says; it does not
|
|
231
237
|
stop tools.
|
|
232
|
-
- Tamper protection is
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
- Tamper protection is decided by **capability, not by tool name**: a global `tools.guard` asks whether this call
|
|
239
|
+
would write a protected file — the paths in its arguments (resolved through `~` and relative paths), redirect
|
|
240
|
+
targets and writing commands in a shell line (delete, move, in-place edit, interpreter `-c`), and the targets
|
|
241
|
+
named by `*** Update File:` markers in a patch body. Protected: `personas.json`, its history file, and their
|
|
242
|
+
backup and temp copies.
|
|
243
|
+
- It is still a behavioural measure, and the bypass surface is stated plainly: on `danger-full-access` you can
|
|
244
|
+
edit the plugin or the profile patch; a path assembled at runtime inside a script, `$VAR` expansion, or an
|
|
245
|
+
alias/function wrapper is invisible to a static read. Harder options live at the machine level: `chflags uchg
|
|
246
|
+
<store>` (run `chflags nouchg` before editing it yourself), or a restricted sandbox with the store owned by
|
|
247
|
+
another user.
|
|
248
|
+
- The decision is per **tool call**: a line that both reads and writes has its writing half refused; the reading
|
|
249
|
+
half is not implicated.
|
|
240
250
|
- Only the **first** match is used — no concatenation. Matching is a linear scan; dozens of rules are fine,
|
|
241
251
|
hundreds want a redesign.
|
|
242
252
|
- Install once per profile: two rows with the same id in one process make the composition fail.
|
|
243
|
-
- **Test boundary**: the suite is unit tests plus source audits (
|
|
253
|
+
- **Test boundary**: the suite is unit tests plus source audits (236 host assertions, 187 client assertions, and a
|
|
244
254
|
generated-artifact drift check; the numbers are counted by the suites themselves, so they cannot fall behind
|
|
245
255
|
the files). It does not drive a browser — UI behaviour is checked by hand today.
|
|
246
256
|
|
|
@@ -261,7 +271,7 @@ docs/ architecture, development notes, design decisions
|
|
|
261
271
|
npm install
|
|
262
272
|
npm run build # generate:remote → tsc → esbuild (writes lib/)
|
|
263
273
|
npm run typecheck # both tsconfigs, no errors
|
|
264
|
-
npm test #
|
|
274
|
+
npm test # 236 + 187 assertions plus the generated-artifact drift check
|
|
265
275
|
dsh --profile web --dump-config | grep -c 'id: agent-persona' # composition self-check, expects 1
|
|
266
276
|
```
|
|
267
277
|
|
package/lib/client.js
CHANGED
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
method("savePrefs", withInput()),
|
|
48
48
|
method("listVersions", withInput()),
|
|
49
49
|
method("versionDiff", withInput()),
|
|
50
|
-
method("restoreVersion", withInput())
|
|
50
|
+
method("restoreVersion", withInput()),
|
|
51
|
+
method("deleteVersion", withInput())
|
|
51
52
|
]
|
|
52
53
|
};
|
|
53
54
|
|
|
@@ -246,7 +247,7 @@
|
|
|
246
247
|
report({ prefs: next, status: prefStatus(patch) });
|
|
247
248
|
syncSidebarModule();
|
|
248
249
|
if (api === void 0 || typeof api.savePrefs !== "function") return next;
|
|
249
|
-
void api.savePrefs(
|
|
250
|
+
void api.savePrefs(patch).then((result) => {
|
|
250
251
|
const view = unwrap === void 0 ? result : unwrap(result);
|
|
251
252
|
const stored = view !== void 0 && view !== null && view.prefs !== void 0 && view.prefs !== null ? writePrefs(view.prefs) : void 0;
|
|
252
253
|
report(stored === void 0 ? { view } : { prefs: { ...next, ...stored }, view });
|
|
@@ -496,6 +497,10 @@
|
|
|
496
497
|
.wsp-switch input:checked + .wsp-switch-track { background: var(--wsp-success); }
|
|
497
498
|
.wsp-switch input:checked + .wsp-switch-track::after { transform: translateX(12px); }
|
|
498
499
|
.wsp-switch input:focus-visible + .wsp-switch-track { outline: 2px solid var(--wsp-accent); outline-offset: 2px; }
|
|
500
|
+
.wsp-sr-only {
|
|
501
|
+
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
|
|
502
|
+
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
|
|
503
|
+
}
|
|
499
504
|
.wsp-switch-text { font-size: 11.5px; color: var(--wsp-muted); }
|
|
500
505
|
|
|
501
506
|
/* \u2500\u2500 editor / preview \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
@@ -746,6 +751,7 @@
|
|
|
746
751
|
.wsp-version-title { font-size: 13px; font-weight: 600; }
|
|
747
752
|
.wsp-version-meta { font-size: 11px; opacity: 0.72; font-variant-numeric: tabular-nums; }
|
|
748
753
|
.wsp-version-preview { font-size: 11px; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
754
|
+
.wsp-version-hash { font-family: var(--wsp-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 11px; opacity: 0.55; }
|
|
749
755
|
.wsp-versions-diff { display: flex; flex-direction: column; gap: 8px; min-width: 0; overflow: hidden; }
|
|
750
756
|
.wsp-diff-head { font-size: 12px; opacity: 0.75; font-variant-numeric: tabular-nums; }
|
|
751
757
|
.wsp-diff-body { overflow: auto; border: 1px solid var(--wsp-line); border-radius: 8px; padding: 8px 0; background: var(--wsp-line-soft); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.55; }
|
|
@@ -756,7 +762,8 @@
|
|
|
756
762
|
`;
|
|
757
763
|
let styleElement;
|
|
758
764
|
const injectStyles = () => {
|
|
759
|
-
if (styleElement !== void 0) return styleElement;
|
|
765
|
+
if (styleElement !== void 0 && styleElement.isConnected) return styleElement;
|
|
766
|
+
if (styleElement !== void 0) styleElement.remove();
|
|
760
767
|
styleElement = document.createElement("style");
|
|
761
768
|
styleElement.setAttribute("data-dsh-agent-persona", "");
|
|
762
769
|
styleElement.textContent = CSS;
|
|
@@ -1151,18 +1158,27 @@
|
|
|
1151
1158
|
const Badge = ({ children, tone }) => h("span", {
|
|
1152
1159
|
className: tone === void 0 ? "wsp-badge" : `wsp-badge wsp-badge-${tone}`
|
|
1153
1160
|
}, children);
|
|
1161
|
+
let switchId = 0;
|
|
1154
1162
|
const Switch = ({ checked, disabled, label, hint, onChange }) => {
|
|
1163
|
+
const descriptionId = React.useMemo(() => `wsp-switch-hint-${switchId += 1}`, []);
|
|
1155
1164
|
const control = h("label", { className: "wsp-switch", title: hint === void 0 ? label : void 0 }, [
|
|
1156
1165
|
h("input", {
|
|
1157
1166
|
key: "i",
|
|
1158
1167
|
type: "checkbox",
|
|
1168
|
+
// Role and state, not just appearance: a checkbox announced as "checkbox" next to a track that
|
|
1169
|
+
// says "switch" is two different controls to a screen reader. The hint belongs in the
|
|
1170
|
+
// description, not in the name — a name that reads a whole sentence is unusable in a list.
|
|
1171
|
+
role: "switch",
|
|
1172
|
+
"aria-checked": checked === true,
|
|
1159
1173
|
checked,
|
|
1160
1174
|
disabled,
|
|
1161
|
-
"aria-label":
|
|
1175
|
+
"aria-label": label,
|
|
1176
|
+
"aria-describedby": hint === void 0 ? void 0 : descriptionId,
|
|
1162
1177
|
onChange: (event) => onChange(event.target.checked)
|
|
1163
1178
|
}),
|
|
1164
1179
|
h("span", { key: "t", className: "wsp-switch-track" }),
|
|
1165
|
-
h("span", { key: "l", className: "wsp-switch-text" }, label)
|
|
1180
|
+
h("span", { key: "l", className: "wsp-switch-text" }, label),
|
|
1181
|
+
hint === void 0 ? null : h("span", { key: "d", id: descriptionId, className: "wsp-sr-only" }, hint)
|
|
1166
1182
|
]);
|
|
1167
1183
|
return hint === void 0 ? control : h(Tooltip, { label: hint, side: "top", delayMs: 260, maxWidth: 320 }, control);
|
|
1168
1184
|
};
|
|
@@ -1188,6 +1204,7 @@
|
|
|
1188
1204
|
versionsError: "",
|
|
1189
1205
|
versionDiff: void 0,
|
|
1190
1206
|
versionIndex: void 0,
|
|
1207
|
+
versionConfirm: false,
|
|
1191
1208
|
models: [],
|
|
1192
1209
|
model: "",
|
|
1193
1210
|
tuneOpen: false,
|
|
@@ -1211,6 +1228,9 @@
|
|
|
1211
1228
|
this.importHighlightRef = React.createRef();
|
|
1212
1229
|
this.importTimer = void 0;
|
|
1213
1230
|
this.statusSeq = 0;
|
|
1231
|
+
this.saveSeq = 0;
|
|
1232
|
+
this.versionSeq = 0;
|
|
1233
|
+
this.clearStatus = () => this.onChange({ status: "" });
|
|
1214
1234
|
this.onChange = (patch) => {
|
|
1215
1235
|
if (!this.mounted) return;
|
|
1216
1236
|
if (patch !== null && typeof patch === "object" && "status" in patch) {
|
|
@@ -1225,6 +1245,7 @@
|
|
|
1225
1245
|
const root = this.rootRef.current;
|
|
1226
1246
|
if (root === null || !root.contains(event.target)) return;
|
|
1227
1247
|
event.preventDefault();
|
|
1248
|
+
if (this.state.busy === "save") return;
|
|
1228
1249
|
void this.save();
|
|
1229
1250
|
};
|
|
1230
1251
|
}
|
|
@@ -1236,6 +1257,11 @@
|
|
|
1236
1257
|
void this.load();
|
|
1237
1258
|
}
|
|
1238
1259
|
componentWillUnmount() {
|
|
1260
|
+
if (this.autosaveTimer !== void 0 && this.isDirty()) {
|
|
1261
|
+
clearTimeout(this.autosaveTimer);
|
|
1262
|
+
this.autosaveTimer = void 0;
|
|
1263
|
+
void this.save();
|
|
1264
|
+
}
|
|
1239
1265
|
clearTimeout(this.autosaveTimer);
|
|
1240
1266
|
clearTimeout(this.importTimer);
|
|
1241
1267
|
this.mounted = false;
|
|
@@ -1285,12 +1311,13 @@
|
|
|
1285
1311
|
this.onChange({ busy: "load", error: "" });
|
|
1286
1312
|
try {
|
|
1287
1313
|
const view = this.unwrap(await api.listPersonas());
|
|
1288
|
-
|
|
1314
|
+
const stored = view?.prefs !== void 0 && view?.prefs !== null ? writePrefs(view.prefs) : void 0;
|
|
1289
1315
|
const wanted = keepOpen === true ? this.state.openId : void 0;
|
|
1290
1316
|
const persona = wanted === void 0 ? void 0 : (view.personas || []).find((item) => item.id === wanted);
|
|
1291
1317
|
this.onChange({
|
|
1292
1318
|
view,
|
|
1293
1319
|
busy: "",
|
|
1320
|
+
...stored === void 0 ? {} : { prefs: { ...this.state.prefs, ...stored } },
|
|
1294
1321
|
openId: persona === void 0 ? void 0 : wanted,
|
|
1295
1322
|
draft: persona === void 0 ? void 0 : this.draftFrom(persona)
|
|
1296
1323
|
});
|
|
@@ -1376,7 +1403,7 @@
|
|
|
1376
1403
|
this.onChange({ versionsError: "\u8FD9\u4E2A\u7248\u672C\u8FD8\u6CA1\u6709\u7248\u672C\u5386\u53F2\u63A5\u53E3\uFF1A\u8BF7\u66F4\u65B0\u63D2\u4EF6\u540E\u91CD\u542F dsh web" });
|
|
1377
1404
|
return;
|
|
1378
1405
|
}
|
|
1379
|
-
this.onChange({ versionsBusy: "list", versionsError: "", versions: { id }, versionDiff: void 0, versionIndex: void 0 });
|
|
1406
|
+
this.onChange({ versionsBusy: "list", versionsError: "", versions: { id }, versionDiff: void 0, versionIndex: void 0, versionConfirm: false });
|
|
1380
1407
|
try {
|
|
1381
1408
|
const data = this.unwrap(await api.listVersions({ id }));
|
|
1382
1409
|
this.onChange({ versionsBusy: "", versions: data });
|
|
@@ -1390,11 +1417,14 @@
|
|
|
1390
1417
|
const api = this.api();
|
|
1391
1418
|
const versions = this.state.versions;
|
|
1392
1419
|
if (api === void 0 || versions === void 0 || index < 0) return;
|
|
1393
|
-
|
|
1420
|
+
const seq = this.versionSeq += 1;
|
|
1421
|
+
this.onChange({ versionsBusy: "diff", versionIndex: index, versionsError: "", versionConfirm: false });
|
|
1394
1422
|
try {
|
|
1395
1423
|
const diff = this.unwrap(await api.versionDiff({ id: versions.id, index }));
|
|
1424
|
+
if (seq !== this.versionSeq) return;
|
|
1396
1425
|
this.onChange({ versionsBusy: "", versionDiff: diff });
|
|
1397
1426
|
} catch (error) {
|
|
1427
|
+
if (seq !== this.versionSeq) return;
|
|
1398
1428
|
this.onChange({ versionsBusy: "", versionDiff: void 0, versionsError: String(error && error.message || error) });
|
|
1399
1429
|
}
|
|
1400
1430
|
}
|
|
@@ -1403,6 +1433,9 @@
|
|
|
1403
1433
|
const api = this.api();
|
|
1404
1434
|
const versions = this.state.versions;
|
|
1405
1435
|
if (api === void 0 || versions === void 0) return;
|
|
1436
|
+
clearTimeout(this.autosaveTimer);
|
|
1437
|
+
this.autosaveTimer = void 0;
|
|
1438
|
+
this.saveSeq += 1;
|
|
1406
1439
|
this.onChange({ versionsBusy: "restore", versionsError: "" });
|
|
1407
1440
|
try {
|
|
1408
1441
|
const view = this.unwrap(await api.restoreVersion({ id: versions.id, index }));
|
|
@@ -1418,6 +1451,36 @@
|
|
|
1418
1451
|
this.onChange({ versionsBusy: "", versionsError: String(error && error.message || error) });
|
|
1419
1452
|
}
|
|
1420
1453
|
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Delete one stored version. Two clicks: the first arms the footer button, the second deletes — a
|
|
1456
|
+
* history entry is the only copy of a text that may not exist anywhere else.
|
|
1457
|
+
*/
|
|
1458
|
+
async deleteVersion(index) {
|
|
1459
|
+
const api = this.api();
|
|
1460
|
+
const versions = this.state.versions;
|
|
1461
|
+
if (api === void 0 || versions === void 0) return;
|
|
1462
|
+
if (this.state.versionConfirm !== true) {
|
|
1463
|
+
this.onChange({ versionConfirm: true });
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
clearTimeout(this.autosaveTimer);
|
|
1467
|
+
this.autosaveTimer = void 0;
|
|
1468
|
+
this.saveSeq += 1;
|
|
1469
|
+
this.onChange({ versionsBusy: "delete", versionsError: "", versionConfirm: false });
|
|
1470
|
+
try {
|
|
1471
|
+
const view = this.unwrap(await api.deleteVersion({ id: versions.id, index }));
|
|
1472
|
+
const found = (view?.personas ?? []).find((item) => item.id === versions.id);
|
|
1473
|
+
this.onChange({
|
|
1474
|
+
view,
|
|
1475
|
+
versionsBusy: "",
|
|
1476
|
+
status: view?.notice ?? "\u5DF2\u5220\u9664\u8BE5\u7248\u672C",
|
|
1477
|
+
draft: this.state.openId === versions.id && found !== void 0 ? this.draftFrom(found) : this.state.draft
|
|
1478
|
+
});
|
|
1479
|
+
await this.openVersions(versions.id);
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
this.onChange({ versionsBusy: "", versionsError: String(error && error.message || error) });
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1421
1484
|
/** Read a session's recent messages so the user can recognise it. */
|
|
1422
1485
|
async openHistory(sessionId) {
|
|
1423
1486
|
const api = this.api();
|
|
@@ -1606,7 +1669,6 @@
|
|
|
1606
1669
|
clearTimeout(this.autosaveTimer);
|
|
1607
1670
|
this.autosaveTimer = setTimeout(() => {
|
|
1608
1671
|
this.autosaveTimer = void 0;
|
|
1609
|
-
this.onChange({ status: "\u6B63\u5728\u81EA\u52A8\u4FDD\u5B58\u2026" });
|
|
1610
1672
|
void this.save();
|
|
1611
1673
|
}, 1200);
|
|
1612
1674
|
}
|
|
@@ -1789,6 +1851,7 @@
|
|
|
1789
1851
|
});
|
|
1790
1852
|
} catch (error) {
|
|
1791
1853
|
this.onChange({ transfer: { ...transfer, busy: false, error: `\u5BFC\u5165\u5931\u8D25\uFF1A${String(error && error.message || error)}` } });
|
|
1854
|
+
await this.load(true);
|
|
1792
1855
|
}
|
|
1793
1856
|
}
|
|
1794
1857
|
async pickImportFile(event) {
|
|
@@ -1808,6 +1871,8 @@
|
|
|
1808
1871
|
const api = this.api();
|
|
1809
1872
|
const draft = this.state.draft;
|
|
1810
1873
|
if (api === void 0 || draft === void 0) return;
|
|
1874
|
+
if (this.state.busy === "save") return;
|
|
1875
|
+
const seq = this.saveSeq += 1;
|
|
1811
1876
|
this.onChange({ busy: "save", error: "", status: "" });
|
|
1812
1877
|
try {
|
|
1813
1878
|
const view = this.unwrap(await api.savePersona({
|
|
@@ -1819,14 +1884,13 @@
|
|
|
1819
1884
|
text: draft.text,
|
|
1820
1885
|
targets: draft.targets
|
|
1821
1886
|
}));
|
|
1887
|
+
if (seq !== this.saveSeq) return;
|
|
1822
1888
|
const persona = (view.personas || []).find((item) => item.id === draft.id);
|
|
1823
|
-
this.
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
draft: persona === void 0 ? void 0 : this.draftFrom(persona),
|
|
1827
|
-
status: view.notice ?? "\u5DF2\u4FDD\u5B58\uFF0C\u4E0B\u4E00\u6761\u6D88\u606F\u5C31\u4F1A\u7528\u4E0A"
|
|
1828
|
-
});
|
|
1889
|
+
const latest = this.state.draft;
|
|
1890
|
+
const draftAfter = latest === draft && persona !== void 0 ? this.draftFrom(persona) : latest;
|
|
1891
|
+
this.onChange({ view, busy: "", draft: draftAfter, status: view.notice ?? "\u5DF2\u4FDD\u5B58\uFF0C\u4E0B\u4E00\u6761\u6D88\u606F\u5C31\u4F1A\u7528\u4E0A" });
|
|
1829
1892
|
} catch (error) {
|
|
1893
|
+
if (seq !== this.saveSeq) return;
|
|
1830
1894
|
this.onChange({ busy: "", error: String(error && error.message || error) });
|
|
1831
1895
|
}
|
|
1832
1896
|
}
|
|
@@ -2447,7 +2511,6 @@ ${state.proposal.trim()}
|
|
|
2447
2511
|
onClick: () => this.onChange({ filter: "" })
|
|
2448
2512
|
}, "\u6E05\u7A7A")
|
|
2449
2513
|
]);
|
|
2450
|
-
const hiddenNote = null;
|
|
2451
2514
|
const conflictNotice = conflicts.size === 0 ? null : h("div", { className: "wsp-conflict-notice", key: "conflicts" }, [
|
|
2452
2515
|
h(IconWarningOutline16, { key: "i" }),
|
|
2453
2516
|
`${conflicts.size} \u6761\u4EBA\u8BBE\u7684\u8303\u56F4\u53EF\u80FD\u88AB\u4E0A\u9762\u7684\u4EBA\u8BBE\u5148\u547D\u4E2D\uFF08\u9F20\u6807\u60AC\u505C\u5BF9\u5E94\u5361\u7247\u770B\u539F\u56E0\uFF09`
|
|
@@ -2500,7 +2563,7 @@ ${state.proposal.trim()}
|
|
|
2500
2563
|
key: `st-${String(state.statusSeq)}`,
|
|
2501
2564
|
text: state.status,
|
|
2502
2565
|
icon: h(IconCheckOutline14, {}),
|
|
2503
|
-
onDone:
|
|
2566
|
+
onDone: this.clearStatus
|
|
2504
2567
|
});
|
|
2505
2568
|
const history = state.history;
|
|
2506
2569
|
const historyModal = h(Modal, {
|
|
@@ -2605,7 +2668,7 @@ ${state.proposal.trim()}
|
|
|
2605
2668
|
key: "guard",
|
|
2606
2669
|
checked: prefs.guard !== false,
|
|
2607
2670
|
label: "\u7981\u6B62\u6A21\u578B\u6539\u5199\u4EBA\u8BBE\u6587\u4EF6",
|
|
2608
|
-
hint: "\
|
|
2671
|
+
hint: "\u6309\u80FD\u529B\u5224\u5B9A\uFF1A\u4EFB\u4F55\u4F1A\u5199\u5165\u6216\u5220\u9664\u4EBA\u8BBE\u5B58\u50A8\uFF08\u542B\u5B83\u7684\u5386\u53F2\u6587\u4EF6\u4E0E\u5907\u4EFD\u526F\u672C\uFF09\u7684\u5DE5\u5177\u8C03\u7528\u90FD\u4F1A\u88AB\u62D2\uFF0C\u4E0D\u770B\u5DE5\u5177\u53EB\u4EC0\u4E48\u540D\u5B57\uFF1B\u8BFB\u53D6\u3001\u641C\u7D22\u3001\u628A\u8DEF\u5F84\u5199\u8FDB\u7B14\u8BB0\u90FD\u4E0D\u53D7\u5F71\u54CD\u3002\u8FD9\u662F\u884C\u4E3A\u5C42\u9762\u7684\u9650\u5236\uFF0C\u4E0D\u662F\u5B89\u5168\u8FB9\u754C\uFF1A\u672C\u673A\u4E0A\u7684\u5176\u5B83\u7A0B\u5E8F\u4ECD\u53EF\u4FEE\u6539\u3002",
|
|
2609
2672
|
onChange: (next) => this.setPref({ guard: next })
|
|
2610
2673
|
}),
|
|
2611
2674
|
h(Switch, {
|
|
@@ -2659,6 +2722,13 @@ ${state.proposal.trim()}
|
|
|
2659
2722
|
size: "sm",
|
|
2660
2723
|
onClick: () => this.onChange({ versions: void 0, versionDiff: void 0, versionsError: "", versionIndex: void 0 })
|
|
2661
2724
|
}, "\u5173\u95ED"),
|
|
2725
|
+
h(Button, {
|
|
2726
|
+
key: "delete",
|
|
2727
|
+
variant: "outline",
|
|
2728
|
+
size: "sm",
|
|
2729
|
+
disabled: state.versionsBusy !== "" || state.versionIndex === void 0 || diffState === void 0,
|
|
2730
|
+
onClick: () => void this.deleteVersion(state.versionIndex)
|
|
2731
|
+
}, state.versionsBusy === "delete" ? "\u5220\u9664\u4E2D\u2026" : state.versionConfirm === true ? "\u518D\u70B9\u4E00\u6B21\uFF1A\u5220\u9664" : "\u5220\u9664\u6B64\u7248\u672C"),
|
|
2662
2732
|
h(Button, {
|
|
2663
2733
|
key: "restore",
|
|
2664
2734
|
variant: "primary",
|
|
@@ -2680,13 +2750,14 @@ ${state.proposal.trim()}
|
|
|
2680
2750
|
{ className: "wsp-version-meta", key: "m" },
|
|
2681
2751
|
`${String(version.at).replace("T", " ").slice(5, 19)}\u3000+${version.added} / \u2212${version.removed}`
|
|
2682
2752
|
),
|
|
2753
|
+
h("span", { className: "wsp-version-hash", key: "h" }, `${version.hash ?? ""}`),
|
|
2683
2754
|
h("span", { className: "wsp-version-preview", key: "p" }, version.preview || "\uFF08\u65E0\u53EF\u663E\u793A\u7684\u6539\u52A8\u884C\uFF09")
|
|
2684
2755
|
]))),
|
|
2685
2756
|
h("div", { className: "wsp-versions-diff", key: "d" }, diffState === void 0 ? h("div", { className: "wsp-card-note" }, "\u5DE6\u4FA7\u9009\u4E00\u4E2A\u7248\u672C\uFF0C\u8FD9\u91CC\u663E\u793A\u5B83\u76F8\u5BF9\u4E0A\u4E00\u7248\u7684\u6539\u52A8\u3002") : [
|
|
2686
2757
|
h(
|
|
2687
2758
|
"div",
|
|
2688
2759
|
{ className: "wsp-diff-head", key: "h" },
|
|
2689
|
-
`${String(diffState.at).replace("T", " ").slice(0, 19)}\u3000\xB7\u3000${diffState.action === "restore" ? "\u6062\u590D" : diffState.action === "create" ? "\u65B0\u5EFA" : "\u4FEE\u6539"}\u3000\xB7\u3000+${diffState.added} / \u2212${diffState.removed}`
|
|
2760
|
+
`${String(diffState.at).replace("T", " ").slice(0, 19)}\u3000\xB7\u3000${diffState.action === "restore" ? "\u6062\u590D" : diffState.action === "create" ? "\u65B0\u5EFA" : "\u4FEE\u6539"}\u3000\xB7\u3000+${diffState.added} / \u2212${diffState.removed}\u3000\xB7\u3000${diffState.hash ?? ""}`
|
|
2690
2761
|
),
|
|
2691
2762
|
h("div", { className: "wsp-diff-body", key: "b" }, diffLines)
|
|
2692
2763
|
])
|
|
@@ -2797,13 +2868,16 @@ ${state.proposal.trim()}
|
|
|
2797
2868
|
])
|
|
2798
2869
|
])
|
|
2799
2870
|
]));
|
|
2800
|
-
return h("div", rootProps, [header, healthNote, prefsRow, transferRow, searchRow,
|
|
2871
|
+
return h("div", rootProps, [header, healthNote, prefsRow, transferRow, searchRow, conflictNotice, h("div", { className: "wsp-cards", key: "cards" }, cards), status, historyModal, confirmModal, versionsModal, importModal]);
|
|
2801
2872
|
}
|
|
2802
2873
|
}
|
|
2803
2874
|
const inject = ["slots", "remote", "connection"];
|
|
2804
2875
|
function apply(ctx) {
|
|
2805
2876
|
const style = injectStyles();
|
|
2806
|
-
ctx.effect(() => () =>
|
|
2877
|
+
ctx.effect(() => () => {
|
|
2878
|
+
style.remove();
|
|
2879
|
+
if (styleElement === style) styleElement = void 0;
|
|
2880
|
+
}, `${NS}: styles`);
|
|
2807
2881
|
const prefs = readPrefs();
|
|
2808
2882
|
if (prefs.showTab) ctx.slots.inject("conversation.view", () => ctx.slots.register({
|
|
2809
2883
|
name: "conversation.view",
|
package/lib/endpoints.d.ts
CHANGED
|
@@ -11,6 +11,6 @@ export declare const RPC_PACKAGE = "dsh-agent-persona";
|
|
|
11
11
|
*/
|
|
12
12
|
export declare const RPC_SERVICE = "agentPersona";
|
|
13
13
|
export declare const RPC_CHANNEL = "/agent-persona";
|
|
14
|
-
export declare const RPC_ENDPOINTS: readonly ['listPersonas', 'savePersona', 'deletePersona', 'movePersona', 'duplicatePersona', 'reorderPersona', 'listTargets', 'sessionHistory', 'sessionPrompt', 'tunePersona', 'listModels', 'savePrefs', 'listVersions', 'versionDiff', 'restoreVersion'];
|
|
14
|
+
export declare const RPC_ENDPOINTS: readonly ['listPersonas', 'savePersona', 'deletePersona', 'movePersona', 'duplicatePersona', 'reorderPersona', 'listTargets', 'sessionHistory', 'sessionPrompt', 'tunePersona', 'listModels', 'savePrefs', 'listVersions', 'versionDiff', 'restoreVersion', 'deleteVersion'];
|
|
15
15
|
/** The three reads that take no argument. Everything else receives one JSON object. */
|
|
16
16
|
export declare const RPC_NO_INPUT: readonly ['listPersonas', 'listTargets', 'listModels'];
|
package/lib/endpoints.js
CHANGED
|
@@ -27,6 +27,7 @@ export const RPC_ENDPOINTS = [
|
|
|
27
27
|
'listVersions',
|
|
28
28
|
'versionDiff',
|
|
29
29
|
'restoreVersion',
|
|
30
|
+
'deleteVersion',
|
|
30
31
|
];
|
|
31
32
|
/** The three reads that take no argument. Everything else receives one JSON object. */
|
|
32
33
|
export const RPC_NO_INPUT = ['listPersonas', 'listTargets', 'listModels'];
|