pi-ui-extend 0.1.21 → 0.1.25
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/README.md +1 -10
- package/bin/pix.mjs +11 -154
- package/dist/app/app.d.ts +1 -0
- package/dist/app/app.js +34 -9
- package/dist/app/cli/startup-info.d.ts +0 -1
- package/dist/app/cli/startup-info.js +0 -3
- package/dist/app/commands/command-session-actions.js +3 -0
- package/dist/app/input/autocomplete-controller.js +0 -1
- package/dist/app/popup/popup-menu-controller.js +7 -1
- package/dist/app/rendering/conversation-entry-renderer.js +29 -40
- package/dist/app/rendering/render-text.d.ts +6 -0
- package/dist/app/rendering/render-text.js +9 -0
- package/dist/app/rendering/tab-line-renderer.js +1 -5
- package/dist/app/rendering/tool-block-renderer.js +7 -1
- package/dist/app/screen/mouse-controller.js +14 -6
- package/dist/app/session/session-event-controller.js +5 -4
- package/dist/app/session/session-lifecycle-controller.js +0 -4
- package/dist/app/session/tabs-controller.d.ts +5 -1
- package/dist/app/session/tabs-controller.js +111 -23
- package/dist/app/types.d.ts +5 -0
- package/dist/app/workspace/workspace-actions-controller.d.ts +3 -0
- package/dist/app/workspace/workspace-actions-controller.js +71 -16
- package/dist/app/workspace/workspace-undo.js +41 -6
- package/dist/markdown-format.d.ts +4 -0
- package/dist/markdown-format.js +6 -1
- package/dist/schemas/pi-tools-suite-schema.d.ts +0 -1
- package/dist/schemas/pi-tools-suite-schema.js +0 -1
- package/dist/theme.js +18 -18
- package/extensions/session-title/config.ts +0 -5
- package/extensions/session-title/index.ts +0 -1
- package/external/pi-tools-suite/README.md +1 -1
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -0
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -1
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +0 -5
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +0 -1
- package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +0 -1
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +1 -1
- package/external/pi-tools-suite/src/telegram-mirror/README.md +81 -46
- package/external/pi-tools-suite/src/telegram-mirror/bot.ts +81 -10
- package/external/pi-tools-suite/src/telegram-mirror/events.ts +6 -38
- package/external/pi-tools-suite/src/telegram-mirror/index.ts +246 -40
- package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +20 -0
- package/external/pi-tools-suite/src/telegram-mirror/multiplexer.ts +247 -17
- package/external/pi-tools-suite/src/telegram-mirror/renderer.ts +75 -78
- package/external/pi-tools-suite/src/todo/index.ts +7 -6
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +1 -1
- package/external/pi-tools-suite/src/web-search/index.ts +139 -2
- package/package.json +7 -7
- package/schemas/pi-tools-suite.json +0 -6
|
@@ -163,7 +163,7 @@ Async-subagents also injects a lightweight oh-my-openagent-style system-prompt s
|
|
|
163
163
|
|
|
164
164
|
When the parent model cannot inspect images, async-subagents adds vision-delegation guidance and can save current-turn image attachments under `.pi/subagents/attachments/` so a `vision` sub-agent can receive them as `imagePaths`. Dynamic provider capabilities can be missing or stale after switching models, so blind parent models can still be configured explicitly with case-insensitive `*` masks under `asyncSubagents.vision.blindModelPatterns` in `~/.config/pi/pi-tools-suite.jsonc`. GLM is no longer treated as blind by async-subagents by default; the main-session `glm-coding-discipline` lookup tool is the preferred path for GLM visual lookups.
|
|
165
165
|
|
|
166
|
-
When a task omits `subagentType`, async-subagents asks a lightweight router model to choose one configured type for each task from the task text/scope and the `types.<name>.description` metadata. Explicit task `subagentType` still wins. Keep type descriptions short, literal, and distinct because they are inserted into the router prompt for a small model. Router settings live under `asyncSubagents.routing` (`enabled`, `model`, `maxTaskChars`, `maxTokens`, `maxRetries`, `
|
|
166
|
+
When a task omits `subagentType`, async-subagents asks a lightweight router model to choose one configured type for each task from the task text/scope and the `types.<name>.description` metadata. Explicit task `subagentType` still wins. Keep type descriptions short, literal, and distinct because they are inserted into the router prompt for a small model. Router settings live under `asyncSubagents.routing` (`enabled`, `model`, `maxTaskChars`, `maxTokens`, `maxRetries`, `timeoutMs`, `debug`); the default router model is `zai/glm-4.5-air`. If the router is disabled, unavailable, aborted, or returns invalid JSON, omitted types fall back to `defaultType`.
|
|
167
167
|
|
|
168
168
|
Define optional `presets` under `asyncSubagents` in `~/.config/pi/pi-tools-suite.jsonc`, `$PI_CONFIG_DIR/pi-tools-suite.jsonc`, or project `.pi/pi-tools-suite.jsonc`, then use `/subagent-preset` or `/subagent-preset-config` to pick one persistent active preset for future spawns across all sessions. Set `AGENTS_PRESET=<name>` before launching Pi to override the saved preset for only the current process/session without changing the saved selection. If Pi is already running, use `/subagent-preset session <name>` for the same process-only override, and `/subagent-preset session-clear` to remove that runtime override. The TUI only selects presets already present in config; it does not edit JSON. If no `asyncSubagents` section exists, run `/subagent-preset init` to insert the bundled sample from `src/async-subagents/async-subagents.sample.jsonc` into the shared config (or to copy a standalone override file when `ASYNC_SUBAGENTS_CONFIG` / `PI_SUBAGENTS_CONFIG` is set). Existing config sections/files are never overwritten. Presets select an agent/model configuration: they can provide global fallback `model`/`thinking`/`extraArgs` and per-role overrides under `asyncSubagents.presets.<name>.types.<subagentType>`. They can also provide ordered `fallbackModels` globally or per-role; when a sub-agent fails with quota/rate-limit errors such as 429, async-subagents immediately tries the next fallback model and remembers the exhausted provider for the current Pi process/session, so later spawns skip that provider until Pi exits. This is intended for provider-level fallback chains such as `antigravity/* → openai-codex/* → zai/*` or `openai-codex/* → zai/*`; omit fallbacks for effectively unlimited providers. Antigravity account rotation has priority over preset fallback: async-subagents only falls back after Antigravity reports that all configured accounts are exhausted for that model. Explicit task model overrides and force-current-model disable preset fallback for that task. The active preset name is stored separately in `~/.pi/agent/subagent-preset-selection.json`.
|
|
169
169
|
|
|
@@ -275,6 +275,7 @@ export async function refreshAntigravityToken(credentials: OAuthCredentials): Pr
|
|
|
275
275
|
);
|
|
276
276
|
return {
|
|
277
277
|
...refreshed.credentials,
|
|
278
|
+
...(oauthClient ? { oauthClient } : {}),
|
|
278
279
|
...(storedAccounts.length > 0 ? { accounts: storedAccounts, activeIndex: nextActiveIndex, email: rotationAccount?.email ?? credentialDetails.email } : {}),
|
|
279
280
|
};
|
|
280
281
|
}
|
|
@@ -37,8 +37,6 @@ export interface SubagentRoutingConfig {
|
|
|
37
37
|
maxTokens?: number;
|
|
38
38
|
/** Router complete() retries. */
|
|
39
39
|
maxRetries?: number;
|
|
40
|
-
/** Router sampling temperature. */
|
|
41
|
-
temperature?: number;
|
|
42
40
|
/** Router request timeout. */
|
|
43
41
|
timeoutMs?: number;
|
|
44
42
|
/** Show best-effort UI warnings when routing falls back. */
|
|
@@ -150,7 +148,6 @@ export const DEFAULT_ROUTING_CONFIG: ResolvedSubagentRoutingConfig = {
|
|
|
150
148
|
maxTaskChars: 1200,
|
|
151
149
|
maxTokens: 512,
|
|
152
150
|
maxRetries: 1,
|
|
153
|
-
temperature: 0,
|
|
154
151
|
timeoutMs: 12_000,
|
|
155
152
|
debug: false,
|
|
156
153
|
};
|
|
@@ -522,8 +519,6 @@ function normalizeRoutingConfig(value: Record<string, unknown>): SubagentRouting
|
|
|
522
519
|
if (maxTokens !== undefined) routing.maxTokens = Math.max(8, Math.round(maxTokens));
|
|
523
520
|
const maxRetries = finiteNumber(value.maxRetries);
|
|
524
521
|
if (maxRetries !== undefined) routing.maxRetries = Math.max(0, Math.round(maxRetries));
|
|
525
|
-
const temperature = finiteNumber(value.temperature);
|
|
526
|
-
if (temperature !== undefined) routing.temperature = Math.min(2, Math.max(0, temperature));
|
|
527
522
|
const timeoutMs = finiteNumber(value.timeoutMs);
|
|
528
523
|
if (timeoutMs !== undefined) routing.timeoutMs = Math.max(1000, Math.round(timeoutMs));
|
|
529
524
|
return routing;
|
|
@@ -23,7 +23,7 @@ export const DEFAULT_PI_TOOLS_SUITE_CONFIG_JSONC = String.raw`{
|
|
|
23
23
|
},
|
|
24
24
|
"asyncSubagents": {
|
|
25
25
|
"defaultType": "quick",
|
|
26
|
-
"routing": { "enabled": true, "model": "zai/glm-4.5-air", "maxTaskChars": 1200, "maxTokens": 512, "maxRetries": 1, "
|
|
26
|
+
"routing": { "enabled": true, "model": "zai/glm-4.5-air", "maxTaskChars": 1200, "maxTokens": 512, "maxRetries": 1, "timeoutMs": 12000, "debug": false },
|
|
27
27
|
"presets": {
|
|
28
28
|
"cheap": {
|
|
29
29
|
"description": "Use cheap GLM/Gemini Flash models for text/code roles; keep vision on the enabled GPT vision model.",
|
|
@@ -20,20 +20,46 @@ The module is a no-op until you add a `telegramMirror` block to
|
|
|
20
20
|
}
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
- `enabled` (boolean, optional): defaults to `true` when the block is
|
|
24
|
+
present and `botToken` + `chatId` are valid.
|
|
25
|
+
- `botToken` (string, required): Telegram Bot API token from @BotFather.
|
|
26
|
+
Empty string disables the mirror.
|
|
27
|
+
- `chatId` (number or string, required): numeric private chat id allowed to
|
|
28
|
+
control the bot. Non-integer disables the mirror.
|
|
28
29
|
|
|
29
|
-
When the block is present and valid, the module
|
|
30
|
-
|
|
30
|
+
When the block is present and valid, the module registers the local
|
|
31
|
+
`/telegram-mirror` and `/tg` slash commands. The bot does not connect until
|
|
32
|
+
you run one of those commands in a pi session.
|
|
33
|
+
|
|
34
|
+
## Activation
|
|
35
|
+
|
|
36
|
+
Run this inside each pi session you want to expose to Telegram:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
/telegram-mirror
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Short alias:
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
/tg
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Useful local variants:
|
|
49
|
+
|
|
50
|
+
- `/telegram-mirror` or `/tg`: connect this pi session to Telegram mirror.
|
|
51
|
+
- `/telegram-mirror status`: show local mirror role and session label.
|
|
52
|
+
- `/telegram-mirror stop`: stop the mirror cluster.
|
|
53
|
+
- `/tg-off`: stop the mirror cluster.
|
|
54
|
+
|
|
55
|
+
After activation, the leader sends a Telegram message with buttons. Use
|
|
56
|
+
`/menu` or `/list` in Telegram any time to reopen the project/session picker.
|
|
31
57
|
|
|
32
58
|
## How to get your chat id
|
|
33
59
|
|
|
34
60
|
Open this URL in a browser (replace `<TOKEN>` with your bot token):
|
|
35
61
|
|
|
36
|
-
```
|
|
62
|
+
```text
|
|
37
63
|
https://api.telegram.org/bot<TOKEN>/getUpdates
|
|
38
64
|
```
|
|
39
65
|
|
|
@@ -60,33 +86,37 @@ so this module elects a **leader** when N pi processes share one bot:
|
|
|
60
86
|
followers race to bind the socket; the first to win becomes the new
|
|
61
87
|
leader. `activeId` resets on failover — run `/use N` again.
|
|
62
88
|
|
|
63
|
-
|
|
89
|
+
Run `/telegram-mirror` in every `pi` process you want available in the
|
|
90
|
+
Telegram picker. Only one process polls Telegram; the rest register as
|
|
91
|
+
followers over IPC.
|
|
64
92
|
|
|
65
93
|
When you start a new pi, it logs `[telegram-mirror] registered with
|
|
66
94
|
leader <label>` on stderr. The leader logs `[telegram-mirror] connected
|
|
67
95
|
as @<botname> (leader)`.
|
|
68
96
|
|
|
69
|
-
### Selecting the
|
|
97
|
+
### Selecting the followed project/session
|
|
70
98
|
|
|
71
|
-
In Telegram, use `/list
|
|
99
|
+
In Telegram, use `/menu`, `/list`, or the inline buttons:
|
|
72
100
|
|
|
73
|
-
```
|
|
101
|
+
```text
|
|
74
102
|
/list
|
|
75
103
|
→
|
|
76
|
-
1. pi-ui-extend (#12345) (leader)
|
|
77
|
-
2. opencode (#67890)
|
|
104
|
+
1. pi-ui-extend (#12345) (leader) [following] — idle
|
|
105
|
+
2. opencode (#67890) — streaming
|
|
78
106
|
3. other-repo (#99999)
|
|
79
107
|
|
|
80
|
-
|
|
108
|
+
Tap a button below, or use /use N.
|
|
81
109
|
```
|
|
82
110
|
|
|
83
|
-
```
|
|
111
|
+
```text
|
|
84
112
|
/use 2
|
|
85
|
-
→ ✅
|
|
113
|
+
→ ✅ Following: opencode (#67890)
|
|
86
114
|
```
|
|
87
115
|
|
|
88
116
|
`/use` accepts a 1-based index from `/list` or a substring of the id/label.
|
|
89
|
-
|
|
117
|
+
Assistant messages are streamed only from the followed session. Status changes
|
|
118
|
+
from other sessions still produce Telegram signals, so you can see when a
|
|
119
|
+
different session starts or finishes work without switching to it.
|
|
90
120
|
|
|
91
121
|
### Cleanup
|
|
92
122
|
|
|
@@ -97,31 +127,38 @@ unlink it automatically (bind fails → connect fails → unlink → retry).
|
|
|
97
127
|
|
|
98
128
|
## Telegram → pix
|
|
99
129
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
130
|
+
- Free text: forwarded to the followed pi session as a user message.
|
|
131
|
+
- `/menu`: show inline project/session picker buttons.
|
|
132
|
+
- `/list`: show all known pi sessions and mark followed.
|
|
133
|
+
- `/use N` or `/use X`: follow by index, id, or label substring.
|
|
134
|
+
- `/abort` or `/stop`: cancel current turn on followed session.
|
|
135
|
+
- `/compact`: trigger context compaction on followed session.
|
|
136
|
+
- `/status`: show idle / streaming state of followed session.
|
|
137
|
+
- `/clear`: best-effort delete known bot messages from the chat.
|
|
138
|
+
- `/say <msg>`: explicit send, for `/`-prefixed text.
|
|
139
|
+
- `/disconnect`: stop the bot cluster-wide.
|
|
140
|
+
- `/new`: not supported via extension API; run `/new` in pi.
|
|
141
|
+
- `/help`: show command list.
|
|
112
142
|
|
|
113
143
|
## Pix → Telegram
|
|
114
144
|
|
|
115
145
|
The leader subscribes to pix streaming events (its own + followers' via IPC)
|
|
116
|
-
and renders one Telegram message per agent turn — but
|
|
117
|
-
|
|
146
|
+
and renders one Telegram message per agent turn — but only assistant-visible
|
|
147
|
+
text from the followed session is streamed:
|
|
118
148
|
|
|
119
|
-
- `before_agent_start` → `user: <prompt>`
|
|
120
149
|
- `message_update` (`text_delta`) → appended to the active message, edited
|
|
121
150
|
in place at ~1.2 s throttle (Telegram rate-limit friendly).
|
|
122
|
-
- `tool_execution_start` → `🔧 tool: <args>` line.
|
|
123
|
-
- `tool_execution_end` → `✅ tool: <summary>` or `❌` on error.
|
|
124
151
|
- `agent_end` → final flush + `— done —` trailer.
|
|
152
|
+
- `agent_start` / `agent_end` from any known session → compact status signal
|
|
153
|
+
such as `🟡 repo (#pid) is streaming` or `🟢 repo (#pid) is idle`.
|
|
154
|
+
|
|
155
|
+
Tool calls, tool results, and thinking deltas are intentionally not mirrored
|
|
156
|
+
to Telegram.
|
|
157
|
+
|
|
158
|
+
Telegram does not expose a full private-chat history wipe API to bots. The
|
|
159
|
+
`/clear` command therefore deletes the messages the bot knows about in this
|
|
160
|
+
process, plus the `/clear` command message when Telegram allows it. Older
|
|
161
|
+
messages from previous bot runs may remain.
|
|
125
162
|
|
|
126
163
|
Messages are paginated at 4096 chars (Telegram's per-message limit).
|
|
127
164
|
Markdown is converted to Telegram HTML with `**bold**`, `*italic*`,
|
|
@@ -147,8 +184,8 @@ in the same config file, then `/reload` pi.
|
|
|
147
184
|
request open. If your network blocks Telegram, you'll see repeating
|
|
148
185
|
`[telegram-mirror] polling: …` errors in stderr and the bot will back
|
|
149
186
|
off up to 60 s between retries.
|
|
150
|
-
- On leader failover, the in-flight streaming output for the
|
|
151
|
-
is lost (the new leader's renderer starts empty).
|
|
187
|
+
- On leader failover, the in-flight streaming output for the followed turn
|
|
188
|
+
is lost (the new leader's renderer starts empty). The followed session also
|
|
152
189
|
resets to the new leader; run `/use N` to switch back to a follower.
|
|
153
190
|
- The cluster is single-host only (unix socket). To mirror across
|
|
154
191
|
machines, use separate bot tokens.
|
|
@@ -157,12 +194,10 @@ in the same config file, then `/reload` pi.
|
|
|
157
194
|
|
|
158
195
|
## Files
|
|
159
196
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
| `renderer.ts` | per-turn buffer, throttled edit, pagination |
|
|
168
|
-
| `format.ts` | markdown → Telegram HTML, chunking |
|
|
197
|
+
- `index.ts`: module factory, activation command, role selection, lifecycle.
|
|
198
|
+
- `bot.ts`: Telegram Bot API fetch client and long-poll loop.
|
|
199
|
+
- `ipc.ts`: unix socket JSON-lines IPC and leader election.
|
|
200
|
+
- `multiplexer.ts`: leader-side registry and active-instance routing.
|
|
201
|
+
- `events.ts`: pix event to sink adapters and context capture.
|
|
202
|
+
- `renderer.ts`: per-turn buffer, throttled edit, pagination.
|
|
203
|
+
- `format.ts`: markdown to Telegram HTML and chunking.
|
|
@@ -13,15 +13,23 @@
|
|
|
13
13
|
|
|
14
14
|
export interface TelegramUpdate {
|
|
15
15
|
update_id: number;
|
|
16
|
-
message?:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
chat: { id: number; type: string };
|
|
16
|
+
message?: TelegramIncomingMessage;
|
|
17
|
+
callback_query?: {
|
|
18
|
+
id: string;
|
|
20
19
|
from?: { id: number; first_name?: string; username?: string };
|
|
21
|
-
|
|
20
|
+
message?: TelegramIncomingMessage;
|
|
21
|
+
data?: string;
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export interface TelegramIncomingMessage {
|
|
26
|
+
message_id: number;
|
|
27
|
+
date: number;
|
|
28
|
+
chat: { id: number; type: string };
|
|
29
|
+
from?: { id: number; first_name?: string; username?: string };
|
|
30
|
+
text?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
export interface TelegramMessage {
|
|
26
34
|
message_id: number;
|
|
27
35
|
chat: { id: number };
|
|
@@ -35,16 +43,33 @@ export interface BotConfig {
|
|
|
35
43
|
timeoutMs?: number;
|
|
36
44
|
}
|
|
37
45
|
|
|
46
|
+
export interface TelegramInlineKeyboardButton {
|
|
47
|
+
text: string;
|
|
48
|
+
callback_data?: string;
|
|
49
|
+
url?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TelegramReplyMarkup {
|
|
53
|
+
inline_keyboard: TelegramInlineKeyboardButton[][];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TelegramBotCommand {
|
|
57
|
+
command: string;
|
|
58
|
+
description: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
38
61
|
interface SendOptions {
|
|
39
62
|
parseMode?: "HTML" | "MarkdownV2" | "Markdown";
|
|
40
63
|
disablePreview?: boolean;
|
|
41
64
|
silent?: boolean;
|
|
42
65
|
replyToMessageId?: number;
|
|
66
|
+
replyMarkup?: TelegramReplyMarkup;
|
|
43
67
|
}
|
|
44
68
|
|
|
45
69
|
interface EditOptions {
|
|
46
70
|
parseMode?: "HTML" | "MarkdownV2" | "Markdown";
|
|
47
71
|
disablePreview?: boolean;
|
|
72
|
+
replyMarkup?: TelegramReplyMarkup;
|
|
48
73
|
}
|
|
49
74
|
|
|
50
75
|
export class TelegramBot {
|
|
@@ -52,6 +77,7 @@ export class TelegramBot {
|
|
|
52
77
|
private readonly allowedChatId: number;
|
|
53
78
|
private readonly timeoutMs: number;
|
|
54
79
|
private readonly controller = new AbortController();
|
|
80
|
+
private readonly sentMessageIds = new Set<number>();
|
|
55
81
|
private polling = false;
|
|
56
82
|
private lastUpdateId = 0;
|
|
57
83
|
private consecutiveErrors = 0;
|
|
@@ -71,6 +97,14 @@ export class TelegramBot {
|
|
|
71
97
|
return this.allowedChatId;
|
|
72
98
|
}
|
|
73
99
|
|
|
100
|
+
get sentIds(): readonly number[] {
|
|
101
|
+
return [...this.sentMessageIds];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
forgetSentId(messageId: number): void {
|
|
105
|
+
this.sentMessageIds.delete(messageId);
|
|
106
|
+
}
|
|
107
|
+
|
|
74
108
|
isAllowedChat(chatId: number): boolean {
|
|
75
109
|
return chatId === this.allowedChatId;
|
|
76
110
|
}
|
|
@@ -79,26 +113,35 @@ export class TelegramBot {
|
|
|
79
113
|
return this.requestJson("GET", "getMe", undefined);
|
|
80
114
|
}
|
|
81
115
|
|
|
116
|
+
async setMyCommands(commands: TelegramBotCommand[]): Promise<void> {
|
|
117
|
+
await this.requestJson("POST", "setMyCommands", { commands });
|
|
118
|
+
}
|
|
119
|
+
|
|
82
120
|
async sendMessage(text: string, options: SendOptions = {}): Promise<TelegramMessage | undefined> {
|
|
83
|
-
|
|
121
|
+
const payload = await this.requestJson<{ ok: boolean; result?: TelegramMessage }>("POST", "sendMessage", {
|
|
84
122
|
chat_id: this.allowedChatId,
|
|
85
123
|
text,
|
|
86
124
|
parse_mode: options.parseMode ?? "HTML",
|
|
87
125
|
disable_web_page_preview: options.disablePreview ?? true,
|
|
88
126
|
disable_notification: options.silent ?? false,
|
|
127
|
+
reply_markup: options.replyMarkup,
|
|
89
128
|
...(options.replyToMessageId ? { reply_to_message_id: options.replyToMessageId } : {}),
|
|
90
129
|
});
|
|
130
|
+
if (payload.result?.message_id) this.sentMessageIds.add(payload.result.message_id);
|
|
131
|
+
return payload.result;
|
|
91
132
|
}
|
|
92
133
|
|
|
93
134
|
async editMessageText(messageId: number, text: string, options: EditOptions = {}): Promise<TelegramMessage | undefined> {
|
|
94
135
|
try {
|
|
95
|
-
|
|
136
|
+
const payload = await this.requestJson<{ ok: boolean; result?: TelegramMessage }>("POST", "editMessageText", {
|
|
96
137
|
chat_id: this.allowedChatId,
|
|
97
138
|
message_id: messageId,
|
|
98
139
|
text,
|
|
99
140
|
parse_mode: options.parseMode ?? "HTML",
|
|
100
141
|
disable_web_page_preview: options.disablePreview ?? true,
|
|
142
|
+
reply_markup: options.replyMarkup,
|
|
101
143
|
});
|
|
144
|
+
return payload.result;
|
|
102
145
|
} catch (error) {
|
|
103
146
|
// Telegram returns 400 "message is not modified" if content is identical.
|
|
104
147
|
// Treat that as success; surface everything else.
|
|
@@ -114,11 +157,30 @@ export class TelegramBot {
|
|
|
114
157
|
chat_id: this.allowedChatId,
|
|
115
158
|
message_id: messageId,
|
|
116
159
|
});
|
|
160
|
+
this.sentMessageIds.delete(messageId);
|
|
117
161
|
} catch {
|
|
118
162
|
// best-effort
|
|
119
163
|
}
|
|
120
164
|
}
|
|
121
165
|
|
|
166
|
+
async deleteKnownMessages(extraMessageIds: readonly number[] = []): Promise<{ attempted: number; deleted: number }> {
|
|
167
|
+
const ids = [...new Set([...this.sentMessageIds, ...extraMessageIds])].sort((a, b) => b - a);
|
|
168
|
+
let deleted = 0;
|
|
169
|
+
for (const id of ids) {
|
|
170
|
+
const before = this.sentMessageIds.has(id);
|
|
171
|
+
await this.deleteMessage(id);
|
|
172
|
+
if (before && !this.sentMessageIds.has(id)) deleted += 1;
|
|
173
|
+
}
|
|
174
|
+
return { attempted: ids.length, deleted };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async answerCallbackQuery(callbackQueryId: string, text?: string): Promise<void> {
|
|
178
|
+
await this.requestJson("POST", "answerCallbackQuery", {
|
|
179
|
+
callback_query_id: callbackQueryId,
|
|
180
|
+
text,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
122
184
|
/**
|
|
123
185
|
* Start long-polling loop. The callback receives every update from the
|
|
124
186
|
* allowed chat; updates from other chats are dropped silently.
|
|
@@ -146,7 +208,7 @@ export class TelegramBot {
|
|
|
146
208
|
}>("POST", "getUpdates", {
|
|
147
209
|
offset: this.lastUpdateId > 0 ? this.lastUpdateId + 1 : undefined,
|
|
148
210
|
timeout: Math.floor(this.timeoutMs / 1000),
|
|
149
|
-
allowed_updates: ["message"],
|
|
211
|
+
allowed_updates: ["message", "callback_query"],
|
|
150
212
|
});
|
|
151
213
|
|
|
152
214
|
this.consecutiveErrors = 0;
|
|
@@ -157,8 +219,9 @@ export class TelegramBot {
|
|
|
157
219
|
|
|
158
220
|
for (const update of payload.result) {
|
|
159
221
|
if (update.update_id > this.lastUpdateId) this.lastUpdateId = update.update_id;
|
|
160
|
-
|
|
161
|
-
if (
|
|
222
|
+
const chatId = getUpdateChatId(update);
|
|
223
|
+
if (chatId === undefined) continue;
|
|
224
|
+
if (!this.isAllowedChat(chatId)) continue;
|
|
162
225
|
try {
|
|
163
226
|
await onUpdate(update);
|
|
164
227
|
} catch (handlerError) {
|
|
@@ -215,6 +278,14 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
215
278
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
216
279
|
}
|
|
217
280
|
|
|
281
|
+
function getUpdateChatId(update: TelegramUpdate): number | undefined {
|
|
282
|
+
// For callback_query Telegram may omit `message` or return an
|
|
283
|
+
// inaccessible message for older inline keyboards. In private chats the
|
|
284
|
+
// callback sender id is the chat id, so use it as a fallback; otherwise the
|
|
285
|
+
// auth gate silently drops button presses.
|
|
286
|
+
return update.message?.chat.id ?? update.callback_query?.message?.chat.id ?? update.callback_query?.from?.id;
|
|
287
|
+
}
|
|
288
|
+
|
|
218
289
|
function removeUndefined(value: Record<string, unknown>): Record<string, unknown> {
|
|
219
290
|
const out: Record<string, unknown> = {};
|
|
220
291
|
for (const [k, v] of Object.entries(value)) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import type { RendererEvent } from "./renderer.js";
|
|
9
|
+
import type { RendererEvent, RendererInstance } from "./renderer.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Minimal sink for rendering events. The leader wires this to a Multiplexer
|
|
@@ -18,14 +18,8 @@ export interface RendererSink {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function registerPixEventHandlers(pi: ExtensionAPI, hooks: PixMirrorHooks): void {
|
|
21
|
-
pi.on("agent_start", () => {
|
|
22
|
-
hooks.getRenderer()?.push({ kind: "turn_start" });
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
pi.on("before_agent_start", (event) => {
|
|
26
|
-
const prompt = event?.prompt?.trim();
|
|
27
|
-
if (!prompt) return;
|
|
28
|
-
hooks.getRenderer()?.push({ kind: "info", text: `user: ${truncate(prompt, 200)}` });
|
|
21
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
22
|
+
hooks.getRenderer()?.push({ kind: "turn_start", instance: hooks.describeInstance(ctx as ExtensionContext | undefined) });
|
|
29
23
|
});
|
|
30
24
|
|
|
31
25
|
pi.on("message_update", (event) => {
|
|
@@ -35,30 +29,8 @@ export function registerPixEventHandlers(pi: ExtensionAPI, hooks: PixMirrorHooks
|
|
|
35
29
|
if (delta) hooks.getRenderer()?.push({ kind: "assistant_text", delta });
|
|
36
30
|
return;
|
|
37
31
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// dedupes further thinking events so we don't spam the chat
|
|
41
|
-
// with streaming thinking chunks.
|
|
42
|
-
hooks.getRenderer()?.push({ kind: "thinking" });
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
pi.on("tool_execution_start", (event) => {
|
|
48
|
-
hooks.getRenderer()?.push({
|
|
49
|
-
kind: "tool_start",
|
|
50
|
-
toolCallId: event.toolCallId,
|
|
51
|
-
toolName: event.toolName,
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
pi.on("tool_execution_end", (event) => {
|
|
56
|
-
hooks.getRenderer()?.push({
|
|
57
|
-
kind: "tool_end",
|
|
58
|
-
toolCallId: event.toolCallId,
|
|
59
|
-
toolName: event.toolName,
|
|
60
|
-
isError: event.isError,
|
|
61
|
-
});
|
|
32
|
+
// Ignore thinking and toolcall events. Telegram mirrors only the
|
|
33
|
+
// user-visible assistant answer, not internal reasoning/tools.
|
|
62
34
|
});
|
|
63
35
|
|
|
64
36
|
pi.on("agent_end", () => {
|
|
@@ -69,6 +41,7 @@ export function registerPixEventHandlers(pi: ExtensionAPI, hooks: PixMirrorHooks
|
|
|
69
41
|
|
|
70
42
|
export interface PixMirrorHooks {
|
|
71
43
|
getRenderer(): RendererSink | undefined;
|
|
44
|
+
describeInstance(ctx: ExtensionContext | undefined): RendererInstance | undefined;
|
|
72
45
|
notifyAgentEnd(): void;
|
|
73
46
|
}
|
|
74
47
|
|
|
@@ -87,8 +60,3 @@ export interface ContextCapture {
|
|
|
87
60
|
captureCompact(fn: () => void): void;
|
|
88
61
|
}
|
|
89
62
|
|
|
90
|
-
function truncate(value: string, max: number): string {
|
|
91
|
-
const collapsed = value.replace(/\s+/g, " ").trim();
|
|
92
|
-
if (collapsed.length <= max) return collapsed;
|
|
93
|
-
return `${collapsed.slice(0, Math.max(0, max - 1))}…`;
|
|
94
|
-
}
|