Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
07e8509
feat(web): redesign settings and setup surfaces
maria-rcks Oct 31, 2026
77196bf
refactor(web): share diagnostics statistic tooltip
maria-rcks Oct 31, 2026
01335b6
chore(web): remove obsolete lint ceiling entries
maria-rcks Oct 31, 2026
e88b658
fix(web): keep snooze fields inside narrow settings panes
maria-rcks Oct 31, 2026
d416bf2
fix(web): preserve room for snooze amounts on mobile
maria-rcks Oct 31, 2026
2dfac98
fix(web): give settings a floating panel shell
maria-rcks Oct 31, 2026
6016bee
fix(web): scope settings navigation surface and insets
maria-rcks Oct 31, 2026
f0d0365
fix(web): match settings scrim and retain mobile scope labels
maria-rcks Oct 31, 2026
20ea3a8
fix(web): match settings content spacing and defer shortcut editors
maria-rcks Oct 31, 2026
6eb7091
perf(web): share shortcut tooltip and skip offscreen row layout
maria-rcks Oct 31, 2026
bd8ab7e
perf(web): defer offscreen shortcut controls until visible
maria-rcks Oct 31, 2026
588062a
fix(web): align diagnostics overview cards with settings design
maria-rcks Oct 31, 2026
2ca933d
fix(web): retain shortcut subgrid alignment when deferring controls
maria-rcks Oct 31, 2026
a4bb8c7
fix(web): match settings page content to design frames
maria-rcks Oct 31, 2026
1c4dcef
fix(web): use compact project choices and browser selects
maria-rcks Oct 31, 2026
fcddec7
fix(server): let partial session scans search farther on refresh
maria-rcks Oct 31, 2026
3135b11
fix(web): align system dialogs and restore guided setup
maria-rcks Oct 31, 2026
4a65cd4
fix(web): keep all configured agents reachable during setup
maria-rcks Oct 31, 2026
034e62f
fix(web): keep setup actions visible and project names readable
maria-rcks Oct 31, 2026
f66740a
fix(web): use provider tile size in publish dialog
maria-rcks Oct 31, 2026
62cf5fb
fix(web): align pull request linking dialog with design
maria-rcks Oct 31, 2026
6155e5a
fix(web): match link dialog title and action dimensions
maria-rcks Oct 31, 2026
26fe04d
fix(web): preserve intrinsic link dialog action widths
maria-rcks Oct 31, 2026
24626f7
fix(web): align settings rows and defer offscreen shortcut layout
maria-rcks Oct 31, 2026
3ae7c01
fix(web): retain the deferred shortcut grid layout
maria-rcks Oct 31, 2026
6af850e
fix(web): keep publish provider names readable in one row
maria-rcks Oct 31, 2026
aaee536
fix(web): position pull request linking within content
maria-rcks Oct 31, 2026
3097399
fix(web): account for both content margins when centering
maria-rcks Oct 31, 2026
6613e94
fix(web): restore settings scope sentence and page breadcrumb
maria-rcks Oct 31, 2026
01a9193
fix(web): wrap settings scope labels on narrow screens
maria-rcks Oct 31, 2026
dfbc4c2
fix(web): settings stay dark in the Iris, Ember, Ocean and Grove them…
flamboh Nov 2, 2026
d1c1dd6
fix(web): the current thread row in Lineage nests inside its card (#3…
SunkenInTime Nov 3, 2026
6191f4e
fix(web): the project picker's computer icon lines up with the other …
SunkenInTime Nov 3, 2026
9ee4ad2
fix(web): the content pane sits evenly in the window and follows the …
SunkenInTime Nov 3, 2026
d821b09
fix(desktop): restore main titlebar insets after pane geometry
maria-rcks Nov 3, 2026
d946db6
fix(web): keep working row hooks ahead of the setup card early return
maria-rcks Nov 5, 2026
72bc427
fix(web): find the pairing button by its redesigned name in the heade…
maria-rcks Nov 6, 2026
0e74d49
fix(web): a failed settings read shows Retry instead of running on de…
shivamhwp Nov 7, 2026
c023319
fix(web): connecting a ChatGPT plan shows its welcome dialog again (#…
shivamhwp Nov 7, 2026
e9051a4
fix(web): keep the boot splash until saved settings load
maria-rcks Nov 7, 2026
cee2054
fix(web): long project names truncate in the settings project list
maria-rcks Nov 8, 2026
2067003
fix(web): the locked machine chip starts on the composer's text line
maria-rcks Nov 8, 2026
87a570d
fix(web): expect the redesigned delete confirmation for archived threads
maria-rcks Nov 9, 2026
83114cb
test(web): drop low-signal tests the redesign added to settings and s…
maria-rcks Nov 9, 2026
c750e24
refactor(web): welcome imports and connect approval each keep one state
maria-rcks Nov 9, 2026
8dc6625
fix(web): settings model labels follow main's provider-free trait dis…
maria-rcks Nov 9, 2026
c114483
fix(web): retain main welcome flow in the redesigned setup
maria-rcks Nov 11, 2026
cbda675
fix(web): drop unused setup detail icons
maria-rcks Nov 11, 2026
07fa8dc
fix(web): count usable sync actions in the git menu
maria-rcks Nov 11, 2026
5100e9f
fix(web): format main integrations in redesigned settings
maria-rcks Nov 11, 2026
b39af49
fix(web): preserve committed retention saves in the settings selector
maria-rcks Nov 11, 2026
020c082
fix(web): clean up replayed settings integrations
maria-rcks Nov 11, 2026
cae4ca8
fix(web): name the preview find shortcut in settings
maria-rcks Nov 11, 2026
34d194a
test(web): assert numeric ages in the retention input
maria-rcks Nov 11, 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
21 changes: 14 additions & 7 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,23 @@ try {
// oxlint-disable-next-line t3code/no-global-process-runtime -- Electron exposes the client platform in its sandboxed preload process.
const clientPlatform = process.platform;

const macosMajorVersion =
clientPlatform === "darwin" ? Number.parseInt(process.getSystemVersion()) : 0;

if (clientPlatform === "darwin") {
// Native buttons ignore Chromium zoom; reserve native points even after reload.
const syncWindowControlInset = () => {
document.documentElement.style.setProperty(
"--desktop-window-controls-inset",
`${30 + 67 / webFrame.getZoomFactor()}px`,
);
const syncWindowGeometry = () => {
const rootStyle = document.documentElement.style;
const zoomFactor = webFrame.getZoomFactor();
rootStyle.setProperty("--desktop-window-controls-inset", `${90 / zoomFactor}px`);
// Measured from macOS 27 window captures; earlier releases draw other corners.
if (macosMajorVersion >= 27) {
rootStyle.setProperty("--desktop-window-corner-radius", `${31.5 / zoomFactor}px`);
rootStyle.setProperty("--desktop-window-corner-shape", "superellipse(2.12)");
}
};
window.addEventListener("DOMContentLoaded", syncWindowControlInset, { once: true });
window.addEventListener("resize", syncWindowControlInset);
window.addEventListener("DOMContentLoaded", syncWindowGeometry, { once: true });
window.addEventListener("resize", syncWindowGeometry);
}

const syncHtmlFullscreen = () => {
Expand Down
3 changes: 1 addition & 2 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ const clientSettings: ClientSettings = {
fontSizePrompt: 14,
fontSizeTerminal: 12,
fontSmoothing: true,
onboardingCompletedAt: null,
panelAnimationDurationMs: 0,
proactivePanelsEnabled: true,
showSkillsInSlashMenu: false,
Expand Down Expand Up @@ -174,7 +173,7 @@ describe("DesktopClientSettings", () => {
const settings = yield* DesktopClientSettings.DesktopClientSettings;
const savedSettings = {
...clientSettings,
onboardingCompletedAt: "2026-09-05T12:00:00.000Z",
fontSmoothing: false,
};
yield* settings.set(savedSettings);
const savedContents = yield* fileSystem.readFileString(environment.clientSettingsPath);
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export const RPC_REQUIRED_SCOPES = {
[WS_METHODS.filesystemGetMetadata]: AuthFilesystemReadScope,
[WS_METHODS.agentSessionsScan]: AuthOrchestrationReadScope,
[WS_METHODS.agentSessionsImport]: AuthOrchestrationOperateScope,
[WS_METHODS.agentSessionsImportWithProgress]: AuthOrchestrationOperateScope,
[WS_METHODS.assetsCreateUrl]: AuthOrchestrationReadScope,
[WS_METHODS.assetsPersistChatAttachments]: AuthOrchestrationOperateScope,
[WS_METHODS.attachmentsCreateUploadUrl]: AuthOrchestrationOperateScope,
Expand Down
34 changes: 30 additions & 4 deletions apps/server/src/cloud/CliTokenManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// @effect-diagnostics nodeBuiltinImport:off - The CLI loopback OAuth callback is a Node HTTP boundary.
import * as NodeHttp from "node:http";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";

import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
import * as Clock from "effect/Clock";
Expand All @@ -26,7 +28,10 @@ import * as HttpRouter from "effect/http/HttpRouter";
import * as HttpServerRequest from "effect/http/HttpServerRequest";
import * as HttpServerResponse from "effect/http/HttpServerResponse";

import { buildConnectAuthorizeRequestUrl } from "@t3tools/shared/connectAuth";
import {
buildConnectAuthorizeRequestUrl,
connectAuthorizeRequestCode,
} from "@t3tools/shared/connectAuth";

import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
import * as ExternalLauncher from "../process/externalLauncher.ts";
Expand All @@ -49,16 +54,24 @@ const DEVICE_AUTHORIZATION_SLOW_DOWN_INCREMENT = Duration.seconds(5);
const OAUTH_REQUEST_TIMEOUT = Duration.seconds(10);
const boldTerminalText = (value: string): string => `\u001b[1m${value}\u001b[22m`;

function formatLoopbackAuthorizationPrompt(authorizationUrl: string): string {
function formatLoopbackAuthorizationPrompt(authorizationUrl: string, code: string): string {
return [
"Open this URL to authorize T3 Connect:",
` ${authorizationUrl}`,
"",
`The browser shows this code: ${boldTerminalText(code)}`,
"",
`Press ${boldTerminalText("Enter")} to open it in your browser.`,
`No browser on this device? Press ${boldTerminalText("H")} to switch to headless mode.`,
].join("\n");
}

/** "host · shell" label the hosted /connect page shows for this terminal. */
function describeCliDevice(): string {
const shell = process.env.SHELL ? NodePath.basename(process.env.SHELL) : "";
return [NodeOS.hostname(), shell].filter((part) => part.length > 0).join(" · ");
}

export type LoopbackAuthorizationResult =
| { readonly _tag: "AuthorizationCode"; readonly code: string }
| { readonly _tag: "HeadlessRequested" };
Expand Down Expand Up @@ -513,13 +526,23 @@ export const make = Effect.gen(function* () {
const metadata = yield* cloudCliOAuthConfig;
const hostedAppUrl = yield* hostedAppUrlConfig;
const { verifier, challenge, state } = yield* makePkceRequest;
const callback = yield* Deferred.make<string>();
const callback = yield* Deferred.make<string, CloudCliAuthorizationDeniedError>();
const layerCallbackRoute = HttpRouter.add(
"GET",
"/callback",
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest;
const url = new URL(request.originalUrl, metadata.redirectUri);
// Cancel on the hosted /connect page returns here with access_denied.
if (
url.searchParams.get("state") === state &&
url.searchParams.get("error") === "access_denied"
) {
yield* Deferred.fail(callback, new CloudCliAuthorizationDeniedError());
return HttpServerResponse.text(
"T3 Connect authorization was cancelled. You can close this tab.",
);
}
const code = url.searchParams.get("code");
if (url.searchParams.get("state") !== state || !code) {
return HttpServerResponse.text("Invalid T3 Connect authorization callback.", {
Expand Down Expand Up @@ -549,8 +572,11 @@ export const make = Effect.gen(function* () {
state,
challenge,
loopbackPort: metadata.loopbackPort,
device: describeCliDevice(),
});
yield* Console.log(formatLoopbackAuthorizationPrompt(authorizationUrl));
yield* Console.log(
formatLoopbackAuthorizationPrompt(authorizationUrl, connectAuthorizeRequestCode(state)),
);
const authorization = yield* waitForLoopbackAuthorization({
authorizationUrl,
callback: Deferred.await(callback).pipe(
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ export const make = Effect.gen(function* () {
threadActiveReorder: true,
threadAutoSettleOptOut: true,
threadProjectFromScratch: true,
agentSessionImportProgress: true,
threadTitleRegeneration: true,
commandOutputStreaming: true,
threadVisitedTracking: true,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/observability/RpcInstrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ const RPC_AGGREGATES = {
[WS_METHODS.filesystemGetMetadata]: "workspace",
[WS_METHODS.agentSessionsScan]: "workspace",
[WS_METHODS.agentSessionsImport]: "workspace",
[WS_METHODS.agentSessionsImportWithProgress]: "workspace",
[WS_METHODS.assetsCreateUrl]: "workspace",
[WS_METHODS.assetsPersistChatAttachments]: "orchestration",
[WS_METHODS.attachmentsCreateUploadUrl]: "workspace",
Expand Down
28 changes: 28 additions & 0 deletions apps/server/src/project/AgentSessionImporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
import * as KeyedLock from "@t3tools/shared/KeyedLock";
import * as AcpRegistryRuntimeCoordinator from "@t3tools/provider-acp-registry/server/AcpRegistryRuntimeCoordinator";
import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
import * as Clock from "effect/Clock";
import * as Context from "effect/Context";
import * as DateTime from "effect/DateTime";
import * as Deferred from "effect/Deferred";
Expand Down Expand Up @@ -270,6 +271,7 @@ const makeImporter = Effect.fn(function* (
run: importer.importRecentAgentThreads({ projectId }),
importNativeHistory: importer.importNativeHistory,
published,
importer,
};
});

Expand Down Expand Up @@ -788,3 +790,29 @@ it.effect("imports under a policy ceiling in the project's default, kept under t
);
}).pipe(Effect.scoped, Effect.provide(persistence)),
);

it.effect("paces progress per import while preserving processed counts", () =>
Effect.gen(function* () {
const { importer } = yield* makeImporter(
Array.from({ length: 5 }, (_, index) => candidate(CODEX_MODEL_SELECTION, `paced-${index}`)),
TestClock.adjust(125),
);
const progress: Array<{ processedCount: number; at: number }> = [];
const report = (processedCount: number) =>
Clock.currentTimeMillis.pipe(
Effect.tap((at) =>
Effect.sync(() => {
progress.push({ processedCount, at });
}),
),
Effect.asVoid,
);
const result = yield* importer.importRecentAgentThreads({ projectId }, report);
expect(result).toEqual({ importedCount: 5, skippedCount: 0 });
expect(progress.map(({ processedCount }) => processedCount)).toEqual([1, 3, 5]);
expect(progress.slice(1).map(({ at }, index) => at - progress[index]!.at)).toEqual([250, 250]);
progress.length = 0;
yield* importer.importRecentAgentThreads({ projectId }, report);
expect(progress.map(({ processedCount }) => processedCount)).toEqual([1]);
}).pipe(Effect.scoped, Effect.provide(persistence)),
);
19 changes: 18 additions & 1 deletion apps/server/src/project/AgentSessionImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from "@t3tools/contracts";
import { normalizeProjectPathForComparison } from "@t3tools/shared/path";
import { resolveProjectSettings } from "@t3tools/shared/projectSettings";
import * as Clock from "effect/Clock";
import * as Context from "effect/Context";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
Expand All @@ -43,6 +44,8 @@ import * as ServerSettings from "../serverSettings.ts";
import * as AgentSessionScanner from "./AgentSessionScanner.ts";
import * as ProjectService from "./ProjectService.ts";

/** Onboarding import counts are a label; a few updates a second is plenty. */
const IMPORT_PROGRESS_INTERVAL_MS = 250;
const IMPORT_EVENT_PREFIX = "agent-session-import:v2";
const CLAUDE_SESSION_ID_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
Expand Down Expand Up @@ -207,8 +210,12 @@ const make = Effect.gen(function* () {
const threadCommands = yield* ThreadCommandExecutor.ThreadCommandExecutor;
const settingsService = yield* ServerSettings.ServerSettingsService;
const ceiling = settingsService.managedPolicy?.limits?.maxRuntimeMode;
/**
* `onProgress` hears the running imported + skipped count at most every 250 ms.
*/
const importRecentAgentThreads = Effect.fn("importRecentAgentThreadsV2")(function* (
input: AgentSessionImportInput,
onProgress?: (processedCount: number) => Effect.Effect<void>,
) {
const project = yield* projects.getById(input.projectId).pipe(
Effect.mapError((cause) => new AgentSessionScanError({ operation: "read-projects", cause })),
Expand Down Expand Up @@ -265,6 +272,16 @@ const make = Effect.gen(function* () {
const importedThreadIds = new Set<ThreadId>();
let importedCount = 0;
let skippedCount = 0;
let lastReportedAt = Number.NEGATIVE_INFINITY;
const reportProgress =
onProgress === undefined
? Effect.void
: Effect.gen(function* () {
const now = yield* Clock.currentTimeMillis;
if (now - lastReportedAt < IMPORT_PROGRESS_INTERVAL_MS) return;
lastReportedAt = now;
yield* onProgress(importedCount + skippedCount);
});

yield* Stream.runForEach(outcomes, (outcome) =>
Effect.gen(function* () {
Expand Down Expand Up @@ -476,7 +493,7 @@ const make = Effect.gen(function* () {
} else if (imported === false) {
skippedCount += 1;
}
}),
}).pipe(Effect.andThen(reportProgress)),
);

return { importedCount, skippedCount } satisfies AgentSessionImportResult;
Expand Down
53 changes: 53 additions & 0 deletions apps/server/src/project/AgentSessionScanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,59 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => {
}),
);

it.effect(
"refreshes a partial scan with a larger record budget and keeps recovered projects",
() =>
Effect.gen(function* () {
const path = yield* Path.Path;
const claudeHomePath = yield* makeTempDir("t3code-claude-home-");
const codexHomePath = yield* makeTempDir("t3code-codex-home-");
const workspace = yield* makeTempDir("t3code-workspace-");
yield* writeTranscript({
filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"),
contents: '{"type":"summary"}\n'.repeat(1_001) + claudeSessionLine(workspace),
mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"),
});

yield* Effect.gen(function* () {
const scanner = yield* AgentSessionScanner.AgentSessionScanner;
const initial = yield* scanner.scan;
expect(initial.truncated).toBe(true);
expect(initial.candidates).toEqual([]);
const recovered = yield* scanner.scan;
expect(recovered.truncated).not.toBe(true);
expect(recovered.candidates.map((candidate) => candidate.path)).toEqual([workspace]);
const refreshed = yield* scanner.scan;
expect(refreshed.candidates.map((candidate) => candidate.path)).toEqual([workspace]);
}).pipe(Effect.provide(layerScannerTest({ claudeHomePath, codexHomePath })));
}),
);

it.effect("refreshes a partial scan with a larger total byte budget", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
const claudeHomePath = yield* makeTempDir("t3code-claude-home-");
const codexHomePath = yield* makeTempDir("t3code-codex-home-");
const workspace = yield* makeTempDir("t3code-workspace-");
const record = claudeSessionLine(workspace);
const contents = " ".repeat(1024 * 1024 - record.length) + record;
for (let index = 0; index < 65; index += 1) {
yield* writeTranscript({
filePath: path.join(claudeHomePath, "projects", "-slug", `${index}.jsonl`),
contents,
mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"),
});
}
yield* Effect.gen(function* () {
const scanner = yield* AgentSessionScanner.AgentSessionScanner;
expect((yield* scanner.scan).truncated).toBe(true);
const recovered = yield* scanner.scan;
expect(recovered.truncated).not.toBe(true);
expect(recovered.candidates.map((candidate) => candidate.path)).toEqual([workspace]);
}).pipe(Effect.provide(layerScannerTest({ claudeHomePath, codexHomePath })));
}),
);

it.effect("reads a complete transcript record at the exact chunk boundary", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
Expand Down
Loading
Loading