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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ bun run clean:workspaces # Clean all workspace node_modules
8. **Linear ticket format** - all tickets (creation, drafting, grooming) follow `.agents/skills/ticket-format/SKILL.md`. Read that file before creating or grooming a ticket.
9. **TanStack DB / Electric live queries are cache-first** - `useLiveQuery` can return persisted rows in `data` while the collection is still not `isReady`. Always render existing rows first. Use `isReady` only to decide what to show when no row/data exists yet: no data + not ready = loading/skeleton/null; no data + ready = empty/not-found. Never hide, blank, or replace existing `data` just because `isReady` is false or `isLoading` is true. This cache-first rendering rule does not apply to write/seeding side effects: wait for strict readiness before deriving missing rows or writing defaults, unless the write is provably idempotent.
10. **No agent attribution in git/gh** - Never mention Claude, Codex, or any AI agent in git or GitHub content — commit messages, PR titles/descriptions, issue text, review comments, branch names, etc. Omit `Co-Authored-By` agent trailers and "Generated with" / "🤖" lines. Write everything as the human developer.
11. **Fork delta tracking** - when a PR intentionally changes O3 Code behavior relative to upstream, update `docs/internal/fork-deltas/registry.md` with the PR link, affected area, short description, and upstream sync note. Keep the full implementation detail in the PR.


---
Expand Down
5 changes: 5 additions & 0 deletions apps/api/vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"ignoreCommand": "bash -lc 'repo=$(git rev-parse --show-toplevel) && exec bash \"$repo/scripts/vercel-ignore-if-unaffected.sh\" api'",
"installCommand": "bun install --frozen-lockfile --filter=@o3dotdev/code-api --concurrent-scripts=1"
}
2 changes: 2 additions & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@
"use-resize-observer": "9.1.0",
"utf-8-validate": "6.0.6",
"uuid": "14.0.0",
"ws": "8.20.0",
"zod": "4.3.6",
"zustand": "5.0.12"
},
Expand All @@ -259,6 +260,7 @@
"@types/react-syntax-highlighter": "15.5.13",
"@types/semver": "7.7.1",
"@types/shell-quote": "1.7.5",
"@types/ws": "8.18.1",
"@vitejs/plugin-react": "5.2.0",
"code-inspector-plugin": "1.4.5",
"cross-env": "10.1.0",
Expand Down
99 changes: 99 additions & 0 deletions apps/desktop/src/lib/trpc/routers/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ import { setupSingleAgent } from "main/lib/agent-setup";
import { hasCustomRingtone } from "main/lib/custom-ringtones";
import { getHostServiceCoordinator } from "main/lib/host-service-coordinator";
import { localDb } from "main/lib/local-db";
import {
normalizePersistedWebAccessSettings,
parseTrustedWebAccessOriginsInput,
} from "main/web-access/helpers";
import {
assertValidWebAccessPort,
getWebAccessSettingsResponse,
startWebAccessServer,
stopWebAccessServer,
} from "main/web-access/server";
import {
DEFAULT_AUTO_APPLY_DEFAULT_PRESET,
DEFAULT_CONFIRM_ON_QUIT,
Expand Down Expand Up @@ -100,6 +110,40 @@ function getSettings() {
return row;
}

const webAccessSettingsInputSchema = z.object({
enabled: z.boolean().optional(),
port: z.number().int().optional(),
trustedOrigins: z.array(z.string()).optional(),
});

function persistWebAccessSettings({
enabled,
port,
trustedOrigins,
}: {
enabled: boolean;
port: number;
trustedOrigins: string[];
}) {
localDb
.insert(settings)
.values({
id: 1,
webAccessEnabled: enabled,
webAccessPort: port,
webAccessTrustedOrigins: trustedOrigins,
})
.onConflictDoUpdate({
target: settings.id,
set: {
webAccessEnabled: enabled,
webAccessPort: port,
webAccessTrustedOrigins: trustedOrigins,
},
})
.run();
}

function readRawTerminalPresets(): PresetWithUnknownMode[] {
const row = getSettings();
return (row.terminalPresets ?? []) as PresetWithUnknownMode[];
Expand Down Expand Up @@ -675,6 +719,61 @@ export const createSettingsRouter = () => {
return { restartedOrgCount };
}),

getWebAccessSettings: publicProcedure.query(() => {
return getWebAccessSettingsResponse();
}),

setWebAccessSettings: publicProcedure
.input(webAccessSettingsInputSchema)
.mutation(async ({ input }) => {
const row = getSettings();
const current = normalizePersistedWebAccessSettings(row);
const next = {
enabled: input.enabled ?? current.enabled,
port: input.port ?? current.port,
trustedOrigins: current.trustedOrigins,
};

try {
assertValidWebAccessPort(next.port);
if (input.trustedOrigins) {
next.trustedOrigins = parseTrustedWebAccessOriginsInput(
input.trustedOrigins,
);
}
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
error instanceof Error
? error.message
: "Invalid Web Access settings.",
});
}

if (next.enabled) {
try {
await startWebAccessServer(next.port);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
error instanceof Error
? error.message
: "Failed to start Web Access.",
});
}
}

persistWebAccessSettings(next);

if (!next.enabled) {
await stopWebAccessServer();
}

return getWebAccessSettingsResponse();
}),

getShowPresetsBar: publicProcedure.query(() => {
const row = getSettings();
return row.showPresetsBar ?? DEFAULT_SHOW_PRESETS_BAR;
Expand Down
96 changes: 96 additions & 0 deletions apps/desktop/src/lib/trpc/routers/ui-state/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { EventEmitter } from "node:events";
import { observable } from "@trpc/server/observable";
import { appState } from "main/lib/app-state";
import type { TabsState, ThemeState } from "main/lib/app-state/schemas";
import {
applySharedUiStatePatch,
ensureSharedUiState,
SHARED_UI_COLLECTION_NAMES,
type SharedUiStateEvent,
} from "shared/shared-ui-state";
import { z } from "zod";
import { publicProcedure, router } from "../..";

Expand Down Expand Up @@ -231,6 +239,49 @@ const themeStateSchema = z.object({
systemDarkThemeId: z.string().optional(),
});

const sharedUiStateEmitter = new EventEmitter();

const sharedUiRouteStateSchema = z.object({
hashPath: z.string().min(1).startsWith("/"),
activeWorkspaceId: z.string().nullable(),
updatedAt: z.number().finite().nonnegative(),
});

const sharedUiCollectionRowsSchema = z.record(z.string(), z.unknown());

const sharedUiCollectionsPatchSchema = z
.object(
Object.fromEntries(
SHARED_UI_COLLECTION_NAMES.map((collectionName) => [
collectionName,
sharedUiCollectionRowsSchema.optional(),
]),
) as Record<
(typeof SHARED_UI_COLLECTION_NAMES)[number],
z.ZodOptional<typeof sharedUiCollectionRowsSchema>
>,
)
.partial();

const sharedUiPatchSchema = z
.object({
clientId: z.string().min(1),
route: sharedUiRouteStateSchema.nullable().optional(),
organizationId: z.string().min(1).optional(),
collections: sharedUiCollectionsPatchSchema.optional(),
})
.refine((input) => input.route !== undefined || input.collections, {
message: "At least one shared UI state field must be provided",
})
.refine((input) => !input.collections || input.organizationId, {
message: "organizationId is required when patching collections",
path: ["organizationId"],
});

const sharedUiSubscribeSchema = z.object({
clientId: z.string().min(1),
});

/**
* UI State router - manages tabs and theme persistence via lowdb
*/
Expand Down Expand Up @@ -272,5 +323,50 @@ export const createUiStateRouter = () => {
return appState.data.hotkeysState;
}),
}),

shared: router({
get: publicProcedure.query(() => {
appState.data.sharedUiState = ensureSharedUiState(
appState.data.sharedUiState,
);
return appState.data.sharedUiState;
}),

patch: publicProcedure
.input(sharedUiPatchSchema)
.mutation(async ({ input }) => {
const nextSnapshot = applySharedUiStatePatch(
ensureSharedUiState(appState.data.sharedUiState),
input,
);
appState.data.sharedUiState = nextSnapshot;
await appState.write();

sharedUiStateEmitter.emit("snapshot", {
type: "snapshot",
sourceClientId: input.clientId,
snapshot: nextSnapshot,
} satisfies SharedUiStateEvent);

return nextSnapshot;
}),

subscribe: publicProcedure
.input(sharedUiSubscribeSchema)
.subscription(({ input }) => {
return observable<SharedUiStateEvent>((emit) => {
const onSnapshot = (event: SharedUiStateEvent) => {
if (event.sourceClientId === input.clientId) return;
emit.next(event);
};

sharedUiStateEmitter.on("snapshot", onSnapshot);

return () => {
sharedUiStateEmitter.off("snapshot", onSnapshot);
};
});
}),
}),
});
};
Loading
Loading