@cryptiklemur/lattice 1.15.0 → 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 +51 -2
- package/bun.lock +9 -0
- 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 +114 -6
- 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/dashboard/DashboardView.tsx +2 -0
- package/client/src/components/project-settings/ProjectEnvironment.tsx +1 -1
- 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/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/workspace/BookmarksView.tsx +156 -0
- package/client/src/components/workspace/TabBar.tsx +34 -5
- package/client/src/components/workspace/WorkspaceView.tsx +29 -6
- package/client/src/hooks/useBookmarks.ts +57 -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/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/src/analytics/engine.ts +43 -9
- package/server/src/daemon.ts +3 -0
- 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 +2 -2
- package/server/src/handlers/settings.ts +5 -2
- package/server/src/handlers/skills.ts +1 -1
- 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 +15 -3
- package/server/src/project/session.ts +1 -1
- package/server/tsconfig.json +4 -0
- package/shared/src/messages.ts +53 -2
- package/shared/src/models.ts +14 -0
- 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
package/.github/workflows/ci.yml
CHANGED
|
@@ -37,8 +37,14 @@ jobs:
|
|
|
37
37
|
- name: Install dependencies
|
|
38
38
|
run: bun install --frozen-lockfile
|
|
39
39
|
|
|
40
|
-
- name: Typecheck
|
|
41
|
-
run: bunx tsc --noEmit -p tsconfig.json
|
|
40
|
+
- name: Typecheck shared
|
|
41
|
+
run: bunx tsc --noEmit -p shared/tsconfig.json
|
|
42
|
+
|
|
43
|
+
- name: Typecheck server
|
|
44
|
+
run: bunx tsc --noEmit -p server/tsconfig.json
|
|
45
|
+
|
|
46
|
+
- name: Typecheck client
|
|
47
|
+
run: bunx tsc --noEmit -p client/tsconfig.json
|
|
42
48
|
|
|
43
49
|
build:
|
|
44
50
|
name: Build
|
|
@@ -70,3 +76,46 @@ jobs:
|
|
|
70
76
|
|
|
71
77
|
- name: Build client
|
|
72
78
|
run: cd client && npx vite build
|
|
79
|
+
|
|
80
|
+
playwright:
|
|
81
|
+
name: Playwright Tests
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
continue-on-error: true
|
|
84
|
+
steps:
|
|
85
|
+
- name: Checkout
|
|
86
|
+
uses: actions/checkout@v5
|
|
87
|
+
|
|
88
|
+
- name: Setup Bun
|
|
89
|
+
uses: oven-sh/setup-bun@v2
|
|
90
|
+
with:
|
|
91
|
+
bun-version: latest
|
|
92
|
+
|
|
93
|
+
- name: Setup Node.js
|
|
94
|
+
uses: actions/setup-node@v4
|
|
95
|
+
with:
|
|
96
|
+
node-version: 22
|
|
97
|
+
|
|
98
|
+
- name: Cache bun install
|
|
99
|
+
uses: actions/cache@v4
|
|
100
|
+
with:
|
|
101
|
+
path: ~/.bun/install/cache
|
|
102
|
+
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
|
103
|
+
restore-keys: |
|
|
104
|
+
${{ runner.os }}-bun-
|
|
105
|
+
|
|
106
|
+
- name: Install dependencies
|
|
107
|
+
run: bun install --frozen-lockfile
|
|
108
|
+
|
|
109
|
+
- name: Install Playwright browsers
|
|
110
|
+
run: bunx playwright install --with-deps chromium
|
|
111
|
+
|
|
112
|
+
- name: Run Playwright tests
|
|
113
|
+
run: bunx playwright test
|
|
114
|
+
|
|
115
|
+
- name: Upload test results
|
|
116
|
+
if: always()
|
|
117
|
+
uses: actions/upload-artifact@v4
|
|
118
|
+
with:
|
|
119
|
+
name: playwright-results
|
|
120
|
+
path: test-results/
|
|
121
|
+
retention-days: 7
|
package/bun.lock
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "@lattice/root",
|
|
7
7
|
"devDependencies": {
|
|
8
|
+
"@playwright/test": "^1.52.0",
|
|
8
9
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
9
10
|
"@semantic-release/github": "^12.0.6",
|
|
10
11
|
"@semantic-release/npm": "^13.1.5",
|
|
@@ -352,6 +353,8 @@
|
|
|
352
353
|
|
|
353
354
|
"@oxc-project/types": ["@oxc-project/types@0.120.0", "", {}, "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg=="],
|
|
354
355
|
|
|
356
|
+
"@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="],
|
|
357
|
+
|
|
355
358
|
"@pnpm/config.env-replace": ["@pnpm/config.env-replace@1.1.0", "", {}, "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w=="],
|
|
356
359
|
|
|
357
360
|
"@pnpm/network.ca-file": ["@pnpm/network.ca-file@1.0.2", "", { "dependencies": { "graceful-fs": "4.2.10" } }, "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA=="],
|
|
@@ -1316,6 +1319,10 @@
|
|
|
1316
1319
|
|
|
1317
1320
|
"pkg-conf": ["pkg-conf@2.1.0", "", { "dependencies": { "find-up": "^2.0.0", "load-json-file": "^4.0.0" } }, "sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g=="],
|
|
1318
1321
|
|
|
1322
|
+
"playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
|
|
1323
|
+
|
|
1324
|
+
"playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],
|
|
1325
|
+
|
|
1319
1326
|
"pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
|
|
1320
1327
|
|
|
1321
1328
|
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
|
@@ -2072,6 +2079,8 @@
|
|
|
2072
2079
|
|
|
2073
2080
|
"pkg-conf/find-up": ["find-up@2.1.0", "", { "dependencies": { "locate-path": "^2.0.0" } }, "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ=="],
|
|
2074
2081
|
|
|
2082
|
+
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
|
2083
|
+
|
|
2075
2084
|
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
|
2076
2085
|
|
|
2077
2086
|
"qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
|
|
@@ -51,9 +51,9 @@ export function QuickStats() {
|
|
|
51
51
|
var d = analytics.data;
|
|
52
52
|
var colors = getChartColors();
|
|
53
53
|
|
|
54
|
-
var costSparkData = d.costOverTime.slice(-7).map(function (e) { return { v: e.total }; });
|
|
55
|
-
var sessionsSparkData = d.sessionsOverTime.slice(-7).map(function (e) { return { v: e.count }; });
|
|
56
|
-
var tokensSparkData = d.tokensOverTime.slice(-7).map(function (e) { return { v: e.input + e.output }; });
|
|
54
|
+
var costSparkData = d.costOverTime.slice(-7).map(function (e: typeof d.costOverTime[number]) { return { v: e.total }; });
|
|
55
|
+
var sessionsSparkData = d.sessionsOverTime.slice(-7).map(function (e: typeof d.sessionsOverTime[number]) { return { v: e.count }; });
|
|
56
|
+
var tokensSparkData = d.tokensOverTime.slice(-7).map(function (e: typeof d.tokensOverTime[number]) { return { v: e.input + e.output }; });
|
|
57
57
|
|
|
58
58
|
var totalTokens = d.totalTokens.input + d.totalTokens.output;
|
|
59
59
|
var cacheHitPct = Math.round(d.cacheHitRate * 100);
|
|
@@ -67,7 +67,7 @@ export function NodeFleetOverview(props: NodeFleetOverviewProps) {
|
|
|
67
67
|
<div className="mt-2.5 pt-2 border-t border-base-content/5">
|
|
68
68
|
<div className="text-[9px] font-mono text-base-content/25 uppercase tracking-widest mb-1.5">Projects</div>
|
|
69
69
|
<div className="flex flex-wrap gap-1">
|
|
70
|
-
{node.projects.map(function (p) {
|
|
70
|
+
{node.projects.map(function (p: typeof node.projects[number]) {
|
|
71
71
|
return (
|
|
72
72
|
<span
|
|
73
73
|
key={p.slug}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useCallback, useState, useMemo } from "react";
|
|
2
|
-
import { Terminal, Info, ArrowDown, Pencil, Copy, Check, Menu, AlertTriangle, Zap, Square, X } from "lucide-react";
|
|
2
|
+
import { Terminal, Info, ArrowDown, Pencil, Copy, Check, Menu, AlertTriangle, Zap, Square, X, Bookmark } from "lucide-react";
|
|
3
3
|
import { LatticeLogomark } from "../ui/LatticeLogomark";
|
|
4
4
|
import { useFocusTrap } from "../../hooks/useFocusTrap";
|
|
5
5
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
@@ -8,7 +8,7 @@ import { useProjects } from "../../hooks/useProjects";
|
|
|
8
8
|
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
9
9
|
import { setSessionTitle, setIsProcessing, setCurrentStatus, setWasInterrupted, setPendingPrefill } from "../../stores/session";
|
|
10
10
|
import { openSettings, openProjectSettings } from "../../stores/sidebar";
|
|
11
|
-
import { openTab } from "../../stores/workspace";
|
|
11
|
+
import { openTab, updateSessionTabTitle } from "../../stores/workspace";
|
|
12
12
|
import { builtinCommands } from "../../commands";
|
|
13
13
|
import { Message } from "./Message";
|
|
14
14
|
import { ToolGroup } from "./ToolGroup";
|
|
@@ -19,15 +19,26 @@ import { StatusBar } from "./StatusBar";
|
|
|
19
19
|
import { useSidebar } from "../../hooks/useSidebar";
|
|
20
20
|
import { useOnline } from "../../hooks/useOnline";
|
|
21
21
|
import { useSpinnerVerb } from "../../hooks/useSpinnerVerb";
|
|
22
|
+
import { useBookmarks } from "../../hooks/useBookmarks";
|
|
22
23
|
import { formatSessionTitle } from "../../utils/formatSessionTitle";
|
|
23
24
|
|
|
24
|
-
export function ChatView() {
|
|
25
|
-
var { messages, isProcessing, sendMessage, activeSessionId, activeSessionTitle, currentStatus, contextUsage, contextBreakdown, lastResponseCost, lastResponseDuration, historyLoading, wasInterrupted, promptSuggestion, failedInput, clearFailedInput, messageQueue, enqueueMessage, removeQueuedMessage, updateQueuedMessage, isBusy, isPlanMode, pendingPrefill } = useSession();
|
|
25
|
+
export function ChatView({ sessionId: tabSessionId, projectSlug: tabProjectSlug }: { sessionId?: string; projectSlug?: string } = {}) {
|
|
26
|
+
var { messages, isProcessing, sendMessage, activeSessionId, activeSessionTitle, currentStatus, contextUsage, contextBreakdown, lastResponseCost, lastResponseDuration, historyLoading, wasInterrupted, promptSuggestion, failedInput, clearFailedInput, messageQueue, enqueueMessage, removeQueuedMessage, updateQueuedMessage, isBusy, isPlanMode, pendingPrefill, activateSession, budgetStatus, budgetExceeded, sendBudgetOverride, dismissBudgetExceeded } = useSession();
|
|
26
27
|
var { activeProject } = useProjects();
|
|
27
28
|
var { toggleDrawer } = useSidebar();
|
|
29
|
+
|
|
30
|
+
useEffect(function () {
|
|
31
|
+
if (!tabSessionId || !tabProjectSlug) return;
|
|
32
|
+
if (activeSessionId === tabSessionId) return;
|
|
33
|
+
activateSession(tabProjectSlug, tabSessionId);
|
|
34
|
+
}, [tabSessionId, tabProjectSlug]);
|
|
28
35
|
var online = useOnline();
|
|
29
36
|
var ws = useWebSocket();
|
|
30
37
|
var spinnerVerb = useSpinnerVerb(isProcessing);
|
|
38
|
+
var { bookmarks, requestSessionBookmarks } = useBookmarks();
|
|
39
|
+
var [showBookmarkDropdown, setShowBookmarkDropdown] = useState<boolean>(false);
|
|
40
|
+
var bookmarkDropdownRef = useRef<HTMLDivElement>(null);
|
|
41
|
+
var bookmarkBtnRef = useRef<HTMLButtonElement>(null);
|
|
31
42
|
var scrollParentRef = useRef<HTMLDivElement>(null);
|
|
32
43
|
var prevLengthRef = useRef<number>(0);
|
|
33
44
|
var isLiveChatRef = useRef<boolean>(false);
|
|
@@ -50,6 +61,24 @@ export function ChatView() {
|
|
|
50
61
|
}
|
|
51
62
|
}, [pendingPrefill, historyLoading]);
|
|
52
63
|
|
|
64
|
+
useEffect(function () {
|
|
65
|
+
if (activeSessionId && !historyLoading) {
|
|
66
|
+
requestSessionBookmarks();
|
|
67
|
+
}
|
|
68
|
+
}, [activeSessionId, historyLoading]);
|
|
69
|
+
|
|
70
|
+
useEffect(function () {
|
|
71
|
+
if (!showBookmarkDropdown) return;
|
|
72
|
+
function handleClick(e: MouseEvent) {
|
|
73
|
+
var target = e.target as Node;
|
|
74
|
+
if (bookmarkDropdownRef.current && bookmarkDropdownRef.current.contains(target)) return;
|
|
75
|
+
if (bookmarkBtnRef.current && bookmarkBtnRef.current.contains(target)) return;
|
|
76
|
+
setShowBookmarkDropdown(false);
|
|
77
|
+
}
|
|
78
|
+
document.addEventListener("mousedown", handleClick);
|
|
79
|
+
return function () { document.removeEventListener("mousedown", handleClick); };
|
|
80
|
+
}, [showBookmarkDropdown]);
|
|
81
|
+
|
|
53
82
|
var [copiedField, setCopiedField] = useState<string | null>(null);
|
|
54
83
|
var [isRenaming, setIsRenaming] = useState<boolean>(false);
|
|
55
84
|
var [renameValue, setRenameValue] = useState<string>("");
|
|
@@ -202,6 +231,7 @@ export function ChatView() {
|
|
|
202
231
|
if (renameValue.trim() !== activeSessionTitle) {
|
|
203
232
|
ws.send({ type: "session:rename", sessionId: activeSessionId, title: renameValue.trim() });
|
|
204
233
|
setSessionTitle(renameValue.trim());
|
|
234
|
+
updateSessionTabTitle(activeSessionId, renameValue.trim());
|
|
205
235
|
}
|
|
206
236
|
}
|
|
207
237
|
setIsRenaming(false);
|
|
@@ -309,6 +339,7 @@ export function ChatView() {
|
|
|
309
339
|
if (args && activeSessionId) {
|
|
310
340
|
ws.send({ type: "session:rename", sessionId: activeSessionId, title: args });
|
|
311
341
|
setSessionTitle(args);
|
|
342
|
+
updateSessionTabTitle(activeSessionId, args);
|
|
312
343
|
} else {
|
|
313
344
|
handleRenameStart();
|
|
314
345
|
}
|
|
@@ -420,6 +451,53 @@ export function ChatView() {
|
|
|
420
451
|
<Pencil size={12} />
|
|
421
452
|
</button>
|
|
422
453
|
)}
|
|
454
|
+
{activeSessionId && bookmarks.length > 0 && (
|
|
455
|
+
<div className="relative">
|
|
456
|
+
<button
|
|
457
|
+
ref={bookmarkBtnRef}
|
|
458
|
+
onClick={function () { setShowBookmarkDropdown(!showBookmarkDropdown); }}
|
|
459
|
+
aria-label="View bookmarks"
|
|
460
|
+
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-mono text-warning/70 hover:text-warning hover:bg-warning/10 transition-colors"
|
|
461
|
+
>
|
|
462
|
+
<Bookmark size={11} />
|
|
463
|
+
<span>{bookmarks.length}</span>
|
|
464
|
+
</button>
|
|
465
|
+
{showBookmarkDropdown && (
|
|
466
|
+
<div
|
|
467
|
+
ref={bookmarkDropdownRef}
|
|
468
|
+
className="absolute top-full left-0 mt-1 z-50 bg-base-300 border border-base-content/15 rounded-lg shadow-xl py-1 w-[280px] max-h-[300px] overflow-y-auto"
|
|
469
|
+
>
|
|
470
|
+
<div className="px-2.5 py-1.5 text-[10px] uppercase tracking-widest text-base-content/40 font-mono font-bold">
|
|
471
|
+
Bookmarks
|
|
472
|
+
</div>
|
|
473
|
+
{bookmarks.map(function (bm) {
|
|
474
|
+
return (
|
|
475
|
+
<button
|
|
476
|
+
key={bm.id}
|
|
477
|
+
type="button"
|
|
478
|
+
onClick={function () {
|
|
479
|
+
setShowBookmarkDropdown(false);
|
|
480
|
+
var el = document.getElementById("msg-" + bm.messageUuid);
|
|
481
|
+
if (el) {
|
|
482
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
483
|
+
el.classList.add("ring-2", "ring-warning/40");
|
|
484
|
+
setTimeout(function () { el!.classList.remove("ring-2", "ring-warning/40"); }, 2000);
|
|
485
|
+
}
|
|
486
|
+
}}
|
|
487
|
+
className="flex items-start gap-2 w-full px-2.5 py-1.5 hover:bg-base-content/5 transition-colors text-left"
|
|
488
|
+
>
|
|
489
|
+
<Bookmark size={10} className="text-warning/60 mt-0.5 flex-shrink-0" />
|
|
490
|
+
<div className="min-w-0 flex-1">
|
|
491
|
+
<div className="text-[11px] text-base-content/60 truncate">{bm.messageText}</div>
|
|
492
|
+
<div className="text-[9px] text-base-content/30 font-mono">{bm.messageType}</div>
|
|
493
|
+
</div>
|
|
494
|
+
</button>
|
|
495
|
+
);
|
|
496
|
+
})}
|
|
497
|
+
</div>
|
|
498
|
+
)}
|
|
499
|
+
</div>
|
|
500
|
+
)}
|
|
423
501
|
</>
|
|
424
502
|
)}
|
|
425
503
|
</div>
|
|
@@ -954,12 +1032,42 @@ export function ChatView() {
|
|
|
954
1032
|
</div>
|
|
955
1033
|
)}
|
|
956
1034
|
|
|
1035
|
+
{budgetExceeded && budgetStatus && (
|
|
1036
|
+
<div className="flex-shrink-0 border-t border-base-300 bg-warning/10 px-4 py-3 flex items-center gap-3">
|
|
1037
|
+
<AlertTriangle size={16} className="text-warning flex-shrink-0" />
|
|
1038
|
+
<div className="flex-1 min-w-0">
|
|
1039
|
+
<div className="text-[13px] text-base-content font-medium">
|
|
1040
|
+
Daily budget exceeded (${budgetStatus.dailySpend.toFixed(2)} / ${budgetStatus.dailyLimit.toFixed(2)})
|
|
1041
|
+
</div>
|
|
1042
|
+
<div className="text-[11px] text-base-content/50">Send anyway?</div>
|
|
1043
|
+
</div>
|
|
1044
|
+
<button
|
|
1045
|
+
onClick={sendBudgetOverride}
|
|
1046
|
+
className="px-3 py-1.5 rounded-lg bg-warning text-warning-content text-[12px] font-semibold hover:bg-warning/80 transition-colors"
|
|
1047
|
+
>
|
|
1048
|
+
Continue
|
|
1049
|
+
</button>
|
|
1050
|
+
<button
|
|
1051
|
+
onClick={dismissBudgetExceeded}
|
|
1052
|
+
className="px-3 py-1.5 rounded-lg border border-base-content/15 text-base-content/60 text-[12px] hover:bg-base-content/5 transition-colors"
|
|
1053
|
+
>
|
|
1054
|
+
Cancel
|
|
1055
|
+
</button>
|
|
1056
|
+
</div>
|
|
1057
|
+
)}
|
|
1058
|
+
|
|
957
1059
|
<div className="flex-shrink-0 border-t border-base-300 bg-base-200 px-2 sm:px-4 pb-3 pt-2">
|
|
958
1060
|
<ChatInput
|
|
959
1061
|
sessionId={activeSessionId}
|
|
960
1062
|
onSend={handleSend}
|
|
961
|
-
disabled={!activeSessionId || !online || isBusy}
|
|
962
|
-
disabledPlaceholder={
|
|
1063
|
+
disabled={!activeSessionId || !online || isBusy || (budgetStatus !== null && budgetStatus.enforcement === "hard-block" && budgetStatus.dailySpend >= budgetStatus.dailyLimit)}
|
|
1064
|
+
disabledPlaceholder={
|
|
1065
|
+
isBusy
|
|
1066
|
+
? "Session in use by another client..."
|
|
1067
|
+
: (budgetStatus !== null && budgetStatus.enforcement === "hard-block" && budgetStatus.dailySpend >= budgetStatus.dailyLimit)
|
|
1068
|
+
? "Daily budget exceeded ($" + budgetStatus.dailySpend.toFixed(2) + " / $" + budgetStatus.dailyLimit.toFixed(2) + ")"
|
|
1069
|
+
: undefined
|
|
1070
|
+
}
|
|
963
1071
|
failedInput={failedInput}
|
|
964
1072
|
onFailedInputConsumed={clearFailedInput}
|
|
965
1073
|
prefillText={prefillText}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { useState, useRef, useEffect, memo } from "react";
|
|
1
|
+
import { useState, useRef, useEffect, memo, useMemo } from "react";
|
|
2
2
|
import Markdown from "react-markdown";
|
|
3
3
|
import remarkGfm from "remark-gfm";
|
|
4
|
-
import { Wrench, TriangleAlert, ChevronDown, ChevronRight, Check, X, Shield, Zap, Link, Copy, SquarePlus } from "lucide-react";
|
|
4
|
+
import { Wrench, TriangleAlert, ChevronDown, ChevronRight, Check, X, Shield, Zap, Link, Copy, SquarePlus, Bookmark, BookmarkCheck } from "lucide-react";
|
|
5
5
|
import type { HistoryMessage, ChatPermissionResponseMessage } from "@lattice/shared";
|
|
6
|
+
import { useStore } from "@tanstack/react-store";
|
|
6
7
|
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
7
8
|
import { getSessionStore, setPendingPrefill } from "../../stores/session";
|
|
9
|
+
import { getBookmarkStore, findBookmarkByUuid } from "../../stores/bookmarks";
|
|
8
10
|
import { ToolResultRenderer } from "./ToolResultRenderer";
|
|
9
11
|
import { formatToolSummary } from "./toolSummary";
|
|
10
12
|
import { PromptQuestion } from "./PromptQuestion";
|
|
@@ -104,9 +106,17 @@ function stripMarkdown(text: string): string {
|
|
|
104
106
|
.trim();
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
function MessageActions(props: { text: string; showNewSession?: boolean }) {
|
|
109
|
+
function MessageActions(props: { text: string; showNewSession?: boolean; messageUuid?: string; messageType?: "user" | "assistant" }) {
|
|
108
110
|
var [copied, setCopied] = useState(false);
|
|
109
111
|
var ws = useWebSocket();
|
|
112
|
+
var bookmarkState = useStore(getBookmarkStore(), function (s) { return s; });
|
|
113
|
+
var isBookmarked = useMemo(function () {
|
|
114
|
+
if (!props.messageUuid) return false;
|
|
115
|
+
for (var i = 0; i < bookmarkState.bookmarks.length; i++) {
|
|
116
|
+
if (bookmarkState.bookmarks[i].messageUuid === props.messageUuid) return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}, [props.messageUuid, bookmarkState.bookmarks]);
|
|
110
120
|
|
|
111
121
|
function handleCopy(e: React.MouseEvent) {
|
|
112
122
|
var content = e.shiftKey ? stripMarkdown(props.text) : props.text;
|
|
@@ -122,6 +132,26 @@ function MessageActions(props: { text: string; showNewSession?: boolean }) {
|
|
|
122
132
|
ws.send({ type: "session:create", projectSlug: state.activeProjectSlug });
|
|
123
133
|
}
|
|
124
134
|
|
|
135
|
+
function handleBookmarkToggle() {
|
|
136
|
+
var state = getSessionStore().state;
|
|
137
|
+
if (!state.activeSessionId || !state.activeProjectSlug || !props.messageUuid || !props.messageType) return;
|
|
138
|
+
if (isBookmarked) {
|
|
139
|
+
var bm = findBookmarkByUuid(props.messageUuid);
|
|
140
|
+
if (bm) {
|
|
141
|
+
ws.send({ type: "bookmark:remove", id: bm.id });
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
ws.send({
|
|
145
|
+
type: "bookmark:add",
|
|
146
|
+
sessionId: state.activeSessionId,
|
|
147
|
+
projectSlug: state.activeProjectSlug,
|
|
148
|
+
messageUuid: props.messageUuid,
|
|
149
|
+
messageText: props.text.slice(0, 100),
|
|
150
|
+
messageType: props.messageType,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
125
155
|
var btnClass = "opacity-0 group-hover/msg:opacity-100 transition-opacity duration-150 text-base-content/20 hover:text-base-content/50 cursor-pointer p-0.5 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:outline-none rounded";
|
|
126
156
|
|
|
127
157
|
return (
|
|
@@ -134,6 +164,11 @@ function MessageActions(props: { text: string; showNewSession?: boolean }) {
|
|
|
134
164
|
<SquarePlus size={11} />
|
|
135
165
|
</button>
|
|
136
166
|
)}
|
|
167
|
+
{props.messageUuid && props.messageType && (
|
|
168
|
+
<button type="button" onClick={handleBookmarkToggle} className={btnClass + (isBookmarked ? " !opacity-100 !text-warning" : "")} title={isBookmarked ? "Remove bookmark" : "Bookmark message"}>
|
|
169
|
+
{isBookmarked ? <BookmarkCheck size={11} /> : <Bookmark size={11} />}
|
|
170
|
+
</button>
|
|
171
|
+
)}
|
|
137
172
|
</>
|
|
138
173
|
);
|
|
139
174
|
}
|
|
@@ -179,7 +214,7 @@ function SkillMessage(props: { skillName: string; content: string; time: string
|
|
|
179
214
|
<div className="chat-footer text-[10px] text-base-content/30 mt-0.5 flex items-center gap-1">
|
|
180
215
|
{props.time}
|
|
181
216
|
<MessageAnchor id={props.uuid} />
|
|
182
|
-
<MessageActions text={"/" + props.skillName} showNewSession />
|
|
217
|
+
<MessageActions text={"/" + props.skillName} showNewSession messageUuid={props.uuid} messageType="user" />
|
|
183
218
|
</div>
|
|
184
219
|
)}
|
|
185
220
|
</div>
|
|
@@ -205,7 +240,7 @@ function UserMessage(props: { message: HistoryMessage }) {
|
|
|
205
240
|
<div className="chat-footer text-[10px] text-base-content/30 mt-0.5 flex items-center gap-1">
|
|
206
241
|
{time}
|
|
207
242
|
<MessageAnchor id={msg.uuid} />
|
|
208
|
-
<MessageActions text={text} showNewSession />
|
|
243
|
+
<MessageActions text={text} showNewSession messageUuid={msg.uuid} messageType="user" />
|
|
209
244
|
</div>
|
|
210
245
|
)}
|
|
211
246
|
</div>
|
|
@@ -256,7 +291,7 @@ function AssistantMessage(props: { message: HistoryMessage; responseCost?: numbe
|
|
|
256
291
|
<span className="text-base-content/15">{formatTokenCount(msg.outputTokens)} out</span>
|
|
257
292
|
)}
|
|
258
293
|
<MessageAnchor id={msg.uuid} />
|
|
259
|
-
<MessageActions text={msg.text || ""} showNewSession />
|
|
294
|
+
<MessageActions text={msg.text || ""} showNewSession messageUuid={msg.uuid} messageType="assistant" />
|
|
260
295
|
</div>
|
|
261
296
|
)}
|
|
262
297
|
</div>
|
|
@@ -90,7 +90,7 @@ export function PromptQuestion(props: PromptQuestionProps) {
|
|
|
90
90
|
)}
|
|
91
91
|
<span className="text-[12px] text-base-content/50">{firstAnswer ? firstAnswer[0] : ""}</span>
|
|
92
92
|
<span className="flex-1" />
|
|
93
|
-
<span className="text-[12px] font-medium text-base-content/70">{firstAnswer ? firstAnswer[1] : ""}</span>
|
|
93
|
+
<span className="text-[12px] font-medium text-base-content/70">{firstAnswer ? String(firstAnswer[1]) : ""}</span>
|
|
94
94
|
<ChevronDown
|
|
95
95
|
size={12}
|
|
96
96
|
className={"text-base-content/25 transition-transform duration-200 " + (expanded ? "rotate-180" : "")}
|
|
@@ -100,7 +100,7 @@ export function PromptQuestion(props: PromptQuestionProps) {
|
|
|
100
100
|
{expanded && answeredQuestion && (
|
|
101
101
|
<div className="px-4 pb-3 border-t border-base-content/5">
|
|
102
102
|
<div className="flex flex-col gap-1 pt-2">
|
|
103
|
-
{answeredQuestion.options.map(function (opt, oi) {
|
|
103
|
+
{answeredQuestion.options.map(function (opt: typeof answeredQuestion.options[number], oi: number) {
|
|
104
104
|
var isChosen = firstAnswer && firstAnswer[1] === opt.label;
|
|
105
105
|
return (
|
|
106
106
|
<div
|
|
@@ -142,7 +142,7 @@ export function PromptQuestion(props: PromptQuestionProps) {
|
|
|
142
142
|
|
|
143
143
|
return (
|
|
144
144
|
<div className="px-5 py-2" aria-live="polite">
|
|
145
|
-
{questions.map(function (q, qi) {
|
|
145
|
+
{questions.map(function (q: typeof questions[number], qi: number) {
|
|
146
146
|
return (
|
|
147
147
|
<div
|
|
148
148
|
key={qi}
|
|
@@ -165,7 +165,7 @@ export function PromptQuestion(props: PromptQuestionProps) {
|
|
|
165
165
|
aria-label={q.question}
|
|
166
166
|
onKeyDown={function (e) { handleKeyDown(e, q.options.length, q, q.options); }}
|
|
167
167
|
>
|
|
168
|
-
{q.options.map(function (opt, oi) {
|
|
168
|
+
{q.options.map(function (opt: typeof q.options[number], oi: number) {
|
|
169
169
|
var isSelected = selectedMulti.has(opt.label);
|
|
170
170
|
var isFocused = focusIndex === oi;
|
|
171
171
|
return (
|
|
@@ -19,7 +19,7 @@ export function TodoCard(props: TodoCardProps) {
|
|
|
19
19
|
var todos = props.message.todos || [];
|
|
20
20
|
if (todos.length === 0) return null;
|
|
21
21
|
|
|
22
|
-
var completed = todos.filter(function (t) { return t.status === "completed"; }).length;
|
|
22
|
+
var completed = todos.filter(function (t: typeof todos[number]) { return t.status === "completed"; }).length;
|
|
23
23
|
var total = todos.length;
|
|
24
24
|
|
|
25
25
|
return (
|
|
@@ -32,7 +32,7 @@ export function TodoCard(props: TodoCardProps) {
|
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
34
|
<div className="px-3 py-2">
|
|
35
|
-
{todos.map(function (todo) {
|
|
35
|
+
{todos.map(function (todo: typeof todos[number]) {
|
|
36
36
|
return (
|
|
37
37
|
<div
|
|
38
38
|
key={todo.id}
|
|
@@ -3,6 +3,7 @@ import { useMesh } from "../../hooks/useMesh";
|
|
|
3
3
|
import { useProjects } from "../../hooks/useProjects";
|
|
4
4
|
import { useSidebar } from "../../hooks/useSidebar";
|
|
5
5
|
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
6
|
+
import { useTimeTick } from "../../hooks/useTimeTick";
|
|
6
7
|
import { LatticeLogomark } from "../ui/LatticeLogomark";
|
|
7
8
|
import { QuickStats } from "../analytics/QuickStats";
|
|
8
9
|
import {
|
|
@@ -25,6 +26,7 @@ function relativeTime(ts: number): string {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export function DashboardView() {
|
|
29
|
+
useTimeTick();
|
|
28
30
|
var { nodes } = useMesh();
|
|
29
31
|
var { projects } = useProjects();
|
|
30
32
|
var sidebar = useSidebar();
|
|
@@ -127,7 +127,7 @@ export function ProjectEnvironment({
|
|
|
127
127
|
</div>
|
|
128
128
|
<div className="flex gap-1.5 items-center">
|
|
129
129
|
<div className="h-9 sm:h-7 px-3 bg-base-300/50 border border-base-content/10 rounded-xl flex items-center font-mono text-[12px] text-base-content/40 w-full">
|
|
130
|
-
{v}
|
|
130
|
+
{String(v)}
|
|
131
131
|
</div>
|
|
132
132
|
</div>
|
|
133
133
|
<span className="text-[10px] uppercase tracking-wider text-base-content/30 w-7 text-center hidden sm:block">
|
|
@@ -19,7 +19,7 @@ export function ProjectRules({
|
|
|
19
19
|
var globalRules = settings.global.rules ?? [];
|
|
20
20
|
|
|
21
21
|
var [rules, setRules] = useState<RuleEntry[]>(function () {
|
|
22
|
-
return (settings.rules ?? []).map(function (r) {
|
|
22
|
+
return (settings.rules ?? []).map(function (r: { filename: string; content: string }) {
|
|
23
23
|
return { filename: r.filename, content: r.content };
|
|
24
24
|
});
|
|
25
25
|
});
|
|
@@ -34,7 +34,7 @@ export function ProjectRules({
|
|
|
34
34
|
if (save.saving) {
|
|
35
35
|
save.confirmSave();
|
|
36
36
|
} else {
|
|
37
|
-
setRules((settings.rules ?? []).map(function (r) {
|
|
37
|
+
setRules((settings.rules ?? []).map(function (r: { filename: string; content: string }) {
|
|
38
38
|
return { filename: r.filename, content: r.content };
|
|
39
39
|
}));
|
|
40
40
|
save.resetFromServer();
|
|
@@ -136,7 +136,7 @@ export function ProjectRules({
|
|
|
136
136
|
)}
|
|
137
137
|
{globalRules.length > 0 && (
|
|
138
138
|
<div className="flex flex-col gap-1.5">
|
|
139
|
-
{globalRules.map(function (rule, idx) {
|
|
139
|
+
{globalRules.map(function (rule: typeof globalRules[number], idx: number) {
|
|
140
140
|
var isExpanded = expandedGlobal.has(idx);
|
|
141
141
|
return (
|
|
142
142
|
<div key={rule.filename + "-" + idx} className="border border-base-content/10 rounded-xl overflow-hidden">
|
|
@@ -46,7 +46,7 @@ export function ProjectSkills({ settings, projectSlug }: ProjectSkillsProps) {
|
|
|
46
46
|
<div>
|
|
47
47
|
<div className="text-[12px] font-semibold text-base-content/40 mb-2">Global Skills</div>
|
|
48
48
|
<div className="space-y-2">
|
|
49
|
-
{globalSkills.map(function (skill) {
|
|
49
|
+
{globalSkills.map(function (skill: typeof globalSkills[number]) {
|
|
50
50
|
return (
|
|
51
51
|
<SkillItem
|
|
52
52
|
key={skill.path}
|
|
@@ -64,7 +64,7 @@ export function ProjectSkills({ settings, projectSlug }: ProjectSkillsProps) {
|
|
|
64
64
|
<div>
|
|
65
65
|
<div className="text-[12px] font-semibold text-base-content/40 mb-2">Project Skills</div>
|
|
66
66
|
<div className="space-y-2">
|
|
67
|
-
{projectSkills.map(function (skill) {
|
|
67
|
+
{projectSkills.map(function (skill: typeof projectSkills[number]) {
|
|
68
68
|
return (
|
|
69
69
|
<SkillItem
|
|
70
70
|
key={skill.path}
|