Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f36b7808a7 | ||
|
|
50b862b1fa | ||
|
|
4668cf8ece | ||
|
|
638155de03 |
+53
-4
@@ -85,10 +85,10 @@ export function createStructuredConnection(
|
||||
<div class="structured-main">
|
||||
<div class="structured-scroll">
|
||||
<div class="structured-rows"></div>
|
||||
</div>
|
||||
<div class="structured-activity" role="status" aria-live="polite" hidden>
|
||||
<span class="structured-activity-pulse" aria-hidden="true"></span>
|
||||
<span class="structured-activity-text">Thinking</span>
|
||||
<div class="structured-activity" role="status" aria-live="polite" hidden>
|
||||
<span class="structured-activity-pulse" aria-hidden="true"></span>
|
||||
<span class="structured-activity-text">Thinking</span>
|
||||
</div>
|
||||
</div>
|
||||
<form class="structured-composer">
|
||||
<button type="button" class="btn btn-ghost btn-icon structured-attach" aria-label="Attach files" title="Attach files (up to 20 MiB each)">
|
||||
@@ -253,6 +253,8 @@ export function createStructuredConnection(
|
||||
let activityTimer: number | null = null;
|
||||
const promptQueue: QueuedPrompt[] = [];
|
||||
let promptRequestPending = false;
|
||||
/** Server path → object URL for uploaded images, so user bubbles can show previews. */
|
||||
const imageObjectUrls = new Map<string, string>();
|
||||
|
||||
// Within 64px, wheel and touch imprecision should still count as following.
|
||||
const FOLLOW_BOTTOM_THRESHOLD_PX = 64;
|
||||
@@ -383,6 +385,9 @@ export function createStructuredConnection(
|
||||
}
|
||||
}
|
||||
|
||||
/** Pattern matching `Attached file "name": "path"` references appended by dispatchPrompt. */
|
||||
const attachedFileRe = /Attached file "(?:[^"\\]|\\.)*": "((?:[^"\\]|\\.)*)"/g;
|
||||
|
||||
function buildRowElement(row: TranscriptRow): HTMLElement | null {
|
||||
const el = buildTranscriptRow(
|
||||
row,
|
||||
@@ -399,6 +404,39 @@ export function createStructuredConnection(
|
||||
if (el.querySelector(".st-tool-head")?.getAttribute("aria-expanded") !== "true") {
|
||||
expandedRowIds.delete(row.id);
|
||||
}
|
||||
// Inject image previews into user message bubbles and strip the path text.
|
||||
if (row.kind === "user" && imageObjectUrls.size > 0) {
|
||||
const data = row.data as { content?: { type: string; text?: string } } | undefined;
|
||||
const text = data?.content?.type === "text" ? data.content.text ?? "" : "";
|
||||
const body = el.querySelector(".st-body");
|
||||
if (body && text) {
|
||||
const images: string[] = [];
|
||||
attachedFileRe.lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = attachedFileRe.exec(text)) !== null) {
|
||||
const serverPath = JSON.parse(`"${match[1]}"`);
|
||||
const objectUrl = imageObjectUrls.get(serverPath);
|
||||
if (objectUrl) images.push(objectUrl);
|
||||
}
|
||||
if (images.length > 0) {
|
||||
// Strip attachment reference lines from the displayed text.
|
||||
const userText = body.querySelector(".st-user-text");
|
||||
if (userText) {
|
||||
const cleaned = text.replace(/\n\nAttached file "[^"]*": "[^"]*"/g, "").trim();
|
||||
userText.textContent = cleaned;
|
||||
if (!cleaned) userText.remove();
|
||||
}
|
||||
for (const src of images) {
|
||||
const img = document.createElement("img");
|
||||
img.className = "st-user-image";
|
||||
img.src = src;
|
||||
img.alt = "Uploaded image";
|
||||
img.loading = "lazy";
|
||||
body.appendChild(img);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
@@ -717,6 +755,7 @@ export function createStructuredConnection(
|
||||
|
||||
function renderActivity(): void {
|
||||
const active = state.turn.responding || state.turn.thinking || awaitingBeginGeneration !== null;
|
||||
const wasHidden = activityEl.hidden;
|
||||
activityEl.hidden = !active;
|
||||
if (!active) {
|
||||
optimisticTurnStartedAt = null;
|
||||
@@ -739,6 +778,11 @@ export function createStructuredConnection(
|
||||
}
|
||||
activityText.textContent = parts.join(" · ");
|
||||
if (activityTimer === null) activityTimer = window.setInterval(renderActivity, 1_000);
|
||||
// Activity lives inside the scroll area; nudge scroll when it first appears.
|
||||
if (wasHidden && followingBottom) {
|
||||
beginTranscriptLayout(true);
|
||||
finishTranscriptLayout();
|
||||
}
|
||||
}
|
||||
|
||||
function reconcileAwaitingBegin(): void {
|
||||
@@ -985,6 +1029,9 @@ export function createStructuredConnection(
|
||||
for (const attachment of submittedAttachments) {
|
||||
if (disposed) return;
|
||||
if (!attachment.path) attachment.path = (await uploadStructuredFile(sessionId, attachment.file)).path;
|
||||
if (attachment.path && attachment.file.type.startsWith("image/")) {
|
||||
imageObjectUrls.set(attachment.path, URL.createObjectURL(attachment.file));
|
||||
}
|
||||
}
|
||||
if (disposed) return;
|
||||
const references = submittedAttachments.map(({ file, path }) =>
|
||||
@@ -1224,6 +1271,8 @@ export function createStructuredConnection(
|
||||
closeCurrentSocket();
|
||||
expandedRowIds.clear();
|
||||
expandedGroupIds.clear();
|
||||
for (const url of imageObjectUrls.values()) URL.revokeObjectURL(url);
|
||||
imageObjectUrls.clear();
|
||||
scrollEl.removeEventListener("scroll", onTranscriptScroll);
|
||||
scrollEl.removeEventListener("click", onTranscriptClick, true);
|
||||
scrollEl.removeEventListener("wheel", stopSmoothFollow);
|
||||
|
||||
+9
-2
@@ -2318,9 +2318,7 @@ input.leaf-rename:focus {
|
||||
gap: var(--sp-2);
|
||||
min-height: 30px;
|
||||
padding: var(--sp-1) var(--sp-3);
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
background: var(--surface-2);
|
||||
font-size: var(--fs-11);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
@@ -2382,6 +2380,15 @@ input.leaf-rename:focus {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.st-user-image {
|
||||
display: block;
|
||||
max-width: min(100%, 480px);
|
||||
max-height: 360px;
|
||||
margin-top: var(--sp-2);
|
||||
border-radius: var(--r-1);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* agent prose: no card, no background — plain text is the point */
|
||||
.st-row-agent .st-body p {
|
||||
margin: 0 0 var(--sp-2);
|
||||
|
||||
Reference in New Issue
Block a user