@cryptiklemur/lattice 1.14.2 → 1.16.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/.github/workflows/ci.yml +121 -0
- package/bun.lock +14 -1
- package/client/src/App.tsx +2 -0
- package/client/src/components/analytics/ChartCard.tsx +6 -10
- package/client/src/components/analytics/QuickStats.tsx +3 -3
- package/client/src/components/analytics/charts/NodeFleetOverview.tsx +1 -1
- package/client/src/components/chat/ChatView.tsx +119 -7
- package/client/src/components/chat/Message.tsx +41 -6
- package/client/src/components/chat/PromptQuestion.tsx +4 -4
- package/client/src/components/chat/TodoCard.tsx +2 -2
- package/client/src/components/chat/ToolResultRenderer.tsx +6 -2
- package/client/src/components/dashboard/DashboardView.tsx +2 -0
- package/client/src/components/mesh/PairingDialog.tsx +6 -17
- package/client/src/components/project-settings/ProjectEnvironment.tsx +1 -1
- package/client/src/components/project-settings/ProjectMemory.tsx +10 -19
- package/client/src/components/project-settings/ProjectRules.tsx +3 -3
- package/client/src/components/project-settings/ProjectSkills.tsx +2 -2
- package/client/src/components/settings/BudgetSettings.tsx +161 -0
- package/client/src/components/settings/Environment.tsx +1 -1
- package/client/src/components/settings/SettingsView.tsx +3 -0
- package/client/src/components/sidebar/AddProjectModal.tsx +7 -11
- package/client/src/components/sidebar/NodeSettingsModal.tsx +6 -11
- package/client/src/components/sidebar/ProjectRail.tsx +11 -1
- package/client/src/components/sidebar/SessionList.tsx +33 -12
- package/client/src/components/sidebar/SettingsSidebar.tsx +2 -1
- package/client/src/components/sidebar/Sidebar.tsx +152 -2
- package/client/src/components/sidebar/UserIsland.tsx +76 -37
- package/client/src/components/ui/IconPicker.tsx +9 -36
- package/client/src/components/ui/KeyboardShortcuts.tsx +129 -0
- package/client/src/components/ui/Toast.tsx +22 -2
- package/client/src/components/workspace/BookmarksView.tsx +156 -0
- package/client/src/components/workspace/TabBar.tsx +34 -5
- package/client/src/components/workspace/TaskEditModal.tsx +7 -2
- package/client/src/components/workspace/WorkspaceView.tsx +29 -6
- package/client/src/hooks/useBookmarks.ts +57 -0
- package/client/src/hooks/useFocusTrap.ts +72 -0
- package/client/src/hooks/useProjects.ts +1 -1
- package/client/src/hooks/useSession.ts +38 -1
- package/client/src/hooks/useTimeTick.ts +35 -0
- package/client/src/hooks/useVoiceRecorder.ts +17 -3
- package/client/src/hooks/useWorkspace.ts +10 -1
- package/client/src/router.tsx +6 -11
- package/client/src/stores/bookmarks.ts +45 -0
- package/client/src/stores/session.ts +24 -0
- package/client/src/stores/sidebar.ts +2 -2
- package/client/src/stores/workspace.ts +114 -3
- package/client/src/vite-env.d.ts +6 -0
- package/client/tsconfig.json +4 -0
- package/package.json +2 -1
- package/playwright.config.ts +19 -0
- package/server/package.json +2 -0
- package/server/src/analytics/engine.ts +43 -9
- package/server/src/daemon.ts +11 -7
- package/server/src/handlers/bookmarks.ts +50 -0
- package/server/src/handlers/chat.ts +64 -0
- package/server/src/handlers/fs.ts +1 -1
- package/server/src/handlers/memory.ts +1 -1
- package/server/src/handlers/mesh.ts +1 -1
- package/server/src/handlers/project-settings.ts +2 -2
- package/server/src/handlers/session.ts +12 -11
- package/server/src/handlers/settings.ts +5 -2
- package/server/src/handlers/skills.ts +1 -1
- package/server/src/logger.ts +12 -0
- package/server/src/mesh/connector.ts +7 -6
- package/server/src/project/bookmarks.ts +83 -0
- package/server/src/project/context-breakdown.ts +1 -1
- package/server/src/project/registry.ts +5 -5
- package/server/src/project/sdk-bridge.ts +77 -6
- package/server/src/project/session.ts +6 -5
- package/server/src/ws/router.ts +5 -4
- package/server/tsconfig.json +4 -0
- package/shared/src/messages.ts +53 -2
- package/shared/src/models.ts +17 -1
- package/shared/src/project-settings.ts +0 -1
- package/shared/tsconfig.json +4 -0
- package/tests/accessibility.spec.ts +77 -0
- package/tests/keyboard-shortcuts.spec.ts +74 -0
- package/tests/message-actions.spec.ts +112 -0
- package/tests/onboarding.spec.ts +72 -0
- package/tests/session-flow.spec.ts +117 -0
- package/tests/session-preview.spec.ts +83 -0
|
@@ -11,10 +11,11 @@ import { randomUUID } from "node:crypto";
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import type { HistoryMessage, SessionPreview, SessionSummary } from "@lattice/shared";
|
|
13
13
|
import { loadConfig } from "../config";
|
|
14
|
+
import { log } from "../logger";
|
|
14
15
|
|
|
15
16
|
function getProjectPath(projectSlug: string): string | null {
|
|
16
17
|
var config = loadConfig();
|
|
17
|
-
var project = config.projects.find(function (p) { return p.slug === projectSlug; });
|
|
18
|
+
var project = config.projects.find(function (p: typeof config.projects[number]) { return p.slug === projectSlug; });
|
|
18
19
|
return project ? project.path : null;
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -417,7 +418,7 @@ export async function listSessions(projectSlug: string): Promise<SessionSummary[
|
|
|
417
418
|
summaries.sort(function (a, b) { return b.updatedAt - a.updatedAt; });
|
|
418
419
|
return summaries;
|
|
419
420
|
} catch (err) {
|
|
420
|
-
|
|
421
|
+
log.session("Failed to list SDK sessions: %O", err);
|
|
421
422
|
return [];
|
|
422
423
|
}
|
|
423
424
|
}
|
|
@@ -442,7 +443,7 @@ export async function loadSessionHistory(projectSlug: string, sessionId: string)
|
|
|
442
443
|
var messages = await getSessionMessages(sessionId, options);
|
|
443
444
|
return convertSessionMessages(messages);
|
|
444
445
|
} catch (err) {
|
|
445
|
-
|
|
446
|
+
log.session("Failed to load session history: %O", err);
|
|
446
447
|
return [];
|
|
447
448
|
}
|
|
448
449
|
}
|
|
@@ -467,7 +468,7 @@ export async function renameSession(projectSlug: string, sessionId: string, titl
|
|
|
467
468
|
await sdkRenameSession(sessionId, title, options);
|
|
468
469
|
return true;
|
|
469
470
|
} catch (err) {
|
|
470
|
-
|
|
471
|
+
log.session("Failed to rename session: %O", err);
|
|
471
472
|
return false;
|
|
472
473
|
}
|
|
473
474
|
}
|
|
@@ -489,7 +490,7 @@ export async function deleteSession(projectSlug: string, sessionId: string): Pro
|
|
|
489
490
|
unlinkSync(sessionFile);
|
|
490
491
|
return true;
|
|
491
492
|
} catch (err) {
|
|
492
|
-
|
|
493
|
+
log.session("Failed to delete session: %O", err);
|
|
493
494
|
return false;
|
|
494
495
|
}
|
|
495
496
|
}
|
package/server/src/ws/router.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ClientMessage } from "@lattice/shared";
|
|
2
2
|
import { sendTo } from "./broadcast";
|
|
3
|
+
import { log } from "../logger";
|
|
3
4
|
|
|
4
5
|
var _registry: typeof import("../project/registry") | null = null;
|
|
5
6
|
var _connector: typeof import("../mesh/connector") | null = null;
|
|
@@ -81,18 +82,18 @@ export function routeMessage(clientId: string, message: ClientMessage): void {
|
|
|
81
82
|
if (result && typeof result.then === "function") {
|
|
82
83
|
result.then(undefined, function (err: unknown) {
|
|
83
84
|
var stack = err instanceof Error ? err.stack : String(err);
|
|
84
|
-
|
|
85
|
+
log.ws("Async handler error for %s: %s", message.type, stack);
|
|
85
86
|
sendTo(clientId, { type: "chat:error", message: "Internal server error processing " + message.type });
|
|
86
87
|
});
|
|
87
88
|
}
|
|
88
89
|
} catch (err) {
|
|
89
90
|
var stack = err instanceof Error ? (err as Error).stack : String(err);
|
|
90
|
-
|
|
91
|
+
log.ws("Handler error for %s: %s", message.type, stack);
|
|
91
92
|
sendTo(clientId, { type: "chat:error", message: "Internal server error processing " + message.type });
|
|
92
93
|
}
|
|
93
94
|
return;
|
|
94
95
|
}
|
|
95
|
-
|
|
96
|
+
log.ws("No handler for message type: %s", message.type);
|
|
96
97
|
sendTo(clientId, { type: "error", message: `Unknown message type: ${message.type}` });
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -112,7 +113,7 @@ function proxyMessage(clientId: string, nodeId: string, projectSlug: string, mes
|
|
|
112
113
|
try {
|
|
113
114
|
getProxy().proxyToRemoteNode(nodeId, projectSlug, clientId, message);
|
|
114
115
|
} catch (err) {
|
|
115
|
-
|
|
116
|
+
log.ws("Failed to proxy message: %O", err);
|
|
116
117
|
sendTo(clientId, { type: "chat:error", message: "Failed to proxy message to remote node" });
|
|
117
118
|
}
|
|
118
119
|
}
|
package/server/tsconfig.json
CHANGED
package/shared/src/messages.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
LatticeConfig,
|
|
6
6
|
LoopStatus,
|
|
7
7
|
MarketplaceSkill,
|
|
8
|
+
MessageBookmark,
|
|
8
9
|
NodeInfo,
|
|
9
10
|
ProjectInfo,
|
|
10
11
|
ScheduledTask,
|
|
@@ -394,6 +395,10 @@ export interface SessionStopExternalMessage {
|
|
|
394
395
|
sessionId: string;
|
|
395
396
|
}
|
|
396
397
|
|
|
398
|
+
export interface BudgetOverrideMessage {
|
|
399
|
+
type: "budget:override";
|
|
400
|
+
}
|
|
401
|
+
|
|
397
402
|
export interface AnalyticsRequestMessage {
|
|
398
403
|
type: "analytics:request";
|
|
399
404
|
requestId: string;
|
|
@@ -418,6 +423,26 @@ export interface AnalyticsErrorMessage {
|
|
|
418
423
|
message: string;
|
|
419
424
|
}
|
|
420
425
|
|
|
426
|
+
export interface BookmarkListMessage {
|
|
427
|
+
type: "bookmark:list";
|
|
428
|
+
projectSlug?: string;
|
|
429
|
+
sessionId?: string;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export interface BookmarkAddMessage {
|
|
433
|
+
type: "bookmark:add";
|
|
434
|
+
sessionId: string;
|
|
435
|
+
projectSlug: string;
|
|
436
|
+
messageUuid: string;
|
|
437
|
+
messageText: string;
|
|
438
|
+
messageType: "user" | "assistant";
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export interface BookmarkRemoveMessage {
|
|
442
|
+
type: "bookmark:remove";
|
|
443
|
+
id: string;
|
|
444
|
+
}
|
|
445
|
+
|
|
421
446
|
export type ClientMessage =
|
|
422
447
|
| SessionCreateMessage
|
|
423
448
|
| SessionActivateMessage
|
|
@@ -475,7 +500,11 @@ export type ClientMessage =
|
|
|
475
500
|
| EditorDetectMessage
|
|
476
501
|
| ChatPromptResponseMessage
|
|
477
502
|
| AnalyticsRequestMessage
|
|
478
|
-
| SessionPreviewRequestMessage
|
|
503
|
+
| SessionPreviewRequestMessage
|
|
504
|
+
| BookmarkListMessage
|
|
505
|
+
| BookmarkAddMessage
|
|
506
|
+
| BookmarkRemoveMessage
|
|
507
|
+
| BudgetOverrideMessage;
|
|
479
508
|
|
|
480
509
|
export interface SessionListMessage {
|
|
481
510
|
type: "session:list";
|
|
@@ -812,6 +841,12 @@ export interface BrowseSuggestionsResultMessage {
|
|
|
812
841
|
}>;
|
|
813
842
|
}
|
|
814
843
|
|
|
844
|
+
export interface BookmarkListResultMessage {
|
|
845
|
+
type: "bookmark:list_result";
|
|
846
|
+
scope: "session" | "all";
|
|
847
|
+
bookmarks: MessageBookmark[];
|
|
848
|
+
}
|
|
849
|
+
|
|
815
850
|
export type ServerMessage =
|
|
816
851
|
| SessionListMessage
|
|
817
852
|
| SessionCreatedMessage
|
|
@@ -875,7 +910,23 @@ export type ServerMessage =
|
|
|
875
910
|
| ChatPlanModeMessage
|
|
876
911
|
| AnalyticsDataMessage
|
|
877
912
|
| AnalyticsErrorMessage
|
|
878
|
-
| SessionPreviewMessage
|
|
913
|
+
| SessionPreviewMessage
|
|
914
|
+
| BookmarkListResultMessage
|
|
915
|
+
| BudgetStatusMessage
|
|
916
|
+
| BudgetExceededMessage;
|
|
917
|
+
|
|
918
|
+
export interface BudgetStatusMessage {
|
|
919
|
+
type: "budget:status";
|
|
920
|
+
dailySpend: number;
|
|
921
|
+
dailyLimit: number;
|
|
922
|
+
enforcement: "warning" | "soft-block" | "hard-block";
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
export interface BudgetExceededMessage {
|
|
926
|
+
type: "budget:exceeded";
|
|
927
|
+
dailySpend: number;
|
|
928
|
+
dailyLimit: number;
|
|
929
|
+
}
|
|
879
930
|
|
|
880
931
|
export interface MeshHelloMessage {
|
|
881
932
|
type: "mesh:hello";
|
package/shared/src/models.ts
CHANGED
|
@@ -58,8 +58,10 @@ export interface Attachment {
|
|
|
58
58
|
lineCount?: number;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export type HistoryMessageType = "user" | "assistant" | "tool_start" | "tool_result" | "permission_request" | "prompt_question" | "todo_update";
|
|
62
|
+
|
|
61
63
|
export interface HistoryMessage {
|
|
62
|
-
type:
|
|
64
|
+
type: HistoryMessageType;
|
|
63
65
|
uuid?: string;
|
|
64
66
|
text?: string;
|
|
65
67
|
toolId?: string;
|
|
@@ -121,6 +123,10 @@ export interface LatticeConfig {
|
|
|
121
123
|
};
|
|
122
124
|
setupComplete?: boolean;
|
|
123
125
|
wsl?: boolean | "auto";
|
|
126
|
+
costBudget?: {
|
|
127
|
+
dailyLimit: number;
|
|
128
|
+
enforcement: "warning" | "soft-block" | "hard-block";
|
|
129
|
+
};
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
export interface StickyNote {
|
|
@@ -158,6 +164,16 @@ export interface MarketplaceSkill {
|
|
|
158
164
|
installs: number;
|
|
159
165
|
}
|
|
160
166
|
|
|
167
|
+
export interface MessageBookmark {
|
|
168
|
+
id: string;
|
|
169
|
+
sessionId: string;
|
|
170
|
+
projectSlug: string;
|
|
171
|
+
messageUuid: string;
|
|
172
|
+
messageText: string;
|
|
173
|
+
messageType: "user" | "assistant";
|
|
174
|
+
createdAt: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
161
177
|
export interface LoopStatus {
|
|
162
178
|
id: string;
|
|
163
179
|
projectSlug: string;
|
package/shared/tsconfig.json
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
test.beforeEach(async function ({ page }) {
|
|
4
|
+
await page.evaluate(function () {
|
|
5
|
+
if ("serviceWorker" in navigator) {
|
|
6
|
+
navigator.serviceWorker.getRegistrations().then(function (registrations) {
|
|
7
|
+
registrations.forEach(function (r) { r.unregister(); });
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
caches.keys().then(function (names) {
|
|
11
|
+
names.forEach(function (name) { caches.delete(name); });
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test.describe("Focus and modal behavior", function () {
|
|
17
|
+
test("Add Project modal traps focus", async function ({ page }) {
|
|
18
|
+
await page.goto("/");
|
|
19
|
+
await page.waitForSelector("[title='Lattice Dashboard']", { timeout: 10000 });
|
|
20
|
+
|
|
21
|
+
var addButton = page.locator("button[title='Add project']");
|
|
22
|
+
await expect(addButton).toBeVisible({ timeout: 5000 });
|
|
23
|
+
await addButton.click();
|
|
24
|
+
await page.waitForTimeout(500);
|
|
25
|
+
|
|
26
|
+
var modal = page.locator("[role='dialog'][aria-label='Add Project']");
|
|
27
|
+
await expect(modal).toBeVisible({ timeout: 5000 });
|
|
28
|
+
|
|
29
|
+
var pathInput = modal.locator("#project-path");
|
|
30
|
+
await expect(pathInput).toBeFocused();
|
|
31
|
+
|
|
32
|
+
var focusableElements = modal.locator(
|
|
33
|
+
"input, button, [tabindex]:not([tabindex='-1'])"
|
|
34
|
+
);
|
|
35
|
+
var focusableCount = await focusableElements.count();
|
|
36
|
+
|
|
37
|
+
for (var i = 0; i < focusableCount + 2; i++) {
|
|
38
|
+
await page.keyboard.press("Tab");
|
|
39
|
+
await page.waitForTimeout(50);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var activeElement = page.locator(":focus");
|
|
43
|
+
var activeInModal = await modal.locator(":focus").count();
|
|
44
|
+
expect(activeInModal).toBeGreaterThan(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("Escape closes the Add Project modal", async function ({ page }) {
|
|
48
|
+
await page.goto("/");
|
|
49
|
+
await page.waitForSelector("[title='Lattice Dashboard']", { timeout: 10000 });
|
|
50
|
+
|
|
51
|
+
var addButton = page.locator("button[title='Add project']");
|
|
52
|
+
await addButton.click();
|
|
53
|
+
await page.waitForTimeout(500);
|
|
54
|
+
|
|
55
|
+
var modal = page.locator("[role='dialog'][aria-label='Add Project']");
|
|
56
|
+
await expect(modal).toBeVisible({ timeout: 5000 });
|
|
57
|
+
|
|
58
|
+
await page.keyboard.press("Escape");
|
|
59
|
+
await page.waitForTimeout(300);
|
|
60
|
+
|
|
61
|
+
await expect(modal).not.toBeVisible();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("connection status dot is present on the Lattice logo", async function ({ page }) {
|
|
65
|
+
await page.goto("/");
|
|
66
|
+
await page.waitForSelector("[title='Lattice Dashboard']", { timeout: 10000 });
|
|
67
|
+
|
|
68
|
+
var dashboardButton = page.locator("[title='Lattice Dashboard']");
|
|
69
|
+
await expect(dashboardButton).toBeVisible();
|
|
70
|
+
|
|
71
|
+
var statusDot = dashboardButton.locator("div.rounded-full.border-\\[1\\.5px\\]");
|
|
72
|
+
await expect(statusDot).toBeVisible();
|
|
73
|
+
|
|
74
|
+
var dotClasses = await statusDot.getAttribute("class");
|
|
75
|
+
expect(dotClasses).toMatch(/bg-success|bg-warning|bg-error/);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
test.beforeEach(async function ({ page }) {
|
|
4
|
+
await page.evaluate(function () {
|
|
5
|
+
if ("serviceWorker" in navigator) {
|
|
6
|
+
navigator.serviceWorker.getRegistrations().then(function (registrations) {
|
|
7
|
+
registrations.forEach(function (r) { r.unregister(); });
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
caches.keys().then(function (names) {
|
|
11
|
+
names.forEach(function (name) { caches.delete(name); });
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test.describe("Keyboard interactions", function () {
|
|
17
|
+
test("pressing ? opens the shortcuts overlay", async function ({ page }) {
|
|
18
|
+
await page.goto("/");
|
|
19
|
+
await page.waitForSelector("[title='Lattice Dashboard']", { timeout: 10000 });
|
|
20
|
+
|
|
21
|
+
await page.keyboard.press("?");
|
|
22
|
+
await page.waitForTimeout(300);
|
|
23
|
+
|
|
24
|
+
var shortcutsHeading = page.locator("text='Keyboard Shortcuts'");
|
|
25
|
+
await expect(shortcutsHeading).toBeVisible({ timeout: 5000 });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("pressing Escape closes the shortcuts overlay", async function ({ page }) {
|
|
29
|
+
await page.goto("/");
|
|
30
|
+
await page.waitForSelector("[title='Lattice Dashboard']", { timeout: 10000 });
|
|
31
|
+
|
|
32
|
+
await page.keyboard.press("?");
|
|
33
|
+
await page.waitForTimeout(300);
|
|
34
|
+
|
|
35
|
+
var shortcutsHeading = page.locator("text='Keyboard Shortcuts'");
|
|
36
|
+
await expect(shortcutsHeading).toBeVisible({ timeout: 5000 });
|
|
37
|
+
|
|
38
|
+
await page.keyboard.press("Escape");
|
|
39
|
+
await page.waitForTimeout(300);
|
|
40
|
+
|
|
41
|
+
await expect(shortcutsHeading).not.toBeVisible();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("Ctrl+K opens the command palette", async function ({ page }) {
|
|
45
|
+
await page.goto("/");
|
|
46
|
+
await page.waitForSelector("[title='Lattice Dashboard']", { timeout: 10000 });
|
|
47
|
+
|
|
48
|
+
await page.keyboard.press("Control+k");
|
|
49
|
+
await page.waitForTimeout(300);
|
|
50
|
+
|
|
51
|
+
var paletteInput = page.locator("input[placeholder*='Search']").or(
|
|
52
|
+
page.locator("[role='dialog'] input[type='text']")
|
|
53
|
+
);
|
|
54
|
+
await expect(paletteInput.first()).toBeVisible({ timeout: 5000 });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("Escape closes the command palette", async function ({ page }) {
|
|
58
|
+
await page.goto("/");
|
|
59
|
+
await page.waitForSelector("[title='Lattice Dashboard']", { timeout: 10000 });
|
|
60
|
+
|
|
61
|
+
await page.keyboard.press("Control+k");
|
|
62
|
+
await page.waitForTimeout(300);
|
|
63
|
+
|
|
64
|
+
var paletteInput = page.locator("input[placeholder*='Search']").or(
|
|
65
|
+
page.locator("[role='dialog'] input[type='text']")
|
|
66
|
+
);
|
|
67
|
+
await expect(paletteInput.first()).toBeVisible({ timeout: 5000 });
|
|
68
|
+
|
|
69
|
+
await page.keyboard.press("Escape");
|
|
70
|
+
await page.waitForTimeout(300);
|
|
71
|
+
|
|
72
|
+
await expect(paletteInput.first()).not.toBeVisible();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
test.beforeEach(async function ({ page }) {
|
|
4
|
+
await page.evaluate(function () {
|
|
5
|
+
if ("serviceWorker" in navigator) {
|
|
6
|
+
navigator.serviceWorker.getRegistrations().then(function (registrations) {
|
|
7
|
+
registrations.forEach(function (r) { r.unregister(); });
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
caches.keys().then(function (names) {
|
|
11
|
+
names.forEach(function (name) { caches.delete(name); });
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test.describe("Chat message hover actions", function () {
|
|
17
|
+
test.beforeEach(async function ({ page }) {
|
|
18
|
+
await page.goto("/");
|
|
19
|
+
await page.waitForSelector("[title='Lattice Dashboard']", { timeout: 10000 });
|
|
20
|
+
|
|
21
|
+
var projectButtons = page.locator("button:has(svg[aria-hidden='true'])").filter({
|
|
22
|
+
has: page.locator("text=/^[A-Z]{2,3}$/"),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
var count = await projectButtons.count();
|
|
26
|
+
if (count === 0) {
|
|
27
|
+
test.skip(true, "No projects available to test");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await projectButtons.first().click();
|
|
32
|
+
await page.waitForTimeout(1500);
|
|
33
|
+
|
|
34
|
+
var sessionButtons = page.locator("button[aria-label^='Session:']");
|
|
35
|
+
var sessionCount = await sessionButtons.count();
|
|
36
|
+
|
|
37
|
+
if (sessionCount === 0) {
|
|
38
|
+
test.skip(true, "No sessions available to test");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await sessionButtons.first().click();
|
|
43
|
+
await page.waitForTimeout(2000);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("hovering a message reveals Copy and New Session buttons", async function ({ page }) {
|
|
47
|
+
var messages = page.locator(".group\\/msg");
|
|
48
|
+
var messageCount = await messages.count();
|
|
49
|
+
|
|
50
|
+
if (messageCount === 0) {
|
|
51
|
+
test.skip(true, "No messages available to test");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
var targetMessage = messages.first();
|
|
56
|
+
await targetMessage.hover();
|
|
57
|
+
await page.waitForTimeout(300);
|
|
58
|
+
|
|
59
|
+
var copyButton = targetMessage.locator("button[title*='Copy']");
|
|
60
|
+
await expect(copyButton).toBeVisible({ timeout: 5000 });
|
|
61
|
+
|
|
62
|
+
var newSessionButton = targetMessage.locator("button[title='Start new session with this message']");
|
|
63
|
+
var hasNewSession = await newSessionButton.count();
|
|
64
|
+
if (hasNewSession > 0) {
|
|
65
|
+
await expect(newSessionButton).toBeVisible();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("clicking New Session creates a session with prefilled input", async function ({ page }) {
|
|
70
|
+
var userMessages = page.locator(".chat.chat-end .group\\/msg, .chat-end.group\\/msg");
|
|
71
|
+
var userMessageCount = await userMessages.count();
|
|
72
|
+
|
|
73
|
+
if (userMessageCount === 0) {
|
|
74
|
+
var allMessages = page.locator(".group\\/msg");
|
|
75
|
+
var allCount = await allMessages.count();
|
|
76
|
+
if (allCount === 0) {
|
|
77
|
+
test.skip(true, "No messages available to test");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await allMessages.first().hover();
|
|
82
|
+
await page.waitForTimeout(300);
|
|
83
|
+
|
|
84
|
+
var newSessionBtn = allMessages.first().locator("button[title='Start new session with this message']");
|
|
85
|
+
if (await newSessionBtn.count() === 0) {
|
|
86
|
+
test.skip(true, "No messages with New Session action available");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await newSessionBtn.click();
|
|
91
|
+
} else {
|
|
92
|
+
await userMessages.first().hover();
|
|
93
|
+
await page.waitForTimeout(300);
|
|
94
|
+
|
|
95
|
+
var newSessionBtn2 = userMessages.first().locator("button[title='Start new session with this message']");
|
|
96
|
+
if (await newSessionBtn2.count() === 0) {
|
|
97
|
+
test.skip(true, "New Session button not found on user message");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await newSessionBtn2.click();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await page.waitForTimeout(2000);
|
|
105
|
+
|
|
106
|
+
var textarea = page.locator("textarea");
|
|
107
|
+
if (await textarea.count() > 0) {
|
|
108
|
+
var value = await textarea.inputValue();
|
|
109
|
+
expect(value.length).toBeGreaterThan(0);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
test.beforeEach(async function ({ page }) {
|
|
4
|
+
await page.evaluate(function () {
|
|
5
|
+
if ("serviceWorker" in navigator) {
|
|
6
|
+
navigator.serviceWorker.getRegistrations().then(function (registrations) {
|
|
7
|
+
registrations.forEach(function (r) { r.unregister(); });
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
caches.keys().then(function (names) {
|
|
11
|
+
names.forEach(function (name) { caches.delete(name); });
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test.describe("Onboarding - new user flow", function () {
|
|
17
|
+
test("dashboard loads with project cards", async function ({ page }) {
|
|
18
|
+
await page.goto("/");
|
|
19
|
+
await page.waitForSelector("[title='Lattice Dashboard']", { timeout: 10000 });
|
|
20
|
+
|
|
21
|
+
var dashboardButton = page.locator("[title='Lattice Dashboard']");
|
|
22
|
+
await expect(dashboardButton).toBeVisible();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("clicking a project shows sessions list in sidebar", async function ({ page }) {
|
|
26
|
+
await page.goto("/");
|
|
27
|
+
await page.waitForSelector("[title='Lattice Dashboard']", { timeout: 10000 });
|
|
28
|
+
|
|
29
|
+
var projectButtons = page.locator("button:has(svg[aria-hidden='true'])").filter({
|
|
30
|
+
has: page.locator("text=/^[A-Z]{2,3}$/"),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
var count = await projectButtons.count();
|
|
34
|
+
if (count === 0) {
|
|
35
|
+
test.skip(true, "No projects available to test");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await projectButtons.first().click();
|
|
40
|
+
await page.waitForTimeout(1500);
|
|
41
|
+
|
|
42
|
+
var sessionLabels = page.locator("text=/Today|Yesterday|This Week|This Month|Older|No sessions yet/");
|
|
43
|
+
await expect(sessionLabels.first()).toBeVisible({ timeout: 10000 });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("project dashboard shows stats", async function ({ page }) {
|
|
47
|
+
await page.goto("/");
|
|
48
|
+
await page.waitForSelector("[title='Lattice Dashboard']", { timeout: 10000 });
|
|
49
|
+
|
|
50
|
+
var projectButtons = page.locator("button:has(svg[aria-hidden='true'])").filter({
|
|
51
|
+
has: page.locator("text=/^[A-Z]{2,3}$/"),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
var count = await projectButtons.count();
|
|
55
|
+
if (count === 0) {
|
|
56
|
+
test.skip(true, "No projects available to test");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await projectButtons.first().click();
|
|
61
|
+
await page.waitForTimeout(1000);
|
|
62
|
+
|
|
63
|
+
var dashboardLink = page.locator("button", { hasText: "Dashboard" });
|
|
64
|
+
if (await dashboardLink.isVisible()) {
|
|
65
|
+
await dashboardLink.click();
|
|
66
|
+
await page.waitForTimeout(1000);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
var sidebar = page.locator("text=/Sessions|MCP Servers|Dashboard/").first();
|
|
70
|
+
await expect(sidebar).toBeVisible({ timeout: 10000 });
|
|
71
|
+
});
|
|
72
|
+
});
|