feat: render the status line as a themed widget in the TUI and document it

omp strips ANSI from extension status text, so setStatus drops the theme colors.
String widgets keep them: in the TUI the line is a belowEditor widget and the
plain footer entry is cleared; other modes keep setStatus.
This commit is contained in:
evandrodevbr committed 2026-10-01 11:28:53 -03:00
1 parent ca05b58f38
commit 9435ff709b
4 files changed
+97 -7

No files matched your search

+2
View File
@@ -2,6 +2,8 @@
## Unreleased ## Unreleased
- `Status Line`: The line under the omp editor now shows a state glyph, the bot name, received/sent message counters and the time of the last message (`telegram ● connected · @bot · ⤵12 ⤴11 · ⏱ 14:32`), plus the next step for non-working states (`/telegram-setup`, `send /start to @bot`, `/telegram-connect`). Glyphs follow omp's `symbolPreset`. omp strips ANSI from extension status text, so in the TUI the line is rendered as a `belowEditor` widget that keeps the theme colors; other modes keep `setStatus`. Counters come from the new leaf domain `lib/traffic.ts`: received on every owner message, sent on every successful `sendMessage`/`sendRichMessage`/media call, and `/telegram-status` gains a `- messages:` line. Impact: connection state, traffic and recovery steps are visible without opening `/telegram-status`.
- `Pairing`: The poll loop persisted the config object it captured when polling started. The store swaps in a fresh object on every persist, so after the initial sync that stale copy lacked later changes and the next offset commit erased the paired owner: `telegram.json` never kept `allowedUserId`, the bridge re-paired on every message and the status flipped back to `awaiting pairing`. The runner now carries only the poll offset onto the store's current config. Impact: first-time pairing sticks, and another Telegram user can no longer be paired by writing before the owner's next message.
- `omp Agent Directory`: Resolve the agent directory from the host's own `getAgentDir()` (registered by the composition root via `setHostAgentDirResolver`) instead of sniffing `execPath`/`argv[1]` for an `omp` prefix. omp installed through Bun runs as `bun …/@oh-my-pi/pi-coding-agent/dist/cli.js`, which matched neither, so with `telegram.json` in `~/.omp/agent` the bridge never connected and wrote its log and bus socket under `~/.pi/agent`. `PI_CODING_AGENT_DIR` still wins; with no host registered the fallback stays `~/.pi/agent`. Impact: a bot token saved by `/telegram-setup` lives in omp's own agent directory and is found again. `lib/paths.ts` stays free of Pi SDK imports, which cost about a second of startup per importing process. - `omp Agent Directory`: Resolve the agent directory from the host's own `getAgentDir()` (registered by the composition root via `setHostAgentDirResolver`) instead of sniffing `execPath`/`argv[1]` for an `omp` prefix. omp installed through Bun runs as `bun …/@oh-my-pi/pi-coding-agent/dist/cli.js`, which matched neither, so with `telegram.json` in `~/.omp/agent` the bridge never connected and wrote its log and bus socket under `~/.pi/agent`. `PI_CODING_AGENT_DIR` still wins; with no host registered the fallback stays `~/.pi/agent`. Impact: a bot token saved by `/telegram-setup` lives in omp's own agent directory and is found again. `lib/paths.ts` stays free of Pi SDK imports, which cost about a second of startup per importing process.
- `omp Settled Boundary`: omp never emits `agent_settled`, the only place the activity runtime finalized a turn. On omp the first terminal `agent_end` (no `willContinue: true`, which omp sets for retries, compaction continuations, todo/plan reminders and async wakes) now drives `onAgentSettled`; hosts that emit `agent_settled` natively are unchanged. Impact: Activity API consumers receive `agent-settled` and the activity id no longer leaks across turns. - `omp Settled Boundary`: omp never emits `agent_settled`, the only place the activity runtime finalized a turn. On omp the first terminal `agent_end` (no `willContinue: true`, which omp sets for retries, compaction continuations, todo/plan reminders and async wakes) now drives `onAgentSettled`; hosts that emit `agent_settled` natively are unchanged. Impact: Activity API consumers receive `agent-settled` and the activity id no longer leaks across turns.
- `omp Current Model`: omp never emits `model_select`, so the stored current model went stale after a local `/model` switch, and omp builds command contexts by spreading a base context, which freezes `ctx.model` at the moment of `/telegram-connect`. On omp the live `ctx.models.current()` now wins over both, and the stored value no longer overrides it. Impact: the Telegram model menu marks the model omp is actually using. - `omp Current Model`: omp never emits `model_select`, so the stored current model went stale after a local `/model` switch, and omp builds command contexts by spreading a base context, which freezes `ctx.model` at the moment of `/telegram-connect`. On omp the live `ctx.models.current()` now wins over both, and the stored value no longer overrides it. Impact: the Telegram model menu marks the model omp is actually using.
+18
View File
@@ -130,6 +130,24 @@ Config is written to `<agent-dir>/telegram.json`. The agent directory is `PI_COD
Profile names: lowercase ASCII letters and digits, up to 32 characters; `default`, `main` and `active` are reserved. Profiles keep isolated polling, diagnostics, Threaded Mode state and local bus transport. Profile names: lowercase ASCII letters and digits, up to 32 characters; `default`, `main` and `active` are reserved. Profiles keep isolated polling, diagnostics, Threaded Mode state and local bus transport.
### Status line
The bridge keeps one live line under the `omp` editor. In the interactive TUI it is a themed widget (`omp` strips color from plain extension status text, so the widget is what keeps the theme colors); in RPC, JSON and print modes it falls back to a plain status entry. The glyphs follow `omp`'s `symbolPreset` (`unicode`, `nerd` or `ascii`).
```text
telegram ● connected · @my_bot · ⤵12 ⤴11 · ⏱ 14:32
telegram ⟳ active +2 · @my_bot · ⤵14 ⤴11 · ⏱ 14:33
telegram ⚠ awaiting pairing · send /start to @my_bot
telegram ○ disconnected · /telegram-connect
telegram ⦸ not configured · /telegram-setup
telegram ✘ error Unable to connect. Is the computer able to access the url?
```
- `⤵N` counts messages received from the paired owner and `⤴N` counts messages the bridge delivered to Telegram (replies, menus and attachments; typing indicators, drafts and edits are not counted). `⏱` is the time of the latest one. Counters live in memory, start at zero when the extension loads, and are shown only while the bridge is up.
- The state word is the same ladder as before: `not configured`, `awaiting pairing`, `electing`, `disconnected`, then `active` while a turn runs or is queued, and `connected`, `leader` or `follower` otherwise.
- Messages a follower instance sends through the leader are counted by the leader process, not by the follower.
- `/telegram-status` repeats the counters as a `- messages:` line.
### Tools available to the agent ### Tools available to the agent
| Tool | Purpose | | Tool | Purpose |
+20 -7
View File
@@ -257,9 +257,16 @@ export interface TelegramStatusBarState {
} }
export interface TelegramStatusRuntimeContext { export interface TelegramStatusRuntimeContext {
/** Host run mode; the colored widget is only used in the interactive TUI. */
mode?: string;
ui: { ui: {
theme: TelegramStatusBarTheme; theme: TelegramStatusBarTheme;
setStatus: (key: string, text: string) => void; setStatus(key: string, text: string | undefined): void;
setWidget?(
key: string,
content: string[] | undefined,
options?: { placement?: "aboveEditor" | "belowEditor" },
): void;
}; };
} }
@@ -590,13 +597,19 @@ export function createTelegramStatusRuntime<
const statusKey = deps.statusKey ?? "telegram"; const statusKey = deps.statusKey ?? "telegram";
return { return {
updateStatus: (ctx, error) => { updateStatus: (ctx, error) => {
ctx.ui.setStatus( const text = buildTelegramStatusBarText(
statusKey, ctx.ui.theme,
buildTelegramStatusBarText( deps.getStatusBarState(ctx, error),
ctx.ui.theme,
deps.getStatusBarState(ctx, error),
),
); );
if (ctx.mode === "tui" && ctx.ui.setWidget) {
// The host strips ANSI from status text, so setStatus would drop the
// theme colors. String widgets keep them; clear the plain footer entry
// so the state is not shown twice.
ctx.ui.setWidget(statusKey, [text], { placement: "belowEditor" });
ctx.ui.setStatus(statusKey, undefined);
return;
}
ctx.ui.setStatus(statusKey, text);
}, },
getStatusLines: (options) => getStatusLines: (options) =>
buildTelegramBridgeStatusLines(deps.getBridgeStatusLineState(), options), buildTelegramBridgeStatusLines(deps.getBridgeStatusLineState(), options),
+57
View File
@@ -318,6 +318,63 @@ test("Status runtime updates the status bar and exposes bridge lines", () => {
]); ]);
}); });
test("Status runtime keeps theme colors in the TUI through a widget and stays plain elsewhere", () => {
const createContext = (mode: string | undefined) => {
const events: string[] = [];
return {
events,
ctx: {
mode,
ui: {
theme: statusBarTheme,
setStatus: (key: string, text: string | undefined) => {
events.push(`status:${key}:${text}`);
},
setWidget: (
key: string,
content: string[] | undefined,
options?: { placement?: string },
) => {
events.push(`widget:${key}:${options?.placement}:${content?.join("|")}`);
},
},
},
};
};
const runtime = createTelegramStatusRuntime({
getStatusBarState: () => ({
hasBotToken: true,
pollingActive: true,
paired: true,
compactionInProgress: false,
processing: false,
queuedStatus: "",
}),
getBridgeStatusLineState: () => {
throw new Error("not used");
},
});
const tui = createContext("tui");
runtime.updateStatus(tui.ctx);
assert.equal(tui.events.length, 2);
assert.match(tui.events[0], /^widget:telegram:belowEditor:<accent>telegram<\/accent> <success>\S+ connected<\/success>$/);
assert.equal(tui.events[1], "status:telegram:undefined");
for (const mode of ["rpc", "json", "print", undefined]) {
const other = createContext(mode);
runtime.updateStatus(other.ctx);
assert.equal(other.events.length, 1, String(mode));
assert.match(other.events[0], /^status:telegram:<accent>telegram<\/accent> <success>\S+ connected<\/success>$/);
}
const noWidgetApi = createContext("tui");
delete (noWidgetApi.ctx.ui as { setWidget?: unknown }).setWidget;
runtime.updateStatus(noWidgetApi.ctx);
assert.equal(noWidgetApi.events.length, 1);
assert.match(noWidgetApi.events[0], /^status:telegram:/);
});
test("Status lines expose thread reconciliation state", () => { test("Status lines expose thread reconciliation state", () => {
const lines = buildTelegramBridgeStatusLines({ const lines = buildTelegramBridgeStatusLines({
botUsername: "demo_bot", botUsername: "demo_bot",