Skip to content

Commit 460806f

Browse files
committed
feat: add Universal Assistant with skill registry and composition
1 parent c6ba20b commit 460806f

36 files changed

Lines changed: 1983 additions & 649 deletions

apps/account/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"preview": "vite preview"
1818
},
1919
"dependencies": {
20-
"@object-ui/i18n": "^3.3.2",
20+
"@object-ui/i18n": "^4.0.1",
2121
"@objectstack/client": "workspace:*",
2222
"@objectstack/client-react": "workspace:*",
2323
"@objectstack/spec": "workspace:*",

apps/dashboard/package.json

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,30 @@
1212
"preview": "vite preview"
1313
},
1414
"dependencies": {
15-
"@object-ui/app-shell": "^3.3.2",
16-
"@object-ui/auth": "^3.3.2",
17-
"@object-ui/collaboration": "^3.3.2",
18-
"@object-ui/components": "^3.3.2",
19-
"@object-ui/core": "^3.3.2",
20-
"@object-ui/data-objectstack": "^3.3.2",
21-
"@object-ui/fields": "^3.3.2",
22-
"@object-ui/i18n": "^3.3.2",
23-
"@object-ui/layout": "^3.3.2",
24-
"@object-ui/mobile": "^3.3.2",
25-
"@object-ui/permissions": "^3.3.2",
26-
"@object-ui/plugin-calendar": "^3.3.2",
27-
"@object-ui/plugin-charts": "^3.3.2",
28-
"@object-ui/plugin-chatbot": "^3.3.2",
29-
"@object-ui/plugin-dashboard": "^3.3.2",
30-
"@object-ui/plugin-detail": "^3.3.2",
31-
"@object-ui/plugin-form": "^3.3.2",
32-
"@object-ui/plugin-grid": "^3.3.2",
33-
"@object-ui/plugin-kanban": "^3.3.2",
34-
"@object-ui/plugin-list": "^3.3.2",
35-
"@object-ui/plugin-report": "^3.3.2",
36-
"@object-ui/plugin-view": "^3.3.2",
37-
"@object-ui/react": "^3.3.2",
38-
"@object-ui/types": "^3.3.2",
15+
"@object-ui/app-shell": "^4.0.1",
16+
"@object-ui/auth": "^4.0.1",
17+
"@object-ui/collaboration": "^4.0.1",
18+
"@object-ui/components": "^4.0.1",
19+
"@object-ui/core": "^4.0.1",
20+
"@object-ui/data-objectstack": "^4.0.1",
21+
"@object-ui/fields": "^4.0.1",
22+
"@object-ui/i18n": "^4.0.1",
23+
"@object-ui/layout": "^4.0.1",
24+
"@object-ui/mobile": "^4.0.1",
25+
"@object-ui/permissions": "^4.0.1",
26+
"@object-ui/plugin-calendar": "^4.0.1",
27+
"@object-ui/plugin-charts": "^4.0.1",
28+
"@object-ui/plugin-chatbot": "^4.0.1",
29+
"@object-ui/plugin-dashboard": "^4.0.1",
30+
"@object-ui/plugin-detail": "^4.0.1",
31+
"@object-ui/plugin-form": "^4.0.1",
32+
"@object-ui/plugin-grid": "^4.0.1",
33+
"@object-ui/plugin-kanban": "^4.0.1",
34+
"@object-ui/plugin-list": "^4.0.1",
35+
"@object-ui/plugin-report": "^4.0.1",
36+
"@object-ui/plugin-view": "^4.0.1",
37+
"@object-ui/react": "^4.0.1",
38+
"@object-ui/types": "^4.0.1",
3939
"clsx": "^2.1.1",
4040
"react": "^19.2.5",
4141
"react-dom": "^19.2.5",

apps/objectos/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"clean": "rm -rf dist node_modules"
1919
},
2020
"dependencies": {
21-
"@example/app-crm": "workspace:*",
21+
"@objectstack/example-crm": "workspace:*",
2222
"@example/app-todo": "workspace:*",
2323
"@hono/node-server": "^2.0.1",
2424
"@libsql/client": "^0.17.3",
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* E2E smoke for the Universal Assistant wiring (Phase E).
5+
*
6+
* Verifies that:
7+
* 1. The Studio page loads and bootstraps MSW + the in-browser ObjectKernel.
8+
* 2. The new ambient-assistant routes (`/api/v1/ai/assistant`,
9+
* `/api/v1/ai/assistant/skills`) are registered and respond with the
10+
* contract the new AiChatPanel relies on.
11+
* 3. The AiChatPanel module that the dev server actually serves contains the
12+
* Universal Assistant exports (no stale Agent-dropdown code).
13+
*
14+
* We intentionally do NOT drive the React UI through the auth flow — Studio
15+
* delegates login to a separate Account SPA, which makes pure-Studio E2E
16+
* coverage of the panel's visible state out of scope. The UI behaviour itself
17+
* is covered by the vitest unit tests in `test/ai-chat-panel.test.tsx`.
18+
*/
19+
20+
import { test, expect } from '@playwright/test';
21+
22+
const STUDIO_PATH = '/?mode=msw';
23+
24+
async function waitForKernel(page: import('@playwright/test').Page) {
25+
await page.goto(STUDIO_PATH, { waitUntil: 'networkidle' });
26+
// Wait for the in-browser kernel to log "Service started" — proves the
27+
// ObjectStack kernel + AI plugin finished bootstrapping inside the page.
28+
await page.waitForFunction(
29+
() => {
30+
const w = window as unknown as { __aiReady?: boolean };
31+
return w.__aiReady === true;
32+
},
33+
null,
34+
{ timeout: 30_000 },
35+
).catch(() => {
36+
/* fall back to a fixed timeout if the marker hook isn't installed */
37+
});
38+
// Allow any post-init redirects (login redirect etc.) to settle.
39+
await page.waitForLoadState('networkidle').catch(() => {});
40+
await page.waitForTimeout(500);
41+
}
42+
43+
test.describe('Universal Assistant — server contract (in-browser kernel)', () => {
44+
test('GET /api/v1/ai/assistant returns { agent, skills, context }', async ({ page }) => {
45+
const consoleLogs: string[] = [];
46+
page.on('console', msg => {
47+
consoleLogs.push(`${msg.type()}: ${msg.text()}`);
48+
});
49+
50+
await waitForKernel(page);
51+
52+
const result = await page.evaluate(async () => {
53+
const res = await fetch(
54+
'/api/v1/ai/assistant?appName=studio&objectName=view',
55+
{ credentials: 'include' },
56+
);
57+
return { status: res.status, body: await res.json().catch(() => null) };
58+
});
59+
60+
// Also try the older /api/v1/ai/agents to confirm AI routes are wired at all
61+
const agentsCheck = await page.evaluate(async () => {
62+
const res = await fetch('/api/v1/ai/agents', { credentials: 'include' });
63+
return { status: res.status };
64+
});
65+
const aiChatCheck = await page.evaluate(async () => {
66+
const res = await fetch('/api/v1/ai/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}', credentials: 'include' });
67+
return { status: res.status };
68+
});
69+
const dataCheck = await page.evaluate(async () => {
70+
const res = await fetch('/api/v1/data/sys_user', { credentials: 'include' });
71+
return { status: res.status };
72+
});
73+
const wellKnown = await page.evaluate(async () => {
74+
const res = await fetch('/.well-known/objectstack', { credentials: 'include' });
75+
return { status: res.status, body: await res.text().catch(() => '') };
76+
});
77+
78+
console.log('--- ALL page console logs ---');
79+
consoleLogs.filter(l => /\[AI\]|\[MSW\]|\[KernelFactory\]|\[Console\]|service.*started|routes registered/i.test(l)).forEach(l => console.log(l));
80+
const kernelDiag = await page.evaluate(async () => {
81+
const w: any = window;
82+
const k = w.__objectStackKernel || w.kernel || null;
83+
if (!k) return { hasKernel: false, keys: Object.keys(w).filter(x => /kernel|stack/i.test(x)) };
84+
return {
85+
hasKernel: true,
86+
services: Array.from(k.services?.keys?.() ?? []),
87+
hasAiRoutes: Array.isArray(k.__aiRoutes),
88+
aiRoutesCount: k.__aiRoutes?.length ?? 0,
89+
aiRoutesPaths: (k.__aiRoutes ?? []).map((r: any) => `${r.method} ${r.path}`).slice(0, 20),
90+
};
91+
});
92+
console.log('--- kernel diag:', JSON.stringify(kernelDiag, null, 2));
93+
console.log('--- /api/v1/ai/chat check:', aiChatCheck);
94+
console.log('--- /api/v1/data/sys_user check:', dataCheck);
95+
console.log('--- /.well-known/objectstack check:', wellKnown);
96+
console.log('--- /api/v1/ai/assistant result:', result);
97+
98+
expect(result.status).toBe(200);
99+
expect(result.body).toMatchObject({
100+
context: expect.objectContaining({ appName: 'studio' }),
101+
});
102+
// `agent` may be null if no default agent is bound; that's fine.
103+
expect(result.body).toHaveProperty('agent');
104+
expect(Array.isArray(result.body.skills)).toBe(true);
105+
});
106+
107+
test('GET /api/v1/ai/assistant/skills returns a skill list', async ({ page }) => {
108+
await waitForKernel(page);
109+
110+
const result = await page.evaluate(async () => {
111+
const res = await fetch('/api/v1/ai/assistant/skills?appName=studio', {
112+
credentials: 'include',
113+
});
114+
return { status: res.status, body: await res.json().catch(() => null) };
115+
});
116+
117+
expect(result.status).toBe(200);
118+
expect(result.body).toHaveProperty('skills');
119+
expect(Array.isArray(result.body.skills)).toBe(true);
120+
});
121+
122+
test('POST /api/v1/ai/assistant/chat accepts the new body shape', async ({ page }) => {
123+
await waitForKernel(page);
124+
125+
const result = await page.evaluate(async () => {
126+
const res = await fetch('/api/v1/ai/assistant/chat', {
127+
method: 'POST',
128+
headers: { 'content-type': 'application/json' },
129+
credentials: 'include',
130+
body: JSON.stringify({
131+
messages: [{ role: 'user', content: 'ping' }],
132+
context: { appName: 'studio', objectName: 'view' },
133+
stream: false,
134+
}),
135+
});
136+
return {
137+
status: res.status,
138+
// The response may be a streamed data-stream; for this contract test
139+
// we only care that the route accepted the new body shape (i.e. did
140+
// not 404 / 400 on the new fields).
141+
contentType: res.headers.get('content-type') ?? '',
142+
};
143+
});
144+
145+
// 200 = responded; 401 = auth-gated (still proves the route exists);
146+
// 500 = handler reached but model not configured. Anything except 404/400
147+
// means the new body shape is accepted by the route layer.
148+
expect([200, 401, 500]).toContain(result.status);
149+
});
150+
});
151+
152+
test.describe('Universal Assistant — bundle wiring', () => {
153+
test('AiChatPanel module exports the new Universal Assistant symbols', async ({ request }) => {
154+
const res = await request.get('/src/components/AiChatPanel.tsx');
155+
expect(res.status()).toBe(200);
156+
const source = await res.text();
157+
158+
// New exports / wiring must be present:
159+
expect(source).toContain('ASSISTANT_CHAT_PATH');
160+
expect(source).toContain('/api/v1/ai/assistant/chat');
161+
expect(source).toContain('useAssistantContext');
162+
expect(source).toContain('useAssistantResolution');
163+
expect(source).toContain('assistant-status');
164+
expect(source).toContain('skill-palette');
165+
166+
// Old Agent-dropdown wiring must be GONE:
167+
expect(source).not.toContain('AGENT_STORAGE_KEY');
168+
expect(source).not.toContain('GENERAL_CHAT_VALUE');
169+
expect(source).not.toContain('loadSelectedAgent');
170+
expect(source).not.toContain('saveSelectedAgent');
171+
});
172+
});

0 commit comments

Comments
 (0)