Skip to content
Draft
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
11 changes: 8 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"//check:tui-compiled": "Check drift on output already generated by build or build:tui; this check never regenerates files. CI and prepublishOnly build first.",
"//test": "Unit shards run small behavioral fixtures. The nightly/check-branch slow-unit lane runs full scale and latency gates in fresh processes once per mode, not once per shard. JSC's optimizing WebAssembly tier stays disabled in tests; production host options are unchanged.",
"dependencies": {
"@cortexkit/common-auth": "^0.38.0",
"@cortexkit/subc-client": "0.11.1",
"@jitl/quickjs-singlefile-cjs-release-asyncify": "0.32.0",
"@opencode-ai/plugin": "^1.17.11",
Expand Down
74 changes: 69 additions & 5 deletions packages/plugin/scripts/build-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const pluginRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const sourceRoot = join(pluginRoot, "src/tui");
const outputRoot = join(pluginRoot, "src/tui-compiled");
const runtimeSpecifiers: Set<string> = new Set(TUI_RUNTIME_SPECIFIERS);
const sidebarBundleName = "quota-sidebar.js";

type TransformSolidSource = (
code: string,
Expand All @@ -23,7 +24,6 @@ type SolidTransformModule = {
transformSolidSource?: TransformSolidSource;
};


/**
* The Solid transform emits every runtime helper AND every control-flow builtin
* (`For`, `Show`, `Index`, `Switch`, `Match`, `ErrorBoundary`, `Suspense`, ...)
Expand Down Expand Up @@ -103,7 +103,9 @@ function redirectBuiltinImports(

function asTransformSolidSource(mod: SolidTransformModule, from: string): TransformSolidSource {
if (typeof mod.transformSolidSource !== "function") {
throw new Error(`@opentui/solid transform loaded from ${from} without transformSolidSource`);
throw new Error(
`@opentui/solid transform loaded from ${from} without transformSolidSource`,
);
}
return mod.transformSolidSource;
}
Expand All @@ -120,7 +122,9 @@ async function resolveSolidTransformPath(): Promise<string> {
const packageJsonUrl = import.meta.resolve(packageJsonSpecifier);
return join(dirname(fileURLToPath(packageJsonUrl)), "scripts/solid-transform.js");
} catch (error) {
errors.push(`import.meta.resolve: ${error instanceof Error ? error.message : String(error)}`);
errors.push(
`import.meta.resolve: ${error instanceof Error ? error.message : String(error)}`,
);
}

try {
Expand Down Expand Up @@ -196,6 +200,61 @@ const transformSolidSource = await loadTransformSolidSource();
const runtimeExportSets = await loadRuntimeExportSets();
const files = await listSourceFiles(sourceRoot);

// OpenTUI does not transform dependency TSX. The two entry paths must ship the
// frame without a second copy of the host's Solid or OpenTUI runtime.
async function bundleSidebar(hostRuntime: boolean): Promise<boolean> {
const outputFile = join(hostRuntime ? outputRoot : sourceRoot, sidebarBundleName);
const result = await Bun.build({
entrypoints: [fileURLToPath(import.meta.resolve("@cortexkit/common-auth/quota-sidebar"))],
target: "bun",
format: "esm",
external: [...runtimeSpecifiers],
plugins: [
{
name: "quota-sidebar-solid",
setup(build) {
build.onResolve({ filter: /^opentui:runtime-module:/ }, (args) => ({
path: args.path,
external: true,
}));
build.onLoad({ filter: /\.tsx$/ }, async (args) => {
const compiled = await transformSolidSource(
await readFile(args.path, "utf8"),
{
filename: args.path,
moduleName: runtimeModuleId("@opentui/solid"),
resolvePath: (specifier) =>
runtimeSpecifiers.has(specifier)
? runtimeModuleId(specifier)
: null,
},
);
const redirected = redirectBuiltinImports(
compiled,
runtimeExportSets,
args.path,
);
return {
contents: hostRuntime
? redirected
: redirected.replace(
/opentui:runtime-module:([^"']+)/g,
(_, id: string) => decodeURIComponent(id),
),
loader: "ts",
};
});
},
},
],
});
if (!result.success) throw new AggregateError(result.logs, "Shared sidebar build failed");
const output = result.outputs[0];
if (result.outputs.length !== 1 || !output)
throw new Error("Expected one shared sidebar bundle");
return writeIfChanged(outputFile, await output.text());
}

// Regenerate in place rather than removing the output tree first. Consumers
// read this directory live: a Parallels VM sharing the checkout caches
// directory lookups, and an rm-then-recreate left it holding a stale negative
Expand All @@ -204,6 +263,10 @@ const files = await listSourceFiles(sourceRoot);
// inode; only outputs whose source is gone are removed.
const expectedOutputs = new Set<string>();
let written = 0;
const sidebarOutput = join(outputRoot, sidebarBundleName);
expectedOutputs.add(sidebarOutput);
if (await bundleSidebar(true)) written++;
const rawSidebarWritten = await bundleSidebar(false);

for (const sourceFile of files) {
const relativePath = relative(sourceRoot, sourceFile);
Expand All @@ -217,7 +280,8 @@ for (const sourceFile of files) {
// the sidebar freezes on its first paint. The virtual ids are required so
// the compiled package binds the host process's single OpenTUI/Solid
// runtime instead of loading a second copy from the plugin package.
if (await compileTsx(transformSolidSource, sourceFile, outputFile, runtimeExportSets)) written++;
if (await compileTsx(transformSolidSource, sourceFile, outputFile, runtimeExportSets))
written++;
} else {
if (await copyPlainTypeScript(sourceFile, outputFile)) written++;
}
Expand All @@ -231,5 +295,5 @@ for (const existing of await listSourceFiles(outputRoot)) {
}

console.log(
`build-tui: checked ${files.length} file(s), wrote ${written}, unchanged ${files.length - written} in ${relative(pluginRoot, outputRoot)}${stale ? ` (removed ${stale} stale)` : ""}`,
`build-tui: checked ${files.length + 1} file(s), wrote ${written}, unchanged ${files.length + 1 - written} in ${relative(pluginRoot, outputRoot)}; raw sidebar wrote ${Number(rawSidebarWritten)}${stale ? ` (removed ${stale} stale)` : ""}`,
);
156 changes: 156 additions & 0 deletions packages/plugin/scripts/render-compiled-sidebar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { mock } from "bun:test";
import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
import { RGBA, rgbToHex } from "@opentui/core";
import { plugin } from "bun";
import { createSignal } from "solid-js";
import type { SidebarSnapshot } from "../src/shared/rpc-types";
import { resolveMagicContextPrefs } from "../src/shared/tui-preferences";
import { runtimeModuleId, TUI_RUNTIME_SPECIFIERS } from "../src/shared/tui-runtime-specifiers";

type RenderCase = {
snapshot: SidebarSnapshot | null;
width?: number;
preferences?: Record<string, unknown>;
changeTheme?: boolean;
matchingBadgeColors?: boolean;
};

const loaded = new Map<string, Record<string, unknown>>();
for (const specifier of TUI_RUNTIME_SPECIFIERS) loaded.set(specifier, await import(specifier));
plugin({
name: "sidebar-render-runtime",
setup(build) {
for (const specifier of TUI_RUNTIME_SPECIFIERS) {
build.module(runtimeModuleId(specifier), () => ({
exports: loaded.get(specifier) ?? {},
loader: "object",
}));
}
},
});

const theme = {
primary: RGBA.fromHex("#ffcc00"),
secondary: RGBA.fromHex("#ffcc00"),
accent: RGBA.fromHex("#ffcc00"),
error: RGBA.fromHex("#ff0000"),
warning: RGBA.fromHex("#ff8800"),
success: RGBA.fromHex("#00ff00"),
info: RGBA.fromHex("#0088ff"),
text: RGBA.fromHex("#ffffff"),
textMuted: RGBA.fromHex("#888888"),
background: RGBA.fromHex("#000000"),
backgroundPanel: RGBA.fromHex("#000000"),
backgroundElement: RGBA.fromHex("#000000"),
backgroundMenu: RGBA.fromHex("#000000"),
border: RGBA.fromHex("#888888"),
borderActive: RGBA.fromHex("#ffffff"),
borderSubtle: RGBA.fromHex("#888888"),
};

let snapshot: SidebarSnapshot | null = null;
let preferences: Record<string, unknown> = {};
// Replace only transport and preference I/O. The shipped slot owns rendering.
mock.module(new URL("../src/tui-compiled/data/context-db.ts", import.meta.url).pathname, () => ({
loadSidebarSnapshot: async () => snapshot,
}));
const prefsUrl = new URL("../src/shared/tui-preferences.ts", import.meta.url).pathname;
const actualPrefs = await import(prefsUrl);
mock.module(prefsUrl, () => ({
...actualPrefs,
readTuiPreferencesFileSync: () => preferences,
watchTuiPreferences: () => () => {},
}));
if (process.argv.includes("--raw-frame")) {
const rawFrame = await import(new URL("../src/tui/quota-sidebar.js", import.meta.url).href);
mock.module(
new URL("../src/tui-compiled/quota-sidebar.js", import.meta.url).pathname,
() => rawFrame,
);
}
const sidebarUrl = new URL("../src/tui-compiled/slots/sidebar-content.tsx", import.meta.url).href;
const { createSidebarContentSlot } = await import(sidebarUrl);
const { testRender } = await import("@opentui/solid");
const inputs = JSON.parse(await Bun.stdin.text()) as RenderCase[];
const results = [];
for (const entry of inputs) {
snapshot = entry.snapshot;
preferences = entry.preferences ?? {};
const prefs = resolveMagicContextPrefs(preferences);
const published = Promise.withResolvers<void>();
const [currentTheme, setTheme] = createSignal(
entry.matchingBadgeColors ? { ...theme, background: theme.accent } : theme,
);
const slot = createSidebarContentSlot({
state: {
path: { directory: "fixture" },
session: { get: () => ({ directory: "fixture" }) },
},
renderer: {
requestRender() {
published.resolve();
},
},
event: { on: () => () => {} },
} as unknown as TuiPluginApi);
try {
const setup = await testRender(
() =>
slot.slots.sidebar_content(
{
theme: {
get current() {
return currentTheme();
},
},
},
{ session_id: "fixture" },
),
{ width: entry.width ?? 42, height: 60 },
);
await Promise.race([
published.promise,
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("snapshot was not published")), 5000),
),
]);
for (let pass = 0; pass < 4; pass++) await setup.renderOnce();
if (entry.changeTheme) {
setTheme({ ...theme, success: RGBA.fromHex("#00aaff") });
for (let pass = 0; pass < 3; pass++) await setup.renderOnce();
}
const spans = setup.captureSpans();
results.push({
frame: setup.captureCharFrame(),
lines: spans.lines.map((line) =>
line.spans.map((span) => ({
text: span.text,
fg: rgbToHex(span.fg),
attributes: span.attributes,
})),
),
collapsed: prefs.collapsed,
});
setup.renderer.destroy();
} catch (error) {
results.push({ error: error instanceof Error ? error.stack : String(error) });
} finally {
slot.dispose();
}
}
process.stdout.write(
process.argv.includes("--frames")
? results
.map((result) =>
result.frame !== undefined
? result.frame
.split("\n")
.map((line) => line.trimEnd())
.join("\n")
.trimEnd()
: result.error,
)
.join("\n\n")
: JSON.stringify(results),
);
process.exit(0);
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,31 @@ export function getMaxMemoryIdForProjects(
return typeof row?.max_id === "number" ? row.max_id : 0;
}

export function countNewMemoriesForM1Union(
db: Database,
projectPaths: readonly string[],
afterId: number,
expiryCutoff: number,
ownIdentities?: readonly string[],
shareCategories?: readonly string[] | null,
): number {
const identities = uniqueValues(projectPaths);
if (identities.length === 0) return 0;
const sharingFilter = buildWorkspaceMemorySqlFilter({
identities,
ownIdentities,
shareCategories,
includeClassificationFields: hasMemoryShareableColumn(db) && hasMemoryScopeColumn(db),
});
const row = db
.prepare(`SELECT COUNT(*) AS count FROM memories
WHERE project_path IN (${sqlPlaceholders(identities)}) AND id > ?
AND status IN ('active', 'permanent')
AND (expires_at IS NULL OR expires_at > ?)${sharingFilter.clause}`)
.get(...identities, afterId, expiryCutoff, ...sharingFilter.params) as { count: number };
return row.count;
}

export function readNewMemoriesForM1Union(
db: Database,
projectPaths: readonly string[],
Expand Down
Loading
Loading