a copy button on the console, and the failure case spelled out
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 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -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',
|
||||
|
||||
+34
-6
@@ -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<string | null>(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) && (
|
||||
<pre className="max-h-40 overflow-auto rounded-md border border-border bg-black p-3 font-mono text-[11px] leading-relaxed whitespace-pre-wrap">
|
||||
<code className={failed ? 'text-red-400' : 'text-neutral-200'}>{output}</code>
|
||||
</pre>
|
||||
<div className="relative">
|
||||
<pre className="max-h-40 overflow-auto rounded-md border border-border bg-black p-3 pr-10 font-mono text-[11px] leading-relaxed whitespace-pre-wrap">
|
||||
<code className={failed ? 'text-red-400' : 'text-neutral-200'}>{output}</code>
|
||||
</pre>
|
||||
|
||||
{/* 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. */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (await copyToClipboard(output)) flashCopied();
|
||||
}}
|
||||
aria-label="Copy this output"
|
||||
title="Copy"
|
||||
className="absolute top-1.5 right-1.5 rounded border border-border/60 bg-black/80 p-1.5 text-neutral-400 transition-colors hover:text-neutral-100"
|
||||
>
|
||||
{copied ? <Check className="size-3.5 text-success" /> : <Copy className="size-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user