@openacp/cli 0.4.8 → 0.4.10

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.
@@ -2428,12 +2428,14 @@ function splitMessage(text, maxLength = 3800) {
2428
2428
  chunks.push(remaining);
2429
2429
  break;
2430
2430
  }
2431
- let splitAt = remaining.lastIndexOf("\n\n", maxLength);
2432
- if (splitAt === -1 || splitAt < maxLength * 0.2) {
2433
- splitAt = remaining.lastIndexOf("\n", maxLength);
2431
+ const wouldLeaveSmall = remaining.length < maxLength * 1.3;
2432
+ const searchLimit = wouldLeaveSmall ? Math.floor(remaining.length / 2) + 300 : maxLength;
2433
+ let splitAt = remaining.lastIndexOf("\n\n", searchLimit);
2434
+ if (splitAt === -1 || splitAt < searchLimit * 0.2) {
2435
+ splitAt = remaining.lastIndexOf("\n", searchLimit);
2434
2436
  }
2435
- if (splitAt === -1 || splitAt < maxLength * 0.2) {
2436
- splitAt = maxLength;
2437
+ if (splitAt === -1 || splitAt < searchLimit * 0.2) {
2438
+ splitAt = searchLimit;
2437
2439
  }
2438
2440
  const candidate = remaining.slice(0, splitAt);
2439
2441
  const fences = candidate.match(/```/g);
@@ -2466,6 +2468,7 @@ var MessageDraft = class {
2466
2468
  flushTimer;
2467
2469
  flushPromise = Promise.resolve();
2468
2470
  lastSentBuffer = "";
2471
+ displayTruncated = false;
2469
2472
  append(text) {
2470
2473
  if (!text) return;
2471
2474
  this.buffer += text;
@@ -2482,16 +2485,20 @@ var MessageDraft = class {
2482
2485
  async flush() {
2483
2486
  if (!this.buffer) return;
2484
2487
  if (this.firstFlushPending) return;
2485
- let displayBuffer = this.buffer;
2486
- if (displayBuffer.length > 3800) {
2487
- let cutAt = displayBuffer.lastIndexOf("\n", 3800);
2488
- if (cutAt < 800) cutAt = 3800;
2489
- displayBuffer = displayBuffer.slice(0, cutAt) + "\n\u2026";
2490
- }
2491
- let html = markdownToTelegramHtml(displayBuffer);
2488
+ const snapshot = this.buffer;
2489
+ let html = markdownToTelegramHtml(snapshot);
2492
2490
  if (!html) return;
2491
+ let truncated = false;
2493
2492
  if (html.length > 4096) {
2494
- html = html.slice(0, 4090) + "\n\u2026";
2493
+ const ratio = 4e3 / html.length;
2494
+ const targetLen = Math.floor(snapshot.length * ratio);
2495
+ let cutAt = snapshot.lastIndexOf("\n", targetLen);
2496
+ if (cutAt < targetLen * 0.5) cutAt = targetLen;
2497
+ html = markdownToTelegramHtml(snapshot.slice(0, cutAt) + "\n\u2026");
2498
+ truncated = true;
2499
+ if (html.length > 4096) {
2500
+ html = html.slice(0, 4090) + "\n\u2026";
2501
+ }
2495
2502
  }
2496
2503
  if (!this.messageId) {
2497
2504
  this.firstFlushPending = true;
@@ -2506,7 +2513,12 @@ var MessageDraft = class {
2506
2513
  );
2507
2514
  if (result) {
2508
2515
  this.messageId = result.message_id;
2509
- this.lastSentBuffer = this.buffer;
2516
+ if (!truncated) {
2517
+ this.lastSentBuffer = snapshot;
2518
+ this.displayTruncated = false;
2519
+ } else {
2520
+ this.displayTruncated = true;
2521
+ }
2510
2522
  }
2511
2523
  } catch {
2512
2524
  } finally {
@@ -2514,13 +2526,20 @@ var MessageDraft = class {
2514
2526
  }
2515
2527
  } else {
2516
2528
  try {
2517
- await this.sendQueue.enqueue(
2529
+ const result = await this.sendQueue.enqueue(
2518
2530
  () => this.bot.api.editMessageText(this.chatId, this.messageId, html, {
2519
2531
  parse_mode: "HTML"
2520
2532
  }),
2521
2533
  { type: "text", key: this.sessionId }
2522
2534
  );
2523
- this.lastSentBuffer = this.buffer;
2535
+ if (result !== void 0) {
2536
+ if (!truncated) {
2537
+ this.lastSentBuffer = snapshot;
2538
+ this.displayTruncated = false;
2539
+ } else {
2540
+ this.displayTruncated = true;
2541
+ }
2542
+ }
2524
2543
  } catch {
2525
2544
  }
2526
2545
  }
@@ -2532,9 +2551,34 @@ var MessageDraft = class {
2532
2551
  }
2533
2552
  await this.flushPromise;
2534
2553
  if (!this.buffer) return this.messageId;
2535
- if (this.messageId && this.buffer === this.lastSentBuffer) {
2554
+ if (this.messageId && this.buffer === this.lastSentBuffer && !this.displayTruncated) {
2536
2555
  return this.messageId;
2537
2556
  }
2557
+ const fullHtml = markdownToTelegramHtml(this.buffer);
2558
+ if (fullHtml.length <= 4096) {
2559
+ try {
2560
+ if (this.messageId) {
2561
+ await this.sendQueue.enqueue(
2562
+ () => this.bot.api.editMessageText(this.chatId, this.messageId, fullHtml, {
2563
+ parse_mode: "HTML"
2564
+ }),
2565
+ { type: "other" }
2566
+ );
2567
+ } else {
2568
+ const msg = await this.sendQueue.enqueue(
2569
+ () => this.bot.api.sendMessage(this.chatId, fullHtml, {
2570
+ message_thread_id: this.threadId,
2571
+ parse_mode: "HTML",
2572
+ disable_notification: true
2573
+ }),
2574
+ { type: "other" }
2575
+ );
2576
+ if (msg) this.messageId = msg.message_id;
2577
+ }
2578
+ return this.messageId;
2579
+ } catch {
2580
+ }
2581
+ }
2538
2582
  const mdChunks = splitMessage(this.buffer);
2539
2583
  for (let i = 0; i < mdChunks.length; i++) {
2540
2584
  const html = markdownToTelegramHtml(mdChunks[i]);
@@ -2620,89 +2664,192 @@ function buildDeepLink(chatId, messageId) {
2620
2664
  return `https://t.me/c/${cleanId}/${messageId}`;
2621
2665
  }
2622
2666
 
2623
- // src/adapters/telegram/commands.ts
2667
+ // src/adapters/telegram/commands/new-session.ts
2668
+ import { InlineKeyboard as InlineKeyboard2 } from "grammy";
2669
+
2670
+ // src/adapters/telegram/commands/admin.ts
2624
2671
  import { InlineKeyboard } from "grammy";
2625
- var log8 = createChildLogger({ module: "telegram-commands" });
2626
- function setupCommands(bot, core, chatId, assistant) {
2627
- bot.command("new", (ctx) => handleNew(ctx, core, chatId, assistant));
2628
- bot.command("newchat", (ctx) => handleNewChat(ctx, core, chatId));
2629
- bot.command("cancel", (ctx) => handleCancel(ctx, core, assistant));
2630
- bot.command("status", (ctx) => handleStatus(ctx, core));
2631
- bot.command("sessions", (ctx) => handleTopics(ctx, core));
2632
- bot.command("agents", (ctx) => handleAgents(ctx, core));
2633
- bot.command("help", (ctx) => handleHelp(ctx));
2634
- bot.command("menu", (ctx) => handleMenu(ctx));
2635
- bot.command("enable_dangerous", (ctx) => handleEnableDangerous(ctx, core));
2636
- bot.command("disable_dangerous", (ctx) => handleDisableDangerous(ctx, core));
2637
- bot.command("restart", (ctx) => handleRestart(ctx, core));
2638
- bot.command("update", (ctx) => handleUpdate(ctx, core));
2639
- bot.command("integrate", (ctx) => handleIntegrate(ctx, core));
2640
- }
2641
- function buildMenuKeyboard() {
2642
- return new InlineKeyboard().text("\u{1F195} New Session", "m:new").text("\u{1F4AC} New Chat", "m:newchat").row().text("\u26D4 Cancel", "m:cancel").text("\u{1F4CA} Status", "m:status").row().text("\u{1F4CB} Sessions", "m:topics").text("\u{1F916} Agents", "m:agents").row().text("\u{1F517} Integrate", "m:integrate").text("\u2753 Help", "m:help").row().text("\u{1F504} Restart", "m:restart").text("\u2B06\uFE0F Update", "m:update");
2672
+ var log8 = createChildLogger({ module: "telegram-cmd-admin" });
2673
+ function buildDangerousModeKeyboard(sessionId, enabled) {
2674
+ return new InlineKeyboard().text(
2675
+ enabled ? "\u{1F510} Disable Dangerous Mode" : "\u2620\uFE0F Enable Dangerous Mode",
2676
+ `d:${sessionId}`
2677
+ );
2643
2678
  }
2644
- function setupMenuCallbacks(bot, core, chatId, systemTopicIds) {
2645
- bot.callbackQuery(/^m:/, async (ctx) => {
2646
- const data = ctx.callbackQuery.data;
2679
+ function setupDangerousModeCallbacks(bot, core) {
2680
+ bot.callbackQuery(/^d:/, async (ctx) => {
2681
+ const sessionId = ctx.callbackQuery.data.slice(2);
2682
+ const session = core.sessionManager.getSession(sessionId);
2683
+ if (session) {
2684
+ session.dangerousMode = !session.dangerousMode;
2685
+ log8.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
2686
+ core.sessionManager.updateSessionDangerousMode(sessionId, session.dangerousMode).catch(() => {
2687
+ });
2688
+ const toastText2 = session.dangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
2689
+ try {
2690
+ await ctx.answerCallbackQuery({ text: toastText2 });
2691
+ } catch {
2692
+ }
2693
+ try {
2694
+ await ctx.editMessageReplyMarkup({
2695
+ reply_markup: buildDangerousModeKeyboard(sessionId, session.dangerousMode)
2696
+ });
2697
+ } catch {
2698
+ }
2699
+ return;
2700
+ }
2701
+ const record = core.sessionManager.getSessionRecord(sessionId);
2702
+ if (!record || record.status === "cancelled" || record.status === "error") {
2703
+ try {
2704
+ await ctx.answerCallbackQuery({ text: "\u26A0\uFE0F Session not found or already ended." });
2705
+ } catch {
2706
+ }
2707
+ return;
2708
+ }
2709
+ const newDangerousMode = !(record.dangerousMode ?? false);
2710
+ core.sessionManager.updateSessionDangerousMode(sessionId, newDangerousMode).catch(() => {
2711
+ });
2712
+ log8.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
2713
+ const toastText = newDangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
2647
2714
  try {
2648
- await ctx.answerCallbackQuery();
2715
+ await ctx.answerCallbackQuery({ text: toastText });
2649
2716
  } catch {
2650
2717
  }
2651
- switch (data) {
2652
- case "m:new":
2653
- await handleNew(ctx, core, chatId);
2654
- break;
2655
- case "m:newchat":
2656
- await handleNewChat(ctx, core, chatId);
2657
- break;
2658
- case "m:cancel":
2659
- await handleCancel(ctx, core);
2660
- break;
2661
- case "m:status":
2662
- await handleStatus(ctx, core);
2663
- break;
2664
- case "m:agents":
2665
- await handleAgents(ctx, core);
2666
- break;
2667
- case "m:help":
2668
- await handleHelp(ctx);
2669
- break;
2670
- case "m:restart":
2671
- await handleRestart(ctx, core);
2672
- break;
2673
- case "m:update":
2674
- await handleUpdate(ctx, core);
2675
- break;
2676
- case "m:integrate":
2677
- await handleIntegrate(ctx, core);
2678
- break;
2679
- case "m:topics":
2680
- await handleTopics(ctx, core);
2681
- break;
2682
- case "m:cleanup:finished":
2683
- await handleCleanup(ctx, core, chatId, ["finished"]);
2684
- break;
2685
- case "m:cleanup:errors":
2686
- await handleCleanup(ctx, core, chatId, ["error", "cancelled"]);
2687
- break;
2688
- case "m:cleanup:all":
2689
- await handleCleanup(ctx, core, chatId, ["finished", "error", "cancelled"]);
2690
- break;
2691
- case "m:cleanup:everything":
2692
- await handleCleanupEverything(ctx, core, chatId, systemTopicIds);
2693
- break;
2694
- case "m:cleanup:everything:confirm":
2695
- await handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicIds);
2696
- break;
2718
+ try {
2719
+ await ctx.editMessageReplyMarkup({
2720
+ reply_markup: buildDangerousModeKeyboard(sessionId, newDangerousMode)
2721
+ });
2722
+ } catch {
2697
2723
  }
2698
2724
  });
2699
2725
  }
2700
- async function handleMenu(ctx) {
2701
- await ctx.reply(`<b>OpenACP Menu</b>
2702
- Choose an action:`, {
2703
- parse_mode: "HTML",
2704
- reply_markup: buildMenuKeyboard()
2705
- });
2726
+ async function handleEnableDangerous(ctx, core) {
2727
+ const threadId = ctx.message?.message_thread_id;
2728
+ if (!threadId) {
2729
+ await ctx.reply("\u26A0\uFE0F This command only works inside a session topic.", { parse_mode: "HTML" });
2730
+ return;
2731
+ }
2732
+ const session = core.sessionManager.getSessionByThread("telegram", String(threadId));
2733
+ if (session) {
2734
+ if (session.dangerousMode) {
2735
+ await ctx.reply("\u2620\uFE0F Dangerous mode is already enabled.", { parse_mode: "HTML" });
2736
+ return;
2737
+ }
2738
+ session.dangerousMode = true;
2739
+ core.sessionManager.updateSessionDangerousMode(session.id, true).catch(() => {
2740
+ });
2741
+ } else {
2742
+ const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
2743
+ if (!record || record.status === "cancelled" || record.status === "error") {
2744
+ await ctx.reply("\u26A0\uFE0F No active session in this topic.", { parse_mode: "HTML" });
2745
+ return;
2746
+ }
2747
+ if (record.dangerousMode) {
2748
+ await ctx.reply("\u2620\uFE0F Dangerous mode is already enabled.", { parse_mode: "HTML" });
2749
+ return;
2750
+ }
2751
+ core.sessionManager.updateSessionDangerousMode(record.sessionId, true).catch(() => {
2752
+ });
2753
+ }
2754
+ await ctx.reply(
2755
+ `\u26A0\uFE0F <b>Dangerous mode enabled</b>
2756
+
2757
+ All permission requests will be auto-approved. Claude can run arbitrary commands without asking.
2758
+
2759
+ Use /disable_dangerous to restore normal behaviour.`,
2760
+ { parse_mode: "HTML" }
2761
+ );
2762
+ }
2763
+ async function handleDisableDangerous(ctx, core) {
2764
+ const threadId = ctx.message?.message_thread_id;
2765
+ if (!threadId) {
2766
+ await ctx.reply("\u26A0\uFE0F This command only works inside a session topic.", { parse_mode: "HTML" });
2767
+ return;
2768
+ }
2769
+ const session = core.sessionManager.getSessionByThread("telegram", String(threadId));
2770
+ if (session) {
2771
+ if (!session.dangerousMode) {
2772
+ await ctx.reply("\u{1F510} Dangerous mode is already disabled.", { parse_mode: "HTML" });
2773
+ return;
2774
+ }
2775
+ session.dangerousMode = false;
2776
+ core.sessionManager.updateSessionDangerousMode(session.id, false).catch(() => {
2777
+ });
2778
+ } else {
2779
+ const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
2780
+ if (!record || record.status === "cancelled" || record.status === "error") {
2781
+ await ctx.reply("\u26A0\uFE0F No active session in this topic.", { parse_mode: "HTML" });
2782
+ return;
2783
+ }
2784
+ if (!record.dangerousMode) {
2785
+ await ctx.reply("\u{1F510} Dangerous mode is already disabled.", { parse_mode: "HTML" });
2786
+ return;
2787
+ }
2788
+ core.sessionManager.updateSessionDangerousMode(record.sessionId, false).catch(() => {
2789
+ });
2790
+ }
2791
+ await ctx.reply("\u{1F510} <b>Dangerous mode disabled</b>\n\nPermission requests will be shown normally.", { parse_mode: "HTML" });
2792
+ }
2793
+ async function handleUpdate(ctx, core) {
2794
+ if (!core.requestRestart) {
2795
+ await ctx.reply("\u26A0\uFE0F Update is not available (no restart handler registered).", { parse_mode: "HTML" });
2796
+ return;
2797
+ }
2798
+ const { getCurrentVersion, getLatestVersion, compareVersions, runUpdate } = await import("./version-VC5CPXBX.js");
2799
+ const current = getCurrentVersion();
2800
+ const statusMsg = await ctx.reply(`\u{1F50D} Checking for updates... (current: v${escapeHtml(current)})`, { parse_mode: "HTML" });
2801
+ const latest = await getLatestVersion();
2802
+ if (!latest) {
2803
+ await ctx.api.editMessageText(ctx.chat.id, statusMsg.message_id, "\u274C Could not check for updates.", { parse_mode: "HTML" });
2804
+ return;
2805
+ }
2806
+ if (compareVersions(current, latest) >= 0) {
2807
+ await ctx.api.editMessageText(ctx.chat.id, statusMsg.message_id, `\u2705 Already up to date (v${escapeHtml(current)}).`, { parse_mode: "HTML" });
2808
+ return;
2809
+ }
2810
+ await ctx.api.editMessageText(
2811
+ ctx.chat.id,
2812
+ statusMsg.message_id,
2813
+ `\u2B07\uFE0F Updating v${escapeHtml(current)} \u2192 v${escapeHtml(latest)}...`,
2814
+ { parse_mode: "HTML" }
2815
+ );
2816
+ const ok = await runUpdate();
2817
+ if (!ok) {
2818
+ await ctx.api.editMessageText(ctx.chat.id, statusMsg.message_id, "\u274C Update failed. Try manually: <code>npm install -g @openacp/cli@latest</code>", { parse_mode: "HTML" });
2819
+ return;
2820
+ }
2821
+ await ctx.api.editMessageText(
2822
+ ctx.chat.id,
2823
+ statusMsg.message_id,
2824
+ `\u2705 Updated to v${escapeHtml(latest)}. Restarting...`,
2825
+ { parse_mode: "HTML" }
2826
+ );
2827
+ await new Promise((r) => setTimeout(r, 500));
2828
+ await core.requestRestart();
2829
+ }
2830
+ async function handleRestart(ctx, core) {
2831
+ if (!core.requestRestart) {
2832
+ await ctx.reply("\u26A0\uFE0F Restart is not available (no restart handler registered).", { parse_mode: "HTML" });
2833
+ return;
2834
+ }
2835
+ await ctx.reply("\u{1F504} <b>Restarting OpenACP...</b>\nRebuilding and restarting. Be back shortly.", { parse_mode: "HTML" });
2836
+ await new Promise((r) => setTimeout(r, 500));
2837
+ await core.requestRestart();
2838
+ }
2839
+
2840
+ // src/adapters/telegram/commands/new-session.ts
2841
+ var log9 = createChildLogger({ module: "telegram-cmd-new-session" });
2842
+ var pendingNewSessions = /* @__PURE__ */ new Map();
2843
+ var PENDING_TIMEOUT_MS = 5 * 60 * 1e3;
2844
+ function cleanupPending(userId) {
2845
+ const pending = pendingNewSessions.get(userId);
2846
+ if (pending) {
2847
+ clearTimeout(pending.timer);
2848
+ pendingNewSessions.delete(userId);
2849
+ }
2850
+ }
2851
+ function botFromCtx(ctx) {
2852
+ return { api: ctx.api };
2706
2853
  }
2707
2854
  async function handleNew(ctx, core, chatId, assistant) {
2708
2855
  const rawMatch = ctx.match;
@@ -2710,16 +2857,112 @@ async function handleNew(ctx, core, chatId, assistant) {
2710
2857
  const args = matchStr.split(" ").filter(Boolean);
2711
2858
  const agentName = args[0];
2712
2859
  const workspace = args[1];
2860
+ if (agentName && workspace) {
2861
+ await createSessionDirect(ctx, core, chatId, agentName, workspace);
2862
+ return;
2863
+ }
2713
2864
  const currentThreadId = ctx.message?.message_thread_id;
2714
- if (assistant && currentThreadId === assistant.topicId && (!agentName || !workspace)) {
2865
+ if (assistant && currentThreadId === assistant.topicId) {
2715
2866
  const assistantSession = assistant.getSession();
2716
2867
  if (assistantSession) {
2717
- const prompt = agentName ? `User wants to create a new session with agent "${agentName}" but didn't specify a workspace. Ask them which workspace to use.` : `User wants to create a new session. Ask them which agent and workspace to use.`;
2868
+ const prompt = agentName ? `User wants to create a new session with agent "${agentName}" but didn't specify a workspace. Ask them which project directory to use as workspace.` : `User wants to create a new session. Guide them through choosing an agent and workspace (project directory).`;
2718
2869
  await assistantSession.enqueuePrompt(prompt);
2719
2870
  return;
2720
2871
  }
2721
2872
  }
2722
- log8.info({ userId: ctx.from?.id, agentName }, "New session command");
2873
+ const userId = ctx.from?.id;
2874
+ if (!userId) return;
2875
+ const agents = core.agentManager.getAvailableAgents();
2876
+ const config = core.configManager.get();
2877
+ if (agentName || agents.length === 1) {
2878
+ const selectedAgent = agentName || config.defaultAgent;
2879
+ await startWorkspaceStep(ctx, core, chatId, userId, selectedAgent);
2880
+ return;
2881
+ }
2882
+ const keyboard = new InlineKeyboard2();
2883
+ for (const agent of agents) {
2884
+ const label = agent.name === config.defaultAgent ? `${agent.name} (default)` : agent.name;
2885
+ keyboard.text(label, `m:new:agent:${agent.name}`).row();
2886
+ }
2887
+ keyboard.text("\u274C Cancel", "m:new:cancel");
2888
+ const msg = await ctx.reply(
2889
+ `\u{1F916} <b>Choose an agent:</b>`,
2890
+ { parse_mode: "HTML", reply_markup: keyboard }
2891
+ );
2892
+ cleanupPending(userId);
2893
+ pendingNewSessions.set(userId, {
2894
+ step: "agent",
2895
+ messageId: msg.message_id,
2896
+ threadId: currentThreadId,
2897
+ timer: setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS)
2898
+ });
2899
+ }
2900
+ async function startWorkspaceStep(ctx, core, chatId, userId, agentName) {
2901
+ const config = core.configManager.get();
2902
+ const baseDir = config.workspace.baseDir;
2903
+ const keyboard = new InlineKeyboard2().text(`\u{1F4C1} Use ${baseDir}`, "m:new:ws:default").row().text("\u270F\uFE0F Enter project path", "m:new:ws:custom").row().text("\u274C Cancel", "m:new:cancel");
2904
+ const text = `\u{1F4C1} <b>Where should ${escapeHtml(agentName)} work?</b>
2905
+
2906
+ Enter the path to your project folder \u2014 the agent will read, write, and run code there.
2907
+
2908
+ Or use the default directory below:`;
2909
+ let msg;
2910
+ try {
2911
+ const pending = pendingNewSessions.get(userId);
2912
+ if (pending?.messageId) {
2913
+ await ctx.api.editMessageText(chatId, pending.messageId, text, {
2914
+ parse_mode: "HTML",
2915
+ reply_markup: keyboard
2916
+ });
2917
+ msg = { message_id: pending.messageId };
2918
+ } else {
2919
+ msg = await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
2920
+ }
2921
+ } catch {
2922
+ msg = await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
2923
+ }
2924
+ cleanupPending(userId);
2925
+ pendingNewSessions.set(userId, {
2926
+ agentName,
2927
+ step: "workspace",
2928
+ messageId: msg.message_id,
2929
+ threadId: ctx.message?.message_thread_id ?? ctx.callbackQuery?.message?.message_thread_id,
2930
+ timer: setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS)
2931
+ });
2932
+ }
2933
+ async function startConfirmStep(ctx, chatId, userId, agentName, workspace) {
2934
+ const keyboard = new InlineKeyboard2().text("\u2705 Create", "m:new:confirm").text("\u274C Cancel", "m:new:cancel");
2935
+ const text = `\u2705 <b>Ready to create session?</b>
2936
+
2937
+ <b>Agent:</b> ${escapeHtml(agentName)}
2938
+ <b>Project:</b> <code>${escapeHtml(workspace)}</code>`;
2939
+ let msg;
2940
+ try {
2941
+ const pending = pendingNewSessions.get(userId);
2942
+ if (pending?.messageId) {
2943
+ await ctx.api.editMessageText(chatId, pending.messageId, text, {
2944
+ parse_mode: "HTML",
2945
+ reply_markup: keyboard
2946
+ });
2947
+ msg = { message_id: pending.messageId };
2948
+ } else {
2949
+ msg = await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
2950
+ }
2951
+ } catch {
2952
+ msg = await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
2953
+ }
2954
+ cleanupPending(userId);
2955
+ pendingNewSessions.set(userId, {
2956
+ agentName,
2957
+ workspace,
2958
+ step: "confirm",
2959
+ messageId: msg.message_id,
2960
+ threadId: ctx.message?.message_thread_id ?? ctx.callbackQuery?.message?.message_thread_id,
2961
+ timer: setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS)
2962
+ });
2963
+ }
2964
+ async function createSessionDirect(ctx, core, chatId, agentName, workspace) {
2965
+ log9.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
2723
2966
  let threadId;
2724
2967
  try {
2725
2968
  const topicName = `\u{1F504} New Session`;
@@ -2728,15 +2971,9 @@ async function handleNew(ctx, core, chatId, assistant) {
2728
2971
  message_thread_id: threadId,
2729
2972
  parse_mode: "HTML"
2730
2973
  });
2731
- const session = await core.handleNewSession(
2732
- "telegram",
2733
- agentName,
2734
- workspace
2735
- );
2974
+ const session = await core.handleNewSession("telegram", agentName, workspace);
2736
2975
  session.threadId = String(threadId);
2737
- await core.sessionManager.updateSessionPlatform(session.id, {
2738
- topicId: threadId
2739
- });
2976
+ await core.sessionManager.updateSessionPlatform(session.id, { topicId: threadId });
2740
2977
  const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
2741
2978
  try {
2742
2979
  await ctx.api.editForumTopic(chatId, threadId, { name: finalName });
@@ -2744,18 +2981,21 @@ async function handleNew(ctx, core, chatId, assistant) {
2744
2981
  }
2745
2982
  await ctx.api.sendMessage(
2746
2983
  chatId,
2747
- `\u2705 Session started
2984
+ `\u2705 <b>Session started</b>
2748
2985
  <b>Agent:</b> ${escapeHtml(session.agentName)}
2749
- <b>Workspace:</b> <code>${escapeHtml(session.workingDirectory)}</code>`,
2986
+ <b>Workspace:</b> <code>${escapeHtml(session.workingDirectory)}</code>
2987
+
2988
+ This is your coding session \u2014 chat here to work with the agent.`,
2750
2989
  {
2751
2990
  message_thread_id: threadId,
2752
2991
  parse_mode: "HTML",
2753
2992
  reply_markup: buildDangerousModeKeyboard(session.id, false)
2754
2993
  }
2755
2994
  );
2756
- session.warmup().catch((err) => log8.error({ err }, "Warm-up error"));
2995
+ session.warmup().catch((err) => log9.error({ err }, "Warm-up error"));
2996
+ return threadId ?? null;
2757
2997
  } catch (err) {
2758
- log8.error({ err }, "Session creation failed");
2998
+ log9.error({ err }, "Session creation failed");
2759
2999
  if (threadId) {
2760
3000
  try {
2761
3001
  await ctx.api.deleteForumTopic(chatId, threadId);
@@ -2764,6 +3004,7 @@ async function handleNew(ctx, core, chatId, assistant) {
2764
3004
  }
2765
3005
  const message = err instanceof Error ? err.message : typeof err === "object" ? JSON.stringify(err) : String(err);
2766
3006
  await ctx.reply(`\u274C ${escapeHtml(message)}`, { parse_mode: "HTML" });
3007
+ return null;
2767
3008
  }
2768
3009
  }
2769
3010
  async function handleNewChat(ctx, core, chatId) {
@@ -2831,19 +3072,182 @@ async function handleNewChat(ctx, core, chatId) {
2831
3072
  parse_mode: "HTML",
2832
3073
  reply_markup: buildDangerousModeKeyboard(session.id, false)
2833
3074
  }
2834
- );
2835
- session.warmup().catch((err) => log8.error({ err }, "Warm-up error"));
2836
- } catch (err) {
2837
- if (newThreadId) {
3075
+ );
3076
+ session.warmup().catch((err) => log9.error({ err }, "Warm-up error"));
3077
+ } catch (err) {
3078
+ if (newThreadId) {
3079
+ try {
3080
+ await ctx.api.deleteForumTopic(chatId, newThreadId);
3081
+ } catch {
3082
+ }
3083
+ }
3084
+ const message = err instanceof Error ? err.message : String(err);
3085
+ await ctx.reply(`\u274C ${escapeHtml(message)}`, { parse_mode: "HTML" });
3086
+ }
3087
+ }
3088
+ async function executeNewSession(bot, core, chatId, agentName, workspace) {
3089
+ const threadId = await createSessionTopic(bot, chatId, "\u{1F504} New Session");
3090
+ const setupMsg = await bot.api.sendMessage(chatId, "\u23F3 Setting up session, please wait...", {
3091
+ message_thread_id: threadId,
3092
+ parse_mode: "HTML"
3093
+ });
3094
+ const firstMsgId = setupMsg.message_id;
3095
+ try {
3096
+ const session = await core.handleNewSession(
3097
+ "telegram",
3098
+ agentName,
3099
+ workspace
3100
+ );
3101
+ session.threadId = String(threadId);
3102
+ await core.sessionManager.updateSessionPlatform(session.id, {
3103
+ topicId: threadId
3104
+ });
3105
+ const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
3106
+ await renameSessionTopic(bot, chatId, threadId, finalName);
3107
+ session.warmup().catch((err) => log9.error({ err }, "Warm-up error"));
3108
+ return { session, threadId, firstMsgId };
3109
+ } catch (err) {
3110
+ try {
3111
+ await bot.api.deleteForumTopic(chatId, threadId);
3112
+ } catch {
3113
+ }
3114
+ throw err;
3115
+ }
3116
+ }
3117
+ async function handlePendingWorkspaceInput(ctx, core, chatId, assistantTopicId) {
3118
+ const userId = ctx.from?.id;
3119
+ if (!userId) return false;
3120
+ const pending = pendingNewSessions.get(userId);
3121
+ if (!pending || !ctx.message?.text) return false;
3122
+ if (pending.step !== "workspace_input" && pending.step !== "workspace") return false;
3123
+ const threadId = ctx.message.message_thread_id;
3124
+ if (threadId && threadId !== assistantTopicId) return false;
3125
+ let workspace = ctx.message.text.trim();
3126
+ if (!workspace || !pending.agentName) {
3127
+ await ctx.reply("\u26A0\uFE0F Please enter a valid directory path.", { parse_mode: "HTML" });
3128
+ return true;
3129
+ }
3130
+ if (!workspace.startsWith("/") && !workspace.startsWith("~")) {
3131
+ const baseDir = core.configManager.get().workspace.baseDir;
3132
+ workspace = `${baseDir.replace(/\/$/, "")}/${workspace}`;
3133
+ }
3134
+ await startConfirmStep(ctx, chatId, userId, pending.agentName, workspace);
3135
+ return true;
3136
+ }
3137
+ async function startInteractiveNewSession(ctx, core, chatId, agentName) {
3138
+ const userId = ctx.from?.id;
3139
+ if (!userId) return;
3140
+ const agents = core.agentManager.getAvailableAgents();
3141
+ const config = core.configManager.get();
3142
+ if (agentName || agents.length === 1) {
3143
+ const selectedAgent = agentName || config.defaultAgent;
3144
+ await startWorkspaceStep(ctx, core, chatId, userId, selectedAgent);
3145
+ return;
3146
+ }
3147
+ const keyboard = new InlineKeyboard2();
3148
+ for (const agent of agents) {
3149
+ const label = agent.name === config.defaultAgent ? `${agent.name} (default)` : agent.name;
3150
+ keyboard.text(label, `m:new:agent:${agent.name}`).row();
3151
+ }
3152
+ keyboard.text("\u274C Cancel", "m:new:cancel");
3153
+ const msg = await ctx.reply(
3154
+ `\u{1F916} <b>Choose an agent:</b>`,
3155
+ { parse_mode: "HTML", reply_markup: keyboard }
3156
+ );
3157
+ cleanupPending(userId);
3158
+ pendingNewSessions.set(userId, {
3159
+ step: "agent",
3160
+ messageId: msg.message_id,
3161
+ threadId: ctx.callbackQuery?.message?.message_thread_id,
3162
+ timer: setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS)
3163
+ });
3164
+ }
3165
+ function setupNewSessionCallbacks(bot, core, chatId) {
3166
+ bot.callbackQuery(/^m:new:/, async (ctx) => {
3167
+ const data = ctx.callbackQuery.data;
3168
+ try {
3169
+ await ctx.answerCallbackQuery();
3170
+ } catch {
3171
+ }
3172
+ if (data.startsWith("m:new:agent:")) {
3173
+ const agentName = data.replace("m:new:agent:", "");
3174
+ const userId = ctx.from?.id;
3175
+ if (userId) await startWorkspaceStep(ctx, core, chatId, userId, agentName);
3176
+ return;
3177
+ }
3178
+ if (data === "m:new:ws:default") {
3179
+ const userId = ctx.from?.id;
3180
+ if (!userId) return;
3181
+ const pending = pendingNewSessions.get(userId);
3182
+ if (!pending?.agentName) return;
3183
+ const workspace = core.configManager.get().workspace.baseDir;
3184
+ await startConfirmStep(ctx, chatId, userId, pending.agentName, workspace);
3185
+ return;
3186
+ }
3187
+ if (data === "m:new:ws:custom") {
3188
+ const userId = ctx.from?.id;
3189
+ if (!userId) return;
3190
+ const pending = pendingNewSessions.get(userId);
3191
+ if (!pending?.agentName) return;
3192
+ try {
3193
+ await ctx.api.editMessageText(
3194
+ chatId,
3195
+ pending.messageId,
3196
+ `\u270F\uFE0F <b>Enter your project path:</b>
3197
+
3198
+ Full path like <code>~/code/my-project</code>
3199
+ Or just the folder name like <code>my-project</code> (will use ${core.configManager.get().workspace.baseDir}/)`,
3200
+ { parse_mode: "HTML" }
3201
+ );
3202
+ } catch {
3203
+ await ctx.reply(
3204
+ `\u270F\uFE0F <b>Enter your project path:</b>`,
3205
+ { parse_mode: "HTML" }
3206
+ );
3207
+ }
3208
+ clearTimeout(pending.timer);
3209
+ pending.step = "workspace_input";
3210
+ pending.timer = setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS);
3211
+ return;
3212
+ }
3213
+ if (data === "m:new:confirm") {
3214
+ const userId = ctx.from?.id;
3215
+ if (!userId) return;
3216
+ const pending = pendingNewSessions.get(userId);
3217
+ if (!pending?.agentName || !pending?.workspace) return;
3218
+ cleanupPending(userId);
3219
+ const confirmMsgId = pending.messageId;
2838
3220
  try {
2839
- await ctx.api.deleteForumTopic(chatId, newThreadId);
3221
+ await ctx.api.editMessageText(chatId, confirmMsgId, `\u23F3 Creating session...`, { parse_mode: "HTML" });
3222
+ } catch {
3223
+ }
3224
+ const resultThreadId = await createSessionDirect(ctx, core, chatId, pending.agentName, pending.workspace);
3225
+ try {
3226
+ if (resultThreadId) {
3227
+ const link = buildDeepLink(chatId, resultThreadId);
3228
+ await ctx.api.editMessageText(chatId, confirmMsgId, `\u2705 Session created \u2192 <a href="${link}">Open topic</a>`, { parse_mode: "HTML" });
3229
+ } else {
3230
+ await ctx.api.editMessageText(chatId, confirmMsgId, `\u274C Session creation failed.`, { parse_mode: "HTML" });
3231
+ }
2840
3232
  } catch {
2841
3233
  }
3234
+ return;
2842
3235
  }
2843
- const message = err instanceof Error ? err.message : String(err);
2844
- await ctx.reply(`\u274C ${escapeHtml(message)}`, { parse_mode: "HTML" });
2845
- }
3236
+ if (data === "m:new:cancel") {
3237
+ const userId = ctx.from?.id;
3238
+ if (userId) cleanupPending(userId);
3239
+ try {
3240
+ await ctx.editMessageText("\u274C Session creation cancelled.", { parse_mode: "HTML" });
3241
+ } catch {
3242
+ }
3243
+ return;
3244
+ }
3245
+ });
2846
3246
  }
3247
+
3248
+ // src/adapters/telegram/commands/session.ts
3249
+ import { InlineKeyboard as InlineKeyboard3 } from "grammy";
3250
+ var log10 = createChildLogger({ module: "telegram-cmd-session" });
2847
3251
  async function handleCancel(ctx, core, assistant) {
2848
3252
  const threadId = ctx.message?.message_thread_id;
2849
3253
  if (!threadId) return;
@@ -2861,14 +3265,14 @@ async function handleCancel(ctx, core, assistant) {
2861
3265
  String(threadId)
2862
3266
  );
2863
3267
  if (session) {
2864
- log8.info({ sessionId: session.id }, "Cancel session command");
3268
+ log10.info({ sessionId: session.id }, "Cancel session command");
2865
3269
  await session.cancel();
2866
3270
  await ctx.reply("\u26D4 Session cancelled.", { parse_mode: "HTML" });
2867
3271
  return;
2868
3272
  }
2869
3273
  const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
2870
3274
  if (record && record.status !== "cancelled" && record.status !== "error") {
2871
- log8.info({ sessionId: record.sessionId }, "Cancel session command (from store)");
3275
+ log10.info({ sessionId: record.sessionId }, "Cancel session command (from store)");
2872
3276
  await core.sessionManager.cancelSession(record.sessionId);
2873
3277
  await ctx.reply("\u26D4 Session cancelled.", { parse_mode: "HTML" });
2874
3278
  }
@@ -2954,8 +3358,7 @@ async function handleTopics(ctx, core) {
2954
3358
  <i>...and ${records.length - MAX_DISPLAY} more</i>` : "";
2955
3359
  const finishedCount = records.filter((r) => r.status === "finished").length;
2956
3360
  const errorCount = records.filter((r) => r.status === "error" || r.status === "cancelled").length;
2957
- const activeCount = records.filter((r) => r.status === "active" || r.status === "initializing").length;
2958
- const keyboard = new InlineKeyboard();
3361
+ const keyboard = new InlineKeyboard3();
2959
3362
  if (finishedCount > 0) {
2960
3363
  keyboard.text(`Cleanup finished (${finishedCount})`, "m:cleanup:finished").row();
2961
3364
  }
@@ -2974,7 +3377,7 @@ ${lines.join("\n")}${truncated}`,
2974
3377
  { parse_mode: "HTML", reply_markup: keyboard }
2975
3378
  );
2976
3379
  } catch (err) {
2977
- log8.error({ err }, "handleTopics error");
3380
+ log10.error({ err }, "handleTopics error");
2978
3381
  await ctx.reply("\u274C Failed to list sessions.", { parse_mode: "HTML" }).catch(() => {
2979
3382
  });
2980
3383
  }
@@ -2998,13 +3401,13 @@ async function handleCleanup(ctx, core, chatId, statuses) {
2998
3401
  try {
2999
3402
  await ctx.api.deleteForumTopic(chatId, topicId);
3000
3403
  } catch (err) {
3001
- log8.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
3404
+ log10.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
3002
3405
  }
3003
3406
  }
3004
3407
  await core.sessionManager.removeRecord(record.sessionId);
3005
3408
  deleted++;
3006
3409
  } catch (err) {
3007
- log8.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3410
+ log10.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3008
3411
  failed++;
3009
3412
  }
3010
3413
  }
@@ -3041,7 +3444,7 @@ async function handleCleanupEverything(ctx, core, chatId, systemTopicIds) {
3041
3444
  const activeWarning = activeCount > 0 ? `
3042
3445
 
3043
3446
  \u26A0\uFE0F <b>${activeCount} active session(s) will be cancelled and their agents stopped!</b>` : "";
3044
- const keyboard = new InlineKeyboard().text("Yes, delete all", "m:cleanup:everything:confirm").text("Cancel", "m:topics");
3447
+ const keyboard = new InlineKeyboard3().text("Yes, delete all", "m:cleanup:everything:confirm").text("Cancel", "m:topics");
3045
3448
  await ctx.reply(
3046
3449
  `<b>Delete ${cleanable.length} topics?</b>
3047
3450
 
@@ -3077,7 +3480,7 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
3077
3480
  try {
3078
3481
  await core.sessionManager.cancelSession(record.sessionId);
3079
3482
  } catch (err) {
3080
- log8.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
3483
+ log10.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
3081
3484
  }
3082
3485
  }
3083
3486
  const topicId = record.platform?.topicId;
@@ -3085,13 +3488,13 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
3085
3488
  try {
3086
3489
  await ctx.api.deleteForumTopic(chatId, topicId);
3087
3490
  } catch (err) {
3088
- log8.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
3491
+ log10.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
3089
3492
  }
3090
3493
  }
3091
3494
  await core.sessionManager.removeRecord(record.sessionId);
3092
3495
  deleted++;
3093
3496
  } catch (err) {
3094
- log8.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3497
+ log10.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3095
3498
  failed++;
3096
3499
  }
3097
3500
  }
@@ -3100,206 +3503,115 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
3100
3503
  { parse_mode: "HTML" }
3101
3504
  );
3102
3505
  }
3103
- async function handleAgents(ctx, core) {
3104
- const agents = core.agentManager.getAvailableAgents();
3105
- const defaultAgent = core.configManager.get().defaultAgent;
3106
- const lines = agents.map(
3107
- (a) => `\u2022 <b>${escapeHtml(a.name)}</b>${a.name === defaultAgent ? " (default)" : ""}
3108
- <code>${escapeHtml(a.command)} ${a.args.map((arg) => escapeHtml(arg)).join(" ")}</code>`
3109
- );
3110
- const text = lines.length > 0 ? `<b>Available Agents:</b>
3111
-
3112
- ${lines.join("\n")}` : `<b>Available Agents:</b>
3113
-
3114
- No agents configured.`;
3115
- await ctx.reply(text, { parse_mode: "HTML" });
3116
- }
3117
- async function handleHelp(ctx) {
3118
- await ctx.reply(
3119
- `<b>OpenACP Commands:</b>
3120
-
3121
- /new [agent] [workspace] \u2014 Create new session
3122
- /newchat \u2014 New chat, same agent &amp; workspace
3123
- /cancel \u2014 Cancel current session
3124
- /status \u2014 Show session/system status
3125
- /agents \u2014 List available agents
3126
- /menu \u2014 Show interactive menu
3127
- /restart \u2014 Restart OpenACP
3128
- /update \u2014 Update to latest version and restart
3129
- /help \u2014 Show this help
3130
-
3131
- Or just chat in the \u{1F916} Assistant topic for help!`,
3132
- { parse_mode: "HTML" }
3133
- );
3134
- }
3135
- function buildDangerousModeKeyboard(sessionId, enabled) {
3136
- return new InlineKeyboard().text(
3137
- enabled ? "\u{1F510} Disable Dangerous Mode" : "\u2620\uFE0F Enable Dangerous Mode",
3138
- `d:${sessionId}`
3139
- );
3506
+ async function executeCancelSession(core, excludeSessionId) {
3507
+ const sessions = core.sessionManager.listSessions("telegram").filter((s) => s.status === "active" && s.id !== excludeSessionId).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
3508
+ const session = sessions[0];
3509
+ if (!session) return null;
3510
+ await session.cancel();
3511
+ return session;
3140
3512
  }
3141
- function setupDangerousModeCallbacks(bot, core) {
3142
- bot.callbackQuery(/^d:/, async (ctx) => {
3143
- const sessionId = ctx.callbackQuery.data.slice(2);
3144
- const session = core.sessionManager.getSession(sessionId);
3145
- if (session) {
3146
- session.dangerousMode = !session.dangerousMode;
3147
- log8.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
3148
- core.sessionManager.updateSessionDangerousMode(sessionId, session.dangerousMode).catch(() => {
3149
- });
3150
- const toastText2 = session.dangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
3151
- try {
3152
- await ctx.answerCallbackQuery({ text: toastText2 });
3153
- } catch {
3154
- }
3155
- try {
3156
- await ctx.editMessageReplyMarkup({
3157
- reply_markup: buildDangerousModeKeyboard(sessionId, session.dangerousMode)
3158
- });
3159
- } catch {
3160
- }
3161
- return;
3162
- }
3163
- const record = core.sessionManager.getSessionRecord(sessionId);
3164
- if (!record || record.status === "cancelled" || record.status === "error") {
3165
- try {
3166
- await ctx.answerCallbackQuery({ text: "\u26A0\uFE0F Session not found or already ended." });
3167
- } catch {
3168
- }
3169
- return;
3170
- }
3171
- const newDangerousMode = !(record.dangerousMode ?? false);
3172
- core.sessionManager.updateSessionDangerousMode(sessionId, newDangerousMode).catch(() => {
3173
- });
3174
- log8.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
3175
- const toastText = newDangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
3513
+ function setupSessionCallbacks(bot, core, chatId, systemTopicIds) {
3514
+ bot.callbackQuery(/^m:cleanup/, async (ctx) => {
3515
+ const data = ctx.callbackQuery.data;
3176
3516
  try {
3177
- await ctx.answerCallbackQuery({ text: toastText });
3517
+ await ctx.answerCallbackQuery();
3178
3518
  } catch {
3179
3519
  }
3180
- try {
3181
- await ctx.editMessageReplyMarkup({
3182
- reply_markup: buildDangerousModeKeyboard(sessionId, newDangerousMode)
3183
- });
3184
- } catch {
3520
+ switch (data) {
3521
+ case "m:cleanup:finished":
3522
+ await handleCleanup(ctx, core, chatId, ["finished"]);
3523
+ break;
3524
+ case "m:cleanup:errors":
3525
+ await handleCleanup(ctx, core, chatId, ["error", "cancelled"]);
3526
+ break;
3527
+ case "m:cleanup:all":
3528
+ await handleCleanup(ctx, core, chatId, ["finished", "error", "cancelled"]);
3529
+ break;
3530
+ case "m:cleanup:everything":
3531
+ await handleCleanupEverything(ctx, core, chatId, systemTopicIds);
3532
+ break;
3533
+ case "m:cleanup:everything:confirm":
3534
+ await handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicIds);
3535
+ break;
3185
3536
  }
3186
3537
  });
3187
3538
  }
3188
- async function handleEnableDangerous(ctx, core) {
3189
- const threadId = ctx.message?.message_thread_id;
3190
- if (!threadId) {
3191
- await ctx.reply("\u26A0\uFE0F This command only works inside a session topic.", { parse_mode: "HTML" });
3192
- return;
3193
- }
3194
- const session = core.sessionManager.getSessionByThread("telegram", String(threadId));
3195
- if (session) {
3196
- if (session.dangerousMode) {
3197
- await ctx.reply("\u2620\uFE0F Dangerous mode is already enabled.", { parse_mode: "HTML" });
3198
- return;
3199
- }
3200
- session.dangerousMode = true;
3201
- core.sessionManager.updateSessionDangerousMode(session.id, true).catch(() => {
3202
- });
3203
- } else {
3204
- const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
3205
- if (!record || record.status === "cancelled" || record.status === "error") {
3206
- await ctx.reply("\u26A0\uFE0F No active session in this topic.", { parse_mode: "HTML" });
3207
- return;
3208
- }
3209
- if (record.dangerousMode) {
3210
- await ctx.reply("\u2620\uFE0F Dangerous mode is already enabled.", { parse_mode: "HTML" });
3211
- return;
3212
- }
3213
- core.sessionManager.updateSessionDangerousMode(record.sessionId, true).catch(() => {
3214
- });
3215
- }
3216
- await ctx.reply(
3217
- `\u26A0\uFE0F <b>Dangerous mode enabled</b>
3218
3539
 
3219
- All permission requests will be auto-approved. Claude can run arbitrary commands without asking.
3220
-
3221
- Use /disable_dangerous to restore normal behaviour.`,
3222
- { parse_mode: "HTML" }
3223
- );
3224
- }
3225
- async function handleUpdate(ctx, core) {
3226
- if (!core.requestRestart) {
3227
- await ctx.reply("\u26A0\uFE0F Update is not available (no restart handler registered).", { parse_mode: "HTML" });
3228
- return;
3229
- }
3230
- const { getCurrentVersion, getLatestVersion, compareVersions, runUpdate } = await import("./version-VC5CPXBX.js");
3231
- const current = getCurrentVersion();
3232
- const statusMsg = await ctx.reply(`\u{1F50D} Checking for updates... (current: v${escapeHtml(current)})`, { parse_mode: "HTML" });
3233
- const latest = await getLatestVersion();
3234
- if (!latest) {
3235
- await ctx.api.editMessageText(ctx.chat.id, statusMsg.message_id, "\u274C Could not check for updates.", { parse_mode: "HTML" });
3236
- return;
3237
- }
3238
- if (compareVersions(current, latest) >= 0) {
3239
- await ctx.api.editMessageText(ctx.chat.id, statusMsg.message_id, `\u2705 Already up to date (v${escapeHtml(current)}).`, { parse_mode: "HTML" });
3240
- return;
3241
- }
3242
- await ctx.api.editMessageText(
3243
- ctx.chat.id,
3244
- statusMsg.message_id,
3245
- `\u2B07\uFE0F Updating v${escapeHtml(current)} \u2192 v${escapeHtml(latest)}...`,
3246
- { parse_mode: "HTML" }
3247
- );
3248
- const ok = await runUpdate();
3249
- if (!ok) {
3250
- await ctx.api.editMessageText(ctx.chat.id, statusMsg.message_id, "\u274C Update failed. Try manually: <code>npm install -g @openacp/cli@latest</code>", { parse_mode: "HTML" });
3251
- return;
3252
- }
3253
- await ctx.api.editMessageText(
3254
- ctx.chat.id,
3255
- statusMsg.message_id,
3256
- `\u2705 Updated to v${escapeHtml(latest)}. Restarting...`,
3257
- { parse_mode: "HTML" }
3258
- );
3259
- await new Promise((r) => setTimeout(r, 500));
3260
- await core.requestRestart();
3540
+ // src/adapters/telegram/commands/menu.ts
3541
+ import { InlineKeyboard as InlineKeyboard4 } from "grammy";
3542
+ function buildMenuKeyboard() {
3543
+ return new InlineKeyboard4().text("\u{1F195} New Session", "m:new").text("\u{1F4CB} Sessions", "m:topics").row().text("\u{1F4CA} Status", "m:status").text("\u{1F916} Agents", "m:agents").row().text("\u{1F517} Integrate", "m:integrate").text("\u2753 Help", "m:help").row().text("\u{1F504} Restart", "m:restart").text("\u2B06\uFE0F Update", "m:update");
3261
3544
  }
3262
- async function handleRestart(ctx, core) {
3263
- if (!core.requestRestart) {
3264
- await ctx.reply("\u26A0\uFE0F Restart is not available (no restart handler registered).", { parse_mode: "HTML" });
3265
- return;
3266
- }
3267
- await ctx.reply("\u{1F504} <b>Restarting OpenACP...</b>\nRebuilding and restarting. Be back shortly.", { parse_mode: "HTML" });
3268
- await new Promise((r) => setTimeout(r, 500));
3269
- await core.requestRestart();
3545
+ async function handleMenu(ctx) {
3546
+ await ctx.reply(`<b>OpenACP Menu</b>
3547
+ Choose an action:`, {
3548
+ parse_mode: "HTML",
3549
+ reply_markup: buildMenuKeyboard()
3550
+ });
3270
3551
  }
3271
- async function handleDisableDangerous(ctx, core) {
3272
- const threadId = ctx.message?.message_thread_id;
3273
- if (!threadId) {
3274
- await ctx.reply("\u26A0\uFE0F This command only works inside a session topic.", { parse_mode: "HTML" });
3275
- return;
3276
- }
3277
- const session = core.sessionManager.getSessionByThread("telegram", String(threadId));
3278
- if (session) {
3279
- if (!session.dangerousMode) {
3280
- await ctx.reply("\u{1F510} Dangerous mode is already disabled.", { parse_mode: "HTML" });
3281
- return;
3282
- }
3283
- session.dangerousMode = false;
3284
- core.sessionManager.updateSessionDangerousMode(session.id, false).catch(() => {
3285
- });
3286
- } else {
3287
- const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
3288
- if (!record || record.status === "cancelled" || record.status === "error") {
3289
- await ctx.reply("\u26A0\uFE0F No active session in this topic.", { parse_mode: "HTML" });
3290
- return;
3291
- }
3292
- if (!record.dangerousMode) {
3293
- await ctx.reply("\u{1F510} Dangerous mode is already disabled.", { parse_mode: "HTML" });
3294
- return;
3295
- }
3296
- core.sessionManager.updateSessionDangerousMode(record.sessionId, false).catch(() => {
3297
- });
3298
- }
3299
- await ctx.reply("\u{1F510} <b>Dangerous mode disabled</b>\n\nPermission requests will be shown normally.", { parse_mode: "HTML" });
3552
+ async function handleAgents(ctx, core) {
3553
+ const agents = core.agentManager.getAvailableAgents();
3554
+ const defaultAgent = core.configManager.get().defaultAgent;
3555
+ const lines = agents.map(
3556
+ (a) => `\u2022 <b>${escapeHtml(a.name)}</b>${a.name === defaultAgent ? " (default)" : ""}
3557
+ <code>${escapeHtml(a.command)} ${a.args.map((arg) => escapeHtml(arg)).join(" ")}</code>`
3558
+ );
3559
+ const text = lines.length > 0 ? `<b>Available Agents:</b>
3560
+
3561
+ ${lines.join("\n")}` : `<b>Available Agents:</b>
3562
+
3563
+ No agents configured.`;
3564
+ await ctx.reply(text, { parse_mode: "HTML" });
3300
3565
  }
3301
- function botFromCtx(ctx) {
3302
- return { api: ctx.api };
3566
+ async function handleHelp(ctx) {
3567
+ await ctx.reply(
3568
+ `\u{1F4D6} <b>OpenACP Help</b>
3569
+
3570
+ \u{1F680} <b>Getting Started</b>
3571
+ Tap \u{1F195} New Session to start coding with AI.
3572
+ Each session gets its own topic \u2014 chat there to work with the agent.
3573
+
3574
+ \u{1F4A1} <b>Common Tasks</b>
3575
+ /new [agent] [workspace] \u2014 Create new session
3576
+ /cancel \u2014 Cancel session (in session topic)
3577
+ /status \u2014 Show session or system status
3578
+ /sessions \u2014 List all sessions
3579
+ /agents \u2014 List available agents
3580
+
3581
+ \u2699\uFE0F <b>System</b>
3582
+ /restart \u2014 Restart OpenACP
3583
+ /update \u2014 Update to latest version
3584
+ /integrate \u2014 Manage agent integrations
3585
+ /menu \u2014 Show action menu
3586
+
3587
+ \u{1F512} <b>Session Options</b>
3588
+ /enable_dangerous \u2014 Auto-approve permissions
3589
+ /disable_dangerous \u2014 Restore permission prompts
3590
+ /handoff \u2014 Continue session in terminal
3591
+ /clear \u2014 Clear assistant history
3592
+
3593
+ \u{1F4AC} Need help? Just ask me in this topic!`,
3594
+ { parse_mode: "HTML" }
3595
+ );
3596
+ }
3597
+ async function handleClear(ctx, assistant) {
3598
+ if (!assistant) {
3599
+ await ctx.reply("\u26A0\uFE0F Assistant is not available.", { parse_mode: "HTML" });
3600
+ return;
3601
+ }
3602
+ const threadId = ctx.message?.message_thread_id;
3603
+ if (threadId !== assistant.topicId) {
3604
+ await ctx.reply("\u2139\uFE0F /clear only works in the Assistant topic.", { parse_mode: "HTML" });
3605
+ return;
3606
+ }
3607
+ await ctx.reply("\u{1F504} Clearing assistant history...", { parse_mode: "HTML" });
3608
+ try {
3609
+ await assistant.respawn();
3610
+ await ctx.reply("\u2705 Assistant history cleared.", { parse_mode: "HTML" });
3611
+ } catch (err) {
3612
+ const message = err instanceof Error ? err.message : String(err);
3613
+ await ctx.reply(`\u274C Failed to clear: <code>${message}</code>`, { parse_mode: "HTML" });
3614
+ }
3303
3615
  }
3304
3616
  var TELEGRAM_MSG_LIMIT = 4096;
3305
3617
  function buildSkillMessages(commands) {
@@ -3320,46 +3632,13 @@ function buildSkillMessages(commands) {
3320
3632
  if (current) messages.push(current);
3321
3633
  return messages;
3322
3634
  }
3323
- async function executeNewSession(bot, core, chatId, agentName, workspace) {
3324
- const threadId = await createSessionTopic(bot, chatId, "\u{1F504} New Session");
3325
- const setupMsg = await bot.api.sendMessage(chatId, "\u23F3 Setting up session, please wait...", {
3326
- message_thread_id: threadId,
3327
- parse_mode: "HTML"
3328
- });
3329
- const firstMsgId = setupMsg.message_id;
3330
- try {
3331
- const session = await core.handleNewSession(
3332
- "telegram",
3333
- agentName,
3334
- workspace
3335
- );
3336
- session.threadId = String(threadId);
3337
- await core.sessionManager.updateSessionPlatform(session.id, {
3338
- topicId: threadId
3339
- });
3340
- const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
3341
- await renameSessionTopic(bot, chatId, threadId, finalName);
3342
- session.warmup().catch((err) => log8.error({ err }, "Warm-up error"));
3343
- return { session, threadId, firstMsgId };
3344
- } catch (err) {
3345
- try {
3346
- await bot.api.deleteForumTopic(chatId, threadId);
3347
- } catch {
3348
- }
3349
- throw err;
3350
- }
3351
- }
3352
- async function executeCancelSession(core, excludeSessionId) {
3353
- const sessions = core.sessionManager.listSessions("telegram").filter((s) => s.status === "active" && s.id !== excludeSessionId).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
3354
- const session = sessions[0];
3355
- if (!session) return null;
3356
- await session.cancel();
3357
- return session;
3358
- }
3635
+
3636
+ // src/adapters/telegram/commands/integrate.ts
3637
+ import { InlineKeyboard as InlineKeyboard5 } from "grammy";
3359
3638
  async function handleIntegrate(ctx, _core) {
3360
3639
  const { listIntegrations } = await import("./integrate-WUPLRJD3.js");
3361
3640
  const agents = listIntegrations();
3362
- const keyboard = new InlineKeyboard();
3641
+ const keyboard = new InlineKeyboard5();
3363
3642
  for (const agent of agents) {
3364
3643
  keyboard.text(`\u{1F916} ${agent}`, `i:agent:${agent}`).row();
3365
3644
  }
@@ -3371,7 +3650,7 @@ Select an agent to manage its integrations.`,
3371
3650
  );
3372
3651
  }
3373
3652
  function buildAgentItemsKeyboard(agentName, items) {
3374
- const keyboard = new InlineKeyboard();
3653
+ const keyboard = new InlineKeyboard5();
3375
3654
  for (const item of items) {
3376
3655
  const installed = item.isInstalled();
3377
3656
  keyboard.text(
@@ -3392,7 +3671,7 @@ function setupIntegrateCallbacks(bot, core) {
3392
3671
  if (data === "i:back") {
3393
3672
  const { listIntegrations } = await import("./integrate-WUPLRJD3.js");
3394
3673
  const agents = listIntegrations();
3395
- const keyboard2 = new InlineKeyboard();
3674
+ const keyboard2 = new InlineKeyboard5();
3396
3675
  for (const agent of agents) {
3397
3676
  keyboard2.text(`\u{1F916} ${agent}`, `i:agent:${agent}`).row();
3398
3677
  }
@@ -3471,6 +3750,61 @@ ${resultText}`,
3471
3750
  }
3472
3751
  });
3473
3752
  }
3753
+
3754
+ // src/adapters/telegram/commands/index.ts
3755
+ function setupCommands(bot, core, chatId, assistant) {
3756
+ bot.command("new", (ctx) => handleNew(ctx, core, chatId, assistant));
3757
+ bot.command("newchat", (ctx) => handleNewChat(ctx, core, chatId));
3758
+ bot.command("cancel", (ctx) => handleCancel(ctx, core, assistant));
3759
+ bot.command("status", (ctx) => handleStatus(ctx, core));
3760
+ bot.command("sessions", (ctx) => handleTopics(ctx, core));
3761
+ bot.command("agents", (ctx) => handleAgents(ctx, core));
3762
+ bot.command("help", (ctx) => handleHelp(ctx));
3763
+ bot.command("menu", (ctx) => handleMenu(ctx));
3764
+ bot.command("enable_dangerous", (ctx) => handleEnableDangerous(ctx, core));
3765
+ bot.command("disable_dangerous", (ctx) => handleDisableDangerous(ctx, core));
3766
+ bot.command("restart", (ctx) => handleRestart(ctx, core));
3767
+ bot.command("update", (ctx) => handleUpdate(ctx, core));
3768
+ bot.command("integrate", (ctx) => handleIntegrate(ctx, core));
3769
+ bot.command("clear", (ctx) => handleClear(ctx, assistant));
3770
+ }
3771
+ function setupAllCallbacks(bot, core, chatId, systemTopicIds) {
3772
+ setupNewSessionCallbacks(bot, core, chatId);
3773
+ setupSessionCallbacks(bot, core, chatId, systemTopicIds);
3774
+ bot.callbackQuery(/^m:/, async (ctx) => {
3775
+ const data = ctx.callbackQuery.data;
3776
+ try {
3777
+ await ctx.answerCallbackQuery();
3778
+ } catch {
3779
+ }
3780
+ switch (data) {
3781
+ case "m:new":
3782
+ await handleNew(ctx, core, chatId);
3783
+ break;
3784
+ case "m:status":
3785
+ await handleStatus(ctx, core);
3786
+ break;
3787
+ case "m:agents":
3788
+ await handleAgents(ctx, core);
3789
+ break;
3790
+ case "m:help":
3791
+ await handleHelp(ctx);
3792
+ break;
3793
+ case "m:restart":
3794
+ await handleRestart(ctx, core);
3795
+ break;
3796
+ case "m:update":
3797
+ await handleUpdate(ctx, core);
3798
+ break;
3799
+ case "m:integrate":
3800
+ await handleIntegrate(ctx, core);
3801
+ break;
3802
+ case "m:topics":
3803
+ await handleTopics(ctx, core);
3804
+ break;
3805
+ }
3806
+ });
3807
+ }
3474
3808
  var STATIC_COMMANDS = [
3475
3809
  { command: "new", description: "Create new session" },
3476
3810
  { command: "newchat", description: "New chat, same agent & workspace" },
@@ -3484,14 +3818,15 @@ var STATIC_COMMANDS = [
3484
3818
  { command: "disable_dangerous", description: "Restore normal permission prompts (session only)" },
3485
3819
  { command: "integrate", description: "Manage agent integrations" },
3486
3820
  { command: "handoff", description: "Continue this session in your terminal" },
3821
+ { command: "clear", description: "Clear assistant history" },
3487
3822
  { command: "restart", description: "Restart OpenACP" },
3488
3823
  { command: "update", description: "Update to latest version and restart" }
3489
3824
  ];
3490
3825
 
3491
3826
  // src/adapters/telegram/permissions.ts
3492
- import { InlineKeyboard as InlineKeyboard2 } from "grammy";
3827
+ import { InlineKeyboard as InlineKeyboard6 } from "grammy";
3493
3828
  import { nanoid as nanoid2 } from "nanoid";
3494
- var log9 = createChildLogger({ module: "telegram-permissions" });
3829
+ var log11 = createChildLogger({ module: "telegram-permissions" });
3495
3830
  var PermissionHandler = class {
3496
3831
  constructor(bot, chatId, getSession, sendNotification) {
3497
3832
  this.bot = bot;
@@ -3508,7 +3843,7 @@ var PermissionHandler = class {
3508
3843
  requestId: request.id,
3509
3844
  options: request.options.map((o) => ({ id: o.id, isAllow: o.isAllow }))
3510
3845
  });
3511
- const keyboard = new InlineKeyboard2();
3846
+ const keyboard = new InlineKeyboard6();
3512
3847
  for (const option of request.options) {
3513
3848
  const emoji = option.isAllow ? "\u2705" : "\u274C";
3514
3849
  keyboard.text(`${emoji} ${option.label}`, `p:${callbackKey}:${option.id}`);
@@ -3551,7 +3886,7 @@ ${escapeHtml(request.description)}`,
3551
3886
  }
3552
3887
  const session = this.getSession(pending.sessionId);
3553
3888
  const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
3554
- log9.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
3889
+ log11.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
3555
3890
  if (session?.permissionGate.requestId === pending.requestId) {
3556
3891
  session.permissionGate.resolve(optionId);
3557
3892
  }
@@ -3568,11 +3903,375 @@ ${escapeHtml(request.description)}`,
3568
3903
  }
3569
3904
  };
3570
3905
 
3906
+ // src/product-guide.ts
3907
+ var PRODUCT_GUIDE = `
3908
+ # OpenACP \u2014 Product Guide
3909
+
3910
+ OpenACP lets you chat with AI coding agents (like Claude Code) through Telegram.
3911
+ You type messages in Telegram, the agent reads/writes/runs code in your project folder, and results stream back in real time.
3912
+
3913
+ ---
3914
+
3915
+ ## Quick Start
3916
+
3917
+ 1. Start OpenACP: \`openacp\` (or \`openacp start\` for background daemon)
3918
+ 2. Open your Telegram group \u2014 you'll see the Assistant topic
3919
+ 3. Tap \u{1F195} New Session or type /new
3920
+ 4. Pick an agent and a project folder
3921
+ 5. Chat in the session topic \u2014 the agent works on your code
3922
+
3923
+ ---
3924
+
3925
+ ## Core Concepts
3926
+
3927
+ ### Sessions
3928
+ A session = one conversation with one AI agent working in one project folder.
3929
+ Each session gets its own Telegram topic. Chat there to give instructions to the agent.
3930
+
3931
+ ### Agents
3932
+ An agent is an AI coding tool (e.g., Claude Code). You can configure multiple agents.
3933
+ The default agent is used when you don't specify one.
3934
+
3935
+ ### Project Folder (Workspace)
3936
+ The directory where the agent reads, writes, and runs code.
3937
+ When creating a session, you choose which folder the agent works in.
3938
+ You can type a full path like \`~/code/my-project\` or just a name like \`my-project\` (it becomes \`<base-dir>/my-project\`).
3939
+
3940
+ ### System Topics
3941
+ - **Assistant** \u2014 Always-on helper that can answer questions, create sessions, check status, troubleshoot
3942
+ - **Notifications** \u2014 System alerts (permission requests, session errors, completions)
3943
+
3944
+ ---
3945
+
3946
+ ## Creating Sessions
3947
+
3948
+ ### From menu
3949
+ Tap \u{1F195} New Session \u2192 choose agent (if multiple) \u2192 choose project folder \u2192 confirm
3950
+
3951
+ ### From command
3952
+ - \`/new\` \u2014 Interactive flow (asks agent + folder)
3953
+ - \`/new claude ~/code/my-project\` \u2014 Create directly with specific agent and folder
3954
+
3955
+ ### From Assistant topic
3956
+ Just ask: "Create a session for my-project with claude" \u2014 the assistant handles it
3957
+
3958
+ ### Quick new chat
3959
+ \`/newchat\` in a session topic \u2014 creates new session with same agent and folder as current one
3960
+
3961
+ ---
3962
+
3963
+ ## Working with Sessions
3964
+
3965
+ ### Chat
3966
+ Type messages in the session topic. The agent responds with code changes, explanations, tool outputs.
3967
+
3968
+ ### What you see while the agent works
3969
+ - **\u{1F4AD} Thinking indicator** \u2014 Shows when the agent is reasoning, with elapsed time
3970
+ - **Text responses** \u2014 Streamed in real time, updated every few seconds
3971
+ - **Tool calls** \u2014 When the agent runs commands or edits files, you see tool name, input, status, and output
3972
+ - **\u{1F4CB} Plan card** \u2014 Visual task progress with completed/in-progress/pending items and progress bar
3973
+ - **"View File" / "View Diff" buttons** \u2014 Opens in browser with Monaco editor (requires tunnel)
3974
+
3975
+ ### Session lifecycle
3976
+ 1. **Creating** \u2014 Topic created, agent spawning
3977
+ 2. **Warming up** \u2014 Agent primes its cache (happens automatically, invisible to you)
3978
+ 3. **Active** \u2014 Ready for your messages
3979
+ 4. **Auto-naming** \u2014 After your first message, the session gets a descriptive name (agent summarizes in ~5 words). The topic title updates automatically.
3980
+ 5. **Finished/Error** \u2014 Session completed or hit an error
3981
+
3982
+ ### Agent skills
3983
+ Some agents provide slash commands (e.g., /compact, /review). Available skills are pinned in the session topic.
3984
+
3985
+ ### Permission requests
3986
+ When the agent wants to run a command, it asks for permission.
3987
+ You see buttons: \u2705 Allow, \u274C Reject (and sometimes "Always Allow").
3988
+ A notification also appears in the Notifications topic with a link to the request.
3989
+
3990
+ ### Dangerous mode
3991
+ Auto-approves ALL permission requests \u2014 the agent runs any command without asking.
3992
+ - Enable: \`/enable_dangerous\` or tap the \u2620\uFE0F button in the session
3993
+ - Disable: \`/disable_dangerous\` or tap the \u{1F510} button
3994
+ - \u26A0\uFE0F Use with caution \u2014 the agent can execute anything
3995
+
3996
+ ### Session timeout
3997
+ Idle sessions are automatically cancelled after a configurable timeout (default: 60 minutes).
3998
+ Configure via \`security.sessionTimeoutMinutes\` in config.
3999
+
4000
+ ---
4001
+
4002
+ ## Session Transfer (Handoff)
4003
+
4004
+ ### Telegram \u2192 Terminal
4005
+ 1. Type \`/handoff\` in a session topic
4006
+ 2. You get a command like \`claude --resume <SESSION_ID>\`
4007
+ 3. Copy and run it in your terminal \u2014 the session continues there with full conversation history
4008
+
4009
+ ### Terminal \u2192 Telegram
4010
+ 1. First time: run \`openacp integrate claude\` to install the handoff skill (one-time setup)
4011
+ 2. In Claude Code, use the /openacp:handoff slash command
4012
+ 3. The session appears as a new topic in Telegram and you can continue chatting there
4013
+
4014
+ ### How it works
4015
+ - The agent session ID is shared between platforms
4016
+ - Conversation history is preserved \u2014 pick up where you left off
4017
+ - The agent that supports resume (e.g., Claude with \`--resume\`) handles the actual transfer
4018
+
4019
+ ---
4020
+
4021
+ ## Managing Sessions
4022
+
4023
+ ### Status
4024
+ - \`/status\` \u2014 Shows active sessions count and details
4025
+ - Ask the Assistant: "What sessions are running?"
4026
+
4027
+ ### List all sessions
4028
+ - \`/sessions\` \u2014 Shows all sessions with status (active, finished, error)
4029
+
4030
+ ### Cancel
4031
+ - \`/cancel\` in a session topic \u2014 cancels that session
4032
+ - Ask the Assistant: "Cancel the stuck session"
4033
+
4034
+ ### Cleanup
4035
+ - From \`/sessions\` \u2192 tap cleanup buttons (finished, errors, all)
4036
+ - Ask the Assistant: "Clean up old sessions"
4037
+
4038
+ ---
4039
+
4040
+ ## Assistant Topic
4041
+
4042
+ The Assistant is an always-on AI helper in its own topic. It can:
4043
+ - Answer questions about OpenACP
4044
+ - Create sessions for you
4045
+ - Check status and health
4046
+ - Cancel sessions
4047
+ - Clean up old sessions
4048
+ - Troubleshoot issues
4049
+ - Manage configuration
4050
+
4051
+ Just chat naturally: "How do I create a session?", "What's the status?", "Something is stuck"
4052
+
4053
+ ### Clear history
4054
+ \`/clear\` in the Assistant topic \u2014 resets the conversation
4055
+
4056
+ ---
4057
+
4058
+ ## System Commands
4059
+
4060
+ | Command | Where | What it does |
4061
+ |---------|-------|-------------|
4062
+ | \`/new [agent] [path]\` | Anywhere | Create new session |
4063
+ | \`/newchat\` | Session topic | New session, same agent + folder |
4064
+ | \`/cancel\` | Session topic | Cancel current session |
4065
+ | \`/status\` | Anywhere | Show status |
4066
+ | \`/sessions\` | Anywhere | List all sessions |
4067
+ | \`/agents\` | Anywhere | List available agents |
4068
+ | \`/enable_dangerous\` | Session topic | Auto-approve all permissions |
4069
+ | \`/disable_dangerous\` | Session topic | Restore permission prompts |
4070
+ | \`/handoff\` | Session topic | Transfer session to terminal |
4071
+ | \`/clear\` | Assistant topic | Clear assistant history |
4072
+ | \`/menu\` | Anywhere | Show action menu |
4073
+ | \`/help\` | Anywhere | Show help |
4074
+ | \`/restart\` | Anywhere | Restart OpenACP |
4075
+ | \`/update\` | Anywhere | Update to latest version |
4076
+ | \`/integrate\` | Anywhere | Manage agent integrations |
4077
+
4078
+ ---
4079
+
4080
+ ## Menu Buttons
4081
+
4082
+ | Button | Action |
4083
+ |--------|--------|
4084
+ | \u{1F195} New Session | Create new session (interactive) |
4085
+ | \u{1F4CB} Sessions | List all sessions with cleanup options |
4086
+ | \u{1F4CA} Status | Show active/total session count |
4087
+ | \u{1F916} Agents | List available agents |
4088
+ | \u{1F517} Integrate | Manage agent integrations |
4089
+ | \u2753 Help | Show help text |
4090
+ | \u{1F504} Restart | Restart OpenACP |
4091
+ | \u2B06\uFE0F Update | Check and install updates |
4092
+
4093
+ ---
4094
+
4095
+ ## CLI Commands
4096
+
4097
+ ### Server
4098
+ - \`openacp\` \u2014 Start (uses configured mode: foreground or daemon)
4099
+ - \`openacp start\` \u2014 Start as background daemon
4100
+ - \`openacp stop\` \u2014 Stop daemon
4101
+ - \`openacp status\` \u2014 Show daemon status
4102
+ - \`openacp logs\` \u2014 Tail daemon logs
4103
+ - \`openacp --foreground\` \u2014 Force foreground mode (useful for debugging or containers)
4104
+
4105
+ ### Auto-start (run on boot)
4106
+ - macOS: installs a LaunchAgent in \`~/Library/LaunchAgents/\`
4107
+ - Linux: installs a systemd user service in \`~/.config/systemd/user/\`
4108
+ - Enabled automatically when you start the daemon. Remove with \`openacp stop\`.
4109
+
4110
+ ### Configuration
4111
+ - \`openacp config\` \u2014 Interactive config editor
4112
+ - \`openacp reset\` \u2014 Delete all data and start fresh
4113
+
4114
+ ### Plugins
4115
+ - \`openacp install <package>\` \u2014 Install adapter plugin (e.g., \`@openacp/adapter-discord\`)
4116
+ - \`openacp uninstall <package>\` \u2014 Remove adapter plugin
4117
+ - \`openacp plugins\` \u2014 List installed plugins
4118
+
4119
+ ### Integration
4120
+ - \`openacp integrate <agent>\` \u2014 Install agent integration (e.g., Claude handoff skill)
4121
+ - \`openacp integrate <agent> --uninstall\` \u2014 Remove integration
4122
+
4123
+ ### API (requires running daemon)
4124
+ \`openacp api <command>\` \u2014 Interact with running daemon:
4125
+
4126
+ | Command | Description |
4127
+ |---------|-------------|
4128
+ | \`status\` | List active sessions |
4129
+ | \`session <id>\` | Session details |
4130
+ | \`new <agent> <path>\` | Create session |
4131
+ | \`send <id> "text"\` | Send prompt |
4132
+ | \`cancel <id>\` | Cancel session |
4133
+ | \`dangerous <id> on/off\` | Toggle dangerous mode |
4134
+ | \`topics [--status x,y]\` | List topics |
4135
+ | \`delete-topic <id> [--force]\` | Delete topic |
4136
+ | \`cleanup [--status x,y]\` | Cleanup old topics |
4137
+ | \`agents\` | List agents |
4138
+ | \`health\` | System health |
4139
+ | \`config\` | Show config |
4140
+ | \`config set <key> <value>\` | Update config |
4141
+ | \`adapters\` | List adapters |
4142
+ | \`tunnel\` | Tunnel status |
4143
+ | \`notify "message"\` | Send notification |
4144
+ | \`version\` | Daemon version |
4145
+ | \`restart\` | Restart daemon |
4146
+
4147
+ ---
4148
+
4149
+ ## File Viewer (Tunnel)
4150
+
4151
+ When tunnel is enabled, file edits and diffs get "View" buttons that open in your browser:
4152
+ - **Monaco Editor** \u2014 Full VS Code editor with syntax highlighting
4153
+ - **Diff viewer** \u2014 Side-by-side or inline comparison
4154
+ - **Line highlighting** \u2014 Click lines to highlight
4155
+ - Dark/light theme toggle
4156
+
4157
+ ### Setup
4158
+ Enable in config: set \`tunnel.enabled\` to \`true\`.
4159
+ Providers: Cloudflare (default, free), ngrok, bore, Tailscale Funnel.
4160
+
4161
+ ---
4162
+
4163
+ ## Configuration
4164
+
4165
+ Config file: \`~/.openacp/config.json\`
4166
+
4167
+ ### Telegram
4168
+ - **telegram.botToken** \u2014 Your Telegram bot token
4169
+ - **telegram.chatId** \u2014 Your Telegram supergroup ID
4170
+
4171
+ ### Agents
4172
+ - **agents.<name>.command** \u2014 Agent executable path (e.g., \`claude\`, \`codex\`)
4173
+ - **agents.<name>.args** \u2014 Arguments to pass to the agent command
4174
+ - **agents.<name>.env** \u2014 Custom environment variables for the agent subprocess
4175
+ - **defaultAgent** \u2014 Which agent to use by default
4176
+
4177
+ ### Workspace
4178
+ - **workspace.baseDir** \u2014 Base directory for project folders (default: \`~/openacp-workspace\`)
4179
+
4180
+ ### Security
4181
+ - **security.allowedUserIds** \u2014 Restrict who can use the bot (empty = everyone)
4182
+ - **security.maxConcurrentSessions** \u2014 Max parallel sessions (default: 5)
4183
+ - **security.sessionTimeoutMinutes** \u2014 Auto-cancel idle sessions (default: 60)
4184
+
4185
+ ### Tunnel / File Viewer
4186
+ - **tunnel.enabled** \u2014 Enable file viewer tunnel
4187
+ - **tunnel.provider** \u2014 Tunnel provider: cloudflare (default, free), ngrok, bore, tailscale
4188
+ - **tunnel.port** \u2014 Local port for tunnel server (default: 3100)
4189
+ - **tunnel.auth.enabled** \u2014 Enable authentication for tunnel URLs
4190
+ - **tunnel.auth.token** \u2014 Auth token for tunnel access
4191
+ - **tunnel.storeTtlMinutes** \u2014 How long viewer links stay cached (default: 60)
4192
+
4193
+ ### Logging
4194
+ - **logging.level** \u2014 Log level: silent, debug, info, warn, error, fatal (default: info)
4195
+ - **logging.logDir** \u2014 Log directory (default: \`~/.openacp/logs\`)
4196
+ - **logging.maxFileSize** \u2014 Max log file size before rotation
4197
+ - **logging.maxFiles** \u2014 Max number of rotated log files
4198
+ - **logging.sessionLogRetentionDays** \u2014 Auto-delete old session logs (default: 30)
4199
+
4200
+ ### Data Retention
4201
+ - **sessionStore.ttlDays** \u2014 How long session records persist (default: 30). Old records are cleaned up automatically.
4202
+
4203
+ ### Environment variables
4204
+ Override config with env vars:
4205
+ - \`OPENACP_TELEGRAM_BOT_TOKEN\`
4206
+ - \`OPENACP_TELEGRAM_CHAT_ID\`
4207
+ - \`OPENACP_DEFAULT_AGENT\`
4208
+ - \`OPENACP_RUN_MODE\` \u2014 foreground or daemon
4209
+ - \`OPENACP_API_PORT\` \u2014 API server port (default: 21420)
4210
+ - \`OPENACP_TUNNEL_ENABLED\`
4211
+ - \`OPENACP_TUNNEL_PORT\`
4212
+ - \`OPENACP_TUNNEL_PROVIDER\`
4213
+ - \`OPENACP_LOG_LEVEL\`
4214
+ - \`OPENACP_LOG_DIR\`
4215
+ - \`OPENACP_DEBUG\` \u2014 Sets log level to debug
4216
+
4217
+ ---
4218
+
4219
+ ## Troubleshooting
4220
+
4221
+ ### Session stuck / not responding
4222
+ - Check status: ask Assistant "Is anything stuck?"
4223
+ - Cancel and create new: \`/cancel\` then \`/new\`
4224
+ - Check system health: Assistant can run health check
4225
+
4226
+ ### Agent not found
4227
+ - Check available agents: \`/agents\`
4228
+ - Verify agent command is installed and in PATH
4229
+ - Check config: agent command + args must be correct
4230
+
4231
+ ### Permission request not showing
4232
+ - Check Notifications topic for the alert
4233
+ - Try \`/enable_dangerous\` to auto-approve (if you trust the agent)
4234
+
4235
+ ### Session disappeared after restart
4236
+ - Sessions persist across restarts
4237
+ - Send a message in the old topic \u2014 it auto-resumes
4238
+ - If topic was deleted, the session record may still exist in status
4239
+
4240
+ ### Bot not responding at all
4241
+ - Check daemon: \`openacp status\`
4242
+ - Check logs: \`openacp logs\`
4243
+ - Restart: \`openacp start\` or \`/restart\`
4244
+
4245
+ ### Messages going to wrong topic
4246
+ - Each session is bound to a specific Telegram topic
4247
+ - If you see messages appearing in the Assistant topic instead of the session topic, try creating a new session
4248
+
4249
+ ### Viewing logs
4250
+ - Session-specific logs: \`~/.openacp/logs/sessions/\`
4251
+ - System logs: \`openacp logs\` to tail live
4252
+ - Set \`OPENACP_DEBUG=true\` for verbose output
4253
+
4254
+ ---
4255
+
4256
+ ## Data & Storage
4257
+
4258
+ All data is stored in \`~/.openacp/\`:
4259
+ - \`config.json\` \u2014 Configuration
4260
+ - \`sessions/\` \u2014 Session records and state
4261
+ - \`topics/\` \u2014 Topic-to-session mappings
4262
+ - \`logs/\` \u2014 System and session logs
4263
+ - \`plugins/\` \u2014 Installed adapter plugins
4264
+ - \`openacp.pid\` \u2014 Daemon PID file
4265
+
4266
+ Session records auto-cleanup: 30 days (configurable via \`sessionStore.ttlDays\`).
4267
+ Session logs auto-cleanup: 30 days (configurable via \`logging.sessionLogRetentionDays\`).
4268
+ `;
4269
+
3571
4270
  // src/adapters/telegram/assistant.ts
3572
- var log10 = createChildLogger({ module: "telegram-assistant" });
4271
+ var log12 = createChildLogger({ module: "telegram-assistant" });
3573
4272
  async function spawnAssistant(core, adapter, assistantTopicId) {
3574
4273
  const config = core.configManager.get();
3575
- log10.info({ agent: config.defaultAgent }, "Creating assistant session...");
4274
+ log12.info({ agent: config.defaultAgent }, "Creating assistant session...");
3576
4275
  const session = await core.sessionManager.createSession(
3577
4276
  "telegram",
3578
4277
  config.defaultAgent,
@@ -3581,7 +4280,7 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
3581
4280
  );
3582
4281
  session.threadId = String(assistantTopicId);
3583
4282
  session.name = "Assistant";
3584
- log10.info({ sessionId: session.id }, "Assistant agent spawned");
4283
+ log12.info({ sessionId: session.id }, "Assistant agent spawned");
3585
4284
  core.wireSessionEvents(session, adapter);
3586
4285
  const allRecords = core.sessionManager.listRecords();
3587
4286
  const activeCount = allRecords.filter((r) => r.status === "active" || r.status === "initializing").length;
@@ -3598,58 +4297,106 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
3598
4297
  };
3599
4298
  const systemPrompt = buildAssistantSystemPrompt(ctx);
3600
4299
  const ready = session.enqueuePrompt(systemPrompt).then(() => {
3601
- log10.info({ sessionId: session.id }, "Assistant system prompt completed");
4300
+ log12.info({ sessionId: session.id }, "Assistant system prompt completed");
3602
4301
  }).catch((err) => {
3603
- log10.warn({ err }, "Assistant system prompt failed");
4302
+ log12.warn({ err }, "Assistant system prompt failed");
3604
4303
  });
3605
4304
  return { session, ready };
3606
4305
  }
4306
+ function buildWelcomeMessage(ctx) {
4307
+ const { activeCount, errorCount, totalCount, agents, defaultAgent } = ctx;
4308
+ const agentList = agents.map((a) => `${a}${a === defaultAgent ? " (default)" : ""}`).join(", ");
4309
+ if (totalCount === 0) {
4310
+ return `\u{1F44B} <b>OpenACP is ready!</b>
4311
+
4312
+ No sessions yet. Tap \u{1F195} New Session to start, or ask me anything!`;
4313
+ }
4314
+ if (errorCount > 0) {
4315
+ return `\u{1F44B} <b>OpenACP is ready!</b>
4316
+
4317
+ \u{1F4CA} ${activeCount} active, ${errorCount} errors / ${totalCount} total
4318
+ \u26A0\uFE0F ${errorCount} session${errorCount > 1 ? "s have" : " has"} errors \u2014 ask me to check if you'd like.
4319
+
4320
+ Agents: ${agentList}`;
4321
+ }
4322
+ return `\u{1F44B} <b>OpenACP is ready!</b>
4323
+
4324
+ \u{1F4CA} ${activeCount} active / ${totalCount} total
4325
+ Agents: ${agentList}`;
4326
+ }
3607
4327
  function buildAssistantSystemPrompt(ctx) {
3608
4328
  const { config, activeSessionCount, totalSessionCount, topicSummary } = ctx;
3609
4329
  const agentNames = Object.keys(config.agents).join(", ");
3610
4330
  const topicBreakdown = topicSummary.map((s) => `${s.status}: ${s.count}`).join(", ") || "none";
3611
- return `You are the OpenACP Assistant. Help users manage their AI coding sessions and topics.
4331
+ return `You are the OpenACP Assistant \u2014 a helpful guide for managing AI coding sessions.
3612
4332
 
3613
4333
  ## Current State
3614
4334
  - Active sessions: ${activeSessionCount} / ${totalSessionCount} total
3615
4335
  - Topics by status: ${topicBreakdown}
3616
4336
  - Available agents: ${agentNames}
3617
4337
  - Default agent: ${config.defaultAgent}
3618
- - Workspace base: ${config.workspace.baseDir}
4338
+ - Workspace base directory: ${config.workspace.baseDir}
4339
+
4340
+ ## Action Playbook
3619
4341
 
3620
- ## Session Management Commands
3621
- These are Telegram bot commands (type directly in chat):
3622
- - /new [agent] [workspace] \u2014 Create new session
3623
- - /newchat \u2014 New chat with same agent & workspace
3624
- - /cancel \u2014 Cancel current session
3625
- - /status \u2014 Show status
3626
- - /agents \u2014 List agents
3627
- - /help \u2014 Show help
4342
+ ### Create Session
4343
+ - The workspace is the project directory where the agent will work (read, write, execute code). It is NOT the base directory \u2014 it should be a specific project folder like \`~/code/my-project\` or \`${config.workspace.baseDir}/my-app\`.
4344
+ - Ask which agent to use (if multiple are configured). Show available: ${agentNames}
4345
+ - Ask which project directory to use as workspace. Suggest \`${config.workspace.baseDir}\` as the base, but explain the user can provide any path.
4346
+ - Confirm before creating: show agent name + full workspace path.
4347
+ - Create via: \`openacp api new <agent> <workspace>\`
3628
4348
 
3629
- ## Management Commands (via CLI)
3630
- You have access to bash. Use these commands to manage OpenACP:
4349
+ ### Check Status / List Sessions
4350
+ - Run \`openacp api status\` for active sessions overview
4351
+ - Run \`openacp api topics\` for full list with statuses
4352
+ - Format the output nicely for the user
3631
4353
 
3632
- ### Session management
4354
+ ### Cancel Session
4355
+ - Run \`openacp api status\` to see what's active
4356
+ - If 1 active session \u2192 ask user to confirm \u2192 \`openacp api cancel <id>\`
4357
+ - If multiple \u2192 list them, ask user which one to cancel
4358
+
4359
+ ### Troubleshoot (Session Stuck, Errors)
4360
+ - Run \`openacp api health\` + \`openacp api status\` to diagnose
4361
+ - Small issue (stuck session) \u2192 suggest cancel + create new
4362
+ - Big issue (system-level) \u2192 suggest restart, ask for confirmation first
4363
+
4364
+ ### Cleanup Old Sessions
4365
+ - Run \`openacp api topics --status finished,error\` to see what can be cleaned
4366
+ - Report the count, ask user to confirm
4367
+ - Execute: \`openacp api cleanup --status <statuses>\`
4368
+
4369
+ ### Configuration
4370
+ - View: \`openacp api config\`
4371
+ - Update: \`openacp api config set <key> <value>\`
4372
+
4373
+ ### Restart / Update
4374
+ - Always ask for confirmation \u2014 these are disruptive actions
4375
+ - Guide user: "Tap \u{1F504} Restart button or type /restart"
4376
+
4377
+ ### Toggle Dangerous Mode
4378
+ - Run \`openacp api dangerous <id> on|off\`
4379
+ - Explain: dangerous mode auto-approves all permission requests \u2014 the agent can run any command without asking
4380
+
4381
+ ## CLI Commands Reference
3633
4382
  \`\`\`bash
4383
+ # Session management
3634
4384
  openacp api status # List active sessions
3635
4385
  openacp api session <id> # Session detail
4386
+ openacp api new <agent> <workspace> # Create new session
3636
4387
  openacp api send <id> "prompt text" # Send prompt to session
3637
4388
  openacp api cancel <id> # Cancel session
3638
4389
  openacp api dangerous <id> on|off # Toggle dangerous mode
3639
- \`\`\`
3640
4390
 
3641
- ### Topic management
3642
- \`\`\`bash
3643
- openacp api topics # List topics
4391
+ # Topic management
4392
+ openacp api topics # List all topics
3644
4393
  openacp api topics --status finished,error
3645
4394
  openacp api delete-topic <id> # Delete topic
3646
4395
  openacp api delete-topic <id> --force # Force delete active
3647
4396
  openacp api cleanup # Cleanup finished topics
3648
4397
  openacp api cleanup --status finished,error
3649
- \`\`\`
3650
4398
 
3651
- ### System
3652
- \`\`\`bash
4399
+ # System
3653
4400
  openacp api health # System health
3654
4401
  openacp api config # Show config
3655
4402
  openacp api config set <key> <value> # Update config
@@ -3661,13 +4408,18 @@ openacp api restart # Restart daemon
3661
4408
  \`\`\`
3662
4409
 
3663
4410
  ## Guidelines
3664
- - When a user asks about sessions or topics, run \`openacp api topics\` or \`openacp api status\` to get current data.
3665
- - When deleting: if the session is active/initializing, warn the user first. Only use --force if they confirm.
3666
- - Use \`openacp api health\` to check system status.
3667
- - Use \`openacp api config\` to check configuration, \`openacp api config set\` to update values.
3668
- - Format responses nicely for Telegram (use bold, code blocks).
3669
- - Be concise and helpful. Respond in the same language the user uses.
3670
- - When creating sessions, guide through: agent selection \u2192 workspace \u2192 confirm.`;
4411
+ - NEVER show \`openacp api ...\` commands to users. These are internal tools for YOU to run silently. Users should only see natural language responses and results.
4412
+ - Run \`openacp api ...\` commands yourself for everything you can. Only guide users to Telegram buttons/menu when needed (e.g., "Tap \u{1F195} New Session" or "Go to the session topic to chat with the agent").
4413
+ - When creating sessions: guide user through agent + workspace choice conversationally, then run the command yourself.
4414
+ - Destructive actions (cancel active session, restart, cleanup) \u2192 always ask user to confirm first in natural language.
4415
+ - Small/obvious issues (clearly stuck session with no activity) \u2192 fix it and report back.
4416
+ - Respond in the same language the user uses.
4417
+ - Format responses for Telegram: use <b>bold</b>, <code>code</code>, keep it concise.
4418
+ - When you don't know something, check with the relevant \`openacp api\` command first before answering.
4419
+ - Talk to users like a helpful assistant, not a CLI manual. Example: "B\u1EA1n c\xF3 2 session \u0111ang ch\u1EA1y. Mu\u1ED1n xem chi ti\u1EBFt kh\xF4ng?" instead of listing commands.
4420
+
4421
+ ## Product Reference
4422
+ ${PRODUCT_GUIDE}`;
3671
4423
  }
3672
4424
  async function handleAssistantMessage(session, text) {
3673
4425
  if (!session) return;
@@ -3680,7 +4432,7 @@ function redirectToAssistant(chatId, assistantTopicId) {
3680
4432
  }
3681
4433
 
3682
4434
  // src/adapters/telegram/activity.ts
3683
- var log11 = createChildLogger({ module: "telegram:activity" });
4435
+ var log13 = createChildLogger({ module: "telegram:activity" });
3684
4436
  var THINKING_REFRESH_MS = 15e3;
3685
4437
  var THINKING_MAX_MS = 3 * 60 * 1e3;
3686
4438
  var ThinkingIndicator = class {
@@ -3712,7 +4464,7 @@ var ThinkingIndicator = class {
3712
4464
  this.startRefreshTimer();
3713
4465
  }
3714
4466
  } catch (err) {
3715
- log11.warn({ err }, "ThinkingIndicator.show() failed");
4467
+ log13.warn({ err }, "ThinkingIndicator.show() failed");
3716
4468
  } finally {
3717
4469
  this.sending = false;
3718
4470
  }
@@ -3785,7 +4537,7 @@ var UsageMessage = class {
3785
4537
  if (result) this.msgId = result.message_id;
3786
4538
  }
3787
4539
  } catch (err) {
3788
- log11.warn({ err }, "UsageMessage.send() failed");
4540
+ log13.warn({ err }, "UsageMessage.send() failed");
3789
4541
  }
3790
4542
  }
3791
4543
  getMsgId() {
@@ -3798,7 +4550,7 @@ var UsageMessage = class {
3798
4550
  try {
3799
4551
  await this.sendQueue.enqueue(() => this.api.deleteMessage(this.chatId, id));
3800
4552
  } catch (err) {
3801
- log11.warn({ err }, "UsageMessage.delete() failed");
4553
+ log13.warn({ err }, "UsageMessage.delete() failed");
3802
4554
  }
3803
4555
  }
3804
4556
  };
@@ -3884,7 +4636,7 @@ var PlanCard = class {
3884
4636
  if (result) this.msgId = result.message_id;
3885
4637
  }
3886
4638
  } catch (err) {
3887
- log11.warn({ err }, "PlanCard flush failed");
4639
+ log13.warn({ err }, "PlanCard flush failed");
3888
4640
  }
3889
4641
  }
3890
4642
  };
@@ -3947,7 +4699,7 @@ var ActivityTracker = class {
3947
4699
  })
3948
4700
  );
3949
4701
  } catch (err) {
3950
- log11.warn({ err }, "ActivityTracker.onComplete() Done send failed");
4702
+ log13.warn({ err }, "ActivityTracker.onComplete() Done send failed");
3951
4703
  }
3952
4704
  }
3953
4705
  }
@@ -4030,7 +4782,7 @@ var TelegramSendQueue = class {
4030
4782
 
4031
4783
  // src/adapters/telegram/action-detect.ts
4032
4784
  import { nanoid as nanoid3 } from "nanoid";
4033
- import { InlineKeyboard as InlineKeyboard3 } from "grammy";
4785
+ import { InlineKeyboard as InlineKeyboard7 } from "grammy";
4034
4786
  var CMD_NEW_RE = /\/new(?:\s+([^\s\u0080-\uFFFF]+)(?:\s+([^\s\u0080-\uFFFF]+))?)?/;
4035
4787
  var CMD_CANCEL_RE = /\/cancel\b/;
4036
4788
  var KW_NEW_RE = /(?:create|new)\s+session/i;
@@ -4077,7 +4829,7 @@ function removeAction(id) {
4077
4829
  actionMap.delete(id);
4078
4830
  }
4079
4831
  function buildActionKeyboard(actionId, action) {
4080
- const keyboard = new InlineKeyboard3();
4832
+ const keyboard = new InlineKeyboard7();
4081
4833
  if (action.action === "new_session") {
4082
4834
  keyboard.text("\u2705 Create session", `a:${actionId}`);
4083
4835
  keyboard.text("\u274C Cancel", `a:dismiss:${actionId}`);
@@ -4109,27 +4861,38 @@ function setupActionCallbacks(bot, core, chatId, getAssistantSessionId) {
4109
4861
  removeAction(actionId);
4110
4862
  try {
4111
4863
  if (action.action === "new_session") {
4112
- await ctx.answerCallbackQuery({ text: "\u23F3 Creating session..." });
4113
- const { threadId, firstMsgId } = await executeNewSession(
4114
- bot,
4115
- core,
4116
- chatId,
4117
- action.agent,
4118
- action.workspace
4119
- );
4120
- const topicLink = `https://t.me/c/${String(chatId).replace("-100", "")}/${firstMsgId ?? threadId}`;
4121
- const originalText = ctx.callbackQuery.message?.text ?? "";
4122
- try {
4123
- await ctx.editMessageText(
4124
- originalText + `
4864
+ if (action.agent && action.workspace) {
4865
+ await ctx.answerCallbackQuery({ text: "\u23F3 Creating session..." });
4866
+ const { threadId, firstMsgId } = await executeNewSession(
4867
+ bot,
4868
+ core,
4869
+ chatId,
4870
+ action.agent,
4871
+ action.workspace
4872
+ );
4873
+ const topicLink = `https://t.me/c/${String(chatId).replace("-100", "")}/${firstMsgId ?? threadId}`;
4874
+ const originalText = ctx.callbackQuery.message?.text ?? "";
4875
+ try {
4876
+ await ctx.editMessageText(
4877
+ originalText + `
4125
4878
 
4126
4879
  \u2705 Session created \u2192 <a href="${topicLink}">Go to topic</a>`,
4127
- { parse_mode: "HTML" }
4128
- );
4129
- } catch {
4130
- await ctx.editMessageReplyMarkup({
4131
- reply_markup: { inline_keyboard: [] }
4132
- });
4880
+ { parse_mode: "HTML" }
4881
+ );
4882
+ } catch {
4883
+ await ctx.editMessageReplyMarkup({
4884
+ reply_markup: { inline_keyboard: [] }
4885
+ });
4886
+ }
4887
+ } else {
4888
+ await ctx.answerCallbackQuery();
4889
+ try {
4890
+ await ctx.editMessageReplyMarkup({
4891
+ reply_markup: { inline_keyboard: [] }
4892
+ });
4893
+ } catch {
4894
+ }
4895
+ await startInteractiveNewSession(ctx, core, chatId, action.agent);
4133
4896
  }
4134
4897
  } else if (action.action === "cancel_session") {
4135
4898
  const assistantId = getAssistantSessionId();
@@ -4174,7 +4937,7 @@ function setupActionCallbacks(bot, core, chatId, getAssistantSessionId) {
4174
4937
  }
4175
4938
 
4176
4939
  // src/adapters/telegram/adapter.ts
4177
- var log12 = createChildLogger({ module: "telegram" });
4940
+ var log14 = createChildLogger({ module: "telegram" });
4178
4941
  function patchedFetch(input, init) {
4179
4942
  if (init?.signal && !(init.signal instanceof AbortSignal)) {
4180
4943
  const nativeController = new AbortController();
@@ -4225,7 +4988,7 @@ var TelegramAdapter = class extends ChannelAdapter {
4225
4988
  this.bot = new Bot(this.telegramConfig.botToken, { client: { fetch: patchedFetch } });
4226
4989
  this.bot.catch((err) => {
4227
4990
  const rootCause = err.error instanceof Error ? err.error : err;
4228
- log12.error({ err: rootCause }, "Telegram bot error");
4991
+ log14.error({ err: rootCause }, "Telegram bot error");
4229
4992
  });
4230
4993
  this.bot.api.config.use(async (prev, method, payload, signal) => {
4231
4994
  const maxRetries = 3;
@@ -4239,7 +5002,7 @@ var TelegramAdapter = class extends ChannelAdapter {
4239
5002
  if (rateLimitedMethods.includes(method)) {
4240
5003
  this.sendQueue.onRateLimited();
4241
5004
  }
4242
- log12.warn(
5005
+ log14.warn(
4243
5006
  { method, retryAfter, attempt: attempt + 1 },
4244
5007
  "Rate limited by Telegram, retrying"
4245
5008
  );
@@ -4291,7 +5054,7 @@ var TelegramAdapter = class extends ChannelAdapter {
4291
5054
  () => this.assistantSession?.id
4292
5055
  );
4293
5056
  setupIntegrateCallbacks(this.bot, this.core);
4294
- setupMenuCallbacks(
5057
+ setupAllCallbacks(
4295
5058
  this.bot,
4296
5059
  this.core,
4297
5060
  this.telegramConfig.chatId,
@@ -4303,7 +5066,23 @@ var TelegramAdapter = class extends ChannelAdapter {
4303
5066
  this.telegramConfig.chatId,
4304
5067
  {
4305
5068
  topicId: this.assistantTopicId,
4306
- getSession: () => this.assistantSession
5069
+ getSession: () => this.assistantSession,
5070
+ respawn: async () => {
5071
+ if (this.assistantSession) {
5072
+ await this.assistantSession.destroy();
5073
+ this.assistantSession = null;
5074
+ }
5075
+ const { session, ready } = await spawnAssistant(
5076
+ this.core,
5077
+ this,
5078
+ this.assistantTopicId
5079
+ );
5080
+ this.assistantSession = session;
5081
+ this.assistantInitializing = true;
5082
+ ready.then(() => {
5083
+ this.assistantInitializing = false;
5084
+ });
5085
+ }
4307
5086
  }
4308
5087
  );
4309
5088
  this.permissionHandler.setupCallbackHandler();
@@ -4348,7 +5127,7 @@ var TelegramAdapter = class extends ChannelAdapter {
4348
5127
  this.setupRoutes();
4349
5128
  this.bot.start({
4350
5129
  allowed_updates: ["message", "callback_query"],
4351
- onStart: () => log12.info(
5130
+ onStart: () => log14.info(
4352
5131
  { chatId: this.telegramConfig.chatId },
4353
5132
  "Telegram bot started"
4354
5133
  )
@@ -4356,27 +5135,24 @@ var TelegramAdapter = class extends ChannelAdapter {
4356
5135
  try {
4357
5136
  const config = this.core.configManager.get();
4358
5137
  const agents = this.core.agentManager.getAvailableAgents();
4359
- const agentList = agents.map((a) => `${escapeHtml(a.name)}${a.name === config.defaultAgent ? " (default)" : ""}`).join(", ");
4360
- const workspace = escapeHtml(config.workspace.baseDir);
4361
5138
  const allRecords = this.core.sessionManager.listRecords();
4362
- const activeCount = allRecords.filter((r) => r.status === "active" || r.status === "initializing").length;
4363
- const welcomeText = `\u{1F44B} <b>OpenACP Assistant</b> is online.
4364
-
4365
- Available agents: ${agentList}
4366
- Workspace: <code>${workspace}</code>
4367
- Sessions: ${activeCount} active / ${allRecords.length} total
4368
-
4369
- <b>Select an action:</b>`;
5139
+ const welcomeText = buildWelcomeMessage({
5140
+ activeCount: allRecords.filter((r) => r.status === "active" || r.status === "initializing").length,
5141
+ errorCount: allRecords.filter((r) => r.status === "error").length,
5142
+ totalCount: allRecords.length,
5143
+ agents: agents.map((a) => a.name),
5144
+ defaultAgent: config.defaultAgent
5145
+ });
4370
5146
  await this.bot.api.sendMessage(this.telegramConfig.chatId, welcomeText, {
4371
5147
  message_thread_id: this.assistantTopicId,
4372
5148
  parse_mode: "HTML",
4373
5149
  reply_markup: buildMenuKeyboard()
4374
5150
  });
4375
5151
  } catch (err) {
4376
- log12.warn({ err }, "Failed to send welcome message");
5152
+ log14.warn({ err }, "Failed to send welcome message");
4377
5153
  }
4378
5154
  try {
4379
- log12.info("Spawning assistant session...");
5155
+ log14.info("Spawning assistant session...");
4380
5156
  const { session, ready } = await spawnAssistant(
4381
5157
  this.core,
4382
5158
  this,
@@ -4384,13 +5160,13 @@ Sessions: ${activeCount} active / ${allRecords.length} total
4384
5160
  );
4385
5161
  this.assistantSession = session;
4386
5162
  this.assistantInitializing = true;
4387
- log12.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
5163
+ log14.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
4388
5164
  ready.then(() => {
4389
5165
  this.assistantInitializing = false;
4390
- log12.info({ sessionId: session.id }, "Assistant ready for user messages");
5166
+ log14.info({ sessionId: session.id }, "Assistant ready for user messages");
4391
5167
  });
4392
5168
  } catch (err) {
4393
- log12.error({ err }, "Failed to spawn assistant");
5169
+ log14.error({ err }, "Failed to spawn assistant");
4394
5170
  this.bot.api.sendMessage(
4395
5171
  this.telegramConfig.chatId,
4396
5172
  `\u26A0\uFE0F <b>Failed to start assistant session.</b>
@@ -4406,11 +5182,14 @@ Sessions: ${activeCount} active / ${allRecords.length} total
4406
5182
  await this.assistantSession.destroy();
4407
5183
  }
4408
5184
  await this.bot.stop();
4409
- log12.info("Telegram bot stopped");
5185
+ log14.info("Telegram bot stopped");
4410
5186
  }
4411
5187
  setupRoutes() {
4412
5188
  this.bot.on("message:text", async (ctx) => {
4413
5189
  const threadId = ctx.message.message_thread_id;
5190
+ if (await handlePendingWorkspaceInput(ctx, this.core, this.telegramConfig.chatId, this.assistantTopicId)) {
5191
+ return;
5192
+ }
4414
5193
  if (!threadId) {
4415
5194
  const html = redirectToAssistant(
4416
5195
  this.telegramConfig.chatId,
@@ -4429,7 +5208,7 @@ Sessions: ${activeCount} active / ${allRecords.length} total
4429
5208
  ctx.replyWithChatAction("typing").catch(() => {
4430
5209
  });
4431
5210
  handleAssistantMessage(this.assistantSession, ctx.message.text).catch(
4432
- (err) => log12.error({ err }, "Assistant error")
5211
+ (err) => log14.error({ err }, "Assistant error")
4433
5212
  );
4434
5213
  return;
4435
5214
  }
@@ -4446,7 +5225,7 @@ Sessions: ${activeCount} active / ${allRecords.length} total
4446
5225
  threadId: String(threadId),
4447
5226
  userId: String(ctx.from.id),
4448
5227
  text: ctx.message.text
4449
- }).catch((err) => log12.error({ err }, "handleMessage error"));
5228
+ }).catch((err) => log14.error({ err }, "handleMessage error"));
4450
5229
  });
4451
5230
  }
4452
5231
  // --- ChannelAdapter implementations ---
@@ -4457,6 +5236,10 @@ Sessions: ${activeCount} active / ${allRecords.length} total
4457
5236
  );
4458
5237
  if (!session) return;
4459
5238
  const threadId = Number(session.threadId);
5239
+ if (!threadId || isNaN(threadId)) {
5240
+ log14.warn({ sessionId, threadId: session.threadId }, "Session has no valid threadId, skipping message");
5241
+ return;
5242
+ }
4460
5243
  switch (content.type) {
4461
5244
  case "thought": {
4462
5245
  const tracker = this.getOrCreateTracker(sessionId, threadId);
@@ -4526,16 +5309,16 @@ Sessions: ${activeCount} active / ${allRecords.length} total
4526
5309
  if (toolState) {
4527
5310
  if (meta.viewerLinks) {
4528
5311
  toolState.viewerLinks = meta.viewerLinks;
4529
- log12.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
5312
+ log14.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
4530
5313
  }
4531
5314
  const viewerFilePath = content.metadata?.viewerFilePath;
4532
5315
  if (viewerFilePath) toolState.viewerFilePath = viewerFilePath;
4533
5316
  if (meta.name) toolState.name = meta.name;
4534
5317
  if (meta.kind) toolState.kind = meta.kind;
4535
5318
  const isTerminal = meta.status === "completed" || meta.status === "failed";
4536
- if (!isTerminal && !meta.viewerLinks) break;
5319
+ if (!isTerminal) break;
4537
5320
  await toolState.ready;
4538
- log12.debug(
5321
+ log14.debug(
4539
5322
  { toolId: meta.id, status: meta.status, hasViewerLinks: !!toolState.viewerLinks, viewerLinks: toolState.viewerLinks, name: toolState.name, msgId: toolState.msgId },
4540
5323
  "Tool completed, preparing edit"
4541
5324
  );
@@ -4557,7 +5340,7 @@ Sessions: ${activeCount} active / ${allRecords.length} total
4557
5340
  )
4558
5341
  );
4559
5342
  } catch (err) {
4560
- log12.warn(
5343
+ log14.warn(
4561
5344
  { err, msgId: toolState.msgId, textLen: formattedText.length, hasViewerLinks: !!merged.viewerLinks },
4562
5345
  "Tool update edit failed"
4563
5346
  );
@@ -4652,7 +5435,7 @@ Task completed.
4652
5435
  }
4653
5436
  }
4654
5437
  async sendPermissionRequest(sessionId, request) {
4655
- log12.info({ sessionId, requestId: request.id }, "Permission request sent");
5438
+ log14.info({ sessionId, requestId: request.id }, "Permission request sent");
4656
5439
  const session = this.core.sessionManager.getSession(
4657
5440
  sessionId
4658
5441
  );
@@ -4660,7 +5443,7 @@ Task completed.
4660
5443
  if (request.description.includes("openacp")) {
4661
5444
  const allowOption = request.options.find((o) => o.isAllow);
4662
5445
  if (allowOption && session.permissionGate.requestId === request.id) {
4663
- log12.info({ sessionId, requestId: request.id }, "Auto-approving openacp command");
5446
+ log14.info({ sessionId, requestId: request.id }, "Auto-approving openacp command");
4664
5447
  session.permissionGate.resolve(allowOption.id);
4665
5448
  }
4666
5449
  return;
@@ -4668,7 +5451,7 @@ Task completed.
4668
5451
  if (session.dangerousMode) {
4669
5452
  const allowOption = request.options.find((o) => o.isAllow);
4670
5453
  if (allowOption && session.permissionGate.requestId === request.id) {
4671
- log12.info({ sessionId, requestId: request.id, optionId: allowOption.id }, "Dangerous mode: auto-approving permission");
5454
+ log14.info({ sessionId, requestId: request.id, optionId: allowOption.id }, "Dangerous mode: auto-approving permission");
4672
5455
  session.permissionGate.resolve(allowOption.id);
4673
5456
  }
4674
5457
  return;
@@ -4679,7 +5462,7 @@ Task completed.
4679
5462
  }
4680
5463
  async sendNotification(notification) {
4681
5464
  if (notification.sessionId === this.assistantSession?.id) return;
4682
- log12.info(
5465
+ log14.info(
4683
5466
  { sessionId: notification.sessionId, type: notification.type },
4684
5467
  "Notification sent"
4685
5468
  );
@@ -4715,7 +5498,7 @@ Task completed.
4715
5498
  );
4716
5499
  }
4717
5500
  async createSessionThread(sessionId, name) {
4718
- log12.info({ sessionId, name }, "Session topic created");
5501
+ log14.info({ sessionId, name }, "Session topic created");
4719
5502
  return String(
4720
5503
  await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
4721
5504
  );
@@ -4744,7 +5527,7 @@ Task completed.
4744
5527
  try {
4745
5528
  await this.bot.api.deleteForumTopic(this.telegramConfig.chatId, topicId);
4746
5529
  } catch (err) {
4747
- log12.warn({ err, sessionId, topicId }, "Failed to delete forum topic (may already be deleted)");
5530
+ log14.warn({ err, sessionId, topicId }, "Failed to delete forum topic (may already be deleted)");
4748
5531
  }
4749
5532
  }
4750
5533
  async sendSkillCommands(sessionId, commands) {
@@ -4817,7 +5600,7 @@ Task completed.
4817
5600
  { disable_notification: true }
4818
5601
  );
4819
5602
  } catch (err) {
4820
- log12.error({ err, sessionId }, "Failed to send skill commands");
5603
+ log14.error({ err, sessionId }, "Failed to send skill commands");
4821
5604
  }
4822
5605
  }
4823
5606
  async cleanupSkillCommands(sessionId) {
@@ -4888,4 +5671,4 @@ export {
4888
5671
  TopicManager,
4889
5672
  TelegramAdapter
4890
5673
  };
4891
- //# sourceMappingURL=chunk-BBPWAWE3.js.map
5674
+ //# sourceMappingURL=chunk-KPI4HGJC.js.map