Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions apps/desktop/scripts/electron-launcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export function makeDevelopmentEnvironmentScript(environment) {
["T3CODE_COMMIT_HASH", environment.T3CODE_COMMIT_HASH],
["T3CODE_OTLP_TRACES_URL", environment.T3CODE_OTLP_TRACES_URL],
["T3CODE_OTLP_EXPORT_INTERVAL_MS", environment.T3CODE_OTLP_EXPORT_INTERVAL_MS],
["T3CODE_OTLP_HEADERS", environment.T3CODE_OTLP_HEADERS],
["T3CODE_OTLP_PROTOCOL", environment.T3CODE_OTLP_PROTOCOL],
["T3CODE_DESKTOP_APP_USER_MODEL_ID", APP_BUNDLE_ID],
].filter((entry) => typeof entry[1] === "string" && entry[1].trim().length > 0);
return [
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/scripts/electron-launcher.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@ describe("electron development launcher", () => {
VITE_DEV_SERVER_URL: "http://127.0.0.1:8526",
T3CODE_PORT: "16566",
T3CODE_HOME: "/tmp/t3",
T3CODE_OTLP_PROTOCOL: "http/protobuf",
});

assert.include(
environmentScript,
"if [ -z \"${VITE_DEV_SERVER_URL:-}\" ]; then export VITE_DEV_SERVER_URL='http://127.0.0.1:8526'; fi",
);
assert.include(
environmentScript,
"if [ -z \"${T3CODE_OTLP_PROTOCOL:-}\" ]; then export T3CODE_OTLP_PROTOCOL='http/protobuf'; fi",
);
assert.notInclude(environmentScript, "\nexport VITE_DEV_SERVER_URL=");
});

Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/app/DesktopConfig.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { OtlpHeadersFromString, OtlpProtocol } from "@t3tools/shared/observability";
import * as Config from "effect/Config";
import * as ConfigProvider from "effect/ConfigProvider";
import * as Option from "effect/Option";
Expand Down Expand Up @@ -48,6 +49,10 @@ export const DesktopConfig = Config.all({
otlpExportIntervalMs: Config.int("T3CODE_OTLP_EXPORT_INTERVAL_MS").pipe(
Config.withDefault(10_000),
),
otlpHeaders: Config.schema(OtlpHeadersFromString, "T3CODE_OTLP_HEADERS").pipe(Config.option),
otlpProtocol: Config.schema(OtlpProtocol, "T3CODE_OTLP_PROTOCOL").pipe(
Config.withDefault("http/json"),
),
appImagePath: trimmedString("APPIMAGE"),
disableAutoUpdate: optionalBoolean("T3CODE_DISABLE_AUTO_UPDATE"),
mockUpdates: optionalBoolean("T3CODE_DESKTOP_MOCK_UPDATES"),
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/src/app/DesktopEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ describe("DesktopEnvironment", () => {
T3CODE_DEV_REMOTE_T3_SERVER_ENTRY_PATH: " /remote/server.mjs ",
T3CODE_OTLP_TRACES_URL: " http://127.0.0.1:4318/v1/traces ",
T3CODE_OTLP_EXPORT_INTERVAL_MS: "2500",
T3CODE_OTLP_HEADERS: "authorization=Basic%20abc%3D%3D,x-tenant=t3",
T3CODE_OTLP_PROTOCOL: "http/protobuf",
},
);

Expand Down Expand Up @@ -85,6 +87,14 @@ describe("DesktopEnvironment", () => {
assert.deepEqual(environment.commitHashOverride, Option.some("0123456789abcdef"));
assert.deepEqual(environment.otlpTracesUrl, Option.some("http://127.0.0.1:4318/v1/traces"));
assert.equal(environment.otlpExportIntervalMs, 2500);
assert.deepEqual(
environment.otlpHeaders,
Option.some({
authorization: "Basic abc==",
"x-tenant": "t3",
}),
);
assert.equal(environment.otlpProtocol, "http/protobuf");
}),
);

Expand All @@ -102,6 +112,7 @@ describe("DesktopEnvironment", () => {
assert.equal(environment.logDir, "/tmp/t3/userdata/logs");
assert.equal(environment.browserArtifactsDir, "/tmp/t3/userdata/browser-artifacts");
assert.equal(environment.serverSettingsPath, "/tmp/t3/userdata/settings.json");
assert.equal(environment.otlpProtocol, "http/json");
}),
);

Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/app/DesktopEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import * as DesktopConfig from "./DesktopConfig.ts";
import { resolveLinuxDesktopEntryName } from "./DesktopEarlyElectronStartup.ts";
import { resolveDesktopBaseDir, resolveDesktopStateDir } from "./DesktopStatePaths.ts";
import { isNightlyDesktopVersion } from "../updates/updateChannels.ts";
import type { OtlpProtocol } from "@t3tools/shared/observability";

export interface MakeDesktopEnvironmentInput {
readonly dirname: string;
Expand Down Expand Up @@ -72,6 +73,8 @@ export class DesktopEnvironment extends Context.Service<
readonly commitHashOverride: Option.Option<string>;
readonly otlpTracesUrl: Option.Option<string>;
readonly otlpExportIntervalMs: number;
readonly otlpHeaders: Option.Option<Record<string, string>>;
readonly otlpProtocol: OtlpProtocol;
readonly branding: DesktopAppBranding;
readonly displayName: string;
readonly appUserModelId: string;
Expand Down Expand Up @@ -225,6 +228,8 @@ const make = Effect.fn("desktop.environment.make")(function* (
commitHashOverride: config.commitHashOverride,
otlpTracesUrl: config.otlpTracesUrl,
otlpExportIntervalMs: config.otlpExportIntervalMs,
otlpHeaders: config.otlpHeaders,
otlpProtocol: config.otlpProtocol,
branding,
displayName,
appUserModelId: Option.getOrElse(config.appUserModelIdOverride, () =>
Expand Down
13 changes: 9 additions & 4 deletions apps/desktop/src/app/DesktopObservability.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { PRIMARY_LOCAL_ENVIRONMENT_ID } from "@t3tools/contracts";
import { makeLocalFileTracer, makeTraceSink } from "@t3tools/shared/observability";
import {
makeLocalFileTracer,
makeTraceSink,
otlpSerializationLayer,
} from "@t3tools/shared/observability";
import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";
import * as Context from "effect/Context";
import * as DateTime from "effect/DateTime";
Expand All @@ -17,7 +21,7 @@ import * as Scope from "effect/Scope";
import * as Semaphore from "effect/Semaphore";
import * as SynchronizedRef from "effect/SynchronizedRef";
import * as Tracer from "effect/Tracer";
import { OtlpExporter, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
import { OtlpExporter, OtlpTracer } from "effect/unstable/observability";

import * as DesktopEnvironment from "./DesktopEnvironment.ts";

Expand Down Expand Up @@ -584,14 +588,15 @@ const tracerLayer = Layer.unwrap(
: yield* OtlpTracer.make({
url: otlpTracesUrl.value,
exportInterval: `${environment.otlpExportIntervalMs} millis`,
headers: Option.getOrUndefined(environment.otlpHeaders),
resource: {
serviceName: "desktop",
attributes: {
"service.runtime": "desktop",
"service.mode": environment.isDevelopment ? "development" : "packaged",
},
},
});
}).pipe(Effect.provide(otlpSerializationLayer(environment.otlpProtocol)));
const tracer = yield* makeLocalFileTracer({
filePath: tracePath,
maxBytes: DESKTOP_LOG_FILE_MAX_BYTES,
Expand All @@ -603,7 +608,7 @@ const tracerLayer = Layer.unwrap(

return Layer.succeed(Tracer.Tracer, tracer);
}),
).pipe(Layer.provide(OtlpExporter.layerFlusher), Layer.provideMerge(OtlpSerialization.layerJson));
).pipe(Layer.provide(OtlpExporter.layerFlusher));

export const layer = Layer.mergeAll(
backendOutputLogFactoryLayer,
Expand Down
15 changes: 11 additions & 4 deletions apps/desktop/src/backend/DesktopBackendConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,10 @@ describe("DesktopBackendConfiguration", () => {
observedProbeRoots.push(root);
return { ok: true, resolvedPath };
},
// The staged runtime carries its own Node, so the preflight must not
// go looking for one in the distro.
// The staged runtime carries its own Node and node-pty, so it must
// not require the mounted server tree's native dependency check.
ensureNodePty: () => {
throw new Error("the staged runtime must not probe for Node");
throw new Error("the staged runtime must not probe for node-pty");
},
}),
},
Expand Down Expand Up @@ -853,10 +853,14 @@ describe("DesktopBackendConfiguration", () => {
const previousWslEnv = process.env.WSLENV;
const previousOpenAiKey = process.env.OPENAI_API_KEY;
const previousAnthropicKey = process.env.ANTHROPIC_API_KEY;
const previousOtlpHeaders = process.env.T3CODE_OTLP_HEADERS;
const previousOtlpProtocol = process.env.T3CODE_OTLP_PROTOCOL;
try {
process.env.WSLENV = "GOPATH/p:OPENAI_API_KEY/u:EMPTY::AZURE_DEVOPS_EXT_PAT/u";
process.env.OPENAI_API_KEY = "openai-key";
process.env.ANTHROPIC_API_KEY = "anthropic-key";
process.env.T3CODE_OTLP_HEADERS = 'authorization="Bearer%20my-token"';
process.env.T3CODE_OTLP_PROTOCOL = "http/protobuf";

yield* Effect.gen(function* () {
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
Expand All @@ -876,13 +880,14 @@ describe("DesktopBackendConfiguration", () => {
assert.equal(config.httpBaseUrl.href, "http://172.27.0.99:5050/");
assert.equal(config.env.OPENAI_API_KEY, "openai-key");
assert.equal(config.env.ANTHROPIC_API_KEY, "anthropic-key");
assert.equal(config.env.T3CODE_OTLP_PROTOCOL, "http/protobuf");
// The existing WSLENV is preserved byte-for-byte (note the empty
// "::" segment survives — WSL ignores it, so we don't normalize
// it away) and ANTHROPIC_API_KEY is appended. OPENAI_API_KEY is
// already declared, so it isn't forwarded twice.
assert.equal(
config.env.WSLENV,
"GOPATH/p:OPENAI_API_KEY/u:EMPTY::AZURE_DEVOPS_EXT_PAT/u:ANTHROPIC_API_KEY",
"GOPATH/p:OPENAI_API_KEY/u:EMPTY::AZURE_DEVOPS_EXT_PAT/u:ANTHROPIC_API_KEY:T3CODE_OTLP_HEADERS:T3CODE_OTLP_PROTOCOL",
);
}).pipe(
Effect.provide(
Expand All @@ -905,6 +910,8 @@ describe("DesktopBackendConfiguration", () => {
restoreEnv("WSLENV", previousWslEnv);
restoreEnv("OPENAI_API_KEY", previousOpenAiKey);
restoreEnv("ANTHROPIC_API_KEY", previousAnthropicKey);
restoreEnv("T3CODE_OTLP_HEADERS", previousOtlpHeaders);
restoreEnv("T3CODE_OTLP_PROTOCOL", previousOtlpProtocol);
}
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);
Expand Down
17 changes: 11 additions & 6 deletions apps/desktop/src/backend/DesktopBackendConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,16 @@ const DESKTOP_BACKEND_ENV_NAMES = [
"T3CODE_TAILSCALE_SERVE_PORT",
] as const;

// Sensitive env vars that the WSL backend needs but Windows process.env won't
// forward across the wsl.exe boundary without WSLENV. The dev-server URL is
// handled separately via a `--dev-url` CLI flag because WSLENV translation of
// Env vars that the WSL backend needs but Windows process.env won't forward
// across the wsl.exe boundary without WSLENV. The dev-server URL is handled
// separately via a `--dev-url` CLI flag because WSLENV translation of
// URL-shaped values (colons / slashes) is unreliable.
const WSL_FORWARDED_ENV_NAMES = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"] as const;
const WSL_FORWARDED_ENV_NAMES = [
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"T3CODE_OTLP_HEADERS",
"T3CODE_OTLP_PROTOCOL",
] as const;

const WSL_SERVER_SYSTEM_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";

Expand Down Expand Up @@ -384,8 +389,8 @@ const runWslPreflight = Effect.fn("desktop.backendConfiguration.wslPreflight")(f
if (input.runtimeArchive !== null) {
const runtime = yield* wslEnv.prepareRuntime(runningDistro, input.runtimeArchive);
if (runtime.ok) {
// The staged runtime is self-contained, so the only question is whether
// it runs here; there is no Node to find or node-pty to load.
// The staged runtime supplies its own Node and node-pty. Provider PATH
// discovery must not require either dependency for runtime readiness.
const stagedProbe = yield* wslEnv.probeRuntime(runningDistro, runtime.linuxAppRoot);
if (stagedProbe.ok) {
yield* wslServerTree.cleanupLegacy;
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/updates/DesktopUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,7 @@ export const make = Effect.gen(function* () {
const { releaseNotes, omittedReleaseCount } = normalizeDesktopUpdateReleaseNotes(
info.releaseNotes,
info.version,
state.channel,
);
yield* setState(
reduceDesktopUpdateStateOnUpdateAvailable(
Expand Down
57 changes: 53 additions & 4 deletions apps/desktop/src/updates/releaseNotes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
"**Full Changelog**: https://github.com/pingdotgg/t3code/compare/old...new",
].join("\n"),
"0.0.36-nightly.20260828.1213",
"nightly",
);

expect(result).toEqual({
Expand Down Expand Up @@ -50,6 +51,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
"<h2>New Contributors</h2><ul><li>@human made their first contribution</li></ul>" +
"<h2>Full Changelog</h2>",
"1.2.3",
"latest",
);

expect(result).toEqual({
Expand All @@ -69,6 +71,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
},
],
"1.2.4",
"latest",
);

expect(result.releaseNotes).toEqual([
Expand All @@ -85,6 +88,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
{ version: "1.2.1", note: "- Older release" },
],
"1.2.3",
"latest",
);

expect(result).toEqual({
Expand All @@ -96,6 +100,42 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
});
});

it("drops releases from other trains before grouping on nightly", () => {
// electron-updater's full changelog is "every version above the running
// one", and preview sorts above nightly, so the preview cuts come first.
const releaseNotes = [
{ version: "0.0.41-preview.20260914.1683", note: "- Maintainer test build" },
{ version: "0.0.41-preview.20260913.1669", note: "- Maintainer test build" },
{ version: "0.0.41-nightly.20260914.1707", note: "- Nightly change 2" },
{ version: "0.0.41-nightly.20260914.1700", note: "- Nightly change 1" },
];

const result = normalizeDesktopUpdateReleaseNotes(
releaseNotes,
"0.0.41-nightly.20260914.1707",
"nightly",
);

expect(result.releaseNotes.map(({ version }) => version)).toEqual([
"0.0.41-nightly.20260914.1707",
"0.0.41-nightly.20260914.1700",
]);
expect(result.omittedReleaseCount).toBe(0);
});

it("keeps only stable releases on the latest channel", () => {
const result = normalizeDesktopUpdateReleaseNotes(
[
{ version: "0.0.42", note: "- Stable change" },
{ version: "0.0.42-nightly.20260915.1710", note: "- Nightly change" },
],
"0.0.42",
"latest",
);

expect(result.releaseNotes.map(({ version }) => version)).toEqual(["0.0.42"]);
});

it("counts valid groups before applying the six-release limit", () => {
const releaseNotes = [
{ version: "1.3.9", note: "- Change 9" },
Expand All @@ -108,7 +148,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
{ version: "1.3.2", note: "- Change 2" },
];

const result = normalizeDesktopUpdateReleaseNotes(releaseNotes, "1.3.9");
const result = normalizeDesktopUpdateReleaseNotes(releaseNotes, "1.3.9", "latest");

expect(result.releaseNotes.map(({ version }) => version)).toEqual([
"1.3.9",
Expand All @@ -122,7 +162,11 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
});

it("decodes valid HTML entities", () => {
const result = normalizeDesktopUpdateReleaseNotes("- Fix &amp; polish &#128512;", "1.0.0");
const result = normalizeDesktopUpdateReleaseNotes(
"- Fix &amp; polish &#128512;",
"1.0.0",
"latest",
);
expect(result).toEqual({
releaseNotes: [{ version: "1.0.0", items: ["Fix & polish 😀"], totalItems: 1 }],
omittedReleaseCount: 0,
Expand All @@ -140,6 +184,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
null,
],
"1.2.3",
"latest",
);

expect(result).toEqual({
Expand All @@ -149,14 +194,18 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
});

it("returns an empty result for an invalid payload", () => {
expect(normalizeDesktopUpdateReleaseNotes({ note: "- Invalid" }, "1.0.0")).toEqual({
expect(normalizeDesktopUpdateReleaseNotes({ note: "- Invalid" }, "1.0.0", "latest")).toEqual({
releaseNotes: [],
omittedReleaseCount: 0,
});
});

it("does not throw on out-of-range numeric entities and keeps the literal", () => {
const result = normalizeDesktopUpdateReleaseNotes("- Broken entity &#9999999999;", "1.0.0");
const result = normalizeDesktopUpdateReleaseNotes(
"- Broken entity &#9999999999;",
"1.0.0",
"latest",
);
expect(result).toEqual({
releaseNotes: [{ version: "1.0.0", items: ["Broken entity &#9999999999;"], totalItems: 1 }],
omittedReleaseCount: 0,
Expand Down
Loading
Loading