@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.
Files changed (162) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/workflows/release.yml +44 -0
  3. package/.impeccable.md +66 -0
  4. package/.releaserc.json +32 -0
  5. package/.serena/project.yml +138 -0
  6. package/CLAUDE.md +35 -0
  7. package/CONTRIBUTING.md +93 -0
  8. package/LICENSE +21 -0
  9. package/README.md +83 -0
  10. package/bun.lock +1459 -0
  11. package/bunfig.toml +2 -0
  12. package/client/index.html +32 -0
  13. package/client/package.json +37 -0
  14. package/client/public/icons/icon-192.svg +11 -0
  15. package/client/public/icons/icon-512.svg +11 -0
  16. package/client/public/manifest.json +24 -0
  17. package/client/public/sw.js +61 -0
  18. package/client/src/App.tsx +28 -0
  19. package/client/src/components/auth/PassphrasePrompt.tsx +70 -0
  20. package/client/src/components/chat/ChatInput.tsx +241 -0
  21. package/client/src/components/chat/ChatView.tsx +727 -0
  22. package/client/src/components/chat/Message.tsx +362 -0
  23. package/client/src/components/chat/ModelSelector.tsx +87 -0
  24. package/client/src/components/chat/PermissionModeSelector.tsx +41 -0
  25. package/client/src/components/chat/StatusBar.tsx +50 -0
  26. package/client/src/components/chat/ToolGroup.tsx +129 -0
  27. package/client/src/components/chat/ToolResultRenderer.tsx +343 -0
  28. package/client/src/components/chat/toolSummary.ts +41 -0
  29. package/client/src/components/dashboard/DashboardView.tsx +219 -0
  30. package/client/src/components/dashboard/ProjectDashboardView.tsx +168 -0
  31. package/client/src/components/mesh/NodeBadge.tsx +24 -0
  32. package/client/src/components/mesh/PairingDialog.tsx +281 -0
  33. package/client/src/components/panels/FileBrowser.tsx +241 -0
  34. package/client/src/components/panels/StickyNotes.tsx +187 -0
  35. package/client/src/components/panels/Terminal.tsx +128 -0
  36. package/client/src/components/project-settings/ProjectClaude.tsx +304 -0
  37. package/client/src/components/project-settings/ProjectEnvironment.tsx +235 -0
  38. package/client/src/components/project-settings/ProjectGeneral.tsx +76 -0
  39. package/client/src/components/project-settings/ProjectMcp.tsx +232 -0
  40. package/client/src/components/project-settings/ProjectPermissions.tsx +209 -0
  41. package/client/src/components/project-settings/ProjectRules.tsx +277 -0
  42. package/client/src/components/project-settings/ProjectSettingsView.tsx +99 -0
  43. package/client/src/components/project-settings/ProjectSkills.tsx +91 -0
  44. package/client/src/components/settings/Appearance.tsx +151 -0
  45. package/client/src/components/settings/ClaudeSettings.tsx +151 -0
  46. package/client/src/components/settings/Environment.tsx +185 -0
  47. package/client/src/components/settings/GlobalMcp.tsx +207 -0
  48. package/client/src/components/settings/GlobalSkills.tsx +125 -0
  49. package/client/src/components/settings/MeshStatus.tsx +145 -0
  50. package/client/src/components/settings/SettingsView.tsx +57 -0
  51. package/client/src/components/settings/SkillMarketplace.tsx +175 -0
  52. package/client/src/components/settings/mcp-shared.tsx +194 -0
  53. package/client/src/components/settings/skill-shared.tsx +177 -0
  54. package/client/src/components/setup/SetupWizard.tsx +750 -0
  55. package/client/src/components/sidebar/NodeSettingsModal.tsx +180 -0
  56. package/client/src/components/sidebar/ProjectDropdown.tsx +43 -0
  57. package/client/src/components/sidebar/ProjectRail.tsx +291 -0
  58. package/client/src/components/sidebar/SearchFilter.tsx +52 -0
  59. package/client/src/components/sidebar/SessionList.tsx +384 -0
  60. package/client/src/components/sidebar/SettingsSidebar.tsx +128 -0
  61. package/client/src/components/sidebar/Sidebar.tsx +209 -0
  62. package/client/src/components/sidebar/UserIsland.tsx +59 -0
  63. package/client/src/components/sidebar/UserMenu.tsx +101 -0
  64. package/client/src/components/ui/CommandPalette.tsx +321 -0
  65. package/client/src/components/ui/ErrorBoundary.tsx +56 -0
  66. package/client/src/components/ui/IconPicker.tsx +209 -0
  67. package/client/src/components/ui/LatticeLogomark.tsx +19 -0
  68. package/client/src/components/ui/PopupMenu.tsx +98 -0
  69. package/client/src/components/ui/SaveFooter.tsx +38 -0
  70. package/client/src/components/ui/Toast.tsx +112 -0
  71. package/client/src/hooks/useMesh.ts +89 -0
  72. package/client/src/hooks/useProjectSettings.ts +56 -0
  73. package/client/src/hooks/useProjects.ts +66 -0
  74. package/client/src/hooks/useSaveState.ts +59 -0
  75. package/client/src/hooks/useSession.ts +317 -0
  76. package/client/src/hooks/useSidebar.ts +74 -0
  77. package/client/src/hooks/useSkills.ts +30 -0
  78. package/client/src/hooks/useTheme.ts +114 -0
  79. package/client/src/hooks/useWebSocket.ts +26 -0
  80. package/client/src/main.tsx +10 -0
  81. package/client/src/providers/WebSocketProvider.tsx +146 -0
  82. package/client/src/router.tsx +391 -0
  83. package/client/src/stores/mesh.ts +78 -0
  84. package/client/src/stores/session.ts +322 -0
  85. package/client/src/stores/sidebar.ts +336 -0
  86. package/client/src/stores/theme.ts +44 -0
  87. package/client/src/styles/global.css +167 -0
  88. package/client/src/styles/theme-vars.css +18 -0
  89. package/client/src/themes/index.ts +79 -0
  90. package/client/src/utils/findDuplicateKeys.ts +12 -0
  91. package/client/tsconfig.json +14 -0
  92. package/client/vite.config.ts +20 -0
  93. package/package.json +46 -0
  94. package/server/package.json +22 -0
  95. package/server/src/auth/passphrase.ts +48 -0
  96. package/server/src/config.ts +55 -0
  97. package/server/src/daemon.ts +338 -0
  98. package/server/src/features/ralph-loop.ts +173 -0
  99. package/server/src/features/scheduler.ts +281 -0
  100. package/server/src/features/sticky-notes.ts +102 -0
  101. package/server/src/handlers/chat.ts +194 -0
  102. package/server/src/handlers/fs.ts +84 -0
  103. package/server/src/handlers/loop.ts +37 -0
  104. package/server/src/handlers/mesh.ts +125 -0
  105. package/server/src/handlers/notes.ts +45 -0
  106. package/server/src/handlers/project-settings.ts +174 -0
  107. package/server/src/handlers/scheduler.ts +47 -0
  108. package/server/src/handlers/session.ts +159 -0
  109. package/server/src/handlers/settings.ts +109 -0
  110. package/server/src/handlers/skills.ts +380 -0
  111. package/server/src/handlers/terminal.ts +70 -0
  112. package/server/src/identity.ts +26 -0
  113. package/server/src/index.ts +190 -0
  114. package/server/src/mesh/connector.ts +209 -0
  115. package/server/src/mesh/discovery.ts +123 -0
  116. package/server/src/mesh/pairing.ts +94 -0
  117. package/server/src/mesh/peers.ts +52 -0
  118. package/server/src/mesh/proxy.ts +103 -0
  119. package/server/src/mesh/session-sync.ts +107 -0
  120. package/server/src/project/context-breakdown.ts +289 -0
  121. package/server/src/project/file-browser.ts +106 -0
  122. package/server/src/project/project-files.ts +267 -0
  123. package/server/src/project/registry.ts +57 -0
  124. package/server/src/project/sdk-bridge.ts +566 -0
  125. package/server/src/project/session.ts +432 -0
  126. package/server/src/project/terminal.ts +69 -0
  127. package/server/src/tls.ts +51 -0
  128. package/server/src/ws/broadcast.ts +31 -0
  129. package/server/src/ws/router.ts +104 -0
  130. package/server/src/ws/server.ts +2 -0
  131. package/server/tsconfig.json +16 -0
  132. package/shared/package.json +11 -0
  133. package/shared/src/constants.ts +7 -0
  134. package/shared/src/index.ts +4 -0
  135. package/shared/src/messages.ts +638 -0
  136. package/shared/src/models.ts +136 -0
  137. package/shared/src/project-settings.ts +45 -0
  138. package/shared/tsconfig.json +11 -0
  139. package/themes/amoled.json +20 -0
  140. package/themes/ayu-light.json +9 -0
  141. package/themes/catppuccin-latte.json +9 -0
  142. package/themes/catppuccin-mocha.json +9 -0
  143. package/themes/clay-light.json +10 -0
  144. package/themes/clay.json +10 -0
  145. package/themes/dracula.json +9 -0
  146. package/themes/everforest-light.json +9 -0
  147. package/themes/everforest.json +9 -0
  148. package/themes/github-light.json +9 -0
  149. package/themes/gruvbox-dark.json +9 -0
  150. package/themes/gruvbox-light.json +9 -0
  151. package/themes/monokai.json +9 -0
  152. package/themes/nord-light.json +9 -0
  153. package/themes/nord.json +9 -0
  154. package/themes/one-dark.json +9 -0
  155. package/themes/one-light.json +9 -0
  156. package/themes/rose-pine-dawn.json +9 -0
  157. package/themes/rose-pine.json +9 -0
  158. package/themes/solarized-dark.json +9 -0
  159. package/themes/solarized-light.json +9 -0
  160. package/themes/tokyo-night-light.json +9 -0
  161. package/themes/tokyo-night.json +9 -0
  162. 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
+ }