From fee7799d7ef437bb0e274b4876be1ea5a2172cc9 Mon Sep 17 00:00:00 2001 From: pyh Date: Wed, 30 Sep 2026 16:41:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=8A=E6=B8=85=E7=90=86=E4=B8=B4=E6=97=B6?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=BB=8E=E9=80=9A=E7=94=A8=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E7=A7=BB=E5=88=B0=E6=8F=92=E4=BB=B6=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++-- client.js | 91 +++++++++++++++++++++++++++++++++----------------- locale/en.json | 2 +- locale/zh.json | 2 +- 4 files changed, 66 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index f524608..68054c5 100644 --- a/README.md +++ b/README.md @@ -30,14 +30,16 @@ DSH 原本只能归档会话——归档只是把会话从列表里移开,日 ### 2. 清理临时文件 -设置 → 通用设置 → **清理临时文件**,清理 spill 后端留下的**孤儿**目录。判定偏保守: +**设置 → 插件 → 会话彻底删除**(点会话彻底删除那一行右侧的配置按钮),清理 spill 后端留下的**孤儿**目录。判定偏保守: 1. 目录名必须精确等于 `session-`——这是 spill 后端唯一会生成的形状; 2. 该名字不属于任何已知会话(`sessionQuery.listSessions()` 已经把存量会话与内存中正在运行的会话合并成同一份语料,所以「已知」只需要一个来源); 3. 只在 spill 的 root 内操作(后端当前 root + 系统临时目录下的 `dsh-spill*`); 4. 符号链接一律跳过、不跟随;单项失败只记警告。 -结果直接显示在该行(清理了几个目录、几个文件、释放多少空间),可反复点击。 +结果直接显示在该页(清理了几个目录、几个文件、释放多少空间),可反复点击。 + +这个页面占的是 `plugins.row.config` 槽位——和官方插件「终端」的配置页是同一个位置。注册了这个槽位的 key(`<包名>#<行 id>`)之后,插件行就会长出配置入口,所以本插件不声明 `Config` 也能有自己的页面。 ## 行为与边界 diff --git a/client.js b/client.js index 9e91a54..81c2987 100644 --- a/client.js +++ b/client.js @@ -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 `#` 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 `#`, 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)) }, } }, diff --git a/locale/en.json b/locale/en.json index 875fb7b..1280c74 100644 --- a/locale/en.json +++ b/locale/en.json @@ -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." } } diff --git a/locale/zh.json b/locale/zh.json index 2a483eb..e77c919 100644 --- a/locale/zh.json +++ b/locale/zh.json @@ -1,6 +1,6 @@ { "meta": { "title": "会话彻底删除", - "description": "彻底删除一个会话:先停止它,再删除会话日志、子智能体日志与工具输出溢出文件,并清理相关注册表引用;设置 → 通用设置里的「清理临时文件」可清理已删除会话遗留的孤立临时文件。" + "description": "彻底删除一个会话:先停止它,再删除会话日志、子智能体日志与工具输出溢出文件,并清理相关注册表引用;这个插件的页面里可清理已删除会话遗留的孤立临时文件。" } }