From 774cff9a74bbd937021e27da524b96dbb7319a7c Mon Sep 17 00:00:00 2001
From: 0x1f99d
Date: Fri, 13 Mar 2026 03:10:07 +1100
Subject: [PATCH 01/40] ci(github): add pull request size labels (#901)
---
.github/workflows/pr-size.yml | 166 +++++++++++++++++++++++++++++++++
.github/workflows/pr-vouch.yml | 58 ++++++++----
CONTRIBUTING.md | 2 +-
3 files changed, 208 insertions(+), 18 deletions(-)
create mode 100644 .github/workflows/pr-size.yml
diff --git a/.github/workflows/pr-size.yml b/.github/workflows/pr-size.yml
new file mode 100644
index 0000000000000..7a338a76ea1d2
--- /dev/null
+++ b/.github/workflows/pr-size.yml
@@ -0,0 +1,166 @@
+name: PR Size
+
+on:
+ pull_request_target:
+ types: [opened, reopened, synchronize, ready_for_review, converted_to_draft]
+
+permissions:
+ contents: read
+ issues: write
+ pull-requests: read
+
+jobs:
+ label:
+ name: Label PR size
+ runs-on: ubuntu-24.04
+ concurrency:
+ group: pr-size-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+ steps:
+ - name: Sync PR size label
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const issueNumber = context.payload.pull_request.number;
+ const additions = context.payload.pull_request.additions ?? 0;
+ const deletions = context.payload.pull_request.deletions ?? 0;
+ const changedLines = additions + deletions;
+
+ const managedLabels = [
+ {
+ name: "size:XS",
+ color: "0e8a16",
+ description: "0-9 changed lines (additions + deletions).",
+ },
+ {
+ name: "size:S",
+ color: "5ebd3e",
+ description: "10-29 changed lines (additions + deletions).",
+ },
+ {
+ name: "size:M",
+ color: "fbca04",
+ description: "30-99 changed lines (additions + deletions).",
+ },
+ {
+ name: "size:L",
+ color: "fe7d37",
+ description: "100-499 changed lines (additions + deletions).",
+ },
+ {
+ name: "size:XL",
+ color: "d93f0b",
+ description: "500-999 changed lines (additions + deletions).",
+ },
+ {
+ name: "size:XXL",
+ color: "b60205",
+ description: "1,000+ changed lines (additions + deletions).",
+ },
+ ];
+
+ const managedLabelNames = new Set(managedLabels.map((label) => label.name));
+
+ const resolveSizeLabel = (totalChangedLines) => {
+ if (totalChangedLines < 10) {
+ return "size:XS";
+ }
+
+ if (totalChangedLines < 30) {
+ return "size:S";
+ }
+
+ if (totalChangedLines < 100) {
+ return "size:M";
+ }
+
+ if (totalChangedLines < 500) {
+ return "size:L";
+ }
+
+ if (totalChangedLines < 1000) {
+ return "size:XL";
+ }
+
+ return "size:XXL";
+ };
+
+ for (const label of managedLabels) {
+ try {
+ const { data: existing } = await github.rest.issues.getLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ name: label.name,
+ });
+
+ if (
+ existing.color !== label.color ||
+ (existing.description ?? "") !== label.description
+ ) {
+ await github.rest.issues.updateLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ name: label.name,
+ color: label.color,
+ description: label.description,
+ });
+ }
+ } catch (error) {
+ if (error.status !== 404) {
+ throw error;
+ }
+
+ try {
+ await github.rest.issues.createLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ name: label.name,
+ color: label.color,
+ description: label.description,
+ });
+ } catch (createError) {
+ if (createError.status !== 422) {
+ throw createError;
+ }
+ }
+ }
+ }
+
+ const nextLabelName = resolveSizeLabel(changedLines);
+
+ const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issueNumber,
+ per_page: 100,
+ });
+
+ for (const label of currentLabels) {
+ if (!managedLabelNames.has(label.name) || label.name === nextLabelName) {
+ continue;
+ }
+
+ try {
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issueNumber,
+ name: label.name,
+ });
+ } catch (removeError) {
+ if (removeError.status !== 404) {
+ throw removeError;
+ }
+ }
+ }
+
+ if (!currentLabels.some((label) => label.name === nextLabelName)) {
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issueNumber,
+ labels: [nextLabelName],
+ });
+ }
+
+ core.info(`PR #${issueNumber}: ${changedLines} changed lines -> ${nextLabelName}`);
diff --git a/.github/workflows/pr-vouch.yml b/.github/workflows/pr-vouch.yml
index 976a9a097e75b..350a71b6b8a94 100644
--- a/.github/workflows/pr-vouch.yml
+++ b/.github/workflows/pr-vouch.yml
@@ -111,7 +111,7 @@ jobs:
},
];
- const managedLabelNames = managedLabels.map((label) => label.name);
+ const managedLabelNames = new Set(managedLabels.map((label) => label.name));
for (const label of managedLabels) {
try {
@@ -138,13 +138,19 @@ jobs:
throw error;
}
- await github.rest.issues.createLabel({
- owner: context.repo.owner,
- repo: context.repo.repo,
- name: label.name,
- color: label.color,
- description: label.description,
- });
+ try {
+ await github.rest.issues.createLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ name: label.name,
+ color: label.color,
+ description: label.description,
+ });
+ } catch (createError) {
+ if (createError.status !== 422) {
+ throw createError;
+ }
+ }
}
}
@@ -159,17 +165,35 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
+ per_page: 100,
});
- const preservedLabels = currentLabels
- .map((label) => label.name)
- .filter((name) => !managedLabelNames.includes(name));
+ for (const label of currentLabels) {
+ if (!managedLabelNames.has(label.name) || label.name === nextLabelName) {
+ continue;
+ }
- await github.rest.issues.setLabels({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: issueNumber,
- labels: [...preservedLabels, nextLabelName],
- });
+ try {
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issueNumber,
+ name: label.name,
+ });
+ } catch (removeError) {
+ if (removeError.status !== 404) {
+ throw removeError;
+ }
+ }
+ }
+
+ if (!currentLabels.some((label) => label.name === nextLabelName)) {
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issueNumber,
+ labels: [nextLabelName],
+ });
+ }
core.info(`PR #${issueNumber}: ${status} -> ${nextLabelName}`);
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2021ecdcb750f..8b734a99bbb09 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,7 +8,7 @@ You can still open an issue or PR, but please do so knowing there is a high chan
If that sounds annoying, that is because it is. This project is still early and we are trying to keep scope, quality, and direction under control.
-PRs are automatically labeled with a `vouch:*` trust status.
+PRs are automatically labeled with a `vouch:*` trust status and a `size:*` diff size based on changed lines.
If you are an external contributor, expect `vouch:unvouched` until we explicitly add you to [.github/VOUCHED.td](.github/VOUCHED.td).
From a01d712751f8103001b5e728d64c12bc7c3d652a Mon Sep 17 00:00:00 2001
From: Magnus Buvarp
Date: Thu, 12 Mar 2026 18:42:17 +0100
Subject: [PATCH 02/40] fix(web): resolve preferred editor from available
editors & introduce `useLocalStorage` helper (#662)
Co-authored-by: Julius Marminge
---
apps/web/src/appSettings.test.ts | 15 --
apps/web/src/appSettings.ts | 128 ++--------------
apps/web/src/components/ChatMarkdown.tsx | 4 +-
apps/web/src/components/ChatView.logic.ts | 21 +--
apps/web/src/components/ChatView.tsx | 27 +---
apps/web/src/components/DiffPanel.tsx | 5 +-
apps/web/src/components/GitActionsControl.tsx | 5 +-
.../src/components/ThreadTerminalDrawer.tsx | 4 +-
apps/web/src/components/chat/OpenInPicker.tsx | 99 ++++++------
apps/web/src/components/ui/sidebar.tsx | 8 +-
apps/web/src/editorPreferences.ts | 35 +++++
apps/web/src/hooks/useLocalStorage.ts | 143 ++++++++++++++++++
apps/web/src/routes/__root.tsx | 12 +-
apps/web/src/routes/_chat.settings.tsx | 14 +-
apps/web/src/terminal-links.ts | 22 ---
15 files changed, 283 insertions(+), 259 deletions(-)
create mode 100644 apps/web/src/editorPreferences.ts
create mode 100644 apps/web/src/hooks/useLocalStorage.ts
diff --git a/apps/web/src/appSettings.test.ts b/apps/web/src/appSettings.test.ts
index 213e4cd3d41fc..e7e9a67e164f7 100644
--- a/apps/web/src/appSettings.test.ts
+++ b/apps/web/src/appSettings.test.ts
@@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest";
import {
getAppModelOptions,
- getSlashModelOptions,
normalizeCustomModelSlugs,
resolveAppModelSelection,
} from "./appSettings";
@@ -58,17 +57,3 @@ describe("resolveAppModelSelection", () => {
expect(resolveAppModelSelection("codex", [], "")).toBe("gpt-5.4");
});
});
-
-describe("getSlashModelOptions", () => {
- it("includes saved custom model slugs for /model command suggestions", () => {
- const options = getSlashModelOptions("codex", ["custom/internal-model"], "", "gpt-5.3-codex");
-
- expect(options.some((option) => option.slug === "custom/internal-model")).toBe(true);
- });
-
- it("filters slash-model suggestions across built-in and custom model names", () => {
- const options = getSlashModelOptions("codex", ["openai/gpt-oss-120b"], "oss", "gpt-5.3-codex");
-
- expect(options.map((option) => option.slug)).toEqual(["openai/gpt-oss-120b"]);
- });
-});
diff --git a/apps/web/src/appSettings.ts b/apps/web/src/appSettings.ts
index 5ed218fb254e7..e5018e0bfdac2 100644
--- a/apps/web/src/appSettings.ts
+++ b/apps/web/src/appSettings.ts
@@ -1,7 +1,8 @@
-import { useCallback, useSyncExternalStore } from "react";
+import { useCallback } from "react";
import { Option, Schema } from "effect";
import { type ProviderKind } from "@t3tools/contracts";
import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";
+import { useLocalStorage } from "./hooks/useLocalStorage";
const APP_SETTINGS_STORAGE_KEY = "t3code:app-settings:v1";
const MAX_CUSTOM_MODEL_COUNT = 32;
@@ -34,10 +35,6 @@ export interface AppModelOption {
const DEFAULT_APP_SETTINGS = AppSettingsSchema.makeUnsafe({});
-let listeners: Array<() => void> = [];
-let cachedRawSettings: string | null | undefined;
-let cachedSnapshot: AppSettings = DEFAULT_APP_SETTINGS;
-
export function normalizeCustomModelSlugs(
models: Iterable,
provider: ProviderKind = "codex",
@@ -67,13 +64,6 @@ export function normalizeCustomModelSlugs(
return normalizedModels;
}
-function normalizeAppSettings(settings: AppSettings): AppSettings {
- return {
- ...settings,
- customCodexModels: normalizeCustomModelSlugs(settings.customCodexModels, "codex"),
- };
-}
-
export function getAppModelOptions(
provider: ProviderKind,
customModels: readonly string[],
@@ -143,112 +133,26 @@ export function resolveAppModelSelection(
);
}
-export function getSlashModelOptions(
- provider: ProviderKind,
- customModels: readonly string[],
- query: string,
- selectedModel?: string | null,
-): AppModelOption[] {
- const normalizedQuery = query.trim().toLowerCase();
- const options = getAppModelOptions(provider, customModels, selectedModel);
- if (!normalizedQuery) {
- return options;
- }
-
- return options.filter((option) => {
- const searchSlug = option.slug.toLowerCase();
- const searchName = option.name.toLowerCase();
- return searchSlug.includes(normalizedQuery) || searchName.includes(normalizedQuery);
- });
-}
-
-function emitChange(): void {
- for (const listener of listeners) {
- listener();
- }
-}
-
-function parsePersistedSettings(value: string | null): AppSettings {
- if (!value) {
- return DEFAULT_APP_SETTINGS;
- }
-
- try {
- return normalizeAppSettings(Schema.decodeSync(Schema.fromJsonString(AppSettingsSchema))(value));
- } catch {
- return DEFAULT_APP_SETTINGS;
- }
-}
-
-export function getAppSettingsSnapshot(): AppSettings {
- if (typeof window === "undefined") {
- return DEFAULT_APP_SETTINGS;
- }
-
- const raw = window.localStorage.getItem(APP_SETTINGS_STORAGE_KEY);
- if (raw === cachedRawSettings) {
- return cachedSnapshot;
- }
-
- cachedRawSettings = raw;
- cachedSnapshot = parsePersistedSettings(raw);
- return cachedSnapshot;
-}
-
-function persistSettings(next: AppSettings): void {
- if (typeof window === "undefined") return;
-
- const raw = JSON.stringify(next);
- try {
- if (raw !== cachedRawSettings) {
- window.localStorage.setItem(APP_SETTINGS_STORAGE_KEY, raw);
- }
- } catch {
- // Best-effort persistence only.
- }
-
- cachedRawSettings = raw;
- cachedSnapshot = next;
-}
-
-function subscribe(listener: () => void): () => void {
- listeners.push(listener);
-
- const onStorage = (event: StorageEvent) => {
- if (event.key === APP_SETTINGS_STORAGE_KEY) {
- emitChange();
- }
- };
-
- window.addEventListener("storage", onStorage);
- return () => {
- listeners = listeners.filter((entry) => entry !== listener);
- window.removeEventListener("storage", onStorage);
- };
-}
-
export function useAppSettings() {
- const settings = useSyncExternalStore(
- subscribe,
- getAppSettingsSnapshot,
- () => DEFAULT_APP_SETTINGS,
+ const [settings, setSettings] = useLocalStorage(
+ APP_SETTINGS_STORAGE_KEY,
+ DEFAULT_APP_SETTINGS,
+ AppSettingsSchema,
);
- const updateSettings = useCallback((patch: Partial) => {
- const next = normalizeAppSettings(
- Schema.decodeSync(AppSettingsSchema)({
- ...getAppSettingsSnapshot(),
+ const updateSettings = useCallback(
+ (patch: Partial) => {
+ setSettings((prev) => ({
+ ...prev,
...patch,
- }),
- );
- persistSettings(next);
- emitChange();
- }, []);
+ }));
+ },
+ [setSettings],
+ );
const resetSettings = useCallback(() => {
- persistSettings(DEFAULT_APP_SETTINGS);
- emitChange();
- }, []);
+ setSettings(DEFAULT_APP_SETTINGS);
+ }, [setSettings]);
return {
settings,
diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx
index f4298fc22208a..3f8876ec145d1 100644
--- a/apps/web/src/components/ChatMarkdown.tsx
+++ b/apps/web/src/components/ChatMarkdown.tsx
@@ -20,13 +20,13 @@ import React, {
import type { Components } from "react-markdown";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
+import { openInPreferredEditor } from "../editorPreferences";
import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffRendering";
import { fnv1a32 } from "../lib/diffRendering";
import { LRUCache } from "../lib/lruCache";
import { useTheme } from "../hooks/useTheme";
import { resolveMarkdownFileLinkTarget } from "../markdown-links";
import { readNativeApi } from "../nativeApi";
-import { preferredTerminalEditor } from "../terminal-links";
class CodeHighlightErrorBoundary extends React.Component<
{ fallback: ReactNode; children: ReactNode },
@@ -254,7 +254,7 @@ function ChatMarkdown({ text, cwd, isStreaming = false }: ChatMarkdownProps) {
event.stopPropagation();
const api = readNativeApi();
if (api) {
- void api.shell.openInEditor(targetPath, preferredTerminalEditor());
+ void openInPreferredEditor(api, targetPath);
} else {
console.warn("Native API not found. Unable to open file in editor.");
}
diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts
index 4cd64af9f168c..59e2904310605 100644
--- a/apps/web/src/components/ChatView.logic.ts
+++ b/apps/web/src/components/ChatView.logic.ts
@@ -1,29 +1,14 @@
-import { type ProviderKind, type ThreadId } from "@t3tools/contracts";
+import { ProjectId, type ProviderKind, type ThreadId } from "@t3tools/contracts";
import { type ChatMessage, type Thread } from "../types";
import { randomUUID } from "~/lib/utils";
import { getAppModelOptions } from "../appSettings";
import { type ComposerImageAttachment, type DraftThreadState } from "../composerDraftStore";
+import { Schema } from "effect";
export const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "t3code:last-invoked-script-by-project";
const WORKTREE_BRANCH_PREFIX = "t3code";
-export function readLastInvokedScriptByProjectFromStorage(): Record {
- const stored = localStorage.getItem(LAST_INVOKED_SCRIPT_BY_PROJECT_KEY);
- if (!stored) return {};
-
- try {
- const parsed: unknown = JSON.parse(stored);
- if (!parsed || typeof parsed !== "object") return {};
- return Object.fromEntries(
- Object.entries(parsed).filter(
- (entry): entry is [string, string] =>
- typeof entry[0] === "string" && typeof entry[1] === "string",
- ),
- );
- } catch {
- return {};
- }
-}
+export const LastInvokedScriptByProjectSchema = Schema.Record(ProjectId, Schema.String);
export function buildLocalDraftThread(
threadId: ThreadId,
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
index d81e024f3ab31..76a5610ced483 100644
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -148,13 +148,14 @@ import {
collectUserMessageBlobPreviewUrls,
getCustomModelOptionsByProvider,
LAST_INVOKED_SCRIPT_BY_PROJECT_KEY,
+ LastInvokedScriptByProjectSchema,
PullRequestDialogState,
readFileAsDataUrl,
- readLastInvokedScriptByProjectFromStorage,
revokeBlobPreviewUrl,
revokeUserMessagePreviewUrls,
SendPhase,
} from "./ChatView.logic";
+import { useLocalStorage } from "~/hooks/useLocalStorage";
const ATTACHMENT_PREVIEW_HANDOFF_TTL_MS = 5000;
const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`;
@@ -267,9 +268,11 @@ export default function ChatView({ threadId }: ChatViewProps) {
const [composerTrigger, setComposerTrigger] = useState(() =>
detectComposerTrigger(prompt, prompt.length),
);
- const [lastInvokedScriptByProjectId, setLastInvokedScriptByProjectId] = useState<
- Record
- >(() => readLastInvokedScriptByProjectFromStorage());
+ const [lastInvokedScriptByProjectId, setLastInvokedScriptByProjectId] = useLocalStorage(
+ LAST_INVOKED_SCRIPT_BY_PROJECT_KEY,
+ {},
+ LastInvokedScriptByProjectSchema,
+ );
const messagesScrollRef = useRef(null);
const [messagesScrollElement, setMessagesScrollElement] = useState(null);
const shouldAutoScrollRef = useRef(true);
@@ -1196,6 +1199,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
setThreadError,
storeNewTerminal,
storeSetActiveTerminal,
+ setLastInvokedScriptByProjectId,
terminalState.activeTerminalId,
terminalState.runningTerminalIds,
terminalState.terminalIds,
@@ -1438,21 +1442,6 @@ export default function ChatView({ threadId }: ChatViewProps) {
[serverThread],
);
- useEffect(() => {
- try {
- if (Object.keys(lastInvokedScriptByProjectId).length === 0) {
- localStorage.removeItem(LAST_INVOKED_SCRIPT_BY_PROJECT_KEY);
- return;
- }
- localStorage.setItem(
- LAST_INVOKED_SCRIPT_BY_PROJECT_KEY,
- JSON.stringify(lastInvokedScriptByProjectId),
- );
- } catch {
- // Ignore storage write failures (private mode, quota exceeded, etc.)
- }
- }, [lastInvokedScriptByProjectId]);
-
// Auto-scroll on new messages
const messageCount = timelineMessages.length;
const scrollMessagesToBottom = useCallback((behavior: ScrollBehavior = "auto") => {
diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx
index 1293f51f7ed52..b0f1b91c35753 100644
--- a/apps/web/src/components/DiffPanel.tsx
+++ b/apps/web/src/components/DiffPanel.tsx
@@ -12,11 +12,12 @@ import {
useRef,
useState,
} from "react";
+import { openInPreferredEditor } from "../editorPreferences";
import { gitBranchesQueryOptions } from "~/lib/gitReactQuery";
import { checkpointDiffQueryOptions } from "~/lib/providerReactQuery";
import { cn } from "~/lib/utils";
import { readNativeApi } from "../nativeApi";
-import { preferredTerminalEditor, resolvePathLinkTarget } from "../terminal-links";
+import { resolvePathLinkTarget } from "../terminal-links";
import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";
import { isElectron } from "../env";
import { useTheme } from "../hooks/useTheme";
@@ -311,7 +312,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
const api = readNativeApi();
if (!api) return;
const targetPath = activeCwd ? resolvePathLinkTarget(filePath, activeCwd) : filePath;
- void api.shell.openInEditor(targetPath, preferredTerminalEditor()).catch((error) => {
+ void openInPreferredEditor(api, targetPath).catch((error) => {
console.warn("Failed to open diff file in editor.", error);
});
},
diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx
index 379fb5a840cfc..ea1865bc86194 100644
--- a/apps/web/src/components/GitActionsControl.tsx
+++ b/apps/web/src/components/GitActionsControl.tsx
@@ -31,6 +31,7 @@ import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover";
import { ScrollArea } from "~/components/ui/scroll-area";
import { Textarea } from "~/components/ui/textarea";
import { toastManager } from "~/components/ui/toast";
+import { openInPreferredEditor } from "~/editorPreferences";
import {
gitBranchesQueryOptions,
gitInitMutationOptions,
@@ -40,7 +41,7 @@ import {
gitStatusQueryOptions,
invalidateGitQueries,
} from "~/lib/gitReactQuery";
-import { preferredTerminalEditor, resolvePathLinkTarget } from "~/terminal-links";
+import { resolvePathLinkTarget } from "~/terminal-links";
import { readNativeApi } from "~/nativeApi";
interface GitActionsControlProps {
@@ -581,7 +582,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
return;
}
const target = resolvePathLinkTarget(filePath, gitCwd);
- void api.shell.openInEditor(target, preferredTerminalEditor()).catch((error) => {
+ void openInPreferredEditor(api, target).catch((error) => {
toastManager.add({
type: "error",
title: "Unable to open file",
diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx
index cce2ed1338243..7861212e48692 100644
--- a/apps/web/src/components/ThreadTerminalDrawer.tsx
+++ b/apps/web/src/components/ThreadTerminalDrawer.tsx
@@ -12,10 +12,10 @@ import {
useState,
} from "react";
import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover";
+import { openInPreferredEditor } from "../editorPreferences";
import {
extractTerminalLinks,
isTerminalLinkActivation,
- preferredTerminalEditor,
resolvePathLinkTarget,
} from "../terminal-links";
import { isTerminalClearShortcut, terminalNavigationShortcutData } from "../keybindings";
@@ -236,7 +236,7 @@ function TerminalViewport({
}
const target = resolvePathLinkTarget(match.text, cwd);
- void api.shell.openInEditor(target, preferredTerminalEditor()).catch((error) => {
+ void openInPreferredEditor(api, target).catch((error) => {
writeSystemMessage(
latestTerminal,
error instanceof Error ? error.message : "Unable to open path",
diff --git a/apps/web/src/components/chat/OpenInPicker.tsx b/apps/web/src/components/chat/OpenInPicker.tsx
index 701204c1be939..2e09da03c19b1 100644
--- a/apps/web/src/components/chat/OpenInPicker.tsx
+++ b/apps/web/src/components/chat/OpenInPicker.tsx
@@ -1,6 +1,7 @@
-import { EDITORS, type EditorId, type ResolvedKeybindingsConfig } from "@t3tools/contracts";
-import { memo, useCallback, useEffect, useMemo, useState } from "react";
+import { EditorId, type ResolvedKeybindingsConfig } from "@t3tools/contracts";
+import { memo, useCallback, useEffect, useMemo } from "react";
import { isOpenFavoriteEditorShortcut, shortcutLabelForCommand } from "../../keybindings";
+import { usePreferredEditor } from "../../editorPreferences";
import { ChevronDownIcon, FolderClosedIcon } from "lucide-react";
import { Button } from "../ui/button";
import { Group, GroupSeparator } from "../ui/group";
@@ -9,7 +10,35 @@ import { CursorIcon, Icon, VisualStudioCode, Zed } from "../Icons";
import { isMacPlatform, isWindowsPlatform } from "~/lib/utils";
import { readNativeApi } from "~/nativeApi";
-const LAST_EDITOR_KEY = "t3code:last-editor";
+const resolveOptions = (platform: string, availableEditors: ReadonlyArray) => {
+ const baseOptions: ReadonlyArray<{ label: string; Icon: Icon; value: EditorId }> = [
+ {
+ label: "Cursor",
+ Icon: CursorIcon,
+ value: "cursor",
+ },
+ {
+ label: "VS Code",
+ Icon: VisualStudioCode,
+ value: "vscode",
+ },
+ {
+ label: "Zed",
+ Icon: Zed,
+ value: "zed",
+ },
+ {
+ label: isMacPlatform(platform)
+ ? "Finder"
+ : isWindowsPlatform(platform)
+ ? "Explorer"
+ : "Files",
+ Icon: FolderClosedIcon,
+ value: "file-manager",
+ },
+ ];
+ return baseOptions.filter((option) => availableEditors.includes(option.value));
+};
export const OpenInPicker = memo(function OpenInPicker({
keybindings,
@@ -20,61 +49,23 @@ export const OpenInPicker = memo(function OpenInPicker({
availableEditors: ReadonlyArray;
openInCwd: string | null;
}) {
- const [lastEditor, setLastEditor] = useState(() => {
- const stored = localStorage.getItem(LAST_EDITOR_KEY);
- return EDITORS.some((e) => e.id === stored) ? (stored as EditorId) : EDITORS[0].id;
- });
-
- const allOptions = useMemo>(
- () => [
- {
- label: "Cursor",
- Icon: CursorIcon,
- value: "cursor",
- },
- {
- label: "VS Code",
- Icon: VisualStudioCode,
- value: "vscode",
- },
- {
- label: "Zed",
- Icon: Zed,
- value: "zed",
- },
- {
- label: isMacPlatform(navigator.platform)
- ? "Finder"
- : isWindowsPlatform(navigator.platform)
- ? "Explorer"
- : "Files",
- Icon: FolderClosedIcon,
- value: "file-manager",
- },
- ],
- [],
- );
+ const [preferredEditor, setPreferredEditor] = usePreferredEditor(availableEditors);
const options = useMemo(
- () => allOptions.filter((option) => availableEditors.includes(option.value)),
- [allOptions, availableEditors],
+ () => resolveOptions(navigator.platform, availableEditors),
+ [availableEditors],
);
-
- const effectiveEditor = options.some((option) => option.value === lastEditor)
- ? lastEditor
- : (options[0]?.value ?? null);
- const primaryOption = options.find(({ value }) => value === effectiveEditor) ?? null;
+ const primaryOption = options.find(({ value }) => value === preferredEditor) ?? null;
const openInEditor = useCallback(
(editorId: EditorId | null) => {
const api = readNativeApi();
if (!api || !openInCwd) return;
- const editor = editorId ?? effectiveEditor;
+ const editor = editorId ?? preferredEditor;
if (!editor) return;
void api.shell.openInEditor(openInCwd, editor);
- localStorage.setItem(LAST_EDITOR_KEY, editor);
- setLastEditor(editor);
+ setPreferredEditor(editor);
},
- [effectiveEditor, openInCwd, setLastEditor],
+ [preferredEditor, openInCwd, setPreferredEditor],
);
const openFavoriteEditorShortcutLabel = useMemo(
@@ -87,22 +78,22 @@ export const OpenInPicker = memo(function OpenInPicker({
const api = readNativeApi();
if (!isOpenFavoriteEditorShortcut(e, keybindings)) return;
if (!api || !openInCwd) return;
- if (!effectiveEditor) return;
+ if (!preferredEditor) return;
e.preventDefault();
- void api.shell.openInEditor(openInCwd, effectiveEditor);
+ void api.shell.openInEditor(openInCwd, preferredEditor);
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
- }, [effectiveEditor, keybindings, openInCwd]);
+ }, [preferredEditor, keybindings, openInCwd]);
return (
@@ -650,9 +655,13 @@ function formatWorkingTimer(startIso: string, endIso: string): string | null {
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
}
-function formatMessageMeta(createdAt: string, duration: string | null): string {
- if (!duration) return formatTimestamp(createdAt);
- return `${formatTimestamp(createdAt)} • ${duration}`;
+function formatMessageMeta(
+ createdAt: string,
+ duration: string | null,
+ timestampFormat: TimestampFormat,
+): string {
+ if (!duration) return formatTimestamp(createdAt, timestampFormat);
+ return `${formatTimestamp(createdAt, timestampFormat)} • ${duration}`;
}
function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string {
diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx
index c0e9a7ef9fbcb..1007b3130f626 100644
--- a/apps/web/src/routes/_chat.settings.tsx
+++ b/apps/web/src/routes/_chat.settings.tsx
@@ -11,6 +11,13 @@ import { serverConfigQueryOptions } from "../lib/serverReactQuery";
import { ensureNativeApi } from "../nativeApi";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
+import {
+ Select,
+ SelectItem,
+ SelectPopup,
+ SelectTrigger,
+ SelectValue,
+} from "../components/ui/select";
import { Switch } from "../components/ui/switch";
import { APP_VERSION } from "../branding";
import { SidebarInset } from "~/components/ui/sidebar";
@@ -49,6 +56,12 @@ const MODEL_PROVIDER_SETTINGS: Array<{
},
] as const;
+const TIMESTAMP_FORMAT_LABELS = {
+ locale: "System default",
+ "12-hour": "12-hour",
+ "24-hour": "24-hour",
+} as const;
+
function getCustomModelsForProvider(
settings: ReturnType["settings"],
provider: ProviderKind,
@@ -210,43 +223,89 @@ function SettingsRouteView() {
Appearance
- Choose how T3 Code handles light and dark mode.
+ Choose how T3 Code looks across the app.
-
- {THEME_OPTIONS.map((option) => {
- const selected = theme === option.value;
- return (
-
setTheme(option.value)}
- >
-
- {option.label}
- {option.description}
-
- {selected ? (
-
- Selected
+
+
+ {THEME_OPTIONS.map((option) => {
+ const selected = theme === option.value;
+ return (
+ setTheme(option.value)}
+ >
+
+ {option.label}
+ {option.description}
- ) : null}
-
- );
- })}
-
+ {selected ? (
+
+ Selected
+
+ ) : null}
+
+ );
+ })}
+
-
- Active theme: {resolvedTheme}
-
+
+ Active theme: {resolvedTheme}
+
+
+
+
+
Timestamp format
+
+ System default follows your browser or OS time format. 12-hour{" "}
+ and 24-hour force the hour cycle.
+
+
+
+
+
+ {settings.timestampFormat !== defaults.timestampFormat ? (
+
+
+ updateSettings({
+ timestampFormat: defaults.timestampFormat,
+ })
+ }
+ >
+ Restore default
+
+
+ ) : null}
+
diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts
index aa8f3ffc35604..4e395b8b263e5 100644
--- a/apps/web/src/session-logic.ts
+++ b/apps/web/src/session-logic.ts
@@ -89,14 +89,6 @@ export type TimelineEntry =
entry: WorkLogEntry;
};
-export function formatTimestamp(isoDate: string): string {
- return new Intl.DateTimeFormat(undefined, {
- hour: "numeric",
- minute: "2-digit",
- second: "2-digit",
- }).format(new Date(isoDate));
-}
-
export function formatDuration(durationMs: number): string {
if (!Number.isFinite(durationMs) || durationMs < 0) return "0ms";
if (durationMs < 1_000) return `${Math.max(1, Math.round(durationMs))}ms`;
diff --git a/apps/web/src/timestampFormat.test.ts b/apps/web/src/timestampFormat.test.ts
new file mode 100644
index 0000000000000..f45ada7341c06
--- /dev/null
+++ b/apps/web/src/timestampFormat.test.ts
@@ -0,0 +1,30 @@
+import { describe, expect, it } from "vitest";
+
+import { getTimestampFormatOptions } from "./timestampFormat";
+
+describe("getTimestampFormatOptions", () => {
+ it("omits hour12 when locale formatting is requested", () => {
+ expect(getTimestampFormatOptions("locale", true)).toEqual({
+ hour: "numeric",
+ minute: "2-digit",
+ second: "2-digit",
+ });
+ });
+
+ it("builds a 12-hour formatter with seconds when requested", () => {
+ expect(getTimestampFormatOptions("12-hour", true)).toEqual({
+ hour: "numeric",
+ minute: "2-digit",
+ second: "2-digit",
+ hour12: true,
+ });
+ });
+
+ it("builds a 24-hour formatter without seconds when requested", () => {
+ expect(getTimestampFormatOptions("24-hour", false)).toEqual({
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: false,
+ });
+ });
+});
diff --git a/apps/web/src/timestampFormat.ts b/apps/web/src/timestampFormat.ts
new file mode 100644
index 0000000000000..d4ffa3c376056
--- /dev/null
+++ b/apps/web/src/timestampFormat.ts
@@ -0,0 +1,49 @@
+import { type TimestampFormat } from "./appSettings";
+
+export function getTimestampFormatOptions(
+ timestampFormat: TimestampFormat,
+ includeSeconds: boolean,
+): Intl.DateTimeFormatOptions {
+ const baseOptions: Intl.DateTimeFormatOptions = {
+ hour: "numeric",
+ minute: "2-digit",
+ ...(includeSeconds ? { second: "2-digit" } : {}),
+ };
+
+ if (timestampFormat === "locale") {
+ return baseOptions;
+ }
+
+ return {
+ ...baseOptions,
+ hour12: timestampFormat === "12-hour",
+ };
+}
+
+const timestampFormatterCache = new Map();
+
+function getTimestampFormatter(
+ timestampFormat: TimestampFormat,
+ includeSeconds: boolean,
+): Intl.DateTimeFormat {
+ const cacheKey = `${timestampFormat}:${includeSeconds ? "seconds" : "minutes"}`;
+ const cachedFormatter = timestampFormatterCache.get(cacheKey);
+ if (cachedFormatter) {
+ return cachedFormatter;
+ }
+
+ const formatter = new Intl.DateTimeFormat(
+ undefined,
+ getTimestampFormatOptions(timestampFormat, includeSeconds),
+ );
+ timestampFormatterCache.set(cacheKey, formatter);
+ return formatter;
+}
+
+export function formatTimestamp(isoDate: string, timestampFormat: TimestampFormat): string {
+ return getTimestampFormatter(timestampFormat, true).format(new Date(isoDate));
+}
+
+export function formatShortTimestamp(isoDate: string, timestampFormat: TimestampFormat): string {
+ return getTimestampFormatter(timestampFormat, false).format(new Date(isoDate));
+}
From 5c03fb62c4ede0a347938b796a611d5f6b9d61fb Mon Sep 17 00:00:00 2001
From: WilgotM
Date: Fri, 13 Mar 2026 00:28:32 +0100
Subject: [PATCH 24/40] Add Antigravity to supported editors in Open menu
(macOS, Windows, and Linux) (#841)
Co-authored-by: Julius Marminge
---
apps/server/src/open.test.ts | 122 ++++++++++--------
apps/web/src/components/Icons.tsx | 9 ++
apps/web/src/components/chat/OpenInPicker.tsx | 7 +-
packages/contracts/src/editor.ts | 1 +
4 files changed, 82 insertions(+), 57 deletions(-)
diff --git a/apps/server/src/open.test.ts b/apps/server/src/open.test.ts
index 0f864554e91e1..649e88462c630 100644
--- a/apps/server/src/open.test.ts
+++ b/apps/server/src/open.test.ts
@@ -1,8 +1,7 @@
-import fs from "node:fs";
-import os from "node:os";
-import path from "node:path";
-
-import { assert, describe, it } from "@effect/vitest";
+import * as NodeServices from "@effect/platform-node/NodeServices";
+import { assert, it } from "@effect/vitest";
+import { assertSuccess } from "@effect/vitest/utils";
+import { FileSystem, Path, Effect } from "effect";
import {
isCommandAvailable,
@@ -10,12 +9,19 @@ import {
resolveAvailableEditors,
resolveEditorLaunch,
} from "./open";
-import { Effect } from "effect";
-import { assertSuccess } from "@effect/vitest/utils";
-describe("resolveEditorLaunch", () => {
+it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
it.effect("returns commands for command-based editors", () =>
Effect.gen(function* () {
+ const antigravityLaunch = yield* resolveEditorLaunch(
+ { cwd: "/tmp/workspace", editor: "antigravity" },
+ "darwin",
+ );
+ assert.deepEqual(antigravityLaunch, {
+ command: "agy",
+ args: ["/tmp/workspace"],
+ });
+
const cursorLaunch = yield* resolveEditorLaunch(
{ cwd: "/tmp/workspace", editor: "cursor" },
"darwin",
@@ -117,7 +123,7 @@ describe("resolveEditorLaunch", () => {
);
});
-describe("launchDetached", () => {
+it.layer(NodeServices.layer)("launchDetached", (it) => {
it.effect("resolves when command can be spawned", () =>
Effect.gen(function* () {
const result = yield* launchDetached({
@@ -139,26 +145,20 @@ describe("launchDetached", () => {
);
});
-describe("isCommandAvailable", () => {
- function withTempDir(run: (dir: string) => void): void {
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-open-"));
- try {
- run(dir);
- } finally {
- fs.rmSync(dir, { recursive: true, force: true });
- }
- }
-
- it("resolves win32 commands with PATHEXT", () => {
- withTempDir((dir) => {
- fs.writeFileSync(path.join(dir, "code.CMD"), "@echo off\r\n", "utf8");
+it.layer(NodeServices.layer)("isCommandAvailable", (it) => {
+ it.effect("resolves win32 commands with PATHEXT", () =>
+ Effect.gen(function* () {
+ const fs = yield* FileSystem.FileSystem;
+ const path = yield* Path.Path;
+ const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-open-test-" });
+ yield* fs.writeFileString(path.join(dir, "code.CMD"), "@echo off\r\n");
const env = {
PATH: dir,
PATHEXT: ".COM;.EXE;.BAT;.CMD",
} satisfies NodeJS.ProcessEnv;
assert.equal(isCommandAvailable("code", { platform: "win32", env }), true);
- });
- });
+ }),
+ );
it("returns false when a command is not on PATH", () => {
const env = {
@@ -168,55 +168,65 @@ describe("isCommandAvailable", () => {
assert.equal(isCommandAvailable("definitely-not-installed", { platform: "win32", env }), false);
});
- it("does not treat bare files without executable extension as available on win32", () => {
- withTempDir((dir) => {
- fs.writeFileSync(path.join(dir, "npm"), "echo nope\r\n", "utf8");
+ it.effect("does not treat bare files without executable extension as available on win32", () =>
+ Effect.gen(function* () {
+ const fs = yield* FileSystem.FileSystem;
+ const path = yield* Path.Path;
+ const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-open-test-" });
+ yield* fs.writeFileString(path.join(dir, "npm"), "echo nope\r\n");
const env = {
PATH: dir,
PATHEXT: ".COM;.EXE;.BAT;.CMD",
} satisfies NodeJS.ProcessEnv;
assert.equal(isCommandAvailable("npm", { platform: "win32", env }), false);
- });
- });
+ }),
+ );
- it("appends PATHEXT for commands with non-executable extensions on win32", () => {
- withTempDir((dir) => {
- fs.writeFileSync(path.join(dir, "my.tool.CMD"), "@echo off\r\n", "utf8");
+ it.effect("appends PATHEXT for commands with non-executable extensions on win32", () =>
+ Effect.gen(function* () {
+ const fs = yield* FileSystem.FileSystem;
+ const path = yield* Path.Path;
+ const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-open-test-" });
+ yield* fs.writeFileString(path.join(dir, "my.tool.CMD"), "@echo off\r\n");
const env = {
PATH: dir,
PATHEXT: ".COM;.EXE;.BAT;.CMD",
} satisfies NodeJS.ProcessEnv;
assert.equal(isCommandAvailable("my.tool", { platform: "win32", env }), true);
- });
- });
+ }),
+ );
- it("uses platform-specific PATH delimiter for platform overrides", () => {
- withTempDir((firstDir) => {
- withTempDir((secondDir) => {
- fs.writeFileSync(path.join(secondDir, "code.CMD"), "@echo off\r\n", "utf8");
- const env = {
- PATH: `${firstDir};${secondDir}`,
- PATHEXT: ".COM;.EXE;.BAT;.CMD",
- } satisfies NodeJS.ProcessEnv;
- assert.equal(isCommandAvailable("code", { platform: "win32", env }), true);
- });
- });
- });
+ it.effect("uses platform-specific PATH delimiter for platform overrides", () =>
+ Effect.gen(function* () {
+ const fs = yield* FileSystem.FileSystem;
+ const path = yield* Path.Path;
+ const firstDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-open-test-" });
+ const secondDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-open-test-" });
+ yield* fs.writeFileString(path.join(firstDir, "code.CMD"), "@echo off\r\n");
+ yield* fs.writeFileString(path.join(secondDir, "code.CMD"), "MZ");
+ const env = {
+ PATH: `${firstDir};${secondDir}`,
+ PATHEXT: ".COM;.EXE;.BAT;.CMD",
+ } satisfies NodeJS.ProcessEnv;
+ assert.equal(isCommandAvailable("code", { platform: "win32", env }), true);
+ }),
+ );
});
-describe("resolveAvailableEditors", () => {
- it("returns only editors whose launch commands are available", () => {
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-editors-"));
- try {
- fs.writeFileSync(path.join(dir, "cursor.CMD"), "@echo off\r\n", "utf8");
- fs.writeFileSync(path.join(dir, "explorer.EXE"), "MZ", "utf8");
+it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => {
+ it.effect("returns installed editors for command launches", () =>
+ Effect.gen(function* () {
+ const fs = yield* FileSystem.FileSystem;
+ const path = yield* Path.Path;
+ const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editors-" });
+
+ yield* fs.writeFileString(path.join(dir, "cursor.CMD"), "@echo off\r\n");
+ yield* fs.writeFileString(path.join(dir, "explorer.CMD"), "MZ");
const editors = resolveAvailableEditors("win32", {
PATH: dir,
PATHEXT: ".COM;.EXE;.BAT;.CMD",
});
assert.deepEqual(editors, ["cursor", "file-manager"]);
- } finally {
- fs.rmSync(dir, { recursive: true, force: true });
- }
- });
+ }),
+ );
});
diff --git a/apps/web/src/components/Icons.tsx b/apps/web/src/components/Icons.tsx
index 4e1a586d695c2..9a3ddbaa0fc08 100644
--- a/apps/web/src/components/Icons.tsx
+++ b/apps/web/src/components/Icons.tsx
@@ -287,6 +287,15 @@ export const Gemini: Icon = (props) => (
);
+const ANTIGRAVITY_ICON_DATA_URL =
+ "data:image/png;base64,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";
+
+export const AntigravityIcon: Icon = (props) => (
+
+);
+
export const OpenCodeIcon: Icon = (props) => (