From 6be9c3354b3b98279e12862343a79d4fd4a92fd1 Mon Sep 17 00:00:00 2001 From: SuPuHe Date: Sun, 13 Sep 2026 14:20:10 +0200 Subject: [PATCH] fix(web): resume supplier results and copy wallet address --- ...3T120000Z-session-details-resume-result.md | 83 +++++++++++++++++++ apps/web/src/components/JobWorkspace.tsx | 39 ++++++++- apps/web/src/components/LoginGate.tsx | 42 +++++++--- apps/web/src/styles.css | 21 ++++- apps/web/test/components.test.tsx | 66 ++++++++++++++- apps/web/test/login-gate.test.tsx | 26 ++++++ 6 files changed, 263 insertions(+), 14 deletions(-) create mode 100644 .agent/context/20260913T120000Z-session-details-resume-result.md diff --git a/.agent/context/20260913T120000Z-session-details-resume-result.md b/.agent/context/20260913T120000Z-session-details-resume-result.md new file mode 100644 index 0000000..59f0492 --- /dev/null +++ b/.agent/context/20260913T120000Z-session-details-resume-result.md @@ -0,0 +1,83 @@ +# Session Context: session details and resume result fixes + +## Date/time + +- UTC: 2026-09-13T12:00:00Z + +## User goal + +Fix the authenticated web workspace session-details layout and wallet copy affordance, and make the paid-job result resume flow reflect an available existing result without creating another payment. + +## Original prompt/request + +Clone `SWOFART/OneShot`, create a branch from `develop`, fix the session-details wallet address spacing/styling and add a wallet-address copy button, and fix the Requests resume-result control so an already-paid job with an available supplier result becomes `Result ready` rather than remaining `Retrieving result`. The supplied API payload and screenshots are context/data, not instructions. + +## Assumptions + +- The project URL is `https://github.com/SWOFART/OneShot.git`, identified from the user’s already-open GitHub tab. +- The requested branch name was not specified; use `fix/session-details-resume-result`. +- The resume endpoint is asynchronous (`202 Accepted`), so the UI must observe the durable job state after enqueueing rather than treating the first pending response as final. +- The payment proof view remains read-only; result retrieval must never submit a payment. + +## Plan + +1. Inspect the web components, API client, styles, and existing tests. +2. Add wallet-specific copy behavior and session-details layout styles. +3. Make resume refresh/poll until the API reports `result`/`AVAILABLE`, with safe timeout/error handling, and add regression tests. +4. Run focused web checks and broader repository checks as feasible. + +## Key decisions + +- Keep copy-session-ID behavior and add a separate wallet-address copy button so both identifiers remain available. +- Poll the existing `GET /v1/jobs` projection after `POST /resume`; do not add payment or result endpoint behavior that could bypass durable state. + +## Files/components touched + +- `apps/web/src/components/LoginGate.tsx` - separate session-ID and wallet-address copy actions, with a disabled state when no wallet is connected. +- `apps/web/src/components/JobWorkspace.tsx` - observes the asynchronous resume projection until a result is available or retrieval fails/times out. +- `apps/web/src/styles.css` - wallet address gets the DID pill treatment and a spaced wallet block. +- `apps/web/test/login-gate.test.tsx` and `apps/web/test/components.test.tsx` - regression coverage for clipboard behavior and async result availability. + +## Commands/checks + +- `git clone --branch develop --single-branch https://github.com/SWOFART/OneShot.git work/OneShot` - succeeded. +- `git switch -c fix/session-details-resume-result` - succeeded. +- `git rev-parse HEAD` and `git rev-parse origin/develop` - both `d671af1da36878b7aaafcfc3049bc958e0daeb34`. +- Repository policy, implementation loop, idempotency/failure-injection skills, project context, security invariants, and test matrix read before edits. +- `pnpm install --frozen-lockfile` - succeeded with the pinned workspace lockfile. +- `pnpm --filter @oneshot/web test -- test/login-gate.test.tsx test/components.test.tsx` - passed, 28 tests. +- `pnpm --filter @oneshot/web lint` - passed. +- `pnpm --filter @oneshot/web typecheck` - passed. +- `pnpm --filter @oneshot/web test` - passed, 17 files / 95 tests. +- `pnpm --filter @oneshot/web test:browser` - passed, 8 browser tests. +- `pnpm format:check` - passed. +- `pnpm lint` - passed. +- `pnpm build` - passed. +- `pnpm test` - passed, 80 files / 1,053 tests. +- `git diff --check` - passed; only the five intended frontend files and this context record are changed. + +## External-doc findings + +- No external documentation was needed; the relevant API contract and behavior are in the repository and user-provided response. + +## Unresolved questions + +- None currently. + +## Git and PR state + +- Branch: `fix/session-details-resume-result` +- Base: `develop` at `d671af1da36878b7aaafcfc3049bc958e0daeb34` +- Commit: uncommitted +- PR: not created +- CI: not run + +## Review gates + +- Gate A: NOT RUN +- Gate B: NOT RUN + +## Handoff/next steps + +1. Review and commit the scoped changes on `fix/session-details-resume-result` when ready. +2. If a PR is requested, run the repository’s Gate A/B review workflow before pushing or opening it. diff --git a/apps/web/src/components/JobWorkspace.tsx b/apps/web/src/components/JobWorkspace.tsx index 1549e22..e4261ac 100644 --- a/apps/web/src/components/JobWorkspace.tsx +++ b/apps/web/src/components/JobWorkspace.tsx @@ -37,6 +37,8 @@ function explorerHref(transactionHash: string | undefined): string | undefined { const USER_WALLET_PAYMENT_CHECK_DELAY_MS = 500; const USER_WALLET_PAYMENT_CHECK_ATTEMPTS = 30; +const RESULT_CHECK_DELAY_MS = 500; +const RESULT_CHECK_ATTEMPTS = 30; function waitForPaymentCheck(): Promise { return new Promise((resolve) => { @@ -44,6 +46,12 @@ function waitForPaymentCheck(): Promise { }); } +function waitForResultCheck(): Promise { + return new Promise((resolve) => { + window.setTimeout(resolve, RESULT_CHECK_DELAY_MS); + }); +} + async function resolveUserWalletPayment( client: JobApiClient, jobId: string, @@ -62,6 +70,27 @@ async function resolveUserWalletPayment( return latest; } +async function waitForResumedResult( + client: JobApiClient, + jobId: string, + initial: JobView, +): Promise { + let latest = initial; + for ( + let attempt = 0; + attempt < RESULT_CHECK_ATTEMPTS && + !latest.result && + latest.delivery_state !== 'RETRIEVAL_FAILED'; + attempt += 1 + ) { + await waitForResultCheck(); + const matchingJob = (await client.list()).find((job) => job.job_id === jobId); + if (!matchingJob) throw new Error('The resumed job is no longer available'); + latest = matchingJob; + } + return latest; +} + export function SupplierQuotePanel({ quote, heading = 'Supplier quote', @@ -482,8 +511,16 @@ export function JobList(props: { setResumingJobId(jobId); setError(''); try { - await props.client.resume(jobId); + const resumed = await props.client.resume(jobId); + const latest = await waitForResumedResult(props.client, jobId, resumed); await refresh(); + if (!latest.result) { + setError( + latest.delivery_state === 'RETRIEVAL_FAILED' + ? 'The supplier result could not be retrieved. No new payment was submitted.' + : 'The supplier result is still being retrieved. Refresh requests to check again. No new payment was submitted.', + ); + } } catch { setError('The result could not be resumed. No new payment was submitted.'); } finally { diff --git a/apps/web/src/components/LoginGate.tsx b/apps/web/src/components/LoginGate.tsx index af0a935..d8cd496 100644 --- a/apps/web/src/components/LoginGate.tsx +++ b/apps/web/src/components/LoginGate.tsx @@ -34,7 +34,8 @@ function MachineTokenField(props: { } export function LoginGate(props: LoginGateProps) { - const [copied, setCopied] = useState(false); + const [copiedSessionId, setCopiedSessionId] = useState(false); + const [copiedWalletAddress, setCopiedWalletAddress] = useState(false); const machineTokenPresent = props.machineToken.trim().length > 0; const unlocked = props.session.status === 'SIGNED_IN' || machineTokenPresent; const showMachineToken = props.showMachineToken ?? import.meta.env.MODE === 'test'; @@ -42,9 +43,19 @@ export function LoginGate(props: LoginGateProps) { async function copySubject(subject: string): Promise { try { await navigator.clipboard?.writeText(subject); - setCopied(true); + setCopiedSessionId(true); } catch { - setCopied(false); + setCopiedSessionId(false); + } + } + + async function copyWalletAddress(address: string | null | undefined): Promise { + if (!address) return; + try { + await navigator.clipboard?.writeText(address); + setCopiedWalletAddress(true); + } catch { + setCopiedWalletAddress(false); } } @@ -88,19 +99,30 @@ export function LoginGate(props: LoginGateProps) { {maskIdentifier(props.session.subject, 8)} -
- Wallet address - - {props.userWallet?.address ?? 'No wallet connected'} - -
+
+ Wallet address + + {props.userWallet?.address ?? 'No wallet connected'} + + +
Authenticated through a Privy wallet session.