dsh-agent-persona 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 ADDED
@@ -0,0 +1,185 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+
11
+ - **A 人设 tab on the conversation page**: which persona the session matched, the system prompt it actually sent
12
+ (rendered and source views), and the full management UI in place.
13
+ - **An explicit default persona** (`默认人设`): it takes every session no reach rule claimed, and only one persona
14
+ can hold the flag — marking a new one releases the previous.
15
+ - **Interface preferences** — `编辑后自动保存`, `在对话页显示「人设」标签`, `注册到 dsh-better-sidebar` — stored in the
16
+ persona file's `prefs` field, so they share the data's scope; the browser keeps only a load-time mirror.
17
+ - **A sidebar card** for `dsh-better-sidebar`, registered through that plugin's own service. The switch that
18
+ controls it is **not shown at all** when that plugin is absent.
19
+ - **A Schemastery config**: `storePath`, `tuneProvider`, `tuneModel`, validated when the row loads.
20
+ - **TypeScript sources with a build**: `src/**` compiles to `lib/`, and a generator derives the remote artifacts
21
+ (`src/remote.ts`, `src/typert.ts`) from the single endpoint list in `src/endpoints.ts`; `npm test` fails if the
22
+ generated files drift.
23
+
24
+ ### Changed
25
+
26
+ - **A persona with no reach applies nowhere.** It used to act as a catch-all; a place-less persona is a draft now,
27
+ and where nothing matched the harness prompt stands. `默认人设` is the explicit way to ask for a fallback.
28
+ - **Replace mode also drops the harness identity line**, not only the deployment's persona line.
29
+ - Display preferences take effect immediately rather than on the next page load: the conversation view and the
30
+ sidebar card are registered and unregistered at runtime.
31
+
32
+ ### Fixed
33
+
34
+ - The remote manifest is generated in the shape the mount consumes: an `id` per descriptor, a strict input codec,
35
+ `parameters: []` for the reads that take none, and the service name `agentPersona`.
36
+ - The default flag survives a store round trip again — the reader no longer drops it — and a save whose payload
37
+ omits the flag keeps the stored value instead of clearing it.
38
+ - The session view's own draft and dirty check cover the default flag, and its switch no longer calls a method
39
+ that only the settings component has. Together those made the switch look inert and let a save turn it off.
40
+
41
+ ## [0.1.0] - 2026-09-17
42
+
43
+ ### Fixed
44
+
45
+ - 跳到最新 kept a fixed four-message window, so a turn where the agent answered several times pushed the user's
46
+ question out of it. The tail now returns everything from the **last user input** onwards (bounded at 60
47
+ messages while walking).
48
+ - A message whose Markdown failed to render fell back to plain text **as a whole**, which is what made some
49
+ bubbles show raw Markdown. Messages are now split into bounded Markdown-safe chunks (blank-line boundaries,
50
+ never inside a code fence) and each chunk renders on its own: one awkward chunk degrades with a visible note,
51
+ the rest of the message still renders.
52
+ - `npm test` now also runs a small client-side suite (`test/client-helpers.test.mjs`) that lifts the client's pure
53
+ helpers out of the source and asserts the splitter is lossless and fence-safe.
54
+
55
+ - 跳到最新 did nothing: the runtime forwarded a hand-written subset of the history options and dropped ,
56
+ so the call degraded to a normal head read while the dialog labelled it as the tail. The forwarding is now an
57
+ exported, asserted .
58
+ ### Added
59
+
60
+ - **展开全文 per bubble.** Every message now carries (its event index in the log), so a message that was
61
+ clipped at 1000 characters offers 展开全文: one
62
+ read replaces that single bubble with the full text (and 收起全文 puts the short text back without re-reading).
63
+ Nothing else in the dialog is re-fetched.
64
+ ### Changed
65
+
66
+ - The per-message text cap is 1000 characters (clipping stays markdown-safe), and 跳到最新 no longer appends the
67
+ rest of the conversation: it walks to the end and replaces the view with the last exchange only, so one click
68
+ answers "what were we just talking about". 从头看 returns to the sequential view, whose footer states that it
69
+ reads from the beginning.
70
+ ### Fixed
71
+
72
+ - **Empty sessions are no longer offered.** A session created with a preset but never typed into
73
+ (`sessionListMetadata.blank`, the same flag the sidebar hides on — found as a 5-event, 0.5 KB,
74
+ `agentPreset: 'cordis'` session) holds no conversation to give a persona, so the picker drops it and counts it
75
+ in the hidden tally.
76
+
77
+ - **Sub-agent and archived sessions no longer appear in the picker.** A sub-agent session (spawned by the
78
+ model: `origin: 'subagent'` or `delegationDepth > 0`) and an archived one cannot be opened or typed into
79
+ from the sidebar, so listing them invited personas that could never apply. Archived ids come from the
80
+ workspace registry, falling back to the workspace store file. The page reports how many were hidden, so a
81
+ missing session is never a mystery.
82
+
83
+ - **Sessions could not be read at all**: `sessionPersistence.open(id, access)` takes the access mode as a
84
+ second, required argument; the reader called `open(id)` and every candidate failed, so the conversation
85
+ dialog always reported "no reader". It now opens with `'read'` (never taking write ownership) and retries
86
+ id-only signatures for version tolerance.
87
+
88
+ ### Changed
89
+
90
+ - **Resolution is specificity-first.** A session-id rule now outranks a workspace rule and an exact value
91
+ outranks a pattern (`targetSpecificity`); list order only breaks ties between equally specific rules. A persona
92
+ pointed at one exact session therefore beats a persona pointed at the whole directory, whatever the order.
93
+
94
+ - The session picker shows how often each session was talked to (`sessionStats.turns` from the same
95
+ projection read as the title), beside the short id and age; picker menus are widened to fit that row.
96
+
97
+ - The settings section paints its title and description (and a skeleton card list) before any host call
98
+ resolves, instead of showing a bare 「加载中…」; a mount race with the remote namespace retries briefly rather
99
+ than flashing an error.
100
+
101
+ - Renamed the package from `dsh-workspace-persona` to **`dsh-agent-persona`** (settings page: 「Agent人设」).
102
+ Everything it writes now lives in `$DSH_HOME/dsh-agent-persona/`.
103
+ - The collapsed card keeps only what reads at a glance — chevron, state dot, name, state, scope chips — and
104
+ reordering / duplicating / deleting moved into the card's `⋯` menu.
105
+
106
+ ### Added
107
+
108
+ - **Exactly one persona per workspace or session.** A save that claims a place takes it away from the persona
109
+ that held it (exact values only, reported back to the page), and the pickers label places that are already
110
+ taken.
111
+ - **Injection modes** `append` (default) and `replace`, implemented by listening to the
112
+ `system-prompt/assemble` waterfall and dropping the deployment's own `deployment:persona-prefix` line for
113
+ `replace`. A preset or subagent persona that shadowed the section is never overwritten.
114
+ - **Session history**: pick a session, hit the button next to the picker, and read that conversation. Reads are
115
+ windowed (`sessionHistory({id, offset, events})`) so the log is never read up front: the page shows what it
116
+ got, reports 已显示 N 条, and only reads the next window when 继续加载 is pressed. Messages are capped at 4000
117
+ characters. Picker rows carry a short session id and the session's age on the right.
118
+ - **Search** appears once a profile holds four or more personas and filters on name, workspace title/path and
119
+ session title/id, with an explicit 「没有匹配「…」的人设」 state.
120
+ - **Drag to reorder**: a grip on each card header drags it to an absolute position (new `reorderPersona` remote
121
+ method, `reorderPersonas` is the exported pure function); the ⋯ menu's 上移/下移 stay as the keyboard path,
122
+ and dragging is disabled while a search filter is active so a drop target is never ambiguous.
123
+ - **Conflicts are visible**: a persona whose exact row would be shadowed by an *earlier* persona's prefix /
124
+ regex / contains row gets a 「可能被覆盖」 tag (hover explains which rule wins), plus a summary line above the
125
+ list. Exact duplicates never reach this state — saving takes the claim over instead.
126
+ - **The save shortcut is visible**: a ⌘S / Ctrl+S key chip sits next to the save button and the button carries
127
+ the same hint in its tooltip.
128
+ - The persona counts are two tags — 「N 条人设」 and a success-toned 「N 条在用」 — instead of a sentence in the
129
+ meta line, and the store path moved to the right of them.
130
+ - **Unsaved changes are visible**: the open card carries a 未保存 tag while its draft differs from what is
131
+ stored, and switching cards or collapsing reports 「改动没有保存」 in the status line instead of dropping it
132
+ silently.
133
+ - The status line is sticky at the bottom of the page so save/error feedback stays visible while the list
134
+ scrolls; icon buttons grew from 24px to 28px hit targets.
135
+ - The section header now matches the shipped settings pages exactly: an `<h2>` title at 18px/600, a 13px
136
+ `p` intro in the muted label colour, no leading icon (the count moved into the meta line). Measured side by
137
+ side against the official 「Agent 预设」 page in the same DOM: identical font size, weight and colour.
138
+ - The page is titled with the Agent-preset glyph (the same one the settings shell uses for 「Agent 预设」); the
139
+ settings **nav** icon itself is the shell's and cannot be set by a plugin — `settings.section` accepts only
140
+ `id` / `order` / `label`, and the shell falls back to one generic glyph for every id it does not ship.
141
+ - Creating a persona moved out of the page's top-right corner into a dashed **新建人设** row at the end of the
142
+ list (the conventional place for an "add" affordance), keyboard reachable via Enter/Space; the header is now
143
+ just the title and the count.
144
+ - A collapsed card shows its scope on a **second line**, workspaces and sessions apart, with human names
145
+ (workspace titles, session titles) instead of raw paths; the picker marks a claimed target as 当前人设 when it
146
+ belongs to the persona being edited, instead of appearing to be someone else's.
147
+ - The conversation dialog is a two-sided chat (your input right, the agent left, both in bubbles) and hides
148
+ plugin-injected user-side content — `AGENTS.md` (13k chars in a real session), runtime snapshots, the skill
149
+ catalog, goal rounds — reporting only 「已隐藏 N 条插件注入内容」.
150
+ - The scope pickers can go back from 「自己输入…」 to the list, and the match mode only appears in that
151
+ manual mode.
152
+ - Session entries in the picker are labelled with their DSH title, and with the session's first prompt (or
153
+ its latest prompt) when no title exists yet — all three come out of a **single read** of DSH's projection
154
+ cache. The dropdown never touches a session log: those are 2.6x-5.8x larger and zstd-compressed
155
+ append-only files that cannot be seeked.
156
+
157
+
158
+ ### Added
159
+
160
+ - **Many personas, each scoped independently.** A persona is a first-class record
161
+ (`id` / `name` / `enabled` / `text` / `targets[]`), and every target is
162
+ `{ kind: 'workspace' | 'sessionId', match: 'exact' | 'prefix' | 'regex' | 'contains', value }`.
163
+ - **Dropdown scope pickers.** The settings page fills its pickers from the host's
164
+ workspaces and from DSH's own sessions (newest first, with title, working
165
+ directory and a relative timestamp), so nobody has to type a path or a session
166
+ id. The two kinds are mutually exclusive per row — switching a row's type clears
167
+ its value — and "自己输入…" keeps the advanced match modes reachable.
168
+ - **Resolution rules**: list order is priority (first match wins), rows inside one
169
+ persona are OR-ed, a persona with no rows is the default persona, a disabled
170
+ persona is skipped, and a matching persona with an empty text means "no persona
171
+ here".
172
+ - **System-prompt injection** as section `agent-persona` at order 1
173
+ (`DEPLOYMENT_PERSONA_PREFIX + 1`), i.e. above the workspace `AGENTS.md`.
174
+ - **One directory for the data**: `$DSH_HOME/dsh-agent-persona/` holds
175
+ `personas.json` and a `state.json` heartbeat. The data is re-read on every prompt
176
+ assembly (mtimeNs/size stamp), so an edit applies to the next message.
177
+ - **AI rework** (polish / expand / compress / draft) that uses the model DSH itself
178
+ is set to use, overridable per deployment with `tuneProvider` / `tuneModel`. It
179
+ only ever produces a proposal.
180
+ - **Zero runtime dependencies**: a plain ESM host half plus a hand-written
181
+ `__ModuleLoader__` client half (no build step), with 89 offline assertions in
182
+ `npm test`.
183
+
184
+ [Unreleased]: https://github.com/awoodwhale/dsh-agent-persona/compare/v0.1.0...HEAD
185
+ [0.1.0]: https://github.com/awoodwhale/dsh-agent-persona/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 awoodwhale
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,233 @@
1
+ # dsh-agent-persona
2
+
3
+ 给不同的**工作区**、不同的**会话**,用不同的 system prompt 人设。
4
+
5
+ [![npm](https://img.shields.io/npm/v/dsh-agent-persona)](https://www.npmjs.com/package/dsh-agent-persona)
6
+ [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE)
7
+ [![ci](https://github.com/awoodwhale/dsh-agent-persona/actions/workflows/ci.yml/badge.svg)](https://github.com/awoodwhale/dsh-agent-persona/actions/workflows/ci.yml)
8
+
9
+ [English](./README_EN.md)
10
+
11
+ ## 它能做什么
12
+
13
+ 一个 DSH 部署里常常同时跑着好几摊活:写前端、写文档、盯告警、做代码审查。它们需要不同的身份、口径和约束。
14
+ 这个插件把人设按**工作区**或**会话**分发出去,每一份都是 system prompt 里的一段:
15
+
16
+ - **多个人设,各自声明适用范围**:一条人设可以同时用于若干个工作区目录和若干个会话;匹配方式有
17
+ 完全一致、开头是、包含、正则匹配四种。
18
+ - **越具体越优先**:会话 ID 的规则优先于工作区,精确优先于前缀/正则;只有具体度相同时才按列表顺序决定。
19
+ - **显式默认人设**:可以把某一条标成「默认人设」,它会收下所有没被位置命中的会话 —— 同一时刻只允许一条。
20
+ 不标就没有兜底,那些会话继续用 DSH 自带的 system prompt。
21
+ - **一个位置只能属于一条人设**:把某个工作区/会话配给新人设时,原来占着它的那条会自动失去它,并告诉你从谁那里转来的。
22
+ - **追加或替换**:保留 DSH 自带的身份提示、把人设接在后面,或者去掉那句身份提示只用人设。
23
+ - **正文为空 = 这里不要人设**:给某个位置加一行、正文留空,就显式地在那里关掉人设。
24
+ - **改完下一条消息生效**,不用重启(正文按文件时间戳在每次组装时重读)。
25
+
26
+ 三处界面,各管一件事:
27
+
28
+ | 界面 | 位置 | 用途 |
29
+ |---|---|---|
30
+ | **设置页** | 设置 → Agent 人设 | 新建/编辑/删除/复制/排序人设;写正文、Markdown 预览、让 AI 改一版 |
31
+ | **会话视图** | 对话页顶部的 **人设** 标签 | 看这条会话命中了哪条人设、**实际发出去的 system prompt**(渲染/源码两视图),以及就地管理全部人设 |
32
+ | **侧边栏卡片** | dsh-better-sidebar 的右侧栏 | 把上面那个「人设」页面挂进右侧栏(需要装了该插件) |
33
+
34
+ 三个偏好开关(`编辑后自动保存` / `在对话页显示「人设」标签` / `注册到 dsh-better-sidebar`)在
35
+ **管理页**与**设置 → 通用设置**里各有一份 —— 后一个位置不受这些开关影响,所以关掉显示位置后永远能从那里打开。
36
+
37
+ ## 为什么它压得住 AGENTS.md
38
+
39
+ DSH 会把工作区指令(`AGENTS.md` 之类)变成一条 **user 角色**的消息,那条消息自己的导语就写着
40
+ *"They do not override system, developer, or direct user instructions."* 而这个插件写的是
41
+ **system prompt 的一个 section**(section 名 `agent-persona`,order 1,紧跟在身份行之后)。
42
+
43
+ 这个先后关系是框架定的,不是插件自己声明的 —— 也正因为如此,工作区里怎么改文件都动不了这层人设。
44
+
45
+ ## 装
46
+
47
+ **实测过的 DSH 版本:`0.1.5-rc.1`(作者的开发环境)与 `0.1.5-rc.2`。** `package.json` 的 `dsh.compatibility`
48
+ 声明的就是这两项 —— 含义是"我们在这些版本上跑过",**不是**对更早或更新版本做过兼容工作的承诺;
49
+ 其它版本请自行验证。
50
+
51
+ 侧边栏卡片还需要 **`dsh-better-sidebar`**:没装它时,「注册到 dsh-better-sidebar」这个开关不会出现
52
+ (没有可注册的地方),插件其余功能照常。
53
+
54
+ ```bash
55
+ dsh plugin --profile web add dsh-agent-persona
56
+ ```
57
+
58
+ 装完**重启一次 `dsh web`**:profile 的 bundles 只在启动时读取,插件行是从那里挂进去的。
59
+
60
+ 重启后能看到:
61
+
62
+ - 设置左栏的 **Agent 人设**;
63
+ - `cat ~/.dsh/dsh-agent-persona/state.json` —— 插件的加载心跳,写明了它加载了哪个文件、人设存哪、几条人设。
64
+
65
+ 仓库里也有本地安装脚本:`bash scripts/install.sh`(Windows 是 `scripts/install.ps1`)。
66
+
67
+ 卸载:`dsh plugin --profile web remove dsh-agent-persona`,重启即可;人设文件不会被删。
68
+
69
+ ## 用起来
70
+
71
+ ![人设列表](./docs/images/settings-list.png)
72
+
73
+ 点 **新建人设** → 在「适用范围」里加行,选工作区或会话 → 写正文 → 打开启用开关 → 保存。
74
+
75
+ - **新建出来的人设默认停用**,所以多一条人设不会影响任何现有会话,配好了再开。
76
+ - **适用范围**是一行一行加的。每行先选类型(**工作区目录** 或 **某个会话**),再从下拉框里挑具体是谁。
77
+ 两种类型互斥:把一行从「工作区」切成「某个会话」,这一行原来选的值会被清掉,重新选。
78
+ - 下拉框里的工作区来自 DSH 的工作区列表(带会话数),会话来自 DSH 的全部会话,按时间倒序,
79
+ 带**标题**、工作目录和「几分钟前」—— 不记得 session id 是什么没关系,认标题就行。
80
+ - 想用前缀、正则这类高级匹配,选 **自己输入…** 就会变成输入框。
81
+ - 折叠状态下,卡片第二行分开列出这条人设用在了哪些**工作区**、哪些**会话**;**一个位置都没加**时写着
82
+ 「未设置位置:不生效,该会话仍用 DSH 自带的 prompt」——那是停在草稿状态的人设,不是兜底。
83
+ - 想让它兜底,就打开卡片里的 **默认人设** 开关;列表行上会出现绿色的「默认」标记。
84
+
85
+ 展开一张卡是这样:
86
+
87
+ ![展开编辑](./docs/images/settings-editor.png)
88
+
89
+ ### 看这条会话聊了什么
90
+
91
+ 下拉框旁边那个按钮会打开一个**只读**弹窗:左右气泡式对话(你的输入在右、Agent 的回复在左)。
92
+
93
+ - **只显示你真正敲进去的内容**:工作区指令(`AGENTS.md`)、运行时上下文快照、skill 目录说明这类
94
+ **插件注入**的"看起来像用户消息"的东西一律不展示,弹窗顶部只写「已隐藏 N 条插件注入内容」,
95
+ 不占位置,也不会让你困惑"这条我没说过"。
96
+ - 消息过长会被截断,就地提供 **展开全文**(只重读这一条,换掉这一个气泡)与 **收起全文**。
97
+ - **跳到最新** 只显示最后一个用户输入及之后的回复。
98
+
99
+ ### 对话页的「人设」标签
100
+
101
+ ![会话视图](./docs/images/view-persona.png)
102
+
103
+ 选中一条会话后,对话页顶部有 **人设 / 提示词 / 管理** 三个面板:
104
+
105
+ - **人设**:这条会话命中了哪条人设、**依据是哪几条规则**(命中依据)、正文(Markdown 渲染),
106
+ 以及它自己的状态标记(在用 / 已停用、是否默认人设、注入方式);下面是一个就地编辑器
107
+ (名称、默认人设开关、注入方式、正文),改动后卡片头会出现「未保存 · 点击保存」。
108
+ - **提示词**:这条会话**实际发出去的** system prompt,可以切换「渲染 / 源码」、复制、刷新。
109
+ 没有命中任何人设时,这里显示的就是 DSH 原生注入的 prompt —— 不做任何特殊处理。
110
+ - **管理**:与设置页同一套卡片(新建、启用/停用、编辑、排序、复制、删除),就地管理。
111
+
112
+ ### 三个偏好
113
+
114
+ | 开关 | 默认 | 含义 | 生效时机 |
115
+ |---|---|---|---|
116
+ | 编辑后自动保存 | 关 | 停手约 1 秒自动保存编辑区内容 | 立即;切换到人设规则行(工作区/会话)时**不**自动保存,避免存下半截选择 |
117
+ | 在对话页显示「人设」标签 | 开 | 是否注册对话页那个视图 | 立即(运行时注册/注销) |
118
+ | 注册到 dsh-better-sidebar | 开 | 是否把「人设」页面注册进 dsh-better-sidebar 的右侧栏 | 立即;**没装该插件时这个开关不显示** |
119
+
120
+ 偏好存在人设文件的 `prefs` 字段里(不是浏览器),所以它和数据的可见范围一致;浏览器里只留一份镜像,
121
+ 供"加载时决定要不要注册视图"用。
122
+
123
+ ## 注入方式:追加还是替换
124
+
125
+ | 方式 | 效果 |
126
+ |---|---|
127
+ | **追加**(默认) | 保留 DSH 自带的那句身份提示,这条人设接在它后面 |
128
+ | **替换** | 去掉 DSH 自带的身份句(以及部署自己写的 persona 段),只用这条人设 |
129
+
130
+ 「替换」只动**部署自己写的**那句 persona;如果某个 Agent 预设在自己作用域里写了 persona(把这一段遮蔽了),
131
+ 插件不会去覆盖它 —— 别人的身份提示优先。
132
+
133
+ ## 匹配是怎么算的
134
+
135
+ 宿主在每次组装 system prompt 时按这个顺序过一遍:
136
+
137
+ 1. 人设停用了 → 跳过;
138
+ 2. 一条人设的**任意一行**匹配上,它就是候选,按"具体度"比较(会话 ID 3 分 > 工作区 1 分;精确 > 前缀/包含/正则);
139
+ 3. 具体度相同 → 取列表里靠上的那条(顺序在卡片的 `⋯` 菜单里调);
140
+ 4. **一个位置都没加的人设不参与匹配**(草稿态),除非它被标成「默认人设」;
141
+ 5. 谁都没命中 → 看有没有「默认人设」,有就用它,没有就不注入;
142
+ 6. 赢家的正文是空的 → 不注入任何人设(这是"这里明确不要人设"的表达方式)。
143
+
144
+ 四种匹配方式里只有 `完全一致` 会把路径归一化(`/a/project/` 和 `/a/project` 算同一个);
145
+ `开头是`、`包含`、`正则匹配` 都是拿你写的字符串直接比。值空着、正则写错、或者会话没有那个字段,
146
+ 都算「不匹配」,不会报错。
147
+
148
+ ## 文件与配置
149
+
150
+ ```
151
+ ~/.dsh/dsh-agent-persona/
152
+ ├── personas.json # 人设数据 + 偏好,权限 600,原子写入
153
+ └── state.json # 加载心跳,每次加载时重写
154
+ ```
155
+
156
+ 正文里可以写 `{{model}}` 和 `{{cwd}}`,渲染时替换成当前的值;其他 `{{...}}` 会被去掉(未注册的变量会让
157
+ 整个组装抛错,所以这里做了保护)。
158
+
159
+ 配置项(Schemastery schema,加载时校验,写错类型会**加载失败**而不是被忽略):
160
+
161
+ | 配置 | 默认 | 说明 |
162
+ |---|---|---|
163
+ | `storePath` | `''` | 人设文件位置;留空即 `$DSH_HOME/dsh-agent-persona/personas.json` |
164
+ | `tuneProvider` | `''` | AI 调优用哪个 provider;留空即 DSH 当前设置的模型 |
165
+ | `tuneModel` | `''` | 同上,模型 id |
166
+
167
+ ### 多个 profile 各用自己的人设文件(可选)
168
+
169
+ 人设数据默认是**全局一份**(`$DSH_HOME/dsh-agent-persona/personas.json`,与其它插件的状态目录同一约定),
170
+ 所以同一台机器上的多个 profile 共用同一份人设。要让某个 profile 独立,在**该 profile** 的
171
+ `cordis.patch.yml` 里覆盖它自己的路径(是覆盖字段,**不要**再 insert 一行):
172
+
173
+ ```yaml
174
+ # ~/.dsh/profiles/<该 profile>/cordis.patch.yml
175
+ - id: agent-persona
176
+ config:
177
+ storePath: /Users/you/.dsh/profiles/<该 profile>/personas.json
178
+ ```
179
+
180
+ 也可以用 `DSH_HOME` 环境变量把整个 `~/.dsh` 指向别的目录(那样连会话记录、其它插件状态一起隔离)。
181
+ 当前生效的文件路径始终显示在「Agent 人设」页面的 meta 行上(`保存在 …`),不会出现"改了半天不知道改的是哪一份"。
182
+
183
+ ## AI 改一版用哪个模型
184
+
185
+ 默认用 **DSH 当前设置的模型**,设置页里的下拉框会把它预选好;想固定别的模型就用上面的 `tuneProvider`/`tuneModel`。
186
+ AI 只产出建议稿,点「采用」或「追加」才会进编辑框,再点保存才落盘。
187
+
188
+ ## 几句实话
189
+
190
+ - **人设是行为约束,不是安全边界**。它约束模型怎么说,不阻止模型用工具。
191
+ - `personas.json` 就是个**普通文件**,任何有文件工具权限的会话都写得动它 —— 这个插件**没有**注册
192
+ `tools.guard`。真要「工作区里的会话改不动人设」,得在本机层面做:`chmod 400` + 换个属主,
193
+ 或者把 DSH 跑在受限沙箱里。
194
+ - 多处同时匹配时**只取第一条**,不拼接;规则是线性扫的,几十条没问题,几百条就该重新设计了。
195
+ - 一个 profile 里只装一次。同一个进程挂两个同 id 的行会让组合起不来。
196
+ - **测试的边界**:仓库里的测试是**单元测试 + 源码审计**(134 条宿主断言 + 15 条客户端断言 + 生成产物漂移校验),
197
+ 它们保证逻辑与代码形状,**不驱动浏览器** —— 界面行为(视图渲染、开关、侧边栏卡片)目前靠人工核对。
198
+
199
+ ## 开发
200
+
201
+ 源码是 TypeScript,`lib/` 是构建产物(不进 git,`npm publish` 前由 `prepare` 构建):
202
+
203
+ ```
204
+ src/index.ts host 半边:存储、匹配、system-prompt section、远程服务
205
+ src/client.ts client 半边:设置页 + 会话视图 + 侧边栏卡片(打包成单文件 lib/client.js)
206
+ src/endpoints.ts 远程方法的单一来源(生成 src/remote.ts 与 src/typert.ts)
207
+ scripts/ 生成器 + 安装脚本
208
+ test/ 单元测试与源码审计
209
+ docs/ 架构、开发笔记、设计决策
210
+ ```
211
+
212
+ ```bash
213
+ npm install
214
+ npm run build # generate:remote → tsc → esbuild(产出 lib/)
215
+ npm run typecheck # 两个 tsconfig 都不报错
216
+ npm test # 134 + 15 条断言 + 生成产物漂移校验
217
+ dsh --profile web --dump-config | grep -c 'id: agent-persona' # 组合自检,应输出 1
218
+ ```
219
+
220
+ 改代码之后怎么生效:
221
+
222
+ - `src/**` 改完必须 **`npm run build`**(`lib/` 是产物)。
223
+ - 客户端产物(`lib/client.js`)改**内容**:客户端会按内容重算,**刷新页面**即可;别改文件名。
224
+ - 宿主产物(`lib/index.js`)改**内容**:ESM 按 URL 缓存模块,**不会**自动重载 —— 重启 `dsh web`。
225
+ - `package.json` 的 `dsh.bundle.patch` 或 profile 的 `bundles` 有改动:必须重启。
226
+ - 生成产物(`src/remote.ts`、`src/typert.ts`)**不要手改**:改 `src/endpoints.ts` 后跑
227
+ `npm run generate:remote`,`npm test` 会校验它们是否与来源一致。
228
+
229
+ 细节在 [docs/development.md](./docs/development.md),内部结构在 [docs/architecture.md](./docs/architecture.md)。
230
+
231
+ ## 许可
232
+
233
+ Apache-2.0,见 [LICENSE](./LICENSE)。