Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
837f6b8
feat(web): double-click chat header title to rename thread (#7817)
UtkarshUsername Aug 21, 2026
292c6dd
fix(web): model picker no longer shows a double border (#7772)
RakshithBhat03 Aug 21, 2026
9b5d416
fix(web): give sidebar un-settle button a tooltip (#7796)
UtkarshUsername Aug 21, 2026
c3e3709
fix(web): render oversized terminal graphemes without crashing (#7809)
Lucenx9 Aug 21, 2026
e0b4f46
feat(web): cmd+enter to create thread in background (#7821)
extoci Aug 21, 2026
b381fdb
fix(web): launcher shortcuts no longer hijack the empty composer (#7794)
Lucenx9 Aug 21, 2026
44e4a70
feat(desktop): choose external project icons (#7823)
Bil0000 Aug 21, 2026
592c598
perf(web): dedupe terminal mouse motion reports (#7845)
t3-code[bot] Aug 21, 2026
421088c
fix(search): oversized thread queries no longer crash clients (#6633)
Lucenx9 Aug 22, 2026
035058a
fix(mobile): stop a directly-saved backend from hiding its T3 Connect…
Rishet11 Aug 22, 2026
11f0513
feat(analytics): threads and turns now know which client started them…
t3dotgg Aug 22, 2026
ce91284
fix(web): stop marking mixed tool runs as failed (#7893)
t3dotgg Aug 22, 2026
f34b9d3
fix(web): command-click spaced folder links (#6439)
SunkenInTime Aug 22, 2026
2274444
fix(chat): stop pushing follow-up messages to the top (#7897)
t3dotgg Aug 22, 2026
0ede2ed
test(desktop): remove redundant release note assertion (#7873)
t3-code[bot] Aug 22, 2026
2c4158f
fix(web): handle wide ordered-list marker edge cases (#7856)
abcdmku Aug 22, 2026
49c2b44
fix(ssh): restore user PATH for remote servers (#7213)
gbarros-dev Aug 22, 2026
5a0063d
Merge remote-tracking branch 'upstream/main' into yordis/chore-sync-u…
yordis Aug 22, 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
28 changes: 28 additions & 0 deletions apps/desktop/src/electron/ElectronDialog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,34 @@ describe("ElectronDialog", () => {
}).pipe(Effect.provide(ElectronDialog.layer)),
);

it.effect("opens a single-file picker when multiple selections are disabled", () =>
Effect.gen(function* () {
showOpenDialogMock.mockResolvedValue({
canceled: false,
filePaths: ["/pictures/icon.png"],
});
const dialog = yield* ElectronDialog.ElectronDialog;

const paths = yield* dialog.pickFiles({
owner: Option.none(),
defaultPath: Option.some("/project"),
filters: [{ name: "Images", extensions: ["png"] }],
multiple: false,
});

assert.deepEqual(paths, ["/pictures/icon.png"]);
assert.deepEqual(showOpenDialogMock.mock.calls, [
[
{
defaultPath: "/project",
filters: [{ name: "Images", extensions: ["png"] }],
properties: ["openFile"],
},
],
]);
}).pipe(Effect.provide(ElectronDialog.layer)),
);

it.effect("preserves message box request context and cause", () =>
Effect.gen(function* () {
const cause = new Error("message box failed");
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/electron/ElectronDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export interface ElectronDialogPickFilesInput {
readonly owner: Option.Option<Electron.BrowserWindow>;
readonly defaultPath: Option.Option<string>;
readonly filters: readonly Electron.FileFilter[];
readonly multiple: boolean;
}

export class ElectronDialog extends Context.Service<
Expand Down Expand Up @@ -144,7 +145,7 @@ export const make = ElectronDialog.of({
});
const defaultPath = Option.getOrNull(input.defaultPath);
const openDialogOptions: Electron.OpenDialogOptions = {
properties: ["openFile", "multiSelections"],
properties: input.multiple ? ["openFile", "multiSelections"] : ["openFile"],
filters: [...input.filters],
...(defaultPath === null ? {} : { defaultPath }),
};
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
openExternal,
probeRemoteEditors,
pickFolder,
pickProjectFavicon,
pickThemeFiles,
setTheme,
showContextMenu,
Expand Down Expand Up @@ -82,6 +83,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(setWslOnly);

yield* ipc.handle(pickFolder);
yield* ipc.handle(pickProjectFavicon);
yield* ipc.handle(pickThemeFiles);
yield* ipc.handle(setTheme);
yield* ipc.handle(showContextMenu);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const PICK_FOLDER_CHANNEL = "desktop:pick-folder";
export const PICK_PROJECT_FAVICON_CHANNEL = "desktop:pick-project-favicon";
export const PICK_THEME_FILES_CHANNEL = "desktop:pick-theme-files";
export const SET_THEME_CHANNEL = "desktop:set-theme";
export const CONTEXT_MENU_CHANNEL = "desktop:context-menu";
Expand Down
43 changes: 42 additions & 1 deletion apps/desktop/src/ipc/methods/window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import { assert, describe, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import { vi } from "vite-plus/test";

import type * as Electron from "electron";

import * as DesktopBackendManager from "../../backend/DesktopBackendManager.ts";
import * as DesktopBackendPool from "../../backend/DesktopBackendPool.ts";
import * as ElectronDialog from "../../electron/ElectronDialog.ts";
import * as ElectronWindow from "../../electron/ElectronWindow.ts";
import { getLocalEnvironmentBootstraps, getWindowFullscreenState } from "./window.ts";
import {
getLocalEnvironmentBootstraps,
getWindowFullscreenState,
pickProjectFavicon,
} from "./window.ts";

const readyWslConfig: DesktopBackendManager.DesktopBackendStartConfig = {
executablePath: "wsl.exe",
Expand Down Expand Up @@ -146,3 +152,38 @@ describe("getWindowFullscreenState", () => {
);
});
});

describe("pickProjectFavicon", () => {
it.effect("opens a single-image picker from the project directory", () =>
Effect.gen(function* () {
const pickFiles = vi.fn(() => Effect.succeed(["/pictures/icon.png"]));
const result = yield* pickProjectFavicon.handler("/project").pipe(
Effect.provide(
Layer.mergeAll(
Layer.mock(ElectronDialog.ElectronDialog)({ pickFiles }),
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
);

assert.strictEqual(result, "/pictures/icon.png");
assert.deepEqual(pickFiles.mock.calls, [
[
{
owner: Option.none(),
defaultPath: Option.some("/project"),
multiple: false,
filters: [
{
name: "Images",
extensions: ["avif", "gif", "ico", "jpeg", "jpg", "png", "svg", "webp"],
},
],
},
],
]);
}),
);
});
24 changes: 24 additions & 0 deletions apps/desktop/src/ipc/methods/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type DesktopEnvironmentBootstrap,
type PickedThemeFile,
} from "@t3tools/contracts";
import { WORKSPACE_IMAGE_PREVIEW_EXTENSIONS } from "@t3tools/shared/filePreview";
import { isCommandAvailable } from "@t3tools/shared/shell";
import * as NodeOS from "node:os";
import * as FileSystem from "effect/FileSystem";
Expand Down Expand Up @@ -234,6 +235,28 @@ export const pickFolder = DesktopIpc.makeIpcMethod({
}),
});

export const pickProjectFavicon = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PICK_PROJECT_FAVICON_CHANNEL,
payload: Schema.UndefinedOr(Schema.String),
result: Schema.NullOr(Schema.String),
handler: Effect.fn("desktop.ipc.window.pickProjectFavicon")(function* (initialPath) {
const dialog = yield* ElectronDialog.ElectronDialog;
const electronWindow = yield* ElectronWindow.ElectronWindow;
const paths = yield* dialog.pickFiles({
owner: yield* electronWindow.focusedMainOrFirst,
defaultPath: Option.fromNullishOr(initialPath),
multiple: false,
filters: [
{
name: "Images",
extensions: WORKSPACE_IMAGE_PREVIEW_EXTENSIONS.map((extension) => extension.slice(1)),
},
],
});
return paths[0] ?? null;
}),
});

export const setTheme = DesktopIpc.makeIpcMethod({
channel: IpcChannels.SET_THEME_CHANNEL,
payload: DesktopThemeSchema,
Expand Down Expand Up @@ -323,6 +346,7 @@ export const pickThemeFiles = DesktopIpc.makeIpcMethod({
owner: yield* electronWindow.focusedMainOrFirst,
defaultPath: defaultPath ? Option.some(extensionsDir) : Option.none(),
filters: [{ name: "JSON", extensions: ["json"] }],
multiple: true,
});
if (paths.length === 0) {
return null;
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ contextBridge.exposeInMainWorld("desktopBridge", {
return null;
}
},
pickProjectFavicon: (initialPath) =>
ipcRenderer.invoke(IpcChannels.PICK_PROJECT_FAVICON_CHANNEL, initialPath),
pickThemeFiles: () => ipcRenderer.invoke(IpcChannels.PICK_THEME_FILES_CHANNEL, undefined),
setTheme: (theme) => ipcRenderer.invoke(IpcChannels.SET_THEME_CHANNEL, theme),
showContextMenu: (items, position) =>
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/updates/releaseNotes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,6 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
});

it("does not throw on out-of-range numeric entities and keeps the literal", () => {
expect(() =>
normalizeDesktopUpdateReleaseNotes("- Broken entity &#9999999999;", "1.0.0"),
).not.toThrow();
const notes = normalizeDesktopUpdateReleaseNotes("- Broken entity &#9999999999;", "1.0.0");
expect(notes).toEqual([{ version: "1.0.0", items: ["Broken entity &#9999999999;"] }]);
});
Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/src/connection/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Queue from "effect/Queue";
import * as Stream from "effect/Stream";
import Constants from "expo-constants";
import * as Network from "expo-network";
import { AppState } from "react-native";

Expand Down Expand Up @@ -166,7 +167,7 @@ const capabilitiesLayer = Layer.effectContext(
Context.add(
ClientPresentation,
ClientPresentation.of({
metadata: authClientMetadata(),
metadata: authClientMetadata(Constants.expoConfig?.version),
scopes: AuthStandardClientScopes,
}),
),
Expand Down
31 changes: 30 additions & 1 deletion apps/mobile/src/features/connection/environmentSections.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { EnvironmentId } from "@t3tools/contracts";
import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay";
import { describe, expect, it } from "vite-plus/test";
import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-types";
import { splitEnvironmentSections } from "./environmentSections";
import { relayManagedEnvironmentIds, splitEnvironmentSections } from "./environmentSections";

function connectedEnvironment(
input: Omit<Partial<ConnectedEnvironmentSummary>, "environmentId"> & {
Expand Down Expand Up @@ -34,6 +34,17 @@ function cloudEnvironment(environmentId: string): RelayClientEnvironmentRecord {
};
}

describe("relayManagedEnvironmentIds", () => {
it("leaves out a backend that was saved directly", () => {
const ids = relayManagedEnvironmentIds([
connectedEnvironment({ environmentId: "environment-local", isRelayManaged: false }),
connectedEnvironment({ environmentId: "environment-cloud", isRelayManaged: true }),
]);

expect([...ids]).toEqual([EnvironmentId.make("environment-cloud")]);
});
});

describe("mobile environment settings sections", () => {
it("keeps saved relay-managed connections under T3 Connect", () => {
const local = connectedEnvironment({
Expand Down Expand Up @@ -111,6 +122,24 @@ describe("mobile environment settings sections", () => {
expect(sections.availableCloudEnvironments).toEqual([]);
});

it("still offers a cloud environment saved directly as a local backend", () => {
const local = connectedEnvironment({
environmentId: "environment-cloud",
isRelayManaged: false,
});

const sections = splitEnvironmentSections({
connectedEnvironments: [local],
cloudEnvironments: [cloudEnvironment("environment-cloud")],
});

expect(sections.localEnvironments).toEqual([local]);
expect(sections.connectedCloudEnvironments).toEqual([]);
expect(
sections.availableCloudEnvironments.map((environment) => environment.environmentId),
).toEqual([EnvironmentId.make("environment-cloud")]);
});

it("keeps failed relay environments in the local connection row", () => {
const cloud = connectedEnvironment({
environmentId: "environment-cloud",
Expand Down
22 changes: 19 additions & 3 deletions apps/mobile/src/features/connection/environmentSections.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EnvironmentId } from "@t3tools/contracts";
import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay";
import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-types";

Expand All @@ -12,10 +13,25 @@ export interface EnvironmentSections {
readonly availableCloudEnvironments: ReadonlyArray<RelayClientEnvironmentRecord>;
}

export function splitEnvironmentSections(input: EnvironmentSectionsInput): EnvironmentSections {
const savedEnvironmentIds = new Set(
input.connectedEnvironments.map((environment) => environment.environmentId),
/**
* Ids of the environments that already occupy a T3 Connect slot. A backend saved directly is
* not one of them, so it must not suppress the cloud environment that happens to share its id.
*/
export function relayManagedEnvironmentIds(
environments: ReadonlyArray<{
readonly environmentId: EnvironmentId;
readonly isRelayManaged: boolean;
}>,
): ReadonlySet<EnvironmentId> {
return new Set(
environments
.filter((environment) => environment.isRelayManaged)
.map((environment) => environment.environmentId),
);
}

export function splitEnvironmentSections(input: EnvironmentSectionsInput): EnvironmentSections {
const savedEnvironmentIds = relayManagedEnvironmentIds(input.connectedEnvironments);

return {
localEnvironments: input.connectedEnvironments.filter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useEnvironments } from "../../state/environments";
import { relayEnvironmentDiscovery } from "../../state/relay";
import { useAtomCommand } from "../../state/use-atom-command";
import { projectWorkspaceEnvironment, type WorkspaceEnvironment } from "../../state/workspaceModel";
import { relayManagedEnvironmentIds } from "./environmentSections";

export interface RelayEnvironmentView {
readonly environment: RelayClientEnvironmentRecord;
Expand Down Expand Up @@ -49,7 +50,7 @@ export function useConnectionController() {
[environments],
);
const registeredIds = useMemo(
() => new Set(connectedEnvironments.map((environment) => environment.environmentId)),
() => relayManagedEnvironmentIds(connectedEnvironments),
[connectedEnvironments],
);
const relayEnvironments = useMemo<ReadonlyArray<RelayEnvironmentView>>(
Expand Down
Loading
Loading