pi-mono/apps/p2/plugins/sessions/SessionOptions.tsx
Mario Zechner 0a730c8a76 fix(p2): archiving a viewed session shows no error and starts no refused worker
Archiving from the session sheet navigates away first, which unmounted SessionOptions and released its Sessions
reference; Chord cancels a released reference's calls, so the archive call rejected with "p2.sessions: released",
shown as the composer toast over the owner-notice lane. The archive now holds a reference of its own until it is done,
and the server runs its steps in a root of its own.

The archive stopped the worker with Hosts.stop, which restarts it for its holders: the page's lingering
conversation started a worker the archived session refused (resolve_failed, "Worker: Session is archived" notice).
SessionWorkerControl.suspend stops it and leaves its holders suspended until a restore (retry) or their release;
archive, a failed fork's archived copy and an archived session's index rebuild use it.
2026-11-04 13:19:01 +01:00

65 lines
2.2 KiB
TypeScript

// Native catalog operations beneath the copied mobile Session sheet.
import { BACKGROUND_CONTEXT, liveIn, whenLive } from "@p2/app/v1";
import {
closeLayers,
conversationRoute,
MoreIcon,
openLayer,
useComposerState,
useHost,
useNavigation,
useObject,
useReadable,
useRoute,
useSheet,
} from "@p2/app/v1/web";
import { directoryPickerLayer } from "../envs/contract.web.tsx";
import { Sessions } from "./contract.ts";
import { SessionSheet } from "./SessionSheet.tsx";
export function SessionOptions() {
const host = useHost(),
nav = useNavigation(),
route = useRoute();
const service = useObject(Sessions, {}, { variant: "owner" }),
rows = useReadable(service?.sessions);
const session = rows?.find((row) => row.id === route.session),
sessionSheet = useSheet("session");
const { setError } = useComposerState();
const perform = (operation: Promise<unknown> | undefined) =>
void operation?.catch((error) => setError(error instanceof Error ? error.message : String(error)));
if (!service || !session || route.conversation !== 1) return null;
return (
<>
<button
type="button"
className="icon-button"
aria-label="Session options"
onClick={() => openLayer({ kind: "sheet", name: "session" })}
>
<MoreIcon />
</button>
{sessionSheet && (
<SessionSheet
session={session}
onClose={closeLayers}
onRename={(title) => perform(service?.rename({ id: session.id, title }, BACKGROUND_CONTEXT))}
onPin={() => perform(service?.pin({ id: session.id, pinned: !session.pinned }, BACKGROUND_CONTEXT))}
onShare={() => openLayer({ kind: "sheet", name: "share" })}
onCwd={() => openLayer(directoryPickerLayer(session.env ?? null, session.cwd))}
onArchive={() => {
closeLayers();
void nav.replace(conversationRoute(route, undefined));
// Leaving the session unmounts this view and releases `service`, which cancels its calls: the
// archive holds a reference of its own until it is done.
const archiving = host.hold(Sessions, {}, BACKGROUND_CONTEXT);
perform(
whenLive(archiving)
.then(() => liveIn(archiving, "owner")?.archive({ id: session.id }, BACKGROUND_CONTEXT))
.finally(() => archiving.release()),
);
}}
/>
)}
</>
);
}