Skip to content

Commit f4da8ce

Browse files
only21milclaude
andcommitted
fix(web): device tools keep the typed text when a command fails
The device tools' Open URL, Launch app and iOS push fields cleared on every settlement of their command, so a failed command showed the error and also wiped the text the user typed. useDeviceControls' act now resolves true only when the action succeeds, and false on failure or when it skips a busy or unavailable device. SubmitRow clears only on true, and the push row's no-app fallback resolves false. The other rows ignore the result, so their prop types widen to Promise<unknown>. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 15fe283 commit f4da8ce

3 files changed

Lines changed: 23 additions & 18 deletions

File tree

‎apps/web/src/components/device/DeviceToolsPanel.tsx‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ export function DeviceToolsPanel(props: {
312312
onSubmit={(payload) =>
313313
foregroundApp
314314
? act({ type: "sendPush", appId: foregroundApp.id, payload })
315-
: Promise.resolve()
315+
: Promise.resolve(false)
316316
}
317317
/>
318318
{!foregroundApp ? (
@@ -349,7 +349,7 @@ function SwitchRow(props: {
349349
readonly label: string;
350350
readonly checked: boolean | undefined;
351351
readonly disabled: boolean;
352-
readonly onChange: (value: boolean) => Promise<void>;
352+
readonly onChange: (value: boolean) => Promise<unknown>;
353353
}) {
354354
return (
355355
<Row label={props.label}>
@@ -370,7 +370,7 @@ function ChoiceSelect<V extends string>(props: {
370370
readonly options: ReadonlyArray<{ readonly value: V; readonly label: string }>;
371371
readonly disabled: boolean;
372372
readonly placeholder?: string;
373-
readonly onChange: (value: V) => Promise<void>;
373+
readonly onChange: (value: V) => Promise<unknown>;
374374
}) {
375375
const current = props.options.find((option) => option.value === props.value);
376376
return (
@@ -405,13 +405,15 @@ function SubmitRow(props: {
405405
readonly placeholder: string;
406406
readonly action: string;
407407
readonly disabled: boolean;
408-
readonly onSubmit: (value: string) => Promise<void>;
408+
readonly onSubmit: (value: string) => Promise<boolean>;
409409
}) {
410410
const [value, setValue] = useState("");
411411
const submit = () => {
412412
const trimmed = value.trim();
413413
if (!trimmed) return;
414-
void props.onSubmit(trimmed).then(() => setValue(""));
414+
void props.onSubmit(trimmed).then((sent) => {
415+
if (sent) setValue("");
416+
});
415417
};
416418
return (
417419
<form
@@ -445,8 +447,8 @@ function SubmitRow(props: {
445447
function LocationSection(props: {
446448
readonly disabled: boolean;
447449
readonly canClear: boolean;
448-
readonly onSet: (latitude: number, longitude: number) => Promise<void>;
449-
readonly onClear: () => Promise<void>;
450+
readonly onSet: (latitude: number, longitude: number) => Promise<unknown>;
451+
readonly onClear: () => Promise<unknown>;
450452
}) {
451453
const [latitude, setLatitude] = useState("");
452454
const [longitude, setLongitude] = useState("");
@@ -541,7 +543,7 @@ function PermissionsSection(props: {
541543
appId: string,
542544
permission: DevicePermission,
543545
decision: "grant" | "revoke" | "reset",
544-
) => Promise<void>;
546+
) => Promise<unknown>;
545547
}) {
546548
const [appId, setAppId] = useState("");
547549
const [permission, setPermission] = useState<DevicePermission>("camera");

‎apps/web/src/components/device/useDeviceControls.browser.test.tsx‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,21 +116,23 @@ it("serializes rapid actions and retains confirmed settings while pending or aft
116116
await vi.waitFor(() => expect(controls.detail?.settings.appearance).toBe("light"));
117117
const response = deferred();
118118
action.mockReturnValueOnce(response.promise);
119-
let pending!: Promise<void>;
119+
let pending!: Promise<boolean>;
120120
await act(async () => {
121121
pending = controls.act({ type: "setAppearance", value: "dark" });
122-
await controls.act({ type: "setAppearance", value: "light" });
122+
expect(await controls.act({ type: "setAppearance", value: "light" })).toBe(false);
123123
});
124124
expect(action).toHaveBeenCalledOnce();
125125
expect(controls.disabled).toBe(true);
126126
expect(controls.detail?.settings.appearance).toBe("light");
127127
await act(async () => {
128128
response.resolve(snapshot("dark"));
129-
await pending;
129+
expect(await pending).toBe(true);
130130
});
131131
expect(controls.detail?.settings.appearance).toBe("dark");
132132
action.mockResolvedValueOnce({ _tag: "Failure", cause: offline });
133-
await act(async () => controls.act({ type: "setAppearance", value: "light" }));
133+
await act(async () => {
134+
expect(await controls.act({ type: "setAppearance", value: "light" })).toBe(false);
135+
});
134136
expect(controls.detail?.settings.appearance).toBe("dark");
135137
expect(controls.error).toBe("Device offline");
136138
expect(controls.disabled).toBe(false);
@@ -142,7 +144,7 @@ it("confirms settings again after a command sent before the panel reopened", asy
142144
await vi.waitFor(() => expect(controls.detail).not.toBeNull());
143145
const response = deferred();
144146
action.mockReturnValueOnce(response.promise);
145-
let pending!: Promise<void>;
147+
let pending!: Promise<boolean>;
146148
await act(async () => {
147149
pending = controls.act({ type: "setAppearance", value: "dark" });
148150
});
@@ -174,7 +176,7 @@ it("does not let a read sent before an action overwrite the action's answer", as
174176
await screen.rerender(view(true));
175177
await vi.waitFor(() => expect(read).toHaveBeenCalledTimes(2));
176178
action.mockResolvedValueOnce(snapshot("dark"));
177-
let pending!: Promise<void>;
179+
let pending!: Promise<boolean>;
178180
await act(async () => {
179181
pending = controls.act({ type: "setAppearance", value: "dark" });
180182
});

‎apps/web/src/components/device/useDeviceControls.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,22 @@ export function useDeviceControls(options: {
3939

4040
const available = detail !== null && visible;
4141
const act = async (body: ActionBody) => {
42-
if (busy.current || !available) return;
42+
if (busy.current || !available) return false;
4343
busy.current = true;
4444
setPending(true);
4545
setView((current) => ({ ...current, error: null }));
4646
return runAction({
4747
environmentId,
4848
input: { hostId, deviceId, ...body } as DeviceActionInput,
4949
})
50-
.then((result) =>
50+
.then((result) => {
5151
setView((current) =>
5252
result._tag === "Success"
5353
? { detail: result.value, error: null }
5454
: { ...current, error: formatEnvironmentQueryError(result.cause) },
55-
),
56-
)
55+
);
56+
return result._tag === "Success";
57+
})
5758
.finally(() => {
5859
busy.current = false;
5960
setPending(false);

0 commit comments

Comments
 (0)