Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,29 @@
{
"command": "truthlog.openOutput",
"title": "TruthLog: Open Output"
},
{
"command": "truthlog.copyAsMarkdown",
"title": "TruthLog: Copy as Markdown",
"icon": "$(copy)"
}
],
"menus": {
"commandPalette": [
{
"command": "truthlog.openOutput",
"when": "false"
},
{
"command": "truthlog.copyAsMarkdown",
"when": "false"
}
],
"view/item/context": [
{
"command": "truthlog.copyAsMarkdown",
"when": "view == truthlog.history",
"group": "1_copy"
}
],
"view/title": [
Expand Down
40 changes: 40 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,27 @@ function renderOutputDocument(execution: Execution): string {
return `${header}\n${execution.output}`;
}

/**
* Wraps renderOutputDocument()'s existing header+output formatting in a
* fenced code block, so pasting it into a chat with an AI agent (the
* motivating use case: pasting back the ground truth to correct a false
* claim) renders as a single readable block instead of losing its layout to
* markdown reflow.
*/
function renderAsMarkdown(execution: Execution): string {
return '```\n' + renderOutputDocument(execution) + '\n```';
}

function executionIdFromCommandArg(arg: unknown): string | undefined {
if (typeof arg === 'string') {
return arg;
}
if (typeof arg === 'object' && arg !== null && typeof (arg as { id?: unknown }).id === 'string') {
return (arg as { id: string }).id;
}
return undefined;
}

export function activate(context: vscode.ExtensionContext): TruthLogTestApi {
outputChannel = vscode.window.createOutputChannel('TruthLog');
context.subscriptions.push(outputChannel);
Expand Down Expand Up @@ -269,6 +290,25 @@ export function activate(context: vscode.ExtensionContext): TruthLogTestApi {
})
);

context.subscriptions.push(
vscode.commands.registerCommand('truthlog.copyAsMarkdown', async (arg: unknown) => {
const id = executionIdFromCommandArg(arg);
if (id === undefined) {
log(`WARNING truthlog.copyAsMarkdown called without a valid execution: ${JSON.stringify(arg)}`);
return;
}
const execution = store.getById(id);
if (!execution) {
log(`WARNING truthlog.copyAsMarkdown: execution "${id}" is no longer in history.`);
void vscode.window.showWarningMessage(
'TruthLog: that execution is no longer in history (it was cleared or trimmed).'
);
return;
}
await vscode.env.clipboard.writeText(renderAsMarkdown(execution));
})
);

context.subscriptions.push(
vscode.commands.registerCommand('truthlog.clear', () => {
store.clear();
Expand Down
37 changes: 37 additions & 0 deletions src/test/suite/recording.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,43 @@ describe('TruthLog recording pipeline (steps 4-9)', function () {
}
});

it('step 7b: truthlog.copyAsMarkdown copies command, exit code, and output as a fenced block', async () => {
const marker = 'TRUTHLOG_COPY_AS_MARKDOWN_MARKER';
const execution = await runAndWaitForRecord(
api.store,
shellIntegration,
`node -e "console.log('${marker}')"`
);

// Real invocations come from the tree's right-click context menu, which
// VS Code invokes with the whole selected Execution element (not just
// its id) as the command argument - unlike truthlog.openOutput, which is
// wired to a manually-specified id via item.command.arguments.
await vscode.commands.executeCommand('truthlog.copyAsMarkdown', execution);

const clipboardText = await vscode.env.clipboard.readText();
console.log('RAW clipboard text after truthlog.copyAsMarkdown:', JSON.stringify(clipboardText));

assert.ok(clipboardText.startsWith('```\n'), 'expected the copied text to be a fenced markdown code block');
assert.ok(clipboardText.trim().endsWith('```'), 'expected the fenced code block to be closed');
assert.ok(clipboardText.includes(execution.command), 'expected the copied text to include the command');
assert.ok(clipboardText.includes('exit code: 0'), 'expected the copied text to include the exit code');
assert.ok(clipboardText.includes(marker), 'expected the copied text to include the raw output');
});

it('step 7c: truthlog.copyAsMarkdown also accepts a bare execution id', async () => {
const execution = await runAndWaitForRecord(api.store, shellIntegration, 'echo copy-by-id');
await vscode.env.clipboard.writeText(''); // reset so a stale value can't produce a false pass

await vscode.commands.executeCommand('truthlog.copyAsMarkdown', execution.id);

const clipboardText = await vscode.env.clipboard.readText();
assert.ok(
clipboardText.includes('echo copy-by-id'),
`expected copying by bare id to copy the right execution, got: ${JSON.stringify(clipboardText)}`
);
});

it('step 8: truthlog.toggleFailuresOnly filters the tree to non-zero exits', async () => {
const pass = await runAndWaitForRecord(api.store, shellIntegration, 'echo filter-pass');
const fail = await runAndWaitForRecord(api.store, shellIntegration, 'node -e "process.exit(2)"');
Expand Down