dsh-memoir 0.5.1 → 0.5.3
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/README.en.md +18 -6
- package/README.md +18 -6
- package/cordis.patch.yml +5 -0
- package/lib/autodistill.d.ts +29 -7
- package/lib/autodistill.js +45 -20
- package/lib/client.js +207 -2
- package/lib/client.js.map +2 -2
- package/lib/index.d.ts +13 -0
- package/lib/index.js +33 -5
- package/lib/routes.d.ts +13 -1
- package/lib/routes.js +37 -2
- package/lib/settings.d.ts +40 -0
- package/lib/settings.js +122 -0
- package/package.json +11 -11
package/README.en.md
CHANGED
|
@@ -81,9 +81,11 @@ need long-tail history? memoir_read (local relevance-ranked recall)
|
|
|
81
81
|
|
|
82
82
|
**Session Snapshot freezing semantics**: one session's injected text is built once and frozen (stable prompt prefix, maximizing prompt-prefix cache hits); the current session does not re-consume memory it just wrote, and a new session rebuilds and sees the latest memory. Since v0.4.2, when there is no unique session identity (session.id / agent.id), freezing is skipped — a cache miss beats wrongly reusing another session's snapshot.
|
|
83
83
|
|
|
84
|
-
## v0.5.
|
|
84
|
+
## v0.5.3 Web-configurable auto-distill and rc2 compatibility
|
|
85
85
|
|
|
86
|
-
- The development and peer-dependency baseline is `@deepseek-ai/dsh-* 0.1.
|
|
86
|
+
- The development and peer-dependency baseline is `@deepseek-ai/dsh-* 0.1.1-rc.2`.
|
|
87
|
+
- Auto-distill now supports per-agent worked-turn intervals, time cooldowns, and tool-call thresholds; defaults `1 / 0 / 1` preserve prior behavior.
|
|
88
|
+
- The Web panel now includes Auto-distill Settings for enabling/disabling and editing those three parameters. Saves apply to subsequent turns immediately and persist in `~/.dsh/dsh-memoir.settings.json`; one action removes the Web override and restores the profile values captured at startup.
|
|
87
89
|
- Store format v3 migrates v2 entries without changing their `id`, content, or timestamp. The first mutation materializes `importance`, `pinned`, `status`, `supersedes`, and `tags`; startup reads do not rewrite old files.
|
|
88
90
|
- Retrieval defaults to `active`. Archived and superseded history is retained and can be inspected from the Web panel. Explicit `supersedes` marks its targets as superseded; history is never deleted automatically.
|
|
89
91
|
- Agents can use `memoir_update` to edit an entry's section, title, content, and lifecycle in place; the Web panel also supports editing, pinning, marking superseded, archiving, and restoring.
|
|
@@ -116,11 +118,12 @@ Curated-query Top-5 hit rate: 100% (quality gate ≥ 90%, see `test/recall-quali
|
|
|
116
118
|
|
|
117
119
|
## GUI
|
|
118
120
|
|
|
119
|
-
The v0.4 Project / Global / Search / Add / Delete / Diagnostics architecture is kept
|
|
121
|
+
The v0.4 Project / Global / Search / Add / Delete / Diagnostics architecture is kept and extended:
|
|
120
122
|
|
|
121
123
|
- **Search unified on RetrievalEngine**: a non-empty query calls `GET /api/dsh-memoir/search` — the same BM25 ranking as the agent's `memoir_read` — results ordered by relevance with scores shown
|
|
122
124
|
- **Hot Memory Inspector**: expand to see the Hot Memory that will actually be injected for the current workspace (Actions / Lessons / Recent state) — i.e. "what exactly the next session inherits"
|
|
123
125
|
- **Retrieval Diagnostics**: Retrieval Index (docs/terms/epoch), Query Cache (hits/misses/evictions/hit rate/size/capacity), Last Query (latency/returned), Session Snapshot (hash/createdAt/storeRevision)
|
|
126
|
+
- **Auto-distill Settings (v0.5.3)**: enable or disable auto-distill and edit the worked-turn interval, cooldown minutes, and minimum tool calls in the panel; validated saves persist and apply immediately without restarting DSH
|
|
124
127
|
|
|
125
128
|
## Screenshots
|
|
126
129
|
|
|
@@ -148,6 +151,7 @@ The v0.4 Project / Global / Search / Add / Delete / Diagnostics architecture is
|
|
|
148
151
|
|
|
149
152
|
```text
|
|
150
153
|
~/.dsh/dsh-memoir.json ← structured JSON (single source of truth / SSOT)
|
|
154
|
+
~/.dsh/dsh-memoir.settings.json ← auto-distill overrides saved by the Web panel
|
|
151
155
|
<workspace>/PROJECT_MEMORY.md ← human-readable projection regenerated from the JSON (git-friendly)
|
|
152
156
|
|
|
153
157
|
No cloud memory DB · No embedding API · No vector DB
|
|
@@ -167,6 +171,9 @@ Add a `config` block on the plugin row in `cordis.patch.yml` (all optional; defa
|
|
|
167
171
|
enabled: true # master switch (tools, routes, prompt section)
|
|
168
172
|
announceToAgent: true # system-prompt announcement section
|
|
169
173
|
autoDistill: true # auto distill reminder after each worked turn
|
|
174
|
+
autoDistillEvery: 1 # remind at most once per N worked turns
|
|
175
|
+
autoDistillCooldownMin: 0 # require M minutes between successful reminders
|
|
176
|
+
autoDistillMinTools: 1 # triggering turn must contain at least K tool calls
|
|
170
177
|
hotMemoryTokens: 900 # Hot Memory target tokens
|
|
171
178
|
hotMemoryMaxTokens: 1200 # Hot Memory hard ceiling (never exceeded)
|
|
172
179
|
readDefaultLimit: 8 # memoir_read default result count
|
|
@@ -175,6 +182,10 @@ Add a `config` block on the plugin row in `cordis.patch.yml` (all optional; defa
|
|
|
175
182
|
queryCacheSize: 128 # ranked-query LRU cache size
|
|
176
183
|
```
|
|
177
184
|
|
|
185
|
+
The three auto-distill frequency conditions are combined with AND and isolated per agent. Idle, aborted, subagent, and prior-`memoir_record` turns do not advance the interval. A worked turn below `autoDistillMinTools` advances the interval but cannot trigger by itself. Cooldown changes only after a successful steer.
|
|
186
|
+
|
|
187
|
+
The auto-distill fields in `cordis.patch.yml` are startup defaults. Since v0.5.3, the Auto-distill Settings section at the bottom of the Memory panel can override them: saving atomically writes `~/.dsh/dsh-memoir.settings.json`, and subsequent turns read the new policy immediately. Restore Startup Config removes that override and returns to the profile values resolved when the plugin mounted. Other Hot Memory, recall, and cache settings remain profile-managed.
|
|
188
|
+
|
|
178
189
|
## Design Trade-offs
|
|
179
190
|
|
|
180
191
|
- **Bounded vs full injection**: v0.3 injected the full history into the prompt and it kept growing; v0.4+ injects only budgeted Hot Memory, with long-tail history recalled on demand. Token benchmarks below.
|
|
@@ -183,6 +194,7 @@ Add a `config` block on the plugin row in `cordis.patch.yml` (all optional; defa
|
|
|
183
194
|
- **Multi-process safety**: store record/remove runs inside a cross-process critical section on `~/.dsh/dsh-memoir.lock` (exclusive O_EXCL creation with timeout); the section force-reloads from disk before mutating, so two interleaved DSH processes lose no updates (v0.4.2).
|
|
184
195
|
- **Windows paths**: canonical keys are fully lowercased (`C:\A` / `c:\a\` / `C:/A` share one bucket) while display paths keep the original casing (v0.4.2).
|
|
185
196
|
- **GUI and Agent share one engine**: panel search and `memoir_read` use the same RetrievalEngine instead of separate filter logic (v0.4.2).
|
|
197
|
+
- **Auto-distill cadence**: the default still reminds after every worked turn; research-heavy sessions can combine interval, cooldown, and activity thresholds and tune them immediately from the Web panel (v0.5.3).
|
|
186
198
|
|
|
187
199
|
## Use Cases
|
|
188
200
|
|
|
@@ -213,7 +225,7 @@ Each plugin has its own focus — pick per need; no "which is stronger" narrativ
|
|
|
213
225
|
pnpm install # install devDeps (typescript, esbuild, @deepseek-ai/* type packages)
|
|
214
226
|
pnpm run build # tsc builds the host + esbuild builds the client bundle
|
|
215
227
|
pnpm run typecheck # full type check (src + test)
|
|
216
|
-
pnpm test #
|
|
228
|
+
pnpm test # 154 tests: store (incl. multi-process lock) / settings / snapshot / selector / retrieval / tools / routes / auto-distill / integration / client pure logic / bundle protocol & purity / release notes
|
|
217
229
|
npm run bench # benchmark (100/1k/10k/100k entries); results written to bench/report.md
|
|
218
230
|
```
|
|
219
231
|
|
|
@@ -230,7 +242,7 @@ v0.4.2 benchmark summary (node v22.23.2, budget 900/1200 tokens; full report in
|
|
|
230
242
|
|
|
231
243
|
## Implementation
|
|
232
244
|
|
|
233
|
-
- **Full-stack TypeScript**: `src/host/*.ts` (store / tools / retrieval / selector / snapshot / routes / autodistill / index — tsc emits `lib/*.js`) + `src/client/*.ts(x)` (esbuild emits the `lib/client.js` closure-factory bundle).
|
|
245
|
+
- **Full-stack TypeScript**: `src/host/*.ts` (store / settings / tools / retrieval / selector / snapshot / routes / autodistill / index — tsc emits `lib/*.js`) + `src/client/*.ts(x)` (esbuild emits the `lib/client.js` closure-factory bundle).
|
|
234
246
|
- **Two-sided plugin**: the host half registers the agent tools, `/api/dsh-memoir` routes, the `agent/turn-stopping` auto-distill listener, and the per-project system-prompt injection section; the client half renders the panel. Runtime deps are official NPM SDK packages only.
|
|
235
247
|
- Mounted via the `dsh.bundle.patch` manifest (`insert` row in `cordis.patch.yml`); no DSH source changes.
|
|
236
248
|
- Auto-distill safety boundaries: top-level sessions only (subagents / nested delegations skipped), turns with tool activity that haven't recorded yet, aborted turns skipped, at most one steer per turn.
|
|
@@ -247,7 +259,7 @@ Bug reports must include screenshot / log evidence, a smoke test, code reference
|
|
|
247
259
|
|
|
248
260
|
## Release
|
|
249
261
|
|
|
250
|
-
Current stable release: **v0.5.
|
|
262
|
+
Current stable release: **v0.5.3** (2026-08-22) · [GitHub Release](https://github.com/Qinling-Melon-Farmers/dsh-memoir/releases/tag/v0.5.3) · [npm](https://www.npmjs.com/package/dsh-memoir/v/0.5.3). Full history is in [CHANGELOG.md](./CHANGELOG.md).
|
|
251
263
|
|
|
252
264
|
Every version keeps Chinese and English release notes in sync. GitHub Releases show Chinese by default and place the English notes in a collapsible `English` section.
|
|
253
265
|
|
package/README.md
CHANGED
|
@@ -81,9 +81,11 @@ memoir_record 沉淀工作 / 教训 / 下一步
|
|
|
81
81
|
|
|
82
82
|
**Session Snapshot 冻结语义**:同一 session 的注入文本只构建一次并冻结(prompt 前缀稳定,最大化 prompt-prefix cache 命中);当前 session 不重新消费自己刚写的记忆,新 session 重建并看到最新记忆。v0.4.2 起,没有唯一会话身份(session.id / agent.id)时**不做冻结**——宁可 cache miss,不可跨 session 错复用旧快照。
|
|
83
83
|
|
|
84
|
-
## v0.5.
|
|
84
|
+
## v0.5.3 Web 可调自动收尾与 rc2 兼容性
|
|
85
85
|
|
|
86
|
-
- 当前开发基线为 `@deepseek-ai/dsh-* 0.1.
|
|
86
|
+
- 当前开发基线为 `@deepseek-ai/dsh-* 0.1.1-rc.2`;peer dependency 与开发依赖已统一到 rc2。
|
|
87
|
+
- 自动收尾支持按 agent 配置 worked-turn 间隔、时间冷却和工具调用阈值;默认值 `1 / 0 / 1` 与旧版行为一致。
|
|
88
|
+
- Web 面板新增「自动蒸馏设置」,可启停并修改上述三个参数;保存后立即作用于后续回合,并持久化到 `~/.dsh/dsh-memoir.settings.json`。可一键删除 Web 覆盖并恢复本次启动时的 profile 配置。
|
|
87
89
|
- 存储格式从 v2 迁移到 v3:旧条目保持原有 `id`、内容和时间,首次变更时补齐 `importance`、`pinned`、`status`、`supersedes` 与 `tags`;启动读取不会重写旧文件。
|
|
88
90
|
- 默认只召回 `active` 条目;归档和被替代条目保留在历史中,可在 Web 面板切换状态查看。显式 `supersedes` 会把目标条目标记为 `superseded`,不会自动删除历史。
|
|
89
91
|
- Agent 可用 `memoir_update` 原地编辑条目的分类、标题、正文和生命周期;Web 面板也支持编辑、置顶、标记过时、归档与恢复。
|
|
@@ -116,11 +118,12 @@ curated 查询 Top-5 命中率 100%(质量门禁 ≥90%,见 `test/recall-qua
|
|
|
116
118
|
|
|
117
119
|
## GUI
|
|
118
120
|
|
|
119
|
-
保留 v0.4 的 Project / Global / Search / Add / Delete / Diagnostics
|
|
121
|
+
保留 v0.4 的 Project / Global / Search / Add / Delete / Diagnostics 架构,并持续扩展:
|
|
120
122
|
|
|
121
123
|
- **搜索统一走 RetrievalEngine**:query 非空时面板调用 `GET /api/dsh-memoir/search`,与 agent 的 `memoir_read` 共用同一套 BM25 排序,结果按相关性排列并显示分数
|
|
122
124
|
- **Hot Memory Inspector**:展开查看当前工作区实际会被注入的 Hot Memory(Actions / Lessons / Recent state),即「下一会话到底自动继承什么」
|
|
123
125
|
- **Retrieval Diagnostics**:Retrieval Index(docs/terms/epoch)、Query Cache(hits/misses/evictions/hit rate/size/capacity)、Last Query(latency/returned)、Session Snapshot(hash/createdAt/storeRevision)
|
|
126
|
+
- **自动蒸馏设置(v0.5.3)**:面板内启停 auto-distill,编辑 worked-turn 间隔、冷却分钟数和最低工具调用数;严格校验后持久化并即时应用,无需重启 DSH
|
|
124
127
|
|
|
125
128
|
## 界面预览
|
|
126
129
|
|
|
@@ -148,6 +151,7 @@ curated 查询 Top-5 命中率 100%(质量门禁 ≥90%,见 `test/recall-qua
|
|
|
148
151
|
|
|
149
152
|
```text
|
|
150
153
|
~/.dsh/dsh-memoir.json ← 结构化 JSON(唯一事实源 / SSOT)
|
|
154
|
+
~/.dsh/dsh-memoir.settings.json ← Web 面板保存的自动蒸馏覆盖设置
|
|
151
155
|
<工作区>/PROJECT_MEMORY.md ← 由 JSON 重新生成的人类可读投影(git 友好)
|
|
152
156
|
|
|
153
157
|
No cloud memory DB · No embedding API · No vector DB
|
|
@@ -167,6 +171,9 @@ JSON 是 source of truth,Markdown 是 generated projection:面板、工具
|
|
|
167
171
|
enabled: true # 总开关(工具、路由、注入段)
|
|
168
172
|
announceToAgent: true # system prompt 公告段
|
|
169
173
|
autoDistill: true # 每轮有实际工作的回合结束自动提醒归纳
|
|
174
|
+
autoDistillEvery: 1 # 每 N 个 worked turn 最多提醒一次
|
|
175
|
+
autoDistillCooldownMin: 0 # 两次成功提醒之间至少间隔 M 分钟
|
|
176
|
+
autoDistillMinTools: 1 # 触发提醒的 turn 至少包含 K 次工具调用
|
|
170
177
|
hotMemoryTokens: 900 # Hot Memory 目标 token 数
|
|
171
178
|
hotMemoryMaxTokens: 1200 # Hot Memory 硬上限(永不超过)
|
|
172
179
|
readDefaultLimit: 8 # memoir_read 默认返回条数
|
|
@@ -175,6 +182,10 @@ JSON 是 source of truth,Markdown 是 generated projection:面板、工具
|
|
|
175
182
|
queryCacheSize: 128 # 排序查询的 LRU 缓存大小
|
|
176
183
|
```
|
|
177
184
|
|
|
185
|
+
三个 auto-distill 频率条件按 AND 关系判定并按 agent 隔离。idle、aborted、subagent、已调用 `memoir_record` 的 turn 不推进间隔计数;低于 `autoDistillMinTools` 的 worked turn 会推进间隔,但自身不能触发提醒。冷却只在 steer 成功后更新。
|
|
186
|
+
|
|
187
|
+
`cordis.patch.yml` 中的 auto-distill 字段是启动默认值。v0.5.3 起,可在记忆面板底部展开「自动蒸馏设置」进行修改:保存会原子写入 `~/.dsh/dsh-memoir.settings.json`,后续 turn 立即读取新策略;「恢复启动配置」会删除该覆盖文件并回到本次插件挂载时解析出的 profile 值。其余 Hot Memory、召回和缓存参数仍由 profile 配置。
|
|
188
|
+
|
|
178
189
|
## Design Trade-offs
|
|
179
190
|
|
|
180
191
|
- **有界注入 vs 全量注入**:v0.3 把完整历史注入 prompt,越用越膨胀;v0.4+ 只注入预算内的 Hot Memory,长尾历史按需召回。token 基准见下方 Benchmark。
|
|
@@ -183,6 +194,7 @@ JSON 是 source of truth,Markdown 是 generated projection:面板、工具
|
|
|
183
194
|
- **多进程安全**:store 的 record/remove 走 `~/.dsh/dsh-memoir.lock` 跨进程临界区(O_EXCL 独占创建 + 超时),临界区内强制从磁盘重读再改,两个 DSH 进程交错写入不丢更新(v0.4.2)。
|
|
184
195
|
- **Windows 路径**:canonical key 全小写(`C:\A` / `c:\a\` / `C:/A` 一个桶),display path 保留原始大小写(v0.4.2)。
|
|
185
196
|
- **GUI 与 Agent 同源**:面板搜索与 `memoir_read` 共用 RetrievalEngine,不再各写一套过滤逻辑(v0.4.2)。
|
|
197
|
+
- **自动收尾节奏**:默认仍逐 worked turn 提醒;研究型会话可组合轮次间隔、冷却与活动阈值降低打扰,并从 Web 面板即时调节(v0.5.3)。
|
|
186
198
|
|
|
187
199
|
## Use Cases
|
|
188
200
|
|
|
@@ -213,7 +225,7 @@ JSON 是 source of truth,Markdown 是 generated projection:面板、工具
|
|
|
213
225
|
pnpm install # 安装 devDeps(typescript、esbuild、@deepseek-ai/* 类型包)
|
|
214
226
|
pnpm run build # tsc 构建 host + esbuild 构建 client bundle
|
|
215
227
|
pnpm run typecheck # 全量类型检查(src + test)
|
|
216
|
-
pnpm test #
|
|
228
|
+
pnpm test # 154 项测试:store(含多进程锁) / settings / snapshot / selector / retrieval / tools / routes / 自动收尾 / 集成 / client 纯逻辑 / bundle 协议与纯净性 / 发布说明
|
|
217
229
|
npm run bench # benchmark(100/1k/10k/100k 条目),结果写入 bench/report.md
|
|
218
230
|
```
|
|
219
231
|
|
|
@@ -230,7 +242,7 @@ v0.4.2 benchmark 摘要(node v22.23.2,budget 900/1200 tokens;完整报告
|
|
|
230
242
|
|
|
231
243
|
## 实现说明
|
|
232
244
|
|
|
233
|
-
- **TypeScript 全栈**:`src/host/*.ts`(store / tools / retrieval / selector / snapshot / routes / autodistill / index,tsc 构建出 `lib/*.js`)+ `src/client/*.ts(x)`(esbuild 打出 `lib/client.js` 闭包工厂 bundle)。
|
|
245
|
+
- **TypeScript 全栈**:`src/host/*.ts`(store / settings / tools / retrieval / selector / snapshot / routes / autodistill / index,tsc 构建出 `lib/*.js`)+ `src/client/*.ts(x)`(esbuild 打出 `lib/client.js` 闭包工厂 bundle)。
|
|
234
246
|
- **双面插件**:host 半注册 agent 工具、`/api/dsh-memoir` 路由、`agent/turn-stopping` 自动收尾监听与按项目求值的 system prompt 注入段;client 半提供面板。运行时仅依赖官方 NPM SDK。
|
|
235
247
|
- 通过 `dsh.bundle.patch` manifest(`cordis.patch.yml` 的 `insert` 行)挂载,不改 DSH 源码。
|
|
236
248
|
- 自动收尾安全边界:仅顶级会话(跳过 subagent / 嵌套委托)、仅「有工具调用且未记录过」的回合、已中止回合不打扰、每回合至多一次。
|
|
@@ -248,7 +260,7 @@ PR 请先提 Issue 讨论。
|
|
|
248
260
|
|
|
249
261
|
## Release
|
|
250
262
|
|
|
251
|
-
当前稳定版:**v0.5.
|
|
263
|
+
当前稳定版:**v0.5.3**(2026-08-22) · [GitHub Release](https://github.com/Qinling-Melon-Farmers/dsh-memoir/releases/tag/v0.5.3) · [npm](https://www.npmjs.com/package/dsh-memoir/v/0.5.3)。完整历史见 [CHANGELOG.md](./CHANGELOG.md)。
|
|
252
264
|
|
|
253
265
|
每个版本的更新日志均同步维护中英文;GitHub Release 默认展开中文,英文说明收纳在可折叠的 `English` 区域。
|
|
254
266
|
|
package/cordis.patch.yml
CHANGED
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
# enabled: true # master switch (tools, routes, prompt section)
|
|
17
17
|
# announceToAgent: true # system-prompt announcement
|
|
18
18
|
# autoDistill: true # steer a distill prompt after each worked turn
|
|
19
|
+
# autoDistillEvery: 1 # remind at most once per N worked turns
|
|
20
|
+
# autoDistillCooldownMin: 0 # minimum minutes between successful reminders
|
|
21
|
+
# autoDistillMinTools: 1 # minimum tool calls on the triggering turn
|
|
22
|
+
# These auto-distill values are startup defaults. v0.5.3+ can persist a live
|
|
23
|
+
# Web-panel override in ~/.dsh/dsh-memoir.settings.json and restore defaults.
|
|
19
24
|
- insert:
|
|
20
25
|
- id: memoir
|
|
21
26
|
name: dsh-memoir
|
package/lib/autodistill.d.ts
CHANGED
|
@@ -19,11 +19,13 @@ export interface TurnEventLike {
|
|
|
19
19
|
type: string;
|
|
20
20
|
data?: unknown;
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
export declare function turnActivity(events: readonly TurnEventLike[], turn: number): {
|
|
22
|
+
export interface TurnActivity {
|
|
24
23
|
worked: boolean;
|
|
25
24
|
recorded: boolean;
|
|
26
|
-
|
|
25
|
+
toolCalls: number;
|
|
26
|
+
}
|
|
27
|
+
/** Scan the tail of a session log for one turn's tool activity. */
|
|
28
|
+
export declare function turnActivity(events: readonly TurnEventLike[], turn: number): TurnActivity;
|
|
27
29
|
/** The agent surface the turn-stopping listener needs. */
|
|
28
30
|
export interface AutoDistillAgentLike {
|
|
29
31
|
id: string;
|
|
@@ -38,11 +40,21 @@ export interface AutoDistillAgentLike {
|
|
|
38
40
|
}
|
|
39
41
|
/** Subagent sessions (and any nested delegation) never get distilled. */
|
|
40
42
|
export declare function isSubagentSession(agent: AutoDistillAgentLike): boolean;
|
|
41
|
-
|
|
43
|
+
export interface AutoDistillPolicy {
|
|
44
|
+
every: number;
|
|
45
|
+
cooldownMs: number;
|
|
46
|
+
minTools: number;
|
|
47
|
+
}
|
|
48
|
+
/** Per-agent frequency, cooldown, and duplicate-turn state (with pruning). */
|
|
42
49
|
export declare class AutoDistillGate {
|
|
43
|
-
private
|
|
44
|
-
/**
|
|
45
|
-
|
|
50
|
+
private states;
|
|
51
|
+
/**
|
|
52
|
+
* Consume one eligible worked turn and decide whether all policy conditions
|
|
53
|
+
* are ready. Duplicate events never advance the worked-turn counter.
|
|
54
|
+
*/
|
|
55
|
+
consume(agentId: string, turn: number, toolCalls: number, policy: AutoDistillPolicy, now: number): boolean;
|
|
56
|
+
/** Record a successful steer; failed steer attempts do not start cooldown. */
|
|
57
|
+
recordSteer(agentId: string, now: number): void;
|
|
46
58
|
/** Drop all state for one agent (disposal hygiene). */
|
|
47
59
|
forget(agentId: string): void;
|
|
48
60
|
}
|
|
@@ -62,4 +74,14 @@ export interface AutoDistillWire {
|
|
|
62
74
|
*/
|
|
63
75
|
export declare function installAutoDistill(wire: AutoDistillWire, options: {
|
|
64
76
|
enabled: () => boolean;
|
|
77
|
+
every?: number;
|
|
78
|
+
cooldownMin?: number;
|
|
79
|
+
minTools?: number;
|
|
80
|
+
/** Optional live policy source used by the Web settings panel. */
|
|
81
|
+
policy?: () => {
|
|
82
|
+
every?: number;
|
|
83
|
+
cooldownMin?: number;
|
|
84
|
+
minTools?: number;
|
|
85
|
+
};
|
|
86
|
+
now?: () => number;
|
|
65
87
|
}): () => void;
|
package/lib/autodistill.js
CHANGED
|
@@ -19,8 +19,8 @@ export const DISTILL_PROMPT = '(dsh-memoir 自动收尾)本轮工作已结
|
|
|
19
19
|
export const AUTO_DISTILL_PLUGIN = 'dsh-memoir';
|
|
20
20
|
/** Scan the tail of a session log for one turn's tool activity. */
|
|
21
21
|
export function turnActivity(events, turn) {
|
|
22
|
-
let worked = false;
|
|
23
22
|
let recorded = false;
|
|
23
|
+
let toolCalls = 0;
|
|
24
24
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
25
25
|
const event = events[i];
|
|
26
26
|
const data = event.data;
|
|
@@ -31,39 +31,54 @@ export function turnActivity(events, turn) {
|
|
|
31
31
|
if (data.turn !== turn)
|
|
32
32
|
continue;
|
|
33
33
|
if (event.type === 'tool/call') {
|
|
34
|
-
|
|
34
|
+
toolCalls += 1;
|
|
35
35
|
if (data.name === 'memoir_record')
|
|
36
36
|
recorded = true;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
return { worked, recorded };
|
|
39
|
+
return { worked: toolCalls > 0, recorded, toolCalls };
|
|
40
40
|
}
|
|
41
41
|
/** Subagent sessions (and any nested delegation) never get distilled. */
|
|
42
42
|
export function isSubagentSession(agent) {
|
|
43
43
|
return agent.session.header.origin === 'subagent' || (agent.session.header.delegationDepth ?? 0) > 0;
|
|
44
44
|
}
|
|
45
|
-
/** Per-agent
|
|
45
|
+
/** Per-agent frequency, cooldown, and duplicate-turn state (with pruning). */
|
|
46
46
|
export class AutoDistillGate {
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
states = new Map();
|
|
48
|
+
/**
|
|
49
|
+
* Consume one eligible worked turn and decide whether all policy conditions
|
|
50
|
+
* are ready. Duplicate events never advance the worked-turn counter.
|
|
51
|
+
*/
|
|
52
|
+
consume(agentId, turn, toolCalls, policy, now) {
|
|
53
|
+
let state = this.states.get(agentId);
|
|
54
|
+
if (state === undefined) {
|
|
55
|
+
state = { processedTurns: new Set(), workedSinceSteer: 0 };
|
|
56
|
+
this.states.set(agentId, state);
|
|
54
57
|
}
|
|
55
|
-
if (
|
|
58
|
+
if (state.processedTurns.has(turn))
|
|
56
59
|
return false;
|
|
57
|
-
|
|
58
|
-
for (const
|
|
59
|
-
if (
|
|
60
|
-
|
|
60
|
+
state.processedTurns.add(turn);
|
|
61
|
+
for (const value of [...state.processedTurns]) {
|
|
62
|
+
if (value < turn - 100)
|
|
63
|
+
state.processedTurns.delete(value);
|
|
61
64
|
}
|
|
62
|
-
|
|
65
|
+
state.workedSinceSteer += 1;
|
|
66
|
+
const intervalReady = state.workedSinceSteer >= policy.every;
|
|
67
|
+
const activityReady = toolCalls >= policy.minTools;
|
|
68
|
+
const cooldownReady = state.lastSteeredAt === undefined || now - state.lastSteeredAt >= policy.cooldownMs;
|
|
69
|
+
return intervalReady && activityReady && cooldownReady;
|
|
70
|
+
}
|
|
71
|
+
/** Record a successful steer; failed steer attempts do not start cooldown. */
|
|
72
|
+
recordSteer(agentId, now) {
|
|
73
|
+
const state = this.states.get(agentId);
|
|
74
|
+
if (state === undefined)
|
|
75
|
+
return;
|
|
76
|
+
state.workedSinceSteer = 0;
|
|
77
|
+
state.lastSteeredAt = now;
|
|
63
78
|
}
|
|
64
79
|
/** Drop all state for one agent (disposal hygiene). */
|
|
65
80
|
forget(agentId) {
|
|
66
|
-
this.
|
|
81
|
+
this.states.delete(agentId);
|
|
67
82
|
}
|
|
68
83
|
}
|
|
69
84
|
/**
|
|
@@ -73,6 +88,8 @@ export class AutoDistillGate {
|
|
|
73
88
|
*/
|
|
74
89
|
export function installAutoDistill(wire, options) {
|
|
75
90
|
const gate = new AutoDistillGate();
|
|
91
|
+
const integerAtLeast = (value, fallback, minimum) => typeof value === 'number' && Number.isFinite(value) ? Math.max(minimum, Math.floor(value)) : fallback;
|
|
92
|
+
const numberAtLeast = (value, fallback, minimum) => typeof value === 'number' && Number.isFinite(value) ? Math.max(minimum, value) : fallback;
|
|
76
93
|
return wire.on('agent/turn-stopping', (payload) => {
|
|
77
94
|
if (!options.enabled())
|
|
78
95
|
return;
|
|
@@ -81,14 +98,22 @@ export function installAutoDistill(wire, options) {
|
|
|
81
98
|
return;
|
|
82
99
|
if (signal.aborted)
|
|
83
100
|
return;
|
|
84
|
-
const { worked, recorded } = turnActivity(agent.session.events, turn);
|
|
101
|
+
const { worked, recorded, toolCalls } = turnActivity(agent.session.events, turn);
|
|
85
102
|
if (!worked || recorded)
|
|
86
103
|
return;
|
|
87
|
-
|
|
104
|
+
const live = options.policy?.();
|
|
105
|
+
const policy = {
|
|
106
|
+
every: integerAtLeast(live?.every ?? options.every, 1, 1),
|
|
107
|
+
cooldownMs: numberAtLeast(live?.cooldownMin ?? options.cooldownMin, 0, 0) * 60_000,
|
|
108
|
+
minTools: integerAtLeast(live?.minTools ?? options.minTools, 1, 1),
|
|
109
|
+
};
|
|
110
|
+
const now = options.now?.() ?? Date.now();
|
|
111
|
+
if (!gate.consume(agent.id, turn, toolCalls, policy, now))
|
|
88
112
|
return;
|
|
89
113
|
agent.steer(createUserMessage({
|
|
90
114
|
content: [{ type: 'text', text: DISTILL_PROMPT }],
|
|
91
115
|
source: { kind: 'plugin', plugin: AUTO_DISTILL_PLUGIN },
|
|
92
116
|
}));
|
|
117
|
+
gate.recordSteer(agent.id, now);
|
|
93
118
|
});
|
|
94
119
|
}
|
package/lib/client.js
CHANGED
|
@@ -114,6 +114,29 @@ var MemoirApi = class {
|
|
|
114
114
|
const response = await this.fetchImpl("/api/dsh-memoir/diagnostics" + query({ path }));
|
|
115
115
|
return readEnvelope(response);
|
|
116
116
|
}
|
|
117
|
+
/** Read the live auto-distill policy and whether it comes from Web overrides. */
|
|
118
|
+
async settings() {
|
|
119
|
+
const response = await this.fetchImpl("/api/dsh-memoir/settings");
|
|
120
|
+
return readEnvelope(response);
|
|
121
|
+
}
|
|
122
|
+
/** Persist and immediately apply the auto-distill policy. */
|
|
123
|
+
async updateSettings(settings) {
|
|
124
|
+
const response = await this.fetchImpl("/api/dsh-memoir/settings", {
|
|
125
|
+
method: "PUT",
|
|
126
|
+
headers: { "content-type": "application/json" },
|
|
127
|
+
body: JSON.stringify(settings)
|
|
128
|
+
});
|
|
129
|
+
return readEnvelope(response);
|
|
130
|
+
}
|
|
131
|
+
/** Remove the Web override and restore the profile defaults captured at boot. */
|
|
132
|
+
async resetSettings() {
|
|
133
|
+
const response = await this.fetchImpl("/api/dsh-memoir/settings", {
|
|
134
|
+
method: "DELETE",
|
|
135
|
+
headers: { "content-type": "application/json" },
|
|
136
|
+
body: "{}"
|
|
137
|
+
});
|
|
138
|
+
return readEnvelope(response);
|
|
139
|
+
}
|
|
117
140
|
/**
|
|
118
141
|
* Ranked search over the host RetrievalEngine (v0.4.2) — the same ranking
|
|
119
142
|
* memoir_read uses, so the GUI search and the agent recall never diverge.
|
|
@@ -254,7 +277,28 @@ var dictionaries = {
|
|
|
254
277
|
"lifecycle.supersede": "\u6807\u8BB0\u5DF2\u66FF\u4EE3",
|
|
255
278
|
"lifecycle.unsupersede": "\u6062\u590D\u6D3B\u8DC3",
|
|
256
279
|
"lifecycle.archive": "\u5F52\u6863",
|
|
257
|
-
"lifecycle.restore": "\u6062\u590D"
|
|
280
|
+
"lifecycle.restore": "\u6062\u590D",
|
|
281
|
+
"settings.title": "\u81EA\u52A8\u84B8\u998F\u8BBE\u7F6E",
|
|
282
|
+
"settings.description": "\u4FEE\u6539\u540E\u7ACB\u5373\u4F5C\u7528\u4E8E\u540E\u7EED\u56DE\u5408\uFF0C\u5E76\u6301\u4E45\u5316\u5230\u672C\u673A DSH \u7528\u6237\u76EE\u5F55\u3002",
|
|
283
|
+
"settings.enabled": "\u542F\u7528\u81EA\u52A8\u84B8\u998F",
|
|
284
|
+
"settings.enabledHint": "\u5728\u9876\u5C42 agent \u7684\u6709\u6548\u5DE5\u4F5C\u56DE\u5408\u7ED3\u675F\u65F6\u63D0\u9192\u5F52\u7EB3\u8BB0\u5FC6\u3002",
|
|
285
|
+
"settings.every": "\u5DE5\u4F5C\u56DE\u5408\u95F4\u9694",
|
|
286
|
+
"settings.everyHint": "\u6BCF\u7D2F\u8BA1 N \u4E2A worked turn \u6700\u591A\u63D0\u9192\u4E00\u6B21\uFF08\u6700\u5C0F 1\uFF09\u3002",
|
|
287
|
+
"settings.cooldown": "\u51B7\u5374\u65F6\u95F4\uFF08\u5206\u949F\uFF09",
|
|
288
|
+
"settings.cooldownHint": "\u4E24\u6B21\u6210\u529F\u63D0\u9192\u81F3\u5C11\u95F4\u9694 M \u5206\u949F\uFF08\u6700\u5C0F 0\uFF09\u3002",
|
|
289
|
+
"settings.minTools": "\u6700\u4F4E\u5DE5\u5177\u8C03\u7528\u6570",
|
|
290
|
+
"settings.minToolsHint": "\u89E6\u53D1\u56DE\u5408\u81F3\u5C11\u5305\u542B K \u6B21\u5DE5\u5177\u8C03\u7528\uFF08\u6700\u5C0F 1\uFF09\u3002",
|
|
291
|
+
"settings.andHint": "\u4E09\u4E2A\u9891\u7387\u6761\u4EF6\u6309 AND \u5224\u5B9A\uFF1Bidle\u3001aborted\u3001subagent \u548C\u5DF2\u8BB0\u5F55\u8BB0\u5FC6\u7684\u56DE\u5408\u4E0D\u4F1A\u89E6\u53D1\u3002",
|
|
292
|
+
"settings.source": "\u5F53\u524D\u6765\u6E90",
|
|
293
|
+
"settings.source.profile": "\u542F\u52A8\u914D\u7F6E",
|
|
294
|
+
"settings.source.web": "Web \u6301\u4E45\u5316\u8986\u76D6",
|
|
295
|
+
"settings.save": "\u4FDD\u5B58\u5E76\u7ACB\u5373\u5E94\u7528",
|
|
296
|
+
"settings.reset": "\u6062\u590D\u542F\u52A8\u914D\u7F6E",
|
|
297
|
+
"settings.saved": "\u5DF2\u4FDD\u5B58\uFF0C\u540E\u7EED\u56DE\u5408\u7ACB\u5373\u751F\u6548\u3002",
|
|
298
|
+
"settings.resetDone": "\u5DF2\u6062\u590D\u672C\u6B21\u542F\u52A8\u65F6\u7684\u914D\u7F6E\u3002",
|
|
299
|
+
"settings.invalid": "\u8BF7\u8F93\u5165\u6709\u6548\u6570\u503C\uFF1A\u95F4\u9694\u548C\u5DE5\u5177\u6570\u4E3A\u4E0D\u5C0F\u4E8E 1 \u7684\u6574\u6570\uFF0C\u51B7\u5374\u65F6\u95F4\u4E0D\u5C0F\u4E8E 0\u3002",
|
|
300
|
+
"settings.loadFailed": "\u8BBE\u7F6E\u52A0\u8F7D\u5931\u8D25",
|
|
301
|
+
"settings.saveFailed": "\u8BBE\u7F6E\u4FDD\u5B58\u5931\u8D25"
|
|
258
302
|
},
|
|
259
303
|
en: {
|
|
260
304
|
"entry.label": "Memory",
|
|
@@ -316,7 +360,28 @@ var dictionaries = {
|
|
|
316
360
|
"lifecycle.supersede": "Mark superseded",
|
|
317
361
|
"lifecycle.unsupersede": "Restore active",
|
|
318
362
|
"lifecycle.archive": "Archive",
|
|
319
|
-
"lifecycle.restore": "Restore"
|
|
363
|
+
"lifecycle.restore": "Restore",
|
|
364
|
+
"settings.title": "Auto-distill settings",
|
|
365
|
+
"settings.description": "Changes apply to subsequent turns immediately and persist in the local DSH user directory.",
|
|
366
|
+
"settings.enabled": "Enable auto-distill",
|
|
367
|
+
"settings.enabledHint": "Prompt top-level agents to distill memory after eligible worked turns.",
|
|
368
|
+
"settings.every": "Worked-turn interval",
|
|
369
|
+
"settings.everyHint": "Remind at most once per N accumulated worked turns (minimum 1).",
|
|
370
|
+
"settings.cooldown": "Cooldown (minutes)",
|
|
371
|
+
"settings.cooldownHint": "Require at least M minutes between successful reminders (minimum 0).",
|
|
372
|
+
"settings.minTools": "Minimum tool calls",
|
|
373
|
+
"settings.minToolsHint": "The triggering turn must contain at least K tool calls (minimum 1).",
|
|
374
|
+
"settings.andHint": "All three frequency conditions use AND semantics; idle, aborted, subagent, and already-recorded turns do not trigger.",
|
|
375
|
+
"settings.source": "Current source",
|
|
376
|
+
"settings.source.profile": "startup profile config",
|
|
377
|
+
"settings.source.web": "persistent Web override",
|
|
378
|
+
"settings.save": "Save and apply now",
|
|
379
|
+
"settings.reset": "Restore startup config",
|
|
380
|
+
"settings.saved": "Saved; subsequent turns use the new policy immediately.",
|
|
381
|
+
"settings.resetDone": "Restored the configuration captured at startup.",
|
|
382
|
+
"settings.invalid": "Enter valid values: interval and tool count are integers of at least 1; cooldown is at least 0.",
|
|
383
|
+
"settings.loadFailed": "Failed to load settings",
|
|
384
|
+
"settings.saveFailed": "Failed to save settings"
|
|
320
385
|
}
|
|
321
386
|
};
|
|
322
387
|
var SECTION_KEYS = ["work", "lessons", "actions", "note"];
|
|
@@ -385,6 +450,7 @@ html[data-dsh-memoir-active] [class*="centerCol"] > *:not([data-dsh-memoir-view]
|
|
|
385
450
|
border-radius: 6px; padding: 6px 12px; cursor: pointer; font-size: 13px;
|
|
386
451
|
}
|
|
387
452
|
.memoir-primary:hover { filter: brightness(1.05); }
|
|
453
|
+
.memoir-primary:disabled, .memoir-iconbtn:disabled { cursor: not-allowed; opacity: .45; }
|
|
388
454
|
|
|
389
455
|
.memoir-body { flex: 1; overflow-y: auto; padding: 4px 14px 16px; }
|
|
390
456
|
.memoir-section-title {
|
|
@@ -434,6 +500,27 @@ html[data-dsh-memoir-active] [class*="centerCol"] > *:not([data-dsh-memoir-view]
|
|
|
434
500
|
.memoir-field textarea { min-height: 72px; resize: vertical; }
|
|
435
501
|
.memoir-form-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
436
502
|
|
|
503
|
+
/* v0.5.3 live auto-distill settings */
|
|
504
|
+
.memoir-settings { margin: 0 14px; border-top: 1px solid var(--border, rgba(0,0,0,.1)); padding-top: 8px; }
|
|
505
|
+
.memoir-settings-body {
|
|
506
|
+
display: flex; flex-direction: column; gap: 10px; margin-top: 7px; padding: 10px;
|
|
507
|
+
border: 1px solid var(--border, rgba(0,0,0,.1)); border-radius: 8px;
|
|
508
|
+
background: var(--bg-card, rgba(0,0,0,.02)); max-height: min(50vh, 430px); overflow-y: auto;
|
|
509
|
+
}
|
|
510
|
+
.memoir-settings-description, .memoir-settings-note, .memoir-settings-source { font-size: 11px; opacity: .72; line-height: 1.45; }
|
|
511
|
+
.memoir-settings-switch { display: flex; align-items: flex-start; gap: 8px; cursor: pointer; }
|
|
512
|
+
.memoir-settings-switch input { margin-top: 3px; }
|
|
513
|
+
.memoir-settings-switch span { display: flex; flex-direction: column; gap: 2px; }
|
|
514
|
+
.memoir-settings-switch small, .memoir-settings-grid small { font-size: 10px; opacity: .65; line-height: 1.35; }
|
|
515
|
+
.memoir-settings-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
|
|
516
|
+
.memoir-settings-grid .memoir-field { min-width: 0; }
|
|
517
|
+
.memoir-settings-grid .memoir-field > span { font-size: 11px; opacity: .8; }
|
|
518
|
+
.memoir-settings-feedback { padding: 0; }
|
|
519
|
+
.memoir-settings-success { color: var(--success, #15803d); font-size: 12px; }
|
|
520
|
+
@media (max-width: 760px) {
|
|
521
|
+
.memoir-settings-grid { grid-template-columns: 1fr; }
|
|
522
|
+
}
|
|
523
|
+
|
|
437
524
|
/* Sidebar entry row (plain DOM, matches the shell's nav rows). */
|
|
438
525
|
.memoir-entry-row {
|
|
439
526
|
display: flex; align-items: center; gap: 8px; width: 100%;
|
|
@@ -723,6 +810,123 @@ function AddForm({ t, onSubmit, onCancel }) {
|
|
|
723
810
|
] })
|
|
724
811
|
] });
|
|
725
812
|
}
|
|
813
|
+
function AutoDistillSettingsPanel({ api, t, refreshKey, onChanged }) {
|
|
814
|
+
const [open, setOpen] = (0, import_react.useState)(false);
|
|
815
|
+
const [settings, setSettings] = (0, import_react.useState)(null);
|
|
816
|
+
const [source, setSource] = (0, import_react.useState)("profile");
|
|
817
|
+
const [every, setEvery] = (0, import_react.useState)("1");
|
|
818
|
+
const [cooldown, setCooldown] = (0, import_react.useState)("0");
|
|
819
|
+
const [minTools, setMinTools] = (0, import_react.useState)("1");
|
|
820
|
+
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
821
|
+
const [message, setMessage] = (0, import_react.useState)(null);
|
|
822
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
823
|
+
const applySnapshot = (snapshot) => {
|
|
824
|
+
setError(null);
|
|
825
|
+
setSettings(snapshot.settings);
|
|
826
|
+
setSource(snapshot.source);
|
|
827
|
+
setEvery(String(snapshot.settings.autoDistillEvery));
|
|
828
|
+
setCooldown(String(snapshot.settings.autoDistillCooldownMin));
|
|
829
|
+
setMinTools(String(snapshot.settings.autoDistillMinTools));
|
|
830
|
+
};
|
|
831
|
+
(0, import_react.useEffect)(() => {
|
|
832
|
+
let cancelled = false;
|
|
833
|
+
api.settings().then((value) => {
|
|
834
|
+
if (!cancelled) applySnapshot(value);
|
|
835
|
+
}).catch((e) => {
|
|
836
|
+
if (!cancelled) setError(`${t("settings.loadFailed")}: ${e.message}`);
|
|
837
|
+
});
|
|
838
|
+
return () => {
|
|
839
|
+
cancelled = true;
|
|
840
|
+
};
|
|
841
|
+
}, [api, refreshKey]);
|
|
842
|
+
const save = () => {
|
|
843
|
+
if (settings === null) return;
|
|
844
|
+
const parsedEvery = Number(every);
|
|
845
|
+
const parsedCooldown = Number(cooldown);
|
|
846
|
+
const parsedMinTools = Number(minTools);
|
|
847
|
+
if (!Number.isSafeInteger(parsedEvery) || parsedEvery < 1 || !Number.isFinite(parsedCooldown) || parsedCooldown < 0 || !Number.isSafeInteger(parsedMinTools) || parsedMinTools < 1) {
|
|
848
|
+
setMessage(null);
|
|
849
|
+
setError(t("settings.invalid"));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
setBusy(true);
|
|
853
|
+
setError(null);
|
|
854
|
+
setMessage(null);
|
|
855
|
+
api.updateSettings({
|
|
856
|
+
autoDistill: settings.autoDistill,
|
|
857
|
+
autoDistillEvery: parsedEvery,
|
|
858
|
+
autoDistillCooldownMin: parsedCooldown,
|
|
859
|
+
autoDistillMinTools: parsedMinTools
|
|
860
|
+
}).then((value) => {
|
|
861
|
+
applySnapshot(value);
|
|
862
|
+
setMessage(t("settings.saved"));
|
|
863
|
+
onChanged();
|
|
864
|
+
}).catch((e) => setError(`${t("settings.saveFailed")}: ${e.message}`)).finally(() => setBusy(false));
|
|
865
|
+
};
|
|
866
|
+
const reset = () => {
|
|
867
|
+
setBusy(true);
|
|
868
|
+
setError(null);
|
|
869
|
+
setMessage(null);
|
|
870
|
+
api.resetSettings().then((value) => {
|
|
871
|
+
applySnapshot(value);
|
|
872
|
+
setMessage(t("settings.resetDone"));
|
|
873
|
+
onChanged();
|
|
874
|
+
}).catch((e) => setError(`${t("settings.saveFailed")}: ${e.message}`)).finally(() => setBusy(false));
|
|
875
|
+
};
|
|
876
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "memoir-settings", children: [
|
|
877
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", className: "memoir-diagnostics-toggle", onClick: () => setOpen((value) => !value), children: [
|
|
878
|
+
t("settings.title"),
|
|
879
|
+
" ",
|
|
880
|
+
open ? "\u25BE" : "\u25B8"
|
|
881
|
+
] }),
|
|
882
|
+
open ? settings === null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "memoir-settings-body", children: error ?? "\u2026" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "memoir-settings-body", children: [
|
|
883
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "memoir-settings-description", children: t("settings.description") }),
|
|
884
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "memoir-settings-switch", children: [
|
|
885
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
886
|
+
"input",
|
|
887
|
+
{
|
|
888
|
+
type: "checkbox",
|
|
889
|
+
checked: settings.autoDistill,
|
|
890
|
+
onChange: (event) => setSettings({ ...settings, autoDistill: event.target.checked })
|
|
891
|
+
}
|
|
892
|
+
),
|
|
893
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
|
|
894
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("settings.enabled") }),
|
|
895
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: t("settings.enabledHint") })
|
|
896
|
+
] })
|
|
897
|
+
] }),
|
|
898
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "memoir-settings-grid", children: [
|
|
899
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "memoir-field", children: [
|
|
900
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("settings.every") }),
|
|
901
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "number", min: "1", step: "1", value: every, onChange: (event) => setEvery(event.target.value) }),
|
|
902
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: t("settings.everyHint") })
|
|
903
|
+
] }),
|
|
904
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "memoir-field", children: [
|
|
905
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("settings.cooldown") }),
|
|
906
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "number", min: "0", step: "0.1", value: cooldown, onChange: (event) => setCooldown(event.target.value) }),
|
|
907
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: t("settings.cooldownHint") })
|
|
908
|
+
] }),
|
|
909
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "memoir-field", children: [
|
|
910
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("settings.minTools") }),
|
|
911
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "number", min: "1", step: "1", value: minTools, onChange: (event) => setMinTools(event.target.value) }),
|
|
912
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: t("settings.minToolsHint") })
|
|
913
|
+
] })
|
|
914
|
+
] }),
|
|
915
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "memoir-settings-note", children: t("settings.andHint") }),
|
|
916
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "memoir-settings-source", children: [
|
|
917
|
+
t("settings.source"),
|
|
918
|
+
": ",
|
|
919
|
+
t(`settings.source.${source}`)
|
|
920
|
+
] }),
|
|
921
|
+
error !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "memoir-error memoir-settings-feedback", children: error }) : null,
|
|
922
|
+
message !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "memoir-settings-success", children: message }) : null,
|
|
923
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "memoir-form-actions", children: [
|
|
924
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "memoir-iconbtn", disabled: busy || source === "profile", onClick: reset, children: t("settings.reset") }),
|
|
925
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "memoir-primary", disabled: busy, onClick: save, children: t("settings.save") })
|
|
926
|
+
] })
|
|
927
|
+
] }) : null
|
|
928
|
+
] });
|
|
929
|
+
}
|
|
726
930
|
function MemoirPanel({ controller, api, cwdTracker, t }) {
|
|
727
931
|
const cwd = (0, import_react.useSyncExternalStore)(cwdTracker.subscribe, cwdTracker.getSnapshot);
|
|
728
932
|
const [tab, setTab] = (0, import_react.useState)("project");
|
|
@@ -890,6 +1094,7 @@ function MemoirPanel({ controller, api, cwdTracker, t }) {
|
|
|
890
1094
|
] }, p.key);
|
|
891
1095
|
}) }),
|
|
892
1096
|
busy ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "memoir-empty", children: "\u2026" }) : null,
|
|
1097
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AutoDistillSettingsPanel, { api, t, refreshKey, onChanged: reload }),
|
|
893
1098
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "memoir-inspector", children: [
|
|
894
1099
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", className: "memoir-diagnostics-toggle", onClick: () => setInspectorOpen((v) => !v), children: [
|
|
895
1100
|
t("inspector.title"),
|