@agimon-ai/po-file-edit 0.0.0-alpha.2 → 0.0.0-alpha.21
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 +149 -21
- package/README.md +3 -3
- package/dist/apiContracts.cjs +1 -1
- package/dist/apiContracts.cjs.map +1 -1
- package/dist/apiContracts.mjs +1 -1
- package/dist/apiContracts.mjs.map +1 -1
- package/dist/extensions/native.mjs +10 -963
- package/dist/extensions/native.mjs.map +1 -1
- package/dist/extensions/server.cjs +337 -111
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.d.mts +2 -2
- package/dist/extensions/server.d.mts.map +1 -1
- package/dist/extensions/server.mjs +363 -138
- package/dist/extensions/server.mjs.map +1 -1
- package/dist/extensions/web.mjs +44 -354
- package/dist/extensions/web.mjs.map +1 -1
- package/package.json +24 -23
|
@@ -1,49 +1,203 @@
|
|
|
1
|
-
import { composeServerProviderOverrides, defineServerPlugin } from "@agimon-ai/po-core/
|
|
2
|
-
import { composeRootHooks, defineChannel, defineHook, defineRoot, defineRoute } from "@agimon-ai/po-core/extensionFile";
|
|
3
|
-
import
|
|
1
|
+
import { composeServerProviderOverrides, defineServerPlugin } from "@agimon-ai/po-core/serverPlugin";
|
|
2
|
+
import { composeRootHooks, composeRootInjects, defineChannel, defineHook, defineRoot, defineRoute, runCleanups } from "@agimon-ai/po-core/extensionFile";
|
|
3
|
+
import { PO_CHILD_SESSION_HOOK_REGISTRY_SERVICE, readPoChildSessionLabels } from "@agimon-ai/po-core/childSession";
|
|
4
|
+
import { WORKFLOW_STEP_SESSION_PROVENANCE } from "@agimon-ai/po-core/hubChannel";
|
|
5
|
+
import fs from "node:fs/promises";
|
|
4
6
|
import path from "node:path";
|
|
7
|
+
import { parseFileHeader } from "@agimon-ai/po-hashline";
|
|
8
|
+
import { computeFileTag, displayPath, resolveInputPath, resolveReadInputPath } from "@agimon-ai/po-hashline/files";
|
|
9
|
+
import fs$1 from "node:fs";
|
|
5
10
|
import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "@agimon-ai/po-core/childProcess";
|
|
6
|
-
import {
|
|
7
|
-
import ignore from "ignore";
|
|
8
|
-
import { execFile, execFileSync } from "node:child_process";
|
|
11
|
+
import { createPoTelemetry } from "@agimon-ai/po-telemetry";
|
|
9
12
|
import { createHash } from "node:crypto";
|
|
10
13
|
import { piAgentDirectory } from "@agimon-ai/po-core/piSettings";
|
|
14
|
+
import { execFile } from "node:child_process";
|
|
11
15
|
import { promisify } from "node:util";
|
|
12
|
-
import
|
|
16
|
+
import ignore from "ignore";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import { loadPoConfig, resolvePlanningPlansDirectory } from "@agimon-ai/po-config";
|
|
13
19
|
import { Hono } from "hono";
|
|
14
|
-
//#region src/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
//#region src/constants/editPresence.ts
|
|
21
|
+
const PRESENCE_NOTE_TITLE = "[Po: other active agents edited files in this result]";
|
|
22
|
+
const PRESENCE_NOTE_GUIDANCE = "Preserve unrelated edits. If coordination would help, you can use intercom to send to the session id, or ask the subagent by name if it is on your team. Contact is optional; this notice does not block editing.";
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/services/editPresence/index.ts
|
|
25
|
+
const PATH_TOOLS = /* @__PURE__ */ new Set([
|
|
26
|
+
"read",
|
|
27
|
+
"edit",
|
|
28
|
+
"write"
|
|
29
|
+
]);
|
|
30
|
+
const EDIT_TOOLS = /* @__PURE__ */ new Set(["edit", "write"]);
|
|
31
|
+
const MAX_GREP_FILES = 50;
|
|
32
|
+
const MAX_NOTE_LINES = 10;
|
|
33
|
+
const MINUTE_MS = 6e4;
|
|
34
|
+
const SHORT_ID_LENGTH = 8;
|
|
35
|
+
/** Pi's built-in grep prints match rows as `relative/path:line: text`. */
|
|
36
|
+
const PI_GREP_ROW = /^(.+?):(\d+): /u;
|
|
37
|
+
function forgetPresence(ledger, agentKey) {
|
|
38
|
+
for (const [file, entries] of ledger) {
|
|
39
|
+
entries.delete(agentKey);
|
|
40
|
+
if (entries.size === 0) ledger.delete(file);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function stringArg(args, key) {
|
|
44
|
+
if (typeof args !== "object" || args === null) return void 0;
|
|
45
|
+
const value = args[key];
|
|
46
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
47
|
+
}
|
|
48
|
+
function textOf(content) {
|
|
49
|
+
return content.map((part) => part.type === "text" && "text" in part && typeof part.text === "string" ? part.text : "").join("\n");
|
|
50
|
+
}
|
|
51
|
+
async function grepFiles(call) {
|
|
52
|
+
const lines = textOf(call.content).split("\n");
|
|
53
|
+
const headers = lines.flatMap((line) => {
|
|
54
|
+
const header = parseFileHeader(line);
|
|
55
|
+
return header === void 0 ? [] : [resolveInputPath(header.path, call.cwd)];
|
|
56
|
+
});
|
|
57
|
+
if (headers.length > 0) return [...new Set(headers)].slice(0, MAX_GREP_FILES);
|
|
58
|
+
const rows = lines.flatMap((line) => {
|
|
59
|
+
const match = PI_GREP_ROW.exec(line)?.[1];
|
|
60
|
+
return match === void 0 ? [] : [match];
|
|
61
|
+
});
|
|
62
|
+
if (rows.length === 0) return [];
|
|
63
|
+
const searchPath = resolveInputPath(stringArg(call.args, "path") ?? ".", call.cwd);
|
|
64
|
+
if ((await fs.stat(searchPath).catch(() => void 0))?.isFile()) return [searchPath];
|
|
65
|
+
return [...new Set(rows.map((row) => path.resolve(searchPath, row)))].slice(0, MAX_GREP_FILES);
|
|
66
|
+
}
|
|
67
|
+
async function touchedFiles(call) {
|
|
68
|
+
if (PATH_TOOLS.has(call.toolName)) {
|
|
69
|
+
const input = stringArg(call.args, "path");
|
|
70
|
+
return input === void 0 ? [] : [await resolveReadInputPath(input, call.cwd)];
|
|
71
|
+
}
|
|
72
|
+
return call.toolName === "grep" ? grepFiles(call) : [];
|
|
73
|
+
}
|
|
74
|
+
function ago(elapsed) {
|
|
75
|
+
const minutes = Math.floor(elapsed / MINUTE_MS);
|
|
76
|
+
return minutes < 1 ? "under 1m ago" : `${minutes}m ago`;
|
|
77
|
+
}
|
|
78
|
+
function liveOthers(ledger, file, self) {
|
|
79
|
+
const entries = ledger.get(file);
|
|
80
|
+
if (entries === void 0) return [];
|
|
81
|
+
const live = [];
|
|
82
|
+
for (const [key, entry] of entries) {
|
|
83
|
+
if (key === self) continue;
|
|
84
|
+
if (entry.agent.isLive()) live.push(entry);
|
|
85
|
+
else entries.delete(key);
|
|
86
|
+
}
|
|
87
|
+
return live.sort((left, right) => right.at - left.at);
|
|
88
|
+
}
|
|
89
|
+
function record(ledger, file, agent, tag, at) {
|
|
90
|
+
const entries = ledger.get(file) ?? /* @__PURE__ */ new Map();
|
|
91
|
+
entries.set(agent.key, {
|
|
92
|
+
agent,
|
|
93
|
+
tag,
|
|
94
|
+
at
|
|
95
|
+
});
|
|
96
|
+
ledger.set(file, entries);
|
|
97
|
+
}
|
|
98
|
+
function renderNote(lines) {
|
|
99
|
+
const shown = lines.slice(0, MAX_NOTE_LINES);
|
|
100
|
+
const more = lines.length - shown.length;
|
|
101
|
+
return [
|
|
102
|
+
PRESENCE_NOTE_TITLE,
|
|
103
|
+
...shown,
|
|
104
|
+
...more > 0 ? [`- and ${more} more`] : [],
|
|
105
|
+
PRESENCE_NOTE_GUIDANCE
|
|
106
|
+
].join("\n");
|
|
19
107
|
}
|
|
20
108
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* record, and once when deciding what to list. A caller that built its own
|
|
25
|
-
* matcher would let the two drift apart.
|
|
26
|
-
*
|
|
27
|
-
* The test answers false for anything it cannot safely match, so a path that
|
|
28
|
-
* escapes the project stays visible rather than being hidden by accident.
|
|
109
|
+
* Notes live editors of the files a tool call touched, then records the caller's own successful edit.
|
|
110
|
+
* Returns the content with one note appended, or undefined when there is nothing to add, so callers
|
|
111
|
+
* never rewrite a result needlessly.
|
|
29
112
|
*/
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
113
|
+
async function observePresence(ledger, agent, call, now = Date.now()) {
|
|
114
|
+
const recording = EDIT_TOOLS.has(call.toolName) && !call.isError;
|
|
115
|
+
if (!recording && ledger.size === 0) return void 0;
|
|
116
|
+
const lines = [];
|
|
117
|
+
for (const file of await touchedFiles(call)) {
|
|
118
|
+
const real = await fs.realpath(file).catch(() => void 0);
|
|
119
|
+
if (real === void 0) continue;
|
|
120
|
+
const others = liveOthers(ledger, real, agent.key);
|
|
121
|
+
let bytes;
|
|
122
|
+
if (others.length > 0) {
|
|
123
|
+
bytes = await fs.readFile(real).catch(() => void 0);
|
|
124
|
+
const current = bytes === void 0 ? void 0 : computeFileTag(bytes);
|
|
125
|
+
const shown = displayPath(file, call.cwd);
|
|
126
|
+
for (const entry of others) {
|
|
127
|
+
const state = entry.tag === current ? "unchanged" : "changed";
|
|
128
|
+
lines.push(`- ${shown}: ${entry.agent.label()}, ${ago(now - entry.at)}, file ${state} since their edit.`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (recording) {
|
|
132
|
+
bytes ??= await fs.readFile(real).catch(() => void 0);
|
|
133
|
+
if (bytes !== void 0) record(ledger, real, agent, computeFileTag(bytes), now);
|
|
134
|
+
}
|
|
40
135
|
}
|
|
136
|
+
if (lines.length === 0) return void 0;
|
|
137
|
+
return [...call.content, {
|
|
138
|
+
type: "text",
|
|
139
|
+
text: renderNote(lines)
|
|
140
|
+
}];
|
|
41
141
|
}
|
|
42
|
-
/**
|
|
43
|
-
function
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
142
|
+
/** Edit presence for one hub session and the in-process children it spawns. */
|
|
143
|
+
function createSessionPresence(options) {
|
|
144
|
+
const { sessionId, ledger } = options;
|
|
145
|
+
const sessionAgent = {
|
|
146
|
+
key: `session:${sessionId}`,
|
|
147
|
+
label: () => `session ${sessionId}${options.workflowStep ? " (workflow step)" : ""}`,
|
|
148
|
+
contact: sessionId,
|
|
149
|
+
isLive: options.isLive
|
|
150
|
+
};
|
|
151
|
+
const safely = async (work) => {
|
|
152
|
+
try {
|
|
153
|
+
return await work();
|
|
154
|
+
} catch (error) {
|
|
155
|
+
options.onError?.(error);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
toolResult: {
|
|
161
|
+
event: "tool_result",
|
|
162
|
+
async handle(event, execution) {
|
|
163
|
+
const toolName = event.toolName;
|
|
164
|
+
if (execution.sessionId !== sessionId || typeof toolName !== "string" || !Array.isArray(event.content)) return;
|
|
165
|
+
const content = event.content;
|
|
166
|
+
const next = await safely(() => observePresence(ledger, sessionAgent, {
|
|
167
|
+
toolName,
|
|
168
|
+
args: event.args,
|
|
169
|
+
content,
|
|
170
|
+
isError: event.isError === true,
|
|
171
|
+
cwd: execution.cwd
|
|
172
|
+
}));
|
|
173
|
+
return next === void 0 ? void 0 : { content: next };
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
childHooks: { bind(scope) {
|
|
177
|
+
const { runId, parentSessionId, agent } = scope.request;
|
|
178
|
+
const child = {
|
|
179
|
+
key: `run:${runId}`,
|
|
180
|
+
label: () => `${options.labelFor(runId) ?? `${agent} (run ${runId.slice(0, SHORT_ID_LENGTH)})`} (subagent of session ${parentSessionId})`,
|
|
181
|
+
contact: parentSessionId,
|
|
182
|
+
isLive: () => !scope.signal.aborted
|
|
183
|
+
};
|
|
184
|
+
return {
|
|
185
|
+
beforeTool: () => void 0,
|
|
186
|
+
async afterTool(event) {
|
|
187
|
+
const next = await safely(() => observePresence(ledger, child, {
|
|
188
|
+
toolName: event.toolName,
|
|
189
|
+
args: event.args,
|
|
190
|
+
content: event.content,
|
|
191
|
+
isError: event.isError,
|
|
192
|
+
cwd: scope.request.cwd || options.cwd
|
|
193
|
+
}));
|
|
194
|
+
return next === void 0 ? void 0 : { content: next };
|
|
195
|
+
},
|
|
196
|
+
dispose: async () => forgetPresence(ledger, child.key)
|
|
197
|
+
};
|
|
198
|
+
} },
|
|
199
|
+
dispose: () => forgetPresence(ledger, sessionAgent.key)
|
|
200
|
+
};
|
|
47
201
|
}
|
|
48
202
|
//#endregion
|
|
49
203
|
//#region src/services/lineDiff/index.ts
|
|
@@ -493,7 +647,7 @@ var EditTracker = class {
|
|
|
493
647
|
//#endregion
|
|
494
648
|
//#region src/services/fileEditPaths/index.ts
|
|
495
649
|
/** The directory this package owns, under whichever root holds agent state. */
|
|
496
|
-
const STATE_DIRECTORY = "
|
|
650
|
+
const STATE_DIRECTORY = "po-file-edit";
|
|
497
651
|
function hash(value) {
|
|
498
652
|
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
499
653
|
}
|
|
@@ -511,7 +665,7 @@ var FileEditPaths = class {
|
|
|
511
665
|
}
|
|
512
666
|
/**
|
|
513
667
|
* Where every session's state lives: the Pi agent directory, the same root
|
|
514
|
-
* every other
|
|
668
|
+
* every other Po package writes to, and never inside the working tree.
|
|
515
669
|
*
|
|
516
670
|
* An earlier layout put this in the repository's git common directory. That
|
|
517
671
|
* is git's own storage: nothing in git's lifecycle prunes a directory we add
|
|
@@ -531,8 +685,8 @@ var FileEditPaths = class {
|
|
|
531
685
|
*/
|
|
532
686
|
timelinePath(cwd, sessionKey) {
|
|
533
687
|
const directory = this.stateDirectory();
|
|
534
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
535
|
-
return path.join(directory, `${hash(fs.realpathSync(cwd))}-${hash(sessionKey)}.jsonl`);
|
|
688
|
+
fs$1.mkdirSync(directory, { recursive: true });
|
|
689
|
+
return path.join(directory, `${hash(fs$1.realpathSync(cwd))}-${hash(sessionKey)}.jsonl`);
|
|
536
690
|
}
|
|
537
691
|
/**
|
|
538
692
|
* Where this session's content snapshots live: beside its timeline and named
|
|
@@ -541,31 +695,6 @@ var FileEditPaths = class {
|
|
|
541
695
|
snapshotsPath(cwd, sessionKey) {
|
|
542
696
|
return `${this.timelinePath(cwd, sessionKey).replace(/\.jsonl$/u, "")}.blobs`;
|
|
543
697
|
}
|
|
544
|
-
legacyStateDirectory(cwd) {
|
|
545
|
-
const common = this.gitCommonDirectory(cwd);
|
|
546
|
-
return common === void 0 ? void 0 : path.join(common, STATE_DIRECTORY);
|
|
547
|
-
}
|
|
548
|
-
/** Only still read so the state an older build left behind can be cleared away. */
|
|
549
|
-
gitCommonDirectory(cwd) {
|
|
550
|
-
try {
|
|
551
|
-
return execFileSync("git", [
|
|
552
|
-
"rev-parse",
|
|
553
|
-
"--path-format=absolute",
|
|
554
|
-
"--git-common-dir"
|
|
555
|
-
], {
|
|
556
|
-
cwd,
|
|
557
|
-
encoding: "utf8",
|
|
558
|
-
stdio: [
|
|
559
|
-
"ignore",
|
|
560
|
-
"pipe",
|
|
561
|
-
"ignore"
|
|
562
|
-
]
|
|
563
|
-
}).trim() || void 0;
|
|
564
|
-
} catch (error) {
|
|
565
|
-
if (error instanceof Error) return void 0;
|
|
566
|
-
throw error;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
698
|
};
|
|
570
699
|
//#endregion
|
|
571
700
|
//#region src/services/gitStatus/index.ts
|
|
@@ -653,6 +782,41 @@ var NodeGitStatusAdapter = class {
|
|
|
653
782
|
}
|
|
654
783
|
}
|
|
655
784
|
};
|
|
785
|
+
//#endregion
|
|
786
|
+
//#region src/services/poIgnore/index.ts
|
|
787
|
+
function matchablePath(relPath) {
|
|
788
|
+
const normalized = relPath.replace(/\\/gu, "/");
|
|
789
|
+
if (normalized.length === 0 || normalized.includes("\0") || normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/") || /^[A-Za-z]:\//u.test(normalized)) return;
|
|
790
|
+
return normalized;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Builds the project's ignore test, or nothing when there are no rules.
|
|
794
|
+
*
|
|
795
|
+
* Shared because the same rules are needed twice: once when deciding what to
|
|
796
|
+
* record, and once when deciding what to list. A caller that built its own
|
|
797
|
+
* matcher would let the two drift apart.
|
|
798
|
+
*
|
|
799
|
+
* The test answers false for anything it cannot safely match, so a path that
|
|
800
|
+
* escapes the project stays visible rather than being hidden by accident.
|
|
801
|
+
*/
|
|
802
|
+
function createPoIgnoreMatcher(content) {
|
|
803
|
+
try {
|
|
804
|
+
if (content.trim().length === 0) return void 0;
|
|
805
|
+
const matcher = ignore().add(content);
|
|
806
|
+
return (relPath) => {
|
|
807
|
+
const candidate = matchablePath(relPath);
|
|
808
|
+
return candidate !== void 0 && matcher.ignores(candidate);
|
|
809
|
+
};
|
|
810
|
+
} catch {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
/** Applies project-local gitignore rules while keeping unsafe candidates visible. */
|
|
815
|
+
function filterPoIgnoredFiles(items, content) {
|
|
816
|
+
const ignored = createPoIgnoreMatcher(content);
|
|
817
|
+
if (ignored === void 0) return items;
|
|
818
|
+
return items.filter((item) => !ignored(item.relPath));
|
|
819
|
+
}
|
|
656
820
|
/** How much of a file is read before deciding it is binary. */
|
|
657
821
|
const BINARY_SAMPLE_BYTES$1 = 8192;
|
|
658
822
|
const MISSING_CODES = /* @__PURE__ */ new Set([
|
|
@@ -672,7 +836,7 @@ var NodeSnapshotStoreAdapter = class {
|
|
|
672
836
|
async capture(filePath) {
|
|
673
837
|
let raw;
|
|
674
838
|
try {
|
|
675
|
-
raw = await fs
|
|
839
|
+
raw = await fs.readFile(filePath);
|
|
676
840
|
} catch (error) {
|
|
677
841
|
if (hasCode$2(error, MISSING_CODES)) return void 0;
|
|
678
842
|
throw error;
|
|
@@ -685,19 +849,19 @@ var NodeSnapshotStoreAdapter = class {
|
|
|
685
849
|
const hash = createHash("sha256").update(content, "utf8").digest("hex");
|
|
686
850
|
const blobPath = path.join(this.requireDirectory(), hash);
|
|
687
851
|
try {
|
|
688
|
-
await fs
|
|
852
|
+
await fs.access(blobPath);
|
|
689
853
|
return hash;
|
|
690
854
|
} catch (error) {
|
|
691
855
|
if (!hasCode$2(error, MISSING_CODES)) throw error;
|
|
692
856
|
}
|
|
693
|
-
await fs
|
|
694
|
-
await fs
|
|
857
|
+
await fs.mkdir(this.requireDirectory(), { recursive: true });
|
|
858
|
+
await fs.writeFile(blobPath, content, "utf8");
|
|
695
859
|
return hash;
|
|
696
860
|
}
|
|
697
861
|
async read(hash) {
|
|
698
862
|
if (!/^[0-9a-f]{64}$/u.test(hash)) return void 0;
|
|
699
863
|
try {
|
|
700
|
-
return await fs
|
|
864
|
+
return await fs.readFile(path.join(this.requireDirectory(), hash), "utf8");
|
|
701
865
|
} catch (error) {
|
|
702
866
|
if (hasCode$2(error, MISSING_CODES)) return void 0;
|
|
703
867
|
throw error;
|
|
@@ -705,7 +869,7 @@ var NodeSnapshotStoreAdapter = class {
|
|
|
705
869
|
}
|
|
706
870
|
async clear() {
|
|
707
871
|
if (this.directory === void 0) return;
|
|
708
|
-
await fs
|
|
872
|
+
await fs.rm(this.directory, {
|
|
709
873
|
recursive: true,
|
|
710
874
|
force: true
|
|
711
875
|
});
|
|
@@ -869,7 +1033,7 @@ var TimelineStore = class {
|
|
|
869
1033
|
this.filePath = filePath;
|
|
870
1034
|
}
|
|
871
1035
|
async append(event) {
|
|
872
|
-
await this.withLock(async () => fs
|
|
1036
|
+
await this.withLock(async () => fs.appendFile(this.requirePath(), `${JSON.stringify(event)}\n`, "utf8"));
|
|
873
1037
|
}
|
|
874
1038
|
async list() {
|
|
875
1039
|
return foldEntries(confirmedChanges(await this.events()));
|
|
@@ -879,7 +1043,7 @@ var TimelineStore = class {
|
|
|
879
1043
|
}
|
|
880
1044
|
async clear() {
|
|
881
1045
|
try {
|
|
882
|
-
await fs
|
|
1046
|
+
await fs.unlink(this.requirePath());
|
|
883
1047
|
} catch (error) {
|
|
884
1048
|
if (!hasCode$1(error, "ENOENT")) throw error;
|
|
885
1049
|
}
|
|
@@ -888,7 +1052,7 @@ var TimelineStore = class {
|
|
|
888
1052
|
async events() {
|
|
889
1053
|
let content;
|
|
890
1054
|
try {
|
|
891
|
-
content = await fs
|
|
1055
|
+
content = await fs.readFile(this.requirePath(), "utf8");
|
|
892
1056
|
} catch (error) {
|
|
893
1057
|
if (hasCode$1(error, "ENOENT")) return [];
|
|
894
1058
|
throw error;
|
|
@@ -912,7 +1076,7 @@ var TimelineStore = class {
|
|
|
912
1076
|
/** Waits for the lock file, breaking one an earlier process left behind. */
|
|
913
1077
|
async acquire(lockPath) {
|
|
914
1078
|
for (let attempt = 0; attempt < LOCK_RETRIES; attempt += 1) try {
|
|
915
|
-
return await fs
|
|
1079
|
+
return await fs.open(lockPath, "wx");
|
|
916
1080
|
} catch (error) {
|
|
917
1081
|
if (!hasCode$1(error, "EEXIST")) throw error;
|
|
918
1082
|
if (await this.breakStale(lockPath)) continue;
|
|
@@ -923,9 +1087,9 @@ var TimelineStore = class {
|
|
|
923
1087
|
/** Whether the lock is now free to take, having removed it when its holder is gone. */
|
|
924
1088
|
async breakStale(lockPath) {
|
|
925
1089
|
try {
|
|
926
|
-
const stats = await fs
|
|
1090
|
+
const stats = await fs.stat(lockPath);
|
|
927
1091
|
if (Date.now() - stats.mtimeMs < LOCK_STALE_MS) return false;
|
|
928
|
-
await fs
|
|
1092
|
+
await fs.unlink(lockPath);
|
|
929
1093
|
return true;
|
|
930
1094
|
} catch (error) {
|
|
931
1095
|
if (hasCode$1(error, "ENOENT")) return true;
|
|
@@ -934,7 +1098,7 @@ var TimelineStore = class {
|
|
|
934
1098
|
}
|
|
935
1099
|
async release(lockPath) {
|
|
936
1100
|
try {
|
|
937
|
-
await fs
|
|
1101
|
+
await fs.unlink(lockPath);
|
|
938
1102
|
} catch (error) {
|
|
939
1103
|
if (!hasCode$1(error, "ENOENT")) console.warn(`Could not remove timeline lock: ${String(error)}`);
|
|
940
1104
|
}
|
|
@@ -1024,7 +1188,7 @@ var NodeTreeManifestAdapter = class {
|
|
|
1024
1188
|
}
|
|
1025
1189
|
async fingerprint(filePath) {
|
|
1026
1190
|
try {
|
|
1027
|
-
const stat = await fs
|
|
1191
|
+
const stat = await fs.stat(filePath);
|
|
1028
1192
|
return stat.isFile() ? fingerprintOf(stat.size, stat.mtimeMs) : void 0;
|
|
1029
1193
|
} catch {
|
|
1030
1194
|
return;
|
|
@@ -1032,7 +1196,7 @@ var NodeTreeManifestAdapter = class {
|
|
|
1032
1196
|
}
|
|
1033
1197
|
async modifiedAt(filePath) {
|
|
1034
1198
|
try {
|
|
1035
|
-
const stat = await fs
|
|
1199
|
+
const stat = await fs.stat(filePath);
|
|
1036
1200
|
return stat.isFile() ? stat.mtimeMs : void 0;
|
|
1037
1201
|
} catch {
|
|
1038
1202
|
return;
|
|
@@ -1053,7 +1217,7 @@ var NodeTreeManifestAdapter = class {
|
|
|
1053
1217
|
if (depth > this.maxDepth) return true;
|
|
1054
1218
|
let listing;
|
|
1055
1219
|
try {
|
|
1056
|
-
listing = await fs
|
|
1220
|
+
listing = await fs.readdir(directory, { withFileTypes: true });
|
|
1057
1221
|
} catch {
|
|
1058
1222
|
return false;
|
|
1059
1223
|
}
|
|
@@ -1070,7 +1234,7 @@ var NodeTreeManifestAdapter = class {
|
|
|
1070
1234
|
}
|
|
1071
1235
|
if (!entry.isFile()) continue;
|
|
1072
1236
|
try {
|
|
1073
|
-
const stat = await fs
|
|
1237
|
+
const stat = await fs.stat(entryPath);
|
|
1074
1238
|
entries.set(entryPath, fingerprintOf(stat.size, stat.mtimeMs));
|
|
1075
1239
|
} catch {}
|
|
1076
1240
|
}
|
|
@@ -1082,7 +1246,7 @@ var NodeTreeManifestAdapter = class {
|
|
|
1082
1246
|
/** The frame type the hub publishes and the web plugin claims; declared in the poWeb block too. */
|
|
1083
1247
|
const filesChannelType = "file_edits";
|
|
1084
1248
|
/** Footer status key whose presence shows the activity group; the extension publishes it. */
|
|
1085
|
-
const filesStatusKey = "
|
|
1249
|
+
const filesStatusKey = "po-file-edit-files";
|
|
1086
1250
|
//#endregion
|
|
1087
1251
|
//#region src/types/apiRoutes.ts
|
|
1088
1252
|
/**
|
|
@@ -1201,7 +1365,7 @@ function describe(error) {
|
|
|
1201
1365
|
async function readWorking(filePath) {
|
|
1202
1366
|
let raw;
|
|
1203
1367
|
try {
|
|
1204
|
-
raw = await fs
|
|
1368
|
+
raw = await fs.readFile(filePath);
|
|
1205
1369
|
} catch {
|
|
1206
1370
|
return {
|
|
1207
1371
|
content: "",
|
|
@@ -1241,8 +1405,8 @@ async function isRealPathInside(root, candidate) {
|
|
|
1241
1405
|
let realRoot;
|
|
1242
1406
|
let real;
|
|
1243
1407
|
try {
|
|
1244
|
-
realRoot = await fs
|
|
1245
|
-
real = await fs
|
|
1408
|
+
realRoot = await fs.realpath(root);
|
|
1409
|
+
real = await fs.realpath(candidate);
|
|
1246
1410
|
} catch (error) {
|
|
1247
1411
|
return hasCode(error, "ENOENT");
|
|
1248
1412
|
}
|
|
@@ -1369,6 +1533,13 @@ function createFileEditsApi(options = {}) {
|
|
|
1369
1533
|
const root = path.resolve(cwd);
|
|
1370
1534
|
return isInside(root, filePath) && await isRealPathInside(root, filePath);
|
|
1371
1535
|
};
|
|
1536
|
+
/** The Po-owned folder holding the file, checked the same two ways as the working directory. */
|
|
1537
|
+
const readableRootOf = async (filePath) => {
|
|
1538
|
+
for (const root of options.readableRoots?.() ?? []) {
|
|
1539
|
+
const resolved = path.resolve(root);
|
|
1540
|
+
if (isInside(resolved, filePath) && await isRealPathInside(resolved, filePath)) return resolved;
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1372
1543
|
app.get(routes.detail.path, async (context) => {
|
|
1373
1544
|
const requested = context.req.query(PATH_QUERY_PARAM);
|
|
1374
1545
|
const versions = await recordedVersions(requested);
|
|
@@ -1401,10 +1572,11 @@ function createFileEditsApi(options = {}) {
|
|
|
1401
1572
|
if (cwd === "") return context.json({ error: "This session has no working directory to read from." }, 403);
|
|
1402
1573
|
const root = path.resolve(cwd);
|
|
1403
1574
|
const filePath = path.resolve(root, requested);
|
|
1404
|
-
|
|
1575
|
+
const inSession = await isInsideSession(filePath);
|
|
1576
|
+
if (!inSession && await readableRootOf(filePath) === void 0) return context.json({ error: "The path leaves the session directory." }, 403);
|
|
1405
1577
|
const body = {
|
|
1406
1578
|
path: filePath,
|
|
1407
|
-
relPath: path.relative(root, filePath),
|
|
1579
|
+
relPath: inSession ? path.relative(root, filePath) : filePath,
|
|
1408
1580
|
working: await readWorking(filePath)
|
|
1409
1581
|
};
|
|
1410
1582
|
return context.json(body);
|
|
@@ -1426,7 +1598,7 @@ function createFileEditsApi(options = {}) {
|
|
|
1426
1598
|
error: "The file changed since it was opened.",
|
|
1427
1599
|
hash: current.hash
|
|
1428
1600
|
}, 409);
|
|
1429
|
-
await fs
|
|
1601
|
+
await fs.writeFile(filePath, request.content, "utf8");
|
|
1430
1602
|
const body = { hash: hashOf(request.content) };
|
|
1431
1603
|
return context.json(body);
|
|
1432
1604
|
});
|
|
@@ -1436,7 +1608,7 @@ function createFileEditsApi(options = {}) {
|
|
|
1436
1608
|
const filePath = path.resolve(cwd, requested);
|
|
1437
1609
|
if (!await isInsideSession(filePath)) return context.json({ error: "Files outside the session directory are read-only." }, 403);
|
|
1438
1610
|
try {
|
|
1439
|
-
await fs
|
|
1611
|
+
await fs.unlink(filePath);
|
|
1440
1612
|
} catch (error) {
|
|
1441
1613
|
if (!hasCode(error, "ENOENT")) return context.json({ error: `The file could not be deleted: ${describe(error)}` }, 409);
|
|
1442
1614
|
}
|
|
@@ -1448,10 +1620,15 @@ function createFileEditsApi(options = {}) {
|
|
|
1448
1620
|
const api = {
|
|
1449
1621
|
basePath: API_BASE_PATH,
|
|
1450
1622
|
start(context) {
|
|
1623
|
+
const environment = context.environment ?? process.env;
|
|
1624
|
+
const cwd = context.cwd;
|
|
1451
1625
|
const app = createFileEditsApi({
|
|
1452
1626
|
paths: new FileEditPaths(context.environment),
|
|
1453
1627
|
...context.sessionId === void 0 ? {} : { sessionId: context.sessionId },
|
|
1454
|
-
...
|
|
1628
|
+
...cwd === void 0 ? {} : {
|
|
1629
|
+
cwd,
|
|
1630
|
+
readableRoots: () => plansRoots(cwd, environment)
|
|
1631
|
+
}
|
|
1455
1632
|
});
|
|
1456
1633
|
return {
|
|
1457
1634
|
fetch: (request) => app.fetch(request),
|
|
@@ -1459,6 +1636,20 @@ const api = {
|
|
|
1459
1636
|
};
|
|
1460
1637
|
}
|
|
1461
1638
|
};
|
|
1639
|
+
/**
|
|
1640
|
+
* The plans directory Plan mode writes to, resolved the way Plan resolves it, so
|
|
1641
|
+
* a plan link in the transcript opens in the file view. A config that cannot be
|
|
1642
|
+
* read admits nothing extra rather than failing the preview.
|
|
1643
|
+
*/
|
|
1644
|
+
function plansRoots(cwd, environment) {
|
|
1645
|
+
const home = environment.HOME ?? os.homedir();
|
|
1646
|
+
try {
|
|
1647
|
+
const configured = loadPoConfig(cwd, home, environment).modes?.planning?.plansDirectory;
|
|
1648
|
+
return [resolvePlanningPlansDirectory(configured, cwd, home)];
|
|
1649
|
+
} catch {
|
|
1650
|
+
return [];
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1462
1653
|
//#endregion
|
|
1463
1654
|
//#region src/services/sessionStateSweep/index.ts
|
|
1464
1655
|
/**
|
|
@@ -1484,7 +1675,7 @@ const STATE_MAX_AGE_MS = 6048e5;
|
|
|
1484
1675
|
const TIMELINE_SUFFIX = ".jsonl";
|
|
1485
1676
|
const SNAPSHOTS_SUFFIX = ".blobs";
|
|
1486
1677
|
async function remove(target) {
|
|
1487
|
-
await fs
|
|
1678
|
+
await fs.rm(target, {
|
|
1488
1679
|
recursive: true,
|
|
1489
1680
|
force: true
|
|
1490
1681
|
});
|
|
@@ -1493,7 +1684,7 @@ async function remove(target) {
|
|
|
1493
1684
|
async function lastTouched(paths) {
|
|
1494
1685
|
let newest;
|
|
1495
1686
|
for (const target of paths) try {
|
|
1496
|
-
const stat = await fs
|
|
1687
|
+
const stat = await fs.stat(target);
|
|
1497
1688
|
if (newest === void 0 || stat.mtimeMs > newest) newest = stat.mtimeMs;
|
|
1498
1689
|
} catch {}
|
|
1499
1690
|
return newest;
|
|
@@ -1503,7 +1694,7 @@ async function sweepSessionState(options) {
|
|
|
1503
1694
|
const { directory, keep, removeDirectory = false, now = Date.now(), maxAgeMs = STATE_MAX_AGE_MS } = options;
|
|
1504
1695
|
let entries;
|
|
1505
1696
|
try {
|
|
1506
|
-
entries = await fs
|
|
1697
|
+
entries = await fs.readdir(directory);
|
|
1507
1698
|
} catch {
|
|
1508
1699
|
return 0;
|
|
1509
1700
|
}
|
|
@@ -1535,20 +1726,21 @@ async function sweepSessionState(options) {
|
|
|
1535
1726
|
await remove(orphan);
|
|
1536
1727
|
}
|
|
1537
1728
|
if (removeDirectory) try {
|
|
1538
|
-
await fs
|
|
1729
|
+
await fs.rmdir(directory);
|
|
1539
1730
|
} catch {}
|
|
1540
1731
|
return removed;
|
|
1541
1732
|
}
|
|
1542
1733
|
//#endregion
|
|
1543
1734
|
//#region src/services/webFilesChannel/index.ts
|
|
1544
1735
|
/**
|
|
1545
|
-
* The changed-files channel consumes snapshots emitted by the owning session
|
|
1546
|
-
*
|
|
1736
|
+
* The changed-files channel consumes snapshots emitted by the owning session's
|
|
1737
|
+
* server plugin. Durable timeline reads seed a channel that starts after session
|
|
1738
|
+
* startup.
|
|
1547
1739
|
*/
|
|
1548
1740
|
/** Whether the file is still there to be opened. */
|
|
1549
1741
|
function stillExists(filePath) {
|
|
1550
1742
|
try {
|
|
1551
|
-
return fs.statSync(filePath).isFile();
|
|
1743
|
+
return fs$1.statSync(filePath).isFile();
|
|
1552
1744
|
} catch {
|
|
1553
1745
|
return false;
|
|
1554
1746
|
}
|
|
@@ -1566,7 +1758,7 @@ function stillExists(filePath) {
|
|
|
1566
1758
|
function readSessionFiles(timelinePath, cwd) {
|
|
1567
1759
|
let content;
|
|
1568
1760
|
try {
|
|
1569
|
-
content = fs.readFileSync(timelinePath, "utf8");
|
|
1761
|
+
content = fs$1.readFileSync(timelinePath, "utf8");
|
|
1570
1762
|
} catch {
|
|
1571
1763
|
return [];
|
|
1572
1764
|
}
|
|
@@ -1580,7 +1772,7 @@ function readSessionFiles(timelinePath, cwd) {
|
|
|
1580
1772
|
diffable: isDiffable(foldVersions(events, entry.path))
|
|
1581
1773
|
}));
|
|
1582
1774
|
try {
|
|
1583
|
-
return
|
|
1775
|
+
return filterPoIgnoredFiles(items, fs$1.readFileSync(path.join(cwd, ".poignore"), "utf8"));
|
|
1584
1776
|
} catch {
|
|
1585
1777
|
return items;
|
|
1586
1778
|
}
|
|
@@ -1636,10 +1828,10 @@ function createFilesChannel() {
|
|
|
1636
1828
|
}
|
|
1637
1829
|
//#endregion
|
|
1638
1830
|
//#region src/services/fileEditSession/index.ts
|
|
1639
|
-
const
|
|
1640
|
-
function
|
|
1831
|
+
const PO_IGNORE_FILE = ".poignore";
|
|
1832
|
+
function readPoIgnore(cwd) {
|
|
1641
1833
|
try {
|
|
1642
|
-
const matcher =
|
|
1834
|
+
const matcher = createPoIgnoreMatcher(fs$1.readFileSync(path.join(cwd, PO_IGNORE_FILE), "utf8"));
|
|
1643
1835
|
return matcher === void 0 ? void 0 : (filePath) => matcher(path.relative(cwd, filePath));
|
|
1644
1836
|
} catch {
|
|
1645
1837
|
return;
|
|
@@ -1656,10 +1848,10 @@ function createFileEditSession({ agent, host }) {
|
|
|
1656
1848
|
const timeline = new TimelineStore();
|
|
1657
1849
|
const snapshots = new NodeSnapshotStoreAdapter();
|
|
1658
1850
|
const editTracker = new EditTracker(timeline, snapshots, new NodeTreeManifestAdapter(), { git: new NodeGitStatusAdapter() });
|
|
1659
|
-
if (host.context.directEvents === void 0) throw new Error("File-edit
|
|
1851
|
+
if (host.context.directEvents === void 0) throw new Error("File-edit server plugin requires the session direct event bus.");
|
|
1660
1852
|
const directEvents = host.context.directEvents;
|
|
1661
|
-
const telemetry =
|
|
1662
|
-
serviceName: "
|
|
1853
|
+
const telemetry = createPoTelemetry({
|
|
1854
|
+
serviceName: "po-file-edit",
|
|
1663
1855
|
packageName: "@agimon-ai/po-file-edit",
|
|
1664
1856
|
env: environment,
|
|
1665
1857
|
enableLogs: true,
|
|
@@ -1682,7 +1874,7 @@ function createFileEditSession({ agent, host }) {
|
|
|
1682
1874
|
/**
|
|
1683
1875
|
* The channel carries the rows; the status decides whether the group exists.
|
|
1684
1876
|
*
|
|
1685
|
-
* A cockpit drives this
|
|
1877
|
+
* A cockpit drives this server plugin and never the Pi runtime, so without the status
|
|
1686
1878
|
* the dock had the file list on the wire and no frame to draw it in. The empty
|
|
1687
1879
|
* string is deliberate: the group declares `hideWhenEmpty`, so a session that
|
|
1688
1880
|
* has edited nothing publishes a key with no content and stays hidden.
|
|
@@ -1708,20 +1900,19 @@ function createFileEditSession({ agent, host }) {
|
|
|
1708
1900
|
const dispose = () => {
|
|
1709
1901
|
if (disposal) return disposal;
|
|
1710
1902
|
closed = true;
|
|
1711
|
-
disposal = (
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
if (lastRuntime)
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
})();
|
|
1903
|
+
disposal = runCleanups([
|
|
1904
|
+
stopActivity,
|
|
1905
|
+
async () => {
|
|
1906
|
+
if (lastRuntime && !environment[SUBAGENT_CHILD_ENV]) await timeline.clear();
|
|
1907
|
+
},
|
|
1908
|
+
async () => {
|
|
1909
|
+
if (lastRuntime && !environment[SUBAGENT_CHILD_ENV]) await snapshots.clear();
|
|
1910
|
+
},
|
|
1911
|
+
async () => {
|
|
1912
|
+
if (lastRuntime) await telemetry.recordEvent("po_file_edit.timeline_finished", { outcome: "closed" });
|
|
1913
|
+
},
|
|
1914
|
+
() => telemetry.shutdown()
|
|
1915
|
+
], "File-edit session cleanup failed.");
|
|
1725
1916
|
return disposal;
|
|
1726
1917
|
};
|
|
1727
1918
|
return {
|
|
@@ -1739,7 +1930,7 @@ function createFileEditSession({ agent, host }) {
|
|
|
1739
1930
|
const snapshotsPath = paths.snapshotsPath(executionContext.cwd, sessionKey);
|
|
1740
1931
|
timeline.initialize(timelinePath);
|
|
1741
1932
|
snapshots.initialize(snapshotsPath);
|
|
1742
|
-
const isIgnored =
|
|
1933
|
+
const isIgnored = readPoIgnore(executionContext.cwd);
|
|
1743
1934
|
editTracker.reset({
|
|
1744
1935
|
exclude: [
|
|
1745
1936
|
timelinePath,
|
|
@@ -1762,16 +1953,11 @@ function createFileEditSession({ agent, host }) {
|
|
|
1762
1953
|
directory: paths.stateDirectory(),
|
|
1763
1954
|
keep: timelinePath
|
|
1764
1955
|
});
|
|
1765
|
-
const legacy = paths.legacyStateDirectory(executionContext.cwd);
|
|
1766
|
-
if (legacy !== void 0 && legacy !== paths.stateDirectory()) await sweepSessionState({
|
|
1767
|
-
directory: legacy,
|
|
1768
|
-
removeDirectory: true
|
|
1769
|
-
});
|
|
1770
1956
|
} catch {}
|
|
1771
1957
|
});
|
|
1772
1958
|
if (!initialized) {
|
|
1773
1959
|
initialized = true;
|
|
1774
|
-
await track(() => telemetry.recordEvent("
|
|
1960
|
+
await track(() => telemetry.recordEvent("po_file_edit.timeline_started", { outcome: "initialized" }));
|
|
1775
1961
|
}
|
|
1776
1962
|
return () => active === runtime ? stopActivity() : Promise.resolve();
|
|
1777
1963
|
}
|
|
@@ -1788,7 +1974,7 @@ function createFileEditSession({ agent, host }) {
|
|
|
1788
1974
|
const hookStartedAt = Date.now();
|
|
1789
1975
|
await editTracker.start(toolCallId, toolName, event.args, executionContext.cwd);
|
|
1790
1976
|
if (active !== runtime) return;
|
|
1791
|
-
await telemetry.recordEvent("
|
|
1977
|
+
await telemetry.recordEvent("po_file_edit.edit_started", {
|
|
1792
1978
|
"tool.name": toolName,
|
|
1793
1979
|
hook_ms: Date.now() - hookStartedAt
|
|
1794
1980
|
});
|
|
@@ -1809,7 +1995,7 @@ function createFileEditSession({ agent, host }) {
|
|
|
1809
1995
|
await editTracker.end(toolCallId, isError, executionContext.cwd);
|
|
1810
1996
|
if (active !== runtime) return;
|
|
1811
1997
|
publish(runtime);
|
|
1812
|
-
await telemetry.recordEvent("
|
|
1998
|
+
await telemetry.recordEvent("po_file_edit.edit_finished", {
|
|
1813
1999
|
"tool.name": toolName,
|
|
1814
2000
|
"tool.result.error": isError,
|
|
1815
2001
|
...startedAt === void 0 ? {} : { duration_ms: Date.now() - startedAt },
|
|
@@ -1824,12 +2010,38 @@ function createFileEditSession({ agent, host }) {
|
|
|
1824
2010
|
}
|
|
1825
2011
|
//#endregion
|
|
1826
2012
|
//#region src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts
|
|
2013
|
+
function createPresence({ agent, host, context }) {
|
|
2014
|
+
const ledger = host.context.editPresence;
|
|
2015
|
+
if (!agent || !ledger) return void 0;
|
|
2016
|
+
const execution = agent.context;
|
|
2017
|
+
return createSessionPresence({
|
|
2018
|
+
ledger,
|
|
2019
|
+
sessionId: execution.sessionId,
|
|
2020
|
+
cwd: execution.cwd,
|
|
2021
|
+
workflowStep: execution.sessionContext?.provenance === WORKFLOW_STEP_SESSION_PROVENANCE,
|
|
2022
|
+
isLive: () => host.context.sessionService?.isLive(execution.sessionId) ?? true,
|
|
2023
|
+
labelFor: (runId) => readPoChildSessionLabels(context)?.labelFor(runId),
|
|
2024
|
+
onError: (error) => host.context.onNotice(`Edit presence failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
1827
2027
|
const createFileEditServerRoot = (context) => {
|
|
1828
2028
|
const runtime = createFileEditSession(context);
|
|
2029
|
+
const presence = createPresence(context);
|
|
1829
2030
|
return {
|
|
1830
|
-
value:
|
|
2031
|
+
value: {
|
|
2032
|
+
...runtime,
|
|
2033
|
+
...presence === void 0 ? {} : { presence }
|
|
2034
|
+
},
|
|
1831
2035
|
activities: runtime.activities,
|
|
1832
|
-
|
|
2036
|
+
...presence === void 0 ? {} : { services: [(cordis) => {
|
|
2037
|
+
cordis.inject([PO_CHILD_SESSION_HOOK_REGISTRY_SERVICE], (registryContext) => {
|
|
2038
|
+
const registration = registryContext.get(PO_CHILD_SESSION_HOOK_REGISTRY_SERVICE).register(presence.childHooks);
|
|
2039
|
+
return () => registration.dispose();
|
|
2040
|
+
});
|
|
2041
|
+
}] },
|
|
2042
|
+
onDispose: async (pluginContext) => {
|
|
2043
|
+
await runCleanups([() => presence?.dispose(), () => runtime.onDispose?.(pluginContext)], "File-edit root cleanup failed.");
|
|
2044
|
+
}
|
|
1833
2045
|
};
|
|
1834
2046
|
};
|
|
1835
2047
|
//#endregion
|
|
@@ -1858,23 +2070,32 @@ var tool_execution_end_server_default = defineHook((context) => context.root.hoo
|
|
|
1858
2070
|
//#region src/extensions/workspaces/sessions/(backend)/hook/tool-execution-start.server.ts
|
|
1859
2071
|
var tool_execution_start_server_default = defineHook((context) => context.root.hooks[0]);
|
|
1860
2072
|
//#endregion
|
|
2073
|
+
//#region src/extensions/workspaces/sessions/(backend)/hook/tool-result.server.ts
|
|
2074
|
+
var tool_result_server_default = defineHook(({ root }) => ({
|
|
2075
|
+
event: "tool_result",
|
|
2076
|
+
handle: async (event, execution) => root.presence?.toolResult.handle(event, execution)
|
|
2077
|
+
}));
|
|
2078
|
+
//#endregion
|
|
1861
2079
|
//#region generated/server.ts
|
|
1862
2080
|
const at = (value, context) => typeof value === "function" ? value(context) : value;
|
|
2081
|
+
/* v8 ignore next -- generated normalization branch */
|
|
2082
|
+
const defined = (values) => values.filter((value) => value !== void 0);
|
|
1863
2083
|
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
1864
2084
|
...identity,
|
|
1865
2085
|
...value
|
|
1866
2086
|
};
|
|
1867
|
-
const
|
|
2087
|
+
const serverPlugin = defineServerPlugin({
|
|
1868
2088
|
name: "@agimon-ai/po-file-edit",
|
|
1869
2089
|
providerOverrides: composeServerProviderOverrides(root_server_default),
|
|
2090
|
+
injects: composeRootInjects({ session: [root_server_default] }),
|
|
1870
2091
|
global: () => ({ get channels() {
|
|
1871
2092
|
return [() => via({ frameType: "file_edits" }, fileEdits_server_default())];
|
|
1872
2093
|
} }),
|
|
1873
|
-
workspace: () => ({ get channels() {
|
|
1874
|
-
return [() => via({ frameType: "file_edits" }, fileEdits_server_default())];
|
|
2094
|
+
workspace: (context) => ({ get channels() {
|
|
2095
|
+
return defined([context.hosted("global") ? void 0 : () => via({ frameType: "file_edits" }, fileEdits_server_default())]);
|
|
1875
2096
|
} }),
|
|
1876
2097
|
session: async (context) => {
|
|
1877
|
-
const scopeSession = await root_server_default
|
|
2098
|
+
const scopeSession = await context.scopeRoot("session", root_server_default, context);
|
|
1878
2099
|
const contextSession = {
|
|
1879
2100
|
...context,
|
|
1880
2101
|
root: scopeSession.value
|
|
@@ -1885,10 +2106,14 @@ const facet = defineServerPlugin({
|
|
|
1885
2106
|
return [at(route_server_default, contextSession)];
|
|
1886
2107
|
},
|
|
1887
2108
|
get channels() {
|
|
1888
|
-
return [() => via({ frameType: "file_edits" }, fileEdits_server_default())];
|
|
2109
|
+
return defined([context.hosted("global") ? void 0 : () => via({ frameType: "file_edits" }, fileEdits_server_default())]);
|
|
1889
2110
|
},
|
|
1890
2111
|
get hooks() {
|
|
1891
|
-
return [
|
|
2112
|
+
return [
|
|
2113
|
+
via({ event: "tool_execution_end" }, at(tool_execution_end_server_default, contextSession)),
|
|
2114
|
+
via({ event: "tool_execution_start" }, at(tool_execution_start_server_default, contextSession)),
|
|
2115
|
+
via({ event: "tool_result" }, at(tool_result_server_default, contextSession))
|
|
2116
|
+
];
|
|
1892
2117
|
},
|
|
1893
2118
|
services: [...scopeSession.services ?? []],
|
|
1894
2119
|
get activities() {
|
|
@@ -1898,6 +2123,6 @@ const facet = defineServerPlugin({
|
|
|
1898
2123
|
}
|
|
1899
2124
|
});
|
|
1900
2125
|
//#endregion
|
|
1901
|
-
export {
|
|
2126
|
+
export { serverPlugin as default, serverPlugin };
|
|
1902
2127
|
|
|
1903
2128
|
//# sourceMappingURL=server.mjs.map
|