Skip to content

Commit 1b0d2fa

Browse files
committed
fix
1 parent 939e06d commit 1b0d2fa

File tree

9 files changed

+123
-112
lines changed

9 files changed

+123
-112
lines changed

packages/cli/src/commands/add.ts

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -197,40 +197,40 @@ export const add = new Command()
197197

198198
agentIntegration = agent as AgentIntegration;
199199

200-
if (projectInfo.installedAgents.length > 0) {
201-
const installedNames = formatInstalledAgentNames(
202-
projectInfo.installedAgents,
203-
);
204-
205-
const { action } = await prompts({
206-
type: "select",
207-
name: "action",
208-
message: "How would you like to proceed?",
209-
choices: [
210-
{
211-
title: `Replace ${installedNames} with ${getAgentDisplayName(agentIntegration)}`,
212-
value: "replace",
213-
},
214-
{
215-
title: `Add ${getAgentDisplayName(agentIntegration)} alongside existing`,
216-
value: "add",
217-
},
218-
{ title: "Cancel", value: "cancel" },
219-
],
220-
});
200+
if (projectInfo.installedAgents.length > 0) {
201+
const installedNames = formatInstalledAgentNames(
202+
projectInfo.installedAgents,
203+
);
221204

222-
if (!action || action === "cancel") {
223-
logger.break();
224-
logger.log("Changes cancelled.");
225-
logger.break();
226-
process.exit(0);
227-
}
205+
const { action } = await prompts({
206+
type: "select",
207+
name: "action",
208+
message: "How would you like to proceed?",
209+
choices: [
210+
{
211+
title: `Replace ${installedNames} with ${getAgentDisplayName(agentIntegration)}`,
212+
value: "replace",
213+
},
214+
{
215+
title: `Add ${getAgentDisplayName(agentIntegration)} alongside existing`,
216+
value: "add",
217+
},
218+
{ title: "Cancel", value: "cancel" },
219+
],
220+
});
221+
222+
if (!action || action === "cancel") {
223+
logger.break();
224+
logger.log("Changes cancelled.");
225+
logger.break();
226+
process.exit(0);
227+
}
228228

229-
if (action === "replace") {
230-
agentsToRemove = [...projectInfo.installedAgents] as Agent[];
229+
if (action === "replace") {
230+
agentsToRemove = [...projectInfo.installedAgents] as Agent[];
231+
}
231232
}
232233
}
233-
}
234234
} else {
235235
logger.break();
236236
logger.error("Please specify an agent to connect.");

packages/cli/src/commands/init.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -465,9 +465,7 @@ export const init = new Command()
465465
}
466466

467467
const hasLayoutChanges =
468-
!result.noChanges &&
469-
result.originalContent &&
470-
result.newContent;
468+
!result.noChanges && result.originalContent && result.newContent;
471469
const hasPackageJsonChanges =
472470
packageJsonResult.success &&
473471
!packageJsonResult.noChanges &&

packages/cli/src/commands/remove.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ const VERSION = process.env.VERSION ?? "0.0.1";
2121
export const remove = new Command()
2222
.name("remove")
2323
.description("disconnect React Grab from your agent")
24-
.argument(
25-
"[agent]",
26-
`agent to disconnect (${AGENTS.join(", ")}, mcp)`,
27-
)
24+
.argument("[agent]", `agent to disconnect (${AGENTS.join(", ")}, mcp)`)
2825
.option("-y, --yes", "skip confirmation prompts", false)
2926
.option(
3027
"-c, --cwd <cwd>",

packages/cli/test/install-mcp.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,7 @@ describe("installTomlClient", () => {
231231
const content = fs.readFileSync(client.configPath, "utf8");
232232
expect(content).toContain("[mcp_servers.react-grab-mcp]");
233233
expect(content).toContain('command = "npx"');
234-
expect(content).toContain(
235-
'args = ["-y", "@react-grab/mcp", "--stdio"]',
236-
);
234+
expect(content).toContain('args = ["-y", "@react-grab/mcp", "--stdio"]');
237235
});
238236

239237
it("should append to an existing TOML file", () => {
@@ -262,7 +260,7 @@ describe("installTomlClient", () => {
262260
const content = fs.readFileSync(client.configPath, "utf8");
263261
expect(content).toContain('command = "npx"');
264262
expect(content).not.toContain('command = "old"');
265-
expect(content).toContain('[other]');
263+
expect(content).toContain("[other]");
266264
});
267265

268266
it("should create nested directories if needed", () => {

packages/mcp/src/client.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import type {
2-
init,
3-
ReactGrabAPI,
4-
Plugin,
5-
AgentContext,
6-
} from "react-grab/core";
1+
import type { init, ReactGrabAPI, Plugin, AgentContext } from "react-grab/core";
72
import { DEFAULT_MCP_PORT } from "./constants.js";
83

94
interface McpPluginOptions {
@@ -22,9 +17,7 @@ const sendContextToServer = async (
2217
}).catch(() => {});
2318
};
2419

25-
export const createMcpPlugin = (
26-
options: McpPluginOptions = {},
27-
): Plugin => {
20+
export const createMcpPlugin = (options: McpPluginOptions = {}): Plugin => {
2821
const port = options.port ?? DEFAULT_MCP_PORT;
2922
const contextUrl = `http://localhost:${port}/context`;
3023

packages/mcp/src/server.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ const sleep = (ms: number): Promise<void> =>
1616
new Promise((resolve) => setTimeout(resolve, ms));
1717

1818
const agentContextSchema = z.object({
19-
content: z.array(z.string()).describe("Array of context strings (HTML + component stack traces)"),
19+
content: z
20+
.array(z.string())
21+
.describe("Array of context strings (HTML + component stack traces)"),
2022
prompt: z.string().optional().describe("User prompt or instruction"),
2123
});
2224

@@ -97,8 +99,14 @@ const createHttpServer = (port: number): Server => {
9799
const url = new URL(request.url ?? "/", `http://localhost:${port}`);
98100

99101
response.setHeader("Access-Control-Allow-Origin", "*");
100-
response.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, OPTIONS");
101-
response.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
102+
response.setHeader(
103+
"Access-Control-Allow-Methods",
104+
"POST, GET, DELETE, OPTIONS",
105+
);
106+
response.setHeader(
107+
"Access-Control-Allow-Headers",
108+
"Content-Type, mcp-session-id",
109+
);
102110
response.setHeader("Access-Control-Expose-Headers", "mcp-session-id");
103111

104112
if (request.method === "OPTIONS") {
@@ -107,9 +115,9 @@ const createHttpServer = (port: number): Server => {
107115
}
108116

109117
if (url.pathname === "/health") {
110-
response.writeHead(200, { "Content-Type": "application/json" }).end(
111-
JSON.stringify({ status: "ok" }),
112-
);
118+
response
119+
.writeHead(200, { "Content-Type": "application/json" })
120+
.end(JSON.stringify({ status: "ok" }));
113121
return;
114122
}
115123

@@ -125,13 +133,13 @@ const createHttpServer = (port: number): Server => {
125133
context: agentContextSchema.parse(body),
126134
submittedAt: Date.now(),
127135
};
128-
response.writeHead(200, { "Content-Type": "application/json" }).end(
129-
JSON.stringify({ status: "ok" }),
130-
);
136+
response
137+
.writeHead(200, { "Content-Type": "application/json" })
138+
.end(JSON.stringify({ status: "ok" }));
131139
} catch {
132-
response.writeHead(400, { "Content-Type": "application/json" }).end(
133-
JSON.stringify({ error: "Invalid context payload" }),
134-
);
140+
response
141+
.writeHead(400, { "Content-Type": "application/json" })
142+
.end(JSON.stringify({ error: "Invalid context payload" }));
135143
}
136144
return;
137145
}
@@ -166,9 +174,13 @@ const createHttpServer = (port: number): Server => {
166174
return;
167175
}
168176

169-
response.writeHead(400, { "Content-Type": "application/json" }).end(
170-
JSON.stringify({ error: "No valid session. Send an initialize request first." }),
171-
);
177+
response
178+
.writeHead(400, { "Content-Type": "application/json" })
179+
.end(
180+
JSON.stringify({
181+
error: "No valid session. Send an initialize request first.",
182+
}),
183+
);
172184
return;
173185
}
174186

@@ -231,12 +243,15 @@ export const startMcpServer = async ({
231243
await mcpServer.server.connect(transport);
232244

233245
startHttpServer(port).then(
234-
() => console.error(`React Grab context server listening on port ${port}`),
246+
() =>
247+
console.error(`React Grab context server listening on port ${port}`),
235248
(error) => console.error(`Failed to start context server: ${error}`),
236249
);
237250
return;
238251
}
239252

240253
await startHttpServer(port);
241-
console.log(`React Grab MCP server listening on http://localhost:${port}/mcp`);
254+
console.log(
255+
`React Grab MCP server listening on http://localhost:${port}/mcp`,
256+
);
242257
};

packages/react-grab/e2e/fixtures.ts

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -903,37 +903,40 @@ const createReactGrabPageObject = (page: Page): ReactGrabPageObject => {
903903
}, ATTRIBUTE_NAME);
904904
};
905905

906-
const getSelectionLabelBounds = async (): Promise<SelectionLabelBounds | null> => {
907-
return page.evaluate((attrName) => {
908-
const host = document.querySelector(`[${attrName}]`);
909-
const shadowRoot = host?.shadowRoot;
910-
if (!shadowRoot) return null;
911-
const root = shadowRoot.querySelector(`[${attrName}]`);
912-
if (!root) return null;
906+
const getSelectionLabelBounds =
907+
async (): Promise<SelectionLabelBounds | null> => {
908+
return page.evaluate((attrName) => {
909+
const host = document.querySelector(`[${attrName}]`);
910+
const shadowRoot = host?.shadowRoot;
911+
if (!shadowRoot) return null;
912+
const root = shadowRoot.querySelector(`[${attrName}]`);
913+
if (!root) return null;
913914

914-
const label = root.querySelector<HTMLElement>(
915-
"[data-react-grab-selection-label]",
916-
);
917-
if (!label) return null;
915+
const label = root.querySelector<HTMLElement>(
916+
"[data-react-grab-selection-label]",
917+
);
918+
if (!label) return null;
918919

919-
const toRect = (rect: DOMRect) => ({
920-
x: rect.x,
921-
y: rect.y,
922-
width: rect.width,
923-
height: rect.height,
924-
});
920+
const toRect = (rect: DOMRect) => ({
921+
x: rect.x,
922+
y: rect.y,
923+
width: rect.width,
924+
height: rect.height,
925+
});
925926

926-
const arrowElement = label.querySelector<HTMLElement>(
927-
"[data-react-grab-arrow]",
928-
);
927+
const arrowElement = label.querySelector<HTMLElement>(
928+
"[data-react-grab-arrow]",
929+
);
929930

930-
return {
931-
label: toRect(label.getBoundingClientRect()),
932-
arrow: arrowElement ? toRect(arrowElement.getBoundingClientRect()) : null,
933-
viewport: { width: window.innerWidth, height: window.innerHeight },
934-
};
935-
}, ATTRIBUTE_NAME);
936-
};
931+
return {
932+
label: toRect(label.getBoundingClientRect()),
933+
arrow: arrowElement
934+
? toRect(arrowElement.getBoundingClientRect())
935+
: null,
936+
viewport: { width: window.innerWidth, height: window.innerHeight },
937+
};
938+
}, ATTRIBUTE_NAME);
939+
};
937940

938941
const isSelectionLabelVisible = async (): Promise<boolean> => {
939942
const info = await getSelectionLabelInfo();

packages/react-grab/src/constants.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ export const FADE_COMPLETE_BUFFER_MS = 150;
1111
export const DISMISS_ANIMATION_BUFFER_MS = 50;
1212
export const KEYDOWN_SPAM_TIMEOUT_MS = 200;
1313
export const BLUR_DEACTIVATION_THRESHOLD_MS = 500;
14-
export const INPUT_FOCUS_ACTIVATION_DELAY_MS = 150;
15-
export const INPUT_TEXT_SELECTION_ACTIVATION_DELAY_MS = 300;
16-
export const DEFAULT_KEY_HOLD_DURATION_MS = 75;
14+
export const INPUT_FOCUS_ACTIVATION_DELAY_MS = 200;
15+
export const INPUT_TEXT_SELECTION_ACTIVATION_DELAY_MS = 400;
16+
export const DEFAULT_KEY_HOLD_DURATION_MS = 100;
1717
export const MIN_HOLD_FOR_ACTIVATION_AFTER_COPY_MS = 200;
1818
export const RECENT_THRESHOLD_MS = 10_000;
1919
export const ACTION_CYCLE_IDLE_TRIGGER_MS = 600;

packages/react-grab/src/utils/copy-content.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,12 @@ interface LexicalNode {
2929
source?: string;
3030
}
3131

32-
const generateUuid = (): string => {
33-
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
34-
const random = (Math.random() * 16) | 0;
35-
const value = char === "x" ? random : (random & 0x3) | 0x8;
36-
return value.toString(16);
32+
const generateUuid = (): string =>
33+
"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (character) => {
34+
const randomNibble = (Math.random() * 16) | 0;
35+
const hexValue = character === "x" ? randomNibble : (randomNibble & 0x3) | 0x8;
36+
return hexValue.toString(16);
3737
});
38-
};
3938

4039
const createMentionNode = (
4140
displayName: string,
@@ -67,13 +66,26 @@ const createTextNode = (text: string): LexicalNode => ({
6766
version: 1,
6867
});
6968

69+
const escapeHtml = (text: string): string =>
70+
text
71+
.replace(/&/g, "&amp;")
72+
.replace(/</g, "&lt;")
73+
.replace(/>/g, "&gt;")
74+
.replace(/"/g, "&quot;");
75+
76+
interface ClipboardData {
77+
plainText: string;
78+
htmlContent: string;
79+
lexicalData: string;
80+
}
81+
7082
// HACK: Cursor's Lexical editor only reads content from registered commands/files,
7183
// not from embedded clipboard data. We include the content after the mention chip
7284
// so Cursor can actually read it.
73-
const createLexicalClipboardData = (
85+
const createClipboardData = (
7486
content: string,
7587
elementName: string,
76-
): { plainText: string; htmlContent: string; lexicalData: string } => {
88+
): ClipboardData => {
7789
const mentionKey = String(Math.floor(Math.random() * 10000));
7890
const namespaceUuid = generateUuid();
7991
const displayName = `<${elementName}>`;
@@ -100,14 +112,9 @@ const createLexicalClipboardData = (
100112
selectedOption,
101113
};
102114

103-
const escapedMentionMetadata = JSON.stringify(mentionMetadata).replace(
104-
/"/g,
105-
"&quot;",
106-
);
107-
108115
return {
109116
plainText: `@${displayName}\n\n${content}\n`,
110-
htmlContent: `<meta charset='utf-8'><span data-mention-key="${mentionKey}" data-lexical-mention="true" data-mention-name="${displayName}" data-typeahead-type="[object Object]" data-mention-metadata="${escapedMentionMetadata}">@${displayName}</span><pre><code>${content}</code></pre>`,
117+
htmlContent: `<meta charset='utf-8'><pre><code>${escapeHtml(content)}</code></pre>`,
111118
lexicalData: JSON.stringify({
112119
namespace: `chat-input${namespaceUuid}-pane`,
113120
nodes: [
@@ -128,7 +135,7 @@ export const copyContent = (
128135
options?: CopyContentOptions,
129136
): boolean => {
130137
const elementName = options?.name ?? "div";
131-
const { plainText, htmlContent, lexicalData } = createLexicalClipboardData(
138+
const { plainText, htmlContent, lexicalData } = createClipboardData(
132139
content,
133140
elementName,
134141
);

0 commit comments

Comments
 (0)