@hienlh/ppm 0.9.39 → 0.9.41
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/CHANGELOG.md +3 -50
- package/dist/web/assets/browser-tab--V6I70pH.js +1 -0
- package/dist/web/assets/chat-tab-CrkhvVjF.js +10 -0
- package/dist/web/assets/code-editor-BfMyExLp.js +2 -0
- package/dist/web/assets/{database-viewer-TjRo2b8_.js → database-viewer-CeRUrZKj.js} +1 -1
- package/dist/web/assets/{diff-viewer-BMhCz0xk.js → diff-viewer-D2p3WTMS.js} +1 -1
- package/dist/web/assets/{extension-webview-DiVdlE2r.js → extension-webview-DQWAHMlR.js} +1 -1
- package/dist/web/assets/git-graph-BWRMlCdK.js +1 -0
- package/dist/web/assets/index-C7esr4gM.css +2 -0
- package/dist/web/assets/index-DU6UVgQY.js +30 -0
- package/dist/web/assets/keybindings-store-BE2T8jM9.js +1 -0
- package/dist/web/assets/{markdown-renderer-IyEzLrC6.js → markdown-renderer-C7lKs47M.js} +4 -4
- package/dist/web/assets/{postgres-viewer-CSynGGkJ.js → postgres-viewer-Cr9jpBNd.js} +1 -1
- package/dist/web/assets/{settings-tab-BdI4HhRa.js → settings-tab-DKy-YDg2.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-C5mviyU5.js → sqlite-viewer-9AmeF-Zs.js} +1 -1
- package/dist/web/assets/square-oPKIkJiw.js +1 -0
- package/dist/web/assets/{terminal-tab-CDyC1grg.js → terminal-tab-DFhB4Rxh.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-DcVicB_i.js → use-monaco-theme-B7XLw-OX.js} +1 -1
- package/dist/web/index.html +2 -3
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +3 -33
- package/docs/project-changelog.md +0 -47
- package/docs/project-roadmap.md +7 -14
- package/docs/system-architecture.md +2 -65
- package/package.json +1 -1
- package/src/server/index.ts +0 -7
- package/src/server/routes/settings.ts +1 -72
- package/src/services/config.service.ts +1 -1
- package/src/services/db.service.ts +1 -279
- package/src/services/git.service.ts +2 -2
- package/src/types/config.ts +0 -26
- package/src/web/components/browser/browser-tab.tsx +128 -97
- package/src/web/components/chat/chat-history-bar.tsx +3 -8
- package/src/web/components/layout/command-palette.tsx +1 -1
- package/src/web/components/settings/settings-tab.tsx +1 -4
- package/src/web/hooks/use-url-sync.ts +1 -1
- package/dist/web/assets/browser-tab-DnIsHiCc.js +0 -1
- package/dist/web/assets/chat-tab-il6D4jql.js +0 -10
- package/dist/web/assets/code-editor-BUc1jBqm.js +0 -2
- package/dist/web/assets/git-graph-4eGJ8B1A.js +0 -1
- package/dist/web/assets/index-BmcV1di6.js +0 -30
- package/dist/web/assets/index-CcFDEPCo.css +0 -2
- package/dist/web/assets/keybindings-store--5T5hsAj.js +0 -1
- package/dist/web/assets/tab-store-BXMIUvsE.js +0 -1
- package/docs/streaming-input-guide.md +0 -267
- package/snapshot-state.md +0 -1526
- package/src/services/ppmbot/ppmbot-formatter.ts +0 -88
- package/src/services/ppmbot/ppmbot-memory.ts +0 -333
- package/src/services/ppmbot/ppmbot-service.ts +0 -545
- package/src/services/ppmbot/ppmbot-session.ts +0 -199
- package/src/services/ppmbot/ppmbot-streamer.ts +0 -288
- package/src/services/ppmbot/ppmbot-telegram.ts +0 -279
- package/src/types/ppmbot.ts +0 -103
- package/src/web/components/settings/ppmbot-settings-section.tsx +0 -270
- package/test-session-ops.mjs +0 -444
- package/test-tokens.mjs +0 -212
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
TelegramUpdate,
|
|
3
|
-
TelegramMessage,
|
|
4
|
-
TelegramSentMessage,
|
|
5
|
-
PPMBotCommand,
|
|
6
|
-
} from "../../types/ppmbot.ts";
|
|
7
|
-
|
|
8
|
-
const TELEGRAM_API = "https://api.telegram.org/bot";
|
|
9
|
-
const POLL_TIMEOUT = 25;
|
|
10
|
-
const MIN_EDIT_INTERVAL = 1000;
|
|
11
|
-
const BOT_TOKEN_RE = /^\d+:[A-Za-z0-9_-]{30,50}$/;
|
|
12
|
-
|
|
13
|
-
/** Known PPMBot slash commands */
|
|
14
|
-
const COMMANDS = new Set([
|
|
15
|
-
"start", "project", "new", "sessions", "resume",
|
|
16
|
-
"status", "stop", "memory", "forget", "remember", "help",
|
|
17
|
-
]);
|
|
18
|
-
|
|
19
|
-
export type UpdateHandler = (update: TelegramUpdate) => Promise<void>;
|
|
20
|
-
|
|
21
|
-
export class PPMBotTelegram {
|
|
22
|
-
private token: string;
|
|
23
|
-
private offset = 0;
|
|
24
|
-
private running = false;
|
|
25
|
-
private abortController: AbortController | null = null;
|
|
26
|
-
private retryCount = 0;
|
|
27
|
-
|
|
28
|
-
/** Track last edit time per chatId:messageId to throttle */
|
|
29
|
-
private lastEditTime = new Map<string, number>();
|
|
30
|
-
|
|
31
|
-
constructor(token: string) {
|
|
32
|
-
if (!BOT_TOKEN_RE.test(token)) {
|
|
33
|
-
throw new Error("Invalid Telegram bot token format");
|
|
34
|
-
}
|
|
35
|
-
this.token = token;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ── Polling ─────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
/** Register bot commands with Telegram so they show in the menu */
|
|
41
|
-
async registerCommands(): Promise<void> {
|
|
42
|
-
try {
|
|
43
|
-
await this.callApi("setMyCommands", {
|
|
44
|
-
commands: [
|
|
45
|
-
{ command: "start", description: "Greeting + list projects" },
|
|
46
|
-
{ command: "project", description: "Switch project" },
|
|
47
|
-
{ command: "new", description: "Fresh session (current project)" },
|
|
48
|
-
{ command: "sessions", description: "List recent sessions" },
|
|
49
|
-
{ command: "resume", description: "Resume a previous session" },
|
|
50
|
-
{ command: "status", description: "Current project/session info" },
|
|
51
|
-
{ command: "stop", description: "End current session" },
|
|
52
|
-
{ command: "memory", description: "Show project memories" },
|
|
53
|
-
{ command: "forget", description: "Remove matching memories" },
|
|
54
|
-
{ command: "remember", description: "Save a fact" },
|
|
55
|
-
{ command: "help", description: "Show all commands" },
|
|
56
|
-
],
|
|
57
|
-
});
|
|
58
|
-
console.log("[ppmbot] Commands registered");
|
|
59
|
-
} catch (err) {
|
|
60
|
-
console.warn("[ppmbot] Failed to register commands:", (err as Error).message);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Start long-polling loop. Calls handler for each update. */
|
|
65
|
-
async startPolling(handler: UpdateHandler): Promise<void> {
|
|
66
|
-
if (this.running) return;
|
|
67
|
-
this.running = true;
|
|
68
|
-
this.retryCount = 0;
|
|
69
|
-
|
|
70
|
-
// Register commands on startup
|
|
71
|
-
await this.registerCommands();
|
|
72
|
-
|
|
73
|
-
console.log("[ppmbot] Polling started");
|
|
74
|
-
|
|
75
|
-
while (this.running) {
|
|
76
|
-
try {
|
|
77
|
-
const updates = await this.getUpdates();
|
|
78
|
-
this.retryCount = 0;
|
|
79
|
-
|
|
80
|
-
for (const update of updates) {
|
|
81
|
-
this.offset = update.update_id + 1;
|
|
82
|
-
// Fire-and-forget: don't block polling on handler execution
|
|
83
|
-
// Per-chatId serialization is handled by processing lock in service
|
|
84
|
-
handler(update).catch((err) => {
|
|
85
|
-
console.error("[ppmbot] Handler error:", (err as Error).message);
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
} catch (err) {
|
|
89
|
-
if (!this.running) break;
|
|
90
|
-
this.retryCount++;
|
|
91
|
-
const delay = Math.min(1000 * 2 ** this.retryCount, 30_000);
|
|
92
|
-
console.error(
|
|
93
|
-
`[ppmbot] Poll error (retry ${this.retryCount}): ${(err as Error).message}. Retrying in ${delay}ms`,
|
|
94
|
-
);
|
|
95
|
-
await Bun.sleep(delay);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
console.log("[ppmbot] Polling stopped");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Stop polling gracefully */
|
|
103
|
-
stop(): void {
|
|
104
|
-
this.running = false;
|
|
105
|
-
this.abortController?.abort();
|
|
106
|
-
this.lastEditTime.clear();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
get isRunning(): boolean {
|
|
110
|
-
return this.running;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ── Telegram API Methods ────────────────────────────────────────
|
|
114
|
-
|
|
115
|
-
/** Fetch updates via long-polling */
|
|
116
|
-
private async getUpdates(): Promise<TelegramUpdate[]> {
|
|
117
|
-
this.abortController = new AbortController();
|
|
118
|
-
const fetchTimeout = setTimeout(
|
|
119
|
-
() => this.abortController?.abort(),
|
|
120
|
-
(POLL_TIMEOUT + 10) * 1000,
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
const res = await fetch(`${TELEGRAM_API}${this.token}/getUpdates`, {
|
|
125
|
-
method: "POST",
|
|
126
|
-
headers: { "Content-Type": "application/json" },
|
|
127
|
-
body: JSON.stringify({
|
|
128
|
-
offset: this.offset,
|
|
129
|
-
timeout: POLL_TIMEOUT,
|
|
130
|
-
allowed_updates: ["message"],
|
|
131
|
-
}),
|
|
132
|
-
signal: this.abortController.signal,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
const json = (await res.json()) as { ok: boolean; result?: TelegramUpdate[] };
|
|
136
|
-
if (!json.ok || !json.result) return [];
|
|
137
|
-
return json.result;
|
|
138
|
-
} finally {
|
|
139
|
-
clearTimeout(fetchTimeout);
|
|
140
|
-
this.abortController = null;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Send a text message */
|
|
145
|
-
async sendMessage(
|
|
146
|
-
chatId: number | string,
|
|
147
|
-
text: string,
|
|
148
|
-
parseMode: "HTML" | "Markdown" = "HTML",
|
|
149
|
-
): Promise<TelegramSentMessage | null> {
|
|
150
|
-
try {
|
|
151
|
-
const res = await this.callApi("sendMessage", {
|
|
152
|
-
chat_id: chatId,
|
|
153
|
-
text,
|
|
154
|
-
parse_mode: parseMode,
|
|
155
|
-
disable_web_page_preview: true,
|
|
156
|
-
});
|
|
157
|
-
const json = (await res.json()) as { ok: boolean; result?: TelegramSentMessage; description?: string };
|
|
158
|
-
if (!json.ok) {
|
|
159
|
-
console.error(`[ppmbot] sendMessage failed: ${json.description}`);
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
return json.result ?? null;
|
|
163
|
-
} catch (err) {
|
|
164
|
-
console.error(`[ppmbot] sendMessage error: ${(err as Error).message}`);
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Edit an existing message text (throttled at 1s intervals) */
|
|
170
|
-
async editMessage(
|
|
171
|
-
chatId: number | string,
|
|
172
|
-
messageId: number,
|
|
173
|
-
text: string,
|
|
174
|
-
parseMode: "HTML" | "Markdown" = "HTML",
|
|
175
|
-
): Promise<boolean> {
|
|
176
|
-
const key = `${chatId}:${messageId}`;
|
|
177
|
-
const now = Date.now();
|
|
178
|
-
const lastEdit = this.lastEditTime.get(key) ?? 0;
|
|
179
|
-
if (now - lastEdit < MIN_EDIT_INTERVAL) return false;
|
|
180
|
-
|
|
181
|
-
this.lastEditTime.set(key, now);
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
const res = await this.callApi("editMessageText", {
|
|
185
|
-
chat_id: chatId,
|
|
186
|
-
message_id: messageId,
|
|
187
|
-
text,
|
|
188
|
-
parse_mode: parseMode,
|
|
189
|
-
disable_web_page_preview: true,
|
|
190
|
-
});
|
|
191
|
-
const json = (await res.json()) as { ok: boolean; description?: string };
|
|
192
|
-
if (!json.ok) {
|
|
193
|
-
if (json.description?.includes("not modified")) return true;
|
|
194
|
-
console.error(`[ppmbot] editMessage failed: ${json.description}`);
|
|
195
|
-
return false;
|
|
196
|
-
}
|
|
197
|
-
return true;
|
|
198
|
-
} catch (err) {
|
|
199
|
-
console.error(`[ppmbot] editMessage error: ${(err as Error).message}`);
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** Force-edit (bypass throttle) — used for final message */
|
|
205
|
-
async editMessageFinal(
|
|
206
|
-
chatId: number | string,
|
|
207
|
-
messageId: number,
|
|
208
|
-
text: string,
|
|
209
|
-
parseMode: "HTML" | "Markdown" = "HTML",
|
|
210
|
-
): Promise<boolean> {
|
|
211
|
-
const key = `${chatId}:${messageId}`;
|
|
212
|
-
this.lastEditTime.delete(key);
|
|
213
|
-
return this.editMessage(chatId, messageId, text, parseMode);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Send "typing" chat action */
|
|
217
|
-
async sendTyping(chatId: number | string): Promise<void> {
|
|
218
|
-
try {
|
|
219
|
-
await this.callApi("sendChatAction", {
|
|
220
|
-
chat_id: chatId,
|
|
221
|
-
action: "typing",
|
|
222
|
-
});
|
|
223
|
-
} catch {
|
|
224
|
-
// Best-effort, ignore errors
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Delete a message */
|
|
229
|
-
async deleteMessage(chatId: number | string, messageId: number): Promise<void> {
|
|
230
|
-
try {
|
|
231
|
-
await this.callApi("deleteMessage", {
|
|
232
|
-
chat_id: chatId,
|
|
233
|
-
message_id: messageId,
|
|
234
|
-
});
|
|
235
|
-
} catch {
|
|
236
|
-
// Best-effort
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// ── Command Parsing ─────────────────────────────────────────────
|
|
241
|
-
|
|
242
|
-
/** Parse a Telegram message into a PPMBotCommand if it starts with / */
|
|
243
|
-
static parseCommand(message: TelegramMessage): PPMBotCommand | null {
|
|
244
|
-
const text = message.text ?? message.caption ?? "";
|
|
245
|
-
if (!text.startsWith("/")) return null;
|
|
246
|
-
|
|
247
|
-
const match = text.match(/^\/(\w+)(?:@\S+)?\s*(.*)/s);
|
|
248
|
-
if (!match) return null;
|
|
249
|
-
|
|
250
|
-
const command = match[1]!.toLowerCase();
|
|
251
|
-
if (!COMMANDS.has(command)) return null;
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
command,
|
|
255
|
-
args: match[2]?.trim() ?? "",
|
|
256
|
-
chatId: message.chat.id,
|
|
257
|
-
messageId: message.message_id,
|
|
258
|
-
userId: message.from?.id ?? 0,
|
|
259
|
-
username: message.from?.username,
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// ── Private Helpers ─────────────────────────────────────────────
|
|
264
|
-
|
|
265
|
-
private async callApi(method: string, body: Record<string, unknown>): Promise<Response> {
|
|
266
|
-
const controller = new AbortController();
|
|
267
|
-
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
268
|
-
try {
|
|
269
|
-
return await fetch(`${TELEGRAM_API}${this.token}/${method}`, {
|
|
270
|
-
method: "POST",
|
|
271
|
-
headers: { "Content-Type": "application/json" },
|
|
272
|
-
body: JSON.stringify(body),
|
|
273
|
-
signal: controller.signal,
|
|
274
|
-
});
|
|
275
|
-
} finally {
|
|
276
|
-
clearTimeout(timeout);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
package/src/types/ppmbot.ts
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
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
|
-
/** PPMBot session row from SQLite */
|
|
25
|
-
export interface PPMBotSessionRow {
|
|
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
|
-
/** PPMBot memory row from SQLite */
|
|
38
|
-
export interface PPMBotMemoryRow {
|
|
39
|
-
id: number;
|
|
40
|
-
project: string;
|
|
41
|
-
content: string;
|
|
42
|
-
category: PPMBotMemoryCategory;
|
|
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 PPMBotMemoryCategory =
|
|
51
|
-
| "fact"
|
|
52
|
-
| "decision"
|
|
53
|
-
| "preference"
|
|
54
|
-
| "architecture"
|
|
55
|
-
| "issue";
|
|
56
|
-
|
|
57
|
-
/** Active session state tracked in memory (not DB) */
|
|
58
|
-
export interface PPMBotActiveSession {
|
|
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 PPMBotCommand {
|
|
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: PPMBotMemoryCategory;
|
|
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 PPMBotPairedChat {
|
|
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
|
-
}
|
|
@@ -1,270 +0,0 @@
|
|
|
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 PPMBotConfig {
|
|
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 PPMBotSettingsSection() {
|
|
31
|
-
const [config, setConfig] = useState<PPMBotConfig | 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<PPMBotConfig>("/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<PPMBotConfig> = {
|
|
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<PPMBotConfig>("/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 PPMBot</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
|
-
}
|