Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
4739b5c
feat(web): redesign settings and setup surfaces
maria-rcks Oct 31, 2026
442f81e
refactor(web): share diagnostics statistic tooltip
maria-rcks Oct 31, 2026
76e5aa5
chore(web): remove obsolete lint ceiling entries
maria-rcks Oct 31, 2026
c99b75a
fix(web): keep snooze fields inside narrow settings panes
maria-rcks Oct 31, 2026
98f3d1c
fix(web): preserve room for snooze amounts on mobile
maria-rcks Oct 31, 2026
e95e3e9
fix(web): give settings a floating panel shell
maria-rcks Oct 31, 2026
1128d25
fix(web): scope settings navigation surface and insets
maria-rcks Oct 31, 2026
83f5b1f
fix(web): match settings scrim and retain mobile scope labels
maria-rcks Oct 31, 2026
9f28605
fix(web): match settings content spacing and defer shortcut editors
maria-rcks Oct 31, 2026
66730c3
perf(web): share shortcut tooltip and skip offscreen row layout
maria-rcks Oct 31, 2026
f343184
perf(web): defer offscreen shortcut controls until visible
maria-rcks Oct 31, 2026
bedf717
fix(web): align diagnostics overview cards with settings design
maria-rcks Oct 31, 2026
0d301a5
fix(web): retain shortcut subgrid alignment when deferring controls
maria-rcks Oct 31, 2026
5a450bc
fix(web): match settings page content to design frames
maria-rcks Oct 31, 2026
ef32dc3
fix(web): use compact project choices and browser selects
maria-rcks Oct 31, 2026
cb6f1c3
fix(server): let partial session scans search farther on refresh
maria-rcks Oct 31, 2026
fc177d2
fix(web): align system dialogs and restore guided setup
maria-rcks Oct 31, 2026
64d69f1
fix(web): keep all configured agents reachable during setup
maria-rcks Oct 31, 2026
3df59fb
fix(web): keep setup actions visible and project names readable
maria-rcks Oct 31, 2026
ac90ee3
fix(web): use provider tile size in publish dialog
maria-rcks Oct 31, 2026
1137e6b
fix(web): align pull request linking dialog with design
maria-rcks Oct 31, 2026
f637a28
fix(web): match link dialog title and action dimensions
maria-rcks Oct 31, 2026
6e5beff
fix(web): preserve intrinsic link dialog action widths
maria-rcks Oct 31, 2026
8579d30
fix(web): align settings rows and defer offscreen shortcut layout
maria-rcks Oct 31, 2026
dc0017b
fix(web): retain the deferred shortcut grid layout
maria-rcks Oct 31, 2026
b1f3b75
fix(web): keep publish provider names readable in one row
maria-rcks Oct 31, 2026
52e49a4
fix(web): position pull request linking within content
maria-rcks Oct 31, 2026
b8a7b27
fix(web): account for both content margins when centering
maria-rcks Oct 31, 2026
e40d33f
fix(web): restore settings scope sentence and page breadcrumb
maria-rcks Oct 31, 2026
adf4058
fix(web): wrap settings scope labels on narrow screens
maria-rcks Oct 31, 2026
4739c7d
fix(web): settings stay dark in the Iris, Ember, Ocean and Grove them…
flamboh Nov 2, 2026
d24bab9
fix(web): the current thread row in Lineage nests inside its card (#3…
SunkenInTime Nov 3, 2026
0411bfc
fix(web): the project picker's computer icon lines up with the other …
SunkenInTime Nov 3, 2026
f8d2d38
fix(web): the content pane sits evenly in the window and follows the …
SunkenInTime Nov 3, 2026
acf5c93
fix(desktop): restore main titlebar insets after pane geometry
maria-rcks Nov 3, 2026
e827c32
fix(web): keep working row hooks ahead of the setup card early return
maria-rcks Nov 5, 2026
e4f7529
fix(web): find the pairing button by its redesigned name in the heade…
maria-rcks Nov 6, 2026
8021dd9
fix(web): a failed settings read shows Retry instead of running on de…
shivamhwp Nov 7, 2026
395ba7f
fix(web): connecting a ChatGPT plan shows its welcome dialog again (#…
shivamhwp Nov 7, 2026
93b52de
fix(web): keep the boot splash until saved settings load
maria-rcks Nov 7, 2026
c0da0a7
fix(web): long project names truncate in the settings project list
maria-rcks Nov 8, 2026
d26b0e6
fix(web): the locked machine chip starts on the composer's text line
maria-rcks Nov 8, 2026
795db45
fix(web): expect the redesigned delete confirmation for archived threads
maria-rcks Nov 9, 2026
74bb004
test(web): drop low-signal tests the redesign added to settings and s…
maria-rcks Nov 9, 2026
275e1bf
refactor(web): welcome imports and connect approval each keep one state
maria-rcks Nov 9, 2026
0d63d3b
fix(web): settings model labels follow main's provider-free trait dis…
maria-rcks Nov 9, 2026
cced585
fix(web): retain main welcome flow in the redesigned setup
maria-rcks Nov 11, 2026
04cb886
fix(web): drop unused setup detail icons
maria-rcks Nov 11, 2026
e1eda97
fix(web): count usable sync actions in the git menu
maria-rcks Nov 11, 2026
7b5aa4f
fix(web): format main integrations in redesigned settings
maria-rcks Nov 11, 2026
759aafd
fix(web): preserve committed retention saves in the settings selector
maria-rcks Nov 11, 2026
69c8cd2
fix(web): clean up replayed settings integrations
maria-rcks Nov 11, 2026
38d2ea9
fix(web): name the preview find shortcut in settings
maria-rcks Nov 11, 2026
eb77513
test(web): assert numeric ages in the retention input
maria-rcks Nov 11, 2026
db6dbce
fix(web): translate the settings search placeholder
maria-rcks Nov 12, 2026
dbd8da5
fix(web): publish dialog's failure banner uses the shared error alert…
shivamhwp Nov 12, 2026
cf52be0
test(web): fold the update-advisory banner case and drop a label-copy…
shivamhwp Nov 12, 2026
8196901
fix(web): the branch picker opens its pull request rows again and the…
maria-rcks Nov 12, 2026
319f31f
fix(web): main's kept commit message, publish IME guard and checkout …
maria-rcks Nov 14, 2026
7397717
test(web): follow redesigned settings controls and isolate import RPCs
maria-rcks Nov 14, 2026
78d1a30
fix(web): settings lists keep main's dividers and the project chooser…
maria-rcks Nov 15, 2026
d8f3389
fix: count only new imports in the pacing test and format the samplin…
maria-rcks Nov 15, 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 @@ -163,6 +163,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 @@ -260,6 +260,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 @@ -171,6 +171,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
29 changes: 29 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 {
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
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 @@ -281,6 +282,7 @@ const makeImporter = Effect.fn(function* (
run: importer.importRecentAgentThreads({ projectId }),
importNativeHistory: importer.importNativeHistory,
published,
importer,
};
});

Expand Down Expand Up @@ -1051,3 +1053,30 @@ it.effect("repeated imports reach linked-worktree sessions behind the scan budge
expect(oldest.worktreePath).toBe(worktree);
}).pipe(Effect.scoped, Effect.provide(Layer.merge(persistence, NodeServices.layer))),
);

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;
// Already-imported threads count as neither imported nor skipped.
yield* importer.importRecentAgentThreads({ projectId }, report);
expect(progress.map(({ processedCount }) => processedCount)).toEqual([0]);
}).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 @@ -24,6 +24,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 @@ -47,6 +48,8 @@ const isProjectionStoreThreadNotFoundError = Schema.is(
ProjectionStore.ProjectionStoreThreadNotFoundError,
);

/** 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 @@ -205,8 +208,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 @@ -266,6 +273,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 @@ -492,7 +509,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 @@ -1235,6 +1235,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