From c268e8438a227263243f500f444d5e5e18edd4df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 29 Aug 2026 01:00:13 +0100 Subject: [PATCH] a copy button on the console, and the failure case spelled out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit a failure is something somebody PASTES — into a search, into a message, back to whoever wrote the thing that broke. selecting text inside a scrolled box, on a phone, is the point at which people screenshot an error instead, and a screenshot of a stack trace helps nobody. the button is pinned to the corner of the BOX and not scrolled with the text: one that leaves the viewport when the transcript is long is missing exactly when the transcript is worth copying. `pr-10` keeps the output from running underneath it, and it carries its own dark background because it sits over the console rather than beside it. `copyToClipboard` from helpers, the same one CopyField uses, rather than reaching for navigator.clipboard here. the no-timer-on-failure rule was already the behaviour; the comment now says the part that was only implied. the half-done case is the one that matters and it is named: a clone that worked followed by an install that did not is a failure, `ok` is false, and the transcript of BOTH stays. the only thing that clears it is starting the next attempt — the one moment it stops being true. Co-Authored-By: Claude Opus 5 --- manifest.ts | 2 +- web/WebsiteCheckout.tsx | 40 ++++++++++++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/manifest.ts b/manifest.ts index 1b499d5..a617a93 100644 --- a/manifest.ts +++ b/manifest.ts @@ -35,7 +35,7 @@ import type { PluginManifest } from '@@/plugins/manifest'; */ export const manifest: PluginManifest = { publisher: 'officerdev', - version: '2026.08.29.12', + version: '2026.08.29.13', platform: '>=1.0.0', label: 'Fofos', diff --git a/web/WebsiteCheckout.tsx b/web/WebsiteCheckout.tsx index 4497fb5..f792fd2 100644 --- a/web/WebsiteCheckout.tsx +++ b/web/WebsiteCheckout.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { CircleAlert, CircleCheck, CircleDashed, DownloadCloud, Loader2, RefreshCw } from 'lucide-react'; +import { Check, CircleAlert, CircleCheck, CircleDashed, Copy, DownloadCloud, Loader2, RefreshCw } from 'lucide-react'; import { useClient } from 'hooks/useClient'; +import { copyToClipboard } from 'helpers/clipboard'; import { Button } from '@/components/ui/button'; import { readableError } from './errors'; import { useFlash } from './useFlash'; @@ -26,8 +27,16 @@ import { useFlash } from './useFlash'; * been read by the time it finishes, and left on screen it becomes furniture that outlives its own * truth — still sitting there after the root has been changed underneath it. * - * A failure is the opposite. It stays until something else happens, because it is the only copy of - * why, and a transcript that scrolled past is one somebody has to reproduce to read. + * A failure is the opposite. It gets no timer at all rather than a long one — the only thing that + * takes it off the screen is starting the next attempt, which is the one moment it stops being true. + * That includes the half-done case, which is the most important of the three: a clone that worked + * followed by an install that did not is a failure, `ok` is false, and the transcript of both stays. + * + * ── And it can be copied ── + * + * A failure is something somebody pastes — into a search, into a message, back to whoever wrote the + * thing that broke. Selecting text inside a scrolled box, on a phone, is the point at which people + * take a screenshot of an error message instead, and a screenshot of a stack trace helps nobody. */ type Status = { @@ -53,6 +62,7 @@ export const WebsiteCheckout = ({ rootPath }: { rootPath: string }) => { const [output, setOutput] = useState(null); const [failed, setFailed] = useState(false); const [showing, show] = useFlash(); + const [copied, flashCopied] = useFlash(1500); const load = useCallback(async () => { try { @@ -147,9 +157,27 @@ export const WebsiteCheckout = ({ rootPath }: { rootPath: string }) => { rather than like a paragraph of the form. Scrolled, not truncated: the useful line of a failure is usually the last one. */} {output && (failed || showing) && ( -
-          {output}
-        
+
+
+            {output}
+          
+ + {/* Pinned to the corner of the BOX, not scrolled with the text — a copy button that leaves + the viewport when the transcript is long is missing exactly when the transcript is worth + copying. `pr-10` above keeps the text out from under it. Its own dark background because + it sits over the output rather than beside it. */} + +
)} );