@1agh/maude 0.38.1 → 0.39.1
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/apps/studio/acp/bridge.ts +144 -5
- package/apps/studio/acp/index.ts +11 -1
- package/apps/studio/api.ts +21 -0
- package/apps/studio/client/app.jsx +1 -0
- package/apps/studio/client/panels/ChatPanel.jsx +190 -4
- package/apps/studio/client/panels/acp-runtime.js +108 -0
- package/apps/studio/client/styles/6-acp-chat.css +123 -1
- package/apps/studio/dist/client.bundle.js +14 -14
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/http.ts +23 -3
- package/apps/studio/test/acp-attachment-serve.test.ts +74 -0
- package/apps/studio/test/acp-bridge.test.ts +132 -0
- package/apps/studio/test/acp-commands.test.ts +8 -3
- package/apps/studio/test/acp-origin-gate.test.ts +6 -4
- package/apps/studio/test/canvas-origin-gate.test.ts +5 -0
- package/apps/studio/test/chat-attachments.test.ts +117 -0
- package/apps/studio/test/fixtures/mock-acp-agent.mjs +24 -1
- package/apps/studio/whats-new.json +18 -0
- package/package.json +8 -8
|
@@ -390,6 +390,114 @@ function safeJson(value) {
|
|
|
390
390
|
}
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
// ── chat-attachment refs (image thumbnails + lightbox) ──
|
|
394
|
+
// A pasted image lives in the feed under two spellings: the LIVE bubble holds
|
|
395
|
+
// the collapsed chip token ([image-1] — resolved via the per-chat attachments
|
|
396
|
+
// map), while a RELOADED bubble (transcript) holds the already-expanded
|
|
397
|
+
// absolute path under `_chat/attachments/`. Both funnel into the same
|
|
398
|
+
// `<sha8>.<ext>` name the GET /_api/acp/attachment route serves. Pure — tested
|
|
399
|
+
// in test/chat-attachments.test.ts.
|
|
400
|
+
const ATTACHMENT_NAME = '[0-9a-f]{8}\\.(?:png|jpe?g|gif|webp)';
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* The content-addressed basename when the string is a `_chat/attachments/` path
|
|
404
|
+
* (absolute or relative), else null. Non-attachment paths never match.
|
|
405
|
+
*/
|
|
406
|
+
export function attachmentName(absPathOrText) {
|
|
407
|
+
const m = new RegExp(`(?:^|/)_chat/attachments/(${ATTACHMENT_NAME})$`).exec(
|
|
408
|
+
String(absPathOrText || '').trim()
|
|
409
|
+
);
|
|
410
|
+
return m ? m[1] : null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Split bubble text into ordered segments the renderer walks:
|
|
415
|
+
* { type:'text', text } — plain run, rendered verbatim
|
|
416
|
+
* { type:'chip', token, kind } — [image|file|link-N] (live bubble)
|
|
417
|
+
* { type:'attachment', name, raw } — expanded _chat/attachments path (reload)
|
|
418
|
+
*/
|
|
419
|
+
export function extractAttachmentRefs(text) {
|
|
420
|
+
const s = String(text || '');
|
|
421
|
+
const re = new RegExp(
|
|
422
|
+
`\\[(image|file|link)-\\d+\\]|\\S*/_chat/attachments/(${ATTACHMENT_NAME})`,
|
|
423
|
+
'g'
|
|
424
|
+
);
|
|
425
|
+
const segs = [];
|
|
426
|
+
let last = 0;
|
|
427
|
+
let m;
|
|
428
|
+
while ((m = re.exec(s))) {
|
|
429
|
+
if (m.index > last) segs.push({ type: 'text', text: s.slice(last, m.index) });
|
|
430
|
+
if (m[1]) segs.push({ type: 'chip', token: m[0], kind: m[1] });
|
|
431
|
+
else segs.push({ type: 'attachment', name: m[2], raw: m[0] });
|
|
432
|
+
last = m.index + m[0].length;
|
|
433
|
+
}
|
|
434
|
+
if (last < s.length) segs.push({ type: 'text', text: s.slice(last) });
|
|
435
|
+
return segs;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Image paths under the project's design root that an ASSISTANT message text
|
|
440
|
+
* references (e.g. `/design:screenshot` replying "Saved to: .design/_history/…/
|
|
441
|
+
* 001.png") → servable same-origin URLs for the thumbnail strip (DDR-145).
|
|
442
|
+
* Render-only: the main origin already serves designRoot statics with
|
|
443
|
+
* containment; nothing outside `<designRel>/` ever matches, SVG stays excluded
|
|
444
|
+
* (scriptable), and the per-message cap bounds a hostile/hallucinated wall of
|
|
445
|
+
* paths. Returns unique URLs in first-mention order.
|
|
446
|
+
*
|
|
447
|
+
* Containment is enforced client-side, NOT delegated to the server (DDR-145
|
|
448
|
+
* security follow-up): the assistant text is partly untrusted (tool output /
|
|
449
|
+
* indirect injection), so a token carrying a `..` dot-segment or ANY
|
|
450
|
+
* percent-encoding is rejected outright — otherwise `.design/../../etc/x.png`
|
|
451
|
+
* would collapse to `/etc/x.png` in the browser (and `..%2f` would decode
|
|
452
|
+
* server-side), silently widening the fetch surface from designRel to the whole
|
|
453
|
+
* repoRoot. The server's `safePathUnderRoot` stays the backstop; this makes the
|
|
454
|
+
* client guarantee match the docstring instead of leaning on it.
|
|
455
|
+
*/
|
|
456
|
+
export function designImageRefs(text, designRel = '.design', cap = 6) {
|
|
457
|
+
const rel = String(designRel || '.design').replace(/^\/+|\/+$/g, '');
|
|
458
|
+
if (!rel) return [];
|
|
459
|
+
const esc = rel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
460
|
+
// A path token: optional abs/relative prefix, then `<rel>/…/<img>`. Boundaries
|
|
461
|
+
// tolerate the markdown the paths arrive wrapped in (backticks, quotes,
|
|
462
|
+
// parens) and shed trailing punctuation via the lookahead.
|
|
463
|
+
// Bounded quantifiers ({0,256}/{1,256}) keep the per-start scan constant-time —
|
|
464
|
+
// a long failing candidate (attacker-influenceable assistant text) can't drive
|
|
465
|
+
// superlinear backtracking. No real designRoot path segment run approaches 256.
|
|
466
|
+
const re = new RegExp(
|
|
467
|
+
`(?:^|[\\s\`'"(\\[])((?:[^\\s\`'"()\\[\\]]{0,256}/)?${esc}/[^\\s\`'"()\\[\\]]{1,256}?\\.(?:png|jpe?g|gif|webp))(?=[\\s\`'")\\]]|[.,:;!?]|$)`,
|
|
468
|
+
'gi'
|
|
469
|
+
);
|
|
470
|
+
const urls = [];
|
|
471
|
+
let m;
|
|
472
|
+
while ((m = re.exec(String(text || ''))) && urls.length < cap) {
|
|
473
|
+
const raw = m[1];
|
|
474
|
+
const at = raw.lastIndexOf(`${rel}/`);
|
|
475
|
+
if (at !== 0 && raw[at - 1] !== '/') continue; // `not-.design/x.png` must not match
|
|
476
|
+
const relPath = raw.slice(at);
|
|
477
|
+
const url = `/${relPath}`;
|
|
478
|
+
// Traversal guard — ALLOWLIST the canonical form, don't blocklist escape
|
|
479
|
+
// spellings (DDR-145 security follow-up). Two lanes the browser/server
|
|
480
|
+
// normalize differently:
|
|
481
|
+
// 1. Reject ANY percent-encoding outright — a plain designRoot path in
|
|
482
|
+
// assistant prose never carries `%`, but `..%2f` survives the WHATWG
|
|
483
|
+
// `URL` parse (pathname keeps `%2F` literal) and then the SERVER's
|
|
484
|
+
// `safePathUnderRoot` decodes it out of designRel.
|
|
485
|
+
// 2. Parse exactly as the browser will (WHATWG collapses `..` dot-segments
|
|
486
|
+
// AND rewrites `\`→`/` for http(s)) and require the result byte-identical
|
|
487
|
+
// AND still under `/rel/` — closes `..`, `..\`, and mixed spellings.
|
|
488
|
+
if (relPath.includes('%')) continue;
|
|
489
|
+
let canon;
|
|
490
|
+
try {
|
|
491
|
+
canon = new URL(url, 'http://x').pathname;
|
|
492
|
+
} catch {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (canon !== url || !canon.startsWith(`/${rel}/`)) continue;
|
|
496
|
+
if (!urls.includes(url)) urls.push(url);
|
|
497
|
+
}
|
|
498
|
+
return urls;
|
|
499
|
+
}
|
|
500
|
+
|
|
393
501
|
// Expand collapsed paste chips ([image-1]/[file-1]/[link-1]) back to the real
|
|
394
502
|
// path/URL the user pasted, so Claude receives the actual value while the chat
|
|
395
503
|
// bubble keeps the compact badge. Unknown tokens (e.g. a stale one the user typed
|
|
@@ -1002,15 +1002,137 @@
|
|
|
1002
1002
|
flex-shrink: 0;
|
|
1003
1003
|
}
|
|
1004
1004
|
|
|
1005
|
+
/* Pasted-image thumbnail — a focusable button in the user bubble; clicking
|
|
1006
|
+
opens the lightbox. Sized like a chip row, never dominating the bubble. */
|
|
1007
|
+
.chat-thumb-btn {
|
|
1008
|
+
appearance: none;
|
|
1009
|
+
display: inline-block;
|
|
1010
|
+
padding: 0;
|
|
1011
|
+
margin: 2px 0;
|
|
1012
|
+
border: 0;
|
|
1013
|
+
background: transparent;
|
|
1014
|
+
border-radius: var(--radius-sm);
|
|
1015
|
+
cursor: zoom-in;
|
|
1016
|
+
line-height: 0;
|
|
1017
|
+
vertical-align: middle;
|
|
1018
|
+
}
|
|
1019
|
+
.chat-thumb {
|
|
1020
|
+
display: block;
|
|
1021
|
+
max-width: 180px;
|
|
1022
|
+
max-height: 140px;
|
|
1023
|
+
object-fit: cover;
|
|
1024
|
+
border: 1px solid var(--border-default);
|
|
1025
|
+
border-radius: var(--radius-sm);
|
|
1026
|
+
transition: border-color var(--dur-fast, 120ms) ease;
|
|
1027
|
+
}
|
|
1028
|
+
.chat-thumb-btn:hover .chat-thumb {
|
|
1029
|
+
border-color: var(--border-strong);
|
|
1030
|
+
}
|
|
1031
|
+
.chat-thumb-btn:focus-visible {
|
|
1032
|
+
outline: none;
|
|
1033
|
+
}
|
|
1034
|
+
.chat-thumb-btn:focus-visible .chat-thumb {
|
|
1035
|
+
border-color: var(--accent);
|
|
1036
|
+
box-shadow: 0 0 0 3px var(--accent-tint);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/* Thumbnail strip under an assistant bubble — every designRoot image path the
|
|
1040
|
+
agent mentioned (DDR-145), previewed without leaving the chat. */
|
|
1041
|
+
.chat-thumbrow {
|
|
1042
|
+
display: flex;
|
|
1043
|
+
flex-wrap: wrap;
|
|
1044
|
+
gap: var(--space-2);
|
|
1045
|
+
margin-top: var(--space-2);
|
|
1046
|
+
}
|
|
1047
|
+
/* A REFERENCED image (assistant merely named the path) — captioned with its
|
|
1048
|
+
filename so it reads as "a file that was referenced", not first-class agent
|
|
1049
|
+
media (attacker F2, DDR-145). */
|
|
1050
|
+
.chat-thumb-fig {
|
|
1051
|
+
margin: 0;
|
|
1052
|
+
display: flex;
|
|
1053
|
+
flex-direction: column;
|
|
1054
|
+
gap: 2px;
|
|
1055
|
+
max-width: 180px;
|
|
1056
|
+
}
|
|
1057
|
+
.chat-thumb-cap {
|
|
1058
|
+
font-family: var(--font-mono);
|
|
1059
|
+
font-size: var(--type-xs);
|
|
1060
|
+
color: var(--fg-3);
|
|
1061
|
+
overflow: hidden;
|
|
1062
|
+
text-overflow: ellipsis;
|
|
1063
|
+
white-space: nowrap;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/* Lightbox — single fixed overlay above the whole shell (ESC / backdrop / ×).
|
|
1067
|
+
Scrim mixes the base background so it tracks light/dark themes. z sits above
|
|
1068
|
+
the shell modal band (.gi-modal 9600) but below the tour/topmost layer
|
|
1069
|
+
(10050+) — an enlarged image must never bury the guided tour. */
|
|
1070
|
+
.chat-lightbox {
|
|
1071
|
+
position: fixed;
|
|
1072
|
+
inset: 0;
|
|
1073
|
+
z-index: 9700;
|
|
1074
|
+
display: grid;
|
|
1075
|
+
place-items: center;
|
|
1076
|
+
background: color-mix(in oklch, var(--bg-0) 80%, transparent);
|
|
1077
|
+
animation: chat-lightbox-in 120ms ease;
|
|
1078
|
+
}
|
|
1079
|
+
.chat-lightbox img {
|
|
1080
|
+
max-width: 90vw;
|
|
1081
|
+
max-height: 90vh;
|
|
1082
|
+
object-fit: contain;
|
|
1083
|
+
border: 1px solid var(--border-default);
|
|
1084
|
+
border-radius: var(--radius-sm);
|
|
1085
|
+
background: var(--bg-1);
|
|
1086
|
+
box-shadow: var(--shadow-lg);
|
|
1087
|
+
}
|
|
1088
|
+
.chat-lightbox-close {
|
|
1089
|
+
position: absolute;
|
|
1090
|
+
top: var(--space-4);
|
|
1091
|
+
right: var(--space-4);
|
|
1092
|
+
appearance: none;
|
|
1093
|
+
width: 32px;
|
|
1094
|
+
height: 32px;
|
|
1095
|
+
display: grid;
|
|
1096
|
+
place-items: center;
|
|
1097
|
+
border: 1px solid var(--border-default);
|
|
1098
|
+
border-radius: var(--radius-sm);
|
|
1099
|
+
background: var(--bg-2);
|
|
1100
|
+
color: var(--fg-1);
|
|
1101
|
+
font-size: var(--type-base);
|
|
1102
|
+
line-height: 1;
|
|
1103
|
+
cursor: pointer;
|
|
1104
|
+
}
|
|
1105
|
+
.chat-lightbox-close:hover {
|
|
1106
|
+
color: var(--fg-0);
|
|
1107
|
+
background: var(--bg-3);
|
|
1108
|
+
}
|
|
1109
|
+
.chat-lightbox-close:focus-visible {
|
|
1110
|
+
outline: none;
|
|
1111
|
+
border-color: var(--accent);
|
|
1112
|
+
box-shadow: 0 0 0 3px var(--accent-tint);
|
|
1113
|
+
}
|
|
1114
|
+
@keyframes chat-lightbox-in {
|
|
1115
|
+
from {
|
|
1116
|
+
opacity: 0;
|
|
1117
|
+
}
|
|
1118
|
+
to {
|
|
1119
|
+
opacity: 1;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1005
1123
|
@media (prefers-reduced-motion: reduce) {
|
|
1006
1124
|
.chat-status-dot--working,
|
|
1007
1125
|
.chat-tool-dot--run,
|
|
1008
1126
|
.chat-activity-spin,
|
|
1009
1127
|
.st-assistant[data-busy='true'],
|
|
1010
1128
|
.chat-dot--busy,
|
|
1011
|
-
.chat-msg
|
|
1129
|
+
.chat-msg,
|
|
1130
|
+
.chat-lightbox {
|
|
1012
1131
|
animation: none;
|
|
1013
1132
|
}
|
|
1133
|
+
.chat-thumb {
|
|
1134
|
+
transition: none;
|
|
1135
|
+
}
|
|
1014
1136
|
}
|
|
1015
1137
|
}
|
|
1016
1138
|
|