Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@t3tools/desktop",
"version": "0.0.31",
"version": "0.0.32",
"private": true,
"type": "module",
"main": "dist-electron/main.cjs",
Expand Down
23 changes: 13 additions & 10 deletions apps/mobile/src/components/CompactBrandTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../native/native-glass";
const IOS_NATIVE_LEADING_TITLE_OFFSET = -6;
const IPAD_NATIVE_LEADING_TITLE_OFFSET = 7;

/**
* Horizontal correction applied to content rendered in the brand title slot,
* shared with the connection-status swap so both align identically.
*/
export function brandTitleOffset(nativeLeadingItem: boolean): number {
if (Platform.OS !== "ios") return 0;
if (nativeLeadingItem) {
return Platform.isPad ? IPAD_NATIVE_LEADING_TITLE_OFFSET : IOS_NATIVE_LEADING_TITLE_OFFSET;
}
return Platform.isPad ? IPAD_HOME_TITLE_OFFSET : 0;
}

/**
* Compact brand lockup sized for native navigation bars.
*/
Expand All @@ -28,16 +40,7 @@ export function CompactBrandTitle(
const mutedColor = useThemeColor("--color-foreground-muted");
const subtleColor = useThemeColor("--color-subtle");
const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant);
const titleOffset =
Platform.OS !== "ios"
? 0
: props.nativeLeadingItem
? Platform.isPad
? IPAD_NATIVE_LEADING_TITLE_OFFSET
: IOS_NATIVE_LEADING_TITLE_OFFSET
: Platform.isPad
? IPAD_HOME_TITLE_OFFSET
: 0;
const titleOffset = brandTitleOffset(props.nativeLeadingItem === true);

return (
<View
Expand Down
52 changes: 39 additions & 13 deletions apps/mobile/src/features/home/HomeHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands
import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items";
import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar";
import type { HomeProjectSortOrder } from "./homeThreadList";
import {
getConnectionAwareBrandHeaderOptions,
WorkspaceConnectionTitle,
} from "./WorkspaceConnectionTitle";
import {
buildHomeListFilterMenu,
type HomeListFilterMenuEnvironment,
Expand Down Expand Up @@ -83,6 +87,7 @@ export function HomeHeader(props: {
readonly onHideSettledThreadsChange: (hide: boolean) => void;
readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void;
readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;
readonly onOpenEnvironments: () => void;
readonly onOpenSettings: () => void;
readonly onStartNewTask: () => void;
}) {
Expand Down Expand Up @@ -336,17 +341,27 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
>
<View className="w-full max-w-[720px] self-center gap-3">
<View className="flex-row items-center gap-2.5">
<View className="flex-1 flex-row items-center gap-2">
<T3Wordmark color={iconColor} height={15} />
<RNText className="-ml-0.5 text-[21px] font-t3-medium tracking-[-0.5px] text-foreground-muted">
Code
</RNText>
<View className="rounded-full bg-subtle px-2 py-0.75">
<RNText className="text-[11px] font-t3-bold tracking-[1.1px] text-foreground-muted uppercase">
Alpha
</RNText>
</View>
</View>
{/* Brand slot doubles as the connection status surface: while an
environment reconnects, the lockup fades to a status label in
place (no layout shift in the list below). */}
<WorkspaceConnectionTitle
grow
onPress={props.onOpenEnvironments}
brand={
<View className="flex-row items-center gap-2">
{/* Mirrors the desktop SidebarBrand: T3 mark + muted "Code". */}
<T3Wordmark color={iconColor} height={15} />
<RNText className="-ml-0.5 text-[21px] font-t3-medium tracking-[-0.5px] text-foreground-muted">
Code
</RNText>
<View className="rounded-full bg-subtle px-2 py-0.75">
<RNText className="text-[11px] font-t3-bold tracking-[1.1px] text-foreground-muted uppercase">
{stageLabel}
</RNText>
</View>
</View>
}
/>

{alternateModes.map((mode) => (
<Pressable
Expand Down Expand Up @@ -506,8 +521,19 @@ function IosHomeHeader(props: HomeHeaderProps) {
<NativeStackScreenOptions
optionsVersion={[filterMenu.items, props.listMode, headerTitle, useSolidBoardHeader]}
options={{
title: headerTitle,
headerTitle,
// The iOS Home header owns the native title, so the connection
// status has to swap in here — the in-flow header above (Android)
// is not mounted on this path. The list-mode title is passed
// through so it survives the swap.
...getConnectionAwareBrandHeaderOptions({
onOpenEnvironments: props.onOpenEnvironments,
title: headerTitle,
brand: (
<RNText className="text-[18px] font-t3-bold text-foreground" numberOfLines={1}>
{headerTitle}
</RNText>
),
}),
headerTintColor: iconColor,
// Explicitly toggle glass ↔ solid when switching modes so board
// underlap does not stick after leaving Board, and vice versa.
Expand Down
13 changes: 8 additions & 5 deletions apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
import { AsyncResult } from "effect/unstable/reactivity";
import { useCallback, useEffect, useMemo, useState } from "react";

import { getCompactBrandHeaderOptions } from "../../components/CompactBrandTitle";
import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
import { useProjects, useThreadShells } from "../../state/entities";
import {
Expand Down Expand Up @@ -57,6 +56,7 @@ export function HomeRouteScreen() {
unsnoozeThread,
pinThread,
unpinThread,
movePinnedThread,
unsettleThread,
} = useThreadListActions();
const pendingTasks = usePendingNewTasks();
Expand Down Expand Up @@ -187,7 +187,9 @@ export function HomeRouteScreen() {
onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
>
<>
{/* Title is owned by HomeHeader (tracks list mode). */}
{/* Title is owned by HomeHeader (tracks list mode), which carries the
connection-aware brand slot — no native-stack title to avoid
showing the status twice. */}
<HomeHeader
environments={environments}
projects={projectFilterOptions}
Expand All @@ -209,6 +211,9 @@ export function HomeRouteScreen() {
onOwnershipFilterChange={setOwnershipFilter}
onOwnershipRelationChange={setOwnershipRelation}
onHideSettledThreadsChange={setHideSettledThreads}
onOpenEnvironments={() =>
navigation.navigate("SettingsSheet", { screen: "SettingsEnvironments" })
}
onOpenSettings={() => navigation.navigate("SettingsSheet", { screen: "Settings" })}
onProjectSortOrderChange={setProjectSortOrder}
onSearchQueryChange={setSearchQuery}
Expand All @@ -235,10 +240,8 @@ export function HomeRouteScreen() {
onUnpinThread={unpinThread}
onClearEnvironments={clearSelectedEnvironments}
onToggleEnvironment={toggleSelectedEnvironmentId}
onMovePinnedThread={movePinnedThread}
onProjectChange={setSelectedProjectKey}
onOpenEnvironments={() =>
navigation.navigate("SettingsSheet", { screen: "SettingsEnvironments" })
}
onOpenSettings={() => navigation.navigate("SettingsSheet", { screen: "Settings" })}
onProjectSortOrderChange={setProjectSortOrder}
onSearchQueryChange={setSearchQuery}
Expand Down
91 changes: 49 additions & 42 deletions apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type EnvironmentThreadSearchMatch,
} from "@t3tools/client-runtime/state/thread-search";
import { effectiveSettled } from "@t3tools/client-runtime/state/thread-settled";
import { sortPinnedThreadsByOrderKey } from "@t3tools/client-runtime/state/thread-sort";
import type {
EnvironmentId,
SidebarProjectGroupingMode,
Expand Down Expand Up @@ -84,8 +85,6 @@ import {
type HomeProjectSortOrder,
} from "./homeThreadList";
import { SwipeableScrollGateProvider, useSwipeableScrollGate } from "./thread-swipe-actions";
import { WorkspaceConnectionStatus } from "./WorkspaceConnectionStatus";
import { shouldShowWorkspaceConnectionStatus } from "./workspace-connection-status";

/* ─── Types ──────────────────────────────────────────────────────────── */

Expand Down Expand Up @@ -123,7 +122,6 @@ interface HomeScreenProps {
readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void;
readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;
readonly onAddConnection: () => void;
readonly onOpenEnvironments: () => void;
readonly onOpenSettings: () => void;
readonly onStartNewTask: () => void;
readonly onSelectThread: (thread: EnvironmentThreadShell) => void;
Expand All @@ -139,6 +137,10 @@ interface HomeScreenProps {
readonly onUnsettleThread: (thread: EnvironmentThreadShell) => void;
readonly onPinThread: (thread: EnvironmentThreadShell) => Promise<boolean>;
readonly onUnpinThread: (thread: EnvironmentThreadShell) => Promise<boolean>;
readonly onMovePinnedThread: (
thread: EnvironmentThreadShell,
direction: "up" | "down",
) => Promise<boolean>;
readonly onSelectPendingTask: (pendingTask: PendingNewTask) => void;
readonly onDeletePendingTask: (pendingTask: PendingNewTask) => void;
readonly onNewThreadInProject: (project: EnvironmentProject) => void;
Expand Down Expand Up @@ -558,6 +560,12 @@ export function HomeScreen(props: HomeScreenProps) {
},
[props.onPinThread],
);
const handleMovePinnedThread = useCallback(
(thread: EnvironmentThreadShell, direction: "up" | "down") => {
void props.onMovePinnedThread(thread, direction);
},
[props.onMovePinnedThread],
);
const handleUnpinThread = useCallback(
(thread: EnvironmentThreadShell) => {
void props.onUnpinThread(thread);
Expand Down Expand Up @@ -814,6 +822,29 @@ export function HomeScreen(props: HomeScreenProps) {
showProjectThreadList,
visibleRecentEntries,
]);
const pinReorderEnvironmentIds = useMemo(() => {
const supported = new Set<EnvironmentId>();
for (const [environmentId, config] of serverConfigs) {
if (config.environment.capabilities.threadPinReorder === true) {
supported.add(environmentId);
}
}
return supported;
}, [serverConfigs]);
// Canonical arranged pinned order (reorder-capable threads only) for the
// Move up/down position flags. Computed from all shells, not the rendered
// list, so search/scope filtering never disables or misdirects a move.
const arrangedPinnedKeys = useMemo(() => {
const pinned = sortPinnedThreadsByOrderKey(
props.threads.filter(
(thread) =>
thread.pinnedAt != null &&
thread.archivedAt === null &&
pinReorderEnvironmentIds.has(thread.environmentId),
),
);
return pinned.map((thread) => `${thread.environmentId}:${thread.id}`);
}, [pinReorderEnvironmentIds, props.threads]);
const threadListV2Layout = useMemo(() => {
if (!threadListV2Enabled)
return {
Expand Down Expand Up @@ -1000,11 +1031,18 @@ export function HomeScreen(props: HomeScreenProps) {
onSettleThread={handleSettleThread}
snoozeSupported={snoozeEnvironmentIds.has(thread.environmentId)}
pinningSupported={pinningEnvironmentIds.has(thread.environmentId)}
pinReorderSupported={pinReorderEnvironmentIds.has(thread.environmentId)}
canMovePinnedUp={arrangedPinnedKeys.indexOf(`${thread.environmentId}:${thread.id}`) > 0}
canMovePinnedDown={(() => {
const index = arrangedPinnedKeys.indexOf(`${thread.environmentId}:${thread.id}`);
return index !== -1 && index < arrangedPinnedKeys.length - 1;
})()}
onSnoozeThread={handleSnoozeThread}
onUnsnoozeThread={handleUnsnoozeThread}
onUnsettleThread={handleUnsettleThread}
onPinThread={handlePinThread}
onUnpinThread={handleUnpinThread}
onMovePinnedThread={handleMovePinnedThread}
onChangeRequestState={handleChangeRequestState}
projectCwd={
projectCwdByKey.get(scopedProjectKey(thread.environmentId, thread.projectId)) ?? null
Expand All @@ -1017,6 +1055,8 @@ export function HomeScreen(props: HomeScreenProps) {
[
handleChangeRequestState,
handleDeleteThread,
arrangedPinnedKeys,
handleMovePinnedThread,
handlePinThread,
handleSettleThread,
handleSnoozeThread,
Expand All @@ -1026,6 +1066,7 @@ export function HomeScreen(props: HomeScreenProps) {
handleSwipeableWillOpen,
handleUnsettleThread,
pinningEnvironmentIds,
pinReorderEnvironmentIds,
projectByKey,
projectCwdByKey,
props.onArchiveThread,
Expand Down Expand Up @@ -1194,20 +1235,13 @@ export function HomeScreen(props: HomeScreenProps) {
}
return map;
}, [props.savedConnectionsById]);
const shouldShowConnectionStatus = shouldShowWorkspaceConnectionStatus(props.catalogState);
// Connection state surfaces in the header title slot
// (WorkspaceConnectionTitle) — nothing renders inside the list, so
// reconnects never shift the rows.
const emptyState = deriveEmptyState({
catalogState: props.catalogState,
projectCount: props.projects.length,
});
const connectionStatus =
shouldShowConnectionStatus && Platform.OS !== "ios" ? (
<View
className="absolute left-0 right-0 items-center"
style={{ bottom: Math.max(insets.bottom, 18) + 76 }}
>
<WorkspaceConnectionStatus state={props.catalogState} onPress={props.onOpenEnvironments} />
</View>
) : null;

// Board owns its empty chrome; connection-level empty still applies below
// for Recent/Projects when the workspace has no threads at all.
Expand All @@ -1228,41 +1262,17 @@ export function HomeScreen(props: HomeScreenProps) {
onAction={!props.catalogState.hasReadyEnvironment ? props.onAddConnection : undefined}
variant="plain"
/>
{emptyState.loading && !shouldShowConnectionStatus ? (
{emptyState.loading ? (
<View className="mt-4 items-center">
<ActivityIndicator color={accentColor} />
</View>
) : null}
{shouldShowConnectionStatus && Platform.OS === "ios" ? (
<View className="mt-4">
<WorkspaceConnectionStatus
state={props.catalogState}
onPress={props.onOpenEnvironments}
variant="sidebar"
/>
</View>
) : null}
</View>
{connectionStatus}
</View>
);
}

const listHeader = (
<>
{Platform.OS === "ios" ? null : <HomeTopContentSpacer />}

{shouldShowConnectionStatus && Platform.OS === "ios" ? (
<View className="pb-4">
<WorkspaceConnectionStatus
state={props.catalogState}
onPress={props.onOpenEnvironments}
variant="sidebar"
/>
</View>
) : null}
</>
);
const listHeader = Platform.OS === "ios" ? null : <HomeTopContentSpacer />;

// Project scoping lives in the header filter menu (no inline chip row on
// mobile — the menu is the one filter surface).
Expand Down Expand Up @@ -1343,7 +1353,6 @@ export function HomeScreen(props: HomeScreenProps) {
onSettleThread={props.onSettleThread}
onUnsettleThread={props.onUnsettleThread}
/>
{connectionStatus}
</View>
);
}
Expand Down Expand Up @@ -1394,7 +1403,6 @@ export function HomeScreen(props: HomeScreenProps) {
}}
/>
</SwipeableScrollGateProvider>
{connectionStatus}
</View>
);
}
Expand Down Expand Up @@ -1465,7 +1473,6 @@ export function HomeScreen(props: HomeScreenProps) {
}
/>
</SwipeableScrollGateProvider>
{connectionStatus}
</View>
);
}
Loading
Loading