把清理临时文件从通用设置移到插件页

This commit is contained in:
pyh
2026-09-30 16:41:03 +08:00
parent da8e5e43a4
commit fee7799d7e
4 changed files with 66 additions and 35 deletions
+4 -2
View File
@@ -30,14 +30,16 @@ DSH 原本只能归档会话——归档只是把会话从列表里移开,日
### 2. 清理临时文件
设置 → 通用设置 → **清理临时文件**,清理 spill 后端留下的**孤儿**目录。判定偏保守:
**设置 → 插件 → 会话彻底删除**(点会话彻底删除那一行右侧的配置按钮),清理 spill 后端留下的**孤儿**目录。判定偏保守:
1. 目录名必须精确等于 `session-<sha256(会话 id) 前 12 位>`——这是 spill 后端唯一会生成的形状;
2. 该名字不属于任何已知会话(`sessionQuery.listSessions()` 已经把存量会话与内存中正在运行的会话合并成同一份语料,所以「已知」只需要一个来源);
3. 只在 spill 的 root 内操作(后端当前 root + 系统临时目录下的 `dsh-spill*`);
4. 符号链接一律跳过、不跟随;单项失败只记警告。
结果直接显示在该行(清理了几个目录、几个文件、释放多少空间),可反复点击。
结果直接显示在该页(清理了几个目录、几个文件、释放多少空间),可反复点击。
这个页面占的是 `plugins.row.config` 槽位——和官方插件「终端」的配置页是同一个位置。注册了这个槽位的 key(`<包名>#<行 id>`)之后,插件行就会长出配置入口,所以本插件不声明 `Config` 也能有自己的页面。
## 行为与边界
+60 -31
View File
@@ -6,12 +6,20 @@
* posts to the Host half's authenticated route; the Host owns every policy
* decision and the Client only renders its answer.
*
* The temporary-file sweep is the plugin's other operation, and it lives on
* this plugin's own page under Settings → Plugins: the entry registers into
* `plugins.row.config` under the `<package name>#<row id>` key the manager
* dispatches, which also gives the row its configure control. That is the same
* seat a bundled plugin's configuration form occupies, so the plugin's only
* control sits beside everything else the manager says about it instead of
* taking a row in General settings.
*
* Two client-authoring rules shape this file:
*
* - No Harness Client module is imported. Those packages change without notice,
* a plain-JavaScript bundle has no type check, and a throwing component blanks
* the whole slot entry, so the menu row, the row button, the dialog, and the
* General-settings row re-implement the shipped markup, stylesheet
* plugin page's section re-implement the shipped markup, stylesheet
* declarations, and focus/Escape behavior themselves. Class names are renamed
* under `dsd-` and every color, radius, elevation, and motion value stays a
* `--dsw-*` theme-token reference.
@@ -35,8 +43,16 @@ window.__ModuleLoader__.load({
const MENU_ORDER = 500
/** Between the shipped `archive` (100) and `pin` (200) hover buttons. */
const ROW_ACTION_ORDER = 150
/** General-settings position: after the log-upload row, before the version. */
const SETTINGS_ORDER = 96
/**
* The key `plugins.row.config` is dispatched under: the manager composes it
* as `<package name>#<row id>`, with the row id read from this bundle's own
* patch. Both halves are literals from `package.json` and
* `cordis.patch.yml`; a typo costs the configure control silently, so they
* are the only two places renaming has to reach.
*/
const PACKAGE = '@dsh-plugin/session-delete'
const ROW_ID = 'session-delete'
const CONFIG_KEY = `${PACKAGE}#${ROW_ID}`
const TOOLTIP_DELAY_MS = 500
const TOOLTIP_GAP = 8
/** Document base captured at bundle registration, before any routing. */
@@ -110,10 +126,11 @@ window.__ModuleLoader__.load({
.dsd-row-button{border-radius:var(--dsw-radius-xs);cursor:pointer;width:16px;height:16px;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}
.dsd-row-button:hover{color:var(--dsw-alias-label-primary)}
.dsd-row-button:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}
.dsd-row{border-bottom:.5px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}
.dsd-row-text{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}
.dsd-row-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}
.dsd-row-desc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}
.dsd-stack{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}
.dsd-section{flex-direction:column;gap:12px;display:flex}
.dsd-section-head{align-items:center;gap:16px;display:flex}
.dsd-section-title{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;font-weight:500;line-height:22px}
.dsd-section-desc{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;font-weight:400;line-height:18px}
.dsd-tooltip{display:inline-flex;align-items:center;gap:8px;position:fixed;z-index:100;width:max-content;max-width:50vw;padding:3px 7px;border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-tooltip-bg);color:var(--dsw-static-neutral-bluish-00);font-size:13px;line-height:20px;white-space:pre-line;overflow-wrap:break-word;pointer-events:none;transform:translateX(-100%);animation:dsd-tooltip-in 150ms var(--ds-ease-in-out)}
@keyframes dsd-tooltip-in{from{opacity:0}}
@media (prefers-reduced-motion: reduce){.dsd-tooltip{animation:none}}
@@ -311,14 +328,23 @@ window.__ModuleLoader__.load({
}
/**
* The General-settings row: sweep spilled tool output whose conversation no
* longer exists. The owner supplies nothing, so copy, control, and result
* all belong to this entry.
* This plugin's page under Settings → Plugins: sweep spilled tool output
* whose conversation no longer exists.
*
* The owner asks every `plugins.row.config` entry for two views. `summary`
* is the one-liner on the row, and this plugin's manifest description
* already fills that seat, so `summary` renders nothing and the page keeps
* the text the manager read from `locale/*.json`. `page` is the section the
* manager renders with its own heading, breadcrumb, and title; copy,
* control, and result belong to this entry.
*/
function TempCleanupRow(props) {
function TempCleanupPanel(props) {
const t = translate(props.t)
const cleanOrphans = props.cleanOrphans
// Every hook sits above the view guard: React must see the same hook
// order in both views, so the guard can only skip the output.
const [state, setState] = React.useState({ phase: 'idle', failed: false, message: '' })
if (props.view !== 'page') return null
const busy = state.phase === 'busy'
const run = async () => {
setState({ phase: 'busy', failed: false, message: '' })
@@ -343,23 +369,24 @@ window.__ModuleLoader__.load({
}
return h(React.Fragment, null,
h('style', null, CSS),
h('div', { className: 'dsd-row' },
h('div', { className: 'dsd-row-text' },
h('div', { className: 'dsd-row-title' }, t('cleanup.title')),
h('div', { className: 'dsd-row-desc' }, t('cleanup.description')),
state.phase === 'done' && h('div', {
className: `dsd-status${state.failed ? ' dsd-status-error' : ''}`,
role: state.failed ? 'alert' : 'status',
}, state.message)),
h('button', {
type: 'button',
className: 'dsd-button dsd-outline',
disabled: busy,
onClick: run,
}, busy ? t('cleanup.pending') : t('cleanup.action'))))
h('div', { className: 'dsd-section' },
h('div', { className: 'dsd-section-head' },
h('div', { className: 'dsd-stack' },
h('h4', { className: 'dsd-section-title' }, t('cleanup.title')),
h('p', { className: 'dsd-section-desc' }, t('cleanup.description'))),
h('button', {
type: 'button',
className: 'dsd-button dsd-outline',
disabled: busy,
onClick: run,
}, busy ? t('cleanup.pending') : t('cleanup.action'))),
state.phase === 'done' && h('p', {
className: `dsd-status${state.failed ? ' dsd-status-error' : ''}`,
role: state.failed ? 'alert' : 'status',
}, state.message)))
}
/** Render a byte count the way the settings row reports reclaimed space. */
/** Render a byte count the way the cleanup result reports reclaimed space. */
function formatBytes(bytes) {
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'
const units = ['B', 'KB', 'MB', 'GB']
@@ -623,13 +650,15 @@ window.__ModuleLoader__.load({
}),
}, DeleteSessionDialog))
ctx.slots.inject('settings.general.item', () => ctx.slots.register({
name: 'settings.general.item',
id: `${NS}-temp-cleanup`,
order: SETTINGS_ORDER,
// The cleanup control belongs on this plugin's own page: registering
// this cell key is what makes the row grow a configure control, and the
// manager renders the entry for both its summary and page views.
ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
name: 'plugins.row.config',
key: CONFIG_KEY,
locale: NS,
inject: () => ({ cleanOrphans }),
}, TempCleanupRow))
}, TempCleanupPanel))
},
}
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"meta": {
"title": "Session delete",
"description": "Permanently delete a conversation: stop it, remove its session log, subagent logs, and spilled tool output, then drop every registry reference. Settings → General's “Clean up temporary files” sweeps what conversations that are gone left behind."
"description": "Permanently delete a conversation: stop it, remove its session log, subagent logs, and spilled tool output, then drop every registry reference. This plugin's own page sweeps what conversations that are gone left behind."
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"meta": {
"title": "会话彻底删除",
"description": "彻底删除一个会话:先停止它,再删除会话日志、子智能体日志与工具输出溢出文件,并清理相关注册表引用;设置 → 通用设置里的「清理临时文件」可清理已删除会话遗留的孤立临时文件。"
"description": "彻底删除一个会话:先停止它,再删除会话日志、子智能体日志与工具输出溢出文件,并清理相关注册表引用;这个插件的页面里可清理已删除会话遗留的孤立临时文件。"
}
}