Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2d427691d |
+43
-59
@@ -253,8 +253,6 @@ 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;
|
||||
@@ -385,9 +383,6 @@ 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,
|
||||
@@ -404,39 +399,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -524,6 +486,7 @@ export function createStructuredConnection(
|
||||
const liveGroupIds = new Set([...groups.keys()]);
|
||||
for (const id of expandedGroupIds) if (!liveGroupIds.has(id)) expandedGroupIds.delete(id);
|
||||
|
||||
const prevGroupEls = new Map(groupEls);
|
||||
rowEls.clear();
|
||||
groupEls.clear();
|
||||
const children: HTMLElement[] = [];
|
||||
@@ -542,23 +505,37 @@ export function createStructuredConnection(
|
||||
rowEls.set(memberRow.id, memberEl);
|
||||
return [memberEl];
|
||||
});
|
||||
const groupEl = buildToolGroup(
|
||||
groupId,
|
||||
group.rows,
|
||||
memberEls,
|
||||
expandedGroupIds.has(groupId),
|
||||
(expanded) => {
|
||||
if (expanded) expandedGroupIds.add(groupId);
|
||||
else expandedGroupIds.delete(groupId);
|
||||
settleDisclosureLayout();
|
||||
},
|
||||
group.toolCount,
|
||||
);
|
||||
if (group.rows.some((r) => enteringRowIds.has(r.id))) {
|
||||
groupEl.classList.add("st-row-enter");
|
||||
|
||||
// Reuse existing group element when possible — avoids tearing down
|
||||
// and recreating the wrapper, which restarts CSS transitions (visible
|
||||
// flash) and destroys keyboard/focus state.
|
||||
const existing = prevGroupEls.get(groupId);
|
||||
if (existing) {
|
||||
const titleEl = existing.querySelector<HTMLElement>(".st-tool-group-title");
|
||||
if (titleEl) titleEl.textContent = `${group.toolCount} tool calls`;
|
||||
const inner = existing.querySelector<HTMLElement>(".st-disclosure-inner");
|
||||
if (inner) inner.replaceChildren(...memberEls);
|
||||
groupEls.set(groupId, existing);
|
||||
children.push(existing);
|
||||
} else {
|
||||
const groupEl = buildToolGroup(
|
||||
groupId,
|
||||
group.rows,
|
||||
memberEls,
|
||||
expandedGroupIds.has(groupId),
|
||||
(expanded) => {
|
||||
if (expanded) expandedGroupIds.add(groupId);
|
||||
else expandedGroupIds.delete(groupId);
|
||||
settleDisclosureLayout();
|
||||
},
|
||||
group.toolCount,
|
||||
);
|
||||
if (group.rows.some((r) => enteringRowIds.has(r.id))) {
|
||||
groupEl.classList.add("st-row-enter");
|
||||
}
|
||||
groupEls.set(groupId, groupEl);
|
||||
children.push(groupEl);
|
||||
}
|
||||
groupEls.set(groupId, groupEl);
|
||||
children.push(groupEl);
|
||||
continue;
|
||||
}
|
||||
// Resolved approvals not absorbed into a tool group are hidden — the
|
||||
@@ -673,12 +650,14 @@ export function createStructuredConnection(
|
||||
}
|
||||
}
|
||||
|
||||
let configDirty = false;
|
||||
function renderConfig(): void {
|
||||
const options = sessionConfigOptions();
|
||||
configEl.hidden = options.length === 0;
|
||||
if (options.length === 0) {
|
||||
configMenu.hidden = true;
|
||||
configTrigger.setAttribute("aria-expanded", "false");
|
||||
configDirty = false;
|
||||
return;
|
||||
}
|
||||
const ordered = [...options].sort((a, b) => {
|
||||
@@ -692,6 +671,15 @@ export function createStructuredConnection(
|
||||
configTrigger.title = summary;
|
||||
configTrigger.setAttribute("aria-label", `Session settings. ${summary.replaceAll("\n", ". ")}`);
|
||||
configTrigger.disabled = configBusy;
|
||||
|
||||
// When the menu is open and the user is interacting (not mid-change),
|
||||
// skip the DOM rebuild so open <select> dropdowns aren't destroyed.
|
||||
// Mark dirty so the next open rebuilds with fresh data.
|
||||
if (!configMenu.hidden && !configBusy) {
|
||||
configDirty = true;
|
||||
return;
|
||||
}
|
||||
configDirty = false;
|
||||
configMenu.replaceChildren();
|
||||
for (const option of ordered) {
|
||||
const row = document.createElement("label");
|
||||
@@ -823,6 +811,7 @@ export function createStructuredConnection(
|
||||
|
||||
configTrigger.addEventListener("click", () => {
|
||||
const opening = configMenu.hidden;
|
||||
if (opening && configDirty) renderConfig();
|
||||
configMenu.hidden = !opening;
|
||||
configTrigger.setAttribute("aria-expanded", String(opening));
|
||||
});
|
||||
@@ -1029,9 +1018,6 @@ 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 }) =>
|
||||
@@ -1271,8 +1257,6 @@ 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);
|
||||
|
||||
@@ -2380,15 +2380,6 @@ 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