talon-agent 5.4.1 → 5.5.0

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.
@@ -89,6 +89,36 @@ function applyInlineFormatting(input: string, urls: string[]): string {
89
89
  return out;
90
90
  }
91
91
 
92
+ /**
93
+ * Block-level passes over already-escaped text: ATX headings → bold lines,
94
+ * runs of `> ` lines → one `<blockquote>`. Placeholders for code are opaque
95
+ * `\x00…\x00` tokens, so a `#` inside a fenced block is never seen here.
96
+ */
97
+ function applyBlockFormatting(input: string): string {
98
+ const lines = input.split("\n");
99
+ const out: string[] = [];
100
+ let quote: string[] | null = null;
101
+ const flushQuote = () => {
102
+ if (quote) out.push(`<blockquote>${quote.join("\n")}</blockquote>`);
103
+ quote = null;
104
+ };
105
+ for (const line of lines) {
106
+ const quoted = /^&gt;[ \t]?(.*)$/.exec(line);
107
+ if (quoted) {
108
+ (quote ??= []).push(quoted[1] ?? "");
109
+ continue;
110
+ }
111
+ flushQuote();
112
+ // `#` must be followed by whitespace to be a heading: `#993` is a
113
+ // reference, not a title. Trailing closing hashes (`## Title ##`) are
114
+ // markdown too and are dropped.
115
+ const heading = /^#{1,6}[ \t]+(.+?)(?:[ \t]+#+)?[ \t]*$/.exec(line);
116
+ out.push(heading ? `<b>${heading[1]}</b>` : line);
117
+ }
118
+ flushQuote();
119
+ return out.join("\n");
120
+ }
121
+
92
122
  /**
93
123
  * Convert Markdown output to Telegram-safe HTML.
94
124
  *
@@ -145,6 +175,15 @@ export function markdownToTelegramHtml(text: string): string {
145
175
  // oxlint-disable-next-line no-control-regex
146
176
  processed = processed.replace(/[^`\x00]+/g, (segment) => escapeHtml(segment));
147
177
 
178
+ // Step 3b: Block-level markdown. Telegram HTML has no headings, so an
179
+ // ATX heading becomes a bold line — the alternative is the `###` going
180
+ // out raw, and `# Title` alone turns `#Title` into a hashtag entity.
181
+ // Only `#` followed by whitespace is a heading: `#993` is a PR/hashtag
182
+ // reference and stays literal. Runs of `> ` lines become a blockquote,
183
+ // which Telegram does render. The text is already escaped here, so the
184
+ // quote marker is `&gt;`.
185
+ processed = applyBlockFormatting(processed);
186
+
148
187
  // Step 4: Apply inline formatting, but only keep it if the result is
149
188
  // actually parseable. `processed` at this point holds escaped text plus
150
189
  // tag-free placeholders, so it is a safe unformatted fallback.
@@ -14,7 +14,14 @@ import {
14
14
  import { getChatSettings } from "../../../storage/chat-settings.js";
15
15
  import { getSessionInfo } from "../../../storage/sessions.js";
16
16
  import { getLoadedPlugins } from "../../../core/plugin/index.js";
17
- import { getPooledBackend } from "../../../core/engine/backend-controller/index.js";
17
+ import {
18
+ getPoolConfig,
19
+ getPooledBackend,
20
+ } from "../../../core/engine/backend-controller/index.js";
21
+ import {
22
+ collectBackendUsage,
23
+ formatHeadroom,
24
+ } from "../../../core/engine/backend-router/index.js";
18
25
  import type { Command, CommandContext } from "../command-registry.js";
19
26
 
20
27
  type BackendRef = CommandContext["backend"];
@@ -146,6 +153,27 @@ async function writePlan(ctx: CommandContext, be: BackendRef): Promise<void> {
146
153
  );
147
154
  }
148
155
  }
156
+ await writeHeadroom(ctx);
157
+ }
158
+
159
+ /**
160
+ * What the plan-aware router sees: one comparable figure per backend, so
161
+ * "why did that sub-agent run on agy?" is answerable from `/status`.
162
+ * Ledger-derived rows say so — they are Talon's own count, not the
163
+ * provider's.
164
+ */
165
+ async function writeHeadroom(ctx: CommandContext): Promise<void> {
166
+ const entries = await collectBackendUsage(getPoolConfig() ?? undefined).catch(
167
+ () => [],
168
+ );
169
+ if (entries.length === 0) return;
170
+ ctx.renderer.writeln();
171
+ ctx.renderer.writeln(` ${pc.bold("Headroom")}`);
172
+ for (const entry of entries) {
173
+ ctx.renderer.writeln(
174
+ ` ${(entry.label || entry.id).padEnd(14)}${pc.dim(formatHeadroom(entry.headroom))}`,
175
+ );
176
+ }
149
177
  }
150
178
 
151
179
  function writePlugins(ctx: CommandContext): void {
package/src/util/log.ts CHANGED
@@ -68,6 +68,7 @@ export type LogComponent =
68
68
  | "memory"
69
69
  | "stickers"
70
70
  | "backup"
71
+ | "router"
71
72
  | "backend-controller";
72
73
 
73
74
  const LOG_FILE = files.log;