authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-09 01:11:20-08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-02-09 01:11:20-08:00
log70a9a043eecc0da4d11f30416add232b4bf6e0f1
tree4ae28914083e29f210d790175b572d26ffdecee3
parenteef57db955614e346091769bc6d4e80e0068e9b6
signature Signed by PGP key B5690EEEBB952194

oxc stack (#1)

* oxc stacak * kewl * unused * unused

37 files changed, 217 insertions(+), 407 deletions(-)

.oxfmtrc.json created+12
...@@ -0,0 +1,12 @@
1{
2 "$schema": "./node_modules/oxfmt/configuration_schema.json",
3 "ignorePatterns": [
4 ".plans",
5 "dist",
6 "dist-electron",
7 "node_modules",
8 "bun.lock",
9 "*.tsbuildinfo"
10 ],
11 "experimentalSortPackageJson": {}
12}
.oxlintrc.json created+13
...@@ -0,0 +1,13 @@
1{
2 "$schema": "./node_modules/oxlint/configuration_schema.json",
3 "ignorePatterns": ["dist", "dist-electron", "node_modules", "bun.lock", "*.tsbuildinfo"],
4 "plugins": ["eslint", "oxc", "react", "unicorn", "typescript"],
5 "categories": {
6 "correctness": "warn",
7 "suspicious": "warn",
8 "perf": "warn"
9 },
10 "rules": {
11 "react-in-jsx-scope": "off"
12 }
13}
.vscode/extensions.json created+3
...@@ -0,0 +1,3 @@
1{
2 "recommendations": ["oxc.oxc-vscode"]
3}
.vscode/settings.json created+8
...@@ -0,0 +1,8 @@
1{
2 "editor.formatOnSave": true,
3 "editor.defaultFormatter": "oxc.oxc-vscode",
4 "editor.codeActionsOnSave": {
5 "source.fixAll.oxc": "always"
6 },
7 "oxc.unusedDisableDirectives": "warn"
8}
README.md+1
...@@ -49,6 +49,7 @@ Mode changes apply across all threads. Existing live sessions are restarted so o...@@ -49,6 +49,7 @@ Mode changes apply across all threads. Existing live sessions are restarted so o
49- `.github/workflows/ci.yml` runs `bun run lint`, `bun run typecheck`, and `bun run test` on pull requests and pushes to `main`.49- `.github/workflows/ci.yml` runs `bun run lint`, `bun run typecheck`, and `bun run test` on pull requests and pushes to `main`.
5050
51Optional:51Optional:
52
52- `ELECTRON_RENDERER_PORT=5180 bun run dev` if `5173` is already in use.53- `ELECTRON_RENDERER_PORT=5180 bun run dev` if `5173` is already in use.
5354
54## Provider architecture55## Provider architecture
apps/desktop/package.json+1-3
...@@ -4,14 +4,13 @@...@@ -4,14 +4,13 @@
4 "private": true,4 "private": true,
5 "main": "dist-electron/main.js",5 "main": "dist-electron/main.js",
6 "scripts": {6 "scripts": {
7 "dev": "concurrently -k -n BUNDLE,ELECTRON \"bun run dev:bundle\" \"bun run dev:electron\"",7 "dev": "bun run --parallel dev:bundle dev:electron",
8 "dev:bundle": "tsup --watch",8 "dev:bundle": "tsup --watch",
9 "dev:electron": "bun run scripts/dev-electron.mjs",9 "dev:electron": "bun run scripts/dev-electron.mjs",
10 "build": "tsup",10 "build": "tsup",
11 "start": "electron dist-electron/main.js",11 "start": "electron dist-electron/main.js",
12 "postinstall": "electron-rebuild",12 "postinstall": "electron-rebuild",
13 "typecheck": "tsc --noEmit",13 "typecheck": "tsc --noEmit",
14 "lint": "biome check src/",
15 "test": "vitest run",14 "test": "vitest run",
16 "smoke-test": "node scripts/smoke-test.mjs"15 "smoke-test": "node scripts/smoke-test.mjs"
17 },16 },
...@@ -23,7 +22,6 @@...@@ -23,7 +22,6 @@
23 "devDependencies": {22 "devDependencies": {
24 "@electron/rebuild": "^3.7.0",23 "@electron/rebuild": "^3.7.0",
25 "@types/node": "^22.10.2",24 "@types/node": "^22.10.2",
26 "concurrently": "^9.1.2",
27 "electronmon": "^2.0.2",25 "electronmon": "^2.0.2",
28 "tsup": "^8.3.5",26 "tsup": "^8.3.5",
29 "typescript": "^5.7.3",27 "typescript": "^5.7.3",
apps/desktop/scripts/dev-electron.mjs+2-7
...@@ -6,15 +6,10 @@ const port = Number(process.env.ELECTRON_RENDERER_PORT ?? 5173);...@@ -6,15 +6,10 @@ const port = Number(process.env.ELECTRON_RENDERER_PORT ?? 5173);
6const devServerUrl = `http://localhost:${port}`;6const devServerUrl = `http://localhost:${port}`;
77
8await waitOn({8await waitOn({
9 resources: [9 resources: [`tcp:${port}`, "file:dist-electron/main.js", "file:dist-electron/preload.js"],
10 `tcp:${port}`,
11 "file:dist-electron/main.js",
12 "file:dist-electron/preload.js",
13 ],
14});10});
1511
16const command =12const command = process.platform === "win32" ? "electronmon.cmd" : "electronmon";
17 process.platform === "win32" ? "electronmon.cmd" : "electronmon";
18const child = spawn(command, ["dist-electron/main.js"], {13const child = spawn(command, ["dist-electron/main.js"], {
19 stdio: "inherit",14 stdio: "inherit",
20 env: {15 env: {
apps/desktop/src/codexAppServerManager.test.ts+3-9
...@@ -1,9 +1,6 @@...@@ -1,9 +1,6 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
22
3import {3import { classifyCodexStderrLine, normalizeCodexModelSlug } from "./codexAppServerManager";
4 classifyCodexStderrLine,
5 normalizeCodexModelSlug,
6} from "./codexAppServerManager";
74
8describe("classifyCodexStderrLine", () => {5describe("classifyCodexStderrLine", () => {
9 it("ignores empty lines", () => {6 it("ignores empty lines", () => {
...@@ -23,8 +20,7 @@ describe("classifyCodexStderrLine", () => {...@@ -23,8 +20,7 @@ describe("classifyCodexStderrLine", () => {
23 });20 });
2421
25 it("keeps unknown structured errors", () => {22 it("keeps unknown structured errors", () => {
26 const line =23 const line = "2026-02-08T04:24:20.085687Z ERROR codex_core::runtime: unrecoverable failure";
27 "2026-02-08T04:24:20.085687Z ERROR codex_core::runtime: unrecoverable failure";
28 expect(classifyCodexStderrLine(line)).toEqual({24 expect(classifyCodexStderrLine(line)).toEqual({
29 message: line,25 message: line,
30 });26 });
...@@ -45,9 +41,7 @@ describe("normalizeCodexModelSlug", () => {...@@ -45,9 +41,7 @@ describe("normalizeCodexModelSlug", () => {
45 });41 });
4642
47 it("prefers codex id when model differs", () => {43 it("prefers codex id when model differs", () => {
48 expect(normalizeCodexModelSlug("gpt-5.3", "gpt-5.3-codex")).toBe(44 expect(normalizeCodexModelSlug("gpt-5.3", "gpt-5.3-codex")).toBe("gpt-5.3-codex");
49 "gpt-5.3-codex",
50 );
51 });45 });
5246
53 it("keeps non-aliased models as-is", () => {47 it("keeps non-aliased models as-is", () => {
apps/desktop/src/codexAppServerManager.ts+24-89
...@@ -91,9 +91,7 @@ export function normalizeCodexModelSlug(...@@ -91,9 +91,7 @@ export function normalizeCodexModelSlug(
91 return normalized;91 return normalized;
92}92}
9393
94export function classifyCodexStderrLine(94export function classifyCodexStderrLine(rawLine: string): { message: string } | null {
95 rawLine: string,
96): { message: string } | null {
97 const line = rawLine.replaceAll(ANSI_ESCAPE_REGEX, "").trim();95 const line = rawLine.replaceAll(ANSI_ESCAPE_REGEX, "").trim();
98 if (!line) {96 if (!line) {
99 return null;97 return null;
...@@ -106,9 +104,7 @@ export function classifyCodexStderrLine(...@@ -106,9 +104,7 @@ export function classifyCodexStderrLine(
106 return null;104 return null;
107 }105 }
108106
109 const isBenignError = BENIGN_ERROR_LOG_SNIPPETS.some((snippet) =>107 const isBenignError = BENIGN_ERROR_LOG_SNIPPETS.some((snippet) => line.includes(snippet));
110 line.includes(snippet),
111 );
112 if (isBenignError) {108 if (isBenignError) {
113 return null;109 return null;
114 }110 }
...@@ -124,9 +120,7 @@ export interface CodexAppServerManagerEvents {...@@ -124,9 +120,7 @@ export interface CodexAppServerManagerEvents {
124export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEvents> {120export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEvents> {
125 private readonly sessions = new Map<string, CodexSessionContext>();121 private readonly sessions = new Map<string, CodexSessionContext>();
126122
127 async startSession(123 async startSession(input: ProviderSessionStartInput): Promise<ProviderSession> {
128 input: ProviderSessionStartInput,
129 ): Promise<ProviderSession> {
130 const sessionId = randomUUID();124 const sessionId = randomUUID();
131 const now = new Date().toISOString();125 const now = new Date().toISOString();
132126
...@@ -160,11 +154,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -160,11 +154,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
160 this.sessions.set(sessionId, context);154 this.sessions.set(sessionId, context);
161 this.attachProcessListeners(context);155 this.attachProcessListeners(context);
162156
163 this.emitLifecycleEvent(157 this.emitLifecycleEvent(context, "session/connecting", "Starting codex app-server");
164 context,
165 "session/connecting",
166 "Starting codex app-server",
167 );
168158
169 try {159 try {
170 await this.sendRequest(context, "initialize", {160 await this.sendRequest(context, "initialize", {
...@@ -188,10 +178,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -188,10 +178,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
188 experimentalRawEvents: false,178 experimentalRawEvents: false,
189 });179 });
190180
191 const threadId = this.readString(181 const threadId = this.readString(this.readObject(threadStart)?.thread, "id");
192 this.readObject(threadStart)?.thread,
193 "id",
194 );
195 if (!threadId) {182 if (!threadId) {
196 throw new Error("thread/start response did not include a thread id.");183 throw new Error("thread/start response did not include a thread id.");
197 }184 }
...@@ -200,30 +187,21 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -200,30 +187,21 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
200 status: "ready",187 status: "ready",
201 threadId,188 threadId,
202 });189 });
203 this.emitLifecycleEvent(190 this.emitLifecycleEvent(context, "session/ready", `Connected to thread ${threadId}`);
204 context,
205 "session/ready",
206 `Connected to thread ${threadId}`,
207 );
208 return { ...context.session };191 return { ...context.session };
209 } catch (error) {192 } catch (error) {
210 const message =193 const message = error instanceof Error ? error.message : "Failed to start Codex session.";
211 error instanceof Error
212 ? error.message
213 : "Failed to start Codex session.";
214 this.updateSession(context, {194 this.updateSession(context, {
215 status: "error",195 status: "error",
216 lastError: message,196 lastError: message,
217 });197 });
218 this.emitErrorEvent(context, "session/startFailed", message);198 this.emitErrorEvent(context, "session/startFailed", message);
219 this.stopSession(sessionId);199 this.stopSession(sessionId);
220 throw new Error(message);200 throw new Error(message, { cause: error });
221 }201 }
222 }202 }
223203
224 async sendTurn(204 async sendTurn(input: ProviderSendTurnInput): Promise<ProviderTurnStartResult> {
225 input: ProviderSendTurnInput,
226 ): Promise<ProviderTurnStartResult> {
227 const context = this.requireSession(input.sessionId);205 const context = this.requireSession(input.sessionId);
228 if (!context.session.threadId) {206 if (!context.session.threadId) {
229 throw new Error("Session is missing a thread id.");207 throw new Error("Session is missing a thread id.");
...@@ -252,11 +230,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -252,11 +230,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
252 turnStartParams.effort = input.effort;230 turnStartParams.effort = input.effort;
253 }231 }
254232
255 const response = await this.sendRequest(233 const response = await this.sendRequest(context, "turn/start", turnStartParams);
256 context,
257 "turn/start",
258 turnStartParams,
259 );
260234
261 const turn = this.readObject(this.readObject(response), "turn");235 const turn = this.readObject(this.readObject(response), "turn");
262 const turnId = this.readString(turn, "id");236 const turnId = this.readString(turn, "id");
...@@ -498,10 +472,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -498,10 +472,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
498 });472 });
499473
500 if (notification.method === "thread/started") {474 if (notification.method === "thread/started") {
501 const threadId = this.readString(475 const threadId = this.readString(this.readObject(notification.params)?.thread, "id");
502 this.readObject(notification.params)?.thread,
503 "id",
504 );
505 if (threadId) {476 if (threadId) {
506 this.updateSession(context, { threadId });477 this.updateSession(context, { threadId });
507 }478 }
...@@ -509,10 +480,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -509,10 +480,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
509 }480 }
510481
511 if (notification.method === "turn/started") {482 if (notification.method === "turn/started") {
512 const turnId = this.readString(483 const turnId = this.readString(this.readObject(notification.params)?.turn, "id");
513 this.readObject(notification.params)?.turn,
514 "id",
515 );
516 this.updateSession(context, {484 this.updateSession(context, {
517 status: "running",485 status: "running",
518 activeTurnId: turnId,486 activeTurnId: turnId,
...@@ -523,10 +491,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -523,10 +491,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
523 if (notification.method === "turn/completed") {491 if (notification.method === "turn/completed") {
524 const turn = this.readObject(notification.params, "turn");492 const turn = this.readObject(notification.params, "turn");
525 const status = this.readString(turn, "status");493 const status = this.readString(turn, "status");
526 const errorMessage = this.readString(494 const errorMessage = this.readString(this.readObject(turn, "error"), "message");
527 this.readObject(turn, "error"),
528 "message",
529 );
530 this.updateSession(context, {495 this.updateSession(context, {
531 status: status === "failed" ? "error" : "ready",496 status: status === "failed" ? "error" : "ready",
532 activeTurnId: undefined,497 activeTurnId: undefined,
...@@ -536,10 +501,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -536,10 +501,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
536 }501 }
537502
538 if (notification.method === "error") {503 if (notification.method === "error") {
539 const message = this.readString(504 const message = this.readString(this.readObject(notification.params)?.error, "message");
540 this.readObject(notification.params)?.error,
541 "message",
542 );
543 const willRetry = this.readBoolean(notification.params, "willRetry");505 const willRetry = this.readBoolean(notification.params, "willRetry");
544506
545 this.updateSession(context, {507 this.updateSession(context, {
...@@ -549,10 +511,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -549,10 +511,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
549 }511 }
550 }512 }
551513
552 private handleServerRequest(514 private handleServerRequest(context: CodexSessionContext, request: JsonRpcRequest): void {
553 context: CodexSessionContext,
554 request: JsonRpcRequest,
555 ): void {
556 const route = this.readRouteFields(request.params);515 const route = this.readRouteFields(request.params);
557 const requestKind = this.requestKindForMethod(request.method);516 const requestKind = this.requestKindForMethod(request.method);
558 let requestId: string | undefined;517 let requestId: string | undefined;
...@@ -609,10 +568,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -609,10 +568,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
609 });568 });
610 }569 }
611570
612 private handleResponse(571 private handleResponse(context: CodexSessionContext, response: JsonRpcResponse): void {
613 context: CodexSessionContext,
614 response: JsonRpcResponse,
615 ): void {
616 const key = String(response.id);572 const key = String(response.id);
617 const pending = context.pending.get(key);573 const pending = context.pending.get(key);
618 if (!pending) {574 if (!pending) {
...@@ -623,11 +579,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -623,11 +579,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
623 context.pending.delete(key);579 context.pending.delete(key);
624580
625 if (response.error?.message) {581 if (response.error?.message) {
626 pending.reject(582 pending.reject(new Error(`${pending.method} failed: ${String(response.error.message)}`));
627 new Error(
628 `${pending.method} failed: ${String(response.error.message)}`,
629 ),
630 );
631 return;583 return;
632 }584 }
633585
...@@ -674,11 +626,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -674,11 +626,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
674 context.child.stdin.write(`${encoded}\n`);626 context.child.stdin.write(`${encoded}\n`);
675 }627 }
676628
677 private emitLifecycleEvent(629 private emitLifecycleEvent(context: CodexSessionContext, method: string, message: string): void {
678 context: CodexSessionContext,
679 method: string,
680 message: string,
681 ): void {
682 this.emitEvent({630 this.emitEvent({
683 id: randomUUID(),631 id: randomUUID(),
684 kind: "session",632 kind: "session",
...@@ -690,11 +638,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -690,11 +638,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
690 });638 });
691 }639 }
692640
693 private emitErrorEvent(641 private emitErrorEvent(context: CodexSessionContext, method: string, message: string): void {
694 context: CodexSessionContext,
695 method: string,
696 message: string,
697 ): void {
698 this.emitEvent({642 this.emitEvent({
699 id: randomUUID(),643 id: randomUUID(),
700 kind: "error",644 kind: "error",
...@@ -710,10 +654,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -710,10 +654,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
710 this.emit("event", event);654 this.emit("event", event);
711 }655 }
712656
713 private updateSession(657 private updateSession(context: CodexSessionContext, updates: Partial<ProviderSession>): void {
714 context: CodexSessionContext,
715 updates: Partial<ProviderSession>,
716 ): void {
717 context.session = {658 context.session = {
718 ...context.session,659 ...context.session,
719 ...updates,660 ...updates,
...@@ -762,8 +703,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -762,8 +703,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
762 }703 }
763704
764 const candidate = value as Record<string, unknown>;705 const candidate = value as Record<string, unknown>;
765 const hasId =706 const hasId = typeof candidate.id === "string" || typeof candidate.id === "number";
766 typeof candidate.id === "string" || typeof candidate.id === "number";
767 const hasMethod = typeof candidate.method === "string";707 const hasMethod = typeof candidate.method === "string";
768 return hasId && !hasMethod;708 return hasId && !hasMethod;
769 }709 }
...@@ -783,11 +723,9 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -783,11 +723,9 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
783 this.readString(params, "threadId") ??723 this.readString(params, "threadId") ??
784 this.readString(this.readObject(params, "thread"), "id");724 this.readString(this.readObject(params, "thread"), "id");
785 const turnId =725 const turnId =
786 this.readString(params, "turnId") ??726 this.readString(params, "turnId") ?? this.readString(this.readObject(params, "turn"), "id");
787 this.readString(this.readObject(params, "turn"), "id");
788 const itemId =727 const itemId =
789 this.readString(params, "itemId") ??728 this.readString(params, "itemId") ?? this.readString(this.readObject(params, "item"), "id");
790 this.readString(this.readObject(params, "item"), "id");
791729
792 if (threadId) {730 if (threadId) {
793 route.threadId = threadId;731 route.threadId = threadId;
...@@ -804,10 +742,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve...@@ -804,10 +742,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
804 return route;742 return route;
805 }743 }
806744
807 private readObject(745 private readObject(value: unknown, key?: string): Record<string, unknown> | undefined {
808 value: unknown,
809 key?: string,
810 ): Record<string, unknown> | undefined {
811 const target =746 const target =
812 key === undefined747 key === undefined
813 ? value748 ? value
apps/desktop/src/ipcHelpers.test.ts+3-5
...@@ -55,11 +55,9 @@ describe("withParsedPayload", () => {...@@ -55,11 +55,9 @@ describe("withParsedPayload", () => {
5555
56describe("withParsedArgs", () => {56describe("withParsedArgs", () => {
57 it("parses tuple arguments before invoking handler", () => {57 it("parses tuple arguments before invoking handler", () => {
58 const handler = vi.fn(58 const handler = vi.fn((_event: unknown, sessionId: string, data: string) => {
59 (_event: unknown, sessionId: string, data: string) => {59 return `${sessionId}:${data}`;
60 return `${sessionId}:${data}`;60 });
61 },
62 );
63 const wrapped = withParsedArgs(61 const wrapped = withParsedArgs(
64 {62 {
65 parse(args: unknown[]): [string, string] {63 parse(args: unknown[]): [string, string] {
apps/desktop/src/main.ts+12-26
...@@ -105,8 +105,7 @@ function registerIpcHandlers(): void {...@@ -105,8 +105,7 @@ function registerIpcHandlers(): void {
105 );105 );
106106
107 ipcMain.handle(IPC_CHANNELS.dialogPickFolder, async () => {107 ipcMain.handle(IPC_CHANNELS.dialogPickFolder, async () => {
108 const owner =108 const owner = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0];
109 BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0];
110 const result = owner109 const result = owner
111 ? await dialog.showOpenDialog(owner, {110 ? await dialog.showOpenDialog(owner, {
112 properties: ["openDirectory", "createDirectory"],111 properties: ["openDirectory", "createDirectory"],
...@@ -175,12 +174,9 @@ function registerIpcHandlers(): void {...@@ -175,12 +174,9 @@ function registerIpcHandlers(): void {
175 // Provider handlers174 // Provider handlers
176 ipcMain.handle(175 ipcMain.handle(
177 IPC_CHANNELS.providerSessionStart,176 IPC_CHANNELS.providerSessionStart,
178 withParsedPayload(177 withParsedPayload(providerSessionStartInputSchema, async (_event, payload) => {
179 providerSessionStartInputSchema,178 return providerManager.startSession(payload);
180 async (_event, payload) => {179 }),
181 return providerManager.startSession(payload);
182 },
183 ),
184 );180 );
185181
186 ipcMain.handle(182 ipcMain.handle(
...@@ -192,12 +188,9 @@ function registerIpcHandlers(): void {...@@ -192,12 +188,9 @@ function registerIpcHandlers(): void {
192188
193 ipcMain.handle(189 ipcMain.handle(
194 IPC_CHANNELS.providerTurnInterrupt,190 IPC_CHANNELS.providerTurnInterrupt,
195 withParsedPayload(191 withParsedPayload(providerInterruptTurnInputSchema, async (_event, payload) => {
196 providerInterruptTurnInputSchema,192 await providerManager.interruptTurn(payload);
197 async (_event, payload) => {193 }),
198 await providerManager.interruptTurn(payload);
199 },
200 ),
201 );194 );
202195
203 ipcMain.handle(196 ipcMain.handle(
...@@ -212,12 +205,9 @@ function registerIpcHandlers(): void {...@@ -212,12 +205,9 @@ function registerIpcHandlers(): void {
212205
213 ipcMain.handle(206 ipcMain.handle(
214 IPC_CHANNELS.providerSessionStop,207 IPC_CHANNELS.providerSessionStop,
215 withParsedPayload(208 withParsedPayload(providerStopSessionInputSchema, async (_event, payload) => {
216 providerStopSessionInputSchema,209 providerManager.stopSession(payload);
217 async (_event, payload) => {210 }),
218 providerManager.stopSession(payload);
219 },
220 ),
221 );211 );
222212
223 ipcMain.handle(IPC_CHANNELS.providerSessionList, async () => {213 ipcMain.handle(IPC_CHANNELS.providerSessionList, async () => {
...@@ -225,18 +215,14 @@ function registerIpcHandlers(): void {...@@ -225,18 +215,14 @@ function registerIpcHandlers(): void {
225 });215 });
226}216}
227217
228async function runTerminalCommand(218async function runTerminalCommand(input: TerminalCommandInput): Promise<TerminalCommandResult> {
229 input: TerminalCommandInput,
230): Promise<TerminalCommandResult> {
231 const shellPath =219 const shellPath =
232 process.platform === "win32"220 process.platform === "win32"
233 ? (process.env.ComSpec ?? "cmd.exe")221 ? (process.env.ComSpec ?? "cmd.exe")
234 : (process.env.SHELL ?? "/bin/sh");222 : (process.env.SHELL ?? "/bin/sh");
235223
236 const args =224 const args =
237 process.platform === "win32"225 process.platform === "win32" ? ["/d", "/s", "/c", input.command] : ["-lc", input.command];
238 ? ["/d", "/s", "/c", input.command]
239 : ["-lc", input.command];
240226
241 return new Promise((resolve, reject) => {227 return new Promise((resolve, reject) => {
242 const child = spawn(shellPath, args, {228 const child = spawn(shellPath, args, {
apps/desktop/src/preload.ts+8-16
...@@ -18,14 +18,12 @@ const nativeApi: NativeApi = {...@@ -18,14 +18,12 @@ const nativeApi: NativeApi = {
18 agent: {18 agent: {
19 spawn: (config) => ipcRenderer.invoke(IPC_CHANNELS.agentSpawn, config),19 spawn: (config) => ipcRenderer.invoke(IPC_CHANNELS.agentSpawn, config),
20 kill: (sessionId) => ipcRenderer.invoke(IPC_CHANNELS.agentKill, sessionId),20 kill: (sessionId) => ipcRenderer.invoke(IPC_CHANNELS.agentKill, sessionId),
21 write: (sessionId, data) =>21 write: (sessionId, data) => ipcRenderer.invoke(IPC_CHANNELS.agentWrite, sessionId, data),
22 ipcRenderer.invoke(IPC_CHANNELS.agentWrite, sessionId, data),
23 onOutput: (callback) => {22 onOutput: (callback) => {
24 const listener = (_event: Electron.IpcRendererEvent, chunk: unknown) =>23 const listener = (_event: Electron.IpcRendererEvent, chunk: unknown) =>
25 callback(chunk as Parameters<typeof callback>[0]);24 callback(chunk as Parameters<typeof callback>[0]);
26 ipcRenderer.on(IPC_CHANNELS.agentOutput, listener);25 ipcRenderer.on(IPC_CHANNELS.agentOutput, listener);
27 return () =>26 return () => ipcRenderer.removeListener(IPC_CHANNELS.agentOutput, listener);
28 ipcRenderer.removeListener(IPC_CHANNELS.agentOutput, listener);
29 },27 },
30 onExit: (callback) => {28 onExit: (callback) => {
31 const listener = (_event: Electron.IpcRendererEvent, exit: unknown) =>29 const listener = (_event: Electron.IpcRendererEvent, exit: unknown) =>
...@@ -35,23 +33,17 @@ const nativeApi: NativeApi = {...@@ -35,23 +33,17 @@ const nativeApi: NativeApi = {
35 },33 },
36 },34 },
37 providers: {35 providers: {
38 startSession: (input) =>36 startSession: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerSessionStart, input),
39 ipcRenderer.invoke(IPC_CHANNELS.providerSessionStart, input),37 sendTurn: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerTurnStart, input),
40 sendTurn: (input) =>38 interruptTurn: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerTurnInterrupt, input),
41 ipcRenderer.invoke(IPC_CHANNELS.providerTurnStart, input),39 respondToRequest: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerRequestRespond, input),
42 interruptTurn: (input) =>40 stopSession: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerSessionStop, input),
43 ipcRenderer.invoke(IPC_CHANNELS.providerTurnInterrupt, input),
44 respondToRequest: (input) =>
45 ipcRenderer.invoke(IPC_CHANNELS.providerRequestRespond, input),
46 stopSession: (input) =>
47 ipcRenderer.invoke(IPC_CHANNELS.providerSessionStop, input),
48 listSessions: () => ipcRenderer.invoke(IPC_CHANNELS.providerSessionList),41 listSessions: () => ipcRenderer.invoke(IPC_CHANNELS.providerSessionList),
49 onEvent: (callback) => {42 onEvent: (callback) => {
50 const listener = (_event: Electron.IpcRendererEvent, payload: unknown) =>43 const listener = (_event: Electron.IpcRendererEvent, payload: unknown) =>
51 callback(payload as Parameters<typeof callback>[0]);44 callback(payload as Parameters<typeof callback>[0]);
52 ipcRenderer.on(IPC_CHANNELS.providerEvent, listener);45 ipcRenderer.on(IPC_CHANNELS.providerEvent, listener);
53 return () =>46 return () => ipcRenderer.removeListener(IPC_CHANNELS.providerEvent, listener);
54 ipcRenderer.removeListener(IPC_CHANNELS.providerEvent, listener);
55 },47 },
56 },48 },
57 shell: {49 shell: {
apps/desktop/src/processManager.ts+1-4
...@@ -68,7 +68,6 @@ export class ProcessManager extends EventEmitter<ProcessManagerEvents> {...@@ -68,7 +68,6 @@ export class ProcessManager extends EventEmitter<ProcessManagerEvents> {
68 }68 }
6969
70 private spawnPty(sessionId: string, config: AgentConfig): string {70 private spawnPty(sessionId: string, config: AgentConfig): string {
71 // eslint-disable-next-line @typescript-eslint/no-var-requires
72 const pty = require("node-pty") as typeof import("node-pty");71 const pty = require("node-pty") as typeof import("node-pty");
7372
74 const ptyProcess = pty.spawn(config.command, config.args, {73 const ptyProcess = pty.spawn(config.command, config.args, {
...@@ -76,9 +75,7 @@ export class ProcessManager extends EventEmitter<ProcessManagerEvents> {...@@ -76,9 +75,7 @@ export class ProcessManager extends EventEmitter<ProcessManagerEvents> {
76 cols: 120,75 cols: 120,
77 rows: 30,76 rows: 30,
78 cwd: config.cwd ?? process.cwd(),77 cwd: config.cwd ?? process.cwd(),
79 env: (config.env78 env: (config.env ? { ...process.env, ...config.env } : process.env) as Record<string, string>,
80 ? { ...process.env, ...config.env }
81 : process.env) as Record<string, string>,
82 });79 });
8380
84 this.ptySessions.set(sessionId, ptyProcess);81 this.ptySessions.set(sessionId, ptyProcess);
apps/desktop/src/todoStore.ts+1-5
...@@ -82,11 +82,7 @@ export class TodoStore {...@@ -82,11 +82,7 @@ export class TodoStore {
82 }82 }
8383
84 private async persist(): Promise<void> {84 private async persist(): Promise<void> {
85 await fs.writeFile(85 await fs.writeFile(this.filePath, JSON.stringify(this.todos, null, 2), "utf8");
86 this.filePath,
87 JSON.stringify(this.todos, null, 2),
88 "utf8",
89 );
90 }86 }
9187
92 private async runExclusive<T>(operation: () => Promise<T>): Promise<T> {88 private async runExclusive<T>(operation: () => Promise<T>): Promise<T> {
apps/desktop/tsconfig.json+1-1
...@@ -3,7 +3,7 @@...@@ -3,7 +3,7 @@
3 "compilerOptions": {3 "compilerOptions": {
4 "composite": true,4 "composite": true,
5 "types": ["node", "electron"],5 "types": ["node", "electron"],
6 "lib": ["ES2022", "DOM"]6 "lib": ["ES2023", "DOM"]
7 },7 },
8 "include": ["src", "tsup.config.ts"]8 "include": ["src", "tsup.config.ts"]
9}9}
apps/renderer/package.json+1-2
...@@ -8,15 +8,14 @@...@@ -8,15 +8,14 @@
8 "build": "vite build",8 "build": "vite build",
9 "preview": "vite preview",9 "preview": "vite preview",
10 "typecheck": "tsc --noEmit",10 "typecheck": "tsc --noEmit",
11 "lint": "biome check src/",
12 "test": "vitest run --passWithNoTests"11 "test": "vitest run --passWithNoTests"
13 },12 },
14 "dependencies": {13 "dependencies": {
15 "@acme/contracts": "workspace:*",14 "@acme/contracts": "workspace:*",
16 "highlight.js": "^11.11.1",15 "highlight.js": "^11.11.1",
17 "react-markdown": "^10.1.0",
18 "react": "^19.0.0",16 "react": "^19.0.0",
19 "react-dom": "^19.0.0",17 "react-dom": "^19.0.0",
18 "react-markdown": "^10.1.0",
20 "rehype-highlight": "^7.0.2",19 "rehype-highlight": "^7.0.2",
21 "remark-gfm": "^4.0.1",20 "remark-gfm": "^4.0.1",
22 "zod": "^3.24.1"21 "zod": "^3.24.1"
apps/renderer/src/components/ChatMarkdown.tsx+1-3
...@@ -18,9 +18,7 @@ export default function ChatMarkdown({ text }: ChatMarkdownProps) {...@@ -18,9 +18,7 @@ export default function ChatMarkdown({ text }: ChatMarkdownProps) {
18 <div className="chat-markdown text-sm leading-relaxed text-[#d0d0d0]">18 <div className="chat-markdown text-sm leading-relaxed text-[#d0d0d0]">
19 <ReactMarkdown19 <ReactMarkdown
20 remarkPlugins={[remarkGfm]}20 remarkPlugins={[remarkGfm]}
21 rehypePlugins={[21 rehypePlugins={[[rehypeHighlight, { detect: true, ignoreMissing: true }]]}
22 [rehypeHighlight, { detect: true, ignoreMissing: true }],
23 ]}
24 components={markdownComponents}22 components={markdownComponents}
25 >23 >
26 {text}24 {text}
apps/renderer/src/components/ChatView.tsx+20-50
...@@ -144,9 +144,7 @@ export default function ChatView() {...@@ -144,9 +144,7 @@ export default function ChatView() {
144 const editorMenuRef = useRef<HTMLDivElement>(null);144 const editorMenuRef = useRef<HTMLDivElement>(null);
145145
146 const activeThread = state.threads.find((t) => t.id === state.activeThreadId);146 const activeThread = state.threads.find((t) => t.id === state.activeThreadId);
147 const activeProject = state.projects.find(147 const activeProject = state.projects.find((p) => p.id === activeThread?.projectId);
148 (p) => p.id === activeThread?.projectId,
149 );
150 const selectedModel = resolveModelSlug(148 const selectedModel = resolveModelSlug(
151 activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL,149 activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL,
152 );150 );
...@@ -164,7 +162,7 @@ export default function ChatView() {...@@ -164,7 +162,7 @@ export default function ChatView() {
164 );162 );
165 const assistantCompletionByItemId = useMemo(() => {163 const assistantCompletionByItemId = useMemo(() => {
166 const map = new Map<string, string>();164 const map = new Map<string, string>();
167 const ordered = [...(activeThread?.events ?? [])].reverse();165 const ordered = [...(activeThread?.events ?? [])].toReversed();
168 for (const event of ordered) {166 for (const event of ordered) {
169 if (event.method !== "item/completed") continue;167 if (event.method !== "item/completed") continue;
170 if (!event.itemId) continue;168 if (!event.itemId) continue;
...@@ -264,18 +262,15 @@ export default function ChatView() {...@@ -264,18 +262,15 @@ export default function ChatView() {
264 // Auto-scroll on new messages262 // Auto-scroll on new messages
265 const messageCount = activeThread?.messages.length ?? 0;263 const messageCount = activeThread?.messages.length ?? 0;
266 const workLogCount = workLogEntries.length;264 const workLogCount = workLogEntries.length;
267 // biome-ignore lint/correctness/useExhaustiveDependencies: trigger on message count change
268 useEffect(() => {265 useEffect(() => {
269 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });266 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
270 }, [messageCount]);267 }, [messageCount]);
271 // biome-ignore lint/correctness/useExhaustiveDependencies: auto-scroll while active work-log events stream in
272 useEffect(() => {268 useEffect(() => {
273 if (phase !== "running") return;269 if (phase !== "running") return;
274 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });270 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
275 }, [phase, workLogCount]);271 }, [phase, workLogCount]);
276272
277 // Auto-resize textarea273 // Auto-resize textarea
278 // biome-ignore lint/correctness/useExhaustiveDependencies: trigger on prompt change
279 useEffect(() => {274 useEffect(() => {
280 const ta = textareaRef.current;275 const ta = textareaRef.current;
281 if (!ta) return;276 if (!ta) return;
...@@ -298,10 +293,7 @@ export default function ChatView() {...@@ -298,10 +293,7 @@ export default function ChatView() {
298293
299 const handleClickOutside = (event: MouseEvent) => {294 const handleClickOutside = (event: MouseEvent) => {
300 if (!modelMenuRef.current) return;295 if (!modelMenuRef.current) return;
301 if (296 if (event.target instanceof Node && !modelMenuRef.current.contains(event.target)) {
302 event.target instanceof Node &&
303 !modelMenuRef.current.contains(event.target)
304 ) {
305 setIsModelMenuOpen(false);297 setIsModelMenuOpen(false);
306 }298 }
307 };299 };
...@@ -394,8 +386,7 @@ export default function ChatView() {...@@ -394,8 +386,7 @@ export default function ChatView() {
394386
395 // Auto-title from first message387 // Auto-title from first message
396 if (activeThread.messages.length === 0) {388 if (activeThread.messages.length === 0) {
397 const title =389 const title = trimmed.length > 50 ? `${trimmed.slice(0, 50)}...` : trimmed;
398 trimmed.length > 50 ? `${trimmed.slice(0, 50)}...` : trimmed;
399 dispatch({390 dispatch({
400 type: "SET_THREAD_TITLE",391 type: "SET_THREAD_TITLE",
401 threadId: activeThread.id,392 threadId: activeThread.id,
...@@ -501,9 +492,7 @@ export default function ChatView() {...@@ -501,9 +492,7 @@ export default function ChatView() {
501 <div className="drag-region h-[52px] shrink-0" />492 <div className="drag-region h-[52px] shrink-0" />
502 <div className="flex flex-1 items-center justify-center">493 <div className="flex flex-1 items-center justify-center">
503 <div className="text-center">494 <div className="text-center">
504 <p className="text-sm">495 <p className="text-sm">Select a thread or create a new one to get started.</p>
505 Select a thread or create a new one to get started.
506 </p>
507 </div>496 </div>
508 </div>497 </div>
509 </div>498 </div>
...@@ -513,7 +502,7 @@ export default function ChatView() {...@@ -513,7 +502,7 @@ export default function ChatView() {
513 return (502 return (
514 <div className="flex flex-1 flex-col bg-[#0c0c0c]">503 <div className="flex flex-1 flex-col bg-[#0c0c0c]">
515 {/* Top bar */}504 {/* Top bar */}
516 <header className="drag-region flex items-center justify-between border-b border-white/[0.08] px-5 pt-[28px] pb-3">505 <header className="drag-region flex items-center justify-between border-b border-white/8 px-5 pt-[28px] pb-3">
517 <div className="flex items-center gap-3">506 <div className="flex items-center gap-3">
518 <h2 className="text-sm font-medium text-[#e0e0e0]">507 <h2 className="text-sm font-medium text-[#e0e0e0]">
519 {activeThread.title}508 {activeThread.title}
...@@ -654,9 +643,7 @@ export default function ChatView() {...@@ -654,9 +643,7 @@ export default function ChatView() {
654 <div className="flex-1 overflow-y-auto px-5 py-4">643 <div className="flex-1 overflow-y-auto px-5 py-4">
655 {activeThread.messages.length === 0 && !isWorking ? (644 {activeThread.messages.length === 0 && !isWorking ? (
656 <div className="flex h-full items-center justify-center">645 <div className="flex h-full items-center justify-center">
657 <p className="text-sm text-[#a0a0a0]/30">646 <p className="text-sm text-[#a0a0a0]/30">Send a message to start the conversation.</p>
658 Send a message to start the conversation.
659 </p>
660 </div>647 </div>
661 ) : (648 ) : (
662 <div className="mx-auto max-w-3xl space-y-4">649 <div className="mx-auto max-w-3xl space-y-4">
...@@ -770,7 +757,7 @@ export default function ChatView() {...@@ -770,7 +757,7 @@ export default function ChatView() {
770 {/* Input bar */}757 {/* Input bar */}
771 <div className="px-5 pb-4 pt-2">758 <div className="px-5 pb-4 pt-2">
772 <form onSubmit={onSend} className="mx-auto max-w-3xl">759 <form onSubmit={onSend} className="mx-auto max-w-3xl">
773 <div className="group rounded-[20px] border border-white/[0.08] bg-[#141416] transition-colors duration-200 focus-within:border-white/[0.16]">760 <div className="group rounded-[20px] border border-white/8 bg-[#141416] transition-colors duration-200 focus-within:border-white/16">
774 {/* Textarea area */}761 {/* Textarea area */}
775 <div className="px-4 pt-4 pb-2">762 <div className="px-4 pt-4 pb-2">
776 <textarea763 <textarea
...@@ -781,9 +768,7 @@ export default function ChatView() {...@@ -781,9 +768,7 @@ export default function ChatView() {
781 onChange={(e) => setPrompt(e.target.value)}768 onChange={(e) => setPrompt(e.target.value)}
782 onKeyDown={onKeyDown}769 onKeyDown={onKeyDown}
783 placeholder={770 placeholder={
784 phase === "disconnected"771 phase === "disconnected" ? "Ask for follow-up changes" : "Ask anything..."
785 ? "Ask for follow-up changes"
786 : "Ask anything..."
787 }772 }
788 disabled={isSending || isConnecting}773 disabled={isSending || isConnecting}
789 />774 />
...@@ -796,12 +781,10 @@ export default function ChatView() {...@@ -796,12 +781,10 @@ export default function ChatView() {
796 <div className="relative" ref={modelMenuRef}>781 <div className="relative" ref={modelMenuRef}>
797 <button782 <button
798 type="button"783 type="button"
799 className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/[0.06] hover:text-[#d0d0d0]"784 className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/6 hover:text-[#d0d0d0]"
800 onClick={() => setIsModelMenuOpen((open) => !open)}785 onClick={() => setIsModelMenuOpen((open) => !open)}
801 >786 >
802 <span className="max-w-[180px] truncate">787 <span className="max-w-[180px] truncate">{selectedModel}</span>
803 {selectedModel}
804 </span>
805 <svg788 <svg
806 width="10"789 width="10"
807 height="10"790 height="10"
...@@ -820,10 +803,8 @@ export default function ChatView() {...@@ -820,10 +803,8 @@ export default function ChatView() {
820 </svg>803 </svg>
821 </button>804 </button>
822 {isModelMenuOpen && (805 {isModelMenuOpen && (
823 <div className="absolute bottom-full left-0 z-20 mb-2 w-[320px] rounded-2xl border border-white/[0.1] bg-[#1b1b1d]/95 p-2 shadow-[0_16px_40px_rgba(0,0,0,0.55)] backdrop-blur">806 <div className="absolute bottom-full left-0 z-20 mb-2 w-[320px] rounded-2xl border border-white/10 bg-[#1b1b1d]/95 p-2 shadow-[0_16px_40px_rgba(0,0,0,0.55)] backdrop-blur">
824 <p className="px-2 py-1 text-[11px] text-[#a0a0a0]/70">807 <p className="px-2 py-1 text-[11px] text-[#a0a0a0]/70">Select model</p>
825 Select model
826 </p>
827 <div className="max-h-72 overflow-y-auto">808 <div className="max-h-72 overflow-y-auto">
828 {modelOptions.map((model) => {809 {modelOptions.map((model) => {
829 const isSelected = model === selectedModel;810 const isSelected = model === selectedModel;
...@@ -833,8 +814,8 @@ export default function ChatView() {...@@ -833,8 +814,8 @@ export default function ChatView() {
833 type="button"814 type="button"
834 className={`mb-0.5 flex w-full items-center justify-between gap-2 rounded-xl px-2 py-2 text-left font-mono text-sm transition-colors duration-150 ${815 className={`mb-0.5 flex w-full items-center justify-between gap-2 rounded-xl px-2 py-2 text-left font-mono text-sm transition-colors duration-150 ${
835 isSelected816 isSelected
836 ? "bg-white/[0.08] text-white"817 ? "bg-white/8 text-white"
837 : "text-[#d4d4d4] hover:bg-white/[0.05]"818 : "text-[#d4d4d4] hover:bg-white/5"
838 }`}819 }`}
839 onClick={() => onModelSelect(model)}820 onClick={() => onModelSelect(model)}
840 >821 >
...@@ -855,17 +836,14 @@ export default function ChatView() {...@@ -855,17 +836,14 @@ export default function ChatView() {
855 </div>836 </div>
856837
857 {/* Divider */}838 {/* Divider */}
858 <div className="mx-0.5 h-4 w-px bg-white/[0.08]" />839 <div className="mx-0.5 h-4 w-px bg-white/8" />
859840
860 {/* Reasoning effort */}841 {/* Reasoning effort */}
861 <label842 <label
862 className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/[0.06] hover:text-[#d0d0d0]"843 className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/6 hover:text-[#d0d0d0]"
863 htmlFor="reasoning-effort"844 htmlFor="reasoning-effort"
864 >845 >
865 <span>846 <span>{selectedEffort.charAt(0).toUpperCase() + selectedEffort.slice(1)}</span>
866 {selectedEffort.charAt(0).toUpperCase() +
867 selectedEffort.slice(1)}
868 </span>
869 <select847 <select
870 id="reasoning-effort"848 id="reasoning-effort"
871 className="absolute opacity-0 w-0 h-0"849 className="absolute opacity-0 w-0 h-0"
...@@ -873,11 +851,7 @@ export default function ChatView() {...@@ -873,11 +851,7 @@ export default function ChatView() {
873 onChange={(event) => setSelectedEffort(event.target.value)}851 onChange={(event) => setSelectedEffort(event.target.value)}
874 >852 >
875 {REASONING_OPTIONS.map((effort) => (853 {REASONING_OPTIONS.map((effort) => (
876 <option854 <option key={effort} value={effort} className="bg-[#1b1b1d]">
877 key={effort}
878 value={effort}
879 className="bg-[#1b1b1d]"
880 >
881 {effort}855 {effort}
882 {effort === DEFAULT_REASONING ? " (default)" : ""}856 {effort === DEFAULT_REASONING ? " (default)" : ""}
883 </option>857 </option>
...@@ -1004,11 +978,7 @@ export default function ChatView() {...@@ -1004,11 +978,7 @@ export default function ChatView() {
1004 className="flex h-8 w-8 items-center justify-center rounded-full bg-white/90 text-[#0c0c0c] transition-all duration-150 hover:bg-white hover:scale-105 disabled:opacity-30 disabled:hover:scale-100"978 className="flex h-8 w-8 items-center justify-center rounded-full bg-white/90 text-[#0c0c0c] transition-all duration-150 hover:bg-white hover:scale-105 disabled:opacity-30 disabled:hover:scale-100"
1005 disabled={isSending || isConnecting || !prompt.trim()}979 disabled={isSending || isConnecting || !prompt.trim()}
1006 aria-label={980 aria-label={
1007 isConnecting981 isConnecting ? "Connecting" : isSending ? "Sending" : "Send message"
1008 ? "Connecting"
1009 : isSending
1010 ? "Sending"
1011 : "Send message"
1012 }982 }
1013 >983 >
1014 {isConnecting || isSending ? (984 {isConnecting || isSending ? (
apps/renderer/src/components/DiffPanel.tsx+4-12
...@@ -7,9 +7,7 @@ export default function DiffPanel() {...@@ -7,9 +7,7 @@ export default function DiffPanel() {
7 <aside className="flex h-full w-[400px] shrink-0 flex-col border-l border-white/[0.08] bg-[#141414]">7 <aside className="flex h-full w-[400px] shrink-0 flex-col border-l border-white/[0.08] bg-[#141414]">
8 {/* Header */}8 {/* Header */}
9 <div className="drag-region flex items-center justify-between border-b border-white/[0.08] px-4 pt-[28px] pb-3">9 <div className="drag-region flex items-center justify-between border-b border-white/[0.08] px-4 pt-[28px] pb-3">
10 <h3 className="text-xs font-medium text-[#e0e0e0]">10 <h3 className="text-xs font-medium text-[#e0e0e0]">Uncommitted changes</h3>
11 Uncommitted changes
12 </h3>
13 <button11 <button
14 type="button"12 type="button"
15 className="text-[#a0a0a0]/40 transition-colors duration-150 hover:text-[#a0a0a0]/70"13 className="text-[#a0a0a0]/40 transition-colors duration-150 hover:text-[#a0a0a0]/70"
...@@ -27,15 +25,11 @@ export default function DiffPanel() {...@@ -27,15 +25,11 @@ export default function DiffPanel() {
27 <span className="rounded bg-emerald-500/20 px-1.5 py-0.5 text-[9px] font-medium text-emerald-400">25 <span className="rounded bg-emerald-500/20 px-1.5 py-0.5 text-[9px] font-medium text-emerald-400">
28 M26 M
29 </span>27 </span>
30 <span className="font-mono text-xs text-[#a0a0a0]/60">28 <span className="font-mono text-xs text-[#a0a0a0]/60">src/App.tsx</span>
31 src/App.tsx
32 </span>
33 </div>29 </div>
34 <div className="space-y-0.5 font-mono text-[11px]">30 <div className="space-y-0.5 font-mono text-[11px]">
35 <div className="text-red-400/60">- const oldValue = "hello";</div>31 <div className="text-red-400/60">- const oldValue = "hello";</div>
36 <div className="text-emerald-400/60">32 <div className="text-emerald-400/60">+ const newValue = "world";</div>
37 + const newValue = "world";
38 </div>
39 </div>33 </div>
40 </div>34 </div>
4135
...@@ -44,9 +38,7 @@ export default function DiffPanel() {...@@ -44,9 +38,7 @@ export default function DiffPanel() {
44 <span className="rounded bg-blue-500/20 px-1.5 py-0.5 text-[9px] font-medium text-blue-400">38 <span className="rounded bg-blue-500/20 px-1.5 py-0.5 text-[9px] font-medium text-blue-400">
45 A39 A
46 </span>40 </span>
47 <span className="font-mono text-xs text-[#a0a0a0]/60">41 <span className="font-mono text-xs text-[#a0a0a0]/60">src/utils.ts</span>
48 src/utils.ts
49 </span>
50 </div>42 </div>
51 </div>43 </div>
52 </div>44 </div>
apps/renderer/src/components/Sidebar.tsx+14-22
...@@ -120,7 +120,7 @@ export default function Sidebar() {...@@ -120,7 +120,7 @@ export default function Sidebar() {
120 }, [handleNewThread, state.activeThreadId, state.projects, state.threads]);120 }, [handleNewThread, state.activeThreadId, state.projects, state.threads]);
121121
122 return (122 return (
123 <aside className="sidebar flex h-full w-[260px] shrink-0 flex-col border-r border-white/[0.08] bg-[#141414]">123 <aside className="sidebar flex h-full w-[260px] shrink-0 flex-col border-r border-white/8 bg-[#141414]">
124 {/* Drag region / traffic light space */}124 {/* Drag region / traffic light space */}
125 <div className="drag-region h-[52px] shrink-0" />125 <div className="drag-region h-[52px] shrink-0" />
126 {/* Branding */}126 {/* Branding */}
...@@ -128,16 +128,14 @@ export default function Sidebar() {...@@ -128,16 +128,14 @@ export default function Sidebar() {
128 <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-white text-xs font-bold text-[#0c0c0c]">128 <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-white text-xs font-bold text-[#0c0c0c]">
129 CT129 CT
130 </div>130 </div>
131 <span className="text-sm font-semibold tracking-tight text-[#e0e0e0]">131 <span className="text-sm font-semibold tracking-tight text-[#e0e0e0]">CodeThing</span>
132 CodeThing
133 </span>
134 </div>132 </div>
135133
136 {/* New thread (global) */}134 {/* New thread (global) */}
137 <div className="px-3 pb-3">135 <div className="px-3 pb-3">
138 <button136 <button
139 type="button"137 type="button"
140 className="flex w-full items-center gap-2 rounded-lg border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-xs text-[#a0a0a0]/80 transition-colors duration-150 hover:bg-white/[0.07]"138 className="flex w-full items-center gap-2 rounded-lg border border-white/8 bg-white/4 px-3 py-2 text-xs text-[#a0a0a0]/80 transition-colors duration-150 hover:bg-white/[0.07]"
141 onClick={() => {139 onClick={() => {
142 if (state.projects.length === 0) {140 if (state.projects.length === 0) {
143 setAddingProject(true);141 setAddingProject(true);
...@@ -169,10 +167,8 @@ export default function Sidebar() {...@@ -169,10 +167,8 @@ export default function Sidebar() {
169 {/* Project header */}167 {/* Project header */}
170 <button168 <button
171 type="button"169 type="button"
172 className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 hover:bg-white/[0.05]"170 className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 hover:bg-white/5"
173 onClick={() =>171 onClick={() => dispatch({ type: "TOGGLE_PROJECT", projectId: project.id })}
174 dispatch({ type: "TOGGLE_PROJECT", projectId: project.id })
175 }
176 >172 >
177 <span className="text-[10px] text-[#a0a0a0]/50">173 <span className="text-[10px] text-[#a0a0a0]/50">
178 {project.expanded ? "â–¼" : "â–¶"}174 {project.expanded ? "â–¼" : "â–¶"}
...@@ -180,14 +176,12 @@ export default function Sidebar() {...@@ -180,14 +176,12 @@ export default function Sidebar() {
180 <span className="flex-1 truncate text-xs font-medium text-[#e0e0e0]/90">176 <span className="flex-1 truncate text-xs font-medium text-[#e0e0e0]/90">
181 {project.name}177 {project.name}
182 </span>178 </span>
183 <span className="text-[10px] text-[#a0a0a0]/40">179 <span className="text-[10px] text-[#a0a0a0]/40">{threads.length}</span>
184 {threads.length}
185 </span>
186 </button>180 </button>
187181
188 {/* Threads */}182 {/* Threads */}
189 {project.expanded && (183 {project.expanded && (
190 <div className="ml-2 border-l border-white/[0.06] pl-2">184 <div className="ml-2 border-l border-white/6 pl-2">
191 {threads.map((thread) => {185 {threads.map((thread) => {
192 const isActive = state.activeThreadId === thread.id;186 const isActive = state.activeThreadId === thread.id;
193 const threadStatus = threadStatusLabel(187 const threadStatus = threadStatusLabel(
...@@ -198,9 +192,7 @@ export default function Sidebar() {...@@ -198,9 +192,7 @@ export default function Sidebar() {
198 key={thread.id}192 key={thread.id}
199 type="button"193 type="button"
200 className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 ${194 className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 ${
201 isActive195 isActive ? "bg-white/10 text-white" : "text-[#a0a0a0]/70 hover:bg-white/4"
202 ? "bg-white/10 text-white"
203 : "text-[#a0a0a0]/70 hover:bg-white/[0.04]"
204 }`}196 }`}
205 onClick={() =>197 onClick={() =>
206 dispatch({198 dispatch({
...@@ -254,12 +246,12 @@ export default function Sidebar() {...@@ -254,12 +246,12 @@ export default function Sidebar() {
254246
255 {/* Add project form */}247 {/* Add project form */}
256 {addingProject ? (248 {addingProject ? (
257 <div className="border-t border-white/[0.08] p-3">249 <div className="border-t border-white/8 p-3">
258 <p className="mb-2 text-[10px] font-medium uppercase tracking-wider text-[#a0a0a0]/50">250 <p className="mb-2 text-[10px] font-medium uppercase tracking-wider text-[#a0a0a0]/50">
259 Add project251 Add project
260 </p>252 </p>
261 <input253 <input
262 className="mb-2 w-full rounded-md border border-white/[0.1] bg-white/[0.04] px-2 py-1.5 font-mono text-xs text-[#e0e0e0] placeholder:text-[#a0a0a0]/30 focus:border-white/30 focus:outline-none"254 className="mb-2 w-full rounded-md border border-white/10 bg-white/4 px-2 py-1.5 font-mono text-xs text-[#e0e0e0] placeholder:text-[#a0a0a0]/30 focus:border-white/30 focus:outline-none"
263 placeholder="/path/to/project"255 placeholder="/path/to/project"
264 value={newCwd}256 value={newCwd}
265 onChange={(e) => setNewCwd(e.target.value)}257 onChange={(e) => setNewCwd(e.target.value)}
...@@ -271,7 +263,7 @@ export default function Sidebar() {...@@ -271,7 +263,7 @@ export default function Sidebar() {
271 {api && (263 {api && (
272 <button264 <button
273 type="button"265 type="button"
274 className="mb-2 flex w-full items-center justify-center rounded-md border border-white/[0.1] px-2 py-1.5 text-xs text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/[0.04] disabled:cursor-not-allowed disabled:opacity-60"266 className="mb-2 flex w-full items-center justify-center rounded-md border border-white/10 px-2 py-1.5 text-xs text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/4 disabled:cursor-not-allowed disabled:opacity-60"
275 onClick={() => void handlePickFolder()}267 onClick={() => void handlePickFolder()}
276 disabled={isPickingFolder}268 disabled={isPickingFolder}
277 >269 >
...@@ -288,7 +280,7 @@ export default function Sidebar() {...@@ -288,7 +280,7 @@ export default function Sidebar() {
288 </button>280 </button>
289 <button281 <button
290 type="button"282 type="button"
291 className="flex-1 rounded-md border border-white/[0.1] px-2 py-1 text-xs text-[#a0a0a0]/60 transition-colors duration-150 hover:bg-white/[0.04]"283 className="flex-1 rounded-md border border-white/10 px-2 py-1 text-xs text-[#a0a0a0]/60 transition-colors duration-150 hover:bg-white/4"
292 onClick={() => setAddingProject(false)}284 onClick={() => setAddingProject(false)}
293 >285 >
294 Cancel286 Cancel
...@@ -296,10 +288,10 @@ export default function Sidebar() {...@@ -296,10 +288,10 @@ export default function Sidebar() {
296 </div>288 </div>
297 </div>289 </div>
298 ) : (290 ) : (
299 <div className="border-t border-white/[0.08] p-3">291 <div className="border-t border-white/8 p-3">
300 <button292 <button
301 type="button"293 type="button"
302 className="flex w-full items-center justify-center gap-1 rounded-md border border-dashed border-white/[0.12] py-2 text-xs text-[#a0a0a0]/50 transition-colors duration-150 hover:border-white/[0.2] hover:text-[#a0a0a0]/70"294 className="flex w-full items-center justify-center gap-1 rounded-md border border-dashed border-white/12 py-2 text-xs text-[#a0a0a0]/50 transition-colors duration-150 hover:border-white/20 hover:text-[#a0a0a0]/70"
303 onClick={() => setAddingProject(true)}295 onClick={() => setAddingProject(true)}
304 >296 >
305 + Add project297 + Add project
apps/renderer/src/index.css+3-4
...@@ -5,8 +5,8 @@...@@ -5,8 +5,8 @@
5}5}
66
7body {7body {
8 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,8 font-family:
9 "Helvetica Neue", Arial, sans-serif;9 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
10 margin: 0;10 margin: 0;
11 padding: 0;11 padding: 0;
12 min-height: 100vh;12 min-height: 100vh;
...@@ -29,8 +29,7 @@ pre,...@@ -29,8 +29,7 @@ pre,
29code,29code,
30textarea,30textarea,
31input {31input {
32 font-family: "SF Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo,32 font-family: "SF Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
33 monospace;
34}33}
3534
36/* Window drag region (frameless titlebar) */35/* Window drag region (frameless titlebar) */
apps/renderer/src/model-logic.test.ts+1-5
...@@ -1,9 +1,5 @@...@@ -1,9 +1,5 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
2import {2import { DEFAULT_MODEL, normalizeModelSlug, resolveModelSlug } from "./model-logic";
3 DEFAULT_MODEL,
4 normalizeModelSlug,
5 resolveModelSlug,
6} from "./model-logic";
73
8describe("normalizeModelSlug", () => {4describe("normalizeModelSlug", () => {
9 it("maps 5.3 aliases to canonical codex slug", () => {5 it("maps 5.3 aliases to canonical codex slug", () => {
apps/renderer/src/persistenceSchema.ts+4-11
...@@ -69,10 +69,7 @@ export interface PersistedStoreSnapshot {...@@ -69,10 +69,7 @@ export interface PersistedStoreSnapshot {
69 runtimeMode: RuntimeMode;69 runtimeMode: RuntimeMode;
70}70}
7171
72function maybeMigrateLegacyModel(72function maybeMigrateLegacyModel(model: string, isLegacyPayload: boolean): string {
73 model: string,
74 isLegacyPayload: boolean,
75): string {
76 if (!isLegacyPayload) {73 if (!isLegacyPayload) {
77 return model;74 return model;
78 }75 }
...@@ -86,9 +83,7 @@ function hydrateProject(...@@ -86,9 +83,7 @@ function hydrateProject(
86): Project {83): Project {
87 return {84 return {
88 ...project,85 ...project,
89 model: resolveModelSlug(86 model: resolveModelSlug(maybeMigrateLegacyModel(project.model, isLegacyPayload)),
90 maybeMigrateLegacyModel(project.model, isLegacyPayload),
91 ),
92 };87 };
93}88}
9489
...@@ -100,9 +95,7 @@ function hydrateThread(...@@ -100,9 +95,7 @@ function hydrateThread(
100 id: thread.id,95 id: thread.id,
101 projectId: thread.projectId,96 projectId: thread.projectId,
102 title: thread.title,97 title: thread.title,
103 model: resolveModelSlug(98 model: resolveModelSlug(maybeMigrateLegacyModel(thread.model, isLegacyPayload)),
104 maybeMigrateLegacyModel(thread.model, isLegacyPayload),
105 ),
106 session: null,99 session: null,
107 messages: thread.messages.map((message) => ({100 messages: thread.messages.map((message) => ({
108 ...message,101 ...message,
...@@ -139,7 +132,7 @@ export function hydratePersistedState(...@@ -139,7 +132,7 @@ export function hydratePersistedState(
139 .filter((thread) => projectIds.has(thread.projectId));132 .filter((thread) => projectIds.has(thread.projectId));
140 const hasActiveThread = Boolean(133 const hasActiveThread = Boolean(
141 parsedState.data.activeThreadId &&134 parsedState.data.activeThreadId &&
142 threads.some((thread) => thread.id === parsedState.data.activeThreadId),135 threads.some((thread) => thread.id === parsedState.data.activeThreadId),
143 );136 );
144137
145 return {138 return {
apps/renderer/src/session-logic.test.ts+2-7
...@@ -22,9 +22,7 @@ function makeEvent(overrides: Partial<ProviderEvent>): ProviderEvent {...@@ -22,9 +22,7 @@ function makeEvent(overrides: Partial<ProviderEvent>): ProviderEvent {
22 };22 };
23}23}
2424
25function makeSession(25function makeSession(overrides: Partial<ProviderSession> = {}): ProviderSession {
26 overrides: Partial<ProviderSession> = {},
27): ProviderSession {
28 return {26 return {
29 sessionId: "sess-1",27 sessionId: "sess-1",
30 provider: "codex",28 provider: "codex",
...@@ -99,10 +97,7 @@ describe("deriveTimelineEntries", () => {...@@ -99,10 +97,7 @@ describe("deriveTimelineEntries", () => {
9997
100 const timeline = deriveTimelineEntries(messages, workEntries);98 const timeline = deriveTimelineEntries(messages, workEntries);
10199
102 expect(timeline.map((entry) => entry.id)).toEqual([100 expect(timeline.map((entry) => entry.id)).toEqual(["work:w-1", "message:m-1"]);
103 "work:w-1",
104 "message:m-1",
105 ]);
106 });101 });
107});102});
108103
apps/renderer/src/session-logic.ts+5-17
...@@ -1,8 +1,4 @@...@@ -1,8 +1,4 @@
1import type {1import type { ProviderEvent, ProviderKind, ProviderSession } from "@acme/contracts";
2 ProviderEvent,
3 ProviderKind,
4 ProviderSession,
5} from "@acme/contracts";
6import type { ChatMessage, SessionPhase } from "./types";2import type { ChatMessage, SessionPhase } from "./types";
73
8export const PROVIDER_OPTIONS: Array<{4export const PROVIDER_OPTIONS: Array<{
...@@ -49,10 +45,7 @@ export function formatDuration(durationMs: number): string {...@@ -49,10 +45,7 @@ export function formatDuration(durationMs: number): string {
49 return `${minutes}m ${seconds}s`;45 return `${minutes}m ${seconds}s`;
50}46}
5147
52export function formatElapsed(48export function formatElapsed(startIso: string, endIso: string | undefined): string | null {
53 startIso: string,
54 endIso: string | undefined,
55): string | null {
56 if (!endIso) return null;49 if (!endIso) return null;
5750
58 const startedAt = Date.parse(startIso);51 const startedAt = Date.parse(startIso);
...@@ -304,7 +297,7 @@ export function deriveWorkLogEntries(...@@ -304,7 +297,7 @@ export function deriveWorkLogEntries(
304 events: ProviderEvent[],297 events: ProviderEvent[],
305 turnId: string | undefined,298 turnId: string | undefined,
306): WorkLogEntry[] {299): WorkLogEntry[] {
307 const ordered = [...events].reverse();300 const ordered = [...events].toReversed();
308 const entries: WorkLogEntry[] = [];301 const entries: WorkLogEntry[] = [];
309 const turnStartedAtIso = turnId302 const turnStartedAtIso = turnId
310 ? ordered.find((event) => {303 ? ordered.find((event) => {
...@@ -312,9 +305,7 @@ export function deriveWorkLogEntries(...@@ -312,9 +305,7 @@ export function deriveWorkLogEntries(
312 return eventTurnId(event) === turnId;305 return eventTurnId(event) === turnId;
313 })?.createdAt306 })?.createdAt
314 : undefined;307 : undefined;
315 const turnStartedAt = turnStartedAtIso308 const turnStartedAt = turnStartedAtIso ? Date.parse(turnStartedAtIso) : Number.NaN;
316 ? Date.parse(turnStartedAtIso)
317 : Number.NaN;
318309
319 for (const event of ordered) {310 for (const event of ordered) {
320 if (turnId) {311 if (turnId) {
...@@ -428,10 +419,7 @@ export function derivePhase(session: ProviderSession | null): SessionPhase {...@@ -428,10 +419,7 @@ export function derivePhase(session: ProviderSession | null): SessionPhase {
428 return "ready";419 return "ready";
429}420}
430421
431export function evolveSession(422export function evolveSession(previous: ProviderSession, event: ProviderEvent): ProviderSession {
432 previous: ProviderSession,
433 event: ProviderEvent,
434): ProviderSession {
435 const payload = asObject(event.payload);423 const payload = asObject(event.payload);
436424
437 if (event.method === "thread/started") {425 if (event.method === "thread/started") {
apps/renderer/src/store.ts+2-9
...@@ -116,10 +116,7 @@ function updateThread(...@@ -116,10 +116,7 @@ function updateThread(
116 return threads.map((t) => (t.id === threadId ? updater(t) : t));116 return threads.map((t) => (t.id === threadId ? updater(t) : t));
117}117}
118118
119function findThreadBySessionId(119function findThreadBySessionId(threads: Thread[], sessionId: string): Thread | undefined {
120 threads: Thread[],
121 sessionId: string,
122): Thread | undefined {
123 return threads.find((t) => t.session?.sessionId === sessionId);120 return threads.find((t) => t.session?.sessionId === sessionId);
124}121}
125122
...@@ -325,11 +322,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {...@@ -325,11 +322,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
325 persistState(state);322 persistState(state);
326 }, [state]);323 }, [state]);
327324
328 return createElement(325 return createElement(StoreContext.Provider, { value: { state, dispatch } }, children);
329 StoreContext.Provider,
330 { value: { state, dispatch } },
331 children,
332 );
333}326}
334327
335export function useStore() {328export function useStore() {
apps/renderer/tsconfig.json+1-1
...@@ -3,7 +3,7 @@...@@ -3,7 +3,7 @@
3 "compilerOptions": {3 "compilerOptions": {
4 "composite": true,4 "composite": true,
5 "jsx": "react-jsx",5 "jsx": "react-jsx",
6 "lib": ["ES2022", "DOM", "DOM.Iterable"],6 "lib": ["ES2023", "DOM", "DOM.Iterable"],
7 "types": ["vite/client"]7 "types": ["vite/client"]
8 },8 },
9 "include": ["src", "vite.config.ts"]9 "include": ["src", "vite.config.ts"]
biome.json deleted-29
...@@ -1,29 +0,0 @@
1{
2 "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
3 "organizeImports": {
4 "enabled": true
5 },
6 "linter": {
7 "enabled": true,
8 "rules": {
9 "recommended": true,
10 "correctness": {
11 "noUnusedImports": "warn",
12 "noUnusedVariables": "warn"
13 }
14 }
15 },
16 "formatter": {
17 "indentStyle": "space",
18 "indentWidth": 2
19 },
20 "files": {
21 "ignore": [
22 "dist",
23 "dist-electron",
24 "node_modules",
25 "bun.lock",
26 "*.tsbuildinfo"
27 ]
28 }
29}
bun.lock+42-30
...@@ -5,7 +5,8 @@...@@ -5,7 +5,8 @@
5 "": {5 "": {
6 "name": "ct-round-5",6 "name": "ct-round-5",
7 "devDependencies": {7 "devDependencies": {
8 "@biomejs/biome": "^1.9.0",8 "oxfmt": "^0.28.0",
9 "oxlint": "^1.43.0",
9 "turbo": "^2.3.3",10 "turbo": "^2.3.3",
10 "typescript": "^5.7.3",11 "typescript": "^5.7.3",
11 "vitest": "^3.0.0",12 "vitest": "^3.0.0",
...@@ -22,7 +23,6 @@...@@ -22,7 +23,6 @@
22 "devDependencies": {23 "devDependencies": {
23 "@electron/rebuild": "^3.7.0",24 "@electron/rebuild": "^3.7.0",
24 "@types/node": "^22.10.2",25 "@types/node": "^22.10.2",
25 "concurrently": "^9.1.2",
26 "electronmon": "^2.0.2",26 "electronmon": "^2.0.2",
27 "tsup": "^8.3.5",27 "tsup": "^8.3.5",
28 "typescript": "^5.7.3",28 "typescript": "^5.7.3",
...@@ -110,24 +110,6 @@...@@ -110,24 +110,6 @@
110110
111 "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],111 "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
112112
113 "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="],
114
115 "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="],
116
117 "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="],
118
119 "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="],
120
121 "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="],
122
123 "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="],
124
125 "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="],
126
127 "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="],
128
129 "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
130
131 "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="],113 "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="],
132114
133 "@electron/node-gyp": ["@electron/node-gyp@github:electron/node-gyp#06b29aa", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": "./bin/node-gyp.js" }, "electron-node-gyp-06b29aa"],115 "@electron/node-gyp": ["@electron/node-gyp@github:electron/node-gyp#06b29aa", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": "./bin/node-gyp.js" }, "electron-node-gyp-06b29aa"],
...@@ -216,6 +198,38 @@...@@ -216,6 +198,38 @@
216198
217 "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="],199 "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="],
218200
201 "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jmUfF7cNJPw57bEK7sMIqrYRgn4LH428tSgtgLTCtjuGuu1ShREyrkeB7y8HtkXRfhBs4lVY+HMLhqElJvZ6ww=="],
202
203 "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-S6vlV8S7jbjzJOSjfVg2CimUC0r7/aHDLdUm/3+/B/SU/s1jV7ivqWkMv1/8EB43d1BBwT9JQ60ZMTkBqeXSFA=="],
204
205 "@oxfmt/linux-arm64-gnu": ["@oxfmt/linux-arm64-gnu@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-TfJkMZjePbLiskmxFXVAbGI/OZtD+y+fwS0wyW8O6DWG0ARTf0AipY9zGwGoOdpFuXOJceXvN4SHGLbYNDMY4Q=="],
206
207 "@oxfmt/linux-arm64-musl": ["@oxfmt/linux-arm64-musl@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-7fyQUdW203v4WWGr1T3jwTz4L7KX9y5DeATryQ6fLT6QQp9GEuct8/k0lYhd+ys42iTV/IkJF20e3YkfSOOILg=="],
208
209 "@oxfmt/linux-x64-gnu": ["@oxfmt/linux-x64-gnu@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-sRKqAvEonuz0qr1X1ncUZceOBJerKzkO2gZIZmosvy/JmqyffpIFL3OE2tqacFkeDhrC+dNYQpusO8zsfHo3pw=="],
210
211 "@oxfmt/linux-x64-musl": ["@oxfmt/linux-x64-musl@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-fW6czbXutX/tdQe8j4nSIgkUox9RXqjyxwyWXUDItpoDkoXllq17qbD7GVc0whrEhYQC6hFE1UEAcDypLJoSzw=="],
212
213 "@oxfmt/win32-arm64": ["@oxfmt/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-D/HDeQBAQRjTbD9OLV6kRDcStrIfO+JsUODDCdGmhRfNX8LPCx95GpfyybpZfn3wVF8Jq/yjPXV1xLkQ+s7RcA=="],
214
215 "@oxfmt/win32-x64": ["@oxfmt/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-4+S2j4OxOIyo8dz5osm5dZuL0yVmxXvtmNdHB5xyGwAWVvyWNvf7tCaQD7w2fdSsAXQLOvK7KFQrHFe33nJUCA=="],
216
217 "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.43.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C/GhObv/pQZg34NOzB6Mk8x0wc9AKj8fXzJF8ZRKTsBPyHusC6AZ6bba0QG0TUufw1KWuD0j++oebQfWeiFXNw=="],
218
219 "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.43.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-4NjfUtEEH8ewRQ2KlZGmm6DyrvypMdHwBnQT92vD0dLScNOQzr0V9O8Ua4IWXdeCNl/XMVhAV3h4/3YEYern5A=="],
220
221 "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.43.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-75tf1HvwdZ3ebk83yMbSB+moAEWK98mYqpXiaFAi6Zshie7r+Cx5PLXZFUEqkscenoZ+fcNXakHxfn94V6nf1g=="],
222
223 "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.43.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-BHV4fb36T2p/7bpA9fiJ5ayt7oJbiYX10nklW5arYp4l9/9yG/FQC5J4G1evzbJ/YbipF9UH0vYBAm5xbqGrvw=="],
224
225 "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.43.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1l3nvnzWWse1YHibzZ4HQXdF/ibfbKZhp9IguElni3bBqEyPEyurzZ0ikWynDxKGXqZa+UNXTFuU1NRVX1RJ3g=="],
226
227 "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.43.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+jNYgLGRFTJxJuaSOZJBwlYo5M0TWRw0+3y5MHOL4ArrIdHyCthg6r4RbVWrsR1qUfUE1VSSHQ2bfbC99RXqMg=="],
228
229 "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.43.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-dvs1C/HCjCyGTURMagiHprsOvVTT3omDiSzi5Qw0D4QFJ1pEaNlfBhVnOUYgUfS6O7Mcmj4+G+sidRsQcWQ/kA=="],
230
231 "@oxlint/win32-x64": ["@oxlint/win32-x64@1.43.0", "", { "os": "win32", "cpu": "x64" }, "sha512-bSuItSU8mTSDsvmmLTepTdCL2FkJI6dwt9tot/k0EmiYF+ArRzmsl4lXVLssJNRV5lJEc5IViyTrh7oiwrjUqA=="],
232
219 "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],233 "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
220234
221 "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],235 "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
...@@ -470,8 +484,6 @@...@@ -470,8 +484,6 @@
470484
471 "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],485 "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
472486
473 "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="],
474
475 "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],487 "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
476488
477 "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],489 "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
...@@ -904,6 +916,10 @@...@@ -904,6 +916,10 @@
904916
905 "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],917 "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
906918
919 "oxfmt": ["oxfmt@0.28.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.28.0", "@oxfmt/darwin-x64": "0.28.0", "@oxfmt/linux-arm64-gnu": "0.28.0", "@oxfmt/linux-arm64-musl": "0.28.0", "@oxfmt/linux-x64-gnu": "0.28.0", "@oxfmt/linux-x64-musl": "0.28.0", "@oxfmt/win32-arm64": "0.28.0", "@oxfmt/win32-x64": "0.28.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-3+hhBqPE6Kp22KfJmnstrZbl+KdOVSEu1V0ABaFIg1rYLtrMgrupx9znnHgHLqKxAVHebjTdiCJDk30CXOt6cw=="],
920
921 "oxlint": ["oxlint@1.43.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.43.0", "@oxlint/darwin-x64": "1.43.0", "@oxlint/linux-arm64-gnu": "1.43.0", "@oxlint/linux-arm64-musl": "1.43.0", "@oxlint/linux-x64-gnu": "1.43.0", "@oxlint/linux-x64-musl": "1.43.0", "@oxlint/win32-arm64": "1.43.0", "@oxlint/win32-x64": "1.43.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.11.2" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-xiqTCsKZch+R61DPCjyqUVP2MhkQlRRYxLRBeBDi+dtQJ90MOgdcjIktvDCgXz0bgtx94EQzHEndsizZjMX2OA=="],
922
907 "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],923 "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
908924
909 "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],925 "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],
...@@ -1014,8 +1030,6 @@...@@ -1014,8 +1030,6 @@
10141030
1015 "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],1031 "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
10161032
1017 "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
1018
1019 "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],1033 "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
10201034
1021 "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],1035 "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
...@@ -1058,7 +1072,7 @@...@@ -1058,7 +1072,7 @@
10581072
1059 "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="],1073 "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="],
10601074
1061 "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],1075 "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
10621076
1063 "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],1077 "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
10641078
...@@ -1076,7 +1090,7 @@...@@ -1076,7 +1090,7 @@
10761090
1077 "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],1091 "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
10781092
1079 "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],1093 "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
10801094
1081 "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],1095 "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
10821096
...@@ -1210,8 +1224,6 @@...@@ -1210,8 +1224,6 @@
12101224
1211 "cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],1225 "cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
12121226
1213 "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
1214
1215 "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],1227 "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
12161228
1217 "electron/@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="],1229 "electron/@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="],
...@@ -1246,14 +1258,14 @@...@@ -1246,14 +1258,14 @@
12461258
1247 "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],1259 "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
12481260
1261 "vitest/tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
1262
1249 "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],1263 "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
12501264
1251 "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],1265 "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
12521266
1253 "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],1267 "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
12541268
1255 "electronmon/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
1256
1257 "rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],1269 "rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
12581270
1259 "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],1271 "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
package.json+13-7
...@@ -1,23 +1,29 @@...@@ -1,23 +1,29 @@
1{1{
2 "name": "ct-round-5",2 "name": "ct-round-5",
3 "private": true,3 "private": true,
4 "packageManager": "bun@1.1.38",4 "workspaces": [
5 "workspaces": ["apps/*", "packages/*"],5 "apps/*",
6 "packages/*"
7 ],
6 "scripts": {8 "scripts": {
7 "dev": "bun run build:contracts && turbo run dev --parallel",9 "dev": "bun run build:contracts && turbo run dev --parallel",
8 "build": "turbo run build",10 "build": "turbo run build",
9 "typecheck": "turbo run typecheck",11 "typecheck": "turbo run typecheck",
10 "lint": "turbo run lint",12 "lint": "oxlint --report-unused-disable-directives",
11 "test": "turbo run test",13 "test": "turbo run test",
12 "format": "biome format --write .",14 "fmt": "oxfmt",
13 "check": "biome check .",
14 "smoke-test": "turbo run smoke-test",15 "smoke-test": "turbo run smoke-test",
15 "build:contracts": "bun run --cwd packages/contracts build"16 "build:contracts": "bun run --cwd packages/contracts build"
16 },17 },
17 "devDependencies": {18 "devDependencies": {
18 "@biomejs/biome": "^1.9.0",19 "oxfmt": "^0.28.0",
20 "oxlint": "^1.43.0",
19 "turbo": "^2.3.3",21 "turbo": "^2.3.3",
20 "typescript": "^5.7.3",22 "typescript": "^5.7.3",
21 "vitest": "^3.0.0"23 "vitest": "^3.0.0"
22 }24 },
25 "engines": {
26 "bun": ">=1.3.9"
27 },
28 "packageManager": "bun@1.3.9"
23}29}
packages/contracts/package.json+3-2
...@@ -2,6 +2,9 @@...@@ -2,6 +2,9 @@
2 "name": "@acme/contracts",2 "name": "@acme/contracts",
3 "version": "0.0.0",3 "version": "0.0.0",
4 "private": true,4 "private": true,
5 "files": [
6 "dist"
7 ],
5 "type": "module",8 "type": "module",
6 "main": "./dist/index.cjs",9 "main": "./dist/index.cjs",
7 "module": "./dist/index.js",10 "module": "./dist/index.js",
...@@ -13,12 +16,10 @@...@@ -13,12 +16,10 @@
13 "require": "./dist/index.cjs"16 "require": "./dist/index.cjs"
14 }17 }
15 },18 },
16 "files": ["dist"],
17 "scripts": {19 "scripts": {
18 "dev": "tsup src/index.ts --format esm,cjs --dts --watch --clean",20 "dev": "tsup src/index.ts --format esm,cjs --dts --watch --clean",
19 "build": "tsup src/index.ts --format esm,cjs --dts --clean",21 "build": "tsup src/index.ts --format esm,cjs --dts --clean",
20 "typecheck": "tsc --noEmit",22 "typecheck": "tsc --noEmit",
21 "lint": "biome check src/",
22 "test": "vitest run"23 "test": "vitest run"
23 },24 },
24 "dependencies": {25 "dependencies": {
packages/contracts/src/ipc.ts+2-6
...@@ -62,12 +62,8 @@ export interface NativeApi {...@@ -62,12 +62,8 @@ export interface NativeApi {
62 onExit: (callback: (exit: AgentExit) => void) => () => void;62 onExit: (callback: (exit: AgentExit) => void) => () => void;
63 };63 };
64 providers: {64 providers: {
65 startSession: (65 startSession: (input: ProviderSessionStartInput) => Promise<ProviderSession>;
66 input: ProviderSessionStartInput,66 sendTurn: (input: ProviderSendTurnInput) => Promise<ProviderTurnStartResult>;
67 ) => Promise<ProviderSession>;
68 sendTurn: (
69 input: ProviderSendTurnInput,
70 ) => Promise<ProviderTurnStartResult>;
71 interruptTurn: (input: ProviderInterruptTurnInput) => Promise<void>;67 interruptTurn: (input: ProviderInterruptTurnInput) => Promise<void>;
72 respondToRequest: (input: ProviderRespondToRequestInput) => Promise<void>;68 respondToRequest: (input: ProviderRespondToRequestInput) => Promise<void>;
73 stopSession: (input: ProviderStopSessionInput) => Promise<void>;69 stopSession: (input: ProviderStopSessionInput) => Promise<void>;
packages/contracts/src/model.test.ts+1-6
...@@ -1,11 +1,6 @@...@@ -1,11 +1,6 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
22
3import {3import { DEFAULT_MODEL, MODEL_OPTIONS, normalizeModelSlug, resolveModelSlug } from "./model";
4 DEFAULT_MODEL,
5 MODEL_OPTIONS,
6 normalizeModelSlug,
7 resolveModelSlug,
8} from "./model";
94
10describe("normalizeModelSlug", () => {5describe("normalizeModelSlug", () => {
11 it("maps known aliases to canonical slugs", () => {6 it("maps known aliases to canonical slugs", () => {
packages/contracts/src/model.ts+2-8
...@@ -1,8 +1,4 @@...@@ -1,8 +1,4 @@
1export const MODEL_OPTIONS = [1export const MODEL_OPTIONS = ["gpt-5.3-codex", "gpt-5.2-codex", "gpt-5.2"] as const;
2 "gpt-5.3-codex",
3 "gpt-5.2-codex",
4 "gpt-5.2",
5] as const;
62
7export const DEFAULT_MODEL = "gpt-5.3-codex";3export const DEFAULT_MODEL = "gpt-5.3-codex";
84
...@@ -11,9 +7,7 @@ export const MODEL_SLUG_ALIASES: Record<string, string> = {...@@ -11,9 +7,7 @@ export const MODEL_SLUG_ALIASES: Record<string, string> = {
11 "gpt-5.3": "gpt-5.3-codex",7 "gpt-5.3": "gpt-5.3-codex",
12};8};
139
14export function normalizeModelSlug(10export function normalizeModelSlug(model: string | null | undefined): string | null {
15 model: string | null | undefined,
16): string | null {
17 if (typeof model !== "string") {11 if (typeof model !== "string") {
18 return null;12 return null;
19 }13 }
packages/contracts/src/provider.ts+2-6
...@@ -105,9 +105,7 @@ export const providerEventSchema = z.object({...@@ -105,9 +105,7 @@ export const providerEventSchema = z.object({
105});105});
106106
107export type ProviderKind = z.infer<typeof providerKindSchema>;107export type ProviderKind = z.infer<typeof providerKindSchema>;
108export type ProviderApprovalPolicy = z.infer<108export type ProviderApprovalPolicy = z.infer<typeof providerApprovalPolicySchema>;
109 typeof providerApprovalPolicySchema
110>;
111export type ProviderSandboxMode = z.infer<typeof providerSandboxModeSchema>;109export type ProviderSandboxMode = z.infer<typeof providerSandboxModeSchema>;
112export type ProviderRequestKind = z.infer<typeof providerRequestKindSchema>;110export type ProviderRequestKind = z.infer<typeof providerRequestKindSchema>;
113export type ProviderApprovalDecision = z.infer<111export type ProviderApprovalDecision = z.infer<
...@@ -115,9 +113,7 @@ export type ProviderApprovalDecision = z.infer<...@@ -115,9 +113,7 @@ export type ProviderApprovalDecision = z.infer<
115>;113>;
116export type ProviderSessionStatus = z.infer<typeof providerSessionStatusSchema>;114export type ProviderSessionStatus = z.infer<typeof providerSessionStatusSchema>;
117export type ProviderSession = z.infer<typeof providerSessionSchema>;115export type ProviderSession = z.infer<typeof providerSessionSchema>;
118export type ProviderSessionStartInput = z.input<116export type ProviderSessionStartInput = z.input<typeof providerSessionStartInputSchema>;
119 typeof providerSessionStartInputSchema
120>;
121export type ProviderSendTurnInput = z.input<typeof providerSendTurnInputSchema>;117export type ProviderSendTurnInput = z.input<typeof providerSendTurnInputSchema>;
122export type ProviderTurnStartResult = z.infer<118export type ProviderTurnStartResult = z.infer<
123 typeof providerTurnStartResultSchema119 typeof providerTurnStartResultSchema
tsconfig.base.json+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1{1{
2 "compilerOptions": {2 "compilerOptions": {
3 "target": "ES2022",3 "target": "ES2023",
4 "module": "ESNext",4 "module": "ESNext",
5 "moduleResolution": "Bundler",5 "moduleResolution": "Bundler",
6 "strict": true,6 "strict": true,
turbo.json-4
...@@ -14,10 +14,6 @@...@@ -14,10 +14,6 @@
14 "dependsOn": ["^typecheck"],14 "dependsOn": ["^typecheck"],
15 "outputs": []15 "outputs": []
16 },16 },
17 "lint": {
18 "dependsOn": ["^lint"],
19 "outputs": []
20 },
21 "test": {17 "test": {
22 "dependsOn": ["^build"],18 "dependsOn": ["^build"],
23 "outputs": []19 "outputs": []