dsh-auto-compaction 0.2.1 → 0.2.2

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.
Files changed (3) hide show
  1. package/lib/client.js +150 -3
  2. package/lib/index.js +42 -12
  3. package/package.json +2 -2
package/lib/client.js CHANGED
@@ -43,6 +43,10 @@ window.__ModuleLoader__.load({
43
43
  { id: "topbar", label: "顶部条" },
44
44
  { id: "corner", label: "右上角" },
45
45
  ];
46
+ function presetLabel(id) {
47
+ for (var i = 0; i < PRESETS.length; i += 1) if (PRESETS[i].id === id) return PRESETS[i].label;
48
+ return String(id || "未命名预设");
49
+ }
46
50
  var EXAMPLE_WINDOW = 250000, EXAMPLE_RESERVED = 20000, EXAMPLE_HEADROOM = 10000;
47
51
 
48
52
  var DISPLAY_STYLES = ["orbit", "strip", "ring", "topbar", "corner"];
@@ -480,7 +484,7 @@ window.__ModuleLoader__.load({
480
484
  indicator,
481
485
  h("div", { style: { fontSize: 12.5, fontWeight: 600, color: STATUS_TEXT } }, title),
482
486
  h("div", { style: { fontSize: 11, color: STATUS_MUTED } }, cardSub),
483
- h("div", { style: { fontSize: 10.5, color: STATUS_MUTED, opacity: 0.85 } }, "完成后整段历史收成一条交接文档")
487
+ h("div", { style: { fontSize: 10.5, color: STATUS_MUTED, opacity: 0.85 } }, "完成后把最旧的这段历史收成一条交接文档")
484
488
  )
485
489
  ),
486
490
  document.body
@@ -687,8 +691,29 @@ window.__ModuleLoader__.load({
687
691
  var draft = draftState[0], setDraft = draftState[1];
688
692
  var statusState = React.useState({ loading: true, error: "", saving: false, saved: false });
689
693
  var status = statusState[0], setStatus = statusState[1];
694
+ var scopeState = React.useState({ loading: true, error: "", value: null });
695
+ var scope = scopeState[0], setScope = scopeState[1];
696
+ var tokenState = React.useState(0);
697
+ var token = tokenState[0], setToken = tokenState[1];
690
698
  ensureStatusStyles();
691
699
 
700
+ // The compaction slot lives in the preset YAML, not in our settings file,
701
+ // so the truth is read from disk each time the section opens.
702
+ React.useEffect(function () {
703
+ if (!connection) {
704
+ setScope({ loading: false, error: "连接服务不可用", value: null });
705
+ return undefined;
706
+ }
707
+ var alive = true;
708
+ setScope(function (previous) { return { loading: true, error: "", value: previous.value }; });
709
+ call(connection, "scope").then(function (value) {
710
+ if (alive) setScope({ loading: false, error: "", value: value });
711
+ }).catch(function (error) {
712
+ if (alive) setScope({ loading: false, error: String(error && error.message || error), value: null });
713
+ });
714
+ return function () { alive = false; };
715
+ }, [connection, token]);
716
+
692
717
  React.useEffect(function () {
693
718
  var alive = true;
694
719
  if (!connection) { setStatus({ loading: false, error: "连接服务不可用", saving: false, saved: false }); return; }
@@ -715,6 +740,80 @@ window.__ModuleLoader__.load({
715
740
  var defaultTemplate = base.defaultTemplate || DEFAULT_TEMPLATE;
716
741
  var themeStyle = normalizeDisplayStyle(base.displayStyle);
717
742
 
743
+ // Detection only: who compacts is decided by the preset slot (read from
744
+ // disk) times the 启用插件 switch. This row never changes either of them.
745
+ var SCOPE_TONE = {
746
+ native: { dot: "#6b7178", text: C.muted },
747
+ plugin: { dot: C.accent, text: "#cfe0ff" },
748
+ part: { dot: "#d29922", text: "#e6c98a" },
749
+ err: { dot: "#e5534b", text: "#f0b8b4" },
750
+ };
751
+ var scopeView = (function () {
752
+ var st = scope.value;
753
+ if (!st) {
754
+ return {
755
+ tone: "err",
756
+ text: scope.loading ? "读取中…" : "读不到生效情况",
757
+ sub: scope.loading
758
+ ? "正在读取各 Agent 预设里的 compaction 槽位。"
759
+ : "读不到预设里的 compaction 槽位:" + (scope.error || "未知原因") + "。",
760
+ rows: [], retry: true,
761
+ };
762
+ }
763
+ var list = Array.isArray(st.presets) ? st.presets : [];
764
+ var rows = list.map(function (p) {
765
+ return {
766
+ key: String(p.id), label: presetLabel(p.id), taken: p.owner === "plugin",
767
+ owner: p.owner === "plugin" ? "插件" : p.owner === "native" ? "原生" : "未知",
768
+ };
769
+ });
770
+ var names = rows.map(function (r) { return r.label; });
771
+ var takenNames = rows.filter(function (r) { return r.taken; }).map(function (r) { return r.label; });
772
+ var restNames = rows.filter(function (r) { return !r.taken; }).map(function (r) { return r.label; });
773
+ var taken = Number(st.taken) || 0;
774
+ var total = Number(st.total) || list.length;
775
+ var globalSaved = st.scope !== "preset";
776
+ var scopeLead = globalSaved
777
+ ? "全局生效:"
778
+ : "按预设生效:";
779
+ if (total === 0) {
780
+ return {
781
+ tone: "err", text: "读不到预设", rows: [], retry: true,
782
+ sub: "没找到带 handoff 槽位的可编辑预设文件,所以无法判断压缩由谁负责。确认 DSH 已生成预设后重新读取。",
783
+ };
784
+ }
785
+ if (taken === 0) {
786
+ return {
787
+ tone: "native", text: "DSH 原生压缩", rows: rows, retry: false,
788
+ sub: "各预设的 compaction 槽位都还在原生实现手里,所以压缩目前由 DSH 自己完成。保存本页设置会自动接管 " + names.join("、") + "(写入预设文件并留备份,重启 DSH 后生效)。",
789
+ };
790
+ }
791
+ if (!enabled) {
792
+ return {
793
+ tone: "native", text: "DSH 原生压缩(插件已停用)", rows: rows, retry: false,
794
+ sub: taken < total
795
+ ? "「启用插件」已关闭,压缩由 DSH 原生实现负责;" + takenNames.join("、") + " 的槽位仍指向插件," + restNames.join("、") + " 本来就走原生。"
796
+ : "「启用插件」已关闭:槽位虽然指向插件,但插件把压缩交回 DSH 原生实现。打开开关即恢复,不需要重启。",
797
+ };
798
+ }
799
+ if (taken < total) {
800
+ return {
801
+ tone: "part", text: taken + "/" + total + " 预设已接管", rows: rows, retry: false,
802
+ sub: scopeLead + "已交给插件:" + takenNames.join("、") + ";" + restNames.join("、") + " 仍走各自的原生压缩。",
803
+ };
804
+ }
805
+ return {
806
+ tone: "plugin",
807
+ text: globalSaved ? "已由插件接管(全局)" : "已由插件接管",
808
+ rows: rows, retry: false,
809
+ sub: scopeLead + (globalSaved
810
+ ? "这 " + total + " 个带 handoff 槽位的可编辑预设(" + names.join("、") + ")"
811
+ : "勾选的 " + taken + " 个预设(" + takenNames.join("、") + ")")
812
+ + "的 compaction 槽位指向插件,且「启用插件」是开的,压缩由本插件的交接文档负责。",
813
+ };
814
+ })();
815
+ var scopeTone = SCOPE_TONE[scopeView.tone] || SCOPE_TONE.native;
816
+
718
817
  var budget = EXAMPLE_WINDOW - EXAMPLE_RESERVED;
719
818
  var ceiling = budget - EXAMPLE_HEADROOM;
720
819
  var eff = Math.min(Math.round(budget * ratio / 100), ceiling);
@@ -726,6 +825,20 @@ window.__ModuleLoader__.load({
726
825
  (settings.templateFile || "") !== template ||
727
826
  normalizeDisplayStyle(settings.displayStyle) !== themeStyle;
728
827
 
828
+ // The detection row reports what is on disk, so every unsaved choice that
829
+ // would change that report has to be named — otherwise it looks ignored.
830
+ var enabledIsPending = settings !== null && (settings.enabled !== false) !== enabled;
831
+ var scopeIsPending = settings !== null && (settings.scope === "preset") !== scopePreset;
832
+ var presetsIsPending = settings !== null && scopePreset &&
833
+ JSON.stringify(presets.slice().sort()) !== JSON.stringify((settings.presets || []).slice().sort());
834
+ var pendingParts = [];
835
+ if (enabledIsPending) pendingParts.push("「启用插件」改成" + (enabled ? "开" : "关"));
836
+ if (scopeIsPending) pendingParts.push("生效范围改成「" + (scopePreset ? "按预设生效" : "全局生效") + "」");
837
+ if (presetsIsPending) pendingParts.push("勾选的预设改成" + (presets.length ? presets.map(presetLabel).join("、") : "(都不勾)"));
838
+ var pendingNote = pendingParts.length
839
+ ? "还没保存:" + pendingParts.join(";") + "。保存后开关立刻生效,槽位改动要重启 DSH 才加载。"
840
+ : "";
841
+
729
842
  function patch(next) { setDraft(Object.assign({}, base, next)); setStatus({ loading: false, error: "", saving: false, saved: false }); }
730
843
  function setPreset(id) {
731
844
  var next = presets.indexOf(id) >= 0 ? presets.filter(function (v) { return v !== id; }) : presets.concat([id]);
@@ -744,10 +857,12 @@ window.__ModuleLoader__.load({
744
857
  setSettings(value); setDraft(value);
745
858
  publishDisplayStyle(value && value.displayStyle);
746
859
  setStatus({ loading: false, error: "", saving: false, saved: true });
860
+ setToken(token + 1);
747
861
  }).catch(function (error) {
748
862
  setStatus({ loading: false, error: String(error && error.message || error), saving: false, saved: false });
749
863
  });
750
864
  }
865
+
751
866
  function discard() { setDraft(settings); setStatus({ loading: false, error: "", saving: false, saved: false }); }
752
867
  function open(kind) {
753
868
  call(connection, "open", { kind: kind, path: template }).then(function () {
@@ -808,6 +923,38 @@ window.__ModuleLoader__.load({
808
923
  })
809
924
  ) : null,
810
925
 
926
+ h("div", { style: rowStyle },
927
+ h("div", { style: { minWidth: 0 } },
928
+ h("div", { style: labelStyle }, "当前生效情况"),
929
+ h("div", { style: subStyle }, scopeView.sub),
930
+ scopeView.rows.length
931
+ ? h("div", { style: { display: "flex", gap: "6px 16px", flexWrap: "wrap", marginTop: 7 } },
932
+ scopeView.rows.map(function (r) {
933
+ return h("span", { key: r.key, style: { display: "inline-flex", alignItems: "center", gap: 6, fontSize: 12, color: C.muted } },
934
+ h("i", { "aria-hidden": true, style: { width: 6, height: 6, borderRadius: "50%", background: r.taken ? C.accent : "#6b7178" } }),
935
+ r.label,
936
+ h("em", { style: { fontStyle: "normal", color: C.faint } }, r.owner));
937
+ })
938
+ )
939
+ : null,
940
+ pendingNote ? h("div", { style: { fontSize: 11.5, color: "#d29922", marginTop: 6 } }, pendingNote) : null,
941
+ scope.error && scope.value ? h("div", { style: { fontSize: 11.5, color: "#e5534b", marginTop: 6 } }, "读取失败:" + scope.error) : null
942
+ ),
943
+ h("div", { style: { display: "inline-flex", alignItems: "center", gap: 12, flex: "0 0 auto" } },
944
+ h("span", { "aria-live": "polite", style: { display: "inline-flex", alignItems: "center", gap: 7, fontSize: 12.5, color: scopeTone.text } },
945
+ h("i", { "aria-hidden": true, style: { width: 7, height: 7, borderRadius: "50%", background: scopeTone.dot } }),
946
+ scopeView.text
947
+ ),
948
+ scopeView.retry && !scope.loading
949
+ ? h("button", {
950
+ type: "button",
951
+ onClick: function () { setToken(token + 1); },
952
+ style: Object.assign({}, btnBase, { fontSize: 11.5, padding: "4px 9px" }),
953
+ }, "重新读取")
954
+ : null
955
+ )
956
+ ),
957
+
811
958
  h("div", { style: { padding: "12px 0", borderBottom: "1px solid " + C.line } },
812
959
  h("div", null,
813
960
  h("div", { style: labelStyle }, "压缩显示方式", h("span", { style: newBadgeStyle }, "新")),
@@ -881,8 +1028,8 @@ window.__ModuleLoader__.load({
881
1028
  h("ol", { style: { margin: 0, paddingLeft: 18, color: C.muted, fontSize: 12.5, lineHeight: 1.85 } },
882
1029
  h("li", null, "到点后不立即动手,等 agent 结束当前回合。"),
883
1030
  h("li", null, "用会话自己的前缀(命中缓存)让模型按模板写交接文档,不额外全量 prefill。"),
884
- h("li", null, "把整段历史替换成这一条消息;旧消息仍在会话日志里,rewind 可回滚。"),
885
- h("li", null, "下一轮只剩 系统提示词 + 工具定义 + 交接文档。")
1031
+ h("li", null, "只替换最旧的这一段历史,换成一条交接文档;最近约 16% 窗口的原文逐字留着,切点也不会把工具调用和它的结果拆开。"),
1032
+ h("li", null, "下一轮 = 系统提示词 + 工具定义 + 这条交接文档 + 那段逐字保留的最近消息;旧消息仍在会话日志里,rewind 可回滚。")
886
1033
  ),
887
1034
  h("div", { style: { marginTop: 8, fontSize: 11.5, color: C.faint } }, "手动压缩:会话里发送 /compact 可随时折叠一次,不受这个比例限制。")
888
1035
  ),
package/lib/index.js CHANGED
@@ -143,9 +143,9 @@ function scopeScriptPath() {
143
143
  }
144
144
  }
145
145
 
146
- function execSwitchOnce(command, env, script) {
146
+ function execSwitchOnce(command, env, script, action) {
147
147
  return new Promise((resolve, reject) => {
148
- execFile(command, [script, "enable"], { timeout: 6000, windowsHide: true, env }, (error, stdout, stderr) => {
148
+ execFile(command, [script, action], { timeout: 6000, windowsHide: true, env }, (error, stdout, stderr) => {
149
149
  if (error) {
150
150
  error.stdout = stdout;
151
151
  error.stderr = stderr;
@@ -158,15 +158,14 @@ function execSwitchOnce(command, env, script) {
158
158
  }
159
159
 
160
160
  /**
161
- * Apply the current settings sidecar to the preset/profile YAML. `node` is the
162
- * normal path; the Electron binary with ELECTRON_RUN_AS_NODE is the fallback
163
- * when the host has no node on PATH.
161
+ * Run one preset-scope action. `node` is the normal path; the Electron binary
162
+ * with ELECTRON_RUN_AS_NODE is the fallback when the host has no node on PATH.
164
163
  */
165
- async function applyScopeSwitch() {
164
+ async function runScope(action) {
166
165
  const script = scopeScriptPath();
167
166
  if (!existsSync(script)) {
168
167
  const message = "preset-scope.mjs not found: " + script;
169
- trace("scope switch failed: " + message);
168
+ trace("scope " + action + " failed: " + message);
170
169
  return { ok: false, message };
171
170
  }
172
171
  const explicit = process.env.DSH_NODE;
@@ -179,10 +178,11 @@ async function applyScopeSwitch() {
179
178
  let lastError;
180
179
  for (const attempt of attempts) {
181
180
  try {
182
- const result = await execSwitchOnce(attempt.command, attempt.env, script);
183
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
184
- trace("scope switch ok" + (output ? " | " + output : ""));
185
- return { ok: true, output };
181
+ const result = await execSwitchOnce(attempt.command, attempt.env, script, action);
182
+ const output = `${result.stdout ?? ""}`.trim();
183
+ const noise = `${result.stderr ?? ""}`.trim();
184
+ trace("scope " + action + " ok" + (output ? " | " + output : "") + (noise ? " | " + noise : ""));
185
+ return { ok: true, output, noise };
186
186
  } catch (error) {
187
187
  lastError = error;
188
188
  if (error?.code !== "ENOENT") break;
@@ -190,10 +190,34 @@ async function applyScopeSwitch() {
190
190
  }
191
191
  const output = `${lastError?.stdout ?? ""}${lastError?.stderr ?? ""}`.trim();
192
192
  const message = (lastError?.message ?? String(lastError)) + (output ? "\n" + output : "");
193
- trace("scope switch failed: " + message);
193
+ trace("scope " + action + " failed: " + message);
194
194
  return { ok: false, message };
195
195
  }
196
196
 
197
+ /** Who actually compacts right now, straight from the preset YAML on disk. */
198
+ async function readScopeState() {
199
+ const result = await runScope("state");
200
+ if (!result.ok) return { ok: false, message: result.message };
201
+ const line = String(result.output ?? "").split(/\r?\n/).filter(Boolean).pop() ?? "";
202
+ try {
203
+ return { ok: true, value: JSON.parse(line) };
204
+ } catch (error) {
205
+ const message = "unreadable scope state: " + (error instanceof Error ? error.message : String(error));
206
+ trace("scope state failed: " + message + " | " + line.slice(0, 200));
207
+ return { ok: false, message };
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Apply the current settings sidecar to the preset/profile YAML.
213
+ */
214
+ async function applyScopeSwitch() {
215
+ const result = await runScope("enable");
216
+ return result.ok
217
+ ? { ok: true, output: result.output }
218
+ : { ok: false, message: result.message };
219
+ }
220
+
197
221
  const CHANNEL = "/dsh-auto-compaction";
198
222
 
199
223
  function readSettings() {
@@ -265,6 +289,12 @@ export function apply(ctx, config = {}) {
265
289
  }
266
290
  return { ok: true, value: { ...next, switchApplied: true, defaultTemplate: TEMPLATE_FILE } };
267
291
  }
292
+ if (endpoint === "scope") {
293
+ const state = await readScopeState();
294
+ return state.ok
295
+ ? { ok: true, value: state.value }
296
+ : { ok: false, error: { code: "auto-compaction/scope-unknown", message: state.message } };
297
+ }
268
298
  if (endpoint === "open") {
269
299
  trace("open " + JSON.stringify(payload));
270
300
  return await handleOpen(ctx, String(payload?.kind ?? "file") + ":" + Date.now(), String(payload?.path ?? "") || TEMPLATE_FILE);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-auto-compaction",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "exports": {
@@ -51,6 +51,6 @@
51
51
  "access": "public"
52
52
  },
53
53
  "dependencies": {
54
- "dsh-auto-compaction-engine": "^0.2.1"
54
+ "dsh-auto-compaction-engine": "^0.2.2"
55
55
  }
56
56
  }