@circuitwall/jarela 2.4.0 → 2.5.0
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -1
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +14 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/dashboard/pricing-task/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/documents/sources/[id]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/documents/sources/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/events/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/agents/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/fill/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/refine/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/turn/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/config/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/secrets/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/packages/defaults/[id]/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/packages/reload/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/packages/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/page-capture/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js +578 -58
- package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/[id]/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/route.js +118 -26
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/tools/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/update/apply/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/watchers/[id]/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/watchers/route.js +1 -1
- package/.next/standalone/.next/server/app/page.js +31202 -30960
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -1
- package/.next/standalone/.next/server/chunks/2471.js +17 -17
- package/.next/standalone/.next/server/chunks/2653.js +80 -1
- package/.next/standalone/.next/server/chunks/3399.js +10695 -10695
- package/.next/standalone/.next/server/chunks/3566.js +1 -1
- package/.next/standalone/.next/server/chunks/4189.js +1 -1
- package/.next/standalone/.next/server/chunks/4508.js +2 -2
- package/.next/standalone/.next/server/chunks/4596.js +28 -4
- package/.next/standalone/.next/server/chunks/46.js +18 -3
- package/.next/standalone/.next/server/chunks/466.js +53 -7
- package/.next/standalone/.next/server/chunks/4955.js +30 -3
- package/.next/standalone/.next/server/chunks/5585.js +28 -4
- package/.next/standalone/.next/server/chunks/6649.js +1 -1
- package/.next/standalone/.next/server/chunks/9164.js +65 -14
- package/.next/standalone/.next/server/chunks/9590.js +18 -3
- package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
- package/.next/standalone/.next/server/middleware.js +18 -3
- package/.next/standalone/.next/server/pages/404.html +14 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/{1395-e4a99912b8f83044.js → 1395-bfe1f3ef760c0f50.js} +54 -8
- package/.next/standalone/.next/static/chunks/app/{layout-9bc0b10127cb7d42.js → layout-7b24ad13faa3944b.js} +69 -2
- package/.next/standalone/.next/static/chunks/app/{page-04748847c6ff866f.js → page-2ef8d75d40eb4311.js} +10661 -10352
- package/.next/standalone/.next/static/css/c160ea862133acd2.css +5 -0
- package/.next/standalone/docs/adoption/changesets.json +2756 -0
- package/.next/standalone/package.json +1 -1
- package/CHANGELOG.md +33 -0
- package/api/client.models.test.ts +10 -0
- package/api/client.startup.test.ts +129 -0
- package/api/client.ts +51 -9
- package/api/thread-metadata.test.ts +72 -0
- package/api/types.ts +6 -3
- package/app/api/v1/providers/[provider]/models/route.test.ts +216 -6
- package/app/api/v1/providers/[provider]/models/route.ts +259 -40
- package/app/api/v1/threads/[thread_id]/route.ts +26 -23
- package/app/globals.css +119 -29
- package/app/layout.tsx +12 -0
- package/components/agents/AgentsPanel.tsx +5 -13
- package/components/chat/ChatView.tsx +9 -18
- package/components/chat/InputBar.tsx +1 -1
- package/components/chat/MessageBubble.tsx +1 -1
- package/components/chat/MessageList.test.tsx +45 -4
- package/components/chat/MessageList.tsx +123 -89
- package/components/chat/ToolList.tsx +88 -84
- package/components/chat/useThreadActivity.test.tsx +2 -2
- package/components/chat/useThreadActivity.ts +2 -2
- package/components/chat/useThreadData.ts +1 -30
- package/components/chat/useThreadMetadataPolling.test.tsx +128 -0
- package/components/chat/useThreadMetadataPolling.ts +50 -0
- package/components/chat/useUserProfileAndAgent.test.tsx +31 -0
- package/components/chat/useUserProfileAndAgent.ts +13 -2
- package/components/dashboard/DashboardPanel.tsx +1 -1
- package/components/dashboard/Series.tsx +34 -21
- package/components/dashboard/SummaryCards.tsx +1 -1
- package/components/layout/AppShell.tsx +22 -7
- package/components/layout/MenuPanel.tsx +14 -24
- package/components/profile/ProfilePanel.tsx +2 -2
- package/components/settings/AppearancePanel.tsx +70 -3
- package/components/settings/SettingsPanel.tsx +2 -2
- package/components/tools/ToolsPanel.tsx +2 -7
- package/components/ui/AppearancePreferences.test.tsx +68 -0
- package/components/ui/BootScreen.test.tsx +44 -0
- package/components/ui/BootScreen.tsx +24 -11
- package/components/ui/NotificationStatus.test.tsx +2 -2
- package/components/ui/NotificationStatus.tsx +5 -5
- package/components/ui/SubTabBar.tsx +2 -3
- package/contexts/ThemeContext.tsx +84 -1
- package/hooks/useAgentSession.test.ts +12 -1
- package/hooks/useAgentSession.ts +6 -4
- package/hooks/useUrlSync.test.ts +13 -0
- package/hooks/useUrlSync.ts +0 -4
- package/lib/db/migrations.test.ts +61 -0
- package/lib/db/migrations.ts +13 -0
- package/lib/embeddings/performance.test.ts +114 -0
- package/lib/stores/dashboard-metrics-cache.test.ts +99 -0
- package/lib/stores/dashboard-metrics.ts +30 -4
- package/lib/stores/message-embedding-jobs.test.ts +27 -0
- package/lib/stores/message-embedding-jobs.ts +5 -3
- package/lib/stores/message-usage.test.ts +21 -1
- package/lib/stores/pricing-cache.test.ts +31 -0
- package/lib/stores/pricing.ts +3 -0
- package/lib/tools/packages/default-packages.test.ts +21 -0
- package/lib/tools/packages/optional-package.test.ts +72 -0
- package/lib/tools/packages/optional-package.ts +33 -4
- package/lib/ui/navigate.test.ts +13 -0
- package/lib/ui/navigate.ts +10 -2
- package/package.json +1 -1
- package/.next/standalone/.next/static/css/2669e8744cc8c212.css +0 -5
- /package/.next/standalone/.next/static/{hwujW4hR1b-r3k5WdMxZO → 544OFJV-q0RC6Gjgje4LT}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{hwujW4hR1b-r3k5WdMxZO → 544OFJV-q0RC6Gjgje4LT}/_ssgManifest.js +0 -0
|
@@ -87,6 +87,33 @@ describe("message embedding jobs", () => {
|
|
|
87
87
|
expect(nextMessageEmbeddingDelayMs(currentTime)).toBeGreaterThan(0);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
it("finds the earliest pending deadline or processing lease without including terminal jobs", () => {
|
|
91
|
+
const nowMs = Date.now();
|
|
92
|
+
expect(nextMessageEmbeddingDelayMs(nowMs)).toBeNull();
|
|
93
|
+
for (const id of ["pending", "processing", "done", "no-lease"]) {
|
|
94
|
+
addMessage(id, "synthetic embedding input");
|
|
95
|
+
enqueueMessageEmbeddingJob(id, "synthetic embedding input");
|
|
96
|
+
}
|
|
97
|
+
const db = getDb();
|
|
98
|
+
db.prepare("UPDATE message_embedding_jobs SET available_at_ms=? WHERE message_id='pending'").run(nowMs + 3000);
|
|
99
|
+
db.prepare("UPDATE message_embedding_jobs SET state='processing',lease_until_ms=? WHERE message_id='processing'")
|
|
100
|
+
.run(nowMs + 1000);
|
|
101
|
+
db.exec("UPDATE message_embedding_jobs SET state='done',available_at_ms=0 WHERE message_id='done'");
|
|
102
|
+
db.exec("UPDATE message_embedding_jobs SET state='processing',lease_until_ms=NULL WHERE message_id='no-lease'");
|
|
103
|
+
expect(nextMessageEmbeddingDelayMs(nowMs)).toBe(1000);
|
|
104
|
+
db.exec("DELETE FROM message_embedding_jobs WHERE message_id='processing'");
|
|
105
|
+
expect(nextMessageEmbeddingDelayMs(nowMs)).toBe(3000);
|
|
106
|
+
db.exec("DELETE FROM message_embedding_jobs WHERE message_id='pending'");
|
|
107
|
+
expect(nextMessageEmbeddingDelayMs(nowMs)).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("returns zero for an already-expired processing lease", () => {
|
|
111
|
+
addMessage("expired", "synthetic expired embedding input");
|
|
112
|
+
enqueueMessageEmbeddingJob("expired", "synthetic expired embedding input");
|
|
113
|
+
getDb().exec("UPDATE message_embedding_jobs SET state='processing',lease_until_ms=1 WHERE message_id='expired'");
|
|
114
|
+
expect(nextMessageEmbeddingDelayMs(100)).toBe(0);
|
|
115
|
+
});
|
|
116
|
+
|
|
90
117
|
it("cascades pending work when its source message is deleted", () => {
|
|
91
118
|
addMessage("msg-4", "message pending embedding");
|
|
92
119
|
enqueueMessageEmbeddingJob("msg-4", "message pending embedding");
|
|
@@ -102,9 +102,11 @@ export function claimMessageEmbeddingJobs(
|
|
|
102
102
|
|
|
103
103
|
export function nextMessageEmbeddingDelayMs(nowMs = Date.now()): number | null {
|
|
104
104
|
const row = getDb().prepare(
|
|
105
|
-
`SELECT MIN(
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
`SELECT MIN(next_at_ms) AS next_at_ms FROM (
|
|
106
|
+
SELECT MIN(available_at_ms) AS next_at_ms FROM message_embedding_jobs WHERE state='pending'
|
|
107
|
+
UNION ALL
|
|
108
|
+
SELECT MIN(lease_until_ms) AS next_at_ms FROM message_embedding_jobs WHERE state='processing'
|
|
109
|
+
)`,
|
|
108
110
|
).get() as { next_at_ms?: number | null } | undefined;
|
|
109
111
|
return typeof row?.next_at_ms === "number" ? Math.max(0, row.next_at_ms - nowMs) : null;
|
|
110
112
|
}
|
|
@@ -6,7 +6,8 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
const tmpRoot = mkdtempSync(join(tmpdir(), "jarela-test-message-usage-"));
|
|
7
7
|
process.env.JARELA_DB_DIR = tmpRoot;
|
|
8
8
|
|
|
9
|
-
const { recordMessageUsage, getMessageUsageByIds } = await import("./message-usage");
|
|
9
|
+
const { recordMessageUsage, getMessageUsageByIds, getLatestMessageUsageForThread } = await import("./message-usage");
|
|
10
|
+
const { getDb } = await import("@/lib/db");
|
|
10
11
|
const getMessageUsage = (id: string) => getMessageUsageByIds([id]).get(id) ?? null;
|
|
11
12
|
|
|
12
13
|
afterAll(() => {
|
|
@@ -14,6 +15,25 @@ afterAll(() => {
|
|
|
14
15
|
});
|
|
15
16
|
|
|
16
17
|
describe("message_usage snapshot store (ADR-0041)", () => {
|
|
18
|
+
it("returns the latest usage for only the requested thread", () => {
|
|
19
|
+
const db = getDb();
|
|
20
|
+
for (const [id, thread, timestamp] of [
|
|
21
|
+
["ordered-old", "ordered-thread", "2026-10-01T00:00:00.000Z"],
|
|
22
|
+
["ordered-other", "other-thread", "2026-10-18T00:00:00.000Z"],
|
|
23
|
+
["ordered-new", "ordered-thread", "2026-10-10T00:00:00.000Z"],
|
|
24
|
+
]) {
|
|
25
|
+
recordMessageUsage({
|
|
26
|
+
message_id: id, thread_id: thread, agent_id: "synthetic", agent_name: "Synthetic",
|
|
27
|
+
provider: "synthetic", model_id: "synthetic", model_config_name: null,
|
|
28
|
+
input_tokens: 1, output_tokens: 1, input_rate_usd_per_mtok: null,
|
|
29
|
+
output_rate_usd_per_mtok: null, cost_usd: 0,
|
|
30
|
+
});
|
|
31
|
+
db.prepare("UPDATE message_usage SET created_at=? WHERE message_id=?").run(timestamp, id);
|
|
32
|
+
}
|
|
33
|
+
expect(getLatestMessageUsageForThread("ordered-thread")?.message_id).toBe("ordered-new");
|
|
34
|
+
expect(getLatestMessageUsageForThread("missing-thread")).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
17
37
|
it("records and reads back a usage snapshot", () => {
|
|
18
38
|
recordMessageUsage({
|
|
19
39
|
message_id: "m1",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({ statSync: vi.fn(), readFileSync: vi.fn() }));
|
|
4
|
+
vi.mock("node:fs", () => mocks);
|
|
5
|
+
vi.mock("./known-rates", () => ({ knownRateModelIds: () => [], knownRateFor: () => null }));
|
|
6
|
+
const { getPricingTables } = await import("./pricing");
|
|
7
|
+
|
|
8
|
+
describe("pricing snapshot read cache", () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("does not read or parse the snapshot again when its mtime is unchanged", () => {
|
|
14
|
+
mocks.statSync.mockReturnValue({ mtimeMs: 100 });
|
|
15
|
+
mocks.readFileSync.mockReturnValue(JSON.stringify({ generated_at: "first", sources: [] }));
|
|
16
|
+
const first = getPricingTables();
|
|
17
|
+
expect(getPricingTables()).toBe(first);
|
|
18
|
+
expect(mocks.statSync).toHaveBeenCalledTimes(2);
|
|
19
|
+
expect(mocks.readFileSync).toHaveBeenCalledTimes(1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("reloads rates when the snapshot changes", () => {
|
|
23
|
+
mocks.statSync.mockReturnValue({ mtimeMs: 200 });
|
|
24
|
+
mocks.readFileSync.mockReturnValue(JSON.stringify({ generated_at: "second", sources: [] }));
|
|
25
|
+
expect(getPricingTables().generatedAt).toBe("second");
|
|
26
|
+
mocks.statSync.mockReturnValue({ mtimeMs: 300 });
|
|
27
|
+
mocks.readFileSync.mockReturnValue(JSON.stringify({ generated_at: "third", sources: [] }));
|
|
28
|
+
expect(getPricingTables().generatedAt).toBe("third");
|
|
29
|
+
expect(mocks.readFileSync).toHaveBeenCalledTimes(2);
|
|
30
|
+
});
|
|
31
|
+
});
|
package/lib/stores/pricing.ts
CHANGED
|
@@ -112,6 +112,9 @@ function readSnapshotSync(): { path: string; snapshot: PricingSnapshot | null; m
|
|
|
112
112
|
try {
|
|
113
113
|
const p = snapshotPath();
|
|
114
114
|
const stat = statSync(p);
|
|
115
|
+
if (cached?.path === p && cached.mtimeMs === stat.mtimeMs) {
|
|
116
|
+
return { path: p, snapshot: null, mtimeMs: stat.mtimeMs };
|
|
117
|
+
}
|
|
115
118
|
const raw = readFileSync(p, "utf8");
|
|
116
119
|
return { path: p, snapshot: JSON.parse(raw) as PricingSnapshot, mtimeMs: stat.mtimeMs };
|
|
117
120
|
} catch {
|
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
registerDefaultPackages,
|
|
15
15
|
_resetDefaultPackages,
|
|
16
16
|
} = await import("./default-packages");
|
|
17
|
+
const { registeredTools, registeredCategory, registeredCapability } = await import("../runtime/registry");
|
|
17
18
|
|
|
18
19
|
const { setPackageDisabled, listDisabledPackages } = await import(
|
|
19
20
|
"@/lib/stores/disabled-packages"
|
|
@@ -52,6 +53,26 @@ describe("default LangChain packages", () => {
|
|
|
52
53
|
expect(listDefaultPackages().every((p) => p.installed === false)).toBe(true);
|
|
53
54
|
});
|
|
54
55
|
|
|
56
|
+
it("registers workspace packages with their declared categories and capabilities", () => {
|
|
57
|
+
const previousPackagesDir = process.env.JARELA_PACKAGES_DIR;
|
|
58
|
+
process.env.JARELA_PACKAGES_DIR = "";
|
|
59
|
+
try {
|
|
60
|
+
registerDefaultPackages();
|
|
61
|
+
|
|
62
|
+
expect(listDefaultPackages().every((p) => p.installed)).toBe(true);
|
|
63
|
+
const tools = registeredTools();
|
|
64
|
+
expect(tools.length).toBeGreaterThan(0);
|
|
65
|
+
for (const tool of tools) {
|
|
66
|
+
expect(registeredCategory(tool.name)).toBeDefined();
|
|
67
|
+
expect(["read", "write", "execute"]).toContain(registeredCapability(tool.name));
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
_resetDefaultPackages();
|
|
71
|
+
if (previousPackagesDir === undefined) delete process.env.JARELA_PACKAGES_DIR;
|
|
72
|
+
else process.env.JARELA_PACKAGES_DIR = previousPackagesDir;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
55
76
|
it("findDefaultPackage returns the descriptor for a known id", () => {
|
|
56
77
|
const d = findDefaultPackage("github");
|
|
57
78
|
expect(d).not.toBeNull();
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { isOptionalPackageInstalled, loadOptionalPackage } from "./optional-package";
|
|
6
|
+
|
|
7
|
+
describe("optional package resolution", () => {
|
|
8
|
+
let managedDir: string;
|
|
9
|
+
let originalPackagesDir: string | undefined;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
originalPackagesDir = process.env.JARELA_PACKAGES_DIR;
|
|
13
|
+
managedDir = mkdtempSync(join(tmpdir(), "jarela-optional-packages-"));
|
|
14
|
+
process.env.JARELA_PACKAGES_DIR = managedDir;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
if (originalPackagesDir === undefined) delete process.env.JARELA_PACKAGES_DIR;
|
|
19
|
+
else process.env.JARELA_PACKAGES_DIR = originalPackagesDir;
|
|
20
|
+
rmSync(managedDir, { recursive: true, force: true });
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("prefers a package installed in the managed packages directory", () => {
|
|
25
|
+
const packageDir = join(managedDir, "node_modules", "fake-optional-package");
|
|
26
|
+
mkdirSync(packageDir, { recursive: true });
|
|
27
|
+
writeFileSync(join(packageDir, "package.json"), JSON.stringify({
|
|
28
|
+
name: "fake-optional-package",
|
|
29
|
+
version: "1.0.0",
|
|
30
|
+
main: "index.cjs",
|
|
31
|
+
}));
|
|
32
|
+
writeFileSync(join(packageDir, "index.cjs"), 'module.exports = { source: "managed" };');
|
|
33
|
+
|
|
34
|
+
expect(loadOptionalPackage("fake-optional-package")).toEqual({ source: "managed" });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it.each([
|
|
38
|
+
"@circuitwall/atlassian-langchain",
|
|
39
|
+
"@circuitwall/github-langchain",
|
|
40
|
+
"@circuitwall/icloud-langchain",
|
|
41
|
+
"@circuitwall/jira-align-langchain",
|
|
42
|
+
"@circuitwall/ms-todo-langchain",
|
|
43
|
+
])("falls back to the app workspace for %s", (packageName) => {
|
|
44
|
+
delete process.env.JARELA_PACKAGES_DIR;
|
|
45
|
+
expect(loadOptionalPackage(packageName)).not.toBeNull();
|
|
46
|
+
expect(isOptionalPackageInstalled(packageName)).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("exposes exports from the workspace fallback", () => {
|
|
50
|
+
delete process.env.JARELA_PACKAGES_DIR;
|
|
51
|
+
const loaded = loadOptionalPackage<{ githubFetch?: unknown }>("@circuitwall/github-langchain");
|
|
52
|
+
|
|
53
|
+
expect(loaded).not.toBeNull();
|
|
54
|
+
expect(typeof loaded?.githubFetch).toBe("function");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("treats an absent optional package as a normal miss", () => {
|
|
58
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
59
|
+
|
|
60
|
+
expect(loadOptionalPackage("@circuitwall/not-installed")).toBeNull();
|
|
61
|
+
expect(error).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("does not treat unrelated app dependencies as installed optional packages", () => {
|
|
65
|
+
expect(loadOptionalPackage("@langchain/core")).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("honors an explicit managed directory even when a workspace package exists", () => {
|
|
69
|
+
expect(loadOptionalPackage("@circuitwall/github-langchain")).toBeNull();
|
|
70
|
+
expect(isOptionalPackageInstalled("@circuitwall/github-langchain")).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -22,12 +22,40 @@ function managedPackagePath(packageName: string): string {
|
|
|
22
22
|
return join(getPackagesDir(), "node_modules", packageName);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
interface ResolvedOptionalPackage {
|
|
26
|
+
req: OptionalRequire;
|
|
27
|
+
path: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveOptionalPackage(packageName: string): ResolvedOptionalPackage | null {
|
|
31
|
+
const managedReq = createRuntimeRequire(join(getPackagesDir(), "_anchor"));
|
|
32
|
+
try {
|
|
33
|
+
return {
|
|
34
|
+
req: managedReq,
|
|
35
|
+
path: managedReq.resolve(managedPackagePath(packageName)),
|
|
36
|
+
};
|
|
37
|
+
} catch {
|
|
38
|
+
// Workspace builds expose bundled @circuitwall packages from the app root.
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (process.env.JARELA_PACKAGES_DIR?.trim()) return null;
|
|
42
|
+
if (!packageName.startsWith("@circuitwall/")) return null;
|
|
43
|
+
|
|
44
|
+
const appReq = createRuntimeRequire(join(process.cwd(), "package.json"));
|
|
45
|
+
try {
|
|
46
|
+
return { req: appReq, path: appReq.resolve(packageName) };
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
25
52
|
export function loadOptionalPackage<T extends AnyModule>(packageName: string): T | null {
|
|
26
53
|
const override = testOverrides.get(packageName);
|
|
27
54
|
if (override) return override as T;
|
|
55
|
+
const resolved = resolveOptionalPackage(packageName);
|
|
56
|
+
if (!resolved) return null;
|
|
28
57
|
try {
|
|
29
|
-
|
|
30
|
-
return req(managedPackagePath(packageName)) as T;
|
|
58
|
+
return resolved.req(resolved.path) as T;
|
|
31
59
|
} catch (err) {
|
|
32
60
|
// Keep the optional boundary non-fatal, but leave the actual load failure
|
|
33
61
|
// visible so missing peer dependencies are diagnosable from server logs.
|
|
@@ -68,9 +96,10 @@ export function optionalFunction<T extends (...args: any[]) => any>(
|
|
|
68
96
|
}
|
|
69
97
|
|
|
70
98
|
export function isOptionalPackageInstalled(packageName: string): boolean {
|
|
99
|
+
const resolved = resolveOptionalPackage(packageName);
|
|
100
|
+
if (!resolved) return false;
|
|
71
101
|
try {
|
|
72
|
-
|
|
73
|
-
req(managedPackagePath(packageName));
|
|
102
|
+
resolved.req(resolved.path);
|
|
74
103
|
return true;
|
|
75
104
|
} catch (err) {
|
|
76
105
|
console.error(`[optional-package] failed to load ${packageName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parseHref } from "./navigate";
|
|
3
|
+
|
|
4
|
+
describe("parseHref legacy Knowledge routes", () => {
|
|
5
|
+
it.each([
|
|
6
|
+
"/?tab=documents",
|
|
7
|
+
"/?tab=memory",
|
|
8
|
+
"/?tab=tools&item=documents",
|
|
9
|
+
"/?tab=tools&item=memory",
|
|
10
|
+
])("routes %s to Knowledge", (href) => {
|
|
11
|
+
expect(parseHref(href)).toMatchObject({ tab: "memory", item: undefined, external: false });
|
|
12
|
+
});
|
|
13
|
+
});
|
package/lib/ui/navigate.ts
CHANGED
|
@@ -40,8 +40,16 @@ export function parseHref(input: string | null | undefined): ParsedHref {
|
|
|
40
40
|
|
|
41
41
|
const params = new URLSearchParams(queryStr);
|
|
42
42
|
const tabRaw = params.get("tab");
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
let tab = TABS.includes(tabRaw as Tab) ? (tabRaw as Tab) : undefined;
|
|
44
|
+
let item = params.get("item") ?? undefined;
|
|
45
|
+
if (
|
|
46
|
+
tab === "documents" ||
|
|
47
|
+
tab === "memory" ||
|
|
48
|
+
(tab === "tools" && (item === "memory" || item === "documents"))
|
|
49
|
+
) {
|
|
50
|
+
tab = "memory";
|
|
51
|
+
item = undefined;
|
|
52
|
+
}
|
|
45
53
|
const thread = params.get("thread") ?? undefined;
|
|
46
54
|
const agent = params.get("agent") ?? undefined;
|
|
47
55
|
return { tab, item, thread, agent, hash, external: false };
|
package/package.json
CHANGED