@neuralnomads/codenomad-dev 0.20.0-dev-20260910-abaf6eba → 0.20.0-dev-20260911-ea37f97b
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/dist/api-types.js.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/opencode/pruning-installation.js +65 -0
- package/dist/opencode/pruning-installation.js.map +1 -0
- package/dist/opencode/pruning-installation.test.js +101 -0
- package/dist/opencode/pruning-installation.test.js.map +1 -0
- package/dist/opencode/pruning-lifecycle.js +66 -0
- package/dist/opencode/pruning-lifecycle.js.map +1 -0
- package/dist/opencode/session-pruning/claim-fence.js +33 -0
- package/dist/opencode/session-pruning/claim-fence.js.map +1 -0
- package/dist/opencode/session-pruning/claim-fence.test.js +104 -0
- package/dist/opencode/session-pruning/claim-fence.test.js.map +1 -0
- package/dist/opencode/session-pruning/contract.js +40 -0
- package/dist/opencode/session-pruning/contract.js.map +1 -0
- package/dist/opencode/session-pruning/database-path.js +17 -0
- package/dist/opencode/session-pruning/database-path.js.map +1 -0
- package/dist/opencode/session-pruning/database-path.test.js +22 -0
- package/dist/opencode/session-pruning/database-path.test.js.map +1 -0
- package/dist/opencode/session-pruning/desktop-plugin.js +14 -0
- package/dist/opencode/session-pruning/desktop-plugin.js.map +1 -0
- package/dist/opencode/session-pruning/index.js +2 -0
- package/dist/opencode/session-pruning/index.js.map +1 -0
- package/dist/opencode/session-pruning/isolated-store.js +18 -0
- package/dist/opencode/session-pruning/isolated-store.js.map +1 -0
- package/dist/opencode/session-pruning/planner.js +36 -0
- package/dist/opencode/session-pruning/planner.js.map +1 -0
- package/dist/opencode/session-pruning/plugin.js +45 -0
- package/dist/opencode/session-pruning/plugin.js.map +1 -0
- package/dist/opencode/session-pruning/presence.js +42 -0
- package/dist/opencode/session-pruning/presence.js.map +1 -0
- package/dist/opencode/session-pruning/preview-store.js +21 -0
- package/dist/opencode/session-pruning/preview-store.js.map +1 -0
- package/dist/opencode/session-pruning/pruning.test.js +172 -0
- package/dist/opencode/session-pruning/pruning.test.js.map +1 -0
- package/dist/opencode/session-pruning/revision.js +18 -0
- package/dist/opencode/session-pruning/revision.js.map +1 -0
- package/dist/opencode/session-pruning/service.js +56 -0
- package/dist/opencode/session-pruning/service.js.map +1 -0
- package/dist/opencode/session-pruning/service.test.js +74 -0
- package/dist/opencode/session-pruning/service.test.js.map +1 -0
- package/dist/opencode/session-pruning/storage-path.js +6 -0
- package/dist/opencode/session-pruning/storage-path.js.map +1 -0
- package/dist/opencode/session-pruning/transaction.js +53 -0
- package/dist/opencode/session-pruning/transaction.js.map +1 -0
- package/dist/opencode/session-pruning/tui-reload.js +84 -0
- package/dist/opencode/session-pruning/tui-reload.js.map +1 -0
- package/dist/opencode/session-pruning/tui-reload.native.js +269 -0
- package/dist/opencode/session-pruning/tui-reload.native.js.map +1 -0
- package/dist/opencode/session-pruning/tui-reload.test.js +71 -0
- package/dist/opencode/session-pruning/tui-reload.test.js.map +1 -0
- package/dist/opencode/session-pruning/tui.js +12 -0
- package/dist/opencode/session-pruning/tui.js.map +1 -0
- package/dist/plugins/session-pruning/plugin.mjs +26559 -0
- package/dist/server/__tests__/instance-proxy.test.js +10 -5
- package/dist/server/__tests__/instance-proxy.test.js.map +1 -1
- package/dist/server/http-server.js +2 -1
- package/dist/server/http-server.js.map +1 -1
- package/dist/server/routes/session-pruning.js +45 -0
- package/dist/server/routes/session-pruning.js.map +1 -0
- package/dist/server/routes/session-pruning.test.js +98 -0
- package/dist/server/routes/session-pruning.test.js.map +1 -0
- package/dist/workspaces/manager.js +1 -0
- package/dist/workspaces/manager.js.map +1 -1
- package/package.json +6 -3
- package/public/assets/App-0-FcHzPS.js +44 -0
- package/public/assets/App-0-FcHzPS.js.map +1 -0
- package/public/assets/{FilesTab-BVJawmU7.js → FilesTab-CRpGDs1B.js} +3 -3
- package/public/assets/{FilesTab-BVJawmU7.js.map → FilesTab-CRpGDs1B.js.map} +1 -1
- package/public/assets/{GitChangesTab-BVdfbJ_l.js → GitChangesTab-DxFebTiq.js} +3 -3
- package/public/assets/{GitChangesTab-BVdfbJ_l.js.map → GitChangesTab-DxFebTiq.js.map} +1 -1
- package/public/assets/{SplitFilePanel-CSR5-RHY.js → SplitFilePanel-DgTMrrO-.js} +2 -2
- package/public/assets/{SplitFilePanel-CSR5-RHY.js.map → SplitFilePanel-DgTMrrO-.js.map} +1 -1
- package/public/assets/{StatusTab-DIodfPQd.js → StatusTab-LKUA9cbH.js} +2 -2
- package/public/assets/{StatusTab-DIodfPQd.js.map → StatusTab-LKUA9cbH.js.map} +1 -1
- package/public/assets/{align-justify-kDFhSvpm.js → align-justify-v_yPXSys.js} +2 -2
- package/public/assets/{align-justify-kDFhSvpm.js.map → align-justify-v_yPXSys.js.map} +1 -1
- package/public/assets/{bundle-full-B4eYITdG.js → bundle-full-HzL_Jtqk.js} +2 -2
- package/public/assets/{bundle-full-B4eYITdG.js.map → bundle-full-HzL_Jtqk.js.map} +1 -1
- package/public/assets/{client-state-D3g-WH8W.js → client-state-CF-PJ0GG.js} +2 -2
- package/public/assets/{client-state-D3g-WH8W.js.map → client-state-CF-PJ0GG.js.map} +1 -1
- package/public/assets/{diff-viewer-BLL0OcUK.js → diff-viewer-BFDQBDVy.js} +2 -2
- package/public/assets/{diff-viewer-BLL0OcUK.js.map → diff-viewer-BFDQBDVy.js.map} +1 -1
- package/public/assets/{index-D21P0GMO.js → index-8iPlm2r0.js} +2 -2
- package/public/assets/index-8iPlm2r0.js.map +1 -0
- package/public/assets/{index-DnqYBc2b.js → index-B185mC7m.js} +2 -2
- package/public/assets/index-B185mC7m.js.map +1 -0
- package/public/assets/{index-DLtH2OSD.js → index-Bbj1xF8i.js} +2 -2
- package/public/assets/index-Bbj1xF8i.js.map +1 -0
- package/public/assets/{index-BtuH3zKP.js → index-BsmFN3k1.js} +2 -2
- package/public/assets/index-BsmFN3k1.js.map +1 -0
- package/public/assets/{index-CrOPzgHN.js → index-C52CfqK3.js} +2 -2
- package/public/assets/index-C52CfqK3.js.map +1 -0
- package/public/assets/{index-DWnsOFes.js → index-CHYmKl10.js} +2 -2
- package/public/assets/index-CHYmKl10.js.map +1 -0
- package/public/assets/{index-DbfYo3i-.js → index-CTKYD_VJ.js} +2 -2
- package/public/assets/index-CTKYD_VJ.js.map +1 -0
- package/public/assets/index-D2UHSOG5.js +2 -0
- package/public/assets/{index-BIqxsbaj.js.map → index-D2UHSOG5.js.map} +1 -1
- package/public/assets/{index-DLRrUgI7.js → index-DDoB_Z36.js} +2 -2
- package/public/assets/index-DDoB_Z36.js.map +1 -0
- package/public/assets/{index-e5yyN2yK.js → index-DcwAtZw-.js} +3 -3
- package/public/assets/{index-e5yyN2yK.js.map → index-DcwAtZw-.js.map} +1 -1
- package/public/assets/{index-ZnQhzm1X.js → index-DxUD3RYr.js} +2 -2
- package/public/assets/{index-ZnQhzm1X.js.map → index-DxUD3RYr.js.map} +1 -1
- package/public/assets/{index-CeF-jpOV.js → index-RK9gf0uk.js} +2 -2
- package/public/assets/{index-CeF-jpOV.js.map → index-RK9gf0uk.js.map} +1 -1
- package/public/assets/{loading-CaoeVfIV.js → loading-BSB82snY.js} +2 -2
- package/public/assets/{loading-CaoeVfIV.js.map → loading-BSB82snY.js.map} +1 -1
- package/public/assets/main-CZztwrfv.js +48 -0
- package/public/assets/main-CZztwrfv.js.map +1 -0
- package/public/assets/{markdown-BeJ9YIFU.js → markdown-DtidoL2Z.js} +4 -4
- package/public/assets/{markdown-BeJ9YIFU.js.map → markdown-DtidoL2Z.js.map} +1 -1
- package/public/assets/{monaco-viewer-Cc9bWg7I.js → monaco-viewer-CpxDfB-6.js} +2 -2
- package/public/assets/monaco-viewer-CpxDfB-6.js.map +1 -0
- package/public/assets/{startup-state-settings-card-BvpJJ6ei.js → startup-state-settings-card-BY741Rt0.js} +2 -2
- package/public/assets/{startup-state-settings-card-BvpJJ6ei.js.map → startup-state-settings-card-BY741Rt0.js.map} +1 -1
- package/public/assets/{terminal-square-DBCSEAmh.js → terminal-square-BdC_jwm3.js} +2 -2
- package/public/assets/{terminal-square-DBCSEAmh.js.map → terminal-square-BdC_jwm3.js.map} +1 -1
- package/public/assets/{tool-call-BJiSHSx7.js → tool-call-DbtCsJ4x.js} +4 -4
- package/public/assets/{tool-call-BJiSHSx7.js.map → tool-call-DbtCsJ4x.js.map} +1 -1
- package/public/assets/{unified-picker-CAEyj3a0.js → unified-picker-C0eo1chJ.js} +2 -2
- package/public/assets/{unified-picker-CAEyj3a0.js.map → unified-picker-C0eo1chJ.js.map} +1 -1
- package/public/assets/{wrap-text-DJNV0ypl.js → wrap-text-BuMtdvMx.js} +2 -2
- package/public/assets/{wrap-text-DJNV0ypl.js.map → wrap-text-BuMtdvMx.js.map} +1 -1
- package/public/index.html +3 -3
- package/public/loading.html +3 -3
- package/public/sw.js +1 -1
- package/public/sw.js.map +1 -1
- package/public/assets/App-0ECD8eRb.js +0 -43
- package/public/assets/App-0ECD8eRb.js.map +0 -1
- package/public/assets/index-BIqxsbaj.js +0 -2
- package/public/assets/index-BtuH3zKP.js.map +0 -1
- package/public/assets/index-CrOPzgHN.js.map +0 -1
- package/public/assets/index-D21P0GMO.js.map +0 -1
- package/public/assets/index-DLRrUgI7.js.map +0 -1
- package/public/assets/index-DLtH2OSD.js.map +0 -1
- package/public/assets/index-DWnsOFes.js.map +0 -1
- package/public/assets/index-DbfYo3i-.js.map +0 -1
- package/public/assets/index-DnqYBc2b.js.map +0 -1
- package/public/assets/main-CeMcnDRn.js +0 -48
- package/public/assets/main-CeMcnDRn.js.map +0 -1
- package/public/assets/monaco-viewer-Cc9bWg7I.js.map +0 -1
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// Executed by tui-reload.test.ts in a browser-conditioned child process. Only
|
|
2
|
+
// the transport is a fixture: publication, pagination, indexing, invalidation,
|
|
3
|
+
// coalescing and Solid stores all come from the unmodified installed npm cache.
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import { createRoot } from "solid-js";
|
|
8
|
+
import { createPruningReload } from "./tui-reload";
|
|
9
|
+
import tui from "./tui";
|
|
10
|
+
// Use the exact native client already locked through @opencode/plugin. No npm
|
|
11
|
+
// install, daemon discovery, user config, database, or checkout source access.
|
|
12
|
+
const clientEntry = new URL(import.meta.resolve("@opencode/client/solid"));
|
|
13
|
+
const manifest = JSON.parse(readFileSync(new URL("../../package.json", clientEntry), "utf8"));
|
|
14
|
+
assert.equal(manifest.name, "@opencode/client");
|
|
15
|
+
console.log(`Testing native Solid cache ${manifest.version}`);
|
|
16
|
+
const { createData } = await import(clientEntry.href);
|
|
17
|
+
function deferred() {
|
|
18
|
+
let resolve;
|
|
19
|
+
let reject;
|
|
20
|
+
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
|
|
21
|
+
return { promise, resolve, reject };
|
|
22
|
+
}
|
|
23
|
+
function message(id, removed = false) {
|
|
24
|
+
return {
|
|
25
|
+
id, type: "assistant", agent: "build", model: { providerID: "fixture", id: "fixture" },
|
|
26
|
+
time: { created: 1, completed: 2 }, finish: "stop",
|
|
27
|
+
content: [
|
|
28
|
+
...(removed ? [{ type: "reasoning", text: "REMOVED", time: { created: 1, completed: 2 } }] : []),
|
|
29
|
+
{ type: "text", text: `KEEP-${id}` },
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function page(messages, next) {
|
|
34
|
+
return { data: messages, cursor: { next } };
|
|
35
|
+
}
|
|
36
|
+
const event = {
|
|
37
|
+
type: "rpc.codenomad.session-pruning.pruned", location: { directory: "/fixture" },
|
|
38
|
+
data: { sessionID: "s", messageID: "older", revision: "a".repeat(64) },
|
|
39
|
+
};
|
|
40
|
+
const turn = () => new Promise(resolve => setImmediate(resolve));
|
|
41
|
+
function fixture() {
|
|
42
|
+
const listeners = new Set();
|
|
43
|
+
const requests = [];
|
|
44
|
+
const api = {
|
|
45
|
+
message: {
|
|
46
|
+
list: (input) => {
|
|
47
|
+
assert.equal(input.sessionID, "s");
|
|
48
|
+
const response = deferred();
|
|
49
|
+
requests.push({ cursor: input.cursor, response });
|
|
50
|
+
return response.promise;
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
let disposeRoot;
|
|
55
|
+
const data = createRoot(dispose => {
|
|
56
|
+
disposeRoot = dispose;
|
|
57
|
+
return createData({
|
|
58
|
+
api: () => api, directory: "/fixture",
|
|
59
|
+
event: {
|
|
60
|
+
on: () => () => { },
|
|
61
|
+
listen: listener => { listeners.add(listener); return () => { listeners.delete(listener); }; },
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
data.session.remember({
|
|
66
|
+
id: "s", projectID: "p", cost: 0,
|
|
67
|
+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
68
|
+
time: { created: 1, updated: 2 }, location: { directory: "/fixture" },
|
|
69
|
+
});
|
|
70
|
+
const reload = createPruningReload(data.session);
|
|
71
|
+
return {
|
|
72
|
+
data, reload, requests,
|
|
73
|
+
emitPrune() {
|
|
74
|
+
// The custom RPC envelope is validated by the companion, not the
|
|
75
|
+
// generated native-event union. This is the simulated transport boundary.
|
|
76
|
+
const details = { ...event, id: "evt_prune", created: 3 };
|
|
77
|
+
for (const listener of listeners)
|
|
78
|
+
listener({ name: details.type, details });
|
|
79
|
+
},
|
|
80
|
+
async seed() {
|
|
81
|
+
const sync = data.session.message.sync("s");
|
|
82
|
+
requests[0].response.resolve(page([message("latest")], "older-page"));
|
|
83
|
+
await sync;
|
|
84
|
+
},
|
|
85
|
+
text: () => JSON.stringify(data.session.message.list("s")),
|
|
86
|
+
dispose() { reload.dispose(); disposeRoot(); },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
test("the published plugin entrypoint consumes the host's original native cache", async (t) => {
|
|
90
|
+
const f = fixture();
|
|
91
|
+
t.after(() => f.dispose());
|
|
92
|
+
// Matches the audited host's `data: host.data` assignment; no hidden cache
|
|
93
|
+
// fields or replacement methods are attached by the test or companion.
|
|
94
|
+
// The headless fixture omits unrelated renderer/UI services.
|
|
95
|
+
const cleanup = await tui.setup({ data: f.data });
|
|
96
|
+
t.after(() => { if (typeof cleanup === "function")
|
|
97
|
+
return cleanup(); });
|
|
98
|
+
await f.seed();
|
|
99
|
+
const older = f.data.session.message.loadMore("s");
|
|
100
|
+
f.emitPrune();
|
|
101
|
+
f.requests[1].response.resolve(page([message("older", true)]));
|
|
102
|
+
await older;
|
|
103
|
+
await turn();
|
|
104
|
+
assert.equal(f.requests.length, 3);
|
|
105
|
+
f.requests[2].response.resolve(page([message("latest")], "older-page"));
|
|
106
|
+
await turn();
|
|
107
|
+
assert(!f.text().includes("REMOVED"));
|
|
108
|
+
});
|
|
109
|
+
test("control: invalidate/sync alone leaves REMOVED after late pagination", async (t) => {
|
|
110
|
+
const f = fixture();
|
|
111
|
+
t.after(() => f.dispose());
|
|
112
|
+
await f.seed();
|
|
113
|
+
const older = f.data.session.message.loadMore("s");
|
|
114
|
+
f.data.session.message.invalidate("s");
|
|
115
|
+
const sync = f.data.session.message.sync("s");
|
|
116
|
+
f.requests[2].response.resolve(page([message("latest")], "older-page"));
|
|
117
|
+
await sync;
|
|
118
|
+
assert(!f.text().includes("REMOVED"));
|
|
119
|
+
f.requests[1].response.resolve(page([message("older", true)]));
|
|
120
|
+
await older;
|
|
121
|
+
await turn();
|
|
122
|
+
assert(f.text().includes("REMOVED"), "The control must reproduce the reviewer's actual native-cache failure");
|
|
123
|
+
assert.equal(f.requests.length, 3, "There is no subsequent native refresh");
|
|
124
|
+
});
|
|
125
|
+
for (const trigger of [event, { type: "server.connected" }]) {
|
|
126
|
+
test(`companion joins pending publication and authoritatively rereads on ${trigger.type}`, async (t) => {
|
|
127
|
+
const f = fixture();
|
|
128
|
+
t.after(() => f.dispose());
|
|
129
|
+
await f.seed();
|
|
130
|
+
const older = f.data.session.message.loadMore("s");
|
|
131
|
+
assert(f.data.session.message.loading("s"));
|
|
132
|
+
f.reload.event(trigger);
|
|
133
|
+
assert.equal(f.requests.length, 2, "Joining does not request another page or read too early");
|
|
134
|
+
// The TUI itself can sync while the companion waits. Reproduce the exact
|
|
135
|
+
// latest-read -> late old-page ordering, then demand one final reread.
|
|
136
|
+
const intermediate = f.data.session.message.sync("s");
|
|
137
|
+
f.requests[2].response.resolve(page([message("latest")], "older-page"));
|
|
138
|
+
await intermediate;
|
|
139
|
+
f.requests[1].response.resolve(page([message("older", true)]));
|
|
140
|
+
await older;
|
|
141
|
+
await turn();
|
|
142
|
+
assert.equal(f.requests.length, 4);
|
|
143
|
+
assert.equal(f.requests[3].cursor, undefined, "Final read must replace, not merge, the old window");
|
|
144
|
+
f.requests[3].response.resolve(page([message("latest")], "older-page"));
|
|
145
|
+
await turn();
|
|
146
|
+
assert(!f.text().includes("REMOVED"));
|
|
147
|
+
assert.equal(f.data.session.message.get("s", "older"), undefined);
|
|
148
|
+
// Dropping history permanently is not a fix: it must still paginate back
|
|
149
|
+
// to the pruned message with its remaining content and index intact.
|
|
150
|
+
const cleanOlder = f.data.session.message.loadMore("s");
|
|
151
|
+
f.requests[4].response.resolve(page([message("older")]));
|
|
152
|
+
await cleanOlder;
|
|
153
|
+
assert(f.text().includes("KEEP-older"));
|
|
154
|
+
assert(!f.text().includes("REMOVED"));
|
|
155
|
+
assert.equal(f.data.session.message.get("s", "older")?.id, "older");
|
|
156
|
+
await turn();
|
|
157
|
+
assert.equal(f.requests.length, 5, "No next event or polling is required");
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
test("joins all-pages publication, including a prune during a buffered older page", async (t) => {
|
|
161
|
+
const f = fixture();
|
|
162
|
+
t.after(() => f.dispose());
|
|
163
|
+
await f.seed();
|
|
164
|
+
const older = f.data.session.message.loadMore("s", { all: true });
|
|
165
|
+
f.requests[1].response.resolve(page([message("older", true)], "oldest-page"));
|
|
166
|
+
await turn();
|
|
167
|
+
f.reload.event(event);
|
|
168
|
+
f.reload.event(event);
|
|
169
|
+
assert.equal(f.requests.length, 3);
|
|
170
|
+
f.requests[2].response.resolve(page([message("oldest")]));
|
|
171
|
+
await older;
|
|
172
|
+
await turn();
|
|
173
|
+
assert.equal(f.requests[3].cursor, undefined);
|
|
174
|
+
f.requests[3].response.resolve(page([message("latest")], "older-page"));
|
|
175
|
+
await turn();
|
|
176
|
+
// A second event while the first refresh was pending receives a fresh pass.
|
|
177
|
+
assert.equal(f.requests.length, 5);
|
|
178
|
+
f.requests[4].response.resolve(page([message("latest")], "older-page"));
|
|
179
|
+
await turn();
|
|
180
|
+
assert(!f.text().includes("REMOVED"));
|
|
181
|
+
});
|
|
182
|
+
test("failed pagination does not suppress the authoritative refresh", async (t) => {
|
|
183
|
+
const f = fixture();
|
|
184
|
+
t.after(() => f.dispose());
|
|
185
|
+
await f.seed();
|
|
186
|
+
const older = f.data.session.message.loadMore("s");
|
|
187
|
+
const rejected = assert.rejects(older, /offline/);
|
|
188
|
+
f.reload.event(event);
|
|
189
|
+
f.requests[1].response.reject(new Error("offline"));
|
|
190
|
+
await rejected;
|
|
191
|
+
await turn();
|
|
192
|
+
assert.equal(f.requests.length, 3);
|
|
193
|
+
assert.equal(f.requests[2].cursor, undefined);
|
|
194
|
+
f.requests[2].response.resolve(page([message("latest")]));
|
|
195
|
+
await turn();
|
|
196
|
+
assert(!f.data.session.message.loading("s"));
|
|
197
|
+
assert(!f.text().includes("REMOVED"));
|
|
198
|
+
});
|
|
199
|
+
test("a prune during an in-flight sync forces a second authoritative pass", async (t) => {
|
|
200
|
+
const f = fixture();
|
|
201
|
+
t.after(() => f.dispose());
|
|
202
|
+
await f.seed();
|
|
203
|
+
f.reload.event(event);
|
|
204
|
+
f.reload.event(event);
|
|
205
|
+
f.requests[1].response.resolve(page([message("latest", true)]));
|
|
206
|
+
await turn();
|
|
207
|
+
assert.equal(f.requests.length, 3);
|
|
208
|
+
f.requests[2].response.resolve(page([message("latest")]));
|
|
209
|
+
await turn();
|
|
210
|
+
assert(!f.text().includes("REMOVED"));
|
|
211
|
+
});
|
|
212
|
+
test("joins both an older pagination and a latest-window sync started before pruning", async (t) => {
|
|
213
|
+
const f = fixture();
|
|
214
|
+
t.after(() => f.dispose());
|
|
215
|
+
await f.seed();
|
|
216
|
+
const older = f.data.session.message.loadMore("s");
|
|
217
|
+
f.data.session.message.invalidate("s");
|
|
218
|
+
const oldSync = f.data.session.message.sync("s");
|
|
219
|
+
f.reload.event(event);
|
|
220
|
+
f.requests[1].response.resolve(page([message("older", true)]));
|
|
221
|
+
await older;
|
|
222
|
+
await turn();
|
|
223
|
+
assert.equal(f.requests.length, 3, "Native sync serialization must also drain the pre-prune window");
|
|
224
|
+
f.requests[2].response.resolve(page([message("latest", true)], "older-page"));
|
|
225
|
+
await oldSync;
|
|
226
|
+
await turn();
|
|
227
|
+
assert.equal(f.requests.length, 4);
|
|
228
|
+
f.requests[3].response.resolve(page([message("latest")], "older-page"));
|
|
229
|
+
await turn();
|
|
230
|
+
assert(!f.text().includes("REMOVED"));
|
|
231
|
+
});
|
|
232
|
+
test("an aborted TUI page cannot abandon the companion's final reread", async (t) => {
|
|
233
|
+
const f = fixture();
|
|
234
|
+
t.after(() => f.dispose());
|
|
235
|
+
await f.seed();
|
|
236
|
+
const controller = new AbortController();
|
|
237
|
+
const older = f.data.session.message.loadMore("s", { signal: controller.signal });
|
|
238
|
+
f.reload.event(event);
|
|
239
|
+
controller.abort();
|
|
240
|
+
f.requests[1].response.resolve(page([message("older", true)]));
|
|
241
|
+
await older;
|
|
242
|
+
await turn();
|
|
243
|
+
// Native loadMore retries when the joined page did not publish. That new
|
|
244
|
+
// request starts after pruning; the final authoritative read still follows.
|
|
245
|
+
assert.equal(f.requests[2].cursor, "older-page");
|
|
246
|
+
f.requests[2].response.resolve(page([message("older")]));
|
|
247
|
+
await turn();
|
|
248
|
+
assert.equal(f.requests[3].cursor, undefined);
|
|
249
|
+
f.requests[3].response.resolve(page([message("latest")], "older-page"));
|
|
250
|
+
await turn();
|
|
251
|
+
assert(!f.text().includes("REMOVED"));
|
|
252
|
+
assert.equal(f.requests.length, 4);
|
|
253
|
+
});
|
|
254
|
+
test("disposing the companion releases its join without cancelling TUI-owned pagination", async (t) => {
|
|
255
|
+
const f = fixture();
|
|
256
|
+
t.after(() => f.dispose());
|
|
257
|
+
await f.seed();
|
|
258
|
+
const older = f.data.session.message.loadMore("s");
|
|
259
|
+
f.reload.event(event);
|
|
260
|
+
f.reload.dispose();
|
|
261
|
+
await turn();
|
|
262
|
+
assert(f.data.session.message.loading("s"), "The TUI still owns its request");
|
|
263
|
+
f.requests[1].response.resolve(page([message("older")]));
|
|
264
|
+
await older;
|
|
265
|
+
await turn();
|
|
266
|
+
assert.equal(f.requests.length, 2, "A disposed companion must not start a refresh");
|
|
267
|
+
assert(f.text().includes("KEEP-older"));
|
|
268
|
+
});
|
|
269
|
+
//# sourceMappingURL=tui-reload.native.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tui-reload.native.js","sourceRoot":"","sources":["../../../src/opencode/session-pruning/tui-reload.native.ts"],"names":[],"mappings":"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","sourcesContent":["// Executed by tui-reload.test.ts in a browser-conditioned child process. Only\n// the transport is a fixture: publication, pagination, indexing, invalidation,\n// coalescing and Solid stores all come from the unmodified installed npm cache.\nimport assert from \"node:assert/strict\"\nimport { readFileSync } from \"node:fs\"\nimport { test } from \"node:test\"\nimport { createRoot } from \"solid-js\"\nimport type { CreateDataInput, Data } from \"@opencode/client/solid\"\nimport type { MessageListOutput, OpenCodeEvent, SessionMessageInfo } from \"@opencode/client\"\nimport { createPruningReload } from \"./tui-reload\"\nimport tui from \"./tui\"\n\n// Use the exact native client already locked through @opencode/plugin. No npm\n// install, daemon discovery, user config, database, or checkout source access.\nconst clientEntry = new URL(import.meta.resolve(\"@opencode/client/solid\"))\nconst manifest = JSON.parse(readFileSync(new URL(\"../../package.json\", clientEntry), \"utf8\"))\nassert.equal(manifest.name, \"@opencode/client\")\nconsole.log(`Testing native Solid cache ${manifest.version}`)\nconst { createData } = await import(clientEntry.href) as typeof import(\"@opencode/client/solid\")\n\nfunction deferred<T>() {\n let resolve!: (value: T) => void\n let reject!: (error: Error) => void\n const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no })\n return { promise, resolve, reject }\n}\n\nfunction message(id: string, removed = false): SessionMessageInfo {\n return {\n id, type: \"assistant\", agent: \"build\", model: { providerID: \"fixture\", id: \"fixture\" },\n time: { created: 1, completed: 2 }, finish: \"stop\",\n content: [\n ...(removed ? [{ type: \"reasoning\" as const, text: \"REMOVED\", time: { created: 1, completed: 2 } }] : []),\n { type: \"text\", text: `KEEP-${id}` },\n ],\n }\n}\n\nfunction page(messages: SessionMessageInfo[], next?: string): MessageListOutput {\n return { data: messages, cursor: { next } }\n}\n\nconst event = {\n type: \"rpc.codenomad.session-pruning.pruned\", location: { directory: \"/fixture\" },\n data: { sessionID: \"s\", messageID: \"older\", revision: \"a\".repeat(64) },\n}\nconst turn = () => new Promise<void>(resolve => setImmediate(resolve))\n\nfunction fixture() {\n const listeners = new Set<Parameters<CreateDataInput[\"event\"][\"listen\"]>[0]>()\n const requests: Array<{ cursor?: string; response: ReturnType<typeof deferred<MessageListOutput>> }> = []\n const api = {\n message: {\n list: (input: { sessionID: string; cursor?: string }) => {\n assert.equal(input.sessionID, \"s\")\n const response = deferred<MessageListOutput>()\n requests.push({ cursor: input.cursor, response })\n return response.promise\n },\n },\n } as unknown as ReturnType<CreateDataInput[\"api\"]>\n let disposeRoot!: () => void\n const data: Data = createRoot(dispose => {\n disposeRoot = dispose\n return createData({\n api: () => api, directory: \"/fixture\",\n event: {\n on: () => () => {},\n listen: listener => { listeners.add(listener); return () => { listeners.delete(listener) } },\n },\n })\n })\n data.session.remember({\n id: \"s\", projectID: \"p\", cost: 0,\n tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },\n time: { created: 1, updated: 2 }, location: { directory: \"/fixture\" },\n })\n const reload = createPruningReload(data.session)\n return {\n data, reload, requests,\n emitPrune() {\n // The custom RPC envelope is validated by the companion, not the\n // generated native-event union. This is the simulated transport boundary.\n const details = { ...event, id: \"evt_prune\", created: 3 } as unknown as OpenCodeEvent\n for (const listener of listeners) listener({ name: details.type, details })\n },\n async seed() {\n const sync = data.session.message.sync(\"s\")\n requests[0].response.resolve(page([message(\"latest\")], \"older-page\"))\n await sync\n },\n text: () => JSON.stringify(data.session.message.list(\"s\")),\n dispose() { reload.dispose(); disposeRoot() },\n }\n}\n\ntest(\"the published plugin entrypoint consumes the host's original native cache\", async t => {\n const f = fixture(); t.after(() => f.dispose())\n // Matches the audited host's `data: host.data` assignment; no hidden cache\n // fields or replacement methods are attached by the test or companion.\n // The headless fixture omits unrelated renderer/UI services.\n const cleanup = await tui.setup!({ data: f.data } as unknown as Parameters<NonNullable<typeof tui.setup>>[0])\n t.after(() => { if (typeof cleanup === \"function\") return cleanup() })\n await f.seed()\n const older = f.data.session.message.loadMore(\"s\")\n f.emitPrune()\n f.requests[1].response.resolve(page([message(\"older\", true)]))\n await older\n await turn()\n assert.equal(f.requests.length, 3)\n f.requests[2].response.resolve(page([message(\"latest\")], \"older-page\"))\n await turn()\n assert(!f.text().includes(\"REMOVED\"))\n})\n\ntest(\"control: invalidate/sync alone leaves REMOVED after late pagination\", async t => {\n const f = fixture(); t.after(() => f.dispose())\n await f.seed()\n const older = f.data.session.message.loadMore(\"s\")\n f.data.session.message.invalidate(\"s\")\n const sync = f.data.session.message.sync(\"s\")\n f.requests[2].response.resolve(page([message(\"latest\")], \"older-page\"))\n await sync\n assert(!f.text().includes(\"REMOVED\"))\n f.requests[1].response.resolve(page([message(\"older\", true)]))\n await older\n await turn()\n assert(f.text().includes(\"REMOVED\"), \"The control must reproduce the reviewer's actual native-cache failure\")\n assert.equal(f.requests.length, 3, \"There is no subsequent native refresh\")\n})\n\nfor (const trigger of [event, { type: \"server.connected\" }]) {\n test(`companion joins pending publication and authoritatively rereads on ${trigger.type}`, async t => {\n const f = fixture(); t.after(() => f.dispose())\n await f.seed()\n const older = f.data.session.message.loadMore(\"s\")\n assert(f.data.session.message.loading(\"s\"))\n f.reload.event(trigger)\n assert.equal(f.requests.length, 2, \"Joining does not request another page or read too early\")\n\n // The TUI itself can sync while the companion waits. Reproduce the exact\n // latest-read -> late old-page ordering, then demand one final reread.\n const intermediate = f.data.session.message.sync(\"s\")\n f.requests[2].response.resolve(page([message(\"latest\")], \"older-page\"))\n await intermediate\n f.requests[1].response.resolve(page([message(\"older\", true)]))\n await older\n await turn()\n assert.equal(f.requests.length, 4)\n assert.equal(f.requests[3].cursor, undefined, \"Final read must replace, not merge, the old window\")\n f.requests[3].response.resolve(page([message(\"latest\")], \"older-page\"))\n await turn()\n assert(!f.text().includes(\"REMOVED\"))\n assert.equal(f.data.session.message.get(\"s\", \"older\"), undefined)\n\n // Dropping history permanently is not a fix: it must still paginate back\n // to the pruned message with its remaining content and index intact.\n const cleanOlder = f.data.session.message.loadMore(\"s\")\n f.requests[4].response.resolve(page([message(\"older\")]))\n await cleanOlder\n assert(f.text().includes(\"KEEP-older\"))\n assert(!f.text().includes(\"REMOVED\"))\n assert.equal(f.data.session.message.get(\"s\", \"older\")?.id, \"older\")\n await turn()\n assert.equal(f.requests.length, 5, \"No next event or polling is required\")\n })\n}\n\ntest(\"joins all-pages publication, including a prune during a buffered older page\", async t => {\n const f = fixture(); t.after(() => f.dispose())\n await f.seed()\n const older = f.data.session.message.loadMore(\"s\", { all: true })\n f.requests[1].response.resolve(page([message(\"older\", true)], \"oldest-page\"))\n await turn()\n f.reload.event(event)\n f.reload.event(event)\n assert.equal(f.requests.length, 3)\n f.requests[2].response.resolve(page([message(\"oldest\")]))\n await older\n await turn()\n assert.equal(f.requests[3].cursor, undefined)\n f.requests[3].response.resolve(page([message(\"latest\")], \"older-page\"))\n await turn()\n // A second event while the first refresh was pending receives a fresh pass.\n assert.equal(f.requests.length, 5)\n f.requests[4].response.resolve(page([message(\"latest\")], \"older-page\"))\n await turn()\n assert(!f.text().includes(\"REMOVED\"))\n})\n\ntest(\"failed pagination does not suppress the authoritative refresh\", async t => {\n const f = fixture(); t.after(() => f.dispose())\n await f.seed()\n const older = f.data.session.message.loadMore(\"s\")\n const rejected = assert.rejects(older, /offline/)\n f.reload.event(event)\n f.requests[1].response.reject(new Error(\"offline\"))\n await rejected\n await turn()\n assert.equal(f.requests.length, 3)\n assert.equal(f.requests[2].cursor, undefined)\n f.requests[2].response.resolve(page([message(\"latest\")]))\n await turn()\n assert(!f.data.session.message.loading(\"s\"))\n assert(!f.text().includes(\"REMOVED\"))\n})\n\ntest(\"a prune during an in-flight sync forces a second authoritative pass\", async t => {\n const f = fixture(); t.after(() => f.dispose())\n await f.seed()\n f.reload.event(event)\n f.reload.event(event)\n f.requests[1].response.resolve(page([message(\"latest\", true)]))\n await turn()\n assert.equal(f.requests.length, 3)\n f.requests[2].response.resolve(page([message(\"latest\")]))\n await turn()\n assert(!f.text().includes(\"REMOVED\"))\n})\n\ntest(\"joins both an older pagination and a latest-window sync started before pruning\", async t => {\n const f = fixture(); t.after(() => f.dispose())\n await f.seed()\n const older = f.data.session.message.loadMore(\"s\")\n f.data.session.message.invalidate(\"s\")\n const oldSync = f.data.session.message.sync(\"s\")\n f.reload.event(event)\n f.requests[1].response.resolve(page([message(\"older\", true)]))\n await older\n await turn()\n assert.equal(f.requests.length, 3, \"Native sync serialization must also drain the pre-prune window\")\n f.requests[2].response.resolve(page([message(\"latest\", true)], \"older-page\"))\n await oldSync\n await turn()\n assert.equal(f.requests.length, 4)\n f.requests[3].response.resolve(page([message(\"latest\")], \"older-page\"))\n await turn()\n assert(!f.text().includes(\"REMOVED\"))\n})\n\ntest(\"an aborted TUI page cannot abandon the companion's final reread\", async t => {\n const f = fixture(); t.after(() => f.dispose())\n await f.seed()\n const controller = new AbortController()\n const older = f.data.session.message.loadMore(\"s\", { signal: controller.signal })\n f.reload.event(event)\n controller.abort()\n f.requests[1].response.resolve(page([message(\"older\", true)]))\n await older\n await turn()\n // Native loadMore retries when the joined page did not publish. That new\n // request starts after pruning; the final authoritative read still follows.\n assert.equal(f.requests[2].cursor, \"older-page\")\n f.requests[2].response.resolve(page([message(\"older\")]))\n await turn()\n assert.equal(f.requests[3].cursor, undefined)\n f.requests[3].response.resolve(page([message(\"latest\")], \"older-page\"))\n await turn()\n assert(!f.text().includes(\"REMOVED\"))\n assert.equal(f.requests.length, 4)\n})\n\ntest(\"disposing the companion releases its join without cancelling TUI-owned pagination\", async t => {\n const f = fixture(); t.after(() => f.dispose())\n await f.seed()\n const older = f.data.session.message.loadMore(\"s\")\n f.reload.event(event)\n f.reload.dispose()\n await turn()\n assert(f.data.session.message.loading(\"s\"), \"The TUI still owns its request\")\n f.requests[1].response.resolve(page([message(\"older\")]))\n await older\n await turn()\n assert.equal(f.requests.length, 2, \"A disposed companion must not start a refresh\")\n assert(f.text().includes(\"KEEP-older\"))\n})\n"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { createPruningReload } from "./tui-reload";
|
|
6
|
+
test("TUI companion coalesces invalidations and rereads after a missed-event reconnect", async () => {
|
|
7
|
+
const invalidated = [];
|
|
8
|
+
const read = [];
|
|
9
|
+
let release;
|
|
10
|
+
const pending = new Promise(resolve => { release = resolve; });
|
|
11
|
+
const reload = createPruningReload({
|
|
12
|
+
list: () => [{ id: "s" }, { id: "unloaded" }],
|
|
13
|
+
get: id => id === "s" ? { location: { directory: "/work" } } : undefined,
|
|
14
|
+
message: {
|
|
15
|
+
list: id => id === "s" ? [{}] : [],
|
|
16
|
+
loading: () => false,
|
|
17
|
+
loadMore: async () => assert.fail("unexpected pagination"),
|
|
18
|
+
invalidate: id => { invalidated.push(id); },
|
|
19
|
+
sync: async (id) => { read.push(id); await pending; },
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const event = { type: "rpc.codenomad.session-pruning.pruned", location: { directory: "/work" }, data: { sessionID: "s", messageID: "m", revision: "a".repeat(64) } };
|
|
23
|
+
reload.event(event);
|
|
24
|
+
reload.event(event);
|
|
25
|
+
assert.deepEqual(read, ["s"]);
|
|
26
|
+
release();
|
|
27
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
28
|
+
assert.deepEqual(read, ["s", "s"]);
|
|
29
|
+
reload.event({ type: "server.connected" });
|
|
30
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
31
|
+
assert.deepEqual(read, ["s", "s", "s"]);
|
|
32
|
+
assert(invalidated.includes("unloaded"));
|
|
33
|
+
reload.dispose();
|
|
34
|
+
reload.event(event);
|
|
35
|
+
assert.equal(read.length, 3);
|
|
36
|
+
});
|
|
37
|
+
test("TUI companion rejects foreign locations, malformed events and unknown sessions", () => {
|
|
38
|
+
const reload = createPruningReload({
|
|
39
|
+
list: () => [], get: id => id === "s" ? { location: { directory: "/work" } } : undefined,
|
|
40
|
+
message: {
|
|
41
|
+
list: () => [], loading: () => false, loadMore: async () => assert.fail("unexpected pagination"),
|
|
42
|
+
invalidate: () => assert.fail("unexpected invalidation"), sync: async () => assert.fail("unexpected read"),
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
reload.event({ type: "rpc.other.pruned", data: {} });
|
|
46
|
+
reload.event({ type: "rpc.codenomad.session-pruning.pruned", data: {} });
|
|
47
|
+
for (const sessionID of ["s", "other"])
|
|
48
|
+
reload.event({ type: "rpc.codenomad.session-pruning.pruned", location: { directory: "/elsewhere" }, data: { sessionID, messageID: "m", revision: "a".repeat(64) } });
|
|
49
|
+
reload.dispose();
|
|
50
|
+
});
|
|
51
|
+
test("TUI reload regressions against the installed published Solid cache", () => {
|
|
52
|
+
// Keep Solid's browser conditions out of the server test process. Resolve the
|
|
53
|
+
// native client from the pinned plugin dependency, not a checkout, global
|
|
54
|
+
// install, copied cache implementation, or a network install during tests.
|
|
55
|
+
const env = { ...process.env };
|
|
56
|
+
delete env.NODE_TEST_CONTEXT;
|
|
57
|
+
const result = spawnSync(process.execPath, [
|
|
58
|
+
"--conditions=browser", "--import", "tsx", "--test",
|
|
59
|
+
fileURLToPath(new URL("./tui-reload.native.ts", import.meta.url)),
|
|
60
|
+
], { encoding: "utf8", timeout: 30_000, env });
|
|
61
|
+
assert.ifError(result.error);
|
|
62
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
63
|
+
assert.match(result.stdout, /control: invalidate\/sync/, "The child must actually execute its native-cache tests");
|
|
64
|
+
});
|
|
65
|
+
test("TUI companion refuses a cache without public pagination coordination", () => {
|
|
66
|
+
assert.throws(() => createPruningReload({
|
|
67
|
+
list: () => [], get: () => undefined,
|
|
68
|
+
message: { list: () => [], invalidate: () => { }, sync: async () => { } },
|
|
69
|
+
}), /pagination publication API is unavailable/);
|
|
70
|
+
});
|
|
71
|
+
//# sourceMappingURL=tui-reload.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tui-reload.test.js","sourceRoot":"","sources":["../../../src/opencode/session-pruning/tui-reload.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { test } from \"node:test\"\nimport { spawnSync } from \"node:child_process\"\nimport { fileURLToPath } from \"node:url\"\nimport { createPruningReload } from \"./tui-reload\"\n\ntest(\"TUI companion coalesces invalidations and rereads after a missed-event reconnect\", async () => {\n const invalidated: string[] = []\n const read: string[] = []\n let release!: () => void\n const pending = new Promise<void>(resolve => { release = resolve })\n const reload = createPruningReload({\n list: () => [{ id: \"s\" }, { id: \"unloaded\" }],\n get: id => id === \"s\" ? { location: { directory: \"/work\" } } : undefined,\n message: {\n list: id => id === \"s\" ? [{}] : [],\n loading: () => false,\n loadMore: async () => assert.fail(\"unexpected pagination\"),\n invalidate: id => { invalidated.push(id) },\n sync: async id => { read.push(id); await pending },\n },\n })\n const event = { type: \"rpc.codenomad.session-pruning.pruned\", location: { directory: \"/work\" }, data: { sessionID: \"s\", messageID: \"m\", revision: \"a\".repeat(64) } }\n reload.event(event); reload.event(event)\n assert.deepEqual(read, [\"s\"])\n release()\n await new Promise(resolve => setImmediate(resolve))\n assert.deepEqual(read, [\"s\", \"s\"])\n reload.event({ type: \"server.connected\" })\n await new Promise(resolve => setImmediate(resolve))\n assert.deepEqual(read, [\"s\", \"s\", \"s\"])\n assert(invalidated.includes(\"unloaded\"))\n reload.dispose()\n reload.event(event)\n assert.equal(read.length, 3)\n})\n\ntest(\"TUI companion rejects foreign locations, malformed events and unknown sessions\", () => {\n const reload = createPruningReload({\n list: () => [], get: id => id === \"s\" ? { location: { directory: \"/work\" } } : undefined,\n message: {\n list: () => [], loading: () => false, loadMore: async () => assert.fail(\"unexpected pagination\"),\n invalidate: () => assert.fail(\"unexpected invalidation\"), sync: async () => assert.fail(\"unexpected read\"),\n },\n })\n reload.event({ type: \"rpc.other.pruned\", data: {} })\n reload.event({ type: \"rpc.codenomad.session-pruning.pruned\", data: {} })\n for (const sessionID of [\"s\", \"other\"]) reload.event({ type: \"rpc.codenomad.session-pruning.pruned\", location: { directory: \"/elsewhere\" }, data: { sessionID, messageID: \"m\", revision: \"a\".repeat(64) } })\n reload.dispose()\n})\n\ntest(\"TUI reload regressions against the installed published Solid cache\", () => {\n // Keep Solid's browser conditions out of the server test process. Resolve the\n // native client from the pinned plugin dependency, not a checkout, global\n // install, copied cache implementation, or a network install during tests.\n const env = { ...process.env }\n delete env.NODE_TEST_CONTEXT\n const result = spawnSync(process.execPath, [\n \"--conditions=browser\", \"--import\", \"tsx\", \"--test\",\n fileURLToPath(new URL(\"./tui-reload.native.ts\", import.meta.url)),\n ], { encoding: \"utf8\", timeout: 30_000, env })\n assert.ifError(result.error)\n assert.equal(result.status, 0, `${result.stdout}\\n${result.stderr}`)\n assert.match(result.stdout, /control: invalidate\\/sync/, \"The child must actually execute its native-cache tests\")\n})\n\ntest(\"TUI companion refuses a cache without public pagination coordination\", () => {\n assert.throws(() => createPruningReload({\n list: () => [], get: () => undefined,\n message: { list: () => [], invalidate: () => {}, sync: async () => {} },\n }), /pagination publication API is unavailable/)\n})\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Plugin } from "@opencode/plugin/tui";
|
|
2
|
+
import { createPruningReload } from "./tui-reload";
|
|
3
|
+
// Companion entry point: no UI replacement, settings mutation or SQL in the TUI.
|
|
4
|
+
export default Plugin.define({
|
|
5
|
+
id: "codenomad-session-pruning-tui",
|
|
6
|
+
setup(ctx) {
|
|
7
|
+
const reload = createPruningReload(ctx.data.session);
|
|
8
|
+
const unsubscribe = ctx.data.listen(({ details }) => reload.event(details));
|
|
9
|
+
return () => { reload.dispose(); unsubscribe(); };
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
//# sourceMappingURL=tui.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tui.js","sourceRoot":"","sources":["../../../src/opencode/session-pruning/tui.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAElD,iFAAiF;AACjF,eAAe,MAAM,CAAC,MAAM,CAAC;IAC3B,EAAE,EAAE,+BAA+B;IACnC,KAAK,CAAC,GAAG;QACP,MAAM,MAAM,GAAG,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACpD,MAAM,WAAW,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAA;QAC3E,OAAO,GAAG,EAAE,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA,CAAC,CAAC,CAAA;IAClD,CAAC;CACF,CAAC,CAAA","sourcesContent":["import { Plugin } from \"@opencode/plugin/tui\"\nimport { createPruningReload } from \"./tui-reload\"\n\n// Companion entry point: no UI replacement, settings mutation or SQL in the TUI.\nexport default Plugin.define({\n id: \"codenomad-session-pruning-tui\",\n setup(ctx) {\n const reload = createPruningReload(ctx.data.session)\n const unsubscribe = ctx.data.listen(({ details }) => reload.event(details))\n return () => { reload.dispose(); unsubscribe() }\n },\n})\n"]}
|