forked from continuedev/continue
-
Notifications
You must be signed in to change notification settings - Fork 1
/
share.ts
100 lines (82 loc) · 3.37 KB
/
share.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import * as fs from "node:fs";
import { homedir } from "node:os";
import path from "path";
import { languageForFilepath } from "../../autocomplete/constructPrompt.js";
import { SlashCommand } from "../../index.js";
import { stripImages } from "../../llm/countTokens.js";
// 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: "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 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 \`${outPath}\`.`;
},
};
export default ShareSlashCommand;