opencode-tbot 0.1.0 โ†’ 0.1.2

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
@@ -1,12 +1,15 @@
1
- import { i as preparePluginConfiguration, s as loadAppConfig } from "./assets/plugin-config-Crgl_PZz.js";
2
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
- import { basename, dirname, extname } from "node:path";
1
+ import { i as preparePluginConfiguration, s as loadAppConfig } from "./assets/plugin-config-BYsYAzvx.js";
2
+ import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, extname, join } from "node:path";
4
+ import { parse, printParseErrorCode } from "jsonc-parser";
4
5
  import { z } from "zod";
5
6
  import { OpenRouter } from "@openrouter/sdk";
6
7
  import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
7
8
  import { randomUUID } from "node:crypto";
8
9
  import { run } from "@grammyjs/runner";
9
10
  import { Bot, InlineKeyboard } from "grammy";
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { fileURLToPath } from "node:url";
10
13
  //#region src/infra/utils/redact.ts
11
14
  var REDACTED = "[REDACTED]";
12
15
  var DEFAULT_PREVIEW_LENGTH = 160;
@@ -243,7 +246,14 @@ var OpenCodeClient = class {
243
246
  this.fetchFn = fetchFn;
244
247
  }
245
248
  async getHealth() {
246
- return unwrapSdkData(await this.client.global.health(SDK_OPTIONS));
249
+ const healthEndpoint = this.client.global?.health;
250
+ if (typeof healthEndpoint === "function") return unwrapSdkData(await healthEndpoint.call(this.client.global, SDK_OPTIONS));
251
+ const rawClient = getRawSdkClient(this.client);
252
+ if (!rawClient) throw new Error("OpenCode SDK client does not expose a compatible health endpoint.");
253
+ return unwrapSdkData(await rawClient.get({
254
+ url: "/global/health",
255
+ ...SDK_OPTIONS
256
+ }));
247
257
  }
248
258
  async abortSession(sessionId) {
249
259
  return unwrapSdkData(await this.client.session.abort({ sessionID: sessionId }, SDK_OPTIONS));
@@ -490,6 +500,9 @@ function unwrapSdkData(response) {
490
500
  if (response && typeof response === "object" && "data" in response) return response.data;
491
501
  return response;
492
502
  }
503
+ function getRawSdkClient(client) {
504
+ return client.client ?? client._client ?? null;
505
+ }
493
506
  async function resolveProviderAvailability(config, fetchFn) {
494
507
  const configuredProviders = Object.entries(config.provider ?? {});
495
508
  const availabilityEntries = await Promise.all(configuredProviders.map(async ([providerId, providerConfig]) => [providerId, await fetchProviderAvailableModelIds(providerConfig, fetchFn)]));
@@ -582,7 +595,7 @@ async function readStateFile(filePath, createDefaultState) {
582
595
  const content = await readFile(filePath, "utf8");
583
596
  return JSON.parse(content);
584
597
  } catch (error) {
585
- if (isMissingFileError(error)) return createDefaultState();
598
+ if (isMissingFileError$1(error)) return createDefaultState();
586
599
  throw error;
587
600
  }
588
601
  }
@@ -595,7 +608,7 @@ async function writeStateFile(filePath, state) {
595
608
  function cloneState(state) {
596
609
  return JSON.parse(JSON.stringify(state));
597
610
  }
598
- function isMissingFileError(error) {
611
+ function isMissingFileError$1(error) {
599
612
  return error instanceof Error && "code" in error && error.code === "ENOENT";
600
613
  }
601
614
  //#endregion
@@ -949,11 +962,13 @@ var GetPathUseCase = class {
949
962
  //#endregion
950
963
  //#region src/use-cases/get-status.usecase.ts
951
964
  var GetStatusUseCase = class {
952
- constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase) {
965
+ constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo) {
953
966
  this.getHealthUseCase = getHealthUseCase;
954
967
  this.getPathUseCase = getPathUseCase;
955
968
  this.listLspUseCase = listLspUseCase;
956
969
  this.listMcpUseCase = listMcpUseCase;
970
+ this.listSessionsUseCase = listSessionsUseCase;
971
+ this.sessionRepo = sessionRepo;
957
972
  }
958
973
  async execute(input) {
959
974
  const [health, path, lsp, mcp] = await Promise.allSettled([
@@ -962,9 +977,13 @@ var GetStatusUseCase = class {
962
977
  this.listLspUseCase.execute({ chatId: input.chatId }),
963
978
  this.listMcpUseCase.execute({ chatId: input.chatId })
964
979
  ]);
980
+ const pathResult = mapSettledResult(path);
981
+ const [plugins, workspace] = await Promise.all([loadConfiguredPluginsResult(pathResult), loadWorkspaceStatusResult(input.chatId, pathResult, this.listSessionsUseCase, this.sessionRepo)]);
965
982
  return {
966
983
  health: mapSettledResult(health),
967
- path: mapSettledResult(path),
984
+ path: pathResult,
985
+ plugins,
986
+ workspace,
968
987
  lsp: mapSettledResult(lsp),
969
988
  mcp: mapSettledResult(mcp)
970
989
  };
@@ -980,6 +999,88 @@ function mapSettledResult(result) {
980
999
  status: "error"
981
1000
  };
982
1001
  }
1002
+ async function loadConfiguredPluginsResult(path) {
1003
+ if (path.status === "error") return {
1004
+ error: path.error,
1005
+ status: "error"
1006
+ };
1007
+ try {
1008
+ return {
1009
+ data: await loadConfiguredPlugins(path.data.config),
1010
+ status: "ok"
1011
+ };
1012
+ } catch (error) {
1013
+ return {
1014
+ error,
1015
+ status: "error"
1016
+ };
1017
+ }
1018
+ }
1019
+ async function loadConfiguredPlugins(configFilePath) {
1020
+ const resolvedConfigFilePath = await resolveOpenCodeConfigFilePath(configFilePath);
1021
+ let content;
1022
+ try {
1023
+ content = await readFile(resolvedConfigFilePath, "utf8");
1024
+ } catch (error) {
1025
+ if (isMissingFileError(error)) return {
1026
+ configFilePath: resolvedConfigFilePath,
1027
+ plugins: []
1028
+ };
1029
+ throw error;
1030
+ }
1031
+ const parseErrors = [];
1032
+ const parsed = parse(content, parseErrors, { allowTrailingComma: true });
1033
+ if (parseErrors.length > 0) {
1034
+ const errorSummary = parseErrors.map((error) => printParseErrorCode(error.error)).join(", ");
1035
+ throw new Error(`Failed to parse ${resolvedConfigFilePath}: ${errorSummary}`);
1036
+ }
1037
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {
1038
+ configFilePath: resolvedConfigFilePath,
1039
+ plugins: []
1040
+ };
1041
+ const pluginSpecs = Array.isArray(parsed.plugin) ? parsed.plugin : [];
1042
+ return {
1043
+ configFilePath: resolvedConfigFilePath,
1044
+ plugins: [...new Set(pluginSpecs.filter((value) => typeof value === "string").map((value) => value.trim()).filter((value) => value.length > 0))]
1045
+ };
1046
+ }
1047
+ async function loadWorkspaceStatusResult(chatId, path, listSessionsUseCase, sessionRepo) {
1048
+ if (path.status === "error") return {
1049
+ error: path.error,
1050
+ status: "error"
1051
+ };
1052
+ const binding = await sessionRepo.getByChatId(chatId);
1053
+ let currentProject = path.data.directory;
1054
+ let currentSession = binding?.sessionId ?? null;
1055
+ try {
1056
+ const sessions = await listSessionsUseCase.execute({ chatId });
1057
+ currentProject = sessions.currentDirectory;
1058
+ currentSession = sessions.currentSessionId ? formatSessionStatusLabel(sessions.sessions.find((session) => session.id === sessions.currentSessionId) ?? null, sessions.currentSessionId) : null;
1059
+ } catch {}
1060
+ return {
1061
+ data: {
1062
+ currentProject,
1063
+ currentSession
1064
+ },
1065
+ status: "ok"
1066
+ };
1067
+ }
1068
+ function formatSessionStatusLabel(session, fallbackId) {
1069
+ if (!session) return fallbackId;
1070
+ const title = session.title.trim() || session.slug || session.id;
1071
+ return title === session.slug ? title : `${title} (${session.slug})`;
1072
+ }
1073
+ async function resolveOpenCodeConfigFilePath(configPath) {
1074
+ try {
1075
+ return (await stat(configPath)).isDirectory() ? join(configPath, "opencode.json") : configPath;
1076
+ } catch (error) {
1077
+ if (isMissingFileError(error)) return configPath;
1078
+ throw error;
1079
+ }
1080
+ }
1081
+ function isMissingFileError(error) {
1082
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
1083
+ }
983
1084
  function resolveSelectedAgent(agents, requestedAgentName) {
984
1085
  const visibleAgents = agents.filter((agent) => !agent.hidden);
985
1086
  if (requestedAgentName) {
@@ -1499,9 +1600,9 @@ function createContainer(config, opencodeClient, logger) {
1499
1600
  const listAgentsUseCase = new ListAgentsUseCase(sessionRepo, opencodeClient);
1500
1601
  const listLspUseCase = new ListLspUseCase(sessionRepo, opencodeClient);
1501
1602
  const listMcpUseCase = new ListMcpUseCase(sessionRepo, opencodeClient);
1502
- const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase);
1503
- const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
1504
1603
  const listSessionsUseCase = new ListSessionsUseCase(sessionRepo, opencodeClient);
1604
+ const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo);
1605
+ const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
1505
1606
  const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, logger);
1506
1607
  const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger, foregroundSessionTracker);
1507
1608
  const switchAgentUseCase = new SwitchAgentUseCase(sessionRepo, opencodeClient, logger);
@@ -2504,6 +2605,23 @@ function stringifyUnknown(value) {
2504
2605
  }
2505
2606
  }
2506
2607
  //#endregion
2608
+ //#region src/app/package-info.ts
2609
+ var OPENCODE_TBOT_VERSION = resolvePackageVersion();
2610
+ function resolvePackageVersion() {
2611
+ let directory = dirname(fileURLToPath(import.meta.url));
2612
+ while (true) {
2613
+ const packageFilePath = join(directory, "package.json");
2614
+ if (existsSync(packageFilePath)) try {
2615
+ const parsed = JSON.parse(readFileSync(packageFilePath, "utf8"));
2616
+ if (typeof parsed.version === "string" && parsed.version.trim().length > 0) return parsed.version;
2617
+ } catch {}
2618
+ const parentDirectory = dirname(directory);
2619
+ if (parentDirectory === directory) break;
2620
+ directory = parentDirectory;
2621
+ }
2622
+ return "unknown";
2623
+ }
2624
+ //#endregion
2507
2625
  //#region src/bot/presenters/message.presenter.ts
2508
2626
  var VARIANT_ORDER = [
2509
2627
  "minimal",
@@ -2517,24 +2635,22 @@ var VARIANT_ORDER = [
2517
2635
  function presentStatusMessage(input, copy = BOT_COPY) {
2518
2636
  const layout = getStatusLayoutCopy(copy);
2519
2637
  const sections = [
2520
- presentStatusPlainSection(layout.serverTitle, presentStatusPlainServerLines(input, copy, layout)),
2638
+ presentStatusPlainSection(layout.overviewTitle, presentStatusPlainOverviewLines(input, copy, layout)),
2521
2639
  presentStatusPlainSection(layout.workspaceTitle, presentStatusPlainWorkspaceLines(input, copy, layout)),
2640
+ presentStatusPlainSection(layout.pluginsTitle, presentStatusPlainPluginLines(input, copy, layout)),
2522
2641
  presentStatusPlainSection(layout.mcpTitle, presentStatusPlainMcpLines(input, copy, layout)),
2523
- presentStatusPlainSection(layout.lspTitle, presentStatusPlainLspLines(input, copy, layout)),
2524
- layout.divider,
2525
- `${layout.lastUpdatedLabel}: ${formatStatusDate(/* @__PURE__ */ new Date())}`
2642
+ presentStatusPlainSection(layout.lspTitle, presentStatusPlainLspLines(input, copy, layout))
2526
2643
  ];
2527
2644
  return presentStatusSections(layout.pageTitle, sections);
2528
2645
  }
2529
2646
  function presentStatusMarkdownMessage(input, copy = BOT_COPY) {
2530
2647
  const layout = getStatusLayoutCopy(copy);
2531
2648
  const sections = [
2532
- presentStatusMarkdownSection(layout.serverTitle, presentStatusMarkdownServerLines(input, copy, layout)),
2649
+ presentStatusMarkdownSection(layout.overviewTitle, presentStatusMarkdownOverviewLines(input, copy, layout)),
2533
2650
  presentStatusMarkdownSection(layout.workspaceTitle, presentStatusMarkdownWorkspaceLines(input, copy, layout)),
2651
+ presentStatusMarkdownSection(layout.pluginsTitle, presentStatusMarkdownPluginLines(input, copy, layout)),
2534
2652
  presentStatusMarkdownSection(layout.mcpTitle, presentStatusMarkdownMcpLines(input, copy, layout)),
2535
- presentStatusMarkdownSection(layout.lspTitle, presentStatusMarkdownLspLines(input, copy, layout)),
2536
- layout.divider,
2537
- `_${layout.lastUpdatedLabel}: ${formatStatusDate(/* @__PURE__ */ new Date())}_`
2653
+ presentStatusMarkdownSection(layout.lspTitle, presentStatusMarkdownLspLines(input, copy, layout))
2538
2654
  ];
2539
2655
  return presentStatusSections(`# ${layout.pageTitle}`, sections);
2540
2656
  }
@@ -2551,205 +2667,213 @@ function presentStatusPlainSection(title, lines) {
2551
2667
  function presentStatusMarkdownSection(title, lines) {
2552
2668
  return [`## ${title}`, ...lines].join("\n");
2553
2669
  }
2554
- function presentStatusPlainServerLines(input, copy, layout) {
2555
- if (input.health.status === "error") return presentStatusPlainErrorLines(input.health.error, copy, layout);
2556
- return [presentPlainStatusBullet(layout.statusLabel, formatHealthBadge(input.health.data.healthy, layout)), presentPlainStatusBullet(layout.versionLabel, input.health.data.version)];
2670
+ function presentStatusPlainOverviewLines(input, copy, layout) {
2671
+ const lines = [presentPlainStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout))];
2672
+ if (input.health.status === "error") return [
2673
+ ...lines,
2674
+ ...presentStatusPlainErrorDetailLines(input.health.error, copy, layout),
2675
+ presentPlainStatusBullet(layout.tbotVersionLabel, OPENCODE_TBOT_VERSION)
2676
+ ];
2677
+ return [
2678
+ ...lines,
2679
+ presentPlainStatusBullet(layout.openCodeVersionLabel, input.health.data.version),
2680
+ presentPlainStatusBullet(layout.tbotVersionLabel, OPENCODE_TBOT_VERSION)
2681
+ ];
2557
2682
  }
2558
- function presentStatusMarkdownServerLines(input, copy, layout) {
2559
- if (input.health.status === "error") return presentStatusMarkdownErrorLines(input.health.error, copy, layout);
2560
- return [presentMarkdownStatusBullet(layout.statusLabel, formatHealthBadge(input.health.data.healthy, layout)), presentMarkdownStatusBullet(layout.versionLabel, input.health.data.version)];
2683
+ function presentStatusMarkdownOverviewLines(input, copy, layout) {
2684
+ const lines = [presentMarkdownStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout))];
2685
+ if (input.health.status === "error") return [
2686
+ ...lines,
2687
+ ...presentStatusMarkdownErrorDetailLines(input.health.error, copy, layout),
2688
+ presentMarkdownStatusBullet(layout.tbotVersionLabel, OPENCODE_TBOT_VERSION)
2689
+ ];
2690
+ return [
2691
+ ...lines,
2692
+ presentMarkdownStatusBullet(layout.openCodeVersionLabel, input.health.data.version),
2693
+ presentMarkdownStatusBullet(layout.tbotVersionLabel, OPENCODE_TBOT_VERSION)
2694
+ ];
2561
2695
  }
2562
2696
  function presentStatusPlainWorkspaceLines(input, copy, layout) {
2563
- if (input.path.status === "error") return presentStatusPlainErrorLines(input.path.error, copy, layout);
2564
- return [presentPlainStatusBullet(layout.currentDirectoryLabel, input.path.data.directory)];
2697
+ if (input.workspace.status === "error") return presentStatusPlainErrorLines(input.workspace.error, copy, layout);
2698
+ return [presentPlainStatusBullet(layout.currentProjectLabel, input.workspace.data.currentProject), presentPlainStatusBullet(layout.currentSessionLabel, input.workspace.data.currentSession ?? copy.common.notSelected)];
2565
2699
  }
2566
2700
  function presentStatusMarkdownWorkspaceLines(input, copy, layout) {
2567
- if (input.path.status === "error") return presentStatusMarkdownErrorLines(input.path.error, copy, layout);
2568
- return [presentMarkdownStatusBullet(layout.currentDirectoryLabel, input.path.data.directory, { codeValue: true })];
2701
+ if (input.workspace.status === "error") return presentStatusMarkdownErrorLines(input.workspace.error, copy, layout);
2702
+ return [presentMarkdownStatusBullet(layout.currentProjectLabel, input.workspace.data.currentProject, { codeValue: true }), presentMarkdownStatusBullet(layout.currentSessionLabel, input.workspace.data.currentSession ?? copy.common.notSelected)];
2703
+ }
2704
+ function presentStatusPlainPluginLines(input, copy, layout) {
2705
+ if (input.plugins.status === "error") return presentStatusPlainErrorLines(input.plugins.error, copy, layout);
2706
+ if (input.plugins.data.plugins.length === 0) return [...presentPlainEmptyStatusLines(layout.noPluginsMessage, layout)];
2707
+ return [`- ${layout.configuredPluginsLabel}:`, ...input.plugins.data.plugins.map((plugin) => ` - ${plugin}`)];
2708
+ }
2709
+ function presentStatusMarkdownPluginLines(input, copy, layout) {
2710
+ if (input.plugins.status === "error") return presentStatusMarkdownErrorLines(input.plugins.error, copy, layout);
2711
+ if (input.plugins.data.plugins.length === 0) return [...presentMarkdownEmptyStatusLines(layout.noPluginsMessage, layout)];
2712
+ return [`- **${layout.configuredPluginsLabel}:**`, ...input.plugins.data.plugins.map((plugin) => ` - \`${plugin}\``)];
2569
2713
  }
2570
2714
  function presentStatusPlainLspLines(input, copy, layout) {
2571
- return buildPlainStatusTable([
2572
- layout.languageLabel,
2573
- layout.statusLabel,
2574
- layout.rootLabel
2575
- ], buildLspStatusRows(input, copy, layout));
2715
+ if (input.lsp.status === "error") return presentStatusPlainErrorLines(input.lsp.error, copy, layout);
2716
+ if (input.lsp.data.statuses.length === 0) return presentPlainEmptyStatusLines(copy.lsp.none, layout);
2717
+ return input.lsp.data.statuses.flatMap((status) => presentPlainStatusGroup(status.name, [{
2718
+ label: layout.statusLabel,
2719
+ value: formatLspStatusBadge(status)
2720
+ }, {
2721
+ codeValue: !!status.root,
2722
+ label: layout.rootLabel,
2723
+ value: status.root || "-"
2724
+ }]));
2576
2725
  }
2577
2726
  function presentStatusMarkdownLspLines(input, copy, layout) {
2578
- return buildMarkdownStatusTable([
2579
- layout.languageLabel,
2580
- layout.statusLabel,
2581
- layout.rootLabel
2582
- ], buildLspStatusRows(input, copy, layout));
2727
+ if (input.lsp.status === "error") return presentStatusMarkdownErrorLines(input.lsp.error, copy, layout);
2728
+ if (input.lsp.data.statuses.length === 0) return presentMarkdownEmptyStatusLines(copy.lsp.none, layout);
2729
+ return input.lsp.data.statuses.flatMap((status) => presentMarkdownStatusGroup(status.name, [{
2730
+ label: layout.statusLabel,
2731
+ value: formatLspStatusBadge(status)
2732
+ }, {
2733
+ codeValue: !!status.root,
2734
+ label: layout.rootLabel,
2735
+ value: status.root || "-"
2736
+ }]));
2583
2737
  }
2584
2738
  function presentStatusPlainMcpLines(input, copy, layout) {
2585
- return buildPlainStatusTable([
2586
- layout.serviceLabel,
2587
- layout.statusLabel,
2588
- layout.mcpNotesLabel
2589
- ], buildMcpStatusRows(input, copy, layout));
2739
+ if (input.mcp.status === "error") return presentStatusPlainErrorLines(input.mcp.error, copy, layout);
2740
+ if (input.mcp.data.statuses.length === 0) return presentPlainEmptyStatusLines(copy.mcp.none, layout);
2741
+ return input.mcp.data.statuses.flatMap(({ name, status }) => presentPlainStatusGroup(name, [{
2742
+ label: layout.statusLabel,
2743
+ value: formatMcpStatusBadge(status, layout)
2744
+ }, {
2745
+ label: layout.mcpNotesLabel,
2746
+ value: formatMcpStatusNotes(status, copy, layout)
2747
+ }]));
2590
2748
  }
2591
2749
  function presentStatusMarkdownMcpLines(input, copy, layout) {
2592
- return buildMarkdownStatusTable([
2593
- layout.serviceLabel,
2594
- layout.statusLabel,
2595
- layout.mcpNotesLabel
2596
- ], buildMcpStatusRows(input, copy, layout));
2597
- }
2598
- function buildLspStatusRows(input, copy, layout) {
2599
- if (input.lsp.status === "error") return [[
2600
- "-",
2601
- layout.errorStatus,
2602
- flattenStatusError(input.lsp.error, copy)
2603
- ]];
2604
- if (input.lsp.data.statuses.length === 0) return [[
2605
- "-",
2606
- layout.noneStatus,
2607
- copy.lsp.none
2608
- ]];
2609
- return input.lsp.data.statuses.map((status) => [
2610
- status.name,
2611
- formatLspStatusBadge(status, copy),
2612
- status.root || "-"
2613
- ]);
2614
- }
2615
- function buildMcpStatusRows(input, copy, layout) {
2616
- if (input.mcp.status === "error") return [[
2617
- "-",
2618
- layout.errorStatus,
2619
- flattenStatusError(input.mcp.error, copy)
2620
- ]];
2621
- if (input.mcp.data.statuses.length === 0) return [[
2622
- "-",
2623
- layout.noneStatus,
2624
- copy.mcp.none
2625
- ]];
2626
- return input.mcp.data.statuses.map(({ name, status }) => [
2627
- name,
2628
- formatMcpStatusBadge(status, copy, layout),
2629
- formatMcpStatusNotes(status, layout)
2630
- ]);
2631
- }
2632
- function buildPlainStatusTable(headers, rows) {
2633
- return [
2634
- formatPlainTableRow(headers),
2635
- formatPlainTableSeparator(headers.length),
2636
- ...rows.map((row) => formatPlainTableRow(row))
2637
- ];
2750
+ if (input.mcp.status === "error") return presentStatusMarkdownErrorLines(input.mcp.error, copy, layout);
2751
+ if (input.mcp.data.statuses.length === 0) return presentMarkdownEmptyStatusLines(copy.mcp.none, layout);
2752
+ return input.mcp.data.statuses.flatMap(({ name, status }) => presentMarkdownStatusGroup(name, [{
2753
+ label: layout.statusLabel,
2754
+ value: formatMcpStatusBadge(status, layout)
2755
+ }, {
2756
+ label: layout.mcpNotesLabel,
2757
+ value: formatMcpStatusNotes(status, copy, layout)
2758
+ }], { codeName: true }));
2638
2759
  }
2639
- function buildMarkdownStatusTable(headers, rows) {
2640
- return [
2641
- `| ${headers.map(sanitizeTableCell).join(" | ")} |`,
2642
- `| ${headers.map(() => "---").join(" | ")} |`,
2643
- ...rows.map((row) => `| ${row.map(sanitizeTableCell).join(" | ")} |`)
2644
- ];
2760
+ function presentStatusPlainErrorLines(error, copy, layout) {
2761
+ return [presentPlainStatusBullet(layout.statusLabel, layout.errorStatus), ...presentStatusPlainErrorDetailLines(error, copy, layout)];
2762
+ }
2763
+ function presentStatusPlainErrorDetailLines(error, copy, layout) {
2764
+ return splitStatusLines(presentError(error, copy)).map((line) => presentPlainStatusBullet(layout.detailsLabel, line));
2645
2765
  }
2646
- function formatPlainTableRow(columns) {
2647
- return `| ${columns.map(sanitizeTableCell).join(" | ")} |`;
2766
+ function presentStatusMarkdownErrorLines(error, copy, layout) {
2767
+ return [presentMarkdownStatusBullet(layout.statusLabel, layout.errorStatus), ...presentStatusMarkdownErrorDetailLines(error, copy, layout)];
2648
2768
  }
2649
- function formatPlainTableSeparator(columnCount) {
2650
- return `| ${Array.from({ length: columnCount }, () => "---").join(" | ")} |`;
2769
+ function presentStatusMarkdownErrorDetailLines(error, copy, layout) {
2770
+ return splitStatusLines(presentError(error, copy)).map((line) => presentMarkdownStatusBullet(layout.detailsLabel, line));
2651
2771
  }
2652
- function sanitizeTableCell(value) {
2653
- return value.replace(/\r\n?/g, " / ").replace(/\|/g, "/").trim();
2772
+ function presentPlainEmptyStatusLines(message, layout) {
2773
+ return [presentPlainStatusBullet(layout.statusLabel, layout.noneStatus), presentPlainStatusBullet(layout.detailsLabel, message)];
2654
2774
  }
2655
- function presentStatusPlainErrorLines(error, copy, layout) {
2656
- const detailLines = splitStatusLines(presentError(error, copy));
2657
- return [presentPlainStatusBullet(layout.statusLabel, layout.errorStatus), ...detailLines.map((line) => presentPlainStatusBullet(layout.detailsLabel, line))];
2775
+ function presentMarkdownEmptyStatusLines(message, layout) {
2776
+ return [presentMarkdownStatusBullet(layout.statusLabel, layout.noneStatus), presentMarkdownStatusBullet(layout.detailsLabel, message)];
2658
2777
  }
2659
- function presentStatusMarkdownErrorLines(error, copy, layout) {
2660
- const detailLines = splitStatusLines(presentError(error, copy));
2661
- return [presentMarkdownStatusBullet(layout.statusLabel, layout.errorStatus), ...detailLines.map((line) => presentMarkdownStatusBullet(layout.detailsLabel, line))];
2778
+ function presentPlainStatusGroup(name, details) {
2779
+ return [`- ${normalizeStatusInlineValue(name)}`, ...details.map((detail) => ` - ${detail.label}: ${formatStatusValue(detail.value)}`)];
2662
2780
  }
2663
- function flattenStatusError(error, copy) {
2664
- return splitStatusLines(presentError(error, copy)).join(" / ");
2781
+ function presentMarkdownStatusGroup(name, details, options = {}) {
2782
+ return [`- ${options.codeName ? `\`${normalizeStatusInlineValue(name)}\`` : `**${normalizeStatusInlineValue(name)}**`}`, ...details.map((detail) => detail.codeValue ? ` - **${detail.label}:** \`${formatStatusValue(detail.value)}\`` : ` - **${detail.label}:** ${formatStatusValue(detail.value)}`)];
2665
2783
  }
2666
2784
  function presentPlainStatusBullet(label, value) {
2667
- return `- ${label}: ${value}`;
2785
+ return `- ${label}: ${formatStatusValue(value)}`;
2668
2786
  }
2669
2787
  function presentMarkdownStatusBullet(label, value, options = {}) {
2670
- return options.codeValue ? `- **${label}:** \`${value}\`` : `- **${label}:** ${value}`;
2788
+ return options.codeValue ? `- **${label}:** \`${formatStatusValue(value)}\`` : `- **${label}:** ${formatStatusValue(value)}`;
2671
2789
  }
2672
2790
  function splitStatusLines(text) {
2673
2791
  return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
2674
2792
  }
2675
2793
  function formatHealthBadge(healthy, layout) {
2676
- return healthy ? layout.healthyStatus : layout.errorStatus;
2794
+ return healthy ? "๐ŸŸข" : layout.errorStatus;
2677
2795
  }
2678
- function formatLspStatusBadge(status, copy) {
2796
+ function formatLspStatusBadge(status) {
2679
2797
  switch (status.status) {
2680
- case "connected": return `๐ŸŸข ${copy.lsp.connected}`;
2681
- case "error": return `๐Ÿ”ด ${copy.lsp.error}`;
2798
+ case "connected": return "๐ŸŸข";
2799
+ case "error": return "๐Ÿ”ด";
2682
2800
  }
2683
2801
  return status.status;
2684
2802
  }
2685
- function formatMcpStatusBadge(status, copy, layout) {
2803
+ function formatMcpStatusBadge(status, layout) {
2686
2804
  switch (status.status) {
2687
- case "connected": return `๐ŸŸข ${copy.mcp.connected}`;
2688
- case "disabled": return `โšช ${copy.mcp.disabled}`;
2689
- case "needs_auth": return `๐ŸŸก ${copy.mcp.needsAuth}`;
2805
+ case "connected": return "๐ŸŸข";
2806
+ case "disabled": return "โšช";
2807
+ case "needs_auth": return "๐ŸŸก";
2690
2808
  case "failed": return layout.mcpFailedStatus;
2691
2809
  case "needs_client_registration": return layout.mcpRegistrationRequiredStatus;
2692
2810
  }
2693
2811
  return status;
2694
2812
  }
2695
- function formatMcpStatusNotes(status, layout) {
2813
+ function formatMcpStatusNotes(status, copy, layout) {
2696
2814
  switch (status.status) {
2697
2815
  case "connected": return layout.okLabel;
2698
- case "disabled": return "-";
2699
- case "needs_auth": return "-";
2816
+ case "disabled": return copy.mcp.disabled;
2817
+ case "needs_auth": return copy.mcp.needsAuth;
2700
2818
  case "failed": return status.error;
2701
2819
  case "needs_client_registration": return status.error;
2702
2820
  }
2703
2821
  return status;
2704
2822
  }
2705
- function formatStatusDate(value) {
2706
- return value.toISOString().slice(0, 10);
2823
+ function formatStatusValue(value) {
2824
+ const normalized = value.replace(/\r\n?/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join(" / ");
2825
+ return normalized.length > 0 ? normalized : "-";
2826
+ }
2827
+ function normalizeStatusInlineValue(value) {
2828
+ return formatStatusValue(value);
2707
2829
  }
2708
2830
  function getStatusLayoutCopy(copy) {
2709
2831
  if (copy.systemStatus.title === BOT_COPY.systemStatus.title) return {
2710
- currentDirectoryLabel: "Current Directory",
2832
+ connectivityLabel: "Connectivity",
2833
+ configuredPluginsLabel: "Configured Plugins",
2834
+ currentProjectLabel: "Current Project",
2835
+ currentSessionLabel: "Current Session",
2711
2836
  detailsLabel: "Details",
2712
- divider: "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€",
2713
- errorStatus: "๐Ÿ”ด Unhealthy",
2714
- healthyStatus: "๐ŸŸข Healthy",
2715
- languageLabel: "Language",
2716
- lastUpdatedLabel: "Last updated",
2717
- lspTitle: "๐Ÿง  LSP (Language Server)",
2837
+ errorStatus: "๐Ÿ”ด",
2838
+ lspTitle: "๐Ÿง  LSP",
2839
+ mcpFailedStatus: "๐Ÿ”ด",
2718
2840
  mcpNotesLabel: "Notes",
2719
- mcpRegistrationRequiredStatus: "๐ŸŸก Registration Required",
2720
- mcpTitle: "๐Ÿ”Œ MCP (Model Context Protocol)",
2721
- mcpFailedStatus: "๐Ÿ”ด Failed",
2722
- noneStatus: "โšช None",
2841
+ mcpRegistrationRequiredStatus: "๐ŸŸก",
2842
+ mcpTitle: "๐Ÿ”Œ MCP",
2843
+ noPluginsMessage: "No plugins configured in the OpenCode config.",
2844
+ noneStatus: "โšช",
2845
+ openCodeVersionLabel: "OpenCode Version",
2723
2846
  okLabel: "OK",
2847
+ overviewTitle: "๐Ÿ–ฅ๏ธ Overview",
2724
2848
  pageTitle: "๐Ÿ“Š Service Status",
2849
+ pluginsTitle: "๐Ÿงฉ Plugins",
2725
2850
  rootLabel: "Root",
2726
- serverTitle: "๐Ÿ–ฅ๏ธ Server",
2727
- serviceLabel: "Service",
2728
2851
  statusLabel: "Status",
2729
- versionLabel: "Version",
2852
+ tbotVersionLabel: "opencode-tbot Version",
2730
2853
  workspaceTitle: "๐Ÿ“ Workspace"
2731
2854
  };
2732
2855
  return {
2733
- currentDirectoryLabel: "ๅฝ“ๅ‰็›ฎๅฝ•",
2856
+ connectivityLabel: "่ฟž้€šๆ€ง",
2857
+ configuredPluginsLabel: "ๅทฒ้…็ฝฎๆ’ไปถ",
2858
+ currentProjectLabel: "ๅฝ“ๅ‰้กน็›ฎ",
2859
+ currentSessionLabel: "ๅฝ“ๅ‰ไผš่ฏ",
2734
2860
  detailsLabel: "่ฏฆๆƒ…",
2735
- divider: "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€",
2736
- errorStatus: "๐Ÿ”ด ๅผ‚ๅธธ",
2737
- healthyStatus: "๐ŸŸข ๅฅๅบท",
2738
- languageLabel: "่ฏญ่จ€",
2739
- lastUpdatedLabel: "ๆœ€ๅŽๆ›ดๆ–ฐ",
2740
- lspTitle: "๐Ÿง  LSP (Language Server)",
2861
+ errorStatus: "๐Ÿ”ด",
2862
+ lspTitle: "๐Ÿง  LSP",
2863
+ mcpFailedStatus: "๐Ÿ”ด",
2741
2864
  mcpNotesLabel: "่ฏดๆ˜Ž",
2742
- mcpRegistrationRequiredStatus: "๐ŸŸก ้œ€่ฆๆณจๅ†Œ",
2743
- mcpTitle: "๐Ÿ”Œ MCP (Model Context Protocol)",
2744
- mcpFailedStatus: "๐Ÿ”ด ๅคฑ่ดฅ",
2745
- noneStatus: "โšช ๆ— ",
2865
+ mcpRegistrationRequiredStatus: "๐ŸŸก",
2866
+ mcpTitle: "๐Ÿ”Œ MCP",
2867
+ noPluginsMessage: "ๅฝ“ๅ‰ OpenCode ้…็ฝฎไธญๆœช้…็ฝฎๆ’ไปถใ€‚",
2868
+ noneStatus: "โšช",
2869
+ openCodeVersionLabel: "OpenCode็‰ˆๆœฌ",
2746
2870
  okLabel: "ๆญฃๅธธ",
2871
+ overviewTitle: "๐Ÿ–ฅ๏ธ ๆฆ‚่งˆ",
2747
2872
  pageTitle: "๐Ÿ“Š ๆœๅŠก็Šถๆ€",
2873
+ pluginsTitle: "๐Ÿงฉ ๆ’ไปถ",
2748
2874
  rootLabel: "ๆ น็›ฎๅฝ•",
2749
- serverTitle: "๐Ÿ–ฅ๏ธ ๆœๅŠก็ซฏ",
2750
- serviceLabel: "ๆœๅŠก",
2751
2875
  statusLabel: "็Šถๆ€",
2752
- versionLabel: "็‰ˆๆœฌ",
2876
+ tbotVersionLabel: "opencode-tbot็‰ˆๆœฌ",
2753
2877
  workspaceTitle: "๐Ÿ“ ๅทฅไฝœๅŒบ"
2754
2878
  };
2755
2879
  }