@cryptiklemur/lattice 1.15.0 → 1.16.1

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 (64) hide show
  1. package/.github/workflows/ci.yml +51 -2
  2. package/bun.lock +9 -0
  3. package/client/src/components/analytics/QuickStats.tsx +3 -3
  4. package/client/src/components/analytics/charts/NodeFleetOverview.tsx +1 -1
  5. package/client/src/components/chat/ChatView.tsx +114 -6
  6. package/client/src/components/chat/Message.tsx +41 -6
  7. package/client/src/components/chat/PromptQuestion.tsx +4 -4
  8. package/client/src/components/chat/TodoCard.tsx +2 -2
  9. package/client/src/components/dashboard/DashboardView.tsx +2 -0
  10. package/client/src/components/project-settings/ProjectEnvironment.tsx +1 -1
  11. package/client/src/components/project-settings/ProjectRules.tsx +3 -3
  12. package/client/src/components/project-settings/ProjectSkills.tsx +2 -2
  13. package/client/src/components/settings/BudgetSettings.tsx +161 -0
  14. package/client/src/components/settings/Environment.tsx +1 -1
  15. package/client/src/components/settings/SettingsView.tsx +3 -0
  16. package/client/src/components/sidebar/SessionList.tsx +33 -12
  17. package/client/src/components/sidebar/SettingsSidebar.tsx +2 -1
  18. package/client/src/components/sidebar/Sidebar.tsx +152 -2
  19. package/client/src/components/sidebar/UserIsland.tsx +76 -37
  20. package/client/src/components/ui/IconPicker.tsx +9 -36
  21. package/client/src/components/workspace/BookmarksView.tsx +156 -0
  22. package/client/src/components/workspace/TabBar.tsx +34 -5
  23. package/client/src/components/workspace/WorkspaceView.tsx +29 -6
  24. package/client/src/hooks/useBookmarks.ts +57 -0
  25. package/client/src/hooks/useProjects.ts +1 -1
  26. package/client/src/hooks/useSession.ts +38 -1
  27. package/client/src/hooks/useTimeTick.ts +35 -0
  28. package/client/src/hooks/useVoiceRecorder.ts +17 -3
  29. package/client/src/hooks/useWorkspace.ts +10 -1
  30. package/client/src/stores/bookmarks.ts +45 -0
  31. package/client/src/stores/session.ts +24 -0
  32. package/client/src/stores/sidebar.ts +2 -2
  33. package/client/src/stores/workspace.ts +114 -3
  34. package/client/src/vite-env.d.ts +6 -0
  35. package/client/tsconfig.json +4 -0
  36. package/package.json +2 -1
  37. package/playwright.config.ts +19 -0
  38. package/server/src/analytics/engine.ts +43 -9
  39. package/server/src/daemon.ts +3 -0
  40. package/server/src/handlers/bookmarks.ts +50 -0
  41. package/server/src/handlers/chat.ts +64 -0
  42. package/server/src/handlers/fs.ts +1 -1
  43. package/server/src/handlers/memory.ts +1 -1
  44. package/server/src/handlers/mesh.ts +1 -1
  45. package/server/src/handlers/project-settings.ts +2 -2
  46. package/server/src/handlers/session.ts +2 -2
  47. package/server/src/handlers/settings.ts +5 -2
  48. package/server/src/handlers/skills.ts +1 -1
  49. package/server/src/project/bookmarks.ts +83 -0
  50. package/server/src/project/context-breakdown.ts +1 -1
  51. package/server/src/project/registry.ts +5 -5
  52. package/server/src/project/sdk-bridge.ts +15 -3
  53. package/server/src/project/session.ts +1 -1
  54. package/server/tsconfig.json +4 -0
  55. package/shared/src/messages.ts +53 -2
  56. package/shared/src/models.ts +14 -0
  57. package/shared/src/project-settings.ts +0 -1
  58. package/shared/tsconfig.json +4 -0
  59. package/tests/accessibility.spec.ts +77 -0
  60. package/tests/keyboard-shortcuts.spec.ts +74 -0
  61. package/tests/message-actions.spec.ts +112 -0
  62. package/tests/onboarding.spec.ts +72 -0
  63. package/tests/session-flow.spec.ts +117 -0
  64. package/tests/session-preview.spec.ts +83 -0
@@ -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
+ });
@@ -0,0 +1,117 @@
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("Session switching", 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
+
35
+ test("clicking a session loads the chat view with messages", async function ({ page }) {
36
+ var sessionButtons = page.locator("button[aria-label^='Session:']");
37
+ var sessionCount = await sessionButtons.count();
38
+
39
+ if (sessionCount === 0) {
40
+ test.skip(true, "No sessions available to test");
41
+ return;
42
+ }
43
+
44
+ await sessionButtons.first().click();
45
+ await page.waitForTimeout(2000);
46
+
47
+ var chatMessages = page.locator(".chat");
48
+ await expect(chatMessages.first()).toBeVisible({ timeout: 10000 });
49
+ });
50
+
51
+ test("message content is visible in chat view", async function ({ page }) {
52
+ var sessionButtons = page.locator("button[aria-label^='Session:']");
53
+ var sessionCount = await sessionButtons.count();
54
+
55
+ if (sessionCount === 0) {
56
+ test.skip(true, "No sessions available to test");
57
+ return;
58
+ }
59
+
60
+ await sessionButtons.first().click();
61
+ await page.waitForTimeout(2000);
62
+
63
+ var chatBubbles = page.locator(".chat-bubble");
64
+ await expect(chatBubbles.first()).toBeVisible({ timeout: 10000 });
65
+ var text = await chatBubbles.first().textContent();
66
+ expect(text).toBeTruthy();
67
+ });
68
+
69
+ test("session title appears in the header", async function ({ page }) {
70
+ var sessionButtons = page.locator("button[aria-label^='Session:']");
71
+ var sessionCount = await sessionButtons.count();
72
+
73
+ if (sessionCount === 0) {
74
+ test.skip(true, "No sessions available to test");
75
+ return;
76
+ }
77
+
78
+ var sessionLabel = await sessionButtons.first().getAttribute("aria-label");
79
+ var expectedTitle = sessionLabel?.replace("Session: ", "") ?? "";
80
+
81
+ await sessionButtons.first().click();
82
+ await page.waitForTimeout(2000);
83
+
84
+ var headerTitle = page.locator("h1, [class*='font-mono'][class*='font-bold']").filter({
85
+ hasText: expectedTitle,
86
+ });
87
+
88
+ await expect(headerTitle.first()).toBeVisible({ timeout: 10000 });
89
+ });
90
+
91
+ test("switching sessions loads different messages", async function ({ page }) {
92
+ var sessionButtons = page.locator("button[aria-label^='Session:']");
93
+ var sessionCount = await sessionButtons.count();
94
+
95
+ if (sessionCount < 2) {
96
+ test.skip(true, "Need at least 2 sessions to test switching");
97
+ return;
98
+ }
99
+
100
+ await sessionButtons.first().click();
101
+ await page.waitForTimeout(2000);
102
+
103
+ var firstSessionMessages = page.locator(".chat-bubble");
104
+ await expect(firstSessionMessages.first()).toBeVisible({ timeout: 10000 });
105
+ var firstMessageText = await firstSessionMessages.first().textContent();
106
+
107
+ await sessionButtons.nth(1).click();
108
+ await page.waitForTimeout(2000);
109
+
110
+ var secondSessionMessages = page.locator(".chat-bubble");
111
+ await expect(secondSessionMessages.first()).toBeVisible({ timeout: 10000 });
112
+ var secondMessageText = await secondSessionMessages.first().textContent();
113
+
114
+ var activeSession = page.locator("button[aria-label^='Session:'][aria-current='true']");
115
+ await expect(activeSession).toHaveCount(1);
116
+ });
117
+ });
@@ -0,0 +1,83 @@
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("Sidebar hover preview", 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
+
35
+ test("hovering a session shows a preview popover with stats", async function ({ page }) {
36
+ var sessionButtons = page.locator("button[aria-label^='Session:']");
37
+ var sessionCount = await sessionButtons.count();
38
+
39
+ if (sessionCount === 0) {
40
+ test.skip(true, "No sessions available to test");
41
+ return;
42
+ }
43
+
44
+ await sessionButtons.first().hover();
45
+ await page.waitForTimeout(500);
46
+
47
+ var previewPopover = page.locator(".fixed.z-\\[9999\\].w-\\[270px\\]");
48
+ await expect(previewPopover).toBeVisible({ timeout: 5000 });
49
+
50
+ var costIndicator = previewPopover.locator("text=/\\$[0-9]|<\\$0\\.01/");
51
+ await expect(costIndicator).toBeVisible({ timeout: 5000 });
52
+
53
+ var durationIndicator = previewPopover.locator("text=/\\d+[smh]/");
54
+ await expect(durationIndicator).toBeVisible({ timeout: 5000 });
55
+
56
+ var messageCountIndicator = previewPopover.locator("text=/\\d+ msgs/");
57
+ await expect(messageCountIndicator).toBeVisible({ timeout: 5000 });
58
+
59
+ var modelIndicator = previewPopover.locator("text=/claude|sonnet|opus|haiku|unknown/i");
60
+ await expect(modelIndicator).toBeVisible({ timeout: 5000 });
61
+ });
62
+
63
+ test("preview disappears when mouse leaves the session", async function ({ page }) {
64
+ var sessionButtons = page.locator("button[aria-label^='Session:']");
65
+ var sessionCount = await sessionButtons.count();
66
+
67
+ if (sessionCount === 0) {
68
+ test.skip(true, "No sessions available to test");
69
+ return;
70
+ }
71
+
72
+ await sessionButtons.first().hover();
73
+ await page.waitForTimeout(500);
74
+
75
+ var previewPopover = page.locator(".fixed.z-\\[9999\\].w-\\[270px\\]");
76
+ await expect(previewPopover).toBeVisible({ timeout: 5000 });
77
+
78
+ await page.locator("[title='Lattice Dashboard']").hover();
79
+ await page.waitForTimeout(300);
80
+
81
+ await expect(previewPopover).not.toBeVisible();
82
+ });
83
+ });