@cryptiklemur/lattice 1.3.0 → 1.4.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/bun.lock +705 -2
- package/client/index.html +1 -13
- package/client/package.json +6 -1
- package/client/src/App.tsx +2 -0
- package/client/src/commands.ts +36 -0
- package/client/src/components/chat/AttachmentChips.tsx +116 -0
- package/client/src/components/chat/ChatInput.tsx +250 -73
- package/client/src/components/chat/ChatView.tsx +242 -10
- package/client/src/components/chat/CommandPalette.tsx +162 -0
- package/client/src/components/chat/Message.tsx +23 -2
- package/client/src/components/chat/PromptQuestion.tsx +271 -0
- package/client/src/components/chat/TodoCard.tsx +57 -0
- package/client/src/components/chat/ToolResultRenderer.tsx +2 -1
- package/client/src/components/chat/VoiceRecorder.tsx +85 -0
- package/client/src/components/project-settings/ProjectMemory.tsx +12 -2
- package/client/src/components/project-settings/ProjectNotifications.tsx +48 -0
- package/client/src/components/project-settings/ProjectRules.tsx +10 -1
- package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
- package/client/src/components/settings/Appearance.tsx +1 -0
- package/client/src/components/settings/ClaudeSettings.tsx +10 -0
- package/client/src/components/settings/Editor.tsx +123 -0
- package/client/src/components/settings/GlobalMcp.tsx +10 -1
- package/client/src/components/settings/GlobalMemory.tsx +19 -0
- package/client/src/components/settings/GlobalRules.tsx +149 -0
- package/client/src/components/settings/GlobalSkills.tsx +10 -0
- package/client/src/components/settings/Notifications.tsx +88 -0
- package/client/src/components/settings/SettingsView.tsx +12 -0
- package/client/src/components/settings/skill-shared.tsx +2 -1
- package/client/src/components/setup/SetupWizard.tsx +1 -1
- package/client/src/components/sidebar/NodeSettingsModal.tsx +23 -1
- package/client/src/components/sidebar/ProjectDropdown.tsx +176 -27
- package/client/src/components/sidebar/SettingsSidebar.tsx +11 -1
- package/client/src/components/sidebar/Sidebar.tsx +35 -2
- package/client/src/components/sidebar/UserIsland.tsx +18 -7
- package/client/src/components/ui/UpdatePrompt.tsx +47 -0
- package/client/src/components/workspace/FileBrowser.tsx +174 -0
- package/client/src/components/workspace/FileTree.tsx +129 -0
- package/client/src/components/workspace/FileViewer.tsx +211 -0
- package/client/src/components/workspace/NoteCard.tsx +119 -0
- package/client/src/components/workspace/NotesView.tsx +102 -0
- package/client/src/components/workspace/ScheduledTasksView.tsx +117 -0
- package/client/src/components/workspace/SplitPane.tsx +81 -0
- package/client/src/components/workspace/TabBar.tsx +185 -0
- package/client/src/components/workspace/TaskCard.tsx +158 -0
- package/client/src/components/workspace/TaskEditModal.tsx +114 -0
- package/client/src/components/{panels/Terminal.tsx → workspace/TerminalInstance.tsx} +50 -7
- package/client/src/components/workspace/TerminalView.tsx +110 -0
- package/client/src/components/workspace/WorkspaceView.tsx +116 -0
- package/client/src/hooks/useAttachments.ts +280 -0
- package/client/src/hooks/useEditorConfig.ts +28 -0
- package/client/src/hooks/useIdleDetection.ts +44 -0
- package/client/src/hooks/useInstallPrompt.ts +53 -0
- package/client/src/hooks/useNotifications.ts +54 -0
- package/client/src/hooks/useOnline.ts +6 -0
- package/client/src/hooks/useSession.ts +110 -4
- package/client/src/hooks/useSpinnerVerb.ts +36 -0
- package/client/src/hooks/useSwipeDrawer.ts +275 -0
- package/client/src/hooks/useVoiceRecorder.ts +123 -0
- package/client/src/hooks/useWorkspace.ts +48 -0
- package/client/src/providers/WebSocketProvider.tsx +18 -0
- package/client/src/router.tsx +48 -20
- package/client/src/stores/session.ts +136 -0
- package/client/src/stores/sidebar.ts +3 -2
- package/client/src/stores/workspace.ts +254 -0
- package/client/src/styles/global.css +123 -0
- package/client/src/utils/editorUrl.ts +62 -0
- package/client/vite.config.ts +53 -1
- package/package.json +1 -1
- package/server/src/daemon.ts +11 -1
- package/server/src/features/scheduler.ts +23 -0
- package/server/src/features/sticky-notes.ts +5 -3
- package/server/src/handlers/attachment.ts +172 -0
- package/server/src/handlers/chat.ts +43 -2
- package/server/src/handlers/editor.ts +40 -0
- package/server/src/handlers/fs.ts +10 -2
- package/server/src/handlers/memory.ts +3 -0
- package/server/src/handlers/notes.ts +4 -2
- package/server/src/handlers/scheduler.ts +18 -1
- package/server/src/handlers/session.ts +14 -8
- package/server/src/handlers/settings.ts +37 -2
- package/server/src/handlers/terminal.ts +13 -6
- package/server/src/project/pty-worker.cjs +83 -0
- package/server/src/project/sdk-bridge.ts +266 -11
- package/server/src/project/terminal.ts +78 -34
- package/shared/src/messages.ts +145 -4
- package/shared/src/models.ts +27 -1
- package/shared/src/project-settings.ts +1 -1
- package/tp.js +19 -0
- package/client/public/manifest.json +0 -24
- package/client/public/sw.js +0 -61
- package/client/src/components/panels/FileBrowser.tsx +0 -241
- package/client/src/components/panels/StickyNotes.tsx +0 -187
|
@@ -101,6 +101,13 @@ html, body {
|
|
|
101
101
|
font-family: var(--font-mono);
|
|
102
102
|
font-size: 13px;
|
|
103
103
|
line-height: 1.6;
|
|
104
|
+
overflow-x: auto;
|
|
105
|
+
max-width: 100%;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.prose {
|
|
109
|
+
overflow-wrap: break-word;
|
|
110
|
+
word-break: break-word;
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
.prose code {
|
|
@@ -116,6 +123,79 @@ html, body {
|
|
|
116
123
|
background: transparent;
|
|
117
124
|
}
|
|
118
125
|
|
|
126
|
+
.prose .table-wrapper {
|
|
127
|
+
overflow-x: auto;
|
|
128
|
+
margin: 0.75em 0;
|
|
129
|
+
border-radius: 0.5rem;
|
|
130
|
+
border: 1px solid oklch(from var(--color-base-content) l c h / 0.08);
|
|
131
|
+
background: oklch(from var(--color-base-100) l c h / 0.6);
|
|
132
|
+
scrollbar-width: thin;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.prose table {
|
|
136
|
+
border-collapse: collapse;
|
|
137
|
+
width: 100%;
|
|
138
|
+
font-size: 12px;
|
|
139
|
+
font-family: var(--font-mono);
|
|
140
|
+
margin: 0;
|
|
141
|
+
border: none;
|
|
142
|
+
line-height: 1.5;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.prose thead {
|
|
146
|
+
background: oklch(from var(--color-base-content) l c h / 0.04);
|
|
147
|
+
position: sticky;
|
|
148
|
+
top: 0;
|
|
149
|
+
z-index: 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.prose th {
|
|
153
|
+
text-align: left;
|
|
154
|
+
font-weight: 600;
|
|
155
|
+
font-size: 10px;
|
|
156
|
+
text-transform: uppercase;
|
|
157
|
+
letter-spacing: 0.06em;
|
|
158
|
+
padding: 8px 14px;
|
|
159
|
+
color: oklch(from var(--color-base-content) l c h / 0.35);
|
|
160
|
+
border-bottom: 1px solid oklch(from var(--color-base-content) l c h / 0.1);
|
|
161
|
+
white-space: nowrap;
|
|
162
|
+
user-select: none;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.prose td {
|
|
166
|
+
padding: 8px 14px;
|
|
167
|
+
color: oklch(from var(--color-base-content) l c h / 0.65);
|
|
168
|
+
border-bottom: 1px solid oklch(from var(--color-base-content) l c h / 0.04);
|
|
169
|
+
vertical-align: top;
|
|
170
|
+
white-space: normal;
|
|
171
|
+
min-width: 60px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.prose td code {
|
|
175
|
+
font-size: 11px;
|
|
176
|
+
white-space: nowrap;
|
|
177
|
+
padding: 1px 5px;
|
|
178
|
+
border-radius: 3px;
|
|
179
|
+
background: oklch(from var(--color-primary) l c h / 0.08);
|
|
180
|
+
color: oklch(from var(--color-primary) l c h / 0.8);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.prose tbody tr:last-child td {
|
|
184
|
+
border-bottom: none;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.prose tbody tr {
|
|
188
|
+
transition: background-color 150ms ease;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.prose tbody tr:nth-child(even) {
|
|
192
|
+
background: oklch(from var(--color-base-content) l c h / 0.015);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.prose tbody tr:hover {
|
|
196
|
+
background: oklch(from var(--color-primary) l c h / 0.04);
|
|
197
|
+
}
|
|
198
|
+
|
|
119
199
|
.bg-lattice-grid {
|
|
120
200
|
background-color: transparent;
|
|
121
201
|
}
|
|
@@ -156,6 +236,40 @@ html, body {
|
|
|
156
236
|
}
|
|
157
237
|
}
|
|
158
238
|
|
|
239
|
+
/* Override DaisyUI drawer for smooth slide transitions on mobile.
|
|
240
|
+
DaisyUI defaults use opacity on the whole drawer-side which causes
|
|
241
|
+
an instant vanish. We keep drawer-side always opacity:1 and instead
|
|
242
|
+
slide the content panel + fade the overlay independently. */
|
|
243
|
+
@media (max-width: 1023px) {
|
|
244
|
+
/* Keep the container itself always fully opaque so the slide is visible.
|
|
245
|
+
Use visibility + pointer-events to control interactivity. */
|
|
246
|
+
.drawer-side {
|
|
247
|
+
opacity: 1 !important;
|
|
248
|
+
transition: visibility 0s linear 0.25s !important;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.drawer-toggle:checked ~ .drawer-side {
|
|
252
|
+
transition: visibility 0s linear 0s !important;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* Sidebar content: slide in/out using translate (matches DaisyUI's property) */
|
|
256
|
+
.drawer-side > *:not(.drawer-overlay) {
|
|
257
|
+
will-change: translate;
|
|
258
|
+
transition: translate 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* Overlay: fade in/out */
|
|
262
|
+
.drawer-side > .drawer-overlay {
|
|
263
|
+
will-change: opacity;
|
|
264
|
+
opacity: 0;
|
|
265
|
+
transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.drawer-toggle:checked ~ .drawer-side > .drawer-overlay {
|
|
269
|
+
opacity: 1;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
159
273
|
.scrollbar-hidden {
|
|
160
274
|
scrollbar-width: none;
|
|
161
275
|
-ms-overflow-style: none;
|
|
@@ -165,6 +279,15 @@ html, body {
|
|
|
165
279
|
display: none;
|
|
166
280
|
}
|
|
167
281
|
|
|
282
|
+
@keyframes waveform {
|
|
283
|
+
0%, 100% { height: 4px; opacity: 0.3; }
|
|
284
|
+
50% { height: 16px; opacity: 0.8; }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.animate-waveform {
|
|
288
|
+
animation: waveform 0.6s ease-in-out infinite;
|
|
289
|
+
}
|
|
290
|
+
|
|
168
291
|
@media (prefers-reduced-motion: reduce) {
|
|
169
292
|
*, *::before, *::after {
|
|
170
293
|
animation-duration: 0.01ms !important;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
var JETBRAINS_IDS: Record<string, string> = {
|
|
2
|
+
webstorm: "webstorm",
|
|
3
|
+
intellij: "idea",
|
|
4
|
+
pycharm: "pycharm",
|
|
5
|
+
goland: "goland",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function toWindowsPath(linuxPath: string, wslDistro: string): string {
|
|
9
|
+
return "\\\\" + "wsl.localhost\\" + wslDistro + linuxPath.replace(/\//g, "\\");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildJetBrainsUrl(ideId: string, filePath: string, line?: number, projectName?: string): string {
|
|
13
|
+
var url = "jetbrains://" + ideId + "/navigate/reference?";
|
|
14
|
+
if (projectName) {
|
|
15
|
+
url += "project=" + encodeURIComponent(projectName);
|
|
16
|
+
}
|
|
17
|
+
if (filePath) {
|
|
18
|
+
url += (projectName ? "&" : "") + "path=" + encodeURIComponent(filePath);
|
|
19
|
+
if (line) {
|
|
20
|
+
url += "&line=" + line;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return url;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface EditorUrlOptions {
|
|
27
|
+
editorType: string;
|
|
28
|
+
projectPath: string;
|
|
29
|
+
filePath: string;
|
|
30
|
+
line?: number;
|
|
31
|
+
wslDistro?: string;
|
|
32
|
+
ideProjectName?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getEditorUrl(editorType: string, projectPath: string, filePath: string, line?: number, wslDistro?: string, ideProjectName?: string): string | null {
|
|
36
|
+
var fullPath = filePath === "." ? projectPath : projectPath + "/" + filePath;
|
|
37
|
+
var resolvedPath = wslDistro ? toWindowsPath(fullPath, wslDistro) : fullPath;
|
|
38
|
+
|
|
39
|
+
var jetbrainsId = JETBRAINS_IDS[editorType];
|
|
40
|
+
if (jetbrainsId) {
|
|
41
|
+
if (ideProjectName) {
|
|
42
|
+
var jbPath = filePath === "." ? "" : filePath;
|
|
43
|
+
return buildJetBrainsUrl(jetbrainsId, jbPath, line, ideProjectName);
|
|
44
|
+
}
|
|
45
|
+
return buildJetBrainsUrl(jetbrainsId, resolvedPath, line);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (editorType === "vscode" || editorType === "vscode-insiders" || editorType === "cursor") {
|
|
49
|
+
var scheme = editorType;
|
|
50
|
+
var isFile = filePath !== ".";
|
|
51
|
+
var lineSuffix = isFile ? ":" + (line || 1) : "";
|
|
52
|
+
if (wslDistro) {
|
|
53
|
+
return scheme + "://vscode-remote/wsl+" + wslDistro + fullPath + lineSuffix;
|
|
54
|
+
}
|
|
55
|
+
return scheme + "://file/" + resolvedPath + lineSuffix;
|
|
56
|
+
}
|
|
57
|
+
if (editorType === "sublime") {
|
|
58
|
+
return "subl://open?url=file://" + encodeURIComponent(resolvedPath) + (line ? "&line=" + line : "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
}
|
package/client/vite.config.ts
CHANGED
|
@@ -1,9 +1,52 @@
|
|
|
1
1
|
import { defineConfig } from "vite";
|
|
2
2
|
import react from "@vitejs/plugin-react";
|
|
3
3
|
import tailwindcss from "@tailwindcss/vite";
|
|
4
|
+
import { VitePWA } from "vite-plugin-pwa";
|
|
4
5
|
|
|
5
6
|
export default defineConfig({
|
|
6
|
-
plugins: [
|
|
7
|
+
plugins: [
|
|
8
|
+
tailwindcss(),
|
|
9
|
+
react(),
|
|
10
|
+
VitePWA({
|
|
11
|
+
registerType: "prompt",
|
|
12
|
+
workbox: {
|
|
13
|
+
globPatterns: ["**/*.{js,css,html,svg,png,woff2}"],
|
|
14
|
+
navigateFallback: "/index.html",
|
|
15
|
+
navigateFallbackDenylist: [/^\/ws/, /^\/api/],
|
|
16
|
+
runtimeCaching: [
|
|
17
|
+
{
|
|
18
|
+
urlPattern: /^https?:\/\/.*\.(?:js|css|woff2)$/,
|
|
19
|
+
handler: "CacheFirst",
|
|
20
|
+
options: {
|
|
21
|
+
cacheName: "static-assets",
|
|
22
|
+
expiration: { maxEntries: 100, maxAgeSeconds: 30 * 24 * 60 * 60 },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
urlPattern: /^https?:\/\/.*\.(?:svg|png|jpg|jpeg|gif|webp)$/,
|
|
27
|
+
handler: "CacheFirst",
|
|
28
|
+
options: {
|
|
29
|
+
cacheName: "images",
|
|
30
|
+
expiration: { maxEntries: 50, maxAgeSeconds: 30 * 24 * 60 * 60 },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
manifest: {
|
|
36
|
+
name: "Lattice",
|
|
37
|
+
short_name: "Lattice",
|
|
38
|
+
description: "Multi-machine agentic dashboard for Claude Code",
|
|
39
|
+
display: "standalone",
|
|
40
|
+
start_url: "/",
|
|
41
|
+
theme_color: "#0d0d0d",
|
|
42
|
+
background_color: "#0d0d0d",
|
|
43
|
+
icons: [
|
|
44
|
+
{ src: "/icons/icon-192.svg", sizes: "192x192", type: "image/svg+xml", purpose: "maskable" },
|
|
45
|
+
{ src: "/icons/icon-512.svg", sizes: "512x512", type: "image/svg+xml", purpose: "maskable" },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
],
|
|
7
50
|
server: {
|
|
8
51
|
host: "0.0.0.0",
|
|
9
52
|
open: true,
|
|
@@ -11,9 +54,18 @@ export default defineConfig({
|
|
|
11
54
|
"/ws": {
|
|
12
55
|
target: "ws://localhost:7654",
|
|
13
56
|
ws: true,
|
|
57
|
+
configure: function (proxy) {
|
|
58
|
+
proxy.on("error", function () {});
|
|
59
|
+
proxy.on("proxyReqWs", function (_proxyReq, _req, socket) {
|
|
60
|
+
socket.on("error", function () {});
|
|
61
|
+
});
|
|
62
|
+
},
|
|
14
63
|
},
|
|
15
64
|
"/api": {
|
|
16
65
|
target: "http://localhost:7654",
|
|
66
|
+
configure: function (proxy) {
|
|
67
|
+
proxy.on("error", function () {});
|
|
68
|
+
},
|
|
17
69
|
},
|
|
18
70
|
},
|
|
19
71
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Multi-machine agentic dashboard for Claude Code. Monitor sessions, manage MCP servers and skills, orchestrate across mesh-networked nodes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Aaron Scherer <me@aaronscherer.me>",
|
package/server/src/daemon.ts
CHANGED
|
@@ -14,7 +14,9 @@ import { ensureCerts } from "./tls";
|
|
|
14
14
|
import type { ClientMessage, MeshMessage } from "@lattice/shared";
|
|
15
15
|
import "./handlers/session";
|
|
16
16
|
import "./handlers/chat";
|
|
17
|
-
import
|
|
17
|
+
import "./handlers/attachment";
|
|
18
|
+
import { loadInterruptedSessions, unwatchSessionLock } from "./project/sdk-bridge";
|
|
19
|
+
import { clearActiveSession, getActiveSession } from "./handlers/chat";
|
|
18
20
|
import "./handlers/fs";
|
|
19
21
|
import "./handlers/terminal";
|
|
20
22
|
import "./handlers/settings";
|
|
@@ -25,9 +27,11 @@ import "./handlers/scheduler";
|
|
|
25
27
|
import "./handlers/notes";
|
|
26
28
|
import "./handlers/skills";
|
|
27
29
|
import "./handlers/memory";
|
|
30
|
+
import "./handlers/editor";
|
|
28
31
|
import { startScheduler } from "./features/scheduler";
|
|
29
32
|
import { loadNotes } from "./features/sticky-notes";
|
|
30
33
|
import { cleanupClientTerminals } from "./handlers/terminal";
|
|
34
|
+
import { cleanupClient as cleanupClientAttachments } from "./handlers/attachment";
|
|
31
35
|
|
|
32
36
|
interface WsData {
|
|
33
37
|
id: string;
|
|
@@ -299,8 +303,14 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
|
|
|
299
303
|
}
|
|
300
304
|
},
|
|
301
305
|
close(ws: ServerWebSocket<WsData>) {
|
|
306
|
+
var activeSession = getActiveSession(ws.data.id);
|
|
307
|
+
if (activeSession) {
|
|
308
|
+
unwatchSessionLock(activeSession.sessionId);
|
|
309
|
+
}
|
|
310
|
+
clearActiveSession(ws.data.id);
|
|
302
311
|
removeClient(ws.data.id);
|
|
303
312
|
cleanupClientTerminals(ws.data.id);
|
|
313
|
+
cleanupClientAttachments(ws.data.id);
|
|
304
314
|
console.log(`[lattice] Client disconnected: ${ws.data.id}`);
|
|
305
315
|
},
|
|
306
316
|
},
|
|
@@ -245,6 +245,29 @@ export function createTask(data: {
|
|
|
245
245
|
return task;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
export function updateTask(taskId: string, data: { name?: string; prompt?: string; cron?: string }): ScheduledTask | null {
|
|
249
|
+
var task: ScheduledTask | null = null;
|
|
250
|
+
for (var i = 0; i < tasks.length; i++) {
|
|
251
|
+
if (tasks[i].id === taskId) {
|
|
252
|
+
task = tasks[i];
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (!task) return null;
|
|
257
|
+
|
|
258
|
+
if (data.cron && data.cron !== task.cron) {
|
|
259
|
+
var parsed = parseCron(data.cron);
|
|
260
|
+
if (!parsed) return null;
|
|
261
|
+
task.cron = data.cron;
|
|
262
|
+
task.nextRunAt = task.enabled ? nextRunTime(data.cron) : null;
|
|
263
|
+
}
|
|
264
|
+
if (data.name !== undefined) task.name = data.name;
|
|
265
|
+
if (data.prompt !== undefined) task.prompt = data.prompt;
|
|
266
|
+
task.updatedAt = Date.now();
|
|
267
|
+
saveSchedules();
|
|
268
|
+
return task;
|
|
269
|
+
}
|
|
270
|
+
|
|
248
271
|
export function deleteTask(taskId: string): boolean {
|
|
249
272
|
var idx = -1;
|
|
250
273
|
for (var i = 0; i < tasks.length; i++) {
|
|
@@ -61,17 +61,19 @@ function saveNotes(): void {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export function listNotes(): StickyNote[] {
|
|
65
|
-
return notes.slice();
|
|
64
|
+
export function listNotes(projectSlug?: string): StickyNote[] {
|
|
65
|
+
if (!projectSlug) return notes.slice();
|
|
66
|
+
return notes.filter(function (n) { return n.projectSlug === projectSlug; });
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
export function createNote(content: string): StickyNote {
|
|
69
|
+
export function createNote(content: string, projectSlug?: string): StickyNote {
|
|
69
70
|
var now = Date.now();
|
|
70
71
|
var note: StickyNote = {
|
|
71
72
|
id: "note_" + now + "_" + randomBytes(3).toString("hex"),
|
|
72
73
|
content,
|
|
73
74
|
createdAt: now,
|
|
74
75
|
updatedAt: now,
|
|
76
|
+
projectSlug,
|
|
75
77
|
};
|
|
76
78
|
notes.push(note);
|
|
77
79
|
saveNotes();
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { Attachment } from "@lattice/shared";
|
|
2
|
+
import type { AttachmentChunkMessage, AttachmentCompleteMessage, ClientMessage } from "@lattice/shared";
|
|
3
|
+
import { registerHandler } from "../ws/router";
|
|
4
|
+
import { sendTo } from "../ws/broadcast";
|
|
5
|
+
|
|
6
|
+
interface PendingUpload {
|
|
7
|
+
chunks: Map<number, Buffer>;
|
|
8
|
+
totalChunks: number;
|
|
9
|
+
receivedCount: number;
|
|
10
|
+
createdAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
var stores = new Map<string, Map<string, PendingUpload>>();
|
|
14
|
+
var completed = new Map<string, Map<string, Attachment>>();
|
|
15
|
+
|
|
16
|
+
var TTL_MS = 5 * 60 * 1000;
|
|
17
|
+
var CLEANUP_INTERVAL_MS = 60 * 1000;
|
|
18
|
+
|
|
19
|
+
function getClientStore(clientId: string): Map<string, PendingUpload> {
|
|
20
|
+
var store = stores.get(clientId);
|
|
21
|
+
if (!store) {
|
|
22
|
+
store = new Map();
|
|
23
|
+
stores.set(clientId, store);
|
|
24
|
+
}
|
|
25
|
+
return store;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getClientCompleted(clientId: string): Map<string, Attachment> {
|
|
29
|
+
var store = completed.get(clientId);
|
|
30
|
+
if (!store) {
|
|
31
|
+
store = new Map();
|
|
32
|
+
completed.set(clientId, store);
|
|
33
|
+
}
|
|
34
|
+
return store;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
registerHandler("attachment", function (clientId: string, message: ClientMessage) {
|
|
38
|
+
if (message.type === "attachment:chunk") {
|
|
39
|
+
var msg = message as AttachmentChunkMessage;
|
|
40
|
+
var store = getClientStore(clientId);
|
|
41
|
+
|
|
42
|
+
var pending = store.get(msg.attachmentId);
|
|
43
|
+
if (!pending) {
|
|
44
|
+
pending = {
|
|
45
|
+
chunks: new Map(),
|
|
46
|
+
totalChunks: msg.totalChunks,
|
|
47
|
+
receivedCount: 0,
|
|
48
|
+
createdAt: Date.now(),
|
|
49
|
+
};
|
|
50
|
+
store.set(msg.attachmentId, pending);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (pending.chunks.has(msg.chunkIndex)) {
|
|
54
|
+
sendTo(clientId, {
|
|
55
|
+
type: "attachment:error",
|
|
56
|
+
attachmentId: msg.attachmentId,
|
|
57
|
+
error: "Duplicate chunk index: " + msg.chunkIndex,
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pending.chunks.set(msg.chunkIndex, Buffer.from(msg.data, "base64"));
|
|
63
|
+
pending.receivedCount++;
|
|
64
|
+
|
|
65
|
+
sendTo(clientId, {
|
|
66
|
+
type: "attachment:progress",
|
|
67
|
+
attachmentId: msg.attachmentId,
|
|
68
|
+
received: pending.receivedCount,
|
|
69
|
+
total: pending.totalChunks,
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (message.type === "attachment:complete") {
|
|
75
|
+
var msg = message as AttachmentCompleteMessage;
|
|
76
|
+
var store = getClientStore(clientId);
|
|
77
|
+
var pending = store.get(msg.attachmentId);
|
|
78
|
+
|
|
79
|
+
if (!pending) {
|
|
80
|
+
sendTo(clientId, {
|
|
81
|
+
type: "attachment:error",
|
|
82
|
+
attachmentId: msg.attachmentId,
|
|
83
|
+
error: "No chunks received for this attachment",
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (pending.receivedCount !== pending.totalChunks) {
|
|
89
|
+
sendTo(clientId, {
|
|
90
|
+
type: "attachment:error",
|
|
91
|
+
attachmentId: msg.attachmentId,
|
|
92
|
+
error: "Missing chunks: received " + pending.receivedCount + " of " + pending.totalChunks,
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
var buffers: Buffer[] = [];
|
|
98
|
+
for (var i = 0; i < pending.totalChunks; i++) {
|
|
99
|
+
var chunk = pending.chunks.get(i);
|
|
100
|
+
if (!chunk) {
|
|
101
|
+
sendTo(clientId, {
|
|
102
|
+
type: "attachment:error",
|
|
103
|
+
attachmentId: msg.attachmentId,
|
|
104
|
+
error: "Missing chunk at index " + i,
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
buffers.push(chunk);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
var assembled = Buffer.concat(buffers);
|
|
112
|
+
var isText = msg.attachmentType === "paste" || isTextMimeType(msg.mimeType);
|
|
113
|
+
var content = isText ? assembled.toString("utf-8") : assembled.toString("base64");
|
|
114
|
+
|
|
115
|
+
var attachment: Attachment = {
|
|
116
|
+
type: msg.attachmentType,
|
|
117
|
+
name: msg.name,
|
|
118
|
+
content,
|
|
119
|
+
mimeType: msg.mimeType,
|
|
120
|
+
size: msg.size,
|
|
121
|
+
lineCount: msg.lineCount,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
var completedStore = getClientCompleted(clientId);
|
|
125
|
+
completedStore.set(msg.attachmentId, attachment);
|
|
126
|
+
store.delete(msg.attachmentId);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
function isTextMimeType(mime: string): boolean {
|
|
132
|
+
if (mime.startsWith("text/")) return true;
|
|
133
|
+
var textTypes = [
|
|
134
|
+
"application/json",
|
|
135
|
+
"application/xml",
|
|
136
|
+
"application/javascript",
|
|
137
|
+
"application/typescript",
|
|
138
|
+
"application/x-yaml",
|
|
139
|
+
"application/yaml",
|
|
140
|
+
"image/svg+xml",
|
|
141
|
+
];
|
|
142
|
+
return textTypes.indexOf(mime) !== -1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function getAttachments(clientId: string, ids: string[]): Attachment[] {
|
|
146
|
+
var store = getClientCompleted(clientId);
|
|
147
|
+
var result: Attachment[] = [];
|
|
148
|
+
for (var i = 0; i < ids.length; i++) {
|
|
149
|
+
var att = store.get(ids[i]);
|
|
150
|
+
if (att) {
|
|
151
|
+
result.push(att);
|
|
152
|
+
store.delete(ids[i]);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function cleanupClient(clientId: string): void {
|
|
159
|
+
stores.delete(clientId);
|
|
160
|
+
completed.delete(clientId);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
setInterval(function () {
|
|
164
|
+
var now = Date.now();
|
|
165
|
+
stores.forEach(function (store) {
|
|
166
|
+
store.forEach(function (pending, id) {
|
|
167
|
+
if (now - pending.createdAt > TTL_MS) {
|
|
168
|
+
store.delete(id);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}, CLEANUP_INTERVAL_MS);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { ChatSendMessage, ChatPermissionResponseMessage, ChatSetPermissionModeMessage, ClientMessage } from "@lattice/shared";
|
|
1
|
+
import type { ChatSendMessage, ChatPermissionResponseMessage, ChatSetPermissionModeMessage, ChatPromptResponseMessage, ClientMessage } from "@lattice/shared";
|
|
2
2
|
import { registerHandler } from "../ws/router";
|
|
3
3
|
import { sendTo } from "../ws/broadcast";
|
|
4
4
|
import { getProjectBySlug } from "../project/registry";
|
|
5
5
|
import { loadConfig } from "../config";
|
|
6
6
|
import { startChatStream, getPendingPermission, deletePendingPermission, addAutoApprovedTool, setSessionPermissionOverride, getActiveStream, buildPermissionRule } from "../project/sdk-bridge";
|
|
7
|
+
import { getAttachments } from "./attachment";
|
|
7
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
9
|
import { join } from "node:path";
|
|
9
10
|
|
|
@@ -122,10 +123,15 @@ registerHandler("chat", function (clientId: string, message: ClientMessage) {
|
|
|
122
123
|
var config = loadConfig();
|
|
123
124
|
var env = Object.assign({}, config.globalEnv, project.env);
|
|
124
125
|
|
|
126
|
+
var attachments = sendMsg.attachmentIds
|
|
127
|
+
? getAttachments(clientId, sendMsg.attachmentIds)
|
|
128
|
+
: [];
|
|
129
|
+
|
|
125
130
|
startChatStream({
|
|
126
131
|
projectSlug: active.projectSlug,
|
|
127
132
|
sessionId: active.sessionId,
|
|
128
133
|
text: sendMsg.text,
|
|
134
|
+
attachments,
|
|
129
135
|
clientId,
|
|
130
136
|
cwd: project.path,
|
|
131
137
|
env: Object.keys(env).length > 0 ? env : undefined,
|
|
@@ -137,7 +143,17 @@ registerHandler("chat", function (clientId: string, message: ClientMessage) {
|
|
|
137
143
|
}
|
|
138
144
|
|
|
139
145
|
if (message.type === "chat:cancel") {
|
|
140
|
-
|
|
146
|
+
var active = activeSessionByClient.get(clientId);
|
|
147
|
+
if (!active) {
|
|
148
|
+
sendTo(clientId, { type: "chat:error", message: "No active session." });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
var stream = getActiveStream(active.sessionId);
|
|
152
|
+
if (!stream) {
|
|
153
|
+
sendTo(clientId, { type: "chat:error", message: "No active stream to cancel." });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
stream.interrupt().catch(function () {});
|
|
141
157
|
return;
|
|
142
158
|
}
|
|
143
159
|
|
|
@@ -176,6 +192,31 @@ registerHandler("chat", function (clientId: string, message: ClientMessage) {
|
|
|
176
192
|
return;
|
|
177
193
|
}
|
|
178
194
|
|
|
195
|
+
if (message.type === "chat:prompt_response") {
|
|
196
|
+
var promptRespMsg = message as ChatPromptResponseMessage;
|
|
197
|
+
var pendingPrompt = getPendingPermission(promptRespMsg.requestId);
|
|
198
|
+
if (!pendingPrompt || pendingPrompt.promptType !== "question") {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
var updatedInput = Object.assign({}, pendingPrompt.input, {
|
|
203
|
+
answers: promptRespMsg.answers,
|
|
204
|
+
});
|
|
205
|
+
if (promptRespMsg.annotations) {
|
|
206
|
+
(updatedInput as Record<string, unknown>).annotations = promptRespMsg.annotations;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
pendingPrompt.resolve({
|
|
210
|
+
behavior: "allow",
|
|
211
|
+
updatedInput: updatedInput,
|
|
212
|
+
toolUseID: pendingPrompt.toolUseID,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
sendTo(clientId, { type: "chat:prompt_resolved", requestId: promptRespMsg.requestId });
|
|
216
|
+
deletePendingPermission(promptRespMsg.requestId);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
179
220
|
if (message.type === "chat:set_permission_mode") {
|
|
180
221
|
var modeMsg = message as ChatSetPermissionModeMessage;
|
|
181
222
|
var activeSession = activeSessionByClient.get(clientId);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import type { ClientMessage, EditorDetectMessage } from "@lattice/shared";
|
|
3
|
+
import { registerHandler } from "../ws/router";
|
|
4
|
+
import { sendTo } from "../ws/broadcast";
|
|
5
|
+
|
|
6
|
+
var binaryNames: Record<string, string[]> = {
|
|
7
|
+
"vscode": ["code"],
|
|
8
|
+
"vscode-insiders": ["code-insiders"],
|
|
9
|
+
"cursor": ["cursor"],
|
|
10
|
+
"webstorm": ["webstorm", "webstorm.sh", "wstorm"],
|
|
11
|
+
"intellij": ["idea", "idea.sh"],
|
|
12
|
+
"pycharm": ["pycharm", "pycharm.sh", "charm"],
|
|
13
|
+
"goland": ["goland", "goland.sh"],
|
|
14
|
+
"notepad++": ["notepad++"],
|
|
15
|
+
"sublime": ["subl", "sublime_text"],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function detectEditorPath(editorType: string): string | null {
|
|
19
|
+
var names = binaryNames[editorType];
|
|
20
|
+
if (!names) return null;
|
|
21
|
+
|
|
22
|
+
for (var i = 0; i < names.length; i++) {
|
|
23
|
+
try {
|
|
24
|
+
var result = execSync("which " + names[i], { encoding: "utf-8", timeout: 3000 }).trim();
|
|
25
|
+
if (result) return result;
|
|
26
|
+
} catch {
|
|
27
|
+
// not found
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
registerHandler("editor", function (clientId: string, message: ClientMessage) {
|
|
34
|
+
if (message.type === "editor:detect") {
|
|
35
|
+
var detectMsg = message as EditorDetectMessage;
|
|
36
|
+
var detectedPath = detectEditorPath(detectMsg.editorType);
|
|
37
|
+
sendTo(clientId, { type: "editor:detect_result", editorType: detectMsg.editorType, path: detectedPath });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
});
|