| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 49ab023 commit 93152df
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -939,10 +939,12 @@ async function exportSession(id) { | |||
| 939 | 939 | 'Copied ' + turns + ' turn' + (turns === 1 ? '' : 's') + ' as Markdown.', 'Save as file…'); | |
| 940 | 940 | if (pick !== 'Save as file…') { return; } | |
| 941 | 941 | ||
| 942 | - // Derive a filename from the title so a folder of exports stays readable. The title has already | ||
| 943 | - // been through redactSecrets above, but it is sanitised again here for the FILESYSTEM's sake — | ||
| 944 | - // a slash or a colon in a title is a path, not a name. | ||
| 945 | - const stem = String(entry.title || 'session').toLowerCase() | ||
| 942 | + // Derive a filename from the title so a folder of exports stays readable. REDACT it first: the exported | ||
| 943 | + // body was scrubbed, but `entry.title` here is the raw index title (toMarkdown redacts its own copy, not | ||
| 944 | + // this variable), and the filesystem sanitiser below only strips slashes/colons — a credential's own | ||
| 945 | + // characters survive it (`ghp_ABC…` → `ghp-abc…`), so a token in a title would leak into the save path. | ||
| 946 | + const safeTitle = sessionMemory.redactSecrets(String(entry.title || 'session')); | ||
| 947 | + const stem = safeTitle.toLowerCase() | ||
| 946 | 948 | .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'session'; | |
| 947 | 949 | const target = await vscode.window.showSaveDialog({ | |
| 948 | 950 | filters: { Markdown: ['md'] }, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -166,7 +166,10 @@ function toMarkdown(meta, messages, opts) { | |||
| 166 | 166 | ||
| 167 | 167 | for (const t of turns) { | |
| 168 | 168 | md += '\n---\n\n**' + (t.role === 'user' ? 'You' : 'LevelCode') + '**\n\n'; | |
| 169 | - md += scrub(String(t.text)).trim() + '\n'; | ||
| 169 | + // Verbatim aside from redaction — NOT trimmed. Trimming changes Markdown semantics: it de-indents a | ||
| 170 | + // leading 4-space (indented code block) and eats trailing " " (a hard line break). The `**role**\n\n` | ||
| 171 | + // above already supplies the blank line an indented block needs after it. | ||
| 172 | + md += scrub(String(t.text)) + '\n'; | ||
| 170 | 173 | } | |
| 171 | 174 | // An empty session still exports — a file with a header and no turns is a truthful answer, and | |
| 172 | 175 | // silently producing nothing would read as a broken button. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -138,6 +138,15 @@ test('EXPORT: roles are BOLD, never headings — a turn owns the heading levels' | |||
| 138 | 138 | assert.match(md, /^## Step one$/m, "the turn's own headings are untouched"); | |
| 139 | 139 | }); | |
| 140 | 140 | ||
| 141 | + test('EXPORT: turn text is VERBATIM — indented code + a trailing hard-break survive (not trimmed)', () => { | ||
| 142 | + // Trimming the turn would de-indent a leading 4-space (killing an indented code block) and eat the two | ||
| 143 | + // trailing spaces of a Markdown hard line break. Export must preserve the text as written, save redaction. | ||
| 144 | + const turn = ' indented = code()\n\nfinal line ends with a hard break '; | ||
| 145 | + const md = E.toMarkdown(META, [{ role: 'assistant', content: turn }]); | ||
| 146 | + assert.match(md, /\*\*LevelCode\*\*\n\n {4}indented = code\(\)/, 'the leading 4-space indent survives (indented code block)'); | ||
| 147 | + assert.match(md, /hard break {2}\n/, 'the trailing two-space hard line break survives'); | ||
| 148 | + }); | ||
| 149 | + | ||
| 141 | 150 | test('EXPORT: scrubs, because export is the first surface that SHARES a session', () => { | |
| 142 | 151 | // chat-sessions-design §10: "anything that later shares a session must scrub — that is that | |
| 143 | 152 | // feature's burden." A token pasted into chat must not ride into a pull request. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -258,4 +258,9 @@ test('NO-JUMP: line 2 swaps file·time ⇄ actions in a fixed min-height row (no | |||
| 258 | 258 | } | |
| 259 | 259 | }); | |
| 260 | 260 | ||
| 261 | + test('EXPORT: the body is scrubbed on the way out, and the save-dialog filename is redacted too', () => { | ||
| 262 | + assert.match(ext, /toMarkdown\(entry, m\.transcript\(id\), \{ redact: sessionMemory\.redactSecrets \}\)/, 'the exported body is scrubbed'); | ||
| 263 | + assert.match(ext, /redactSecrets\(String\(entry\.title[\s\S]{0,140}stem/, 'and the default filename is redacted BEFORE the save path (a title token cannot leak into the dialog)'); | ||
| 264 | + }); | ||
| 265 | + | ||
| 261 | 266 | console.log('sessionsUi: ' + n + ' tests passed'); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments