Skip to content

Commit 981210b

Browse files
authored
Merge branch 'main' into fix/deepseek-provider
2 parents 6ded550 + 7fdb768 commit 981210b

File tree

24 files changed

+2294
-361
lines changed

24 files changed

+2294
-361
lines changed

.githooks/pre-commit

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
#!/bin/bash
1+
#!/bin/sh
22

3-
set -euo pipefail
3+
set -eu
44

5-
mapfile -t staged_rust_files < <(git diff --cached --name-only --diff-filter=ACMR -- '*.rs')
5+
staged_rust_files=$(git diff --cached --name-only --diff-filter=ACMR -- '*.rs')
66

7-
if [ "${#staged_rust_files[@]}" -eq 0 ]; then
7+
if [ -z "$staged_rust_files" ]; then
88
exit 0
99
fi
1010

11-
if ! git diff --quiet -- "${staged_rust_files[@]}"; then
11+
if ! echo "$staged_rust_files" | xargs git diff --quiet --; then
1212
echo "pre-commit: staged Rust files have unstaged changes." >&2
1313
echo "Please stage or stash those changes, then commit again." >&2
1414
exit 1
@@ -17,4 +17,4 @@ fi
1717
echo "pre-commit: running cargo fmt --all"
1818
cargo fmt --all
1919

20-
git add -- "${staged_rust_files[@]}"
20+
echo "$staged_rust_files" | xargs git add --

docs/design-docs/workers-tab.md

Lines changed: 509 additions & 0 deletions
Large diffs are not rendered by default.

interface/src/api/client.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,7 @@ export interface CronExecutionsParams {
661661
export interface ProviderStatus {
662662
anthropic: boolean;
663663
openai: boolean;
664+
openai_chatgpt: boolean;
664665
openrouter: boolean;
665666
zhipu: boolean;
666667
groq: boolean;
@@ -669,10 +670,12 @@ export interface ProviderStatus {
669670
deepseek: boolean;
670671
xai: boolean;
671672
mistral: boolean;
673+
gemini: boolean;
672674
ollama: boolean;
673675
opencode_zen: boolean;
674676
nvidia: boolean;
675677
minimax: boolean;
678+
minimax_cn: boolean;
676679
moonshot: boolean;
677680
zai_coding_plan: boolean;
678681
}
@@ -695,6 +698,20 @@ export interface ProviderModelTestResponse {
695698
sample: string | null;
696699
}
697700

701+
export interface OpenAiOAuthBrowserStartResponse {
702+
success: boolean;
703+
message: string;
704+
authorization_url: string | null;
705+
state: string | null;
706+
}
707+
708+
export interface OpenAiOAuthBrowserStatusResponse {
709+
found: boolean;
710+
done: boolean;
711+
success: boolean;
712+
message: string | null;
713+
}
714+
698715
// -- Model Types --
699716

700717
export interface ModelInfo {
@@ -1153,6 +1170,28 @@ export const api = {
11531170
}
11541171
return response.json() as Promise<ProviderModelTestResponse>;
11551172
},
1173+
startOpenAiOAuthBrowser: async (params: {model: string}) => {
1174+
const response = await fetch(`${API_BASE}/providers/openai/oauth/browser/start`, {
1175+
method: "POST",
1176+
headers: { "Content-Type": "application/json" },
1177+
body: JSON.stringify({
1178+
model: params.model,
1179+
}),
1180+
});
1181+
if (!response.ok) {
1182+
throw new Error(`API error: ${response.status}`);
1183+
}
1184+
return response.json() as Promise<OpenAiOAuthBrowserStartResponse>;
1185+
},
1186+
openAiOAuthBrowserStatus: async (state: string) => {
1187+
const response = await fetch(
1188+
`${API_BASE}/providers/openai/oauth/browser/status?state=${encodeURIComponent(state)}`,
1189+
);
1190+
if (!response.ok) {
1191+
throw new Error(`API error: ${response.status}`);
1192+
}
1193+
return response.json() as Promise<OpenAiOAuthBrowserStatusResponse>;
1194+
},
11561195
removeProvider: async (provider: string) => {
11571196
const response = await fetch(`${API_BASE}/providers/${encodeURIComponent(provider)}`, {
11581197
method: "DELETE",

interface/src/components/CortexChatPanel.tsx

Lines changed: 131 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ function ToolActivityIndicator({ activity }: { activity: ToolActivity[] }) {
1515
if (activity.length === 0) return null;
1616

1717
return (
18-
<div className="flex flex-col gap-1 px-3 py-2">
18+
<div className="flex flex-wrap items-center gap-1.5 mt-2">
1919
{activity.map((tool, index) => (
20-
<div
20+
<span
2121
key={`${tool.tool}-${index}`}
22-
className="flex items-center gap-2 rounded bg-app-darkBox/40 px-2 py-1"
22+
className="inline-flex items-center gap-1.5 rounded-full bg-app-box/60 px-2.5 py-0.5"
2323
>
2424
{tool.status === "running" ? (
2525
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-amber-400" />
@@ -28,60 +28,141 @@ function ToolActivityIndicator({ activity }: { activity: ToolActivity[] }) {
2828
)}
2929
<span className="font-mono text-tiny text-ink-faint">{tool.tool}</span>
3030
{tool.status === "done" && tool.result_preview && (
31-
<span className="min-w-0 flex-1 truncate text-tiny text-ink-faint/60">
31+
<span className="min-w-0 max-w-[120px] truncate text-tiny text-ink-faint/60">
3232
{tool.result_preview.slice(0, 80)}
3333
</span>
3434
)}
35-
</div>
35+
</span>
3636
))}
3737
</div>
3838
);
3939
}
4040

41+
function ThinkingIndicator() {
42+
return (
43+
<div className="flex items-center gap-1.5 py-1">
44+
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-ink-faint" />
45+
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-ink-faint [animation-delay:0.2s]" />
46+
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-ink-faint [animation-delay:0.4s]" />
47+
</div>
48+
);
49+
}
50+
51+
function CortexChatInput({
52+
value,
53+
onChange,
54+
onSubmit,
55+
isStreaming,
56+
}: {
57+
value: string;
58+
onChange: (value: string) => void;
59+
onSubmit: () => void;
60+
isStreaming: boolean;
61+
}) {
62+
const textareaRef = useRef<HTMLTextAreaElement>(null);
63+
64+
useEffect(() => {
65+
textareaRef.current?.focus();
66+
}, []);
67+
68+
useEffect(() => {
69+
const textarea = textareaRef.current;
70+
if (!textarea) return;
71+
72+
const adjustHeight = () => {
73+
textarea.style.height = "auto";
74+
const scrollHeight = textarea.scrollHeight;
75+
const maxHeight = 160;
76+
textarea.style.height = `${Math.min(scrollHeight, maxHeight)}px`;
77+
textarea.style.overflowY = scrollHeight > maxHeight ? "auto" : "hidden";
78+
};
79+
80+
adjustHeight();
81+
textarea.addEventListener("input", adjustHeight);
82+
return () => textarea.removeEventListener("input", adjustHeight);
83+
}, [value]);
84+
85+
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
86+
if (event.key === "Enter" && !event.shiftKey) {
87+
event.preventDefault();
88+
onSubmit();
89+
}
90+
};
91+
92+
return (
93+
<div className="rounded-xl border border-app-line/50 bg-app-box/40 backdrop-blur-xl transition-colors duration-200 hover:border-app-line/70">
94+
<div className="flex items-end gap-2 p-2.5">
95+
<textarea
96+
ref={textareaRef}
97+
value={value}
98+
onChange={(event) => onChange(event.target.value)}
99+
onKeyDown={handleKeyDown}
100+
placeholder={isStreaming ? "Waiting for response..." : "Message the cortex..."}
101+
disabled={isStreaming}
102+
rows={1}
103+
className="flex-1 resize-none bg-transparent px-1 py-1 text-sm text-ink placeholder:text-ink-faint/60 focus:outline-none disabled:opacity-40"
104+
style={{ maxHeight: "160px" }}
105+
/>
106+
<button
107+
type="button"
108+
onClick={onSubmit}
109+
disabled={isStreaming || !value.trim()}
110+
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-accent text-white transition-all duration-150 hover:bg-accent-deep disabled:opacity-30 disabled:hover:bg-accent"
111+
>
112+
<svg
113+
width="14"
114+
height="14"
115+
viewBox="0 0 24 24"
116+
fill="none"
117+
stroke="currentColor"
118+
strokeWidth="2"
119+
strokeLinecap="round"
120+
strokeLinejoin="round"
121+
>
122+
<path d="M12 19V5M5 12l7-7 7 7" />
123+
</svg>
124+
</button>
125+
</div>
126+
</div>
127+
);
128+
}
129+
41130
export function CortexChatPanel({ agentId, channelId, onClose }: CortexChatPanelProps) {
42131
const { messages, isStreaming, error, toolActivity, sendMessage, newThread } = useCortexChat(agentId, channelId);
43132
const [input, setInput] = useState("");
44133
const messagesEndRef = useRef<HTMLDivElement>(null);
45-
const inputRef = useRef<HTMLInputElement>(null);
46134

47-
// Auto-scroll on new messages or tool activity
48135
useEffect(() => {
49136
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
50137
}, [messages.length, isStreaming, toolActivity.length]);
51138

52-
// Focus input on mount
53-
useEffect(() => {
54-
inputRef.current?.focus();
55-
}, []);
56-
57-
const handleSubmit = (event: React.FormEvent) => {
58-
event.preventDefault();
139+
const handleSubmit = () => {
59140
const trimmed = input.trim();
60141
if (!trimmed || isStreaming) return;
61142
setInput("");
62143
sendMessage(trimmed);
63144
};
64145

65146
return (
66-
<div className="flex h-full w-full flex-col bg-app-darkBox/30">
147+
<div className="flex h-full w-full flex-col">
67148
{/* Header */}
68-
<div className="flex h-12 items-center justify-between border-b border-app-line/50 px-4">
149+
<div className="flex h-10 items-center justify-between border-b border-app-line/50 px-3">
69150
<div className="flex items-center gap-2">
70151
<span className="text-sm font-medium text-ink">Cortex</span>
71152
{channelId && (
72-
<span className="rounded bg-violet-500/10 px-1.5 py-0.5 text-tiny text-violet-400">
73-
{channelId.length > 24 ? `${channelId.slice(0, 24)}...` : channelId}
153+
<span className="rounded-full bg-app-box px-2 py-0.5 text-tiny text-ink-faint">
154+
{channelId.length > 20 ? `${channelId.slice(0, 20)}...` : channelId}
74155
</span>
75156
)}
76157
</div>
77-
<div className="flex items-center gap-1">
158+
<div className="flex items-center gap-0.5">
78159
<Button
79160
onClick={newThread}
80161
variant="ghost"
81162
size="icon"
82163
disabled={isStreaming}
83164
className="h-7 w-7"
84-
title="New chat"
165+
title="New thread"
85166
>
86167
<HugeiconsIcon icon={PlusSignIcon} className="h-3.5 w-3.5" />
87168
</Button>
@@ -101,51 +182,39 @@ export function CortexChatPanel({ agentId, channelId, onClose }: CortexChatPanel
101182

102183
{/* Messages */}
103184
<div className="flex-1 overflow-y-auto">
104-
<div className="flex flex-col gap-3 p-4">
185+
<div className="flex flex-col gap-5 p-3 pb-4">
105186
{messages.length === 0 && !isStreaming && (
106-
<p className="py-8 text-center text-sm text-ink-faint">
107-
Ask the cortex anything
108-
</p>
187+
<div className="flex items-center justify-center py-12">
188+
<p className="text-sm text-ink-faint">Ask the cortex anything</p>
189+
</div>
109190
)}
191+
110192
{messages.map((message) => (
111-
<div
112-
key={message.id}
113-
className={`rounded-md px-3 py-2 ${
114-
message.role === "user"
115-
? "ml-8 bg-accent/10"
116-
: "mr-2 bg-app-darkBox/50"
117-
}`}
118-
>
119-
<span className={`text-tiny font-medium ${
120-
message.role === "user" ? "text-accent-faint" : "text-violet-400"
121-
}`}>
122-
{message.role === "user" ? "admin" : "cortex"}
123-
</span>
124-
<div className="mt-0.5 text-sm text-ink-dull">
125-
{message.role === "assistant" ? (
193+
<div key={message.id}>
194+
{message.role === "user" ? (
195+
<div className="flex justify-end">
196+
<div className="max-w-[85%] rounded-2xl rounded-br-md bg-accent/10 px-3 py-2">
197+
<p className="text-sm text-ink">{message.content}</p>
198+
</div>
199+
</div>
200+
) : (
201+
<div className="text-sm text-ink-dull">
126202
<Markdown>{message.content}</Markdown>
127-
) : (
128-
<p>{message.content}</p>
129-
)}
130-
</div>
203+
</div>
204+
)}
131205
</div>
132206
))}
207+
208+
{/* Streaming state */}
133209
{isStreaming && (
134-
<div className="mr-2 rounded-md bg-app-darkBox/50 px-3 py-2">
135-
<span className="text-tiny font-medium text-violet-400">cortex</span>
210+
<div>
136211
<ToolActivityIndicator activity={toolActivity} />
137-
{toolActivity.length === 0 && (
138-
<div className="mt-1 flex items-center gap-1">
139-
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-violet-400" />
140-
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-violet-400 [animation-delay:0.2s]" />
141-
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-violet-400 [animation-delay:0.4s]" />
142-
<span className="ml-1 text-tiny text-ink-faint">thinking...</span>
143-
</div>
144-
)}
212+
{toolActivity.length === 0 && <ThinkingIndicator />}
145213
</div>
146214
)}
215+
147216
{error && (
148-
<div className="rounded-md border border-red-500/20 bg-red-500/10 px-3 py-2 text-sm text-red-400">
217+
<div className="rounded-lg border border-red-500/20 bg-red-500/5 px-3 py-2.5 text-sm text-red-400">
149218
{error}
150219
</div>
151220
)}
@@ -154,27 +223,14 @@ export function CortexChatPanel({ agentId, channelId, onClose }: CortexChatPanel
154223
</div>
155224

156225
{/* Input */}
157-
<form onSubmit={handleSubmit} className="border-t border-app-line/50 p-3">
158-
<div className="flex gap-2">
159-
<input
160-
ref={inputRef}
161-
type="text"
162-
value={input}
163-
onChange={(event) => setInput(event.target.value)}
164-
placeholder={isStreaming ? "Waiting for response..." : "Message the cortex..."}
165-
disabled={isStreaming}
166-
className="flex-1 rounded-md border border-app-line bg-app-darkBox px-3 py-1.5 text-sm text-ink placeholder:text-ink-faint focus:border-violet-500/50 focus:outline-none disabled:opacity-50"
167-
/>
168-
<Button
169-
type="submit"
170-
disabled={isStreaming || !input.trim()}
171-
size="sm"
172-
className="bg-violet-500/20 text-violet-400 hover:bg-violet-500/30"
173-
>
174-
Send
175-
</Button>
176-
</div>
177-
</form>
226+
<div className="border-t border-app-line/50 p-3">
227+
<CortexChatInput
228+
value={input}
229+
onChange={setInput}
230+
onSubmit={handleSubmit}
231+
isStreaming={isStreaming}
232+
/>
233+
</div>
178234
</div>
179235
);
180236
}

interface/src/components/ModelSelect.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const PROVIDER_LABELS: Record<string, string> = {
1616
anthropic: "Anthropic",
1717
openrouter: "OpenRouter",
1818
openai: "OpenAI",
19+
"openai-chatgpt": "ChatGPT Plus (OAuth)",
1920
deepseek: "DeepSeek",
2021
xai: "xAI",
2122
mistral: "Mistral",
@@ -129,6 +130,7 @@ export function ModelSelect({
129130
"openrouter",
130131
"anthropic",
131132
"openai",
133+
"openai-chatgpt",
132134
"ollama",
133135
"deepseek",
134136
"xai",

0 commit comments

Comments
 (0)