Skip to content

Commit 8dfa886

Browse files
authored
Merge pull request #21 from OpenKnots/okcode/readme-onboarding-edgecases
Add provider onboarding and doctor diagnostics
2 parents bb85ada + 337dcfc commit 8dfa886

5 files changed

Lines changed: 306 additions & 8 deletions

File tree

README.md

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,60 @@ A minimal web GUI for coding agents. Currently supports Codex and Claude, with m
44

55
## Quick Start
66

7-
> [!WARNING]
8-
> You need [Codex CLI](https://github.com/openai/codex) installed and authorized for OK Code to work.
9-
107
```bash
118
npx okcode
129
```
1310

11+
This starts the OK Code server and opens your browser. The app automatically detects which providers you have installed.
12+
1413
Or install the [desktop app from the Releases page](https://github.com/OpenKnots/okcode/releases).
1514

15+
### Provider Setup
16+
17+
OK Code supports multiple AI providers. You need **at least one** configured to start coding.
18+
19+
<details>
20+
<summary><strong>Option A: OpenAI (Codex CLI)</strong></summary>
21+
22+
1. Install: `npm install -g @openai/codex`
23+
2. Authenticate: `codex login`
24+
3. Verify: `codex login status`
25+
26+
If using a custom model provider (Azure OpenAI, Portkey, etc.), configure `model_provider` in `~/.codex/config.toml` instead — no `codex login` needed.
27+
28+
</details>
29+
30+
<details>
31+
<summary><strong>Option B: Anthropic (Claude Code)</strong></summary>
32+
33+
1. Install: `npm install -g @anthropic-ai/claude-code`
34+
2. Authenticate: `claude auth login`
35+
3. Verify: `claude auth status`
36+
37+
</details>
38+
39+
> [!TIP]
40+
> You can install both providers and switch between them per session.
41+
42+
### Troubleshooting
43+
44+
Run the built-in diagnostic to check your setup:
45+
46+
```bash
47+
npx okcode doctor
48+
```
49+
50+
If OK Code shows a provider error banner after launch:
51+
52+
| Banner message | Fix |
53+
| ------------------------------ | ------------------------------------------------- |
54+
| "not installed or not on PATH" | Install the CLI (see above), then restart OK Code |
55+
| "not authenticated" | Run the login command for that provider |
56+
| "version check failed" | Update the CLI to the latest version |
57+
58+
> [!NOTE]
59+
> OK Code launches even without providers configured — you can explore the UI and configure provider binary paths from **Settings** before starting a session.
60+
1661
## Development Setup
1762

1863
**Prerequisites**: [Bun](https://bun.sh) >= 1.3.9, [Node.js](https://nodejs.org) >= 24.13.1

apps/server/src/doctor.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* doctor - CLI diagnostic command.
3+
*
4+
* Runs provider health checks and prints a summary to the terminal
5+
* without starting the full server.
6+
*
7+
* @module doctor
8+
*/
9+
import { Effect } from "effect";
10+
import { Command } from "effect/unstable/cli";
11+
12+
import {
13+
checkCodexProviderStatus,
14+
checkClaudeProviderStatus,
15+
} from "./provider/Layers/ProviderHealth";
16+
import type { ServerProviderStatus } from "@okcode/contracts";
17+
import { fixPath } from "./os-jank";
18+
19+
const STATUS_ICONS: Record<string, string> = {
20+
ready: "\u2705",
21+
warning: "\u26A0\uFE0F",
22+
error: "\u274C",
23+
};
24+
25+
const AUTH_LABELS: Record<string, string> = {
26+
authenticated: "authenticated",
27+
unauthenticated: "not authenticated",
28+
unknown: "unknown",
29+
};
30+
31+
const PROVIDER_LABELS: Record<string, string> = {
32+
codex: "Codex (OpenAI)",
33+
claudeAgent: "Claude (Anthropic)",
34+
};
35+
36+
function printStatus(status: ServerProviderStatus): void {
37+
const icon = STATUS_ICONS[status.status] ?? "?";
38+
const label = PROVIDER_LABELS[status.provider] ?? status.provider;
39+
const auth = AUTH_LABELS[status.authStatus] ?? status.authStatus;
40+
41+
console.log("");
42+
console.log(` ${icon} ${label}`);
43+
console.log(` Status: ${status.status}`);
44+
console.log(` Auth: ${auth}`);
45+
if (status.message) {
46+
console.log(` Detail: ${status.message}`);
47+
}
48+
}
49+
50+
const doctorProgram = Effect.gen(function* () {
51+
// Fix PATH so CLIs installed via nvm/volta/etc. are reachable.
52+
fixPath();
53+
54+
console.log("OK Code Doctor");
55+
console.log("==============");
56+
console.log("");
57+
console.log("Checking provider health...");
58+
59+
const statuses = yield* Effect.all([checkCodexProviderStatus, checkClaudeProviderStatus], {
60+
concurrency: "unbounded",
61+
});
62+
63+
for (const status of statuses) {
64+
printStatus(status);
65+
}
66+
67+
const readyCount = statuses.filter((s) => s.status === "ready").length;
68+
console.log("");
69+
if (readyCount === 0) {
70+
console.log("No providers are ready. Set up at least one provider to start coding:");
71+
console.log("");
72+
console.log(" Codex: npm install -g @openai/codex && codex login");
73+
console.log(" Claude: npm install -g @anthropic-ai/claude-code && claude auth login");
74+
} else if (readyCount === statuses.length) {
75+
console.log("All providers are ready.");
76+
} else {
77+
console.log(`${readyCount} of ${statuses.length} providers ready.`);
78+
}
79+
console.log("");
80+
});
81+
82+
export const doctorCmd = Command.make("doctor").pipe(
83+
Command.withDescription("Check provider health and system requirements."),
84+
Command.withHandler(() => doctorProgram),
85+
);

apps/server/src/main.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { Server } from "./wsServer";
2727
import { ServerLoggerLive } from "./serverLogger";
2828
import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService";
2929
import { AnalyticsService } from "./telemetry/Services/AnalyticsService";
30+
import { doctorCmd } from "./doctor";
3031

3132
export class StartupError extends Data.TaggedError("StartupError")<{
3233
readonly message: string;
@@ -329,7 +330,7 @@ const logWebSocketEventsFlag = Flag.boolean("log-websocket-events").pipe(
329330
Flag.optional,
330331
);
331332

332-
export const okcodeCli = Command.make("okcode", {
333+
const serveCmd = Command.make("okcode", {
333334
mode: modeFlag,
334335
port: portFlag,
335336
host: hostFlag,
@@ -342,4 +343,7 @@ export const okcodeCli = Command.make("okcode", {
342343
}).pipe(
343344
Command.withDescription("Run the OK Code server."),
344345
Command.withHandler((input) => Effect.scoped(makeServerProgram(input))),
346+
Command.withSubcommands([doctorCmd]),
345347
);
348+
349+
export { serveCmd as okcodeCli };
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { type ServerProviderStatus } from "@okcode/contracts";
2+
import { memo, useState } from "react";
3+
import { useNavigate } from "@tanstack/react-router";
4+
import {
5+
CheckCircle2Icon,
6+
ChevronDownIcon,
7+
ChevronRightIcon,
8+
CircleAlertIcon,
9+
SettingsIcon,
10+
TerminalIcon,
11+
XCircleIcon,
12+
} from "lucide-react";
13+
import { Button } from "../ui/button";
14+
15+
const PROVIDER_CONFIG = {
16+
codex: {
17+
label: "OpenAI (Codex CLI)",
18+
installCmd: "npm install -g @openai/codex",
19+
authCmd: "codex login",
20+
verifyCmd: "codex login status",
21+
},
22+
claudeAgent: {
23+
label: "Anthropic (Claude Code)",
24+
installCmd: "npm install -g @anthropic-ai/claude-code",
25+
authCmd: "claude auth login",
26+
verifyCmd: "claude auth status",
27+
},
28+
} as const;
29+
30+
function StatusIcon({ status }: { status: ServerProviderStatus["status"] }) {
31+
switch (status) {
32+
case "ready":
33+
return <CheckCircle2Icon className="size-4 text-emerald-500" />;
34+
case "warning":
35+
return <CircleAlertIcon className="size-4 text-amber-500" />;
36+
case "error":
37+
return <XCircleIcon className="size-4 text-red-400" />;
38+
}
39+
}
40+
41+
function ProviderRow({ status }: { status: ServerProviderStatus }) {
42+
const [expanded, setExpanded] = useState(status.status !== "ready");
43+
const config = PROVIDER_CONFIG[status.provider as keyof typeof PROVIDER_CONFIG];
44+
if (!config) return null;
45+
46+
return (
47+
<div className="rounded-lg border border-border bg-card/50 p-3">
48+
<button
49+
type="button"
50+
className="flex w-full items-center gap-2.5 text-left text-sm"
51+
onClick={() => setExpanded((v) => !v)}
52+
>
53+
<StatusIcon status={status.status} />
54+
<span className="flex-1 font-medium text-foreground">{config.label}</span>
55+
{expanded ? (
56+
<ChevronDownIcon className="size-3.5 text-muted-foreground" />
57+
) : (
58+
<ChevronRightIcon className="size-3.5 text-muted-foreground" />
59+
)}
60+
</button>
61+
62+
{status.status !== "ready" && status.message && (
63+
<p className="mt-1.5 ml-6.5 text-xs text-muted-foreground">{status.message}</p>
64+
)}
65+
66+
{expanded && status.status !== "ready" && (
67+
<div className="mt-3 ml-6.5 space-y-2">
68+
<div className="space-y-1.5">
69+
<Step n={1} label="Install">
70+
<Code>{config.installCmd}</Code>
71+
</Step>
72+
<Step n={2} label="Authenticate">
73+
<Code>{config.authCmd}</Code>
74+
</Step>
75+
<Step n={3} label="Verify">
76+
<Code>{config.verifyCmd}</Code>
77+
</Step>
78+
</div>
79+
</div>
80+
)}
81+
82+
{status.status === "ready" && (
83+
<p className="mt-1 ml-6.5 text-xs text-emerald-600 dark:text-emerald-400">Ready</p>
84+
)}
85+
</div>
86+
);
87+
}
88+
89+
function Step({ n, label, children }: { n: number; label: string; children: React.ReactNode }) {
90+
return (
91+
<div className="flex items-baseline gap-2 text-xs">
92+
<span className="shrink-0 text-muted-foreground">
93+
{n}. {label}:
94+
</span>
95+
{children}
96+
</div>
97+
);
98+
}
99+
100+
function Code({ children }: { children: React.ReactNode }) {
101+
return (
102+
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[11px] text-foreground">
103+
{children}
104+
</code>
105+
);
106+
}
107+
108+
export const ProviderSetupCard = memo(function ProviderSetupCard({
109+
providers,
110+
}: {
111+
providers: ReadonlyArray<ServerProviderStatus>;
112+
}) {
113+
const navigate = useNavigate();
114+
const readyCount = providers.filter((p) => p.status === "ready").length;
115+
116+
// Don't show if all providers are ready
117+
if (readyCount === providers.length && providers.length > 0) {
118+
return null;
119+
}
120+
121+
return (
122+
<div className="w-full max-w-md space-y-4">
123+
<div className="space-y-1.5 text-center">
124+
<div className="mx-auto mb-3 flex size-10 items-center justify-center rounded-xl border border-border bg-card shadow-sm">
125+
<TerminalIcon className="size-5 text-foreground" />
126+
</div>
127+
<h2 className="text-lg font-semibold text-foreground">Set up a provider</h2>
128+
<p className="text-sm text-muted-foreground">
129+
{readyCount === 0
130+
? "Connect at least one AI provider to start coding."
131+
: `${readyCount} of ${providers.length} providers ready. Set up another provider or start coding.`}
132+
</p>
133+
</div>
134+
135+
<div className="space-y-2">
136+
{providers.map((status) => (
137+
<ProviderRow key={status.provider} status={status} />
138+
))}
139+
</div>
140+
141+
<div className="flex items-center justify-center gap-2 pt-1">
142+
<Button variant="outline" size="sm" onClick={() => void navigate({ to: "/settings" })}>
143+
<SettingsIcon />
144+
Settings
145+
</Button>
146+
</div>
147+
148+
<p className="text-center text-xs text-muted-foreground/60">
149+
Run <Code>npx okcode doctor</Code> to diagnose setup issues from the terminal.
150+
</p>
151+
</div>
152+
);
153+
});

apps/web/src/routes/_chat.index.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1+
import { useQuery } from "@tanstack/react-query";
12
import { createFileRoute } from "@tanstack/react-router";
23

34
import { isElectron } from "../env";
5+
import { ProviderSetupCard } from "../components/chat/ProviderSetupCard";
46
import { SidebarTrigger } from "../components/ui/sidebar";
7+
import { serverConfigQueryOptions } from "../lib/serverReactQuery";
58

69
function ChatIndexRouteView() {
10+
const serverConfigQuery = useQuery(serverConfigQueryOptions());
11+
const providers = serverConfigQuery.data?.providers ?? [];
12+
const hasReadyProvider = providers.some((p) => p.status === "ready");
13+
714
return (
815
<div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background text-muted-foreground/40">
916
{!isElectron && (
@@ -21,10 +28,14 @@ function ChatIndexRouteView() {
2128
</div>
2229
)}
2330

24-
<div className="flex flex-1 items-center justify-center">
25-
<div className="text-center">
26-
<p className="text-sm">Select a thread or create a new one to get started.</p>
27-
</div>
31+
<div className="flex flex-1 items-center justify-center p-6">
32+
{!hasReadyProvider && providers.length > 0 ? (
33+
<ProviderSetupCard providers={providers} />
34+
) : (
35+
<div className="text-center">
36+
<p className="text-sm">Select a thread or create a new one to get started.</p>
37+
</div>
38+
)}
2839
</div>
2940
</div>
3041
);

0 commit comments

Comments
 (0)