-
Notifications
You must be signed in to change notification settings - Fork 2
fix: support flat .txt transcripts on Windows (Cursor >= 0.47) #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ import * as fs from 'fs'; | |
| import * as path from 'path'; | ||
| import * as os from 'os'; | ||
| import * as vscode from 'vscode'; | ||
| import { parseTranscriptLine, ParsedStatus } from './transcriptParser'; | ||
| import { parseTranscriptLine, parseFlatTxtChunk, ParsedStatus } from './transcriptParser'; | ||
| import { isHooksInstalled, getStateFilePath } from './hooksInstaller'; | ||
|
|
||
| export class CursorWatcher implements vscode.Disposable { | ||
|
|
@@ -169,22 +169,37 @@ export class CursorWatcher implements vscode.Disposable { | |
| try { | ||
| const entries = fs.readdirSync(this.transcriptsDir, { withFileTypes: true }); | ||
| for (const entry of entries) { | ||
| if (!entry.isDirectory()) continue; | ||
| const jsonlPath = path.join(this.transcriptsDir, entry.name, entry.name + '.jsonl'); | ||
| if (fs.existsSync(jsonlPath) && !this.filePositions.has(jsonlPath)) { | ||
| this.log.appendLine(`[scan] New transcript: ${entry.name}`); | ||
| this.watchFile(jsonlPath); | ||
| // Format A: sub-directory with <uuid>/<uuid>.jsonl (Linux/Mac) | ||
| 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; | ||
| } | ||
| if (this.filePositions.has(jsonlPath)) { | ||
| this.readNewContent(jsonlPath); | ||
|
|
||
| // Format B: flat <uuid>.txt file (Windows / Cursor ≥ 0.47) | ||
| if (entry.isFile() && entry.name.endsWith('.txt')) { | ||
| const txtPath = path.join(this.transcriptsDir, entry.name); | ||
| if (!this.filePositions.has(txtPath)) { | ||
| this.log.appendLine(`[scan] New TXT transcript: ${entry.name}`); | ||
| this.watchFile(txtPath, true); | ||
| } else { | ||
| this.readNewContent(txtPath, true); | ||
| } | ||
| } | ||
| } | ||
| } catch (e) { | ||
| this.log.appendLine(`[scan] Error: ${e}`); | ||
| } | ||
| } | ||
|
|
||
| private watchFile(filePath: string) { | ||
| private watchFile(filePath: string, isFlatTxt = false) { | ||
| try { | ||
| const fd = fs.openSync(filePath, 'r'); | ||
| const stat = fs.fstatSync(fd); | ||
|
|
@@ -193,17 +208,17 @@ export class CursorWatcher implements vscode.Disposable { | |
| this.log.appendLine(`[watch] ${path.basename(filePath)} from pos ${this.filePositions.get(filePath)}`); | ||
|
|
||
| const watcher = fs.watch(filePath, { persistent: false }, () => { | ||
| this.readNewContent(filePath); | ||
| this.readNewContent(filePath, isFlatTxt); | ||
| }); | ||
| this.watchers.push(watcher); | ||
|
|
||
| this.readNewContent(filePath); | ||
| this.readNewContent(filePath, isFlatTxt); | ||
| } catch (e) { | ||
| this.log.appendLine(`[watch] Error: ${filePath} ${e}`); | ||
| } | ||
| } | ||
|
|
||
| private readNewContent(filePath: string) { | ||
| private readNewContent(filePath: string, isFlatTxt = false) { | ||
| const prevPos = this.filePositions.get(filePath) ?? 0; | ||
|
|
||
| let fd: number; | ||
|
|
@@ -227,16 +242,12 @@ export class CursorWatcher implements vscode.Disposable { | |
| this.log.appendLine(`[read] ${path.basename(filePath)} +${bytesToRead} bytes (${prevPos} → ${stat.size})`); | ||
|
|
||
| const text = buf.toString('utf-8'); | ||
| 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(); | ||
| } | ||
|
|
||
| if (isFlatTxt) { | ||
| this.processStatus(parseFlatTxtChunk(text)); | ||
| } else { | ||
| for (const line of text.split('\n').filter(l => l.trim())) { | ||
| this.processStatus(parseTranscriptLine(line)); | ||
| } | ||
| } | ||
|
Comment on lines
+246
to
252
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 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));
}
} |
||
| } catch (e) { | ||
|
|
@@ -245,6 +256,15 @@ export class CursorWatcher implements vscode.Disposable { | |
| } | ||
| } | ||
|
|
||
| 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(); | ||
| } | ||
| } | ||
|
|
||
| private resetIdleTimer() { | ||
| if (this.idleTimer) clearTimeout(this.idleTimer); | ||
| this.idleTimer = setTimeout(() => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for handling existing JSONL transcripts (Format A) can be improved to avoid a redundant call to
readNewContentfor new files and to align with the more robustif/elsestructure you've used for new TXT transcripts (Format B). This change will preventreadNewContentfrom being called twice when a new transcript is detected.