Merge PR #150 from aakhter/cod-166-codex-generated-artifact-attachments: Codex generated artifacts as attachment cards

Includes review fixes: source arg threaded through the deps lambda (was silently dropped), codex-mode gating, realpath-first trust decisions with homedir-anchored markers, image thumbnail passthrough, ANSI-stripped scanning.

# Conflicts:
#	src/session.ts
This commit is contained in:
Codeman maintainer
2026-07-12 20:08:32 +02:00
11 changed files with 402 additions and 27 deletions
+17 -9
View File
@@ -84,7 +84,7 @@ import { SessionAutoOps } from './session-auto-ops.js';
import { detectUsageLimitPause } from './usage-limit-patterns.js';
import { SessionTaskCache } from './session-task-cache.js';
import { InteractivePtyExitBreaker } from './session-pty-exit-breaker.js';
import { parseAttachmentMagicLinks } from './attachment-magic.js';
import { parseTerminalAttachmentRequests } from './attachment-magic.js';
import {
sanitizeAttachmentHistory,
upsertAttachmentHistory as upsertAttachmentHistoryList,
@@ -1257,18 +1257,26 @@ export class Session extends EventEmitter {
.replace(/\x1b\[\?(?:1000|1001|1002|1003|1005|1006|1007)[hl]/g, '');
}
// Scan terminal output for `codeman://attach?path=...` magic links and emit
// an attachmentRequested event for each newly-seen absolute path. The web
// server turns these into registered attachment cards.
const attachmentPaths = parseAttachmentMagicLinks(data);
for (const attachmentPath of attachmentPaths) {
if (this._attachmentMagicSeen.has(attachmentPath)) continue;
this._attachmentMagicSeen.add(attachmentPath);
// Scan terminal output for attachment requests. `codeman://attach?...` is an
// explicit magic link (all modes); Codex generated images report
// `Saved to: file://...` — that scanner (and its relaxed trust policy) is
// only enabled for codex-mode sessions. The web server applies the trust
// boundary for each request source.
const attachmentRequests = parseTerminalAttachmentRequests(data, { codexArtifacts: this.mode === 'codex' });
for (const request of attachmentRequests) {
const seenKey = `${request.source}:${request.path}`;
if (this._attachmentMagicSeen.has(seenKey)) continue;
this._attachmentMagicSeen.add(seenKey);
if (this._attachmentMagicSeen.size > 200) {
const oldest = this._attachmentMagicSeen.values().next().value;
if (oldest) this._attachmentMagicSeen.delete(oldest);
}
this.emit('attachmentRequested', { sessionId: this.id, path: attachmentPath, timestamp: Date.now() });
this.emit('attachmentRequested', {
sessionId: this.id,
path: request.path,
source: request.source,
timestamp: Date.now(),
});
}
// BufferAccumulator handles auto-trimming when max size exceeded