|
|
@@ -68,6 +68,9 @@ function readSessions(): CodexSessionMeta[] {
|
|
|
sandbox: item.sandbox ?? DEFAULT_RUN_OPTIONS.sandbox,
|
|
|
approvalPolicy: item.approvalPolicy ?? DEFAULT_RUN_OPTIONS.approvalPolicy,
|
|
|
external: item.external === true,
|
|
|
+ artifactManifest: Array.isArray(item.artifactManifest)
|
|
|
+ ? item.artifactManifest.filter((p: unknown): p is string => typeof p === 'string' && !!p)
|
|
|
+ : undefined,
|
|
|
}));
|
|
|
} catch {
|
|
|
return [];
|
|
|
@@ -712,6 +715,8 @@ export const useCodexChatStore = defineStore('codex-chat', {
|
|
|
} finally {
|
|
|
this.runningThreads = this.runningThreads.filter((id) => id !== threadId);
|
|
|
await this.ensureEvents(threadId);
|
|
|
+ // Codex 亲口改过的文件累计进会话产物清单(精确归属,跨回合持久)
|
|
|
+ this.accumulateArtifactManifest(threadId);
|
|
|
// 回合结束后标记新产物(与上一轮的快照对比),并刷新本会话产物
|
|
|
await this.refreshArtifacts(undefined, true);
|
|
|
await this.loadSessionFiles();
|
|
|
@@ -778,10 +783,44 @@ export const useCodexChatStore = defineStore('codex-chat', {
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
- * 加载「本会话产物」。
|
|
|
+ * 把该会话**历次**回合里 patch 记录过的文件路径累计进 `artifactManifest`。
|
|
|
*
|
|
|
- * 判定口径:mtime ≥ 会话创建时间。会话创建后 Codex 产出或改过的文件都会命中;
|
|
|
- * 别的会话更早生成的文件不会混进来。并行会话改同一个文件时会同时出现在两边,属可接受误差。
|
|
|
+ * 以事件流为准(不是只看最后一轮):文件一旦被本会话改过就一直归属它,
|
|
|
+ * 不会因为后续回合没再动它而消失。路径取自 `*** Add/Update/Delete File:` 段落头,
|
|
|
+ * 认不出的段落不记——宁缺勿错。
|
|
|
+ */
|
|
|
+ accumulateArtifactManifest(threadId: string) {
|
|
|
+ const events = this.events[threadId] ?? [];
|
|
|
+ if (!events.length) return;
|
|
|
+ const session = this.sessions.find((item) => item.threadId === threadId);
|
|
|
+ if (!session) return;
|
|
|
+ const manifest = new Set(session.artifactManifest ?? []);
|
|
|
+ let added = false;
|
|
|
+ for (const event of events) {
|
|
|
+ if (event.kind !== 'file') continue;
|
|
|
+ for (const change of collectCodexArtifactChanges([event])) {
|
|
|
+ if (change.path) {
|
|
|
+ const before = manifest.size;
|
|
|
+ manifest.add(change.path);
|
|
|
+ if (manifest.size !== before) added = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!added) return;
|
|
|
+ this.sessions = this.sessions.map((item) =>
|
|
|
+ item.threadId === threadId ? { ...item, artifactManifest: [...manifest] } : item,
|
|
|
+ );
|
|
|
+ this.persistSessions();
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加载「本会话产物」——混合口径(精确优先,时间兜底)。
|
|
|
+ *
|
|
|
+ * 1. **精确**:`artifactManifest`(Codex 每回合 patch 里亲口记录的改动文件,累计持久);
|
|
|
+ * 2. **兜底**:清单没覆盖到的情况——历史会话(升级前没有清单)、exec 直接写文件
|
|
|
+ * (不产生 patch 事件)、patch 解析失败——回落到 mtime ≥ 会话创建时间。
|
|
|
+ *
|
|
|
+ * 两者取并集:清单保证「自己改过的必在」,mtime 保证「兜底场景不空」。
|
|
|
*/
|
|
|
async loadSessionFiles() {
|
|
|
const root = this.resolveCwd();
|
|
|
@@ -793,9 +832,12 @@ export const useCodexChatStore = defineStore('codex-chat', {
|
|
|
this.sessionFiles.loading = true;
|
|
|
try {
|
|
|
const result = await codexArtifactApi.codexArtifactListAll({ root });
|
|
|
+ const manifest = new Set(session.artifactManifest ?? []);
|
|
|
const since = Date.parse(session.createdAt);
|
|
|
this.sessionFiles = {
|
|
|
- entries: result.entries.filter((item) => Date.parse(item.mtime) >= since),
|
|
|
+ entries: result.entries.filter(
|
|
|
+ (item) => manifest.has(item.relative) || Date.parse(item.mtime) >= since,
|
|
|
+ ),
|
|
|
truncated: result.truncated,
|
|
|
loading: false,
|
|
|
};
|