@cryptiklemur/lattice 1.17.1 → 1.18.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/.serena/memories/code_style_and_conventions.md +35 -0
- package/.serena/memories/project_overview.md +51 -0
- package/.serena/memories/suggested_commands.md +32 -0
- package/.serena/memories/task_completion_checklist.md +15 -0
- package/.serena/project.yml +14 -0
- package/client/src/components/dashboard/DashboardView.tsx +2 -1
- package/client/src/components/dashboard/ProjectDashboardView.tsx +5 -3
- package/client/src/components/sidebar/Sidebar.tsx +8 -2
- package/client/src/lib/workspace-url.ts +217 -0
- package/client/src/router.tsx +1 -7
- package/client/src/stores/sidebar.ts +204 -55
- package/client/src/stores/workspace.ts +32 -0
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Code Style and Conventions
|
|
2
|
+
|
|
3
|
+
## JavaScript/TypeScript
|
|
4
|
+
- Use `const`/`let` (NOT `var`)
|
|
5
|
+
- Use named function declarations, not arrow functions — except for anonymous callbacks where arrows are fine
|
|
6
|
+
- ESM throughout (server + client)
|
|
7
|
+
- Follow .editorconfig: 2-space indent, LF line endings, UTF-8
|
|
8
|
+
|
|
9
|
+
## Icons
|
|
10
|
+
- Use `lucide-react` for ALL icons
|
|
11
|
+
- Import individually: `import { Settings, Moon } from "lucide-react"`
|
|
12
|
+
- No emojis for icons or in UI
|
|
13
|
+
|
|
14
|
+
## Comments
|
|
15
|
+
- No section separator comments (// ========)
|
|
16
|
+
- No organizational header comments
|
|
17
|
+
- Keep code clean without visual separators
|
|
18
|
+
|
|
19
|
+
## Design System
|
|
20
|
+
- See `.impeccable.md` for comprehensive design guidelines
|
|
21
|
+
- Dark-first with 23 base16 themes via OKLCH CSS variables
|
|
22
|
+
- Tailwind + daisyUI component framework
|
|
23
|
+
- Typography: JetBrains Mono (headings/code), IBM Plex Sans (body)
|
|
24
|
+
- Three surface tiers: Chrome (base-200), Stage (base-100 + dot-grid), Elevated (base-300 + shadow)
|
|
25
|
+
- Never hardcode colors — always use CSS variables/theme tokens
|
|
26
|
+
- WCAG AA contrast, prefers-reduced-motion, keyboard-first
|
|
27
|
+
|
|
28
|
+
## Types
|
|
29
|
+
- `HistoryMessage` uses a discriminated union type (`HistoryMessageType`)
|
|
30
|
+
- Shared types in `shared/src/models.ts`
|
|
31
|
+
- Message protocol in `shared/src/messages.ts`
|
|
32
|
+
- All `var` in existing code is legacy — new code should use `const`/`let`
|
|
33
|
+
|
|
34
|
+
## Pre-existing Errors
|
|
35
|
+
- DO NOT EVER LEAVE PRE-EXISTING ERRORS. FIX THEM.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Lattice — Project Overview
|
|
2
|
+
|
|
3
|
+
**Purpose**: Multi-machine agentic dashboard for Claude Code. Web UI for monitoring sessions, managing projects, tracking costs, and orchestrating across mesh-networked nodes.
|
|
4
|
+
|
|
5
|
+
**Status**: Alpha (pre-1.0), work directly on main branch.
|
|
6
|
+
|
|
7
|
+
## Tech Stack
|
|
8
|
+
|
|
9
|
+
- **Runtime**: Bun (server), Node (CI), Vite (client bundler)
|
|
10
|
+
- **Server**: Bun WebSocket server with typed message protocol
|
|
11
|
+
- **Client**: React 19 + Vite 8 + Tailwind CSS + daisyUI
|
|
12
|
+
- **Shared**: TypeScript types and constants (no build step, imported directly)
|
|
13
|
+
- **State**: Tanstack Store (client), Tanstack Router (routing)
|
|
14
|
+
- **Sessions**: Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`)
|
|
15
|
+
- **Charts**: Recharts
|
|
16
|
+
- **Testing**: Playwright (chromium only)
|
|
17
|
+
- **CI**: GitHub Actions (typecheck + build + Playwright)
|
|
18
|
+
- **Release**: semantic-release on push to main
|
|
19
|
+
|
|
20
|
+
## Monorepo Structure
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
lattice/
|
|
24
|
+
├── shared/ — TypeScript types, message definitions, constants
|
|
25
|
+
├── server/ — Bun WebSocket server, handlers, analytics, mesh networking
|
|
26
|
+
├── client/ — React + Vite dashboard, UI components, 23 themes
|
|
27
|
+
├── themes/ — Base16 theme JSON files (dark + light)
|
|
28
|
+
├── tests/ — Playwright test files
|
|
29
|
+
└── docs/ — Screenshots, audit docs
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Architecture
|
|
33
|
+
|
|
34
|
+
- Server communicates with clients via WebSocket using typed messages defined in `shared/`
|
|
35
|
+
- Sessions managed through Claude Agent SDK
|
|
36
|
+
- 23 base16 themes with OKLCH color space
|
|
37
|
+
- PWA with service worker caching (Workbox)
|
|
38
|
+
- Mesh networking for multi-machine orchestration
|
|
39
|
+
- Structured logging with `debug` package (lattice:* namespaces)
|
|
40
|
+
|
|
41
|
+
## Key Features
|
|
42
|
+
|
|
43
|
+
- Real-time chat with tool approval, context tracking, cost per message
|
|
44
|
+
- Session tabs with split-pane support
|
|
45
|
+
- Message bookmarks (per-session + global)
|
|
46
|
+
- Analytics dashboard (15+ chart types)
|
|
47
|
+
- Daily cost budget with configurable enforcement
|
|
48
|
+
- Keyboard shortcuts overlay (press ?)
|
|
49
|
+
- Session hover previews, auto-titling, date range search
|
|
50
|
+
- MCP server management, skill marketplace
|
|
51
|
+
- Memory management UI
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Suggested Commands
|
|
2
|
+
|
|
3
|
+
## Development
|
|
4
|
+
- `bun run dev` — Start server (--watch) + Vite dev server, hot reloads both
|
|
5
|
+
- `bun run build` — Build all workspaces (only client produces output)
|
|
6
|
+
- `bun run typecheck` — Run tsc across all workspaces
|
|
7
|
+
|
|
8
|
+
## Testing
|
|
9
|
+
- `bunx playwright test` — Run all Playwright tests (server must be running on :7654)
|
|
10
|
+
- `bunx playwright test tests/session-flow.spec.ts` — Run single test file
|
|
11
|
+
|
|
12
|
+
## Type Checking (per-workspace, as CI does it)
|
|
13
|
+
- `bunx tsc -p shared/tsconfig.json` — Build shared types (must run first)
|
|
14
|
+
- `bunx tsc --noEmit -p server/tsconfig.json` — Typecheck server
|
|
15
|
+
- `bunx tsc --noEmit -p client/tsconfig.json` — Typecheck client
|
|
16
|
+
|
|
17
|
+
## Client Build
|
|
18
|
+
- `cd client && npx vite build` — Build client for production
|
|
19
|
+
|
|
20
|
+
## Server
|
|
21
|
+
- `bun run server/src/index.ts daemon` — Run server daemon directly
|
|
22
|
+
- `bun run server/src/index.ts daemon --port 8080` — Custom port
|
|
23
|
+
- Default port: 7654, binds to 0.0.0.0
|
|
24
|
+
|
|
25
|
+
## Environment
|
|
26
|
+
- `ANTHROPIC_API_KEY` — Optional, uses `claude setup-token` if not set
|
|
27
|
+
- `DEBUG=lattice:*` — Enable structured debug logging
|
|
28
|
+
|
|
29
|
+
## Git
|
|
30
|
+
- Commit messages: Angular Commit Convention (feat/fix/refactor/etc)
|
|
31
|
+
- Releases: automated via semantic-release on push to main
|
|
32
|
+
- Never add AI attribution to commits
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Task Completion Checklist
|
|
2
|
+
|
|
3
|
+
When a coding task is completed, verify the following:
|
|
4
|
+
|
|
5
|
+
1. **Type check** — Run `bunx tsc --noEmit -p client/tsconfig.json` and `bunx tsc --noEmit -p server/tsconfig.json` (build shared first with `bunx tsc -p shared/tsconfig.json`)
|
|
6
|
+
2. **Build** — Run `cd client && npx vite build` to verify client builds
|
|
7
|
+
3. **Server build** — Run `bun build server/src/index.ts --target=bun --outdir=/tmp/check` to verify server bundles
|
|
8
|
+
4. **No pre-existing errors** — Fix any errors you encounter, don't leave them
|
|
9
|
+
5. **Code style** — const/let (not var), named functions (not arrows except callbacks), lucide-react icons only
|
|
10
|
+
6. **Git** — Only commit when explicitly asked. Angular Commit Convention. No AI attribution.
|
|
11
|
+
|
|
12
|
+
## Important Notes
|
|
13
|
+
- The PWA service worker caches old bundles. After rebuilding client, users need to clear SW cache in browser to see changes.
|
|
14
|
+
- Server serves from `client/dist/` (built output), NOT from Vite dev server. Client changes require `npx vite build` to appear on port 7654.
|
|
15
|
+
- Vite dev server runs on :5173 separately but can't connect WebSocket without the Bun server on :7654.
|
package/.serena/project.yml
CHANGED
|
@@ -136,3 +136,17 @@ symbol_info_budget:
|
|
|
136
136
|
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
|
137
137
|
# Extends the list from the global configuration, merging the two lists.
|
|
138
138
|
read_only_memory_patterns: []
|
|
139
|
+
|
|
140
|
+
# list of regex patterns for memories to completely ignore.
|
|
141
|
+
# Matching memories will not appear in list_memories or activate_project output
|
|
142
|
+
# and cannot be accessed via read_memory or write_memory.
|
|
143
|
+
# To access ignored memory files, use the read_file tool on the raw file path.
|
|
144
|
+
# Extends the list from the global configuration, merging the two lists.
|
|
145
|
+
# Example: ["_archive/.*", "_episodes/.*"]
|
|
146
|
+
ignored_memory_patterns: []
|
|
147
|
+
|
|
148
|
+
# advanced configuration option allowing to configure language server-specific options.
|
|
149
|
+
# Maps the language key to the options.
|
|
150
|
+
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
|
|
151
|
+
# No documentation on options means no options are available.
|
|
152
|
+
ls_specific_settings: {}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "lucide-react";
|
|
13
13
|
import type { ServerMessage, SessionSummary, LatticeConfig } from "@lattice/shared";
|
|
14
14
|
import { formatSessionTitle } from "../../utils/formatSessionTitle";
|
|
15
|
+
import { openSessionTab } from "../../stores/workspace";
|
|
15
16
|
|
|
16
17
|
function relativeTime(ts: number): string {
|
|
17
18
|
var diff = Date.now() - ts;
|
|
@@ -157,7 +158,7 @@ export function DashboardView() {
|
|
|
157
158
|
return (
|
|
158
159
|
<button
|
|
159
160
|
key={s.id}
|
|
160
|
-
onClick={function () { sidebar.navigateToSession(s.projectSlug, s.id); }}
|
|
161
|
+
onClick={function () { openSessionTab(s.id, s.projectSlug, s.title); sidebar.navigateToSession(s.projectSlug, s.id); }}
|
|
161
162
|
className="flex items-center gap-3 px-3 py-2 rounded-xl border border-base-content/15 bg-base-200 hover:border-base-content/30 transition-colors duration-[120ms] cursor-pointer text-left focus-visible:ring-2 focus-visible:ring-primary"
|
|
162
163
|
>
|
|
163
164
|
<MessageSquare size={12} className="text-base-content/30 flex-shrink-0" />
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "lucide-react";
|
|
10
10
|
import type { ProjectSettingsSection } from "../../stores/sidebar";
|
|
11
11
|
import type { SessionSummary, ServerMessage } from "@lattice/shared";
|
|
12
|
+
import { openSessionTab } from "../../stores/workspace";
|
|
12
13
|
|
|
13
14
|
function StatCard({ label, value, icon }: { label: string; value: string | number; icon: React.ReactNode }) {
|
|
14
15
|
return (
|
|
@@ -74,9 +75,10 @@ export function ProjectDashboardView() {
|
|
|
74
75
|
sidebar.openProjectSettings(section);
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
function goToSession(
|
|
78
|
+
function goToSession(s: SessionSummary) {
|
|
78
79
|
if (activeProject) {
|
|
79
|
-
|
|
80
|
+
openSessionTab(s.id, activeProject.slug, s.title);
|
|
81
|
+
sidebar.setActiveSessionId(s.id);
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
|
|
@@ -130,7 +132,7 @@ export function ProjectDashboardView() {
|
|
|
130
132
|
return (
|
|
131
133
|
<button
|
|
132
134
|
key={s.id}
|
|
133
|
-
onClick={function () { goToSession(s
|
|
135
|
+
onClick={function () { goToSession(s); }}
|
|
134
136
|
className="flex items-center gap-3 px-3 py-2 rounded-xl border border-base-content/15 bg-base-300 hover:border-base-content/30 transition-colors duration-[120ms] cursor-pointer text-left focus-visible:ring-2 focus-visible:ring-primary"
|
|
135
137
|
>
|
|
136
138
|
<MessageSquare size={12} className="text-base-content/30 flex-shrink-0" />
|
|
@@ -10,7 +10,7 @@ import { useSidebar } from "../../hooks/useSidebar";
|
|
|
10
10
|
import { useSession } from "../../hooks/useSession";
|
|
11
11
|
import { clearSession } from "../../stores/session";
|
|
12
12
|
import { useOnline } from "../../hooks/useOnline";
|
|
13
|
-
import { openTab, openSessionTab } from "../../stores/workspace";
|
|
13
|
+
import { openTab, openSessionTab, getWorkspaceStore } from "../../stores/workspace";
|
|
14
14
|
import { getSidebarStore, goToAnalytics } from "../../stores/sidebar";
|
|
15
15
|
import { ProjectRail } from "./ProjectRail";
|
|
16
16
|
import { SessionList } from "./SessionList";
|
|
@@ -214,7 +214,13 @@ export function Sidebar({ onSessionSelect }: { onSessionSelect?: () => void }) {
|
|
|
214
214
|
if (!sidebar.activeProjectSlug || !sidebar.activeSessionId) return;
|
|
215
215
|
if (!activeProject) return;
|
|
216
216
|
initialActivatedRef.current = true;
|
|
217
|
-
|
|
217
|
+
const wsState = getWorkspaceStore().state;
|
|
218
|
+
const alreadyHasTab = wsState.tabs.some(function (t) {
|
|
219
|
+
return t.type === "chat" && t.sessionId === sidebar.activeSessionId;
|
|
220
|
+
});
|
|
221
|
+
if (!alreadyHasTab) {
|
|
222
|
+
openSessionTab(sidebar.activeSessionId, sidebar.activeProjectSlug, "Session");
|
|
223
|
+
}
|
|
218
224
|
session.activateSession(sidebar.activeProjectSlug, sidebar.activeSessionId);
|
|
219
225
|
}, [sidebar.activeProjectSlug, sidebar.activeSessionId, activeProject]);
|
|
220
226
|
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { Tab, Pane, TabType, WorkspaceState } from "../stores/workspace";
|
|
2
|
+
|
|
3
|
+
const TAB_TYPES: Set<string> = new Set(["chat", "files", "terminal", "notes", "tasks", "bookmarks", "analytics"]);
|
|
4
|
+
|
|
5
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
|
+
|
|
7
|
+
export function shortSessionId(uuid: string): string {
|
|
8
|
+
return uuid.slice(0, 8);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function encodeWorkspaceUrl(
|
|
12
|
+
state: WorkspaceState,
|
|
13
|
+
tabs: Tab[],
|
|
14
|
+
primaryProjectSlug: string | null,
|
|
15
|
+
): string {
|
|
16
|
+
if (tabs.length === 0) return "";
|
|
17
|
+
if (tabs.length === 1 && tabs[0].id === "chat" && !tabs[0].sessionId) return "";
|
|
18
|
+
|
|
19
|
+
const panes = state.panes;
|
|
20
|
+
const activePaneId = state.activePaneId;
|
|
21
|
+
|
|
22
|
+
function encodeTab(tab: Tab, isActive: boolean): string {
|
|
23
|
+
let token = tab.type;
|
|
24
|
+
if (tab.type === "chat" && tab.sessionId) {
|
|
25
|
+
const shortId = shortSessionId(tab.sessionId);
|
|
26
|
+
if (tab.projectSlug && tab.projectSlug !== primaryProjectSlug) {
|
|
27
|
+
token += ":" + shortId + "." + tab.projectSlug;
|
|
28
|
+
} else {
|
|
29
|
+
token += ":" + shortId;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (isActive) {
|
|
33
|
+
token += "*";
|
|
34
|
+
}
|
|
35
|
+
return token;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const paneTokens: string[] = [];
|
|
39
|
+
for (let pi = 0; pi < panes.length; pi++) {
|
|
40
|
+
const pane = panes[pi];
|
|
41
|
+
const tabTokens: string[] = [];
|
|
42
|
+
for (let ti = 0; ti < pane.tabIds.length; ti++) {
|
|
43
|
+
const tabId = pane.tabIds[ti];
|
|
44
|
+
const tab = tabs.find(function (t) { return t.id === tabId; });
|
|
45
|
+
if (!tab) continue;
|
|
46
|
+
const isActive = tabId === pane.activeTabId;
|
|
47
|
+
tabTokens.push(encodeTab(tab, isActive));
|
|
48
|
+
}
|
|
49
|
+
paneTokens.push(tabTokens.join(","));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let result = paneTokens.join("|");
|
|
53
|
+
|
|
54
|
+
if (panes.length > 1 && state.splitDirection) {
|
|
55
|
+
const dirChar = state.splitDirection === "horizontal" ? "h" : "v";
|
|
56
|
+
let activePaneIndex = 0;
|
|
57
|
+
for (let i = 0; i < panes.length; i++) {
|
|
58
|
+
if (panes[i].id === activePaneId) {
|
|
59
|
+
activePaneIndex = i;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
result += "|" + dirChar + activePaneIndex;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface DecodedWorkspace {
|
|
70
|
+
tabs: Tab[];
|
|
71
|
+
panes: Pane[];
|
|
72
|
+
activePaneId: string;
|
|
73
|
+
splitDirection: "horizontal" | "vertical" | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function decodeWorkspaceUrl(
|
|
77
|
+
tParam: string,
|
|
78
|
+
primaryProjectSlug: string | null,
|
|
79
|
+
resolveFullId: (shortId: string, projectSlug: string) => string | null,
|
|
80
|
+
): DecodedWorkspace {
|
|
81
|
+
const segments = tParam.split("|");
|
|
82
|
+
const tabs: Tab[] = [];
|
|
83
|
+
const panes: Pane[] = [];
|
|
84
|
+
let splitDirection: "horizontal" | "vertical" | null = null;
|
|
85
|
+
let activePaneIndex = 0;
|
|
86
|
+
|
|
87
|
+
const labels: Record<TabType, string> = {
|
|
88
|
+
chat: "Chat",
|
|
89
|
+
files: "Files",
|
|
90
|
+
terminal: "Terminal",
|
|
91
|
+
notes: "Notes",
|
|
92
|
+
tasks: "Tasks",
|
|
93
|
+
bookmarks: "Bookmarks",
|
|
94
|
+
analytics: "Analytics",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
let splitMeta: string | null = null;
|
|
98
|
+
const paneSegments: string[] = [];
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < segments.length; i++) {
|
|
101
|
+
const seg = segments[i];
|
|
102
|
+
if (seg.length <= 3 && /^[hv]\d$/.test(seg)) {
|
|
103
|
+
splitMeta = seg;
|
|
104
|
+
} else {
|
|
105
|
+
paneSegments.push(seg);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (splitMeta) {
|
|
110
|
+
splitDirection = splitMeta[0] === "h" ? "horizontal" : "vertical";
|
|
111
|
+
activePaneIndex = parseInt(splitMeta[1], 10);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (let pi = 0; pi < paneSegments.length; pi++) {
|
|
115
|
+
const paneId = "pane-" + (pi + 1);
|
|
116
|
+
const tabTokens = paneSegments[pi].split(",").filter(function (s) { return s.length > 0; });
|
|
117
|
+
const paneTabIds: string[] = [];
|
|
118
|
+
let paneActiveTabId = "";
|
|
119
|
+
|
|
120
|
+
for (let ti = 0; ti < tabTokens.length; ti++) {
|
|
121
|
+
let token = tabTokens[ti];
|
|
122
|
+
const isActive = token.endsWith("*");
|
|
123
|
+
if (isActive) {
|
|
124
|
+
token = token.slice(0, -1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const colonIdx = token.indexOf(":");
|
|
128
|
+
let tabType: TabType;
|
|
129
|
+
let sessionShortId: string | null = null;
|
|
130
|
+
let projectSlug: string | null = null;
|
|
131
|
+
|
|
132
|
+
if (colonIdx !== -1) {
|
|
133
|
+
tabType = token.slice(0, colonIdx) as TabType;
|
|
134
|
+
const rest = token.slice(colonIdx + 1);
|
|
135
|
+
const dotIdx = rest.indexOf(".");
|
|
136
|
+
if (dotIdx !== -1) {
|
|
137
|
+
sessionShortId = rest.slice(0, dotIdx);
|
|
138
|
+
projectSlug = rest.slice(dotIdx + 1);
|
|
139
|
+
} else {
|
|
140
|
+
sessionShortId = rest;
|
|
141
|
+
projectSlug = primaryProjectSlug;
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
tabType = token as TabType;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!TAB_TYPES.has(tabType)) continue;
|
|
148
|
+
|
|
149
|
+
let tab: Tab;
|
|
150
|
+
if (tabType === "chat" && sessionShortId && projectSlug) {
|
|
151
|
+
const fullId = resolveFullId(sessionShortId, projectSlug);
|
|
152
|
+
const resolvedId = fullId || sessionShortId;
|
|
153
|
+
const tabId = "chat-" + resolvedId;
|
|
154
|
+
tab = {
|
|
155
|
+
id: tabId,
|
|
156
|
+
type: "chat",
|
|
157
|
+
label: "Session",
|
|
158
|
+
closeable: true,
|
|
159
|
+
sessionId: resolvedId,
|
|
160
|
+
projectSlug: projectSlug,
|
|
161
|
+
};
|
|
162
|
+
} else if (tabType === "chat") {
|
|
163
|
+
tab = { id: "chat", type: "chat", label: "Chat", closeable: false };
|
|
164
|
+
} else {
|
|
165
|
+
tab = {
|
|
166
|
+
id: tabType,
|
|
167
|
+
type: tabType,
|
|
168
|
+
label: labels[tabType],
|
|
169
|
+
closeable: true,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const existingIdx = tabs.findIndex(function (t) { return t.id === tab.id; });
|
|
174
|
+
if (existingIdx === -1) {
|
|
175
|
+
tabs.push(tab);
|
|
176
|
+
}
|
|
177
|
+
paneTabIds.push(tab.id);
|
|
178
|
+
|
|
179
|
+
if (isActive) {
|
|
180
|
+
paneActiveTabId = tab.id;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (paneTabIds.length > 0) {
|
|
185
|
+
if (!paneActiveTabId) {
|
|
186
|
+
paneActiveTabId = paneTabIds[0];
|
|
187
|
+
}
|
|
188
|
+
panes.push({ id: paneId, tabIds: paneTabIds, activeTabId: paneActiveTabId });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (panes.length === 0) {
|
|
193
|
+
const defaultTab: Tab = { id: "chat", type: "chat", label: "Chat", closeable: false };
|
|
194
|
+
tabs.push(defaultTab);
|
|
195
|
+
panes.push({ id: "pane-1", tabIds: ["chat"], activeTabId: "chat" });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const resolvedActivePaneIndex = Math.min(activePaneIndex, panes.length - 1);
|
|
199
|
+
const activePaneId = panes[resolvedActivePaneIndex].id;
|
|
200
|
+
|
|
201
|
+
if (panes.length < 2) {
|
|
202
|
+
splitDirection = null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { tabs, panes, activePaneId, splitDirection };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function isLegacySessionUrl(pathname: string): { projectSlug: string; sessionId: string } | null {
|
|
209
|
+
const parts = pathname.split("/").filter(function (p) { return p.length > 0; });
|
|
210
|
+
if (parts.length < 2) return null;
|
|
211
|
+
if (parts[0] === "settings") return null;
|
|
212
|
+
if (parts[1] === "settings") return null;
|
|
213
|
+
if (UUID_PATTERN.test(parts[1])) {
|
|
214
|
+
return { projectSlug: parts[0], sessionId: parts[1] };
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
package/client/src/router.tsx
CHANGED
|
@@ -532,12 +532,6 @@ var projectRoute = createRoute({
|
|
|
532
532
|
component: IndexPage,
|
|
533
533
|
});
|
|
534
534
|
|
|
535
|
-
var sessionRoute = createRoute({
|
|
536
|
-
getParentRoute: function () { return rootRoute; },
|
|
537
|
-
path: "/$projectSlug/$sessionId",
|
|
538
|
-
component: IndexPage,
|
|
539
|
-
});
|
|
540
|
-
|
|
541
535
|
var settingsRoute = createRoute({
|
|
542
536
|
getParentRoute: function () { return rootRoute; },
|
|
543
537
|
path: "/settings/$section",
|
|
@@ -562,7 +556,7 @@ var projectSettingsIndexRoute = createRoute({
|
|
|
562
556
|
component: IndexPage,
|
|
563
557
|
});
|
|
564
558
|
|
|
565
|
-
var routeTree = rootRoute.addChildren([indexRoute, settingsIndexRoute, settingsRoute, projectSettingsIndexRoute, projectSettingsRoute, projectRoute
|
|
559
|
+
var routeTree = rootRoute.addChildren([indexRoute, settingsIndexRoute, settingsRoute, projectSettingsIndexRoute, projectSettingsRoute, projectRoute]);
|
|
566
560
|
|
|
567
561
|
export var router = createRouter({ routeTree });
|
|
568
562
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Store } from "@tanstack/react-store";
|
|
2
2
|
import type { ProjectSettingsSection } from "@lattice/shared";
|
|
3
|
+
import { encodeWorkspaceUrl, decodeWorkspaceUrl, isLegacySessionUrl, shortSessionId } from "../lib/workspace-url";
|
|
4
|
+
import type { DecodedWorkspace } from "../lib/workspace-url";
|
|
5
|
+
import { getWorkspaceStore, restoreWorkspace, setUrlSyncCallback } from "./workspace";
|
|
3
6
|
|
|
4
7
|
export type { ProjectSettingsSection };
|
|
5
8
|
|
|
@@ -32,33 +35,82 @@ export interface SidebarState {
|
|
|
32
35
|
confirmRemoveSlug: string | null;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
const SETTINGS_SECTIONS: SettingsSection[] = ["appearance", "claude", "environment", "mcp", "skills", "nodes", "editor", "rules", "memory", "notifications", "budget"];
|
|
39
|
+
|
|
40
|
+
interface ParsedUrl {
|
|
41
|
+
projectSlug: string | null;
|
|
42
|
+
sessionId: string | null;
|
|
43
|
+
settingsSection: SettingsSection | null;
|
|
44
|
+
projectSettingsSection: ProjectSettingsSection | null;
|
|
45
|
+
tParam: string | null;
|
|
46
|
+
decodedWorkspace: DecodedWorkspace | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveFullIdFromNothing(_shortId: string, _projectSlug: string): string | null {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseInitialUrl(): ParsedUrl {
|
|
54
|
+
const path = window.location.pathname;
|
|
55
|
+
const parts = path.split("/").filter(function (p) { return p.length > 0; });
|
|
56
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
57
|
+
const tParam = searchParams.get("t");
|
|
36
58
|
|
|
37
|
-
function parseInitialUrl(): { projectSlug: string | null; sessionId: string | null; settingsSection: SettingsSection | null; projectSettingsSection: ProjectSettingsSection | null } {
|
|
38
|
-
var path = window.location.pathname;
|
|
39
|
-
var parts = path.split("/").filter(function (p) { return p.length > 0; });
|
|
40
59
|
if (parts[0] === "settings") {
|
|
41
|
-
|
|
60
|
+
const section = parts[1] as SettingsSection;
|
|
42
61
|
if (section && SETTINGS_SECTIONS.indexOf(section) !== -1) {
|
|
43
|
-
return { projectSlug: null, sessionId: null, settingsSection: section, projectSettingsSection: null };
|
|
62
|
+
return { projectSlug: null, sessionId: null, settingsSection: section, projectSettingsSection: null, tParam: null, decodedWorkspace: null };
|
|
44
63
|
}
|
|
45
|
-
return { projectSlug: null, sessionId: null, settingsSection: "appearance", projectSettingsSection: null };
|
|
64
|
+
return { projectSlug: null, sessionId: null, settingsSection: "appearance", projectSettingsSection: null, tParam: null, decodedWorkspace: null };
|
|
46
65
|
}
|
|
66
|
+
|
|
47
67
|
if (parts.length >= 2 && parts[1] === "settings") {
|
|
48
|
-
return { projectSlug: parts[0], sessionId: null, settingsSection: null, projectSettingsSection: (parts[2] || "general") as ProjectSettingsSection };
|
|
68
|
+
return { projectSlug: parts[0], sessionId: null, settingsSection: null, projectSettingsSection: (parts[2] || "general") as ProjectSettingsSection, tParam: null, decodedWorkspace: null };
|
|
49
69
|
}
|
|
50
|
-
|
|
51
|
-
|
|
70
|
+
|
|
71
|
+
const legacy = isLegacySessionUrl(path);
|
|
72
|
+
if (legacy && !tParam) {
|
|
73
|
+
const shortId = shortSessionId(legacy.sessionId);
|
|
74
|
+
const newUrl = "/" + legacy.projectSlug + "?t=chat:" + shortId + "*";
|
|
75
|
+
window.history.replaceState(null, "", newUrl);
|
|
76
|
+
const decoded = decodeWorkspaceUrl("chat:" + shortId + "*", legacy.projectSlug, resolveFullIdFromNothing);
|
|
77
|
+
return {
|
|
78
|
+
projectSlug: legacy.projectSlug,
|
|
79
|
+
sessionId: legacy.sessionId,
|
|
80
|
+
settingsSection: null,
|
|
81
|
+
projectSettingsSection: null,
|
|
82
|
+
tParam: "chat:" + shortId + "*",
|
|
83
|
+
decodedWorkspace: decoded,
|
|
84
|
+
};
|
|
52
85
|
}
|
|
53
|
-
|
|
54
|
-
|
|
86
|
+
|
|
87
|
+
if (parts.length >= 1 && parts[0] !== "settings") {
|
|
88
|
+
const projectSlug = parts[0];
|
|
89
|
+
if (tParam) {
|
|
90
|
+
const decoded = decodeWorkspaceUrl(tParam, projectSlug, resolveFullIdFromNothing);
|
|
91
|
+
let sessionId: string | null = null;
|
|
92
|
+
const activePane = decoded.panes.find(function (p) { return p.id === decoded.activePaneId; });
|
|
93
|
+
if (activePane) {
|
|
94
|
+
const activeTab = decoded.tabs.find(function (t) { return t.id === activePane.activeTabId; });
|
|
95
|
+
if (activeTab && activeTab.type === "chat" && activeTab.sessionId) {
|
|
96
|
+
sessionId = activeTab.sessionId;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { projectSlug, sessionId, settingsSection: null, projectSettingsSection: null, tParam, decodedWorkspace: decoded };
|
|
100
|
+
}
|
|
101
|
+
return { projectSlug, sessionId: null, settingsSection: null, projectSettingsSection: null, tParam: null, decodedWorkspace: null };
|
|
55
102
|
}
|
|
56
|
-
|
|
103
|
+
|
|
104
|
+
return { projectSlug: null, sessionId: null, settingsSection: null, projectSettingsSection: null, tParam: null, decodedWorkspace: null };
|
|
57
105
|
}
|
|
58
106
|
|
|
59
|
-
|
|
107
|
+
const initialUrl = parseInitialUrl();
|
|
60
108
|
|
|
61
|
-
|
|
109
|
+
if (initialUrl.decodedWorkspace) {
|
|
110
|
+
restoreWorkspace(initialUrl.decodedWorkspace);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const sidebarStore = new Store<SidebarState>({
|
|
62
114
|
activeProjectSlug: initialUrl.settingsSection ? null : initialUrl.projectSlug,
|
|
63
115
|
activeSessionId: initialUrl.settingsSection ? null : initialUrl.sessionId,
|
|
64
116
|
sidebarMode: (initialUrl.settingsSection || initialUrl.projectSettingsSection) ? "settings" : "project",
|
|
@@ -67,7 +119,7 @@ var sidebarStore = new Store<SidebarState>({
|
|
|
67
119
|
: initialUrl.projectSettingsSection
|
|
68
120
|
? { type: "project-settings", section: initialUrl.projectSettingsSection }
|
|
69
121
|
: initialUrl.projectSlug
|
|
70
|
-
? (initialUrl.sessionId ? { type: "chat" } : { type: "project-dashboard" })
|
|
122
|
+
? (initialUrl.sessionId || initialUrl.tParam ? { type: "chat" } : { type: "project-dashboard" })
|
|
71
123
|
: { type: "dashboard" },
|
|
72
124
|
previousView: null,
|
|
73
125
|
userMenuOpen: false,
|
|
@@ -78,19 +130,67 @@ var sidebarStore = new Store<SidebarState>({
|
|
|
78
130
|
confirmRemoveSlug: null,
|
|
79
131
|
});
|
|
80
132
|
|
|
81
|
-
|
|
82
|
-
|
|
133
|
+
let lastEncodedUrl = "";
|
|
134
|
+
|
|
135
|
+
function pushUrl(projectSlug: string | null, replace: boolean = false): void {
|
|
136
|
+
const wsState = getWorkspaceStore().state;
|
|
137
|
+
const encoded = encodeWorkspaceUrl(wsState, wsState.tabs, projectSlug);
|
|
138
|
+
let path = "/";
|
|
83
139
|
if (projectSlug) {
|
|
84
140
|
path = "/" + projectSlug;
|
|
85
|
-
if (
|
|
86
|
-
path
|
|
141
|
+
if (encoded) {
|
|
142
|
+
path += "?t=" + encoded;
|
|
87
143
|
}
|
|
88
144
|
}
|
|
89
|
-
|
|
90
|
-
|
|
145
|
+
const hash = window.location.hash;
|
|
146
|
+
const fullUrl = path + hash;
|
|
147
|
+
if (window.location.pathname + window.location.search !== path) {
|
|
148
|
+
if (replace) {
|
|
149
|
+
window.history.replaceState(null, "", fullUrl);
|
|
150
|
+
} else {
|
|
151
|
+
window.history.pushState(null, "", fullUrl);
|
|
152
|
+
}
|
|
91
153
|
}
|
|
154
|
+
lastEncodedUrl = encoded;
|
|
92
155
|
}
|
|
93
156
|
|
|
157
|
+
export function syncUrlFromWorkspace(): void {
|
|
158
|
+
const state = sidebarStore.state;
|
|
159
|
+
if (state.sidebarMode === "settings") return;
|
|
160
|
+
if (!state.activeProjectSlug) return;
|
|
161
|
+
|
|
162
|
+
const wsState = getWorkspaceStore().state;
|
|
163
|
+
const encoded = encodeWorkspaceUrl(wsState, wsState.tabs, state.activeProjectSlug);
|
|
164
|
+
|
|
165
|
+
if (encoded === lastEncodedUrl) return;
|
|
166
|
+
|
|
167
|
+
const isActiveTabChangeOnly = detectActiveTabChangeOnly(lastEncodedUrl, encoded);
|
|
168
|
+
lastEncodedUrl = encoded;
|
|
169
|
+
|
|
170
|
+
let path = "/" + state.activeProjectSlug;
|
|
171
|
+
if (encoded) {
|
|
172
|
+
path += "?t=" + encoded;
|
|
173
|
+
}
|
|
174
|
+
const hash = window.location.hash;
|
|
175
|
+
const fullUrl = path + hash;
|
|
176
|
+
|
|
177
|
+
if (window.location.pathname + window.location.search !== path) {
|
|
178
|
+
if (isActiveTabChangeOnly) {
|
|
179
|
+
window.history.replaceState(null, "", fullUrl);
|
|
180
|
+
} else {
|
|
181
|
+
window.history.pushState(null, "", fullUrl);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function detectActiveTabChangeOnly(oldEncoded: string, newEncoded: string): boolean {
|
|
187
|
+
const oldStripped = oldEncoded.replace(/\*/g, "");
|
|
188
|
+
const newStripped = newEncoded.replace(/\*/g, "");
|
|
189
|
+
return oldStripped === newStripped;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
setUrlSyncCallback(syncUrlFromWorkspace);
|
|
193
|
+
|
|
94
194
|
export function getSidebarStore(): Store<SidebarState> {
|
|
95
195
|
return sidebarStore;
|
|
96
196
|
}
|
|
@@ -107,11 +207,17 @@ export function setActiveProjectSlug(slug: string | null): void {
|
|
|
107
207
|
projectDropdownOpen: false,
|
|
108
208
|
};
|
|
109
209
|
});
|
|
110
|
-
|
|
210
|
+
let path = "/";
|
|
211
|
+
if (slug) {
|
|
212
|
+
path = "/" + slug;
|
|
213
|
+
}
|
|
214
|
+
if (window.location.pathname + window.location.search !== path) {
|
|
215
|
+
window.history.pushState(null, "", path);
|
|
216
|
+
}
|
|
217
|
+
lastEncodedUrl = "";
|
|
111
218
|
}
|
|
112
219
|
|
|
113
220
|
export function setActiveSessionId(sessionId: string | null): void {
|
|
114
|
-
var state = sidebarStore.state;
|
|
115
221
|
sidebarStore.setState(function (s) {
|
|
116
222
|
return {
|
|
117
223
|
...s,
|
|
@@ -119,7 +225,6 @@ export function setActiveSessionId(sessionId: string | null): void {
|
|
|
119
225
|
activeView: sessionId ? { type: "chat" } : s.activeView,
|
|
120
226
|
};
|
|
121
227
|
});
|
|
122
|
-
pushUrl(state.activeProjectSlug, sessionId);
|
|
123
228
|
}
|
|
124
229
|
|
|
125
230
|
export function navigateToSession(projectSlug: string, sessionId: string): void {
|
|
@@ -134,7 +239,6 @@ export function navigateToSession(projectSlug: string, sessionId: string): void
|
|
|
134
239
|
projectDropdownOpen: false,
|
|
135
240
|
};
|
|
136
241
|
});
|
|
137
|
-
pushUrl(projectSlug, sessionId);
|
|
138
242
|
}
|
|
139
243
|
|
|
140
244
|
export function openSettings(section: SettingsSection): void {
|
|
@@ -148,7 +252,7 @@ export function openSettings(section: SettingsSection): void {
|
|
|
148
252
|
projectDropdownOpen: false,
|
|
149
253
|
};
|
|
150
254
|
});
|
|
151
|
-
|
|
255
|
+
const path = "/settings/" + section;
|
|
152
256
|
if (window.location.pathname !== path) {
|
|
153
257
|
window.history.pushState(null, "", path);
|
|
154
258
|
}
|
|
@@ -161,7 +265,7 @@ export function setSettingsSection(section: SettingsSection): void {
|
|
|
161
265
|
activeView: { type: "settings", section: section },
|
|
162
266
|
};
|
|
163
267
|
});
|
|
164
|
-
|
|
268
|
+
const path = "/settings/" + section;
|
|
165
269
|
if (window.location.pathname !== path) {
|
|
166
270
|
window.history.replaceState(null, "", path);
|
|
167
271
|
}
|
|
@@ -178,8 +282,8 @@ export function openProjectSettings(section: ProjectSettingsSection): void {
|
|
|
178
282
|
projectDropdownOpen: false,
|
|
179
283
|
};
|
|
180
284
|
});
|
|
181
|
-
|
|
182
|
-
|
|
285
|
+
const state = sidebarStore.state;
|
|
286
|
+
const path = "/" + state.activeProjectSlug + "/settings/" + section;
|
|
183
287
|
if (window.location.pathname !== path) {
|
|
184
288
|
window.history.pushState(null, "", path);
|
|
185
289
|
}
|
|
@@ -192,16 +296,16 @@ export function setProjectSettingsSection(section: ProjectSettingsSection): void
|
|
|
192
296
|
activeView: { type: "project-settings", section: section },
|
|
193
297
|
};
|
|
194
298
|
});
|
|
195
|
-
|
|
196
|
-
|
|
299
|
+
const state = sidebarStore.state;
|
|
300
|
+
const path = "/" + state.activeProjectSlug + "/settings/" + section;
|
|
197
301
|
if (window.location.pathname !== path) {
|
|
198
302
|
window.history.replaceState(null, "", path);
|
|
199
303
|
}
|
|
200
304
|
}
|
|
201
305
|
|
|
202
306
|
export function exitSettings(): void {
|
|
203
|
-
|
|
204
|
-
|
|
307
|
+
const state = sidebarStore.state;
|
|
308
|
+
const restored = state.previousView ?? { type: "chat" } as ActiveView;
|
|
205
309
|
sidebarStore.setState(function (s) {
|
|
206
310
|
return {
|
|
207
311
|
...s,
|
|
@@ -210,11 +314,7 @@ export function exitSettings(): void {
|
|
|
210
314
|
previousView: null,
|
|
211
315
|
};
|
|
212
316
|
});
|
|
213
|
-
|
|
214
|
-
pushUrl(state.activeProjectSlug, null);
|
|
215
|
-
} else {
|
|
216
|
-
pushUrl(state.activeProjectSlug, state.activeSessionId);
|
|
217
|
-
}
|
|
317
|
+
pushUrl(state.activeProjectSlug);
|
|
218
318
|
}
|
|
219
319
|
|
|
220
320
|
export function goToProjectDashboard(): void {
|
|
@@ -228,8 +328,15 @@ export function goToProjectDashboard(): void {
|
|
|
228
328
|
projectDropdownOpen: false,
|
|
229
329
|
};
|
|
230
330
|
});
|
|
231
|
-
|
|
232
|
-
|
|
331
|
+
const state = sidebarStore.state;
|
|
332
|
+
let path = "/";
|
|
333
|
+
if (state.activeProjectSlug) {
|
|
334
|
+
path = "/" + state.activeProjectSlug;
|
|
335
|
+
}
|
|
336
|
+
if (window.location.pathname + window.location.search !== path) {
|
|
337
|
+
window.history.pushState(null, "", path);
|
|
338
|
+
}
|
|
339
|
+
lastEncodedUrl = "";
|
|
233
340
|
}
|
|
234
341
|
|
|
235
342
|
export function goToDashboard(): void {
|
|
@@ -244,54 +351,96 @@ export function goToDashboard(): void {
|
|
|
244
351
|
projectDropdownOpen: false,
|
|
245
352
|
};
|
|
246
353
|
});
|
|
247
|
-
|
|
354
|
+
if (window.location.pathname + window.location.search !== "/") {
|
|
355
|
+
window.history.pushState(null, "", "/");
|
|
356
|
+
}
|
|
357
|
+
lastEncodedUrl = "";
|
|
248
358
|
}
|
|
249
359
|
|
|
250
360
|
export function goToAnalytics(): void {
|
|
251
361
|
sidebarStore.setState(function (state) {
|
|
252
362
|
return { ...state, activeView: { type: "analytics" }, sidebarMode: "project" };
|
|
253
363
|
});
|
|
254
|
-
pushUrl(sidebarStore.state.activeProjectSlug
|
|
364
|
+
pushUrl(sidebarStore.state.activeProjectSlug);
|
|
255
365
|
}
|
|
256
366
|
|
|
257
367
|
export function handlePopState(): void {
|
|
258
|
-
|
|
259
|
-
|
|
368
|
+
const path = window.location.pathname;
|
|
369
|
+
const parts = path.split("/").filter(function (p) { return p.length > 0; });
|
|
370
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
371
|
+
const tParam = searchParams.get("t");
|
|
372
|
+
|
|
373
|
+
if (parts[0] === "settings") {
|
|
374
|
+
const section = (parts[1] || "appearance") as SettingsSection;
|
|
260
375
|
sidebarStore.setState(function (state) {
|
|
261
376
|
return {
|
|
262
377
|
...state,
|
|
263
378
|
sidebarMode: "settings",
|
|
264
|
-
activeView: { type: "settings", section:
|
|
379
|
+
activeView: { type: "settings", section: section },
|
|
265
380
|
userMenuOpen: false,
|
|
266
381
|
projectDropdownOpen: false,
|
|
267
382
|
};
|
|
268
383
|
});
|
|
269
|
-
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (parts.length >= 2 && parts[1] === "settings") {
|
|
388
|
+
const projectSettingsSection = (parts[2] || "general") as ProjectSettingsSection;
|
|
270
389
|
sidebarStore.setState(function (state) {
|
|
271
390
|
return {
|
|
272
391
|
...state,
|
|
273
|
-
activeProjectSlug:
|
|
392
|
+
activeProjectSlug: parts[0],
|
|
274
393
|
sidebarMode: "settings",
|
|
275
|
-
activeView: { type: "project-settings", section:
|
|
394
|
+
activeView: { type: "project-settings", section: projectSettingsSection },
|
|
276
395
|
userMenuOpen: false,
|
|
277
396
|
projectDropdownOpen: false,
|
|
278
397
|
};
|
|
279
398
|
});
|
|
280
|
-
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const projectSlug = parts.length > 0 ? parts[0] : null;
|
|
403
|
+
|
|
404
|
+
if (tParam && projectSlug) {
|
|
405
|
+
const decoded = decodeWorkspaceUrl(tParam, projectSlug, resolveFullIdFromNothing);
|
|
406
|
+
restoreWorkspace(decoded);
|
|
407
|
+
lastEncodedUrl = tParam;
|
|
408
|
+
|
|
409
|
+
let sessionId: string | null = null;
|
|
410
|
+
const activePane = decoded.panes.find(function (p) { return p.id === decoded.activePaneId; });
|
|
411
|
+
if (activePane) {
|
|
412
|
+
const activeTab = decoded.tabs.find(function (t) { return t.id === activePane.activeTabId; });
|
|
413
|
+
if (activeTab && activeTab.type === "chat" && activeTab.sessionId) {
|
|
414
|
+
sessionId = activeTab.sessionId;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
281
418
|
sidebarStore.setState(function (state) {
|
|
282
419
|
return {
|
|
283
420
|
...state,
|
|
284
|
-
activeProjectSlug:
|
|
285
|
-
activeSessionId:
|
|
421
|
+
activeProjectSlug: projectSlug,
|
|
422
|
+
activeSessionId: sessionId,
|
|
286
423
|
sidebarMode: "project",
|
|
287
|
-
activeView:
|
|
288
|
-
? (url.sessionId ? { type: "chat" } : { type: "project-dashboard" })
|
|
289
|
-
: { type: "dashboard" },
|
|
424
|
+
activeView: sessionId || decoded.tabs.length > 0 ? { type: "chat" } : { type: "project-dashboard" },
|
|
290
425
|
userMenuOpen: false,
|
|
291
426
|
projectDropdownOpen: false,
|
|
292
427
|
};
|
|
293
428
|
});
|
|
429
|
+
return;
|
|
294
430
|
}
|
|
431
|
+
|
|
432
|
+
sidebarStore.setState(function (state) {
|
|
433
|
+
return {
|
|
434
|
+
...state,
|
|
435
|
+
activeProjectSlug: projectSlug,
|
|
436
|
+
activeSessionId: null,
|
|
437
|
+
sidebarMode: "project",
|
|
438
|
+
activeView: projectSlug ? { type: "project-dashboard" } : { type: "dashboard" },
|
|
439
|
+
userMenuOpen: false,
|
|
440
|
+
projectDropdownOpen: false,
|
|
441
|
+
};
|
|
442
|
+
});
|
|
443
|
+
lastEncodedUrl = "";
|
|
295
444
|
}
|
|
296
445
|
|
|
297
446
|
export function toggleUserMenu(): void {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Store } from "@tanstack/react-store";
|
|
2
|
+
import type { DecodedWorkspace } from "../lib/workspace-url";
|
|
2
3
|
|
|
3
4
|
export type TabType = "chat" | "files" | "terminal" | "notes" | "tasks" | "bookmarks" | "analytics";
|
|
4
5
|
|
|
@@ -364,3 +365,34 @@ export function getActiveSessionTab(): Tab | null {
|
|
|
364
365
|
if (!activeTab || activeTab.type !== "chat") return null;
|
|
365
366
|
return activeTab;
|
|
366
367
|
}
|
|
368
|
+
|
|
369
|
+
let urlSyncCallback: (() => void) | null = null;
|
|
370
|
+
let urlSyncSuppressed = false;
|
|
371
|
+
|
|
372
|
+
export function setUrlSyncCallback(cb: () => void): void {
|
|
373
|
+
urlSyncCallback = cb;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function notifyUrlSync(): void {
|
|
377
|
+
if (!urlSyncSuppressed && urlSyncCallback) {
|
|
378
|
+
urlSyncCallback();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function restoreWorkspace(data: DecodedWorkspace): void {
|
|
383
|
+
urlSyncSuppressed = true;
|
|
384
|
+
workspaceStore.setState(function (state) {
|
|
385
|
+
return {
|
|
386
|
+
...state,
|
|
387
|
+
tabs: data.tabs,
|
|
388
|
+
panes: data.panes,
|
|
389
|
+
activePaneId: data.activePaneId,
|
|
390
|
+
splitDirection: data.splitDirection,
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
urlSyncSuppressed = false;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
workspaceStore.subscribe(function () {
|
|
397
|
+
notifyUrlSync();
|
|
398
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"description": "Multi-machine agentic dashboard for Claude Code. Monitor sessions, manage MCP servers and skills, orchestrate across mesh-networked nodes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Aaron Scherer <me@aaronscherer.me>",
|