@100xprompt/brain 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +48 -0
- package/README.md +172 -2
- package/package.json +48 -4
- package/src/adapters/aider/index.ts +8 -0
- package/src/adapters/aider/reader.ts +129 -0
- package/src/adapters/aider/records.ts +220 -0
- package/src/adapters/aider/writer.ts +269 -0
- package/src/adapters/amazon-q-cli/index.ts +8 -0
- package/src/adapters/amazon-q-cli/reader.ts +84 -0
- package/src/adapters/amazon-q-cli/records.ts +191 -0
- package/src/adapters/amazon-q-cli/writer.ts +370 -0
- package/src/adapters/amazon-q-ide/index.ts +9 -0
- package/src/adapters/amazon-q-ide/reader.ts +131 -0
- package/src/adapters/amazon-q-ide/records.ts +141 -0
- package/src/adapters/amazon-q-ide/writer.ts +325 -0
- package/src/adapters/antigravity/index.ts +9 -0
- package/src/adapters/antigravity/proto.ts +194 -0
- package/src/adapters/antigravity/reader.ts +424 -0
- package/src/adapters/antigravity/records.ts +236 -0
- package/src/adapters/antigravity/writer.ts +463 -0
- package/src/adapters/auggie/index.ts +36 -0
- package/src/adapters/claude-code/index.ts +8 -0
- package/src/adapters/claude-code/reader.ts +639 -0
- package/src/adapters/claude-code/records.ts +88 -0
- package/src/adapters/claude-code/writer.ts +329 -0
- package/src/adapters/cline/index.ts +12 -0
- package/src/adapters/cline/reader.ts +432 -0
- package/src/adapters/cline/records.ts +151 -0
- package/src/adapters/cline/vscdb.ts +56 -0
- package/src/adapters/cline/writer.ts +656 -0
- package/src/adapters/cline/xml.ts +177 -0
- package/src/adapters/cline4/index.ts +9 -0
- package/src/adapters/cline4/reader.ts +179 -0
- package/src/adapters/cline4/records.ts +92 -0
- package/src/adapters/cline4/writer.ts +361 -0
- package/src/adapters/codebuff/index.ts +8 -0
- package/src/adapters/codebuff/project.ts +207 -0
- package/src/adapters/codebuff/reader.ts +215 -0
- package/src/adapters/codebuff/records.ts +173 -0
- package/src/adapters/codebuff/writer.ts +383 -0
- package/src/adapters/codex/index.ts +8 -0
- package/src/adapters/codex/native.ts +239 -0
- package/src/adapters/codex/reader.ts +747 -0
- package/src/adapters/codex/records.ts +114 -0
- package/src/adapters/codex/writer.ts +479 -0
- package/src/adapters/continue/index.ts +9 -0
- package/src/adapters/continue/reader.ts +68 -0
- package/src/adapters/continue/records.ts +418 -0
- package/src/adapters/continue/writer.ts +162 -0
- package/src/adapters/copilot-cli/index.ts +9 -0
- package/src/adapters/copilot-cli/reader.ts +186 -0
- package/src/adapters/copilot-cli/records.ts +241 -0
- package/src/adapters/copilot-cli/writer.ts +353 -0
- package/src/adapters/crush/index.ts +8 -0
- package/src/adapters/crush/reader.ts +180 -0
- package/src/adapters/crush/records.ts +169 -0
- package/src/adapters/crush/writer.ts +333 -0
- package/src/adapters/cursor-cli/index.ts +405 -0
- package/src/adapters/cursor-ide/agentkv.ts +95 -0
- package/src/adapters/cursor-ide/index.ts +501 -0
- package/src/adapters/cursor-ide/transcripts.ts +122 -0
- package/src/adapters/cursor-ide/workspaces.ts +119 -0
- package/src/adapters/cursor-ide/writer.ts +170 -0
- package/src/adapters/devin/index.ts +8 -0
- package/src/adapters/devin/reader.ts +305 -0
- package/src/adapters/devin/records.ts +277 -0
- package/src/adapters/devin/writer.ts +523 -0
- package/src/adapters/factory-droid/index.ts +12 -0
- package/src/adapters/factory-droid/reader.ts +184 -0
- package/src/adapters/factory-droid/records.ts +79 -0
- package/src/adapters/factory-droid/writer.ts +350 -0
- package/src/adapters/gemini-cli/exchanges.ts +228 -0
- package/src/adapters/gemini-cli/index.ts +396 -0
- package/src/adapters/gemini-cli/records.ts +521 -0
- package/src/adapters/gemini-cli/writer.ts +354 -0
- package/src/adapters/goose/index.ts +8 -0
- package/src/adapters/goose/reader.ts +138 -0
- package/src/adapters/goose/records.ts +199 -0
- package/src/adapters/goose/writer.ts +340 -0
- package/src/adapters/grok-cli/image.ts +44 -0
- package/src/adapters/grok-cli/index.ts +8 -0
- package/src/adapters/grok-cli/reader.ts +302 -0
- package/src/adapters/grok-cli/records.ts +172 -0
- package/src/adapters/grok-cli/writer.ts +329 -0
- package/src/adapters/hermes/index.ts +8 -0
- package/src/adapters/hermes/reader.ts +249 -0
- package/src/adapters/hermes/records.ts +285 -0
- package/src/adapters/hermes/writer.ts +452 -0
- package/src/adapters/iflow-cli/index.ts +72 -0
- package/src/adapters/iflow-cli/records.ts +215 -0
- package/src/adapters/iflow-cli/writer.ts +204 -0
- package/src/adapters/index.ts +112 -0
- package/src/adapters/kilo-cli/index.ts +21 -0
- package/src/adapters/kilo-code/index.ts +11 -0
- package/src/adapters/kilo-code/writer.ts +92 -0
- package/src/adapters/kimi/index.ts +9 -0
- package/src/adapters/kimi/reader.ts +92 -0
- package/src/adapters/kimi/records.ts +221 -0
- package/src/adapters/kimi/writer.ts +367 -0
- package/src/adapters/kiro-cli/index.ts +8 -0
- package/src/adapters/kiro-cli/reader.ts +87 -0
- package/src/adapters/kiro-cli/records.ts +260 -0
- package/src/adapters/kiro-cli/writer.ts +305 -0
- package/src/adapters/kiro-ide/index.ts +8 -0
- package/src/adapters/kiro-ide/writer.ts +52 -0
- package/src/adapters/letta/index.ts +8 -0
- package/src/adapters/letta/reader.ts +177 -0
- package/src/adapters/letta/records.ts +119 -0
- package/src/adapters/letta/writer.ts +413 -0
- package/src/adapters/mastracode/index.ts +8 -0
- package/src/adapters/mastracode/reader.ts +323 -0
- package/src/adapters/mastracode/records.ts +336 -0
- package/src/adapters/mastracode/writer.ts +385 -0
- package/src/adapters/mistral-vibe/index.ts +9 -0
- package/src/adapters/mistral-vibe/reader.ts +62 -0
- package/src/adapters/mistral-vibe/records.ts +104 -0
- package/src/adapters/mistral-vibe/writer.ts +359 -0
- package/src/adapters/muse/index.ts +10 -0
- package/src/adapters/muse/reader.ts +332 -0
- package/src/adapters/muse/records.ts +104 -0
- package/src/adapters/muse/writer.ts +387 -0
- package/src/adapters/omp/index.ts +13 -0
- package/src/adapters/omp/reader.ts +314 -0
- package/src/adapters/omp/records.ts +153 -0
- package/src/adapters/omp/writer.ts +216 -0
- package/src/adapters/opencode/ids.ts +52 -0
- package/src/adapters/opencode/index.ts +31 -0
- package/src/adapters/opencode/note.ts +8 -0
- package/src/adapters/opencode/reader.ts +452 -0
- package/src/adapters/opencode/tools.ts +174 -0
- package/src/adapters/opencode/v2.ts +367 -0
- package/src/adapters/opencode/writer.ts +379 -0
- package/src/adapters/openhands/index.ts +8 -0
- package/src/adapters/openhands/reader.ts +222 -0
- package/src/adapters/openhands/records.ts +136 -0
- package/src/adapters/openhands/writer.ts +404 -0
- package/src/adapters/pearai/index.ts +10 -0
- package/src/adapters/pi/index.ts +28 -0
- package/src/adapters/pi/reader.ts +261 -0
- package/src/adapters/pi/records.ts +99 -0
- package/src/adapters/pi/writer.ts +362 -0
- package/src/adapters/qoder-cli/index.ts +8 -0
- package/src/adapters/qoder-cli/reader.ts +83 -0
- package/src/adapters/qoder-cli/records.ts +45 -0
- package/src/adapters/qoder-cli/writer.ts +72 -0
- package/src/adapters/qwen-code/index.ts +84 -0
- package/src/adapters/qwen-code/records.ts +194 -0
- package/src/adapters/qwen-code/writer.ts +224 -0
- package/src/adapters/tabnine-cli/index.ts +8 -0
- package/src/adapters/tabnine-cli/reader.ts +106 -0
- package/src/adapters/tabnine-cli/records.ts +33 -0
- package/src/adapters/tabnine-cli/writer.ts +156 -0
- package/src/adapters/types.ts +73 -0
- package/src/adapters/void/index.ts +9 -0
- package/src/adapters/void/reader.ts +48 -0
- package/src/adapters/void/records.ts +285 -0
- package/src/adapters/void/writer.ts +93 -0
- package/src/adapters/vscode-copilot/index.ts +9 -0
- package/src/adapters/vscode-copilot/reader.ts +212 -0
- package/src/adapters/vscode-copilot/records.ts +388 -0
- package/src/adapters/vscode-copilot/shared.ts +68 -0
- package/src/adapters/vscode-copilot/writer.ts +546 -0
- package/src/adapters/zed/index.ts +8 -0
- package/src/adapters/zed/reader.ts +198 -0
- package/src/adapters/zed/records.ts +87 -0
- package/src/adapters/zed/writer.ts +341 -0
- package/src/brief-writers.ts +71 -0
- package/src/brief.ts +238 -0
- package/src/cli.ts +326 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/connect.ts +845 -0
- package/src/gateway/signin.ts +323 -0
- package/src/handoff.ts +104 -0
- package/src/hooks.ts +455 -0
- package/src/memory/audit.ts +514 -0
- package/src/memory/blobs.ts +94 -0
- package/src/memory/cli.ts +117 -0
- package/src/memory/distill.ts +381 -0
- package/src/memory/externalize.ts +435 -0
- package/src/memory/lookups.ts +70 -0
- package/src/memory/mcp-install.ts +401 -0
- package/src/memory/mcp-server.ts +296 -0
- package/src/memory/prepare.ts +179 -0
- package/src/memory/preview.ts +154 -0
- package/src/memory/project.ts +35 -0
- package/src/memory/redact.ts +72 -0
- package/src/memory/relevance.ts +187 -0
- package/src/memory/repomap.ts +516 -0
- package/src/memory/store.ts +270 -0
- package/src/memory/sync.ts +166 -0
- package/src/memory/tidy.ts +193 -0
- package/src/memory/types.ts +118 -0
- package/src/org/admin-ops.ts +96 -0
- package/src/org/auth.ts +112 -0
- package/src/org/backup.ts +115 -0
- package/src/org/cli.ts +438 -0
- package/src/org/client.ts +404 -0
- package/src/org/config.ts +44 -0
- package/src/org/crypto.ts +537 -0
- package/src/org/db.ts +435 -0
- package/src/org/graph.ts +318 -0
- package/src/org/oidc.ts +435 -0
- package/src/org/policy.ts +398 -0
- package/src/org/server.ts +920 -0
- package/src/org/sso.ts +316 -0
- package/src/org/tls.ts +66 -0
- package/src/org/types.ts +193 -0
- package/src/self.ts +68 -0
- package/src/session/attachments.ts +53 -0
- package/src/session/peekcache.ts +50 -0
- package/src/session/types.ts +104 -0
- package/src/session/util.ts +130 -0
- package/src/sqlite.ts +48 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { copyFileSync, mkdirSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { exists } from "../../fsutil.ts"
|
|
4
|
+
import type { ToolKind } from "../../session/types.ts"
|
|
5
|
+
|
|
6
|
+
// Helpers the VS Code chat family's writers share (Copilot Chat, Continue, PearAI, Void): picking the
|
|
7
|
+
// arguments a tool call names in another tool's vocabulary, shell quoting, and the backup taken before
|
|
8
|
+
// a writer changes a database another program owns.
|
|
9
|
+
|
|
10
|
+
type Json = Record<string, unknown>
|
|
11
|
+
|
|
12
|
+
const isObj = (v: unknown): v is Json => typeof v === "object" && v !== null && !Array.isArray(v)
|
|
13
|
+
|
|
14
|
+
/** The first non-empty string among `keys` in a tool call's input (arrays of strings are joined with spaces). */
|
|
15
|
+
export function pick(input: unknown, keys: string[]): string | undefined {
|
|
16
|
+
if (!isObj(input)) return undefined
|
|
17
|
+
for (const k of keys) {
|
|
18
|
+
const v = input[k]
|
|
19
|
+
if (typeof v === "string" && v) return v
|
|
20
|
+
if (Array.isArray(v) && v.length && v.every((x) => typeof x === "string")) return v.join(" ")
|
|
21
|
+
}
|
|
22
|
+
return undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Argument names other tools use for the same thing. */
|
|
26
|
+
export const ARG = {
|
|
27
|
+
path: ["file_path", "filePath", "filepath", "path", "absolute_path", "target_file", "file", "uri"],
|
|
28
|
+
dir: ["dir_path", "dirPath", "path", "directory", "target_directory", "relative_workspace_path", "dir"],
|
|
29
|
+
content: ["content", "contents", "file_text", "text", "code"],
|
|
30
|
+
old: ["old_string", "oldString", "old_str", "old_text"],
|
|
31
|
+
new: ["new_string", "newString", "new_str", "new_text"],
|
|
32
|
+
command: ["command", "cmd", "commandLine"],
|
|
33
|
+
query: ["query", "pattern", "regex", "search", "q"],
|
|
34
|
+
url: ["url", "uri", "href"],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A tool call in the target's own terms, when its kind has an equivalent there; undefined keeps it as it was. */
|
|
38
|
+
export type ToolMap = Partial<Record<ToolKind, (input: unknown) => { name: string; args: Json } | undefined>>
|
|
39
|
+
|
|
40
|
+
export function mapTool(map: ToolMap, kind: ToolKind, input: unknown): { name: string; args: Json } | undefined {
|
|
41
|
+
return map[kind]?.(input)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Tool names every provider accepts: letters, digits, _ and -, at most 64. */
|
|
45
|
+
export const safeToolName = (name: string) => name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64) || "tool"
|
|
46
|
+
|
|
47
|
+
/** A tool's input as an object, as the targets' tool-call records require. */
|
|
48
|
+
export const asArgs = (input: unknown): Json => (isObj(input) ? input : input === undefined || input === null ? {} : { input })
|
|
49
|
+
|
|
50
|
+
export const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Copies a database another program owns (with its -wal and -shm files) to ~/.brain/backups/<tool>/<stamp>/
|
|
54
|
+
* before a writer changes it. Returns the folder; nothing is copied for files that do not exist yet.
|
|
55
|
+
*/
|
|
56
|
+
export function backup(home: string, tool: string, files: string[], now = new Date()): string {
|
|
57
|
+
const dir = join(home, ".brain", "backups", tool, now.toISOString().replace(/[:.]/g, "-"))
|
|
58
|
+
for (const f of files) {
|
|
59
|
+
for (const side of ["", "-wal", "-shm"]) {
|
|
60
|
+
if (!exists(f + side)) continue
|
|
61
|
+
// Files from different folders can share a name (two state.vscdb); keep their folder's name too.
|
|
62
|
+
const name = `${f.split("/").slice(-2).join("__")}${side}`
|
|
63
|
+
mkdirSync(dir, { recursive: true })
|
|
64
|
+
copyFileSync(f + side, join(dir, name))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return dir
|
|
68
|
+
}
|
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite"
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto"
|
|
3
|
+
import { mkdirSync, rmSync, statSync, writeFileSync } from "node:fs"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { toolLabel } from "../../brief.ts"
|
|
6
|
+
import { appSupport, exists, isDir, isRunning, readJson } from "../../fsutil.ts"
|
|
7
|
+
import { imageDataUrl, parseDataUrl } from "../../session/attachments.ts"
|
|
8
|
+
import type { Attachment, Session, Turn } from "../../session/types.ts"
|
|
9
|
+
import { countTurns, firstUserText, unpaired } from "../../session/util.ts"
|
|
10
|
+
import { continuedNote } from "../claude-code/records.ts"
|
|
11
|
+
import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
|
|
12
|
+
import { reader } from "./reader.ts"
|
|
13
|
+
import { LABEL, TESTED_VERSION, TOOL } from "./records.ts"
|
|
14
|
+
import { ARG, type ToolMap, asArgs, backup, mapTool, pick, safeToolName, shellQuote } from "./shared.ts"
|
|
15
|
+
|
|
16
|
+
// Continues a portable session in Copilot Chat in VS Code as a new local chat: the session is written the
|
|
17
|
+
// way VS Code 1.144 stores its own (chatSessionStore.ts and chatSessionOperationLog.ts at the 1.144 commit
|
|
18
|
+
// 2c33b50b), and an entry is added to the chat index so the Chat view's history lists it:
|
|
19
|
+
//
|
|
20
|
+
// - User/workspaceStorage/<workspace id>/chatSessions/<uuid>.jsonl: a mutation log holding one Initial
|
|
21
|
+
// entry, {kind: 0, v: ISerializableChatData v3}, which is what VS Code writes for a new session.
|
|
22
|
+
// - The same folder's state.vscdb, ItemTable key 'chat.ChatSessionStore.index': {version: 1, entries}.
|
|
23
|
+
// VS Code lists history from this index, not from the folder, and throws the whole index away if any
|
|
24
|
+
// entry is malformed, so the entry is built as getSessionMetadata builds it and merged, never replaced.
|
|
25
|
+
//
|
|
26
|
+
// Each request carries two records of its reply, as Copilot's agent does. The response parts are what
|
|
27
|
+
// the chat shows (markdown, tool invocations, thinking). result.metadata.toolCallRounds and
|
|
28
|
+
// toolCallResults are what Copilot Chat 0.72 replays to the model on the next request
|
|
29
|
+
// (AgentConversationHistory, ChatToolCalls): each round is one model call with its text and tool calls,
|
|
30
|
+
// and each result is a LanguageModelToolResult marshalled with $mid 20/21. The request's agent is
|
|
31
|
+
// github.copilot.editsAgent: Copilot keeps result metadata in history only for its own participants.
|
|
32
|
+
//
|
|
33
|
+
// The note saying where the session came from goes in the first request's renderedUserMessage, which
|
|
34
|
+
// Copilot sends to the model in place of the request text while the chat shows only the person's words.
|
|
35
|
+
// A compaction becomes a system-initiated request whose single round carries the summary
|
|
36
|
+
// (metadata.summaries), which Copilot renders as <conversation-summary> in place of everything before it.
|
|
37
|
+
//
|
|
38
|
+
// state.vscdb is a database VS Code keeps open and rewrites from memory, so this writer runs only with
|
|
39
|
+
// VS Code closed and backs the file up first.
|
|
40
|
+
|
|
41
|
+
export const TESTED_VSCODE = ["1.144"]
|
|
42
|
+
export const TESTED_COPILOT = ["0.72"]
|
|
43
|
+
|
|
44
|
+
const APP = "Visual Studio Code.app"
|
|
45
|
+
const INDEX_KEY = "chat.ChatSessionStore.index"
|
|
46
|
+
const AGENT_ID = "github.copilot.editsAgent"
|
|
47
|
+
|
|
48
|
+
/** The installed VS Code and its built-in Copilot Chat, read from the app bundle. */
|
|
49
|
+
export function installedVSCode(home: string): { vscode?: string; copilot?: string } {
|
|
50
|
+
for (const root of ["/Applications", join(home, "Applications")]) {
|
|
51
|
+
const app = join(root, APP, "Contents", "Resources", "app")
|
|
52
|
+
const v = readJson<{ version?: string }>(join(app, "package.json"))?.version
|
|
53
|
+
if (!v) continue
|
|
54
|
+
const c = readJson<{ version?: string }>(join(app, "extensions", "copilot", "package.json"))?.version
|
|
55
|
+
return { vscode: v, ...(c ? { copilot: c } : {}) }
|
|
56
|
+
}
|
|
57
|
+
return {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const line = (v: string | undefined) => v?.split(".").slice(0, 2).join(".")
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* VS Code's id for a single-folder workspace (workspaces.ts getSingleFolderWorkspaceIdentifier): the md5 of
|
|
64
|
+
* the folder path and, on macOS, its birth time in milliseconds. Node rounds the birth time to whole
|
|
65
|
+
* milliseconds where Bun truncates, so it is rounded here as VS Code (on Node) sees it.
|
|
66
|
+
*/
|
|
67
|
+
export function workspaceId(folder: string): string {
|
|
68
|
+
const birth = Math.round(statSync(folder).birthtimeMs)
|
|
69
|
+
return createHash("md5").update(folder).update(String(birth)).digest("hex")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A file:// URI as VS Code's URI.toString() writes it: everything but unreserved characters and / is escaped. */
|
|
73
|
+
export function fileUri(path: string): string {
|
|
74
|
+
let out = ""
|
|
75
|
+
for (const ch of path) out += /^[A-Za-z0-9\-._~/]$/.test(ch) ? ch : encodeURIComponent(ch)
|
|
76
|
+
return `file://${out}`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** VS Code's user folder under a home. */
|
|
80
|
+
export const userDir = (home: string) => join(appSupport(home), "Code", "User")
|
|
81
|
+
|
|
82
|
+
/** The agent descriptor VS Code serializes with each request (IChatAgentData of Copilot's agent mode). */
|
|
83
|
+
function agentData(extensionVersion: string) {
|
|
84
|
+
return {
|
|
85
|
+
extensionId: { value: "GitHub.copilot-chat", _lower: "github.copilot-chat" },
|
|
86
|
+
extensionVersion,
|
|
87
|
+
publisherDisplayName: "GitHub",
|
|
88
|
+
extensionPublisherId: "GitHub",
|
|
89
|
+
extensionDisplayName: "GitHub Copilot Chat",
|
|
90
|
+
id: AGENT_ID,
|
|
91
|
+
description: "Edit files in your workspace in agent mode",
|
|
92
|
+
when: "config.chat.agent.enabled",
|
|
93
|
+
metadata: { themeIcon: { id: "tools" }, hasFollowups: false, supportIssueReporting: false },
|
|
94
|
+
name: "agent",
|
|
95
|
+
fullName: "GitHub Copilot",
|
|
96
|
+
isDefault: true,
|
|
97
|
+
locations: ["panel"],
|
|
98
|
+
modes: ["agent"],
|
|
99
|
+
slashCommands: [],
|
|
100
|
+
disambiguation: [],
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Copilot's own names for what the call does (as its model sees them in 0.72), where the arguments map
|
|
105
|
+
// cleanly; anything else keeps its name.
|
|
106
|
+
const TOOLS: ToolMap = {
|
|
107
|
+
read: (i) => {
|
|
108
|
+
const filePath = pick(i, ARG.path)
|
|
109
|
+
return filePath ? { name: "read_file", args: { filePath } } : undefined
|
|
110
|
+
},
|
|
111
|
+
write: (i) => {
|
|
112
|
+
const filePath = pick(i, ARG.path)
|
|
113
|
+
const content = pick(i, ARG.content)
|
|
114
|
+
return filePath && content !== undefined ? { name: "create_file", args: { filePath, content } } : undefined
|
|
115
|
+
},
|
|
116
|
+
edit: (i) => {
|
|
117
|
+
const filePath = pick(i, ARG.path)
|
|
118
|
+
const oldString = pick(i, ARG.old)
|
|
119
|
+
const newString = pick(i, ARG.new)
|
|
120
|
+
return filePath && oldString !== undefined && newString !== undefined ? { name: "replace_string_in_file", args: { filePath, oldString, newString } } : undefined
|
|
121
|
+
},
|
|
122
|
+
shell: (i) => {
|
|
123
|
+
const command = pick(i, ARG.command)
|
|
124
|
+
return command ? { name: "run_in_terminal", args: { command, explanation: "", isBackground: false } } : undefined
|
|
125
|
+
},
|
|
126
|
+
list: (i) => {
|
|
127
|
+
const path = pick(i, ARG.dir)
|
|
128
|
+
return path ? { name: "list_dir", args: { path } } : undefined
|
|
129
|
+
},
|
|
130
|
+
search: (i) => {
|
|
131
|
+
const query = pick(i, ARG.query)
|
|
132
|
+
return query ? { name: "grep_search", args: { query, isRegexp: false } } : undefined
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const markdown = (value: string) => ({ value, supportThemeIcons: false, supportHtml: false })
|
|
137
|
+
|
|
138
|
+
/** A parsed request message with one text part, as ChatRequestTextPart serializes. */
|
|
139
|
+
function message(text: string) {
|
|
140
|
+
const lines = text.split("\n")
|
|
141
|
+
return {
|
|
142
|
+
text,
|
|
143
|
+
parts: [
|
|
144
|
+
{
|
|
145
|
+
range: { start: 0, endExclusive: text.length },
|
|
146
|
+
editorRange: { startLineNumber: 1, startColumn: 1, endLineNumber: lines.length, endColumn: lines[lines.length - 1]!.length + 1 },
|
|
147
|
+
text,
|
|
148
|
+
kind: "text",
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type Call = Extract<Turn, { kind: "tool_call" }>
|
|
155
|
+
type Result = Extract<Turn, { kind: "tool_result" }>
|
|
156
|
+
|
|
157
|
+
interface Round {
|
|
158
|
+
id: string
|
|
159
|
+
response: string
|
|
160
|
+
calls: Array<{ id: string; name: string; arguments: string; source: Call }>
|
|
161
|
+
/** Reasoning shown before this round in the chat. */
|
|
162
|
+
thinking: string[]
|
|
163
|
+
/** A result for one of this round's calls has come back: the model call is over. */
|
|
164
|
+
closed: boolean
|
|
165
|
+
summary?: string
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface Req {
|
|
169
|
+
at?: number
|
|
170
|
+
text: string
|
|
171
|
+
/** A request the chat shows as a system notification: the continued-from note or a compaction. */
|
|
172
|
+
system?: string
|
|
173
|
+
/** This request's text is the continued-from note itself. */
|
|
174
|
+
isNote?: boolean
|
|
175
|
+
rounds: Round[]
|
|
176
|
+
/** Pasted images, as the chat's attachment entries (variableData.variables). */
|
|
177
|
+
images?: Record<string, unknown>[]
|
|
178
|
+
model?: string
|
|
179
|
+
usage?: { input?: number; output?: number }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface ChatPlan {
|
|
183
|
+
/** The ISerializableChatData written as the log's Initial entry. */
|
|
184
|
+
data: Record<string, unknown>
|
|
185
|
+
/** The session's entry in chat.ChatSessionStore.index. */
|
|
186
|
+
entry: Record<string, unknown>
|
|
187
|
+
lost: string[]
|
|
188
|
+
/** What a read-back must find. */
|
|
189
|
+
expect: { users: number; calls: number }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A pasted image as VS Code 1.144 stores it in a request's variableData.variables: the chat attachment entry
|
|
194
|
+
* {kind:"image", id, name, value, mimeType, isPasted, references} with the bytes as {$base64} (mw.toExport turns a Uint8Array
|
|
195
|
+
* value into that and mw.fromExport back; Copilot Chat then builds a ChatReferenceBinaryData from value and mimeType, and
|
|
196
|
+
* ignores an entry with no value). Undefined when the image is gone, too large or not a format Copilot takes.
|
|
197
|
+
*/
|
|
198
|
+
export function imageVariable(a: Attachment): Record<string, unknown> | undefined {
|
|
199
|
+
const url = imageDataUrl(a)
|
|
200
|
+
const img = url ? parseDataUrl(url) : undefined
|
|
201
|
+
if (!img) return undefined
|
|
202
|
+
return {
|
|
203
|
+
kind: "image",
|
|
204
|
+
id: createHash("sha256").update(Buffer.from(img.base64, "base64")).digest("hex"),
|
|
205
|
+
name: a.name ?? "Pasted Image",
|
|
206
|
+
icon: { id: "file-media" },
|
|
207
|
+
value: { $base64: img.base64 },
|
|
208
|
+
mimeType: img.mime,
|
|
209
|
+
isPasted: true,
|
|
210
|
+
references: [],
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The chat VS Code would store for `session` continued as local chat `sessionId`. Pure. */
|
|
215
|
+
export function planChat(session: Session, opts: { sessionId: string; now: Date; extensionVersion: string }): ChatPlan {
|
|
216
|
+
const label = toolLabel(session.header.tool)
|
|
217
|
+
const drops = new Map<string, number>()
|
|
218
|
+
const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
|
|
219
|
+
const results = new Map<string, Result>()
|
|
220
|
+
const callIds = new Set<string>()
|
|
221
|
+
for (const t of session.turns) {
|
|
222
|
+
if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
|
|
223
|
+
if (t.kind === "tool_call") callIds.add(t.id)
|
|
224
|
+
}
|
|
225
|
+
const note = continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10))
|
|
226
|
+
|
|
227
|
+
const reqs: Req[] = []
|
|
228
|
+
let req: Req | undefined
|
|
229
|
+
let round: Round | undefined
|
|
230
|
+
let pendingThinking: string[] = []
|
|
231
|
+
let model = session.header.models.at(-1)
|
|
232
|
+
const seen = new Set<string>()
|
|
233
|
+
const roundId = () => randomUUID()
|
|
234
|
+
// Call ids: kept when every provider accepts them, else replaced, the same way for the call and its result.
|
|
235
|
+
const ids = new Map<string, string>()
|
|
236
|
+
const callId = (id: string) => {
|
|
237
|
+
let v = ids.get(id)
|
|
238
|
+
if (!v) {
|
|
239
|
+
v = /^[A-Za-z0-9_-]{1,64}$/.test(id) && ![...ids.values()].includes(id) ? id : `call_${randomUUID().replace(/-/g, "").slice(0, 24)}`
|
|
240
|
+
ids.set(id, v)
|
|
241
|
+
}
|
|
242
|
+
return v
|
|
243
|
+
}
|
|
244
|
+
const at = (s?: string) => {
|
|
245
|
+
const t = s ? Date.parse(s) : Number.NaN
|
|
246
|
+
return Number.isFinite(t) ? t : undefined
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const openRequest = (text: string, when?: string, system?: string, isNote = false) => {
|
|
250
|
+
req = { text, rounds: [], ...(system ? { system } : {}), ...(isNote ? { isNote } : {}), ...(at(when) ? { at: at(when) } : {}) }
|
|
251
|
+
reqs.push(req)
|
|
252
|
+
round = undefined
|
|
253
|
+
return req
|
|
254
|
+
}
|
|
255
|
+
const currentRound = (when?: string): Round => {
|
|
256
|
+
// Assistant output before any request opens one that carries the note in place of a person's words.
|
|
257
|
+
if (!req) openRequest(note, when, `Continued from ${label}`, true)
|
|
258
|
+
if (!round || round.closed) {
|
|
259
|
+
round = { id: roundId(), response: "", calls: [], thinking: pendingThinking, closed: false }
|
|
260
|
+
pendingThinking = []
|
|
261
|
+
req!.rounds.push(round)
|
|
262
|
+
}
|
|
263
|
+
return round
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const t of session.turns) {
|
|
267
|
+
switch (t.kind) {
|
|
268
|
+
case "user": {
|
|
269
|
+
let text = t.text
|
|
270
|
+
const images: Record<string, unknown>[] = []
|
|
271
|
+
for (const a of t.attachments ?? []) {
|
|
272
|
+
if (a.kind === "file" && a.path) text += `\n\n[Attached file: ${a.path}]`
|
|
273
|
+
else {
|
|
274
|
+
const v = imageVariable(a)
|
|
275
|
+
if (v) images.push(v)
|
|
276
|
+
else drop("attachments")
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!text.trim()) {
|
|
280
|
+
if (images.length) drop("attachments without text", images.length)
|
|
281
|
+
break
|
|
282
|
+
}
|
|
283
|
+
if (pendingThinking.length) drop("reasoning with no reply after it", pendingThinking.length)
|
|
284
|
+
pendingThinking = []
|
|
285
|
+
openRequest(text, t.at).images = images
|
|
286
|
+
break
|
|
287
|
+
}
|
|
288
|
+
case "assistant": {
|
|
289
|
+
if (!t.text.trim()) break
|
|
290
|
+
if (t.model) model = t.model
|
|
291
|
+
// Text after a tool call is the next model call.
|
|
292
|
+
if (round?.calls.length) round.closed = true
|
|
293
|
+
const r = currentRound(t.at)
|
|
294
|
+
r.response = r.response ? `${r.response}\n\n${t.text}` : t.text
|
|
295
|
+
req!.model = model
|
|
296
|
+
if (t.usage) req!.usage = { input: (req!.usage?.input ?? 0) + (t.usage.input ?? 0), output: (req!.usage?.output ?? 0) + (t.usage.output ?? 0) }
|
|
297
|
+
if (!req!.at && at(t.at)) req!.at = at(t.at)
|
|
298
|
+
break
|
|
299
|
+
}
|
|
300
|
+
case "tool_call": {
|
|
301
|
+
if (seen.has(t.id)) {
|
|
302
|
+
drop("repeated tool call ids")
|
|
303
|
+
break
|
|
304
|
+
}
|
|
305
|
+
seen.add(t.id)
|
|
306
|
+
const r = currentRound(t.at)
|
|
307
|
+
const mapped = mapTool(TOOLS, t.toolKind, t.input)
|
|
308
|
+
r.calls.push({ id: callId(t.id), name: mapped?.name ?? safeToolName(t.name), arguments: JSON.stringify(mapped?.args ?? asArgs(t.input)), source: t })
|
|
309
|
+
if (model) req!.model = model
|
|
310
|
+
break
|
|
311
|
+
}
|
|
312
|
+
case "tool_result":
|
|
313
|
+
if (!callIds.has(t.callId)) drop("tool results without a call")
|
|
314
|
+
else if (round?.calls.some((c) => c.source.id === t.callId)) round.closed = true
|
|
315
|
+
break
|
|
316
|
+
case "reasoning":
|
|
317
|
+
// Readable reasoning is shown as the chat's thinking part; Copilot replays only its provider's own.
|
|
318
|
+
if (t.text.trim()) {
|
|
319
|
+
pendingThinking.push(t.text)
|
|
320
|
+
drop("reasoning (shown in the chat, not sent back to the model)")
|
|
321
|
+
}
|
|
322
|
+
break
|
|
323
|
+
case "compaction": {
|
|
324
|
+
const r = openRequest(`Summary of the earlier conversation in ${label}`, t.at, `Earlier conversation in ${label}, summarized`)
|
|
325
|
+
round = { id: roundId(), response: "", calls: [], thinking: [], closed: true, summary: t.summary }
|
|
326
|
+
r.rounds.push(round)
|
|
327
|
+
break
|
|
328
|
+
}
|
|
329
|
+
case "file_edit":
|
|
330
|
+
if (!t.callId || !callIds.has(t.callId)) drop("file edits outside a tool call")
|
|
331
|
+
break
|
|
332
|
+
case "subagent":
|
|
333
|
+
drop("subagent sessions (their call and result are kept)")
|
|
334
|
+
break
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (pendingThinking.length) drop("reasoning with no reply after it", pendingThinking.length)
|
|
338
|
+
if (!reqs.length) throw new Error("nothing to continue: the session has no messages")
|
|
339
|
+
|
|
340
|
+
// Requests, with timestamps that never go backwards.
|
|
341
|
+
let clock = (reqs.find((r) => r.at)?.at ?? opts.now.getTime()) - 1
|
|
342
|
+
const tick = (t?: number) => (clock = t && t > clock ? t : clock + 1)
|
|
343
|
+
const agent = agentData(opts.extensionVersion)
|
|
344
|
+
const copilotSession = randomUUID()
|
|
345
|
+
let users = 0
|
|
346
|
+
let calls = 0
|
|
347
|
+
let noteGiven = reqs.some((r) => r.isNote)
|
|
348
|
+
const requests = reqs.map((r) => {
|
|
349
|
+
const timestamp = tick(r.at)
|
|
350
|
+
const toolCallResults: Record<string, unknown> = {}
|
|
351
|
+
const response: unknown[] = []
|
|
352
|
+
for (const round of r.rounds) {
|
|
353
|
+
for (const text of round.thinking) response.push({ kind: "thinking", value: text })
|
|
354
|
+
if (round.response) response.push(markdown(round.response))
|
|
355
|
+
for (const c of round.calls) {
|
|
356
|
+
const res = results.get(c.source.id)
|
|
357
|
+
if (!res) drop("tool calls without a result (closed as errors)")
|
|
358
|
+
const ok = res?.ok ?? false
|
|
359
|
+
let output = res?.output ?? "No result was recorded for this call; the session moved before it finished."
|
|
360
|
+
if (res?.fullPath) output += `\n\n[Full output: ${res.fullPath}]`
|
|
361
|
+
// Copilot's tools report failure in the text, and so does the reader.
|
|
362
|
+
if (!ok && !/^\s*error\b/i.test(output)) output = `Error: ${output}`
|
|
363
|
+
toolCallResults[c.id] = { $mid: 20, content: [{ $mid: 21, value: output }] }
|
|
364
|
+
response.push({
|
|
365
|
+
kind: "toolInvocationSerialized",
|
|
366
|
+
invocationMessage: `Ran \`${c.name}\``,
|
|
367
|
+
pastTenseMessage: `Ran \`${c.name}\``,
|
|
368
|
+
resultDetails: { input: c.arguments, output: [{ type: "embed", isText: true, value: output }], ...(ok ? {} : { isError: true }) },
|
|
369
|
+
isConfirmed: { type: 1 },
|
|
370
|
+
isComplete: true,
|
|
371
|
+
toolCallId: c.id,
|
|
372
|
+
toolId: c.name,
|
|
373
|
+
})
|
|
374
|
+
calls++
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const rounds = r.rounds.map((x) => ({ id: x.id, response: x.response, toolCalls: x.calls.map((c) => ({ id: c.id, name: c.name, arguments: c.arguments })), toolInputRetry: 0 }))
|
|
378
|
+
const summaries = r.rounds.filter((x) => x.summary).map((x) => ({ toolCallRoundId: x.id, text: x.summary! }))
|
|
379
|
+
if (summaries.length) response.unshift(markdown(summaries.at(-1)!.text))
|
|
380
|
+
const answered = rounds.length > 0
|
|
381
|
+
const completedAt = tick()
|
|
382
|
+
const metadata: Record<string, unknown> = {
|
|
383
|
+
...(answered ? { toolCallRounds: rounds, toolCallResults } : {}),
|
|
384
|
+
...(summaries.length ? { summaries } : {}),
|
|
385
|
+
modelMessageId: randomUUID(),
|
|
386
|
+
responseId: randomUUID(),
|
|
387
|
+
sessionId: copilotSession,
|
|
388
|
+
agentId: AGENT_ID,
|
|
389
|
+
codeBlocks: [],
|
|
390
|
+
}
|
|
391
|
+
if (!r.system) {
|
|
392
|
+
users++
|
|
393
|
+
if (!noteGiven) {
|
|
394
|
+
// Sent to the model in place of the request text; the chat shows only the person's words.
|
|
395
|
+
metadata.renderedUserMessage = [{ type: 1, text: `${note}\n\n${r.text}` }]
|
|
396
|
+
noteGiven = true
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
requestId: `request_${randomUUID()}`,
|
|
401
|
+
timestamp,
|
|
402
|
+
message: message(r.text),
|
|
403
|
+
variableData: { variables: r.images ?? [] },
|
|
404
|
+
response,
|
|
405
|
+
responseId: `response_${randomUUID()}`,
|
|
406
|
+
// A request the session moved on from before any reply is kept as a cancelled one, as VS Code keeps them.
|
|
407
|
+
result: answered
|
|
408
|
+
? { timings: { firstProgress: 0, totalElapsed: completedAt - timestamp }, metadata, ...(r.model ? { details: r.model } : {}) }
|
|
409
|
+
: { errorDetails: { message: "Cancelled" }, metadata },
|
|
410
|
+
followups: [],
|
|
411
|
+
modelState: { value: answered ? 1 : 2, completedAt },
|
|
412
|
+
agent,
|
|
413
|
+
contentReferences: [],
|
|
414
|
+
codeCitations: [],
|
|
415
|
+
timeSpentWaiting: timestamp,
|
|
416
|
+
...(r.usage?.input !== undefined ? { promptTokens: r.usage.input } : {}),
|
|
417
|
+
...(r.usage?.output !== undefined ? { completionTokens: r.usage.output } : {}),
|
|
418
|
+
...(r.system ? { isSystemInitiated: true, systemInitiatedLabel: r.system } : {}),
|
|
419
|
+
}
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
const title = `${(session.header.title ?? firstUserText(session) ?? "Session").replace(/\s+/g, " ").trim().slice(0, 80)} (from ${label})`
|
|
423
|
+
const created = requests[0]!.timestamp
|
|
424
|
+
const last = requests.at(-1)!
|
|
425
|
+
const data = {
|
|
426
|
+
version: TESTED_VERSION,
|
|
427
|
+
creationDate: created,
|
|
428
|
+
customTitle: title,
|
|
429
|
+
initialLocation: "panel",
|
|
430
|
+
// Restores in agent mode, so the next request goes to the agent whose history this is.
|
|
431
|
+
inputState: { attachments: [], mode: { id: "agent", kind: "agent" }, inputText: "", selections: [], contrib: {} },
|
|
432
|
+
responderUsername: "GitHub Copilot",
|
|
433
|
+
sessionId: opts.sessionId,
|
|
434
|
+
requests,
|
|
435
|
+
hasPendingEdits: false,
|
|
436
|
+
pendingRequests: [],
|
|
437
|
+
}
|
|
438
|
+
const entry = {
|
|
439
|
+
sessionId: opts.sessionId,
|
|
440
|
+
title,
|
|
441
|
+
lastMessageDate: last.timestamp,
|
|
442
|
+
timing: { created, lastRequestStarted: last.timestamp, lastRequestEnded: last.modelState.completedAt },
|
|
443
|
+
initialLocation: "panel",
|
|
444
|
+
hasPendingEdits: false,
|
|
445
|
+
isEmpty: false,
|
|
446
|
+
isExternal: false,
|
|
447
|
+
lastResponseState: last.modelState.value,
|
|
448
|
+
}
|
|
449
|
+
const lost = [...drops].map(([what, n]) => `${what} (${n})`)
|
|
450
|
+
const remapped = [...ids].filter(([a, b]) => a !== b).length
|
|
451
|
+
if (remapped) lost.push(`tool call ids, replaced with ids every provider accepts (${remapped})`)
|
|
452
|
+
return { data, entry, lost, expect: { users, calls } }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const value = (db: Database, key: string): string | undefined => {
|
|
456
|
+
const row = db.query("SELECT value FROM ItemTable WHERE key = ?").get(key) as { value: string | Uint8Array } | null
|
|
457
|
+
return row ? (typeof row.value === "string" ? row.value : new TextDecoder().decode(row.value)) : undefined
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** The chat index in a workspace's state.vscdb, or a new one. */
|
|
461
|
+
function readIndex(db: Database): { version: 1; entries: Record<string, unknown> } {
|
|
462
|
+
const text = value(db, INDEX_KEY)
|
|
463
|
+
if (text === undefined) {
|
|
464
|
+
// With no index, VS Code moves a workspace's chats out of its older key on its next start; an index
|
|
465
|
+
// written first would stop that, so the person opens the folder once and VS Code moves them itself.
|
|
466
|
+
if (value(db, "interactive.sessions") !== undefined) throw new Error("this workspace still has chats in VS Code's older store; open the folder in VS Code once (so it moves them), quit it, and try again")
|
|
467
|
+
return { version: 1, entries: {} }
|
|
468
|
+
}
|
|
469
|
+
const index = JSON.parse(text)
|
|
470
|
+
if (index?.version !== 1 || typeof index.entries !== "object" || index.entries === null) throw new Error(`the chat index in ${db.filename} has a format this writer does not know (version ${String(index?.version)})`)
|
|
471
|
+
return index
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** The Copilot Chat writer; `versions` and `running` are for tests, which must not depend on the installed app. */
|
|
475
|
+
export function vscodeCopilotWriter(opts: { versions?: { vscode?: string; copilot?: string }; running?: () => boolean } = {}): Writer {
|
|
476
|
+
const installed = (home: string) => opts.versions ?? installedVSCode(home)
|
|
477
|
+
const running = opts.running ?? (() => isRunning("Code"))
|
|
478
|
+
return {
|
|
479
|
+
tool: TOOL,
|
|
480
|
+
label: LABEL,
|
|
481
|
+
way: "native",
|
|
482
|
+
|
|
483
|
+
async ready(w: WriteOptions): Promise<Readiness> {
|
|
484
|
+
const { vscode, copilot } = installed(w.home)
|
|
485
|
+
if (!vscode) return { ok: false, why: "VS Code is not installed (no Visual Studio Code.app in /Applications or ~/Applications)" }
|
|
486
|
+
if (!TESTED_VSCODE.includes(line(vscode)!)) return { ok: false, why: `VS Code ${vscode} is not a version this writer was checked against (${TESTED_VSCODE.join(", ")})` }
|
|
487
|
+
if (!copilot || !TESTED_COPILOT.includes(line(copilot)!)) return { ok: false, why: `Copilot Chat ${copilot ?? "(not found)"} is not a version this writer was checked against (${TESTED_COPILOT.join(", ")})` }
|
|
488
|
+
if (running()) return { ok: false, why: "VS Code is running; quit it first (it keeps the chat index in a database it rewrites from memory)" }
|
|
489
|
+
if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; VS Code keeps chats per workspace folder` }
|
|
490
|
+
return { ok: true }
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
async write(session: Session, w: WriteOptions): Promise<WriteResult> {
|
|
494
|
+
if (!w.dryRun && running()) throw new Error("VS Code is running; quit it first")
|
|
495
|
+
const { copilot } = installed(w.home)
|
|
496
|
+
const sessionId = randomUUID()
|
|
497
|
+
const storage = join(userDir(w.home), "workspaceStorage", workspaceId(w.cwd))
|
|
498
|
+
const file = join(storage, "chatSessions", `${sessionId}.jsonl`)
|
|
499
|
+
const db = join(storage, "state.vscdb")
|
|
500
|
+
const plan = planChat(session, { sessionId, now: new Date(), extensionVersion: copilot ?? `${TESTED_COPILOT[0]}.0` })
|
|
501
|
+
const written = [file, db, ...(exists(join(storage, "workspace.json")) ? [] : [join(storage, "workspace.json")])]
|
|
502
|
+
let backedUp: string | undefined
|
|
503
|
+
if (!w.dryRun) {
|
|
504
|
+
mkdirSync(join(storage, "chatSessions"), { recursive: true })
|
|
505
|
+
// VS Code writes this beside a workspace's storage the first time it opens the folder.
|
|
506
|
+
if (!exists(join(storage, "workspace.json"))) writeFileSync(join(storage, "workspace.json"), JSON.stringify({ folder: fileUri(w.cwd) }, undefined, 2))
|
|
507
|
+
writeFileSync(file, `${JSON.stringify({ kind: 0, v: plan.data })}\n`, { flag: "wx" })
|
|
508
|
+
try {
|
|
509
|
+
const back = await reader.read({ tool: TOOL, sourceId: sessionId, path: file }, w.home)
|
|
510
|
+
const n = countTurns(back)
|
|
511
|
+
const pairs = unpaired(back)
|
|
512
|
+
if (n.user !== plan.expect.users || n.tool_call !== plan.expect.calls || pairs.calls.length || pairs.results.length)
|
|
513
|
+
throw new Error(`the new chat did not read back intact (${n.user} of ${plan.expect.users} requests, ${n.tool_call} of ${plan.expect.calls} tool calls, ${pairs.calls.length + pairs.results.length} unpaired)`)
|
|
514
|
+
if (exists(db)) backedUp = backup(w.home, TOOL, [db])
|
|
515
|
+
const conn = new Database(db, { create: true })
|
|
516
|
+
try {
|
|
517
|
+
// The schema VS Code creates (base/parts/storage/node/storage.ts); a no-op on an existing store.
|
|
518
|
+
conn.exec("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)")
|
|
519
|
+
conn.transaction(() => {
|
|
520
|
+
const index = readIndex(conn)
|
|
521
|
+
index.entries[sessionId] = plan.entry
|
|
522
|
+
conn.query("INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)").run(INDEX_KEY, JSON.stringify(index))
|
|
523
|
+
})()
|
|
524
|
+
} finally {
|
|
525
|
+
conn.close()
|
|
526
|
+
}
|
|
527
|
+
} catch (e) {
|
|
528
|
+
rmSync(file, { force: true })
|
|
529
|
+
throw e
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
tool: TOOL,
|
|
534
|
+
way: "native",
|
|
535
|
+
sessionId,
|
|
536
|
+
written,
|
|
537
|
+
open: `code ${shellQuote(w.cwd)}`,
|
|
538
|
+
note: `In the Chat view, open the chat history (Show Chats…) and pick "${plan.entry.title}".${backedUp ? ` The workspace's state.vscdb was backed up to ${backedUp}.` : ""}`,
|
|
539
|
+
lost: plan.lost,
|
|
540
|
+
dryRun: !!w.dryRun,
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export const vscodeCopilotNativeWriter = vscodeCopilotWriter()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Reader, Writer } from "../types.ts"
|
|
2
|
+
import { zedReader } from "./reader.ts"
|
|
3
|
+
import { zedNativeWriter } from "./writer.ts"
|
|
4
|
+
|
|
5
|
+
// Zed's agent panel: reads native agent threads from threads.db, and continues any session as a new native
|
|
6
|
+
// thread there (written only while Zed is closed, after a backup).
|
|
7
|
+
export const readers: Reader[] = [zedReader]
|
|
8
|
+
export const writers: Writer[] = [zedNativeWriter]
|