* review: harden readonly git checks and lint verification * test: satisfy readonly reviewer sandbox typing * test: fix readonly reviewer sandbox assertions * test: remove impossible readonly sandbox guards * test: relax readonly sandbox assertions * test: cast readonly sandbox expectation shape * test: cast readonly sandbox expectation through unknown * Phase 0 — STEP_DONE/TASK_DONE split + owner-follow-up integration smoke * Add STEP_DONE guards, verdict storage, and stale delivery suppression * style: format paired stepdone telemetry files * Route STEP_DONE through reviewer * Add structured Discord attachments * style: format structured attachment test * Fix room thread bot output parity * Remove duplicate work item attachment migration from owner branch * Remove legacy dashboard rooms renderer * Extract dashboard parsed body renderer * Extract dashboard redaction helpers * Extract dashboard RoomCardV2 component * Include RoomCardV2 test in vitest suite * Extract dashboard RoomBoardV2 component * Extract dashboard EmptyState component * Extract dashboard InboxPanel component * Split dashboard InboxPanel card renderer * Extract dashboard TaskPanel component * Extract dashboard UsagePanel component * Fix dashboard room thread chunk rendering * Extract dashboard ServicePanel component * Render dashboard live progress markdown * Extract dashboard SettingsPanel component * Fix structured attachment rendering
102 lines
3.0 KiB
TypeScript
102 lines
3.0 KiB
TypeScript
import path from 'path';
|
|
|
|
import {
|
|
extractImageTagPaths,
|
|
normalizeEjclawStructuredOutput,
|
|
} from '../agent-protocol.js';
|
|
import type { OutboundAttachment } from '../types.js';
|
|
|
|
const IMAGE_EXTS = /\.(png|jpe?g|gif|webp|bmp)$/i;
|
|
const MD_LINK_RE = /\[[^\]]*\]\((\/[^)]+)\)/g;
|
|
|
|
export interface PreparedDiscordOutbound {
|
|
text: string;
|
|
cleanText: string;
|
|
attachments: OutboundAttachment[];
|
|
attachmentSource: 'structured' | 'md-link' | 'image-tag' | 'none';
|
|
silent: boolean;
|
|
}
|
|
|
|
function extractMarkdownImageAttachments(text: string): {
|
|
cleanText: string;
|
|
attachments: OutboundAttachment[];
|
|
} {
|
|
const attachments: OutboundAttachment[] = [];
|
|
const seen = new Set<string>();
|
|
|
|
const cleanText = text.replace(MD_LINK_RE, (_full, rawPath: string) => {
|
|
const trimmed = rawPath.trim();
|
|
if (IMAGE_EXTS.test(trimmed)) {
|
|
if (!seen.has(trimmed)) {
|
|
attachments.push({
|
|
path: trimmed,
|
|
name: path.basename(trimmed),
|
|
});
|
|
seen.add(trimmed);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
const basename = path.basename(trimmed.replace(/#.*$/, ''));
|
|
const lineMatch = trimmed.match(/#L(\d+)/);
|
|
return lineMatch ? `\`${basename}:${lineMatch[1]}\`` : `\`${basename}\``;
|
|
});
|
|
|
|
return { cleanText, attachments };
|
|
}
|
|
|
|
function imageTagPathsToAttachments(paths: string[]): OutboundAttachment[] {
|
|
return paths
|
|
.filter((filePath) => IMAGE_EXTS.test(filePath))
|
|
.map((filePath) => ({
|
|
path: filePath,
|
|
name: path.basename(filePath),
|
|
}));
|
|
}
|
|
|
|
export function prepareDiscordOutbound(
|
|
text: string,
|
|
optionAttachments: OutboundAttachment[] | undefined,
|
|
): PreparedDiscordOutbound {
|
|
const normalized = normalizeEjclawStructuredOutput(text);
|
|
if (normalized.output?.visibility === 'silent') {
|
|
return {
|
|
text: '',
|
|
cleanText: '',
|
|
attachments: [],
|
|
attachmentSource: 'none',
|
|
silent: true,
|
|
};
|
|
}
|
|
|
|
const structuredOutput =
|
|
normalized.output?.visibility === 'public' ? normalized.output : null;
|
|
const outboundText = structuredOutput?.text ?? normalized.result ?? text;
|
|
const structuredAttachments =
|
|
optionAttachments && optionAttachments.length > 0
|
|
? optionAttachments
|
|
: (structuredOutput?.attachments ?? []);
|
|
const hasStructuredAttachments = structuredAttachments.length > 0;
|
|
const markdownExtracted = extractMarkdownImageAttachments(outboundText);
|
|
const imageTagExtracted = extractImageTagPaths(markdownExtracted.cleanText);
|
|
const legacyImageTagAttachments = imageTagPathsToAttachments(
|
|
imageTagExtracted.imagePaths,
|
|
);
|
|
|
|
return {
|
|
text: outboundText,
|
|
cleanText: imageTagExtracted.cleanText,
|
|
attachments: hasStructuredAttachments
|
|
? structuredAttachments
|
|
: [...markdownExtracted.attachments, ...legacyImageTagAttachments],
|
|
attachmentSource: hasStructuredAttachments
|
|
? 'structured'
|
|
: markdownExtracted.attachments.length > 0
|
|
? 'md-link'
|
|
: legacyImageTagAttachments.length > 0
|
|
? 'image-tag'
|
|
: 'none',
|
|
silent: false,
|
|
};
|
|
}
|