opencode-tbot 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.js CHANGED
@@ -211,6 +211,14 @@ function buildOpenCodeSdkConfig(options) {
211
211
  ...apiKey ? { auth: apiKey } : {}
212
212
  };
213
213
  }
214
+ var OpenCodePromptTimeoutError = class extends Error {
215
+ data;
216
+ constructor(data) {
217
+ super(data.message ?? "OpenCode prompt timed out.");
218
+ this.name = "OpenCodePromptTimeoutError";
219
+ this.data = data;
220
+ }
221
+ };
214
222
  var EMPTY_RESPONSE_TEXT = "OpenCode returned empty response.";
215
223
  var PROMPT_MESSAGE_POLL_INITIAL_DELAYS_MS = [
216
224
  0,
@@ -220,8 +228,11 @@ var PROMPT_MESSAGE_POLL_INITIAL_DELAYS_MS = [
220
228
  1e3
221
229
  ];
222
230
  var PROMPT_MESSAGE_POLL_INTERVAL_MS = 2e3;
231
+ var PROMPT_POLL_REQUEST_TIMEOUT_MS = 15e3;
232
+ var PROMPT_SEND_TIMEOUT_MS = 3e4;
223
233
  var PROMPT_MESSAGE_POLL_TIMEOUT_MS = 6e4;
224
234
  var PROMPT_MESSAGE_POLL_LIMIT = 20;
235
+ var PROMPT_LOG_SERVICE = "opencode-tbot";
225
236
  var STRUCTURED_REPLY_SCHEMA = {
226
237
  type: "json_schema",
227
238
  retryCount: 2,
@@ -243,6 +254,11 @@ var StructuredReplySchema = z.object({ body_md: z.string() });
243
254
  var OpenCodeClient = class {
244
255
  client;
245
256
  fetchFn;
257
+ promptRequestTimeouts = {
258
+ pollRequestMs: PROMPT_POLL_REQUEST_TIMEOUT_MS,
259
+ sendMs: PROMPT_SEND_TIMEOUT_MS,
260
+ totalPollMs: PROMPT_MESSAGE_POLL_TIMEOUT_MS
261
+ };
246
262
  modelCache = {
247
263
  expiresAt: 0,
248
264
  promise: null,
@@ -287,8 +303,7 @@ var OpenCodeClient = class {
287
303
  return unwrapSdkData(await this.client.mcp.status(directory ? { directory } : void 0, SDK_OPTIONS));
288
304
  }
289
305
  async getSessionStatuses() {
290
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/session/status" });
291
- return unwrapSdkData(await this.client.session.status(void 0, SDK_OPTIONS));
306
+ return this.loadSessionStatuses();
292
307
  }
293
308
  async listProjects() {
294
309
  if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/project" });
@@ -394,7 +409,7 @@ var OpenCodeClient = class {
394
409
  structured
395
410
  };
396
411
  let bestCandidate = selectPromptResponseCandidate([data], candidateOptions) ?? data;
397
- const deadlineAt = Date.now() + PROMPT_MESSAGE_POLL_TIMEOUT_MS;
412
+ const deadlineAt = Date.now() + this.promptRequestTimeouts.totalPollMs;
398
413
  let idleStatusSeen = false;
399
414
  let attempt = 0;
400
415
  while (true) {
@@ -412,7 +427,7 @@ var OpenCodeClient = class {
412
427
  if (!shouldPollPromptMessage(next, structured)) return bestCandidate;
413
428
  }
414
429
  }
415
- const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions);
430
+ const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "poll-messages");
416
431
  if (latest) {
417
432
  bestCandidate = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions) ?? bestCandidate;
418
433
  if (!shouldPollPromptMessage(bestCandidate, structured)) return bestCandidate;
@@ -423,57 +438,114 @@ var OpenCodeClient = class {
423
438
  }
424
439
  if (Date.now() >= deadlineAt) break;
425
440
  }
426
- const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions);
427
- return selectPromptResponseCandidate([bestCandidate, latest], candidateOptions) ?? bestCandidate;
441
+ const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "final-scan");
442
+ const resolved = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions) ?? bestCandidate;
443
+ if (shouldPollPromptMessage(resolved, structured)) {
444
+ const error = createOpenCodePromptTimeoutError({
445
+ sessionId: input.sessionId,
446
+ stage: "final-scan",
447
+ timeoutMs: this.promptRequestTimeouts.totalPollMs,
448
+ messageId: messageId ?? void 0
449
+ });
450
+ this.logPromptRequestFailure(error, {
451
+ sessionId: input.sessionId,
452
+ stage: "final-scan",
453
+ timeoutMs: this.promptRequestTimeouts.totalPollMs,
454
+ messageId
455
+ });
456
+ throw error;
457
+ }
458
+ return resolved;
428
459
  }
429
460
  async fetchPromptMessage(sessionId, messageId) {
430
461
  try {
431
- if (hasRawSdkMethod(this.client, "get")) return normalizePromptResponse(await this.requestRaw("get", {
432
- url: "/session/{sessionID}/message/{messageID}",
433
- path: {
462
+ return await this.runPromptRequestWithTimeout({
463
+ sessionId,
464
+ stage: "poll-message",
465
+ timeoutMs: this.promptRequestTimeouts.pollRequestMs,
466
+ messageId
467
+ }, async (signal) => {
468
+ if (hasRawSdkMethod(this.client, "get")) return normalizePromptResponse(await this.requestRaw("get", {
469
+ url: "/session/{sessionID}/message/{messageID}",
470
+ path: {
471
+ sessionID: sessionId,
472
+ messageID: messageId
473
+ },
474
+ signal
475
+ }));
476
+ if (typeof this.client.session.message !== "function") return null;
477
+ return normalizePromptResponse(unwrapSdkData(await this.client.session.message({
434
478
  sessionID: sessionId,
435
479
  messageID: messageId
436
- }
437
- }));
438
- if (typeof this.client.session.message !== "function") return null;
439
- return normalizePromptResponse(unwrapSdkData(await this.client.session.message({
440
- sessionID: sessionId,
441
- messageID: messageId
442
- }, SDK_OPTIONS)));
443
- } catch {
480
+ }, {
481
+ ...SDK_OPTIONS,
482
+ signal
483
+ })));
484
+ });
485
+ } catch (error) {
486
+ this.logPromptRequestFailure(error, {
487
+ sessionId,
488
+ stage: "poll-message",
489
+ timeoutMs: this.promptRequestTimeouts.pollRequestMs,
490
+ messageId
491
+ });
444
492
  return null;
445
493
  }
446
494
  }
447
495
  async captureKnownMessageIds(sessionId) {
448
- const messages = await this.fetchRecentPromptMessages(sessionId);
496
+ const messages = await this.fetchRecentPromptMessages(sessionId, "capture-known-messages");
449
497
  if (!messages) return /* @__PURE__ */ new Set();
450
498
  return new Set(messages.map((message) => extractMessageId(message.info)).filter((id) => typeof id === "string" && id.length > 0));
451
499
  }
452
- async fetchRecentPromptMessages(sessionId) {
500
+ async fetchRecentPromptMessages(sessionId, stage) {
453
501
  try {
454
- if (hasRawSdkMethod(this.client, "get")) return normalizePromptResponses(await this.requestRaw("get", {
455
- url: "/session/{sessionID}/message",
456
- path: { sessionID: sessionId },
457
- query: { limit: PROMPT_MESSAGE_POLL_LIMIT }
458
- }));
459
- if (typeof this.client.session.messages !== "function") return null;
460
- return normalizePromptResponses(unwrapSdkData(await this.client.session.messages({
461
- sessionID: sessionId,
462
- limit: PROMPT_MESSAGE_POLL_LIMIT
463
- }, SDK_OPTIONS)));
464
- } catch {
502
+ return await this.runPromptRequestWithTimeout({
503
+ sessionId,
504
+ stage,
505
+ timeoutMs: this.promptRequestTimeouts.pollRequestMs
506
+ }, async (signal) => {
507
+ if (hasRawSdkMethod(this.client, "get")) return normalizePromptResponses(await this.requestRaw("get", {
508
+ url: "/session/{sessionID}/message",
509
+ path: { sessionID: sessionId },
510
+ query: { limit: PROMPT_MESSAGE_POLL_LIMIT },
511
+ signal
512
+ }));
513
+ if (typeof this.client.session.messages !== "function") return null;
514
+ return normalizePromptResponses(unwrapSdkData(await this.client.session.messages({
515
+ sessionID: sessionId,
516
+ limit: PROMPT_MESSAGE_POLL_LIMIT
517
+ }, {
518
+ ...SDK_OPTIONS,
519
+ signal
520
+ })));
521
+ });
522
+ } catch (error) {
523
+ this.logPromptRequestFailure(error, {
524
+ sessionId,
525
+ stage,
526
+ timeoutMs: this.promptRequestTimeouts.pollRequestMs
527
+ });
465
528
  return null;
466
529
  }
467
530
  }
468
531
  async fetchPromptSessionStatus(sessionId) {
469
532
  try {
470
- return (await this.getSessionStatuses())[sessionId] ?? null;
471
- } catch {
533
+ return (await this.runPromptRequestWithTimeout({
534
+ sessionId,
535
+ stage: "poll-status",
536
+ timeoutMs: this.promptRequestTimeouts.pollRequestMs
537
+ }, async (signal) => this.loadSessionStatuses(signal)))[sessionId] ?? null;
538
+ } catch (error) {
539
+ this.logPromptRequestFailure(error, {
540
+ sessionId,
541
+ stage: "poll-status",
542
+ timeoutMs: this.promptRequestTimeouts.pollRequestMs
543
+ });
472
544
  return null;
473
545
  }
474
546
  }
475
- async findLatestPromptResponse(sessionId, options) {
476
- const messages = await this.fetchRecentPromptMessages(sessionId);
547
+ async findLatestPromptResponse(sessionId, options, stage) {
548
+ const messages = await this.fetchRecentPromptMessages(sessionId, stage);
477
549
  if (!messages || messages.length === 0) return null;
478
550
  return selectPromptResponseCandidate(messages, options);
479
551
  }
@@ -497,25 +569,44 @@ var OpenCodeClient = class {
497
569
  return unwrapSdkData(await this.client.config.providers(void 0, SDK_OPTIONS));
498
570
  }
499
571
  async sendPromptRequest(input, parts) {
500
- if (hasRawSdkMethod(this.client, "post")) return normalizePromptResponse(await this.requestRaw("post", {
501
- url: "/session/{sessionID}/message",
502
- path: { sessionID: input.sessionId },
503
- body: {
504
- ...input.agent ? { agent: input.agent } : {},
505
- ...input.structured ? { format: STRUCTURED_REPLY_SCHEMA } : {},
506
- ...input.model ? { model: input.model } : {},
507
- ...input.variant ? { variant: input.variant } : {},
508
- parts
509
- }
510
- }));
511
- return normalizePromptResponse(unwrapSdkData(await this.client.session.prompt({
512
- sessionID: input.sessionId,
513
- ...input.agent ? { agent: input.agent } : {},
514
- ...input.structured ? { format: STRUCTURED_REPLY_SCHEMA } : {},
515
- ...input.model ? { model: input.model } : {},
516
- ...input.variant ? { variant: input.variant } : {},
517
- parts
518
- }, SDK_OPTIONS)));
572
+ try {
573
+ return await this.runPromptRequestWithTimeout({
574
+ sessionId: input.sessionId,
575
+ stage: "send-prompt",
576
+ timeoutMs: this.promptRequestTimeouts.sendMs
577
+ }, async (signal) => {
578
+ if (hasRawSdkMethod(this.client, "post")) return normalizePromptResponse(await this.requestRaw("post", {
579
+ url: "/session/{sessionID}/message",
580
+ path: { sessionID: input.sessionId },
581
+ body: {
582
+ ...input.agent ? { agent: input.agent } : {},
583
+ ...input.structured ? { format: STRUCTURED_REPLY_SCHEMA } : {},
584
+ ...input.model ? { model: input.model } : {},
585
+ ...input.variant ? { variant: input.variant } : {},
586
+ parts
587
+ },
588
+ signal
589
+ }));
590
+ return normalizePromptResponse(unwrapSdkData(await this.client.session.prompt({
591
+ sessionID: input.sessionId,
592
+ ...input.agent ? { agent: input.agent } : {},
593
+ ...input.structured ? { format: STRUCTURED_REPLY_SCHEMA } : {},
594
+ ...input.model ? { model: input.model } : {},
595
+ ...input.variant ? { variant: input.variant } : {},
596
+ parts
597
+ }, {
598
+ ...SDK_OPTIONS,
599
+ signal
600
+ })));
601
+ });
602
+ } catch (error) {
603
+ this.logPromptRequestFailure(error, {
604
+ sessionId: input.sessionId,
605
+ stage: "send-prompt",
606
+ timeoutMs: this.promptRequestTimeouts.sendMs
607
+ });
608
+ throw error;
609
+ }
519
610
  }
520
611
  async requestRaw(method, options) {
521
612
  const handler = getRawSdkClient(this.client)?.[method];
@@ -525,6 +616,67 @@ var OpenCodeClient = class {
525
616
  ...options
526
617
  }));
527
618
  }
619
+ async loadSessionStatuses(signal) {
620
+ if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", {
621
+ url: "/session/status",
622
+ ...signal ? { signal } : {}
623
+ });
624
+ return unwrapSdkData(await this.client.session.status(void 0, {
625
+ ...SDK_OPTIONS,
626
+ ...signal ? { signal } : {}
627
+ }));
628
+ }
629
+ async runPromptRequestWithTimeout(input, operation) {
630
+ const startedAt = Date.now();
631
+ const controller = new AbortController();
632
+ let timeoutHandle = null;
633
+ const timeoutPromise = new Promise((_, reject) => {
634
+ timeoutHandle = setTimeout(() => {
635
+ reject(createOpenCodePromptTimeoutError({
636
+ sessionId: input.sessionId,
637
+ stage: input.stage,
638
+ timeoutMs: input.timeoutMs,
639
+ messageId: input.messageId ?? void 0,
640
+ elapsedMs: Date.now() - startedAt
641
+ }));
642
+ controller.abort();
643
+ }, input.timeoutMs);
644
+ });
645
+ try {
646
+ return await Promise.race([operation(controller.signal), timeoutPromise]);
647
+ } finally {
648
+ if (timeoutHandle !== null) clearTimeout(timeoutHandle);
649
+ }
650
+ }
651
+ logPromptRequestFailure(error, input) {
652
+ if (error instanceof OpenCodePromptTimeoutError) {
653
+ this.logPromptRequest("warn", {
654
+ elapsedMs: error.data.elapsedMs,
655
+ messageId: error.data.messageId,
656
+ sessionId: error.data.sessionId,
657
+ stage: error.data.stage,
658
+ timeoutMs: error.data.timeoutMs
659
+ }, "OpenCode prompt request timed out");
660
+ return;
661
+ }
662
+ this.logPromptRequest("warn", {
663
+ error,
664
+ messageId: input.messageId ?? void 0,
665
+ sessionId: input.sessionId,
666
+ stage: input.stage,
667
+ timeoutMs: input.timeoutMs
668
+ }, "OpenCode prompt request failed");
669
+ }
670
+ logPromptRequest(level, extra, message) {
671
+ const log = this.client.app?.log;
672
+ if (typeof log !== "function") return;
673
+ log.call(this.client.app, {
674
+ service: PROMPT_LOG_SERVICE,
675
+ level,
676
+ message,
677
+ extra
678
+ }).catch(() => void 0);
679
+ }
528
680
  };
529
681
  function createOpenCodeClientFromSdkClient(client, fetchFn = fetch) {
530
682
  return new OpenCodeClient(void 0, client, fetchFn);
@@ -696,6 +848,13 @@ function extractMessageId(message) {
696
848
  function delay(ms) {
697
849
  return new Promise((resolve) => setTimeout(resolve, ms));
698
850
  }
851
+ function createOpenCodePromptTimeoutError(input) {
852
+ return new OpenCodePromptTimeoutError({
853
+ ...input,
854
+ elapsedMs: input.elapsedMs ?? input.timeoutMs,
855
+ message: input.message ?? "The OpenCode host did not finish this request in time."
856
+ });
857
+ }
699
858
  function getPromptMessagePollDelayMs(attempt) {
700
859
  return PROMPT_MESSAGE_POLL_INITIAL_DELAYS_MS[attempt] ?? PROMPT_MESSAGE_POLL_INTERVAL_MS;
701
860
  }
@@ -2255,7 +2414,6 @@ var SUPPORTED_BOT_LANGUAGES = ["en", "zh-CN"];
2255
2414
  var EN_BOT_COPY = {
2256
2415
  commands: {
2257
2416
  start: "Welcome and quick start",
2258
- help: "Show commands and examples",
2259
2417
  status: "Show system status",
2260
2418
  new: "Create a new session",
2261
2419
  agents: "View and switch agents",
@@ -2269,37 +2427,11 @@ var EN_BOT_COPY = {
2269
2427
  "",
2270
2428
  "Talk to your OpenCode server from Telegram.",
2271
2429
  "",
2272
- "## What you can send",
2273
- "- Text prompts",
2274
- "- Images with an optional caption",
2275
- "- Voice messages (requires OpenRouter voice transcription)",
2276
- "",
2277
2430
  "## Quick start",
2278
2431
  "1. Run `/status` to confirm the server is ready.",
2279
2432
  "2. Run `/new [title]` to create a fresh session.",
2280
- "3. Send a text, image, or voice message.",
2281
2433
  "",
2282
- "Use `/help` to see the full command list and examples."
2283
- ] },
2284
- help: { lines: [
2285
- "# Help",
2286
- "",
2287
- "Use this bot to chat with OpenCode from Telegram.",
2288
- "",
2289
- "## Commands",
2290
- "- `/status` Check server, workspace, MCP, and LSP status",
2291
- "- `/new [title]` Create a new session",
2292
- "- `/sessions` View, switch, or rename sessions",
2293
- "- `/agents` View and switch agents",
2294
- "- `/model` View and switch models and reasoning levels",
2295
- "- `/language` Switch the bot display language",
2296
- "- `/cancel` Cancel session rename or abort the running request",
2297
- "",
2298
- "## Examples",
2299
- "- `/new bug triage`",
2300
- "- Send a plain text message directly",
2301
- "- Send an image with a caption",
2302
- "- Send a voice message if OpenRouter voice transcription is configured"
2434
+ "Send a text, image, or voice message directly."
2303
2435
  ] },
2304
2436
  systemStatus: { title: "System Status" },
2305
2437
  common: {
@@ -2336,6 +2468,7 @@ var EN_BOT_COPY = {
2336
2468
  unexpected: "Unexpected error.",
2337
2469
  providerAuth: "Provider authentication failed.",
2338
2470
  requestAborted: "Request was aborted.",
2471
+ promptTimeout: "OpenCode request timed out.",
2339
2472
  structuredOutput: "Structured output validation failed.",
2340
2473
  voiceNotConfigured: "Voice transcription is not configured.",
2341
2474
  voiceDownload: "Failed to download the Telegram voice file.",
@@ -2490,7 +2623,6 @@ var EN_BOT_COPY = {
2490
2623
  var ZH_CN_BOT_COPY = {
2491
2624
  commands: {
2492
2625
  start: "查看欢迎与快速开始",
2493
- help: "查看命令说明与示例",
2494
2626
  status: "查看系统状态",
2495
2627
  new: "新建会话",
2496
2628
  agents: "查看并切换代理",
@@ -2504,37 +2636,11 @@ var ZH_CN_BOT_COPY = {
2504
2636
  "",
2505
2637
  "通过 Telegram 直接和 OpenCode 服务对话。",
2506
2638
  "",
2507
- "## 支持的输入",
2508
- "- 文本消息",
2509
- "- 图片 (可附带 caption)",
2510
- "- 语音消息 (需先配置 OpenRouter 语音转写)",
2511
- "",
2512
2639
  "## 快速开始",
2513
2640
  "1. 先运行 `/status` 确认服务状态正常。",
2514
2641
  "2. 运行 `/new [title]` 创建一个新会话。",
2515
- "3. 直接发送文本、图片或语音消息。",
2516
- "",
2517
- "更多命令和示例请查看 `/help`。"
2518
- ] },
2519
- help: { lines: [
2520
- "# 帮助",
2521
- "",
2522
- "使用这个机器人可以通过 Telegram 与 OpenCode 对话。",
2523
- "",
2524
- "## 命令",
2525
- "- `/status` 查看服务、工作区、MCP 和 LSP 状态",
2526
- "- `/new [title]` 创建一个新会话",
2527
- "- `/sessions` 查看、切换或重命名会话",
2528
- "- `/agents` 查看并切换代理",
2529
- "- `/model` 查看并切换模型与推理级别",
2530
- "- `/language` 切换机器人的显示语言",
2531
- "- `/cancel` 取消会话重命名或中止当前请求",
2532
2642
  "",
2533
- "## 示例",
2534
- "- `/new bug triage`",
2535
- "- 直接发送一条文本消息",
2536
- "- 发送一张带 caption 的图片",
2537
- "- 如果已配置 OpenRouter 语音转写,直接发送语音消息"
2643
+ "直接发送文本、图片或语音消息即可。"
2538
2644
  ] },
2539
2645
  systemStatus: { title: "系统状态" },
2540
2646
  common: {
@@ -2571,6 +2677,7 @@ var ZH_CN_BOT_COPY = {
2571
2677
  unexpected: "发生未知错误。",
2572
2678
  providerAuth: "Provider 认证失败。",
2573
2679
  requestAborted: "请求已中止。",
2680
+ promptTimeout: "OpenCode 响应超时。",
2574
2681
  structuredOutput: "结构化输出校验失败。",
2575
2682
  voiceNotConfigured: "未配置语音转写服务。",
2576
2683
  voiceDownload: "下载 Telegram 语音文件失败。",
@@ -2747,10 +2854,6 @@ function getTelegramCommands(language = "en") {
2747
2854
  command: "start",
2748
2855
  description: copy.commands.start
2749
2856
  },
2750
- {
2751
- command: "help",
2752
- description: copy.commands.help
2753
- },
2754
2857
  {
2755
2858
  command: "status",
2756
2859
  description: copy.commands.status
@@ -2918,6 +3021,10 @@ function normalizeError(error, copy) {
2918
3021
  message: copy.errors.requestAborted,
2919
3022
  cause: extractMessage(error.data) ?? null
2920
3023
  };
3024
+ if (isNamedError(error, "OpenCodePromptTimeoutError")) return {
3025
+ message: copy.errors.promptTimeout,
3026
+ cause: null
3027
+ };
2921
3028
  if (isNamedError(error, "StructuredOutputError")) return {
2922
3029
  message: copy.errors.structuredOutput,
2923
3030
  cause: joinNonEmptyParts([extractMessage(error.data), extractRetries(error.data)])
@@ -3579,6 +3686,112 @@ function registerCancelCommand(bot, dependencies) {
3579
3686
  });
3580
3687
  }
3581
3688
  //#endregion
3689
+ //#region src/bot/commands/language.ts
3690
+ async function handleLanguageCommand(ctx, dependencies) {
3691
+ const language = await getChatLanguage(dependencies.sessionRepo, ctx.chat.id);
3692
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3693
+ try {
3694
+ await syncTelegramCommandsForChat(ctx.api, ctx.chat.id, language);
3695
+ await ctx.reply(presentLanguageMessage(language, copy), { reply_markup: buildLanguageKeyboard(language, copy) });
3696
+ } catch (error) {
3697
+ dependencies.logger.error({ error }, "failed to show language options");
3698
+ await ctx.reply(presentError(error, copy));
3699
+ }
3700
+ }
3701
+ async function switchLanguageForChat(api, chatId, language, dependencies) {
3702
+ const currentCopy = await getChatCopy(dependencies.sessionRepo, chatId);
3703
+ if (!isBotLanguage(language)) return {
3704
+ found: false,
3705
+ copy: currentCopy
3706
+ };
3707
+ await setChatLanguage(dependencies.sessionRepo, chatId, language);
3708
+ await syncTelegramCommandsForChat(api, chatId, language);
3709
+ return {
3710
+ found: true,
3711
+ copy: await getChatCopy(dependencies.sessionRepo, chatId),
3712
+ language
3713
+ };
3714
+ }
3715
+ async function presentLanguageSwitchForChat(chatId, api, language, dependencies) {
3716
+ const result = await switchLanguageForChat(api, chatId, language, dependencies);
3717
+ if (!result.found) return {
3718
+ found: false,
3719
+ copy: result.copy,
3720
+ text: result.copy.language.expired,
3721
+ keyboard: buildLanguageKeyboard(await getChatLanguage(dependencies.sessionRepo, chatId), result.copy)
3722
+ };
3723
+ return {
3724
+ found: true,
3725
+ copy: result.copy,
3726
+ text: presentLanguageSwitchMessage(result.language, result.copy),
3727
+ keyboard: buildLanguageKeyboard(result.language, result.copy)
3728
+ };
3729
+ }
3730
+ function registerLanguageCommand(bot, dependencies) {
3731
+ bot.command("language", async (ctx) => {
3732
+ await handleLanguageCommand(ctx, dependencies);
3733
+ });
3734
+ }
3735
+ //#endregion
3736
+ //#region src/bot/commands/models.ts
3737
+ async function handleModelsCommand(ctx, dependencies) {
3738
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3739
+ try {
3740
+ const result = await dependencies.listModelsUseCase.execute({ chatId: ctx.chat.id });
3741
+ if (result.models.length === 0) {
3742
+ await ctx.reply(copy.models.none);
3743
+ return;
3744
+ }
3745
+ const { keyboard, page } = buildModelsKeyboard(result.models, 0, copy);
3746
+ await ctx.reply(presentModelsMessage({
3747
+ currentModelId: result.currentModelId,
3748
+ currentModelProviderId: result.currentModelProviderId,
3749
+ currentModelVariant: result.currentModelVariant,
3750
+ models: result.models,
3751
+ page: page.page
3752
+ }, copy), { reply_markup: keyboard });
3753
+ } catch (error) {
3754
+ dependencies.logger.error({ error }, "failed to list models");
3755
+ await ctx.reply(presentError(error, copy));
3756
+ }
3757
+ }
3758
+ function registerModelsCommand(bot, dependencies) {
3759
+ bot.command(["model", "models"], async (ctx) => {
3760
+ await handleModelsCommand(ctx, dependencies);
3761
+ });
3762
+ }
3763
+ //#endregion
3764
+ //#region src/bot/commands/new.ts
3765
+ async function handleNewCommand(ctx, dependencies) {
3766
+ const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3767
+ try {
3768
+ const title = extractSessionTitle(ctx);
3769
+ const result = await dependencies.createSessionUseCase.execute({
3770
+ chatId: ctx.chat.id,
3771
+ title
3772
+ });
3773
+ await ctx.reply(presentSessionCreatedMessage(result.session, copy));
3774
+ } catch (error) {
3775
+ dependencies.logger.error({ error }, "failed to create new session");
3776
+ await ctx.reply(presentError(error, copy));
3777
+ }
3778
+ }
3779
+ function registerNewCommand(bot, dependencies) {
3780
+ bot.command("new", async (ctx) => {
3781
+ await handleNewCommand(ctx, dependencies);
3782
+ });
3783
+ }
3784
+ function extractSessionTitle(ctx) {
3785
+ if (typeof ctx.match === "string") {
3786
+ const title = ctx.match.trim();
3787
+ return title ? title : null;
3788
+ }
3789
+ const messageText = ctx.message?.text?.trim();
3790
+ if (!messageText) return null;
3791
+ const title = messageText.match(/^\/new(?:@\S+)?(?:\s+([\s\S]*))?$/i)?.[1]?.trim();
3792
+ return title ? title : null;
3793
+ }
3794
+ //#endregion
3582
3795
  //#region src/services/telegram/telegram-format.ts
3583
3796
  var MAX_TELEGRAM_MESSAGE_LENGTH = 4096;
3584
3797
  var TRUNCATED_SUFFIX = "...";
@@ -3912,142 +4125,6 @@ function escapeLinkDestination(url) {
3912
4125
  return url.replace(/\\/g, "\\\\").replace(/\)/g, "\\)").replace(/\(/g, "\\(");
3913
4126
  }
3914
4127
  //#endregion
3915
- //#region src/bot/presenters/static.presenter.ts
3916
- function presentStartMarkdownMessage(copy = BOT_COPY) {
3917
- return copy.start.lines.join("\n");
3918
- }
3919
- function presentHelpMarkdownMessage(copy = BOT_COPY) {
3920
- return copy.help.lines.join("\n");
3921
- }
3922
- //#endregion
3923
- //#region src/bot/commands/help.ts
3924
- async function handleHelpCommand(ctx, dependencies) {
3925
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
3926
- const reply = buildTelegramStaticReply(presentHelpMarkdownMessage(copy));
3927
- try {
3928
- await ctx.reply(reply.preferred.text, reply.preferred.options);
3929
- } catch (error) {
3930
- if (reply.preferred.options) {
3931
- dependencies.logger.error({ error }, "failed to send help markdown reply, falling back to plain text");
3932
- await ctx.reply(reply.fallback.text);
3933
- return;
3934
- }
3935
- dependencies.logger.error({ error }, "failed to show help message");
3936
- await ctx.reply(presentError(error, copy));
3937
- }
3938
- }
3939
- function registerHelpCommand(bot, dependencies) {
3940
- bot.command("help", async (ctx) => {
3941
- await handleHelpCommand(ctx, dependencies);
3942
- });
3943
- }
3944
- //#endregion
3945
- //#region src/bot/commands/language.ts
3946
- async function handleLanguageCommand(ctx, dependencies) {
3947
- const language = await getChatLanguage(dependencies.sessionRepo, ctx.chat.id);
3948
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3949
- try {
3950
- await syncTelegramCommandsForChat(ctx.api, ctx.chat.id, language);
3951
- await ctx.reply(presentLanguageMessage(language, copy), { reply_markup: buildLanguageKeyboard(language, copy) });
3952
- } catch (error) {
3953
- dependencies.logger.error({ error }, "failed to show language options");
3954
- await ctx.reply(presentError(error, copy));
3955
- }
3956
- }
3957
- async function switchLanguageForChat(api, chatId, language, dependencies) {
3958
- const currentCopy = await getChatCopy(dependencies.sessionRepo, chatId);
3959
- if (!isBotLanguage(language)) return {
3960
- found: false,
3961
- copy: currentCopy
3962
- };
3963
- await setChatLanguage(dependencies.sessionRepo, chatId, language);
3964
- await syncTelegramCommandsForChat(api, chatId, language);
3965
- return {
3966
- found: true,
3967
- copy: await getChatCopy(dependencies.sessionRepo, chatId),
3968
- language
3969
- };
3970
- }
3971
- async function presentLanguageSwitchForChat(chatId, api, language, dependencies) {
3972
- const result = await switchLanguageForChat(api, chatId, language, dependencies);
3973
- if (!result.found) return {
3974
- found: false,
3975
- copy: result.copy,
3976
- text: result.copy.language.expired,
3977
- keyboard: buildLanguageKeyboard(await getChatLanguage(dependencies.sessionRepo, chatId), result.copy)
3978
- };
3979
- return {
3980
- found: true,
3981
- copy: result.copy,
3982
- text: presentLanguageSwitchMessage(result.language, result.copy),
3983
- keyboard: buildLanguageKeyboard(result.language, result.copy)
3984
- };
3985
- }
3986
- function registerLanguageCommand(bot, dependencies) {
3987
- bot.command("language", async (ctx) => {
3988
- await handleLanguageCommand(ctx, dependencies);
3989
- });
3990
- }
3991
- //#endregion
3992
- //#region src/bot/commands/models.ts
3993
- async function handleModelsCommand(ctx, dependencies) {
3994
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
3995
- try {
3996
- const result = await dependencies.listModelsUseCase.execute({ chatId: ctx.chat.id });
3997
- if (result.models.length === 0) {
3998
- await ctx.reply(copy.models.none);
3999
- return;
4000
- }
4001
- const { keyboard, page } = buildModelsKeyboard(result.models, 0, copy);
4002
- await ctx.reply(presentModelsMessage({
4003
- currentModelId: result.currentModelId,
4004
- currentModelProviderId: result.currentModelProviderId,
4005
- currentModelVariant: result.currentModelVariant,
4006
- models: result.models,
4007
- page: page.page
4008
- }, copy), { reply_markup: keyboard });
4009
- } catch (error) {
4010
- dependencies.logger.error({ error }, "failed to list models");
4011
- await ctx.reply(presentError(error, copy));
4012
- }
4013
- }
4014
- function registerModelsCommand(bot, dependencies) {
4015
- bot.command(["model", "models"], async (ctx) => {
4016
- await handleModelsCommand(ctx, dependencies);
4017
- });
4018
- }
4019
- //#endregion
4020
- //#region src/bot/commands/new.ts
4021
- async function handleNewCommand(ctx, dependencies) {
4022
- const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4023
- try {
4024
- const title = extractSessionTitle(ctx);
4025
- const result = await dependencies.createSessionUseCase.execute({
4026
- chatId: ctx.chat.id,
4027
- title
4028
- });
4029
- await ctx.reply(presentSessionCreatedMessage(result.session, copy));
4030
- } catch (error) {
4031
- dependencies.logger.error({ error }, "failed to create new session");
4032
- await ctx.reply(presentError(error, copy));
4033
- }
4034
- }
4035
- function registerNewCommand(bot, dependencies) {
4036
- bot.command("new", async (ctx) => {
4037
- await handleNewCommand(ctx, dependencies);
4038
- });
4039
- }
4040
- function extractSessionTitle(ctx) {
4041
- if (typeof ctx.match === "string") {
4042
- const title = ctx.match.trim();
4043
- return title ? title : null;
4044
- }
4045
- const messageText = ctx.message?.text?.trim();
4046
- if (!messageText) return null;
4047
- const title = messageText.match(/^\/new(?:@\S+)?(?:\s+([\s\S]*))?$/i)?.[1]?.trim();
4048
- return title ? title : null;
4049
- }
4050
- //#endregion
4051
4128
  //#region src/bot/commands/status.ts
4052
4129
  async function handleStatusCommand(ctx, dependencies) {
4053
4130
  const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
@@ -4088,6 +4165,11 @@ function registerSessionsCommand(bot, dependencies) {
4088
4165
  });
4089
4166
  }
4090
4167
  //#endregion
4168
+ //#region src/bot/presenters/static.presenter.ts
4169
+ function presentStartMarkdownMessage(copy = BOT_COPY) {
4170
+ return copy.start.lines.join("\n");
4171
+ }
4172
+ //#endregion
4091
4173
  //#region src/bot/commands/start.ts
4092
4174
  async function handleStartCommand(ctx, dependencies) {
4093
4175
  const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
@@ -4604,7 +4686,6 @@ function registerBot(bot, container, options) {
4604
4686
  bot.use(createLoggingMiddleware(container.logger));
4605
4687
  bot.use(createAuthMiddleware(options.telegramAllowedChatIds));
4606
4688
  registerStartCommand(bot, container);
4607
- registerHelpCommand(bot, container);
4608
4689
  registerStatusCommand(bot, container);
4609
4690
  registerNewCommand(bot, container);
4610
4691
  registerAgentsCommand(bot, container);