Skip to content

Commit d22ead6

Browse files
committed
Let the Forge trace be read, copied and saved, and keep the viewport square
Developer: Seif Hashish Project: HashCortX Summary: The trace drawer records the only account of what a run did — which model answered, which one rate-limited, where the seconds went — and none of it could leave the drawer. Selection is switched off application-wide and granted back to a named list of surfaces; the trace was not on it, so the text was inert, not merely awkward to select. It now opts in, and Copy and Export sit beside Clear, both reading the entries through one function so the clipboard and the file can never disagree. Copy falls back to selecting the text when the clipboard is refused. Opening that drawer also distorted the model. The viewport recomputes its camera on a window resize and nothing else, while the drawer animates the canvas mount from thirty-two pixels to two hundred without the window changing at all — so the camera kept an aspect ratio for a box it no longer occupied. Measured with the drawer open: the canvas held 1.451 against a container of 1.827, a quarter of the height squashed out of the picture. A ResizeObserver on the mount now catches that, and every other panel or split that moves it. Verification: - Drove Forge in a headless browser before and after. Before: computed user-select none and an empty selection; one button. After: selectable text, three buttons. - Neutralised the observer and measured again — canvas aspect 1.451 in a 1.827 container. Restored: 1.829 against 1.827. - npm run check — 1,384 passed, 0 failed - cargo test --manifest-path src-tauri/Cargo.toml — 93 passed, 0 failed (no Rust changed) - Not yet exercised in a packaged build
1 parent b2da69d commit d22ead6

4 files changed

Lines changed: 83 additions & 1 deletion

File tree

scripts/checks/app-size.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,12 @@ const LINE_BUDGET = {
8181
// thrown away.
8282
'modes/systems/mode.js': 4246,
8383
'modes/virtual-os/mode.js': 3808,
84-
'modes/forge/mode.js': 3756,
84+
// 3820, up from 3756. The trace drawer could be read and never taken: no
85+
// selection, no copy, no file. Most of these lines are the two handlers and
86+
// the single reader that turns the entries into text for both. The rest is
87+
// the observer that keeps the viewport's aspect when the drawer changes the
88+
// canvas's height without the window resizing.
89+
'modes/forge/mode.js': 3820,
8590
'modes/agent-maker/mode.js': 2980,
8691
'modes/finance/mode.js': 2705,
8792
'modes/code/mode.js': 2715,

src/modes/forge/mode.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,17 @@ body.forge-studio-mode #app { display: none; }
897897

898898
.frg-trace-btn:hover { background: rgba(75,210,190,0.09); }
899899

900+
/* styles.css switches selection off for the whole application and grants it
901+
back to a named list — inputs, chat bubbles, pre and code. The trace was on
902+
neither side of that, so a run could be read and never taken: no selection,
903+
no copy, and a rate-limit chain that could only be photographed. */
904+
.frg-trace-entries,
905+
.frg-trace-entries * {
906+
user-select: text;
907+
-webkit-user-select: text;
908+
cursor: text;
909+
}
910+
900911
.frg-trace-entries {
901912
flex: 1 1 0;
902913
min-height: 0;

src/modes/forge/mode.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
let renderer = null;
2929
let scene = null;
3030
let camera = null;
31+
let mountResizeObserver = null;
3132
let controls = null;
3233
let modelGroup = null;
3334
let particleGroup = null;
@@ -283,6 +284,22 @@
283284
if (dot) dot.className = "frg-trace-dot " + traceKind(statusCls);
284285
}
285286

287+
// The trace as plain text, in the order it happened. One reader for both the
288+
// clipboard and the file, so the two can never drift apart.
289+
function traceAsText() {
290+
const host = $("frgTraceEntries");
291+
if (!host) return "";
292+
const lines = Array.from(host.querySelectorAll(".frg-trace-entry")).map((row) => {
293+
const cell = (sel) => (row.querySelector(sel)?.textContent || "").trim();
294+
const tokens = cell(".trace-tokens");
295+
return [cell(".trace-time"), cell(".trace-agent"), cell(".trace-msg"), tokens]
296+
.filter(Boolean).join(" ");
297+
});
298+
if (!lines.length) return "";
299+
const header = `3D Forge trace · ${new Date().toLocaleString()}`;
300+
return [header, "=".repeat(header.length), "", ...lines, ""].join("\n");
301+
}
302+
286303
function setAgentState(id, state) {
287304
const el = document.querySelector(`[data-frg-agent="${id}"] .frg-agent-state`);
288305
if (el) el.textContent = state;
@@ -669,6 +686,22 @@
669686
scene.add(starField);
670687

671688
window.addEventListener("resize", resize);
689+
690+
// The window is not the only thing that changes this canvas's size. The
691+
// trace drawer animates from 32px to 200px, panels collapse, the inspector
692+
// opens — none of those raise a window resize, so the camera kept an aspect
693+
// ratio for a box it no longer occupied and the model came out stretched.
694+
// Watching the mount catches every one of them, including the frames of a
695+
// CSS transition, which is what keeps the picture honest while the drawer
696+
// slides rather than only once it lands.
697+
try {
698+
const mount = $("frgCanvasMount");
699+
if (mount && window.ResizeObserver) {
700+
mountResizeObserver?.disconnect();
701+
mountResizeObserver = new ResizeObserver(() => resize());
702+
mountResizeObserver.observe(mount);
703+
}
704+
} catch {}
672705
wireContextLoss();
673706
resize();
674707
initialized = true;
@@ -3596,6 +3629,37 @@ Do not add floating decorations or abstract markers. Structure must add load-bea
35963629
tc.classList.toggle("expanded", open);
35973630
tc.classList.toggle("collapsed", !open);
35983631
});
3632+
$("frgTraceCopyBtn")?.addEventListener("click", async (e) => {
3633+
e.stopPropagation();
3634+
const text = traceAsText();
3635+
const btn = e.currentTarget;
3636+
if (!text) { btn.textContent = "Empty"; setTimeout(() => { btn.textContent = "Copy"; }, 1200); return; }
3637+
try {
3638+
await navigator.clipboard.writeText(text);
3639+
btn.textContent = "Copied";
3640+
} catch {
3641+
// No clipboard permission: select it instead, so the keyboard still works.
3642+
const host = $("frgTraceEntries");
3643+
if (host) {
3644+
const range = document.createRange();
3645+
range.selectNodeContents(host);
3646+
const sel = window.getSelection();
3647+
sel.removeAllRanges(); sel.addRange(range);
3648+
}
3649+
btn.textContent = "Selected";
3650+
}
3651+
setTimeout(() => { btn.textContent = "Copy"; }, 1400);
3652+
});
3653+
$("frgTraceExportBtn")?.addEventListener("click", async (e) => {
3654+
e.stopPropagation();
3655+
const text = traceAsText();
3656+
const btn = e.currentTarget;
3657+
if (!text) { btn.textContent = "Empty"; setTimeout(() => { btn.textContent = "Export"; }, 1200); return; }
3658+
const stamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-");
3659+
const ok = await downloadBlob(`forge-trace-${stamp}.txt`, new Blob([text], { type: "text/plain" }));
3660+
btn.textContent = ok ? "Saved" : "Export";
3661+
setTimeout(() => { btn.textContent = "Export"; }, 1400);
3662+
});
35993663
$("frgTraceClearBtn")?.addEventListener("click", (e) => {
36003664
e.stopPropagation();
36013665
const entries = $("frgTraceEntries");

src/modes/forge/panel.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@
190190
<span class="frg-trace-label">Trace</span>
191191
<span class="frg-trace-summary" id="frgTraceSummary">No run yet</span>
192192
<div class="frg-trace-actions">
193+
<button class="frg-trace-btn" id="frgTraceCopyBtn" title="Copy the whole trace to the clipboard">Copy</button>
194+
<button class="frg-trace-btn" id="frgTraceExportBtn" title="Save the whole trace as a text file">Export</button>
193195
<button class="frg-trace-btn" id="frgTraceClearBtn">Clear</button>
194196
</div>
195197
<svg class="frg-trace-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>

0 commit comments

Comments
 (0)