Repository navigation
feat(web,mobile): take over a device another thread still holds #32862
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6a776e5
1b15169
4692840
3cde288
0c7a599
b715441
8138144
4422ed2
8fa225d
1ee9153
ab98312
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,14 @@ | ||
| import { DeviceHostUpdates } from "./DeviceHostUpdates"; | ||
| import { scopeThreadRef } from "@t3tools/client-runtime/environment"; | ||
| import { otherThreadDeviceSessions } from "@t3tools/client-runtime/state/device"; | ||
| import { threadRuntimeIsActive } from "@t3tools/client-runtime/state/shell"; | ||
| import { backgroundWorkHoldsCompletion } from "@t3tools/shared/orchestrationV2PendingBackgroundWork"; | ||
| import type { | ||
| DevicePlatform, | ||
| DeviceServiceState, | ||
| DeviceSession, | ||
| DeviceSummary, | ||
| EnvironmentId, | ||
| ScopedThreadRef, | ||
| } from "@t3tools/contracts"; | ||
| import { ArrowUpRight, CircleAlert, Play, Smartphone, X } from "lucide-react"; | ||
|
|
@@ -29,8 +35,10 @@ import { | |
| NO_DEVICE_PROBE_ATOM, | ||
| useDeviceState, | ||
| } from "~/state/device"; | ||
| import { useThreadShell } from "~/state/entities"; | ||
| import { formatEnvironmentQueryError } from "~/state/query"; | ||
| import { useAtomCommand } from "~/state/use-atom-command"; | ||
| import { ThreadContextChip } from "../ThreadContextChip"; | ||
| import { DeviceLoadingView } from "./DeviceLoadingView"; | ||
| import { | ||
| DeviceSetup, | ||
|
|
@@ -95,6 +103,7 @@ export function DevicePanel(props: { | |
| const activeHost = activeDevice | ||
| ? state.hosts.find((host) => host.id === activeDevice.hostId) | ||
| : undefined; | ||
| const holders = activeSession ? otherThreadDeviceSessions(state.sessions, activeSession) : []; | ||
|
|
||
| const grouped = groupDevices(state); | ||
|
|
||
|
|
@@ -174,6 +183,16 @@ export function DevicePanel(props: { | |
| }); | ||
| }; | ||
|
|
||
| const takeOver = (holder: DeviceSession) => { | ||
| const settleOperation = startOperation(null); | ||
| return close({ | ||
| environmentId, | ||
| input: { threadId: holder.threadId, hostId: holder.hostId, deviceId: holder.deviceId }, | ||
| }).then((result) => | ||
| settleOperation(result._tag === "Failure" ? formatEnvironmentQueryError(result.cause) : null), | ||
| ); | ||
| }; | ||
|
|
||
| const bootingDevices = | ||
| state.bootingDevices?.filter((device) => device.threadId === threadId) ?? []; | ||
| const { ready: hostReady, busy: hostBusy } = deviceHubReadiness(state); | ||
|
|
@@ -242,6 +261,14 @@ export function DevicePanel(props: { | |
| minute. | ||
| </div> | ||
| ) : null} | ||
| {holders.map((holder) => ( | ||
| <DeviceHolderNotice | ||
| key={holder.threadId} | ||
| environmentId={environmentId} | ||
| holder={holder} | ||
| onTakeOver={() => takeOver(holder)} | ||
| /> | ||
| ))} | ||
| {operationError ? ( | ||
| <DeviceNotice onDismiss={() => setOperation(null)}>{operationError}</DeviceNotice> | ||
| ) : null} | ||
|
|
@@ -386,6 +413,47 @@ export function DevicePanel(props: { | |
| ); | ||
| } | ||
|
|
||
| function DeviceHolderNotice(props: { | ||
| readonly environmentId: EnvironmentId; | ||
| readonly holder: DeviceSession; | ||
| readonly onTakeOver: () => Promise<unknown>; | ||
| }) { | ||
| const shell = useThreadShell(scopeThreadRef(props.environmentId, props.holder.threadId)); | ||
| const running = | ||
| threadRuntimeIsActive(shell?.runtime) || | ||
| backgroundWorkHoldsCompletion(shell?.pendingBackgroundTasks ?? []); | ||
| const [takingOver, setTakingOver] = useState(false); | ||
| return ( | ||
| <div | ||
| role="status" | ||
| className="flex flex-wrap items-center gap-x-2 gap-y-1 border-b px-3 py-2 text-xs text-muted-foreground" | ||
| > | ||
| <span className="min-w-0 flex-1 basis-56"> | ||
| Busy in{" "} | ||
| <ThreadContextChip | ||
| record={{ | ||
| environmentId: props.environmentId, | ||
| threadId: props.holder.threadId, | ||
| title: "another thread", | ||
| }} | ||
| /> | ||
| {running ? ", which is running." : shell ? ", which isn't running." : "."} | ||
| </span> | ||
| <Button | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Identify the holder in each Take over button’s accessible name. If two other threads have this device open, both buttons are named “Take over”, but each closes a different thread’s session. A screen-reader user navigating by buttons cannot tell which action will stop which thread’s device access. Include the holder’s thread title and a distinguishing thread identifier in the button’s accessible name. (raw.githubusercontent.com)
|
||
| size="xs" | ||
| aria-label={`Take over from ${shell?.title.trim() || "another thread"}`} | ||
| disabled={takingOver} | ||
| onClick={() => { | ||
| setTakingOver(true); | ||
| void props.onTakeOver().finally(() => setTakingOver(false)); | ||
| }} | ||
| > | ||
| {takingOver ? "Taking over…" : "Take over"} | ||
| </Button> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function DeviceNotice(props: { readonly onDismiss: () => void; readonly children: ReactNode }) { | ||
| return ( | ||
| <div | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: pingdotgg/t3code
Length of output: 9656
Script executed:
Repository: pingdotgg/t3code
Length of output: 19350
Script executed:
Repository: pingdotgg/t3code
Length of output: 26163
Script executed:
Repository: pingdotgg/t3code
Length of output: 10437
Script executed:
Repository: pingdotgg/t3code
Length of output: 1167
Avoid claiming that a holder with background work is not running.
A pending background task can leave the runtime status idle while the server still considers the holder active. The notice can therefore show “isn't running” and offer a takeover that the server refuses.
Remove the inactive claim here and in the mobile banner.
Suggested fix
Committable suggestion