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() {...@@ -21,10 +21,16 @@ function createSendTurnHarness() {
21 };21 };
2222
23 const requireSession = vi23 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 )
25 .mockReturnValue(context);28 .mockReturnValue(context);
26 const sendRequest = vi29 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 )
28 .mockResolvedValue({34 .mockResolvedValue({
29 turn: {35 turn: {
30 id: "turn_1",36 id: "turn_1",
...@@ -109,7 +115,8 @@ describe("isRecoverableThreadResumeError", () => {...@@ -109,7 +115,8 @@ describe("isRecoverableThreadResumeError", () => {
109115
110describe("sendTurn", () => {116describe("sendTurn", () => {
111 it("sends text and image user input items to turn/start", async () => {117 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
114 const result = await manager.sendTurn({121 const result = await manager.sendTurn({
115 sessionId: "sess_1",122 sessionId: "sess_1",
apps/server/src/codexAppServerManager.ts+2-4
...@@ -258,8 +258,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -258,8 +258,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
258 }258 }
259259
260 const turnInput: Array<260 const turnInput: Array<
261 | { type: "text"; text: string; text_elements: [] }261 { type: "text"; text: string; text_elements: [] } | { type: "image"; url: string }
262 | { type: "image"; url: string }
263 > = [];262 > = [];
264 if (input.input) {263 if (input.input) {
265 turnInput.push({264 turnInput.push({
...@@ -283,8 +282,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -283,8 +282,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
283 const turnStartParams: {282 const turnStartParams: {
284 threadId: string;283 threadId: string;
285 input: Array<284 input: Array<
286 | { type: "text"; text: string; text_elements: [] }285 { type: "text"; text: string; text_elements: [] } | { type: "image"; url: string }
287 | { type: "image"; url: string }
288 >;286 >;
289 model?: string;287 model?: string;
290 effort?: string;288 effort?: string;
apps/server/src/coreServices.ts+1-4
...@@ -28,8 +28,5 @@ export interface TextGenerationService {...@@ -28,8 +28,5 @@ export interface TextGenerationService {
28 generateCommitMessage(28 generateCommitMessage(
29 input: CommitMessageGenerationInput,29 input: CommitMessageGenerationInput,
30 ): Promise<CommitMessageGenerationResult>;30 ): Promise<CommitMessageGenerationResult>;
31 generatePrContent(31 generatePrContent(input: PrContentGenerationInput): Promise<PrContentGenerationResult>;
32 input: PrContentGenerationInput,
33 ): Promise<PrContentGenerationResult>;
34}32}
35
apps/server/src/git.test.ts+1-5
...@@ -570,11 +570,7 @@ describe("git integration", () => {...@@ -570,11 +570,7 @@ describe("git integration", () => {
570 expect(context!.stagedSummary.length).toBeGreaterThan(0);570 expect(context!.stagedSummary.length).toBeGreaterThan(0);
571 expect(context!.stagedPatch.length).toBeGreaterThan(0);571 expect(context!.stagedPatch.length).toBeGreaterThan(0);
572572
573 const created = await core.commit(573 const created = await core.commit(tmp.path, "Add README update", "- include updated content");
574 tmp.path,
575 "Add README update",
576 "- include updated content",
577 );
578 expect(created.commitSha.length).toBeGreaterThan(0);574 expect(created.commitSha.length).toBeGreaterThan(0);
579 expect(await git(tmp.path, "log -1 --pretty=%s")).toBe("Add README update");575 expect(await git(tmp.path, "log -1 --pretty=%s")).toBe("Add README update");
580 });576 });
apps/server/src/git.ts+4-12
...@@ -101,7 +101,8 @@ function parseNumstatEntries(...@@ -101,7 +101,8 @@ function parseNumstatEntries(
101 for (const line of stdout.split(/\r?\n/g)) {101 for (const line of stdout.split(/\r?\n/g)) {
102 if (line.trim().length === 0) continue;102 if (line.trim().length === 0) continue;
103 const [addedRaw, deletedRaw, ...pathParts] = line.split("\t");103 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();
105 if (rawPath.length === 0) continue;106 if (rawPath.length === 0) continue;
106 const added = Number.parseInt(addedRaw ?? "0", 10);107 const added = Number.parseInt(addedRaw ?? "0", 10);
107 const deleted = Number.parseInt(deletedRaw ?? "0", 10);108 const deleted = Number.parseInt(deletedRaw ?? "0", 10);
...@@ -311,21 +312,12 @@ export class GitCoreService {...@@ -311,21 +312,12 @@ export class GitCoreService {
311 async prepareCommitContext(cwd: string): Promise<GitPreparedCommitContext | null> {312 async prepareCommitContext(cwd: string): Promise<GitPreparedCommitContext | null> {
312 await this.git(cwd, ["add", "-A"]);313 await this.git(cwd, ["add", "-A"]);
313314
314 const stagedSummary = await this.gitStdout(cwd, [315 const stagedSummary = await this.gitStdout(cwd, ["diff", "--cached", "--name-status"]);
315 "diff",
316 "--cached",
317 "--name-status",
318 ]);
319 if (trimStdout(stagedSummary).length === 0) {316 if (trimStdout(stagedSummary).length === 0) {
320 return null;317 return null;
321 }318 }
322319
323 const stagedPatch = await this.gitStdout(cwd, [320 const stagedPatch = await this.gitStdout(cwd, ["diff", "--cached", "--patch", "--minimal"]);
324 "diff",
325 "--cached",
326 "--patch",
327 "--minimal",
328 ]);
329321
330 return {322 return {
331 stagedSummary,323 stagedSummary,
apps/server/src/gitManager.test.ts+3-10
...@@ -5,11 +5,7 @@ import path from "node:path";...@@ -5,11 +5,7 @@ import path from "node:path";
5import { afterEach, describe, expect, it } from "vitest";5import { afterEach, describe, expect, it } from "vitest";
66
7import { GitManager } from "./gitManager";7import { GitManager } from "./gitManager";
8import {8import { type ProcessRunOptions, type ProcessRunResult, runProcess } from "./processRunner";
9 type ProcessRunOptions,
10 type ProcessRunResult,
11 runProcess,
12} from "./processRunner";
139
14interface FakeGhScenario {10interface FakeGhScenario {
15 prListSequence?: string[];11 prListSequence?: string[];
...@@ -71,9 +67,7 @@ async function createBareRemote(): Promise<string> {...@@ -71,9 +67,7 @@ async function createBareRemote(): Promise<string> {
71 return remoteDir;67 return remoteDir;
72}68}
7369
74function createTextGenerator(70function createTextGenerator(overrides: Partial<FakeGitTextGenerator> = {}): FakeGitTextGenerator {
75 overrides: Partial<FakeGitTextGenerator> = {},
76): FakeGitTextGenerator {
77 return {71 return {
78 generateCommitMessage: async () => ({72 generateCommitMessage: async () => ({
79 subject: "Implement stacked git actions",73 subject: "Implement stacked git actions",
...@@ -126,8 +120,7 @@ function createRunnerWithFakeGh(scenario: FakeGhScenario = {}): {...@@ -126,8 +120,7 @@ function createRunnerWithFakeGh(scenario: FakeGhScenario = {}): {
126 if (args[0] === "pr" && args[1] === "create") {120 if (args[0] === "pr" && args[1] === "create") {
127 return {121 return {
128 stdout:122 stdout:
129 (scenario.createdPrUrl ??123 (scenario.createdPrUrl ?? "https://github.com/pingdotgg/codething-mvp/pull/101") + "\n",
130 "https://github.com/pingdotgg/codething-mvp/pull/101") + "\n",
131 stderr: "",124 stderr: "",
132 code: 0,125 code: 0,
133 signal: null,126 signal: null,
apps/server/src/gitManager.ts+10-40
...@@ -20,11 +20,7 @@ import type {...@@ -20,11 +20,7 @@ import type {
20} from "./coreServices";20} from "./coreServices";
21import { CodexTextGenerator } from "./codexTextGenerator";21import { CodexTextGenerator } from "./codexTextGenerator";
22import { GitCoreService } from "./git";22import { GitCoreService } from "./git";
23import {23import { type ProcessRunOptions, type ProcessRunResult, runProcess } from "./processRunner";
24 type ProcessRunOptions,
25 type ProcessRunResult,
26 runProcess,
27} from "./processRunner";
2824
29const GH_CLI_TIMEOUT_MS = 30_000;25const GH_CLI_TIMEOUT_MS = 30_000;
3026
...@@ -58,11 +54,7 @@ function parseOpenPrList(raw: unknown): OpenPrInfo[] {...@@ -58,11 +54,7 @@ function parseOpenPrList(raw: unknown): OpenPrInfo[] {
58 const url = record.url;54 const url = record.url;
59 const baseRefName = record.baseRefName;55 const baseRefName = record.baseRefName;
60 const headRefName = record.headRefName;56 const headRefName = record.headRefName;
61 if (57 if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) {
62 typeof number !== "number" ||
63 !Number.isInteger(number) ||
64 number <= 0
65 ) {
66 continue;58 continue;
67 }59 }
68 if (60 if (
...@@ -98,8 +90,7 @@ function sanitizeCommitMessage(...@@ -98,8 +90,7 @@ function sanitizeCommitMessage(
98): CommitMessageGenerationResult {90): CommitMessageGenerationResult {
99 const rawSubject = generated.subject.trim().split(/\r?\n/g)[0]?.trim() ?? "";91 const rawSubject = generated.subject.trim().split(/\r?\n/g)[0]?.trim() ?? "";
100 const subject = rawSubject.replace(/[.]+$/g, "").trim();92 const subject = rawSubject.replace(/[.]+$/g, "").trim();
101 const safeSubject =93 const safeSubject = subject.length > 0 ? subject.slice(0, 72).trimEnd() : "Update project files";
102 subject.length > 0 ? subject.slice(0, 72).trimEnd() : "Update project files";
103 return {94 return {
104 subject: safeSubject,95 subject: safeSubject,
105 body: generated.body.trim(),96 body: generated.body.trim(),
...@@ -143,10 +134,7 @@ function extractBranchFromRef(ref: string): string {...@@ -143,10 +134,7 @@ function extractBranchFromRef(ref: string): string {
143 return normalized.slice(firstSlash + 1).trim();134 return normalized.slice(firstSlash + 1).trim();
144}135}
145136
146function asCommandNotFound(137function asCommandNotFound(command: string, error: unknown): Error | undefined {
147 command: string,
148 error: unknown,
149): Error | undefined {
150 if (!(error instanceof Error)) return undefined;138 if (!(error instanceof Error)) return undefined;
151 if (!error.message.includes(`Command not found: ${command}`)) return undefined;139 if (!error.message.includes(`Command not found: ${command}`)) return undefined;
152 if (command === "gh") {140 if (command === "gh") {
...@@ -226,9 +214,7 @@ export class GitManager {...@@ -226,9 +214,7 @@ export class GitManager {
226 };214 };
227 }215 }
228216
229 async runStackedAction(217 async runStackedAction(raw: GitRunStackedActionInput): Promise<GitRunStackedActionResult> {
230 raw: GitRunStackedActionInput,
231 ): Promise<GitRunStackedActionResult> {
232 const input = gitRunStackedActionInputSchema.parse(raw);218 const input = gitRunStackedActionInputSchema.parse(raw);
233 const wantsPush = input.action !== "commit";219 const wantsPush = input.action !== "commit";
234 const wantsPr = input.action === "commit_push_pr";220 const wantsPr = input.action === "commit_push_pr";
...@@ -241,11 +227,7 @@ export class GitManager {...@@ -241,11 +227,7 @@ export class GitManager {
241 throw new Error("Cannot create a pull request from detached HEAD.");227 throw new Error("Cannot create a pull request from detached HEAD.");
242 }228 }
243229
244 const commit = await this.runCommitStep(230 const commit = await this.runCommitStep(input.cwd, initialStatus.branch, input.commitMessage);
245 input.cwd,
246 initialStatus.branch,
247 input.commitMessage,
248 );
249231
250 const push = wantsPush232 const push = wantsPush
251 ? await this.gitCore.pushCurrentBranch(input.cwd, initialStatus.branch)233 ? await this.gitCore.pushCurrentBranch(input.cwd, initialStatus.branch)
...@@ -353,10 +335,7 @@ export class GitManager {...@@ -353,10 +335,7 @@ export class GitManager {
353 throw asCommandNotFound("codex", error) ?? error;335 throw asCommandNotFound("codex", error) ?? error;
354 }336 }
355337
356 const bodyFile = path.join(338 const bodyFile = path.join(os.tmpdir(), `t3code-pr-body-${process.pid}-${randomUUID()}.md`);
357 os.tmpdir(),
358 `t3code-pr-body-${process.pid}-${randomUUID()}.md`,
359 );
360 await fs.writeFile(bodyFile, generated.body, "utf8");339 await fs.writeFile(bodyFile, generated.body, "utf8");
361340
362 try {341 try {
...@@ -401,10 +380,7 @@ export class GitManager {...@@ -401,10 +380,7 @@ export class GitManager {
401 };380 };
402 }381 }
403382
404 private async findOpenPr(383 private async findOpenPr(cwd: string, branch: string): Promise<OpenPrInfo | null> {
405 cwd: string,
406 branch: string,
407 ): Promise<OpenPrInfo | null> {
408 const stdout = await this.runGhStdout(cwd, [384 const stdout = await this.runGhStdout(cwd, [
409 "pr",385 "pr",
410 "list",386 "list",
...@@ -437,10 +413,7 @@ export class GitManager {...@@ -437,10 +413,7 @@ export class GitManager {
437 branch: string,413 branch: string,
438 upstreamRef: string | null,414 upstreamRef: string | null,
439 ): Promise<string> {415 ): Promise<string> {
440 const configured = await this.gitCore.readConfigValue(416 const configured = await this.gitCore.readConfigValue(cwd, `branch.${branch}.gh-merge-base`);
441 cwd,
442 `branch.${branch}.gh-merge-base`,
443 );
444 if (configured) return configured;417 if (configured) return configured;
445418
446 if (upstreamRef) {419 if (upstreamRef) {
...@@ -477,10 +450,7 @@ export class GitManager {...@@ -477,10 +450,7 @@ export class GitManager {
477 return result.stdout;450 return result.stdout;
478 }451 }
479452
480 private async runGhCommand(453 private async runGhCommand(cwd: string, args: readonly string[]): Promise<ProcessRunResult> {
481 cwd: string,
482 args: readonly string[],
483 ): Promise<ProcessRunResult> {
484 try {454 try {
485 return await this.run("gh", args, {455 return await this.run("gh", args, {
486 cwd,456 cwd,
apps/server/src/processRunner.test.ts+5-13
...@@ -5,23 +5,15 @@ import { runProcess } from "./processRunner";...@@ -5,23 +5,15 @@ import { runProcess } from "./processRunner";
5describe("runProcess", () => {5describe("runProcess", () => {
6 it("fails when output exceeds max buffer in default mode", async () => {6 it("fails when output exceeds max buffer in default mode", async () => {
7 await expect(7 await expect(
8 runProcess(8 runProcess("node", ["-e", "process.stdout.write('x'.repeat(2048))"], { maxBufferBytes: 128 }),
9 "node",
10 ["-e", "process.stdout.write('x'.repeat(2048))"],
11 { maxBufferBytes: 128 },
12 ),
13 ).rejects.toThrow("exceeded stdout buffer limit");9 ).rejects.toThrow("exceeded stdout buffer limit");
14 });10 });
1511
16 it("truncates output when outputMode is truncate", async () => {12 it("truncates output when outputMode is truncate", async () => {
17 const result = await runProcess(13 const result = await runProcess("node", ["-e", "process.stdout.write('x'.repeat(2048))"], {
18 "node",14 maxBufferBytes: 128,
19 ["-e", "process.stdout.write('x'.repeat(2048))"],15 outputMode: "truncate",
20 {16 });
21 maxBufferBytes: 128,
22 outputMode: "truncate",
23 },
24 );
2517
26 expect(result.code).toBe(0);18 expect(result.code).toBe(0);
27 expect(result.stdout.length).toBeLessThanOrEqual(128);19 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 {...@@ -24,11 +24,7 @@ function commandLabel(command: string, args: readonly string[]): string {
24 return [command, ...args].join(" ");24 return [command, ...args].join(" ");
25}25}
2626
27function normalizeSpawnError(27function normalizeSpawnError(command: string, args: readonly string[], error: unknown): Error {
28 command: string,
29 args: readonly string[],
30 error: unknown,
31): Error {
32 if (!(error instanceof Error)) {28 if (!(error instanceof Error)) {
33 return new Error(`Failed to run ${commandLabel(command, args)}.`);29 return new Error(`Failed to run ${commandLabel(command, args)}.`);
34 }30 }
...@@ -38,9 +34,7 @@ function normalizeSpawnError(...@@ -38,9 +34,7 @@ function normalizeSpawnError(
38 return new Error(`Command not found: ${command}`);34 return new Error(`Command not found: ${command}`);
39 }35 }
4036
41 return new Error(37 return new Error(`Failed to run ${commandLabel(command, args)}: ${error.message}`);
42 `Failed to run ${commandLabel(command, args)}: ${error.message}`,
43 );
44}38}
4539
46function normalizeExitError(40function normalizeExitError(
...@@ -56,17 +50,11 @@ function normalizeExitError(...@@ -56,17 +50,11 @@ function normalizeExitError(
56 return new Error(`${commandLabel(command, args)} ${reason}.${detail}`);50 return new Error(`${commandLabel(command, args)} ${reason}.${detail}`);
57}51}
5852
59function normalizeStdinError(53function normalizeStdinError(command: string, args: readonly string[], error: unknown): Error {
60 command: string,
61 args: readonly string[],
62 error: unknown,
63): Error {
64 if (!(error instanceof Error)) {54 if (!(error instanceof Error)) {
65 return new Error(`Failed to write stdin for ${commandLabel(command, args)}.`);55 return new Error(`Failed to write stdin for ${commandLabel(command, args)}.`);
66 }56 }
67 return new Error(57 return new Error(`Failed to write stdin for ${commandLabel(command, args)}: ${error.message}`);
68 `Failed to write stdin for ${commandLabel(command, args)}: ${error.message}`,
69 );
70}58}
7159
72function normalizeBufferError(60function normalizeBufferError(
...@@ -161,22 +149,13 @@ export async function runProcess(...@@ -161,22 +149,13 @@ export async function runProcess(
161 });149 });
162 };150 };
163151
164 const appendOutput = (152 const appendOutput = (stream: "stdout" | "stderr", chunk: Buffer | string): Error | null => {
165 stream: "stdout" | "stderr",153 const chunkBuffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
166 chunk: Buffer | string,
167 ): Error | null => {
168 const chunkBuffer =
169 typeof chunk === "string" ? Buffer.from(chunk) : chunk;
170 const text = chunkBuffer.toString();154 const text = chunkBuffer.toString();
171 const byteLength = chunkBuffer.length;155 const byteLength = chunkBuffer.length;
172 if (stream === "stdout") {156 if (stream === "stdout") {
173 if (outputMode === "truncate") {157 if (outputMode === "truncate") {
174 const appended = appendChunkWithinLimit(158 const appended = appendChunkWithinLimit(stdout, stdoutBytes, chunkBuffer, maxBufferBytes);
175 stdout,
176 stdoutBytes,
177 chunkBuffer,
178 maxBufferBytes,
179 );
180 stdout = appended.next;159 stdout = appended.next;
181 stdoutBytes = appended.nextBytes;160 stdoutBytes = appended.nextBytes;
182 stdoutTruncated = stdoutTruncated || appended.truncated;161 stdoutTruncated = stdoutTruncated || appended.truncated;
...@@ -189,12 +168,7 @@ export async function runProcess(...@@ -189,12 +168,7 @@ export async function runProcess(
189 }168 }
190 } else {169 } else {
191 if (outputMode === "truncate") {170 if (outputMode === "truncate") {
192 const appended = appendChunkWithinLimit(171 const appended = appendChunkWithinLimit(stderr, stderrBytes, chunkBuffer, maxBufferBytes);
193 stderr,
194 stderrBytes,
195 chunkBuffer,
196 maxBufferBytes,
197 );
198 stderr = appended.next;172 stderr = appended.next;
199 stderrBytes = appended.nextBytes;173 stderrBytes = appended.nextBytes;
200 stderrTruncated = stderrTruncated || appended.truncated;174 stderrTruncated = stderrTruncated || appended.truncated;
...@@ -241,10 +215,7 @@ export async function runProcess(...@@ -241,10 +215,7 @@ export async function runProcess(
241 };215 };
242216
243 finalize(() => {217 finalize(() => {
244 if (218 if (!options.allowNonZeroExit && (timedOut || (code !== null && code !== 0))) {
245 !options.allowNonZeroExit &&
246 (timedOut || (code !== null && code !== 0))
247 ) {
248 reject(normalizeExitError(command, args, result));219 reject(normalizeExitError(command, args, result));
249 return;220 return;
250 }221 }
apps/server/src/ptyAdapter.ts+2-10
...@@ -40,12 +40,7 @@ function resolveNodePtySpawnHelperPath(): string | null {...@@ -40,12 +40,7 @@ function resolveNodePtySpawnHelperPath(): string | null {
40 const candidates = [40 const candidates = [
41 path.join(packageDir, "build", "Release", "spawn-helper"),41 path.join(packageDir, "build", "Release", "spawn-helper"),
42 path.join(packageDir, "build", "Debug", "spawn-helper"),42 path.join(packageDir, "build", "Debug", "spawn-helper"),
43 path.join(43 path.join(packageDir, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
44 packageDir,
45 "prebuilds",
46 `${process.platform}-${process.arch}`,
47 "spawn-helper",
48 ),
49 ];44 ];
5045
51 for (const candidate of candidates) {46 for (const candidate of candidates) {
...@@ -128,10 +123,7 @@ export class NodePtyAdapter implements PtyAdapter {...@@ -128,10 +123,7 @@ export class NodePtyAdapter implements PtyAdapter {
128 cols: input.cols,123 cols: input.cols,
129 rows: input.rows,124 rows: input.rows,
130 env: input.env,125 env: input.env,
131 name:126 name: globalThis.process.platform === "win32" ? "xterm-color" : "xterm-256color",
132 globalThis.process.platform === "win32"
133 ? "xterm-color"
134 : "xterm-256color",
135 });127 });
136 return new NodePtyProcess(ptyProcess);128 return new NodePtyProcess(ptyProcess);
137 }129 }
apps/server/src/terminalManager.test.ts+14-17
...@@ -2,7 +2,11 @@ import fs from "node:fs";...@@ -2,7 +2,11 @@ import fs from "node:fs";
2import os from "node:os";2import os from "node:os";
3import path from "node:path";3import 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";
6import { afterEach, describe, expect, it } from "vitest";10import { afterEach, describe, expect, it } from "vitest";
711
8import type { PtyAdapter, PtyExitEvent, PtyProcess, PtySpawnInput } from "./ptyAdapter";12import type { PtyAdapter, PtyExitEvent, PtyProcess, PtySpawnInput } from "./ptyAdapter";
...@@ -161,7 +165,10 @@ describe("TerminalManager", () => {...@@ -161,7 +165,10 @@ describe("TerminalManager", () => {
161165
162 it("spawns lazily and reuses running terminal per thread", async () => {166 it("spawns lazily and reuses running terminal per thread", async () => {
163 const { manager, ptyAdapter } = makeManager();167 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 ]);
165 const third = await manager.open(openInput());172 const third = await manager.open(openInput());
166173
167 expect(first.threadId).toBe("thread-1");174 expect(first.threadId).toBe("thread-1");
...@@ -299,18 +306,14 @@ describe("TerminalManager", () => {...@@ -299,18 +306,14 @@ describe("TerminalManager", () => {
299 hasRunningSubprocess = true;306 hasRunningSubprocess = true;
300 await waitFor(307 await waitFor(
301 () =>308 () =>
302 events.some(309 events.some((event) => event.type === "activity" && event.hasRunningSubprocess === true),
303 (event) => event.type === "activity" && event.hasRunningSubprocess === true,
304 ),
305 1_200,310 1_200,
306 );311 );
307312
308 hasRunningSubprocess = false;313 hasRunningSubprocess = false;
309 await waitFor(314 await waitFor(
310 () =>315 () =>
311 events.some(316 events.some((event) => event.type === "activity" && event.hasRunningSubprocess === false),
312 (event) => event.type === "activity" && event.hasRunningSubprocess === false,
313 ),
314 1_200,317 1_200,
315 );318 );
316319
...@@ -368,12 +371,8 @@ describe("TerminalManager", () => {...@@ -368,12 +371,8 @@ describe("TerminalManager", () => {
368371
369 expect(defaultProcess.killed).toBe(true);372 expect(defaultProcess.killed).toBe(true);
370 expect(sidecarProcess.killed).toBe(true);373 expect(sidecarProcess.killed).toBe(true);
371 expect(fs.existsSync(multiTerminalHistoryLogPath(logsDir, "thread-1", "default"))).toBe(374 expect(fs.existsSync(multiTerminalHistoryLogPath(logsDir, "thread-1", "default"))).toBe(false);
372 false,375 expect(fs.existsSync(multiTerminalHistoryLogPath(logsDir, "thread-1", "sidecar"))).toBe(false);
373 );
374 expect(fs.existsSync(multiTerminalHistoryLogPath(logsDir, "thread-1", "sidecar"))).toBe(
375 false,
376 );
377376
378 manager.dispose();377 manager.dispose();
379 });378 });
...@@ -415,9 +414,7 @@ describe("TerminalManager", () => {...@@ -415,9 +414,7 @@ describe("TerminalManager", () => {
415 } else {414 } else {
416 expect(415 expect(
417 ptyAdapter.spawnInputs.some((input) =>416 ptyAdapter.spawnInputs.some((input) =>
418 ["/bin/zsh", "/bin/bash", "/bin/sh", "zsh", "bash", "sh"].includes(417 ["/bin/zsh", "/bin/bash", "/bin/sh", "zsh", "bash", "sh"].includes(input.shell),
419 input.shell,
420 ),
421 ),418 ),
422 ).toBe(true);419 ).toBe(true);
423 }420 }
apps/server/src/terminalManager.ts+6-26
...@@ -26,11 +26,7 @@ import { runProcess } from "./processRunner";...@@ -26,11 +26,7 @@ import { runProcess } from "./processRunner";
26const DEFAULT_HISTORY_LINE_LIMIT = 5_000;26const DEFAULT_HISTORY_LINE_LIMIT = 5_000;
27const DEFAULT_PERSIST_DEBOUNCE_MS = 40;27const DEFAULT_PERSIST_DEBOUNCE_MS = 40;
28const DEFAULT_SUBPROCESS_POLL_INTERVAL_MS = 1_000;28const DEFAULT_SUBPROCESS_POLL_INTERVAL_MS = 1_000;
29const TERMINAL_ENV_BLOCKLIST = new Set([29const TERMINAL_ENV_BLOCKLIST = new Set(["PORT", "ELECTRON_RENDERER_PORT", "ELECTRON_RUN_AS_NODE"]);
30 "PORT",
31 "ELECTRON_RENDERER_PORT",
32 "ELECTRON_RUN_AS_NODE",
33]);
3430
35type TerminalSubprocessChecker = (terminalPid: number) => Promise<boolean>;31type TerminalSubprocessChecker = (terminalPid: number) => Promise<boolean>;
3632
...@@ -102,12 +98,7 @@ function resolveShellCandidates(shellResolver: () => string): string[] {...@@ -102,12 +98,7 @@ function resolveShellCandidates(shellResolver: () => string): string[] {
102 const requested = normalizeShellCommand(shellResolver());98 const requested = normalizeShellCommand(shellResolver());
10399
104 if (process.platform === "win32") {100 if (process.platform === "win32") {
105 return uniqueShells([101 return uniqueShells([requested, process.env.ComSpec ?? null, "powershell.exe", "cmd.exe"]);
106 requested,
107 process.env.ComSpec ?? null,
108 "powershell.exe",
109 "cmd.exe",
110 ]);
111 }102 }
112103
113 return uniqueShells([104 return uniqueShells([
...@@ -431,11 +422,7 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {...@@ -431,11 +422,7 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
431 const input = terminalCloseInputSchema.parse(raw);422 const input = terminalCloseInputSchema.parse(raw);
432 await this.runWithThreadLock(input.threadId, async () => {423 await this.runWithThreadLock(input.threadId, async () => {
433 if (input.terminalId) {424 if (input.terminalId) {
434 await this.closeSession(425 await this.closeSession(input.threadId, input.terminalId, input.deleteHistory === true);
435 input.threadId,
436 input.terminalId,
437 input.deleteHistory === true,
438 );
439 return;426 return;
440 }427 }
441428
...@@ -517,13 +504,9 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {...@@ -517,13 +504,9 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
517504
518 if (!ptyProcess) {505 if (!ptyProcess) {
519 const detail =506 const detail =
520 lastSpawnError instanceof Error507 lastSpawnError instanceof Error ? lastSpawnError.message : "Terminal start failed";
521 ? lastSpawnError.message
522 : "Terminal start failed";
523 const tried =508 const tried =
524 shellCandidates.length > 0509 shellCandidates.length > 0 ? ` Tried shells: ${shellCandidates.join(", ")}.` : "";
525 ? ` Tried shells: ${shellCandidates.join(", ")}.`
526 : "";
527 throw new Error(`${detail}.${tried}`.trim());510 throw new Error(`${detail}.${tried}`.trim());
528 }511 }
529512
...@@ -969,10 +952,7 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {...@@ -969,10 +952,7 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
969 return path.join(this.logsDir, `${legacySafeThreadId(threadId)}.log`);952 return path.join(this.logsDir, `${legacySafeThreadId(threadId)}.log`);
970 }953 }
971954
972 private async runWithThreadLock<T>(955 private async runWithThreadLock<T>(threadId: string, task: () => Promise<T>): Promise<T> {
973 threadId: string,
974 task: () => Promise<T>,
975 ): Promise<T> {
976 const previous = this.threadLocks.get(threadId) ?? Promise.resolve();956 const previous = this.threadLocks.get(threadId) ?? Promise.resolve();
977 let release: () => void = () => {};957 let release: () => void = () => {};
978 const current = new Promise<void>((resolve) => {958 const current = new Promise<void>((resolve) => {
apps/server/src/wsServer.test.ts+2-6
...@@ -224,9 +224,7 @@ describe("WebSocket Server", () => {...@@ -224,9 +224,7 @@ describe("WebSocket Server", () => {
224 ...(options.authToken ? { authToken: options.authToken } : {}),224 ...(options.authToken ? { authToken: options.authToken } : {}),
225 projectRegistry: new ProjectRegistry(stateDir),225 projectRegistry: new ProjectRegistry(stateDir),
226 ...(options.gitManager ? { gitManager: options.gitManager as never } : {}),226 ...(options.gitManager ? { gitManager: options.gitManager as never } : {}),
227 ...(options.terminalManager227 ...(options.terminalManager ? { terminalManager: options.terminalManager } : {}),
228 ? { terminalManager: options.terminalManager }
229 : {}),
230 });228 });
231 }229 }
232230
...@@ -602,9 +600,7 @@ describe("WebSocket Server", () => {...@@ -602,9 +600,7 @@ describe("WebSocket Server", () => {
602 it("returns errors from git.runStackedAction", async () => {600 it("returns errors from git.runStackedAction", async () => {
603 const gitManager = {601 const gitManager = {
604 status: vi.fn(),602 status: vi.fn(),
605 runStackedAction: vi603 runStackedAction: vi.fn().mockRejectedValue(new Error("Cannot push from detached HEAD.")),
606 .fn()
607 .mockRejectedValue(new Error("Cannot push from detached HEAD.")),
608 };604 };
609605
610 server = createTestServer({ cwd: "/test", gitManager });606 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...@@ -214,13 +214,16 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B
214 const branch = branchByName.get(branchName);214 const branch = branchByName.get(branchName);
215 if (!branch) return null;215 if (!branch) return null;
216216
217 const hasSecondaryWorktree = branch.worktreePath && branch.worktreePath !== activeProject.cwd;217 const hasSecondaryWorktree =
218 branch.worktreePath && branch.worktreePath !== activeProject.cwd;
218 return (219 return (
219 <ComboboxItem220 <ComboboxItem
220 hideIndicator221 hideIndicator
221 key={branchName}222 key={branchName}
222 value={branchName}223 value={branchName}
223 className={branchName === activeThread.branch ? "bg-accent text-foreground" : undefined}224 className={
225 branchName === activeThread.branch ? "bg-accent text-foreground" : undefined
226 }
224 onClick={() => selectBranch(branch)}227 onClick={() => selectBranch(branch)}
225 >228 >
226 <div className="flex w-full items-center justify-between gap-2">229 <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() {...@@ -1304,7 +1304,9 @@ export default function ChatView() {
1304 </form>1304 </form>
1305 </div>1305 </div>
13061306
1307 {isGitRepo && <BranchToolbar envMode={envMode} onEnvModeChange={setEnvMode} envLocked={envLocked} />}1307 {isGitRepo && (
1308 <BranchToolbar envMode={envMode} onEnvModeChange={setEnvMode} envLocked={envLocked} />
1309 )}
13081310
1309 {activeThread.terminalOpen && api && activeProject && (1311 {activeThread.terminalOpen && api && activeProject && (
1310 <ThreadTerminalDrawer1312 <ThreadTerminalDrawer
apps/web/src/components/GitActionsControl.logic.test.ts+22-5
...@@ -383,7 +383,11 @@ describe("when: working tree has local changes", () => {...@@ -383,7 +383,11 @@ describe("when: working tree has local changes", () => {
383383
384describe("when: on default branch without open PR", () => {384describe("when: on default branch without open PR", () => {
385 it("resolveQuickAction returns commit and push when local changes exist", () => {385 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 );
387 assert.deepInclude(quick, {391 assert.deepInclude(quick, {
388 kind: "run_action",392 kind: "run_action",
389 action: "commit_push",393 action: "commit_push",
...@@ -393,7 +397,11 @@ describe("when: on default branch without open PR", () => {...@@ -393,7 +397,11 @@ describe("when: on default branch without open PR", () => {
393 });397 });
394398
395 it("resolveQuickAction returns push when branch is ahead", () => {399 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 );
397 assert.deepInclude(quick, {405 assert.deepInclude(quick, {
398 kind: "run_action",406 kind: "run_action",
399 action: "commit_push",407 action: "commit_push",
...@@ -405,7 +413,10 @@ describe("when: on default branch without open PR", () => {...@@ -405,7 +413,10 @@ describe("when: on default branch without open PR", () => {
405413
406describe("when: working tree has local changes and branch is behind upstream", () => {414describe("when: working tree has local changes and branch is behind upstream", () => {
407 it("resolveQuickAction still prefers commit, push, and create PR", () => {415 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 );
409 assert.deepInclude(quick, {420 assert.deepInclude(quick, {
410 kind: "run_action",421 kind: "run_action",
411 action: "commit_push_pr",422 action: "commit_push_pr",
...@@ -486,7 +497,10 @@ describe("when: HEAD is detached and there are no local changes", () => {...@@ -486,7 +497,10 @@ describe("when: HEAD is detached and there are no local changes", () => {
486497
487describe("when: branch has no upstream configured", () => {498describe("when: branch has no upstream configured", () => {
488 it("resolveQuickAction returns disabled commit state when clean and no commits are ahead", () => {499 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 );
490 assert.deepInclude(quick, {504 assert.deepInclude(quick, {
491 kind: "show_hint",505 kind: "show_hint",
492 label: "Commit",506 label: "Commit",
...@@ -540,7 +554,10 @@ describe("when: branch has no upstream configured", () => {...@@ -540,7 +554,10 @@ describe("when: branch has no upstream configured", () => {
540 });554 });
541555
542 it("buildMenuItems disables push and create PR when no commits are ahead", () => {556 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 );
544 assert.deepEqual(items, [561 assert.deepEqual(items, [
545 {562 {
546 id: "commit",563 id: "commit",
apps/web/src/components/GitActionsControl.logic.ts+14-12
...@@ -1,4 +1,8 @@...@@ -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
3export type GitActionIconName = "commit" | "push" | "pr";7export type GitActionIconName = "commit" | "push" | "pr";
48
...@@ -29,16 +33,20 @@ function shortenSha(sha: string | undefined): string | null {...@@ -29,16 +33,20 @@ function shortenSha(sha: string | undefined): string | null {
29 return sha.slice(0, SHORT_SHA_LENGTH);33 return sha.slice(0, SHORT_SHA_LENGTH);
30}34}
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 {
33 if (!value) return undefined;40 if (!value) return undefined;
34 if (value.length <= maxLength) return value;41 if (value.length <= maxLength) return value;
35 if (maxLength <= 3) return "...".slice(0, maxLength);42 if (maxLength <= 3) return "...".slice(0, maxLength);
36 return `${value.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;43 return `${value.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
37}44}
3845
39export function summarizeGitResult(46export function summarizeGitResult(result: GitRunStackedActionResult): {
40 result: GitRunStackedActionResult,47 title: string;
41): { title: string; description?: string } {48 description?: string;
49} {
42 const withDescription = (title: string, description: string | undefined) =>50 const withDescription = (title: string, description: string | undefined) =>
43 description ? { title, description } : { title };51 description ? { title, description } : { title };
4452
...@@ -79,12 +87,7 @@ export function buildMenuItems(...@@ -79,12 +87,7 @@ export function buildMenuItems(
79 const hasOpenPr = gitStatus.openPr !== null;87 const hasOpenPr = gitStatus.openPr !== null;
80 const isBehind = gitStatus.behindCount > 0;88 const isBehind = gitStatus.behindCount > 0;
81 const canCommit = !isBusy && hasChanges;89 const canCommit = !isBusy && hasChanges;
82 const canPush =90 const canPush = !isBusy && hasBranch && !hasChanges && !isBehind && gitStatus.aheadCount > 0;
83 !isBusy &&
84 hasBranch &&
85 !hasChanges &&
86 !isBehind &&
87 gitStatus.aheadCount > 0;
88 const canCreatePr =91 const canCreatePr =
89 !isBusy &&92 !isBusy &&
90 hasBranch &&93 hasBranch &&
...@@ -215,4 +218,3 @@ export function requiresDefaultBranchConfirmation(...@@ -215,4 +218,3 @@ export function requiresDefaultBranchConfirmation(
215 if (!isDefaultBranch) return false;218 if (!isDefaultBranch) return false;
216 return action === "commit_push" || action === "commit_push_pr";219 return action === "commit_push" || action === "commit_push_pr";
217}220}
218
apps/web/src/components/GitActionsControl.tsx+14-19
...@@ -1,8 +1,4 @@...@@ -1,8 +1,4 @@
1import {1import { type GitStatusResult, type GitStackedAction, type NativeApi } from "@t3tools/contracts";
2 type GitStatusResult,
3 type GitStackedAction,
4 type NativeApi,
5} from "@t3tools/contracts";
6import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";2import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
7import { useCallback, useMemo, useState } from "react";3import { useCallback, useMemo, useState } from "react";
8import { ChevronDownIcon, CloudUploadIcon, GitCommitIcon, InfoIcon } from "lucide-react";4import { ChevronDownIcon, CloudUploadIcon, GitCommitIcon, InfoIcon } from "lucide-react";
...@@ -195,9 +191,9 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -195,9 +191,9 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
195 ? (quickAction.hint ?? "This action is currently unavailable.")191 ? (quickAction.hint ?? "This action is currently unavailable.")
196 : null;192 : null;
197 const [isQuickActionPopoverOpen, setIsQuickActionPopoverOpen] = useState(false);193 const [isQuickActionPopoverOpen, setIsQuickActionPopoverOpen] = useState(false);
198 const [openDisabledMenuItemId, setOpenDisabledMenuItemId] = useState<GitActionMenuItem["id"] | null>(194 const [openDisabledMenuItemId, setOpenDisabledMenuItemId] = useState<
199 null,195 GitActionMenuItem["id"] | null
200 );196 >(null);
201197
202 const refreshGitStatus = useCallback(async () => {198 const refreshGitStatus = useCallback(async () => {
203 if (!api || !gitCwd) return;199 if (!api || !gitCwd) return;
...@@ -240,13 +236,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -240,13 +236,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
240 }, [api, gitStatus?.openPr?.url]);236 }, [api, gitStatus?.openPr?.url]);
241237
242 const runGitActionWithToast = useCallback(238 const runGitActionWithToast = useCallback(
243 async ({239 async ({ action, commitMessage }: { action: GitStackedAction; commitMessage?: string }) => {
244 action,
245 commitMessage,
246 }: {
247 action: GitStackedAction;
248 commitMessage?: string;
249 }) => {
250 const confirmed = await maybeConfirmPushToDefaultBranch(action);240 const confirmed = await maybeConfirmPushToDefaultBranch(action);
251 if (!confirmed) return;241 if (!confirmed) return;
252242
...@@ -396,8 +386,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -396,8 +386,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
396 : activeDialogAction === "push"386 : activeDialogAction === "push"
397 ? "commit_push"387 ? "commit_push"
398 : "commit_push_pr";388 : "commit_push_pr";
399 const commitMessage =389 const commitMessage = activeDialogAction === "commit" ? dialogCommitMessage.trim() : "";
400 activeDialogAction === "commit" ? dialogCommitMessage.trim() : "";
401 setActiveDialogAction(null);390 setActiveDialogAction(null);
402 void runGitActionWithToast({391 void runGitActionWithToast({
403 action,392 action,
...@@ -503,7 +492,11 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -503,7 +492,11 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
503 </MenuTrigger>492 </MenuTrigger>
504 <MenuPopup align="end" sideOffset={4}>493 <MenuPopup align="end" sideOffset={4}>
505 {gitActionMenuItems.map((item) => {494 {gitActionMenuItems.map((item) => {
506 const disabledReason = getMenuActionDisabledReason(item, gitStatus, isGitActionRunning);495 const disabledReason = getMenuActionDisabledReason(
496 item,
497 gitStatus,
498 isGitActionRunning,
499 );
507 if (item.disabled && disabledReason) {500 if (item.disabled && disabledReason) {
508 return (501 return (
509 <Popover502 <Popover
...@@ -611,7 +604,9 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -611,7 +604,9 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
611 <>604 <>
612 <span className="text-success">+{gitStatus.workingTree.insertions}</span>605 <span className="text-success">+{gitStatus.workingTree.insertions}</span>
613 <span className="text-muted-foreground"> / </span>606 <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>
615 </>610 </>
616 )}611 )}
617 </span>612 </span>
apps/web/src/components/ThreadTerminalDrawer.tsx+13-18
...@@ -28,16 +28,11 @@ const MAX_DRAWER_HEIGHT_RATIO = 0.75;...@@ -28,16 +28,11 @@ const MAX_DRAWER_HEIGHT_RATIO = 0.75;
2828
29function maxDrawerHeight(): number {29function maxDrawerHeight(): number {
30 if (typeof window === "undefined") return DEFAULT_THREAD_TERMINAL_HEIGHT;30 if (typeof window === "undefined") return DEFAULT_THREAD_TERMINAL_HEIGHT;
31 return Math.max(31 return Math.max(MIN_DRAWER_HEIGHT, Math.floor(window.innerHeight * MAX_DRAWER_HEIGHT_RATIO));
32 MIN_DRAWER_HEIGHT,
33 Math.floor(window.innerHeight * MAX_DRAWER_HEIGHT_RATIO),
34 );
35}32}
3633
37function clampDrawerHeight(height: number): number {34function clampDrawerHeight(height: number): number {
38 const safeHeight = Number.isFinite(height)35 const safeHeight = Number.isFinite(height) ? height : DEFAULT_THREAD_TERMINAL_HEIGHT;
39 ? height
40 : DEFAULT_THREAD_TERMINAL_HEIGHT;
41 const maxHeight = maxDrawerHeight();36 const maxHeight = maxDrawerHeight();
42 return Math.min(Math.max(Math.round(safeHeight), MIN_DRAWER_HEIGHT), maxHeight);37 return Math.min(Math.max(Math.round(safeHeight), MIN_DRAWER_HEIGHT), maxHeight);
43}38}
...@@ -51,8 +46,7 @@ function terminalThemeFromApp(): ITheme {...@@ -51,8 +46,7 @@ function terminalThemeFromApp(): ITheme {
51 const bodyStyles = getComputedStyle(document.body);46 const bodyStyles = getComputedStyle(document.body);
52 const background =47 const background =
53 bodyStyles.backgroundColor || (isDark ? "rgb(14, 18, 24)" : "rgb(255, 255, 255)");48 bodyStyles.backgroundColor || (isDark ? "rgb(14, 18, 24)" : "rgb(255, 255, 255)");
54 const foreground =49 const foreground = bodyStyles.color || (isDark ? "rgb(237, 241, 247)" : "rgb(28, 33, 41)");
55 bodyStyles.color || (isDark ? "rgb(237, 241, 247)" : "rgb(28, 33, 41)");
5650
57 if (isDark) {51 if (isDark) {
58 return {52 return {
...@@ -146,8 +140,7 @@ function TerminalViewport({...@@ -146,8 +140,7 @@ function TerminalViewport({
146 lineHeight: 1.2,140 lineHeight: 1.2,
147 fontSize: 12,141 fontSize: 12,
148 scrollback: 5_000,142 scrollback: 5_000,
149 fontFamily:143 fontFamily: '"SF Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace',
150 '"SF Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace',
151 theme: terminalThemeFromApp(),144 theme: terminalThemeFromApp(),
152 });145 });
153 terminal.loadAddon(fitAddon);146 terminal.loadAddon(fitAddon);
...@@ -538,8 +531,9 @@ export default function ThreadTerminalDrawer({...@@ -538,8 +531,9 @@ export default function ThreadTerminalDrawer({
538 return indexByTerminal >= 0 ? indexByTerminal : 0;531 return indexByTerminal >= 0 ? indexByTerminal : 0;
539 }, [activeTerminalGroupId, resolvedActiveTerminalId, resolvedTerminalGroups]);532 }, [activeTerminalGroupId, resolvedActiveTerminalId, resolvedTerminalGroups]);
540533
541 const visibleTerminalIds =534 const visibleTerminalIds = resolvedTerminalGroups[resolvedActiveGroupIndex]?.terminalIds ?? [
542 resolvedTerminalGroups[resolvedActiveGroupIndex]?.terminalIds ?? [resolvedActiveTerminalId];535 resolvedActiveTerminalId,
536 ];
543 const hasTerminalSidebar = normalizedTerminalIds.length > 1;537 const hasTerminalSidebar = normalizedTerminalIds.length > 1;
544 const isSplitView = visibleTerminalIds.length > 1;538 const isSplitView = visibleTerminalIds.length > 1;
545 const showGroupHeaders =539 const showGroupHeaders =
...@@ -707,9 +701,7 @@ export default function ThreadTerminalDrawer({...@@ -707,9 +701,7 @@ export default function ThreadTerminalDrawer({
707 <div701 <div
708 key={terminalId}702 key={terminalId}
709 className={`min-h-0 min-w-0 border-l first:border-l-0 ${703 className={`min-h-0 min-w-0 border-l first:border-l-0 ${
710 terminalId === resolvedActiveTerminalId704 terminalId === resolvedActiveTerminalId ? "border-border" : "border-border/70"
711 ? "border-border"
712 : "border-border/70"
713 }`}705 }`}
714 onMouseDown={() => {706 onMouseDown={() => {
715 if (terminalId !== resolvedActiveTerminalId) {707 if (terminalId !== resolvedActiveTerminalId) {
...@@ -785,7 +777,8 @@ export default function ThreadTerminalDrawer({...@@ -785,7 +777,8 @@ export default function ThreadTerminalDrawer({
785777
786 <div className="min-h-0 flex-1 overflow-y-auto px-1 py-1">778 <div className="min-h-0 flex-1 overflow-y-auto px-1 py-1">
787 {resolvedTerminalGroups.map((terminalGroup, groupIndex) => {779 {resolvedTerminalGroups.map((terminalGroup, groupIndex) => {
788 const isGroupActive = terminalGroup.terminalIds.includes(resolvedActiveTerminalId);780 const isGroupActive =
781 terminalGroup.terminalIds.includes(resolvedActiveTerminalId);
789 const groupActiveTerminalId = isGroupActive782 const groupActiveTerminalId = isGroupActive
790 ? resolvedActiveTerminalId783 ? resolvedActiveTerminalId
791 : (terminalGroup.terminalIds[0] ?? resolvedActiveTerminalId);784 : (terminalGroup.terminalIds[0] ?? resolvedActiveTerminalId);
...@@ -808,7 +801,9 @@ export default function ThreadTerminalDrawer({...@@ -808,7 +801,9 @@ export default function ThreadTerminalDrawer({
808 </button>801 </button>
809 )}802 )}
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 >
812 {terminalGroup.terminalIds.map((terminalId) => {807 {terminalGroup.terminalIds.map((terminalId) => {
813 const isActive = terminalId === resolvedActiveTerminalId;808 const isActive = terminalId === resolvedActiveTerminalId;
814 return (809 return (
apps/web/src/components/ui/alert-dialog.tsx+10-38
...@@ -11,15 +11,10 @@ const AlertDialog = AlertDialogPrimitive.Root;...@@ -11,15 +11,10 @@ const AlertDialog = AlertDialogPrimitive.Root;
11const AlertDialogPortal = AlertDialogPrimitive.Portal;11const AlertDialogPortal = AlertDialogPrimitive.Portal;
1212
13function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) {13function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) {
14 return (14 return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
15 <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
16 );
17}15}
1816
19function AlertDialogBackdrop({17function AlertDialogBackdrop({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {
20 className,
21 ...props
22}: AlertDialogPrimitive.Backdrop.Props) {
23 return (18 return (
24 <AlertDialogPrimitive.Backdrop19 <AlertDialogPrimitive.Backdrop
25 className={cn(20 className={cn(
...@@ -32,10 +27,7 @@ function AlertDialogBackdrop({...@@ -32,10 +27,7 @@ function AlertDialogBackdrop({
32 );27 );
33}28}
3429
35function AlertDialogViewport({30function AlertDialogViewport({ className, ...props }: AlertDialogPrimitive.Viewport.Props) {
36 className,
37 ...props
38}: AlertDialogPrimitive.Viewport.Props) {
39 return (31 return (
40 <AlertDialogPrimitive.Viewport32 <AlertDialogPrimitive.Viewport
41 className={cn(33 className={cn(
...@@ -59,10 +51,7 @@ function AlertDialogPopup({...@@ -59,10 +51,7 @@ function AlertDialogPopup({
59 <AlertDialogPortal>51 <AlertDialogPortal>
60 <AlertDialogBackdrop />52 <AlertDialogBackdrop />
61 <AlertDialogViewport53 <AlertDialogViewport
62 className={cn(54 className={cn(bottomStickOnMobile && "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12")}
63 bottomStickOnMobile &&
64 "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12",
65 )}
66 >55 >
67 <AlertDialogPrimitive.Popup56 <AlertDialogPrimitive.Popup
68 className={cn(57 className={cn(
...@@ -79,16 +68,10 @@ function AlertDialogPopup({...@@ -79,16 +68,10 @@ function AlertDialogPopup({
79 );68 );
80}69}
8170
82function AlertDialogHeader({71function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
83 className,
84 ...props
85}: React.ComponentProps<"div">) {
86 return (72 return (
87 <div73 <div
88 className={cn(74 className={cn("flex flex-col gap-2 p-6 text-center max-sm:pb-4 sm:text-left", className)}
89 "flex flex-col gap-2 p-6 text-center max-sm:pb-4 sm:text-left",
90 className,
91 )}
92 data-slot="alert-dialog-header"75 data-slot="alert-dialog-header"
93 {...props}76 {...props}
94 />77 />
...@@ -116,26 +99,17 @@ function AlertDialogFooter({...@@ -116,26 +99,17 @@ function AlertDialogFooter({
116 );99 );
117}100}
118101
119function AlertDialogTitle({102function AlertDialogTitle({ className, ...props }: AlertDialogPrimitive.Title.Props) {
120 className,
121 ...props
122}: AlertDialogPrimitive.Title.Props) {
123 return (103 return (
124 <AlertDialogPrimitive.Title104 <AlertDialogPrimitive.Title
125 className={cn(105 className={cn("font-heading font-semibold text-xl leading-none", className)}
126 "font-heading font-semibold text-xl leading-none",
127 className,
128 )}
129 data-slot="alert-dialog-title"106 data-slot="alert-dialog-title"
130 {...props}107 {...props}
131 />108 />
132 );109 );
133}110}
134111
135function AlertDialogDescription({112function AlertDialogDescription({ className, ...props }: AlertDialogPrimitive.Description.Props) {
136 className,
137 ...props
138}: AlertDialogPrimitive.Description.Props) {
139 return (113 return (
140 <AlertDialogPrimitive.Description114 <AlertDialogPrimitive.Description
141 className={cn("text-muted-foreground text-sm", className)}115 className={cn("text-muted-foreground text-sm", className)}
...@@ -146,9 +120,7 @@ function AlertDialogDescription({...@@ -146,9 +120,7 @@ function AlertDialogDescription({
146}120}
147121
148function AlertDialogClose(props: AlertDialogPrimitive.Close.Props) {122function AlertDialogClose(props: AlertDialogPrimitive.Close.Props) {
149 return (123 return <AlertDialogPrimitive.Close data-slot="alert-dialog-close" {...props} />;
150 <AlertDialogPrimitive.Close data-slot="alert-dialog-close" {...props} />
151 );
152}124}
153125
154export {126export {
apps/web/src/components/ui/alert.tsx+4-12
...@@ -11,10 +11,8 @@ const alertVariants = cva(...@@ -11,10 +11,8 @@ const alertVariants = cva(
11 },11 },
12 variants: {12 variants: {
13 variant: {13 variant: {
14 default:14 default: "bg-transparent dark:bg-input/32 [&>svg]:text-muted-foreground",
15 "bg-transparent dark:bg-input/32 [&>svg]:text-muted-foreground",15 error: "border-destructive/32 bg-destructive/4 [&>svg]:text-destructive",
16 error:
17 "border-destructive/32 bg-destructive/4 [&>svg]:text-destructive",
18 info: "border-info/32 bg-info/4 [&>svg]:text-info",16 info: "border-info/32 bg-info/4 [&>svg]:text-info",
19 success: "border-success/32 bg-success/4 [&>svg]:text-success",17 success: "border-success/32 bg-success/4 [&>svg]:text-success",
20 warning: "border-warning/32 bg-warning/4 [&>svg]:text-warning",18 warning: "border-warning/32 bg-warning/4 [&>svg]:text-warning",
...@@ -48,16 +46,10 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {...@@ -48,16 +46,10 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
48 );46 );
49}47}
5048
51function AlertDescription({49function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
52 className,
53 ...props
54}: React.ComponentProps<"div">) {
55 return (50 return (
56 <div51 <div
57 className={cn(52 className={cn("flex flex-col gap-2.5 text-muted-foreground [svg~&]:col-start-2", className)}
58 "flex flex-col gap-2.5 text-muted-foreground [svg~&]:col-start-2",
59 className,
60 )}
61 data-slot="alert-description"53 data-slot="alert-description"
62 {...props}54 {...props}
63 />55 />
apps/web/src/components/ui/autocomplete.tsx+15-62
...@@ -121,11 +121,7 @@ function AutocompletePopup({...@@ -121,11 +121,7 @@ function AutocompletePopup({
121 );121 );
122}122}
123123
124function AutocompleteItem({124function AutocompleteItem({ className, children, ...props }: AutocompletePrimitive.Item.Props) {
125 className,
126 children,
127 ...props
128}: AutocompletePrimitive.Item.Props) {
129 return (125 return (
130 <AutocompletePrimitive.Item126 <AutocompletePrimitive.Item
131 className={cn(127 className={cn(
...@@ -140,10 +136,7 @@ function AutocompleteItem({...@@ -140,10 +136,7 @@ function AutocompleteItem({
140 );136 );
141}137}
142138
143function AutocompleteSeparator({139function AutocompleteSeparator({ className, ...props }: AutocompletePrimitive.Separator.Props) {
144 className,
145 ...props
146}: AutocompletePrimitive.Separator.Props) {
147 return (140 return (
148 <AutocompletePrimitive.Separator141 <AutocompletePrimitive.Separator
149 className={cn("mx-2 my-1 h-px bg-border last:hidden", className)}142 className={cn("mx-2 my-1 h-px bg-border last:hidden", className)}
...@@ -153,10 +146,7 @@ function AutocompleteSeparator({...@@ -153,10 +146,7 @@ function AutocompleteSeparator({
153 );146 );
154}147}
155148
156function AutocompleteGroup({149function AutocompleteGroup({ className, ...props }: AutocompletePrimitive.Group.Props) {
157 className,
158 ...props
159}: AutocompletePrimitive.Group.Props) {
160 return (150 return (
161 <AutocompletePrimitive.Group151 <AutocompletePrimitive.Group
162 className={cn("[[role=group]+&]:mt-1.5", className)}152 className={cn("[[role=group]+&]:mt-1.5", className)}
...@@ -166,26 +156,17 @@ function AutocompleteGroup({...@@ -166,26 +156,17 @@ function AutocompleteGroup({
166 );156 );
167}157}
168158
169function AutocompleteGroupLabel({159function AutocompleteGroupLabel({ className, ...props }: AutocompletePrimitive.GroupLabel.Props) {
170 className,
171 ...props
172}: AutocompletePrimitive.GroupLabel.Props) {
173 return (160 return (
174 <AutocompletePrimitive.GroupLabel161 <AutocompletePrimitive.GroupLabel
175 className={cn(162 className={cn("px-2 py-1.5 font-medium text-muted-foreground text-xs", className)}
176 "px-2 py-1.5 font-medium text-muted-foreground text-xs",
177 className,
178 )}
179 data-slot="autocomplete-group-label"163 data-slot="autocomplete-group-label"
180 {...props}164 {...props}
181 />165 />
182 );166 );
183}167}
184168
185function AutocompleteEmpty({169function AutocompleteEmpty({ className, ...props }: AutocompletePrimitive.Empty.Props) {
186 className,
187 ...props
188}: AutocompletePrimitive.Empty.Props) {
189 return (170 return (
190 <AutocompletePrimitive.Empty171 <AutocompletePrimitive.Empty
191 className={cn(172 className={cn(
...@@ -198,36 +179,21 @@ function AutocompleteEmpty({...@@ -198,36 +179,21 @@ function AutocompleteEmpty({
198 );179 );
199}180}
200181
201function AutocompleteRow({182function AutocompleteRow({ className, ...props }: AutocompletePrimitive.Row.Props) {
202 className,
203 ...props
204}: AutocompletePrimitive.Row.Props) {
205 return (183 return (
206 <AutocompletePrimitive.Row184 <AutocompletePrimitive.Row className={className} data-slot="autocomplete-row" {...props} />
207 className={className}
208 data-slot="autocomplete-row"
209 {...props}
210 />
211 );185 );
212}186}
213187
214function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) {188function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) {
215 return (189 return <AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} />;
216 <AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} />
217 );
218}190}
219191
220function AutocompleteList({192function AutocompleteList({ className, ...props }: AutocompletePrimitive.List.Props) {
221 className,
222 ...props
223}: AutocompletePrimitive.List.Props) {
224 return (193 return (
225 <ScrollArea scrollbarGutter scrollFade>194 <ScrollArea scrollbarGutter scrollFade>
226 <AutocompletePrimitive.List195 <AutocompletePrimitive.List
227 className={cn(196 className={cn("not-empty:scroll-py-1 not-empty:p-1 in-data-has-overflow-y:pe-3", className)}
228 "not-empty:scroll-py-1 not-empty:p-1 in-data-has-overflow-y:pe-3",
229 className,
230 )}
231 data-slot="autocomplete-list"197 data-slot="autocomplete-list"
232 {...props}198 {...props}
233 />199 />
...@@ -235,10 +201,7 @@ function AutocompleteList({...@@ -235,10 +201,7 @@ function AutocompleteList({
235 );201 );
236}202}
237203
238function AutocompleteClear({204function AutocompleteClear({ className, ...props }: AutocompletePrimitive.Clear.Props) {
239 className,
240 ...props
241}: AutocompletePrimitive.Clear.Props) {
242 return (205 return (
243 <AutocompletePrimitive.Clear206 <AutocompletePrimitive.Clear
244 className={cn(207 className={cn(
...@@ -253,10 +216,7 @@ function AutocompleteClear({...@@ -253,10 +216,7 @@ function AutocompleteClear({
253 );216 );
254}217}
255218
256function AutocompleteStatus({219function AutocompleteStatus({ className, ...props }: AutocompletePrimitive.Status.Props) {
257 className,
258 ...props
259}: AutocompletePrimitive.Status.Props) {
260 return (220 return (
261 <AutocompletePrimitive.Status221 <AutocompletePrimitive.Status
262 className={cn(222 className={cn(
...@@ -269,15 +229,8 @@ function AutocompleteStatus({...@@ -269,15 +229,8 @@ function AutocompleteStatus({
269 );229 );
270}230}
271231
272function AutocompleteCollection({232function AutocompleteCollection({ ...props }: AutocompletePrimitive.Collection.Props) {
273 ...props233 return <AutocompletePrimitive.Collection data-slot="autocomplete-collection" {...props} />;
274}: AutocompletePrimitive.Collection.Props) {
275 return (
276 <AutocompletePrimitive.Collection
277 data-slot="autocomplete-collection"
278 {...props}
279 />
280 );
281}234}
282235
283function AutocompleteTrigger({236function AutocompleteTrigger({
apps/web/src/components/ui/badge.tsx+4-8
...@@ -21,17 +21,13 @@ const badgeVariants = cva(...@@ -21,17 +21,13 @@ const badgeVariants = cva(
21 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]",21 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]",
22 },22 },
23 variant: {23 variant: {
24 default:24 default: "bg-primary text-primary-foreground [button&,a&]:hover:bg-primary/90",
25 "bg-primary text-primary-foreground [button&,a&]:hover:bg-primary/90",25 destructive: "bg-destructive text-white [button&,a&]:hover:bg-destructive/90",
26 destructive:26 error: "bg-destructive/8 text-destructive-foreground dark:bg-destructive/16",
27 "bg-destructive text-white [button&,a&]:hover:bg-destructive/90",
28 error:
29 "bg-destructive/8 text-destructive-foreground dark:bg-destructive/16",
30 info: "bg-info/8 text-info-foreground dark:bg-info/16",27 info: "bg-info/8 text-info-foreground dark:bg-info/16",
31 outline:28 outline:
32 "border-input bg-background text-foreground dark:bg-input/32 [button&,a&]:hover:bg-accent/50 dark:[button&,a&]:hover:bg-input/48",29 "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:30 secondary: "bg-secondary text-secondary-foreground [button&,a&]:hover:bg-secondary/90",
34 "bg-secondary text-secondary-foreground [button&,a&]:hover:bg-secondary/90",
35 success: "bg-success/8 text-success-foreground dark:bg-success/16",31 success: "bg-success/8 text-success-foreground dark:bg-success/16",
36 warning: "bg-warning/8 text-warning-foreground dark:bg-warning/16",32 warning: "bg-warning/8 text-warning-foreground dark:bg-warning/16",
37 },33 },
apps/web/src/components/ui/card.tsx+12-60
...@@ -5,11 +5,7 @@ import { useRender } from "@base-ui/react/use-render";...@@ -5,11 +5,7 @@ import { useRender } from "@base-ui/react/use-render";
55
6import { cn } from "~/lib/utils";6import { cn } from "~/lib/utils";
77
8function Card({8function Card({ className, render, ...props }: useRender.ComponentProps<"div">) {
9 className,
10 render,
11 ...props
12}: useRender.ComponentProps<"div">) {
13 const defaultProps = {9 const defaultProps = {
14 className: cn(10 className: cn(
15 "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%)]",11 "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({...@@ -25,11 +21,7 @@ function Card({
25 });21 });
26}22}
2723
28function CardFrame({24function CardFrame({ className, render, ...props }: useRender.ComponentProps<"div">) {
29 className,
30 render,
31 ...props
32}: useRender.ComponentProps<"div">) {
33 const defaultProps = {25 const defaultProps = {
34 className: cn(26 className: cn(
35 "[--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",27 "[--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({...@@ -45,11 +37,7 @@ function CardFrame({
45 });37 });
46}38}
4739
48function CardFrameHeader({40function CardFrameHeader({ className, render, ...props }: useRender.ComponentProps<"div">) {
49 className,
50 render,
51 ...props
52}: useRender.ComponentProps<"div">) {
53 const defaultProps = {41 const defaultProps = {
54 className: cn("relative flex flex-col px-6 py-4", className),42 className: cn("relative flex flex-col px-6 py-4", className),
55 "data-slot": "card-frame-header",43 "data-slot": "card-frame-header",
...@@ -62,11 +50,7 @@ function CardFrameHeader({...@@ -62,11 +50,7 @@ function CardFrameHeader({
62 });50 });
63}51}
6452
65function CardFrameTitle({53function CardFrameTitle({ className, render, ...props }: useRender.ComponentProps<"div">) {
66 className,
67 render,
68 ...props
69}: useRender.ComponentProps<"div">) {
70 const defaultProps = {54 const defaultProps = {
71 className: cn("font-semibold text-sm", className),55 className: cn("font-semibold text-sm", className),
72 "data-slot": "card-frame-title",56 "data-slot": "card-frame-title",
...@@ -79,11 +63,7 @@ function CardFrameTitle({...@@ -79,11 +63,7 @@ function CardFrameTitle({
79 });63 });
80}64}
8165
82function CardFrameDescription({66function CardFrameDescription({ className, render, ...props }: useRender.ComponentProps<"div">) {
83 className,
84 render,
85 ...props
86}: useRender.ComponentProps<"div">) {
87 const defaultProps = {67 const defaultProps = {
88 className: cn("text-muted-foreground text-sm", className),68 className: cn("text-muted-foreground text-sm", className),
89 "data-slot": "card-frame-description",69 "data-slot": "card-frame-description",
...@@ -96,11 +76,7 @@ function CardFrameDescription({...@@ -96,11 +76,7 @@ function CardFrameDescription({
96 });76 });
97}77}
9878
99function CardFrameFooter({79function CardFrameFooter({ className, render, ...props }: useRender.ComponentProps<"div">) {
100 className,
101 render,
102 ...props
103}: useRender.ComponentProps<"div">) {
104 const defaultProps = {80 const defaultProps = {
105 className: cn("px-6 py-4", className),81 className: cn("px-6 py-4", className),
106 "data-slot": "card-frame-footer",82 "data-slot": "card-frame-footer",
...@@ -113,11 +89,7 @@ function CardFrameFooter({...@@ -113,11 +89,7 @@ function CardFrameFooter({
113 });89 });
114}90}
11591
116function CardHeader({92function CardHeader({ className, render, ...props }: useRender.ComponentProps<"div">) {
117 className,
118 render,
119 ...props
120}: useRender.ComponentProps<"div">) {
121 const defaultProps = {93 const defaultProps = {
122 className: cn(94 className: cn(
123 "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]",95 "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({...@@ -133,11 +105,7 @@ function CardHeader({
133 });105 });
134}106}
135107
136function CardTitle({108function CardTitle({ className, render, ...props }: useRender.ComponentProps<"div">) {
137 className,
138 render,
139 ...props
140}: useRender.ComponentProps<"div">) {
141 const defaultProps = {109 const defaultProps = {
142 className: cn("font-semibold text-lg leading-none", className),110 className: cn("font-semibold text-lg leading-none", className),
143 "data-slot": "card-title",111 "data-slot": "card-title",
...@@ -150,11 +118,7 @@ function CardTitle({...@@ -150,11 +118,7 @@ function CardTitle({
150 });118 });
151}119}
152120
153function CardDescription({121function CardDescription({ className, render, ...props }: useRender.ComponentProps<"div">) {
154 className,
155 render,
156 ...props
157}: useRender.ComponentProps<"div">) {
158 const defaultProps = {122 const defaultProps = {
159 className: cn("text-muted-foreground text-sm", className),123 className: cn("text-muted-foreground text-sm", className),
160 "data-slot": "card-description",124 "data-slot": "card-description",
...@@ -167,11 +131,7 @@ function CardDescription({...@@ -167,11 +131,7 @@ function CardDescription({
167 });131 });
168}132}
169133
170function CardAction({134function CardAction({ className, render, ...props }: useRender.ComponentProps<"div">) {
171 className,
172 render,
173 ...props
174}: useRender.ComponentProps<"div">) {
175 const defaultProps = {135 const defaultProps = {
176 className: cn(136 className: cn(
177 "col-start-2 row-span-2 row-start-1 self-start justify-self-end inline-flex",137 "col-start-2 row-span-2 row-start-1 self-start justify-self-end inline-flex",
...@@ -187,11 +147,7 @@ function CardAction({...@@ -187,11 +147,7 @@ function CardAction({
187 });147 });
188}148}
189149
190function CardPanel({150function CardPanel({ className, render, ...props }: useRender.ComponentProps<"div">) {
191 className,
192 render,
193 ...props
194}: useRender.ComponentProps<"div">) {
195 const defaultProps = {151 const defaultProps = {
196 className: cn(152 className: cn(
197 "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",153 "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({...@@ -207,11 +163,7 @@ function CardPanel({
207 });163 });
208}164}
209165
210function CardFooter({166function CardFooter({ className, render, ...props }: useRender.ComponentProps<"div">) {
211 className,
212 render,
213 ...props
214}: useRender.ComponentProps<"div">) {
215 const defaultProps = {167 const defaultProps = {
216 className: cn(168 className: cn(
217 "flex items-center p-6 in-[[data-slot=card]:has(>[data-slot=card-panel])]:pt-4",169 "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;...@@ -23,18 +23,10 @@ const CommandDialogPortal = CommandDialogPrimitive.Portal;
23const CommandCreateHandle = CommandDialogPrimitive.createHandle;23const CommandCreateHandle = CommandDialogPrimitive.createHandle;
2424
25function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) {25function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) {
26 return (26 return <CommandDialogPrimitive.Trigger data-slot="command-dialog-trigger" {...props} />;
27 <CommandDialogPrimitive.Trigger
28 data-slot="command-dialog-trigger"
29 {...props}
30 />
31 );
32}27}
3328
34function CommandDialogBackdrop({29function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.Backdrop.Props) {
35 className,
36 ...props
37}: CommandDialogPrimitive.Backdrop.Props) {
38 return (30 return (
39 <CommandDialogPrimitive.Backdrop31 <CommandDialogPrimitive.Backdrop
40 className={cn(32 className={cn(
...@@ -47,10 +39,7 @@ function CommandDialogBackdrop({...@@ -47,10 +39,7 @@ function CommandDialogBackdrop({
47 );39 );
48}40}
4941
50function CommandDialogViewport({42function CommandDialogViewport({ className, ...props }: CommandDialogPrimitive.Viewport.Props) {
51 className,
52 ...props
53}: CommandDialogPrimitive.Viewport.Props) {
54 return (43 return (
55 <CommandDialogPrimitive.Viewport44 <CommandDialogPrimitive.Viewport
56 className={cn(45 className={cn(
...@@ -63,11 +52,7 @@ function CommandDialogViewport({...@@ -63,11 +52,7 @@ function CommandDialogViewport({
63 );52 );
64}53}
6554
66function CommandDialogPopup({55function CommandDialogPopup({ className, children, ...props }: CommandDialogPrimitive.Popup.Props) {
67 className,
68 children,
69 ...props
70}: CommandDialogPrimitive.Popup.Props) {
71 return (56 return (
72 <CommandDialogPortal>57 <CommandDialogPortal>
73 <CommandDialogBackdrop />58 <CommandDialogBackdrop />
...@@ -125,10 +110,7 @@ function CommandInput({...@@ -125,10 +110,7 @@ function CommandInput({
125 );110 );
126}111}
127112
128function CommandList({113function CommandList({ className, ...props }: React.ComponentProps<typeof AutocompleteList>) {
129 className,
130 ...props
131}: React.ComponentProps<typeof AutocompleteList>) {
132 return (114 return (
133 <AutocompleteList115 <AutocompleteList
134 className={cn("not-empty:scroll-py-2 not-empty:p-2", className)}116 className={cn("not-empty:scroll-py-2 not-empty:p-2", className)}
...@@ -138,10 +120,7 @@ function CommandList({...@@ -138,10 +120,7 @@ function CommandList({
138 );120 );
139}121}
140122
141function CommandEmpty({123function CommandEmpty({ className, ...props }: React.ComponentProps<typeof AutocompleteEmpty>) {
142 className,
143 ...props
144}: React.ComponentProps<typeof AutocompleteEmpty>) {
145 return (124 return (
146 <AutocompleteEmpty125 <AutocompleteEmpty
147 className={cn("not-empty:py-6", className)}126 className={cn("not-empty:py-6", className)}
...@@ -160,17 +139,8 @@ function CommandPanel({ className, ...props }: React.ComponentProps<"div">) {...@@ -160,17 +139,8 @@ function CommandPanel({ className, ...props }: React.ComponentProps<"div">) {
160 );139 );
161}140}
162141
163function CommandGroup({142function CommandGroup({ className, ...props }: React.ComponentProps<typeof AutocompleteGroup>) {
164 className,143 return <AutocompleteGroup className={className} data-slot="command-group" {...props} />;
165 ...props
166}: React.ComponentProps<typeof AutocompleteGroup>) {
167 return (
168 <AutocompleteGroup
169 className={className}
170 data-slot="command-group"
171 {...props}
172 />
173 );
174}144}
175145
176function CommandGroupLabel({146function CommandGroupLabel({
...@@ -178,30 +148,17 @@ function CommandGroupLabel({...@@ -178,30 +148,17 @@ function CommandGroupLabel({
178 ...props148 ...props
179}: React.ComponentProps<typeof AutocompleteGroupLabel>) {149}: React.ComponentProps<typeof AutocompleteGroupLabel>) {
180 return (150 return (
181 <AutocompleteGroupLabel151 <AutocompleteGroupLabel className={className} data-slot="command-group-label" {...props} />
182 className={className}
183 data-slot="command-group-label"
184 {...props}
185 />
186 );152 );
187}153}
188154
189function CommandCollection({155function CommandCollection({ ...props }: React.ComponentProps<typeof AutocompleteCollection>) {
190 ...props
191}: React.ComponentProps<typeof AutocompleteCollection>) {
192 return <AutocompleteCollection data-slot="command-collection" {...props} />;156 return <AutocompleteCollection data-slot="command-collection" {...props} />;
193}157}
194158
195function CommandItem({159function CommandItem({ className, ...props }: React.ComponentProps<typeof AutocompleteItem>) {
196 className,
197 ...props
198}: React.ComponentProps<typeof AutocompleteItem>) {
199 return (160 return (
200 <AutocompleteItem161 <AutocompleteItem className={cn("py-1.5", className)} data-slot="command-item" {...props} />
201 className={cn("py-1.5", className)}
202 data-slot="command-item"
203 {...props}
204 />
205 );162 );
206}163}
207164
apps/web/src/components/ui/dialog.tsx+5-20
...@@ -20,10 +20,7 @@ function DialogClose(props: DialogPrimitive.Close.Props) {...@@ -20,10 +20,7 @@ function DialogClose(props: DialogPrimitive.Close.Props) {
20 return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;20 return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
21}21}
2222
23function DialogBackdrop({23function DialogBackdrop({ className, ...props }: DialogPrimitive.Backdrop.Props) {
24 className,
25 ...props
26}: DialogPrimitive.Backdrop.Props) {
27 return (24 return (
28 <DialogPrimitive.Backdrop25 <DialogPrimitive.Backdrop
29 className={cn(26 className={cn(
...@@ -36,10 +33,7 @@ function DialogBackdrop({...@@ -36,10 +33,7 @@ function DialogBackdrop({
36 );33 );
37}34}
3835
39function DialogViewport({36function DialogViewport({ className, ...props }: DialogPrimitive.Viewport.Props) {
40 className,
41 ...props
42}: DialogPrimitive.Viewport.Props) {
43 return (37 return (
44 <DialogPrimitive.Viewport38 <DialogPrimitive.Viewport
45 className={cn(39 className={cn(
...@@ -66,10 +60,7 @@ function DialogPopup({...@@ -66,10 +60,7 @@ function DialogPopup({
66 <DialogPortal>60 <DialogPortal>
67 <DialogBackdrop />61 <DialogBackdrop />
68 <DialogViewport62 <DialogViewport
69 className={cn(63 className={cn(bottomStickOnMobile && "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12")}
70 bottomStickOnMobile &&
71 "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12",
72 )}
73 >64 >
74 <DialogPrimitive.Popup65 <DialogPrimitive.Popup
75 className={cn(66 className={cn(
...@@ -135,20 +126,14 @@ function DialogFooter({...@@ -135,20 +126,14 @@ function DialogFooter({
135function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {126function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
136 return (127 return (
137 <DialogPrimitive.Title128 <DialogPrimitive.Title
138 className={cn(129 className={cn("font-heading font-semibold text-xl leading-none", className)}
139 "font-heading font-semibold text-xl leading-none",
140 className,
141 )}
142 data-slot="dialog-title"130 data-slot="dialog-title"
143 {...props}131 {...props}
144 />132 />
145 );133 );
146}134}
147135
148function DialogDescription({136function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) {
149 className,
150 ...props
151}: DialogPrimitive.Description.Props) {
152 return (137 return (
153 <DialogPrimitive.Description138 <DialogPrimitive.Description
154 className={cn("text-muted-foreground text-sm", className)}139 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">) {...@@ -18,10 +18,7 @@ function Empty({ className, ...props }: React.ComponentProps<"div">) {
18function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {18function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
19 return (19 return (
20 <div20 <div
21 className={cn(21 className={cn("flex max-w-sm flex-col items-center text-center", className)}
22 "flex max-w-sm flex-col items-center text-center",
23 className,
24 )}
25 data-slot="empty-header"22 data-slot="empty-header"
26 {...props}23 {...props}
27 />24 />
...@@ -73,10 +70,7 @@ function EmptyMedia({...@@ -73,10 +70,7 @@ function EmptyMedia({
73 />70 />
74 </>71 </>
75 )}72 )}
76 <div73 <div className={cn(emptyMediaVariants({ className, variant }))} {...props} />
77 className={cn(emptyMediaVariants({ className, variant }))}
78 {...props}
79 />
80 </div>74 </div>
81 );75 );
82}76}
...@@ -117,11 +111,4 @@ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {...@@ -117,11 +111,4 @@ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
117 );111 );
118}112}
119113
120export {114export { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia };
121 Empty,
122 EmptyHeader,
123 EmptyTitle,
124 EmptyDescription,
125 EmptyContent,
126 EmptyMedia,
127};
apps/web/src/components/ui/field.tsx+3-18
...@@ -29,18 +29,11 @@ function FieldLabel({ className, ...props }: FieldPrimitive.Label.Props) {...@@ -29,18 +29,11 @@ function FieldLabel({ className, ...props }: FieldPrimitive.Label.Props) {
2929
30function FieldItem({ className, ...props }: FieldPrimitive.Item.Props) {30function FieldItem({ className, ...props }: FieldPrimitive.Item.Props) {
31 return (31 return (
32 <FieldPrimitive.Item32 <FieldPrimitive.Item className={cn("flex", className)} data-slot="field-item" {...props} />
33 className={cn("flex", className)}
34 data-slot="field-item"
35 {...props}
36 />
37 );33 );
38}34}
3935
40function FieldDescription({36function FieldDescription({ className, ...props }: FieldPrimitive.Description.Props) {
41 className,
42 ...props
43}: FieldPrimitive.Description.Props) {
44 return (37 return (
45 <FieldPrimitive.Description38 <FieldPrimitive.Description
46 className={cn("text-muted-foreground text-xs", className)}39 className={cn("text-muted-foreground text-xs", className)}
...@@ -63,12 +56,4 @@ function FieldError({ className, ...props }: FieldPrimitive.Error.Props) {...@@ -63,12 +56,4 @@ function FieldError({ className, ...props }: FieldPrimitive.Error.Props) {
63const FieldControl = FieldPrimitive.Control;56const FieldControl = FieldPrimitive.Control;
64const FieldValidity = FieldPrimitive.Validity;57const FieldValidity = FieldPrimitive.Validity;
6558
66export {59export { Field, FieldLabel, FieldDescription, FieldError, FieldControl, FieldItem, FieldValidity };
67 Field,
68 FieldLabel,
69 FieldDescription,
70 FieldError,
71 FieldControl,
72 FieldItem,
73 FieldValidity,
74};
apps/web/src/components/ui/fieldset.tsx+1-4
...@@ -13,10 +13,7 @@ function Fieldset({ className, ...props }: FieldsetPrimitive.Root.Props) {...@@ -13,10 +13,7 @@ function Fieldset({ className, ...props }: FieldsetPrimitive.Root.Props) {
13 />13 />
14 );14 );
15}15}
16function FieldsetLegend({16function FieldsetLegend({ className, ...props }: FieldsetPrimitive.Legend.Props) {
17 className,
18 ...props
19}: FieldsetPrimitive.Legend.Props) {
20 return (17 return (
21 <FieldsetPrimitive.Legend18 <FieldsetPrimitive.Legend
22 className={cn("font-semibold text-foreground", className)}19 className={cn("font-semibold text-foreground", className)}
apps/web/src/components/ui/input-group.tsx+4-10
...@@ -60,9 +60,9 @@ function InputGroupAddon({...@@ -60,9 +60,9 @@ function InputGroupAddon({
60 if (isInteractive) return;60 if (isInteractive) return;
61 e.preventDefault();61 e.preventDefault();
62 const parent = e.currentTarget.parentElement;62 const parent = e.currentTarget.parentElement;
63 const input = parent?.querySelector<63 const input = parent?.querySelector<HTMLInputElement | HTMLTextAreaElement>(
64 HTMLInputElement | HTMLTextAreaElement64 "input, textarea",
65 >("input, textarea");65 );
66 if (input && !parent?.querySelector("input:focus, textarea:focus")) {66 if (input && !parent?.querySelector("input:focus, textarea:focus")) {
67 input.focus();67 input.focus();
68 }68 }
...@@ -92,10 +92,4 @@ function InputGroupTextarea({ className, ...props }: TextareaProps) {...@@ -92,10 +92,4 @@ function InputGroupTextarea({ className, ...props }: TextareaProps) {
92 return <Textarea className={className} unstyled {...props} />;92 return <Textarea className={className} unstyled {...props} />;
93}93}
9494
95export {95export { InputGroup, InputGroupAddon, InputGroupText, InputGroupInput, InputGroupTextarea };
96 InputGroup,
97 InputGroupAddon,
98 InputGroupText,
99 InputGroupInput,
100 InputGroupTextarea,
101};
apps/web/src/components/ui/input.tsx+2-6
...@@ -5,10 +5,7 @@ import type * as React from "react";...@@ -5,10 +5,7 @@ import type * as React from "react";
55
6import { cn } from "~/lib/utils";6import { cn } from "~/lib/utils";
77
8type InputProps = Omit<8type InputProps = Omit<InputPrimitive.Props & React.RefAttributes<HTMLInputElement>, "size"> & {
9 InputPrimitive.Props & React.RefAttributes<HTMLInputElement>,
10 "size"
11> & {
12 size?: "sm" | "default" | "lg" | number;9 size?: "sm" | "default" | "lg" | number;
13 unstyled?: boolean;10 unstyled?: boolean;
14 nativeInput?: boolean;11 nativeInput?: boolean;
...@@ -23,8 +20,7 @@ function Input({...@@ -23,8 +20,7 @@ function Input({
23}: InputProps) {20}: InputProps) {
24 const inputClassName = cn(21 const inputClassName = cn(
25 "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]",22 "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" &&23 size === "sm" && "h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5",
27 "h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5",
28 size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5",24 size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5",
29 props.type === "search" &&25 props.type === "search" &&
30 "[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none",26 "[&::-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";...@@ -5,11 +5,7 @@ import { useRender } from "@base-ui/react/use-render";
55
6import { cn } from "~/lib/utils";6import { cn } from "~/lib/utils";
77
8function Label({8function Label({ className, render, ...props }: useRender.ComponentProps<"label">) {
9 className,
10 render,
11 ...props
12}: useRender.ComponentProps<"label">) {
13 const defaultProps = {9 const defaultProps = {
14 className: cn(10 className: cn(
15 "inline-flex items-center gap-2 text-base/4.5 sm:text-sm/4 font-medium text-foreground",11 "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;...@@ -12,17 +12,9 @@ const Menu = MenuPrimitive.Root;
1212
13const MenuPortal = MenuPrimitive.Portal;13const MenuPortal = MenuPrimitive.Portal;
1414
15function MenuTrigger({15function MenuTrigger({ className, children, ...props }: MenuPrimitive.Trigger.Props) {
16 className,
17 children,
18 ...props
19}: MenuPrimitive.Trigger.Props) {
20 return (16 return (
21 <MenuPrimitive.Trigger17 <MenuPrimitive.Trigger className={className} data-slot="menu-trigger" {...props}>
22 className={className}
23 data-slot="menu-trigger"
24 {...props}
25 >
26 {children}18 {children}
27 </MenuPrimitive.Trigger>19 </MenuPrimitive.Trigger>
28 );20 );
...@@ -63,9 +55,7 @@ function MenuPopup({...@@ -63,9 +55,7 @@ function MenuPopup({
63 data-slot="menu-popup"55 data-slot="menu-popup"
64 {...props}56 {...props}
65 >57 >
66 <div className="max-h-(--available-height) w-full overflow-y-auto p-1">58 <div className="max-h-(--available-height) w-full overflow-y-auto p-1">{children}</div>
67 {children}
68 </div>
69 </MenuPrimitive.Popup>59 </MenuPrimitive.Popup>
70 </MenuPrimitive.Positioner>60 </MenuPrimitive.Positioner>
71 </MenuPrimitive.Portal>61 </MenuPrimitive.Portal>
...@@ -113,9 +103,7 @@ function MenuCheckboxItem({...@@ -113,9 +103,7 @@ function MenuCheckboxItem({
113 checked={checked}103 checked={checked}
114 className={cn(104 className={cn(
115 "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",105 "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"106 variant === "switch" ? "grid-cols-[1fr_auto] gap-4 pe-1.5" : "grid-cols-[1rem_1fr] pe-4",
117 ? "grid-cols-[1fr_auto] gap-4 pe-1.5"
118 : "grid-cols-[1rem_1fr] pe-4",
119 className,107 className,
120 )}108 )}
121 data-slot="menu-checkbox-item"109 data-slot="menu-checkbox-item"
...@@ -159,11 +147,7 @@ function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) {...@@ -159,11 +147,7 @@ function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) {
159 return <MenuPrimitive.RadioGroup data-slot="menu-radio-group" {...props} />;147 return <MenuPrimitive.RadioGroup data-slot="menu-radio-group" {...props} />;
160}148}
161149
162function MenuRadioItem({150function MenuRadioItem({ className, children, ...props }: MenuPrimitive.RadioItem.Props) {
163 className,
164 children,
165 ...props
166}: MenuPrimitive.RadioItem.Props) {
167 return (151 return (
168 <MenuPrimitive.RadioItem152 <MenuPrimitive.RadioItem
169 className={cn(153 className={cn(
apps/web/src/components/ui/popover.tsx+3-14
...@@ -8,17 +8,9 @@ const PopoverCreateHandle = PopoverPrimitive.createHandle;...@@ -8,17 +8,9 @@ const PopoverCreateHandle = PopoverPrimitive.createHandle;
88
9const Popover = PopoverPrimitive.Root;9const Popover = PopoverPrimitive.Root;
1010
11function PopoverTrigger({11function PopoverTrigger({ className, children, ...props }: PopoverPrimitive.Trigger.Props) {
12 className,
13 children,
14 ...props
15}: PopoverPrimitive.Trigger.Props) {
16 return (12 return (
17 <PopoverPrimitive.Trigger13 <PopoverPrimitive.Trigger className={className} data-slot="popover-trigger" {...props}>
18 className={className}
19 data-slot="popover-trigger"
20 {...props}
21 >
22 {children}14 {children}
23 </PopoverPrimitive.Trigger>15 </PopoverPrimitive.Trigger>
24 );16 );
...@@ -94,10 +86,7 @@ function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {...@@ -94,10 +86,7 @@ function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
94 );86 );
95}87}
9688
97function PopoverDescription({89function PopoverDescription({ className, ...props }: PopoverPrimitive.Description.Props) {
98 className,
99 ...props
100}: PopoverPrimitive.Description.Props) {
101 return (90 return (
102 <PopoverPrimitive.Description91 <PopoverPrimitive.Description
103 className={cn("text-muted-foreground text-sm", className)}92 className={cn("text-muted-foreground text-sm", className)}
apps/web/src/components/ui/scroll-area.tsx+2-6
...@@ -15,17 +15,13 @@ function ScrollArea({...@@ -15,17 +15,13 @@ function ScrollArea({
15 scrollbarGutter?: boolean;15 scrollbarGutter?: boolean;
16}) {16}) {
17 return (17 return (
18 <ScrollAreaPrimitive.Root18 <ScrollAreaPrimitive.Root className={cn("size-full min-h-0", className)} {...props}>
19 className={cn("size-full min-h-0", className)}
20 {...props}
21 >
22 <ScrollAreaPrimitive.Viewport19 <ScrollAreaPrimitive.Viewport
23 className={cn(20 className={cn(
24 "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",21 "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",
25 scrollFade &&22 scrollFade &&
26 "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]",23 "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 &&24 scrollbarGutter && "data-has-overflow-y:pe-2.5 data-has-overflow-x:pb-2.5",
28 "data-has-overflow-y:pe-2.5 data-has-overflow-x:pb-2.5",
29 )}25 )}
30 data-slot="scroll-area-viewport"26 data-slot="scroll-area-viewport"
31 >27 >
apps/web/src/components/ui/separator.tsx+1-5
...@@ -2,11 +2,7 @@ import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";...@@ -2,11 +2,7 @@ import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
22
3import { cn } from "~/lib/utils";3import { cn } from "~/lib/utils";
44
5function Separator({5function Separator({ className, orientation = "horizontal", ...props }: SeparatorPrimitive.Props) {
6 className,
7 orientation = "horizontal",
8 ...props
9}: SeparatorPrimitive.Props) {
10 return (6 return (
11 <SeparatorPrimitive7 <SeparatorPrimitive
12 className={cn(8 className={cn(
apps/web/src/components/ui/sheet.tsx+2-8
...@@ -144,20 +144,14 @@ function SheetFooter({...@@ -144,20 +144,14 @@ function SheetFooter({
144function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {144function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
145 return (145 return (
146 <SheetPrimitive.Title146 <SheetPrimitive.Title
147 className={cn(147 className={cn("font-heading font-semibold text-xl leading-none", className)}
148 "font-heading font-semibold text-xl leading-none",
149 className,
150 )}
151 data-slot="sheet-title"148 data-slot="sheet-title"
152 {...props}149 {...props}
153 />150 />
154 );151 );
155}152}
156153
157function SheetDescription({154function SheetDescription({ className, ...props }: SheetPrimitive.Description.Props) {
158 className,
159 ...props
160}: SheetPrimitive.Description.Props) {
161 return (155 return (
162 <SheetPrimitive.Description156 <SheetPrimitive.Description
163 className={cn("text-muted-foreground text-sm", className)}157 className={cn("text-muted-foreground text-sm", className)}
apps/web/src/components/ui/spinner.tsx+1-4
...@@ -1,10 +1,7 @@...@@ -1,10 +1,7 @@
1import { Loader2Icon } from "lucide-react";1import { Loader2Icon } from "lucide-react";
2import { cn } from "~/lib/utils";2import { cn } from "~/lib/utils";
33
4function Spinner({4function Spinner({ className, ...props }: React.ComponentProps<typeof Loader2Icon>) {
5 className,
6 ...props
7}: React.ComponentProps<typeof Loader2Icon>) {
8 return (5 return (
9 <Loader2Icon6 <Loader2Icon
10 aria-label="Loading"7 aria-label="Loading"
apps/web/src/components/ui/textarea.tsx+2-8
...@@ -11,12 +11,7 @@ type TextareaProps = React.ComponentProps<"textarea"> & {...@@ -11,12 +11,7 @@ type TextareaProps = React.ComponentProps<"textarea"> & {
11 unstyled?: boolean;11 unstyled?: boolean;
12};12};
1313
14function Textarea({14function Textarea({ className, size = "default", unstyled = false, ...props }: TextareaProps) {
15 className,
16 size = "default",
17 unstyled = false,
18 ...props
19}: TextareaProps) {
20 return (15 return (
21 <span16 <span
22 className={17 className={
...@@ -36,8 +31,7 @@ function Textarea({...@@ -36,8 +31,7 @@ function Textarea({
36 "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",31 "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",
37 size === "sm" &&32 size === "sm" &&
38 "min-h-16.5 px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1)-1px)] max-sm:min-h-19.5",33 "min-h-16.5 px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1)-1px)] max-sm:min-h-19.5",
39 size === "lg" &&34 size === "lg" && "min-h-18.5 py-[calc(--spacing(2)-1px)] max-sm:min-h-21.5",
40 "min-h-18.5 py-[calc(--spacing(2)-1px)] max-sm:min-h-21.5",
41 )}35 )}
42 data-slot="textarea"36 data-slot="textarea"
43 {...mergeProps(defaultProps, props)}37 {...mergeProps(defaultProps, props)}
apps/web/src/components/ui/toast.tsx+8-29
...@@ -35,11 +35,7 @@ interface ToastProviderProps extends Toast.Provider.Props {...@@ -35,11 +35,7 @@ interface ToastProviderProps extends Toast.Provider.Props {
35 position?: ToastPosition;35 position?: ToastPosition;
36}36}
3737
38function ToastProvider({38function ToastProvider({ children, position = "top-right", ...props }: ToastProviderProps) {
39 children,
40 position = "top-right",
41 ...props
42}: ToastProviderProps) {
43 return (39 return (
44 <Toast.Provider toastManager={toastManager} {...props}>40 <Toast.Provider toastManager={toastManager} {...props}>
45 {children}41 {children}
...@@ -69,9 +65,7 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {...@@ -69,9 +65,7 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
69 data-slot="toast-viewport"65 data-slot="toast-viewport"
70 >66 >
71 {toasts.map((toast) => {67 {toasts.map((toast) => {
72 const Icon = toast.type68 const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null;
73 ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]
74 : null;
7569
76 return (70 return (
77 <Toast.Root71 <Toast.Root
...@@ -139,10 +133,7 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {...@@ -139,10 +133,7 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
139 )}133 )}
140134
141 <div className="flex flex-col gap-0.5">135 <div className="flex flex-col gap-0.5">
142 <Toast.Title136 <Toast.Title className="font-medium" data-slot="toast-title" />
143 className="font-medium"
144 data-slot="toast-title"
145 />
146 <Toast.Description137 <Toast.Description
147 className="text-muted-foreground"138 className="text-muted-foreground"
148 data-slot="toast-description"139 data-slot="toast-description"
...@@ -150,10 +141,7 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {...@@ -150,10 +141,7 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
150 </div>141 </div>
151 </div>142 </div>
152 {toast.actionProps && (143 {toast.actionProps && (
153 <Toast.Action144 <Toast.Action className={buttonVariants({ size: "xs" })} data-slot="toast-action">
154 className={buttonVariants({ size: "xs" })}
155 data-slot="toast-action"
156 >
157 {toast.actionProps.children}145 {toast.actionProps.children}
158 </Toast.Action>146 </Toast.Action>
159 )}147 )}
...@@ -180,16 +168,10 @@ function AnchoredToasts() {...@@ -180,16 +168,10 @@ function AnchoredToasts() {
180168
181 return (169 return (
182 <Toast.Portal data-slot="toast-portal-anchored">170 <Toast.Portal data-slot="toast-portal-anchored">
183 <Toast.Viewport171 <Toast.Viewport className="outline-none" data-slot="toast-viewport-anchored">
184 className="outline-none"
185 data-slot="toast-viewport-anchored"
186 >
187 {toasts.map((toast) => {172 {toasts.map((toast) => {
188 const Icon = toast.type173 const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null;
189 ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]174 const tooltipStyle = (toast.data as { tooltipStyle?: boolean })?.tooltipStyle ?? false;
190 : null;
191 const tooltipStyle =
192 (toast.data as { tooltipStyle?: boolean })?.tooltipStyle ?? false;
193 const positionerProps = toast.positionerProps;175 const positionerProps = toast.positionerProps;
194176
195 if (!positionerProps?.anchor) {177 if (!positionerProps?.anchor) {
...@@ -231,10 +213,7 @@ function AnchoredToasts() {...@@ -231,10 +213,7 @@ function AnchoredToasts() {
231 )}213 )}
232214
233 <div className="flex flex-col gap-0.5">215 <div className="flex flex-col gap-0.5">
234 <Toast.Title216 <Toast.Title className="font-medium" data-slot="toast-title" />
235 className="font-medium"
236 data-slot="toast-title"
237 />
238 <Toast.Description217 <Toast.Description
239 className="text-muted-foreground"218 className="text-muted-foreground"
240 data-slot="toast-description"219 data-slot="toast-description"
apps/web/src/persistenceSchema.test.ts+4-12
...@@ -55,9 +55,7 @@ describe("hydratePersistedState", () => {...@@ -55,9 +55,7 @@ describe("hydratePersistedState", () => {
55 expect(hydrated?.threads[0]?.terminalGroups).toEqual([55 expect(hydrated?.threads[0]?.terminalGroups).toEqual([
56 { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID] },56 { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID] },
57 ]);57 ]);
58 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(58 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`);
59 `group-${DEFAULT_THREAD_TERMINAL_ID}`,
60 );
61 expect(hydrated?.threads[0]?.messages[0]?.streaming).toBe(false);59 expect(hydrated?.threads[0]?.messages[0]?.streaming).toBe(false);
62 expect(hydrated?.runtimeMode).toBe("full-access");60 expect(hydrated?.runtimeMode).toBe("full-access");
63 });61 });
...@@ -105,9 +103,7 @@ describe("hydratePersistedState", () => {...@@ -105,9 +103,7 @@ describe("hydratePersistedState", () => {
105 expect(hydrated?.threads[0]?.terminalGroups).toEqual([103 expect(hydrated?.threads[0]?.terminalGroups).toEqual([
106 { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID] },104 { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID] },
107 ]);105 ]);
108 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(106 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`);
109 `group-${DEFAULT_THREAD_TERMINAL_ID}`,
110 );
111 expect(hydrated?.activeThreadId).toBe("t-1");107 expect(hydrated?.activeThreadId).toBe("t-1");
112 expect(hydrated?.runtimeMode).toBe("full-access");108 expect(hydrated?.runtimeMode).toBe("full-access");
113 });109 });
...@@ -221,9 +217,7 @@ describe("hydratePersistedState", () => {...@@ -221,9 +217,7 @@ describe("hydratePersistedState", () => {
221 },217 },
222 { id: "group-term-3", terminalIds: ["term-3"] },218 { id: "group-term-3", terminalIds: ["term-3"] },
223 ]);219 ]);
224 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(220 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`);
225 `group-${DEFAULT_THREAD_TERMINAL_ID}`,
226 );
227 });221 });
228222
229 it("defaults terminalHeight when hydrating v5 payloads", () => {223 it("defaults terminalHeight when hydrating v5 payloads", () => {
...@@ -261,9 +255,7 @@ describe("hydratePersistedState", () => {...@@ -261,9 +255,7 @@ describe("hydratePersistedState", () => {
261 expect(hydrated?.threads[0]?.terminalGroups).toEqual([255 expect(hydrated?.threads[0]?.terminalGroups).toEqual([
262 { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID] },256 { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID] },
263 ]);257 ]);
264 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(258 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`);
265 `group-${DEFAULT_THREAD_TERMINAL_ID}`,
266 );
267 });259 });
268});260});
269261
apps/web/src/persistenceSchema.ts+11-8
...@@ -52,9 +52,7 @@ const persistedThreadSchema = z.object({...@@ -52,9 +52,7 @@ const persistedThreadSchema = z.object({
52 title: z.string().min(1),52 title: z.string().min(1),
53 model: z.string().min(1),53 model: z.string().min(1),
54 terminalOpen: z.boolean().default(false),54 terminalOpen: z.boolean().default(false),
55 terminalHeight: z.number().int().min(120).max(4_096).default(55 terminalHeight: z.number().int().min(120).max(4_096).default(DEFAULT_THREAD_TERMINAL_HEIGHT),
56 DEFAULT_THREAD_TERMINAL_HEIGHT,
57 ),
58 terminalIds: z.array(z.string().trim().min(1)).default([DEFAULT_THREAD_TERMINAL_ID]),56 terminalIds: z.array(z.string().trim().min(1)).default([DEFAULT_THREAD_TERMINAL_ID]),
59 activeTerminalId: z.string().trim().min(1).default(DEFAULT_THREAD_TERMINAL_ID),57 activeTerminalId: z.string().trim().min(1).default(DEFAULT_THREAD_TERMINAL_ID),
60 terminalGroups: z.array(persistedTerminalGroupSchema).default([]),58 terminalGroups: z.array(persistedTerminalGroupSchema).default([]),
...@@ -149,9 +147,10 @@ function hydrateThread(...@@ -149,9 +147,10 @@ function hydrateThread(
149 thread: z.infer<typeof persistedThreadSchema>,147 thread: z.infer<typeof persistedThreadSchema>,
150 isLegacyPayload: boolean,148 isLegacyPayload: boolean,
151): Thread {149): Thread {
152 const terminalIds = [...new Set(thread.terminalIds.map((id) => id.trim()).filter((id) => id.length > 0))];150 const terminalIds = [
153 const safeTerminalIds =151 ...new Set(thread.terminalIds.map((id) => id.trim()).filter((id) => id.length > 0)),
154 terminalIds.length > 0 ? terminalIds : [DEFAULT_THREAD_TERMINAL_ID];152 ];
153 const safeTerminalIds = terminalIds.length > 0 ? terminalIds : [DEFAULT_THREAD_TERMINAL_ID];
155 const activeTerminalId = safeTerminalIds.includes(thread.activeTerminalId)154 const activeTerminalId = safeTerminalIds.includes(thread.activeTerminalId)
156 ? thread.activeTerminalId155 ? thread.activeTerminalId
157 : (safeTerminalIds[0] ?? DEFAULT_THREAD_TERMINAL_ID);156 : (safeTerminalIds[0] ?? DEFAULT_THREAD_TERMINAL_ID);
...@@ -197,7 +196,9 @@ function hydrateThread(...@@ -197,7 +196,9 @@ function hydrateThread(
197196
198 if (normalizedGroups.length === 0 && thread.terminalLayout === "split") {197 if (normalizedGroups.length === 0 && thread.terminalLayout === "split") {
199 const splitTerminalIds = [198 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 ),
201 ].filter((terminalId) => safeTerminalIdSet.has(terminalId));202 ].filter((terminalId) => safeTerminalIdSet.has(terminalId));
202 if (splitTerminalIds.length >= 2) {203 if (splitTerminalIds.length >= 2) {
203 const splitGroupTerminalIds = splitTerminalIds.slice(0, 2);204 const splitGroupTerminalIds = splitTerminalIds.slice(0, 2);
...@@ -228,7 +229,9 @@ function hydrateThread(...@@ -228,7 +229,9 @@ function hydrateThread(
228 const activeGroupIndex =229 const activeGroupIndex =
229 activeGroupIndexFromId >= 0230 activeGroupIndexFromId >= 0
230 ? activeGroupIndexFromId231 ? activeGroupIndexFromId
231 : (activeGroupIndexFromTerminal >= 0 ? activeGroupIndexFromTerminal : 0);232 : activeGroupIndexFromTerminal >= 0
233 ? activeGroupIndexFromTerminal
234 : 0;
232 const activeTerminalGroupId =235 const activeTerminalGroupId =
233 normalizedGroups[activeGroupIndex]?.id ??236 normalizedGroups[activeGroupIndex]?.id ??
234 normalizedGroups[0]?.id ??237 normalizedGroups[0]?.id ??
apps/web/src/store.test.ts+1-5
...@@ -244,11 +244,7 @@ describe("store reducer thread continuity", () => {...@@ -244,11 +244,7 @@ describe("store reducer thread continuity", () => {
244 terminalId: "term-3",244 terminalId: "term-3",
245 });245 });
246246
247 expect(next.threads[0]?.terminalIds).toEqual([247 expect(next.threads[0]?.terminalIds).toEqual([DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3"]);
248 DEFAULT_THREAD_TERMINAL_ID,
249 "term-2",
250 "term-3",
251 ]);
252 expect(next.threads[0]?.terminalGroups).toEqual([248 expect(next.threads[0]?.terminalGroups).toEqual([
253 {249 {
254 id: `group-${DEFAULT_THREAD_TERMINAL_ID}`,250 id: `group-${DEFAULT_THREAD_TERMINAL_ID}`,
apps/web/src/store.ts+12-27
...@@ -101,17 +101,12 @@ function readPersistedState(): AppState {...@@ -101,17 +101,12 @@ function readPersistedState(): AppState {
101101
102 try {102 try {
103 const rawCurrent = window.localStorage.getItem(PERSISTED_STATE_KEY);103 const rawCurrent = window.localStorage.getItem(PERSISTED_STATE_KEY);
104 const legacyValues = LEGACY_PERSISTED_STATE_KEYS.map((key) =>104 const legacyValues = LEGACY_PERSISTED_STATE_KEYS.map((key) => window.localStorage.getItem(key));
105 window.localStorage.getItem(key),
106 );
107 const rawLegacy = legacyValues.find((value) => value !== null) ?? null;105 const rawLegacy = legacyValues.find((value) => value !== null) ?? null;
108 const raw = rawCurrent ?? rawLegacy;106 const raw = rawCurrent ?? rawLegacy;
109 if (!raw) return initialState;107 if (!raw) return initialState;
110 const rawCodethingV1 = window.localStorage.getItem("codething:renderer-state:v1");108 const rawCodethingV1 = window.localStorage.getItem("codething:renderer-state:v1");
111 const hydrated = hydratePersistedState(109 const hydrated = hydratePersistedState(raw, !rawCurrent && raw === rawCodethingV1);
112 raw,
113 !rawCurrent && raw === rawCodethingV1,
114 );
115 if (!hydrated) return initialState;110 if (!hydrated) return initialState;
116111
117 return { ...hydrated, diffOpen: false };112 return { ...hydrated, diffOpen: false };
...@@ -183,10 +178,7 @@ function assignUniqueGroupId(groupId: string, usedGroupIds: Set<string>): string...@@ -183,10 +178,7 @@ function assignUniqueGroupId(groupId: string, usedGroupIds: Set<string>): string
183 return uniqueGroupId;178 return uniqueGroupId;
184}179}
185180
186function normalizeTerminalGroups(181function normalizeTerminalGroups(thread: Thread, terminalIds: string[]): ThreadTerminalGroup[] {
187 thread: Thread,
188 terminalIds: string[],
189): ThreadTerminalGroup[] {
190 const validTerminalIdSet = new Set(terminalIds);182 const validTerminalIdSet = new Set(terminalIds);
191 const assignedTerminalIds = new Set<string>();183 const assignedTerminalIds = new Set<string>();
192 const usedGroupIds = new Set<string>();184 const usedGroupIds = new Set<string>();
...@@ -248,14 +240,13 @@ function normalizeThreadTerminals(thread: Thread): Thread {...@@ -248,14 +240,13 @@ function normalizeThreadTerminals(thread: Thread): Thread {
248 const activeGroupIndexFromId = terminalGroups.findIndex(240 const activeGroupIndexFromId = terminalGroups.findIndex(
249 (group) => group.id === thread.activeTerminalGroupId,241 (group) => group.id === thread.activeTerminalGroupId,
250 );242 );
251 const activeGroupIndexFromTerminal = findGroupIndexByTerminalId(243 const activeGroupIndexFromTerminal = findGroupIndexByTerminalId(terminalGroups, activeTerminalId);
252 terminalGroups,
253 activeTerminalId,
254 );
255 const activeGroupIndex =244 const activeGroupIndex =
256 activeGroupIndexFromId >= 0245 activeGroupIndexFromId >= 0
257 ? activeGroupIndexFromId246 ? activeGroupIndexFromId
258 : (activeGroupIndexFromTerminal >= 0 ? activeGroupIndexFromTerminal : 0);247 : activeGroupIndexFromTerminal >= 0
248 ? activeGroupIndexFromTerminal
249 : 0;
259 const activeTerminalGroupId =250 const activeTerminalGroupId =
260 terminalGroups[activeGroupIndex]?.id ??251 terminalGroups[activeGroupIndex]?.id ??
261 terminalGroups[0]?.id ??252 terminalGroups[0]?.id ??
...@@ -301,9 +292,9 @@ function closeThreadTerminal(thread: Thread, terminalId: string): Thread {...@@ -301,9 +292,9 @@ function closeThreadTerminal(thread: Thread, terminalId: string): Thread {
301 const closedTerminalGroupIndex = closedTerminalGroup292 const closedTerminalGroupIndex = closedTerminalGroup
302 ? closedTerminalGroup.terminalIds.indexOf(terminalId)293 ? closedTerminalGroup.terminalIds.indexOf(terminalId)
303 : -1;294 : -1;
304 const remainingTerminalsInClosedGroup = (295 const remainingTerminalsInClosedGroup = (closedTerminalGroup?.terminalIds ?? []).filter(
305 closedTerminalGroup?.terminalIds ?? []296 (id) => id !== terminalId,
306 ).filter((id) => id !== terminalId);297 );
307 const nextActiveTerminalId =298 const nextActiveTerminalId =
308 thread.activeTerminalId === terminalId299 thread.activeTerminalId === terminalId
309 ? (remainingTerminalsInClosedGroup[300 ? (remainingTerminalsInClosedGroup[
...@@ -562,10 +553,7 @@ export function reducer(state: AppState, action: Action): AppState {...@@ -562,10 +553,7 @@ export function reducer(state: AppState, action: Action): AppState {
562 activeGroupIndex = terminalGroups.length - 1;553 activeGroupIndex = terminalGroups.length - 1;
563 }554 }
564555
565 const existingGroupIndex = findGroupIndexByTerminalId(556 const existingGroupIndex = findGroupIndexByTerminalId(terminalGroups, action.terminalId);
566 terminalGroups,
567 action.terminalId,
568 );
569 if (existingGroupIndex >= 0) {557 if (existingGroupIndex >= 0) {
570 terminalGroups[existingGroupIndex]!.terminalIds = terminalGroups[558 terminalGroups[existingGroupIndex]!.terminalIds = terminalGroups[
571 existingGroupIndex559 existingGroupIndex
...@@ -674,10 +662,7 @@ export function reducer(state: AppState, action: Action): AppState {...@@ -674,10 +662,7 @@ export function reducer(state: AppState, action: Action): AppState {
674 threads: updateThread(state.threads, action.event.threadId, (thread) => {662 threads: updateThread(state.threads, action.event.threadId, (thread) => {
675 const normalizedThread = normalizeThreadTerminals(thread);663 const normalizedThread = normalizeThreadTerminals(thread);
676 const runningTerminalIdSet = new Set(normalizedThread.runningTerminalIds);664 const runningTerminalIdSet = new Set(normalizedThread.runningTerminalIds);
677 if (665 if (action.event.type === "started" || action.event.type === "restarted") {
678 action.event.type === "started" ||
679 action.event.type === "restarted"
680 ) {
681 runningTerminalIdSet.delete(action.event.terminalId);666 runningTerminalIdSet.delete(action.event.terminalId);
682 } else if (action.event.type === "activity") {667 } else if (action.event.type === "activity") {
683 if (action.event.hasRunningSubprocess) {668 if (action.event.hasRunningSubprocess) {
apps/web/src/terminal-links.test.ts+1-4
...@@ -57,10 +57,7 @@ describe("resolvePathLinkTarget", () => {...@@ -57,10 +57,7 @@ describe("resolvePathLinkTarget", () => {
5757
58 it("keeps absolute paths unchanged", () => {58 it("keeps absolute paths unchanged", () => {
59 expect(59 expect(
60 resolvePathLinkTarget(60 resolvePathLinkTarget("/Users/julius/project/src/main.ts:12", "/Users/julius/project"),
61 "/Users/julius/project/src/main.ts:12",
62 "/Users/julius/project",
63 ),
64 ).toBe("/Users/julius/project/src/main.ts:12");61 ).toBe("/Users/julius/project/src/main.ts:12");
65 });62 });
66});63});
apps/web/src/terminal-shortcuts.test.ts+13-38
...@@ -19,70 +19,45 @@ function event(overrides: Partial<ShortcutEventLike> = {}): ShortcutEventLike {...@@ -19,70 +19,45 @@ function event(overrides: Partial<ShortcutEventLike> = {}): ShortcutEventLike {
1919
20describe("isTerminalToggleShortcut", () => {20describe("isTerminalToggleShortcut", () => {
21 it("matches Cmd+J on macOS", () => {21 it("matches Cmd+J on macOS", () => {
22 expect(22 expect(isTerminalToggleShortcut(event({ metaKey: true }), "MacIntel")).toBe(true);
23 isTerminalToggleShortcut(event({ metaKey: true }), "MacIntel"),
24 ).toBe(true);
25 });23 });
2624
27 it("matches Ctrl+J on non-macOS", () => {25 it("matches Ctrl+J on non-macOS", () => {
28 expect(26 expect(isTerminalToggleShortcut(event({ ctrlKey: true }), "Win32")).toBe(true);
29 isTerminalToggleShortcut(event({ ctrlKey: true }), "Win32"),
30 ).toBe(true);
31 });27 });
3228
33 it("rejects wrong modifiers", () => {29 it("rejects wrong modifiers", () => {
34 expect(30 expect(isTerminalToggleShortcut(event({ ctrlKey: true, shiftKey: true }), "Win32")).toBe(false);
35 isTerminalToggleShortcut(31 expect(isTerminalToggleShortcut(event({ metaKey: true, altKey: true }), "MacIntel")).toBe(
36 event({ ctrlKey: true, shiftKey: true }),32 false,
37 "Win32",33 );
38 ),
39 ).toBe(false);
40 expect(
41 isTerminalToggleShortcut(event({ metaKey: true, altKey: true }), "MacIntel"),
42 ).toBe(false);
43 });34 });
4435
45 it("rejects non-j keys", () => {36 it("rejects non-j keys", () => {
46 expect(37 expect(isTerminalToggleShortcut(event({ key: "k", ctrlKey: true }), "Linux")).toBe(false);
47 isTerminalToggleShortcut(event({ key: "k", ctrlKey: true }), "Linux"),
48 ).toBe(false);
49 });38 });
50});39});
5140
52describe("isTerminalClearShortcut", () => {41describe("isTerminalClearShortcut", () => {
53 it("matches Ctrl+L on all platforms", () => {42 it("matches Ctrl+L on all platforms", () => {
54 expect(43 expect(isTerminalClearShortcut(event({ key: "l", ctrlKey: true }), "Linux")).toBe(true);
55 isTerminalClearShortcut(event({ key: "l", ctrlKey: true }), "Linux"),44 expect(isTerminalClearShortcut(event({ key: "l", ctrlKey: true }), "MacIntel")).toBe(true);
56 ).toBe(true);
57 expect(
58 isTerminalClearShortcut(event({ key: "l", ctrlKey: true }), "MacIntel"),
59 ).toBe(true);
60 });45 });
6146
62 it("matches Cmd+K on macOS", () => {47 it("matches Cmd+K on macOS", () => {
63 expect(48 expect(isTerminalClearShortcut(event({ key: "k", metaKey: true }), "MacIntel")).toBe(true);
64 isTerminalClearShortcut(event({ key: "k", metaKey: true }), "MacIntel"),
65 ).toBe(true);
66 });49 });
6750
68 it("rejects Cmd+K on non-macOS", () => {51 it("rejects Cmd+K on non-macOS", () => {
69 expect(52 expect(isTerminalClearShortcut(event({ key: "k", metaKey: true }), "Win32")).toBe(false);
70 isTerminalClearShortcut(event({ key: "k", metaKey: true }), "Win32"),
71 ).toBe(false);
72 });53 });
7354
74 it("rejects wrong modifiers", () => {55 it("rejects wrong modifiers", () => {
75 expect(56 expect(
76 isTerminalClearShortcut(57 isTerminalClearShortcut(event({ key: "l", ctrlKey: true, shiftKey: true }), "Linux"),
77 event({ key: "l", ctrlKey: true, shiftKey: true }),
78 "Linux",
79 ),
80 ).toBe(false);58 ).toBe(false);
81 expect(59 expect(
82 isTerminalClearShortcut(60 isTerminalClearShortcut(event({ key: "k", metaKey: true, altKey: true }), "MacIntel"),
83 event({ key: "k", metaKey: true, altKey: true }),
84 "MacIntel",
85 ),
86 ).toBe(false);61 ).toBe(false);
87 });62 });
88});63});
apps/web/src/terminal-shortcuts.ts+1-7
...@@ -31,13 +31,7 @@ export function isTerminalClearShortcut(...@@ -31,13 +31,7 @@ export function isTerminalClearShortcut(
31): boolean {31): boolean {
32 const key = event.key.toLowerCase();32 const key = event.key.toLowerCase();
3333
34 if (34 if (key === "l" && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
35 key === "l" &&
36 event.ctrlKey &&
37 !event.metaKey &&
38 !event.altKey &&
39 !event.shiftKey
40 ) {
41 return true;35 return true;
42 }36 }
4337
packages/contracts/src/git.test.ts+1-3
...@@ -47,9 +47,7 @@ describe("git contracts", () => {...@@ -47,9 +47,7 @@ describe("git contracts", () => {
47 it("accepts supported stacked actions", () => {47 it("accepts supported stacked actions", () => {
48 expect(gitStackedActionSchema.parse("commit")).toBe("commit");48 expect(gitStackedActionSchema.parse("commit")).toBe("commit");
49 expect(gitStackedActionSchema.parse("commit_push")).toBe("commit_push");49 expect(gitStackedActionSchema.parse("commit_push")).toBe("commit_push");
50 expect(gitStackedActionSchema.parse("commit_push_pr")).toBe(50 expect(gitStackedActionSchema.parse("commit_push_pr")).toBe("commit_push_pr");
51 "commit_push_pr",
52 );
53 });51 });
5452
55 it("parses stacked action input", () => {53 it("parses stacked action input", () => {
packages/contracts/src/git.ts+3-15
...@@ -100,23 +100,11 @@ export const gitStatusResultSchema = z.object({...@@ -100,23 +100,11 @@ export const gitStatusResultSchema = z.object({
100 .nullable(),100 .nullable(),
101});101});
102102
103export const gitStackedActionSchema = z.enum([103export const gitStackedActionSchema = z.enum(["commit", "commit_push", "commit_push_pr"]);
104 "commit",
105 "commit_push",
106 "commit_push_pr",
107]);
108104
109const gitCommitStepStatusSchema = z.enum(["created", "skipped_no_changes"]);105const gitCommitStepStatusSchema = z.enum(["created", "skipped_no_changes"]);
110const gitPushStepStatusSchema = z.enum([106const gitPushStepStatusSchema = z.enum(["pushed", "skipped_not_requested", "skipped_up_to_date"]);
111 "pushed",107const gitPrStepStatusSchema = z.enum(["created", "opened_existing", "skipped_not_requested"]);
112 "skipped_not_requested",
113 "skipped_up_to_date",
114]);
115const gitPrStepStatusSchema = z.enum([
116 "created",
117 "opened_existing",
118 "skipped_not_requested",
119]);
120108
121export const gitRunStackedActionInputSchema = z.object({109export const gitRunStackedActionInputSchema = z.object({
122 cwd: z.string().trim().min(1),110 cwd: z.string().trim().min(1),
packages/contracts/src/ipc.ts+1-3
...@@ -105,9 +105,7 @@ export interface NativeApi {...@@ -105,9 +105,7 @@ export interface NativeApi {
105 // Stacked action API105 // Stacked action API
106 pull: (input: GitPullInput) => Promise<GitPullResult>;106 pull: (input: GitPullInput) => Promise<GitPullResult>;
107 status: (input: GitStatusInput) => Promise<GitStatusResult>;107 status: (input: GitStatusInput) => Promise<GitStatusResult>;
108 runStackedAction: (108 runStackedAction: (input: GitRunStackedActionInput) => Promise<GitRunStackedActionResult>;
109 input: GitRunStackedActionInput,
110 ) => Promise<GitRunStackedActionResult>;
111 };109 };
112 contextMenu: {110 contextMenu: {
113 show: <T extends string>(111 show: <T extends string>(
packages/contracts/src/terminal.ts+1-6
...@@ -36,12 +36,7 @@ export const terminalCloseInputSchema = terminalThreadInputSchema.extend({...@@ -36,12 +36,7 @@ export const terminalCloseInputSchema = terminalThreadInputSchema.extend({
36 deleteHistory: z.boolean().optional(),36 deleteHistory: z.boolean().optional(),
37});37});
3838
39export const terminalSessionStatusSchema = z.enum([39export const terminalSessionStatusSchema = z.enum(["starting", "running", "exited", "error"]);
40 "starting",
41 "running",
42 "exited",
43 "error",
44]);
4540
46export const terminalSessionSnapshotSchema = z.object({41export const terminalSessionSnapshotSchema = z.object({
47 threadId: z.string().min(1),42 threadId: z.string().min(1),