@agimon-ai/po-file-edit 0.0.0-alpha.3 → 0.0.0-alpha.5

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/README.md CHANGED
@@ -18,7 +18,7 @@ tree comparison. See [What this cannot see](#what-this-cannot-see) for gaps in t
18
18
 
19
19
  ## Install
20
20
 
21
- `po init` and `dpi init` include File Edit in `default.packages` in `.po/modes.yaml`.
21
+ `po init` include File Edit in `default.packages` in `.po/modes.yaml`.
22
22
  Remove it or move it to a named layer to change which major modes load it.
23
23
 
24
24
  | Entry | Purpose |
@@ -62,7 +62,7 @@ and keeps it, content-addressed, beside the session timeline; both go when the s
62
62
 
63
63
  The timeline contains file paths, and the snapshot store holds copies of the file content the
64
64
  session changed. That content can be sensitive. Both live under the Pi agent directory,
65
- `$PI_CODING_AGENT_DIR` or `~/.po/agent/doom-file-edit`, never inside the working tree, and should be
65
+ `$PI_CODING_AGENT_DIR` or `~/.po/agent/po-file-edit`, never inside the working tree, and should be
66
66
  protected with the same access controls as Pi session data.
67
67
 
68
68
  A parent session removes its own timeline and snapshots on final disposal. Child sessions share the parent timeline and do not clear it. Activity selection stops retain review data for reactivation. A session that is killed cannot, so
@@ -1 +1 @@
1
- {"version":3,"file":"native.mjs","names":["channelFileEdits"],"sources":["../../src/types/webFiles.ts","../../src/extensions/workspaces/sessions/(frontend)/_lib/filesModel.ts","../../src/extensions/workspaces/sessions/(frontend)/_lib/filesStore.native.ts","../../src/extensions/workspaces/sessions/(frontend)/channel/file-edits.native.ts","../../generated/native.ts"],"sourcesContent":["import type { FileEditTool } from './domain';\n\n/**\n * View types shared by this package's hub channel and its web plugin. Wire\n * JSON only, no node types, so the cockpit bundle can carry them; the\n * cockpit receives them as 'file_edits' channel payloads.\n *\n * The channel carries the list alone. One file's diffs and content are large\n * and only wanted once a reader opens the file, so those come from the session\n * API instead (see fileEditsApi.ts).\n */\n\n/** One file this session changed, as the activity dock lists it. */\nexport interface FilesItemView {\n /** Absolute path, and the identity every API route takes back. */\n path: string;\n /** Path relative to the session's working directory, which is what a row shows. */\n relPath: string;\n /** The tool behind the most recent change. */\n tool: FileEditTool;\n /** When that change happened, epoch milliseconds. */\n at: number;\n /** How many changes this session recorded for the file. */\n count: number;\n /**\n * Whether any change captured a baseline, so a diff can be shown at all. A\n * file found by comparing the tree around a bash call has no \"before\", so it\n * is listed and readable but never diffed.\n */\n diffable: boolean;\n}\n\n/** The frame type the hub publishes and the web plugin claims; declared in the poWeb block too. */\nexport const filesChannelType = 'file_edits';\n\n/** Footer status key whose presence shows the activity group; the extension publishes it. */\nexport const filesStatusKey = 'doom-file-edit-files';\n","import type { FilesItemView } from '../../../../../types/webFiles';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isItem(value: unknown): value is FilesItemView {\n return (\n isRecord(value) &&\n typeof value.path === 'string' &&\n typeof value.relPath === 'string' &&\n typeof value.tool === 'string' &&\n typeof value.at === 'number' &&\n typeof value.count === 'number'\n );\n}\n\nexport interface FilesPayload {\n items: FilesItemView[];\n}\n\n/** The shared wire gate for browser and native stores. */\nexport function parseFilesPayload(input: unknown): FilesPayload | null {\n if (!isRecord(input) || !Array.isArray(input.items)) return null;\n return { items: input.items.filter(isItem) };\n}\n","import { defineSessionStore } from '@agimon-ai/po-core/native';\nimport type { ReviewComment } from '@agimon-ai/po-web-components';\n\nimport type { FileEditsDetailView } from '../../../../../types/fileEditsApi';\nimport { filesChannelType, type FilesItemView } from '../../../../../types/webFiles';\nimport { parseFilesPayload, type FilesPayload } from './filesModel';\nexport type { FilesPayload } from './filesModel';\n\n/**\n * One session's record: the hub's last report plus whatever this page does with\n * it. Records are immutable values; the reducer and every action return a new one.\n *\n * Detail is cached per file rather than per open tab, because a reader closes a\n * file and comes back to it, and refetching the same history for the same click\n * is work the session already did.\n */\nexport interface FilesSession {\n items: FilesItemView[];\n /** One file's fetched history, keyed by absolute path. */\n detail: Readonly<Record<string, FileEditsDetailView>>;\n /** Paths with a detail request in flight, so a panel can say it is loading. */\n loading: readonly string[];\n /** What a failed fetch or a refused save reported, keyed by absolute path. */\n errors: Readonly<Record<string, string>>;\n /** Review notes waiting to be sent, in the order they were written. */\n comments: readonly ReviewComment[];\n}\n\nconst empty: FilesSession = { items: [], detail: {}, loading: [], errors: {}, comments: [] };\n\nexport const files = defineSessionStore<FilesSession>(empty);\n\n/** The plugin's session data channel: 'file_edits' payloads into the store. */\nexport const filesChannel = files.channel<FilesPayload>({\n channel: filesChannelType,\n parse: parseFilesPayload,\n // Only the list is replaced. The cached detail and the pending notes belong\n // to files a reader opened, not to the list: a file the session deletes stops\n // being listed, and pruning here would blank the tab still open on it and\n // then refetch it on the next frame.\n reduce: (current, { items }) => ({ ...current, items }),\n});\n\nexport function markLoading(sessionId: string, filePath: string): void {\n files.update(sessionId, (current) =>\n current.loading.includes(filePath)\n ? current\n : { ...current, loading: [...current.loading, filePath], errors: withoutKey(current.errors, filePath) },\n );\n}\n\nexport function storeDetail(sessionId: string, detail: FileEditsDetailView): void {\n files.update(sessionId, (current) => ({\n ...current,\n detail: { ...current.detail, [detail.path]: detail },\n loading: current.loading.filter((path) => path !== detail.path),\n errors: withoutKey(current.errors, detail.path),\n }));\n}\n\nexport function storeError(sessionId: string, filePath: string, message: string): void {\n files.update(sessionId, (current) => ({\n ...current,\n loading: current.loading.filter((path) => path !== filePath),\n errors: { ...current.errors, [filePath]: message },\n }));\n}\n\nexport function addComment(sessionId: string, comment: ReviewComment): void {\n files.update(sessionId, (current) => ({ ...current, comments: [...current.comments, comment] }));\n}\n\nexport function removeComment(sessionId: string, id: string): void {\n files.update(sessionId, (current) => ({\n ...current,\n comments: current.comments.filter((comment) => comment.id !== id),\n }));\n}\n\nexport function clearComments(sessionId: string, filePath: string): void {\n files.update(sessionId, (current) => ({\n ...current,\n comments: current.comments.filter((comment) => comment.path !== filePath),\n }));\n}\n\nfunction withoutKey(source: Readonly<Record<string, string>>, key: string): Record<string, string> {\n return Object.fromEntries(Object.entries(source).filter(([entry]) => entry !== key));\n}\n","import { defineChannelFile } from '@agimon-ai/po-core/native';\n\nimport { filesChannel } from '../_lib/filesStore.native';\nexport default defineChannelFile(filesChannel);\n","// Generated by @agimon-ai/po-build. Do not edit by hand.\nimport { defineNativePlugin, type WebPluginContributions } from '@agimon-ai/po-core/native';\n\nimport channelFileEdits from '../src/extensions/workspaces/sessions/(frontend)/channel/file-edits.native';\n\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nexport const nativePlugin = defineNativePlugin({\n id: 'files',\n session: {\n get channels(): WebPluginContributions['channels'] { return [via({ channel: 'file_edits' }, channelFileEdits)]; },\n },\n});\n"],"mappings":";;;AAiCA,MAAa,mBAAmB;;;AC/BhC,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,OAAO,OAAwC;CACtD,OACE,SAAS,KAAK,KACd,OAAO,MAAM,SAAS,YACtB,OAAO,MAAM,YAAY,YACzB,OAAO,MAAM,SAAS,YACtB,OAAO,MAAM,OAAO,YACpB,OAAO,MAAM,UAAU;AAE3B;;AAOA,SAAgB,kBAAkB,OAAqC;CACrE,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,MAAM,QAAQ,MAAM,KAAK,GAAG,OAAO;CAC5D,OAAO,EAAE,OAAO,MAAM,MAAM,OAAO,MAAM,EAAE;AAC7C;;ACQA,MAAa,eAHQ,mBAAiC;CAFxB,OAAO,CAAC;CAAG,QAAQ,CAAC;CAAG,SAAS,CAAC;CAAG,QAAQ,CAAC;CAAG,UAAU,CAAC;AAEnC,CAG1B,CAAA,CAAM,QAAsB;CACtD,SAAS;CACT,OAAO;CAKP,SAAS,SAAS,EAAE,aAAa;EAAE,GAAG;EAAS;CAAM;AACvD,CAAC;;;ACtCD,IAAA,4BAAe,kBAAkB,YAAY;;;ACE7C,MAAM,OAAU,UAAmC,UAChD,UAAU,KAAA,IAAY,KAAA,IAAY;CAAE,GAAG;CAAU,GAAI;AAAiB;AAEzE,MAAa,eAAe,mBAAmB;CAC7C,IAAI;CACJ,SAAS,EACP,IAAI,WAA+C;EAAE,OAAO,CAAC,IAAI,EAAE,SAAS,aAAa,GAAGA,yBAAgB,CAAC;CAAG,EAClH;AACF,CAAC"}
1
+ {"version":3,"file":"native.mjs","names":["channelFileEdits"],"sources":["../../src/types/webFiles.ts","../../src/extensions/workspaces/sessions/(frontend)/_lib/filesModel.ts","../../src/extensions/workspaces/sessions/(frontend)/_lib/filesStore.native.ts","../../src/extensions/workspaces/sessions/(frontend)/channel/file-edits.native.ts","../../generated/native.ts"],"sourcesContent":["import type { FileEditTool } from './domain';\n\n/**\n * View types shared by this package's hub channel and its web plugin. Wire\n * JSON only, no node types, so the cockpit bundle can carry them; the\n * cockpit receives them as 'file_edits' channel payloads.\n *\n * The channel carries the list alone. One file's diffs and content are large\n * and only wanted once a reader opens the file, so those come from the session\n * API instead (see fileEditsApi.ts).\n */\n\n/** One file this session changed, as the activity dock lists it. */\nexport interface FilesItemView {\n /** Absolute path, and the identity every API route takes back. */\n path: string;\n /** Path relative to the session's working directory, which is what a row shows. */\n relPath: string;\n /** The tool behind the most recent change. */\n tool: FileEditTool;\n /** When that change happened, epoch milliseconds. */\n at: number;\n /** How many changes this session recorded for the file. */\n count: number;\n /**\n * Whether any change captured a baseline, so a diff can be shown at all. A\n * file found by comparing the tree around a bash call has no \"before\", so it\n * is listed and readable but never diffed.\n */\n diffable: boolean;\n}\n\n/** The frame type the hub publishes and the web plugin claims; declared in the poWeb block too. */\nexport const filesChannelType = 'file_edits';\n\n/** Footer status key whose presence shows the activity group; the extension publishes it. */\nexport const filesStatusKey = 'po-file-edit-files';\n","import type { FilesItemView } from '../../../../../types/webFiles';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isItem(value: unknown): value is FilesItemView {\n return (\n isRecord(value) &&\n typeof value.path === 'string' &&\n typeof value.relPath === 'string' &&\n typeof value.tool === 'string' &&\n typeof value.at === 'number' &&\n typeof value.count === 'number'\n );\n}\n\nexport interface FilesPayload {\n items: FilesItemView[];\n}\n\n/** The shared wire gate for browser and native stores. */\nexport function parseFilesPayload(input: unknown): FilesPayload | null {\n if (!isRecord(input) || !Array.isArray(input.items)) return null;\n return { items: input.items.filter(isItem) };\n}\n","import { defineSessionStore } from '@agimon-ai/po-core/native';\nimport type { ReviewComment } from '@agimon-ai/po-web-components';\n\nimport type { FileEditsDetailView } from '../../../../../types/fileEditsApi';\nimport { filesChannelType, type FilesItemView } from '../../../../../types/webFiles';\nimport { parseFilesPayload, type FilesPayload } from './filesModel';\nexport type { FilesPayload } from './filesModel';\n\n/**\n * One session's record: the hub's last report plus whatever this page does with\n * it. Records are immutable values; the reducer and every action return a new one.\n *\n * Detail is cached per file rather than per open tab, because a reader closes a\n * file and comes back to it, and refetching the same history for the same click\n * is work the session already did.\n */\nexport interface FilesSession {\n items: FilesItemView[];\n /** One file's fetched history, keyed by absolute path. */\n detail: Readonly<Record<string, FileEditsDetailView>>;\n /** Paths with a detail request in flight, so a panel can say it is loading. */\n loading: readonly string[];\n /** What a failed fetch or a refused save reported, keyed by absolute path. */\n errors: Readonly<Record<string, string>>;\n /** Review notes waiting to be sent, in the order they were written. */\n comments: readonly ReviewComment[];\n}\n\nconst empty: FilesSession = { items: [], detail: {}, loading: [], errors: {}, comments: [] };\n\nexport const files = defineSessionStore<FilesSession>(empty);\n\n/** The plugin's session data channel: 'file_edits' payloads into the store. */\nexport const filesChannel = files.channel<FilesPayload>({\n channel: filesChannelType,\n parse: parseFilesPayload,\n // Only the list is replaced. The cached detail and the pending notes belong\n // to files a reader opened, not to the list: a file the session deletes stops\n // being listed, and pruning here would blank the tab still open on it and\n // then refetch it on the next frame.\n reduce: (current, { items }) => ({ ...current, items }),\n});\n\nexport function markLoading(sessionId: string, filePath: string): void {\n files.update(sessionId, (current) =>\n current.loading.includes(filePath)\n ? current\n : { ...current, loading: [...current.loading, filePath], errors: withoutKey(current.errors, filePath) },\n );\n}\n\nexport function storeDetail(sessionId: string, detail: FileEditsDetailView): void {\n files.update(sessionId, (current) => ({\n ...current,\n detail: { ...current.detail, [detail.path]: detail },\n loading: current.loading.filter((path) => path !== detail.path),\n errors: withoutKey(current.errors, detail.path),\n }));\n}\n\nexport function storeError(sessionId: string, filePath: string, message: string): void {\n files.update(sessionId, (current) => ({\n ...current,\n loading: current.loading.filter((path) => path !== filePath),\n errors: { ...current.errors, [filePath]: message },\n }));\n}\n\nexport function addComment(sessionId: string, comment: ReviewComment): void {\n files.update(sessionId, (current) => ({ ...current, comments: [...current.comments, comment] }));\n}\n\nexport function removeComment(sessionId: string, id: string): void {\n files.update(sessionId, (current) => ({\n ...current,\n comments: current.comments.filter((comment) => comment.id !== id),\n }));\n}\n\nexport function clearComments(sessionId: string, filePath: string): void {\n files.update(sessionId, (current) => ({\n ...current,\n comments: current.comments.filter((comment) => comment.path !== filePath),\n }));\n}\n\nfunction withoutKey(source: Readonly<Record<string, string>>, key: string): Record<string, string> {\n return Object.fromEntries(Object.entries(source).filter(([entry]) => entry !== key));\n}\n","import { defineChannelFile } from '@agimon-ai/po-core/native';\n\nimport { filesChannel } from '../_lib/filesStore.native';\nexport default defineChannelFile(filesChannel);\n","// Generated by @agimon-ai/po-build. Do not edit by hand.\nimport { defineNativePlugin, type WebPluginContributions } from '@agimon-ai/po-core/native';\n\nimport channelFileEdits from '../src/extensions/workspaces/sessions/(frontend)/channel/file-edits.native';\n\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nexport const nativePlugin = defineNativePlugin({\n id: 'files',\n session: {\n get channels(): WebPluginContributions['channels'] { return [via({ channel: 'file_edits' }, channelFileEdits)]; },\n },\n});\n"],"mappings":";;;AAiCA,MAAa,mBAAmB;;;AC/BhC,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,OAAO,OAAwC;CACtD,OACE,SAAS,KAAK,KACd,OAAO,MAAM,SAAS,YACtB,OAAO,MAAM,YAAY,YACzB,OAAO,MAAM,SAAS,YACtB,OAAO,MAAM,OAAO,YACpB,OAAO,MAAM,UAAU;AAE3B;;AAOA,SAAgB,kBAAkB,OAAqC;CACrE,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,MAAM,QAAQ,MAAM,KAAK,GAAG,OAAO;CAC5D,OAAO,EAAE,OAAO,MAAM,MAAM,OAAO,MAAM,EAAE;AAC7C;;ACQA,MAAa,eAHQ,mBAAiC;CAFxB,OAAO,CAAC;CAAG,QAAQ,CAAC;CAAG,SAAS,CAAC;CAAG,QAAQ,CAAC;CAAG,UAAU,CAAC;AAEnC,CAG1B,CAAA,CAAM,QAAsB;CACtD,SAAS;CACT,OAAO;CAKP,SAAS,SAAS,EAAE,aAAa;EAAE,GAAG;EAAS;CAAM;AACvD,CAAC;;;ACtCD,IAAA,4BAAe,kBAAkB,YAAY;;;ACE7C,MAAM,OAAU,UAAmC,UAChD,UAAU,KAAA,IAAY,KAAA,IAAY;CAAE,GAAG;CAAU,GAAI;AAAiB;AAEzE,MAAa,eAAe,mBAAmB;CAC7C,IAAI;CACJ,SAAS,EACP,IAAI,WAA+C;EAAE,OAAO,CAAC,IAAI,EAAE,SAAS,aAAa,GAAGA,yBAAgB,CAAC;CAAG,EAClH;AACF,CAAC"}
@@ -38,12 +38,12 @@ let node_fs = require("node:fs");
38
38
  node_fs = __toESM(node_fs, 1);
39
39
  let _agimon_ai_po_core_childProcess = require("@agimon-ai/po-core/childProcess");
40
40
  let _agimon_ai_po_telemetry = require("@agimon-ai/po-telemetry");
41
- let ignore = require("ignore");
42
- ignore = __toESM(ignore, 1);
43
- let node_child_process = require("node:child_process");
44
41
  let node_crypto = require("node:crypto");
45
42
  let _agimon_ai_po_core_piSettings = require("@agimon-ai/po-core/piSettings");
43
+ let node_child_process = require("node:child_process");
46
44
  let node_util = require("node:util");
45
+ let ignore = require("ignore");
46
+ ignore = __toESM(ignore, 1);
47
47
  let node_os = require("node:os");
48
48
  node_os = __toESM(node_os, 1);
49
49
  let _agimon_ai_po_config = require("@agimon-ai/po-config");
@@ -231,41 +231,6 @@ function createSessionPresence(options) {
231
231
  };
232
232
  }
233
233
  //#endregion
234
- //#region src/services/doomIgnore/index.ts
235
- function matchablePath(relPath) {
236
- const normalized = relPath.replace(/\\/gu, "/");
237
- if (normalized.length === 0 || normalized.includes("\0") || normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/") || /^[A-Za-z]:\//u.test(normalized)) return;
238
- return normalized;
239
- }
240
- /**
241
- * Builds the project's ignore test, or nothing when there are no rules.
242
- *
243
- * Shared because the same rules are needed twice: once when deciding what to
244
- * record, and once when deciding what to list. A caller that built its own
245
- * matcher would let the two drift apart.
246
- *
247
- * The test answers false for anything it cannot safely match, so a path that
248
- * escapes the project stays visible rather than being hidden by accident.
249
- */
250
- function createDoomIgnoreMatcher(content) {
251
- try {
252
- if (content.trim().length === 0) return void 0;
253
- const matcher = (0, ignore.default)().add(content);
254
- return (relPath) => {
255
- const candidate = matchablePath(relPath);
256
- return candidate !== void 0 && matcher.ignores(candidate);
257
- };
258
- } catch {
259
- return;
260
- }
261
- }
262
- /** Applies project-local gitignore rules while keeping unsafe candidates visible. */
263
- function filterDoomIgnoredFiles(items, content) {
264
- const ignored = createDoomIgnoreMatcher(content);
265
- if (ignored === void 0) return items;
266
- return items.filter((item) => !ignored(item.relPath));
267
- }
268
- //#endregion
269
234
  //#region src/services/lineDiff/index.ts
270
235
  /**
271
236
  * The line difference between two versions of a file, as bounded hunks.
@@ -713,7 +678,7 @@ var EditTracker = class {
713
678
  //#endregion
714
679
  //#region src/services/fileEditPaths/index.ts
715
680
  /** The directory this package owns, under whichever root holds agent state. */
716
- const STATE_DIRECTORY = "doom-file-edit";
681
+ const STATE_DIRECTORY = "po-file-edit";
717
682
  function hash(value) {
718
683
  return (0, node_crypto.createHash)("sha256").update(value).digest("hex").slice(0, 16);
719
684
  }
@@ -731,7 +696,7 @@ var FileEditPaths = class {
731
696
  }
732
697
  /**
733
698
  * Where every session's state lives: the Pi agent directory, the same root
734
- * every other Doom package writes to, and never inside the working tree.
699
+ * every other Po package writes to, and never inside the working tree.
735
700
  *
736
701
  * An earlier layout put this in the repository's git common directory. That
737
702
  * is git's own storage: nothing in git's lifecycle prunes a directory we add
@@ -761,31 +726,6 @@ var FileEditPaths = class {
761
726
  snapshotsPath(cwd, sessionKey) {
762
727
  return `${this.timelinePath(cwd, sessionKey).replace(/\.jsonl$/u, "")}.blobs`;
763
728
  }
764
- legacyStateDirectory(cwd) {
765
- const common = this.gitCommonDirectory(cwd);
766
- return common === void 0 ? void 0 : node_path.default.join(common, STATE_DIRECTORY);
767
- }
768
- /** Only still read so the state an older build left behind can be cleared away. */
769
- gitCommonDirectory(cwd) {
770
- try {
771
- return (0, node_child_process.execFileSync)("git", [
772
- "rev-parse",
773
- "--path-format=absolute",
774
- "--git-common-dir"
775
- ], {
776
- cwd,
777
- encoding: "utf8",
778
- stdio: [
779
- "ignore",
780
- "pipe",
781
- "ignore"
782
- ]
783
- }).trim() || void 0;
784
- } catch (error) {
785
- if (error instanceof Error) return void 0;
786
- throw error;
787
- }
788
- }
789
729
  };
790
730
  //#endregion
791
731
  //#region src/services/gitStatus/index.ts
@@ -873,6 +813,41 @@ var NodeGitStatusAdapter = class {
873
813
  }
874
814
  }
875
815
  };
816
+ //#endregion
817
+ //#region src/services/poIgnore/index.ts
818
+ function matchablePath(relPath) {
819
+ const normalized = relPath.replace(/\\/gu, "/");
820
+ if (normalized.length === 0 || normalized.includes("\0") || normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/") || /^[A-Za-z]:\//u.test(normalized)) return;
821
+ return normalized;
822
+ }
823
+ /**
824
+ * Builds the project's ignore test, or nothing when there are no rules.
825
+ *
826
+ * Shared because the same rules are needed twice: once when deciding what to
827
+ * record, and once when deciding what to list. A caller that built its own
828
+ * matcher would let the two drift apart.
829
+ *
830
+ * The test answers false for anything it cannot safely match, so a path that
831
+ * escapes the project stays visible rather than being hidden by accident.
832
+ */
833
+ function createPoIgnoreMatcher(content) {
834
+ try {
835
+ if (content.trim().length === 0) return void 0;
836
+ const matcher = (0, ignore.default)().add(content);
837
+ return (relPath) => {
838
+ const candidate = matchablePath(relPath);
839
+ return candidate !== void 0 && matcher.ignores(candidate);
840
+ };
841
+ } catch {
842
+ return;
843
+ }
844
+ }
845
+ /** Applies project-local gitignore rules while keeping unsafe candidates visible. */
846
+ function filterPoIgnoredFiles(items, content) {
847
+ const ignored = createPoIgnoreMatcher(content);
848
+ if (ignored === void 0) return items;
849
+ return items.filter((item) => !ignored(item.relPath));
850
+ }
876
851
  /** How much of a file is read before deciding it is binary. */
877
852
  const BINARY_SAMPLE_BYTES$1 = 8192;
878
853
  const MISSING_CODES = /* @__PURE__ */ new Set([
@@ -1302,7 +1277,7 @@ var NodeTreeManifestAdapter = class {
1302
1277
  /** The frame type the hub publishes and the web plugin claims; declared in the poWeb block too. */
1303
1278
  const filesChannelType = "file_edits";
1304
1279
  /** Footer status key whose presence shows the activity group; the extension publishes it. */
1305
- const filesStatusKey = "doom-file-edit-files";
1280
+ const filesStatusKey = "po-file-edit-files";
1306
1281
  //#endregion
1307
1282
  //#region src/types/apiRoutes.ts
1308
1283
  /**
@@ -1700,7 +1675,7 @@ const api = {
1700
1675
  function plansRoots(cwd, environment) {
1701
1676
  const home = environment.HOME ?? node_os.default.homedir();
1702
1677
  try {
1703
- const configured = (0, _agimon_ai_po_config.loadDoomConfig)(cwd, home, environment).modes?.planning?.plansDirectory;
1678
+ const configured = (0, _agimon_ai_po_config.loadPoConfig)(cwd, home, environment).modes?.planning?.plansDirectory;
1704
1679
  return [(0, _agimon_ai_po_config.resolvePlanningPlansDirectory)(configured, cwd, home)];
1705
1680
  } catch {
1706
1681
  return [];
@@ -1827,7 +1802,7 @@ function readSessionFiles(timelinePath, cwd) {
1827
1802
  diffable: isDiffable(foldVersions(events, entry.path))
1828
1803
  }));
1829
1804
  try {
1830
- return filterDoomIgnoredFiles(items, node_fs.default.readFileSync(node_path.default.join(cwd, ".poignore"), "utf8"));
1805
+ return filterPoIgnoredFiles(items, node_fs.default.readFileSync(node_path.default.join(cwd, ".poignore"), "utf8"));
1831
1806
  } catch {
1832
1807
  return items;
1833
1808
  }
@@ -1883,10 +1858,10 @@ function createFilesChannel() {
1883
1858
  }
1884
1859
  //#endregion
1885
1860
  //#region src/services/fileEditSession/index.ts
1886
- const DOOM_IGNORE_FILE = ".poignore";
1887
- function readDoomIgnore(cwd) {
1861
+ const PO_IGNORE_FILE = ".poignore";
1862
+ function readPoIgnore(cwd) {
1888
1863
  try {
1889
- const matcher = createDoomIgnoreMatcher(node_fs.default.readFileSync(node_path.default.join(cwd, DOOM_IGNORE_FILE), "utf8"));
1864
+ const matcher = createPoIgnoreMatcher(node_fs.default.readFileSync(node_path.default.join(cwd, PO_IGNORE_FILE), "utf8"));
1890
1865
  return matcher === void 0 ? void 0 : (filePath) => matcher(node_path.default.relative(cwd, filePath));
1891
1866
  } catch {
1892
1867
  return;
@@ -1905,8 +1880,8 @@ function createFileEditSession({ agent, host }) {
1905
1880
  const editTracker = new EditTracker(timeline, snapshots, new NodeTreeManifestAdapter(), { git: new NodeGitStatusAdapter() });
1906
1881
  if (host.context.directEvents === void 0) throw new Error("File-edit headless facet requires the session direct event bus.");
1907
1882
  const directEvents = host.context.directEvents;
1908
- const telemetry = (0, _agimon_ai_po_telemetry.createDoomTelemetry)({
1909
- serviceName: "doom-file-edit",
1883
+ const telemetry = (0, _agimon_ai_po_telemetry.createPoTelemetry)({
1884
+ serviceName: "po-file-edit",
1910
1885
  packageName: "@agimon-ai/po-file-edit",
1911
1886
  env: environment,
1912
1887
  enableLogs: true,
@@ -1963,7 +1938,7 @@ function createFileEditSession({ agent, host }) {
1963
1938
  await timeline.clear();
1964
1939
  await snapshots.clear();
1965
1940
  }
1966
- await telemetry.recordEvent("doom_file_edit.timeline_finished", { outcome: "closed" });
1941
+ await telemetry.recordEvent("po_file_edit.timeline_finished", { outcome: "closed" });
1967
1942
  }
1968
1943
  } finally {
1969
1944
  await telemetry.shutdown();
@@ -1986,7 +1961,7 @@ function createFileEditSession({ agent, host }) {
1986
1961
  const snapshotsPath = paths.snapshotsPath(executionContext.cwd, sessionKey);
1987
1962
  timeline.initialize(timelinePath);
1988
1963
  snapshots.initialize(snapshotsPath);
1989
- const isIgnored = readDoomIgnore(executionContext.cwd);
1964
+ const isIgnored = readPoIgnore(executionContext.cwd);
1990
1965
  editTracker.reset({
1991
1966
  exclude: [
1992
1967
  timelinePath,
@@ -2009,16 +1984,11 @@ function createFileEditSession({ agent, host }) {
2009
1984
  directory: paths.stateDirectory(),
2010
1985
  keep: timelinePath
2011
1986
  });
2012
- const legacy = paths.legacyStateDirectory(executionContext.cwd);
2013
- if (legacy !== void 0 && legacy !== paths.stateDirectory()) await sweepSessionState({
2014
- directory: legacy,
2015
- removeDirectory: true
2016
- });
2017
1987
  } catch {}
2018
1988
  });
2019
1989
  if (!initialized) {
2020
1990
  initialized = true;
2021
- await track(() => telemetry.recordEvent("doom_file_edit.timeline_started", { outcome: "initialized" }));
1991
+ await track(() => telemetry.recordEvent("po_file_edit.timeline_started", { outcome: "initialized" }));
2022
1992
  }
2023
1993
  return () => active === runtime ? stopActivity() : Promise.resolve();
2024
1994
  }
@@ -2035,7 +2005,7 @@ function createFileEditSession({ agent, host }) {
2035
2005
  const hookStartedAt = Date.now();
2036
2006
  await editTracker.start(toolCallId, toolName, event.args, executionContext.cwd);
2037
2007
  if (active !== runtime) return;
2038
- await telemetry.recordEvent("doom_file_edit.edit_started", {
2008
+ await telemetry.recordEvent("po_file_edit.edit_started", {
2039
2009
  "tool.name": toolName,
2040
2010
  hook_ms: Date.now() - hookStartedAt
2041
2011
  });
@@ -2056,7 +2026,7 @@ function createFileEditSession({ agent, host }) {
2056
2026
  await editTracker.end(toolCallId, isError, executionContext.cwd);
2057
2027
  if (active !== runtime) return;
2058
2028
  publish(runtime);
2059
- await telemetry.recordEvent("doom_file_edit.edit_finished", {
2029
+ await telemetry.recordEvent("po_file_edit.edit_finished", {
2060
2030
  "tool.name": toolName,
2061
2031
  "tool.result.error": isError,
2062
2032
  ...startedAt === void 0 ? {} : { duration_ms: Date.now() - startedAt },
@@ -2081,7 +2051,7 @@ function createPresence({ agent, host, context }) {
2081
2051
  cwd: execution.cwd,
2082
2052
  workflowStep: execution.sessionContext?.provenance === _agimon_ai_po_core_hubChannel.WORKFLOW_STEP_SESSION_PROVENANCE,
2083
2053
  isLive: () => host.context.sessionService?.isLive(execution.sessionId) ?? true,
2084
- labelFor: (runId) => (0, _agimon_ai_po_core_childSession.readDoomChildSessionLabels)(context)?.labelFor(runId),
2054
+ labelFor: (runId) => (0, _agimon_ai_po_core_childSession.readPoChildSessionLabels)(context)?.labelFor(runId),
2085
2055
  onError: (error) => host.context.onNotice(`Edit presence failed: ${error instanceof Error ? error.message : String(error)}`)
2086
2056
  });
2087
2057
  }
@@ -2094,10 +2064,12 @@ const createFileEditServerRoot = (context) => {
2094
2064
  ...presence === void 0 ? {} : { presence }
2095
2065
  },
2096
2066
  activities: runtime.activities,
2097
- ...presence === void 0 ? {} : { services: [(cordis) => cordis.inject([_agimon_ai_po_core_childSession.DOOM_CHILD_SESSION_HOOK_REGISTRY_SERVICE], (registryContext) => {
2098
- const registration = registryContext.get(_agimon_ai_po_core_childSession.DOOM_CHILD_SESSION_HOOK_REGISTRY_SERVICE).register(presence.childHooks);
2099
- return () => registration.dispose();
2100
- })] },
2067
+ ...presence === void 0 ? {} : { services: [(cordis) => {
2068
+ cordis.inject([_agimon_ai_po_core_childSession.PO_CHILD_SESSION_HOOK_REGISTRY_SERVICE], (registryContext) => {
2069
+ const registration = registryContext.get(_agimon_ai_po_core_childSession.PO_CHILD_SESSION_HOOK_REGISTRY_SERVICE).register(presence.childHooks);
2070
+ return () => registration.dispose();
2071
+ });
2072
+ }] },
2101
2073
  onDispose: async (pluginContext) => {
2102
2074
  presence?.dispose();
2103
2075
  await runtime.onDispose?.(pluginContext);