amalgm 0.1.103 → 0.1.104
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/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/lib/email-embeds.js +169 -81
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-c-00599c-ffffff.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-c-5c6bc0-ffffff.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-c-68217a-ffffff.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-go-00add8-ffffff.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-j-e76f00-ffffff.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-js-f7df1e-1c1917.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-kt-7f52ff-ffffff.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-php-777bb4-ffffff.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-rb-cc342d-ffffff.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-rs-ce4a1f-ffffff.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-s-cd6799-ffffff.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-sw-f05138-ffffff.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-ts-3178c6-ffffff.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-v-42b883-ffffff.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-box-ffffff.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-braces-facc15.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-braces-fb923c.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-calendarclock-d6d3d1.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-database-60a5fa.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-externallink-ffffff.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-file-a8a29e.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-filearchive-facc15.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-fileimage-2dd4bf.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-fileimage-c084fc.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-filespreadsheet-4ade80.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-filetext-60a5fa.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-filetext-a8a29e.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-filetext-f87171.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-folder-d6d3d1.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-globe-d6d3d1.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-lock-facc15.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-messagesquare-ffffff.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-music-f472b6.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-package-38bdf8.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-package-facc15.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-settings-a8a29e.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-settings-d6d3d1.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-settings-f87171.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-terminal-4ade80.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-video-f472b6.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-zap-fbbf24.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-path-css3-1572b6.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-path-html5-e34f26.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-path-markdown-a8a29e.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-path-python-4b8bbe.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-react.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-skill.png +0 -0
- package/runtime/scripts/amalgm-mcp/lib/email-icons.js +53 -0
- package/runtime/scripts/amalgm-mcp/lib/email-md.js +18 -3
- package/runtime/scripts/amalgm-mcp/lib/file-icon-registry.json +995 -0
- package/runtime/scripts/amalgm-mcp/notify/email-media.js +16 -4
- package/runtime/scripts/amalgm-mcp/notify/index.js +6 -1
- package/runtime/scripts/amalgm-mcp/tests/email-embeds.test.js +97 -0
- package/runtime/scripts/amalgm-mcp/tests/email-media.test.js +19 -0
package/package.json
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* primitives: FileEmbed (media), EmbedChrome (the floating name/options
|
|
4
4
|
* pills on expanded embeds), and the tile:/full: entity renders.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* The UI standard is EmbedChrome: top-left `[icon] filename` and top-right
|
|
7
|
+
* `[external-link] Open` pills inside the embed body. Email keeps that visual
|
|
8
|
+
* contract with table-built pills instead of CSS positioning: Gmail reflows
|
|
9
|
+
* absolute/fixed chrome, but it preserves tables. Body mechanics are still
|
|
10
|
+
* surface-specific: Gmail/Outlook drop <video> but render its fallback content;
|
|
11
|
+
* Apple Mail and iOS Mail play video inline.
|
|
11
12
|
*
|
|
12
13
|
* Media table mirrors amalgm-ui/lib/rendering/media.ts; visual values
|
|
13
14
|
* mirror EMAIL_TOKENS in email-md.js / lib/rendering/tokens.ts. Keep all
|
|
@@ -24,6 +25,9 @@ const COLOR = {
|
|
|
24
25
|
link: '#60a5fa', // blue-400
|
|
25
26
|
};
|
|
26
27
|
|
|
28
|
+
const FILE_ICON_REGISTRY = require('./file-icon-registry.json');
|
|
29
|
+
const { iconContentId } = require('./email-icons');
|
|
30
|
+
|
|
27
31
|
const EXTENSION_MIME = {
|
|
28
32
|
// Images
|
|
29
33
|
png: 'image/png',
|
|
@@ -80,8 +84,8 @@ function mediaMimeOf(nameOrUrl) {
|
|
|
80
84
|
return EXTENSION_MIME[mediaExtensionOf(nameOrUrl)] || null;
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
function mediaKindOf(nameOrUrl) {
|
|
84
|
-
const mime = mediaMimeOf(nameOrUrl);
|
|
87
|
+
function mediaKindOf(nameOrUrl, explicitMime) {
|
|
88
|
+
const mime = explicitMime || mediaMimeOf(nameOrUrl);
|
|
85
89
|
if (!mime) return 'unknown';
|
|
86
90
|
if (mime.startsWith('image/')) return 'image';
|
|
87
91
|
if (mime.startsWith('video/')) return 'video';
|
|
@@ -99,6 +103,51 @@ function mediaNameOf(nameOrUrl) {
|
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
105
|
|
|
106
|
+
function decodeURIComponentSafe(value) {
|
|
107
|
+
try {
|
|
108
|
+
return decodeURIComponent(value);
|
|
109
|
+
} catch {
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Decode a URL/path/pseudo-url into the basename that should appear in file
|
|
116
|
+
* chrome. This mirrors amalgm-ui/lib/rendering/embedIdentity.ts: file-backed
|
|
117
|
+
* embed tabs show the real filename, never markdown alt text.
|
|
118
|
+
*/
|
|
119
|
+
function fileBasenameOf(value) {
|
|
120
|
+
const raw = String(value || '').trim();
|
|
121
|
+
if (!raw) return '';
|
|
122
|
+
|
|
123
|
+
const attached = raw.match(/^attached:(.*)$/i);
|
|
124
|
+
if (attached) return decodeURIComponentSafe(attached[1]).replace(/\\/g, '/').split('/').pop() || '';
|
|
125
|
+
|
|
126
|
+
const amalgmFile = raw.match(/^amalgm:\/\/file\/([^?#]+)/i);
|
|
127
|
+
if (amalgmFile) return decodeURIComponentSafe(amalgmFile[1]).replace(/\\/g, '/').split('/').pop() || '';
|
|
128
|
+
|
|
129
|
+
const withoutQuery = raw.split(/[?#]/)[0];
|
|
130
|
+
const base = withoutQuery.replace(/\\/g, '/').split('/').pop() || '';
|
|
131
|
+
return decodeURIComponentSafe(base);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function resolveFileEmbedIdentity({ name, path: filePath, url, mimeType, fallback }) {
|
|
135
|
+
const label =
|
|
136
|
+
fileBasenameOf(name) ||
|
|
137
|
+
fileBasenameOf(filePath) ||
|
|
138
|
+
fileBasenameOf(url) ||
|
|
139
|
+
String(fallback || '').trim() ||
|
|
140
|
+
'File';
|
|
141
|
+
const mime = String(mimeType || '').trim() || mediaMimeOf(label) || mediaMimeOf(filePath || '') || mediaMimeOf(url || '');
|
|
142
|
+
const kind = mediaKindOf(label || filePath || url || '', mime || undefined);
|
|
143
|
+
return {
|
|
144
|
+
label,
|
|
145
|
+
extension: mediaExtensionOf(label),
|
|
146
|
+
mimeType: mime || null,
|
|
147
|
+
kind: kind === 'unknown' ? 'file' : kind,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
102
151
|
function isHttpUrl(url) {
|
|
103
152
|
return /^https?:\/\//i.test(url);
|
|
104
153
|
}
|
|
@@ -116,69 +165,117 @@ function isEmbeddableImageSrc(url) {
|
|
|
116
165
|
// --- EmbedChrome (email form) ------------------------------------------------
|
|
117
166
|
|
|
118
167
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
168
|
+
* Shared icon registry bridge. The React FileTypeIcon and email renderer both
|
|
169
|
+
* resolve from file-icon-registry.json. Email renders the same descriptor as a
|
|
170
|
+
* CID PNG because Gmail strips inline SVG inside message bodies.
|
|
122
171
|
*/
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
172
|
+
function normalizedBasename(value) {
|
|
173
|
+
return String(value || '').replace(/\\/g, '/').split('/').pop().trim().toLowerCase();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function matchesSpecialFilename(rule, normalizedName) {
|
|
177
|
+
return Boolean(
|
|
178
|
+
(rule.equals || []).includes(normalizedName) ||
|
|
179
|
+
(rule.startsWith || []).some((prefix) => normalizedName.startsWith(prefix)) ||
|
|
180
|
+
(rule.endsWith || []).some((suffix) => normalizedName.endsWith(suffix)),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function resolveFileIconDescriptor(name) {
|
|
185
|
+
const normalizedName = normalizedBasename(name);
|
|
186
|
+
const special = (FILE_ICON_REGISTRY.specialFilenameIcons || []).find((rule) => matchesSpecialFilename(rule, normalizedName));
|
|
187
|
+
if (special) return special.icon;
|
|
188
|
+
const ext = mediaExtensionOf(normalizedName);
|
|
189
|
+
return FILE_ICON_REGISTRY.extensionIcons[ext] || FILE_ICON_REGISTRY.defaults.file;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function resolveEntityIconDescriptor(type) {
|
|
193
|
+
const normalized = type === 'conversation' ? 'chat' : type === 'trigger' ? 'event' : type;
|
|
194
|
+
return FILE_ICON_REGISTRY.entityIcons[normalized] || FILE_ICON_REGISTRY.entityIcons.entity || FILE_ICON_REGISTRY.defaults.file;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function iconHtml(descriptor) {
|
|
198
|
+
if (!descriptor) descriptor = FILE_ICON_REGISTRY.defaults.file;
|
|
199
|
+
const contentId = iconContentId(descriptor);
|
|
200
|
+
return `<img src="cid:${contentId}" alt="" width="14" height="14" style="display:block;width:14px;height:14px;border:0;outline:none;text-decoration:none">`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function openIconHtml() {
|
|
204
|
+
return iconHtml({ kind: 'lucide', name: 'ExternalLink', colorClass: 'text-white', color: COLOR.text });
|
|
205
|
+
}
|
|
134
206
|
|
|
135
|
-
function
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
207
|
+
function chromeLabelParts(name) {
|
|
208
|
+
const value = String(name || 'File');
|
|
209
|
+
const solidLength = 12;
|
|
210
|
+
const fadeLength = 5;
|
|
211
|
+
if (value.length <= solidLength + 1) return { solid: value, fade: '' };
|
|
212
|
+
return {
|
|
213
|
+
solid: value.slice(0, solidLength),
|
|
214
|
+
fade: value.slice(solidLength, solidLength + fadeLength),
|
|
215
|
+
};
|
|
139
216
|
}
|
|
140
217
|
|
|
141
218
|
/**
|
|
142
|
-
* One
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* border carrying the rounded shape and white text.
|
|
219
|
+
* One floating pill, matching EmbedChrome: 32px tall, black fill, stone-800
|
|
220
|
+
* border, rounded-xl, white 13px text. This is intentionally a table, not
|
|
221
|
+
* inline flex/span layout, so Gmail keeps the icon and filename together.
|
|
146
222
|
*/
|
|
147
|
-
function
|
|
148
|
-
return
|
|
223
|
+
function chromePill(cells, extra = '') {
|
|
224
|
+
return (
|
|
225
|
+
`<table role="presentation" cellpadding="0" cellspacing="0" style="display:inline-table;height:32px;background:${COLOR.bgChip};border:1px solid ${COLOR.pillBorder};border-radius:12px;border-collapse:separate;box-shadow:0 14px 35px rgba(0,0,0,0.45);${extra}">` +
|
|
226
|
+
`<tr>${cells}</tr></table>`
|
|
227
|
+
);
|
|
149
228
|
}
|
|
150
229
|
|
|
151
|
-
/** `[glyph] name` —
|
|
152
|
-
function namePill(
|
|
230
|
+
/** `[glyph] name` — title text fades after the tab-width budget, matching the UI treatment. */
|
|
231
|
+
function namePill(iconDescriptor, name) {
|
|
232
|
+
const { solid, fade } = chromeLabelParts(name);
|
|
233
|
+
const fadeOpacities = [0.75, 0.5, 0.3, 0.16, 0.06];
|
|
153
234
|
const label =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
235
|
+
escapeText(solid) +
|
|
236
|
+
(fade
|
|
237
|
+
.split('')
|
|
238
|
+
.map((char, index) => `<span style="color:rgba(255,255,255,${fadeOpacities[index] || 0.06})">${escapeText(char)}</span>`)
|
|
239
|
+
.join(''));
|
|
240
|
+
const labelStyle = fade ? 'width:122px;max-width:122px;' : 'max-width:224px;';
|
|
241
|
+
return chromePill(
|
|
242
|
+
`<td style="padding:0 0 0 12px;vertical-align:middle;line-height:0;width:18px">${iconHtml(iconDescriptor)}</td>` +
|
|
243
|
+
`<td style="padding:0 12px 0 8px;vertical-align:middle">` +
|
|
244
|
+
`<div title="${escapeAttr(name)}" style="display:block;${labelStyle}overflow:hidden;white-space:nowrap;word-break:normal;overflow-wrap:normal;font-size:13px;line-height:30px;color:${COLOR.text};font-weight:400">${label}</div>` +
|
|
245
|
+
`</td>`
|
|
246
|
+
);
|
|
157
247
|
}
|
|
158
248
|
|
|
159
249
|
/** The single "Open ↗" affordance — a link when href is given. */
|
|
160
250
|
function openPill(href) {
|
|
161
|
-
const
|
|
251
|
+
const pill = chromePill(
|
|
252
|
+
`<td style="padding:0 0 0 12px;vertical-align:middle;line-height:0;width:14px">${openIconHtml()}</td>` +
|
|
253
|
+
`<td style="padding:0 12px 0 7px;vertical-align:middle;font-size:13px;line-height:30px;color:${COLOR.text};font-weight:400;white-space:nowrap">Open</td>`
|
|
254
|
+
);
|
|
162
255
|
return href
|
|
163
|
-
? `<a href="${escapeAttr(href)}" target="_blank" style="text-decoration:none">${
|
|
164
|
-
:
|
|
256
|
+
? `<a href="${escapeAttr(href)}" target="_blank" style="text-decoration:none;display:inline-block">${pill}</a>`
|
|
257
|
+
: pill;
|
|
165
258
|
}
|
|
166
259
|
|
|
167
260
|
/**
|
|
168
|
-
* The expanded-embed card: rounded-16 stone-900 border, black fill,
|
|
169
|
-
*
|
|
170
|
-
*
|
|
261
|
+
* The expanded-embed card: rounded-16 stone-900 border, black fill, and
|
|
262
|
+
* floating-style pills inside the body. The chrome row is a table with black
|
|
263
|
+
* background, not an email-only header label: it is the email-safe form of
|
|
264
|
+
* EmbedChrome's top-left/top-right tabs.
|
|
171
265
|
*/
|
|
172
|
-
function embedShell({
|
|
173
|
-
const left = name ? namePill(
|
|
266
|
+
function embedShell({ iconDescriptor, name, action, body = '', minHeight = 96 }) {
|
|
267
|
+
const left = name ? namePill(iconDescriptor, name) : '';
|
|
174
268
|
const right = action ? openPill(action.href) : '';
|
|
269
|
+
const content = body || `<div style="height:${minHeight}px;line-height:${minHeight}px;font-size:0;background:${COLOR.bg}"> </div>`;
|
|
175
270
|
return (
|
|
176
271
|
`<div style="border:1px solid ${COLOR.border};border-radius:16px;background:${COLOR.bg};margin:16px 0;overflow:hidden">` +
|
|
177
|
-
`<
|
|
178
|
-
`<
|
|
179
|
-
`<td align="
|
|
272
|
+
`<div style="padding:12px 12px 10px;background:${COLOR.bg}">` +
|
|
273
|
+
`<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse"><tr>` +
|
|
274
|
+
`<td align="left" style="padding:0;vertical-align:top;line-height:0">${left}</td>` +
|
|
275
|
+
`<td align="right" style="padding:0;vertical-align:top;line-height:0">${right}</td>` +
|
|
180
276
|
`</tr></table>` +
|
|
181
|
-
|
|
277
|
+
`</div>` +
|
|
278
|
+
content +
|
|
182
279
|
`</div>`
|
|
183
280
|
);
|
|
184
281
|
}
|
|
@@ -221,7 +318,8 @@ function posterBlock(posterSrc, label, href) {
|
|
|
221
318
|
* frame when one is provided, a play card otherwise.
|
|
222
319
|
*/
|
|
223
320
|
function renderVideoEmbed(label, url, poster) {
|
|
224
|
-
const
|
|
321
|
+
const identity = resolveFileEmbedIdentity({ url, fallback: label || 'Video' });
|
|
322
|
+
const name = identity.label;
|
|
225
323
|
const safeUrl = escapeAttr(url);
|
|
226
324
|
const posterSrc = poster && isEmbeddableImageSrc(poster) ? poster : null;
|
|
227
325
|
const fallback = posterSrc
|
|
@@ -233,7 +331,7 @@ function renderVideoEmbed(label, url, poster) {
|
|
|
233
331
|
`<video controls preload="metadata" src="${safeUrl}"${posterSrc ? ` poster="${escapeAttr(posterSrc)}"` : ''} style="display:block;width:100%;max-height:480px;background:${COLOR.bg}">` +
|
|
234
332
|
fallback +
|
|
235
333
|
`</video>`;
|
|
236
|
-
return embedShell({
|
|
334
|
+
return embedShell({ iconDescriptor: resolveFileIconDescriptor(name), name, action: { href: url }, body });
|
|
237
335
|
}
|
|
238
336
|
|
|
239
337
|
/**
|
|
@@ -243,7 +341,8 @@ function renderVideoEmbed(label, url, poster) {
|
|
|
243
341
|
* Gmail gives attached videos a playable preview natively.
|
|
244
342
|
*/
|
|
245
343
|
function renderAttachedVideoCard(label, poster) {
|
|
246
|
-
const
|
|
344
|
+
const identity = resolveFileEmbedIdentity({ url: `attached:${label || ''}`, fallback: label || 'Video' });
|
|
345
|
+
const name = identity.label;
|
|
247
346
|
const posterSrc = poster && isEmbeddableImageSrc(poster) ? poster : null;
|
|
248
347
|
// Poster is the animating GIF in Gmail; the real video rides in the
|
|
249
348
|
// attachment strip. No in-body note — the header + strip carry it.
|
|
@@ -253,29 +352,19 @@ function renderAttachedVideoCard(label, poster) {
|
|
|
253
352
|
? `<a href="https://amalgm.ai" target="_blank" style="text-decoration:none;display:block"><img src="${escapeAttr(posterSrc)}" alt="${escapeAttr(name)}" style="display:block;width:100%;height:auto;background:${COLOR.bg}"></a>`
|
|
254
353
|
: `<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr>` +
|
|
255
354
|
`<td align="center" style="padding:44px 16px;background:${COLOR.bg}">${PLAY_PILL}</td></tr></table>`;
|
|
256
|
-
return embedShell({
|
|
355
|
+
return embedShell({ iconDescriptor: resolveFileIconDescriptor(name), name, action: { href: 'https://amalgm.ai' }, body });
|
|
257
356
|
}
|
|
258
357
|
|
|
259
358
|
/**
|
|
260
|
-
* Non-playable file in an embed position (pdf/docx/audio/anything):
|
|
261
|
-
*
|
|
359
|
+
* Non-playable file in an embed position (pdf/docx/audio/anything): email
|
|
360
|
+
* cannot safely preview arbitrary local documents, so the chrome carries the
|
|
361
|
+
* identity and open affordance without inventing a second email-only file row.
|
|
262
362
|
*/
|
|
263
363
|
function renderFileCardEmbed(label, url) {
|
|
264
|
-
const
|
|
265
|
-
const
|
|
364
|
+
const identity = resolveFileEmbedIdentity({ url, fallback: label || 'File' });
|
|
365
|
+
const name = identity.label;
|
|
266
366
|
const http = isHttpUrl(url);
|
|
267
|
-
|
|
268
|
-
const extChip = ext
|
|
269
|
-
? `<span style="font-size:10px;color:${COLOR.textMuted};padding-left:8px">${escapeText(ext)}</span>`
|
|
270
|
-
: '';
|
|
271
|
-
const meta = http
|
|
272
|
-
? ''
|
|
273
|
-
: `<span style="font-size:12px;color:${COLOR.textMuted};padding-left:10px">On your computer</span>`;
|
|
274
|
-
const body =
|
|
275
|
-
`<div style="padding:6px 16px 16px">` +
|
|
276
|
-
`<span style="font-size:13px;color:${COLOR.text}">${escapeText(name)}</span>${extChip}${meta}` +
|
|
277
|
-
`</div>`;
|
|
278
|
-
return embedShell({ glyph, name, action: { href: http ? url : 'https://amalgm.ai' }, body });
|
|
367
|
+
return embedShell({ iconDescriptor: resolveFileIconDescriptor(name), name, action: { href: http ? url : 'https://amalgm.ai' } });
|
|
279
368
|
}
|
|
280
369
|
|
|
281
370
|
/**
|
|
@@ -299,7 +388,7 @@ function renderMediaEmbed(alt, url, poster) {
|
|
|
299
388
|
try {
|
|
300
389
|
name = decodeURIComponent(name);
|
|
301
390
|
} catch {}
|
|
302
|
-
return renderAttachedVideoCard(
|
|
391
|
+
return renderAttachedVideoCard(name, poster);
|
|
303
392
|
}
|
|
304
393
|
const amalgmMatch = url.match(/^amalgm:\/\/file\/([^\s)?"]+)/);
|
|
305
394
|
if (amalgmMatch) {
|
|
@@ -339,15 +428,18 @@ function entityTypeLabel(type) {
|
|
|
339
428
|
}
|
|
340
429
|
|
|
341
430
|
/**
|
|
342
|
-
* Inline tile — the email form of
|
|
343
|
-
*
|
|
431
|
+
* Inline tile — the email-safe form of ChatRow: one black rounded row with an
|
|
432
|
+
* icon and title. No email-only type eyebrow.
|
|
344
433
|
*/
|
|
345
434
|
function renderEntityTile(label, type) {
|
|
346
435
|
const safeLabel = escapeText(label || entityTypeLabel(type));
|
|
436
|
+
const iconDescriptor = resolveEntityIconDescriptor(type);
|
|
347
437
|
return (
|
|
348
|
-
`<a href="https://amalgm.ai" target="_blank" style="text-decoration:none;display:inline-block;background:${COLOR.
|
|
349
|
-
`<
|
|
350
|
-
`<
|
|
438
|
+
`<a href="https://amalgm.ai" target="_blank" style="text-decoration:none;display:inline-block;background:${COLOR.bg};border:1px solid ${COLOR.border};border-radius:12px;padding:8px 12px;margin:2px 0;vertical-align:middle">` +
|
|
439
|
+
`<table role="presentation" cellpadding="0" cellspacing="0" style="border-collapse:collapse"><tr>` +
|
|
440
|
+
`<td style="padding:0 10px 0 0;vertical-align:middle;line-height:0">${iconHtml(iconDescriptor)}</td>` +
|
|
441
|
+
`<td style="padding:0;vertical-align:middle;font-size:14px;line-height:18px;color:${COLOR.text};font-weight:650">${safeLabel}</td>` +
|
|
442
|
+
`</tr></table>` +
|
|
351
443
|
`</a>`
|
|
352
444
|
);
|
|
353
445
|
}
|
|
@@ -359,13 +451,7 @@ function renderEntityTile(label, type) {
|
|
|
359
451
|
*/
|
|
360
452
|
function renderEntityExpanded(label, type) {
|
|
361
453
|
const display = label || entityTypeLabel(type);
|
|
362
|
-
|
|
363
|
-
`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr>` +
|
|
364
|
-
`<td align="center" style="padding:40px 16px 44px">` +
|
|
365
|
-
`<span style="display:block;font-size:11px;line-height:14px;color:${COLOR.textMuted};text-transform:uppercase;letter-spacing:0.04em;padding-bottom:6px">${escapeText(entityTypeLabel(type))}</span>` +
|
|
366
|
-
`<span style="display:block;font-size:16px;font-weight:650;color:${COLOR.text}">${escapeText(display)}</span>` +
|
|
367
|
-
`</td></tr></table>`;
|
|
368
|
-
return embedShell({ glyph: EMBED_GLYPH.entity, name: display, action: { href: 'https://amalgm.ai' }, body });
|
|
454
|
+
return embedShell({ iconDescriptor: resolveEntityIconDescriptor(type), name: display, action: { href: 'https://amalgm.ai' } });
|
|
369
455
|
}
|
|
370
456
|
|
|
371
457
|
function renderEntityEmbed(mode, type, label) {
|
|
@@ -377,6 +463,8 @@ module.exports = {
|
|
|
377
463
|
mediaMimeOf,
|
|
378
464
|
mediaKindOf,
|
|
379
465
|
mediaNameOf,
|
|
466
|
+
fileBasenameOf,
|
|
467
|
+
resolveFileEmbedIdentity,
|
|
380
468
|
isHttpUrl,
|
|
381
469
|
isCidUrl,
|
|
382
470
|
isEmbeddableImageSrc,
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-c-00599c-ffffff.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-c-5c6bc0-ffffff.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-c-68217a-ffffff.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-go-00add8-ffffff.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-j-e76f00-ffffff.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-js-f7df1e-1c1917.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-kt-7f52ff-ffffff.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-php-777bb4-ffffff.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-rb-cc342d-ffffff.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-rs-ce4a1f-ffffff.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-s-cd6799-ffffff.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-sw-f05138-ffffff.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-ts-3178c6-ffffff.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-letter-v-42b883-ffffff.png
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-calendarclock-d6d3d1.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-database-60a5fa.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-externallink-ffffff.png
ADDED
|
Binary file
|
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-filearchive-facc15.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-fileimage-2dd4bf.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-fileimage-c084fc.png
ADDED
|
Binary file
|
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-filetext-60a5fa.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-filetext-a8a29e.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-filetext-f87171.png
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-messagesquare-ffffff.png
ADDED
|
Binary file
|
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-package-38bdf8.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-package-facc15.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-settings-a8a29e.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-settings-d6d3d1.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-settings-f87171.png
ADDED
|
Binary file
|
package/runtime/scripts/amalgm-mcp/lib/email-icon-assets/amalgm-icon-lucide-terminal-4ade80.png
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const ICON_ASSET_DIR = path.join(__dirname, 'email-icon-assets');
|
|
5
|
+
const MAX_TOTAL_ATTACHMENTS = 8;
|
|
6
|
+
|
|
7
|
+
function sanitizeId(value) {
|
|
8
|
+
return String(value || 'icon')
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/#[0-9a-f]+/gi, (match) => match.slice(1))
|
|
11
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
12
|
+
.replace(/^-+|-+$/g, '')
|
|
13
|
+
.slice(0, 80) || 'icon';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function iconContentId(descriptor) {
|
|
17
|
+
if (!descriptor) return 'amalgm-icon-file';
|
|
18
|
+
if (descriptor.kind === 'lucide') return `amalgm-icon-lucide-${sanitizeId(descriptor.name)}-${sanitizeId(descriptor.color)}`;
|
|
19
|
+
if (descriptor.kind === 'letter') return `amalgm-icon-letter-${sanitizeId(descriptor.letters)}-${sanitizeId(descriptor.bg)}-${sanitizeId(descriptor.fg)}`;
|
|
20
|
+
if (descriptor.kind === 'path') return `amalgm-icon-path-${sanitizeId(descriptor.name)}-${sanitizeId(descriptor.fill)}`;
|
|
21
|
+
if (descriptor.kind === 'react') return 'amalgm-icon-react';
|
|
22
|
+
if (descriptor.kind === 'skill') return 'amalgm-icon-skill';
|
|
23
|
+
return 'amalgm-icon-file';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function collectIconContentIds(html) {
|
|
27
|
+
const ids = new Set();
|
|
28
|
+
const pattern = /cid:(amalgm-icon-[a-z0-9-]+)/gi;
|
|
29
|
+
let match;
|
|
30
|
+
while ((match = pattern.exec(html))) ids.add(match[1]);
|
|
31
|
+
return Array.from(ids);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function prepareEmailIconAttachments(html, existingAttachments = []) {
|
|
35
|
+
const remaining = Math.max(0, MAX_TOTAL_ATTACHMENTS - existingAttachments.length);
|
|
36
|
+
if (remaining <= 0) return [];
|
|
37
|
+
|
|
38
|
+
const attachments = [];
|
|
39
|
+
for (const contentId of collectIconContentIds(html)) {
|
|
40
|
+
if (attachments.length >= remaining) break;
|
|
41
|
+
const filePath = path.join(ICON_ASSET_DIR, `${contentId}.png`);
|
|
42
|
+
if (!fs.existsSync(filePath)) continue;
|
|
43
|
+
attachments.push({
|
|
44
|
+
filename: `${contentId}.png`,
|
|
45
|
+
content: fs.readFileSync(filePath).toString('base64'),
|
|
46
|
+
contentType: 'image/png',
|
|
47
|
+
contentId,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return attachments;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { iconContentId, prepareEmailIconAttachments };
|
|
@@ -59,6 +59,20 @@ function escapeHtmlText(value) {
|
|
|
59
59
|
.replace(/>/g, '>');
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function parseStandaloneMediaLine(line) {
|
|
63
|
+
const match = line.trim().match(/^!\[([^\]]*)\]\((.*)\)$/);
|
|
64
|
+
if (!match) return null;
|
|
65
|
+
let src = match[2].trim();
|
|
66
|
+
let title = null;
|
|
67
|
+
const titleMatch = src.match(/^(.*)\s+"([^"]*)"$/);
|
|
68
|
+
if (titleMatch) {
|
|
69
|
+
src = titleMatch[1].trim();
|
|
70
|
+
title = titleMatch[2];
|
|
71
|
+
}
|
|
72
|
+
if (!src) return null;
|
|
73
|
+
return { alt: match[1], src, title };
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
// Inline citation for amalgm:// references (files, folders, skills, agents).
|
|
63
77
|
// Codex-style: plain blue text at the surrounding font size — no chip.
|
|
64
78
|
// Non-clickable in email (local resources can't open from a mail client);
|
|
@@ -82,7 +96,8 @@ function inlineFormat(text) {
|
|
|
82
96
|
/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g,
|
|
83
97
|
(_, alt, url) => {
|
|
84
98
|
if (/^attached:/i.test(url)) {
|
|
85
|
-
|
|
99
|
+
const filename = embeds.fileBasenameOf(url) || alt || 'video';
|
|
100
|
+
return `<span style="color:${EMAIL_TOKENS.color.link}">${escapeHtmlText(filename)}</span> <span style="color:${EMAIL_TOKENS.color.textSecondary}">(attached)</span>`;
|
|
86
101
|
}
|
|
87
102
|
if (!embeds.isEmbeddableImageSrc(url)) {
|
|
88
103
|
return `<span style="color:${EMAIL_TOKENS.color.link}">${escapeHtmlText(alt || embeds.mediaNameOf(url) || 'file')}</span>`;
|
|
@@ -257,10 +272,10 @@ function markdownToEmailHtml(md) {
|
|
|
257
272
|
// Standalone media line → block embed (image bare, video in the
|
|
258
273
|
// EmbedChrome card, attached/local/doc files as cards). The optional
|
|
259
274
|
// title carries a video poster: .
|
|
260
|
-
const mediaMatch = trimmed
|
|
275
|
+
const mediaMatch = parseStandaloneMediaLine(trimmed);
|
|
261
276
|
if (mediaMatch) {
|
|
262
277
|
flushAll();
|
|
263
|
-
out.push(embeds.renderMediaEmbed(mediaMatch
|
|
278
|
+
out.push(embeds.renderMediaEmbed(mediaMatch.alt, mediaMatch.src, mediaMatch.title || null));
|
|
264
279
|
continue;
|
|
265
280
|
}
|
|
266
281
|
// Standalone entity render → tile card or expanded card.
|