feat(frontend): expose owned session scopes
This commit is contained in:
@@ -18,10 +18,10 @@ import { Checkbox } from "../components/ui/checkbox";
|
||||
import { Toaster } from "../components/ui/sonner";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
closeSession, listSessions, resumeSession, getSession,
|
||||
closeSession, getMe, listSessions, resumeSession, getSession,
|
||||
renameSession, setSessionGroup, archiveSession, unarchiveSession, deleteSession, prewarmRuntime,
|
||||
} from "../api/sessions";
|
||||
import type { SessionSummary } from "../api/types";
|
||||
import type { Principal, SessionScope, SessionSummary } from "../api/types";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { CSSProperties } from "react";
|
||||
@@ -48,8 +48,10 @@ export function AppShell() {
|
||||
const [streamCursorResetEpoch, setStreamCursorResetEpoch] = useState(0);
|
||||
const [creatingSession, setCreatingSession] = useState(false);
|
||||
const [awaitingQuestion, setAwaitingQuestion] = useState(false);
|
||||
const [sessionScope, setSessionScope] = useState<SessionScope>("mine");
|
||||
const { data: principal } = useQuery<Principal>({ queryKey: ["me"], queryFn: getMe, staleTime: Infinity });
|
||||
const { data: sessions = [] } = useQuery<SessionSummary[]>({
|
||||
queryKey: ["sessions"], queryFn: listSessions, refetchInterval: 10_000,
|
||||
queryKey: ["sessions", sessionScope], queryFn: () => listSessions(sessionScope), refetchInterval: 10_000,
|
||||
});
|
||||
const composerRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
@@ -77,6 +79,12 @@ export function AppShell() {
|
||||
const finalized = activeSession?.status === "finalized";
|
||||
const selectedSessions = sessions.filter((session) => selectedSessionIds.has(session.id));
|
||||
const allSessionsSelected = sessions.length > 0 && selectedSessions.length === sessions.length;
|
||||
const showingAllSessions = sessionScope === "all";
|
||||
const isForeignSession = (session: SessionSummary) => {
|
||||
if (!showingAllSessions || !principal) return false;
|
||||
if (!session.author) return true;
|
||||
return session.author !== principal.subject && session.author !== principal.displayName;
|
||||
};
|
||||
|
||||
function selectActiveSession(id: string | null) {
|
||||
// Keep async Resume completions synchronized before React commits the state update.
|
||||
@@ -245,6 +253,14 @@ export function AppShell() {
|
||||
}
|
||||
}
|
||||
|
||||
function requestArchiveToggle(session: SessionSummary) {
|
||||
if (!session.archived && isForeignSession(session)) {
|
||||
const label = session.author ? `${session.author}'s session` : "this session";
|
||||
if (!window.confirm(`Archive ${label}?`)) return;
|
||||
}
|
||||
void toggleArchive(session);
|
||||
}
|
||||
|
||||
async function deleteSessions(targets: SessionSummary[]) {
|
||||
try {
|
||||
const results = await Promise.allSettled(targets.map((session) => deleteSession(session.id)));
|
||||
@@ -266,6 +282,14 @@ export function AppShell() {
|
||||
}
|
||||
}
|
||||
|
||||
function requestDelete(targets: SessionSummary[]) {
|
||||
if (targets.some(isForeignSession) || allSessionsSelected) {
|
||||
setDeleteTargets(targets);
|
||||
return;
|
||||
}
|
||||
void deleteSessions(targets);
|
||||
}
|
||||
|
||||
function menuFor(s: SessionSummary) {
|
||||
return (
|
||||
<SessionMenu
|
||||
@@ -276,8 +300,8 @@ export function AppShell() {
|
||||
onRename={() => setRenameTarget(s)}
|
||||
onMove={(g) => move(s, g)}
|
||||
onNewGroup={() => newGroup(s)}
|
||||
onArchiveToggle={() => toggleArchive(s)}
|
||||
onDelete={() => { void deleteSessions([s]); }}
|
||||
onArchiveToggle={() => requestArchiveToggle(s)}
|
||||
onDelete={() => requestDelete([s])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -496,6 +520,32 @@ export function AppShell() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{principal?.isAdmin && (
|
||||
<div className="px-4 pb-3">
|
||||
<div className="grid grid-cols-2 gap-1 rounded-lg bg-muted p-1" aria-label="Session scope">
|
||||
<Button
|
||||
variant={sessionScope === "mine" ? "secondary" : "ghost"}
|
||||
size="xs"
|
||||
onClick={() => setSessionScope("mine")}
|
||||
>
|
||||
My sessions
|
||||
</Button>
|
||||
<Button
|
||||
variant={showingAllSessions ? "secondary" : "ghost"}
|
||||
size="xs"
|
||||
onClick={() => setSessionScope("all")}
|
||||
>
|
||||
All sessions
|
||||
</Button>
|
||||
</div>
|
||||
{showingAllSessions && (
|
||||
<p className="mt-2 text-xs font-medium text-amber-700 dark:text-amber-400">
|
||||
Administrator view: all sessions
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* L1 — rail title */}
|
||||
<div className="px-4 pb-1.5 pt-1">
|
||||
<span className="thot-label text-[0.8rem] font-bold tracking-[0.18em] text-primary">
|
||||
@@ -519,8 +569,7 @@ export function AppShell() {
|
||||
size="xs"
|
||||
aria-label={`Delete ${selectedSessions.length} selected sessions`}
|
||||
onClick={() => {
|
||||
if (allSessionsSelected) setDeleteTargets(selectedSessions);
|
||||
else void deleteSessions(selectedSessions);
|
||||
requestDelete(selectedSessions);
|
||||
}}
|
||||
>
|
||||
<Trash2 />
|
||||
@@ -571,6 +620,7 @@ export function AppShell() {
|
||||
menuFor={menuFor}
|
||||
selectedIds={selectedSessionIds}
|
||||
onSelectionChange={setSessionSelected}
|
||||
showOwner={showingAllSessions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -586,6 +636,7 @@ export function AppShell() {
|
||||
menuFor={menuFor}
|
||||
selectedIds={selectedSessionIds}
|
||||
onSelectionChange={setSessionSelected}
|
||||
showOwner={showingAllSessions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -609,6 +660,7 @@ export function AppShell() {
|
||||
menuFor={menuFor}
|
||||
selectedIds={selectedSessionIds}
|
||||
onSelectionChange={setSessionSelected}
|
||||
showOwner={showingAllSessions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user