@hienlh/ppm 0.9.31 → 0.9.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.claude.bak/agent-memory/tester/MEMORY.md +3 -0
  2. package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +32 -0
  3. package/CHANGELOG.md +21 -0
  4. package/dist/web/assets/{browser-tab-DmDrxklj.js → browser-tab-B9nNKjZX.js} +1 -1
  5. package/dist/web/assets/{chat-tab-CMwOy57v.js → chat-tab-6XGhEKaC.js} +2 -2
  6. package/dist/web/assets/{code-editor-jsL0PK8A.js → code-editor-DMZMpzt2.js} +1 -1
  7. package/dist/web/assets/{database-viewer-CBo5yPV-.js → database-viewer-CnP1FFS2.js} +1 -1
  8. package/dist/web/assets/{diff-viewer-Dk-plEOm.js → diff-viewer-Cvwd0XBO.js} +1 -1
  9. package/dist/web/assets/{extension-webview-B0tE14-C.js → extension-webview-DkhsRepr.js} +1 -1
  10. package/dist/web/assets/{git-graph-BsYuai5I.js → git-graph-C3670Nxm.js} +1 -1
  11. package/dist/web/assets/index-CcFDEPCo.css +2 -0
  12. package/dist/web/assets/index-DjIQL8ar.js +30 -0
  13. package/dist/web/assets/keybindings-store-DHh6rwm-.js +1 -0
  14. package/dist/web/assets/{markdown-renderer-lUfZhpU0.js → markdown-renderer-Co04dDdI.js} +1 -1
  15. package/dist/web/assets/{postgres-viewer-sZclUhuS.js → postgres-viewer-D8K1qnnA.js} +1 -1
  16. package/dist/web/assets/{settings-tab-CvbLGbR6.js → settings-tab-64ODAeQZ.js} +1 -1
  17. package/dist/web/assets/{sqlite-viewer-BAjul3Ct.js → sqlite-viewer-ClX7FICB.js} +1 -1
  18. package/dist/web/assets/{terminal-tab-Ds9ymO7D.js → terminal-tab-Dw4IKWGM.js} +1 -1
  19. package/dist/web/assets/{use-monaco-theme-D9bFLaXR.js → use-monaco-theme-DA7EyR70.js} +1 -1
  20. package/dist/web/index.html +2 -2
  21. package/dist/web/sw.js +1 -1
  22. package/docs/codebase-summary.md +33 -3
  23. package/docs/project-changelog.md +47 -0
  24. package/docs/project-roadmap.md +14 -7
  25. package/docs/system-architecture.md +65 -2
  26. package/package.json +1 -1
  27. package/src/server/index.ts +7 -0
  28. package/src/server/routes/proxy.ts +15 -0
  29. package/src/server/routes/settings.ts +74 -1
  30. package/src/services/clawbot/clawbot-formatter.ts +88 -0
  31. package/src/services/clawbot/clawbot-memory.ts +333 -0
  32. package/src/services/clawbot/clawbot-service.ts +500 -0
  33. package/src/services/clawbot/clawbot-session.ts +188 -0
  34. package/src/services/clawbot/clawbot-streamer.ts +245 -0
  35. package/src/services/clawbot/clawbot-telegram.ts +251 -0
  36. package/src/services/config.service.ts +1 -1
  37. package/src/services/db.service.ts +279 -1
  38. package/src/services/proxy-openai-bridge.ts +241 -0
  39. package/src/services/proxy-sdk-bridge.ts +63 -21
  40. package/src/services/proxy.service.ts +33 -0
  41. package/src/types/clawbot.ts +103 -0
  42. package/src/types/config.ts +26 -0
  43. package/src/web/components/chat/chat-history-bar.tsx +8 -3
  44. package/src/web/components/settings/clawbot-settings-section.tsx +270 -0
  45. package/src/web/components/settings/proxy-settings-section.tsx +50 -37
  46. package/src/web/components/settings/proxy-test-section.tsx +48 -25
  47. package/src/web/components/settings/settings-tab.tsx +4 -1
  48. package/src/web/lib/api-settings.ts +2 -0
  49. package/dist/web/assets/index-CJvp0DJT.css +0 -2
  50. package/dist/web/assets/index-DMiaze7L.js +0 -37
  51. package/dist/web/assets/keybindings-store-B01E0k20.js +0 -1
@@ -2,6 +2,7 @@ import { getConfigValue, setConfigValue } from "./db.service.ts";
2
2
  import { accountSelector } from "./account-selector.service.ts";
3
3
  import { accountService } from "./account.service.ts";
4
4
  import { forwardViaSdk } from "./proxy-sdk-bridge.ts";
5
+ import { forwardOpenAiViaSdk } from "./proxy-openai-bridge.ts";
5
6
  import { randomBytes } from "node:crypto";
6
7
 
7
8
  const PROXY_ENABLED_KEY = "proxy_enabled";
@@ -86,6 +87,38 @@ class ProxyService {
86
87
  return this.forwardDirect(path, method, headers, body, token, account);
87
88
  }
88
89
 
90
+ /**
91
+ * Forward an OpenAI-format chat completions request via SDK query().
92
+ * Always uses SDK bridge (works for both OAuth and API key accounts).
93
+ */
94
+ async forwardOpenAi(body: string): Promise<Response> {
95
+ const account = accountSelector.next();
96
+ if (!account) {
97
+ return new Response(
98
+ JSON.stringify({ error: { message: "No active accounts available", type: "server_error" } }),
99
+ { status: 401, headers: { "Content-Type": "application/json" } },
100
+ );
101
+ }
102
+
103
+ let token = account.accessToken;
104
+ if (token.startsWith("sk-ant-oat")) {
105
+ const fresh = await accountService.ensureFreshToken(account.id);
106
+ if (fresh) token = fresh.accessToken;
107
+ }
108
+
109
+ try {
110
+ const parsed = JSON.parse(body);
111
+ this.requestCount++;
112
+ return await forwardOpenAiViaSdk(parsed, { id: account.id, email: account.email, accessToken: token });
113
+ } catch (e) {
114
+ console.error(`[proxy] OpenAI bridge error:`, (e as Error).message);
115
+ return new Response(
116
+ JSON.stringify({ error: { message: (e as Error).message, type: "server_error" } }),
117
+ { status: 502, headers: { "Content-Type": "application/json" } },
118
+ );
119
+ }
120
+ }
121
+
89
122
  /** Direct HTTP forward for API key accounts */
90
123
  private async forwardDirect(
91
124
  path: string,
@@ -0,0 +1,103 @@
1
+ /** Telegram update object (subset we care about) */
2
+ export interface TelegramUpdate {
3
+ update_id: number;
4
+ message?: TelegramMessage;
5
+ edited_message?: TelegramMessage;
6
+ }
7
+
8
+ export interface TelegramMessage {
9
+ message_id: number;
10
+ from?: { id: number; first_name: string; username?: string };
11
+ chat: { id: number; type: "private" | "group" | "supergroup" };
12
+ date: number;
13
+ text?: string;
14
+ caption?: string;
15
+ }
16
+
17
+ /** Sent message result from Telegram API */
18
+ export interface TelegramSentMessage {
19
+ message_id: number;
20
+ chat: { id: number };
21
+ date: number;
22
+ }
23
+
24
+ /** ClawBot session row from SQLite */
25
+ export interface ClawBotSessionRow {
26
+ id: number;
27
+ telegram_chat_id: string;
28
+ session_id: string;
29
+ provider_id: string;
30
+ project_name: string;
31
+ project_path: string;
32
+ is_active: number;
33
+ created_at: number;
34
+ last_message_at: number;
35
+ }
36
+
37
+ /** ClawBot memory row from SQLite */
38
+ export interface ClawBotMemoryRow {
39
+ id: number;
40
+ project: string;
41
+ content: string;
42
+ category: ClawBotMemoryCategory;
43
+ importance: number;
44
+ created_at: number;
45
+ updated_at: number;
46
+ session_id: string | null;
47
+ superseded_by: number | null;
48
+ }
49
+
50
+ export type ClawBotMemoryCategory =
51
+ | "fact"
52
+ | "decision"
53
+ | "preference"
54
+ | "architecture"
55
+ | "issue";
56
+
57
+ /** Active session state tracked in memory (not DB) */
58
+ export interface ClawBotActiveSession {
59
+ telegramChatId: string;
60
+ sessionId: string;
61
+ providerId: string;
62
+ projectName: string;
63
+ projectPath: string;
64
+ /** Telegram message ID being edited for streaming */
65
+ currentMessageId?: number;
66
+ /** Debounce timer for rapid messages */
67
+ debounceTimer?: ReturnType<typeof setTimeout>;
68
+ /** Accumulated debounced text */
69
+ debouncedText?: string;
70
+ }
71
+
72
+ /** Parsed command from Telegram message */
73
+ export interface ClawBotCommand {
74
+ command: string;
75
+ args: string;
76
+ chatId: number;
77
+ messageId: number;
78
+ userId: number;
79
+ username?: string;
80
+ }
81
+
82
+ /** Memory recall result with relevance score */
83
+ export interface MemoryRecallResult {
84
+ id: number;
85
+ content: string;
86
+ category: ClawBotMemoryCategory;
87
+ importance: number;
88
+ project: string;
89
+ /** FTS5 rank score (lower = more relevant) */
90
+ rank?: number;
91
+ }
92
+
93
+ /** Paired chat row from SQLite */
94
+ export interface ClawBotPairedChat {
95
+ id: number;
96
+ telegram_chat_id: string;
97
+ telegram_user_id: string | null;
98
+ display_name: string | null;
99
+ pairing_code: string | null;
100
+ status: "pending" | "approved" | "revoked";
101
+ created_at: number;
102
+ approved_at: number | null;
103
+ }
@@ -9,6 +9,17 @@ export interface TelegramConfig {
9
9
  chat_id: string;
10
10
  }
11
11
 
12
+ export interface ClawBotConfig {
13
+ enabled: boolean;
14
+ default_provider: string;
15
+ default_project: string;
16
+ system_prompt: string;
17
+ show_tool_calls: boolean;
18
+ show_thinking: boolean;
19
+ permission_mode: string;
20
+ debounce_ms: number;
21
+ }
22
+
12
23
  export type ThemeConfig = "light" | "dark" | "system";
13
24
 
14
25
  export interface PpmConfig {
@@ -21,6 +32,7 @@ export interface PpmConfig {
21
32
  ai: AIConfig;
22
33
  push?: PushConfig;
23
34
  telegram?: TelegramConfig;
35
+ clawbot?: ClawBotConfig;
24
36
  cloud_url?: string;
25
37
  }
26
38
 
@@ -85,6 +97,20 @@ export const DEFAULT_CONFIG: PpmConfig = {
85
97
  },
86
98
  },
87
99
  },
100
+ telegram: {
101
+ bot_token: "",
102
+ chat_id: "",
103
+ },
104
+ clawbot: {
105
+ enabled: false,
106
+ default_provider: "claude",
107
+ default_project: "",
108
+ system_prompt: "",
109
+ show_tool_calls: true,
110
+ show_thinking: false,
111
+ permission_mode: "bypassPermissions",
112
+ debounce_ms: 2000,
113
+ },
88
114
  };
89
115
 
90
116
  const VALID_TYPES = ["agent-sdk", "cli", "mock"] as const;
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, useCallback, useRef } from "react";
2
- import { History, Settings2, Loader2, MessageSquare, RefreshCw, Search, Pencil, Check, X, BellOff, Bug, ClipboardCheck, Pin, PinOff, Trash2, Users } from "lucide-react";
2
+ import { History, Settings2, Loader2, MessageSquare, RefreshCw, Search, Pencil, Check, X, BellOff, Bug, ClipboardCheck, Pin, PinOff, Trash2, Users, Bot } from "lucide-react";
3
3
  import { Activity } from "lucide-react";
4
4
  import { api, projectUrl } from "@/lib/api-client";
5
5
  import { useTabStore } from "@/stores/tab-store";
@@ -398,9 +398,14 @@ export function ChatHistoryBar({
398
398
  <>
399
399
  <button
400
400
  onClick={() => openSession(session)}
401
- className="text-[11px] truncate flex-1 text-left"
401
+ className="text-[11px] truncate flex-1 text-left flex items-center gap-1"
402
402
  >
403
- {session.title || "Untitled"}
403
+ {session.title?.startsWith("[Claw]") && (
404
+ <Bot className="size-3 text-muted-foreground shrink-0" />
405
+ )}
406
+ {session.title?.startsWith("[Claw]")
407
+ ? session.title.slice(7)
408
+ : session.title || "Untitled"}
404
409
  </button>
405
410
  <button
406
411
  onClick={(e) => togglePin(e, session)}
@@ -0,0 +1,270 @@
1
+ import { useState, useEffect, useCallback, type ChangeEvent } from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import { Input } from "@/components/ui/input";
4
+ import { Switch } from "@/components/ui/switch";
5
+ import { api } from "@/lib/api-client";
6
+ import { Trash2, CheckCircle, Clock } from "lucide-react";
7
+
8
+ interface ClawBotConfig {
9
+ enabled: boolean;
10
+ default_provider: string;
11
+ default_project: string;
12
+ system_prompt: string;
13
+ show_tool_calls: boolean;
14
+ show_thinking: boolean;
15
+ permission_mode: string;
16
+ debounce_ms: number;
17
+ }
18
+
19
+ interface PairedChat {
20
+ id: number;
21
+ telegram_chat_id: string;
22
+ telegram_user_id: string | null;
23
+ display_name: string | null;
24
+ pairing_code: string | null;
25
+ status: "pending" | "approved";
26
+ created_at: number;
27
+ approved_at: number | null;
28
+ }
29
+
30
+ export function ClawBotSettingsSection() {
31
+ const [config, setConfig] = useState<ClawBotConfig | null>(null);
32
+ const [saving, setSaving] = useState(false);
33
+ const [status, setStatus] = useState<{ type: "ok" | "err"; msg: string } | null>(null);
34
+
35
+ const [enabled, setEnabled] = useState(false);
36
+ const [defaultProject, setDefaultProject] = useState("");
37
+ const [systemPrompt, setSystemPrompt] = useState("");
38
+ const [showToolCalls, setShowToolCalls] = useState(true);
39
+ const [showThinking, setShowThinking] = useState(false);
40
+ const [debounceMs, setDebounceMs] = useState(2000);
41
+
42
+ const [pairedChats, setPairedChats] = useState<PairedChat[]>([]);
43
+ const [approveCode, setApproveCode] = useState("");
44
+ const [approving, setApproving] = useState(false);
45
+
46
+ const fetchPairedChats = useCallback(async () => {
47
+ try {
48
+ const data = await api.get<PairedChat[]>("/api/settings/clawbot/paired");
49
+ setPairedChats(data);
50
+ } catch {}
51
+ }, []);
52
+
53
+ useEffect(() => {
54
+ api.get<ClawBotConfig>("/api/settings/clawbot").then((data) => {
55
+ setConfig(data);
56
+ setEnabled(data.enabled);
57
+ setDefaultProject(data.default_project);
58
+ setSystemPrompt(data.system_prompt);
59
+ setShowToolCalls(data.show_tool_calls);
60
+ setShowThinking(data.show_thinking);
61
+ setDebounceMs(data.debounce_ms);
62
+ }).catch(() => {});
63
+ fetchPairedChats();
64
+ }, [fetchPairedChats]);
65
+
66
+ const save = async () => {
67
+ setSaving(true);
68
+ setStatus(null);
69
+ try {
70
+ const body: Partial<ClawBotConfig> = {
71
+ enabled,
72
+ default_project: defaultProject.trim(),
73
+ system_prompt: systemPrompt,
74
+ show_tool_calls: showToolCalls,
75
+ show_thinking: showThinking,
76
+ debounce_ms: debounceMs,
77
+ };
78
+ const data = await api.put<ClawBotConfig>("/api/settings/clawbot", body);
79
+ setConfig(data);
80
+ setStatus({ type: "ok", msg: enabled ? "Saved — bot started" : "Saved — bot stopped" });
81
+ } catch (e) {
82
+ setStatus({ type: "err", msg: (e as Error).message });
83
+ } finally {
84
+ setSaving(false);
85
+ }
86
+ };
87
+
88
+ const handleApprovePairing = async () => {
89
+ if (!approveCode.trim()) return;
90
+ setApproving(true);
91
+ try {
92
+ await api.post("/api/settings/clawbot/paired/approve", { code: approveCode.trim().toUpperCase() });
93
+ setApproveCode("");
94
+ await fetchPairedChats();
95
+ setStatus({ type: "ok", msg: "Device approved" });
96
+ } catch (e) {
97
+ setStatus({ type: "err", msg: (e as Error).message });
98
+ } finally {
99
+ setApproving(false);
100
+ }
101
+ };
102
+
103
+ const handleRevokePairing = async (chatId: string) => {
104
+ try {
105
+ await api.del(`/api/settings/clawbot/paired/${chatId}`);
106
+ await fetchPairedChats();
107
+ setStatus({ type: "ok", msg: "Device revoked" });
108
+ } catch (e) {
109
+ setStatus({ type: "err", msg: (e as Error).message });
110
+ }
111
+ };
112
+
113
+ if (!config) return <p className="text-xs text-muted-foreground">Loading...</p>;
114
+
115
+ return (
116
+ <div className="space-y-4">
117
+ {/* Enable/Disable */}
118
+ <div className="flex items-center justify-between">
119
+ <div>
120
+ <p className="text-xs font-medium">Enable ClawBot</p>
121
+ <p className="text-[10px] text-muted-foreground">
122
+ Telegram bot that chats with your AI providers
123
+ </p>
124
+ </div>
125
+ <Switch checked={enabled} onCheckedChange={setEnabled} />
126
+ </div>
127
+
128
+ {/* Paired Devices */}
129
+ <div className="space-y-2">
130
+ <p className="text-xs font-medium">Paired Devices</p>
131
+ <p className="text-[10px] text-muted-foreground">
132
+ Send any message to the bot on Telegram to get a pairing code. Enter it below to approve.
133
+ </p>
134
+
135
+ <div className="flex gap-2">
136
+ <Input
137
+ placeholder="Enter pairing code (e.g. A3K7WR)"
138
+ value={approveCode}
139
+ onChange={(e) => setApproveCode(e.target.value.toUpperCase())}
140
+ className="h-8 text-xs font-mono tracking-wider uppercase"
141
+ maxLength={6}
142
+ />
143
+ <Button
144
+ variant="outline"
145
+ size="sm"
146
+ className="h-8 text-xs shrink-0 cursor-pointer"
147
+ disabled={approving || approveCode.length < 6}
148
+ onClick={handleApprovePairing}
149
+ >
150
+ {approving ? "..." : "Approve"}
151
+ </Button>
152
+ </div>
153
+
154
+ {pairedChats.length === 0 ? (
155
+ <p className="text-[10px] text-muted-foreground italic">No paired devices yet.</p>
156
+ ) : (
157
+ <div className="space-y-1">
158
+ {pairedChats.map((chat) => (
159
+ <div
160
+ key={chat.id}
161
+ className="flex items-center justify-between rounded-md border p-2"
162
+ >
163
+ <div className="flex items-center gap-2 min-w-0">
164
+ {chat.status === "approved" ? (
165
+ <CheckCircle className="size-3.5 text-green-500 shrink-0" />
166
+ ) : (
167
+ <Clock className="size-3.5 text-yellow-500 shrink-0" />
168
+ )}
169
+ <div className="min-w-0">
170
+ <p className="text-xs truncate">
171
+ {chat.display_name || `Chat ${chat.telegram_chat_id}`}
172
+ </p>
173
+ <p className="text-[10px] text-muted-foreground">
174
+ {chat.status === "pending" && chat.pairing_code
175
+ ? `Code: ${chat.pairing_code}`
176
+ : chat.status}
177
+ </p>
178
+ </div>
179
+ </div>
180
+ <Button
181
+ variant="ghost"
182
+ size="sm"
183
+ className="h-7 w-7 p-0 text-destructive hover:text-destructive cursor-pointer"
184
+ onClick={() => handleRevokePairing(chat.telegram_chat_id)}
185
+ >
186
+ <Trash2 className="size-3.5" />
187
+ </Button>
188
+ </div>
189
+ ))}
190
+ </div>
191
+ )}
192
+ </div>
193
+
194
+ {/* Default Project */}
195
+ <div className="space-y-1.5">
196
+ <label className="text-[11px] text-muted-foreground">Default Project</label>
197
+ <Input
198
+ placeholder="my-project"
199
+ value={defaultProject}
200
+ onChange={(e) => setDefaultProject(e.target.value)}
201
+ className="h-7 text-xs"
202
+ />
203
+ <p className="text-[10px] text-muted-foreground">
204
+ Project used when starting a new chat. Must match a project name in PPM.
205
+ </p>
206
+ </div>
207
+
208
+ {/* System Prompt */}
209
+ <div className="space-y-1.5">
210
+ <label className="text-[11px] text-muted-foreground">System Prompt</label>
211
+ <textarea
212
+ placeholder="You are a helpful assistant..."
213
+ value={systemPrompt}
214
+ onChange={(e: ChangeEvent<HTMLTextAreaElement>) => setSystemPrompt(e.target.value)}
215
+ className="flex w-full rounded-md border border-input bg-background px-3 py-2 text-xs min-h-[60px] resize-y ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
216
+ rows={3}
217
+ />
218
+ <p className="text-[10px] text-muted-foreground">
219
+ Custom personality/instructions prepended to each session.
220
+ </p>
221
+ </div>
222
+
223
+ {/* Display Toggles */}
224
+ <div className="space-y-2">
225
+ <div className="flex items-center justify-between">
226
+ <p className="text-xs">Show tool calls</p>
227
+ <Switch checked={showToolCalls} onCheckedChange={setShowToolCalls} />
228
+ </div>
229
+ <div className="flex items-center justify-between">
230
+ <p className="text-xs">Show thinking</p>
231
+ <Switch checked={showThinking} onCheckedChange={setShowThinking} />
232
+ </div>
233
+ </div>
234
+
235
+ {/* Debounce */}
236
+ <div className="space-y-1.5">
237
+ <label className="text-[11px] text-muted-foreground">Debounce (ms)</label>
238
+ <Input
239
+ type="number"
240
+ min={0}
241
+ max={30000}
242
+ step={500}
243
+ value={debounceMs}
244
+ onChange={(e) => setDebounceMs(Number(e.target.value))}
245
+ className="h-7 text-xs w-24"
246
+ />
247
+ <p className="text-[10px] text-muted-foreground">
248
+ Merge rapid messages within this window. 0 = no debounce.
249
+ </p>
250
+ </div>
251
+
252
+ {/* Save */}
253
+ <Button
254
+ variant="default"
255
+ size="sm"
256
+ className="h-8 text-xs w-full cursor-pointer"
257
+ disabled={saving}
258
+ onClick={save}
259
+ >
260
+ {saving ? "Saving..." : "Save"}
261
+ </Button>
262
+
263
+ {status && (
264
+ <p className={`text-[11px] ${status.type === "ok" ? "text-green-600 dark:text-green-400" : "text-destructive"}`}>
265
+ {status.msg}
266
+ </p>
267
+ )}
268
+ </div>
269
+ );
270
+ }
@@ -134,43 +134,43 @@ export function ProxySettingsSection() {
134
134
  <ProxyTestButton authKey={settings.authKey!} baseUrl={window.location.origin} />
135
135
  </div>
136
136
 
137
- {/* Local endpoint */}
137
+ {/* Anthropic endpoint */}
138
138
  <div className="space-y-1">
139
- <Label className="text-[10px] text-muted-foreground">Local Endpoint</Label>
139
+ <Label className="text-[10px] text-muted-foreground">Anthropic Endpoint</Label>
140
140
  <div className="flex gap-1.5 items-center">
141
141
  <code className="text-[10px] font-mono bg-muted px-1.5 py-0.5 rounded flex-1 truncate">
142
- {localEndpoint}
142
+ {hasTunnel ? settings.proxyEndpoint : localEndpoint}
143
143
  </code>
144
144
  <Button
145
145
  variant="ghost"
146
146
  size="sm"
147
147
  className="h-6 px-1.5 cursor-pointer shrink-0"
148
- onClick={() => copyToClipboard(localEndpoint, "local")}
148
+ onClick={() => copyToClipboard(hasTunnel ? settings.proxyEndpoint! : localEndpoint, "anthropic")}
149
149
  >
150
- {copied === "local" ? "Copied!" : <Copy className="size-3" />}
150
+ {copied === "anthropic" ? "Copied!" : <Copy className="size-3" />}
151
151
  </Button>
152
152
  </div>
153
153
  </div>
154
154
 
155
- {/* Tunnel endpoint */}
156
- {hasTunnel && settings.proxyEndpoint && (
157
- <div className="space-y-1">
158
- <Label className="text-[10px] text-muted-foreground">Public Endpoint (Tunnel)</Label>
159
- <div className="flex gap-1.5 items-center">
160
- <code className="text-[10px] font-mono bg-muted px-1.5 py-0.5 rounded flex-1 truncate">
161
- {settings.proxyEndpoint}
162
- </code>
163
- <Button
164
- variant="ghost"
165
- size="sm"
166
- className="h-6 px-1.5 cursor-pointer shrink-0"
167
- onClick={() => copyToClipboard(settings.proxyEndpoint!, "tunnel")}
168
- >
169
- {copied === "tunnel" ? "Copied!" : <Copy className="size-3" />}
170
- </Button>
171
- </div>
155
+ {/* OpenAI endpoint */}
156
+ <div className="space-y-1">
157
+ <Label className="text-[10px] text-muted-foreground">OpenAI-Compatible Endpoint</Label>
158
+ <div className="flex gap-1.5 items-center">
159
+ <code className="text-[10px] font-mono bg-muted px-1.5 py-0.5 rounded flex-1 truncate">
160
+ {hasTunnel ? settings.openAiEndpoint : settings.localOpenAiEndpoint}
161
+ </code>
162
+ <Button
163
+ variant="ghost"
164
+ size="sm"
165
+ className="h-6 px-1.5 cursor-pointer shrink-0"
166
+ onClick={() => copyToClipboard(
167
+ hasTunnel ? settings.openAiEndpoint! : settings.localOpenAiEndpoint, "openai",
168
+ )}
169
+ >
170
+ {copied === "openai" ? "Copied!" : <Copy className="size-3" />}
171
+ </Button>
172
172
  </div>
173
- )}
173
+ </div>
174
174
 
175
175
  {!hasTunnel && (
176
176
  <p className="text-[10px] text-muted-foreground">
@@ -178,21 +178,13 @@ export function ProxySettingsSection() {
178
178
  </p>
179
179
  )}
180
180
 
181
- {/* Usage example */}
181
+ {/* Usage examples */}
182
182
  <div className="space-y-1 pt-1">
183
- <Label className="text-[10px] text-muted-foreground">Usage Example</Label>
183
+ <Label className="text-[10px] text-muted-foreground">Anthropic Format</Label>
184
184
  <div className="relative">
185
185
  <pre className="text-[9px] font-mono bg-muted p-2 rounded overflow-x-auto whitespace-pre">
186
- {`# Set as base URL in your tool
187
- ANTHROPIC_BASE_URL=${hasTunnel ? settings.tunnelUrl + "/proxy" : localBaseUrl + "/proxy"}
188
- ANTHROPIC_API_KEY=${settings.authKey}
189
-
190
- # Or use curl
191
- curl ${hasTunnel ? settings.proxyEndpoint : localEndpoint} \\
192
- -H "x-api-key: ${settings.authKey}" \\
193
- -H "content-type: application/json" \\
194
- -H "anthropic-version: 2023-06-01" \\
195
- -d '{"model":"claude-sonnet-4-6","max_tokens":1024,"messages":[{"role":"user","content":"Hello"}]}'`}
186
+ {`ANTHROPIC_BASE_URL=${hasTunnel ? settings.tunnelUrl + "/proxy" : localBaseUrl + "/proxy"}
187
+ ANTHROPIC_API_KEY=${settings.authKey}`}
196
188
  </pre>
197
189
  <Button
198
190
  variant="ghost"
@@ -200,10 +192,31 @@ curl ${hasTunnel ? settings.proxyEndpoint : localEndpoint} \\
200
192
  className="absolute top-1 right-1 h-5 px-1 cursor-pointer"
201
193
  onClick={() => copyToClipboard(
202
194
  `ANTHROPIC_BASE_URL=${hasTunnel ? settings.tunnelUrl + "/proxy" : localBaseUrl + "/proxy"}\nANTHROPIC_API_KEY=${settings.authKey}`,
203
- "example",
195
+ "anthropic-env",
196
+ )}
197
+ >
198
+ {copied === "anthropic-env" ? "Copied!" : <Copy className="size-2.5" />}
199
+ </Button>
200
+ </div>
201
+ </div>
202
+
203
+ <div className="space-y-1">
204
+ <Label className="text-[10px] text-muted-foreground">OpenAI Format</Label>
205
+ <div className="relative">
206
+ <pre className="text-[9px] font-mono bg-muted p-2 rounded overflow-x-auto whitespace-pre">
207
+ {`OPENAI_BASE_URL=${hasTunnel ? settings.tunnelUrl + "/proxy/v1" : localBaseUrl + "/proxy/v1"}
208
+ OPENAI_API_KEY=${settings.authKey}`}
209
+ </pre>
210
+ <Button
211
+ variant="ghost"
212
+ size="sm"
213
+ className="absolute top-1 right-1 h-5 px-1 cursor-pointer"
214
+ onClick={() => copyToClipboard(
215
+ `OPENAI_BASE_URL=${hasTunnel ? settings.tunnelUrl + "/proxy/v1" : localBaseUrl + "/proxy/v1"}\nOPENAI_API_KEY=${settings.authKey}`,
216
+ "openai-env",
204
217
  )}
205
218
  >
206
- {copied === "example" ? "Copied!" : <Copy className="size-2.5" />}
219
+ {copied === "openai-env" ? "Copied!" : <Copy className="size-2.5" />}
207
220
  </Button>
208
221
  </div>
209
222
  </div>