@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.
Files changed (92) hide show
  1. package/bun.lock +705 -2
  2. package/client/index.html +1 -13
  3. package/client/package.json +6 -1
  4. package/client/src/App.tsx +2 -0
  5. package/client/src/commands.ts +36 -0
  6. package/client/src/components/chat/AttachmentChips.tsx +116 -0
  7. package/client/src/components/chat/ChatInput.tsx +250 -73
  8. package/client/src/components/chat/ChatView.tsx +242 -10
  9. package/client/src/components/chat/CommandPalette.tsx +162 -0
  10. package/client/src/components/chat/Message.tsx +23 -2
  11. package/client/src/components/chat/PromptQuestion.tsx +271 -0
  12. package/client/src/components/chat/TodoCard.tsx +57 -0
  13. package/client/src/components/chat/ToolResultRenderer.tsx +2 -1
  14. package/client/src/components/chat/VoiceRecorder.tsx +85 -0
  15. package/client/src/components/project-settings/ProjectMemory.tsx +12 -2
  16. package/client/src/components/project-settings/ProjectNotifications.tsx +48 -0
  17. package/client/src/components/project-settings/ProjectRules.tsx +10 -1
  18. package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
  19. package/client/src/components/settings/Appearance.tsx +1 -0
  20. package/client/src/components/settings/ClaudeSettings.tsx +10 -0
  21. package/client/src/components/settings/Editor.tsx +123 -0
  22. package/client/src/components/settings/GlobalMcp.tsx +10 -1
  23. package/client/src/components/settings/GlobalMemory.tsx +19 -0
  24. package/client/src/components/settings/GlobalRules.tsx +149 -0
  25. package/client/src/components/settings/GlobalSkills.tsx +10 -0
  26. package/client/src/components/settings/Notifications.tsx +88 -0
  27. package/client/src/components/settings/SettingsView.tsx +12 -0
  28. package/client/src/components/settings/skill-shared.tsx +2 -1
  29. package/client/src/components/setup/SetupWizard.tsx +1 -1
  30. package/client/src/components/sidebar/NodeSettingsModal.tsx +23 -1
  31. package/client/src/components/sidebar/ProjectDropdown.tsx +176 -27
  32. package/client/src/components/sidebar/SettingsSidebar.tsx +11 -1
  33. package/client/src/components/sidebar/Sidebar.tsx +35 -2
  34. package/client/src/components/sidebar/UserIsland.tsx +18 -7
  35. package/client/src/components/ui/UpdatePrompt.tsx +47 -0
  36. package/client/src/components/workspace/FileBrowser.tsx +174 -0
  37. package/client/src/components/workspace/FileTree.tsx +129 -0
  38. package/client/src/components/workspace/FileViewer.tsx +211 -0
  39. package/client/src/components/workspace/NoteCard.tsx +119 -0
  40. package/client/src/components/workspace/NotesView.tsx +102 -0
  41. package/client/src/components/workspace/ScheduledTasksView.tsx +117 -0
  42. package/client/src/components/workspace/SplitPane.tsx +81 -0
  43. package/client/src/components/workspace/TabBar.tsx +185 -0
  44. package/client/src/components/workspace/TaskCard.tsx +158 -0
  45. package/client/src/components/workspace/TaskEditModal.tsx +114 -0
  46. package/client/src/components/{panels/Terminal.tsx → workspace/TerminalInstance.tsx} +50 -7
  47. package/client/src/components/workspace/TerminalView.tsx +110 -0
  48. package/client/src/components/workspace/WorkspaceView.tsx +116 -0
  49. package/client/src/hooks/useAttachments.ts +280 -0
  50. package/client/src/hooks/useEditorConfig.ts +28 -0
  51. package/client/src/hooks/useIdleDetection.ts +44 -0
  52. package/client/src/hooks/useInstallPrompt.ts +53 -0
  53. package/client/src/hooks/useNotifications.ts +54 -0
  54. package/client/src/hooks/useOnline.ts +6 -0
  55. package/client/src/hooks/useSession.ts +110 -4
  56. package/client/src/hooks/useSpinnerVerb.ts +36 -0
  57. package/client/src/hooks/useSwipeDrawer.ts +275 -0
  58. package/client/src/hooks/useVoiceRecorder.ts +123 -0
  59. package/client/src/hooks/useWorkspace.ts +48 -0
  60. package/client/src/providers/WebSocketProvider.tsx +18 -0
  61. package/client/src/router.tsx +48 -20
  62. package/client/src/stores/session.ts +136 -0
  63. package/client/src/stores/sidebar.ts +3 -2
  64. package/client/src/stores/workspace.ts +254 -0
  65. package/client/src/styles/global.css +123 -0
  66. package/client/src/utils/editorUrl.ts +62 -0
  67. package/client/vite.config.ts +53 -1
  68. package/package.json +1 -1
  69. package/server/src/daemon.ts +11 -1
  70. package/server/src/features/scheduler.ts +23 -0
  71. package/server/src/features/sticky-notes.ts +5 -3
  72. package/server/src/handlers/attachment.ts +172 -0
  73. package/server/src/handlers/chat.ts +43 -2
  74. package/server/src/handlers/editor.ts +40 -0
  75. package/server/src/handlers/fs.ts +10 -2
  76. package/server/src/handlers/memory.ts +3 -0
  77. package/server/src/handlers/notes.ts +4 -2
  78. package/server/src/handlers/scheduler.ts +18 -1
  79. package/server/src/handlers/session.ts +14 -8
  80. package/server/src/handlers/settings.ts +37 -2
  81. package/server/src/handlers/terminal.ts +13 -6
  82. package/server/src/project/pty-worker.cjs +83 -0
  83. package/server/src/project/sdk-bridge.ts +266 -11
  84. package/server/src/project/terminal.ts +78 -34
  85. package/shared/src/messages.ts +145 -4
  86. package/shared/src/models.ts +27 -1
  87. package/shared/src/project-settings.ts +1 -1
  88. package/tp.js +19 -0
  89. package/client/public/manifest.json +0 -24
  90. package/client/public/sw.js +0 -61
  91. package/client/src/components/panels/FileBrowser.tsx +0 -241
  92. package/client/src/components/panels/StickyNotes.tsx +0 -187
@@ -21,7 +21,11 @@ export function clearActiveProject(clientId: string): void {
21
21
  registerHandler("fs", function (clientId: string, message: ClientMessage) {
22
22
  if (message.type === "fs:list") {
23
23
  var listMsg = message as FsListMessage;
24
- var projectSlug = activeProjectByClient.get(clientId);
24
+ var projectSlug = activeProjectByClient.get(clientId) || listMsg.projectSlug;
25
+ if (listMsg.projectSlug) {
26
+ setActiveProject(clientId, listMsg.projectSlug);
27
+ projectSlug = listMsg.projectSlug;
28
+ }
25
29
  if (!projectSlug) {
26
30
  sendTo(clientId, { type: "chat:error", message: "No active project for fs:list" });
27
31
  return;
@@ -40,7 +44,11 @@ registerHandler("fs", function (clientId: string, message: ClientMessage) {
40
44
 
41
45
  if (message.type === "fs:read") {
42
46
  var readMsg = message as FsReadMessage;
43
- var projectSlugRead = activeProjectByClient.get(clientId);
47
+ var projectSlugRead = activeProjectByClient.get(clientId) || readMsg.projectSlug;
48
+ if (readMsg.projectSlug) {
49
+ setActiveProject(clientId, readMsg.projectSlug);
50
+ projectSlugRead = readMsg.projectSlug;
51
+ }
44
52
  if (!projectSlugRead) {
45
53
  sendTo(clientId, { type: "chat:error", message: "No active project for fs:read" });
46
54
  return;
@@ -7,6 +7,9 @@ import { sendTo } from "../ws/broadcast";
7
7
  import { loadConfig } from "../config";
8
8
 
9
9
  function getMemoryDir(projectSlug: string): string | null {
10
+ if (projectSlug === "__global__") {
11
+ return join(homedir(), ".claude", "memory");
12
+ }
10
13
  var config = loadConfig();
11
14
  var project = config.projects.find(function (p) { return p.slug === projectSlug; });
12
15
  if (!project) return null;
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  ClientMessage,
3
+ NotesListMessage,
3
4
  NotesCreateMessage,
4
5
  NotesUpdateMessage,
5
6
  NotesDeleteMessage,
@@ -10,13 +11,14 @@ import { listNotes, createNote, updateNote, deleteNote } from "../features/stick
10
11
 
11
12
  registerHandler("notes", function (clientId: string, message: ClientMessage) {
12
13
  if (message.type === "notes:list") {
13
- sendTo(clientId, { type: "notes:list_result", notes: listNotes() });
14
+ var listMsg = message as NotesListMessage;
15
+ sendTo(clientId, { type: "notes:list_result", notes: listNotes(listMsg.projectSlug) });
14
16
  return;
15
17
  }
16
18
 
17
19
  if (message.type === "notes:create") {
18
20
  var createMsg = message as NotesCreateMessage;
19
- var note = createNote(createMsg.content);
21
+ var note = createNote(createMsg.content, createMsg.projectSlug);
20
22
  broadcast({ type: "notes:created", note });
21
23
  return;
22
24
  }
@@ -3,10 +3,11 @@ import type {
3
3
  SchedulerCreateMessage,
4
4
  SchedulerDeleteMessage,
5
5
  SchedulerToggleMessage,
6
+ SchedulerUpdateMessage,
6
7
  } from "@lattice/shared";
7
8
  import { registerHandler } from "../ws/router";
8
9
  import { sendTo } from "../ws/broadcast";
9
- import { listTasks, createTask, deleteTask, toggleTask } from "../features/scheduler";
10
+ import { listTasks, createTask, deleteTask, toggleTask, updateTask } from "../features/scheduler";
10
11
 
11
12
  registerHandler("scheduler", function (clientId: string, message: ClientMessage) {
12
13
  if (message.type === "scheduler:list") {
@@ -44,4 +45,20 @@ registerHandler("scheduler", function (clientId: string, message: ClientMessage)
44
45
  sendTo(clientId, { type: "scheduler:tasks", tasks: listTasks() });
45
46
  return;
46
47
  }
48
+
49
+ if (message.type === "scheduler:update") {
50
+ var updateMsg = message as SchedulerUpdateMessage;
51
+ var updated = updateTask(updateMsg.taskId, {
52
+ name: updateMsg.name,
53
+ prompt: updateMsg.prompt,
54
+ cron: updateMsg.cron,
55
+ });
56
+ if (!updated) {
57
+ sendTo(clientId, { type: "chat:error", message: "Failed to update task" });
58
+ return;
59
+ }
60
+ sendTo(clientId, { type: "scheduler:task_updated", task: updated });
61
+ sendTo(clientId, { type: "scheduler:tasks", tasks: listTasks() });
62
+ return;
63
+ }
47
64
  });
@@ -22,7 +22,7 @@ import {
22
22
  import { getContextBreakdown } from "../project/context-breakdown";
23
23
  import { setActiveSession } from "./chat";
24
24
  import { setActiveProject } from "./fs";
25
- import { wasSessionInterrupted, clearInterruptedFlag } from "../project/sdk-bridge";
25
+ import { wasSessionInterrupted, clearInterruptedFlag, isSessionBusy, watchSessionLock, stopExternalSession } from "../project/sdk-bridge";
26
26
 
27
27
  registerHandler("session", function (clientId: string, message: ClientMessage) {
28
28
  if (message.type === "session:list_request") {
@@ -62,13 +62,6 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
62
62
  var createMsg = message as SessionCreateMessage;
63
63
  var session = createSession(createMsg.projectSlug);
64
64
  sendTo(clientId, { type: "session:created", session });
65
- void listSessions(createMsg.projectSlug).then(function (sessions) {
66
- sendTo(clientId, {
67
- type: "session:list",
68
- projectSlug: createMsg.projectSlug,
69
- sessions,
70
- });
71
- });
72
65
  return;
73
66
  }
74
67
 
@@ -76,6 +69,7 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
76
69
  var activateMsg = message as SessionActivateMessage;
77
70
  setActiveSession(clientId, activateMsg.projectSlug, activateMsg.sessionId);
78
71
  setActiveProject(clientId, activateMsg.projectSlug);
72
+ watchSessionLock(activateMsg.sessionId);
79
73
  void Promise.all([
80
74
  loadSessionHistory(activateMsg.projectSlug, activateMsg.sessionId),
81
75
  getSessionTitle(activateMsg.projectSlug, activateMsg.sessionId),
@@ -86,6 +80,7 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
86
80
  if (interrupted) {
87
81
  clearInterruptedFlag(activateMsg.sessionId);
88
82
  }
83
+ var busy = isSessionBusy(activateMsg.sessionId);
89
84
  sendTo(clientId, {
90
85
  type: "session:history",
91
86
  projectSlug: activateMsg.projectSlug,
@@ -93,6 +88,7 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
93
88
  messages: results[0],
94
89
  title: results[1],
95
90
  interrupted: interrupted || undefined,
91
+ busy: busy || undefined,
96
92
  });
97
93
  var usage = results[2];
98
94
  if (usage) {
@@ -156,4 +152,14 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
156
152
  });
157
153
  });
158
154
  }
155
+
156
+ if (message.type === "session:stop_external") {
157
+ var stopMsg = message as { type: string; sessionId: string };
158
+ var stopped = stopExternalSession(stopMsg.sessionId);
159
+ if (stopped) {
160
+ console.log("[lattice] Sent SIGINT to external CLI process for session " + stopMsg.sessionId);
161
+ } else {
162
+ sendTo(clientId, { type: "chat:error", message: "No external process found for this session." });
163
+ }
164
+ }
159
165
  });
@@ -7,7 +7,21 @@ import type { LatticeConfig } from "@lattice/shared";
7
7
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { homedir } from "node:os";
10
- import { readGlobalMcpServers, writeGlobalMcpServers, readGlobalSkills } from "../project/project-files";
10
+ import { readGlobalMcpServers, writeGlobalMcpServers, readGlobalSkills, readGlobalRules } from "../project/project-files";
11
+
12
+ function detectIdeProjectName(projectPath: string): string | undefined {
13
+ try {
14
+ var ideDir = join(projectPath, ".idea");
15
+ if (!existsSync(ideDir)) return undefined;
16
+ var ideNameFile = join(ideDir, ".name");
17
+ if (existsSync(ideNameFile)) {
18
+ var name = readFileSync(ideNameFile, "utf-8").trim();
19
+ if (name) return name;
20
+ }
21
+ return projectPath.split("/").pop() || undefined;
22
+ } catch {}
23
+ return undefined;
24
+ }
11
25
  import { loadOrCreateIdentity } from "../identity";
12
26
 
13
27
  function loadGlobalClaudeMd(): string {
@@ -28,6 +42,21 @@ function saveGlobalClaudeMd(content: string): void {
28
42
  writeFileSync(join(claudeDir, "CLAUDE.md"), content, "utf-8");
29
43
  }
30
44
 
45
+ function loadSpinnerVerbs(): string[] {
46
+ var claudeSettingsPath = join(homedir(), ".claude", "settings.json");
47
+ var defaultVerbs = ["Thinking", "Analyzing", "Processing", "Computing", "Evaluating", "Considering", "Examining", "Reviewing"];
48
+ try {
49
+ var claudeSettings = JSON.parse(readFileSync(claudeSettingsPath, "utf-8"));
50
+ if (claudeSettings.spinnerVerbs) {
51
+ if (claudeSettings.spinnerVerbs.mode === "replace") {
52
+ return claudeSettings.spinnerVerbs.verbs || [];
53
+ }
54
+ return [...defaultVerbs, ...(claudeSettings.spinnerVerbs.verbs || [])];
55
+ }
56
+ } catch {}
57
+ return defaultVerbs;
58
+ }
59
+
31
60
  registerHandler("settings", function (clientId: string, message: ClientMessage) {
32
61
  if (message.type === "settings:get") {
33
62
  var config = loadConfig();
@@ -38,11 +67,14 @@ registerHandler("settings", function (clientId: string, message: ClientMessage)
38
67
  config: configWithClaudeMd,
39
68
  mcpServers: readGlobalMcpServers() as Record<string, import("@lattice/shared").McpServerConfig>,
40
69
  globalSkills: readGlobalSkills(),
70
+ globalRules: readGlobalRules(),
71
+ spinnerVerbs: loadSpinnerVerbs(),
72
+ wslDistro: process.env.WSL_DISTRO_NAME || undefined,
41
73
  });
42
74
  sendTo(clientId, {
43
75
  type: "projects:list",
44
76
  projects: config.projects.map(function (p) {
45
- return { slug: p.slug, path: p.path, title: p.title, nodeId: identity.id, nodeName: config.name, isRemote: false };
77
+ return { slug: p.slug, path: p.path, title: p.title, nodeId: identity.id, nodeName: config.name, isRemote: false, ideProjectName: detectIdeProjectName(p.path) };
46
78
  }),
47
79
  });
48
80
  return;
@@ -93,6 +125,9 @@ registerHandler("settings", function (clientId: string, message: ClientMessage)
93
125
  config: updatedWithClaudeMd,
94
126
  mcpServers: readGlobalMcpServers() as Record<string, import("@lattice/shared").McpServerConfig>,
95
127
  globalSkills: readGlobalSkills(),
128
+ globalRules: readGlobalRules(),
129
+ spinnerVerbs: loadSpinnerVerbs(),
130
+ wslDistro: process.env.WSL_DISTRO_NAME || undefined,
96
131
  });
97
132
  var updatedIdentity = loadOrCreateIdentity();
98
133
  broadcast({
@@ -29,14 +29,21 @@ export function cleanupClientTerminals(clientId: string): void {
29
29
 
30
30
  registerHandler("terminal", function(clientId: string, message: ClientMessage) {
31
31
  if (message.type === "terminal:create") {
32
- var _msg = message as TerminalCreateMessage;
32
+ var createMsg = message as TerminalCreateMessage;
33
33
  var cwd = homedir();
34
34
 
35
- var active = getActiveSession(clientId);
36
- if (active) {
37
- var project = getProjectBySlug(active.projectSlug);
38
- if (project) {
39
- cwd = project.path;
35
+ if (createMsg.projectSlug) {
36
+ var slugProject = getProjectBySlug(createMsg.projectSlug);
37
+ if (slugProject) {
38
+ cwd = slugProject.path;
39
+ }
40
+ } else {
41
+ var active = getActiveSession(clientId);
42
+ if (active) {
43
+ var project = getProjectBySlug(active.projectSlug);
44
+ if (project) {
45
+ cwd = project.path;
46
+ }
40
47
  }
41
48
  }
42
49
 
@@ -0,0 +1,83 @@
1
+ // Worker that runs node-pty in a subprocess, communicating via stdin/stdout JSON messages.
2
+ // Running node-pty in a separate process avoids SIGHUP issues with Bun's --watch mode.
3
+ var pty = require("node-pty");
4
+
5
+ var term = null;
6
+
7
+ process.stdin.setEncoding("utf-8");
8
+ var buffer = "";
9
+
10
+ process.stdin.on("data", function (chunk) {
11
+ buffer += chunk;
12
+ var lines = buffer.split("\n");
13
+ buffer = lines.pop();
14
+ for (var i = 0; i < lines.length; i++) {
15
+ if (!lines[i].trim()) continue;
16
+ try {
17
+ handleMessage(JSON.parse(lines[i]));
18
+ } catch (e) {
19
+ // ignore parse errors
20
+ }
21
+ }
22
+ });
23
+
24
+ function send(msg) {
25
+ process.stdout.write(JSON.stringify(msg) + "\n");
26
+ }
27
+
28
+ function handleMessage(msg) {
29
+ if (msg.type === "create") {
30
+ var shell = process.env.SHELL || "bash";
31
+ term = pty.spawn(shell, [], {
32
+ name: "xterm-256color",
33
+ cols: msg.cols || 80,
34
+ rows: msg.rows || 24,
35
+ cwd: msg.cwd || process.env.HOME,
36
+ env: process.env,
37
+ });
38
+
39
+ term.onData(function (data) {
40
+ send({ type: "data", data: data });
41
+ });
42
+
43
+ term.onExit(function (e) {
44
+ send({ type: "exit", code: e.exitCode || 0 });
45
+ process.exit(0);
46
+ });
47
+
48
+ send({ type: "ready", pid: term.pid });
49
+ }
50
+
51
+ if (msg.type === "input" && term) {
52
+ term.write(msg.data);
53
+ }
54
+
55
+ if (msg.type === "resize" && term) {
56
+ try {
57
+ term.resize(msg.cols, msg.rows);
58
+ } catch (e) {
59
+ // ignore resize errors
60
+ }
61
+ }
62
+
63
+ if (msg.type === "kill") {
64
+ if (term) {
65
+ try { term.kill(); } catch (e) { /* already dead */ }
66
+ }
67
+ process.exit(0);
68
+ }
69
+ }
70
+
71
+ process.on("SIGTERM", function () {
72
+ if (term) {
73
+ try { term.kill(); } catch (e) { /* already dead */ }
74
+ }
75
+ process.exit(0);
76
+ });
77
+
78
+ process.on("SIGHUP", function () {
79
+ if (term) {
80
+ try { term.kill(); } catch (e) { /* already dead */ }
81
+ }
82
+ process.exit(0);
83
+ });