Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
efb7bfc
feat: add Hermes-powered T3 Work
maria-rcks Jul 27, 2026
6d62b74
fix: address PR review findings and hermes provider registry test
maria-rcks Jul 27, 2026
8a888b3
chore: retrigger CI (vp check stdout panic flake)
maria-rcks Jul 27, 2026
d8b4cda
fix: address second-round review findings (connection races, error ca…
maria-rcks Jul 27, 2026
06c8a2e
feat: add Hermes Skills management to T3 Work (capability-gated list/…
maria-rcks Jul 27, 2026
ce2c26d
refactor: hoist schema compiles and nested components, extract shared…
maria-rcks Jul 27, 2026
46d6fee
fix: address fourth-round review findings (catalog security gating, g…
maria-rcks Jul 27, 2026
5b7053b
fix: report disconnected on socket factory failure; distinct mutation…
maria-rcks Jul 27, 2026
5b7d978
fix: bound gateway socket open with a timeout; keep reconnecting stat…
maria-rcks Jul 27, 2026
5c06f50
fix: dispose failed connection attempts synchronously instead of rely…
maria-rcks Jul 27, 2026
a248d63
fix: gate optional Hermes capabilities (session.title, mutation.stabl…
maria-rcks Jul 27, 2026
ae135d7
fix: web UX for Hermes work mode (sidebar partitioning, mode-switch n…
maria-rcks Jul 27, 2026
c6aadb6
fix: import history hydration, historical timestamps, and profile-cor…
maria-rcks Jul 27, 2026
bbda2ad
fix: blur pin button after click so hover actions fade with the pointer
maria-rcks Jul 27, 2026
1f916d6
fix: accept Hermes category-map skills.manage list responses
maria-rcks Jul 27, 2026
b46d0c2
feat(hermes): full imported-history hydration with native activities …
maria-rcks Jul 27, 2026
89332ef
fix(hermes): historical settle timestamps for imports and history/liv…
maria-rcks Jul 27, 2026
771928f
fix(hermes): import hydration review findings
maria-rcks Jul 27, 2026
9e5dd5a
fix(hermes): round7 gateway client + cron review findings
maria-rcks Jul 27, 2026
bf97208
Fix round-7 server review findings
maria-rcks Jul 27, 2026
2b1a1bf
fix(client): round7 UX review fixes
maria-rcks Jul 27, 2026
d9b6ef6
fix(hermes): distinguish unreachable gateways from capability negotia…
devin-ai-integration[bot] Jul 27, 2026
47b418b
fix(web): remove duplicated useNewThreadHandler import and declaration
maria-rcks Jul 27, 2026
0c7cb97
test: extend migration contiguity to migration 049
maria-rcks Jul 27, 2026
8e64a2a
test(web): supply isProjectlessConversation in ThreadDetailsPanel fix…
maria-rcks Jul 27, 2026
22ce604
Fix round-8 review-bot findings
maria-rcks Jul 27, 2026
04445df
fix: preserve historical settledAt in V2 orchestrator, dedupe interim…
maria-rcks Jul 27, 2026
d474a26
fix: reset assistant text when deltas re-stream after a snapshot and …
maria-rcks Jul 27, 2026
e2ab2be
fix: address round-9 review findings (cron capability projection, imp…
maria-rcks Jul 27, 2026
76f018d
fix: propagate Hermes attachment refs, render assistant attachments, …
maria-rcks Jul 28, 2026
d5e28dd
fix: dedupe provider-hydrated duplicate rows in timeline, hide empty-…
maria-rcks Jul 28, 2026
8b4f455
fix: fail loudly on missing file attach reference, scope hydrated-row…
maria-rcks Jul 28, 2026
61d8b01
fix: drop wall-clock window from hydrated-row dedupe (hydrated timest…
maria-rcks Jul 28, 2026
f024045
fix: dedupe attachment-bearing hydrated echoes, merging attachments o…
maria-rcks Jul 28, 2026
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
38 changes: 37 additions & 1 deletion apps/mobile/src/components/ProviderIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useColorScheme } from "react-native";
import { Path, Svg } from "react-native-svg";
import { Circle, Path, Rect, Svg } from "react-native-svg";

type ProviderIconProps = {
readonly provider: string | null | undefined;
Expand All @@ -10,6 +10,42 @@ export function ProviderIcon(props: ProviderIconProps) {
const isDarkMode = useColorScheme() === "dark";
const size = props.size ?? 16;

if (props.provider === "hermes") {
const color = isDarkMode ? "#f4f4f5" : "#18181b";
return (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path fill={color} d="M4 4.5h3.2v5.8h9.6V4.5H20v15h-3.2v-6.1H7.2v6.1H4v-15Z" />
<Path
fill={color}
opacity={0.55}
d="M1.5 7.2 4 8.5v3L1.5 10V7.2Zm21 0L20 8.5v3l2.5-1.5V7.2Z"
/>
</Svg>
);
}

if (props.provider === "openclaw") {
return (
<Svg width={size} height={size} viewBox="0 0 32 32" fill="none">
<Rect width={32} height={32} rx={8} fill="#F04F3D" />
<Path
d="M12.2 14.6c-2.9-.1-5.2-1.8-5.9-4.5 1.7 0 3.2.6 4.2 1.7-.2-2 .5-3.8 2-5 1.1 2.4 1.2 5 .3 7.5m7.2.3c2.9-.1 5.2-1.8 5.9-4.5-1.7 0-3.2.6-4.2 1.7.2-2-.5-3.8-2-5-1.1 2.4-1.2 5-.3 7.5"
stroke="white"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M11.4 17.8c0-2.5 2.1-4.6 4.6-4.6s4.6 2.1 4.6 4.6v2.7c0 3.1-2 5.7-4.6 5.7s-4.6-2.6-4.6-5.7v-2.7Z"
fill="white"
/>
<Path d="M12 18.2h8m-8.5 3.6h9" stroke="#F04F3D" strokeWidth={1.3} strokeLinecap="round" />
<Circle cx={14.1} cy={16.7} r={0.85} fill="#F04F3D" />
<Circle cx={17.9} cy={16.7} r={0.85} fill="#F04F3D" />
</Svg>
);
}

if (props.provider === "claudeAgent") {
return (
<Svg width={size} height={size} viewBox="0 0 256 257" fill="none">
Expand Down
90 changes: 78 additions & 12 deletions apps/mobile/src/features/home/HomeHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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 type { MobileWorkspace } from "../../lib/mobileWorkspace";
import {
buildHomeListFilterMenu,
type HomeListFilterMenuEnvironment,
Expand All @@ -32,6 +33,7 @@ export type HomeHeaderEnvironment = HomeListFilterMenuEnvironment;

export function HomeHeader(props: {
readonly environments: ReadonlyArray<HomeHeaderEnvironment>;
readonly workspace: MobileWorkspace;
readonly projects: ReadonlyArray<HomeListFilterMenuProject>;
readonly searchQuery: string;
readonly selectedEnvironmentId: EnvironmentId | null;
Expand All @@ -40,6 +42,7 @@ export function HomeHeader(props: {
readonly threadSortOrder: SidebarThreadSortOrder;
readonly onSearchQueryChange: (query: string) => void;
readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void;
readonly onWorkspaceChange: (workspace: MobileWorkspace) => void;
readonly onProjectChange: (projectKey: string | null) => void;
readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void;
readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;
Expand All @@ -59,6 +62,23 @@ function checkedMenuState(checked: boolean) {
return checked ? ("on" as const) : undefined;
}

function workspaceMenuActions(props: HomeHeaderProps): MenuAction[] {
return [
{
id: "workspace:work",
title: "T3 Work",
subtitle: "Create, learn, and explore",
state: checkedMenuState(props.workspace === "work"),
},
{
id: "workspace:code",
title: "T3 Code",
subtitle: "Build, debug, and ship",
state: checkedMenuState(props.workspace === "code"),
},
];
}

/** Thread List v2 lays the list out in fixed creation order, so the
sort/group filter controls would be silently ignored — hide them and
key the "customized" icon state off the environment filter alone. */
Expand Down Expand Up @@ -148,6 +168,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
threadListV2Enabled,
],
);
const workspaceActions = useMemo(() => workspaceMenuActions(props), [props.workspace]);
const handleMenuAction = useCallback(
(event: { nativeEvent: { event: string } }) => {
const id = event.nativeEvent.event;
Expand Down Expand Up @@ -196,6 +217,13 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
},
[props],
);
const handleWorkspaceAction = useCallback(
({ nativeEvent }: { readonly nativeEvent: { readonly event: string } }) => {
if (nativeEvent.event === "workspace:work") props.onWorkspaceChange("work");
if (nativeEvent.event === "workspace:code") props.onWorkspaceChange("code");
},
[props],
);

return (
<>
Expand All @@ -208,18 +236,29 @@ 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">
{/* 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">
Alpha
<ControlPillMenu actions={workspaceActions} onPressAction={handleWorkspaceAction}>
<Pressable
accessibilityLabel={`Switch workspace. Current workspace: T3 ${props.workspace === "work" ? "Work" : "Code"}`}
accessibilityRole="button"
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">
{props.workspace === "work" ? "Work" : "Code"}
</RNText>
</View>
</View>
<SymbolView
name="chevron.down"
size={12}
tintColor={mutedColor}
type="monochrome"
/>
<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>
</Pressable>
</ControlPillMenu>

<ControlPillMenu
actions={menuActions}
Expand Down Expand Up @@ -308,7 +347,7 @@ function IosHomeHeader(props: HomeHeaderProps) {
return (
<>
<NativeStackScreenOptions
optionsVersion={filterMenu.items}
optionsVersion={[filterMenu.items, props.workspace]}
options={{
// Static header config (glass, title, fonts) lives in Stack.tsx
// (GLASS_HEADER_OPTIONS). Only dynamic values are set here.
Expand All @@ -329,6 +368,33 @@ function IosHomeHeader(props: HomeHeaderProps) {
unstable_headerToolbarItems:
Platform.OS === "ios"
? () => [
{
type: "menu" as const,
label: `T3 ${props.workspace === "work" ? "Work" : "Code"}`,
accessibilityLabel: `Switch workspace. Current workspace: T3 ${props.workspace === "work" ? "Work" : "Code"}`,
icon: { type: "sfSymbol", name: "chevron.up.chevron.down" } as const,
identifier: "home-workspace",
sharesBackground: false,
variant: "plain" as const,
menu: {
items: [
{
type: "action" as const,
label: "T3 Work",
description: "Create, learn, and explore",
onPress: () => props.onWorkspaceChange("work"),
state: props.workspace === "work" ? ("on" as const) : undefined,
},
{
type: "action" as const,
label: "T3 Code",
description: "Build, debug, and ship",
onPress: () => props.onWorkspaceChange("code"),
state: props.workspace === "code" ? ("on" as const) : undefined,
},
],
},
},
createNativeMailSearchToolbarItem({
composeButtonId: "home-new-task",
composeSystemImageName: "square.and.pencil",
Expand Down
88 changes: 73 additions & 15 deletions apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@ import * as Arr from "effect/Array";
import * as Order from "effect/Order";
import { useNavigation } from "@react-navigation/native";
import { useEffect, useMemo, useState } from "react";
import { Alert } from "react-native";

import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
import { useProjects, useThreadShells } from "../../state/entities";
import { useProjects, useServerConfigs, useThreadShells } from "../../state/entities";
import {
buildProviderDriverMap,
isHermesProviderInstance,
isMobileWorkspaceThread,
resolveHermesConversationTarget,
} from "../../lib/mobileWorkspace";
import { useMobileWorkspace } from "../../state/preferences";
import { usePendingNewTasks } from "../../state/use-pending-new-tasks";
import { useWorkspaceState } from "../../state/workspace";
import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry";
Expand All @@ -25,13 +33,35 @@ export function HomeRouteScreen() {
const { layout } = useAdaptiveWorkspaceLayout();
const projects = useProjects();
const threads = useThreadShells();
const serverConfigs = useServerConfigs();
const [workspace, setWorkspace] = useMobileWorkspace();
const providerDrivers = useMemo(() => buildProviderDriverMap(serverConfigs), [serverConfigs]);
const visibleThreads = useMemo(
() => threads.filter((thread) => isMobileWorkspaceThread(thread, workspace, providerDrivers)),
[providerDrivers, threads, workspace],
);
const { state: catalogState } = useWorkspaceState();
const { savedConnectionsById } = useSavedRemoteConnections();
const navigation = useNavigation();
const [searchQuery, setSearchQuery] = useState("");
const { archiveThread, confirmDeleteThread, settleThread, unsettleThread } =
useThreadListActions();
const pendingTasks = usePendingNewTasks();
const allPendingTasks = usePendingNewTasks();
const pendingTasks = useMemo(
() =>
workspace === "code"
? allPendingTasks
: allPendingTasks.filter(
(task) =>
task.message.modelSelection !== undefined &&
isHermesProviderInstance(
task.message.environmentId,
task.message.modelSelection.instanceId,
providerDrivers,
),
),
[allPendingTasks, providerDrivers, workspace],
);
const { openPendingTask, confirmDeletePendingTask } = usePendingTaskListActions();
const environments = useMemo(
() =>
Expand Down Expand Up @@ -79,6 +109,35 @@ export function HomeRouteScreen() {
setSelectedProjectKey(null);
}
}, [projectFilterOptions, selectedProjectKey]);
const startNewTask = () => {
if (workspace === "code") {
navigation.navigate("NewTaskSheet", { screen: "NewTask" });
return;
}
const target = resolveHermesConversationTarget({
projects,
serverConfigs,
requiredEnvironmentId: selectedEnvironmentId,
});
if (!target) {
Alert.alert(
"Hermes is not ready",
"Enable and configure Hermes on a connected environment before starting a Work conversation.",
);
return;
}
navigation.navigate("NewTaskSheet", {
screen: "NewTaskDraft",
params: {
environmentId: String(target.project.environmentId),
projectId: String(target.project.id),
title: "Hermes",
workspace: "work",
providerInstanceId: String(target.modelSelection.instanceId),
model: target.modelSelection.model,
},
});
};

// In split layouts the persistent sidebar IS the thread list — Home becomes
// an empty detail pane so selecting a thread never transitions layouts.
Expand All @@ -91,38 +150,36 @@ export function HomeRouteScreen() {
<NativeHeaderToolbar.Button
accessibilityLabel="New task"
icon="square.and.pencil"
onPress={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
onPress={startNewTask}
/>
}
/>
<WorkspaceEmptyDetail
onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
/>
<WorkspaceEmptyDetail onStartNewTask={startNewTask} />
</>
);
}

return (
<AndroidHomeFabLayout
onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
>
<AndroidHomeFabLayout onStartNewTask={startNewTask}>
<>
{/* Restore the compact title in case the split branch blanked it. */}
<NativeStackScreenOptions options={{ title: "Threads", headerTitle: "Threads" }} />
<HomeHeader
environments={environments}
projects={projectFilterOptions}
workspace={workspace}
projects={workspace === "work" ? [] : projectFilterOptions}
searchQuery={searchQuery}
selectedEnvironmentId={selectedEnvironmentId}
selectedProjectKey={selectedProjectKey}
selectedProjectKey={workspace === "work" ? null : selectedProjectKey}
projectSortOrder={listOptions.projectSortOrder}
threadSortOrder={listOptions.threadSortOrder}
onEnvironmentChange={setSelectedEnvironmentId}
onWorkspaceChange={setWorkspace}
onProjectChange={setSelectedProjectKey}
onOpenSettings={() => navigation.navigate("SettingsSheet", { screen: "Settings" })}
onProjectSortOrderChange={setProjectSortOrder}
onSearchQueryChange={setSearchQuery}
onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
onStartNewTask={startNewTask}
onThreadSortOrderChange={setThreadSortOrder}
/>

Expand Down Expand Up @@ -164,7 +221,7 @@ export function HomeRouteScreen() {
},
});
}}
onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
onStartNewTask={startNewTask}
onThreadSortOrderChange={setThreadSortOrder}
pendingTasks={pendingTasks}
projectGroupingMode={listOptions.projectGroupingMode}
Expand All @@ -173,8 +230,9 @@ export function HomeRouteScreen() {
savedConnectionsById={savedConnectionsById}
searchQuery={searchQuery}
selectedEnvironmentId={selectedEnvironmentId}
selectedProjectKey={selectedProjectKey}
threads={threads}
selectedProjectKey={workspace === "work" ? null : selectedProjectKey}
threads={visibleThreads}
workspace={workspace}
threadSortOrder={listOptions.threadSortOrder}
/>
</>
Expand Down
Loading
Loading