Repository navigation
Tags: harmoniqs/opencode
Tags
Preview as sidebar companion — reactive file viewer driven by Sidebar… … and Chat (#931) (#315) * feat(session-ui): extract editor-core.ts + consolidate preprocessMarkdown (#919) Slice 1 of #912 — Preview Tab Redesign. - Extract general-purpose CM6 setup functions (baseExtensions, editableExtensions, loadLanguage, buildThemeExtension, buildSyntaxHighlightStyle, detectMode, externalUpdate) from editable-diff-view-core.ts into editor-core.ts. - Consolidate three divergent copies of preprocessMarkdown into markdown-utils.ts with the canonical version (newlines + trim). - Export RENDERABLE_EXTENSIONS constant and isRenderable() helper from markdown-utils.ts for use by subsequent preview-tab slices. - Add explicit package.json exports for the new .ts modules. - editable-diff-view-core.ts re-exports from editor-core.ts for full backward compatibility — all 74 existing diff-view tests pass unchanged. - session-preview-tab.tsx and session-review-file-preview-v2.tsx import preprocessMarkdown from the shared module. * feat(app): Finder-style directory view + navigation state machine (#920) Slice 2 of #912 — Preview Tab Redesign. - Create preview-nav-state.ts with pure navigation state machine (navigateToFolder, selectFile, goBack) and filterDirectoryEntries(). - Create preview-directory-view.tsx — Finder-style folder/file listing with drill-down navigation. - Create preview-file-view.tsx — file routing component (markdown only for now, stubs for image/PDF that Slice 5 fills). - Rewrite session-preview-tab.tsx as thin orchestrator managing PreviewTabState with project-mode (useFile() tree.list/children) and no-project fallback (session-touched renderable files). - 17 unit tests for navigation state transitions and directory filtering. * feat(app): breadcrumb navigation + filetree dropdown + search (#921) Slice 3 of #912 — Preview Tab Redesign. - Create preview-search-utils.ts with parseBreadcrumbSegments() and searchRenderableFiles() (pure substring filter, regex-safe). - Create preview-breadcrumb.tsx with clickable path segments, filetree dropdown (FileTreeV2 static mode via allowed prop), and folder-click interceptor on data-slot directory rows. - Add search bar to session-preview-tab.tsx: replaces directory view with flat substring-matched results, Escape clears, integrated with navigation state (goBack clears search first). - Add recursive tree walk + cache for dropdown/search file enumeration, refreshed when project becomes ready. - 10 unit tests for breadcrumb parsing and file search. * feat(session-ui): CodeMirror preview editor + mode rename raw→edit (#922) Slice 4 of #912 — Preview Tab Redesign. - Create preview-editor.tsx in session-ui: plain EditorView using editor-core.ts shared functions (baseExtensions, editableExtensions, loadLanguage, buildThemeExtension, detectMode). - Full clipboard bridge: data-amc-clipboard="codemirror" attribute + __amcEditor stash with getSelectedText()/cutSelectedText() methods. - Cmd/Ctrl+S via keymap extension triggers immediate save. - External content updates via externalUpdate annotation. - Rename mode "raw" → "edit" in PreviewFileState type, all components, and SegmentedControlV2 labels. - Remove RawEditor textarea component (dead code). - Update preview-file-view.tsx to use PreviewEditor for edit mode. * feat(app): image/PDF rendering + file-type routing (#923) Slice 5 of #912 — Preview Tab Redesign. - Complete preview-file-view.tsx with image and PDF rendering branches: images via <img> centered with object-contain, PDFs via <embed>. - Refactor file-type detection to getFileCategory() returning markdown/ image/pdf/unsupported. - Zoom controls (CSS transform: scale) applied consistently to all types. - Mode toggle (Preview/Edit) hidden for non-markdown types (view-only). - File URLs built via serverSDK /file/read endpoint for binary files. - No-project fallback already scaffolded in Slice 2 using touchedFiles prop with isRenderable() filter. - 12 unit tests for file-type routing and mode toggle visibility. * feat(preview): show all non-ignored files in Finder listing (#925) - filterDirectoryEntries() drops the isRenderable gate — all non-ignored files now appear in the Preview tab's directory browser - walkTree collects all non-ignored file paths (not just renderable) - searchRenderableFiles → searchFiles (broader scope, same logic) - No-project fallback retains its isRenderable filter (unchanged) Part of #925 * feat(preview): text files in CM6 editor + binary/error/oversize guards (#925) - getFileCategory() returns 'text' instead of 'unsupported'; extensionless files (Makefile, Dockerfile, LICENSE) classify as 'text' - fileType signal (null → text/binary/error/too-large) classifies content after async file.read() — checked before the category-based router - Text files open directly in PreviewEditor (edit mode, no toggle) - Binary → 'Binary file — cannot preview' placeholder - Error → 'Could not read file' placeholder - Oversize (>1 MB) → 'File too large to preview in this tab' placeholder - Preview/Edit toggle shown only for markdown (unchanged) - Switch/Match replaces nested Show for cleaner routing Part of #925 * feat(preview): strip Finder, lift previewFile signal, empty state (#933) Strip the Finder UI (directory listing, breadcrumbs, search, walk-tree cache) from SessionPreviewTab. The Preview tab is now a companion viewer driven by external file selection. Changes: - Gut preview-nav-state.ts to PreviewFileState type + createPreviewFileStates() - Rewrite SessionPreviewTab: accepts previewFile Accessor<string|null>, shows empty state ('Select a file from the sidebar') when null - Add previewFile to layout context ephemeral store (per-session, not persisted) - SessionSidePanel reads previewFile from layout context, passes to Preview tab - Delete preview-search-utils.ts (breadcrumb parsing + file search) - Delete preview-breadcrumb.tsx and preview-directory-view.tsx - Remove preview-search-utils from session-ui package exports - Rewrite preview-nav-state tests: 7 tests for file-state persistence Net: -967 lines (1121 deleted, 154 added). All 30 packages typecheck clean. Closes harmoniqs/amicode#933 * feat(app): preview-file bridge listener — auto-opens Preview tab (#934) Add a message listener in the session page for the 'preview-file' bridge message from the amicode extension. On receipt: 1. Sets previewFile in the layout context (the ephemeral signal from #933) 2. Opens the Preview tab in the side panel (tabs().open + setActive) 3. Auto-opens the side panel if closed (reviewPanel.open) The listener uses batch() to avoid intermediate renders. Part of harmoniqs/amicode#934 * fix(file-handler): return binary response for absolute-path binary files (#934) The content handler crashed with a 500 when reading binary files (images, PDFs) via absolute paths outside the workspace. TextDecoder('utf-8', { fatal: true }) throws on invalid UTF-8 bytes — catch the error and return { type: 'binary', content: base64, mimeType } instead. Test (1 new): writes a PNG outside the workspace, requests it via absolute path, and verifies the binary response shape. * feat(preview): toolbar consolidation + binary file rendering (#934) Toolbar: - Save dot moves to Bar 1 (header), immediately after the filename - Zoom controls move to Bar 2 (action bar), alongside the edit/preview toggle - PreviewFileView exposes saveStatus via onSaveStatusChange callback - Bar 2 always visible (zoom always accessible) Binary files (images/PDFs): - Images render via data URI (data:mime;base64,...) from the SDK response - PDFs render via blob URL (base64 → Uint8Array → Blob → createObjectURL) - Switch restructured: image/pdf category matches fire BEFORE the generic binary guard, so renderable binaries reach their renderer - Removed dead fileUrl memo (was pointing at nonexistent /file/read endpoint) - File loading effect detects renderable binaries (image/pdf category + base64 encoding) and stores them in binaryData signal instead of setting fileType='binary' Tests (8 new/updated): - classifyFileType: renderable image/PDF binary → 'text' pass-through - Structural: zoom controls in PreviewFileView, not SessionPreviewTab - Structural: save dot in SessionPreviewTab next to filename - Structural: onSaveStatusChange prop on PreviewFileView - Structural: zoomIn/zoomOut passed as props * fix(csp): add object-src 'self' blob: for PDF embed in Preview tab (#934) The server CSP did not declare object-src, so it fell back to default-src 'self' — which silently blocked blob: URLs used by <embed> for PDF rendering. Add the directive. Test (1 updated): CSP integration test now asserts object-src. * fix(preview): image zoom via CSS sizing, PDF via iframe (#934) Images: Replace transform: scale() + origin-top-left with direct CSS width sizing on the <img> element. The transform approach caused scroll-jumps because the layout box stayed fixed at h-full while the visual size changed — the overflow-auto parent had nothing to scroll. Now zoom=150 sets width: 150% on the img, giving natural overflow. PDFs: Replace <embed> with <iframe> for better webview compatibility. The embed element is a plugin surface with inconsistent behavior across webview contexts; iframe with a blob: URL is more reliable and the browser's built-in PDF viewer handles it natively. Tests (2 new structural): - Image zoom block must not use transform: or origin-top-left - PDF block must use iframe, not embed * fix(preview): CSP frame-src for PDF iframe + defeat Tailwind max-width on image zoom (#934) PDF blank white: the server CSP did not declare frame-src, so <iframe src="blob:..."> fell back to default-src 'self' — which silently blocks blob: URLs. Add frame-src 'self' blob:. Image zoom capped at 100%: Tailwind preflight (base.css:222) sets max-width: 100% on all <img>. The inline width: 150% was clamped. Add max-w-none (overrides the preflight cap) and shrink-0 (prevents flex-shrink from compressing the image back) to the <img> class list. Tests: - CSP integration test asserts frame-src 'self' blob: - Structural test asserts max-w-none and shrink-0 on image element * feat(preview): zoom floor at 100%, PDF zoom via transform (#934) - Zoom floor raised from 50% to 100% — images and PDFs always fill at least the full panel width/height; zoom only goes in, never out past the natural fit. - PDF iframe wrapped in a transform: scale() container with inverse width/height compensation so zoom works for PDFs too. Tests (2 new): zoom floor assertion, PDF iframe has transform zoom. * feat(preview): category-aware zoom floor — 100% for image/pdf, 50% for markdown/text (#934) The zoom floor was global at 100%, preventing markdown and text files from zooming out. Now the floor is determined by the file category: image/pdf stay at 100% (never shrink below panel fit), while markdown/text keep the original 50% floor. The parent (SessionPreviewTab) keeps the global zoomOut floor at 50%. PreviewFileView applies a higher floor via handleZoomOut() which checks the category-specific zoomFloor() before delegating. Test (1 updated): asserts parent has 50% floor and child has zoomFloor. * fix(preview): replace PDF iframe with open-in-editor placeholder (#934) Chromium's built-in PDF viewer is not available inside VS Code's sandboxed webview iframes — <iframe src="blob:..."> silently renders a white page regardless of CSP. No amount of CSP fixing resolves it. Replace with a centered placeholder showing the filename, an icon, and an 'Open in editor' button that posts an open-file message to the extension host, which opens the PDF in a native VS Code tab (via vscode.open — works with any installed PDF viewer extension). Remove the dead pdfBlobUrl signal, blob creation effect, and cleanup. Test (1 updated): PDF block must not contain iframe/embed, must contain open-file action. * feat(preview): render PDFs via PDF.js canvas, not iframe/placeholder (#934) The browser's built-in PDF viewer is blocked in VS Code's sandboxed webview iframes. Instead of a placeholder, use pdfjs-dist (Mozilla's PDF renderer) to decode PDF binary data and paint each page onto <canvas> elements — works in any context. Architecture: - New PdfCanvasView component (pdf-canvas-view.tsx): takes base64 PDF data, decodes → Uint8Array, uses getDocument() + page.render() to paint each page to a canvas. Pages stack vertically with gaps. - Worker disabled (GlobalWorkerOptions.workerSrc = ''): main-thread parsing — fine for a side-panel preview. Avoids CSP and Vite worker bundling complexity. - HiDPI: renders at devicePixelRatio × scale, CSS-sizes at 1× for crisp text on Retina. - Zoom: maps the existing zoom() prop (percentage) to PDF.js scale factor. Re-renders canvases on zoom change. - 'Open in editor' kept as secondary action below the rendered pages. Tests (34 pass): - PDF block must not contain iframe/embed/placeholder - PDF block must use PdfCanvasView - pdfjs-dist imported and worker configured - Open-in-editor action present in canvas view * fix(preview): use globalThis.pdfjsWorker for main-thread PDF parsing (#934) Setting workerSrc='' causes a double-fault in pdfjs-dist v6: the getter throws 'No workerSrc specified', the catch falls through to #setupFakeWorker, which calls the same getter via dynamic import() and throws again — 'Setting up fake worker failed'. Fix: import the worker module directly and inject it on globalThis.pdfjsWorker. This is the one code path in pdfjs-dist v6 that bypasses both new Worker() and the workerSrc getter entirely. PDFWorker.#initialize() sees the handler, routes through LoopbackPort, and runs everything on the main thread. No CSP issue, no blob worker, no external fetch. * fix(preview): safe centering for full scroll reach on images and PDFs (#934) When a flex container centers a child that overflows, the negative (left/top) overflow is clipped — CSS scroll only covers positive offsets. Zoomed images and PDFs were unreachable on their left edge. Fix: switch image and PDF wrappers to inline-flex + min-w-full + min-h-full. The wrapper's width = max(content, container), so centering happens inside a box that covers the full scroll area. When the content fits, it's centered; when it overflows, the wrapper grows to match and scroll covers the full content. * feat(preview): pinch-to-zoom and shift+scroll zoom (#934) Trackpad pinch fires as WheelEvent with ctrlKey: true in Chromium; shift+scroll is the mouse-wheel equivalent. Both now trigger smooth continuous zoom in the Preview tab's content area. Implementation: - SessionPreviewTab exposes onZoomChange(value) — arbitrary zoom clamped to [50, 200], passed alongside the existing zoomIn/zoomOut step functions (buttons still use those). - PreviewFileView attaches a wheel listener on the scroll container via addEventListener with { passive: false } (Solid's onWheel is passive by default and can't preventDefault). - Exponential scaling (Math.exp(-delta * 0.003)) — smooth, invertible, handles both tiny trackpad pinch deltas and large scroll-wheel jumps. - Category-aware floor applied (100% for image/pdf, 50% for text/md). - Normal scroll (no modifier key) passes through unaffected. Tests: 37 structural tests pass. * feat(preview): PDF pages fit container width at 100% zoom (#934) PDF pages previously rendered at their intrinsic size (612×792 CSS px for Letter) regardless of panel width. Images already adapted because they use CSS width: zoom% (relative to container). Fix: compute a base scale that fits the PDF page width to the container at 100% zoom (containerWidth / intrinsicWidth), then multiply by zoom/100 for user zoom on top. A ResizeObserver tracks container width so pages reflow when the panel is resized. At 100%: page fills the panel width exactly (like images). At 200%: twice the panel width — scrollable. Panel resize: pages reflow to fit the new width. Each page computes its own base scale from its intrinsic width, so mixed-size documents (Letter + A3) each fit the container individually. Tests: 38 structural tests pass. * fix(preview): observe scroll container not wrapper, breaking feedback loop (#934) The ResizeObserver was watching the wrapper div (inline-flex), whose size depends on the page canvases, whose size depends on containerWidth, which is set by the observer — a feedback loop causing blank rendering and severe lag from rapid re-render oscillation. Fix: observe the PARENT element (the scroll container) instead. Its width is flex-determined and independent of page content. It only changes when the user resizes the panel, which is exactly what we want. No feedback loop possible. Also: - Fix double-padding subtraction: contentBoxSize already excludes padding, so subtracting WRAPPER_PADDING from it was wrong when the observed element had padding. The scroll container has no padding, so subtracting WRAPPER_PADDING (the wrapper's p-4) once is correct and consistent between initial measurement and observer callback. - Add explicit RenderTask.cancel() in effect cleanup: cancels any in-flight page.render() when zoom/containerWidth changes or the component unmounts, preventing stale renders from painting over the canvas. Race condition audit — all paths verified safe: - containerWidth=0 guard prevents render before measurement - cancelled flag + onCleanup handles stale getPage() callbacks - activeRender.cancel() handles in-flight render tasks - ResizeObserver cleanup on unmount prevents orphan callbacks * feat(preview): zoom anchors to viewport center, not top-left (#934) Before: zooming in/out shifted the view toward the top-left corner because scrollLeft/scrollTop stayed constant while content scaled. After: adjustScrollForZoom computes the viewport center in content coordinates before the zoom, then repositions scroll after the DOM updates so the same content point remains centered. Applied to all three zoom paths: - Pinch/Ctrl+scroll (wheel handler) - Shift+scroll (mouse wheel) - +/- toolbar buttons Uses requestAnimationFrame to let the DOM update first: image CSS reflows synchronously, PDF canvas dimensions settle as a microtask (getPage().then()), and rAF fires after both — so scrollWidth and scrollHeight reflect the new content size before we set scroll pos. Tests: 39 structural tests pass. * feat(preview): raise zoom ceiling from 200% to 500% (#934) Four references updated: zoomIn step clamp, onZoomChange clamp, wheel handler clamp, and the input validation range. Floor unchanged (50% for text/markdown, 100% for image/pdf). * fix(preview): use absolute pixel width for images, fixing left-scroll clipping (#934) Root cause: `width: ${zoom}%` inside an inline-flex wrapper creates a circular CSS dependency. The wrapper sizes from the image's intrinsic (natural) dimensions, then the percentage resolves to a larger value, overflowing the wrapper. justify-center pushes the left overflow into unreachable negative scroll territory. Fix: measure container width via ResizeObserver (same pattern as the working PdfCanvasView) and compute image width as absolute pixels: `(containerWidth - padding) * zoom / 100`. The inline-flex wrapper now sizes correctly around the deterministic pixel value. * feat(preview): zoom layout redesign, editor zoom, left-align toolbar, dirty dot (#937) Four changes to the Preview tab toolbar and indicators: 1. Editor zoom (CM6 font-size): PreviewEditor gains a zoom prop backed by a CM6 Compartment that reactively reconfigures fontSize. Fixes code/text files and markdown-edit-mode not responding to zoom (all three share the same PreviewEditor component). 2. Zoom layout redesign: single pill with [editable %] [reset ↻] [+ over −]. The percentage input is now editable (removed readOnly), the reset button jumps to 100%, and +/− are vertically stacked. 3. Left-aligned toolbar: removed the flex-1 spacer that pushed controls right. Mode toggle and zoom now sit flush-left. 4. VS Code-style dirty dot: replaced the transient saving/saved indicator with a simple gray dot that appears when unsavedContent != null and disappears when the file is clean. Matches VS Code and Files Changed behavior. * fix(preview): zoom input commits on Enter/blur only, clamps to valid range The zoom percentage input was firing onZoomChange on every keystroke via onInput, causing the viewport to jump while typing (e.g. typing '200' triggered zoom to 2 → 20 → 200). - Removed onInput handler entirely — typing is free, no zoom updates - Commit happens on Enter (triggers blur) or click-away (blur) - Escape reverts to the current zoom and blurs without committing - Values are clamped to [zoomFloor(), 500] on commit — typing 9999 gives 500, typing 10 on an image (floor 100) gives 100 * fix(preview): zoom pill before edit/preview toggle in toolbar Swaps the toolbar order so the zoom controls come first (left), followed by the edit/preview mode toggle. The zoom pill is always visible; the toggle only appears for markdown files — putting the stable control first avoids layout shift. * fix(preview): use SDK client for file saves — fixes silent 401 on auth-protected servers The saveFile function used a raw fetch() to POST /file/write with no Authorization header. The Amicode server requires auth (OPENCODE_SERVER_PASSWORD), so the request got a 401 — but fetch() doesn't reject on HTTP errors, so .then() fired, setSaveStatus('saved') ran, and the user saw no indication that the save silently failed. Fix: replace both raw fetch calls (saveFile + onCleanup flush) with serverSDK().client.file.write(), which goes through createSdkForServer and injects the Authorization: Basic header. This matches the review panel's pattern (review-panel-v2.tsx:183). Also: - saveFile now uses async/await + try/catch (SDK throws on non-2xx) - Clears unsavedContent to null after successful save (dirty dot disappears) - Widened onUnsavedContent prop to accept string | null * fix(preview): visible unsaved dot + preserve trailing newlines on file read Two fixes: 1. Unsaved-changes dot was invisible — bg-text-faint is not a valid Tailwind v4 utility in this app (the v2 design system uses v2- prefix). Fixed to bg-v2-text-text-faint. Also fixed the empty-state icon (text-text-faint → text-v2-text-text-faint). 2. The /file/content handler called .trim() on decoded text (lines 112 and 149 of handlers/file.ts), stripping trailing newlines. When a POSIX file ending with \n was read → edited → saved (even reverted to 'original'), the trailing newline was lost, creating a phantom git diff. Removed both .trim() calls — the content should round-trip faithfully. The Files Changed tab is NOT affected — it reads content from the snapshot diff pipeline (git objects), not from /file/content. * feat(preview): remove autosave — save only on explicit Cmd+S The Preview tab was autosaving on a 1-second debounce after every keystroke. This caused unexpected writes to disk (and phantom git diffs when the user was just browsing/experimenting). Removed: - debouncedSave function and saveTimer variable - The debouncedSave() call from handleEdit (now only tracks dirty state) - The onCleanup flush (existed to flush the debounce timer) Kept: - handleImmediateSave (Cmd+S) — the only save path - saveFile with SDK auth + try/catch + unsavedContent clearing - The unsaved dot indicator (appears on first edit, clears on Cmd+S) * feat: zoom pill grayed in edit mode (Preview), floating overlay (Files Changed) Preview tab: - Added isEditing() signal — returns true for text/code (always editing), markdown-in-edit-mode, and false for images/PDFs/markdown-in-preview. - Zoom pill gets opacity-40 + pointer-events-none when isEditing() is true. The pill stays visible (signals 'zoom exists') but is non-interactive. - Removed zoom prop from both PreviewEditor call sites — the editor always renders at base font size, avoiding confusing zoom mismatch when switching between preview and edit modes. Files Changed tab: - Added local zoom state (createSignal(100)) with zoomIn/zoomOut/onZoomChange. - Added a floating zoom overlay (absolute positioned, z-index 20) inside the markdown preview branch — only visible when isPreviewMd() is true, disappears entirely in unified/split diff and edit modes. - Markdown preview now applies CSS transform: scale() with the zoom value, matching the Preview tab's pattern. - Pill styling: semi-transparent backdrop with blur, matching the lock indicator's floating pattern. * feat(files-changed): auto-hide zoom pill on idle (2s) The floating zoom overlay in the Files Changed markdown preview now auto-hides after 2 seconds of mouse inactivity: - Pill starts visible on first render (initial 2s reveal) - Mouse movement over the preview area shows the pill and resets the timer - Mouse leaving the preview area hides the pill immediately - Hovering over the pill itself pauses the timer (no fade mid-interaction) - Leaving the pill resumes the idle timer - Opacity transition (200ms ease) for smooth fade in/out - pointer-events: none when hidden so it doesn't block content interaction * feat(files-changed): pinch/wheel zoom in markdown preview Trackpad pinch (ctrlKey + wheel) and shift+scroll now trigger zoom in the Files Changed markdown preview. The handler: - Checks ctrlKey/shiftKey — normal scroll passes through untouched - preventDefault() to suppress browser default pinch-to-zoom - Exponential zoom factor (Math.exp(-delta * 0.003)) for smooth response - Clamped to [50, 500] via onZoomChange - Shows the zoom pill and resets the 2s idle timer during the gesture Attached via addEventListener with passive: false on the preview wrapper ref (Solid's onWheel is passive by default and can't preventDefault). Normal scroll intentionally does NOT show the pill — keeps the reading experience clean. * feat(preview): floating overlay controls, no action bar Replaces the fixed action bar with floating overlay controls anchored to the top-right of the content area, matching the Files Changed tab's pattern. Both tabs now have a consistent floating zoom pill design. Layout: - Single absolute-positioned flex row (top: 8px, right: 8px, z-index: 20) - Zoom pill on the left, edit/preview toggle on the right - Semi-transparent backdrop blur on both controls - No action bar — maximizes content area Behavior: - Both controls auto-hide after 2s of mouse inactivity - Mouse movement over the content area reveals them - Hovering over the controls pauses the idle timer - Pinch/wheel zoom reveals the controls and resets the timer - Zoom pill disappears entirely in edit mode (<Show when={!isEditing()}>) - Mode toggle only renders for markdown files - 200ms opacity transition for smooth fade For text/code files (always in edit mode), no controls render at all. For images/PDFs, only the zoom pill appears (no mode toggle). * fix: nudge floating overlays left to clear scrollbar (right: 14px) * fix: update httpapi exercise to expect untrimmed file content * fix: add explicit preview-editor export to session-ui package.json
feat(chat): yellow answers, no YOU label, and a WAAPI merge on the bu… …bble lock - the answered-question's answer renders as the prompt bubble's yellow chip, right-aligned to the user's side (same --prompt-bubble-* tokens) - the sticky prompt bubble drops the YOU label — the chip speaks for itself - hand-offs (scroll lock-over, click, send) merge via a 240ms WAAPI pass; the chip element persists — a keyed remount + CSS entrance strobed under smooth scrolling, tried and reverted same day
PreviousNext