I made a vscode extension that forwards frontend errors back to Composer

so i built this cursor extension for y’all frontend devs out there scratching your heads with composer and going back and forth for debugging

it’s a (very) small codebase and I open sourced it so you can tweak it acc to your needs too

also a small working demo here: https://x.com/saketsarin/status/1889056035874525216

have fun :smiley:

22 Likes

Seems useful. I’m kind of tired of copy pasting stuff from my console window into the chat. Beats me why the cursor team haven’t prioritized this. Would be nice if the agent could initiate taking a look at the console logs.

3 Likes

Yeah I’m planning to work on autonomous mode for this so the agent catches these automatically and fix them. But first I want to see if this is PMF haha

Is that even possible through an extension?
It seems like in order for an agent to reiterate its own, it has to open a terminal which it reads from. For example with unit tests, you ask it to run the tasks, and then it runs the command and reads the terminal for the output, and then go ahead and fixes it, and then run the tests again recursively. I’ve tried getting web logs into a terminal, but it seems like vscode doesn’t support it and you can’t start a debugging session from the terminal.

ive already done the hard part of getting the logs into composer. now i just need to perform submit actions until all console errors are fixed. i did a mock run for this but would need to spend a lot of time to actually make it fully autonomous

2 Likes

This is awesome, great work! What I need is something that automatically copies and submits console logs from my terminal back into the chat or composer when the ai model asks for them. Something that would watch for “could you share the log”, and then grabs the current log and shares it with the ai model. I’d be interested in helping to develop this extension if you’re already underway.

1 Like

Yeah this is the autonomous mode I have in the pipeline for next update. I have an approach for this in mind but I’m collecting feedback on improving the current version rn, and make it autonomous when it’s robust enough for working with manually haha.

It’d be really helpful if you could contribute to the repo any way you can!

Thanks for this!

I work on a full-stack app (BA Copilot - generate flowcharts with AI), and I agree that the browser info is the crucial missing piece of context that Composer agent needs.

Bug
Unfortunately, it didn’t work for me.
Can you let me know what I should do to fix it please?
When I click ‘capture tab’, it gives me the errors bottom left:

Long-term
I agree with your longer term plan to feed this into agent to iterate on things autonomously.

Short-term Suggestion - More management over what to send when
In the shorter term, I it would be useful to have the following buttons:

  1. Clear logs
  2. Send tab logs to Composer
  3. Send tab screenshot to Composer
  4. Send tab screenshot + logs to Composer

The reason I suggest clear logs is for context window management and to guide Composer
For example, I may execute a 7 screen flow, but my intuition is that the key info Composer needs is on screen 7.
I don’t want to send logs of all 7 screens into Composer, for it to look at logs that probably aren’t relevant.
In this case I would do the following:

  1. Execute steps up to until screen 6 is showing
  2. Clear logs
  3. Execute step to show screen 7
  4. Send tab logs + screenshot to composer

Short-term Suggestion - Repro steps
This is lower importance to me than the previous suggestion.
It may be useful being able to feed repro steps into composer.
This will reduce the context window compared to sending images to represent steps.
Here is an example from jam.dev

1 Like

thanks for the detailed response! glad to hear this seems useful to you :smiley:

Bug: The issue is related to the clipboard command and I have only tested this on Mac, and used stackoverflow for similar commands on windows and ubuntu. It’s a simple command so maybe you could help me out here and raise a PR for it’s fix on the repo?

Long-term: Glad to hear that!

Management: That’d actually be helpful. Will push this in a few hours :wink:

Repro steps: I didn’t understand what this actually does. Could you explain more please?

Would love to help out if I can, it’s an interesting project. What’s your discord?

1 Like

this is fantastic! thanks for sharing. I’d love to discuss how we could transition toward an autonomous mode, I’ll DM you on twitter

1 Like

sweet!

it’s - saketsarin

sure thing sur :smile:

hey! just pushed a new release with these -

lmk if there’s more feedback!

1 Like

Great concept!

Now that 1.0 is released, you should consider publishing on OpenVSX.org

That will help with discovery since most users just search the Extensions tab, and will also support auto-updates. :slightly_smiling_face:

1 Like

Oh thankyou sm for the suggestion. I wanted to publish it to vscode marketplace but people said it’s not good for cursor extensions. Even specstory has the direct download link to the *.vsix file on its website

Does Cursor support OpenVSX extensions? I’d love to publish there if it does

GO

Also I cant wait until we can YOLOLOOP this…

“give me a [thing] and iterate and pipe all your knowledge to {place}”

EDIT: btw if you havent told the bot “Give me [thing]”… its a good tip as a directive,

For whatever reason it responds better when you tell it what to give you…

1 Like

thanks for the suggestions man! i have the autonomous mode in pipeline and will launch that soon

till then you can try the current version and give any feedback/suggestions you’d like :smile:

ps: it’s up on vscode marketplace too now - Composer Web - Visual Studio Marketplace

1 Like

Hi I tested it from the market place, great work thank you!
I was working on something similar.

What I miss in your extension, are full fledged params from the logs, that are expanded. This definitely helped many times to debug a situation.

I also created a logger that was piping output into a file and this on each fresh requests. Find attached my logger that was actually doing the right job in that terms.

// disable all eslint rules for this file
/* eslint-disable */

const CDP = require("chrome-remote-interface");
const fs = require("fs");
const path = require("path");

interface LogEntry {
  timestamp: number;
  level: string;
  text: string;
}

interface ConsoleMessage {
  text: string;
  type?: string;
  args?: any[];
  stackTrace?: {
    url: string;
    lineNumber: number;
    columnNumber: number;
  }[];
}

interface CallFrame {
  url: string;
  functionName: string;
  lineNumber: number;
  columnNumber: number;
}

interface StackTrace {
  callFrames: CallFrame[];
}

interface ConsoleAPICall {
  type: string;
  args: Array<{
    type: string;
    value?: string;
    description?: string;
  }>;
  stackTrace?: StackTrace;
}

interface ExceptionDetails {
  exceptionId: number;
  text: string;
  lineNumber: number;
  columnNumber: number;
  scriptId: string;
  url: string;
  stackTrace: StackTrace;
  exception: {
    className: string;
    description: string;
  };
}

const getLogPath = (): string => {
  const providedPath = process.argv[2];
  if (!providedPath) return "./browserConsole.log";

  return path.isAbsolute(providedPath) ? providedPath : path.resolve(process.cwd(), providedPath);
};

const captureConsoleLogs = async ({
  outputPath = getLogPath(),
  clearOnRefresh = true,
}: {
  outputPath?: string;
  clearOnRefresh?: boolean;
} = {}) => {
  try {
    const client = await CDP();
    const { Log, Console, Runtime, Page } = client;

    let writeStream = fs.createWriteStream(outputPath, {
      flags: clearOnRefresh ? "w" : "a",
    });
    console.log(`Logging to: ${outputPath}${clearOnRefresh ? " (clearing file)" : ""}`);

    await Promise.all([Log.enable(), Console.enable(), Runtime.enable(), Page.enable()]);

    // Add handler for page refresh
    Page.frameNavigated(() => {
      if (clearOnRefresh) {
        // Close existing stream and create a new one
        writeStream.end();
        writeStream = fs.createWriteStream(outputPath, { flags: "w" });
        console.log(`Page refreshed - Clearing log file: ${outputPath}`);
      }
    });

    Log.entryAdded(({ entry }: { entry: LogEntry }) => {
      writeStream.write(`${entry.timestamp} ${entry.level}: ${entry.text}\n`);
    });

    const formatValue = (arg: any): string => {
      if (!arg) return String(arg);

      if (arg.type === "object" && arg.preview) {
        // Extract just the properties into a clean object
        const cleanObject = arg.preview.properties?.reduce((obj: any, prop: any) => {
          obj[prop.name] = prop.value;
          return obj;
        }, {});

        return JSON.stringify(cleanObject, null, 2);
      }

      return arg.value || arg.description || JSON.stringify(arg, null, 2);
    };

    // Only handle errors in Console.messageAdded
    Console.messageAdded(({ message }: { message: ConsoleMessage }) => {
      if (message.type === "error" && message.args?.[0]?.description) {
        writeStream.write(
          `${new Date().toISOString()} error: ${message.args[0].description}\n` +
            `${message.stackTrace?.map((frame) => `    at ${frame.url}:${frame.lineNumber}:${frame.columnNumber}`).join("\n") || ""}\n`
        );
      }
    });

    // Handle all console API calls here
    Runtime.consoleAPICalled(({ type, args, stackTrace }: ConsoleAPICall) => {
      const timestamp = new Date().toISOString();
      const location = stackTrace?.callFrames?.[0];
      const locationStr = location ? ` (${location.url}:${location.lineNumber}:${location.columnNumber})` : "";

      if (type === "error") {
        const errorMessage = formatValue(args?.[0]) || "Unknown error";
        writeStream.write(
          `${timestamp} error: ${errorMessage}\n` +
            `${stackTrace?.callFrames?.map((frame) => `    at ${frame.url}:${frame.lineNumber}:${frame.columnNumber}`).join("\n") || ""}\n`
        );
      } else {
        const formattedArgs = args?.map(formatValue).join(" ") || "No message";
        writeStream.write(`${timestamp} ${type}${locationStr}: ${formattedArgs}\n`);
      }
    });

    Runtime.exceptionThrown(({ exceptionDetails }: { exceptionDetails: ExceptionDetails }) => {
      const errorMessage = exceptionDetails.exception?.description || exceptionDetails.text;
      const stackTrace =
        exceptionDetails.stackTrace?.callFrames
          ?.map((frame) => `    at ${frame.url}:${frame.lineNumber}:${frame.columnNumber}`)
          .join("\n") || "";

      writeStream.write(`${new Date().toISOString()} error: ${errorMessage}\n` + `Stack trace:\n${stackTrace}\n`);
    });

    process.on("SIGINT", () => {
      writeStream.end();
      client.close();
      process.exit();
    });
  } catch (error) {
    console.error("Failed to connect to Chrome:", error);
    process.exit(1);
  }
};

// Usage:
captureConsoleLogs().catch(console.error);

{
  "name": "logspiper",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start:piper": "ts-node piper.ts",
    "start:browser": "\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\" --remote-debugging-port=9222"
  },
  "dependencies": {
    "@types/chrome-remote-interface": "^0.31.14",
    "@types/node": "^22.10.7",
    "chrome-remote-interface": "^0.33.2",
    "ts-node": "^10.9.2"
  }
}

1 Like

thankyou so much for the feedback and your logger code. really appreciate it

I’ll add this and push ASAP!