@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +3 -50
  2. package/dist/web/assets/browser-tab--V6I70pH.js +1 -0
  3. package/dist/web/assets/chat-tab-CrkhvVjF.js +10 -0
  4. package/dist/web/assets/code-editor-BfMyExLp.js +2 -0
  5. package/dist/web/assets/{database-viewer-TjRo2b8_.js → database-viewer-CeRUrZKj.js} +1 -1
  6. package/dist/web/assets/{diff-viewer-BMhCz0xk.js → diff-viewer-D2p3WTMS.js} +1 -1
  7. package/dist/web/assets/{extension-webview-DiVdlE2r.js → extension-webview-DQWAHMlR.js} +1 -1
  8. package/dist/web/assets/git-graph-BWRMlCdK.js +1 -0
  9. package/dist/web/assets/index-C7esr4gM.css +2 -0
  10. package/dist/web/assets/index-DU6UVgQY.js +30 -0
  11. package/dist/web/assets/keybindings-store-BE2T8jM9.js +1 -0
  12. package/dist/web/assets/{markdown-renderer-IyEzLrC6.js → markdown-renderer-C7lKs47M.js} +4 -4
  13. package/dist/web/assets/{postgres-viewer-CSynGGkJ.js → postgres-viewer-Cr9jpBNd.js} +1 -1
  14. package/dist/web/assets/{settings-tab-BdI4HhRa.js → settings-tab-DKy-YDg2.js} +1 -1
  15. package/dist/web/assets/{sqlite-viewer-C5mviyU5.js → sqlite-viewer-9AmeF-Zs.js} +1 -1
  16. package/dist/web/assets/square-oPKIkJiw.js +1 -0
  17. package/dist/web/assets/{terminal-tab-CDyC1grg.js → terminal-tab-DFhB4Rxh.js} +1 -1
  18. package/dist/web/assets/{use-monaco-theme-DcVicB_i.js → use-monaco-theme-B7XLw-OX.js} +1 -1
  19. package/dist/web/index.html +2 -3
  20. package/dist/web/sw.js +1 -1
  21. package/docs/codebase-summary.md +3 -33
  22. package/docs/project-changelog.md +0 -47
  23. package/docs/project-roadmap.md +7 -14
  24. package/docs/system-architecture.md +2 -65
  25. package/package.json +1 -1
  26. package/src/server/index.ts +0 -7
  27. package/src/server/routes/settings.ts +1 -72
  28. package/src/services/config.service.ts +1 -1
  29. package/src/services/db.service.ts +1 -279
  30. package/src/services/git.service.ts +2 -2
  31. package/src/types/config.ts +0 -26
  32. package/src/web/components/browser/browser-tab.tsx +128 -97
  33. package/src/web/components/chat/chat-history-bar.tsx +3 -8
  34. package/src/web/components/layout/command-palette.tsx +1 -1
  35. package/src/web/components/settings/settings-tab.tsx +1 -4
  36. package/src/web/hooks/use-url-sync.ts +1 -1
  37. package/dist/web/assets/browser-tab-DnIsHiCc.js +0 -1
  38. package/dist/web/assets/chat-tab-il6D4jql.js +0 -10
  39. package/dist/web/assets/code-editor-BUc1jBqm.js +0 -2
  40. package/dist/web/assets/git-graph-4eGJ8B1A.js +0 -1
  41. package/dist/web/assets/index-BmcV1di6.js +0 -30
  42. package/dist/web/assets/index-CcFDEPCo.css +0 -2
  43. package/dist/web/assets/keybindings-store--5T5hsAj.js +0 -1
  44. package/dist/web/assets/tab-store-BXMIUvsE.js +0 -1
  45. package/docs/streaming-input-guide.md +0 -267
  46. package/snapshot-state.md +0 -1526
  47. package/src/services/ppmbot/ppmbot-formatter.ts +0 -88
  48. package/src/services/ppmbot/ppmbot-memory.ts +0 -333
  49. package/src/services/ppmbot/ppmbot-service.ts +0 -545
  50. package/src/services/ppmbot/ppmbot-session.ts +0 -199
  51. package/src/services/ppmbot/ppmbot-streamer.ts +0 -288
  52. package/src/services/ppmbot/ppmbot-telegram.ts +0 -279
  53. package/src/types/ppmbot.ts +0 -103
  54. package/src/web/components/settings/ppmbot-settings-section.tsx +0 -270
  55. package/test-session-ops.mjs +0 -444
  56. 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
- }
@@ -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
- }