authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-14 22:54:58-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-14 23:35:48-08:00
logc743239b5e83db7142cccf99dcacba7fd66b1d25
treeeb5b6822af5c0b87a6d5b744b85bf180df197959
parentf97888a6fd230cff2f0e4e565b446b61c0473c44

fmt


51 files changed, 289 insertions(+), 782 deletions(-)

apps/server/src/codexAppServerManager.test.ts+10-3
......@@ -21,10 +21,16 @@ function createSendTurnHarness() {
2121 };
2222
2323 const requireSession = vi
24 .spyOn(manager as unknown as { requireSession: (sessionId: string) => unknown }, "requireSession")
24 .spyOn(
25 manager as unknown as { requireSession: (sessionId: string) => unknown },
26 "requireSession",
27 )
2528 .mockReturnValue(context);
2629 const sendRequest = vi
27 .spyOn(manager as unknown as { sendRequest: (...args: unknown[]) => Promise<unknown> }, "sendRequest")
30 .spyOn(
31 manager as unknown as { sendRequest: (...args: unknown[]) => Promise<unknown> },
32 "sendRequest",
33 )
2834 .mockResolvedValue({
2935 turn: {
3036 id: "turn_1",
......@@ -109,7 +115,8 @@ describe("isRecoverableThreadResumeError", () => {
109115
110116describe("sendTurn", () => {
111117 it("sends text and image user input items to turn/start", async () => {
112 const { manager, context, requireSession, sendRequest, updateSession } = createSendTurnHarness();
118 const { manager, context, requireSession, sendRequest, updateSession } =
119 createSendTurnHarness();
113120
114121 const result = await manager.sendTurn({
115122 sessionId: "sess_1",
apps/server/src/codexAppServerManager.ts+2-4
......@@ -258,8 +258,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
258258 }
259259
260260 const turnInput: Array<
261 | { type: "text"; text: string; text_elements: [] }
262 | { type: "image"; url: string }
261 { type: "text"; text: string; text_elements: [] } | { type: "image"; url: string }
263262 > = [];
264263 if (input.input) {
265264 turnInput.push({
......@@ -283,8 +282,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
283282 const turnStartParams: {
284283 threadId: string;
285284 input: Array<
286 | { type: "text"; text: string; text_elements: [] }
287 | { type: "image"; url: string }
285 { type: "text"; text: string; text_elements: [] } | { type: "image"; url: string }
288286 >;
289287 model?: string;
290288 effort?: string;
apps/server/src/coreServices.ts+1-4
......@@ -28,8 +28,5 @@ export interface TextGenerationService {
2828 generateCommitMessage(
2929 input: CommitMessageGenerationInput,
3030 ): Promise<CommitMessageGenerationResult>;
31 generatePrContent(
32 input: PrContentGenerationInput,
33 ): Promise<PrContentGenerationResult>;
31 generatePrContent(input: PrContentGenerationInput): Promise<PrContentGenerationResult>;
3432}
35
apps/server/src/git.test.ts+1-5
......@@ -570,11 +570,7 @@ describe("git integration", () => {
570570 expect(context!.stagedSummary.length).toBeGreaterThan(0);
571571 expect(context!.stagedPatch.length).toBeGreaterThan(0);
572572
573 const created = await core.commit(
574 tmp.path,
575 "Add README update",
576 "- include updated content",
577 );
573 const created = await core.commit(tmp.path, "Add README update", "- include updated content");
578574 expect(created.commitSha.length).toBeGreaterThan(0);
579575 expect(await git(tmp.path, "log -1 --pretty=%s")).toBe("Add README update");
580576 });
apps/server/src/git.ts+4-12
......@@ -101,7 +101,8 @@ function parseNumstatEntries(
101101 for (const line of stdout.split(/\r?\n/g)) {
102102 if (line.trim().length === 0) continue;
103103 const [addedRaw, deletedRaw, ...pathParts] = line.split("\t");
104 const rawPath = pathParts.length > 1 ? (pathParts.at(-1) ?? "").trim() : pathParts.join("\t").trim();
104 const rawPath =
105 pathParts.length > 1 ? (pathParts.at(-1) ?? "").trim() : pathParts.join("\t").trim();
105106 if (rawPath.length === 0) continue;
106107 const added = Number.parseInt(addedRaw ?? "0", 10);
107108 const deleted = Number.parseInt(deletedRaw ?? "0", 10);
......@@ -311,21 +312,12 @@ export class GitCoreService {
311312 async prepareCommitContext(cwd: string): Promise<GitPreparedCommitContext | null> {
312313 await this.git(cwd, ["add", "-A"]);
313314
314 const stagedSummary = await this.gitStdout(cwd, [
315 "diff",
316 "--cached",
317 "--name-status",
318 ]);
315 const stagedSummary = await this.gitStdout(cwd, ["diff", "--cached", "--name-status"]);
319316 if (trimStdout(stagedSummary).length === 0) {
320317 return null;
321318 }
322319
323 const stagedPatch = await this.gitStdout(cwd, [
324 "diff",
325 "--cached",
326 "--patch",
327 "--minimal",
328 ]);
320 const stagedPatch = await this.gitStdout(cwd, ["diff", "--cached", "--patch", "--minimal"]);
329321
330322 return {
331323 stagedSummary,
apps/server/src/gitManager.test.ts+3-10
......@@ -5,11 +5,7 @@ import path from "node:path";
55import { afterEach, describe, expect, it } from "vitest";
66
77import { GitManager } from "./gitManager";
8import {
9 type ProcessRunOptions,
10 type ProcessRunResult,
11 runProcess,
12} from "./processRunner";
8import { type ProcessRunOptions, type ProcessRunResult, runProcess } from "./processRunner";
139
1410interface FakeGhScenario {
1511 prListSequence?: string[];
......@@ -71,9 +67,7 @@ async function createBareRemote(): Promise<string> {
7167 return remoteDir;
7268}
7369
74function createTextGenerator(
75 overrides: Partial<FakeGitTextGenerator> = {},
76): FakeGitTextGenerator {
70function createTextGenerator(overrides: Partial<FakeGitTextGenerator> = {}): FakeGitTextGenerator {
7771 return {
7872 generateCommitMessage: async () => ({
7973 subject: "Implement stacked git actions",
......@@ -126,8 +120,7 @@ function createRunnerWithFakeGh(scenario: FakeGhScenario = {}): {
126120 if (args[0] === "pr" && args[1] === "create") {
127121 return {
128122 stdout:
129 (scenario.createdPrUrl ??
130 "https://github.com/pingdotgg/codething-mvp/pull/101") + "\n",
123 (scenario.createdPrUrl ?? "https://github.com/pingdotgg/codething-mvp/pull/101") + "\n",
131124 stderr: "",
132125 code: 0,
133126 signal: null,
apps/server/src/gitManager.ts+10-40
......@@ -20,11 +20,7 @@ import type {
2020} from "./coreServices";
2121import { CodexTextGenerator } from "./codexTextGenerator";
2222import { GitCoreService } from "./git";
23import {
24 type ProcessRunOptions,
25 type ProcessRunResult,
26 runProcess,
27} from "./processRunner";
23import { type ProcessRunOptions, type ProcessRunResult, runProcess } from "./processRunner";
2824
2925const GH_CLI_TIMEOUT_MS = 30_000;
3026
......@@ -58,11 +54,7 @@ function parseOpenPrList(raw: unknown): OpenPrInfo[] {
5854 const url = record.url;
5955 const baseRefName = record.baseRefName;
6056 const headRefName = record.headRefName;
61 if (
62 typeof number !== "number" ||
63 !Number.isInteger(number) ||
64 number <= 0
65 ) {
57 if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) {
6658 continue;
6759 }
6860 if (
......@@ -98,8 +90,7 @@ function sanitizeCommitMessage(
9890): CommitMessageGenerationResult {
9991 const rawSubject = generated.subject.trim().split(/\r?\n/g)[0]?.trim() ?? "";
10092 const subject = rawSubject.replace(/[.]+$/g, "").trim();
101 const safeSubject =
102 subject.length > 0 ? subject.slice(0, 72).trimEnd() : "Update project files";
93 const safeSubject = subject.length > 0 ? subject.slice(0, 72).trimEnd() : "Update project files";
10394 return {
10495 subject: safeSubject,
10596 body: generated.body.trim(),
......@@ -143,10 +134,7 @@ function extractBranchFromRef(ref: string): string {
143134 return normalized.slice(firstSlash + 1).trim();
144135}
145136
146function asCommandNotFound(
147 command: string,
148 error: unknown,
149): Error | undefined {
137function asCommandNotFound(command: string, error: unknown): Error | undefined {
150138 if (!(error instanceof Error)) return undefined;
151139 if (!error.message.includes(`Command not found: ${command}`)) return undefined;
152140 if (command === "gh") {
......@@ -226,9 +214,7 @@ export class GitManager {
226214 };
227215 }
228216
229 async runStackedAction(
230 raw: GitRunStackedActionInput,
231 ): Promise<GitRunStackedActionResult> {
217 async runStackedAction(raw: GitRunStackedActionInput): Promise<GitRunStackedActionResult> {
232218 const input = gitRunStackedActionInputSchema.parse(raw);
233219 const wantsPush = input.action !== "commit";
234220 const wantsPr = input.action === "commit_push_pr";
......@@ -241,11 +227,7 @@ export class GitManager {
241227 throw new Error("Cannot create a pull request from detached HEAD.");
242228 }
243229
244 const commit = await this.runCommitStep(
245 input.cwd,
246 initialStatus.branch,
247 input.commitMessage,
248 );
230 const commit = await this.runCommitStep(input.cwd, initialStatus.branch, input.commitMessage);
249231
250232 const push = wantsPush
251233 ? await this.gitCore.pushCurrentBranch(input.cwd, initialStatus.branch)
......@@ -353,10 +335,7 @@ export class GitManager {
353335 throw asCommandNotFound("codex", error) ?? error;
354336 }
355337
356 const bodyFile = path.join(
357 os.tmpdir(),
358 `t3code-pr-body-${process.pid}-${randomUUID()}.md`,
359 );
338 const bodyFile = path.join(os.tmpdir(), `t3code-pr-body-${process.pid}-${randomUUID()}.md`);
360339 await fs.writeFile(bodyFile, generated.body, "utf8");
361340
362341 try {
......@@ -401,10 +380,7 @@ export class GitManager {
401380 };
402381 }
403382
404 private async findOpenPr(
405 cwd: string,
406 branch: string,
407 ): Promise<OpenPrInfo | null> {
383 private async findOpenPr(cwd: string, branch: string): Promise<OpenPrInfo | null> {
408384 const stdout = await this.runGhStdout(cwd, [
409385 "pr",
410386 "list",
......@@ -437,10 +413,7 @@ export class GitManager {
437413 branch: string,
438414 upstreamRef: string | null,
439415 ): Promise<string> {
440 const configured = await this.gitCore.readConfigValue(
441 cwd,
442 `branch.${branch}.gh-merge-base`,
443 );
416 const configured = await this.gitCore.readConfigValue(cwd, `branch.${branch}.gh-merge-base`);
444417 if (configured) return configured;
445418
446419 if (upstreamRef) {
......@@ -477,10 +450,7 @@ export class GitManager {
477450 return result.stdout;
478451 }
479452
480 private async runGhCommand(
481 cwd: string,
482 args: readonly string[],
483 ): Promise<ProcessRunResult> {
453 private async runGhCommand(cwd: string, args: readonly string[]): Promise<ProcessRunResult> {
484454 try {
485455 return await this.run("gh", args, {
486456 cwd,
apps/server/src/processRunner.test.ts+5-13
......@@ -5,23 +5,15 @@ import { runProcess } from "./processRunner";
55describe("runProcess", () => {
66 it("fails when output exceeds max buffer in default mode", async () => {
77 await expect(
8 runProcess(
9 "node",
10 ["-e", "process.stdout.write('x'.repeat(2048))"],
11 { maxBufferBytes: 128 },
12 ),
8 runProcess("node", ["-e", "process.stdout.write('x'.repeat(2048))"], { maxBufferBytes: 128 }),
139 ).rejects.toThrow("exceeded stdout buffer limit");
1410 });
1511
1612 it("truncates output when outputMode is truncate", async () => {
17 const result = await runProcess(
18 "node",
19 ["-e", "process.stdout.write('x'.repeat(2048))"],
20 {
21 maxBufferBytes: 128,
22 outputMode: "truncate",
23 },
24 );
13 const result = await runProcess("node", ["-e", "process.stdout.write('x'.repeat(2048))"], {
14 maxBufferBytes: 128,
15 outputMode: "truncate",
16 });
2517
2618 expect(result.code).toBe(0);
2719 expect(result.stdout.length).toBeLessThanOrEqual(128);
apps/server/src/processRunner.ts+9-38
......@@ -24,11 +24,7 @@ function commandLabel(command: string, args: readonly string[]): string {
2424 return [command, ...args].join(" ");
2525}
2626
27function normalizeSpawnError(
28 command: string,
29 args: readonly string[],
30 error: unknown,
31): Error {
27function normalizeSpawnError(command: string, args: readonly string[], error: unknown): Error {
3228 if (!(error instanceof Error)) {
3329 return new Error(`Failed to run ${commandLabel(command, args)}.`);
3430 }
......@@ -38,9 +34,7 @@ function normalizeSpawnError(
3834 return new Error(`Command not found: ${command}`);
3935 }
4036
41 return new Error(
42 `Failed to run ${commandLabel(command, args)}: ${error.message}`,
43 );
37 return new Error(`Failed to run ${commandLabel(command, args)}: ${error.message}`);
4438}
4539
4640function normalizeExitError(
......@@ -56,17 +50,11 @@ function normalizeExitError(
5650 return new Error(`${commandLabel(command, args)} ${reason}.${detail}`);
5751}
5852
59function normalizeStdinError(
60 command: string,
61 args: readonly string[],
62 error: unknown,
63): Error {
53function normalizeStdinError(command: string, args: readonly string[], error: unknown): Error {
6454 if (!(error instanceof Error)) {
6555 return new Error(`Failed to write stdin for ${commandLabel(command, args)}.`);
6656 }
67 return new Error(
68 `Failed to write stdin for ${commandLabel(command, args)}: ${error.message}`,
69 );
57 return new Error(`Failed to write stdin for ${commandLabel(command, args)}: ${error.message}`);
7058}
7159
7260function normalizeBufferError(
......@@ -161,22 +149,13 @@ export async function runProcess(
161149 });
162150 };
163151
164 const appendOutput = (
165 stream: "stdout" | "stderr",
166 chunk: Buffer | string,
167 ): Error | null => {
168 const chunkBuffer =
169 typeof chunk === "string" ? Buffer.from(chunk) : chunk;
152 const appendOutput = (stream: "stdout" | "stderr", chunk: Buffer | string): Error | null => {
153 const chunkBuffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
170154 const text = chunkBuffer.toString();
171155 const byteLength = chunkBuffer.length;
172156 if (stream === "stdout") {
173157 if (outputMode === "truncate") {
174 const appended = appendChunkWithinLimit(
175 stdout,
176 stdoutBytes,
177 chunkBuffer,
178 maxBufferBytes,
179 );
158 const appended = appendChunkWithinLimit(stdout, stdoutBytes, chunkBuffer, maxBufferBytes);
180159 stdout = appended.next;
181160 stdoutBytes = appended.nextBytes;
182161 stdoutTruncated = stdoutTruncated || appended.truncated;
......@@ -189,12 +168,7 @@ export async function runProcess(
189168 }
190169 } else {
191170 if (outputMode === "truncate") {
192 const appended = appendChunkWithinLimit(
193 stderr,
194 stderrBytes,
195 chunkBuffer,
196 maxBufferBytes,
197 );
171 const appended = appendChunkWithinLimit(stderr, stderrBytes, chunkBuffer, maxBufferBytes);
198172 stderr = appended.next;
199173 stderrBytes = appended.nextBytes;
200174 stderrTruncated = stderrTruncated || appended.truncated;
......@@ -241,10 +215,7 @@ export async function runProcess(
241215 };
242216
243217 finalize(() => {
244 if (
245 !options.allowNonZeroExit &&
246 (timedOut || (code !== null && code !== 0))
247 ) {
218 if (!options.allowNonZeroExit && (timedOut || (code !== null && code !== 0))) {
248219 reject(normalizeExitError(command, args, result));
249220 return;
250221 }
apps/server/src/ptyAdapter.ts+2-10
......@@ -40,12 +40,7 @@ function resolveNodePtySpawnHelperPath(): string | null {
4040 const candidates = [
4141 path.join(packageDir, "build", "Release", "spawn-helper"),
4242 path.join(packageDir, "build", "Debug", "spawn-helper"),
43 path.join(
44 packageDir,
45 "prebuilds",
46 `${process.platform}-${process.arch}`,
47 "spawn-helper",
48 ),
43 path.join(packageDir, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
4944 ];
5045
5146 for (const candidate of candidates) {
......@@ -128,10 +123,7 @@ export class NodePtyAdapter implements PtyAdapter {
128123 cols: input.cols,
129124 rows: input.rows,
130125 env: input.env,
131 name:
132 globalThis.process.platform === "win32"
133 ? "xterm-color"
134 : "xterm-256color",
126 name: globalThis.process.platform === "win32" ? "xterm-color" : "xterm-256color",
135127 });
136128 return new NodePtyProcess(ptyProcess);
137129 }
apps/server/src/terminalManager.test.ts+14-17
......@@ -2,7 +2,11 @@ import fs from "node:fs";
22import os from "node:os";
33import path from "node:path";
44
5import { DEFAULT_TERMINAL_ID, type TerminalEvent, type TerminalOpenInput } from "@t3tools/contracts";
5import {
6 DEFAULT_TERMINAL_ID,
7 type TerminalEvent,
8 type TerminalOpenInput,
9} from "@t3tools/contracts";
610import { afterEach, describe, expect, it } from "vitest";
711
812import type { PtyAdapter, PtyExitEvent, PtyProcess, PtySpawnInput } from "./ptyAdapter";
......@@ -161,7 +165,10 @@ describe("TerminalManager", () => {
161165
162166 it("spawns lazily and reuses running terminal per thread", async () => {
163167 const { manager, ptyAdapter } = makeManager();
164 const [first, second] = await Promise.all([manager.open(openInput()), manager.open(openInput())]);
168 const [first, second] = await Promise.all([
169 manager.open(openInput()),
170 manager.open(openInput()),
171 ]);
165172 const third = await manager.open(openInput());
166173
167174 expect(first.threadId).toBe("thread-1");
......@@ -299,18 +306,14 @@ describe("TerminalManager", () => {
299306 hasRunningSubprocess = true;
300307 await waitFor(
301308 () =>
302 events.some(
303 (event) => event.type === "activity" && event.hasRunningSubprocess === true,
304 ),
309 events.some((event) => event.type === "activity" && event.hasRunningSubprocess === true),
305310 1_200,
306311 );
307312
308313 hasRunningSubprocess = false;
309314 await waitFor(
310315 () =>
311 events.some(
312 (event) => event.type === "activity" && event.hasRunningSubprocess === false,
313 ),
316 events.some((event) => event.type === "activity" && event.hasRunningSubprocess === false),
314317 1_200,
315318 );
316319
......@@ -368,12 +371,8 @@ describe("TerminalManager", () => {
368371
369372 expect(defaultProcess.killed).toBe(true);
370373 expect(sidecarProcess.killed).toBe(true);
371 expect(fs.existsSync(multiTerminalHistoryLogPath(logsDir, "thread-1", "default"))).toBe(
372 false,
373 );
374 expect(fs.existsSync(multiTerminalHistoryLogPath(logsDir, "thread-1", "sidecar"))).toBe(
375 false,
376 );
374 expect(fs.existsSync(multiTerminalHistoryLogPath(logsDir, "thread-1", "default"))).toBe(false);
375 expect(fs.existsSync(multiTerminalHistoryLogPath(logsDir, "thread-1", "sidecar"))).toBe(false);
377376
378377 manager.dispose();
379378 });
......@@ -415,9 +414,7 @@ describe("TerminalManager", () => {
415414 } else {
416415 expect(
417416 ptyAdapter.spawnInputs.some((input) =>
418 ["/bin/zsh", "/bin/bash", "/bin/sh", "zsh", "bash", "sh"].includes(
419 input.shell,
420 ),
417 ["/bin/zsh", "/bin/bash", "/bin/sh", "zsh", "bash", "sh"].includes(input.shell),
421418 ),
422419 ).toBe(true);
423420 }
apps/server/src/terminalManager.ts+6-26
......@@ -26,11 +26,7 @@ import { runProcess } from "./processRunner";
2626const DEFAULT_HISTORY_LINE_LIMIT = 5_000;
2727const DEFAULT_PERSIST_DEBOUNCE_MS = 40;
2828const DEFAULT_SUBPROCESS_POLL_INTERVAL_MS = 1_000;
29const TERMINAL_ENV_BLOCKLIST = new Set([
30 "PORT",
31 "ELECTRON_RENDERER_PORT",
32 "ELECTRON_RUN_AS_NODE",
33]);
29const TERMINAL_ENV_BLOCKLIST = new Set(["PORT", "ELECTRON_RENDERER_PORT", "ELECTRON_RUN_AS_NODE"]);
3430
3531type TerminalSubprocessChecker = (terminalPid: number) => Promise<boolean>;
3632
......@@ -102,12 +98,7 @@ function resolveShellCandidates(shellResolver: () => string): string[] {
10298 const requested = normalizeShellCommand(shellResolver());
10399
104100 if (process.platform === "win32") {
105 return uniqueShells([
106 requested,
107 process.env.ComSpec ?? null,
108 "powershell.exe",
109 "cmd.exe",
110 ]);
101 return uniqueShells([requested, process.env.ComSpec ?? null, "powershell.exe", "cmd.exe"]);
111102 }
112103
113104 return uniqueShells([
......@@ -431,11 +422,7 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
431422 const input = terminalCloseInputSchema.parse(raw);
432423 await this.runWithThreadLock(input.threadId, async () => {
433424 if (input.terminalId) {
434 await this.closeSession(
435 input.threadId,
436 input.terminalId,
437 input.deleteHistory === true,
438 );
425 await this.closeSession(input.threadId, input.terminalId, input.deleteHistory === true);
439426 return;
440427 }
441428
......@@ -517,13 +504,9 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
517504
518505 if (!ptyProcess) {
519506 const detail =
520 lastSpawnError instanceof Error
521 ? lastSpawnError.message
522 : "Terminal start failed";
507 lastSpawnError instanceof Error ? lastSpawnError.message : "Terminal start failed";
523508 const tried =
524 shellCandidates.length > 0
525 ? ` Tried shells: ${shellCandidates.join(", ")}.`
526 : "";
509 shellCandidates.length > 0 ? ` Tried shells: ${shellCandidates.join(", ")}.` : "";
527510 throw new Error(`${detail}.${tried}`.trim());
528511 }
529512
......@@ -969,10 +952,7 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
969952 return path.join(this.logsDir, `${legacySafeThreadId(threadId)}.log`);
970953 }
971954
972 private async runWithThreadLock<T>(
973 threadId: string,
974 task: () => Promise<T>,
975 ): Promise<T> {
955 private async runWithThreadLock<T>(threadId: string, task: () => Promise<T>): Promise<T> {
976956 const previous = this.threadLocks.get(threadId) ?? Promise.resolve();
977957 let release: () => void = () => {};
978958 const current = new Promise<void>((resolve) => {
apps/server/src/wsServer.test.ts+2-6
......@@ -224,9 +224,7 @@ describe("WebSocket Server", () => {
224224 ...(options.authToken ? { authToken: options.authToken } : {}),
225225 projectRegistry: new ProjectRegistry(stateDir),
226226 ...(options.gitManager ? { gitManager: options.gitManager as never } : {}),
227 ...(options.terminalManager
228 ? { terminalManager: options.terminalManager }
229 : {}),
227 ...(options.terminalManager ? { terminalManager: options.terminalManager } : {}),
230228 });
231229 }
232230
......@@ -602,9 +600,7 @@ describe("WebSocket Server", () => {
602600 it("returns errors from git.runStackedAction", async () => {
603601 const gitManager = {
604602 status: vi.fn(),
605 runStackedAction: vi
606 .fn()
607 .mockRejectedValue(new Error("Cannot push from detached HEAD.")),
603 runStackedAction: vi.fn().mockRejectedValue(new Error("Cannot push from detached HEAD.")),
608604 };
609605
610606 server = createTestServer({ cwd: "/test", gitManager });
apps/web/src/components/BranchToolbar.tsx+5-2
......@@ -214,13 +214,16 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B
214214 const branch = branchByName.get(branchName);
215215 if (!branch) return null;
216216
217 const hasSecondaryWorktree = branch.worktreePath && branch.worktreePath !== activeProject.cwd;
217 const hasSecondaryWorktree =
218 branch.worktreePath && branch.worktreePath !== activeProject.cwd;
218219 return (
219220 <ComboboxItem
220221 hideIndicator
221222 key={branchName}
222223 value={branchName}
223 className={branchName === activeThread.branch ? "bg-accent text-foreground" : undefined}
224 className={
225 branchName === activeThread.branch ? "bg-accent text-foreground" : undefined
226 }
224227 onClick={() => selectBranch(branch)}
225228 >
226229 <div className="flex w-full items-center justify-between gap-2">
apps/web/src/components/ChatView.tsx+3-1
......@@ -1304,7 +1304,9 @@ export default function ChatView() {
13041304 </form>
13051305 </div>
13061306
1307 {isGitRepo && <BranchToolbar envMode={envMode} onEnvModeChange={setEnvMode} envLocked={envLocked} />}
1307 {isGitRepo && (
1308 <BranchToolbar envMode={envMode} onEnvModeChange={setEnvMode} envLocked={envLocked} />
1309 )}
13081310
13091311 {activeThread.terminalOpen && api && activeProject && (
13101312 <ThreadTerminalDrawer
apps/web/src/components/GitActionsControl.logic.test.ts+22-5
......@@ -383,7 +383,11 @@ describe("when: working tree has local changes", () => {
383383
384384describe("when: on default branch without open PR", () => {
385385 it("resolveQuickAction returns commit and push when local changes exist", () => {
386 const quick = resolveQuickAction(status({ branch: "main", hasWorkingTreeChanges: true }), false, true);
386 const quick = resolveQuickAction(
387 status({ branch: "main", hasWorkingTreeChanges: true }),
388 false,
389 true,
390 );
387391 assert.deepInclude(quick, {
388392 kind: "run_action",
389393 action: "commit_push",
......@@ -393,7 +397,11 @@ describe("when: on default branch without open PR", () => {
393397 });
394398
395399 it("resolveQuickAction returns push when branch is ahead", () => {
396 const quick = resolveQuickAction(status({ branch: "main", aheadCount: 2, openPr: null }), false, true);
400 const quick = resolveQuickAction(
401 status({ branch: "main", aheadCount: 2, openPr: null }),
402 false,
403 true,
404 );
397405 assert.deepInclude(quick, {
398406 kind: "run_action",
399407 action: "commit_push",
......@@ -405,7 +413,10 @@ describe("when: on default branch without open PR", () => {
405413
406414describe("when: working tree has local changes and branch is behind upstream", () => {
407415 it("resolveQuickAction still prefers commit, push, and create PR", () => {
408 const quick = resolveQuickAction(status({ hasWorkingTreeChanges: true, behindCount: 1 }), false);
416 const quick = resolveQuickAction(
417 status({ hasWorkingTreeChanges: true, behindCount: 1 }),
418 false,
419 );
409420 assert.deepInclude(quick, {
410421 kind: "run_action",
411422 action: "commit_push_pr",
......@@ -486,7 +497,10 @@ describe("when: HEAD is detached and there are no local changes", () => {
486497
487498describe("when: branch has no upstream configured", () => {
488499 it("resolveQuickAction returns disabled commit state when clean and no commits are ahead", () => {
489 const quick = resolveQuickAction(status({ hasUpstream: false, openPr: null, aheadCount: 0 }), false);
500 const quick = resolveQuickAction(
501 status({ hasUpstream: false, openPr: null, aheadCount: 0 }),
502 false,
503 );
490504 assert.deepInclude(quick, {
491505 kind: "show_hint",
492506 label: "Commit",
......@@ -540,7 +554,10 @@ describe("when: branch has no upstream configured", () => {
540554 });
541555
542556 it("buildMenuItems disables push and create PR when no commits are ahead", () => {
543 const items = buildMenuItems(status({ hasUpstream: false, openPr: null, aheadCount: 0 }), false);
557 const items = buildMenuItems(
558 status({ hasUpstream: false, openPr: null, aheadCount: 0 }),
559 false,
560 );
544561 assert.deepEqual(items, [
545562 {
546563 id: "commit",
apps/web/src/components/GitActionsControl.logic.ts+14-12
......@@ -1,4 +1,8 @@
1import type { GitRunStackedActionResult, GitStackedAction, GitStatusResult } from "@t3tools/contracts";
1import type {
2 GitRunStackedActionResult,
3 GitStackedAction,
4 GitStatusResult,
5} from "@t3tools/contracts";
26
37export type GitActionIconName = "commit" | "push" | "pr";
48
......@@ -29,16 +33,20 @@ function shortenSha(sha: string | undefined): string | null {
2933 return sha.slice(0, SHORT_SHA_LENGTH);
3034}
3135
32function truncateText(value: string | undefined, maxLength = TOAST_DESCRIPTION_MAX): string | undefined {
36function truncateText(
37 value: string | undefined,
38 maxLength = TOAST_DESCRIPTION_MAX,
39): string | undefined {
3340 if (!value) return undefined;
3441 if (value.length <= maxLength) return value;
3542 if (maxLength <= 3) return "...".slice(0, maxLength);
3643 return `${value.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
3744}
3845
39export function summarizeGitResult(
40 result: GitRunStackedActionResult,
41): { title: string; description?: string } {
46export function summarizeGitResult(result: GitRunStackedActionResult): {
47 title: string;
48 description?: string;
49} {
4250 const withDescription = (title: string, description: string | undefined) =>
4351 description ? { title, description } : { title };
4452
......@@ -79,12 +87,7 @@ export function buildMenuItems(
7987 const hasOpenPr = gitStatus.openPr !== null;
8088 const isBehind = gitStatus.behindCount > 0;
8189 const canCommit = !isBusy && hasChanges;
82 const canPush =
83 !isBusy &&
84 hasBranch &&
85 !hasChanges &&
86 !isBehind &&
87 gitStatus.aheadCount > 0;
90 const canPush = !isBusy && hasBranch && !hasChanges && !isBehind && gitStatus.aheadCount > 0;
8891 const canCreatePr =
8992 !isBusy &&
9093 hasBranch &&
......@@ -215,4 +218,3 @@ export function requiresDefaultBranchConfirmation(
215218 if (!isDefaultBranch) return false;
216219 return action === "commit_push" || action === "commit_push_pr";
217220}
218
apps/web/src/components/GitActionsControl.tsx+14-19
......@@ -1,8 +1,4 @@
1import {
2 type GitStatusResult,
3 type GitStackedAction,
4 type NativeApi,
5} from "@t3tools/contracts";
1import { type GitStatusResult, type GitStackedAction, type NativeApi } from "@t3tools/contracts";
62import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
73import { useCallback, useMemo, useState } from "react";
84import { ChevronDownIcon, CloudUploadIcon, GitCommitIcon, InfoIcon } from "lucide-react";
......@@ -195,9 +191,9 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
195191 ? (quickAction.hint ?? "This action is currently unavailable.")
196192 : null;
197193 const [isQuickActionPopoverOpen, setIsQuickActionPopoverOpen] = useState(false);
198 const [openDisabledMenuItemId, setOpenDisabledMenuItemId] = useState<GitActionMenuItem["id"] | null>(
199 null,
200 );
194 const [openDisabledMenuItemId, setOpenDisabledMenuItemId] = useState<
195 GitActionMenuItem["id"] | null
196 >(null);
201197
202198 const refreshGitStatus = useCallback(async () => {
203199 if (!api || !gitCwd) return;
......@@ -240,13 +236,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
240236 }, [api, gitStatus?.openPr?.url]);
241237
242238 const runGitActionWithToast = useCallback(
243 async ({
244 action,
245 commitMessage,
246 }: {
247 action: GitStackedAction;
248 commitMessage?: string;
249 }) => {
239 async ({ action, commitMessage }: { action: GitStackedAction; commitMessage?: string }) => {
250240 const confirmed = await maybeConfirmPushToDefaultBranch(action);
251241 if (!confirmed) return;
252242
......@@ -396,8 +386,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
396386 : activeDialogAction === "push"
397387 ? "commit_push"
398388 : "commit_push_pr";
399 const commitMessage =
400 activeDialogAction === "commit" ? dialogCommitMessage.trim() : "";
389 const commitMessage = activeDialogAction === "commit" ? dialogCommitMessage.trim() : "";
401390 setActiveDialogAction(null);
402391 void runGitActionWithToast({
403392 action,
......@@ -503,7 +492,11 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
503492 </MenuTrigger>
504493 <MenuPopup align="end" sideOffset={4}>
505494 {gitActionMenuItems.map((item) => {
506 const disabledReason = getMenuActionDisabledReason(item, gitStatus, isGitActionRunning);
495 const disabledReason = getMenuActionDisabledReason(
496 item,
497 gitStatus,
498 isGitActionRunning,
499 );
507500 if (item.disabled && disabledReason) {
508501 return (
509502 <Popover
......@@ -611,7 +604,9 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
611604 <>
612605 <span className="text-success">+{gitStatus.workingTree.insertions}</span>
613606 <span className="text-muted-foreground"> / </span>
614 <span className="text-destructive">-{gitStatus.workingTree.deletions}</span>
607 <span className="text-destructive">
608 -{gitStatus.workingTree.deletions}
609 </span>
615610 </>
616611 )}
617612 </span>
apps/web/src/components/ThreadTerminalDrawer.tsx+13-18
......@@ -28,16 +28,11 @@ const MAX_DRAWER_HEIGHT_RATIO = 0.75;
2828
2929function maxDrawerHeight(): number {
3030 if (typeof window === "undefined") return DEFAULT_THREAD_TERMINAL_HEIGHT;
31 return Math.max(
32 MIN_DRAWER_HEIGHT,
33 Math.floor(window.innerHeight * MAX_DRAWER_HEIGHT_RATIO),
34 );
31 return Math.max(MIN_DRAWER_HEIGHT, Math.floor(window.innerHeight * MAX_DRAWER_HEIGHT_RATIO));
3532}
3633
3734function clampDrawerHeight(height: number): number {
38 const safeHeight = Number.isFinite(height)
39 ? height
40 : DEFAULT_THREAD_TERMINAL_HEIGHT;
35 const safeHeight = Number.isFinite(height) ? height : DEFAULT_THREAD_TERMINAL_HEIGHT;
4136 const maxHeight = maxDrawerHeight();
4237 return Math.min(Math.max(Math.round(safeHeight), MIN_DRAWER_HEIGHT), maxHeight);
4338}
......@@ -51,8 +46,7 @@ function terminalThemeFromApp(): ITheme {
5146 const bodyStyles = getComputedStyle(document.body);
5247 const background =
5348 bodyStyles.backgroundColor || (isDark ? "rgb(14, 18, 24)" : "rgb(255, 255, 255)");
54 const foreground =
55 bodyStyles.color || (isDark ? "rgb(237, 241, 247)" : "rgb(28, 33, 41)");
49 const foreground = bodyStyles.color || (isDark ? "rgb(237, 241, 247)" : "rgb(28, 33, 41)");
5650
5751 if (isDark) {
5852 return {
......@@ -146,8 +140,7 @@ function TerminalViewport({
146140 lineHeight: 1.2,
147141 fontSize: 12,
148142 scrollback: 5_000,
149 fontFamily:
150 '"SF Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace',
143 fontFamily: '"SF Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace',
151144 theme: terminalThemeFromApp(),
152145 });
153146 terminal.loadAddon(fitAddon);
......@@ -538,8 +531,9 @@ export default function ThreadTerminalDrawer({
538531 return indexByTerminal >= 0 ? indexByTerminal : 0;
539532 }, [activeTerminalGroupId, resolvedActiveTerminalId, resolvedTerminalGroups]);
540533
541 const visibleTerminalIds =
542 resolvedTerminalGroups[resolvedActiveGroupIndex]?.terminalIds ?? [resolvedActiveTerminalId];
534 const visibleTerminalIds = resolvedTerminalGroups[resolvedActiveGroupIndex]?.terminalIds ?? [
535 resolvedActiveTerminalId,
536 ];
543537 const hasTerminalSidebar = normalizedTerminalIds.length > 1;
544538 const isSplitView = visibleTerminalIds.length > 1;
545539 const showGroupHeaders =
......@@ -707,9 +701,7 @@ export default function ThreadTerminalDrawer({
707701 <div
708702 key={terminalId}
709703 className={`min-h-0 min-w-0 border-l first:border-l-0 ${
710 terminalId === resolvedActiveTerminalId
711 ? "border-border"
712 : "border-border/70"
704 terminalId === resolvedActiveTerminalId ? "border-border" : "border-border/70"
713705 }`}
714706 onMouseDown={() => {
715707 if (terminalId !== resolvedActiveTerminalId) {
......@@ -785,7 +777,8 @@ export default function ThreadTerminalDrawer({
785777
786778 <div className="min-h-0 flex-1 overflow-y-auto px-1 py-1">
787779 {resolvedTerminalGroups.map((terminalGroup, groupIndex) => {
788 const isGroupActive = terminalGroup.terminalIds.includes(resolvedActiveTerminalId);
780 const isGroupActive =
781 terminalGroup.terminalIds.includes(resolvedActiveTerminalId);
789782 const groupActiveTerminalId = isGroupActive
790783 ? resolvedActiveTerminalId
791784 : (terminalGroup.terminalIds[0] ?? resolvedActiveTerminalId);
......@@ -808,7 +801,9 @@ export default function ThreadTerminalDrawer({
808801 </button>
809802 )}
810803
811 <div className={showGroupHeaders ? "ml-1 border-l border-border/60 pl-1.5" : ""}>
804 <div
805 className={showGroupHeaders ? "ml-1 border-l border-border/60 pl-1.5" : ""}
806 >
812807 {terminalGroup.terminalIds.map((terminalId) => {
813808 const isActive = terminalId === resolvedActiveTerminalId;
814809 return (
apps/web/src/components/ui/alert-dialog.tsx+10-38
......@@ -11,15 +11,10 @@ const AlertDialog = AlertDialogPrimitive.Root;
1111const AlertDialogPortal = AlertDialogPrimitive.Portal;
1212
1313function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) {
14 return (
15 <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
16 );
14 return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
1715}
1816
19function AlertDialogBackdrop({
20 className,
21 ...props
22}: AlertDialogPrimitive.Backdrop.Props) {
17function AlertDialogBackdrop({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {
2318 return (
2419 <AlertDialogPrimitive.Backdrop
2520 className={cn(
......@@ -32,10 +27,7 @@ function AlertDialogBackdrop({
3227 );
3328}
3429
35function AlertDialogViewport({
36 className,
37 ...props
38}: AlertDialogPrimitive.Viewport.Props) {
30function AlertDialogViewport({ className, ...props }: AlertDialogPrimitive.Viewport.Props) {
3931 return (
4032 <AlertDialogPrimitive.Viewport
4133 className={cn(
......@@ -59,10 +51,7 @@ function AlertDialogPopup({
5951 <AlertDialogPortal>
6052 <AlertDialogBackdrop />
6153 <AlertDialogViewport
62 className={cn(
63 bottomStickOnMobile &&
64 "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12",
65 )}
54 className={cn(bottomStickOnMobile && "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12")}
6655 >
6756 <AlertDialogPrimitive.Popup
6857 className={cn(
......@@ -79,16 +68,10 @@ function AlertDialogPopup({
7968 );
8069}
8170
82function AlertDialogHeader({
83 className,
84 ...props
85}: React.ComponentProps<"div">) {
71function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
8672 return (
8773 <div
88 className={cn(
89 "flex flex-col gap-2 p-6 text-center max-sm:pb-4 sm:text-left",
90 className,
91 )}
74 className={cn("flex flex-col gap-2 p-6 text-center max-sm:pb-4 sm:text-left", className)}
9275 data-slot="alert-dialog-header"
9376 {...props}
9477 />
......@@ -116,26 +99,17 @@ function AlertDialogFooter({
11699 );
117100}
118101
119function AlertDialogTitle({
120 className,
121 ...props
122}: AlertDialogPrimitive.Title.Props) {
102function AlertDialogTitle({ className, ...props }: AlertDialogPrimitive.Title.Props) {
123103 return (
124104 <AlertDialogPrimitive.Title
125 className={cn(
126 "font-heading font-semibold text-xl leading-none",
127 className,
128 )}
105 className={cn("font-heading font-semibold text-xl leading-none", className)}
129106 data-slot="alert-dialog-title"
130107 {...props}
131108 />
132109 );
133110}
134111
135function AlertDialogDescription({
136 className,
137 ...props
138}: AlertDialogPrimitive.Description.Props) {
112function AlertDialogDescription({ className, ...props }: AlertDialogPrimitive.Description.Props) {
139113 return (
140114 <AlertDialogPrimitive.Description
141115 className={cn("text-muted-foreground text-sm", className)}
......@@ -146,9 +120,7 @@ function AlertDialogDescription({
146120}
147121
148122function AlertDialogClose(props: AlertDialogPrimitive.Close.Props) {
149 return (
150 <AlertDialogPrimitive.Close data-slot="alert-dialog-close" {...props} />
151 );
123 return <AlertDialogPrimitive.Close data-slot="alert-dialog-close" {...props} />;
152124}
153125
154126export {
apps/web/src/components/ui/alert.tsx+4-12
......@@ -11,10 +11,8 @@ const alertVariants = cva(
1111 },
1212 variants: {
1313 variant: {
14 default:
15 "bg-transparent dark:bg-input/32 [&>svg]:text-muted-foreground",
16 error:
17 "border-destructive/32 bg-destructive/4 [&>svg]:text-destructive",
14 default: "bg-transparent dark:bg-input/32 [&>svg]:text-muted-foreground",
15 error: "border-destructive/32 bg-destructive/4 [&>svg]:text-destructive",
1816 info: "border-info/32 bg-info/4 [&>svg]:text-info",
1917 success: "border-success/32 bg-success/4 [&>svg]:text-success",
2018 warning: "border-warning/32 bg-warning/4 [&>svg]:text-warning",
......@@ -48,16 +46,10 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
4846 );
4947}
5048
51function AlertDescription({
52 className,
53 ...props
54}: React.ComponentProps<"div">) {
49function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
5550 return (
5651 <div
57 className={cn(
58 "flex flex-col gap-2.5 text-muted-foreground [svg~&]:col-start-2",
59 className,
60 )}
52 className={cn("flex flex-col gap-2.5 text-muted-foreground [svg~&]:col-start-2", className)}
6153 data-slot="alert-description"
6254 {...props}
6355 />
apps/web/src/components/ui/autocomplete.tsx+15-62
......@@ -121,11 +121,7 @@ function AutocompletePopup({
121121 );
122122}
123123
124function AutocompleteItem({
125 className,
126 children,
127 ...props
128}: AutocompletePrimitive.Item.Props) {
124function AutocompleteItem({ className, children, ...props }: AutocompletePrimitive.Item.Props) {
129125 return (
130126 <AutocompletePrimitive.Item
131127 className={cn(
......@@ -140,10 +136,7 @@ function AutocompleteItem({
140136 );
141137}
142138
143function AutocompleteSeparator({
144 className,
145 ...props
146}: AutocompletePrimitive.Separator.Props) {
139function AutocompleteSeparator({ className, ...props }: AutocompletePrimitive.Separator.Props) {
147140 return (
148141 <AutocompletePrimitive.Separator
149142 className={cn("mx-2 my-1 h-px bg-border last:hidden", className)}
......@@ -153,10 +146,7 @@ function AutocompleteSeparator({
153146 );
154147}
155148
156function AutocompleteGroup({
157 className,
158 ...props
159}: AutocompletePrimitive.Group.Props) {
149function AutocompleteGroup({ className, ...props }: AutocompletePrimitive.Group.Props) {
160150 return (
161151 <AutocompletePrimitive.Group
162152 className={cn("[[role=group]+&]:mt-1.5", className)}
......@@ -166,26 +156,17 @@ function AutocompleteGroup({
166156 );
167157}
168158
169function AutocompleteGroupLabel({
170 className,
171 ...props
172}: AutocompletePrimitive.GroupLabel.Props) {
159function AutocompleteGroupLabel({ className, ...props }: AutocompletePrimitive.GroupLabel.Props) {
173160 return (
174161 <AutocompletePrimitive.GroupLabel
175 className={cn(
176 "px-2 py-1.5 font-medium text-muted-foreground text-xs",
177 className,
178 )}
162 className={cn("px-2 py-1.5 font-medium text-muted-foreground text-xs", className)}
179163 data-slot="autocomplete-group-label"
180164 {...props}
181165 />
182166 );
183167}
184168
185function AutocompleteEmpty({
186 className,
187 ...props
188}: AutocompletePrimitive.Empty.Props) {
169function AutocompleteEmpty({ className, ...props }: AutocompletePrimitive.Empty.Props) {
189170 return (
190171 <AutocompletePrimitive.Empty
191172 className={cn(
......@@ -198,36 +179,21 @@ function AutocompleteEmpty({
198179 );
199180}
200181
201function AutocompleteRow({
202 className,
203 ...props
204}: AutocompletePrimitive.Row.Props) {
182function AutocompleteRow({ className, ...props }: AutocompletePrimitive.Row.Props) {
205183 return (
206 <AutocompletePrimitive.Row
207 className={className}
208 data-slot="autocomplete-row"
209 {...props}
210 />
184 <AutocompletePrimitive.Row className={className} data-slot="autocomplete-row" {...props} />
211185 );
212186}
213187
214188function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) {
215 return (
216 <AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} />
217 );
189 return <AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} />;
218190}
219191
220function AutocompleteList({
221 className,
222 ...props
223}: AutocompletePrimitive.List.Props) {
192function AutocompleteList({ className, ...props }: AutocompletePrimitive.List.Props) {
224193 return (
225194 <ScrollArea scrollbarGutter scrollFade>
226195 <AutocompletePrimitive.List
227 className={cn(
228 "not-empty:scroll-py-1 not-empty:p-1 in-data-has-overflow-y:pe-3",
229 className,
230 )}
196 className={cn("not-empty:scroll-py-1 not-empty:p-1 in-data-has-overflow-y:pe-3", className)}
231197 data-slot="autocomplete-list"
232198 {...props}
233199 />
......@@ -235,10 +201,7 @@ function AutocompleteList({
235201 );
236202}
237203
238function AutocompleteClear({
239 className,
240 ...props
241}: AutocompletePrimitive.Clear.Props) {
204function AutocompleteClear({ className, ...props }: AutocompletePrimitive.Clear.Props) {
242205 return (
243206 <AutocompletePrimitive.Clear
244207 className={cn(
......@@ -253,10 +216,7 @@ function AutocompleteClear({
253216 );
254217}
255218
256function AutocompleteStatus({
257 className,
258 ...props
259}: AutocompletePrimitive.Status.Props) {
219function AutocompleteStatus({ className, ...props }: AutocompletePrimitive.Status.Props) {
260220 return (
261221 <AutocompletePrimitive.Status
262222 className={cn(
......@@ -269,15 +229,8 @@ function AutocompleteStatus({
269229 );
270230}
271231
272function AutocompleteCollection({
273 ...props
274}: AutocompletePrimitive.Collection.Props) {
275 return (
276 <AutocompletePrimitive.Collection
277 data-slot="autocomplete-collection"
278 {...props}
279 />
280 );
232function AutocompleteCollection({ ...props }: AutocompletePrimitive.Collection.Props) {
233 return <AutocompletePrimitive.Collection data-slot="autocomplete-collection" {...props} />;
281234}
282235
283236function AutocompleteTrigger({
apps/web/src/components/ui/badge.tsx+4-8
......@@ -21,17 +21,13 @@ const badgeVariants = cva(
2121 sm: "h-5 min-w-5 rounded-[.25rem] px-[calc(--spacing(1)-1px)] text-xs sm:h-4 sm:min-w-4 sm:text-[.625rem]",
2222 },
2323 variant: {
24 default:
25 "bg-primary text-primary-foreground [button&,a&]:hover:bg-primary/90",
26 destructive:
27 "bg-destructive text-white [button&,a&]:hover:bg-destructive/90",
28 error:
29 "bg-destructive/8 text-destructive-foreground dark:bg-destructive/16",
24 default: "bg-primary text-primary-foreground [button&,a&]:hover:bg-primary/90",
25 destructive: "bg-destructive text-white [button&,a&]:hover:bg-destructive/90",
26 error: "bg-destructive/8 text-destructive-foreground dark:bg-destructive/16",
3027 info: "bg-info/8 text-info-foreground dark:bg-info/16",
3128 outline:
3229 "border-input bg-background text-foreground dark:bg-input/32 [button&,a&]:hover:bg-accent/50 dark:[button&,a&]:hover:bg-input/48",
33 secondary:
34 "bg-secondary text-secondary-foreground [button&,a&]:hover:bg-secondary/90",
30 secondary: "bg-secondary text-secondary-foreground [button&,a&]:hover:bg-secondary/90",
3531 success: "bg-success/8 text-success-foreground dark:bg-success/16",
3632 warning: "bg-warning/8 text-warning-foreground dark:bg-warning/16",
3733 },
apps/web/src/components/ui/card.tsx+12-60
......@@ -5,11 +5,7 @@ import { useRender } from "@base-ui/react/use-render";
55
66import { cn } from "~/lib/utils";
77
8function Card({
9 className,
10 render,
11 ...props
12}: useRender.ComponentProps<"div">) {
8function Card({ className, render, ...props }: useRender.ComponentProps<"div">) {
139 const defaultProps = {
1410 className: cn(
1511 "relative flex flex-col rounded-2xl border bg-card not-dark:bg-clip-padding text-card-foreground shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
......@@ -25,11 +21,7 @@ function Card({
2521 });
2622}
2723
28function CardFrame({
29 className,
30 render,
31 ...props
32}: useRender.ComponentProps<"div">) {
24function CardFrame({ className, render, ...props }: useRender.ComponentProps<"div">) {
3325 const defaultProps = {
3426 className: cn(
3527 "[--clip-top:-1rem] [--clip-bottom:-1rem] *:data-[slot=card]:first:[--clip-top:1px] *:data-[slot=card]:last:[--clip-bottom:1px] flex flex-col relative rounded-2xl border bg-card before:bg-muted/72 not-dark:bg-clip-padding text-card-foreground shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)] *:data-[slot=card]:-m-px *:not-last:data-[slot=card]:rounded-b-xl *:not-last:data-[slot=card]:before:rounded-b-[calc(var(--radius-xl)-1px)] *:not-first:data-[slot=card]:rounded-t-xl *:not-first:data-[slot=card]:before:rounded-t-[calc(var(--radius-xl)-1px)] *:data-[slot=card]:[clip-path:inset(var(--clip-top)_1px_var(--clip-bottom)_1px_round_calc(var(--radius-2xl)-1px))] *:data-[slot=card]:shadow-none *:data-[slot=card]:before:hidden *:data-[slot=card]:bg-clip-padding",
......@@ -45,11 +37,7 @@ function CardFrame({
4537 });
4638}
4739
48function CardFrameHeader({
49 className,
50 render,
51 ...props
52}: useRender.ComponentProps<"div">) {
40function CardFrameHeader({ className, render, ...props }: useRender.ComponentProps<"div">) {
5341 const defaultProps = {
5442 className: cn("relative flex flex-col px-6 py-4", className),
5543 "data-slot": "card-frame-header",
......@@ -62,11 +50,7 @@ function CardFrameHeader({
6250 });
6351}
6452
65function CardFrameTitle({
66 className,
67 render,
68 ...props
69}: useRender.ComponentProps<"div">) {
53function CardFrameTitle({ className, render, ...props }: useRender.ComponentProps<"div">) {
7054 const defaultProps = {
7155 className: cn("font-semibold text-sm", className),
7256 "data-slot": "card-frame-title",
......@@ -79,11 +63,7 @@ function CardFrameTitle({
7963 });
8064}
8165
82function CardFrameDescription({
83 className,
84 render,
85 ...props
86}: useRender.ComponentProps<"div">) {
66function CardFrameDescription({ className, render, ...props }: useRender.ComponentProps<"div">) {
8767 const defaultProps = {
8868 className: cn("text-muted-foreground text-sm", className),
8969 "data-slot": "card-frame-description",
......@@ -96,11 +76,7 @@ function CardFrameDescription({
9676 });
9777}
9878
99function CardFrameFooter({
100 className,
101 render,
102 ...props
103}: useRender.ComponentProps<"div">) {
79function CardFrameFooter({ className, render, ...props }: useRender.ComponentProps<"div">) {
10480 const defaultProps = {
10581 className: cn("px-6 py-4", className),
10682 "data-slot": "card-frame-footer",
......@@ -113,11 +89,7 @@ function CardFrameFooter({
11389 });
11490}
11591
116function CardHeader({
117 className,
118 render,
119 ...props
120}: useRender.ComponentProps<"div">) {
92function CardHeader({ className, render, ...props }: useRender.ComponentProps<"div">) {
12193 const defaultProps = {
12294 className: cn(
12395 "grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 p-6 in-[[data-slot=card]:has(>[data-slot=card-panel])]:pb-4 has-data-[slot=card-action]:grid-cols-[1fr_auto]",
......@@ -133,11 +105,7 @@ function CardHeader({
133105 });
134106}
135107
136function CardTitle({
137 className,
138 render,
139 ...props
140}: useRender.ComponentProps<"div">) {
108function CardTitle({ className, render, ...props }: useRender.ComponentProps<"div">) {
141109 const defaultProps = {
142110 className: cn("font-semibold text-lg leading-none", className),
143111 "data-slot": "card-title",
......@@ -150,11 +118,7 @@ function CardTitle({
150118 });
151119}
152120
153function CardDescription({
154 className,
155 render,
156 ...props
157}: useRender.ComponentProps<"div">) {
121function CardDescription({ className, render, ...props }: useRender.ComponentProps<"div">) {
158122 const defaultProps = {
159123 className: cn("text-muted-foreground text-sm", className),
160124 "data-slot": "card-description",
......@@ -167,11 +131,7 @@ function CardDescription({
167131 });
168132}
169133
170function CardAction({
171 className,
172 render,
173 ...props
174}: useRender.ComponentProps<"div">) {
134function CardAction({ className, render, ...props }: useRender.ComponentProps<"div">) {
175135 const defaultProps = {
176136 className: cn(
177137 "col-start-2 row-span-2 row-start-1 self-start justify-self-end inline-flex",
......@@ -187,11 +147,7 @@ function CardAction({
187147 });
188148}
189149
190function CardPanel({
191 className,
192 render,
193 ...props
194}: useRender.ComponentProps<"div">) {
150function CardPanel({ className, render, ...props }: useRender.ComponentProps<"div">) {
195151 const defaultProps = {
196152 className: cn(
197153 "flex-1 p-6 in-[[data-slot=card]:has(>[data-slot=card-header]:not(.border-b))]:pt-0 in-[[data-slot=card]:has(>[data-slot=card-footer]:not(.border-t))]:pb-0",
......@@ -207,11 +163,7 @@ function CardPanel({
207163 });
208164}
209165
210function CardFooter({
211 className,
212 render,
213 ...props
214}: useRender.ComponentProps<"div">) {
166function CardFooter({ className, render, ...props }: useRender.ComponentProps<"div">) {
215167 const defaultProps = {
216168 className: cn(
217169 "flex items-center p-6 in-[[data-slot=card]:has(>[data-slot=card-panel])]:pt-4",
apps/web/src/components/ui/command.tsx+12-55
......@@ -23,18 +23,10 @@ const CommandDialogPortal = CommandDialogPrimitive.Portal;
2323const CommandCreateHandle = CommandDialogPrimitive.createHandle;
2424
2525function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) {
26 return (
27 <CommandDialogPrimitive.Trigger
28 data-slot="command-dialog-trigger"
29 {...props}
30 />
31 );
26 return <CommandDialogPrimitive.Trigger data-slot="command-dialog-trigger" {...props} />;
3227}
3328
34function CommandDialogBackdrop({
35 className,
36 ...props
37}: CommandDialogPrimitive.Backdrop.Props) {
29function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.Backdrop.Props) {
3830 return (
3931 <CommandDialogPrimitive.Backdrop
4032 className={cn(
......@@ -47,10 +39,7 @@ function CommandDialogBackdrop({
4739 );
4840}
4941
50function CommandDialogViewport({
51 className,
52 ...props
53}: CommandDialogPrimitive.Viewport.Props) {
42function CommandDialogViewport({ className, ...props }: CommandDialogPrimitive.Viewport.Props) {
5443 return (
5544 <CommandDialogPrimitive.Viewport
5645 className={cn(
......@@ -63,11 +52,7 @@ function CommandDialogViewport({
6352 );
6453}
6554
66function CommandDialogPopup({
67 className,
68 children,
69 ...props
70}: CommandDialogPrimitive.Popup.Props) {
55function CommandDialogPopup({ className, children, ...props }: CommandDialogPrimitive.Popup.Props) {
7156 return (
7257 <CommandDialogPortal>
7358 <CommandDialogBackdrop />
......@@ -125,10 +110,7 @@ function CommandInput({
125110 );
126111}
127112
128function CommandList({
129 className,
130 ...props
131}: React.ComponentProps<typeof AutocompleteList>) {
113function CommandList({ className, ...props }: React.ComponentProps<typeof AutocompleteList>) {
132114 return (
133115 <AutocompleteList
134116 className={cn("not-empty:scroll-py-2 not-empty:p-2", className)}
......@@ -138,10 +120,7 @@ function CommandList({
138120 );
139121}
140122
141function CommandEmpty({
142 className,
143 ...props
144}: React.ComponentProps<typeof AutocompleteEmpty>) {
123function CommandEmpty({ className, ...props }: React.ComponentProps<typeof AutocompleteEmpty>) {
145124 return (
146125 <AutocompleteEmpty
147126 className={cn("not-empty:py-6", className)}
......@@ -160,17 +139,8 @@ function CommandPanel({ className, ...props }: React.ComponentProps<"div">) {
160139 );
161140}
162141
163function CommandGroup({
164 className,
165 ...props
166}: React.ComponentProps<typeof AutocompleteGroup>) {
167 return (
168 <AutocompleteGroup
169 className={className}
170 data-slot="command-group"
171 {...props}
172 />
173 );
142function CommandGroup({ className, ...props }: React.ComponentProps<typeof AutocompleteGroup>) {
143 return <AutocompleteGroup className={className} data-slot="command-group" {...props} />;
174144}
175145
176146function CommandGroupLabel({
......@@ -178,30 +148,17 @@ function CommandGroupLabel({
178148 ...props
179149}: React.ComponentProps<typeof AutocompleteGroupLabel>) {
180150 return (
181 <AutocompleteGroupLabel
182 className={className}
183 data-slot="command-group-label"
184 {...props}
185 />
151 <AutocompleteGroupLabel className={className} data-slot="command-group-label" {...props} />
186152 );
187153}
188154
189function CommandCollection({
190 ...props
191}: React.ComponentProps<typeof AutocompleteCollection>) {
155function CommandCollection({ ...props }: React.ComponentProps<typeof AutocompleteCollection>) {
192156 return <AutocompleteCollection data-slot="command-collection" {...props} />;
193157}
194158
195function CommandItem({
196 className,
197 ...props
198}: React.ComponentProps<typeof AutocompleteItem>) {
159function CommandItem({ className, ...props }: React.ComponentProps<typeof AutocompleteItem>) {
199160 return (
200 <AutocompleteItem
201 className={cn("py-1.5", className)}
202 data-slot="command-item"
203 {...props}
204 />
161 <AutocompleteItem className={cn("py-1.5", className)} data-slot="command-item" {...props} />
205162 );
206163}
207164
apps/web/src/components/ui/dialog.tsx+5-20
......@@ -20,10 +20,7 @@ function DialogClose(props: DialogPrimitive.Close.Props) {
2020 return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
2121}
2222
23function DialogBackdrop({
24 className,
25 ...props
26}: DialogPrimitive.Backdrop.Props) {
23function DialogBackdrop({ className, ...props }: DialogPrimitive.Backdrop.Props) {
2724 return (
2825 <DialogPrimitive.Backdrop
2926 className={cn(
......@@ -36,10 +33,7 @@ function DialogBackdrop({
3633 );
3734}
3835
39function DialogViewport({
40 className,
41 ...props
42}: DialogPrimitive.Viewport.Props) {
36function DialogViewport({ className, ...props }: DialogPrimitive.Viewport.Props) {
4337 return (
4438 <DialogPrimitive.Viewport
4539 className={cn(
......@@ -66,10 +60,7 @@ function DialogPopup({
6660 <DialogPortal>
6761 <DialogBackdrop />
6862 <DialogViewport
69 className={cn(
70 bottomStickOnMobile &&
71 "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12",
72 )}
63 className={cn(bottomStickOnMobile && "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12")}
7364 >
7465 <DialogPrimitive.Popup
7566 className={cn(
......@@ -135,20 +126,14 @@ function DialogFooter({
135126function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
136127 return (
137128 <DialogPrimitive.Title
138 className={cn(
139 "font-heading font-semibold text-xl leading-none",
140 className,
141 )}
129 className={cn("font-heading font-semibold text-xl leading-none", className)}
142130 data-slot="dialog-title"
143131 {...props}
144132 />
145133 );
146134}
147135
148function DialogDescription({
149 className,
150 ...props
151}: DialogPrimitive.Description.Props) {
136function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) {
152137 return (
153138 <DialogPrimitive.Description
154139 className={cn("text-muted-foreground text-sm", className)}
apps/web/src/components/ui/empty.tsx+3-16
......@@ -18,10 +18,7 @@ function Empty({ className, ...props }: React.ComponentProps<"div">) {
1818function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
1919 return (
2020 <div
21 className={cn(
22 "flex max-w-sm flex-col items-center text-center",
23 className,
24 )}
21 className={cn("flex max-w-sm flex-col items-center text-center", className)}
2522 data-slot="empty-header"
2623 {...props}
2724 />
......@@ -73,10 +70,7 @@ function EmptyMedia({
7370 />
7471 </>
7572 )}
76 <div
77 className={cn(emptyMediaVariants({ className, variant }))}
78 {...props}
79 />
73 <div className={cn(emptyMediaVariants({ className, variant }))} {...props} />
8074 </div>
8175 );
8276}
......@@ -117,11 +111,4 @@ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
117111 );
118112}
119113
120export {
121 Empty,
122 EmptyHeader,
123 EmptyTitle,
124 EmptyDescription,
125 EmptyContent,
126 EmptyMedia,
127};
114export { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia };
apps/web/src/components/ui/field.tsx+3-18
......@@ -29,18 +29,11 @@ function FieldLabel({ className, ...props }: FieldPrimitive.Label.Props) {
2929
3030function FieldItem({ className, ...props }: FieldPrimitive.Item.Props) {
3131 return (
32 <FieldPrimitive.Item
33 className={cn("flex", className)}
34 data-slot="field-item"
35 {...props}
36 />
32 <FieldPrimitive.Item className={cn("flex", className)} data-slot="field-item" {...props} />
3733 );
3834}
3935
40function FieldDescription({
41 className,
42 ...props
43}: FieldPrimitive.Description.Props) {
36function FieldDescription({ className, ...props }: FieldPrimitive.Description.Props) {
4437 return (
4538 <FieldPrimitive.Description
4639 className={cn("text-muted-foreground text-xs", className)}
......@@ -63,12 +56,4 @@ function FieldError({ className, ...props }: FieldPrimitive.Error.Props) {
6356const FieldControl = FieldPrimitive.Control;
6457const FieldValidity = FieldPrimitive.Validity;
6558
66export {
67 Field,
68 FieldLabel,
69 FieldDescription,
70 FieldError,
71 FieldControl,
72 FieldItem,
73 FieldValidity,
74};
59export { Field, FieldLabel, FieldDescription, FieldError, FieldControl, FieldItem, FieldValidity };
apps/web/src/components/ui/fieldset.tsx+1-4
......@@ -13,10 +13,7 @@ function Fieldset({ className, ...props }: FieldsetPrimitive.Root.Props) {
1313 />
1414 );
1515}
16function FieldsetLegend({
17 className,
18 ...props
19}: FieldsetPrimitive.Legend.Props) {
16function FieldsetLegend({ className, ...props }: FieldsetPrimitive.Legend.Props) {
2017 return (
2118 <FieldsetPrimitive.Legend
2219 className={cn("font-semibold text-foreground", className)}
apps/web/src/components/ui/input-group.tsx+4-10
......@@ -60,9 +60,9 @@ function InputGroupAddon({
6060 if (isInteractive) return;
6161 e.preventDefault();
6262 const parent = e.currentTarget.parentElement;
63 const input = parent?.querySelector<
64 HTMLInputElement | HTMLTextAreaElement
65 >("input, textarea");
63 const input = parent?.querySelector<HTMLInputElement | HTMLTextAreaElement>(
64 "input, textarea",
65 );
6666 if (input && !parent?.querySelector("input:focus, textarea:focus")) {
6767 input.focus();
6868 }
......@@ -92,10 +92,4 @@ function InputGroupTextarea({ className, ...props }: TextareaProps) {
9292 return <Textarea className={className} unstyled {...props} />;
9393}
9494
95export {
96 InputGroup,
97 InputGroupAddon,
98 InputGroupText,
99 InputGroupInput,
100 InputGroupTextarea,
101};
95export { InputGroup, InputGroupAddon, InputGroupText, InputGroupInput, InputGroupTextarea };
apps/web/src/components/ui/input.tsx+2-6
......@@ -5,10 +5,7 @@ import type * as React from "react";
55
66import { cn } from "~/lib/utils";
77
8type InputProps = Omit<
9 InputPrimitive.Props & React.RefAttributes<HTMLInputElement>,
10 "size"
11> & {
8type InputProps = Omit<InputPrimitive.Props & React.RefAttributes<HTMLInputElement>, "size"> & {
129 size?: "sm" | "default" | "lg" | number;
1310 unstyled?: boolean;
1411 nativeInput?: boolean;
......@@ -23,8 +20,7 @@ function Input({
2320}: InputProps) {
2421 const inputClassName = cn(
2522 "h-8.5 w-full min-w-0 rounded-[inherit] px-[calc(--spacing(3)-1px)] leading-8.5 outline-none placeholder:text-muted-foreground/72 sm:h-7.5 sm:leading-7.5 [transition:background-color_5000000s_ease-in-out_0s]",
26 size === "sm" &&
27 "h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5",
23 size === "sm" && "h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5",
2824 size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5",
2925 props.type === "search" &&
3026 "[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none",
apps/web/src/components/ui/label.tsx+1-5
......@@ -5,11 +5,7 @@ import { useRender } from "@base-ui/react/use-render";
55
66import { cn } from "~/lib/utils";
77
8function Label({
9 className,
10 render,
11 ...props
12}: useRender.ComponentProps<"label">) {
8function Label({ className, render, ...props }: useRender.ComponentProps<"label">) {
139 const defaultProps = {
1410 className: cn(
1511 "inline-flex items-center gap-2 text-base/4.5 sm:text-sm/4 font-medium text-foreground",
apps/web/src/components/ui/menu.tsx+5-21
......@@ -12,17 +12,9 @@ const Menu = MenuPrimitive.Root;
1212
1313const MenuPortal = MenuPrimitive.Portal;
1414
15function MenuTrigger({
16 className,
17 children,
18 ...props
19}: MenuPrimitive.Trigger.Props) {
15function MenuTrigger({ className, children, ...props }: MenuPrimitive.Trigger.Props) {
2016 return (
21 <MenuPrimitive.Trigger
22 className={className}
23 data-slot="menu-trigger"
24 {...props}
25 >
17 <MenuPrimitive.Trigger className={className} data-slot="menu-trigger" {...props}>
2618 {children}
2719 </MenuPrimitive.Trigger>
2820 );
......@@ -63,9 +55,7 @@ function MenuPopup({
6355 data-slot="menu-popup"
6456 {...props}
6557 >
66 <div className="max-h-(--available-height) w-full overflow-y-auto p-1">
67 {children}
68 </div>
58 <div className="max-h-(--available-height) w-full overflow-y-auto p-1">{children}</div>
6959 </MenuPrimitive.Popup>
7060 </MenuPrimitive.Positioner>
7161 </MenuPrimitive.Portal>
......@@ -113,9 +103,7 @@ function MenuCheckboxItem({
113103 checked={checked}
114104 className={cn(
115105 "grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default items-center gap-2 rounded-sm py-1 ps-2 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
116 variant === "switch"
117 ? "grid-cols-[1fr_auto] gap-4 pe-1.5"
118 : "grid-cols-[1rem_1fr] pe-4",
106 variant === "switch" ? "grid-cols-[1fr_auto] gap-4 pe-1.5" : "grid-cols-[1rem_1fr] pe-4",
119107 className,
120108 )}
121109 data-slot="menu-checkbox-item"
......@@ -159,11 +147,7 @@ function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) {
159147 return <MenuPrimitive.RadioGroup data-slot="menu-radio-group" {...props} />;
160148}
161149
162function MenuRadioItem({
163 className,
164 children,
165 ...props
166}: MenuPrimitive.RadioItem.Props) {
150function MenuRadioItem({ className, children, ...props }: MenuPrimitive.RadioItem.Props) {
167151 return (
168152 <MenuPrimitive.RadioItem
169153 className={cn(
apps/web/src/components/ui/popover.tsx+3-14
......@@ -8,17 +8,9 @@ const PopoverCreateHandle = PopoverPrimitive.createHandle;
88
99const Popover = PopoverPrimitive.Root;
1010
11function PopoverTrigger({
12 className,
13 children,
14 ...props
15}: PopoverPrimitive.Trigger.Props) {
11function PopoverTrigger({ className, children, ...props }: PopoverPrimitive.Trigger.Props) {
1612 return (
17 <PopoverPrimitive.Trigger
18 className={className}
19 data-slot="popover-trigger"
20 {...props}
21 >
13 <PopoverPrimitive.Trigger className={className} data-slot="popover-trigger" {...props}>
2214 {children}
2315 </PopoverPrimitive.Trigger>
2416 );
......@@ -94,10 +86,7 @@ function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
9486 );
9587}
9688
97function PopoverDescription({
98 className,
99 ...props
100}: PopoverPrimitive.Description.Props) {
89function PopoverDescription({ className, ...props }: PopoverPrimitive.Description.Props) {
10190 return (
10291 <PopoverPrimitive.Description
10392 className={cn("text-muted-foreground text-sm", className)}
apps/web/src/components/ui/scroll-area.tsx+2-6
......@@ -15,17 +15,13 @@ function ScrollArea({
1515 scrollbarGutter?: boolean;
1616}) {
1717 return (
18 <ScrollAreaPrimitive.Root
19 className={cn("size-full min-h-0", className)}
20 {...props}
21 >
18 <ScrollAreaPrimitive.Root className={cn("size-full min-h-0", className)} {...props}>
2219 <ScrollAreaPrimitive.Viewport
2320 className={cn(
2421 "h-full overscroll-contain rounded-[inherit] outline-none transition-shadows focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-has-overflow-x:overscroll-x-contain",
2522 scrollFade &&
2623 "mask-t-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-start)))] mask-b-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-end)))] mask-l-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-start)))] mask-r-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-end)))] [--fade-size:1.5rem]",
27 scrollbarGutter &&
28 "data-has-overflow-y:pe-2.5 data-has-overflow-x:pb-2.5",
24 scrollbarGutter && "data-has-overflow-y:pe-2.5 data-has-overflow-x:pb-2.5",
2925 )}
3026 data-slot="scroll-area-viewport"
3127 >
apps/web/src/components/ui/separator.tsx+1-5
......@@ -2,11 +2,7 @@ import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
22
33import { cn } from "~/lib/utils";
44
5function Separator({
6 className,
7 orientation = "horizontal",
8 ...props
9}: SeparatorPrimitive.Props) {
5function Separator({ className, orientation = "horizontal", ...props }: SeparatorPrimitive.Props) {
106 return (
117 <SeparatorPrimitive
128 className={cn(
apps/web/src/components/ui/sheet.tsx+2-8
......@@ -144,20 +144,14 @@ function SheetFooter({
144144function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
145145 return (
146146 <SheetPrimitive.Title
147 className={cn(
148 "font-heading font-semibold text-xl leading-none",
149 className,
150 )}
147 className={cn("font-heading font-semibold text-xl leading-none", className)}
151148 data-slot="sheet-title"
152149 {...props}
153150 />
154151 );
155152}
156153
157function SheetDescription({
158 className,
159 ...props
160}: SheetPrimitive.Description.Props) {
154function SheetDescription({ className, ...props }: SheetPrimitive.Description.Props) {
161155 return (
162156 <SheetPrimitive.Description
163157 className={cn("text-muted-foreground text-sm", className)}
apps/web/src/components/ui/spinner.tsx+1-4
......@@ -1,10 +1,7 @@
11import { Loader2Icon } from "lucide-react";
22import { cn } from "~/lib/utils";
33
4function Spinner({
5 className,
6 ...props
7}: React.ComponentProps<typeof Loader2Icon>) {
4function Spinner({ className, ...props }: React.ComponentProps<typeof Loader2Icon>) {
85 return (
96 <Loader2Icon
107 aria-label="Loading"
apps/web/src/components/ui/textarea.tsx+2-8
......@@ -11,12 +11,7 @@ type TextareaProps = React.ComponentProps<"textarea"> & {
1111 unstyled?: boolean;
1212};
1313
14function Textarea({
15 className,
16 size = "default",
17 unstyled = false,
18 ...props
19}: TextareaProps) {
14function Textarea({ className, size = "default", unstyled = false, ...props }: TextareaProps) {
2015 return (
2116 <span
2217 className={
......@@ -36,8 +31,7 @@ function Textarea({
3631 "field-sizing-content min-h-17.5 w-full rounded-[inherit] px-[calc(--spacing(3)-1px)] py-[calc(--spacing(1.5)-1px)] outline-none max-sm:min-h-20.5",
3732 size === "sm" &&
3833 "min-h-16.5 px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1)-1px)] max-sm:min-h-19.5",
39 size === "lg" &&
40 "min-h-18.5 py-[calc(--spacing(2)-1px)] max-sm:min-h-21.5",
34 size === "lg" && "min-h-18.5 py-[calc(--spacing(2)-1px)] max-sm:min-h-21.5",
4135 )}
4236 data-slot="textarea"
4337 {...mergeProps(defaultProps, props)}
apps/web/src/components/ui/toast.tsx+8-29
......@@ -35,11 +35,7 @@ interface ToastProviderProps extends Toast.Provider.Props {
3535 position?: ToastPosition;
3636}
3737
38function ToastProvider({
39 children,
40 position = "top-right",
41 ...props
42}: ToastProviderProps) {
38function ToastProvider({ children, position = "top-right", ...props }: ToastProviderProps) {
4339 return (
4440 <Toast.Provider toastManager={toastManager} {...props}>
4541 {children}
......@@ -69,9 +65,7 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
6965 data-slot="toast-viewport"
7066 >
7167 {toasts.map((toast) => {
72 const Icon = toast.type
73 ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]
74 : null;
68 const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null;
7569
7670 return (
7771 <Toast.Root
......@@ -139,10 +133,7 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
139133 )}
140134
141135 <div className="flex flex-col gap-0.5">
142 <Toast.Title
143 className="font-medium"
144 data-slot="toast-title"
145 />
136 <Toast.Title className="font-medium" data-slot="toast-title" />
146137 <Toast.Description
147138 className="text-muted-foreground"
148139 data-slot="toast-description"
......@@ -150,10 +141,7 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
150141 </div>
151142 </div>
152143 {toast.actionProps && (
153 <Toast.Action
154 className={buttonVariants({ size: "xs" })}
155 data-slot="toast-action"
156 >
144 <Toast.Action className={buttonVariants({ size: "xs" })} data-slot="toast-action">
157145 {toast.actionProps.children}
158146 </Toast.Action>
159147 )}
......@@ -180,16 +168,10 @@ function AnchoredToasts() {
180168
181169 return (
182170 <Toast.Portal data-slot="toast-portal-anchored">
183 <Toast.Viewport
184 className="outline-none"
185 data-slot="toast-viewport-anchored"
186 >
171 <Toast.Viewport className="outline-none" data-slot="toast-viewport-anchored">
187172 {toasts.map((toast) => {
188 const Icon = toast.type
189 ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]
190 : null;
191 const tooltipStyle =
192 (toast.data as { tooltipStyle?: boolean })?.tooltipStyle ?? false;
173 const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null;
174 const tooltipStyle = (toast.data as { tooltipStyle?: boolean })?.tooltipStyle ?? false;
193175 const positionerProps = toast.positionerProps;
194176
195177 if (!positionerProps?.anchor) {
......@@ -231,10 +213,7 @@ function AnchoredToasts() {
231213 )}
232214
233215 <div className="flex flex-col gap-0.5">
234 <Toast.Title
235 className="font-medium"
236 data-slot="toast-title"
237 />
216 <Toast.Title className="font-medium" data-slot="toast-title" />
238217 <Toast.Description
239218 className="text-muted-foreground"
240219 data-slot="toast-description"
apps/web/src/persistenceSchema.test.ts+4-12
......@@ -55,9 +55,7 @@ describe("hydratePersistedState", () => {
5555 expect(hydrated?.threads[0]?.terminalGroups).toEqual([
5656 { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID] },
5757 ]);
58 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(
59 `group-${DEFAULT_THREAD_TERMINAL_ID}`,
60 );
58 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`);
6159 expect(hydrated?.threads[0]?.messages[0]?.streaming).toBe(false);
6260 expect(hydrated?.runtimeMode).toBe("full-access");
6361 });
......@@ -105,9 +103,7 @@ describe("hydratePersistedState", () => {
105103 expect(hydrated?.threads[0]?.terminalGroups).toEqual([
106104 { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID] },
107105 ]);
108 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(
109 `group-${DEFAULT_THREAD_TERMINAL_ID}`,
110 );
106 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`);
111107 expect(hydrated?.activeThreadId).toBe("t-1");
112108 expect(hydrated?.runtimeMode).toBe("full-access");
113109 });
......@@ -221,9 +217,7 @@ describe("hydratePersistedState", () => {
221217 },
222218 { id: "group-term-3", terminalIds: ["term-3"] },
223219 ]);
224 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(
225 `group-${DEFAULT_THREAD_TERMINAL_ID}`,
226 );
220 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`);
227221 });
228222
229223 it("defaults terminalHeight when hydrating v5 payloads", () => {
......@@ -261,9 +255,7 @@ describe("hydratePersistedState", () => {
261255 expect(hydrated?.threads[0]?.terminalGroups).toEqual([
262256 { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID] },
263257 ]);
264 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(
265 `group-${DEFAULT_THREAD_TERMINAL_ID}`,
266 );
258 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`);
267259 });
268260});
269261
apps/web/src/persistenceSchema.ts+11-8
......@@ -52,9 +52,7 @@ const persistedThreadSchema = z.object({
5252 title: z.string().min(1),
5353 model: z.string().min(1),
5454 terminalOpen: z.boolean().default(false),
55 terminalHeight: z.number().int().min(120).max(4_096).default(
56 DEFAULT_THREAD_TERMINAL_HEIGHT,
57 ),
55 terminalHeight: z.number().int().min(120).max(4_096).default(DEFAULT_THREAD_TERMINAL_HEIGHT),
5856 terminalIds: z.array(z.string().trim().min(1)).default([DEFAULT_THREAD_TERMINAL_ID]),
5957 activeTerminalId: z.string().trim().min(1).default(DEFAULT_THREAD_TERMINAL_ID),
6058 terminalGroups: z.array(persistedTerminalGroupSchema).default([]),
......@@ -149,9 +147,10 @@ function hydrateThread(
149147 thread: z.infer<typeof persistedThreadSchema>,
150148 isLegacyPayload: boolean,
151149): Thread {
152 const terminalIds = [...new Set(thread.terminalIds.map((id) => id.trim()).filter((id) => id.length > 0))];
153 const safeTerminalIds =
154 terminalIds.length > 0 ? terminalIds : [DEFAULT_THREAD_TERMINAL_ID];
150 const terminalIds = [
151 ...new Set(thread.terminalIds.map((id) => id.trim()).filter((id) => id.length > 0)),
152 ];
153 const safeTerminalIds = terminalIds.length > 0 ? terminalIds : [DEFAULT_THREAD_TERMINAL_ID];
155154 const activeTerminalId = safeTerminalIds.includes(thread.activeTerminalId)
156155 ? thread.activeTerminalId
157156 : (safeTerminalIds[0] ?? DEFAULT_THREAD_TERMINAL_ID);
......@@ -197,7 +196,9 @@ function hydrateThread(
197196
198197 if (normalizedGroups.length === 0 && thread.terminalLayout === "split") {
199198 const splitTerminalIds = [
200 ...new Set((thread.splitTerminalIds ?? []).map((id) => id.trim()).filter((id) => id.length > 0)),
199 ...new Set(
200 (thread.splitTerminalIds ?? []).map((id) => id.trim()).filter((id) => id.length > 0),
201 ),
201202 ].filter((terminalId) => safeTerminalIdSet.has(terminalId));
202203 if (splitTerminalIds.length >= 2) {
203204 const splitGroupTerminalIds = splitTerminalIds.slice(0, 2);
......@@ -228,7 +229,9 @@ function hydrateThread(
228229 const activeGroupIndex =
229230 activeGroupIndexFromId >= 0
230231 ? activeGroupIndexFromId
231 : (activeGroupIndexFromTerminal >= 0 ? activeGroupIndexFromTerminal : 0);
232 : activeGroupIndexFromTerminal >= 0
233 ? activeGroupIndexFromTerminal
234 : 0;
232235 const activeTerminalGroupId =
233236 normalizedGroups[activeGroupIndex]?.id ??
234237 normalizedGroups[0]?.id ??
apps/web/src/store.test.ts+1-5
......@@ -244,11 +244,7 @@ describe("store reducer thread continuity", () => {
244244 terminalId: "term-3",
245245 });
246246
247 expect(next.threads[0]?.terminalIds).toEqual([
248 DEFAULT_THREAD_TERMINAL_ID,
249 "term-2",
250 "term-3",
251 ]);
247 expect(next.threads[0]?.terminalIds).toEqual([DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3"]);
252248 expect(next.threads[0]?.terminalGroups).toEqual([
253249 {
254250 id: `group-${DEFAULT_THREAD_TERMINAL_ID}`,
apps/web/src/store.ts+12-27
......@@ -101,17 +101,12 @@ function readPersistedState(): AppState {
101101
102102 try {
103103 const rawCurrent = window.localStorage.getItem(PERSISTED_STATE_KEY);
104 const legacyValues = LEGACY_PERSISTED_STATE_KEYS.map((key) =>
105 window.localStorage.getItem(key),
106 );
104 const legacyValues = LEGACY_PERSISTED_STATE_KEYS.map((key) => window.localStorage.getItem(key));
107105 const rawLegacy = legacyValues.find((value) => value !== null) ?? null;
108106 const raw = rawCurrent ?? rawLegacy;
109107 if (!raw) return initialState;
110108 const rawCodethingV1 = window.localStorage.getItem("codething:renderer-state:v1");
111 const hydrated = hydratePersistedState(
112 raw,
113 !rawCurrent && raw === rawCodethingV1,
114 );
109 const hydrated = hydratePersistedState(raw, !rawCurrent && raw === rawCodethingV1);
115110 if (!hydrated) return initialState;
116111
117112 return { ...hydrated, diffOpen: false };
......@@ -183,10 +178,7 @@ function assignUniqueGroupId(groupId: string, usedGroupIds: Set<string>): string
183178 return uniqueGroupId;
184179}
185180
186function normalizeTerminalGroups(
187 thread: Thread,
188 terminalIds: string[],
189): ThreadTerminalGroup[] {
181function normalizeTerminalGroups(thread: Thread, terminalIds: string[]): ThreadTerminalGroup[] {
190182 const validTerminalIdSet = new Set(terminalIds);
191183 const assignedTerminalIds = new Set<string>();
192184 const usedGroupIds = new Set<string>();
......@@ -248,14 +240,13 @@ function normalizeThreadTerminals(thread: Thread): Thread {
248240 const activeGroupIndexFromId = terminalGroups.findIndex(
249241 (group) => group.id === thread.activeTerminalGroupId,
250242 );
251 const activeGroupIndexFromTerminal = findGroupIndexByTerminalId(
252 terminalGroups,
253 activeTerminalId,
254 );
243 const activeGroupIndexFromTerminal = findGroupIndexByTerminalId(terminalGroups, activeTerminalId);
255244 const activeGroupIndex =
256245 activeGroupIndexFromId >= 0
257246 ? activeGroupIndexFromId
258 : (activeGroupIndexFromTerminal >= 0 ? activeGroupIndexFromTerminal : 0);
247 : activeGroupIndexFromTerminal >= 0
248 ? activeGroupIndexFromTerminal
249 : 0;
259250 const activeTerminalGroupId =
260251 terminalGroups[activeGroupIndex]?.id ??
261252 terminalGroups[0]?.id ??
......@@ -301,9 +292,9 @@ function closeThreadTerminal(thread: Thread, terminalId: string): Thread {
301292 const closedTerminalGroupIndex = closedTerminalGroup
302293 ? closedTerminalGroup.terminalIds.indexOf(terminalId)
303294 : -1;
304 const remainingTerminalsInClosedGroup = (
305 closedTerminalGroup?.terminalIds ?? []
306 ).filter((id) => id !== terminalId);
295 const remainingTerminalsInClosedGroup = (closedTerminalGroup?.terminalIds ?? []).filter(
296 (id) => id !== terminalId,
297 );
307298 const nextActiveTerminalId =
308299 thread.activeTerminalId === terminalId
309300 ? (remainingTerminalsInClosedGroup[
......@@ -562,10 +553,7 @@ export function reducer(state: AppState, action: Action): AppState {
562553 activeGroupIndex = terminalGroups.length - 1;
563554 }
564555
565 const existingGroupIndex = findGroupIndexByTerminalId(
566 terminalGroups,
567 action.terminalId,
568 );
556 const existingGroupIndex = findGroupIndexByTerminalId(terminalGroups, action.terminalId);
569557 if (existingGroupIndex >= 0) {
570558 terminalGroups[existingGroupIndex]!.terminalIds = terminalGroups[
571559 existingGroupIndex
......@@ -674,10 +662,7 @@ export function reducer(state: AppState, action: Action): AppState {
674662 threads: updateThread(state.threads, action.event.threadId, (thread) => {
675663 const normalizedThread = normalizeThreadTerminals(thread);
676664 const runningTerminalIdSet = new Set(normalizedThread.runningTerminalIds);
677 if (
678 action.event.type === "started" ||
679 action.event.type === "restarted"
680 ) {
665 if (action.event.type === "started" || action.event.type === "restarted") {
681666 runningTerminalIdSet.delete(action.event.terminalId);
682667 } else if (action.event.type === "activity") {
683668 if (action.event.hasRunningSubprocess) {
apps/web/src/terminal-links.test.ts+1-4
......@@ -57,10 +57,7 @@ describe("resolvePathLinkTarget", () => {
5757
5858 it("keeps absolute paths unchanged", () => {
5959 expect(
60 resolvePathLinkTarget(
61 "/Users/julius/project/src/main.ts:12",
62 "/Users/julius/project",
63 ),
60 resolvePathLinkTarget("/Users/julius/project/src/main.ts:12", "/Users/julius/project"),
6461 ).toBe("/Users/julius/project/src/main.ts:12");
6562 });
6663});
apps/web/src/terminal-shortcuts.test.ts+13-38
......@@ -19,70 +19,45 @@ function event(overrides: Partial<ShortcutEventLike> = {}): ShortcutEventLike {
1919
2020describe("isTerminalToggleShortcut", () => {
2121 it("matches Cmd+J on macOS", () => {
22 expect(
23 isTerminalToggleShortcut(event({ metaKey: true }), "MacIntel"),
24 ).toBe(true);
22 expect(isTerminalToggleShortcut(event({ metaKey: true }), "MacIntel")).toBe(true);
2523 });
2624
2725 it("matches Ctrl+J on non-macOS", () => {
28 expect(
29 isTerminalToggleShortcut(event({ ctrlKey: true }), "Win32"),
30 ).toBe(true);
26 expect(isTerminalToggleShortcut(event({ ctrlKey: true }), "Win32")).toBe(true);
3127 });
3228
3329 it("rejects wrong modifiers", () => {
34 expect(
35 isTerminalToggleShortcut(
36 event({ ctrlKey: true, shiftKey: true }),
37 "Win32",
38 ),
39 ).toBe(false);
40 expect(
41 isTerminalToggleShortcut(event({ metaKey: true, altKey: true }), "MacIntel"),
42 ).toBe(false);
30 expect(isTerminalToggleShortcut(event({ ctrlKey: true, shiftKey: true }), "Win32")).toBe(false);
31 expect(isTerminalToggleShortcut(event({ metaKey: true, altKey: true }), "MacIntel")).toBe(
32 false,
33 );
4334 });
4435
4536 it("rejects non-j keys", () => {
46 expect(
47 isTerminalToggleShortcut(event({ key: "k", ctrlKey: true }), "Linux"),
48 ).toBe(false);
37 expect(isTerminalToggleShortcut(event({ key: "k", ctrlKey: true }), "Linux")).toBe(false);
4938 });
5039});
5140
5241describe("isTerminalClearShortcut", () => {
5342 it("matches Ctrl+L on all platforms", () => {
54 expect(
55 isTerminalClearShortcut(event({ key: "l", ctrlKey: true }), "Linux"),
56 ).toBe(true);
57 expect(
58 isTerminalClearShortcut(event({ key: "l", ctrlKey: true }), "MacIntel"),
59 ).toBe(true);
43 expect(isTerminalClearShortcut(event({ key: "l", ctrlKey: true }), "Linux")).toBe(true);
44 expect(isTerminalClearShortcut(event({ key: "l", ctrlKey: true }), "MacIntel")).toBe(true);
6045 });
6146
6247 it("matches Cmd+K on macOS", () => {
63 expect(
64 isTerminalClearShortcut(event({ key: "k", metaKey: true }), "MacIntel"),
65 ).toBe(true);
48 expect(isTerminalClearShortcut(event({ key: "k", metaKey: true }), "MacIntel")).toBe(true);
6649 });
6750
6851 it("rejects Cmd+K on non-macOS", () => {
69 expect(
70 isTerminalClearShortcut(event({ key: "k", metaKey: true }), "Win32"),
71 ).toBe(false);
52 expect(isTerminalClearShortcut(event({ key: "k", metaKey: true }), "Win32")).toBe(false);
7253 });
7354
7455 it("rejects wrong modifiers", () => {
7556 expect(
76 isTerminalClearShortcut(
77 event({ key: "l", ctrlKey: true, shiftKey: true }),
78 "Linux",
79 ),
57 isTerminalClearShortcut(event({ key: "l", ctrlKey: true, shiftKey: true }), "Linux"),
8058 ).toBe(false);
8159 expect(
82 isTerminalClearShortcut(
83 event({ key: "k", metaKey: true, altKey: true }),
84 "MacIntel",
85 ),
60 isTerminalClearShortcut(event({ key: "k", metaKey: true, altKey: true }), "MacIntel"),
8661 ).toBe(false);
8762 });
8863});
apps/web/src/terminal-shortcuts.ts+1-7
......@@ -31,13 +31,7 @@ export function isTerminalClearShortcut(
3131): boolean {
3232 const key = event.key.toLowerCase();
3333
34 if (
35 key === "l" &&
36 event.ctrlKey &&
37 !event.metaKey &&
38 !event.altKey &&
39 !event.shiftKey
40 ) {
34 if (key === "l" && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
4135 return true;
4236 }
4337
packages/contracts/src/git.test.ts+1-3
......@@ -47,9 +47,7 @@ describe("git contracts", () => {
4747 it("accepts supported stacked actions", () => {
4848 expect(gitStackedActionSchema.parse("commit")).toBe("commit");
4949 expect(gitStackedActionSchema.parse("commit_push")).toBe("commit_push");
50 expect(gitStackedActionSchema.parse("commit_push_pr")).toBe(
51 "commit_push_pr",
52 );
50 expect(gitStackedActionSchema.parse("commit_push_pr")).toBe("commit_push_pr");
5351 });
5452
5553 it("parses stacked action input", () => {
packages/contracts/src/git.ts+3-15
......@@ -100,23 +100,11 @@ export const gitStatusResultSchema = z.object({
100100 .nullable(),
101101});
102102
103export const gitStackedActionSchema = z.enum([
104 "commit",
105 "commit_push",
106 "commit_push_pr",
107]);
103export const gitStackedActionSchema = z.enum(["commit", "commit_push", "commit_push_pr"]);
108104
109105const gitCommitStepStatusSchema = z.enum(["created", "skipped_no_changes"]);
110const gitPushStepStatusSchema = z.enum([
111 "pushed",
112 "skipped_not_requested",
113 "skipped_up_to_date",
114]);
115const gitPrStepStatusSchema = z.enum([
116 "created",
117 "opened_existing",
118 "skipped_not_requested",
119]);
106const gitPushStepStatusSchema = z.enum(["pushed", "skipped_not_requested", "skipped_up_to_date"]);
107const gitPrStepStatusSchema = z.enum(["created", "opened_existing", "skipped_not_requested"]);
120108
121109export const gitRunStackedActionInputSchema = z.object({
122110 cwd: z.string().trim().min(1),
packages/contracts/src/ipc.ts+1-3
......@@ -105,9 +105,7 @@ export interface NativeApi {
105105 // Stacked action API
106106 pull: (input: GitPullInput) => Promise<GitPullResult>;
107107 status: (input: GitStatusInput) => Promise<GitStatusResult>;
108 runStackedAction: (
109 input: GitRunStackedActionInput,
110 ) => Promise<GitRunStackedActionResult>;
108 runStackedAction: (input: GitRunStackedActionInput) => Promise<GitRunStackedActionResult>;
111109 };
112110 contextMenu: {
113111 show: <T extends string>(
packages/contracts/src/terminal.ts+1-6
......@@ -36,12 +36,7 @@ export const terminalCloseInputSchema = terminalThreadInputSchema.extend({
3636 deleteHistory: z.boolean().optional(),
3737});
3838
39export const terminalSessionStatusSchema = z.enum([
40 "starting",
41 "running",
42 "exited",
43 "error",
44]);
39export const terminalSessionStatusSchema = z.enum(["starting", "running", "exited", "error"]);
4540
4641export const terminalSessionSnapshotSchema = z.object({
4742 threadId: z.string().min(1),