Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
89ee69e
fix(server): advertise truecolor in the integrated terminal (#7680)
shubhxho Sep 5, 2026
940e823
fix(claude): surface usage-limit pauses in the thread (#7165)
vitalyiegorov Sep 5, 2026
ce4712d
fix: restore UX after performance improvements (#9799)
t3dotgg Sep 5, 2026
57a6b70
fix(codex): show what a file-change approval will change (#8669)
walid-baharwal Sep 5, 2026
4ade365
fix(ci): add fallback Ubuntu package mirrors (#9864)
maria-rcks Sep 5, 2026
03c6cd8
fix(web): copy text over plain HTTP (#8023)
Michel-Liao Sep 5, 2026
d7fe47f
fix(server): dismiss native questions when their turn ends (#9851)
juliusmarminge Sep 5, 2026
d28077e
fix(server): quote copied native provider update commands (#9856)
juliusmarminge Sep 5, 2026
1665d81
fix(release): install the correct Windows Spectre component (#9859)
maria-rcks Sep 5, 2026
1246146
fix(web): show retained runtime diagnostics in the work log (#9870)
juliusmarminge Sep 5, 2026
7f8cf30
fix(web): make sidebar project actions reachable by keyboard and scre…
akj Sep 5, 2026
de1b798
fix(preview): bound automation waits and screenshot captures (#4685)
juliusmarminge Sep 5, 2026
7ee52b0
fix(web): render and filter usage as environments respond (#9860)
juliusmarminge Sep 5, 2026
84b99f3
feat(web): edit usage prices across selected environments (#9861)
juliusmarminge Sep 5, 2026
ac90950
fix(mobile): keep usage tabs below the header when switching (#9876)
juliusmarminge Sep 5, 2026
363cde4
fix(connect): refresh HTTP credentials without reconnecting (#9594)
juliusmarminge Sep 5, 2026
b01771c
fix(web): keep file autosaves active after effect replay (#9878)
juliusmarminge Sep 5, 2026
a0eb239
fix(web): prioritize open panel pull request when copying (#9877)
maria-rcks Sep 5, 2026
9cb4017
fix(cli): resolve projects with missing workspace directories (#9885)
juliusmarminge Sep 5, 2026
931d41f
fix(web): sort equally merge-ready pull requests by diff size (#9887)
maria-rcks Sep 5, 2026
94cc815
fix(chat): show hours for long runs (#9894)
t3dotgg Sep 5, 2026
2fb99a7
fix(server): only run provider updates through the installer that own…
juliusmarminge Sep 5, 2026
df8e0eb
fix(web): discard stale highlights after file edits (#9902)
juliusmarminge Sep 5, 2026
fd77317
feat(web): recall sent prompts with the up arrow (#9173)
t3dotgg Sep 5, 2026
c7dc3cb
fix(server): keep mise-owned npm packages manual-only (#9927)
juliusmarminge Sep 5, 2026
f47a3fe
fix(settings): share restart continuation across environments (#9933)
maria-rcks Sep 5, 2026
7a089b2
fix(server): capture turn checkpoints after all edits finish (#9841)
juliusmarminge Sep 5, 2026
2fa5ef4
fix(web): load file grammar before enabling edits (#9947)
juliusmarminge Sep 5, 2026
e5a87e8
fix(web): deduplicate PR project filter choices (#9948)
juliusmarminge Sep 5, 2026
07d2497
test(web): keep provider field readers private (#9952)
juliusmarminge Sep 5, 2026
a399473
test(mobile): keep composer selection helper private (#9953)
juliusmarminge Sep 5, 2026
f530d7b
test(client-runtime): keep scoped key implementation private (#9955)
juliusmarminge Sep 5, 2026
6b87ce3
fix(web): reveal reselected diff files (#9951)
juliusmarminge Sep 5, 2026
eae770b
refactor(web): remove obsolete changed-files preview helpers (#9956)
juliusmarminge Sep 5, 2026
bc8584b
fix(web): give the browser keybinding notice breathing room (#9964)
juliusmarminge Sep 5, 2026
126ea5c
chore: configure Knip workspace audits (#9958)
juliusmarminge Sep 5, 2026
4a42fc6
refactor(web): prune unused UI and provider code (#9959)
juliusmarminge Sep 5, 2026
2759ef0
chore(mobile): remove obsolete widget wiring script (#9960)
juliusmarminge Sep 5, 2026
00d4618
chore: remove redundant root tooling dependencies (#9961)
juliusmarminge Sep 5, 2026
d2c3e2e
ci: reject unused files and dependencies with Knip (#9962)
juliusmarminge Sep 5, 2026
dfc70a3
test(contracts): keep driver default lookup private (#9968)
juliusmarminge Sep 5, 2026
32142cf
test(server): remove Azure permissions constant snapshot (#9973)
juliusmarminge Sep 5, 2026
1568b3f
refactor(shared): remove unused viewport formatters (#9970)
juliusmarminge Sep 5, 2026
c1e279e
refactor(mobile): remove unused provider option summary (#9971)
juliusmarminge Sep 5, 2026
1782a2a
refactor(client-runtime): remove unused connection phase message (#9972)
juliusmarminge Sep 5, 2026
eda0cec
refactor(mobile): remove unused layout calculations (#9974)
juliusmarminge Sep 5, 2026
044a6e1
refactor(client-runtime): remove unused file position predicate (#9976)
juliusmarminge Sep 5, 2026
5fe29c8
refactor(mobile): remove unused font size steppers (#9975)
juliusmarminge Sep 5, 2026
b3f8dd9
test(server): cover thread lookup through command invariants (#9978)
juliusmarminge Sep 5, 2026
2fc630c
test(server): remove provider equality wrapper fixture (#9979)
juliusmarminge Sep 5, 2026
1550f1b
test(server): assert the dispatched welcome thread model (#9980)
juliusmarminge Sep 5, 2026
14da634
refactor(desktop): remove unused keyring remediation text (#9981)
juliusmarminge Sep 5, 2026
a9caf7b
refactor(desktop): remove test-only Electron error predicates (#9982)
juliusmarminge Sep 5, 2026
ea0487c
refactor(web): remove unused pull request state label (#9984)
juliusmarminge Sep 5, 2026
aefef95
perf(web): keep timeline row reuse engaged while text streams (#9909)
juliusmarminge Sep 5, 2026
3e13333
fix(web): reset markdown widgets when the previewed file changes (#9910)
juliusmarminge Sep 5, 2026
50027bb
fix(mobile): keep highlighting review diffs after a long line (#9911)
juliusmarminge Sep 5, 2026
fc1f543
fix(marketing): align the endorsement carousel with its heading (#9912)
juliusmarminge Sep 5, 2026
f87ecf0
fix(client): keep warm thread resumes live instead of flashing sync (…
juliusmarminge Sep 5, 2026
371e323
test(server): remove authorization prompt snapshots (#9985)
juliusmarminge Sep 5, 2026
10421bc
test(server): remove static OAuth page snapshots (#9986)
juliusmarminge Sep 5, 2026
9867eb1
test(server): remove provider label identity assertion (#9987)
juliusmarminge Sep 5, 2026
94d1fa7
test(server): consolidate agent activity opt-in coverage (#9988)
juliusmarminge Sep 5, 2026
aca2afc
refactor(shared): remove unused preview URL predicate (#9989)
juliusmarminge Sep 5, 2026
c6410d3
refactor(shared): remove unused mention path serializer (#9990)
juliusmarminge Sep 5, 2026
da1bebb
refactor(shared): remove retired PATH capture parser (#9991)
juliusmarminge Sep 5, 2026
3bcde91
refactor(client-runtime): remove unused subagent selectors (#9992)
juliusmarminge Sep 5, 2026
487d176
refactor(web): test the live usage column builder (#9993)
juliusmarminge Sep 5, 2026
c7e93f5
refactor(web): remove unused aspect ratio reconciler (#9994)
juliusmarminge Sep 5, 2026
bf1c1b0
refactor(web): remove obsolete cloud listing helpers (#9995)
juliusmarminge Sep 5, 2026
4f1dc55
test(web): remove composer control style snapshots (#9996)
juliusmarminge Sep 5, 2026
11cb88e
test(web): keep the preview profile label helper private (#9997)
juliusmarminge Sep 5, 2026
3fedf52
test(server): cover raw OpenCode deltas through the adapter (#9977)
juliusmarminge Sep 5, 2026
ba873b8
refactor(web): remove obsolete pull request link opener (#9983)
juliusmarminge Sep 5, 2026
ac93fbf
test(relay): keep the stage slug helper private (#9998)
juliusmarminge Sep 5, 2026
1e24b43
refactor(mobile): keep project selection helper private (#9999)
juliusmarminge Sep 5, 2026
1c59d3b
refactor(mobile): keep review default ID helper private (#10000)
juliusmarminge Sep 5, 2026
3382b26
refactor(mobile): remove unused native style constants (#10001)
juliusmarminge Sep 5, 2026
393d1ff
refactor(mobile): test terminal palettes through public theme API (#1…
juliusmarminge Sep 5, 2026
8d48a31
refactor(mobile): remove unused file tree walkers (#10003)
juliusmarminge Sep 5, 2026
1584076
refactor(shared): keep persisted settings helpers private (#10004)
juliusmarminge Sep 5, 2026
4e5e17f
test(mobile): remove mocked UUID shape assertions (#10006)
juliusmarminge Sep 5, 2026
1449dec
refactor(mobile): test final connection status presentation (#10007)
juliusmarminge Sep 5, 2026
4e59b06
test(web): keep pull request menu items private (#10016)
juliusmarminge Sep 5, 2026
3e544f8
refactor(shared): test favicon selection through public API (#10005)
juliusmarminge Sep 5, 2026
270e021
refactor(server): remove test-only pricing normalizer (#10017)
juliusmarminge Sep 5, 2026
93d4dfa
refactor(web): remove unused desktop update visibility helper (#10014)
juliusmarminge Sep 5, 2026
c9b76e6
refactor(web): remove obsolete provider update helpers (#10015)
juliusmarminge Sep 5, 2026
b7fc81d
refactor(web): remove unused terminal context preview formatter (#10009)
juliusmarminge Sep 5, 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
22 changes: 22 additions & 0 deletions .github/actions/setup-apt-mirrors/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Setup APT mirrors
description: Configure Ubuntu package downloads with automatic mirror failover.
runs:
using: composite
steps:
- shell: bash
run: |
# Replace the existing Blacksmith mirror list as well as direct sources.
printf '%s\tpriority:%s\n' \
https://archive.ubuntu.com/ubuntu 1 \
https://mirrors.edge.kernel.org/ubuntu 2 \
https://mirror.math.princeton.edu/pub/ubuntu 3 \
| sudo tee /etc/apt/blacksmith-ubuntu-mirrors.txt > /dev/null

# APT's mirror transport retries each file against the next server.
sudo find /etc/apt -maxdepth 2 -type f \( -name '*.list' -o -name '*.sources' \) \
-exec sed -i -E \
's#https?://(([^/]+\.)?archive|security)\.ubuntu\.com/ubuntu/?#mirror+file:/etc/apt/blacksmith-ubuntu-mirrors.txt#g' {} +

# Move on to a fallback before an unreachable server exhausts the job.
printf '%s\n' 'Acquire::http::Timeout "15";' 'Acquire::https::Timeout "15";' \
| sudo tee /etc/apt/apt.conf.d/80-mirror-timeouts > /dev/null
16 changes: 14 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,22 @@ jobs:
- name: Ensure Electron runtime is installed
run: vp run --filter @t3tools/desktop ensure:electron

# Export cleanup is still a manual audit; files and dependencies have no baseline.
- name: Check unused files and dependencies
run: vp run knip:check

- name: Check
run: vp check

- name: Typecheck
run: vpr typecheck

- uses: ./.github/actions/setup-apt-mirrors

- name: Install browser secret helper build libraries
run: sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config
run: |
sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config

- name: Build desktop pipeline
run: vp run build:desktop
Expand Down Expand Up @@ -88,8 +96,12 @@ jobs:
- name: Ensure Electron runtime is installed
run: vp run --filter @t3tools/desktop ensure:electron

- uses: ./.github/actions/setup-apt-mirrors

- name: Install browser secret helper build libraries
run: sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config
run: |
sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config

- name: Test
run: vp run --parallel --concurrency-limit 4 --filter '!t3' --filter '!@t3tools/monorepo' test
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ jobs:
- name: Typecheck
run: vp run typecheck

- uses: ./.github/actions/setup-apt-mirrors

- name: Install browser secret helper build libraries
run: sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config

Expand Down Expand Up @@ -519,14 +521,17 @@ jobs:
$setupExe = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\setup.exe"
$proc = Start-Process -FilePath $setupExe `
-ArgumentList "modify", "--installPath", "`"$installPath`"", "--add", `
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64.Spectre", "--quiet", "--norestart" `
"Microsoft.VisualStudio.Component.VC.Runtimes.x86.x64.Spectre", "--quiet", "--norestart" `
-Wait -PassThru -NoNewWindow
if ($null -eq $proc -or $proc.ExitCode -ne 0) {
$code = if ($null -ne $proc) { $proc.ExitCode } else { 1 }
Write-Error "Visual Studio Installer failed with exit code $code"
exit $code
}

- uses: ./.github/actions/setup-apt-mirrors
if: matrix.platform == 'linux'

- name: Install Linux desktop build libraries
if: matrix.platform == 'linux'
shell: bash
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/electron/ElectronDialog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ describe("ElectronDialog", () => {
);

assert.instanceOf(error, ElectronDialog.ElectronDialogPickFolderError);
assert.isTrue(ElectronDialog.isElectronDialogError(error));
assert.strictEqual(error.ownerWindowId, 7);
assert.strictEqual(error.defaultPath, "/workspace");
assert.strictEqual(error.cause, cause);
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/electron/ElectronDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ export const ElectronDialogError = Schema.Union([
ElectronDialogShowErrorBoxError,
]);
export type ElectronDialogError = typeof ElectronDialogError.Type;
export const isElectronDialogError = Schema.is(ElectronDialogError);

export interface ElectronDialogPickFolderInput {
readonly owner: Option.Option<Electron.BrowserWindow>;
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/electron/ElectronTheme.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ describe("ElectronTheme", () => {
const error = yield* Effect.flip(electronTheme.setSource("dark"));

assert.instanceOf(error, ElectronTheme.ElectronThemeSetSourceError);
assert.isTrue(ElectronTheme.isElectronThemeSetSourceError(error));
assert.strictEqual(error.source, "dark");
assert.strictEqual(error.cause, cause);
assert.include(error.message, "dark");
Expand Down
2 changes: 0 additions & 2 deletions apps/desktop/src/electron/ElectronTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ export class ElectronThemeSetSourceError extends Schema.TaggedErrorClass<Electro
}
}

export const isElectronThemeSetSourceError = Schema.is(ElectronThemeSetSourceError);

export class ElectronTheme extends Context.Service<
ElectronTheme,
{
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/electron/ElectronUpdater.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ describe("ElectronUpdater", () => {
const error = yield* updater.checkForUpdates.pipe(Effect.flip);

assert.instanceOf(error, ElectronUpdater.ElectronUpdaterCheckForUpdatesError);
assert.isTrue(ElectronUpdater.isElectronUpdaterError(error));
assert.equal(error.channel, "beta");
assert.strictEqual(error.cause, cause);
assert.equal(error.message, "Electron updater failed to check for updates on channel beta.");
Expand All @@ -89,7 +88,6 @@ describe("ElectronUpdater", () => {
const error = yield* updater.downloadUpdate.pipe(Effect.flip);

assert.instanceOf(error, ElectronUpdater.ElectronUpdaterDownloadUpdateError);
assert.isTrue(ElectronUpdater.isElectronUpdaterError(error));
assert.equal(error.channel, "nightly");
assert.strictEqual(error.cause, cause);
assert.equal(
Expand Down Expand Up @@ -126,7 +124,6 @@ describe("ElectronUpdater", () => {
.pipe(Effect.flip);

assert.instanceOf(error, ElectronUpdater.ElectronUpdaterQuitAndInstallError);
assert.isTrue(ElectronUpdater.isElectronUpdaterError(error));
assert.equal(error.channel, "alpha");
assert.equal(error.isSilent, true);
assert.equal(error.isForceRunAfter, false);
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/electron/ElectronUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export const ElectronUpdaterError = Schema.Union([
ElectronUpdaterQuitAndInstallError,
]);
export type ElectronUpdaterError = typeof ElectronUpdaterError.Type;
export const isElectronUpdaterError = Schema.is(ElectronUpdaterError);

export class ElectronUpdater extends Context.Service<
ElectronUpdater,
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/electron/ElectronWindow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ describe("ElectronWindow", () => {
const error = yield* electronWindow.create(options).pipe(Effect.flip);

assert.instanceOf(error, ElectronWindow.ElectronWindowCreateError);
assert.isTrue(ElectronWindow.isElectronWindowCreateError(error));
assert.deepEqual(error.options, {
title: "T3 Code",
width: 1100,
Expand Down
2 changes: 0 additions & 2 deletions apps/desktop/src/electron/ElectronWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ export class ElectronWindowCreateError extends Schema.TaggedErrorClass<ElectronW
}
}

export const isElectronWindowCreateError = Schema.is(ElectronWindowCreateError);

export class ElectronWindowOperationError extends Schema.TaggedErrorClass<ElectronWindowOperationError>()(
"ElectronWindowOperationError",
{
Expand Down
77 changes: 0 additions & 77 deletions apps/desktop/src/linuxSecretStorage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { describe, expect, it } from "vite-plus/test";
import {
normalizeLinuxPasswordStorePreference,
resolveLinuxPasswordStoreSwitch,
resolveLinuxSecretStorageUnavailableMessage,
} from "./linuxSecretStorage.ts";

const autoSwitch = (env: NodeJS.ProcessEnv) =>
Expand Down Expand Up @@ -124,80 +123,4 @@ describe("linuxSecretStorage", () => {
}),
).toBe("gnome-libsecret");
});

it("uses GNOME Keyring remediation for libsecret and unknown backends", () => {
expect(
resolveLinuxSecretStorageUnavailableMessage({
configuredPreference: "auto",
selectedBackend: "gnome_libsecret",
env: { XDG_CURRENT_DESKTOP: "niri" },
}),
).toContain("GNOME Keyring");
});

it("prefers explicit libsecret selection over KDE desktop heuristics", () => {
expect(
resolveLinuxSecretStorageUnavailableMessage({
configuredPreference: "gnome-libsecret",
selectedBackend: "unknown",
env: { XDG_CURRENT_DESKTOP: "KDE" },
}),
).toContain("GNOME Keyring");
expect(
resolveLinuxSecretStorageUnavailableMessage({
configuredPreference: "auto",
selectedBackend: "gnome_libsecret",
env: { XDG_CURRENT_DESKTOP: "KDE" },
}),
).toContain("GNOME Keyring");
});

it("prefers explicit KWallet preference over selected gnome-libsecret backend", () => {
expect(
resolveLinuxSecretStorageUnavailableMessage({
configuredPreference: "kwallet6",
selectedBackend: "gnome_libsecret",
env: { XDG_CURRENT_DESKTOP: "niri" },
}),
).toContain("KWallet");
expect(
resolveLinuxSecretStorageUnavailableMessage({
configuredPreference: "kwallet",
selectedBackend: "gnome-libsecret",
env: {},
}),
).toContain("KWallet");
});

it("uses KWallet remediation wording for KDE-looking sessions", () => {
expect(
resolveLinuxSecretStorageUnavailableMessage({
configuredPreference: "auto",
selectedBackend: "kwallet6",
env: {},
}),
).toContain("KWallet");
expect(
resolveLinuxSecretStorageUnavailableMessage({
configuredPreference: "auto",
selectedBackend: "unknown",
env: { XDG_CURRENT_DESKTOP: "KDE" },
}),
).toContain("KWallet");
expect(
resolveLinuxSecretStorageUnavailableMessage({
configuredPreference: "auto",
selectedBackend: "unknown",
env: { DESKTOP_SESSION: "plasmawayland" },
}),
).toContain("KWallet");
// A desktop name outranks a bare KDE marker when choosing the wording.
expect(
resolveLinuxSecretStorageUnavailableMessage({
configuredPreference: "auto",
selectedBackend: "unknown",
env: { GDMSESSION: "gnome", KDE_FULL_SESSION: "true" },
}),
).toContain("GNOME Keyring");
});
});
99 changes: 0 additions & 99 deletions apps/desktop/src/linuxSecretStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ const ELECTRON_KDE_DESKTOP = "KDE";
// Chromium recognizes LXQt and still selects basic text for it, so it does need a forced backend.
const ELECTRON_UNPROTECTED_DESKTOPS = new Set(["LXQt"]);

const KDE_NAME_PREFIXES = ["kde", "plasma"];
const NEGATIVE_FLAG_VALUES = new Set(["0", "false", "no", "off"]);

export function normalizeLinuxPasswordStorePreference(
value: unknown,
): LinuxPasswordStorePreference {
Expand Down Expand Up @@ -77,102 +74,6 @@ function electronSelectsProtectedBackend(env: NodeJS.ProcessEnv): boolean {
return false;
}

export function resolveLinuxSecretStorageUnavailableMessage(input: {
readonly configuredPreference: LinuxPasswordStorePreference;
readonly selectedBackend: string | null;
readonly env: NodeJS.ProcessEnv;
}): string {
if (input.configuredPreference === "gnome-libsecret") {
return getGnomeKeyringRemediationMessage();
}

if (
input.configuredPreference === "kwallet" ||
input.configuredPreference === "kwallet5" ||
input.configuredPreference === "kwallet6"
) {
return getKWalletRemediationMessage();
}

const backend = normalizeSelectedStorageBackend(input.selectedBackend);
if (backend === "gnome-libsecret") {
return getGnomeKeyringRemediationMessage();
}

if (
backend === "kwallet" ||
backend === "kwallet5" ||
backend === "kwallet6" ||
looksLikeKdeSession(input.env)
) {
return getKWalletRemediationMessage();
}

return getGnomeKeyringRemediationMessage();
}

function getGnomeKeyringRemediationMessage(): string {
return "T3 Code could not access GNOME Keyring to save this environment credential. Install and start GNOME Keyring, then restart T3 Code.";
}

function getKWalletRemediationMessage(): string {
return "T3 Code could not access KWallet to save this environment credential. Enable the KDE wallet subsystem in System Settings, then restart T3 Code.";
}

// Advisory only: this picks between the GNOME Keyring and KWallet wording in the failure notice. It
// never decides which backend to select, so a loose match costs a user slightly wrong instructions
// rather than an unprotected credential store.
function looksLikeKdeSession(env: NodeJS.ProcessEnv): boolean {
const currentDesktopNames = nonEmptyDesktopNames(env.XDG_CURRENT_DESKTOP);
if (currentDesktopNames.length > 0) {
return currentDesktopNames.some(isKdeDesktopName);
}

const legacyNames = legacyDesktopNames(env);
if (legacyNames.length > 0) {
return legacyNames.some(isKdeDesktopName);
}

return isSet(env.KDE_SESSION_VERSION) || isAffirmativeFlag(env.KDE_FULL_SESSION);
}

function isKdeDesktopName(name: string): boolean {
return KDE_NAME_PREFIXES.some((prefix) => name.startsWith(prefix));
}

function legacyDesktopNames(env: NodeJS.ProcessEnv): string[] {
return [env.XDG_SESSION_DESKTOP, env.DESKTOP_SESSION, env.GDMSESSION].flatMap((entry) => {
const normalized = normalizeDesktopName(entry);
return normalized ? [normalized] : [];
});
}

function nonEmptyDesktopNames(value: string | undefined): string[] {
return splitDesktopNameList(value).flatMap((entry) => {
const normalized = normalizeDesktopName(entry);
return normalized ? [normalized] : [];
});
}

function isSet(value: string | undefined): boolean {
return Boolean(value?.trim());
}

function isAffirmativeFlag(value: string | undefined): boolean {
const normalized = value?.trim().toLowerCase();
return normalized ? !NEGATIVE_FLAG_VALUES.has(normalized) : false;
}

function splitDesktopNameList(value: string | undefined): string[] {
return value?.split(":") ?? [];
}

function normalizeDesktopName(value: string | undefined): string | null {
const normalized = value?.trim().toLowerCase();
return normalized && normalized.length > 0 ? normalized : null;
}

function normalizeSelectedStorageBackend(value: string | null): string | null {
const normalized = value?.trim().toLowerCase().replace(/_/gu, "-");
return normalized && normalized.length > 0 ? normalized : null;
}
Loading
Loading