@cryptiklemur/lattice 1.19.0 → 1.20.1

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.
@@ -7,7 +7,8 @@ import {
7
7
  import { useProjects } from "../../hooks/useProjects";
8
8
  import { useSidebar } from "../../hooks/useSidebar";
9
9
  import { useEditorConfig } from "../../hooks/useEditorConfig";
10
- import { getEditorUrl } from "../../utils/editorUrl";
10
+ import { getEditorUrl, isJetBrainsEditor } from "../../utils/editorUrl";
11
+ import { useWebSocket } from "../../hooks/useWebSocket";
11
12
  import { openTab } from "../../stores/workspace";
12
13
  import { getSidebarStore } from "../../stores/sidebar";
13
14
  import { useState } from "react";
@@ -69,8 +70,27 @@ export function ProjectDropdown(props: ProjectDropdownProps) {
69
70
  }
70
71
  }
71
72
 
73
+ var ws = useWebSocket();
72
74
  var ideUrl = activeProject ? getEditorUrl(editorType, activeProject.path, ".", undefined, wslDistro, activeProject.ideProjectName) : null;
73
75
 
76
+ function handleOpenInIde() {
77
+ if (!activeProject || !ideUrl) return;
78
+ if (isJetBrainsEditor(editorType) && !activeProject.ideProjectName) {
79
+ var handler = function (msg: { type: string; projectSlug?: string; ideProjectName?: string }) {
80
+ if (msg.type !== "editor:ensure-project_result") return;
81
+ if (msg.projectSlug !== activeProject!.slug) return;
82
+ ws.unsubscribe("editor:ensure-project_result", handler as any);
83
+ var updatedUrl = getEditorUrl(editorType, activeProject!.path, ".", undefined, wslDistro, msg.ideProjectName);
84
+ if (updatedUrl) window.location.href = updatedUrl;
85
+ };
86
+ ws.subscribe("editor:ensure-project_result", handler as any);
87
+ ws.send({ type: "editor:ensure-project", projectSlug: activeProject.slug } as any);
88
+ } else {
89
+ window.location.href = ideUrl;
90
+ }
91
+ props.onClose();
92
+ }
93
+
74
94
  function handleOpenTerminal() {
75
95
  openTab("terminal");
76
96
  var state = getSidebarStore().state;
@@ -113,15 +133,14 @@ export function ProjectDropdown(props: ProjectDropdownProps) {
113
133
  Actions
114
134
  </div>
115
135
  {ideUrl && (
116
- <a
136
+ <button
117
137
  role="menuitem"
118
- href={ideUrl}
119
- onClick={function () { props.onClose(); }}
120
- className="w-full flex items-center gap-2.5 px-2.5 py-1.5 rounded-lg text-[12px] text-base-content/60 hover:text-base-content hover:bg-base-content/5 transition-colors"
138
+ onClick={handleOpenInIde}
139
+ className="w-full flex items-center gap-2.5 px-2.5 py-1.5 rounded-lg text-[12px] text-base-content/60 hover:text-base-content hover:bg-base-content/5 transition-colors cursor-pointer"
121
140
  >
122
141
  <ExternalLink size={13} className="flex-shrink-0 text-base-content/30" />
123
142
  Open in IDE
124
- </a>
143
+ </button>
125
144
  )}
126
145
  <button
127
146
  role="menuitem"
@@ -1,4 +1,4 @@
1
- var JETBRAINS_IDS: Record<string, string> = {
1
+ export var JETBRAINS_IDS: Record<string, string> = {
2
2
  webstorm: "web-storm",
3
3
  intellij: "idea",
4
4
  pycharm: "py-charm",
@@ -9,13 +9,11 @@ function toWindowsPath(linuxPath: string, wslDistro: string): string {
9
9
  return "\\\\" + "wsl.localhost\\" + wslDistro + linuxPath.replace(/\//g, "\\");
10
10
  }
11
11
 
12
- function buildJetBrainsUrl(ideId: string, relativePath: string, line?: number, projectName?: string, projectRoot?: string): string {
12
+ function buildJetBrainsUrl(ideId: string, relativePath: string, line?: number, projectName?: string): string {
13
13
  var url = "jetbrains://" + ideId + "/navigate/reference?";
14
14
  var params: string[] = [];
15
15
  if (projectName) {
16
16
  params.push("project=" + encodeURIComponent(projectName));
17
- } else if (projectRoot) {
18
- params.push("origin=" + encodeURIComponent(projectRoot));
19
17
  }
20
18
  if (relativePath) {
21
19
  params.push("path=" + encodeURIComponent(relativePath));
@@ -26,13 +24,8 @@ function buildJetBrainsUrl(ideId: string, relativePath: string, line?: number, p
26
24
  return url + params.join("&");
27
25
  }
28
26
 
29
- export interface EditorUrlOptions {
30
- editorType: string;
31
- projectPath: string;
32
- filePath: string;
33
- line?: number;
34
- wslDistro?: string;
35
- ideProjectName?: string;
27
+ export function isJetBrainsEditor(editorType: string): boolean {
28
+ return editorType in JETBRAINS_IDS;
36
29
  }
37
30
 
38
31
  export function getEditorUrl(editorType: string, projectPath: string, filePath: string, line?: number, wslDistro?: string, ideProjectName?: string): string | null {
@@ -42,8 +35,7 @@ export function getEditorUrl(editorType: string, projectPath: string, filePath:
42
35
  var jetbrainsId = JETBRAINS_IDS[editorType];
43
36
  if (jetbrainsId) {
44
37
  var jbRelativePath = filePath === "." ? "" : filePath;
45
- var jbProjectRoot = wslDistro ? toWindowsPath(projectPath, wslDistro) : projectPath;
46
- return buildJetBrainsUrl(jetbrainsId, jbRelativePath, line, ideProjectName || undefined, ideProjectName ? undefined : jbProjectRoot);
38
+ return buildJetBrainsUrl(jetbrainsId, jbRelativePath, line, ideProjectName || undefined);
47
39
  }
48
40
 
49
41
  if (editorType === "vscode" || editorType === "vscode-insiders" || editorType === "cursor") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.19.0",
3
+ "version": "1.20.1",
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>",
@@ -13,6 +13,7 @@ import { verifyPassphrase, generateSessionToken, addSession, isValidSession } fr
13
13
  import { ensureCerts } from "./tls";
14
14
  import type { ClientMessage, MeshMessage } from "@lattice/shared";
15
15
  import { log } from "./logger";
16
+ import { detectIdeProjectName } from "./handlers/settings";
16
17
  import "./handlers/session";
17
18
  import "./handlers/chat";
18
19
  import "./handlers/attachment";
@@ -381,7 +382,7 @@ export async function startDaemon(portOverride?: number | null): Promise<void> {
381
382
  broadcast({
382
383
  type: "projects:list",
383
384
  projects: currentConfig.projects.map(function (p: typeof currentConfig.projects[number]) {
384
- return { slug: p.slug, path: p.path, title: p.title, nodeId: currentIdentity.id, nodeName: currentConfig.name, isRemote: false };
385
+ return { slug: p.slug, path: p.path, title: p.title, nodeId: currentIdentity.id, nodeName: currentConfig.name, isRemote: false, ideProjectName: detectIdeProjectName(p.path) };
385
386
  }),
386
387
  });
387
388
  }, 10000);
@@ -1,7 +1,13 @@
1
1
  import { execSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
2
4
  import type { ClientMessage, EditorDetectMessage } from "@lattice/shared";
3
5
  import { registerHandler } from "../ws/router";
4
6
  import { sendTo } from "../ws/broadcast";
7
+ import { loadConfig } from "../config";
8
+ import { loadOrCreateIdentity } from "../identity";
9
+ import { broadcast } from "../ws/broadcast";
10
+ import { detectIdeProjectName } from "./settings";
5
11
 
6
12
  var binaryNames: Record<string, string[]> = {
7
13
  "vscode": ["code"],
@@ -30,6 +36,18 @@ function detectEditorPath(editorType: string): string | null {
30
36
  return null;
31
37
  }
32
38
 
39
+ function ensureIdeaProject(projectPath: string, projectTitle: string): string {
40
+ var ideaDir = join(projectPath, ".idea");
41
+ if (!existsSync(ideaDir)) {
42
+ mkdirSync(ideaDir, { recursive: true });
43
+ }
44
+ var nameFile = join(ideaDir, ".name");
45
+ if (!existsSync(nameFile)) {
46
+ writeFileSync(nameFile, projectTitle, "utf-8");
47
+ }
48
+ return projectTitle;
49
+ }
50
+
33
51
  registerHandler("editor", function (clientId: string, message: ClientMessage) {
34
52
  if (message.type === "editor:detect") {
35
53
  var detectMsg = message as EditorDetectMessage;
@@ -37,4 +55,22 @@ registerHandler("editor", function (clientId: string, message: ClientMessage) {
37
55
  sendTo(clientId, { type: "editor:detect_result", editorType: detectMsg.editorType, path: detectedPath });
38
56
  return;
39
57
  }
58
+
59
+ if (message.type === "editor:ensure-project") {
60
+ var ensureMsg = message as { type: string; projectSlug: string };
61
+ var config = loadConfig();
62
+ var project = config.projects.find(function (p: typeof config.projects[number]) { return p.slug === ensureMsg.projectSlug; });
63
+ if (project) {
64
+ var name = ensureIdeaProject(project.path, project.title);
65
+ sendTo(clientId, { type: "editor:ensure-project_result", projectSlug: ensureMsg.projectSlug, ideProjectName: name });
66
+ var identity = loadOrCreateIdentity();
67
+ broadcast({
68
+ type: "projects:list",
69
+ projects: config.projects.map(function (p: typeof config.projects[number]) {
70
+ return { slug: p.slug, path: p.path, title: p.title, nodeId: identity.id, nodeName: config.name, isRemote: false, ideProjectName: detectIdeProjectName(p.path) };
71
+ }),
72
+ });
73
+ }
74
+ return;
75
+ }
40
76
  });
@@ -11,7 +11,7 @@ import { readGlobalMcpServers, writeGlobalMcpServers, readGlobalSkills, readGlob
11
11
  import { sendBudgetStatus } from "./chat";
12
12
  import { buildNodesMessage } from "./mesh";
13
13
 
14
- function detectIdeProjectName(projectPath: string): string | undefined {
14
+ export function detectIdeProjectName(projectPath: string): string | undefined {
15
15
  try {
16
16
  var ideDir = join(projectPath, ".idea");
17
17
  if (!existsSync(ideDir)) return undefined;