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:
1 parent
ca05b58f38
commit
9435ff709b
4 files changed
+97
-7
No files matched your search
@@ -2,6 +2,8 @@
|
||||
|
||||
## 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 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
### 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
|
||||
|
||||
| Tool | Purpose |
|
||||
|
||||
+20
-7
@@ -257,9 +257,16 @@ export interface TelegramStatusBarState {
|
||||
}
|
||||
|
||||
export interface TelegramStatusRuntimeContext {
|
||||
/** Host run mode; the colored widget is only used in the interactive TUI. */
|
||||
mode?: string;
|
||||
ui: {
|
||||
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";
|
||||
return {
|
||||
updateStatus: (ctx, error) => {
|
||||
ctx.ui.setStatus(
|
||||
statusKey,
|
||||
buildTelegramStatusBarText(
|
||||
ctx.ui.theme,
|
||||
deps.getStatusBarState(ctx, error),
|
||||
),
|
||||
const text = buildTelegramStatusBarText(
|
||||
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) =>
|
||||
buildTelegramBridgeStatusLines(deps.getBridgeStatusLineState(), options),
|
||||
|
||||
@@ -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", () => {
|
||||
const lines = buildTelegramBridgeStatusLines({
|
||||
botUsername: "demo_bot",
|
||||
|
||||
Reference in new issue
Block a user