diff --git a/.prettierrc b/.prettierrc index 222861c341..d3be6d22ea 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,5 @@ { "tabWidth": 2, - "useTabs": false + "useTabs": false, + "trailingComma": "all" } diff --git a/core/commands/slash/share.ts b/core/commands/slash/share.ts index 64754960a3..c78b2ae4c0 100644 --- a/core/commands/slash/share.ts +++ b/core/commands/slash/share.ts @@ -1,24 +1,98 @@ +import path from "path"; +import * as fs from "fs"; +import { homedir } from "os"; import { SlashCommand } from "../.."; +import { languageForFilepath } from "../../autocomplete/constructPrompt"; import { stripImages } from "../../llm/countTokens"; +// If useful elsewhere, helper funcs should move to core/util/index.ts or similar +function getOffsetDatetime(date: Date): Date { + const offset = date.getTimezoneOffset(); + const offsetHours = Math.floor(offset / 60); + const offsetMinutes = offset % 60; + date.setHours(date.getHours() - offsetHours); + date.setMinutes(date.getMinutes() - offsetMinutes); + + return date; +} + +function asBasicISOString(date: Date): string { + const isoString = date.toISOString(); + + return isoString.replace(/[-:]|(\.\d+Z)/g, ""); +} + +function reformatCodeBlocks(msgText: string): string { + const codeBlockFenceRegex = /```((.*?\.(\w+))\s*.*)\n/g; + msgText = msgText.replace(codeBlockFenceRegex, + (match, metadata, filename, extension) => { + const lang = languageForFilepath(filename); + return `\`\`\`${extension}\n${lang.comment} ${metadata}\n`; + }, + ); + // Appease the markdown linter + return msgText.replace(/```\n```/g, '```\n\n```'); +} + const ShareSlashCommand: SlashCommand = { name: "share", - description: "Download and share this session", - run: async function* ({ ide, history }) { - let content = `This is a session transcript from [Continue](https://continue.dev) on ${new Date().toLocaleString()}.`; - - for (const msg of history) { - content += `\n\n## ${ - msg.role === "user" ? "User" : "Continue" - }\n\n${stripImages(msg.content)}`; + description: "Export the current chat session to markdown", + run: async function* ({ ide, history, params }) { + const now = new Date(); + + let content = `### [Continue](https://continue.dev) session transcript\n Exported: ${now.toLocaleString()}`; + + // As currently implemented, the /share command is by definition the last + // message in the chat history, this will omit it + for (const msg of history.slice(0, history.length - 1)) { + let msgText = msg.content; + msgText = stripImages(msg.content); + + if (msg.role === "user" && msgText.search("```") > -1) { + msgText = reformatCodeBlocks(msgText); + } + + // format messages as blockquotes + msgText = msgText.replace(/^/gm, "> "); + + content += `\n\n#### ${ + msg.role === "user" ? "_User_" : "_Assistant_" + }\n\n${msgText}`; + } + + let outputDir: string = params?.outputDir; + if (!outputDir) { + outputDir = await ide.getContinueDir(); + } + + if (outputDir.startsWith("~")) { + outputDir = outputDir.replace(/^~/, homedir); + } else if ( + outputDir.startsWith("./") || + outputDir.startsWith(`.\\`) || + outputDir === "." + ) { + const workspaceDirs = await ide.getWorkspaceDirs(); + // Although the most common situation is to have one directory open in a + // workspace it's also possible to have just a file open without an + // associated directory or to use multi-root workspaces in which multiple + // folders are included. We default to using the first item in the list, if + // it exists. + const workspaceDirectory = workspaceDirs?.[0] || ""; + outputDir = outputDir.replace(/^./, workspaceDirectory); + } + + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); } - const continueDir = await ide.getContinueDir(); - const path = `${continueDir}/session.md`; - await ide.writeFile(path, content); - await ide.openFile(path); + const dtString = asBasicISOString(getOffsetDatetime(now)); + const outPath = path.join(outputDir, `${dtString}_session.md`); //TODO: more flexible naming? + + await ide.writeFile(outPath, content); + await ide.openFile(outPath); - yield `The session transcript has been saved to a markdown file at \`${path}\`.`; + yield `The session transcript has been saved to a markdown file at \`${outPath}\`.`; }, }; diff --git a/core/config/default.ts b/core/config/default.ts index adae7fe60a..c0d980ead8 100644 --- a/core/config/default.ts +++ b/core/config/default.ts @@ -39,7 +39,8 @@ export const defaultConfig: SerializedContinueConfig = { }, { name: "share", - description: "Export this session as markdown", + description: "Export the current chat session to markdown", + params: { ouputDir: "Directory in which to export this session" }, }, { name: "cmd", @@ -110,7 +111,8 @@ export const defaultConfigJetBrains: SerializedContinueConfig = { }, { name: "share", - description: "Export this session as markdown", + description: "Export the current chat session to markdown", + params: { ouputDir: "Directory in which to export this session" }, }, ], customCommands: [ diff --git a/core/config/load.ts b/core/config/load.ts index cdd91aacfc..03bd5b4161 100644 --- a/core/config/load.ts +++ b/core/config/load.ts @@ -334,10 +334,10 @@ function finalToBrowserConfig( })), systemMessage: final.systemMessage, completionOptions: final.completionOptions, - slashCommands: final.slashCommands?.map((m) => ({ - name: m.name, - description: m.description, - options: m.params, + slashCommands: final.slashCommands?.map((s) => ({ + name: s.name, + description: s.description, + params: s.params, //PZTODO: is this why params aren't referenced properly by slash commands? })), contextProviders: final.contextProviders?.map((c) => c.description), disableIndexing: final.disableIndexing, diff --git a/docs/docs/config-file-migration.md b/docs/docs/config-file-migration.md index eda986d3a4..e894c71a89 100644 --- a/docs/docs/config-file-migration.md +++ b/docs/docs/config-file-migration.md @@ -131,7 +131,8 @@ After the "Full example" these examples will only show the relevant portion of t }, { "name": "share", - "description": "Download and share this session", + "description": "Export the current chat session to markdown", + "params": { "ouputDir": "Directory in which to export this session" }, "step": "ShareSessionStep" }, { diff --git a/docs/docs/customization/slash-commands.md b/docs/docs/customization/slash-commands.md index da8ac0d433..d521b09ebc 100644 --- a/docs/docs/customization/slash-commands.md +++ b/docs/docs/customization/slash-commands.md @@ -43,7 +43,8 @@ Type "/share" to generate a shareable markdown transcript of your current chat h ```json { "name": "share", - "description": "Download and share this session" + "description": "Export the current chat session to markdown", + "params": { "ouputDir": "Directory in which to export this session" } } ``` diff --git a/docs/docs/walkthroughs/config-file-migration.md b/docs/docs/walkthroughs/config-file-migration.md index 8abb962bb3..9b2df3650c 100644 --- a/docs/docs/walkthroughs/config-file-migration.md +++ b/docs/docs/walkthroughs/config-file-migration.md @@ -130,7 +130,8 @@ After the "Full example" these examples will only show the relevant portion of t }, { "name": "share", - "description": "Download and share this session", + "description": "Export the current chat session to markdown", + "params": { "ouputDir": "Directory in which to save this session" }, "step": "ShareSessionStep" }, { diff --git a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/constants/ServerConstants.kt b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/constants/ServerConstants.kt index f275143407..8bea73fc79 100644 --- a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/constants/ServerConstants.kt +++ b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/constants/ServerConstants.kt @@ -41,7 +41,7 @@ const val DEFAULT_CONFIG = """ }, { "name": "share", - "description": "Download and share this session", + "description": "Export the current chat session to markdown", "step": "ShareSessionStep" }, { diff --git a/extensions/vscode/config_schema.json b/extensions/vscode/config_schema.json index 47f1df2130..f842d625dd 100644 --- a/extensions/vscode/config_schema.json +++ b/extensions/vscode/config_schema.json @@ -1102,6 +1102,29 @@ } } } + }, + { + "if": { + "properties": { + "name": { + "enum": [ + "share" + ] + } + } + }, + "then": { + "properties": { + "params": { + "properties": { + "outputDir": { + "type": "string", + "markdownDescription": "If outputDir is set to `.` or begins with `./` or `.\\`, file will be saved to the current workspace or a subdirectory thereof, respectively. `~` can similarly be used to specify the user's home directory." + } + } + } + } + } } ], "required": ["name", "description"] diff --git a/gui/src/redux/slices/stateSlice.ts b/gui/src/redux/slices/stateSlice.ts index b25edd7a54..ff16fc49c2 100644 --- a/gui/src/redux/slices/stateSlice.ts +++ b/gui/src/redux/slices/stateSlice.ts @@ -108,7 +108,8 @@ const initialState: State = { }, { name: "share", - description: "Download and share this session", + description: "Export the current chat session to markdown", + params: { ouputDir: "Directory in which to save this session" }, }, { name: "cmd",