@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.
Files changed (81) hide show
  1. package/.github/workflows/ci.yml +121 -0
  2. package/bun.lock +14 -1
  3. package/client/src/App.tsx +2 -0
  4. package/client/src/components/analytics/ChartCard.tsx +6 -10
  5. package/client/src/components/analytics/QuickStats.tsx +3 -3
  6. package/client/src/components/analytics/charts/NodeFleetOverview.tsx +1 -1
  7. package/client/src/components/chat/ChatView.tsx +119 -7
  8. package/client/src/components/chat/Message.tsx +41 -6
  9. package/client/src/components/chat/PromptQuestion.tsx +4 -4
  10. package/client/src/components/chat/TodoCard.tsx +2 -2
  11. package/client/src/components/chat/ToolResultRenderer.tsx +6 -2
  12. package/client/src/components/dashboard/DashboardView.tsx +2 -0
  13. package/client/src/components/mesh/PairingDialog.tsx +6 -17
  14. package/client/src/components/project-settings/ProjectEnvironment.tsx +1 -1
  15. package/client/src/components/project-settings/ProjectMemory.tsx +10 -19
  16. package/client/src/components/project-settings/ProjectRules.tsx +3 -3
  17. package/client/src/components/project-settings/ProjectSkills.tsx +2 -2
  18. package/client/src/components/settings/BudgetSettings.tsx +161 -0
  19. package/client/src/components/settings/Environment.tsx +1 -1
  20. package/client/src/components/settings/SettingsView.tsx +3 -0
  21. package/client/src/components/sidebar/AddProjectModal.tsx +7 -11
  22. package/client/src/components/sidebar/NodeSettingsModal.tsx +6 -11
  23. package/client/src/components/sidebar/ProjectRail.tsx +11 -1
  24. package/client/src/components/sidebar/SessionList.tsx +33 -12
  25. package/client/src/components/sidebar/SettingsSidebar.tsx +2 -1
  26. package/client/src/components/sidebar/Sidebar.tsx +152 -2
  27. package/client/src/components/sidebar/UserIsland.tsx +76 -37
  28. package/client/src/components/ui/IconPicker.tsx +9 -36
  29. package/client/src/components/ui/KeyboardShortcuts.tsx +129 -0
  30. package/client/src/components/ui/Toast.tsx +22 -2
  31. package/client/src/components/workspace/BookmarksView.tsx +156 -0
  32. package/client/src/components/workspace/TabBar.tsx +34 -5
  33. package/client/src/components/workspace/TaskEditModal.tsx +7 -2
  34. package/client/src/components/workspace/WorkspaceView.tsx +29 -6
  35. package/client/src/hooks/useBookmarks.ts +57 -0
  36. package/client/src/hooks/useFocusTrap.ts +72 -0
  37. package/client/src/hooks/useProjects.ts +1 -1
  38. package/client/src/hooks/useSession.ts +38 -1
  39. package/client/src/hooks/useTimeTick.ts +35 -0
  40. package/client/src/hooks/useVoiceRecorder.ts +17 -3
  41. package/client/src/hooks/useWorkspace.ts +10 -1
  42. package/client/src/router.tsx +6 -11
  43. package/client/src/stores/bookmarks.ts +45 -0
  44. package/client/src/stores/session.ts +24 -0
  45. package/client/src/stores/sidebar.ts +2 -2
  46. package/client/src/stores/workspace.ts +114 -3
  47. package/client/src/vite-env.d.ts +6 -0
  48. package/client/tsconfig.json +4 -0
  49. package/package.json +2 -1
  50. package/playwright.config.ts +19 -0
  51. package/server/package.json +2 -0
  52. package/server/src/analytics/engine.ts +43 -9
  53. package/server/src/daemon.ts +11 -7
  54. package/server/src/handlers/bookmarks.ts +50 -0
  55. package/server/src/handlers/chat.ts +64 -0
  56. package/server/src/handlers/fs.ts +1 -1
  57. package/server/src/handlers/memory.ts +1 -1
  58. package/server/src/handlers/mesh.ts +1 -1
  59. package/server/src/handlers/project-settings.ts +2 -2
  60. package/server/src/handlers/session.ts +12 -11
  61. package/server/src/handlers/settings.ts +5 -2
  62. package/server/src/handlers/skills.ts +1 -1
  63. package/server/src/logger.ts +12 -0
  64. package/server/src/mesh/connector.ts +7 -6
  65. package/server/src/project/bookmarks.ts +83 -0
  66. package/server/src/project/context-breakdown.ts +1 -1
  67. package/server/src/project/registry.ts +5 -5
  68. package/server/src/project/sdk-bridge.ts +77 -6
  69. package/server/src/project/session.ts +6 -5
  70. package/server/src/ws/router.ts +5 -4
  71. package/server/tsconfig.json +4 -0
  72. package/shared/src/messages.ts +53 -2
  73. package/shared/src/models.ts +17 -1
  74. package/shared/src/project-settings.ts +0 -1
  75. package/shared/tsconfig.json +4 -0
  76. package/tests/accessibility.spec.ts +77 -0
  77. package/tests/keyboard-shortcuts.spec.ts +74 -0
  78. package/tests/message-actions.spec.ts +112 -0
  79. package/tests/onboarding.spec.ts +72 -0
  80. package/tests/session-flow.spec.ts +117 -0
  81. 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
- console.warn("[lattice] Failed to list SDK sessions:", err);
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
- console.warn("[lattice] Failed to load session history:", err);
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
- console.warn("[lattice] Failed to rename session:", err);
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
- console.warn("[lattice] Failed to delete session:", err);
493
+ log.session("Failed to delete session: %O", err);
493
494
  return false;
494
495
  }
495
496
  }
@@ -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
- console.error("[lattice] Async handler error for " + message.type + ":", stack);
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
- console.error("[lattice] Handler error for " + message.type + ":", stack);
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
- console.warn(`[lattice] No handler for message type: ${message.type}`);
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
- console.error("[router] Failed to proxy message:", err);
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
  }
@@ -10,6 +10,10 @@
10
10
  "include": [
11
11
  "src"
12
12
  ],
13
+ "exclude": [
14
+ "dist",
15
+ "node_modules"
16
+ ],
13
17
  "references": [
14
18
  { "path": "../shared" }
15
19
  ]
@@ -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";
@@ -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: string;
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;
@@ -1,6 +1,5 @@
1
1
  export type ProjectIcon =
2
2
  | { type: "lucide"; name: string }
3
- | { type: "emoji"; value: string }
4
3
  | { type: "text"; value: string; color?: string }
5
4
  | { type: "image"; path: string };
6
5
 
@@ -7,5 +7,9 @@
7
7
  "include": [
8
8
  "src"
9
9
  ],
10
+ "exclude": [
11
+ "dist",
12
+ "node_modules"
13
+ ],
10
14
  "references": []
11
15
  }
@@ -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
+ });