Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 35 additions & 15 deletions dsh-mneme/lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -14505,27 +14505,23 @@ window.__ModuleLoader__.load({
name: "main",
key: "dsh-mneme-wiki"
}, () => h(WikiMainPanel, { t }));
// 知识库第三个座位(契约 §3.6)。和上面两个同理:id 与 key 逐字相同,
// 否则宿主 selectPanel(id) 在 main 槽里找不到 entry 直接抛。
// 座位无条件注册,开关只管内容层——这样「未启用」能自己解释去哪儿开,
// 而不是让入口凭空消失(#607 的另一种写法)。
const disposeKbGlyph = ctx.slots.register({
name: "sidebar.panellist",
id: "dsh-mneme-kb",
order: 62,
label: () => t("kb.panel.open")
}, (props) => h(KbGlyph, { size: props && props.size }));
// 知识库第三个座位(契约 §3.6)。id 与 key 逐字相同,否则宿主
// selectPanel(id) 在 main 槽里找不到 entry 直接抛。
// 侧栏那一格不再无条件(#685,反转 #598 评论区 §四的另一半):开关关着
// 时不该常驻一个点进去只有「未启用」的入口。glyph 的登记挪进下面的
// kb 座位闸,读 features 按 kbEnabled 决定;main 仍无条件——#562 那堵墙
// (布局里存着 activePanelId="dsh-mneme-kb" 时少注册 main,宿主
// selectPanel 直接抛),与 wiki 座位同一处理。
const disposeKbMain = ctx.slots.register({
name: "main",
key: "dsh-mneme-kb"
}, () => h(KbMainPanel, { t }));
return () => {
disposeGlyph();
disposeMain();
// wiki glyph 不在这把扫帚里了:它的登记随座位闸走(#598),
// 生命周期归 cordis 上下文,与本回调的成对座位不同源。
// wiki / kb 的 glyph 都不在这把扫帚里了:它们的登记随各自座位闸走
// (#598 / #685),生命周期归 cordis 上下文,与本回调的成对座位不同源。
disposeWikiMain();
disposeKbGlyph();
disposeKbMain();
// 座位被撤(宿主折叠该槽 / 换宿主形态)时把回落装回去:两条路径都
// 只靠各自的 inject 触发,谁不动谁就永远停在「没有入口」的状态。
Expand Down Expand Up @@ -14633,8 +14629,7 @@ window.__ModuleLoader__.load({
// 同一形态——features 拉一次、fail-closed(读不到就不注册,不猜)、开关
// 改动重启生效。glyph 延迟注册走 diary 右栏页签同一条 inject→register
// 路径:宿主每次渲染枚举 entriesOfSlot,晚到的登记下一帧出现。
// main 不在这里——它无条件(#562);kb 座位也不在这里——契约 §3.6
// 刻意选了「入口常驻 + 空态自解释」的另一半(见上面 panellist 块注释)。
// main 不在这里——它无条件(#562)。
ctx.effect(() => {
let dead = false;
apiFetch("/api/dsh-mneme/features")
Expand All @@ -14654,6 +14649,31 @@ window.__ModuleLoader__.load({
});
return () => { dead = true; };
}, "dsh-mneme: chorography seat gate");

// --- 知识库侧栏座位闸(#685):与 choro 同一形态——同一个 features
// 端点、同样 fail-closed(读不到就不注册)、同样只门控侧栏那一格。
// main 留在上面无条件注册(#562 的墙),所以「关着就不注册座位」在
// 这里的含义是「侧栏没有入口、点不进去」,不是把 main 也摘掉:摘掉会
// 让布局里存着 activePanelId="dsh-mneme-kb" 的宿主在 selectPanel 时抛。
ctx.effect(() => {
let dead = false;
apiFetch("/api/dsh-mneme/features")
.then((res) => { if (!res.ok) throw new Error("HTTP " + res.status); return res.json(); })
.then((j) => {
if (dead) return;
if (!((j && j.effective) || {}).kbEnabled) return;
ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
name: "sidebar.panellist",
id: "dsh-mneme-kb",
order: 62,
label: () => t("kb.panel.open")
}, (props) => h(KbGlyph, { size: props && props.size })));
})
.catch((err) => {
console.warn("[dsh-mneme] features fetch failed, kb seat stays off:", err);
});
return () => { dead = true; };
}, "dsh-mneme: kb seat gate");
}

exports.apply = apply;
Expand Down
51 changes: 41 additions & 10 deletions dsh-mneme/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ test("chorography wiki main stays unconditional; the sidebar glyph is feature-ga
clientSource.includes('phase: "disabled"'),
"the disabled empty state must survive for layouts that still point at the wiki panel (#562 keeps main alive for exactly this)"
);
// wiki glyph 的清理随登记进了座位闸(cordis 上下文销毁时一并回收);
// panellist 回调的扫帚只剩记忆对 + wiki 的 main + kb 对。
// wiki / kb 的 glyph 清理都随各自登记进了座位闸(cordis 上下文销毁时一并
// 回收);panellist 回调的扫帚只剩记忆对 + 两个 main。
assert.ok(
/return \(\) => \{\s*disposeGlyph\(\);\s*disposeMain\(\);[\s\S]*?disposeWikiMain\(\);/.test(clientSource),
"the panellist cleanup must dispose the memory pair and wiki main, in order"
Expand Down Expand Up @@ -393,18 +393,48 @@ test("knowledge base organises a whole repo from cards and never fakes a clean r

// 知识库是第三个座位:id 与 main 的 key 必须逐字同名,否则宿主 selectPanel
// 直接抛 "main panel ... is not registered"(#562 在记忆座位上踩过)。
test("knowledge base entry registers its own seat pair in the same panellist block", () => {
assert.ok(clientSource.includes('id: "dsh-mneme-kb"'), "the kb sidebar seat must use its own id");
// #685(反转 #598 评论区 §四的另一半):kb 的侧栏位置改成跟 kbEnabled 走,
// 与 choro 同一形态;main 仍无条件——#562 那堵墙(布局里存着
// activePanelId="dsh-mneme-kb" 时少注册 main,宿主 selectPanel 直接抛)。
test("knowledge base sidebar entry follows kbEnabled; its main seat stays unconditional (#685)", () => {
assert.ok(clientSource.includes('id: "dsh-mneme-kb"'), "the kb sidebar seat must keep its own id");
assert.ok(clientSource.includes('key: "dsh-mneme-kb"'), "the kb main seat key must match the sidebar id verbatim");
assert.ok(clientSource.includes("h(KbMainPanel, { t })"), "the kb main seat must render KbMainPanel");
assert.ok(
clientSource.includes("h(KbGlyph, { size: props && props.size })"),
"the kb glyph takes its size from the host"
);
assert.ok(
/return \(\) => \{\s*disposeGlyph\(\);\s*disposeMain\(\);[\s\S]*?disposeWikiMain\(\);\s*disposeKbGlyph\(\);\s*disposeKbMain\(\);/.test(clientSource),
"the panellist cleanup must dispose the remaining seat pairs (the wiki glyph moved to the feature gate)"
clientSource.includes('"dsh-mneme: kb seat gate"'),
"the kb glyph registration must live behind its own features gate"
);
assert.ok(
/if \(!\(\(j && j\.effective\) \|\| \{\}\)\.kbEnabled\) return;/.test(clientSource),
"the gate must fail-closed on the kbEnabled flag"
);
assert.ok(
clientSource.includes("kb seat stays off"),
"a failed features fetch must leave the kb seat off, not guess"
);
// 侧栏图标不能留在 panellist 回调的无条件注册里——那正是 #685 报的形态。
const gateStart = clientSource.indexOf('ctx.slots.inject("sidebar.panellist"');
const gateEnd = clientSource.indexOf("mountFooterSeat();", gateStart);
const panellistBlock = gateStart === -1 || gateEnd === -1 ? "" : clientSource.slice(gateStart, gateEnd);
assert.ok(panellistBlock.length > 0, "the panellist injection block must still exist");
assert.ok(
panellistBlock.includes('key: "dsh-mneme-kb"'),
"the kb main seat must stay in the unconditional panellist block (#562)"
);
assert.ok(
!/id:\s*"dsh-mneme-kb"/.test(panellistBlock),
"the kb sidebar glyph must not be registered unconditionally anymore (#685)"
);
// kb glyph 的清理随登记进了座位闸;panellist 的扫帚只剩记忆对 + 两个 main。
assert.ok(
/return \(\) => \{\s*disposeGlyph\(\);\s*disposeMain\(\);[\s\S]*?disposeWikiMain\(\);\s*disposeKbMain\(\);/.test(clientSource),
"the panellist cleanup must dispose the memory pair and both main seats (the glyphs live in their gates)"
);
assert.ok(!clientSource.includes("disposeKbGlyph"), "the unconditional kb glyph dispose must be gone with the seat itself");
});

// v0.11.1 美化:文件清单的竖线等宽行换成真 table(用户实测反馈「文件名排列
Expand Down Expand Up @@ -461,8 +491,9 @@ test("panel seats: the fallback mounts and unmounts with the host's declaration"
// spec 存在时点火:一把全点会把新座位的注册塞进老宿主,测出来的就不是真行为。
const fire = (ctx, key) => ctx.injected.filter((i) => i.key === key).map((i) => i.cb());

// 新宿主:panellist 已声明。panellist + main 各三份(记忆 + chorography +
// 知识库三个座位),每对各自 id/key 逐字同名;footer 回落不装。
// 新宿主:panellist 已声明。panellist 里只剩记忆一份图标(wiki 自 #598、
// 知识库自 #685 都改成各自 features 闸里的按需注册),main 三份(记忆 +
// chorography + 知识库)无条件——#562 的墙;footer 回落不装。
// 日记本原有第四对座位,2026-11-09 按桉桉要求撤掉——它现在是记忆面板内的
// 一个标签页,与人格页并列,不再占侧边栏;人格页同理(面板内标签页,不是座位)。
{
Expand All @@ -473,8 +504,8 @@ test("panel seats: the fallback mounts and unmounts with the host's declaration"
fire(ctx, "sidebar.footer.action");
assert.equal(disposes.length, 1);
const ids = (name) => regs.filter((r) => r.alive && r.opts.name === name).map((r) => r.opts.id ?? r.opts.key).sort();
assert.equal(alive(regs, "sidebar.panellist"), 2, "memory + kb glyphs — the wiki glyph is feature-gated (#598), not unconditional");
assert.deepEqual(ids("sidebar.panellist"), ["dsh-mneme", "dsh-mneme-kb"], "每对座位各自 id 逐字唯一;wiki glyph 走座位闸,不在无条件集合里");
assert.equal(alive(regs, "sidebar.panellist"), 1, "memory glyph only — the wiki and kb glyphs are feature-gated (#598, #685)");
assert.deepEqual(ids("sidebar.panellist"), ["dsh-mneme"], "the only unconditional sidebar glyph is the memory entry");
assert.equal(alive(regs, "main"), 3, "memory + wiki + kb pages — wiki main stays unconditional (#562)");
assert.deepEqual(ids("main"), ["dsh-mneme", "dsh-mneme-kb", "dsh-mneme-wiki"], "main key 与侧栏 id 逐字同名(wiki 的 main 无条件保留,#562)");
assert.equal(alive(regs, "sidebar.footer.action"), 0, "the footer fallback must not mount on a host that has the seat");
Expand Down
Loading