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. */} + +
)} );