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