Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions apps/server/src/codexAppServerManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,8 @@ describe("startSession", () => {
it("enables Codex experimental api capabilities during initialize", () => {
expect(buildCodexInitializeParams()).toEqual({
clientInfo: {
name: "t3code_desktop",
title: "T3 Code Desktop",
name: "v3copilot_desktop",
title: "V3 Copilot Desktop",
version: "0.1.0",
},
capabilities: {
Expand Down Expand Up @@ -341,7 +341,7 @@ describe("startSession", () => {
)
.mockImplementation(() => {
throw new Error(
"Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.",
"Codex CLI v0.36.0 is too old for V3 Copilot. Upgrade to v0.37.0 or newer and restart V3 Copilot.",
);
});

Expand All @@ -353,15 +353,15 @@ describe("startSession", () => {
runtimeMode: "full-access",
}),
).rejects.toThrow(
"Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.",
"Codex CLI v0.36.0 is too old for V3 Copilot. Upgrade to v0.37.0 or newer and restart V3 Copilot.",
);
expect(versionCheck).toHaveBeenCalledTimes(1);
expect(events).toEqual([
{
method: "session/startFailed",
kind: "error",
message:
"Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.",
"Codex CLI v0.36.0 is too old for V3 Copilot. Upgrade to v0.37.0 or newer and restart V3 Copilot.",
},
]);
} finally {
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/codexAppServerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,9 @@ export function normalizeCodexModelSlug(
export function buildCodexInitializeParams() {
return {
clientInfo: {
name: "t3code_desktop",
name: "v3copilot_desktop",
title: "V3 Copilot Desktop",
version: "0.0.0",
version: "0.1.0",
},
capabilities: {
experimentalApi: true,
Expand Down
20 changes: 19 additions & 1 deletion apps/server/src/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/
import { spawn } from "node:child_process";
import { accessSync, constants, statSync } from "node:fs";
import { extname, join } from "node:path";
import { extname, isAbsolute, join } from "node:path";

import { EDITORS, type EditorId } from "@t3tools/contracts";
import { ServiceMap, Schema, Effect, Layer } from "effect";
Expand All @@ -25,6 +25,7 @@ export class OpenError extends Schema.TaggedErrorClass<OpenError>()("OpenError",
export interface OpenInEditorInput {
readonly cwd: string;
readonly editor: EditorId;
readonly executablePath?: string | undefined;
}

interface EditorLaunch {
Expand Down Expand Up @@ -207,6 +208,23 @@ export const resolveEditorLaunch = Effect.fnUntraced(function* (
input: OpenInEditorInput,
platform: NodeJS.Platform = process.platform,
): Effect.fn.Return<EditorLaunch, OpenError> {
const explicitExecutablePath = input.executablePath?.trim();
if (explicitExecutablePath && explicitExecutablePath.length > 0) {
if (!isAbsolute(explicitExecutablePath)) {
return yield* new OpenError({
message: `Executable path must be absolute: ${explicitExecutablePath}`,
});
}
if (!isCommandAvailable(explicitExecutablePath, { platform })) {
return yield* new OpenError({
message: `Editor executable path is not runnable: ${explicitExecutablePath}`,
});
}
return shouldUseGotoFlag(input.editor, input.cwd)
? { command: explicitExecutablePath, args: ["--goto", input.cwd] }
: { command: explicitExecutablePath, args: [input.cwd] };
}

const editorDef = EDITORS.find((editor) => editor.id === input.editor);
if (!editorDef) {
return yield* new OpenError({ message: `Unknown editor: ${input.editor}` });
Expand Down
8 changes: 4 additions & 4 deletions apps/server/src/provider/Layers/CopilotAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1592,10 +1592,10 @@ const makeCopilotAdapter = (options?: CopilotAdapterLiveOptions) =>
resumeCursor: record.session.sessionId,
createdAt: record.createdAt,
updatedAt: record.updatedAt,
...(record.cwd ? { cwd: record.cwd } : {}),
...(record.model ? { model: record.model } : {}),
...(record.currentTurnId ? { activeTurnId: record.currentTurnId } : {}),
...(record.lastError ? { lastError: record.lastError } : {}),
cwd: record.cwd,
model: record.model,
activeTurnId: record.currentTurnId,
lastError: record.lastError,
}) satisfies ProviderSession,
),
);
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/Layers/ProviderHealth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ it.layer(NodeServices.layer)("ProviderHealth", (it) => {
assert.strictEqual(status.authStatus, "unknown");
assert.strictEqual(
status.message,
"Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.",
"Codex CLI v0.36.0 is too old for V3 Copilot. Upgrade to v0.37.0 or newer and restart V3 Copilot.",
);
}).pipe(
Effect.provide(
Expand Down
30 changes: 21 additions & 9 deletions apps/web/src/appSettings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useSyncExternalStore } from "react";
import { Option, Schema } from "effect";
import { TrimmedNonEmptyString, type ProviderKind } from "@t3tools/contracts";
import { EditorId, TrimmedNonEmptyString, type ProviderKind } from "@t3tools/contracts";
import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";

const APP_SETTINGS_STORAGE_KEY = "t3code:app-settings:v1";
Expand Down Expand Up @@ -43,6 +43,12 @@ const AppSettingsSchema = Schema.Struct({
customCopilotModels: Schema.Array(Schema.String).pipe(
Schema.withConstructorDefault(() => Option.some([])),
),
preferredEditor: Schema.NullOr(EditorId).pipe(
Schema.withConstructorDefault(() => Option.some(null)),
),
preferredEditorExecutablePath: Schema.String.check(Schema.isMaxLength(4096)).pipe(
Schema.withConstructorDefault(() => Option.some("")),
),
textGenerationModel: Schema.optional(TrimmedNonEmptyString),
});
export type AppSettings = typeof AppSettingsSchema.Type;
Expand Down Expand Up @@ -100,6 +106,7 @@ function normalizeAppSettings(settings: AppSettings): AppSettings {
...settings,
customCodexModels: normalizeCustomModelSlugs(settings.customCodexModels, "codex"),
customCopilotModels: normalizeCustomModelSlugs(settings.customCopilotModels, "copilot"),
preferredEditorExecutablePath: settings.preferredEditorExecutablePath.trim(),
};
}

Expand Down Expand Up @@ -245,6 +252,18 @@ function persistSettings(next: AppSettings): void {
cachedSnapshot = next;
}

export function updateAppSettings(patch: Partial<AppSettings>): AppSettings {
const next = normalizeAppSettings(
Schema.decodeSync(AppSettingsSchema)({
...getAppSettingsSnapshot(),
...patch,
}),
);
persistSettings(next);
emitChange();
return next;
}

function subscribe(listener: () => void): () => void {
listeners.push(listener);

Expand All @@ -269,14 +288,7 @@ export function useAppSettings() {
);

const updateSettings = useCallback((patch: Partial<AppSettings>) => {
const next = normalizeAppSettings(
Schema.decodeSync(AppSettingsSchema)({
...getAppSettingsSnapshot(),
...patch,
}),
);
persistSettings(next);
emitChange();
updateAppSettings(patch);
}, []);

const resetSettings = useCallback(() => {
Expand Down
16 changes: 14 additions & 2 deletions apps/web/src/components/ChatMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { LRUCache } from "../lib/lruCache";
import { useTheme } from "../hooks/useTheme";
import { resolveMarkdownFileLinkTarget } from "../markdown-links";
import { readNativeApi } from "../nativeApi";
import { toastManager } from "./ui/toast";

class CodeHighlightErrorBoundary extends React.Component<
{ fallback: ReactNode; children: ReactNode },
Expand Down Expand Up @@ -255,9 +256,20 @@ function ChatMarkdown({ text, cwd, isStreaming = false }: ChatMarkdownProps) {
event.stopPropagation();
const api = readNativeApi();
if (api) {
void openInPreferredEditor(api, targetPath);
void openInPreferredEditor(api, targetPath).catch((error) => {
toastManager.add({
type: "error",
title: "Unable to open file",
description:
error instanceof Error ? error.message : "Unknown editor launch error.",
});
});
} else {
console.warn("Native API not found. Unable to open file in editor.");
toastManager.add({
type: "error",
title: "Editor opening is unavailable",
description: "Native API is not available in this environment.",
});
}
}}
/>
Expand Down
Loading