From ae72f7108011795f752581a85b2cf9fe6848e8e2 Mon Sep 17 00:00:00 2001
From: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com>
Date: Sun, 19 Jul 2026 13:54:58 -0700
Subject: [PATCH 1/3] feat(web): match Codex rail organization
---
FEATURE_MATRIX.md | 2 +-
THIRD_PARTY_NOTICES.md | 2 +-
apps/desktop/test/bundled-runtime.test.ts | 6 +-
apps/desktop/test/doctor.test.ts | 4 +-
apps/web/src/components/AppShell.tsx | 53 +--
apps/web/src/components/Rail.tsx | 437 ++++++++++++++++--
.../session-runtime/session-management.ts | 55 +++
apps/web/src/lib/session-tree.ts | 51 +-
apps/web/src/state/workspace-store.ts | 65 ++-
apps/web/test/mobile-touch-targets.test.tsx | 15 +-
apps/web/test/session-management.test.ts | 67 ++-
apps/web/test/session-tree.test.ts | 41 +-
apps/web/test/workspace-store.test.ts | 42 ++
compat/omp-app-matrix.json | 21 +-
docs/CODEX_RAIL_PARITY.md | 35 ++
e2e/built-web-server.ts | 120 +++++
e2e/pwa-install.spec.ts | 128 +----
e2e/rail-parity.spec.ts | 108 +++++
packages/fixture-server/src/engine.ts | 3 +
packages/fixture-server/test/engine.test.ts | 2 +
packages/protocol/package.json | 2 +-
packages/protocol/test/distribution.test.ts | 14 +-
pnpm-lock.yaml | 12 +-
.../t3code/imports/f1-shell-20260711.json | 4 +-
scripts/check-release-consistency.test.mjs | 7 +-
vendor/app-wire/manifest.json | 12 +-
vendor/app-wire/oh-my-pi-app-wire-0.6.2.tgz | Bin 0 -> 48654 bytes
27 files changed, 1054 insertions(+), 254 deletions(-)
create mode 100644 docs/CODEX_RAIL_PARITY.md
create mode 100644 e2e/built-web-server.ts
create mode 100644 e2e/rail-parity.spec.ts
create mode 100644 vendor/app-wire/oh-my-pi-app-wire-0.6.2.tgz
diff --git a/FEATURE_MATRIX.md b/FEATURE_MATRIX.md
index 7e3b124..ff530f1 100644
--- a/FEATURE_MATRIX.md
+++ b/FEATURE_MATRIX.md
@@ -31,7 +31,7 @@ OMP authority: `packages/coding-agent/src/session/agent-session.ts`, `session-ma
| Capability | Source command/state | Desktop behavior | T3 reference | Priority |
|---|---|---|---|---|
-| List/recent/search/filter | session store and metadata | Bounded left rail; folder or flat organization; Priority, Last updated, or Manual order; title/folder/host search; attention/running/unread/error filters; pinned shortcuts; running/waiting/failed/unread badges | `Sidebar.tsx`, sidebar logic/tests | Launch |
+| List/recent/search/filter | session store and metadata | Codex-parity left rail: By project or In one list; Priority, Last updated, or Manual order; real project/session dragging with keyboard fallbacks; title/project/host search; attention/running/unread/error filters; pinned shortcuts; five-row Show more; project aliases; reversible hidden projects; bulk read/archive; local-only Finder reveal; direct pin/archive controls | `Rail.tsx`, `session-tree.ts`, workspace store, management helpers, browser tests | Launch |
| New session | `/new` | Create in selected project/host; model/profile defaults visible before first prompt | draft routes and composer draft store | Launch |
| Fast switch and tabs | session IDs and snapshots | One-click/keyboard switch; preserve draft, scroll anchor, panel widths/tabs, terminal focus; no white flash | T3 routes, `composerDraftStore`, `rightPanelStore`, terminal store | Launch |
| Resume | `/resume` | Open existing session by stable ID/path; recover moved/missing files with explicit error | thread routing and reconnect supervisor | Launch |
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index 3ac7a34..41bc2fb 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -6,7 +6,7 @@ T3 Code is selectively referenced for future ports from https://github.com/pingd
## Oh My Pi
-Future adaptations of OMP source use the OMP repository under its repository license. OMP remains runtime authority; adapted files retain OMP attribution and the applicable source license. The vendored `@oh-my-pi/app-wire@0.6.1` package is packed from the public `lyc-aon/oh-my-pi` integration commit `e3e15c03ae95ebbda5f26495cd21213cc53518b1`, source tree `e0f32b279eb4b8cbc403e47d765a226bee99c99f`; tarball SHA-256 `78ec6223e7ad0f4f9e14526a822eaa45b363c856065c08196b2817a2f42740b9`; golden corpus SHA-256 `d5e674095de3d9b3b56a5668bc91cbbf1904b409ea9ea6456c2eabdf272e7870`. Target integration commit is recorded in the Desktop commit history and compatibility matrix.
+Future adaptations of OMP source use the OMP repository under its repository license. OMP remains runtime authority; adapted files retain OMP attribution and the applicable source license. The vendored `@oh-my-pi/app-wire@0.6.2` package is packed from the public `lyc-aon/oh-my-pi` integration commit `04229b1f46547ac7c0617e55a993496ec9725f46`, source tree `8400a3af618e8af11cccf6b20aadcf3a22baf9a1`; tarball SHA-256 `0f13659ebaabd284595558f023731723b928231bc82245f54ea25e79d00b6a6f`; golden corpus SHA-256 `d5e674095de3d9b3b56a5668bc91cbbf1904b409ea9ea6456c2eabdf272e7870`. Target integration commit is recorded in the Desktop commit history and compatibility matrix.
## Oh My Pi icon
diff --git a/apps/desktop/test/bundled-runtime.test.ts b/apps/desktop/test/bundled-runtime.test.ts
index 16ccba2..fc9b2dc 100644
--- a/apps/desktop/test/bundled-runtime.test.ts
+++ b/apps/desktop/test/bundled-runtime.test.ts
@@ -16,7 +16,7 @@ describe("bundled OMP runtime", () => {
const sha256 = createHash("sha256").update(bytes).digest("hex");
await writeFile(join(runtimeRoot, "omp"), bytes);
await writeFile(join(runtimeRoot, "manifest.json"), JSON.stringify({
- version: 1, tag: "t4code-17.0.5-appserver-6", platform: "darwin", arch: "arm64",
+ version: 1, tag: "t4code-17.0.5-appserver-7", platform: "darwin", arch: "arm64",
executable: "omp", size: bytes.length, sha256,
}));
@@ -35,7 +35,7 @@ describe("bundled OMP runtime", () => {
await mkdir(runtimeRoot, { recursive: true });
await writeFile(join(runtimeRoot, "omp"), "wrong");
await writeFile(join(runtimeRoot, "manifest.json"), JSON.stringify({
- version: 1, tag: "t4code-17.0.5-appserver-6", platform: "darwin", arch: "arm64",
+ version: 1, tag: "t4code-17.0.5-appserver-7", platform: "darwin", arch: "arm64",
executable: "omp", size: 5, sha256: "0".repeat(64),
}));
@@ -58,7 +58,7 @@ describe("bundled OMP runtime", () => {
await writeFile(join(runtimeRoot, "omp"), signedBytes);
await writeFile(join(runtimeRoot, "manifest.json"), JSON.stringify({
version: 1,
- tag: "t4code-17.0.5-appserver-6",
+ tag: "t4code-17.0.5-appserver-7",
platform: "darwin",
arch: "arm64",
executable: "omp",
diff --git a/apps/desktop/test/doctor.test.ts b/apps/desktop/test/doctor.test.ts
index d0dd42d..0f345a3 100644
--- a/apps/desktop/test/doctor.test.ts
+++ b/apps/desktop/test/doctor.test.ts
@@ -41,9 +41,9 @@ describe("T4 setup doctor", () => {
const source = await readSourceContract();
expect(source.ompVersion).toBe("17.0.5");
- expect(source.ompTag).toBe("t4code-17.0.5-appserver-6");
+ expect(source.ompTag).toBe("t4code-17.0.5-appserver-7");
expect(source.ompUrl).toBe(
- "https://github.com/lyc-aon/oh-my-pi/tree/t4code-17.0.5-appserver-6",
+ "https://github.com/lyc-aon/oh-my-pi/tree/t4code-17.0.5-appserver-7",
);
});
diff --git a/apps/web/src/components/AppShell.tsx b/apps/web/src/components/AppShell.tsx
index c4cea9e..c3c6bb1 100644
--- a/apps/web/src/components/AppShell.tsx
+++ b/apps/web/src/components/AppShell.tsx
@@ -41,7 +41,8 @@ export function AppShell() {
(state) => state.sessionManualOrderByProjectId,
);
const projectExpandedById = useWorkspace((state) => state.projectExpandedById);
- const dismissedEmptyProjectIds = useWorkspace((state) => state.dismissedEmptyProjectIds);
+ const hiddenProjectIds = useWorkspace((state) => state.hiddenProjectIds);
+ const projectAliasById = useWorkspace((state) => state.projectAliasById);
const lastVisitedAtBySessionId = useWorkspace((state) => state.lastVisitedAtBySessionId);
const lastSeenAttentionOutcomeBySessionKey = useWorkspace(
(state) => state.lastSeenAttentionOutcomeBySessionKey,
@@ -58,20 +59,22 @@ export function AppShell() {
projectExpandedById,
lastVisitedAtBySessionId,
"current",
- dismissedEmptyProjectIds,
+ hiddenProjectIds,
{
filter: railFilter,
query: railQuery,
sort: railSort,
projectManualOrder,
sessionManualOrderByProjectId,
+ projectAliasById,
},
),
[
shellData,
projectExpandedById,
lastVisitedAtBySessionId,
- dismissedEmptyProjectIds,
+ hiddenProjectIds,
+ projectAliasById,
railFilter,
railQuery,
railSort,
@@ -93,6 +96,7 @@ export function AppShell() {
sort: railSort,
projectManualOrder,
sessionManualOrderByProjectId,
+ projectAliasById,
},
),
[
@@ -104,6 +108,7 @@ export function AppShell() {
railSort,
projectManualOrder,
sessionManualOrderByProjectId,
+ projectAliasById,
],
);
const allCurrentGroups = useMemo(
@@ -113,14 +118,14 @@ export function AppShell() {
projectExpandedById,
lastVisitedAtBySessionId,
"current",
- dismissedEmptyProjectIds,
- { sort: railSort, projectManualOrder, sessionManualOrderByProjectId },
+ {},
+ { sort: railSort, projectManualOrder, sessionManualOrderByProjectId, projectAliasById },
),
[
shellData,
projectExpandedById,
lastVisitedAtBySessionId,
- dismissedEmptyProjectIds,
+ projectAliasById,
railSort,
projectManualOrder,
sessionManualOrderByProjectId,
@@ -134,7 +139,7 @@ export function AppShell() {
lastVisitedAtBySessionId,
"archived",
{},
- { sort: railSort, projectManualOrder, sessionManualOrderByProjectId },
+ { sort: railSort, projectManualOrder, sessionManualOrderByProjectId, projectAliasById },
),
[
shellData,
@@ -143,6 +148,7 @@ export function AppShell() {
railSort,
projectManualOrder,
sessionManualOrderByProjectId,
+ projectAliasById,
],
);
const allSessionGroups = useMemo(
@@ -163,24 +169,10 @@ export function AppShell() {
}).urgentCount,
[lastSeenAttentionOutcomeBySessionKey, runtimeSnapshot],
);
- const hiddenEmptyProjectIds = useMemo(() => {
- const currentProjectIds = new Set(
- shellData.sessions
- .filter((session) => session.archivedAt === undefined)
- .map((session) => session.projectId),
- );
- const hostById = new Map(shellData.hosts.map((host) => [host.id, host]));
- return new Set(
- shellData.projects
- .filter(
- (project) =>
- dismissedEmptyProjectIds[project.id] === true &&
- !currentProjectIds.has(project.id) &&
- hostById.get(project.hostId)?.sessionInventoryTruncated !== true,
- )
- .map((project) => project.id),
- );
- }, [dismissedEmptyProjectIds, shellData]);
+ const hiddenProjectIdSet = useMemo(
+ () => new Set(Object.keys(hiddenProjectIds).filter((id) => hiddenProjectIds[id] === true)),
+ [hiddenProjectIds],
+ );
// Desktop mode: start the runtime once. StrictMode's doubled effect and
// HMR remounts are safe — start is idempotent on a global singleton.
@@ -250,13 +242,14 @@ export function AppShell() {
state.projectExpandedById,
state.lastVisitedAtBySessionId,
state.sessionListView,
- state.dismissedEmptyProjectIds,
+ state.hiddenProjectIds,
{
filter: state.railFilter,
query: state.railQuery,
sort: state.railSort,
projectManualOrder: state.projectManualOrder,
sessionManualOrderByProjectId: state.sessionManualOrderByProjectId,
+ projectAliasById: state.projectAliasById,
},
),
);
@@ -319,7 +312,9 @@ export function AppShell() {
!hiddenProjectIdSet.has(group.project.id),
+ )}
onExpand={(projectId) => {
const state = workspaceStore.getState();
state.setRailCollapsed(false);
@@ -335,7 +330,7 @@ export function AppShell() {
archivedCount={archivedCount}
currentCount={currentCount}
groups={groups}
- hiddenEmptyProjectIds={hiddenEmptyProjectIds}
+ hiddenProjectIds={hiddenProjectIdSet}
nowMs={nowMs}
pinnedSessionGroups={allSessionGroups}
view={sessionListView}
@@ -391,7 +386,7 @@ export function AppShell() {
archivedCount={archivedCount}
currentCount={currentCount}
groups={groups}
- hiddenEmptyProjectIds={hiddenEmptyProjectIds}
+ hiddenProjectIds={hiddenProjectIdSet}
nowMs={nowMs}
pinnedSessionGroups={allSessionGroups}
view={sessionListView}
diff --git a/apps/web/src/components/Rail.tsx b/apps/web/src/components/Rail.tsx
index 17591cd..a1d7c1f 100644
--- a/apps/web/src/components/Rail.tsx
+++ b/apps/web/src/components/Rail.tsx
@@ -28,6 +28,9 @@ import {
ChevronDown,
ChevronRight,
CircleStop,
+ CheckCheck,
+ EyeOff,
+ FolderSearch,
Folder,
Inbox,
LayoutList,
@@ -59,6 +62,7 @@ import {
flattenProjectGroups,
formatRelativeTime,
moveIdInManualOrder,
+ moveIdToManualIndex,
type ProjectGroup,
type RailFilter,
type RailOrganization,
@@ -71,6 +75,8 @@ import {
archiveLiveSession,
deleteLiveSession,
managementCommandSupport,
+ projectRevealSupport,
+ revealLiveProject,
renameLiveSession,
restoreLiveSession,
sessionCreateSupport,
@@ -112,6 +118,7 @@ function SessionRowItem({
canMoveUp,
canMoveDown,
onMove,
+ onDrop,
}: {
row: SessionRow;
active: boolean;
@@ -123,6 +130,7 @@ function SessionRowItem({
canMoveUp?: boolean;
canMoveDown?: boolean;
onMove?: (direction: -1 | 1) => void;
+ onDrop?: (sourceId: string) => void;
}) {
const navigate = useNavigate();
const snapshot = useDesktopRuntimeSnapshot();
@@ -266,10 +274,34 @@ function SessionRowItem({
);
return (
-
+
{
+ if (!manual) return;
+ event.stopPropagation();
+ event.preventDefault();
+ event.dataTransfer.dropEffect = "move";
+ }}
+ onDragStart={(event) => {
+ if (!manual) return;
+ event.stopPropagation();
+ event.dataTransfer.effectAllowed = "move";
+ event.dataTransfer.setData("text/plain", `session:${session.id}`);
+ }}
+ onDrop={(event) => {
+ if (!manual || onDrop === undefined) return;
+ event.stopPropagation();
+ event.preventDefault();
+ const value = event.dataTransfer.getData("text/plain");
+ if (value.startsWith("session:")) onDrop(value.slice("session:".length));
+ }}
+ >
+
+
+ {
+ workspaceStore.getState().setSessionPinned(session.id, !pinned);
+ onAnnounce(`${session.title} ${pinned ? "unpinned" : "pinned"}.`);
+ }}
+ type="button"
+ >
+ {pinned ? (
+
+ ) : (
+
+ )}
+
+ }
+ />
+ {pinned ? "Unpin chat" : "Pin chat"}
+
+
+ {
+ const available = archived
+ ? restoreSupport.supported
+ : archiveSupport.supported;
+ if (!available || pending !== null) return;
+ void runAction(archived ? "restore" : "archive");
+ }}
+ title={(archived ? restoreSupport.reason : archiveSupport.reason) ?? undefined}
+ type="button"
+ >
+ {pending === (archived ? "restore" : "archive") ? (
+
+ ) : archived ? (
+
+ ) : (
+
+ )}
+
+ }
+ />
+ {archived ? "Restore chat" : "Archive chat"}
+
+
void;
+ onDrop: (sourceId: string) => void;
onPin: () => void;
+ onAnnounce: (message: string) => void;
view: SessionListView;
}) {
const navigate = useNavigate();
const snapshot = useDesktopRuntimeSnapshot();
const controller = desktopRuntime();
const [pending, setPending] = useState(false);
+ const [renameOpen, setRenameOpen] = useState(false);
+ const [renameValue, setRenameValue] = useState(group.displayName);
const [menuOpen, setMenuOpen] = useState(false);
const [createMenuOpen, setCreateMenuOpen] = useState(false);
const [error, setError] = useState(null);
@@ -692,6 +793,10 @@ function ProjectHeaderRow({
snapshot !== null &&
address !== null &&
snapshot.targets.get(address.targetId)?.kind === "local";
+ const revealSupport =
+ snapshot !== null && address !== null
+ ? projectRevealSupport(snapshot, address)
+ : { supported: false, reason: "Connect to this host to reveal the project" };
const configuredLocalProfiles =
projectIsLocal && snapshot !== null
? [...snapshot.targets.values()].filter((target) => target.kind === "local")
@@ -743,6 +848,63 @@ function ProjectHeaderRow({
const inventoryTruncated = group.host.sessionInventoryTruncated === true;
const showShortcutAction = emptyCurrentProject || (view === "archived" && shortcutHidden);
+ const markAllRead = () => {
+ const visits = Object.fromEntries(
+ group.sessions.map(({ session }) => [
+ session.id,
+ session.latestTurnCompletedAt ?? session.updatedAt,
+ ]),
+ );
+ workspaceStore.getState().markSessionsVisited(visits);
+ onAnnounce(`Marked all sessions in ${group.displayName} as read.`);
+ setMenuOpen(false);
+ };
+
+ const archiveAll = async () => {
+ if (controller === null || snapshot === null || pending) return;
+ const candidates = group.sessions.flatMap(({ session }) => {
+ const sessionAddress = resolveLiveSession(snapshot, session.id);
+ if (sessionAddress === null) return [];
+ const support = managementCommandSupport(snapshot, sessionAddress, "session.archive");
+ return support.supported ? [{ session, address: sessionAddress }] : [];
+ });
+ if (candidates.length === 0) {
+ setError("No sessions in this project can be archived right now.");
+ return;
+ }
+ setPending(true);
+ setError(null);
+ let completed = 0;
+ try {
+ for (const candidate of candidates) {
+ await archiveLiveSession(controller, candidate.address);
+ completed += 1;
+ }
+ onAnnounce(`Archived ${completed} sessions in ${group.displayName}.`);
+ setMenuOpen(false);
+ } catch (cause) {
+ const message = cause instanceof Error ? cause.message : "Project archive failed.";
+ setError(`${completed} archived before the operation stopped. ${message}`);
+ } finally {
+ setPending(false);
+ }
+ };
+
+ const revealProject = async () => {
+ if (controller === null || address === null || pending || !revealSupport.supported) return;
+ setPending(true);
+ setError(null);
+ try {
+ await revealLiveProject(controller, address);
+ onAnnounce(`Revealed ${group.displayName} in Finder.`);
+ setMenuOpen(false);
+ } catch (cause) {
+ setError(cause instanceof Error ? cause.message : "Project reveal failed.");
+ } finally {
+ setPending(false);
+ }
+ };
+
const handleCreate = useCallback(
async (targetAddress: NonNullable) => {
if (!canCreate || controller === null) return;
@@ -763,14 +925,35 @@ function ProjectHeaderRow({
);
return (
-
+
{
+ if (!manual) return;
+ event.preventDefault();
+ event.dataTransfer.dropEffect = "move";
+ }}
+ onDragStart={(event) => {
+ if (!manual) return;
+ event.dataTransfer.effectAllowed = "move";
+ event.dataTransfer.setData("text/plain", `project:${group.project.id}`);
+ }}
+ onDrop={(event) => {
+ if (!manual) return;
+ event.preventDefault();
+ const value = event.dataTransfer.getData("text/plain");
+ if (value.startsWith("project:")) onDrop(value.slice("project:".length));
+ }}
+ >
0 ? `, ${group.unreadCount} unread` : ""}`}
+ aria-label={`${group.displayName}, ${group.sessions.length} ${group.sessions.length === 1 ? "session" : "sessions"}${group.unreadCount > 0 ? `, ${group.unreadCount} unread` : ""}`}
className="flex min-h-11 min-w-0 flex-1 items-center gap-1 rounded-md px-1.5 py-1 text-left outline-none transition-colors duration-(--motion-duration-fast) hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background sm:min-h-0"
data-project-disclosure={group.project.id}
onClick={() =>
@@ -787,7 +970,7 @@ function ProjectHeaderRow({
)}
/>
- {group.project.name}
+ {group.displayName}
{group.host.kind === "remote" && (
@@ -814,7 +997,7 @@ function ProjectHeaderRow({
/>
- {group.project.name}
+ {group.displayName}
{group.host.kind === "remote" ? "Remote host" : "Host profile"}: {group.host.name}
@@ -827,7 +1010,7 @@ function ProjectHeaderRow({
@@ -847,7 +1030,7 @@ function ProjectHeaderRow({
- New session in {group.project.name}
+ New session in {group.displayName}
The OMP profile you choose will own this session.
@@ -927,7 +1110,7 @@ function ProjectHeaderRow({
render={
- {createSupport.reason ?? `New session in ${group.project.name}`}
+ {createSupport.reason ?? `New session in ${group.displayName}`}
) : null}
@@ -968,7 +1151,7 @@ function ProjectHeaderRow({
- {group.project.name}
+ {group.displayName}
)}
- {pinned ? "Unpin working folder" : "Pin working folder"}
+ {pinned ? "Unpin project" : "Pin project"}
+
+
+
+ {view === "current" && group.sessions.length > 0 && (
+
+ )}
{manual && (
<>
))}
+ {view === "current" && !emptyCurrentProject && (
+
+ )}
@@ -1083,6 +1337,58 @@ function ProjectHeaderRow({
{error}
)}
+
);
}
@@ -1112,9 +1418,11 @@ const RAIL_FILTERS: ReadonlyArray<{ readonly value: RailFilter; readonly label:
];
function RailOptionsMenu({
+ hiddenGroups,
organization,
sort,
}: {
+ hiddenGroups: readonly ProjectGroup[];
organization: RailOrganization;
sort: RailSort;
}) {
@@ -1154,7 +1462,7 @@ function RailOptionsMenu({
Organize sidebar
- {option(organization === "by-project", "By working folder", () =>
+ {option(organization === "by-project", "By project", () =>
workspaceStore.getState().setRailOrganization("by-project"),
)}
{option(organization === "flat", "In one list", () =>
@@ -1171,6 +1479,28 @@ function RailOptionsMenu({
{option(sort === "manual", "Manual order", () =>
workspaceStore.getState().setRailSort("manual"),
)}
+ {hiddenGroups.length > 0 && (
+ <>
+
+
+ Hidden projects
+
+ {hiddenGroups.map((group) => (
+
+ ))}
+ >
+ )}
@@ -1181,7 +1511,7 @@ function RailOptionsMenu({
export function Rail({
allGroups,
groups,
- hiddenEmptyProjectIds,
+ hiddenProjectIds,
nowMs,
pinnedSessionGroups,
view,
@@ -1191,7 +1521,7 @@ export function Rail({
}: {
allGroups: readonly ProjectGroup[];
groups: readonly ProjectGroup[];
- hiddenEmptyProjectIds: ReadonlySet
;
+ hiddenProjectIds: ReadonlySet;
nowMs: number;
pinnedSessionGroups: readonly ProjectGroup[];
view: SessionListView;
@@ -1232,8 +1562,12 @@ export function Rail({
});
}, [pinnedSessionIds, pinnedSourceEntries]);
const pinnedGroups = useMemo(
- () => allGroups.filter((group) => pinnedProjectIds[group.project.id] === true),
- [allGroups, pinnedProjectIds],
+ () =>
+ allGroups.filter(
+ (group) =>
+ pinnedProjectIds[group.project.id] === true && !hiddenProjectIds.has(group.project.id),
+ ),
+ [allGroups, hiddenProjectIds, pinnedProjectIds],
);
const matchCount = flatEntries.length;
@@ -1265,6 +1599,34 @@ export function Rail({
);
};
+ const dropProject = (sourceId: string, targetId: string) => {
+ const visibleIds = groups.map((group) => group.project.id);
+ workspaceStore
+ .getState()
+ .setProjectManualOrder(
+ moveIdToManualIndex(projectManualOrder, visibleIds, sourceId, targetId),
+ );
+ };
+
+ const dropSession = (
+ projectId: string,
+ visibleIds: readonly string[],
+ sourceId: string,
+ targetId: string,
+ ) => {
+ workspaceStore
+ .getState()
+ .setSessionManualOrder(
+ projectId,
+ moveIdToManualIndex(
+ sessionManualOrderByProjectId[projectId] ?? [],
+ visibleIds,
+ sourceId,
+ targetId,
+ ),
+ );
+ };
+
const dismissProject = (group: ProjectGroup) => {
const disclosures = [
...(navRef.current?.querySelectorAll("[data-project-disclosure]") ?? []),
@@ -1274,9 +1636,9 @@ export function Rail({
);
const focusTarget =
disclosures[currentIndex + 1] ?? disclosures[currentIndex - 1] ?? navRef.current;
- workspaceStore.getState().setEmptyProjectDismissed(group.project.id, true);
+ workspaceStore.getState().setProjectHidden(group.project.id, true);
setAnnouncement(
- `Removed ${group.project.name} from Working folders. The folder and OMP sessions are unchanged.`,
+ `Removed ${group.displayName} from Projects. The folder and OMP sessions are unchanged.`,
);
requestAnimationFrame(() => {
const target = focusTarget?.isConnected ? focusTarget : navRef.current;
@@ -1297,7 +1659,11 @@ export function Rail({
Sessions
{matchCount} matches
-
+ hiddenProjectIds.has(group.project.id))}
+ organization={organization}
+ sort={sort}
+ />