diff --git a/src/extension.ts b/src/extension.ts index 0e20405..12861b7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3185,6 +3185,65 @@ class CopilotTokenTracker implements vscode.Disposable { }); } + /** + * Opens a JSONL file in a formatted view with array brackets and commas. + * Does not modify the original file. + */ + public async showFormattedJsonlFile(sessionFilePath: string): Promise { + try { + // Read the file content + const fileContent = await fs.promises.readFile(sessionFilePath, 'utf-8'); + + // Parse JSONL into array of objects + const lines = fileContent.trim().split('\n').filter(line => line.trim().length > 0); + const jsonObjects: unknown[] = []; + + for (let i = 0; i < lines.length; i++) { + try { + const obj = JSON.parse(lines[i]); + jsonObjects.push(obj); + } catch (e) { + // Skip malformed lines with detailed warning + this.warn(`Skipping malformed line ${i + 1} in ${sessionFilePath}: ${e}`); + } + } + + // Format as JSON array + const formattedJson = JSON.stringify(jsonObjects, null, 2); + + // Create an untitled document with the formatted content + const fileName = path.basename(sessionFilePath, path.extname(sessionFilePath)); + const prettyUri = vscode.Uri.parse(`untitled:${fileName}-formatted.json`); + + // Check if this document is already open and close it to refresh + const openDoc = vscode.workspace.textDocuments.find(d => d.uri.toString() === prettyUri.toString()); + if (openDoc) { + // Close the existing document so we can create a fresh one with updated content + const editor = vscode.window.visibleTextEditors.find(e => e.document === openDoc); + if (editor) { + await vscode.window.showTextDocument(openDoc, editor.viewColumn); + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + } + } + + // Create and open the document + const doc = await vscode.workspace.openTextDocument(prettyUri); + const editor = await vscode.window.showTextDocument(doc, { preview: true }); + + // Insert the formatted JSON + await editor.edit((editBuilder) => { + editBuilder.insert(new vscode.Position(0, 0), formattedJson); + }); + + // Set language mode to JSON for syntax highlighting + await vscode.languages.setTextDocumentLanguage(doc, 'json'); + + } catch (error) { + this.error(`Error formatting JSONL file ${sessionFilePath}:`, error); + throw error; + } + } + private getLogViewerHtml(webview: vscode.Webview, logData: SessionLogData): string { const nonce = this.getNonce(); const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview', 'logviewer.js')); @@ -3577,6 +3636,17 @@ class CopilotTokenTracker implements vscode.Disposable { } break; + case 'openFormattedJsonlFile': + if (message.file) { + try { + await this.showFormattedJsonlFile(message.file); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + vscode.window.showErrorMessage('Could not open formatted file: ' + message.file + ' (' + errorMsg + ')'); + } + } + break; + case 'revealPath': if (message.path) { try { diff --git a/src/webview/diagnostics/main.ts b/src/webview/diagnostics/main.ts index a6e5c65..e34bf17 100644 --- a/src/webview/diagnostics/main.ts +++ b/src/webview/diagnostics/main.ts @@ -303,6 +303,7 @@ function renderSessionTable(detailedFiles: SessionFileDetails[], isLoading: bool Context Refs First Interaction${getSortIndicator('firstInteraction')} Last Interaction${getSortIndicator('lastInteraction')} + Actions @@ -318,6 +319,9 @@ function renderSessionTable(detailedFiles: SessionFileDetails[], isLoading: bool ${sanitizeNumber(getTotalContextRefs(sf.contextReferences))} ${formatDate(sf.firstInteraction)} ${formatDate(sf.lastInteraction)} + + 📄 View + `).join('')} @@ -739,8 +743,8 @@ function renderLayout(data: DiagnosticsData): void { color: #9aa0a6; margin-top: 4px; } - .session-file-link, .reveal-link { color: #4FC3F7; text-decoration: underline; cursor: pointer; } - .session-file-link:hover, .reveal-link:hover { color: #81D4FA; } + .session-file-link, .reveal-link, .view-formatted-link { color: #4FC3F7; text-decoration: underline; cursor: pointer; } + .session-file-link:hover, .reveal-link:hover, .view-formatted-link:hover { color: #81D4FA; } .empty-session-link { color: #999; } .empty-session-link:hover { color: #aaa; } .button-group { display: flex; gap: 12px; margin-top: 16px; flex-wrap: wrap; } @@ -1203,6 +1207,15 @@ function setupStorageLinkHandlers(): void { }); }); + // View formatted JSONL link handlers + document.querySelectorAll('.view-formatted-link').forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const file = decodeURIComponent((link as HTMLElement).getAttribute('data-file') || ''); + vscode.postMessage({ command: 'openFormattedJsonlFile', file }); + }); + }); + // Reveal link handlers document.querySelectorAll('.reveal-link').forEach(link => { link.addEventListener('click', (e) => {