@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,168 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { useSidebar } from "../../hooks/useSidebar";
|
|
3
|
+
import { useProjectSettings } from "../../hooks/useProjectSettings";
|
|
4
|
+
import { useProjects } from "../../hooks/useProjects";
|
|
5
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
6
|
+
import {
|
|
7
|
+
Menu, Settings, FileText, Terminal, Plug, ScrollText, Shield,
|
|
8
|
+
MessageSquare, ChevronRight,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
import type { ProjectSettingsSection } from "../../stores/sidebar";
|
|
11
|
+
import type { SessionSummary, ServerMessage } from "@lattice/shared";
|
|
12
|
+
|
|
13
|
+
function StatCard({ label, value, icon }: { label: string; value: string | number; icon: React.ReactNode }) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="bg-base-300 rounded-xl border border-base-content/15 p-3 px-4">
|
|
16
|
+
<div className="flex items-center gap-2 mb-1.5">
|
|
17
|
+
{icon}
|
|
18
|
+
<span className="text-[11px] font-semibold tracking-wider uppercase text-base-content/40">{label}</span>
|
|
19
|
+
</div>
|
|
20
|
+
<div className="text-xl font-mono font-bold text-base-content">{value}</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function QuickLink({ label, icon, onClick }: { label: string; icon: React.ReactNode; onClick: () => void }) {
|
|
26
|
+
return (
|
|
27
|
+
<button
|
|
28
|
+
onClick={onClick}
|
|
29
|
+
className="flex items-center gap-2.5 px-3 py-2 rounded-xl border border-base-content/15 bg-base-300 text-[12px] text-base-content/60 hover:text-base-content hover:border-base-content/30 transition-colors duration-[120ms] cursor-pointer focus-visible:ring-2 focus-visible:ring-primary"
|
|
30
|
+
>
|
|
31
|
+
{icon}
|
|
32
|
+
<span className="flex-1 text-left">{label}</span>
|
|
33
|
+
<ChevronRight size={12} className="text-base-content/30" />
|
|
34
|
+
</button>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ProjectDashboardView() {
|
|
39
|
+
var sidebar = useSidebar();
|
|
40
|
+
|
|
41
|
+
if (sidebar.activeView.type !== "project-dashboard") {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var { settings, loading } = useProjectSettings(sidebar.activeProjectSlug);
|
|
46
|
+
var { projects } = useProjects();
|
|
47
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
48
|
+
var [sessions, setSessions] = useState<SessionSummary[]>([]);
|
|
49
|
+
|
|
50
|
+
var activeProject = projects.find(function (p) { return p.slug === sidebar.activeProjectSlug; });
|
|
51
|
+
var projectTitle = activeProject?.title ?? sidebar.activeProjectSlug ?? "Project";
|
|
52
|
+
|
|
53
|
+
useEffect(function () {
|
|
54
|
+
if (!sidebar.activeProjectSlug) return;
|
|
55
|
+
|
|
56
|
+
function handleSessionList(msg: ServerMessage) {
|
|
57
|
+
if (msg.type !== "session:list") return;
|
|
58
|
+
var data = msg as { type: "session:list"; projectSlug: string; sessions: SessionSummary[] };
|
|
59
|
+
if (data.projectSlug === sidebar.activeProjectSlug) {
|
|
60
|
+
var sorted = data.sessions.slice().sort(function (a, b) { return b.updatedAt - a.updatedAt; });
|
|
61
|
+
setSessions(sorted);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
subscribe("session:list", handleSessionList);
|
|
66
|
+
send({ type: "session:list_request", projectSlug: sidebar.activeProjectSlug });
|
|
67
|
+
|
|
68
|
+
return function () {
|
|
69
|
+
unsubscribe("session:list", handleSessionList);
|
|
70
|
+
};
|
|
71
|
+
}, [sidebar.activeProjectSlug]);
|
|
72
|
+
|
|
73
|
+
function goToSection(section: ProjectSettingsSection) {
|
|
74
|
+
sidebar.openProjectSettings(section);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function goToSession(sessionId: string) {
|
|
78
|
+
if (activeProject) {
|
|
79
|
+
sidebar.setActiveSessionId(sessionId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
var sessionCount = sessions.length;
|
|
84
|
+
var mcpCount = settings ? Object.keys(settings.mcpServers).length : 0;
|
|
85
|
+
var globalMcpCount = settings ? Object.keys(settings.global.mcpServers).length : 0;
|
|
86
|
+
var rulesCount = settings ? settings.rules.length : 0;
|
|
87
|
+
var globalRulesCount = settings ? settings.global.rules.length : 0;
|
|
88
|
+
var envCount = settings ? Object.keys(settings.env).length : 0;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="flex-1 overflow-auto px-4 sm:px-8 py-4 sm:py-6 max-w-3xl">
|
|
92
|
+
<div className="mb-6 flex items-center gap-3">
|
|
93
|
+
<button
|
|
94
|
+
className="btn btn-ghost btn-sm btn-square lg:hidden"
|
|
95
|
+
aria-label="Toggle sidebar"
|
|
96
|
+
onClick={sidebar.toggleDrawer}
|
|
97
|
+
>
|
|
98
|
+
<Menu size={18} />
|
|
99
|
+
</button>
|
|
100
|
+
<h1 className="text-lg font-mono font-bold text-base-content">{projectTitle}</h1>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{loading && (
|
|
104
|
+
<div className="text-[13px] text-base-content/40 py-4">Loading...</div>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
{!loading && settings && (
|
|
108
|
+
<div className="space-y-6">
|
|
109
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
110
|
+
<StatCard label="Sessions" value={sessionCount} icon={<MessageSquare size={14} className="text-primary" />} />
|
|
111
|
+
<StatCard label="MCP Servers" value={mcpCount + globalMcpCount} icon={<Plug size={14} className="text-accent" />} />
|
|
112
|
+
<StatCard label="Rules" value={rulesCount + globalRulesCount} icon={<ScrollText size={14} className="text-info" />} />
|
|
113
|
+
<StatCard label="Env Vars" value={envCount} icon={<Terminal size={14} className="text-success" />} />
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{settings.claudeMd && (
|
|
117
|
+
<div>
|
|
118
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2">CLAUDE.md</div>
|
|
119
|
+
<div className="px-3 py-2.5 bg-base-300 border border-base-content/15 rounded-xl font-mono text-[11px] text-base-content/60 leading-relaxed max-h-48 overflow-y-auto">
|
|
120
|
+
<pre className="whitespace-pre-wrap">{settings.claudeMd}</pre>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{sessionCount > 0 && (
|
|
126
|
+
<div>
|
|
127
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2">Recent Sessions</div>
|
|
128
|
+
<div className="flex flex-col gap-1.5">
|
|
129
|
+
{sessions.slice(0, 5).map(function (s) {
|
|
130
|
+
return (
|
|
131
|
+
<button
|
|
132
|
+
key={s.id}
|
|
133
|
+
onClick={function () { goToSession(s.id); }}
|
|
134
|
+
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
|
+
>
|
|
136
|
+
<MessageSquare size={12} className="text-base-content/30 flex-shrink-0" />
|
|
137
|
+
<span className="flex-1 text-[12px] text-base-content truncate">{s.title || "Untitled"}</span>
|
|
138
|
+
<span className="text-[10px] text-base-content/30 font-mono flex-shrink-0">
|
|
139
|
+
{new Date(s.updatedAt).toLocaleDateString()}
|
|
140
|
+
</span>
|
|
141
|
+
<ChevronRight size={12} className="text-base-content/20 flex-shrink-0" />
|
|
142
|
+
</button>
|
|
143
|
+
);
|
|
144
|
+
})}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
<div>
|
|
150
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2">Settings</div>
|
|
151
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
|
152
|
+
<QuickLink label="General" icon={<Settings size={13} className="text-base-content/40" />} onClick={function () { goToSection("general"); }} />
|
|
153
|
+
<QuickLink label="Claude" icon={<FileText size={13} className="text-base-content/40" />} onClick={function () { goToSection("claude"); }} />
|
|
154
|
+
<QuickLink label="Environment" icon={<Terminal size={13} className="text-base-content/40" />} onClick={function () { goToSection("environment"); }} />
|
|
155
|
+
<QuickLink label="MCP Servers" icon={<Plug size={13} className="text-base-content/40" />} onClick={function () { goToSection("mcp"); }} />
|
|
156
|
+
<QuickLink label="Rules" icon={<ScrollText size={13} className="text-base-content/40" />} onClick={function () { goToSection("rules"); }} />
|
|
157
|
+
<QuickLink label="Permissions" icon={<Shield size={13} className="text-base-content/40" />} onClick={function () { goToSection("permissions"); }} />
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div className="text-[11px] text-base-content/30 font-mono truncate">
|
|
162
|
+
{settings.path}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { NodeInfo } from "@lattice/shared";
|
|
2
|
+
|
|
3
|
+
interface NodeBadgeProps {
|
|
4
|
+
node: NodeInfo;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function NodeBadge(props: NodeBadgeProps) {
|
|
8
|
+
var initials = props.node.name.slice(0, 2).toUpperCase();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<span
|
|
12
|
+
title={props.node.name + (props.node.online ? " (online)" : " (offline)")}
|
|
13
|
+
className="inline-flex items-center gap-[3px] px-[5px] py-[1px] rounded-full bg-base-300 border border-base-content/15 text-[10px] font-semibold text-base-content/40 tracking-[0.03em] flex-shrink-0"
|
|
14
|
+
>
|
|
15
|
+
<span
|
|
16
|
+
className={
|
|
17
|
+
"w-[5px] h-[5px] rounded-full flex-shrink-0 inline-block " +
|
|
18
|
+
(props.node.online ? "bg-success" : "bg-base-content/30")
|
|
19
|
+
}
|
|
20
|
+
/>
|
|
21
|
+
{initials}
|
|
22
|
+
</span>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { X, Copy, Check } from "lucide-react";
|
|
3
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
4
|
+
import { useMesh } from "../../hooks/useMesh";
|
|
5
|
+
import { clearInvite } from "../../stores/mesh";
|
|
6
|
+
import type { ServerMessage } from "@lattice/shared";
|
|
7
|
+
|
|
8
|
+
type Tab = "generate" | "enter";
|
|
9
|
+
type PairStatus = "idle" | "connecting" | "paired" | "failed";
|
|
10
|
+
|
|
11
|
+
interface PairingDialogProps {
|
|
12
|
+
isOpen: boolean;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function PairingDialog(props: PairingDialogProps) {
|
|
17
|
+
var ws = useWebSocket();
|
|
18
|
+
var mesh = useMesh();
|
|
19
|
+
var [tab, setTab] = useState<Tab>("generate");
|
|
20
|
+
var [pairCode, setPairCode] = useState("");
|
|
21
|
+
var [pairStatus, setPairStatus] = useState<PairStatus>("idle");
|
|
22
|
+
var [pairError, setPairError] = useState<string | null>(null);
|
|
23
|
+
var [copied, setCopied] = useState(false);
|
|
24
|
+
|
|
25
|
+
var handleKeyDown = useCallback(function (e: KeyboardEvent) {
|
|
26
|
+
if (e.key === "Escape") {
|
|
27
|
+
props.onClose();
|
|
28
|
+
}
|
|
29
|
+
}, [props.onClose]);
|
|
30
|
+
|
|
31
|
+
useEffect(function () {
|
|
32
|
+
if (!props.isOpen) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
36
|
+
return function () {
|
|
37
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
38
|
+
};
|
|
39
|
+
}, [props.isOpen, handleKeyDown]);
|
|
40
|
+
|
|
41
|
+
useEffect(function () {
|
|
42
|
+
if (!props.isOpen) {
|
|
43
|
+
clearInvite();
|
|
44
|
+
setPairCode("");
|
|
45
|
+
setPairStatus("idle");
|
|
46
|
+
setPairError(null);
|
|
47
|
+
setCopied(false);
|
|
48
|
+
setTab("generate");
|
|
49
|
+
}
|
|
50
|
+
}, [props.isOpen]);
|
|
51
|
+
|
|
52
|
+
useEffect(function () {
|
|
53
|
+
if (pairStatus !== "connecting") {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function handler(msg: ServerMessage) {
|
|
58
|
+
if (msg.type === "mesh:paired") {
|
|
59
|
+
setPairStatus("paired");
|
|
60
|
+
setPairError(null);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ws.subscribe("mesh:paired", handler);
|
|
65
|
+
return function () {
|
|
66
|
+
ws.unsubscribe("mesh:paired", handler);
|
|
67
|
+
};
|
|
68
|
+
}, [ws, pairStatus]);
|
|
69
|
+
|
|
70
|
+
function handleGenerateInvite() {
|
|
71
|
+
clearInvite();
|
|
72
|
+
mesh.generateInvite();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function handlePair() {
|
|
76
|
+
var trimmed = pairCode.trim();
|
|
77
|
+
if (!trimmed) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
setPairStatus("connecting");
|
|
81
|
+
setPairError(null);
|
|
82
|
+
|
|
83
|
+
var timeout = setTimeout(function () {
|
|
84
|
+
setPairStatus(function (prev) {
|
|
85
|
+
if (prev === "connecting") {
|
|
86
|
+
setPairError("Pairing timed out. Check the code and try again.");
|
|
87
|
+
return "failed";
|
|
88
|
+
}
|
|
89
|
+
return prev;
|
|
90
|
+
});
|
|
91
|
+
}, 30000);
|
|
92
|
+
|
|
93
|
+
ws.send({ type: "mesh:pair", code: trimmed });
|
|
94
|
+
|
|
95
|
+
return function () {
|
|
96
|
+
clearTimeout(timeout);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function handleCopyCode() {
|
|
101
|
+
if (!mesh.inviteCode) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
navigator.clipboard.writeText(mesh.inviteCode).then(function () {
|
|
105
|
+
setCopied(true);
|
|
106
|
+
setTimeout(function () { setCopied(false); }, 2000);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!props.isOpen) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
role="dialog"
|
|
117
|
+
aria-modal="true"
|
|
118
|
+
aria-label="Pair a node"
|
|
119
|
+
className="fixed inset-0 z-[10000] flex items-center justify-center bg-black/65 backdrop-blur-sm"
|
|
120
|
+
onClick={props.onClose}
|
|
121
|
+
>
|
|
122
|
+
<div
|
|
123
|
+
className="w-[440px] max-w-[calc(100vw-24px)] rounded-xl border border-base-300 bg-base-200 overflow-hidden shadow-2xl"
|
|
124
|
+
onClick={function (e) { e.stopPropagation(); }}
|
|
125
|
+
>
|
|
126
|
+
<div className="flex items-center justify-between px-5 py-4 border-b border-base-300">
|
|
127
|
+
<div className="text-[14px] font-semibold text-base-content">Pair a Node</div>
|
|
128
|
+
<button
|
|
129
|
+
onClick={props.onClose}
|
|
130
|
+
aria-label="Close"
|
|
131
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-base-content"
|
|
132
|
+
>
|
|
133
|
+
<X size={14} />
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div className="flex border-b border-base-300">
|
|
138
|
+
{(["generate", "enter"] as Tab[]).map(function (t) {
|
|
139
|
+
var label = t === "generate" ? "Generate Invite" : "Enter Code";
|
|
140
|
+
var isActive = tab === t;
|
|
141
|
+
return (
|
|
142
|
+
<button
|
|
143
|
+
key={t}
|
|
144
|
+
onClick={function () { setTab(t); }}
|
|
145
|
+
className={
|
|
146
|
+
"flex-1 px-4 py-2.5 text-[13px] cursor-pointer transition-colors duration-[120ms] border-b-2 " +
|
|
147
|
+
(isActive
|
|
148
|
+
? "font-semibold text-base-content border-primary"
|
|
149
|
+
: "font-normal text-base-content/40 border-transparent hover:text-base-content/70")
|
|
150
|
+
}
|
|
151
|
+
>
|
|
152
|
+
{label}
|
|
153
|
+
</button>
|
|
154
|
+
);
|
|
155
|
+
})}
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div className="p-5">
|
|
159
|
+
{tab === "generate" && (
|
|
160
|
+
<div>
|
|
161
|
+
<div className="text-[12px] text-base-content/40 mb-4 leading-relaxed">
|
|
162
|
+
Generate an invite code on this machine and share it with the other node.
|
|
163
|
+
The code encodes this node's address and a one-time auth token.
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{!mesh.inviteCode && (
|
|
167
|
+
<button
|
|
168
|
+
onClick={handleGenerateInvite}
|
|
169
|
+
className="btn btn-primary btn-sm"
|
|
170
|
+
>
|
|
171
|
+
Generate Invite Code
|
|
172
|
+
</button>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{mesh.inviteCode && (
|
|
176
|
+
<div>
|
|
177
|
+
<div className="flex items-center gap-2 px-3.5 py-2.5 rounded bg-base-100 border border-base-300 mb-4">
|
|
178
|
+
<code className="flex-1 font-mono text-[14px] font-semibold text-base-content tracking-[0.08em] break-all">
|
|
179
|
+
{mesh.inviteCode}
|
|
180
|
+
</code>
|
|
181
|
+
<button
|
|
182
|
+
onClick={handleCopyCode}
|
|
183
|
+
title="Copy code"
|
|
184
|
+
className={
|
|
185
|
+
"btn btn-xs gap-1 flex-shrink-0 " +
|
|
186
|
+
(copied ? "btn-success" : "btn-ghost border border-base-300")
|
|
187
|
+
}
|
|
188
|
+
>
|
|
189
|
+
{copied ? <Check size={12} /> : <Copy size={12} />}
|
|
190
|
+
{copied ? "Copied!" : "Copy"}
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{mesh.inviteQr && (
|
|
195
|
+
<div className="flex justify-center mb-4">
|
|
196
|
+
<img
|
|
197
|
+
src={mesh.inviteQr}
|
|
198
|
+
alt="QR code for invite"
|
|
199
|
+
className="w-40 h-40 rounded border border-base-300"
|
|
200
|
+
style={{ imageRendering: "pixelated" }}
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
<button
|
|
206
|
+
onClick={handleGenerateInvite}
|
|
207
|
+
className="text-[12px] text-base-content/40 underline cursor-pointer"
|
|
208
|
+
>
|
|
209
|
+
Generate new code
|
|
210
|
+
</button>
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
{tab === "enter" && (
|
|
217
|
+
<div>
|
|
218
|
+
<div className="text-[12px] text-base-content/40 mb-4 leading-relaxed">
|
|
219
|
+
Paste the invite code generated on the other node to pair with it.
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<input
|
|
223
|
+
type="text"
|
|
224
|
+
value={pairCode}
|
|
225
|
+
onChange={function (e) {
|
|
226
|
+
setPairCode(e.target.value);
|
|
227
|
+
if (pairStatus !== "idle") {
|
|
228
|
+
setPairStatus("idle");
|
|
229
|
+
setPairError(null);
|
|
230
|
+
}
|
|
231
|
+
}}
|
|
232
|
+
onKeyDown={function (e) {
|
|
233
|
+
if (e.key === "Enter") {
|
|
234
|
+
handlePair();
|
|
235
|
+
}
|
|
236
|
+
}}
|
|
237
|
+
placeholder="LTCE-XXXX-XXXX"
|
|
238
|
+
disabled={pairStatus === "connecting" || pairStatus === "paired"}
|
|
239
|
+
className="input input-bordered w-full bg-base-100 text-base-content font-mono text-[14px] tracking-[0.06em] mb-3 focus:border-primary"
|
|
240
|
+
/>
|
|
241
|
+
|
|
242
|
+
{pairStatus === "idle" && (
|
|
243
|
+
<button
|
|
244
|
+
onClick={handlePair}
|
|
245
|
+
disabled={!pairCode.trim()}
|
|
246
|
+
className={
|
|
247
|
+
"btn btn-sm " +
|
|
248
|
+
(pairCode.trim() ? "btn-primary" : "btn-ghost border border-base-300 cursor-not-allowed")
|
|
249
|
+
}
|
|
250
|
+
>
|
|
251
|
+
Pair
|
|
252
|
+
</button>
|
|
253
|
+
)}
|
|
254
|
+
|
|
255
|
+
{pairStatus === "connecting" && (
|
|
256
|
+
<div className="flex items-center gap-2 text-[13px] text-base-content/40">
|
|
257
|
+
<span
|
|
258
|
+
className="w-3 h-3 rounded-full border-2 border-primary border-t-transparent inline-block"
|
|
259
|
+
style={{ animation: "spin 0.6s linear infinite" }}
|
|
260
|
+
/>
|
|
261
|
+
Connecting...
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{pairStatus === "paired" && (
|
|
266
|
+
<div className="flex items-center gap-1.5 text-[13px] font-semibold text-success">
|
|
267
|
+
<Check size={14} />
|
|
268
|
+
Paired successfully!
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
{pairStatus === "failed" && pairError && (
|
|
273
|
+
<div className="text-[12px] text-error mt-2">{pairError}</div>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|