Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
89ee692
fix(web): persist diff view mode (#5731)
leorivastech Aug 8, 2026
c2f8cb7
feat(web): show how many subagents are running at a glance (#5745)
t3dotgg Aug 8, 2026
be01b28
fix(web): add missing cursor-pointer styling to dropdowns and interac…
naMqe-h Aug 9, 2026
e70cdb4
fix(server): stop Claude resume handshakes from completing turns that…
gfsaaser24 Aug 9, 2026
49964e3
chore: vouch gfsaaser24 (#5761)
t3dotgg Aug 9, 2026
7b2cf43
chore: vouch saphid (#5763)
t3dotgg Aug 9, 2026
89c320d
fix(server): stop Codex threads with queued follow-ups (#5762)
t3dotgg Aug 9, 2026
70c423a
fix(web): usage page loses the cost quality panel, gains a back butto…
t3dotgg Aug 9, 2026
a6c9b41
feat(server): agents can now open the images you paste into chat (#5757)
t3dotgg Aug 9, 2026
5208bde
fix(web): pinned reorder no longer reshuffles while writes land (#5767)
t3dotgg Aug 9, 2026
288d8e3
feat(web): overhaul project settings into a real settings page (#5768)
t3dotgg Aug 9, 2026
886195e
fix(web): usage totals no longer jump while devices report in (#5772)
t3dotgg Aug 9, 2026
5bb8c03
fix(server): settle no longer leaves monitors and dev servers running…
t3dotgg Aug 9, 2026
6dbffa0
feat: pick worktree or current checkout per project (#5766)
t3dotgg Aug 9, 2026
6f69b44
fix(web): sidebar rows show the branch again, not a truncated plan st…
t3dotgg Aug 9, 2026
ddaa6af
feat(server): vp run migrate-dev-db seeds worktree dev dbs with real …
t3dotgg Aug 9, 2026
05eb051
feat(web): keep unsent drafts one click away in the sidebar (#5777)
t3dotgg Aug 9, 2026
076e904
feat(web): project icons can be chosen manually (#5775)
t3dotgg Aug 9, 2026
ba9c9ae
fix(server): one greedy agent process no longer takes down the whole …
t3dotgg Aug 9, 2026
be8393d
merge: sync upstream through ba9c9ae81
omegent-app[bot] Aug 9, 2026
885b287
fix: repair the merge welds both adversarial reviews found
omegent-app[bot] Aug 9, 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Full docs live in [docs/](./docs). There's no docs site yet.
- [Install and first run](./docs/user/install.md)
- [Permission modes](./docs/user/permission-modes.md)
- [Keyboard shortcuts](./docs/user/keybindings.md)
- [Customize a project icon](./docs/user/project-settings.md)
- [Remote access from a phone or another machine](./docs/user/remote-access.md)
- [Keeping app and server in sync](./docs/user/updating.md)
- [Source control integrations](./docs/user/source-control.md)
Expand Down
64 changes: 51 additions & 13 deletions apps/mobile/src/components/ProjectFavicon.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { SymbolView } from "./AppSymbol";
import { Image } from "expo-image";
import { useState } from "react";
import { useLayoutEffect, useMemo, useState } from "react";
import { View } from "react-native";
import type { EnvironmentId } from "@t3tools/contracts";
import { isProjectFaviconFallbackUrl } from "@t3tools/shared/projectFavicon";
import {
getProjectFaviconCacheKey,
isProjectFaviconFallbackUrl,
} from "@t3tools/shared/projectFavicon";
import { useThemeColor } from "../lib/useThemeColor";
import { useAssetUrl } from "../state/assets";

/* ─── Favicon cache (matches web pattern) ────────────────────────────── */
const loadedFaviconUrls = new Set<string>();
import {
beginProjectFaviconRequest,
createProjectFaviconRequest,
hasLoadedProjectFavicon,
markProjectFaviconFailed,
markProjectFaviconLoaded,
} from "./projectFaviconCache";

/* ─── Component ──────────────────────────────────────────────────────── */
export function ProjectFavicon(props: {
Expand All @@ -17,19 +24,29 @@ export function ProjectFavicon(props: {
readonly size?: number;
readonly projectTitle: string;
readonly workspaceRoot?: string | null;
readonly faviconPath?: string | null;
}) {
const size = props.size ?? 42;
const faviconUrl = useAssetUrl(
props.environmentId,
props.workspaceRoot === null || props.workspaceRoot === undefined
? null
: { _tag: "project-favicon", cwd: props.workspaceRoot },
: {
_tag: "project-favicon",
cwd: props.workspaceRoot,
...(props.faviconPath ? { path: props.faviconPath } : {}),
},
);
const renderableFaviconUrl = isProjectFaviconFallbackUrl(faviconUrl) ? null : faviconUrl;
const cacheKey =
renderableFaviconUrl && props.workspaceRoot
? getProjectFaviconCacheKey(props.environmentId, props.workspaceRoot, renderableFaviconUrl)
: null;

return (
<ProjectFaviconImage
key={faviconUrl}
key={cacheKey}
cacheKey={cacheKey}
faviconUrl={renderableFaviconUrl}
open={props.open}
projectTitle={props.projectTitle}
Expand All @@ -39,18 +56,32 @@ export function ProjectFavicon(props: {
}

function ProjectFaviconImage(props: {
readonly cacheKey: string | null;
readonly faviconUrl: string | null;
readonly open?: boolean;
readonly projectTitle: string;
readonly size: number;
}) {
const iconMuted = useThemeColor("--color-icon-subtle");
const faviconRequest = useMemo(
() => createProjectFaviconRequest(props.cacheKey, props.faviconUrl),
[props.cacheKey, props.faviconUrl],
);
const [activeFaviconRequest, setActiveFaviconRequest] = useState<typeof faviconRequest>(null);
useLayoutEffect(() => {
if (faviconRequest === null) return;

const endRequest = beginProjectFaviconRequest(faviconRequest);
setActiveFaviconRequest(faviconRequest);
return endRequest;
}, [faviconRequest]);

const [status, setStatus] = useState<"loading" | "loaded" | "error">(() =>
props.faviconUrl && loadedFaviconUrls.has(props.faviconUrl) ? "loaded" : "loading",
hasLoadedProjectFavicon(props.cacheKey) ? "loaded" : "loading",
);

const showImage = props.faviconUrl !== null && status === "loaded";
const requestIsActive = faviconRequest !== null && activeFaviconRequest === faviconRequest;
const showImage = requestIsActive && status === "loaded";

return (
<View
Expand All @@ -72,11 +103,15 @@ function ProjectFaviconImage(props: {
) : null}

{/* Favicon image (hidden until loaded) */}
{props.faviconUrl ? (
{requestIsActive ? (
<Image
key={faviconRequest.faviconUrl}
source={{
uri: props.faviconUrl,
uri: faviconRequest.faviconUrl,
cacheKey: faviconRequest.cacheKey,
}}
cachePolicy="memory-disk"
recyclingKey={faviconRequest.cacheKey}
accessibilityLabel={`${props.projectTitle} favicon`}
style={{
width: props.size,
Expand All @@ -86,10 +121,13 @@ function ProjectFaviconImage(props: {
}}
contentFit="contain"
onLoad={() => {
if (props.faviconUrl) loadedFaviconUrls.add(props.faviconUrl);
if (!markProjectFaviconLoaded(faviconRequest)) return;
setStatus("loaded");
}}
onError={() => setStatus("error")}
onError={() => {
if (!markProjectFaviconFailed(faviconRequest)) return;
setStatus("error");
}}
/>
) : null}
</View>
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ function ProjectGroupLabel(props: {
<View className="flex-row items-center gap-2.5 px-1 pb-2">
<ProjectFavicon
environmentId={props.project.environmentId}
faviconPath={props.project.faviconPath}
projectTitle={props.project.title}
size={18}
workspaceRoot={props.project.workspaceRoot}
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/features/threads/NewTaskRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara
<View className="h-7 w-7 items-center justify-center">
<ProjectFavicon
environmentId={scope.representative.environmentId}
faviconPath={scope.representative.faviconPath}
size={20}
projectTitle={scope.title}
workspaceRoot={scope.representative.workspaceRoot}
Expand Down Expand Up @@ -330,6 +331,7 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara
>
<ProjectFavicon
environmentId={project.environmentId}
faviconPath={project.faviconPath}
size={18}
projectTitle={project.title}
workspaceRoot={project.workspaceRoot}
Expand Down
58 changes: 51 additions & 7 deletions apps/mobile/src/features/threads/new-task-flow-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import type {
EnvironmentId,
ModelSelection,
ProjectReadFileResult,
ProviderInteractionMode,
ProviderOptionSelection,
RuntimeMode,
Expand All @@ -14,8 +15,14 @@ import {
DEFAULT_RUNTIME_MODE,
DEFAULT_SERVER_SETTINGS,
MessageId,
T3_PROJECT_FILE_NAME,
ThreadId,
} from "@t3tools/contracts";
import { parseT3ProjectFile } from "@t3tools/shared/t3ProjectFile";
import {
isDefaultThreadEnvModeSettled,
resolveDefaultThreadEnvMode,
} from "@t3tools/shared/threadEnvMode";
import * as Arr from "effect/Array";
import { pipe } from "effect/Function";

Expand All @@ -31,6 +38,8 @@ import {
} from "../../lib/modelOptions";
import { scopedProjectKey } from "../../lib/scopedEntities";
import { appAtomRegistry } from "../../state/atom-registry";
import { projectEnvironment } from "../../state/projects";
import { useEnvironmentQuery } from "../../state/query";
import {
appendComposerDraftAttachments,
clearComposerDraft,
Expand Down Expand Up @@ -346,11 +355,35 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
const selectedProjectDraft = useComposerDraft(selectedProjectDraftKey);
const prompt = selectedProjectDraft.text;
const attachments = selectedProjectDraft.attachments;
// The server's configured default decides the mode until the user picks one
// explicitly — same resolution web uses for new draft threads.
const defaultWorkspaceMode: WorkspaceMode =
selectedEnvironmentServerConfig?.settings.defaultThreadEnvMode ??
DEFAULT_SERVER_SETTINGS.defaultThreadEnvMode;
// Default mode until the user picks one explicitly — same resolution web
// uses for new draft threads: per-project setting, then the repo's
// checked-in t3.json, then the server's configured default.
const t3ProjectFileQuery = useEnvironmentQuery(
selectedProject !== null && selectedProject.workspaceRoot !== ""
? projectEnvironment.readFile({
environmentId: selectedProject.environmentId,
input: { cwd: selectedProject.workspaceRoot, relativePath: T3_PROJECT_FILE_NAME },
})
: null,
);
const t3ProjectFileData = t3ProjectFileQuery.data as ProjectReadFileResult | null;
const t3ProjectFileDefaultMode = useMemo(() => {
if (t3ProjectFileData === null || t3ProjectFileData.truncated) return null;
return parseT3ProjectFile(t3ProjectFileData.contents)?.defaultThreadEnvMode ?? null;
}, [t3ProjectFileData]);
const defaultWorkspaceMode: WorkspaceMode = resolveDefaultThreadEnvMode({
projectSetting: selectedProject?.defaultThreadEnvMode,
projectFile: t3ProjectFileDefaultMode,
globalDefault: selectedEnvironmentServerConfig?.settings.defaultThreadEnvMode ?? "local",
});
// While unsettled the resolved default is provisional. Nothing may write
// it into the draft during that window (the auto-branch effect does), or
// the frozen interim value beats the t3.json default once it loads.
const defaultWorkspaceModeSettled = isDefaultThreadEnvModeSettled({
explicitMode: selectedProjectDraft.workspaceSelection?.mode,
projectSetting: selectedProject?.defaultThreadEnvMode,
projectFilePending: t3ProjectFileQuery.isPending,
});
const workspaceMode = selectedProjectDraft.workspaceSelection?.mode ?? defaultWorkspaceMode;
const selectedBranchName = selectedProjectDraft.workspaceSelection?.branch ?? null;
const selectedWorktreePath = selectedProjectDraft.workspaceSelection?.worktreePath ?? null;
Expand Down Expand Up @@ -620,7 +653,11 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
}, [refreshBranches, selectedProject]);

useEffect(() => {
if (workspaceMode !== "worktree" || selectedBranchName !== null) {
if (
!defaultWorkspaceModeSettled ||
workspaceMode !== "worktree" ||
selectedBranchName !== null
) {
return;
}
// The default may only exist as origin/<default> (isRemote), which
Expand All @@ -632,7 +669,14 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
if (preferredBranch) {
selectBranch(preferredBranch);
}
}, [allBranchRefs, availableBranches, selectBranch, selectedBranchName, workspaceMode]);
}, [
allBranchRefs,
availableBranches,
defaultWorkspaceModeSettled,
selectBranch,
selectedBranchName,
workspaceMode,
]);

const setRuntimeMode = useCallback(
(value: RuntimeMode) => {
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/threads/thread-list-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export const ThreadListGroupHeader = memo(function ThreadListGroupHeader(props:
>
<ProjectFavicon
environmentId={props.project.environmentId}
faviconPath={props.project.faviconPath}
open={!props.collapsed}
size={compact ? 22 : 18}
projectTitle={props.project.title}
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/src/features/threads/thread-list-v2-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ export const ThreadListV2PendingRow = memo(function ThreadListV2PendingRow(props
{props.project ? (
<ProjectFavicon
environmentId={pendingTask.message.environmentId}
faviconPath={props.project.faviconPath}
size={15}
projectTitle={projectTitle}
workspaceRoot={props.project.workspaceRoot}
Expand Down Expand Up @@ -631,6 +632,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
{props.project ? (
<ProjectFavicon
environmentId={thread.environmentId}
faviconPath={props.project.faviconPath}
size={15}
projectTitle={props.projectTitle ?? props.project.title}
workspaceRoot={props.project.workspaceRoot}
Expand Down Expand Up @@ -836,6 +838,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
<View className="opacity-40">
<ProjectFavicon
environmentId={thread.environmentId}
faviconPath={props.project.faviconPath}
size={15}
projectTitle={props.projectTitle ?? props.project.title}
workspaceRoot={props.project.workspaceRoot}
Expand Down
2 changes: 2 additions & 0 deletions apps/server/integration/providerService.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
ProviderService,
type ProviderServiceShape,
} from "../src/provider/Services/ProviderService.ts";
import * as ServerConfig from "../src/config.ts";
import { ServerSettingsService } from "../src/serverSettings.ts";
import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts";
import { SqlitePersistenceMemory } from "../src/persistence/Layers/Sqlite.ts";
Expand Down Expand Up @@ -93,6 +94,7 @@ const makeIntegrationFixture = (options?: { readonly analytics?: Layer.Layer<Ana
const shared = Layer.mergeAll(
directoryLayer,
Layer.succeed(ProviderAdapterRegistry, registry),
ServerConfig.layerTest(cwd, cwd).pipe(Layer.provide(NodeServices.layer)),
ServerSettingsService.layerTest(DEFAULT_SERVER_SETTINGS),
options?.analytics ?? AnalyticsService.layerTest,
Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers),
Expand Down
Loading
Loading