@cryptiklemur/lattice 0.0.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/.editorconfig +12 -0
- package/.github/workflows/release.yml +44 -0
- package/.impeccable.md +66 -0
- package/.releaserc.json +32 -0
- package/.serena/project.yml +138 -0
- package/CLAUDE.md +35 -0
- package/CONTRIBUTING.md +93 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/bun.lock +1459 -0
- package/bunfig.toml +2 -0
- package/client/index.html +32 -0
- package/client/package.json +37 -0
- package/client/public/icons/icon-192.svg +11 -0
- package/client/public/icons/icon-512.svg +11 -0
- package/client/public/manifest.json +24 -0
- package/client/public/sw.js +61 -0
- package/client/src/App.tsx +28 -0
- package/client/src/components/auth/PassphrasePrompt.tsx +70 -0
- package/client/src/components/chat/ChatInput.tsx +241 -0
- package/client/src/components/chat/ChatView.tsx +727 -0
- package/client/src/components/chat/Message.tsx +362 -0
- package/client/src/components/chat/ModelSelector.tsx +87 -0
- package/client/src/components/chat/PermissionModeSelector.tsx +41 -0
- package/client/src/components/chat/StatusBar.tsx +50 -0
- package/client/src/components/chat/ToolGroup.tsx +129 -0
- package/client/src/components/chat/ToolResultRenderer.tsx +343 -0
- package/client/src/components/chat/toolSummary.ts +41 -0
- package/client/src/components/dashboard/DashboardView.tsx +219 -0
- package/client/src/components/dashboard/ProjectDashboardView.tsx +168 -0
- package/client/src/components/mesh/NodeBadge.tsx +24 -0
- package/client/src/components/mesh/PairingDialog.tsx +281 -0
- package/client/src/components/panels/FileBrowser.tsx +241 -0
- package/client/src/components/panels/StickyNotes.tsx +187 -0
- package/client/src/components/panels/Terminal.tsx +128 -0
- package/client/src/components/project-settings/ProjectClaude.tsx +304 -0
- package/client/src/components/project-settings/ProjectEnvironment.tsx +235 -0
- package/client/src/components/project-settings/ProjectGeneral.tsx +76 -0
- package/client/src/components/project-settings/ProjectMcp.tsx +232 -0
- package/client/src/components/project-settings/ProjectPermissions.tsx +209 -0
- package/client/src/components/project-settings/ProjectRules.tsx +277 -0
- package/client/src/components/project-settings/ProjectSettingsView.tsx +99 -0
- package/client/src/components/project-settings/ProjectSkills.tsx +91 -0
- package/client/src/components/settings/Appearance.tsx +151 -0
- package/client/src/components/settings/ClaudeSettings.tsx +151 -0
- package/client/src/components/settings/Environment.tsx +185 -0
- package/client/src/components/settings/GlobalMcp.tsx +207 -0
- package/client/src/components/settings/GlobalSkills.tsx +125 -0
- package/client/src/components/settings/MeshStatus.tsx +145 -0
- package/client/src/components/settings/SettingsView.tsx +57 -0
- package/client/src/components/settings/SkillMarketplace.tsx +175 -0
- package/client/src/components/settings/mcp-shared.tsx +194 -0
- package/client/src/components/settings/skill-shared.tsx +177 -0
- package/client/src/components/setup/SetupWizard.tsx +750 -0
- package/client/src/components/sidebar/NodeSettingsModal.tsx +180 -0
- package/client/src/components/sidebar/ProjectDropdown.tsx +43 -0
- package/client/src/components/sidebar/ProjectRail.tsx +291 -0
- package/client/src/components/sidebar/SearchFilter.tsx +52 -0
- package/client/src/components/sidebar/SessionList.tsx +384 -0
- package/client/src/components/sidebar/SettingsSidebar.tsx +128 -0
- package/client/src/components/sidebar/Sidebar.tsx +209 -0
- package/client/src/components/sidebar/UserIsland.tsx +59 -0
- package/client/src/components/sidebar/UserMenu.tsx +101 -0
- package/client/src/components/ui/CommandPalette.tsx +321 -0
- package/client/src/components/ui/ErrorBoundary.tsx +56 -0
- package/client/src/components/ui/IconPicker.tsx +209 -0
- package/client/src/components/ui/LatticeLogomark.tsx +19 -0
- package/client/src/components/ui/PopupMenu.tsx +98 -0
- package/client/src/components/ui/SaveFooter.tsx +38 -0
- package/client/src/components/ui/Toast.tsx +112 -0
- package/client/src/hooks/useMesh.ts +89 -0
- package/client/src/hooks/useProjectSettings.ts +56 -0
- package/client/src/hooks/useProjects.ts +66 -0
- package/client/src/hooks/useSaveState.ts +59 -0
- package/client/src/hooks/useSession.ts +317 -0
- package/client/src/hooks/useSidebar.ts +74 -0
- package/client/src/hooks/useSkills.ts +30 -0
- package/client/src/hooks/useTheme.ts +114 -0
- package/client/src/hooks/useWebSocket.ts +26 -0
- package/client/src/main.tsx +10 -0
- package/client/src/providers/WebSocketProvider.tsx +146 -0
- package/client/src/router.tsx +391 -0
- package/client/src/stores/mesh.ts +78 -0
- package/client/src/stores/session.ts +322 -0
- package/client/src/stores/sidebar.ts +336 -0
- package/client/src/stores/theme.ts +44 -0
- package/client/src/styles/global.css +167 -0
- package/client/src/styles/theme-vars.css +18 -0
- package/client/src/themes/index.ts +79 -0
- package/client/src/utils/findDuplicateKeys.ts +12 -0
- package/client/tsconfig.json +14 -0
- package/client/vite.config.ts +20 -0
- package/package.json +46 -0
- package/server/package.json +22 -0
- package/server/src/auth/passphrase.ts +48 -0
- package/server/src/config.ts +55 -0
- package/server/src/daemon.ts +338 -0
- package/server/src/features/ralph-loop.ts +173 -0
- package/server/src/features/scheduler.ts +281 -0
- package/server/src/features/sticky-notes.ts +102 -0
- package/server/src/handlers/chat.ts +194 -0
- package/server/src/handlers/fs.ts +84 -0
- package/server/src/handlers/loop.ts +37 -0
- package/server/src/handlers/mesh.ts +125 -0
- package/server/src/handlers/notes.ts +45 -0
- package/server/src/handlers/project-settings.ts +174 -0
- package/server/src/handlers/scheduler.ts +47 -0
- package/server/src/handlers/session.ts +159 -0
- package/server/src/handlers/settings.ts +109 -0
- package/server/src/handlers/skills.ts +380 -0
- package/server/src/handlers/terminal.ts +70 -0
- package/server/src/identity.ts +26 -0
- package/server/src/index.ts +190 -0
- package/server/src/mesh/connector.ts +209 -0
- package/server/src/mesh/discovery.ts +123 -0
- package/server/src/mesh/pairing.ts +94 -0
- package/server/src/mesh/peers.ts +52 -0
- package/server/src/mesh/proxy.ts +103 -0
- package/server/src/mesh/session-sync.ts +107 -0
- package/server/src/project/context-breakdown.ts +289 -0
- package/server/src/project/file-browser.ts +106 -0
- package/server/src/project/project-files.ts +267 -0
- package/server/src/project/registry.ts +57 -0
- package/server/src/project/sdk-bridge.ts +566 -0
- package/server/src/project/session.ts +432 -0
- package/server/src/project/terminal.ts +69 -0
- package/server/src/tls.ts +51 -0
- package/server/src/ws/broadcast.ts +31 -0
- package/server/src/ws/router.ts +104 -0
- package/server/src/ws/server.ts +2 -0
- package/server/tsconfig.json +16 -0
- package/shared/package.json +11 -0
- package/shared/src/constants.ts +7 -0
- package/shared/src/index.ts +4 -0
- package/shared/src/messages.ts +638 -0
- package/shared/src/models.ts +136 -0
- package/shared/src/project-settings.ts +45 -0
- package/shared/tsconfig.json +11 -0
- package/themes/amoled.json +20 -0
- package/themes/ayu-light.json +9 -0
- package/themes/catppuccin-latte.json +9 -0
- package/themes/catppuccin-mocha.json +9 -0
- package/themes/clay-light.json +10 -0
- package/themes/clay.json +10 -0
- package/themes/dracula.json +9 -0
- package/themes/everforest-light.json +9 -0
- package/themes/everforest.json +9 -0
- package/themes/github-light.json +9 -0
- package/themes/gruvbox-dark.json +9 -0
- package/themes/gruvbox-light.json +9 -0
- package/themes/monokai.json +9 -0
- package/themes/nord-light.json +9 -0
- package/themes/nord.json +9 -0
- package/themes/one-dark.json +9 -0
- package/themes/one-light.json +9 -0
- package/themes/rose-pine-dawn.json +9 -0
- package/themes/rose-pine.json +9 -0
- package/themes/solarized-dark.json +9 -0
- package/themes/solarized-light.json +9 -0
- package/themes/tokyo-night-light.json +9 -0
- package/themes/tokyo-night.json +9 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { Plus, ChevronDown, Search, LayoutDashboard } from "lucide-react";
|
|
3
|
+
import { LatticeLogomark } from "../ui/LatticeLogomark";
|
|
4
|
+
import type { SessionSummary, ServerMessage, SettingsDataMessage } from "@lattice/shared";
|
|
5
|
+
import { useProjects } from "../../hooks/useProjects";
|
|
6
|
+
import { useMesh } from "../../hooks/useMesh";
|
|
7
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
8
|
+
import { useSidebar } from "../../hooks/useSidebar";
|
|
9
|
+
import { useSession } from "../../hooks/useSession";
|
|
10
|
+
import { clearSession } from "../../stores/session";
|
|
11
|
+
import { ProjectRail } from "./ProjectRail";
|
|
12
|
+
import { SessionList } from "./SessionList";
|
|
13
|
+
import { UserIsland } from "./UserIsland";
|
|
14
|
+
import { UserMenu } from "./UserMenu";
|
|
15
|
+
import { SearchFilter } from "./SearchFilter";
|
|
16
|
+
import { ProjectDropdown } from "./ProjectDropdown";
|
|
17
|
+
import { SettingsSidebar } from "./SettingsSidebar";
|
|
18
|
+
|
|
19
|
+
function SectionLabel({ label, actions }: { label: string; actions?: React.ReactNode }) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="px-4 pt-4 pb-2 flex items-center justify-between flex-shrink-0 select-none">
|
|
22
|
+
<span className="text-xs font-bold tracking-wider uppercase text-base-content/40">
|
|
23
|
+
{label}
|
|
24
|
+
</span>
|
|
25
|
+
{actions && (
|
|
26
|
+
<div className="flex items-center gap-0.5">
|
|
27
|
+
{actions}
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function Sidebar({ onSessionSelect }: { onSessionSelect?: () => void }) {
|
|
35
|
+
var { projects, activeProject } = useProjects();
|
|
36
|
+
var { nodes } = useMesh();
|
|
37
|
+
var ws = useWebSocket();
|
|
38
|
+
var sidebar = useSidebar();
|
|
39
|
+
var session = useSession();
|
|
40
|
+
var [sessionSearch, setSessionSearch] = useState<string>("");
|
|
41
|
+
var [sessionSearchOpen, setSessionSearchOpen] = useState<boolean>(false);
|
|
42
|
+
var userIslandRef = useRef<HTMLElement | null>(null);
|
|
43
|
+
var projectHeaderRef = useRef<HTMLElement | null>(null);
|
|
44
|
+
|
|
45
|
+
var localNode = nodes.find(function (n) { return n.isLocal; });
|
|
46
|
+
var [configNodeName, setConfigNodeName] = useState("");
|
|
47
|
+
|
|
48
|
+
useEffect(function () {
|
|
49
|
+
function handleSettingsData(msg: ServerMessage) {
|
|
50
|
+
if (msg.type !== "settings:data") return;
|
|
51
|
+
var data = msg as SettingsDataMessage;
|
|
52
|
+
if (data.config.name) {
|
|
53
|
+
setConfigNodeName(data.config.name);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
ws.subscribe("settings:data", handleSettingsData);
|
|
57
|
+
ws.send({ type: "settings:get" });
|
|
58
|
+
return function () {
|
|
59
|
+
ws.unsubscribe("settings:data", handleSettingsData);
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
var localNodeName = localNode ? localNode.name : configNodeName;
|
|
64
|
+
var initialActivatedRef = useRef<boolean>(false);
|
|
65
|
+
|
|
66
|
+
useEffect(function () {
|
|
67
|
+
if (initialActivatedRef.current) return;
|
|
68
|
+
if (!sidebar.activeProjectSlug || !sidebar.activeSessionId) return;
|
|
69
|
+
if (!activeProject) return;
|
|
70
|
+
initialActivatedRef.current = true;
|
|
71
|
+
session.activateSession(sidebar.activeProjectSlug, sidebar.activeSessionId);
|
|
72
|
+
}, [sidebar.activeProjectSlug, sidebar.activeSessionId, activeProject]);
|
|
73
|
+
|
|
74
|
+
// Ctrl/Cmd+K is handled by the global CommandPalette
|
|
75
|
+
|
|
76
|
+
function handleSessionActivate(s: SessionSummary) {
|
|
77
|
+
if (activeProject) {
|
|
78
|
+
session.activateSession(activeProject.slug, s.id);
|
|
79
|
+
}
|
|
80
|
+
sidebar.closeMenus();
|
|
81
|
+
if (onSessionSelect) {
|
|
82
|
+
onSessionSelect();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function handleNewSession() {
|
|
87
|
+
if (!activeProject?.slug) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
ws.send({ type: "session:create", projectSlug: activeProject.slug });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="flex flex-row h-full w-full overflow-hidden relative">
|
|
95
|
+
<ProjectRail
|
|
96
|
+
projects={projects}
|
|
97
|
+
nodes={nodes}
|
|
98
|
+
activeProjectSlug={sidebar.activeProjectSlug}
|
|
99
|
+
onSelectProject={sidebar.setActiveProjectSlug}
|
|
100
|
+
onDashboardClick={sidebar.goToDashboard}
|
|
101
|
+
isDashboardActive={sidebar.activeView.type === "dashboard"}
|
|
102
|
+
dimmed={sidebar.sidebarMode === "settings"}
|
|
103
|
+
/>
|
|
104
|
+
<div className="flex flex-col flex-1 overflow-hidden min-h-0 bg-base-200 border-r border-base-300">
|
|
105
|
+
{sidebar.sidebarMode === "project" ? (
|
|
106
|
+
<>
|
|
107
|
+
{sidebar.activeView.type === "dashboard" ? (
|
|
108
|
+
<>
|
|
109
|
+
<div className="px-4 py-3 border-b border-base-300 flex-shrink-0 flex items-center gap-2">
|
|
110
|
+
<LatticeLogomark size={18} />
|
|
111
|
+
<span className="text-[13px] font-mono font-bold text-base-content/90">
|
|
112
|
+
Lattice
|
|
113
|
+
</span>
|
|
114
|
+
</div>
|
|
115
|
+
<div className="flex-1 overflow-auto px-4 py-3 pb-16">
|
|
116
|
+
<SectionLabel label="Projects" />
|
|
117
|
+
<div className="text-[12px] text-base-content/40 px-4">
|
|
118
|
+
Select a project from the rail to view sessions.
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</>
|
|
122
|
+
) : (
|
|
123
|
+
<>
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
ref={function (el) { projectHeaderRef.current = el; }}
|
|
127
|
+
onClick={sidebar.toggleProjectDropdown}
|
|
128
|
+
aria-label="Switch project"
|
|
129
|
+
aria-expanded={sidebar.projectDropdownOpen}
|
|
130
|
+
className="w-full px-4 py-3 border-b border-base-300 flex-shrink-0 flex items-center justify-between cursor-pointer hover:bg-base-300/30 transition-colors text-left"
|
|
131
|
+
>
|
|
132
|
+
<span className="text-[13px] font-mono font-bold text-base-content/90">
|
|
133
|
+
{activeProject?.title ?? "No Project"}
|
|
134
|
+
</span>
|
|
135
|
+
<ChevronDown size={14} className="text-base-content/30" />
|
|
136
|
+
</button>
|
|
137
|
+
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
onClick={function () { sidebar.goToProjectDashboard(); }}
|
|
141
|
+
className="flex items-center gap-2 mx-3 mt-2 px-2 py-1.5 rounded-lg text-[11px] text-base-content/40 hover:text-base-content/70 hover:bg-base-300/30 transition-colors"
|
|
142
|
+
>
|
|
143
|
+
<LayoutDashboard size={12} />
|
|
144
|
+
<span className="font-mono tracking-wide">Dashboard</span>
|
|
145
|
+
</button>
|
|
146
|
+
|
|
147
|
+
<SectionLabel
|
|
148
|
+
label="Sessions"
|
|
149
|
+
actions={
|
|
150
|
+
<>
|
|
151
|
+
<button onClick={function () { setSessionSearchOpen(function (v) { return !v; }); }} className="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-base-content" aria-label="Search sessions">
|
|
152
|
+
<Search size={13} />
|
|
153
|
+
</button>
|
|
154
|
+
<button onClick={handleNewSession} className="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-base-content" aria-label="New session">
|
|
155
|
+
<Plus size={13} />
|
|
156
|
+
</button>
|
|
157
|
+
</>
|
|
158
|
+
}
|
|
159
|
+
/>
|
|
160
|
+
{sessionSearchOpen && (
|
|
161
|
+
<SearchFilter
|
|
162
|
+
value={sessionSearch}
|
|
163
|
+
onChange={setSessionSearch}
|
|
164
|
+
onClose={function () { setSessionSearchOpen(false); setSessionSearch(""); }}
|
|
165
|
+
placeholder="Filter sessions..."
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
168
|
+
<SessionList
|
|
169
|
+
projectSlug={activeProject?.slug ?? null}
|
|
170
|
+
activeSessionId={session.activeSessionId}
|
|
171
|
+
onSessionActivate={handleSessionActivate}
|
|
172
|
+
onSessionDeactivate={clearSession}
|
|
173
|
+
filter={sessionSearch}
|
|
174
|
+
/>
|
|
175
|
+
</>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
</>
|
|
179
|
+
) : (
|
|
180
|
+
<SettingsSidebar
|
|
181
|
+
projectName={activeProject?.title ?? "Dashboard"}
|
|
182
|
+
onBack={sidebar.exitSettings}
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div
|
|
188
|
+
ref={function (el) { userIslandRef.current = el; }}
|
|
189
|
+
className="absolute bottom-2 left-2 right-2 z-10 bg-base-300 border border-base-content/15 rounded-xl shadow-lg"
|
|
190
|
+
>
|
|
191
|
+
<UserIsland nodeName={localNodeName} onClick={sidebar.toggleUserMenu} />
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{sidebar.userMenuOpen && (
|
|
195
|
+
<UserMenu
|
|
196
|
+
anchorRef={userIslandRef}
|
|
197
|
+
onClose={sidebar.closeMenus}
|
|
198
|
+
onOpenNodeSettings={sidebar.openNodeSettings}
|
|
199
|
+
/>
|
|
200
|
+
)}
|
|
201
|
+
{sidebar.projectDropdownOpen && (
|
|
202
|
+
<ProjectDropdown
|
|
203
|
+
anchorRef={projectHeaderRef}
|
|
204
|
+
onClose={sidebar.closeMenus}
|
|
205
|
+
/>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Sun, Moon, Settings } from "lucide-react";
|
|
2
|
+
import { useTheme } from "../../hooks/useTheme";
|
|
3
|
+
import { useSidebar } from "../../hooks/useSidebar";
|
|
4
|
+
import pkg from "../../../package.json";
|
|
5
|
+
|
|
6
|
+
interface UserIslandProps {
|
|
7
|
+
nodeName: string;
|
|
8
|
+
onClick: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function UserIsland(props: UserIslandProps) {
|
|
12
|
+
var { mode, toggleMode } = useTheme();
|
|
13
|
+
var sidebar = useSidebar();
|
|
14
|
+
|
|
15
|
+
var initial = props.nodeName.charAt(0).toUpperCase();
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
role="group"
|
|
20
|
+
aria-label="User controls"
|
|
21
|
+
className="flex items-center gap-2 px-3 py-2"
|
|
22
|
+
>
|
|
23
|
+
<button
|
|
24
|
+
onClick={props.onClick}
|
|
25
|
+
className="flex items-center gap-2 flex-1 min-w-0 rounded-lg px-1 py-1 -mx-1 hover:bg-base-content/5 transition-colors duration-[120ms] cursor-pointer"
|
|
26
|
+
aria-label="Node info"
|
|
27
|
+
>
|
|
28
|
+
<div className="w-7 h-7 rounded-full bg-primary text-primary-content text-[12px] font-bold flex items-center justify-center flex-shrink-0">
|
|
29
|
+
{initial}
|
|
30
|
+
</div>
|
|
31
|
+
<div className="flex-1 min-w-0 text-left">
|
|
32
|
+
<div className="text-[13px] font-semibold text-base-content truncate">
|
|
33
|
+
{props.nodeName}
|
|
34
|
+
</div>
|
|
35
|
+
<div className="text-[10px] text-base-content/30 font-mono">
|
|
36
|
+
{"v" + pkg.version}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</button>
|
|
40
|
+
|
|
41
|
+
<div className="flex items-center gap-0.5 flex-shrink-0">
|
|
42
|
+
<button
|
|
43
|
+
aria-label="Global settings"
|
|
44
|
+
onClick={function () { sidebar.openSettings("appearance"); }}
|
|
45
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-base-content transition-colors"
|
|
46
|
+
>
|
|
47
|
+
<Settings size={14} />
|
|
48
|
+
</button>
|
|
49
|
+
<button
|
|
50
|
+
aria-label={mode === "dark" ? "Switch to light mode" : "Switch to dark mode"}
|
|
51
|
+
onClick={function (e) { e.stopPropagation(); toggleMode(); }}
|
|
52
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-base-content transition-colors"
|
|
53
|
+
>
|
|
54
|
+
{mode === "dark" ? <Sun size={14} /> : <Moon size={14} />}
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Settings, RefreshCw, Power } from "lucide-react";
|
|
3
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
4
|
+
|
|
5
|
+
interface UserMenuProps {
|
|
6
|
+
anchorRef: React.RefObject<HTMLElement | null>;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
onOpenNodeSettings: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function UserMenu(props: UserMenuProps) {
|
|
12
|
+
var menuRef = useRef<HTMLDivElement>(null);
|
|
13
|
+
var ws = useWebSocket();
|
|
14
|
+
var [confirmingRestart, setConfirmingRestart] = useState(false);
|
|
15
|
+
var [confirmingShutdown, setConfirmingShutdown] = useState(false);
|
|
16
|
+
|
|
17
|
+
useEffect(function () {
|
|
18
|
+
function handleClickOutside(e: MouseEvent) {
|
|
19
|
+
if (
|
|
20
|
+
menuRef.current &&
|
|
21
|
+
!menuRef.current.contains(e.target as Node) &&
|
|
22
|
+
props.anchorRef.current &&
|
|
23
|
+
!props.anchorRef.current.contains(e.target as Node)
|
|
24
|
+
) {
|
|
25
|
+
props.onClose();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function handleEscape(e: KeyboardEvent) {
|
|
29
|
+
if (e.key === "Escape") {
|
|
30
|
+
props.onClose();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function handleScroll() {
|
|
34
|
+
props.onClose();
|
|
35
|
+
}
|
|
36
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
37
|
+
document.addEventListener("keydown", handleEscape);
|
|
38
|
+
window.addEventListener("scroll", handleScroll, true);
|
|
39
|
+
return function () {
|
|
40
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
41
|
+
document.removeEventListener("keydown", handleEscape);
|
|
42
|
+
window.removeEventListener("scroll", handleScroll, true);
|
|
43
|
+
};
|
|
44
|
+
}, [props.onClose, props.anchorRef]);
|
|
45
|
+
|
|
46
|
+
var style: React.CSSProperties = {};
|
|
47
|
+
if (props.anchorRef.current) {
|
|
48
|
+
var rect = props.anchorRef.current.getBoundingClientRect();
|
|
49
|
+
style.bottom = window.innerHeight - rect.top + 4 + "px";
|
|
50
|
+
style.left = rect.left + "px";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function handleRestart() {
|
|
54
|
+
if (!confirmingRestart) {
|
|
55
|
+
setConfirmingRestart(true);
|
|
56
|
+
setConfirmingShutdown(false);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
ws.send({ type: "settings:restart" } as never);
|
|
60
|
+
props.onClose();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function handleShutdown() {
|
|
64
|
+
if (!confirmingShutdown) {
|
|
65
|
+
setConfirmingShutdown(true);
|
|
66
|
+
setConfirmingRestart(false);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
fetch("/api/shutdown", { method: "POST" });
|
|
70
|
+
props.onClose();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
var itemClass = "w-full flex items-center gap-2 px-2.5 py-[6px] rounded text-[11px] text-left cursor-pointer transition-colors duration-[120ms] text-base-content/70 hover:bg-base-content/5 hover:text-base-content outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-inset";
|
|
74
|
+
var dangerClass = "w-full flex items-center gap-2 px-2.5 py-[6px] rounded text-[11px] text-left cursor-pointer transition-colors duration-[120ms] text-error hover:bg-error/10 outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-inset";
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
ref={menuRef}
|
|
79
|
+
role="menu"
|
|
80
|
+
aria-label="Node menu"
|
|
81
|
+
className="fixed z-[9999] bg-base-300 border border-base-content/10 rounded-lg shadow-xl p-1 min-w-[180px]"
|
|
82
|
+
style={style}
|
|
83
|
+
>
|
|
84
|
+
<button role="menuitem" className={itemClass} onClick={function () { props.onOpenNodeSettings(); props.onClose(); }}>
|
|
85
|
+
<span className="opacity-60 flex-shrink-0"><Settings size={13} /></span>
|
|
86
|
+
Node Settings
|
|
87
|
+
</button>
|
|
88
|
+
|
|
89
|
+
<div className="h-px bg-base-content/8 my-1 mx-2" />
|
|
90
|
+
|
|
91
|
+
<button role="menuitem" className={dangerClass} onClick={handleRestart}>
|
|
92
|
+
<span className="opacity-60 flex-shrink-0"><RefreshCw size={13} /></span>
|
|
93
|
+
{confirmingRestart ? "Click again to restart" : "Restart"}
|
|
94
|
+
</button>
|
|
95
|
+
<button role="menuitem" className={dangerClass} onClick={handleShutdown}>
|
|
96
|
+
<span className="opacity-60 flex-shrink-0"><Power size={13} /></span>
|
|
97
|
+
{confirmingShutdown ? "Click again to shutdown" : "Shutdown"}
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useMemo, useCallback } from "react";
|
|
2
|
+
import { useHotkey } from "@tanstack/react-hotkeys";
|
|
3
|
+
import { Search, Moon, Sun, Settings, Layout, MessageSquare, FolderOpen, Zap, RotateCcw } from "lucide-react";
|
|
4
|
+
import { useProjects } from "../../hooks/useProjects";
|
|
5
|
+
import { useSkills } from "../../hooks/useSkills";
|
|
6
|
+
import { useTheme } from "../../hooks/useTheme";
|
|
7
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
8
|
+
import { useSidebar } from "../../hooks/useSidebar";
|
|
9
|
+
import { getSessionStore } from "../../stores/session";
|
|
10
|
+
import type { SettingsSection } from "../../stores/sidebar";
|
|
11
|
+
|
|
12
|
+
interface Command {
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
group: string;
|
|
17
|
+
icon?: React.ReactNode;
|
|
18
|
+
action: () => void;
|
|
19
|
+
keywords?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function CommandPalette() {
|
|
23
|
+
var [open, setOpen] = useState(false);
|
|
24
|
+
var [query, setQuery] = useState("");
|
|
25
|
+
var [selectedIndex, setSelectedIndex] = useState(0);
|
|
26
|
+
var inputRef = useRef<HTMLInputElement>(null);
|
|
27
|
+
var listRef = useRef<HTMLDivElement>(null);
|
|
28
|
+
|
|
29
|
+
var { projects, setActiveProject } = useProjects();
|
|
30
|
+
var skills = useSkills();
|
|
31
|
+
var { mode, toggleMode, themes, setTheme, currentThemeId } = useTheme();
|
|
32
|
+
var ws = useWebSocket();
|
|
33
|
+
var sidebar = useSidebar();
|
|
34
|
+
|
|
35
|
+
var close = useCallback(function () {
|
|
36
|
+
setOpen(false);
|
|
37
|
+
setQuery("");
|
|
38
|
+
setSelectedIndex(0);
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
useHotkey("Mod+K", function (e) {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
setOpen(function (prev) {
|
|
44
|
+
if (prev) {
|
|
45
|
+
setQuery("");
|
|
46
|
+
setSelectedIndex(0);
|
|
47
|
+
}
|
|
48
|
+
return !prev;
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
useHotkey("Escape", function (e) {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
close();
|
|
55
|
+
}, { enabled: open });
|
|
56
|
+
|
|
57
|
+
// Focus input when opened
|
|
58
|
+
useEffect(function () {
|
|
59
|
+
if (open && inputRef.current) {
|
|
60
|
+
inputRef.current.focus();
|
|
61
|
+
}
|
|
62
|
+
}, [open]);
|
|
63
|
+
|
|
64
|
+
// Build command list
|
|
65
|
+
var commands = useMemo(function (): Command[] {
|
|
66
|
+
var cmds: Command[] = [];
|
|
67
|
+
|
|
68
|
+
// Navigation
|
|
69
|
+
cmds.push({
|
|
70
|
+
id: "nav:dashboard",
|
|
71
|
+
label: "Go to Dashboard",
|
|
72
|
+
group: "Navigation",
|
|
73
|
+
icon: <Layout size={14} />,
|
|
74
|
+
action: function () { sidebar.goToDashboard(); close(); },
|
|
75
|
+
keywords: "home overview",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Projects
|
|
79
|
+
projects.forEach(function (project) {
|
|
80
|
+
cmds.push({
|
|
81
|
+
id: "project:" + project.slug,
|
|
82
|
+
label: project.title || project.slug,
|
|
83
|
+
description: project.path,
|
|
84
|
+
group: "Projects",
|
|
85
|
+
icon: <FolderOpen size={14} />,
|
|
86
|
+
action: function () { setActiveProject(project); close(); },
|
|
87
|
+
keywords: "switch project " + project.slug,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Session actions
|
|
92
|
+
var sessionState = getSessionStore().state;
|
|
93
|
+
if (sessionState.activeProjectSlug) {
|
|
94
|
+
cmds.push({
|
|
95
|
+
id: "session:new",
|
|
96
|
+
label: "New Session",
|
|
97
|
+
group: "Session",
|
|
98
|
+
icon: <MessageSquare size={14} />,
|
|
99
|
+
action: function () {
|
|
100
|
+
ws.send({ type: "session:create", projectSlug: sessionState.activeProjectSlug! });
|
|
101
|
+
close();
|
|
102
|
+
},
|
|
103
|
+
keywords: "create chat conversation",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Settings sections
|
|
108
|
+
var settingsSections: Array<{ id: SettingsSection; label: string; keywords: string }> = [
|
|
109
|
+
{ id: "appearance", label: "Appearance", keywords: "theme visual colors" },
|
|
110
|
+
{ id: "claude", label: "Claude Settings", keywords: "api model key" },
|
|
111
|
+
{ id: "environment", label: "Environment", keywords: "env variables config" },
|
|
112
|
+
{ id: "mcp", label: "MCP Servers", keywords: "model context protocol" },
|
|
113
|
+
{ id: "skills", label: "Skills", keywords: "capabilities commands" },
|
|
114
|
+
{ id: "nodes", label: "Mesh Nodes", keywords: "machines network" },
|
|
115
|
+
];
|
|
116
|
+
settingsSections.forEach(function (section) {
|
|
117
|
+
cmds.push({
|
|
118
|
+
id: "settings:" + section.id,
|
|
119
|
+
label: section.label,
|
|
120
|
+
group: "Settings",
|
|
121
|
+
icon: <Settings size={14} />,
|
|
122
|
+
action: function () { sidebar.openSettings(section.id); close(); },
|
|
123
|
+
keywords: section.keywords,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Theme
|
|
128
|
+
cmds.push({
|
|
129
|
+
id: "theme:toggle",
|
|
130
|
+
label: mode === "dark" ? "Switch to Light Mode" : "Switch to Dark Mode",
|
|
131
|
+
group: "Theme",
|
|
132
|
+
icon: mode === "dark" ? <Sun size={14} /> : <Moon size={14} />,
|
|
133
|
+
action: function () { toggleMode(); close(); },
|
|
134
|
+
keywords: "dark light mode toggle",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
var themeVariant = mode === "dark" ? "dark" : "light";
|
|
138
|
+
themes.filter(function (t) { return t.theme.variant === themeVariant; }).forEach(function (t) {
|
|
139
|
+
if (t.id === currentThemeId) return;
|
|
140
|
+
cmds.push({
|
|
141
|
+
id: "theme:" + t.id,
|
|
142
|
+
label: t.theme.name,
|
|
143
|
+
group: "Theme",
|
|
144
|
+
icon: mode === "dark" ? <Moon size={14} /> : <Sun size={14} />,
|
|
145
|
+
action: function () { setTheme(t.id); close(); },
|
|
146
|
+
keywords: "color scheme " + t.theme.name,
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Skills
|
|
151
|
+
skills.forEach(function (skill) {
|
|
152
|
+
cmds.push({
|
|
153
|
+
id: "skill:" + skill.name,
|
|
154
|
+
label: "/" + skill.name,
|
|
155
|
+
description: skill.description,
|
|
156
|
+
group: "Skills",
|
|
157
|
+
icon: <Zap size={14} />,
|
|
158
|
+
action: function () {
|
|
159
|
+
// Focus chat input and prefill with skill command
|
|
160
|
+
var textarea = document.querySelector("textarea[aria-label='Message input']") as HTMLTextAreaElement | null;
|
|
161
|
+
if (textarea) {
|
|
162
|
+
textarea.value = "/" + skill.name + " ";
|
|
163
|
+
textarea.focus();
|
|
164
|
+
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
|
165
|
+
}
|
|
166
|
+
close();
|
|
167
|
+
},
|
|
168
|
+
keywords: skill.description,
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// System
|
|
173
|
+
cmds.push({
|
|
174
|
+
id: "system:restart",
|
|
175
|
+
label: "Restart Daemon",
|
|
176
|
+
group: "System",
|
|
177
|
+
icon: <RotateCcw size={14} />,
|
|
178
|
+
action: function () { ws.send({ type: "settings:restart" } as any); close(); },
|
|
179
|
+
keywords: "reboot server",
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return cmds;
|
|
183
|
+
}, [projects, skills, mode, themes, currentThemeId, sidebar, ws, close, setActiveProject, toggleMode, setTheme]);
|
|
184
|
+
|
|
185
|
+
// Filter commands
|
|
186
|
+
var filtered = useMemo(function () {
|
|
187
|
+
if (!query.trim()) return commands;
|
|
188
|
+
var q = query.toLowerCase();
|
|
189
|
+
return commands.filter(function (cmd) {
|
|
190
|
+
var searchText = (cmd.label + " " + (cmd.description || "") + " " + (cmd.keywords || "") + " " + cmd.group).toLowerCase();
|
|
191
|
+
// All words in query must match
|
|
192
|
+
var words = q.split(/\s+/);
|
|
193
|
+
for (var i = 0; i < words.length; i++) {
|
|
194
|
+
if (!searchText.includes(words[i])) return false;
|
|
195
|
+
}
|
|
196
|
+
return true;
|
|
197
|
+
});
|
|
198
|
+
}, [query, commands]);
|
|
199
|
+
|
|
200
|
+
// Reset selection when filter changes
|
|
201
|
+
useEffect(function () {
|
|
202
|
+
setSelectedIndex(0);
|
|
203
|
+
}, [filtered.length, query]);
|
|
204
|
+
|
|
205
|
+
// Scroll active item into view
|
|
206
|
+
useEffect(function () {
|
|
207
|
+
if (!listRef.current) return;
|
|
208
|
+
var active = listRef.current.querySelector("[data-active='true']") as HTMLElement | null;
|
|
209
|
+
if (active) {
|
|
210
|
+
active.scrollIntoView({ block: "nearest" });
|
|
211
|
+
}
|
|
212
|
+
}, [selectedIndex]);
|
|
213
|
+
|
|
214
|
+
function handleKeyDown(e: React.KeyboardEvent) {
|
|
215
|
+
if (e.key === "ArrowDown") {
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
setSelectedIndex(function (i) { return i < filtered.length - 1 ? i + 1 : 0; });
|
|
218
|
+
} else if (e.key === "ArrowUp") {
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
setSelectedIndex(function (i) { return i > 0 ? i - 1 : filtered.length - 1; });
|
|
221
|
+
} else if (e.key === "Enter") {
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
if (filtered[selectedIndex]) {
|
|
224
|
+
filtered[selectedIndex].action();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Group filtered commands
|
|
230
|
+
var grouped = useMemo(function () {
|
|
231
|
+
var groups: Array<{ name: string; items: Array<Command & { globalIndex: number }> }> = [];
|
|
232
|
+
var groupMap = new Map<string, Array<Command & { globalIndex: number }>>();
|
|
233
|
+
var globalIdx = 0;
|
|
234
|
+
filtered.forEach(function (cmd) {
|
|
235
|
+
var list = groupMap.get(cmd.group);
|
|
236
|
+
if (!list) {
|
|
237
|
+
list = [];
|
|
238
|
+
groupMap.set(cmd.group, list);
|
|
239
|
+
groups.push({ name: cmd.group, items: list });
|
|
240
|
+
}
|
|
241
|
+
list.push(Object.assign({}, cmd, { globalIndex: globalIdx }));
|
|
242
|
+
globalIdx++;
|
|
243
|
+
});
|
|
244
|
+
return groups;
|
|
245
|
+
}, [filtered]);
|
|
246
|
+
|
|
247
|
+
if (!open) return null;
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<div className="fixed inset-0 z-[9998] flex items-start justify-center pt-[15vh]" onClick={close}>
|
|
251
|
+
<div className="absolute inset-0 bg-black/50" />
|
|
252
|
+
<div
|
|
253
|
+
className="relative w-full max-w-[520px] mx-4 bg-base-200 border border-base-content/10 rounded-xl shadow-2xl overflow-hidden"
|
|
254
|
+
onClick={function (e) { e.stopPropagation(); }}
|
|
255
|
+
>
|
|
256
|
+
<div className="flex items-center gap-2 px-4 py-3 border-b border-base-content/10">
|
|
257
|
+
<Search size={16} className="text-base-content/30 flex-shrink-0" />
|
|
258
|
+
<input
|
|
259
|
+
ref={inputRef}
|
|
260
|
+
type="text"
|
|
261
|
+
value={query}
|
|
262
|
+
onChange={function (e) { setQuery(e.target.value); }}
|
|
263
|
+
onKeyDown={handleKeyDown}
|
|
264
|
+
placeholder="Type a command..."
|
|
265
|
+
className="flex-1 bg-transparent text-[14px] text-base-content outline-none placeholder:text-base-content/30"
|
|
266
|
+
/>
|
|
267
|
+
<kbd className="text-[10px] font-mono text-base-content/20 bg-base-300 px-1.5 py-0.5 rounded">ESC</kbd>
|
|
268
|
+
</div>
|
|
269
|
+
<div ref={listRef} className="max-h-[360px] overflow-y-auto py-1.5">
|
|
270
|
+
{grouped.length === 0 && (
|
|
271
|
+
<div className="px-4 py-8 text-center text-[13px] text-base-content/30">
|
|
272
|
+
No commands found
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
275
|
+
{grouped.map(function (group) {
|
|
276
|
+
return (
|
|
277
|
+
<div key={group.name}>
|
|
278
|
+
<div className="px-4 pt-2 pb-1">
|
|
279
|
+
<span className="text-[9px] uppercase tracking-widest text-base-content/30 font-mono font-bold">{group.name}</span>
|
|
280
|
+
</div>
|
|
281
|
+
{group.items.map(function (cmd) {
|
|
282
|
+
var isActive = cmd.globalIndex === selectedIndex;
|
|
283
|
+
return (
|
|
284
|
+
<button
|
|
285
|
+
key={cmd.id}
|
|
286
|
+
data-active={isActive}
|
|
287
|
+
onMouseEnter={function () { setSelectedIndex(cmd.globalIndex); }}
|
|
288
|
+
onMouseDown={function (e) { e.preventDefault(); }}
|
|
289
|
+
onClick={function () { cmd.action(); }}
|
|
290
|
+
className={
|
|
291
|
+
"flex w-full items-center gap-3 px-4 py-2 text-left transition-colors " +
|
|
292
|
+
(isActive ? "bg-primary/10" : "hover:bg-base-content/5")
|
|
293
|
+
}
|
|
294
|
+
>
|
|
295
|
+
<div className={"flex-shrink-0 " + (isActive ? "text-primary" : "text-base-content/30")}>
|
|
296
|
+
{cmd.icon}
|
|
297
|
+
</div>
|
|
298
|
+
<div className="flex-1 min-w-0">
|
|
299
|
+
<div className={"text-[13px] truncate " + (isActive ? "text-base-content" : "text-base-content/70")}>
|
|
300
|
+
{cmd.label}
|
|
301
|
+
</div>
|
|
302
|
+
{cmd.description && (
|
|
303
|
+
<div className="text-[11px] text-base-content/30 truncate">{cmd.description}</div>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
</button>
|
|
307
|
+
);
|
|
308
|
+
})}
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
})}
|
|
312
|
+
</div>
|
|
313
|
+
<div className="flex items-center gap-3 px-4 py-2 border-t border-base-content/10 text-[10px] text-base-content/25 font-mono">
|
|
314
|
+
<span>↑↓ navigate</span>
|
|
315
|
+
<span>↵ select</span>
|
|
316
|
+
<span>esc close</span>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|