@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
package/bunfig.toml ADDED
@@ -0,0 +1,2 @@
1
+ [install]
2
+ exact = false
@@ -0,0 +1,32 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="theme-color" content="#0d0d0d" />
7
+ <meta name="apple-mobile-web-app-capable" content="yes" />
8
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
9
+ <meta name="apple-mobile-web-app-title" content="Lattice" />
10
+ <link rel="manifest" href="/manifest.json" />
11
+ <link rel="apple-touch-icon" href="/icons/icon-192.svg" />
12
+ <title>Lattice</title>
13
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
14
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
15
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet" />
16
+ </head>
17
+ <body>
18
+ <div id="root"></div>
19
+ <script type="module" src="/src/main.tsx"></script>
20
+ <script>
21
+ if ("serviceWorker" in navigator) {
22
+ window.addEventListener("load", function () {
23
+ navigator.serviceWorker.register("/sw.js").then(function (reg) {
24
+ console.log("[lattice] Service worker registered:", reg.scope);
25
+ }).catch(function (err) {
26
+ console.warn("[lattice] Service worker registration failed:", err);
27
+ });
28
+ });
29
+ }
30
+ </script>
31
+ </body>
32
+ </html>
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@lattice/client",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@lattice/shared": "workspace:*",
13
+ "@tailwindcss/typography": "^0.5.19",
14
+ "@tailwindcss/vite": "^4.2.2",
15
+ "@tanstack/react-hotkeys": "^0.4.2",
16
+ "@tanstack/react-query": "^5.91.2",
17
+ "@tanstack/react-router": "^1.167.5",
18
+ "@tanstack/react-store": "^0.9.2",
19
+ "@tanstack/react-virtual": "^3.13.23",
20
+ "@xterm/addon-fit": "^0.11.0",
21
+ "@xterm/addon-web-links": "^0.12.0",
22
+ "@xterm/xterm": "^6.0.0",
23
+ "daisyui": "^5.5.19",
24
+ "lucide-react": "^0.577.0",
25
+ "react": "^19",
26
+ "react-dom": "^19",
27
+ "react-markdown": "^10.1.0",
28
+ "tailwindcss": "^4.2.2"
29
+ },
30
+ "devDependencies": {
31
+ "@types/react": "^19",
32
+ "@types/react-dom": "^19",
33
+ "@vitejs/plugin-react": "^6",
34
+ "typescript": "^5.9",
35
+ "vite": "^8"
36
+ }
37
+ }
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192" width="192" height="192">
2
+ <rect width="192" height="192" rx="24" fill="#0d0d0d"/>
3
+ <g fill="#e0e0e0">
4
+ <rect x="40" y="60" width="112" height="8" rx="4"/>
5
+ <rect x="40" y="92" width="80" height="8" rx="4"/>
6
+ <rect x="40" y="124" width="96" height="8" rx="4"/>
7
+ <circle cx="152" cy="148" r="20" fill="#4a9eff"/>
8
+ <rect x="148" y="136" width="8" height="12" rx="2" fill="#0d0d0d"/>
9
+ <rect x="144" y="148" width="16" height="8" rx="2" fill="#0d0d0d"/>
10
+ </g>
11
+ </svg>
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
2
+ <rect width="512" height="512" rx="64" fill="#0d0d0d"/>
3
+ <g fill="#e0e0e0">
4
+ <rect x="100" y="160" width="312" height="20" rx="10"/>
5
+ <rect x="100" y="246" width="220" height="20" rx="10"/>
6
+ <rect x="100" y="332" width="264" height="20" rx="10"/>
7
+ <circle cx="412" cy="392" r="56" fill="#4a9eff"/>
8
+ <rect x="400" y="360" width="24" height="32" rx="6" fill="#0d0d0d"/>
9
+ <rect x="388" y="392" width="48" height="24" rx="6" fill="#0d0d0d"/>
10
+ </g>
11
+ </svg>
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "Lattice",
3
+ "short_name": "Lattice",
4
+ "description": "Multi-machine agentic dashboard for Claude Code",
5
+ "display": "standalone",
6
+ "start_url": "/",
7
+ "scope": "/",
8
+ "theme_color": "#0d0d0d",
9
+ "background_color": "#0d0d0d",
10
+ "icons": [
11
+ {
12
+ "src": "/icons/icon-192.svg",
13
+ "sizes": "192x192",
14
+ "type": "image/svg+xml",
15
+ "purpose": "any maskable"
16
+ },
17
+ {
18
+ "src": "/icons/icon-512.svg",
19
+ "sizes": "512x512",
20
+ "type": "image/svg+xml",
21
+ "purpose": "any maskable"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,61 @@
1
+ var CACHE_NAME = "lattice-v1";
2
+ var STATIC_ASSETS = [
3
+ "/",
4
+ "/manifest.json",
5
+ "/icons/icon-192.svg",
6
+ "/icons/icon-512.svg",
7
+ ];
8
+
9
+ self.addEventListener("install", function (event) {
10
+ event.waitUntil(
11
+ caches.open(CACHE_NAME).then(function (cache) {
12
+ return cache.addAll(STATIC_ASSETS);
13
+ }).then(function () {
14
+ return self.skipWaiting();
15
+ })
16
+ );
17
+ });
18
+
19
+ self.addEventListener("activate", function (event) {
20
+ event.waitUntil(
21
+ caches.keys().then(function (keys) {
22
+ return Promise.all(
23
+ keys.filter(function (key) {
24
+ return key !== CACHE_NAME;
25
+ }).map(function (key) {
26
+ return caches.delete(key);
27
+ })
28
+ );
29
+ }).then(function () {
30
+ return self.clients.claim();
31
+ })
32
+ );
33
+ });
34
+
35
+ self.addEventListener("fetch", function (event) {
36
+ var url = new URL(event.request.url);
37
+
38
+ if (url.pathname.startsWith("/ws") || url.pathname.startsWith("/auth")) {
39
+ return;
40
+ }
41
+
42
+ if (event.request.method !== "GET") {
43
+ return;
44
+ }
45
+
46
+ event.respondWith(
47
+ caches.match(event.request).then(function (cached) {
48
+ var networkRequest = fetch(event.request).then(function (response) {
49
+ if (response && response.status === 200 && response.type === "basic") {
50
+ var clone = response.clone();
51
+ caches.open(CACHE_NAME).then(function (cache) {
52
+ cache.put(event.request, clone);
53
+ });
54
+ }
55
+ return response;
56
+ });
57
+
58
+ return cached || networkRequest;
59
+ })
60
+ );
61
+ });
@@ -0,0 +1,28 @@
1
+ import { RouterProvider } from "@tanstack/react-router";
2
+ import { router } from "./router";
3
+ import { WebSocketProvider } from "./providers/WebSocketProvider";
4
+ import { ErrorBoundary } from "./components/ui/ErrorBoundary";
5
+ import { Toast, useToastState } from "./components/ui/Toast";
6
+ import { CommandPalette } from "./components/ui/CommandPalette";
7
+
8
+ function AppInner() {
9
+ var { items, dismiss } = useToastState();
10
+
11
+ return (
12
+ <>
13
+ <RouterProvider router={router} />
14
+ <CommandPalette />
15
+ <Toast items={items} onDismiss={dismiss} />
16
+ </>
17
+ );
18
+ }
19
+
20
+ export function App() {
21
+ return (
22
+ <ErrorBoundary>
23
+ <WebSocketProvider>
24
+ <AppInner />
25
+ </WebSocketProvider>
26
+ </ErrorBoundary>
27
+ );
28
+ }
@@ -0,0 +1,70 @@
1
+ import { useState } from "react";
2
+
3
+ export function PassphrasePrompt() {
4
+ var [passphrase, setPassphrase] = useState("");
5
+ var [error, setError] = useState("");
6
+ var [loading, setLoading] = useState(false);
7
+
8
+ function handleSubmit(e: React.FormEvent) {
9
+ e.preventDefault();
10
+ setError("");
11
+ setLoading(true);
12
+
13
+ fetch("/auth", {
14
+ method: "POST",
15
+ headers: { "Content-Type": "application/json" },
16
+ body: JSON.stringify({ passphrase }),
17
+ })
18
+ .then(function (res) {
19
+ if (res.ok) {
20
+ window.location.reload();
21
+ } else {
22
+ setError("Invalid passphrase.");
23
+ setLoading(false);
24
+ }
25
+ })
26
+ .catch(function () {
27
+ setError("Connection error.");
28
+ setLoading(false);
29
+ });
30
+ }
31
+
32
+ return (
33
+ <div className="min-h-screen bg-base-100 flex items-center justify-center">
34
+ <div className="card bg-base-200 border border-base-300 w-full max-w-[340px] shadow-xl">
35
+ <div className="card-body p-10">
36
+ <h1 className="text-[15px] font-bold tracking-[0.12em] uppercase text-base-content/60 mb-7">
37
+ Lattice
38
+ </h1>
39
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
40
+ <fieldset className="fieldset">
41
+ <legend className="fieldset-legend text-[11px] uppercase tracking-[0.1em] text-base-content/40">
42
+ Passphrase
43
+ </legend>
44
+ <input
45
+ id="passphrase"
46
+ type="password"
47
+ value={passphrase}
48
+ onChange={function (e) { setPassphrase(e.target.value); }}
49
+ autoFocus
50
+ autoComplete="current-password"
51
+ disabled={loading}
52
+ className="input input-bordered w-full bg-base-100 text-base-content text-[14px]"
53
+ />
54
+ </fieldset>
55
+ <button
56
+ type="submit"
57
+ disabled={loading}
58
+ className={"btn btn-primary w-full mt-1 " + (loading ? "cursor-not-allowed" : "")}
59
+ >
60
+ {loading ? "Authenticating..." : "Authenticate"}
61
+ </button>
62
+ {error && (
63
+ <p className="text-[12px] text-error text-center">{error}</p>
64
+ )}
65
+ </form>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,241 @@
1
+ import { useRef, useState, useEffect, useMemo } from "react";
2
+ import { SendHorizontal, Settings } from "lucide-react";
3
+ import { useSkills } from "../../hooks/useSkills";
4
+
5
+ interface ChatInputProps {
6
+ onSend: (text: string) => void;
7
+ disabled: boolean;
8
+ toolbarContent?: React.ReactNode;
9
+ }
10
+
11
+ function getModKey(): string {
12
+ if (typeof navigator === "undefined") return "Ctrl";
13
+ var platform = navigator.platform || "";
14
+ if (platform.indexOf("Mac") !== -1) return "⌘";
15
+ return "Ctrl";
16
+ }
17
+
18
+ export function ChatInput(props: ChatInputProps) {
19
+ var textareaRef = useRef<HTMLTextAreaElement>(null);
20
+ var popupRef = useRef<HTMLDivElement>(null);
21
+ var settingsRef = useRef<HTMLDivElement>(null);
22
+ var settingsBtnRef = useRef<HTMLButtonElement>(null);
23
+ var skills = useSkills();
24
+ var [slashQuery, setSlashQuery] = useState<string | null>(null);
25
+ var [selectedIndex, setSelectedIndex] = useState(0);
26
+ var [showMobileSettings, setShowMobileSettings] = useState(false);
27
+ var modKey = useMemo(getModKey, []);
28
+
29
+ var filtered = useMemo(function () {
30
+ if (slashQuery === null) return [];
31
+ var q = slashQuery.toLowerCase();
32
+ return skills.filter(function (s) {
33
+ return s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q);
34
+ });
35
+ }, [slashQuery, skills]);
36
+
37
+ var isOpen = slashQuery !== null && filtered.length > 0;
38
+
39
+ useEffect(function () {
40
+ setSelectedIndex(0);
41
+ }, [slashQuery]);
42
+
43
+ useEffect(function () {
44
+ if (!isOpen || !popupRef.current) return;
45
+ var active = popupRef.current.querySelector("[data-active='true']") as HTMLElement | null;
46
+ if (active) {
47
+ active.scrollIntoView({ block: "nearest" });
48
+ }
49
+ }, [selectedIndex, isOpen]);
50
+
51
+ useEffect(function () {
52
+ if (!showMobileSettings) return;
53
+ function handleClick(e: MouseEvent) {
54
+ var target = e.target as Node;
55
+ if (settingsRef.current && settingsRef.current.contains(target)) return;
56
+ if (settingsBtnRef.current && settingsBtnRef.current.contains(target)) return;
57
+ setShowMobileSettings(false);
58
+ }
59
+ document.addEventListener("mousedown", handleClick);
60
+ return function () { document.removeEventListener("mousedown", handleClick); };
61
+ }, [showMobileSettings]);
62
+
63
+ function checkSlash() {
64
+ var el = textareaRef.current;
65
+ if (!el) return;
66
+ var val = el.value;
67
+ if (val.startsWith("/")) {
68
+ setSlashQuery(val.slice(1));
69
+ } else {
70
+ setSlashQuery(null);
71
+ }
72
+ }
73
+
74
+ function selectSkill(name: string) {
75
+ var el = textareaRef.current;
76
+ if (!el) return;
77
+ el.value = "/" + name + " ";
78
+ el.focus();
79
+ setSlashQuery(null);
80
+ }
81
+
82
+ function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
83
+ if (isOpen) {
84
+ if (e.key === "ArrowUp") {
85
+ e.preventDefault();
86
+ setSelectedIndex(function (i) { return i > 0 ? i - 1 : filtered.length - 1; });
87
+ return;
88
+ }
89
+ if (e.key === "ArrowDown") {
90
+ e.preventDefault();
91
+ setSelectedIndex(function (i) { return i < filtered.length - 1 ? i + 1 : 0; });
92
+ return;
93
+ }
94
+ if (e.key === "Tab" || (e.key === "Enter" && !e.shiftKey)) {
95
+ e.preventDefault();
96
+ if (filtered[selectedIndex]) {
97
+ selectSkill(filtered[selectedIndex].name);
98
+ }
99
+ return;
100
+ }
101
+ if (e.key === "Escape") {
102
+ e.preventDefault();
103
+ setSlashQuery(null);
104
+ return;
105
+ }
106
+ }
107
+ if (e.key === "Enter" && !e.shiftKey) {
108
+ e.preventDefault();
109
+ submit();
110
+ }
111
+ }
112
+
113
+ function handleInput(e: React.FormEvent<HTMLTextAreaElement>) {
114
+ var el = e.currentTarget;
115
+ el.style.height = "auto";
116
+ el.style.height = Math.min(el.scrollHeight, 160) + "px";
117
+ checkSlash();
118
+ }
119
+
120
+ function submit() {
121
+ var el = textareaRef.current;
122
+ if (!el) {
123
+ return;
124
+ }
125
+ var text = el.value.trim();
126
+ if (!text || props.disabled) {
127
+ return;
128
+ }
129
+ props.onSend(text);
130
+ el.value = "";
131
+ el.style.height = "auto";
132
+ setSlashQuery(null);
133
+ }
134
+
135
+ return (
136
+ <div className="relative">
137
+ {isOpen && (
138
+ <div
139
+ ref={popupRef}
140
+ role="listbox"
141
+ aria-label="Slash commands"
142
+ className="absolute left-0 right-0 bottom-[calc(100%+6px)] max-h-[320px] overflow-y-auto rounded-lg border border-base-content/10 bg-base-300 shadow-lg z-50"
143
+ >
144
+ {filtered.map(function (skill, i) {
145
+ return (
146
+ <button
147
+ key={skill.name}
148
+ data-active={i === selectedIndex}
149
+ onMouseDown={function (e) {
150
+ e.preventDefault();
151
+ selectSkill(skill.name);
152
+ }}
153
+ onMouseEnter={function () { setSelectedIndex(i); }}
154
+ className={
155
+ "flex w-full items-center gap-3 px-3.5 py-2.5 text-left transition-colors " +
156
+ (i === selectedIndex ? "bg-primary/10" : "hover:bg-base-content/5")
157
+ }
158
+ >
159
+ <span className="font-mono text-[12px] text-primary/90 whitespace-nowrap flex-shrink-0">
160
+ /{skill.name}
161
+ </span>
162
+ <span className="text-[11px] text-base-content/40 truncate min-w-0">
163
+ {skill.description}
164
+ </span>
165
+ </button>
166
+ );
167
+ })}
168
+ </div>
169
+ )}
170
+
171
+ {showMobileSettings && (
172
+ <div
173
+ ref={settingsRef}
174
+ className="absolute right-0 bottom-[calc(100%+6px)] rounded-lg border border-base-content/10 bg-base-300 shadow-lg z-50 p-2.5 min-w-[200px] sm:hidden"
175
+ >
176
+ <div className="flex flex-col gap-2">
177
+ {props.toolbarContent}
178
+ </div>
179
+ </div>
180
+ )}
181
+
182
+ <div
183
+ className={
184
+ "border rounded-xl bg-base-300/60 overflow-hidden transition-all duration-150 " +
185
+ (props.disabled
186
+ ? "border-base-content/10 opacity-60"
187
+ : "border-primary/20 focus-within:border-primary/40 focus-within:shadow-[0_0_0_3px_oklch(from_var(--color-primary)_l_c_h/0.1)]")
188
+ }
189
+ >
190
+ <div className="hidden sm:flex items-center gap-1.5 px-3 py-1.5 border-b border-base-content/8 font-mono text-[10px]">
191
+ {props.toolbarContent}
192
+ <span className="flex-1" />
193
+ <span className="text-base-content/20">{modKey}+K commands</span>
194
+ </div>
195
+ <div className="flex items-center gap-2 px-3.5 py-2.5">
196
+ <div className="flex-1 min-w-0 relative">
197
+ <span className="absolute left-0 top-[1px] text-primary/50 font-mono text-[14px] leading-relaxed select-none pointer-events-none">›</span>
198
+ <textarea
199
+ ref={textareaRef}
200
+ aria-label="Message input"
201
+ placeholder={props.disabled ? "Claude is responding..." : "Message Claude..."}
202
+ disabled={props.disabled}
203
+ onKeyDown={handleKeyDown}
204
+ onInput={handleInput}
205
+ rows={1}
206
+ style={{ padding: "1px 0 0 16px", margin: 0, border: "none" }}
207
+ className={
208
+ "w-full resize-none bg-transparent text-base-content text-[14px] leading-relaxed max-h-[160px] overflow-y-auto outline-none placeholder:text-base-content/30 " +
209
+ (props.disabled ? "cursor-not-allowed" : "cursor-text")
210
+ }
211
+ />
212
+ </div>
213
+ <div className="flex items-center gap-1.5 flex-shrink-0">
214
+ <button
215
+ ref={settingsBtnRef}
216
+ aria-label="Chat settings"
217
+ onClick={function () { setShowMobileSettings(!showMobileSettings); }}
218
+ className={"sm:hidden w-8 h-8 rounded-lg flex items-center justify-center transition-colors " + (showMobileSettings ? "bg-base-content/10 text-base-content/60" : "text-base-content/30 hover:text-base-content/50")}
219
+ >
220
+ <Settings size={15} />
221
+ </button>
222
+ <span className="text-[10px] text-base-content/20 font-mono hidden sm:block">⏎ send</span>
223
+ <button
224
+ aria-label="Send message"
225
+ disabled={props.disabled}
226
+ onClick={submit}
227
+ className={
228
+ "w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 transition-all duration-150 outline-none " +
229
+ (props.disabled
230
+ ? "bg-base-content/5 text-base-content/20 cursor-not-allowed"
231
+ : "bg-primary text-primary-content hover:bg-primary/80 cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:ring-offset-2 focus-visible:ring-offset-base-300")
232
+ }
233
+ >
234
+ <SendHorizontal size={14} />
235
+ </button>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ </div>
240
+ );
241
+ }