teamcopilot 0.1.2 → 0.1.4
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/dist/chat/index.js +93 -12
- package/dist/frontend/assets/{cssMode-DPbQk08M.js → cssMode-DHaUFfVN.js} +1 -1
- package/dist/frontend/assets/{freemarker2-DFE4Z2hp.js → freemarker2-CTVtohp4.js} +1 -1
- package/dist/frontend/assets/{handlebars-Dw_PK21Y.js → handlebars-CI1GoG3L.js} +1 -1
- package/dist/frontend/assets/{html-DxZRNRpl.js → html-C1Kdg4Tl.js} +1 -1
- package/dist/frontend/assets/{htmlMode-iGgmnmtJ.js → htmlMode-CslwWXHV.js} +1 -1
- package/dist/frontend/assets/{index-CphuwuEr.js → index-CJMxNDke.js} +205 -205
- package/dist/frontend/assets/index-DrplZfJY.css +1 -0
- package/dist/frontend/assets/{javascript-Bw_u6aiq.js → javascript-D_jyP6QE.js} +1 -1
- package/dist/frontend/assets/{jsonMode-iBiD9w9m.js → jsonMode-BT5z_9Wi.js} +1 -1
- package/dist/frontend/assets/{liquid-nnkE2sbw.js → liquid-CATjnK_z.js} +1 -1
- package/dist/frontend/assets/{mdx-CHi7ETpD.js → mdx-B4zdWJY_.js} +1 -1
- package/dist/frontend/assets/{python--OTnmJv2.js → python-B_opZ_lQ.js} +1 -1
- package/dist/frontend/assets/{razor-C6OgnQD9.js → razor-BKopqk1g.js} +1 -1
- package/dist/frontend/assets/{tsMode-DzsKqqZD.js → tsMode-CMjXzcRc.js} +1 -1
- package/dist/frontend/assets/{typescript-BMZ0H3W7.js → typescript-lbL28oip.js} +1 -1
- package/dist/frontend/assets/{xml-HTDEz_LP.js → xml-BB8-GMIl.js} +1 -1
- package/dist/frontend/assets/{yaml-CVa5ypgE.js → yaml-B5S_93u4.js} +1 -1
- package/dist/frontend/index.html +2 -2
- package/dist/utils/approval-snapshot-common.js +2 -0
- package/dist/utils/chat-session-file-diff.js +120 -0
- package/dist/utils/workspace-sync.js +24 -0
- package/dist/workspace_files/.opencode/plugins/apply-patch-session-diff.ts +343 -0
- package/dist/workspace_files/package.json +1 -1
- package/package.json +1 -1
- package/prisma/generated/client/edge.js +18 -3
- package/prisma/generated/client/index-browser.js +15 -0
- package/prisma/generated/client/index.d.ts +2067 -173
- package/prisma/generated/client/index.js +18 -3
- package/prisma/generated/client/package.json +1 -1
- package/prisma/generated/client/schema.prisma +22 -3
- package/prisma/generated/client/wasm.js +18 -3
- package/prisma/migrations/20260324144454_add_chat_session_tracked_files/migration.sql +21 -0
- package/prisma/schema.prisma +20 -1
- package/dist/frontend/assets/index-Ds8n3J4W.css +0 -1
|
@@ -14,6 +14,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
14
14
|
const ignore_1 = __importDefault(require("ignore"));
|
|
15
15
|
const child_process_1 = require("child_process");
|
|
16
16
|
const util_1 = require("util");
|
|
17
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
17
18
|
const assert_1 = require("./assert");
|
|
18
19
|
const runtime_paths_1 = require("./runtime-paths");
|
|
19
20
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
@@ -22,6 +23,7 @@ const WORKSPACE_DB_FILENAME = "data.db";
|
|
|
22
23
|
const HONEYTOKEN_UUID = "1f9f0b72-5f9f-4c9b-aef1-2fb2e0f6d8c4";
|
|
23
24
|
const HONEYTOKEN_FILE_NAME = `honeytoken-${HONEYTOKEN_UUID}.txt`;
|
|
24
25
|
const WORKSPACE_AZURE_PROVIDER_VERSION = "3.0.48";
|
|
26
|
+
const WORKSPACE_INSTALL_STATE_RELATIVE_PATH = path_1.default.join(".opencode", "install-state.json");
|
|
25
27
|
function getWorkspaceDirFromEnv() {
|
|
26
28
|
let workspaceDir = (0, assert_1.assertEnv)("WORKSPACE_DIR");
|
|
27
29
|
if (!path_1.default.isAbsolute(workspaceDir)) {
|
|
@@ -59,6 +61,9 @@ function shouldSkipManagedDirectoryContent(relativePath) {
|
|
|
59
61
|
if (relativePath === ".opencode/xdg-data" || relativePath.startsWith(".opencode/xdg-data/")) {
|
|
60
62
|
return true;
|
|
61
63
|
}
|
|
64
|
+
if (relativePath === normalizeRelativePath(WORKSPACE_INSTALL_STATE_RELATIVE_PATH)) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
62
67
|
return false;
|
|
63
68
|
}
|
|
64
69
|
function evaluateIgnoreRuleSets(relativePath, isDirectory, ruleSets) {
|
|
@@ -204,11 +209,30 @@ async function initializeWorkspaceNodeDependencies(workspaceDir) {
|
|
|
204
209
|
...existingPackageJson,
|
|
205
210
|
dependencies,
|
|
206
211
|
};
|
|
212
|
+
const installStatePath = path_1.default.join(workspaceDir, WORKSPACE_INSTALL_STATE_RELATIVE_PATH);
|
|
213
|
+
const desiredInstallState = {
|
|
214
|
+
dependencies,
|
|
215
|
+
};
|
|
216
|
+
const desiredInstallStateHash = crypto_1.default
|
|
217
|
+
.createHash("sha256")
|
|
218
|
+
.update(JSON.stringify(desiredInstallState))
|
|
219
|
+
.digest("hex");
|
|
220
|
+
const existingInstallState = fs_1.default.existsSync(installStatePath)
|
|
221
|
+
? JSON.parse(fs_1.default.readFileSync(installStatePath, "utf-8"))
|
|
222
|
+
: null;
|
|
223
|
+
const hasMatchingInstallState = existingInstallState?.hash === desiredInstallStateHash;
|
|
224
|
+
const nodeModulesPath = path_1.default.join(workspaceDir, "node_modules");
|
|
225
|
+
const packageJsonMatches = JSON.stringify(existingPackageJson) === JSON.stringify(workspacePackageJson);
|
|
226
|
+
if (hasMatchingInstallState && packageJsonMatches && fs_1.default.existsSync(nodeModulesPath)) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
207
229
|
fs_1.default.writeFileSync(workspacePackageJsonPath, JSON.stringify(workspacePackageJson, null, 2), "utf-8");
|
|
208
230
|
await execFileAsync("npm", ["install"], {
|
|
209
231
|
cwd: workspaceDir,
|
|
210
232
|
env: process.env,
|
|
211
233
|
});
|
|
234
|
+
fs_1.default.mkdirSync(path_1.default.dirname(installStatePath), { recursive: true });
|
|
235
|
+
fs_1.default.writeFileSync(installStatePath, `${JSON.stringify({ hash: desiredInstallStateHash }, null, 2)}\n`, "utf-8");
|
|
212
236
|
}
|
|
213
237
|
async function initializeWorkspaceDirectory() {
|
|
214
238
|
const workspaceDir = getWorkspaceDirFromEnv();
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
4
|
+
|
|
5
|
+
function getApiBaseUrl(): string {
|
|
6
|
+
const port = process.env.TEAMCOPILOT_PORT?.trim()
|
|
7
|
+
if (!port) {
|
|
8
|
+
throw new Error("TEAMCOPILOT_PORT must be set.")
|
|
9
|
+
}
|
|
10
|
+
return `http://localhost:${port}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SessionLookupResponse {
|
|
14
|
+
error?: unknown
|
|
15
|
+
data?: {
|
|
16
|
+
id?: string
|
|
17
|
+
parentID?: string
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type ToolArgs = Record<string, unknown> | undefined
|
|
22
|
+
|
|
23
|
+
function findPatchPayload(value: unknown): string | null {
|
|
24
|
+
if (typeof value === "string") {
|
|
25
|
+
const normalized = value.replace(/\r\n/g, "\n")
|
|
26
|
+
if (normalized.includes("*** Begin Patch")) {
|
|
27
|
+
return normalized
|
|
28
|
+
}
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!value || typeof value !== "object") {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const nestedValue of Object.values(value as Record<string, unknown>)) {
|
|
37
|
+
const match = findPatchPayload(nestedValue)
|
|
38
|
+
if (match) {
|
|
39
|
+
return match
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractTrackedPathsFromPatch(patchText: string): string[] {
|
|
47
|
+
const normalized = patchText.replace(/\r\n/g, "\n")
|
|
48
|
+
const fileHeaderMatches = normalized.matchAll(/^\s*\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)
|
|
49
|
+
const paths = Array.from(fileHeaderMatches, (match) => match[1].trim()).filter(
|
|
50
|
+
(candidate) => candidate.length > 0
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if (paths.length === 0) {
|
|
54
|
+
throw new Error("Could not determine target path from apply_patch payload.")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const moveToMatches = normalized.matchAll(/^\s*\*\*\* Move to: (.+)$/gm)
|
|
58
|
+
for (const match of moveToMatches) {
|
|
59
|
+
const targetPath = match[1].trim()
|
|
60
|
+
if (targetPath.length > 0) {
|
|
61
|
+
paths.push(targetPath)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return Array.from(new Set(paths.filter((candidate) => candidate.length > 0)))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findNestedStringByKeys(value: unknown, keys: Set<string>): string | null {
|
|
69
|
+
if (!value || typeof value !== "object") {
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const [key, nestedValue] of Object.entries(value as Record<string, unknown>)) {
|
|
74
|
+
if (keys.has(key) && typeof nestedValue === "string" && nestedValue.trim().length > 0) {
|
|
75
|
+
return nestedValue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const nestedMatch = findNestedStringByKeys(nestedValue, keys)
|
|
79
|
+
if (nestedMatch) {
|
|
80
|
+
return nestedMatch
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function tokenizeCommand(command: string): string[] {
|
|
88
|
+
const tokens = command.match(/"[^"]*"|'[^']*'|&&|\|\||[;|]|[^\s]+/g) ?? []
|
|
89
|
+
return tokens.map((token) => {
|
|
90
|
+
if (
|
|
91
|
+
(token.startsWith("\"") && token.endsWith("\"")) ||
|
|
92
|
+
(token.startsWith("'") && token.endsWith("'"))
|
|
93
|
+
) {
|
|
94
|
+
return token.slice(1, -1)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return token
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const CONTROL_TOKENS = new Set(["&&", "||", ";", "|"])
|
|
102
|
+
|
|
103
|
+
function resolveExecutionDirectory(rawCwd: unknown, fallbackDirectory: string): string {
|
|
104
|
+
if (typeof rawCwd !== "string" || rawCwd.trim() === "") {
|
|
105
|
+
return fallbackDirectory
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return path.isAbsolute(rawCwd) ? rawCwd : path.resolve(fallbackDirectory, rawCwd)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveTrackedDeleteTargets(command: string, executionDirectory: string): string[] {
|
|
112
|
+
const tokens = tokenizeCommand(command.trim())
|
|
113
|
+
if (tokens.length === 0) {
|
|
114
|
+
return []
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const resolvedTargets: string[] = []
|
|
118
|
+
let currentDirectory = executionDirectory
|
|
119
|
+
let index = 0
|
|
120
|
+
let atCommandStart = true
|
|
121
|
+
|
|
122
|
+
while (index < tokens.length) {
|
|
123
|
+
const token = tokens[index]
|
|
124
|
+
|
|
125
|
+
if (CONTROL_TOKENS.has(token)) {
|
|
126
|
+
atCommandStart = true
|
|
127
|
+
index += 1
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!atCommandStart) {
|
|
132
|
+
index += 1
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (token === "cd") {
|
|
137
|
+
const destination = tokens[index + 1]
|
|
138
|
+
if (destination && !CONTROL_TOKENS.has(destination)) {
|
|
139
|
+
currentDirectory = path.isAbsolute(destination)
|
|
140
|
+
? path.resolve(destination)
|
|
141
|
+
: path.resolve(currentDirectory, destination)
|
|
142
|
+
index += 2
|
|
143
|
+
} else {
|
|
144
|
+
index += 1
|
|
145
|
+
}
|
|
146
|
+
atCommandStart = false
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (path.basename(token) === "rm") {
|
|
151
|
+
index += 1
|
|
152
|
+
while (index < tokens.length && !CONTROL_TOKENS.has(tokens[index])) {
|
|
153
|
+
const candidate = tokens[index]
|
|
154
|
+
if (candidate === "--" || candidate.startsWith("-")) {
|
|
155
|
+
index += 1
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const resolvedCandidate = path.isAbsolute(candidate)
|
|
160
|
+
? path.resolve(candidate)
|
|
161
|
+
: path.resolve(currentDirectory, candidate)
|
|
162
|
+
if (fs.existsSync(resolvedCandidate) && fs.statSync(resolvedCandidate).isDirectory()) {
|
|
163
|
+
index += 1
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
resolvedTargets.push(resolvedCandidate)
|
|
167
|
+
index += 1
|
|
168
|
+
}
|
|
169
|
+
atCommandStart = false
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
atCommandStart = false
|
|
174
|
+
index += 1
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return Array.from(new Set(resolvedTargets))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function normalizeRelativePath(rawPath: string, workspaceDir: string): string {
|
|
181
|
+
const trimmed = rawPath.trim()
|
|
182
|
+
if (!trimmed) {
|
|
183
|
+
throw new Error("apply_patch hook received an empty file path.")
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const resolvedPath = path.isAbsolute(trimmed)
|
|
187
|
+
? path.resolve(trimmed)
|
|
188
|
+
: path.resolve(workspaceDir, trimmed)
|
|
189
|
+
const relativePath = path.relative(workspaceDir, resolvedPath)
|
|
190
|
+
|
|
191
|
+
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
192
|
+
throw new Error(`apply_patch path '${trimmed}' is outside the workspace.`)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return relativePath.split(path.sep).join("/")
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function tryNormalizeRelativePath(rawPath: string, workspaceDir: string): string | null {
|
|
199
|
+
try {
|
|
200
|
+
return normalizeRelativePath(rawPath, workspaceDir)
|
|
201
|
+
} catch {
|
|
202
|
+
return null
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function readErrorMessageFromResponse(
|
|
207
|
+
response: Response,
|
|
208
|
+
fallbackMessage: string
|
|
209
|
+
): Promise<string> {
|
|
210
|
+
try {
|
|
211
|
+
const text = await response.text()
|
|
212
|
+
if (!text) return fallbackMessage
|
|
213
|
+
try {
|
|
214
|
+
const parsed: unknown = JSON.parse(text)
|
|
215
|
+
if (parsed && typeof parsed === "object" && "message" in parsed) {
|
|
216
|
+
const message = (parsed as { message?: unknown }).message
|
|
217
|
+
if (typeof message === "string" && message.trim().length > 0) {
|
|
218
|
+
return message
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
// Fall back to plain text.
|
|
223
|
+
}
|
|
224
|
+
return text.trim().length > 0 ? text : fallbackMessage
|
|
225
|
+
} catch {
|
|
226
|
+
return fallbackMessage
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function collectTrackedPathsForTool(
|
|
231
|
+
tool: string,
|
|
232
|
+
inputArgs: ToolArgs,
|
|
233
|
+
outputArgs: ToolArgs,
|
|
234
|
+
workspaceDir: string
|
|
235
|
+
): string[] {
|
|
236
|
+
if (tool === "apply_patch") {
|
|
237
|
+
const patchPayload = findPatchPayload(outputArgs) ?? findPatchPayload(inputArgs)
|
|
238
|
+
if (!patchPayload) {
|
|
239
|
+
return []
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return extractTrackedPathsFromPatch(patchPayload)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (tool === "write") {
|
|
246
|
+
const filepath =
|
|
247
|
+
findNestedStringByKeys(outputArgs, new Set(["filepath", "filePath"])) ??
|
|
248
|
+
findNestedStringByKeys(inputArgs, new Set(["filepath", "filePath"]))
|
|
249
|
+
if (!filepath) {
|
|
250
|
+
return []
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return [filepath]
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (tool === "bash") {
|
|
257
|
+
const command =
|
|
258
|
+
findNestedStringByKeys(outputArgs, new Set(["command", "cmd", "script", "arguments"])) ??
|
|
259
|
+
findNestedStringByKeys(inputArgs, new Set(["command", "cmd", "script", "arguments"]))
|
|
260
|
+
if (!command) {
|
|
261
|
+
return []
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const rawCwd =
|
|
265
|
+
(outputArgs && (outputArgs.workdir ?? outputArgs.cwd)) ??
|
|
266
|
+
(inputArgs && (inputArgs.workdir ?? inputArgs.cwd))
|
|
267
|
+
const executionDirectory = resolveExecutionDirectory(rawCwd, workspaceDir)
|
|
268
|
+
|
|
269
|
+
return resolveTrackedDeleteTargets(command, executionDirectory)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return []
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export const ApplyPatchSessionDiffPlugin: Plugin = async ({ client, directory }) => {
|
|
276
|
+
async function resolveRootSessionID(sessionID: string): Promise<string> {
|
|
277
|
+
let currentSessionID = sessionID
|
|
278
|
+
|
|
279
|
+
while (true) {
|
|
280
|
+
const response = (await client.session.get({
|
|
281
|
+
path: {
|
|
282
|
+
id: currentSessionID,
|
|
283
|
+
},
|
|
284
|
+
})) as SessionLookupResponse
|
|
285
|
+
if (response.error) {
|
|
286
|
+
throw new Error(`Failed to resolve root session for ${currentSessionID}`)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const parentID = response.data?.parentID
|
|
290
|
+
if (!parentID) {
|
|
291
|
+
return currentSessionID
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
currentSessionID = parentID
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function captureBaselineForPath(sessionID: string, relativePath: string): Promise<void> {
|
|
299
|
+
const response = await fetch(`${getApiBaseUrl()}/api/chat/sessions/file-diff/capture-baseline`, {
|
|
300
|
+
method: "POST",
|
|
301
|
+
headers: {
|
|
302
|
+
"Content-Type": "application/json",
|
|
303
|
+
Authorization: `Bearer ${sessionID}`,
|
|
304
|
+
},
|
|
305
|
+
body: JSON.stringify({
|
|
306
|
+
path: relativePath,
|
|
307
|
+
}),
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
if (!response.ok) {
|
|
311
|
+
const errorMessage = await readErrorMessageFromResponse(
|
|
312
|
+
response,
|
|
313
|
+
`Failed to capture file baseline for ${relativePath} (HTTP ${response.status})`
|
|
314
|
+
)
|
|
315
|
+
throw new Error(errorMessage)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
"tool.execute.before": async (input, output) => {
|
|
321
|
+
if (!["apply_patch", "write", "bash"].includes(input.tool)) {
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const rawSessionID = typeof input.sessionID === "string" ? input.sessionID.trim() : ""
|
|
326
|
+
if (!rawSessionID) {
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const rootSessionID = await resolveRootSessionID(rawSessionID)
|
|
331
|
+
const paths = collectTrackedPathsForTool(input.tool, input.args, output.args, directory)
|
|
332
|
+
for (const candidatePath of paths) {
|
|
333
|
+
const relativePath = tryNormalizeRelativePath(candidatePath, directory)
|
|
334
|
+
if (!relativePath) {
|
|
335
|
+
continue
|
|
336
|
+
}
|
|
337
|
+
await captureBaselineForPath(rootSessionID, relativePath)
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export default ApplyPatchSessionDiffPlugin
|