@hienlh/ppm 0.9.30 → 0.9.32

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 (47) hide show
  1. package/CHANGELOG.md +18 -5
  2. package/dist/web/assets/{browser-tab-D0o6oSlt.js → browser-tab-B9nNKjZX.js} +1 -1
  3. package/dist/web/assets/{chat-tab-Boo_H1k9.js → chat-tab-6XGhEKaC.js} +2 -2
  4. package/dist/web/assets/{code-editor-DayGetAZ.js → code-editor-DMZMpzt2.js} +1 -1
  5. package/dist/web/assets/{database-viewer-CaxAp1qK.js → database-viewer-CnP1FFS2.js} +1 -1
  6. package/dist/web/assets/{diff-viewer-BvEXe_B4.js → diff-viewer-Cvwd0XBO.js} +1 -1
  7. package/dist/web/assets/{extension-webview-6XProGzB.js → extension-webview-DkhsRepr.js} +1 -1
  8. package/dist/web/assets/{git-graph-CvgIIt2x.js → git-graph-C3670Nxm.js} +1 -1
  9. package/dist/web/assets/index-CcFDEPCo.css +2 -0
  10. package/dist/web/assets/index-DjIQL8ar.js +30 -0
  11. package/dist/web/assets/keybindings-store-DHh6rwm-.js +1 -0
  12. package/dist/web/assets/{markdown-renderer-UCGYJpI-.js → markdown-renderer-Co04dDdI.js} +1 -1
  13. package/dist/web/assets/{postgres-viewer-TV6kyo6B.js → postgres-viewer-D8K1qnnA.js} +1 -1
  14. package/dist/web/assets/{settings-tab-EziN5Pco.js → settings-tab-64ODAeQZ.js} +1 -1
  15. package/dist/web/assets/{sqlite-viewer-D7LPvSkU.js → sqlite-viewer-ClX7FICB.js} +1 -1
  16. package/dist/web/assets/{terminal-tab-C7Hdv1nq.js → terminal-tab-Dw4IKWGM.js} +1 -1
  17. package/dist/web/assets/{use-monaco-theme-CI4vTUsh.js → use-monaco-theme-DA7EyR70.js} +1 -1
  18. package/dist/web/index.html +2 -2
  19. package/dist/web/sw.js +1 -1
  20. package/docs/codebase-summary.md +33 -3
  21. package/docs/project-changelog.md +47 -0
  22. package/docs/project-roadmap.md +14 -7
  23. package/docs/streaming-input-guide.md +267 -0
  24. package/docs/system-architecture.md +65 -2
  25. package/package.json +1 -1
  26. package/snapshot-state.md +1526 -0
  27. package/src/server/index.ts +8 -1
  28. package/src/server/routes/settings.ts +72 -1
  29. package/src/services/clawbot/clawbot-formatter.ts +88 -0
  30. package/src/services/clawbot/clawbot-memory.ts +333 -0
  31. package/src/services/clawbot/clawbot-service.ts +500 -0
  32. package/src/services/clawbot/clawbot-session.ts +188 -0
  33. package/src/services/clawbot/clawbot-streamer.ts +245 -0
  34. package/src/services/clawbot/clawbot-telegram.ts +251 -0
  35. package/src/services/config.service.ts +1 -1
  36. package/src/services/db.service.ts +279 -1
  37. package/src/services/supervisor.ts +10 -0
  38. package/src/types/clawbot.ts +103 -0
  39. package/src/types/config.ts +22 -0
  40. package/src/web/components/chat/chat-history-bar.tsx +8 -3
  41. package/src/web/components/settings/clawbot-settings-section.tsx +270 -0
  42. package/src/web/components/settings/settings-tab.tsx +4 -1
  43. package/test-session-ops.mjs +444 -0
  44. package/test-tokens.mjs +212 -0
  45. package/dist/web/assets/index-CJvp0DJT.css +0 -2
  46. package/dist/web/assets/index-DocPzjV6.js +0 -30
  47. package/dist/web/assets/keybindings-store-2KURy8S3.js +0 -1
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import { mkdirSync, existsSync } from "node:fs";
5
5
 
6
6
  const PPM_DIR = process.env.PPM_HOME || resolve(homedir(), ".ppm");
7
- const CURRENT_SCHEMA_VERSION = 12;
7
+ const CURRENT_SCHEMA_VERSION = 13;
8
8
 
9
9
  let db: Database | null = null;
10
10
  let dbProfile: string | null = null;
@@ -313,6 +313,80 @@ function runMigrations(database: Database): void {
313
313
  PRAGMA user_version = 12;
314
314
  `);
315
315
  }
316
+
317
+ if (current < 13) {
318
+ database.exec(`
319
+ CREATE TABLE IF NOT EXISTS clawbot_sessions (
320
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
321
+ telegram_chat_id TEXT NOT NULL,
322
+ session_id TEXT NOT NULL,
323
+ provider_id TEXT NOT NULL DEFAULT 'claude',
324
+ project_name TEXT NOT NULL,
325
+ project_path TEXT NOT NULL,
326
+ is_active INTEGER NOT NULL DEFAULT 1,
327
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
328
+ last_message_at INTEGER NOT NULL DEFAULT (unixepoch())
329
+ );
330
+
331
+ CREATE INDEX IF NOT EXISTS idx_clawbot_sessions_chat
332
+ ON clawbot_sessions(telegram_chat_id, is_active);
333
+ CREATE INDEX IF NOT EXISTS idx_clawbot_sessions_session
334
+ ON clawbot_sessions(session_id);
335
+
336
+ CREATE TABLE IF NOT EXISTS clawbot_memories (
337
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
338
+ project TEXT NOT NULL,
339
+ content TEXT NOT NULL,
340
+ category TEXT NOT NULL DEFAULT 'fact',
341
+ importance REAL NOT NULL DEFAULT 1.0,
342
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
343
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
344
+ session_id TEXT,
345
+ superseded_by INTEGER REFERENCES clawbot_memories(id)
346
+ );
347
+
348
+ CREATE INDEX IF NOT EXISTS idx_clawbot_memories_project
349
+ ON clawbot_memories(project, superseded_by);
350
+ CREATE INDEX IF NOT EXISTS idx_clawbot_memories_importance
351
+ ON clawbot_memories(importance DESC);
352
+
353
+ CREATE VIRTUAL TABLE IF NOT EXISTS clawbot_memories_fts USING fts5(
354
+ content,
355
+ content='clawbot_memories',
356
+ content_rowid='id'
357
+ );
358
+
359
+ -- FTS5 sync triggers
360
+ CREATE TRIGGER IF NOT EXISTS clawbot_memories_ai AFTER INSERT ON clawbot_memories BEGIN
361
+ INSERT INTO clawbot_memories_fts(rowid, content) VALUES (new.id, new.content);
362
+ END;
363
+
364
+ CREATE TRIGGER IF NOT EXISTS clawbot_memories_ad AFTER DELETE ON clawbot_memories BEGIN
365
+ INSERT INTO clawbot_memories_fts(clawbot_memories_fts, rowid, content) VALUES('delete', old.id, old.content);
366
+ END;
367
+
368
+ CREATE TRIGGER IF NOT EXISTS clawbot_memories_au AFTER UPDATE ON clawbot_memories BEGIN
369
+ INSERT INTO clawbot_memories_fts(clawbot_memories_fts, rowid, content) VALUES('delete', old.id, old.content);
370
+ INSERT INTO clawbot_memories_fts(rowid, content) VALUES (new.id, new.content);
371
+ END;
372
+
373
+ CREATE TABLE IF NOT EXISTS clawbot_paired_chats (
374
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
375
+ telegram_chat_id TEXT NOT NULL UNIQUE,
376
+ telegram_user_id TEXT,
377
+ display_name TEXT,
378
+ pairing_code TEXT,
379
+ status TEXT NOT NULL DEFAULT 'pending',
380
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
381
+ approved_at INTEGER
382
+ );
383
+
384
+ CREATE INDEX IF NOT EXISTS idx_clawbot_paired_status
385
+ ON clawbot_paired_chats(status);
386
+
387
+ PRAGMA user_version = 13;
388
+ `);
389
+ }
316
390
  }
317
391
 
318
392
  // ---------------------------------------------------------------------------
@@ -894,5 +968,209 @@ export function deleteExtensionStorage(extId: string): void {
894
968
  getDb().query("DELETE FROM extension_storage WHERE ext_id = ?").run(extId);
895
969
  }
896
970
 
971
+ // ---------------------------------------------------------------------------
972
+ // ClawBot session helpers
973
+ // ---------------------------------------------------------------------------
974
+
975
+ import type { ClawBotSessionRow, ClawBotMemoryRow, ClawBotPairedChat } from "../types/clawbot.ts";
976
+
977
+ export function getActiveClawBotSession(
978
+ telegramChatId: string,
979
+ projectName: string,
980
+ ): ClawBotSessionRow | null {
981
+ return getDb().query(
982
+ `SELECT * FROM clawbot_sessions
983
+ WHERE telegram_chat_id = ? AND project_name = ? AND is_active = 1
984
+ ORDER BY last_message_at DESC LIMIT 1`,
985
+ ).get(telegramChatId, projectName) as ClawBotSessionRow | null;
986
+ }
987
+
988
+ export function createClawBotSession(
989
+ telegramChatId: string,
990
+ sessionId: string,
991
+ providerId: string,
992
+ projectName: string,
993
+ projectPath: string,
994
+ ): void {
995
+ getDb().query(
996
+ `INSERT INTO clawbot_sessions
997
+ (telegram_chat_id, session_id, provider_id, project_name, project_path)
998
+ VALUES (?, ?, ?, ?, ?)`,
999
+ ).run(telegramChatId, sessionId, providerId, projectName, projectPath);
1000
+ }
1001
+
1002
+ export function deactivateClawBotSession(sessionId: string): void {
1003
+ getDb().query(
1004
+ "UPDATE clawbot_sessions SET is_active = 0 WHERE session_id = ?",
1005
+ ).run(sessionId);
1006
+ }
1007
+
1008
+ export function touchClawBotSession(sessionId: string): void {
1009
+ getDb().query(
1010
+ "UPDATE clawbot_sessions SET last_message_at = unixepoch() WHERE session_id = ?",
1011
+ ).run(sessionId);
1012
+ }
1013
+
1014
+ export function getRecentClawBotSessions(
1015
+ telegramChatId: string,
1016
+ limit = 10,
1017
+ ): ClawBotSessionRow[] {
1018
+ return getDb().query(
1019
+ `SELECT * FROM clawbot_sessions
1020
+ WHERE telegram_chat_id = ?
1021
+ ORDER BY last_message_at DESC LIMIT ?`,
1022
+ ).all(telegramChatId, limit) as ClawBotSessionRow[];
1023
+ }
1024
+
1025
+ // ---------------------------------------------------------------------------
1026
+ // ClawBot memory helpers
1027
+ // ---------------------------------------------------------------------------
1028
+
1029
+ export function insertClawBotMemory(
1030
+ project: string,
1031
+ content: string,
1032
+ category: string,
1033
+ importance: number,
1034
+ sessionId?: string,
1035
+ ): number {
1036
+ const result = getDb().query(
1037
+ `INSERT INTO clawbot_memories (project, content, category, importance, session_id)
1038
+ VALUES (?, ?, ?, ?, ?)`,
1039
+ ).run(project, content, category, importance, sessionId ?? null);
1040
+ return Number(result.lastInsertRowid);
1041
+ }
1042
+
1043
+ export function searchClawBotMemories(
1044
+ project: string,
1045
+ query: string,
1046
+ limit = 20,
1047
+ ): Array<ClawBotMemoryRow & { rank: number }> {
1048
+ return getDb().query(
1049
+ `SELECT m.*, fts.rank
1050
+ FROM clawbot_memories m
1051
+ JOIN clawbot_memories_fts fts ON m.id = fts.rowid
1052
+ WHERE clawbot_memories_fts MATCH ?
1053
+ AND m.project IN (?, '_global')
1054
+ AND m.superseded_by IS NULL
1055
+ ORDER BY fts.rank
1056
+ LIMIT ?`,
1057
+ ).all(query, project, limit) as Array<ClawBotMemoryRow & { rank: number }>;
1058
+ }
1059
+
1060
+ export function getClawBotMemories(
1061
+ project: string,
1062
+ limit = 20,
1063
+ ): ClawBotMemoryRow[] {
1064
+ return getDb().query(
1065
+ `SELECT * FROM clawbot_memories
1066
+ WHERE project IN (?, '_global')
1067
+ AND superseded_by IS NULL
1068
+ ORDER BY importance DESC, updated_at DESC
1069
+ LIMIT ?`,
1070
+ ).all(project, limit) as ClawBotMemoryRow[];
1071
+ }
1072
+
1073
+ export function supersedeClawBotMemory(
1074
+ oldId: number,
1075
+ newId: number,
1076
+ ): void {
1077
+ getDb().query(
1078
+ "UPDATE clawbot_memories SET superseded_by = ? WHERE id = ?",
1079
+ ).run(newId, oldId);
1080
+ }
1081
+
1082
+ export function deleteClawBotMemoriesByTopic(
1083
+ project: string,
1084
+ topic: string,
1085
+ ): number {
1086
+ const matches = getDb().query(
1087
+ `SELECT m.id FROM clawbot_memories m
1088
+ JOIN clawbot_memories_fts fts ON m.id = fts.rowid
1089
+ WHERE clawbot_memories_fts MATCH ?
1090
+ AND m.project IN (?, '_global')
1091
+ AND m.superseded_by IS NULL`,
1092
+ ).all(topic, project) as { id: number }[];
1093
+
1094
+ for (const row of matches) {
1095
+ getDb().query("DELETE FROM clawbot_memories WHERE id = ?").run(row.id);
1096
+ }
1097
+ return matches.length;
1098
+ }
1099
+
1100
+ export function decayClawBotMemories(): void {
1101
+ getDb().query(
1102
+ `UPDATE clawbot_memories
1103
+ SET importance = importance * 0.95,
1104
+ updated_at = unixepoch()
1105
+ WHERE superseded_by IS NULL
1106
+ AND category NOT IN ('preference', 'architecture')
1107
+ AND updated_at < unixepoch() - 604800`,
1108
+ ).run();
1109
+ getDb().query(
1110
+ `DELETE FROM clawbot_memories WHERE importance < 0.1 AND superseded_by IS NULL`,
1111
+ ).run();
1112
+ }
1113
+
1114
+ // ---------------------------------------------------------------------------
1115
+ // ClawBot pairing helpers
1116
+ // ---------------------------------------------------------------------------
1117
+
1118
+ export function createPairingRequest(
1119
+ chatId: string,
1120
+ userId: string,
1121
+ displayName: string,
1122
+ code: string,
1123
+ ): void {
1124
+ getDb().query(
1125
+ `INSERT INTO clawbot_paired_chats (telegram_chat_id, telegram_user_id, display_name, pairing_code, status)
1126
+ VALUES (?, ?, ?, ?, 'pending')
1127
+ ON CONFLICT(telegram_chat_id) DO UPDATE SET
1128
+ telegram_user_id = excluded.telegram_user_id,
1129
+ display_name = excluded.display_name,
1130
+ pairing_code = excluded.pairing_code,
1131
+ status = 'pending',
1132
+ approved_at = NULL`,
1133
+ ).run(chatId, userId, displayName, code);
1134
+ }
1135
+
1136
+ export function approvePairing(chatId: string): void {
1137
+ getDb().query(
1138
+ `UPDATE clawbot_paired_chats
1139
+ SET status = 'approved', pairing_code = NULL, approved_at = unixepoch()
1140
+ WHERE telegram_chat_id = ? AND status = 'pending'`,
1141
+ ).run(chatId);
1142
+ }
1143
+
1144
+ export function revokePairing(chatId: string): void {
1145
+ getDb().query(
1146
+ "UPDATE clawbot_paired_chats SET status = 'revoked' WHERE telegram_chat_id = ?",
1147
+ ).run(chatId);
1148
+ }
1149
+
1150
+ export function getPairingByCode(code: string): ClawBotPairedChat | null {
1151
+ return getDb().query(
1152
+ "SELECT * FROM clawbot_paired_chats WHERE pairing_code = ? AND status = 'pending'",
1153
+ ).get(code) as ClawBotPairedChat | null;
1154
+ }
1155
+
1156
+ export function getPairingByChatId(chatId: string): ClawBotPairedChat | null {
1157
+ return getDb().query(
1158
+ "SELECT * FROM clawbot_paired_chats WHERE telegram_chat_id = ?",
1159
+ ).get(chatId) as ClawBotPairedChat | null;
1160
+ }
1161
+
1162
+ export function listPairedChats(): ClawBotPairedChat[] {
1163
+ return getDb().query(
1164
+ "SELECT * FROM clawbot_paired_chats WHERE status != 'revoked' ORDER BY created_at DESC",
1165
+ ).all() as ClawBotPairedChat[];
1166
+ }
1167
+
1168
+ export function isPairedChat(chatId: string): boolean {
1169
+ const row = getDb().query(
1170
+ "SELECT 1 FROM clawbot_paired_chats WHERE telegram_chat_id = ? AND status = 'approved'",
1171
+ ).get(chatId);
1172
+ return row != null;
1173
+ }
1174
+
897
1175
  // Auto-close on process exit
898
1176
  process.on("beforeExit", closeDb);
@@ -9,6 +9,7 @@ import { resolve } from "node:path";
9
9
  import { homedir } from "node:os";
10
10
  import {
11
11
  readFileSync, writeFileSync, existsSync, mkdirSync, openSync, appendFileSync,
12
+ unlinkSync,
12
13
  } from "node:fs";
13
14
  import { isCompiledBinary } from "./autostart-generator.ts";
14
15
 
@@ -30,6 +31,7 @@ const PPM_DIR = resolve(process.env.PPM_HOME || resolve(homedir(), ".ppm"));
30
31
  const STATUS_FILE = resolve(PPM_DIR, "status.json");
31
32
  const PID_FILE = resolve(PPM_DIR, "ppm.pid");
32
33
  const LOG_FILE = resolve(PPM_DIR, "ppm.log");
34
+ const RESTARTING_FLAG = resolve(PPM_DIR, ".restarting");
33
35
 
34
36
  // ─── State ─────────────────────────────────────────────────────────────
35
37
  let serverChild: Subprocess | null = null;
@@ -417,6 +419,9 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
417
419
  supervisorState = "upgrading";
418
420
  updateStatus({ state: "upgrading" });
419
421
 
422
+ // Set restarting flag so server child's stopTunnel() skips killing the tunnel
423
+ try { writeFileSync(RESTARTING_FLAG, ""); } catch {}
424
+
420
425
  // Kill server child to free the port; keep tunnel alive for domain continuity
421
426
  log("INFO", "Stopping server before spawning new supervisor (tunnel kept alive)");
422
427
  if (serverChild) { try { serverChild.kill(); } catch {} serverChild = null; }
@@ -456,6 +461,7 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
456
461
  // Timeout — new supervisor didn't start, restore old supervisor
457
462
  log("ERROR", "Self-replace timeout: new supervisor did not start");
458
463
  try { child.kill(); } catch {}
464
+ try { unlinkSync(RESTARTING_FLAG); } catch {}
459
465
  shuttingDown = false;
460
466
  notifyStateChange("upgrading", "running", "upgrade_failed");
461
467
  supervisorState = "running";
@@ -463,6 +469,7 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
463
469
  return { success: false, error: "New supervisor failed to start within 30s" };
464
470
  } catch (e) {
465
471
  log("ERROR", `Self-replace error: ${e}`);
472
+ try { unlinkSync(RESTARTING_FLAG); } catch {}
466
473
  shuttingDown = false;
467
474
  notifyStateChange("upgrading", "running", "upgrade_failed");
468
475
  supervisorState = "running";
@@ -631,6 +638,9 @@ export async function runSupervisor(opts: {
631
638
  }) {
632
639
  if (!existsSync(PPM_DIR)) mkdirSync(PPM_DIR, { recursive: true });
633
640
 
641
+ // Clean up restarting flag from previous upgrade/restart
642
+ try { unlinkSync(RESTARTING_FLAG); } catch {}
643
+
634
644
  // Save original argv for self-replace
635
645
  originalArgv = [...process.argv];
636
646
 
@@ -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,16 @@ export const DEFAULT_CONFIG: PpmConfig = {
85
97
  },
86
98
  },
87
99
  },
100
+ clawbot: {
101
+ enabled: false,
102
+ default_provider: "claude",
103
+ default_project: "",
104
+ system_prompt: "",
105
+ show_tool_calls: true,
106
+ show_thinking: false,
107
+ permission_mode: "bypassPermissions",
108
+ debounce_ms: 2000,
109
+ },
88
110
  };
89
111
 
90
112
  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)}