@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
|
@@ -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 =
|
|
7
|
+
const CURRENT_SCHEMA_VERSION = 12;
|
|
8
8
|
|
|
9
9
|
let db: Database | null = null;
|
|
10
10
|
let dbProfile: string | null = null;
|
|
@@ -313,80 +313,6 @@ 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
|
-
}
|
|
390
316
|
}
|
|
391
317
|
|
|
392
318
|
// ---------------------------------------------------------------------------
|
|
@@ -968,209 +894,5 @@ export function deleteExtensionStorage(extId: string): void {
|
|
|
968
894
|
getDb().query("DELETE FROM extension_storage WHERE ext_id = ?").run(extId);
|
|
969
895
|
}
|
|
970
896
|
|
|
971
|
-
// ---------------------------------------------------------------------------
|
|
972
|
-
// PPMBot session helpers
|
|
973
|
-
// ---------------------------------------------------------------------------
|
|
974
|
-
|
|
975
|
-
import type { PPMBotSessionRow, PPMBotMemoryRow, PPMBotPairedChat } from "../types/ppmbot.ts";
|
|
976
|
-
|
|
977
|
-
export function getActivePPMBotSession(
|
|
978
|
-
telegramChatId: string,
|
|
979
|
-
projectName: string,
|
|
980
|
-
): PPMBotSessionRow | 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 PPMBotSessionRow | null;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
export function createPPMBotSession(
|
|
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 deactivatePPMBotSession(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 touchPPMBotSession(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 getRecentPPMBotSessions(
|
|
1015
|
-
telegramChatId: string,
|
|
1016
|
-
limit = 10,
|
|
1017
|
-
): PPMBotSessionRow[] {
|
|
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 PPMBotSessionRow[];
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
// ---------------------------------------------------------------------------
|
|
1026
|
-
// PPMBot memory helpers
|
|
1027
|
-
// ---------------------------------------------------------------------------
|
|
1028
|
-
|
|
1029
|
-
export function insertPPMBotMemory(
|
|
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 searchPPMBotMemories(
|
|
1044
|
-
project: string,
|
|
1045
|
-
query: string,
|
|
1046
|
-
limit = 20,
|
|
1047
|
-
): Array<PPMBotMemoryRow & { 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<PPMBotMemoryRow & { rank: number }>;
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
export function getPPMBotMemories(
|
|
1061
|
-
project: string,
|
|
1062
|
-
limit = 20,
|
|
1063
|
-
): PPMBotMemoryRow[] {
|
|
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 PPMBotMemoryRow[];
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
export function supersedePPMBotMemory(
|
|
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 deletePPMBotMemoriesByTopic(
|
|
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 decayPPMBotMemories(): 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
|
-
// PPMBot 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): PPMBotPairedChat | null {
|
|
1151
|
-
return getDb().query(
|
|
1152
|
-
"SELECT * FROM clawbot_paired_chats WHERE pairing_code = ? AND status = 'pending'",
|
|
1153
|
-
).get(code) as PPMBotPairedChat | null;
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
export function getPairingByChatId(chatId: string): PPMBotPairedChat | null {
|
|
1157
|
-
return getDb().query(
|
|
1158
|
-
"SELECT * FROM clawbot_paired_chats WHERE telegram_chat_id = ?",
|
|
1159
|
-
).get(chatId) as PPMBotPairedChat | null;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
export function listPairedChats(): PPMBotPairedChat[] {
|
|
1163
|
-
return getDb().query(
|
|
1164
|
-
"SELECT * FROM clawbot_paired_chats WHERE status != 'revoked' ORDER BY created_at DESC",
|
|
1165
|
-
).all() as PPMBotPairedChat[];
|
|
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
|
-
|
|
1175
897
|
// Auto-close on process exit
|
|
1176
898
|
process.on("beforeExit", closeDb);
|
|
@@ -404,10 +404,10 @@ class GitService {
|
|
|
404
404
|
// Worktree operations
|
|
405
405
|
// ---------------------------------------------------------------------------
|
|
406
406
|
|
|
407
|
-
/** Parse `git worktree list --porcelain
|
|
407
|
+
/** Parse `git worktree list --porcelain` output into GitWorktree[]. */
|
|
408
408
|
async listWorktrees(projectPath: string): Promise<GitWorktree[]> {
|
|
409
409
|
const git = this.git(projectPath);
|
|
410
|
-
const raw = await git.raw(["worktree", "list", "--porcelain"
|
|
410
|
+
const raw = await git.raw(["worktree", "list", "--porcelain"]);
|
|
411
411
|
const worktrees: GitWorktree[] = [];
|
|
412
412
|
// Blocks are separated by blank lines
|
|
413
413
|
const blocks = raw.trim().split(/\n\n+/);
|
package/src/types/config.ts
CHANGED
|
@@ -9,17 +9,6 @@ export interface TelegramConfig {
|
|
|
9
9
|
chat_id: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export interface PPMBotConfig {
|
|
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
|
-
|
|
23
12
|
export type ThemeConfig = "light" | "dark" | "system";
|
|
24
13
|
|
|
25
14
|
export interface PpmConfig {
|
|
@@ -32,7 +21,6 @@ export interface PpmConfig {
|
|
|
32
21
|
ai: AIConfig;
|
|
33
22
|
push?: PushConfig;
|
|
34
23
|
telegram?: TelegramConfig;
|
|
35
|
-
clawbot?: PPMBotConfig;
|
|
36
24
|
cloud_url?: string;
|
|
37
25
|
}
|
|
38
26
|
|
|
@@ -97,20 +85,6 @@ export const DEFAULT_CONFIG: PpmConfig = {
|
|
|
97
85
|
},
|
|
98
86
|
},
|
|
99
87
|
},
|
|
100
|
-
telegram: {
|
|
101
|
-
bot_token: "",
|
|
102
|
-
chat_id: "",
|
|
103
|
-
},
|
|
104
|
-
clawbot: {
|
|
105
|
-
enabled: false,
|
|
106
|
-
default_provider: "claude",
|
|
107
|
-
default_project: "",
|
|
108
|
-
system_prompt: "You are PPMBot, a helpful AI coding assistant on Telegram. Keep responses concise and mobile-friendly. Use short paragraphs. When showing code, use compact examples. Be direct and helpful.",
|
|
109
|
-
show_tool_calls: true,
|
|
110
|
-
show_thinking: false,
|
|
111
|
-
permission_mode: "bypassPermissions",
|
|
112
|
-
debounce_ms: 2000,
|
|
113
|
-
},
|
|
114
88
|
};
|
|
115
89
|
|
|
116
90
|
const VALID_TYPES = ["agent-sdk", "cli", "mock"] as const;
|
|
@@ -1,45 +1,78 @@
|
|
|
1
|
-
import { useState,
|
|
2
|
-
import { ExternalLink, Globe, Loader2,
|
|
3
|
-
import { useTabStore } from "@/stores/tab-store";
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { Check, Copy, ExternalLink, Globe, Loader2, Square, Wifi } from "lucide-react";
|
|
4
3
|
import { api } from "@/lib/api-client";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
5
|
|
|
6
|
-
interface
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
interface TunnelInfo {
|
|
7
|
+
port: number;
|
|
8
|
+
url: string;
|
|
9
|
+
startedAt: number;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export function BrowserTab(
|
|
12
|
-
const
|
|
13
|
-
const [
|
|
14
|
-
const [tunnelUrl, setTunnelUrl] = useState<string | null>(null);
|
|
12
|
+
export function BrowserTab() {
|
|
13
|
+
const [portInput, setPortInput] = useState("");
|
|
14
|
+
const [tunnels, setTunnels] = useState<TunnelInfo[]>([]);
|
|
15
15
|
const [loading, setLoading] = useState(false);
|
|
16
16
|
const [error, setError] = useState<string | null>(null);
|
|
17
|
-
const
|
|
18
|
-
|
|
17
|
+
const [copiedPort, setCopiedPort] = useState<number | null>(null);
|
|
18
|
+
|
|
19
|
+
const fetchTunnels = useCallback(async () => {
|
|
20
|
+
try {
|
|
21
|
+
const list = await api.get<TunnelInfo[]>("/api/preview/tunnels");
|
|
22
|
+
setTunnels(list);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.warn("[ports] failed to fetch tunnels", e);
|
|
25
|
+
}
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
// Fetch tunnels on mount + poll every 10s
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
fetchTunnels();
|
|
31
|
+
const interval = setInterval(fetchTunnels, 10_000);
|
|
32
|
+
return () => clearInterval(interval);
|
|
33
|
+
}, [fetchTunnels]);
|
|
34
|
+
|
|
35
|
+
const startTunnel = async (port: number) => {
|
|
36
|
+
// Check if already forwarded
|
|
37
|
+
const existing = tunnels.find((t) => t.port === port);
|
|
38
|
+
if (existing) {
|
|
39
|
+
window.open(existing.url, "_blank");
|
|
40
|
+
setPortInput("");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
19
43
|
|
|
20
|
-
const startTunnel = useCallback(async (port: number) => {
|
|
21
44
|
setLoading(true);
|
|
22
45
|
setError(null);
|
|
23
|
-
setTunnelUrl(null);
|
|
24
|
-
|
|
25
46
|
try {
|
|
26
47
|
const res = await api.post<{ port: number; url: string }>("/api/preview/tunnel", { port });
|
|
27
|
-
|
|
28
|
-
|
|
48
|
+
window.open(res.url, "_blank");
|
|
49
|
+
setPortInput("");
|
|
50
|
+
await fetchTunnels();
|
|
29
51
|
} catch (e: any) {
|
|
30
52
|
setError(e.message || `Failed to start tunnel for port ${port}`);
|
|
31
53
|
} finally {
|
|
32
54
|
setLoading(false);
|
|
33
55
|
}
|
|
34
|
-
}
|
|
56
|
+
};
|
|
35
57
|
|
|
36
|
-
const stopTunnel =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
const stopTunnel = async (port: number) => {
|
|
59
|
+
try {
|
|
60
|
+
await api.del(`/api/preview/tunnel/${port}`);
|
|
61
|
+
await fetchTunnels();
|
|
62
|
+
} catch (e: any) {
|
|
63
|
+
toast.error(e.message || `Failed to stop tunnel for port ${port}`);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const copyUrl = (port: number, url: string) => {
|
|
68
|
+
navigator.clipboard.writeText(url).then(() => {
|
|
69
|
+
setCopiedPort(port);
|
|
70
|
+
toast.success("URL copied");
|
|
71
|
+
setTimeout(() => setCopiedPort(null), 2000);
|
|
72
|
+
}).catch(() => {
|
|
73
|
+
toast.error("Failed to copy URL");
|
|
74
|
+
});
|
|
75
|
+
};
|
|
43
76
|
|
|
44
77
|
const handleSubmit = (e: React.FormEvent) => {
|
|
45
78
|
e.preventDefault();
|
|
@@ -48,100 +81,98 @@ export function BrowserTab({ metadata, tabId }: BrowserTabProps) {
|
|
|
48
81
|
else setError("Port must be 1-65535");
|
|
49
82
|
};
|
|
50
83
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<div className="flex flex-col items-center justify-center h-full gap-4 p-6">
|
|
63
|
-
<Globe className="size-12 text-text-subtle" />
|
|
64
|
-
<h2 className="text-lg font-medium text-text-primary">Open Localhost</h2>
|
|
65
|
-
<p className="text-sm text-text-secondary text-center max-w-sm">
|
|
66
|
-
Enter the port of your local dev server to preview it here.
|
|
67
|
-
</p>
|
|
68
|
-
<form onSubmit={handleSubmit} className="flex items-center gap-2 w-full max-w-xs">
|
|
69
|
-
<div className="flex-1 flex items-center gap-2 px-3 py-2.5 rounded-lg bg-surface border border-border focus-within:border-accent/50 transition-colors">
|
|
84
|
+
return (
|
|
85
|
+
<div className="flex flex-col h-full w-full bg-background">
|
|
86
|
+
{/* Header + form */}
|
|
87
|
+
<div className="p-4 md:p-6 border-b border-border bg-surface">
|
|
88
|
+
<div className="flex items-center gap-2 mb-3">
|
|
89
|
+
<Wifi className="size-5 text-accent" />
|
|
90
|
+
<h2 className="text-base font-medium text-text-primary">Port Forwarding</h2>
|
|
91
|
+
</div>
|
|
92
|
+
<form onSubmit={handleSubmit} className="flex items-center gap-2">
|
|
93
|
+
<div className="flex-1 flex items-center gap-2 px-3 py-2.5 rounded-lg bg-background border border-border focus-within:border-accent/50 transition-colors">
|
|
70
94
|
<span className="text-sm text-text-subtle shrink-0">localhost:</span>
|
|
71
95
|
<input
|
|
72
96
|
type="number"
|
|
73
97
|
value={portInput}
|
|
74
|
-
onChange={(e) => setPortInput(e.target.value)}
|
|
98
|
+
onChange={(e) => { setPortInput(e.target.value); setError(null); }}
|
|
75
99
|
placeholder="3000"
|
|
76
100
|
min={1}
|
|
77
101
|
max={65535}
|
|
78
|
-
autoFocus
|
|
79
102
|
className="flex-1 bg-transparent text-sm text-text-primary outline-none placeholder:text-text-subtle min-w-0 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
|
80
103
|
/>
|
|
81
104
|
</div>
|
|
82
105
|
<button
|
|
83
106
|
type="submit"
|
|
84
107
|
disabled={loading || !portInput}
|
|
85
|
-
className="px-4 py-2.5 rounded-lg bg-accent text-white text-sm font-medium hover:bg-accent/90 disabled:opacity-50 transition-colors shrink-0"
|
|
108
|
+
className="px-4 py-2.5 rounded-lg bg-accent text-white text-sm font-medium hover:bg-accent/90 disabled:opacity-50 transition-colors shrink-0 min-w-[72px] flex items-center justify-center"
|
|
86
109
|
>
|
|
87
|
-
{loading ? <Loader2 className="size-4 animate-spin" /> : "
|
|
110
|
+
{loading ? <Loader2 className="size-4 animate-spin" /> : "Forward"}
|
|
88
111
|
</button>
|
|
89
112
|
</form>
|
|
90
|
-
{error && <p className="text-sm text-red-400">{error}</p>}
|
|
113
|
+
{error && <p className="text-sm text-red-400 mt-2">{error}</p>}
|
|
91
114
|
{loading && (
|
|
92
|
-
<div className="flex items-center gap-2 text-sm text-text-secondary">
|
|
93
|
-
<Loader2 className="size-
|
|
94
|
-
<span>Starting tunnel
|
|
115
|
+
<div className="flex items-center gap-2 text-sm text-text-secondary mt-2">
|
|
116
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
117
|
+
<span>Starting tunnel...</span>
|
|
95
118
|
</div>
|
|
96
119
|
)}
|
|
97
120
|
</div>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
121
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
>
|
|
122
|
-
<ExternalLink className="size-3.5" />
|
|
123
|
-
</button>
|
|
124
|
-
<button
|
|
125
|
-
onClick={stopTunnel}
|
|
126
|
-
className="p-1.5 rounded hover:bg-surface-elevated text-red-400 transition-colors"
|
|
127
|
-
title="Stop tunnel"
|
|
128
|
-
>
|
|
129
|
-
<X className="size-3.5" />
|
|
130
|
-
</button>
|
|
131
|
-
</div>
|
|
122
|
+
{/* Tunnel list */}
|
|
123
|
+
<div className="flex-1 overflow-y-auto p-4 md:p-6">
|
|
124
|
+
{tunnels.length === 0 ? (
|
|
125
|
+
<div className="flex flex-col items-center justify-center h-full gap-3 text-center">
|
|
126
|
+
<Globe className="size-10 text-text-subtle" />
|
|
127
|
+
<p className="text-sm text-text-secondary max-w-xs">
|
|
128
|
+
No active ports. Forward a port to access your local dev server from anywhere.
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
) : (
|
|
132
|
+
<div className="space-y-2">
|
|
133
|
+
{tunnels.map((t) => (
|
|
134
|
+
<div
|
|
135
|
+
key={t.port}
|
|
136
|
+
className="flex items-center gap-3 p-3 rounded-lg bg-surface border border-border"
|
|
137
|
+
>
|
|
138
|
+
{/* Port badge */}
|
|
139
|
+
<div className="shrink-0 px-2 py-1 rounded bg-accent/10 text-accent text-xs font-mono font-medium">
|
|
140
|
+
:{t.port}
|
|
141
|
+
</div>
|
|
132
142
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
{/* URL - truncated */}
|
|
144
|
+
<span className="flex-1 text-xs text-text-secondary truncate min-w-0">
|
|
145
|
+
{t.url}
|
|
146
|
+
</span>
|
|
147
|
+
|
|
148
|
+
{/* Actions — 44px touch targets */}
|
|
149
|
+
<div className="flex items-center shrink-0">
|
|
150
|
+
<button
|
|
151
|
+
onClick={() => window.open(t.url, "_blank")}
|
|
152
|
+
className="p-2.5 rounded-md hover:bg-surface-elevated transition-colors"
|
|
153
|
+
title="Open in browser"
|
|
154
|
+
>
|
|
155
|
+
<ExternalLink className="size-4 text-text-secondary" />
|
|
156
|
+
</button>
|
|
157
|
+
<button
|
|
158
|
+
onClick={() => copyUrl(t.port, t.url)}
|
|
159
|
+
className="p-2.5 rounded-md hover:bg-surface-elevated transition-colors"
|
|
160
|
+
title="Copy URL"
|
|
161
|
+
>
|
|
162
|
+
{copiedPort === t.port
|
|
163
|
+
? <Check className="size-4 text-green-400" />
|
|
164
|
+
: <Copy className="size-4 text-text-secondary" />}
|
|
165
|
+
</button>
|
|
166
|
+
<button
|
|
167
|
+
onClick={() => stopTunnel(t.port)}
|
|
168
|
+
className="p-2.5 rounded-md hover:bg-red-500/10 transition-colors"
|
|
169
|
+
title="Stop tunnel"
|
|
170
|
+
>
|
|
171
|
+
<Square className="size-4 text-red-400" />
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
))}
|
|
145
176
|
</div>
|
|
146
177
|
)}
|
|
147
178
|
</div>
|
|
@@ -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
|
|
2
|
+
import { History, Settings2, Loader2, MessageSquare, RefreshCw, Search, Pencil, Check, X, BellOff, Bug, ClipboardCheck, Pin, PinOff, Trash2, Users } 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,14 +398,9 @@ 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"
|
|
402
402
|
>
|
|
403
|
-
{session.title
|
|
404
|
-
<Bot className="size-3 text-muted-foreground shrink-0" />
|
|
405
|
-
)}
|
|
406
|
-
{session.title?.startsWith("[PPM]")
|
|
407
|
-
? session.title.slice(7)
|
|
408
|
-
: session.title || "Untitled"}
|
|
403
|
+
{session.title || "Untitled"}
|
|
409
404
|
</button>
|
|
410
405
|
<button
|
|
411
406
|
onClick={(e) => togglePin(e, session)}
|
|
@@ -161,7 +161,7 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
|
|
|
161
161
|
{ id: "chat", label: "New AI Chat", icon: MessageSquare, action: openNewTab("chat", "AI Chat"), keywords: "ai assistant claude", group: "action", shortcut: formatShortcut(getBinding("open-chat")) },
|
|
162
162
|
{ id: "terminal", label: "New Terminal", icon: Terminal, action: openNewTab("terminal", "Terminal"), keywords: "bash shell console", group: "action", shortcut: formatShortcut(getBinding("open-terminal")) },
|
|
163
163
|
{ id: "git-graph", label: "Git Graph", icon: GitBranch, action: openNewTab("git-graph", "Git Graph"), keywords: "branch history log", group: "action", shortcut: formatShortcut(getBinding("open-git-graph")) },
|
|
164
|
-
{ id: "browser", label: "
|
|
164
|
+
{ id: "browser", label: "Port Forwarding", icon: Globe, action: openNewTab("browser", "Ports"), keywords: "web preview localhost port forward tunnel url", group: "action" },
|
|
165
165
|
{ id: "postgres", label: "PostgreSQL", icon: Database, action: openNewTab("postgres", "PostgreSQL"), keywords: "database pg sql query", group: "action" },
|
|
166
166
|
{ id: "voice-input", label: "Voice Input", icon: Mic, action: () => { window.dispatchEvent(new CustomEvent("toggle-voice-input")); onClose(); }, keywords: "speech microphone dictate voice", group: "action", shortcut: formatShortcut(getBinding("voice-input")) },
|
|
167
167
|
{ id: "git-status", label: "Git Status", icon: GitCommitHorizontal, action: () => { setSidebarActiveTab("git"); onClose(); }, keywords: "changes diff staged", group: "action", shortcut: formatShortcut(getBinding("open-git-status")) },
|