@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.
- package/.claude.bak/agent-memory/tester/MEMORY.md +3 -0
- package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +32 -0
- package/CHANGELOG.md +21 -0
- package/dist/web/assets/{browser-tab-DmDrxklj.js → browser-tab-B9nNKjZX.js} +1 -1
- package/dist/web/assets/{chat-tab-CMwOy57v.js → chat-tab-6XGhEKaC.js} +2 -2
- package/dist/web/assets/{code-editor-jsL0PK8A.js → code-editor-DMZMpzt2.js} +1 -1
- package/dist/web/assets/{database-viewer-CBo5yPV-.js → database-viewer-CnP1FFS2.js} +1 -1
- package/dist/web/assets/{diff-viewer-Dk-plEOm.js → diff-viewer-Cvwd0XBO.js} +1 -1
- package/dist/web/assets/{extension-webview-B0tE14-C.js → extension-webview-DkhsRepr.js} +1 -1
- package/dist/web/assets/{git-graph-BsYuai5I.js → git-graph-C3670Nxm.js} +1 -1
- package/dist/web/assets/index-CcFDEPCo.css +2 -0
- package/dist/web/assets/index-DjIQL8ar.js +30 -0
- package/dist/web/assets/keybindings-store-DHh6rwm-.js +1 -0
- package/dist/web/assets/{markdown-renderer-lUfZhpU0.js → markdown-renderer-Co04dDdI.js} +1 -1
- package/dist/web/assets/{postgres-viewer-sZclUhuS.js → postgres-viewer-D8K1qnnA.js} +1 -1
- package/dist/web/assets/{settings-tab-CvbLGbR6.js → settings-tab-64ODAeQZ.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-BAjul3Ct.js → sqlite-viewer-ClX7FICB.js} +1 -1
- package/dist/web/assets/{terminal-tab-Ds9ymO7D.js → terminal-tab-Dw4IKWGM.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-D9bFLaXR.js → use-monaco-theme-DA7EyR70.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +33 -3
- package/docs/project-changelog.md +47 -0
- package/docs/project-roadmap.md +14 -7
- package/docs/system-architecture.md +65 -2
- package/package.json +1 -1
- package/src/server/index.ts +7 -0
- package/src/server/routes/proxy.ts +15 -0
- package/src/server/routes/settings.ts +74 -1
- package/src/services/clawbot/clawbot-formatter.ts +88 -0
- package/src/services/clawbot/clawbot-memory.ts +333 -0
- package/src/services/clawbot/clawbot-service.ts +500 -0
- package/src/services/clawbot/clawbot-session.ts +188 -0
- package/src/services/clawbot/clawbot-streamer.ts +245 -0
- package/src/services/clawbot/clawbot-telegram.ts +251 -0
- package/src/services/config.service.ts +1 -1
- package/src/services/db.service.ts +279 -1
- package/src/services/proxy-openai-bridge.ts +241 -0
- package/src/services/proxy-sdk-bridge.ts +63 -21
- package/src/services/proxy.service.ts +33 -0
- package/src/types/clawbot.ts +103 -0
- package/src/types/config.ts +26 -0
- package/src/web/components/chat/chat-history-bar.tsx +8 -3
- package/src/web/components/settings/clawbot-settings-section.tsx +270 -0
- package/src/web/components/settings/proxy-settings-section.tsx +50 -37
- package/src/web/components/settings/proxy-test-section.tsx +48 -25
- package/src/web/components/settings/settings-tab.tsx +4 -1
- package/src/web/lib/api-settings.ts +2 -0
- package/dist/web/assets/index-CJvp0DJT.css +0 -2
- package/dist/web/assets/index-DMiaze7L.js +0 -37
- 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
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -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
|
|
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
|
-
{/*
|
|
137
|
+
{/* Anthropic endpoint */}
|
|
138
138
|
<div className="space-y-1">
|
|
139
|
-
<Label className="text-[10px] text-muted-foreground">
|
|
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, "
|
|
148
|
+
onClick={() => copyToClipboard(hasTunnel ? settings.proxyEndpoint! : localEndpoint, "anthropic")}
|
|
149
149
|
>
|
|
150
|
-
{copied === "
|
|
150
|
+
{copied === "anthropic" ? "Copied!" : <Copy className="size-3" />}
|
|
151
151
|
</Button>
|
|
152
152
|
</div>
|
|
153
153
|
</div>
|
|
154
154
|
|
|
155
|
-
{/*
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
</
|
|
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
|
|
181
|
+
{/* Usage examples */}
|
|
182
182
|
<div className="space-y-1 pt-1">
|
|
183
|
-
<Label className="text-[10px] text-muted-foreground">
|
|
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
|
-
{
|
|
187
|
-
|
|
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
|
-
"
|
|
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 === "
|
|
219
|
+
{copied === "openai-env" ? "Copied!" : <Copy className="size-2.5" />}
|
|
207
220
|
</Button>
|
|
208
221
|
</div>
|
|
209
222
|
</div>
|