Skip to content

fix: support flat .txt transcripts on Windows (Cursor >= 0.47)#1

Open
bertheto wants to merge 2 commits intoofershap:mainfrom
bertheto:fix/windows-flat-txt-transcripts
Open

fix: support flat .txt transcripts on Windows (Cursor >= 0.47)#1
bertheto wants to merge 2 commits intoofershap:mainfrom
bertheto:fix/windows-flat-txt-transcripts

Conversation

@bertheto
Copy link

@bertheto bertheto commented Mar 2, 2026

Problem

On Windows with recent versions of Cursor (>= 0.47), agent transcripts are stored as plain-text .txt files placed directly inside agent-transcripts/:

~/.cursor/projects/<project>/agent-transcripts/
  <uuid>.txt   <- flat file, plain text

The existing scanAll() only handles the <uuid>/<uuid>.jsonl sub-directory format used on Linux/Mac, so the watcher finds the transcripts directory correctly but silently skips all .txt files — the office character never reacts to agent activity.

Reproduction

  1. Windows 10/11 with Cursor >= 0.47
  2. Install the extension — the office renders fine
  3. Run an agent — the character stays idle regardless of activity
  4. Check Output > Cursor Office: [start] Watching: ...agent-transcripts is logged but no [scan] or [activity] entries follow

Solution

cursorWatcher.ts

scanAll() now handles both formats:

  • Format A (existing, Linux/Mac): <uuid>/ sub-directory containing <uuid>.jsonl
  • Format B (new, Windows): flat <uuid>.txt file at the root of agent-transcripts/

watchFile() and readNewContent() receive an isFlatTxt flag to route each file to the correct parser.

transcriptParser.ts

New parseFlatTxtChunk(chunk) function parses the plain-text block format:

user:
<user_query>...</user_query>

assistant:
[Thinking] ...
[Tool call] Read
  path: src/app/...
[Tool result] ...

Tool names are mapped directly to activity types:

  • Read, Glob, Grep, SemanticSearch -> reading
  • Shell -> running
  • StrReplace, Write, EditNotebook, Delete -> editing
  • Task -> phoning (subagent delegation)
  • Other tool calls -> typing

[Thinking] blocks and plain assistant text fall back to the existing inferActivityFromText() heuristics.

Testing

Tested on Windows 10 (22H2) with Cursor 0.47. After reload, the office character correctly reacts to agent tool calls in real time.

On Windows with recent versions of Cursor, agent transcripts are stored
as plain-text .txt files directly inside agent-transcripts/ rather than
the <uuid>/<uuid>.jsonl sub-directory structure used on Linux/Mac.

Changes:
- cursorWatcher.ts: scanAll() now detects both formats:
  - Format A (existing): sub-directory with <uuid>/<uuid>.jsonl
  - Format B (new): flat <uuid>.txt file at root of agent-transcripts/
  watchFile() and readNewContent() accept an isFlatTxt flag so each
  format uses the appropriate parser.

- transcriptParser.ts: adds parseFlatTxtChunk(chunk) which parses the
  plain-text block format (user: / assistant: / [Tool call] / [Thinking]
  sections) and maps tool names to activity types (Read->reading,
  Shell->running, StrReplace->editing, Task->phoning).
@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a compatibility issue where the agent activity watcher failed to process transcripts generated by recent versions of Cursor on Windows. The changes introduce support for a new flat .txt transcript format, ensuring that the system can now correctly monitor and react to agent activities across different operating systems and Cursor versions.

Highlights

  • Windows Transcript Support: The system now correctly processes agent transcripts generated by Cursor on Windows (version >= 0.47), which are stored as flat .txt files, in addition to the existing JSONL format.
  • Dual Format Handling: The cursorWatcher.ts file has been updated to detect and handle both the sub-directory based JSONL format (Linux/Mac) and the new flat .txt file format (Windows) for agent transcripts.
  • New Transcript Parser: A new function, parseFlatTxtChunk, was introduced in transcriptParser.ts to specifically parse the plain-text block format of Windows transcripts, extracting agent activities based on tool calls and assistant messages.
  • Activity Mapping: Specific tool calls within the flat .txt transcripts are now mapped to distinct agent activities such as 'reading', 'running', 'editing', and 'phoning', providing more accurate status updates.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/cursorWatcher.ts
    • Imported parseFlatTxtChunk for new transcript parsing logic.
    • Modified scanAll to identify and process both existing JSONL transcript directories and new flat TXT transcript files.
    • Updated watchFile and readNewContent methods to accept an isFlatTxt flag, enabling conditional parsing based on file type.
    • Adjusted readNewContent to route content to either parseFlatTxtChunk or parseTranscriptLine based on the isFlatTxt flag.
  • src/transcriptParser.ts
    • Added parseFlatTxtChunk function to parse multi-line plain-text transcript blocks.
    • Implemented logic within parseFlatTxtChunk to differentiate between 'user' and 'assistant' roles.
    • Introduced regex-based detection for [Tool call] lines and mapped specific tool names (e.g., 'Read', 'Shell', 'StrReplace', 'Task') to corresponding ParsedStatus activities.
    • Included fallback logic to inferActivityFromText for [Thinking] blocks and general assistant text within the flat TXT format.
Activity
  • The author, bertheto, tested the changes on Windows 10 (22H2) with Cursor 0.47 and confirmed that the office character correctly reacts to agent tool calls in real time after the update.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for flat .txt agent transcripts, which is a great improvement for Windows users on recent Cursor versions. The changes look solid, introducing new logic to handle both the existing .jsonl format and the new .txt format. I've identified a few areas for improvement to enhance code quality and maintainability. Specifically, I've suggested refactoring to reduce code duplication in cursorWatcher.ts, improving the file scanning logic to be more efficient, and making the new parsing logic in transcriptParser.ts more readable. Overall, great work on identifying and fixing this platform-specific issue.

Comment on lines +173 to +182
if (entry.isDirectory()) {
const jsonlPath = path.join(this.transcriptsDir, entry.name, entry.name + '.jsonl');
if (fs.existsSync(jsonlPath) && !this.filePositions.has(jsonlPath)) {
this.log.appendLine(`[scan] New JSONL transcript: ${entry.name}`);
this.watchFile(jsonlPath);
}
if (this.filePositions.has(jsonlPath)) {
this.readNewContent(jsonlPath);
}
continue;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for handling existing JSONL transcripts (Format A) can be improved to avoid a redundant call to readNewContent for new files and to align with the more robust if/else structure you've used for new TXT transcripts (Format B). This change will prevent readNewContent from being called twice when a new transcript is detected.

Suggested change
if (entry.isDirectory()) {
const jsonlPath = path.join(this.transcriptsDir, entry.name, entry.name + '.jsonl');
if (fs.existsSync(jsonlPath) && !this.filePositions.has(jsonlPath)) {
this.log.appendLine(`[scan] New JSONL transcript: ${entry.name}`);
this.watchFile(jsonlPath);
}
if (this.filePositions.has(jsonlPath)) {
this.readNewContent(jsonlPath);
}
continue;
if (entry.isDirectory()) {
const jsonlPath = path.join(this.transcriptsDir, entry.name, entry.name + '.jsonl');
if (fs.existsSync(jsonlPath)) {
if (!this.filePositions.has(jsonlPath)) {
this.log.appendLine(`[scan] New JSONL transcript: ${entry.name}`);
this.watchFile(jsonlPath);
} else {
this.readNewContent(jsonlPath);
}
}
continue;
}

Comment on lines +245 to 266
if (isFlatTxt) {
const status = parseFlatTxtChunk(text);
if (status) {
this.log.appendLine(`[activity] ${status.activity}: ${status.statusText}`);
this.onStatusChange(status);
if (status.activity !== 'idle') {
this.resetIdleTimer();
}
}
} else {
const lines = text.split('\n').filter(l => l.trim());
for (const line of lines) {
const status = parseTranscriptLine(line);
if (status) {
this.log.appendLine(`[activity] ${status.activity}: ${status.statusText}`);
this.onStatusChange(status);
if (status.activity !== 'idle') {
this.resetIdleTimer();
}
}
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's significant code duplication in how the parsed status is handled for both flat text and JSONL files. To improve maintainability and reduce redundancy, you can extract the status processing logic into a private helper method, for example processStatus(status: ParsedStatus | null).

private processStatus(status: ParsedStatus | null) {
  if (!status) return;

  this.log.appendLine(`[activity] ${status.activity}: ${status.statusText}`);
  this.onStatusChange(status);
  if (status.activity !== 'idle') {
    this.resetIdleTimer();
  }
}

Then you can simplify this block.

      if (isFlatTxt) {
        this.processStatus(parseFlatTxtChunk(text));
      } else {
        const lines = text.split('\n').filter(l => l.trim());
        for (const line of lines) {
          this.processStatus(parseTranscriptLine(line));
        }
      }

Comment on lines +147 to +160
const tool = toolCallMatch[1]!.toLowerCase();
if (/^(read|glob|grep|semanticsearch)$/.test(tool)) {
return { activity: 'reading', statusText: 'Working...' };
}
if (/^(shell|bash)$/.test(tool)) {
return { activity: 'running', statusText: 'Working...' };
}
if (/^(strreplace|write|editnotebook|delete)$/.test(tool)) {
return { activity: 'editing', statusText: 'Working...' };
}
if (/^(task)$/.test(tool)) {
return { activity: 'phoning', statusText: 'Delegating...' };
}
return { activity: 'typing', statusText: 'Working...' };

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a series of if statements with regular expressions for exact string matching can be less efficient and harder to read than a simple switch statement. Refactoring this to a switch on the tool variable would make the code cleaner and more performant.

Suggested change
const tool = toolCallMatch[1]!.toLowerCase();
if (/^(read|glob|grep|semanticsearch)$/.test(tool)) {
return { activity: 'reading', statusText: 'Working...' };
}
if (/^(shell|bash)$/.test(tool)) {
return { activity: 'running', statusText: 'Working...' };
}
if (/^(strreplace|write|editnotebook|delete)$/.test(tool)) {
return { activity: 'editing', statusText: 'Working...' };
}
if (/^(task)$/.test(tool)) {
return { activity: 'phoning', statusText: 'Delegating...' };
}
return { activity: 'typing', statusText: 'Working...' };
const tool = toolCallMatch[1]!.toLowerCase();
switch (tool) {
case 'read':
case 'glob':
case 'grep':
case 'semanticsearch':
return { activity: 'reading', statusText: 'Working...' };
case 'shell':
case 'bash':
return { activity: 'running', statusText: 'Working...' };
case 'strreplace':
case 'write':
case 'editnotebook':
case 'delete':
return { activity: 'editing', statusText: 'Working...' };
case 'task':
return { activity: 'phoning', statusText: 'Delegating...' };
default:
return { activity: 'typing', statusText: 'Working...' };
}

- cursorWatcher.ts: fix double readNewContent call in Format A (JSONL)
  by using if/else instead of two separate if blocks
- cursorWatcher.ts: extract processStatus() helper to eliminate
  duplicated status-handling logic in readNewContent()
- transcriptParser.ts: replace regex if-chain with switch statement
  for tool-to-activity mapping in parseFlatTxtChunk()
@bertheto
Copy link
Author

bertheto commented Mar 2, 2026

Thanks for the review @gemini-code-assist! All three suggestions have been addressed in 3f5f83f:

  • Format A double call: replaced the two separate \if\ blocks with \if/else\ so
    eadNewContent\ is not called for files that were just handed to \watchFile\ (which already calls it internally).
  • Code duplication: extracted \processStatus(status: ParsedStatus | null)\ as a private helper — both the flat-txt and JSONL paths now delegate to it.
  • Switch vs regex if-chain: replaced the regex-based \if\ series with a \switch\ on the lowercased tool name.

@gemini-code-assist
Copy link

Thanks for the update, @bertheto! I've reviewed the changes in 3f5f83f and can confirm that all three suggestions have been addressed effectively:

  • Format A double call: The scanAll method in src/cursorWatcher.ts now correctly uses an if/else structure within the entry.isDirectory() block, ensuring that readNewContent is not redundantly called for files that watchFile has already processed.
  • Code duplication: The common logic for processing ParsedStatus has been successfully extracted into a new private helper method, processStatus, in src/cursorWatcher.ts. This significantly reduces duplication in readNewContent.
  • Switch vs regex if-chain: The parseFlatTxtChunk function in src/transcriptParser.ts now utilizes a switch statement for mapping tool calls to activities, which is a much cleaner and more readable approach than the previous regex-based if-chain.

Great work on these improvements!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant