Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
44a8e95
fix(web): model settings no longer re-render every row on each star, …
juliusmarminge Nov 14, 2026
3ea253f
fix(server): suppress redundant keybindings change emission on unchan…
playlikeboy Nov 14, 2026
4c76857
fix(web): clicking a label in an inline HTML render no longer jumps t…
juliusmarminge Nov 14, 2026
37714a8
perf(desktop): revealing a file on Windows no longer waits on PowerSh…
lyn1dev Nov 14, 2026
0aeb945
fix(acp): preserve Windows shim stdio pipes (#30544)
ElliotDrel Nov 14, 2026
fcf50ff
fix(connect): T3 Connect recovers when its tunnel is routed to a dead…
ishaanko Nov 14, 2026
6bc105f
fix(editors): find JetBrains IDEs whose Toolbox script is named idea1…
only21mil Nov 14, 2026
580b526
fix(server): respect the default Windows file manager (#28205)
prov3ntus Nov 14, 2026
34cf030
fix(web): pull requests error names the environment that failed (#27882)
juliusmarminge Nov 14, 2026
eea9d15
fix(desktop): Windows folders dropped on a WSL backend insert their W…
juliusmarminge Nov 14, 2026
be78ee4
fix(server): stopping an agent's server no longer wakes Codex as if i…
juliusmarminge Nov 14, 2026
d9619ad
fix(web): stop offering T3 Connect routes for the desktop's own WSL b…
juliusmarminge Nov 14, 2026
a472342
fix(web): screen readers no longer say "Message" twice entering the c…
nibarAhmed Nov 14, 2026
a35fd81
fix(server): settlement sweep reads quiet open pull requests every 5 …
juliusmarminge Nov 14, 2026
31d9b3b
fix(web): edited HTML previews outside the workspace no longer turn w…
sti0 Nov 14, 2026
ddc3652
fix(forgejo): the fj sign-in hint points subpath servers to tea (#36290)
shailist Nov 14, 2026
62ba72f
fix(web): onboarding rechecks an agent when its install command finis…
artbom Nov 14, 2026
bd9ff7e
fix(client-runtime): recover thread list updates after stream defects…
shivamhwp Nov 14, 2026
2dd6cdd
fix(shared): @ mentions complete display names in any script (#36504)
JS-coder-K Nov 14, 2026
56fd98b
perf(web): batch first-send composer updates (#36166)
StiensWout Nov 14, 2026
9803d1a
fix(web): paint the selected environment theme from the first frame o…
darjss Nov 14, 2026
2c61677
fix(web): overlays close on a click over the desktop header (#36250)
rgrvlsk Nov 14, 2026
ea957b5
fix(server): give MCP clients an absolute attachment upload URL (#35907)
LucaDOrso Nov 14, 2026
4a53d80
fix(server): database folders on btrfs are No_COW so SQLite stops fra…
juliusmarminge Nov 14, 2026
7847ec9
fix(web): preserve queued project settings changes (#35741)
saphid Nov 14, 2026
8656cdb
fix(web): reply labels no longer cut an emoji in half (#35701)
ck0i Nov 14, 2026
388424d
fix(shared): capture shell values after profiles replace PATH (#35617)
Lucenx9 Nov 14, 2026
e1f5b68
fix(server): MCP tools say why the orchestrator refused (#36083)
SouthCarpet Nov 14, 2026
5ef603d
fix(web): remove routine success notifications (#36527)
Yash-Singh1 Nov 14, 2026
9d6da45
fix(server): idle delegated tasks when recovery holds follow-ups (#36…
Yash-Singh1 Nov 14, 2026
058eea9
fix(pr): hide unsupported GitCafe review reactions (#36171)
saphitv Nov 14, 2026
0516493
fix(web): align status text and tighten pull request menu spacing (#3…
Yash-Singh1 Nov 14, 2026
d899bdd
fix(gitlab): long merge request descriptions no longer break the list…
fixfon Nov 14, 2026
4c47315
fix(server): overlapping VCS status refreshes share one follow-up ref…
only21mil Nov 14, 2026
25c5032
fix(devices): require a complete Android SDK on SSH hosts (#24566)
juliusmarminge Nov 14, 2026
6d94997
fix(opencode): support explicit background waits (#35272)
AndreiShenets Nov 14, 2026
aecad62
fix(opencode): a failed steer no longer leaves the turn running forev…
only21mil Nov 14, 2026
033bb5a
fix(ssh): failed launches report only their own output (#35717)
saphid Nov 14, 2026
83d2382
fix(opencode): OpenCode 1.x background subagents no longer show as do…
Mach1212 Nov 14, 2026
99071c4
fix(codex): delegate_task children start as Codex subagents (#36525)
juliusmarminge Nov 14, 2026
8247952
fix(marketing): route mobile downloads to app stores (#24577)
juliusmarminge Nov 14, 2026
cee16bd
fix(desktop): start WSL backends in Linux home (#35719)
satyalyadav Nov 14, 2026
96051c6
fix(server): prevent legacy terminal history collisions across thread…
juliusmarminge Nov 14, 2026
cf60628
fix(client-runtime): cache running threads when their view closes (#2…
maria-rcks Nov 14, 2026
0893180
fix(server): report the actual preview snapshot timeout (#24130)
Wosca Nov 14, 2026
a81ec88
fix(clients): allow portable provider handoffs after detach (#24641)
juliusmarminge Nov 14, 2026
bf9b8b7
fix(server): Claude on Bedrock runs the model picked in the composer …
only21mil Nov 14, 2026
70be204
fix: re-ticking a file after a push no longer shows it Viewed (#24151)
only21mil Nov 14, 2026
1ed6e32
fix(ssh): ignore alias commands and TTY requests for T3 connections (…
juliusmarminge Nov 14, 2026
fe873f8
fix(web): confirm before deleting scheduled tasks (#23578)
only21mil Nov 14, 2026
a8e60d4
fix(release): require complete updater manifests (#23700)
parmartejass Nov 14, 2026
ac18091
fix(web): seeded models yield to started thread selections (#24619)
juliusmarminge Nov 14, 2026
33372a2
fix(pi): info notifications no longer render as warnings (#23506)
0xCUB3 Nov 14, 2026
ca7a944
fix(client-runtime): Reconnect works while the network reports offlin…
only21mil Nov 14, 2026
294e061
fix(shared): IPv6 SCP remotes match SSH repository identities (#36547)
StiensWout Nov 14, 2026
5abf383
fix(server): prevent lossy edits of non-UTF-8 workspace files (#36545)
StiensWout Nov 14, 2026
a760a3a
fix(desktop): keyboard-opened message link menus select their first i…
only21mil Nov 14, 2026
e80468b
fix(server): report invalid provider events without a second schema f…
StiensWout Nov 14, 2026
a3477a5
fix(server): branch refresh no longer waits for detailed Git status (…
MINIlaolao Nov 14, 2026
1f55acf
fix(client-runtime): refresh settled counts when threads move project…
StiensWout Nov 14, 2026
00ec286
fix(mobile): adaptive SVG project icons follow the app's color scheme…
tomhmoses Nov 14, 2026
efae4e9
fix(client): retry unread thread visits after reconnect (#36559)
StiensWout Nov 14, 2026
46b508d
fix(web): keep long ACP registry searches within the query limit (#23…
only21mil Nov 14, 2026
248f5b6
fix(preview): browser tabs follow the default appearance (#24658)
juliusmarminge Nov 14, 2026
d530d96
fix(desktop): inherit login-shell Android SDK selection (#24623)
juliusmarminge Nov 14, 2026
a8777e4
fix(server): held agent messages no longer block settling (#24891)
mkantautas Nov 14, 2026
9e180e5
fix(server): file editor saves keep a UTF-8 BOM (#36552)
StiensWout Nov 14, 2026
86d49da
fix(server): open stable Zed when a prerelease Zed is first on PATH (…
only21mil Nov 14, 2026
b2259d3
fix(web): show working state while provider sessions start (#23590)
only21mil Nov 14, 2026
7bf7527
fix(devices): agents can test the mobile app on remote device hosts (…
t3dotgg Nov 14, 2026
410177c
fix(gitlab): create fork merge requests in the source project (#23439)
yashranaway Nov 14, 2026
8b30961
fix(server): allow slow worktree base fetches to finish (#24604)
juliusmarminge Nov 14, 2026
f315da3
fix(shared): PASS and FAIL badges stay readable in dark command outpu…
WesleySmits Nov 14, 2026
947c056
fix(web): a context submenu near the right edge no longer covers its …
Brechard Nov 14, 2026
3fe0fbb
fix(web): keep file uploads when returning a draft to its environment…
StiensWout Nov 14, 2026
fd0ffd0
fix(web): a stale tab no longer erases finalized stashed images (#36574)
StiensWout Nov 14, 2026
2f2730c
fix(server): task_status says starting until the child's provider sta…
only21mil Nov 14, 2026
d2bdd58
fix(server): npm provider updates only offer versions npm will instal…
davisonio Nov 14, 2026
5e06246
fix(web): recovered queued edits keep their mentions and context (#36…
StiensWout Nov 14, 2026
35d01dc
perf(acp): long ACP threads reopen without per-chunk history copies (…
only21mil Nov 14, 2026
4d5d5f8
fix(source-control): preserve .git in Azure DevOps repository names (…
StiensWout Nov 14, 2026
4652544
fix(server): interrupted checkpoints no longer leak temporary packs (…
razzeee Nov 14, 2026
306914f
fix(client-runtime): grouped projects no longer take one folder's nam…
ThomasVuNguyen Nov 14, 2026
bebaf3e
fix(pi): resumed threads run in the worktree after a handoff (#36586)
juliusmarminge Nov 14, 2026
d0a494f
fix(server): honor the configured push remote on tracked branches (#2…
only21mil Nov 14, 2026
000d82d
fix(web): Esc closes the theme editor, not Settings (#25130)
AatmanAJ Nov 14, 2026
c0b140b
fix(server): file changes with an empty list keep their file name (#3…
MinitJain Nov 14, 2026
b4c1bd1
fix(web): re-picking the current model in Settings keeps its traits (…
f4llenz Nov 14, 2026
2b6145b
fix(web): browser tests no longer pick a port Windows has reserved (#…
Zacharon Nov 14, 2026
e4688f7
fix(server): find files in newline-named nested worktrees (#36588)
StiensWout Nov 14, 2026
28f1786
fix(web): recreated folders no longer show deleted files (#36590)
StiensWout Nov 14, 2026
3b18dc2
fix(web): manual compaction counts history beyond the loaded page (#2…
jason5545 Nov 14, 2026
2fa77fb
fix(web): horizontal table scrolling preserves timeline follow (#24045)
nateEc Nov 14, 2026
83a276d
fix(server): setup scripts complete with shell line editors (#25472)
maria-rcks Nov 14, 2026
a61291e
fix(shared): one-letter link schemes no longer become file links on W…
Zacharon Nov 14, 2026
4192234
fix(usage): distinct email-less hub accounts no longer collapse (#36561)
StiensWout Nov 14, 2026
75c544a
fix(pi): async pi-subagents workflow, chain and parallel members show…
juliusmarminge Nov 14, 2026
651accb
fix(web): new browser tabs open at your default zoom and appearance (…
BilalIrfanContact Nov 14, 2026
9e19b7f
feat(server): resume stopped provider threads natively (#36526)
Yash-Singh1 Nov 14, 2026
4574c60
fix(web): preserve saved notebook traceback frame boundaries (#36602)
StiensWout Nov 14, 2026
d6244ac
fix(web): preserve literal entities in EPUB CDATA (#36595)
StiensWout Nov 14, 2026
953ce60
fix(web): changed root files with a leading space open in the diff pa…
StiensWout Nov 14, 2026
43ba62b
fix(server): thread reads wait for pending background work (#36610)
StiensWout Nov 14, 2026
91fa913
fix(server): partial import retries no longer certify missing message…
StiensWout Nov 14, 2026
4b99420
fix(server): background subagents stop running when their Claude sess…
juliusmarminge Nov 14, 2026
b37c959
fix(server): open colon-number project folders correctly in VS Code (…
StiensWout Nov 14, 2026
4121b30
fix(mobile): the open button no longer covers inline page headings (#…
saphid Nov 14, 2026
02e29ce
perf(usage): show saved totals before scanning history (#25711)
hieu-lee Nov 14, 2026
89f1acb
fix(pi): text generation uses the thinking level picked in Settings (…
MarcoDotIO Nov 14, 2026
0fee26d
fix(mobile): launch terminal commands after the measured resize (#36584)
StiensWout Nov 14, 2026
13f89ae
fix(server): repeated imports reach older worktree history (#36608)
StiensWout Nov 14, 2026
8a072ca
fix(ci): pin unenv so release smoke installs resolve its exact peer (…
juliusmarminge Nov 14, 2026
38b3fee
fix(server): cached refs include a newly checked-out external branch …
StiensWout Nov 14, 2026
820f620
fix(web): model picks no longer inherit another model’s traits (#36621)
StiensWout Nov 14, 2026
000501e
chore(fork): merge upstream v0.1.10-nightly.20261114.4682
tebiggs Nov 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/fork/upstream-tag
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.1.10-nightly.20261114.4669
v0.1.10-nightly.20261114.4682
32 changes: 5 additions & 27 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -903,34 +903,12 @@ jobs:
fi
# electron-updater reads one manifest per platform and channel and picks
# the file entry whose name carries the running arch, so the per-arch
# manifests the build jobs wrote are merged back into that one file.
- name: Merge macOS updater manifests
# the file entry whose name carries the running arch. Require every
# platform's two build jobs to have produced matching metadata before
# merging it, so a successful release can never strand one architecture.
- name: Merge desktop updater manifests
if: needs.preflight.outputs.release_channel != 'preview'
run: |
shopt -s nullglob
for x64_manifest in release-assets/*-mac-x64.yml; do
arm64_manifest="${x64_manifest%-x64.yml}.yml"
if [[ -f "$arm64_manifest" ]]; then
node scripts/merge-update-manifests.ts --platform mac "$arm64_manifest" "$x64_manifest"
rm -f "$x64_manifest"
fi
done
- name: Merge Windows updater manifests
if: needs.preflight.outputs.release_channel != 'preview'
run: |
shopt -s nullglob
for x64_manifest in release-assets/*-win-x64.yml; do
arm64_manifest="${x64_manifest%-x64.yml}-arm64.yml"
merged_manifest="${x64_manifest%-win-x64.yml}.yml"
if [[ -f "$arm64_manifest" ]]; then
node scripts/merge-update-manifests.ts --platform win "$x64_manifest" "$arm64_manifest" "$merged_manifest"
rm -f "$x64_manifest" "$arm64_manifest"
else
mv "$x64_manifest" "$merged_manifest"
fi
done
run: node scripts/merge-release-update-manifests.ts release-assets

# Updater manifests and blockmaps are what electron-updater consumes.
# They are only listed for channels an updater is meant to follow.
Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src/backend/DesktopBackendConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ describe("DesktopBackendConfiguration", () => {
);

assert.equal(config.runningDistro, "Ubuntu");
assert.deepEqual(config.args.slice(0, 2), ["-d", "Ubuntu"]);
assert.deepEqual(config.args.slice(0, 5), ["-d", "Ubuntu", "--cd", "~", "--exec"]);
assert.deepEqual(observedDistros, ["Ubuntu", "Ubuntu", "Ubuntu"]);
assert.isTrue(Option.isNone(config.preflightFailure));
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
Expand Down Expand Up @@ -436,6 +436,8 @@ describe("DesktopBackendConfiguration", () => {
assert.deepEqual(config.args, [
"-d",
"Ubuntu",
"--cd",
"~",
"--exec",
"env",
`PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${resolvedPath}`,
Expand Down Expand Up @@ -651,6 +653,8 @@ describe("DesktopBackendConfiguration", () => {
assert.deepEqual(config.args, [
"-d",
"Ubuntu",
"--cd",
"~",
"--exec",
"env",
"PATH=/home/test user's/.nvm/versions/node/v22.0.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/test user/bin:/opt/test's tools/bin:/usr/bin:/bin",
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/backend/DesktopBackendConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,9 @@ const resolveWslStartConfig = Effect.fn("desktop.backendConfiguration.resolveWsl
...baseConfig,
args: [
...distroArgs,
// WSL otherwise inherits the Windows-mounted desktop working directory.
"--cd",
"~",
"--exec",
"env",
`PATH=${launchPath}`,
Expand Down
9 changes: 7 additions & 2 deletions apps/desktop/src/electron/ElectronMenu.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ContextMenuItem } from "@t3tools/contracts";
import type { ContextMenuItem, ContextMenuSource } from "@t3tools/contracts";
import * as HostProcess from "@t3tools/shared/HostProcess";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
Expand All @@ -17,6 +17,7 @@ export interface ElectronMenuContextInput {
readonly window: Electron.BrowserWindow;
readonly items: readonly ContextMenuItem[];
readonly position: Option.Option<ElectronMenuPosition>;
readonly source: Option.Option<ContextMenuSource>;
}

export interface ElectronMenuTemplateInput {
Expand Down Expand Up @@ -259,7 +260,11 @@ export const make = Effect.gen(function* () {
callback: () => complete(Option.none()),
}),
});
menu.popup(popupOptions);
menu.popup(
Option.isSome(input.source)
? { ...popupOptions, sourceType: input.source.value }
: popupOptions,
);
} catch (cause) {
if (completed) {
return;
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 @@ -50,6 +50,7 @@ import {
probeRemoteEditors,
revealWindow,
pickFolder,
toLocalBackendPath,
pickProjectFavicon,
pickThemeFiles,
setTheme,
Expand Down Expand Up @@ -145,6 +146,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(setWslOnly);

yield* ipc.handle(pickFolder);
yield* ipc.handle(toLocalBackendPath);
yield* ipc.handle(pickProjectFavicon);
yield* ipc.handle(pickThemeFiles);
yield* ipc.handle(setTheme);
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 TO_LOCAL_BACKEND_PATH_CHANNEL = "desktop:to-local-backend-path";
export const SET_NOTIFICATION_BADGE_CHANNEL = "desktop:set-notification-badge";
export const PICK_PROJECT_FAVICON_CHANNEL = "desktop:pick-project-favicon";
export const PICK_THEME_FILES_CHANNEL = "desktop:pick-theme-files";
Expand Down
43 changes: 43 additions & 0 deletions apps/desktop/src/ipc/methods/window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import * as DesktopBackendManager from "../../backend/DesktopBackendManager.ts";
import * as DesktopBackendPool from "../../backend/DesktopBackendPool.ts";
import * as DesktopAppSettings from "../../settings/DesktopAppSettings.ts";
import * as DesktopManagedPolicy from "../../app/DesktopManagedPolicy.ts";
import * as DesktopWslEnvironment from "../../wsl/DesktopWslEnvironment.ts";
import * as ElectronDialog from "../../electron/ElectronDialog.ts";
import * as ElectronWindow from "../../electron/ElectronWindow.ts";
import * as IpcChannels from "../channels.ts";
Expand All @@ -36,6 +37,7 @@ import {
probeRemoteEditors,
readMedia,
syncHtmlFullscreen,
toLocalBackendPath,
} from "./window.ts";

const readyWslConfig: DesktopBackendManager.DesktopBackendStartConfig = {
Expand Down Expand Up @@ -185,6 +187,47 @@ describe("getLocalEnvironmentBootstraps", () => {
});
});

describe("toLocalBackendPath", () => {
const { runningDistro: _, ...windowsConfig } = readyWslConfig;
it.effect("converts paths through wslpath only for backends running in WSL", () =>
Effect.gen(function* () {
const convert = (backendId: string, path = "C:\\Users\\me\\project") =>
toLocalBackendPath.handler({ backendId, path });
assert.strictEqual(yield* convert("primary"), "C:\\Users\\me\\project");
assert.strictEqual(yield* convert("wsl:default"), "/drives/c/Users/me/project");
assert.strictEqual(yield* convert("wsl:missing"), null);
assert.strictEqual(
yield* convert("wsl:default", "\\\\wsl.localhost\\ubuntu\\home\\me\\repo"),
"/home/me/repo",
);
assert.strictEqual(yield* convert("wsl:default", "\\\\wsl$\\Debian\\home\\me\\repo"), null);
assert.strictEqual(
yield* convert("primary", "\\\\wsl$\\Debian\\home\\me\\repo"),
"\\\\wsl$\\Debian\\home\\me\\repo",
);
}).pipe(
Effect.provide(
Layer.mergeAll(
DesktopBackendPool.layerTest([
{
...defaultWslInstance,
id: DesktopBackendManager.BackendInstanceId("primary"),
currentConfig: Effect.succeedSome(windowsConfig),
},
defaultWslInstance,
]),
DesktopWslEnvironment.layerTest({
windowsToWslPath: (distro, windowsPath) =>
distro === "Ubuntu" && windowsPath === "C:\\Users\\me\\project"
? Option.some("/drives/c/Users/me/project")
: Option.none(),
}),
),
),
),
);
});

describe("installRelayTelemetryEnabled", () => {
// The renderer reads this over sync IPC, so the answer must not wait on the filesystem.
const relayTelemetryEnabled = (
Expand Down
47 changes: 37 additions & 10 deletions apps/desktop/src/ipc/methods/window.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ContextMenuItemSchema,
ContextMenuSource,
DesktopAppBrandingSchema,
DesktopEnvironmentBootstrapSchema,
DesktopMediaReadSchema,
Expand All @@ -25,6 +26,7 @@ import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as DesktopBackendConfiguration from "../../backend/DesktopBackendConfiguration.ts";
import * as DesktopBackendManager from "../../backend/DesktopBackendManager.ts";
import * as DesktopBackendPool from "../../backend/DesktopBackendPool.ts";
import * as DesktopLocalEnvironmentAuth from "../../backend/DesktopLocalEnvironmentAuth.ts";
import * as DesktopEnvironment from "../../app/DesktopEnvironment.ts";
Expand Down Expand Up @@ -58,6 +60,7 @@ const ContextMenuPosition = Schema.Struct({
const ContextMenuInput = Schema.Struct({
items: Schema.Array(ContextMenuItemSchema),
position: Schema.optionalKey(ContextMenuPosition),
source: Schema.optionalKey(ContextMenuSource),
});

function toWebSocketBaseUrl(httpBaseUrl: URL): string {
Expand Down Expand Up @@ -204,6 +207,20 @@ export const getLocalEnvironmentBearerToken = DesktopIpc.makeIpcMethod({
}),
});

// Ask the distro: its automount root is configurable, so /mnt/<drive> is only the default.
const windowsPathToWsl = Effect.fn("desktop.ipc.window.windowsPathToWsl")(function* (
distro: string | null,
windowsPath: string,
) {
const linuxUncPath = wslUncPathToLinuxPath(windowsPath);
if (linuxUncPath !== null) return Option.some(linuxUncPath);
const wslEnvironment = yield* DesktopWslEnvironment.DesktopWslEnvironment;
return yield* wslEnvironment.windowsToWslPath(
extractDistroFromUncPath(windowsPath) ?? distro,
windowsPath,
);
});

export const pickFolder = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PICK_FOLDER_CHANNEL,
payload: Schema.UndefinedOr(PickFolderOptionsSchema),
Expand Down Expand Up @@ -250,20 +267,29 @@ export const pickFolder = DesktopIpc.makeIpcMethod({
if (!useWsl) {
return selectedPath.value;
}

const linuxUncPath = wslUncPathToLinuxPath(selectedPath.value);
if (linuxUncPath !== null) {
return linuxUncPath;
}

const converted = yield* wslEnvironment.windowsToWslPath(
extractDistroFromUncPath(selectedPath.value) ?? wslDistro,
selectedPath.value,
);
const converted = yield* windowsPathToWsl(wslDistro, selectedPath.value);
return Option.getOrElse(converted, () => selectedPath.value);
}),
});

export const toLocalBackendPath = DesktopIpc.makeIpcMethod({
channel: IpcChannels.TO_LOCAL_BACKEND_PATH_CHANNEL,
payload: Schema.Struct({ backendId: Schema.String, path: Schema.String }),
result: Schema.NullOr(Schema.String),
handler: Effect.fn("desktop.ipc.window.toLocalBackendPath")(function* ({ backendId, path }) {
const pool = yield* DesktopBackendPool.DesktopBackendPool;
const instance = yield* pool.get(DesktopBackendManager.BackendInstanceId(backendId));
if (Option.isNone(instance)) return null;
const config = yield* instance.value.currentConfig;
if (Option.isNone(config) || Option.isSome(config.value.preflightFailure)) return null;
const { runningDistro } = config.value;
if (runningDistro === undefined) return path;
const uncDistro = extractDistroFromUncPath(path);
if (uncDistro !== null && uncDistro.toLowerCase() !== runningDistro.toLowerCase()) return null;
return Option.getOrNull(yield* windowsPathToWsl(runningDistro, path));
}),
});

export const pickProjectFavicon = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PICK_PROJECT_FAVICON_CHANNEL,
payload: Schema.UndefinedOr(Schema.String),
Expand Down Expand Up @@ -316,6 +342,7 @@ export const showContextMenu = DesktopIpc.makeIpcMethod({
window: window.value,
items: input.items,
position: Option.fromNullishOr(input.position),
source: Option.fromNullishOr(input.source),
});
return Option.getOrNull(selectedItemId);
}),
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,14 +238,17 @@ contextBridge.exposeInMainWorld("desktopBridge", {
setWslDistro: (distro) => ipcRenderer.invoke(IpcChannels.SET_WSL_DISTRO_CHANNEL, distro),
setWslOnly: (enabled) => ipcRenderer.invoke(IpcChannels.SET_WSL_ONLY_CHANNEL, enabled),
pickFolder: (options) => ipcRenderer.invoke(IpcChannels.PICK_FOLDER_CHANNEL, options),
toLocalBackendPath: (backendId, path) =>
ipcRenderer.invoke(IpcChannels.TO_LOCAL_BACKEND_PATH_CHANNEL, { backendId, path }),
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) =>
showContextMenu: (items, position, source) =>
ipcRenderer.invoke(IpcChannels.CONTEXT_MENU_CHANNEL, {
items,
...(position === undefined ? {} : { position }),
...(source === undefined ? {} : { source }),
}),
receiveProviderAuthCallback: (url: string) =>
ipcRenderer.invoke(IpcChannels.RECEIVE_PROVIDER_AUTH_CALLBACK_CHANNEL, url),
Expand Down
33 changes: 33 additions & 0 deletions apps/desktop/src/shell/DesktopShellEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,39 @@ describe("DesktopShellEnvironment", () => {
}),
);

it.effect.each([
{
name: "hydrates both Android SDK names when neither is inherited",
inherited: {},
expected: { ANDROID_HOME: "/shell/android-home", ANDROID_SDK_ROOT: "/shell/android-root" },
},
{
// The device host reads ANDROID_HOME before ANDROID_SDK_ROOT, so a shell
// ANDROID_HOME next to an inherited ANDROID_SDK_ROOT would change the SDK.
name: "keeps an inherited ANDROID_SDK_ROOT selected over a login-shell ANDROID_HOME",
inherited: { ANDROID_SDK_ROOT: "/inherited/android-sdk" },
expected: { ANDROID_HOME: undefined, ANDROID_SDK_ROOT: "/inherited/android-sdk" },
},
])("$name", ({ inherited, expected }) =>
Effect.gen(function* () {
const env: NodeJS.ProcessEnv = { SHELL: "/bin/zsh", PATH: "/usr/bin", ...inherited };

yield* runShellEnvironment({
env,
platform: "darwin",
handler: () =>
envOutput({
PATH: "/usr/bin",
ANDROID_HOME: "/shell/android-home",
ANDROID_SDK_ROOT: "/shell/android-root",
}),
});

assert.equal(env.ANDROID_HOME, expected.ANDROID_HOME);
assert.equal(env.ANDROID_SDK_ROOT, expected.ANDROID_SDK_ROOT);
}),
);

it.effect("keeps mise's PATH record with the login shell PATH", () =>
Effect.gen(function* () {
const env: NodeJS.ProcessEnv = {
Expand Down
14 changes: 14 additions & 0 deletions apps/desktop/src/shell/DesktopShellEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export class DesktopShellEnvironment extends Context.Service<

const LOGIN_SHELL_ENV_NAMES = [
"PATH",
"ANDROID_HOME",
"ANDROID_SDK_ROOT",
"DBUS_SESSION_BUS_ADDRESS",
"DISPLAY",
"LANG",
Expand All @@ -114,6 +116,7 @@ const LOGIN_SHELL_ENV_NAMES = [
] as const;
const WINDOWS_PROFILE_ENV_NAMES = ["PATH", "FNM_DIR", "FNM_MULTISHELL_PATH"] as const;
const LOCALE_ENV_NAMES = ["LANG", "LC_ALL", "LC_CTYPE"] as const;
const ANDROID_SDK_ENV_NAMES = ["ANDROID_HOME", "ANDROID_SDK_ROOT"] as const;
const FALLBACK_LC_CTYPE = "en_US.UTF-8";
const WINDOWS_SHELL_CANDIDATES = ["pwsh.exe", "powershell.exe"] as const;
const LOGIN_SHELL_TIMEOUT = Duration.seconds(5);
Expand Down Expand Up @@ -528,6 +531,17 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix
}
}

// The device host picks the Android SDK from ANDROID_HOME before ANDROID_SDK_ROOT,
// so a profile-only ANDROID_HOME must not shadow an inherited ANDROID_SDK_ROOT.
if (ANDROID_SDK_ENV_NAMES.every((name) => Option.isNone(trimNonEmpty(config.env[name])))) {
for (const name of ANDROID_SDK_ENV_NAMES) {
const value = trimNonEmpty(shellEnvironment[name]);
if (Option.isSome(value)) {
config.env[name] = value.value;
}
}
}

// Hydrate locale variables together; inherited LC_ALL can override LANG or LC_CTYPE.
if (
config.platform === "darwin" &&
Expand Down
Loading
Loading