better-dsh-session-deletetool 0.4.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 +299 -0
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/README.zh-CN.md +171 -0
- package/client.js +1466 -0
- package/cordis.patch.yml +4 -0
- package/host.js +1431 -0
- package/icon.svg +6 -0
- package/package.json +62 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
**The package is `better-dsh-session-deletetool` now.** Every name that pointed at the old identity was
|
|
6
|
+
renamed in one pass, so nothing resolves the old spelling any more:
|
|
7
|
+
|
|
8
|
+
- the npm package name, the GitHub repository (`Lzcdebear/better-dsh-session-deletetool`), and the
|
|
9
|
+
`repository` / `homepage` / `bugs` URLs plus every install target in the READMEs;
|
|
10
|
+
- the four Host routes: `/better-dsh-session-deletetool/inspect`, `/delete`, `/catalog` and
|
|
11
|
+
`/delete-batch`;
|
|
12
|
+
- the client module id, the locale namespace, the sidebar menu row, both `shell.overlay` seats and the
|
|
13
|
+
bulk anchor's registration ids, the `data-dsh-plugin` marker, and the React display name;
|
|
14
|
+
- the Host loader row in `cordis.patch.yml` — both the row `id` and the module `name` the profile
|
|
15
|
+
imports;
|
|
16
|
+
- the Host's `[better-dsh-session-deletetool]` log prefix and its effect labels.
|
|
17
|
+
|
|
18
|
+
**The family view is written down now.** The dialogs have drawn a conversation's subagents and derived
|
|
19
|
+
conversations as one tree for several releases, but neither README said so out loud. Both now open that
|
|
20
|
+
section by saying that the sidebar cannot show the relation and the dialog is the one place it is
|
|
21
|
+
drawn, and the npm description says it too.
|
|
22
|
+
|
|
23
|
+
A profile that had the old name linked has to link the new one: the bundle key in
|
|
24
|
+
`dsh.profile.bundles` and the dependency key in the profile's own `package.json` both change.
|
|
25
|
+
|
|
26
|
+
## 0.3.9
|
|
27
|
+
|
|
28
|
+
**Both dialogs now draw the same family tree.** The single conversation's dialog and the bulk one share
|
|
29
|
+
one renderer: a row per Session, and under every row a collapsible **子智能体 (n)** block followed by
|
|
30
|
+
a collapsible **派生对话 (n)** block holding that row's own direct children. A derived conversation
|
|
31
|
+
keeps its own subagents *and* its own derived conversations inside its own block, one step further in,
|
|
32
|
+
so a fork of a fork is nested where it belongs instead of appearing in a second list that only shares
|
|
33
|
+
a heading with its parent.
|
|
34
|
+
|
|
35
|
+
**The duplicated rows are gone.** 0.3.7 drew each family twice over, once per relation, each pass
|
|
36
|
+
walking the whole forest: a subagent hanging under a fork was listed both flat in the root's 子智能体
|
|
37
|
+
branch and again inside its real parent. One forest is now built once (`buildForest`) and drawn once
|
|
38
|
+
(`FamilyTree`), so every row is drawn by exactly one parent — which is what makes the shape readable.
|
|
39
|
+
|
|
40
|
+
**Every row's checkbox means its own subtree.** A row is ticked when everything below it is ticked,
|
|
41
|
+
mixed when only part of it is, and pressing it takes or clears that whole subtree; a block's checkbox
|
|
42
|
+
covers the rows the block lists. The old "selected roots plus excluded children" pair is gone: the
|
|
43
|
+
delete set is one set of ids, so the picture and the count can no longer drift apart.
|
|
44
|
+
|
|
45
|
+
**In the bulk dialog a child can now be ticked on its own.** Every ticked row whose parent is not
|
|
46
|
+
ticked is sent as a delete root of its own, carrying the ticked rows below it — so ticking a single
|
|
47
|
+
subagent deletes just that subagent, and ticking a parent takes its selected family with it.
|
|
48
|
+
|
|
49
|
+
**Each row carries its kind's glyph**: a document for a conversation, a robot head for the 子智能体
|
|
50
|
+
block and a person for a subagent, a chain for the 派生对话 block and a chat bubble for a derived
|
|
51
|
+
conversation — 16px outline paths, `fill: none`, `stroke: currentColor`, matching the icon set they
|
|
52
|
+
sit beside. Nothing else changed: the nesting is still the indentation alone, one step per level, with
|
|
53
|
+
no connectors and no guide lines.
|
|
54
|
+
|
|
55
|
+
## 0.3.7
|
|
56
|
+
|
|
57
|
+
**The connector lines are gone.** The tree's nesting is the indentation alone: one step per level, the
|
|
58
|
+
same reading the rest of the sidebar uses. A drawn guide line under every parent added nothing the
|
|
59
|
+
indent did not already say, and it made the rows busier than the list they sit in.
|
|
60
|
+
|
|
61
|
+
Everything else from 0.3.6 stands: row 1 is this conversation, row 2 is its own subagent branch, row 3
|
|
62
|
+
is the forked conversations with each fork's subagents inside it and a fork of a fork nested in its
|
|
63
|
+
parent's block.
|
|
64
|
+
|
|
65
|
+
## 0.3.6
|
|
66
|
+
|
|
67
|
+
**The picker is the family tree now, drawn as one.** 0.2.1's two flat lists could not say what the
|
|
68
|
+
reference layout says, so the dialog is drawn that way from the top down:
|
|
69
|
+
|
|
70
|
+
- row 1 is this conversation, carrying the whole family's checkbox and named as the sidebar names it;
|
|
71
|
+
- row 2 is one collapsible **子智能体 (n)** branch holding this conversation's own subagents;
|
|
72
|
+
- row 3 is one collapsible **派生对话 (n)** branch, and each fork inside it keeps *its own* subagent
|
|
73
|
+
branch — so a fork of a fork is drawn inside its parent's block, indented one more step, instead of
|
|
74
|
+
being listed beside it. A subagent that spawned a conversation keeps that conversation inside the
|
|
75
|
+
subagent's own block for the same reason.
|
|
76
|
+
- Guide lines drop from each parent to the children it draws, and indent tracks the tree, so a level-3
|
|
77
|
+
row reads as hanging off the level-2 row above it and not merely as "somewhere deeper".
|
|
78
|
+
|
|
79
|
+
**The layout came from the sketch, not the data model.** The Host still answers one deepest-first
|
|
80
|
+
list; the client arranges it into a tree and draws each branch as a *view* of that tree, indented from
|
|
81
|
+
the nearest ancestor the branch actually draws. Nothing is ever indented under a row that is not on
|
|
82
|
+
screen.
|
|
83
|
+
|
|
84
|
+
**Ticks are derived, not stored twice.** A row is in the delete set when it is a selected root or sits
|
|
85
|
+
beneath one. A subagent is a Session of its own, so a fork's checkbox reports the subagents under it
|
|
86
|
+
without claiming them for a second delete — which is what kept the footer's count honest.
|
|
87
|
+
|
|
88
|
+
## 0.3.5
|
|
89
|
+
|
|
90
|
+
**A group now shows its own nesting.** 0.3.4 arranged the family as one tree but then filtered it into
|
|
91
|
+
the two groups, and filtering flattened the tree: a level-2 child stayed indented under a level-1
|
|
92
|
+
parent the group did not draw, so nothing on screen said which level-1 child it belonged to.
|
|
93
|
+
`groupNodes` walks the arranged family in pre-order and counts each group's indent from the nearest
|
|
94
|
+
ancestor *that group draws*, so a parent is followed by its own children, one step in, and the next
|
|
95
|
+
family starts at the top of the column again. A group's checkbox, its root rows and the delete payload
|
|
96
|
+
are unchanged: a picked row still answers to the same root it did before.
|
|
97
|
+
|
|
98
|
+
**The family's read failures are reported, not swallowed.** The descendant list comes from two durable
|
|
99
|
+
sources, and either one failing used to be invisible — the dialog simply drew fewer rows, which is
|
|
100
|
+
indistinguishable from a conversation that genuinely has no children. `/inspect` now carries the
|
|
101
|
+
warnings it collected (a catalog that would not open, a branch that could not be read), and the
|
|
102
|
+
per-Session dialog prints them above the list.
|
|
103
|
+
|
|
104
|
+
## 0.3.4
|
|
105
|
+
|
|
106
|
+
**The per-Session picker is 0.2.1's layout again, with the lineage fixed.** 0.3.2 removed the two
|
|
107
|
+
collapsible groups to fix a nesting bug; that threw away a shape that was doing its job, so the groups
|
|
108
|
+
are back: one master row, then *subagent conversations* and *forked conversations*, each with its own
|
|
109
|
+
count, its own checkbox and its own disclosure caret, and every row keeps its title, its id tail and
|
|
110
|
+
its state badges.
|
|
111
|
+
|
|
112
|
+
**What actually changed is where a row's position comes from.** Both 0.2.1 and 0.3.2 drew rows in the
|
|
113
|
+
order the Host sent them, and the Host sends the family deepest-first because that is the order its
|
|
114
|
+
own delete walk wants. Splitting that into groups put a grandchild in one group and its parent in
|
|
115
|
+
another, where the grandchild became a top-level row and drew indented above the row it hangs under.
|
|
116
|
+
The whole family is now arranged once — every row follows the row it hangs under, with the depth
|
|
117
|
+
counted from the nearest row the picker actually draws — and only then split by relation, so each
|
|
118
|
+
group keeps its own order without ever inverting a parent and its child.
|
|
119
|
+
|
|
120
|
+
**Indentation is one step per level.** Rows are indented by the arranged depth alone (16px a level, the
|
|
121
|
+
same step the bulk dialog uses), where 0.2.1 added a fixed extra level for every row: a
|
|
122
|
+
one-level-deep child read as though it hung off something that was not there.
|
|
123
|
+
|
|
124
|
+
## 0.3.3
|
|
125
|
+
|
|
126
|
+
**Missing subagents in the per-Session dialog, fixed at the source.** The family was read from two
|
|
127
|
+
places, and the subagent half leaned on `ctx.subagents.listDescendants`, which walks that service's
|
|
128
|
+
own session store: when that lookup is unavailable, the call throws and *every* subagent disappears
|
|
129
|
+
from the dialog at once, leaving only the forked conversations the header lineage names. The catalog
|
|
130
|
+
is now read the way the Subagent runtime itself reads it — per parent, from that parent's own
|
|
131
|
+
`subagentCatalog` projection through `sessionQuery.observeSession` — and walked from the target
|
|
132
|
+
downwards. The service call is still made, merged by closest depth, so neither source is a single
|
|
133
|
+
point of failure. Observation leases are freed through `Symbol.dispose` in a `finally`: the lease has
|
|
134
|
+
no `release()`, and a walk that never disposed would pin every parent it read in the query cache.
|
|
135
|
+
|
|
136
|
+
**Nesting now comes from the right parent.** Because the walk descends through each parent's own
|
|
137
|
+
catalog, a subagent a *child conversation* spawned is listed under that child rather than under the
|
|
138
|
+
target, and the header lineage now descends through children the catalog did not produce too. So the
|
|
139
|
+
dialog draws one family per level:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
母会话
|
|
143
|
+
子代理
|
|
144
|
+
子会话
|
|
145
|
+
子会话 1 的子代理
|
|
146
|
+
子会话 2
|
|
147
|
+
子会话 2 的子代理
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## 0.3.2
|
|
151
|
+
|
|
152
|
+
**The 子 / 母 label is gone.** The nesting reads from indentation alone, as it does in the rest of the
|
|
153
|
+
sidebar. A family shows its shape: one step of indent per level, parents above their own children.
|
|
154
|
+
The catalog still reports `hasChildren` and the family counts, so nothing else had to change.
|
|
155
|
+
|
|
156
|
+
**The per-Session picker draws the family as one tree.** It used to split the descendants into two
|
|
157
|
+
collapsible groups by relation — *subagent conversations* and *forked conversations* — and arrange
|
|
158
|
+
each group on its own. A grandchild whose parent landed in the other group was therefore drawn as a
|
|
159
|
+
top-level row, indented, above the row it actually hangs under. The family is now arranged as a whole
|
|
160
|
+
with the same rule the bulk view uses, so every row follows the row it hangs under and the
|
|
161
|
+
indentation is the nesting. The per-kind groups and their collapse controls went with it: the list is
|
|
162
|
+
one tree, and the master checkbox still clears or takes the whole family.
|
|
163
|
+
|
|
164
|
+
## 0.3.1
|
|
165
|
+
|
|
166
|
+
Four fixes to the bulk view, three of them visible on first use.
|
|
167
|
+
|
|
168
|
+
**The glyph's colour now matches its neighbours.** The control was drawn as the source SVG is
|
|
169
|
+
authored — a filled glyph — while the shipped product icon set is outline-only (`fill: none`,
|
|
170
|
+
`stroke: currentColor`, `strokeWidth: 1` on a 16px box). A filled path painting a 32-unit outline
|
|
171
|
+
shape rendered as a blob at 16px, so the button looked empty beside the search icon. The glyph is now
|
|
172
|
+
stroked in the icon set's own convention, with the stroke scaled for the halved viewBox.
|
|
173
|
+
|
|
174
|
+
**Every row carries its 子 / 母 mark.** A conversation with no children of its own in the list left
|
|
175
|
+
the mark blank. A row that is not a child now reads as 母 by default, so the three states are 母,
|
|
176
|
+
子, and 子母 for a child that has children of its own. The mark's cell is sized for two characters, so
|
|
177
|
+
子母 no longer shifts the title along.
|
|
178
|
+
|
|
179
|
+
**Names come from the Session, not from "untitled".** The title read was wrong: the folded snapshot
|
|
180
|
+
is `{ session, title: { title, … } }`, and the code read `value.title` — an object, which failed its
|
|
181
|
+
own string check, so every Session reported no title and both dialogs fell back to *untitled*. Rows
|
|
182
|
+
now resolve a display name the way the sidebar does: the durable title when the log carries one, else
|
|
183
|
+
the final segment of the project directory, else the id. That fixes the bulk list and the 0.2.1
|
|
184
|
+
per-Session dialog together, the latter including its subagent and forked children, which now read as
|
|
185
|
+
the project directory rather than *untitled* when they were never renamed.
|
|
186
|
+
|
|
187
|
+
**Lineage children can carry their own directory.** The descendant walk now passes each child's
|
|
188
|
+
`cwd` from its header, so a descendant that was never named falls back to its project directory
|
|
189
|
+
before falling back to its id.
|
|
190
|
+
|
|
191
|
+
## 0.3.0
|
|
192
|
+
|
|
193
|
+
One control beside the workspace search icon deletes many conversations at once.
|
|
194
|
+
|
|
195
|
+
**A bulk entry beside the search icon.** The sidebar's workspace header gains a trash control, drawn
|
|
196
|
+
from `deleting icon.svg`, immediately left of the search icon. It opens one dialog for every
|
|
197
|
+
conversation the Host knows.
|
|
198
|
+
|
|
199
|
+
**The list is grouped by Workspace and keeps lineages together.** Rows are sectioned by the Workspace
|
|
200
|
+
that owns the conversation's directory, in the registry's own order, with one "未归类" section for
|
|
201
|
+
conversations no Workspace owns. Inside a section, parents lead their children, indented by lineage
|
|
202
|
+
depth. Every row leads with the blue **子** / **母** mark: `母` for a conversation with children in
|
|
203
|
+
this list, `子` for one spawned as a subagent or forked off another, `子母` for both.
|
|
204
|
+
|
|
205
|
+
**Selection follows the family, per row.** Ticking a parent takes its whole family with it; ticking a
|
|
206
|
+
child selects it on its own. A child can be taken back off a ticked family without untick-ing the
|
|
207
|
+
parent, and the footer always states how many conversations the press will delete. The dialog carries
|
|
208
|
+
the same "stop unfinished work first" choice the single-Session dialog offers.
|
|
209
|
+
|
|
210
|
+
**API.** `GET /catalog` answers
|
|
211
|
+
`{ ok, workspaces: [{ key, workspaceId, title, path, sessions[] }], totals }`, and each session row
|
|
212
|
+
carries `id`, `kind` (`root` / `subagent` / `derived`), `depth`, `parentId`, `hasChildren`, `family`,
|
|
213
|
+
`subagents`, `derived`, `title`, `cwd`, `open`, `agent`, `running` and `activity`.
|
|
214
|
+
`POST /delete-batch` takes `{ roots: [{ sessionId, descendants? }], stop? }` and answers
|
|
215
|
+
`{ ok, roots, removed[], failed[] }` — one entry per root, so a failing root is reported without
|
|
216
|
+
aborting the rest.
|
|
217
|
+
|
|
218
|
+
**Where the control is mounted, and why.** `sidebar.workspaces` is a `single` slot, so a second
|
|
219
|
+
registrant would shadow the shipped browser instead of sitting beside it. The plugin therefore
|
|
220
|
+
registers in `sidebar.footer.action` (which renders nothing itself) and mounts its control into the
|
|
221
|
+
browsing region's own search slot through a portal and one `MutationObserver`, re-creating the
|
|
222
|
+
control whenever React re-renders that header. The insertion carries no authority: it only opens the
|
|
223
|
+
dialog, and the two Host routes above do all the work. If a future harness renames that slot's CSS
|
|
224
|
+
class, the control stops appearing and nothing else changes.
|
|
225
|
+
|
|
226
|
+
**Unchanged.** The per-Session "⋯" menu entry and its descendant picker behave exactly as in 0.2.1;
|
|
227
|
+
the bulk path calls the same delete for each selected conversation.
|
|
228
|
+
|
|
229
|
+
## 0.2.1
|
|
230
|
+
|
|
231
|
+
Deleting a conversation is a choice now, not a take-it-all.
|
|
232
|
+
|
|
233
|
+
**Pick what goes with it.** The confirmation dialog lists the session's whole family and lets you
|
|
234
|
+
choose:
|
|
235
|
+
|
|
236
|
+
- one **delete all** checkbox, with an indeterminate state when the selection is partial;
|
|
237
|
+
- one collapsible group per relation — *subagent conversations* and *forked conversations*;
|
|
238
|
+
- one checkbox per descendant, indented by lineage depth, labelled with its title and the tail of its
|
|
239
|
+
id, and badged when it is open or still has work running.
|
|
240
|
+
|
|
241
|
+
Confirming posts exactly the ticked ids, and the button states how many conversations will go.
|
|
242
|
+
Unticked descendants survive as conversation roots.
|
|
243
|
+
|
|
244
|
+
**The family is complete and the selection is validated.** Descendants come from the header lineage
|
|
245
|
+
every Session carries (`SessionHeader.parentSession`) — which covers subagent sessions, forked
|
|
246
|
+
conversations, and the child Sessions Agent Teams provisions — unioned with the durable
|
|
247
|
+
`subagentCatalog` projection for children whose own header no longer reads. The Host validates the
|
|
248
|
+
client's selection against its own walk, so a request can only ever name Sessions in that lineage
|
|
249
|
+
(`400 unknown-descendant` otherwise). Omitting the field still means "all of them".
|
|
250
|
+
|
|
251
|
+
**A gate bug, fixed.** Running work was only checked for the target Session, but DSH's archive
|
|
252
|
+
admission answers per Session: a descendant's running turn or background job was invisible, so a child
|
|
253
|
+
could be deleted mid-run. Every Session in the delete set is now asked, the refusal names each busy
|
|
254
|
+
Session (`activeSessions` in the details, badges on the rows), and `stop: true` dispatches
|
|
255
|
+
`workspace/session-stop` for all of them.
|
|
256
|
+
|
|
257
|
+
**Shells no longer outlive their conversation.** Terminals belong to no admission family, and the
|
|
258
|
+
service only reaps them when the Agent is released — which can be long after the log is gone. Each
|
|
259
|
+
deleted Session's terminals are now closed, reported as `terminalsKilled`.
|
|
260
|
+
|
|
261
|
+
**API.** `GET /inspect` answers with `descendants: { count, subagents, derived, truncated,
|
|
262
|
+
maxDeletable, items[] }`, each item carrying `id`, `kind`, `depth`, `parentId`, `title`, `open`,
|
|
263
|
+
`agent`, `running` and its own `activity`. `POST /delete` takes `{ sessionId, stop?, descendants? }`
|
|
264
|
+
and answers with `kept`, `terminalsKilled` and `warnings` beside the per-Session reports. The
|
|
265
|
+
request-body ceiling is 64 KiB so a few hundred ids fit, and the 200-per-request cap applies to the
|
|
266
|
+
selection rather than to the whole family.
|
|
267
|
+
|
|
268
|
+
**Audited, deliberately unchanged.** Attachment blobs stay (content-addressed and shared; the service
|
|
269
|
+
has no reference counting), and the derived search index keeps reconciling itself.
|
|
270
|
+
|
|
271
|
+
## 0.2.0
|
|
272
|
+
|
|
273
|
+
- **Forked conversations are deleted with their source.** A descendant is now read from the header
|
|
274
|
+
lineage every Session carries (`SessionHeader.parentSession`), which DSH writes for both
|
|
275
|
+
relations: the Subagent runtime sets it with `origin: 'subagent'`, and a fork sets it with
|
|
276
|
+
`isSeeded`. 0.1.0 walked only the subagent catalog, so a conversation forked off the deleted one
|
|
277
|
+
survived.
|
|
278
|
+
- The confirmation dialog names what goes with the delete by kind — *N subagent conversations* and
|
|
279
|
+
*N conversations forked off it* — so deleting a family is never silent.
|
|
280
|
+
- `/inspect` reports `descendants: { count, subagents, derived, ids, capped }`.
|
|
281
|
+
- Each descendant report in the delete response carries its `kind` (`subagent` / `derived`).
|
|
282
|
+
- The lineage walk is breadth-first with a visited set and a depth cap: DSH's own lineage traversal
|
|
283
|
+
has no guard against a hand-edited `parentSession` cycle.
|
|
284
|
+
|
|
285
|
+
## 0.1.0
|
|
286
|
+
|
|
287
|
+
First release.
|
|
288
|
+
|
|
289
|
+
- Adds a **Delete conversation** row (order 500) to every session's `⋯` menu, with a confirmation
|
|
290
|
+
dialog that states the session's real state before committing.
|
|
291
|
+
- Removes the session's artifact directory (every retained format generation), its workspace
|
|
292
|
+
account, its archive/pin membership, and its projection-cache record, then emits
|
|
293
|
+
`api-session/removed` so connected pages drop the row at once.
|
|
294
|
+
- Removes subagent descendants too, deepest first, collected before anything is deleted and capped
|
|
295
|
+
at 200 sessions per delete.
|
|
296
|
+
- Gates on running work only (DSH's own `workspace/session-activity` admission), with a
|
|
297
|
+
**Stop and delete** path that dispatches `workspace/session-stop` first.
|
|
298
|
+
- Deletes sessions the Host still holds open, instead of refusing them: measured on Windows, `rm`
|
|
299
|
+
succeeds with the append handle open and later appends do not resurrect the file.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 陆知辰 (Lzcdebear)
|
|
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
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# better-dsh-session-deletetool
|
|
2
|
+
|
|
3
|
+
**Delete a conversation from DeepSeek Harness — DSH ships archive only.**
|
|
4
|
+
|
|
5
|
+
[中文说明](README.zh-CN.md)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why this exists
|
|
10
|
+
|
|
11
|
+
DSH can only *archive* a conversation. Archiving hides the row from the sidebar and keeps every
|
|
12
|
+
artifact: the log, the workspace account, the projection checkpoint all stay on disk, and the
|
|
13
|
+
conversation can be restored at any time. The persistence seam has no deletion API at all —
|
|
14
|
+
`@deepseek-ai/dsh-session-persistence-jsonl` states it plainly:
|
|
15
|
+
|
|
16
|
+
> **Nothing deletes session files** — logs accumulate under `root` until removed externally; the
|
|
17
|
+
> seam has no deletion API.
|
|
18
|
+
|
|
19
|
+
So a long-lived profile accumulates conversations forever. This plugin adds the missing step: a
|
|
20
|
+
**Delete conversation** row in each session's `⋯` menu that really removes the data — and the dialog
|
|
21
|
+
it opens is also the one place that shows a conversation's whole family before you touch it.
|
|
22
|
+
|
|
23
|
+
## What it deletes
|
|
24
|
+
|
|
25
|
+
For the chosen session, the Host half:
|
|
26
|
+
|
|
27
|
+
1. **removes the session's artifact directory** — the current log, every retained historical format
|
|
28
|
+
generation, and any session-local file in it (`ctx.sessionPersistence.locate(header)` gives the
|
|
29
|
+
path);
|
|
30
|
+
2. **removes the descendants you selected** — child sessions are sessions of their own with their own
|
|
31
|
+
logs. DSH records every child in its parent's header (`SessionHeader.parentSession`), and writes
|
|
32
|
+
that field for every relation it creates: the Subagent runtime sets it with `origin: 'subagent'`,
|
|
33
|
+
a fork sets it with `isSeeded`, and Agent Teams resolves its roster through the same field. The
|
|
34
|
+
plugin walks that lineage breadth-first and unions it with the durable `subagentCatalog`
|
|
35
|
+
projection (`ctx.subagents.listDescendants`), so a child whose own header no longer reads is still
|
|
36
|
+
named. Descendants are deleted **deepest first**, gathered *before* anything is removed, and only
|
|
37
|
+
the ticked ones go — up to 200 per request;
|
|
38
|
+
3. **drops the workspace account** — the id leaves every Workspace record's `sessionIds`
|
|
39
|
+
(`Workspace.detachSession`) and the registry-global archive and pin sets;
|
|
40
|
+
4. **drops the projection checkpoint** — the `session_projcache` domain record and its `<id>.json`
|
|
41
|
+
file;
|
|
42
|
+
5. **closes the shells the session owned** — `ctx.terminals` are part of no admission family, and the
|
|
43
|
+
service only reaps them when the Agent is released, which can be long after the log is gone;
|
|
44
|
+
6. **tells connected pages** — one `api-session/removed` per removed id, the same event the shipped
|
|
45
|
+
Session controller emits when a Session is disposed, so the rows leave the sidebar immediately.
|
|
46
|
+
|
|
47
|
+
### Seeing the family, and choosing what goes with it
|
|
48
|
+
|
|
49
|
+
The sidebar lists a subagent session and a derived conversation as rows of their own, and nothing there
|
|
50
|
+
says which conversation spawned them; DSH itself never draws that relation. This dialog is the one
|
|
51
|
+
place it is drawn: the session's whole family as **one tree**, the branches below the branches
|
|
52
|
+
included.
|
|
53
|
+
|
|
54
|
+
- row 1 is this conversation, carrying the whole family's checkbox and named as the sidebar names it;
|
|
55
|
+
- row 2 is one collapsible **Subagents (n)** branch holding the subagents this conversation spawned;
|
|
56
|
+
- row 3 is one collapsible **Forked conversations (n)** branch, and each fork inside it keeps *its own*
|
|
57
|
+
Subagents branch — so a fork of a fork is drawn inside its parent's block, one step further in,
|
|
58
|
+
instead of being listed beside it. A subagent that spawned a conversation keeps that conversation
|
|
59
|
+
inside the subagent's own block for the same reason;
|
|
60
|
+
- every row has a checkbox, its name and the tail of its id, and is badged when it is open or has
|
|
61
|
+
unfinished work. Names follow the sidebar's own rule — the durable title, else the final segment of
|
|
62
|
+
the project directory, else the id — so nothing reads as *untitled*;
|
|
63
|
+
- the nesting is the **indentation** alone, one step per level: a parent is followed immediately by the
|
|
64
|
+
children it draws, so a level-3 row reads as hanging off the level-2 row above it rather than merely
|
|
65
|
+
as "somewhere deeper".
|
|
66
|
+
|
|
67
|
+
A row's checkbox covers **its own subtree**: ticked when everything below it is ticked, mixed when only
|
|
68
|
+
part of it is, and pressing it takes or clears that whole subtree. A block's checkbox covers the rows
|
|
69
|
+
the block lists. The delete set is one set of ids, with no second "excluded rows" state beside it, so
|
|
70
|
+
the picture and the footer's count cannot drift apart.
|
|
71
|
+
|
|
72
|
+
Leads that could not be read are reported above the list (a branch whose directory would not open, for
|
|
73
|
+
example) instead of quietly costing rows: a missing row and a conversation that genuinely has no
|
|
74
|
+
children used to look exactly alike.
|
|
75
|
+
|
|
76
|
+
Confirming posts exactly the ticked ids and the button states how many conversations will go.
|
|
77
|
+
Unticked descendants survive as conversation roots. The selection is validated against the Host's own
|
|
78
|
+
walk, so a request can only ever name Sessions in that lineage. A fork is a conversation in its own
|
|
79
|
+
right, which is exactly why it is listed with a checkbox instead of being taken silently.
|
|
80
|
+
|
|
81
|
+
### Deleting in bulk
|
|
82
|
+
|
|
83
|
+
A trash control sits in the workspace section header, immediately **left of the search icon** (it
|
|
84
|
+
draws the `deleting icon.svg` glyph). It opens one dialog over every conversation the Host knows:
|
|
85
|
+
|
|
86
|
+
- **Sectioned by Workspace**, in the registry's own order, each headed by the Workspace title and its
|
|
87
|
+
conversation count; conversations no Workspace owns land in a final **Ungrouped** section.
|
|
88
|
+
- **Each conversation is drawn as the same tree the single-session dialog draws**: under a row come its
|
|
89
|
+
collapsible **Subagents (n)** and **Forked conversations (n)** blocks, and a derived conversation
|
|
90
|
+
carries its own level inside its own block, so the hierarchy is the indentation alone.
|
|
91
|
+
- **Names follow the sidebar's own rule**: the durable title when the log carries one, else the final
|
|
92
|
+
segment of the project directory (for example `Project_lzc`), else the id. A conversation that was
|
|
93
|
+
never renamed therefore reads as its directory rather than as *untitled*.
|
|
94
|
+
- **Selection follows the subtree, and a child can be deleted on its own.** Ticking a parent takes the
|
|
95
|
+
ticked rows below it; ticking one subagent without its parent sends that subagent as a delete root of
|
|
96
|
+
its own and leaves the parent alone. The footer button always states how many conversations the press
|
|
97
|
+
will delete.
|
|
98
|
+
|
|
99
|
+
The dialog carries the same **stop unfinished work first** switch as the single-session one, on by
|
|
100
|
+
default. Confirming runs the delete above once per ticked root; a root that fails is listed and the
|
|
101
|
+
rest still go.
|
|
102
|
+
|
|
103
|
+
### What blocks a delete, and what does not
|
|
104
|
+
|
|
105
|
+
**Running work blocks it — being open does not.** The gate is DSH's own archive admission, the
|
|
106
|
+
`workspace/session-activity` waterfall: the Agent registry reports a running turn, the job registry
|
|
107
|
+
reports background jobs, the Subagent runtime reports running descendants, Schedule reports active
|
|
108
|
+
reminders. The question is asked **once per Session in the delete set**, because the admission answers
|
|
109
|
+
per Session: a descendant's running turn or background job is not reported when only the target is
|
|
110
|
+
asked. The dialog names what is still running — on each row and in the summary — and offers
|
|
111
|
+
**Stop and delete**, which dispatches `workspace/session-stop` for every busy Session first, exactly
|
|
112
|
+
what `archiveSession(id, { stopActivity: true })` does.
|
|
113
|
+
|
|
114
|
+
A session the Host still holds open (`ctx.sessions` / `ctx.agents`) is deleted anyway. Measured on
|
|
115
|
+
Windows: `rm` succeeds while the append handle is open, the directory entry disappears at once, and
|
|
116
|
+
later appends land in the unlinked file instead of resurrecting it. An earlier version refused such
|
|
117
|
+
sessions with "switch away first", which wrongly blocked conversations that were not on screen.
|
|
118
|
+
|
|
119
|
+
### Known limits
|
|
120
|
+
|
|
121
|
+
- **Attachment blobs are not removed.** Uploaded images and files live in
|
|
122
|
+
`~/.dsh/attachments/v1/objects/<hash>`, a content-addressed store shared by sessions, and the
|
|
123
|
+
service has no reference counting, so those blobs stay.
|
|
124
|
+
- **The search index looks after itself.** `dsh-session-query-sqlite` is a derived index that
|
|
125
|
+
reconciles against persistence on every search, so a deleted source stops being returned from the
|
|
126
|
+
next search on.
|
|
127
|
+
- **A held-open session may leave one inert cache record.** If the Host still owns the session when
|
|
128
|
+
it is deleted, its disposal can write one more projection checkpoint. That record is never read
|
|
129
|
+
(no log means no session) — a few tens of KB of dead file.
|
|
130
|
+
- **Plugin code changes need a DSH restart.** In a profile whose HMR does not watch module files,
|
|
131
|
+
replacing the code of an installed bundle requires a restart. The client half only needs a page
|
|
132
|
+
refresh.
|
|
133
|
+
|
|
134
|
+
## Install
|
|
135
|
+
|
|
136
|
+
Everything below goes through DSH's own plugin manager, which accepts an install spec
|
|
137
|
+
(`@deepseek-ai/dsh-plugin-manager`): a registry name, a git host shorthand, a repository URL, a
|
|
138
|
+
tarball, or an absolute local path. Pick whichever route your network allows.
|
|
139
|
+
|
|
140
|
+
### 1. Straight from GitHub (needs github.com reachable)
|
|
141
|
+
|
|
142
|
+
Spec:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
github:Lzcdebear/better-dsh-session-deletetool
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
or, equivalently:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
https://github.com/Lzcdebear/better-dsh-session-deletetool
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Give it to DSH:
|
|
155
|
+
|
|
156
|
+
- **In the app:** Settings → Plugins → the install entry, paste the spec. (The Plugin Manager UI and
|
|
157
|
+
the `plugin_manager` tool take exactly the same spec string.)
|
|
158
|
+
- **Through an agent session:** ask the agent to install it — the tool call is `plugin_manager` with
|
|
159
|
+
`action: "install_bundle"` and `target: "github:Lzcdebear/better-dsh-session-deletetool"`.
|
|
160
|
+
|
|
161
|
+
Pin a ref with `#`: `github:Lzcdebear/better-dsh-session-deletetool#v0.1.0`.
|
|
162
|
+
|
|
163
|
+
If the connection check fails, DSH reports a bounded log path. On a network where github.com is not
|
|
164
|
+
reachable, use route 2 or 3 — or point git at your proxy first
|
|
165
|
+
(`git config --global http.proxy http://127.0.0.1:7890`).
|
|
166
|
+
|
|
167
|
+
### 2. From a local copy (works offline)
|
|
168
|
+
|
|
169
|
+
Download the repository (ZIP or `git clone`), unpack it anywhere, then install the **absolute
|
|
170
|
+
directory**:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
plugin_manager { action: "install_bundle", target: "D:\\plugins\\better-dsh-session-deletetool" }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
DSH records a `link:` dependency and reloads the profile. This is the route used to develop the
|
|
177
|
+
plugin, and the one to use behind a restrictive network.
|
|
178
|
+
|
|
179
|
+
### 3. From the release tarball
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
https://github.com/Lzcdebear/better-dsh-session-deletetool/archive/refs/heads/main.tar.gz
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Same install entry as route 1; useful when git is unavailable but HTTPS is not.
|
|
186
|
+
|
|
187
|
+
### After installing
|
|
188
|
+
|
|
189
|
+
The Host half loads with the profile. The Client half appears after the page is refreshed. The row
|
|
190
|
+
shows up in the session `⋯` menu as **删除会话 / Delete conversation**.
|
|
191
|
+
|
|
192
|
+
To remove the plugin again, use the same manager:
|
|
193
|
+
`plugin_manager { action: "remove_bundle", target: "better-dsh-session-deletetool" }` (the bundle key is the
|
|
194
|
+
package name).
|
|
195
|
+
|
|
196
|
+
## Usage
|
|
197
|
+
|
|
198
|
+
1. Open the `⋯` menu on any session row and pick **Delete conversation**.
|
|
199
|
+
2. The dialog asks the Host for the session's real state and states it: what will be deleted,
|
|
200
|
+
whether the Harness still holds it open, and what is still running.
|
|
201
|
+
3. It then draws the whole family as a tree to choose from: this conversation on its own row, then its
|
|
202
|
+
**Subagents (n)** and **Forked conversations (n)** blocks, each derived conversation carrying its own
|
|
203
|
+
level inside its block, and a checkbox on every row (title, id tail, state badge). Untick whatever
|
|
204
|
+
you want to keep.
|
|
205
|
+
4. Confirm. The button states how many conversations will go; if work is running it reads
|
|
206
|
+
**Stop and delete** and stops that work first.
|
|
207
|
+
5. The rows leave the sidebar at once, and the data is gone from disk.
|
|
208
|
+
|
|
209
|
+
For bulk: press the trash control in the workspace section header, **left of the search icon**, tick
|
|
210
|
+
conversations in the Workspace-sectioned list (ticking a parent brings its children, a child can be
|
|
211
|
+
ticked or unticked on its own), then confirm.
|
|
212
|
+
|
|
213
|
+
## HTTP surface
|
|
214
|
+
|
|
215
|
+
The Client half reaches the Host over four same-origin `exact` routes on `ctx.webServer`. A
|
|
216
|
+
build-free plain-JavaScript bundle cannot declare a typed `ctx.remote` namespace (that needs
|
|
217
|
+
generated Typert descriptors), so this uses the same transport the community plugin `dshmarket`
|
|
218
|
+
uses.
|
|
219
|
+
|
|
220
|
+
| Route | Method | Purpose |
|
|
221
|
+
|---|---|---|
|
|
222
|
+
| `/better-dsh-session-deletetool/inspect?sessionId=…` | GET | `stored` / `open` / `agent` / `running` / `activity` / `artifactDirectory` / `warnings` (the leads that could not be read, so a missing branch is never mistaken for a childless one), and `descendants` = `{ count, subagents, derived, truncated, maxDeletable, items[] }` where each item carries `id`, `kind`, `depth`, `parentId`, `title`, `open`, `agent`, `running` and its own `activity` |
|
|
223
|
+
| `/better-dsh-session-deletetool/delete` | POST | body `{ sessionId, stop?, descendants? }` — `descendants` omitted means the whole family, an empty array means the session alone; returns `removed`, `descendants` (each with its `kind`), `kept`, `stoppedActivity`, `terminalsKilled`, `warnings`, `runtime`, `activity`. A name outside the family is refused with `400 unknown-descendant` before anything is removed |
|
|
224
|
+
| `/better-dsh-session-deletetool/catalog` | GET | `{ ok, workspaces: [{ key, workspaceId, title, path, sessions[] }], totals }`; each session row carries `id`, `kind` (`root` / `subagent` / `derived`), `depth`, `parentId`, `hasChildren`, `family`, `subagents`, `derived`, `title`, `cwd`, `open`, `agent`, `running`, `activity`. Section order is the registry's own Workspace order, and the section whose `workspaceId` is `null` is Ungrouped |
|
|
225
|
+
| `/better-dsh-session-deletetool/delete-batch` | POST | body `{ roots: [{ sessionId, descendants? }], stop? }`, each root running the single-session delete once; returns `{ ok, roots, removed[], failed[] }`. A failing root is reported as one `failed` entry and the rest are still attempted. At most 200 roots per request |
|
|
226
|
+
|
|
227
|
+
All four routes carry their own same-origin gate: `Host` must be loopback, `sec-fetch-site` must not
|
|
228
|
+
be `cross-site`, and a present `Origin` must match `Host`. Another site's page cannot reach them.
|
|
229
|
+
|
|
230
|
+
## Layout
|
|
231
|
+
|
|
232
|
+
| File | Role |
|
|
233
|
+
|---|---|
|
|
234
|
+
| `host.js` | Host half: the four routes, artifact/accounting/cache removal, the subagent subtree, and the Workspace-sectioned catalog |
|
|
235
|
+
| `client.js` | Client half: the `sidebar.workspaces.session.menu.item` row (order 500), the bulk control, and the two `shell.overlay` dialogs |
|
|
236
|
+
| `cordis.patch.yml` | Inserts the Host row into the profile's layer stack |
|
|
237
|
+
| `test/host.test.mjs` | Route tests over real temporary directories |
|
|
238
|
+
| `icon.svg` | Plugin artwork (the bulk control draws `deleting icon.svg`) |
|
|
239
|
+
|
|
240
|
+
Styles use only host theme tokens (`--dsw-alias-*`), so light and dark both read; the UI is
|
|
241
|
+
localized (English / 简体中文) through the Client locale service.
|
|
242
|
+
|
|
243
|
+
Why the bulk entry is not a slot registration of its own: `sidebar.workspaces` is a `single` slot, so
|
|
244
|
+
a second registrant would **shadow** the shipped session browser instead of sitting beside it. The
|
|
245
|
+
Client half therefore registers in `sidebar.footer.action` (rendering nothing there) and mounts its
|
|
246
|
+
control into the browser's own search cell through a portal and one `MutationObserver`, which
|
|
247
|
+
re-creates the control when React re-renders that header. That insertion carries no authority beyond
|
|
248
|
+
opening the dialog; the two Host routes do the work. If a future harness renames that cell's CSS
|
|
249
|
+
class, the control stops appearing and nothing else changes.
|
|
250
|
+
|
|
251
|
+
## Development
|
|
252
|
+
|
|
253
|
+
```sh
|
|
254
|
+
node --test test/host.test.mjs
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The tests drive the real `apply()` registration against fake Host services and real temporary
|
|
258
|
+
directories: deletion, the running-work gate and its stop path, the subagent subtree, the inspect
|
|
259
|
+
report, and the refusal paths (bad id, bad body, wrong method, cross-site origin).
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
[MIT](LICENSE)
|
|
264
|
+
|
|
265
|
+
## Author
|
|
266
|
+
|
|
267
|
+
- Bilibili: <https://space.bilibili.com/220996778>
|