@pedrohnas/opencode-telegram 1.2.0 → 1.3.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.
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-41-55-194Z.yml +36 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-42-17-115Z.yml +36 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-43-15-988Z.yml +26 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-43-26-107Z.yml +26 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-45-03-139Z.yml +29 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-45-21-579Z.yml +29 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-45-48-051Z.yml +30 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-46-27-632Z.yml +33 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-46-46-519Z.yml +33 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-47-28-491Z.yml +349 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-47-34-834Z.yml +349 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-47-54-066Z.yml +168 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-48-19-667Z.yml +219 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-49-32-311Z.yml +221 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-49-57-109Z.yml +230 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-50-24-052Z.yml +235 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-50-41-148Z.yml +248 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-51-10-916Z.yml +234 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-51-28-271Z.yml +234 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-52-32-324Z.yml +234 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-52-47-801Z.yml +196 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-56-07-361Z.yml +203 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-56-35-534Z.yml +49 -0
- package/.claude/skills/playwright-cli/data/page-2026-02-09T01-57-04-658Z.yml +52 -0
- package/docs/AUDIT.md +193 -0
- package/docs/PROGRESS.md +188 -0
- package/docs/plans/phase-5.md +410 -0
- package/docs/plans/phase-6.5.md +426 -0
- package/docs/plans/phase-6.md +349 -0
- package/e2e/helpers.ts +34 -0
- package/e2e/phase-5.test.ts +295 -0
- package/e2e/phase-6.5.test.ts +239 -0
- package/e2e/phase-6.test.ts +302 -0
- package/package.json +6 -3
- package/src/api-server.test.ts +309 -0
- package/src/api-server.ts +201 -0
- package/src/bot.test.ts +354 -0
- package/src/bot.ts +200 -2
- package/src/config.test.ts +16 -0
- package/src/config.ts +4 -0
- package/src/event-bus.test.ts +337 -1
- package/src/event-bus.ts +83 -3
- package/src/handlers/agents.test.ts +122 -0
- package/src/handlers/agents.ts +93 -0
- package/src/handlers/media.test.ts +264 -0
- package/src/handlers/media.ts +168 -0
- package/src/handlers/models.test.ts +319 -0
- package/src/handlers/models.ts +191 -0
- package/src/index.ts +15 -0
- package/src/send/draft-stream.test.ts +76 -0
- package/src/send/draft-stream.ts +13 -1
- package/src/session-manager.test.ts +46 -0
- package/src/session-manager.ts +10 -1
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# Phase 5 — Model & Agent Selection
|
|
2
|
+
|
|
3
|
+
**Goal:** Let users pick which AI model and agent to use per-chat via inline
|
|
4
|
+
keyboards. The SDK already supports per-prompt overrides (`session.prompt({
|
|
5
|
+
model, agent })`), so this phase adds the UI layer and per-chat state.
|
|
6
|
+
|
|
7
|
+
## What This Phase Delivers
|
|
8
|
+
|
|
9
|
+
1. **`/model` command** — Shows a two-step inline keyboard:
|
|
10
|
+
first pick a provider, then pick a model from that provider.
|
|
11
|
+
The selection is stored as a per-chat override applied to every subsequent prompt.
|
|
12
|
+
|
|
13
|
+
2. **`/agent` command** — Shows available agents (non-hidden) as an inline keyboard.
|
|
14
|
+
Selecting one stores the override per-chat.
|
|
15
|
+
|
|
16
|
+
3. **Per-chat overrides in SessionEntry** — Optional `modelOverride` and
|
|
17
|
+
`agentOverride` fields, passed to `sdk.session.prompt()` on every message.
|
|
18
|
+
|
|
19
|
+
4. **Reset buttons** — Both `/model` and `/agent` include a "Reset to default"
|
|
20
|
+
button to clear the override.
|
|
21
|
+
|
|
22
|
+
## SDK Endpoints Used
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// Fetch all providers with their models
|
|
26
|
+
sdk.provider.list()
|
|
27
|
+
// → { all: Provider[] }
|
|
28
|
+
// Each Provider: { id, name, models: { [modelID]: Model } }
|
|
29
|
+
// Each Model: { id, name, cost?, limit?, capabilities?, ... }
|
|
30
|
+
|
|
31
|
+
// Fetch all agents
|
|
32
|
+
sdk.app.agents()
|
|
33
|
+
// → Agent[]
|
|
34
|
+
// Each Agent: { name, description?, mode, hidden?, ... }
|
|
35
|
+
|
|
36
|
+
// Per-prompt override (existing — just need to pass the fields)
|
|
37
|
+
sdk.session.prompt({
|
|
38
|
+
sessionID: string,
|
|
39
|
+
parts: [...],
|
|
40
|
+
model?: { providerID: string, modelID: string }, // ← override
|
|
41
|
+
agent?: string, // ← override
|
|
42
|
+
})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Architecture
|
|
46
|
+
|
|
47
|
+
### Model Selection Flow
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
/model
|
|
51
|
+
→ sdk.provider.list()
|
|
52
|
+
→ Filter providers that have ≥1 model
|
|
53
|
+
→ Show provider keyboard (1 per row, max 8)
|
|
54
|
+
→ callback_data: "mdl:{providerID}"
|
|
55
|
+
|
|
56
|
+
User clicks provider
|
|
57
|
+
→ sdk.provider.list() (re-fetch for models)
|
|
58
|
+
→ Show model keyboard for that provider + "⬅ Back" button
|
|
59
|
+
→ callback_data: "mdl:{providerID}:{modelID}" or "mdl:back"
|
|
60
|
+
|
|
61
|
+
User clicks model
|
|
62
|
+
→ sessionManager.get(chatKey) → set modelOverride
|
|
63
|
+
→ editMessageText: "Model set to: {providerName} / {modelName}"
|
|
64
|
+
|
|
65
|
+
User clicks "⬅ Back"
|
|
66
|
+
→ Re-show provider list
|
|
67
|
+
|
|
68
|
+
User clicks "Reset to default"
|
|
69
|
+
→ Clear modelOverride from SessionEntry
|
|
70
|
+
→ editMessageText: "Model reset to default."
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Agent Selection Flow
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
/agent
|
|
77
|
+
→ sdk.app.agents()
|
|
78
|
+
→ Filter: not hidden
|
|
79
|
+
→ Show agent keyboard (1 per row) + "Reset to default" button
|
|
80
|
+
→ callback_data: "agt:{agentName}" or "agt:reset"
|
|
81
|
+
|
|
82
|
+
User clicks agent
|
|
83
|
+
→ sessionManager.get(chatKey) → set agentOverride
|
|
84
|
+
→ editMessageText: "Agent set to: {agentName}"
|
|
85
|
+
|
|
86
|
+
User clicks reset
|
|
87
|
+
→ Clear agentOverride from SessionEntry
|
|
88
|
+
→ editMessageText: "Agent reset to default."
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Override Passing in handleMessage
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
BEFORE:
|
|
95
|
+
sdk.session.prompt({
|
|
96
|
+
sessionID: entry.sessionId,
|
|
97
|
+
parts: [{ type: "text", text }],
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
AFTER:
|
|
101
|
+
sdk.session.prompt({
|
|
102
|
+
sessionID: entry.sessionId,
|
|
103
|
+
parts: [{ type: "text", text }],
|
|
104
|
+
...(entry.modelOverride && { model: entry.modelOverride }),
|
|
105
|
+
...(entry.agentOverride && { agent: entry.agentOverride }),
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Callback Data Design
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
Model callbacks:
|
|
113
|
+
mdl:{providerID} → show models for provider
|
|
114
|
+
mdl:{providerID}:{modelID} → select model
|
|
115
|
+
mdl:back → back to provider list
|
|
116
|
+
mdl:reset → clear model override
|
|
117
|
+
|
|
118
|
+
Examples:
|
|
119
|
+
"mdl:anthropic" = 14 bytes ✓
|
|
120
|
+
"mdl:anthropic:claude-sonnet-4-5-20250929" = 43 bytes ✓
|
|
121
|
+
"mdl:back" = 8 bytes ✓
|
|
122
|
+
"mdl:reset" = 9 bytes ✓
|
|
123
|
+
|
|
124
|
+
Worst case: "mdl:" + 20-char provider + ":" + 38-char model = 63 bytes ✓
|
|
125
|
+
|
|
126
|
+
Agent callbacks:
|
|
127
|
+
agt:{agentName} → select agent
|
|
128
|
+
agt:reset → clear agent override
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
"agt:code" = 8 bytes ✓
|
|
132
|
+
"agt:reset" = 9 bytes ✓
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## SessionEntry Changes
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
export type SessionEntry = {
|
|
139
|
+
sessionId: string
|
|
140
|
+
directory: string
|
|
141
|
+
createdAt: number
|
|
142
|
+
lastAccessAt: number
|
|
143
|
+
modelOverride?: { providerID: string; modelID: string } // ← NEW
|
|
144
|
+
agentOverride?: string // ← NEW
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Grammy Middleware Stack (updated)
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
1. allowlistMiddleware(config.allowedUsers)
|
|
152
|
+
2. bot.command("start", ...)
|
|
153
|
+
3. bot.command("new", ...)
|
|
154
|
+
4. bot.command("list", ...)
|
|
155
|
+
5. bot.command("rename", ...)
|
|
156
|
+
6. bot.command("delete", ...)
|
|
157
|
+
7. bot.command("info", ...)
|
|
158
|
+
8. bot.command("history", ...)
|
|
159
|
+
9. bot.command("summarize", ...)
|
|
160
|
+
10. bot.command("model", ...) ← NEW
|
|
161
|
+
11. bot.command("agent", ...) ← NEW
|
|
162
|
+
12. bot.command("cancel", ...)
|
|
163
|
+
13. bot.on("callback_query:data", ...) ← Extended with mdl: and agt: prefixes
|
|
164
|
+
14. bot.on("message:text", ...) ← Modified (pass overrides to prompt)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## New Files
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
src/
|
|
171
|
+
handlers/
|
|
172
|
+
models.ts ← formatProviderList, formatModelList,
|
|
173
|
+
parseModelCallback, formatCurrentModel,
|
|
174
|
+
handleModel, handleModelSelect
|
|
175
|
+
models.test.ts ← ~18 tests
|
|
176
|
+
agents.ts ← formatAgentList, parseAgentCallback,
|
|
177
|
+
handleAgent, handleAgentSelect
|
|
178
|
+
agents.test.ts ← ~10 tests
|
|
179
|
+
e2e/
|
|
180
|
+
phase-5.test.ts ← 3 E2E tests
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Modified Files
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
src/
|
|
187
|
+
session-manager.ts ← Add modelOverride? and agentOverride? to SessionEntry
|
|
188
|
+
session-manager.test.ts ← 3 new tests for override fields
|
|
189
|
+
bot.ts ← /model, /agent commands, mdl: + agt: callback routing,
|
|
190
|
+
pass overrides in handleMessage prompt call
|
|
191
|
+
bot.test.ts ← ~6 new tests (override passing, new commands)
|
|
192
|
+
index.ts ← Add model/agent to setMyCommands
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## TDD Execution Order (bottom-up by dependency)
|
|
196
|
+
|
|
197
|
+
### Group A — Foundational / Independent Pieces
|
|
198
|
+
|
|
199
|
+
#### A1. session-manager.ts — model/agent override fields (3 new tests)
|
|
200
|
+
|
|
201
|
+
Add optional fields to SessionEntry type. No logic changes needed — `set()`
|
|
202
|
+
already stores the full entry object and `get()` returns it.
|
|
203
|
+
|
|
204
|
+
Tests:
|
|
205
|
+
1. `set/get` with `modelOverride` preserves value
|
|
206
|
+
2. `set/get` with `agentOverride` preserves value
|
|
207
|
+
3. `getOrCreate` returns entry without overrides by default
|
|
208
|
+
|
|
209
|
+
#### A2. handlers/models.ts — model selection (18 tests)
|
|
210
|
+
|
|
211
|
+
Pure functions + thin async handlers.
|
|
212
|
+
|
|
213
|
+
**Pure functions:**
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
formatProviderList(providers: any[]): { text: string; reply_markup: any }
|
|
217
|
+
// → Inline keyboard: one row per provider, button text = provider name
|
|
218
|
+
// → callback_data = "mdl:{providerID}"
|
|
219
|
+
// → Last row: "Reset to default" button (mdl:reset) if override active
|
|
220
|
+
|
|
221
|
+
formatModelList(providerID: string, providerName: string, models: any[]): { text: string; reply_markup: any }
|
|
222
|
+
// → Inline keyboard: one row per model, button text = model name
|
|
223
|
+
// → callback_data = "mdl:{providerID}:{modelID}"
|
|
224
|
+
// → Last row: "⬅ Back" button (mdl:back)
|
|
225
|
+
|
|
226
|
+
parseModelCallback(data: string):
|
|
227
|
+
| { type: "provider"; providerID: string }
|
|
228
|
+
| { type: "model"; providerID: string; modelID: string }
|
|
229
|
+
| { type: "back" }
|
|
230
|
+
| { type: "reset" }
|
|
231
|
+
| null
|
|
232
|
+
|
|
233
|
+
formatCurrentModel(override?: { providerID: string; modelID: string }): string
|
|
234
|
+
// → "Current model: {providerID}/{modelID}" or "Using default model"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Async handlers:**
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
handleModel(params: { sdk, sessionManager, chatKey }):
|
|
241
|
+
Promise<{ text: string; reply_markup: any }>
|
|
242
|
+
// → Fetches providers, prepends current model text, returns provider keyboard
|
|
243
|
+
|
|
244
|
+
handleModelSelect(params: { chatKey, providerID, modelID, sessionManager }):
|
|
245
|
+
Promise<string>
|
|
246
|
+
// → Stores override in SessionEntry, returns confirmation text
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Tests:
|
|
250
|
+
1. `parseModelCallback("mdl:anthropic")` → `{ type: "provider", providerID: "anthropic" }`
|
|
251
|
+
2. `parseModelCallback("mdl:anthropic:claude-sonnet")` → `{ type: "model", providerID: "anthropic", modelID: "claude-sonnet" }`
|
|
252
|
+
3. `parseModelCallback("mdl:back")` → `{ type: "back" }`
|
|
253
|
+
4. `parseModelCallback("mdl:reset")` → `{ type: "reset" }`
|
|
254
|
+
5. `parseModelCallback("invalid")` → `null`
|
|
255
|
+
6. `parseModelCallback("mdl:")` → `null` (empty after prefix)
|
|
256
|
+
7. `formatProviderList` with 2 providers → 2 button rows
|
|
257
|
+
8. `formatProviderList` with 0 providers → "No providers available."
|
|
258
|
+
9. `formatProviderList` filters providers with 0 models
|
|
259
|
+
10. `formatModelList` with 3 models → 3 button rows + back button
|
|
260
|
+
11. `formatModelList` with 0 models → "No models available." + back button
|
|
261
|
+
12. `formatModelList` callback_data includes providerID and modelID
|
|
262
|
+
13. `formatCurrentModel` with override → shows provider/model
|
|
263
|
+
14. `formatCurrentModel` without override → "Using default model"
|
|
264
|
+
15. `handleModel` fetches providers and returns keyboard
|
|
265
|
+
16. `handleModel` returns "No providers" when list is empty
|
|
266
|
+
17. `handleModelSelect` stores override in sessionManager
|
|
267
|
+
18. `handleModelSelect` returns confirmation message
|
|
268
|
+
|
|
269
|
+
#### A3. handlers/agents.ts — agent selection (10 tests)
|
|
270
|
+
|
|
271
|
+
**Pure functions:**
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
formatAgentList(agents: any[]): { text: string; reply_markup: any }
|
|
275
|
+
// → Inline keyboard: one row per agent (non-hidden), button text = name
|
|
276
|
+
// → callback_data = "agt:{name}"
|
|
277
|
+
// → Last row: "Reset to default" button (agt:reset)
|
|
278
|
+
|
|
279
|
+
parseAgentCallback(data: string):
|
|
280
|
+
| { name: string }
|
|
281
|
+
| { action: "reset" }
|
|
282
|
+
| null
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**Async handlers:**
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
handleAgent(params: { sdk, sessionManager, chatKey }):
|
|
289
|
+
Promise<{ text: string; reply_markup: any }>
|
|
290
|
+
|
|
291
|
+
handleAgentSelect(params: { chatKey, agentName, sessionManager }):
|
|
292
|
+
Promise<string>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Tests:
|
|
296
|
+
1. `parseAgentCallback("agt:code")` → `{ name: "code" }`
|
|
297
|
+
2. `parseAgentCallback("agt:reset")` → `{ action: "reset" }`
|
|
298
|
+
3. `parseAgentCallback("invalid")` → `null`
|
|
299
|
+
4. `parseAgentCallback("agt:")` → `null`
|
|
300
|
+
5. `formatAgentList` with 2 agents → 2 rows + reset row
|
|
301
|
+
6. `formatAgentList` filters hidden agents
|
|
302
|
+
7. `formatAgentList` with 0 agents → "No agents available."
|
|
303
|
+
8. `handleAgent` fetches agents and returns keyboard
|
|
304
|
+
9. `handleAgentSelect` stores override in sessionManager
|
|
305
|
+
10. `handleAgentSelect` returns confirmation message
|
|
306
|
+
|
|
307
|
+
### Group B — Wiring (bot.ts + index.ts)
|
|
308
|
+
|
|
309
|
+
#### B4. bot.ts — New commands, callback routing, override passing (6 new tests)
|
|
310
|
+
|
|
311
|
+
Changes:
|
|
312
|
+
- Add `/model` command → calls `handleModel`, replies with keyboard
|
|
313
|
+
- Add `/agent` command → calls `handleAgent`, replies with keyboard
|
|
314
|
+
- Add `mdl:` callback routing (provider select → show models, model select → store, back → providers, reset → clear)
|
|
315
|
+
- Add `agt:` callback routing (select → store, reset → clear)
|
|
316
|
+
- Modify `handleMessage` prompt call to spread `modelOverride` and `agentOverride`
|
|
317
|
+
|
|
318
|
+
New tests:
|
|
319
|
+
1. `handleMessage` passes `modelOverride` to `sdk.session.prompt` when set
|
|
320
|
+
2. `handleMessage` passes `agentOverride` to `sdk.session.prompt` when set
|
|
321
|
+
3. `handleMessage` passes no model/agent when overrides not set
|
|
322
|
+
4. `handleMessage` passes both overrides simultaneously
|
|
323
|
+
5. (integration covered by E2E for /model and /agent commands)
|
|
324
|
+
|
|
325
|
+
#### B5. index.ts — Command menu update
|
|
326
|
+
|
|
327
|
+
Add to `setMyCommands` array:
|
|
328
|
+
```ts
|
|
329
|
+
{ command: "model", description: "Select model" },
|
|
330
|
+
{ command: "agent", description: "Select agent" },
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Group C — E2E Tests
|
|
334
|
+
|
|
335
|
+
#### C6. E2E: phase-5.test.ts (3 tests) + full regression
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
describe("Phase 5 — Model & Agent Selection", () => {
|
|
339
|
+
test("/model shows provider buttons", async () => {
|
|
340
|
+
const reply = await sendAndWait(client, BOT, "/model", 15000)
|
|
341
|
+
assertHasButtons(reply)
|
|
342
|
+
}, 30000)
|
|
343
|
+
|
|
344
|
+
test("/agent shows agent buttons", async () => {
|
|
345
|
+
const reply = await sendAndWait(client, BOT, "/agent", 15000)
|
|
346
|
+
assertHasButtons(reply)
|
|
347
|
+
}, 30000)
|
|
348
|
+
|
|
349
|
+
test("regression: text message still works", async () => {
|
|
350
|
+
const reply = await sendAndWait(client, BOT, "Say hello", 60000)
|
|
351
|
+
expect(reply.text.length).toBeGreaterThan(0)
|
|
352
|
+
}, 90000)
|
|
353
|
+
})
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Full regression: phases 0-5 E2E all pass.
|
|
357
|
+
|
|
358
|
+
## Edge Cases
|
|
359
|
+
|
|
360
|
+
### Model Selection
|
|
361
|
+
- Provider with 0 models → filtered out of list
|
|
362
|
+
- Model ID with colons in it → `parseModelCallback` splits on first two colons only
|
|
363
|
+
- `modelOverride` set but provider deleted from server → prompt fails gracefully (SDK error, not bot crash)
|
|
364
|
+
- User switches session via `/list` → override stays (it's per-chatKey, not per-session)
|
|
365
|
+
|
|
366
|
+
### Agent Selection
|
|
367
|
+
- Hidden agents → filtered out of keyboard
|
|
368
|
+
- Agent name with special chars → callback_data must be safe ASCII
|
|
369
|
+
- No agents configured → "No agents available."
|
|
370
|
+
|
|
371
|
+
### Override Persistence
|
|
372
|
+
- Overrides live in SessionManager (in-memory) → lost on bot restart
|
|
373
|
+
- This is acceptable: model/agent preference is lightweight, user re-selects if needed
|
|
374
|
+
- SessionEntry eviction (LRU/TTL) also clears overrides — consistent behavior
|
|
375
|
+
|
|
376
|
+
## Acceptance Criteria
|
|
377
|
+
|
|
378
|
+
- [ ] `/model` shows provider list with inline keyboard
|
|
379
|
+
- [ ] Selecting provider shows model list for that provider
|
|
380
|
+
- [ ] Selecting model stores override and shows confirmation
|
|
381
|
+
- [ ] "Reset to default" clears model override
|
|
382
|
+
- [ ] "⬅ Back" returns to provider list
|
|
383
|
+
- [ ] `/agent` shows agent list with inline keyboard
|
|
384
|
+
- [ ] Selecting agent stores override and shows confirmation
|
|
385
|
+
- [ ] "Reset to default" clears agent override
|
|
386
|
+
- [ ] Hidden agents excluded from list
|
|
387
|
+
- [ ] Next prompt uses selected model/agent override
|
|
388
|
+
- [ ] Overrides are per-chat (different chats have independent settings)
|
|
389
|
+
- [ ] Commands appear in Telegram "/" menu
|
|
390
|
+
- [ ] `bun test src/` passes (all unit tests)
|
|
391
|
+
- [ ] `bun test ./e2e/phase-5.test.ts` passes
|
|
392
|
+
- [ ] All Phase 0-4 E2E tests still pass (regression)
|
|
393
|
+
|
|
394
|
+
## Estimated Scope
|
|
395
|
+
|
|
396
|
+
- 2 new source files + 2 test files + 1 E2E test file
|
|
397
|
+
- ~200-250 LOC (src) + ~350-400 LOC (tests)
|
|
398
|
+
- Modified: session-manager.ts, session-manager.test.ts, bot.ts, bot.test.ts, index.ts
|
|
399
|
+
|
|
400
|
+
### Test Count Estimate
|
|
401
|
+
|
|
402
|
+
| File | New Tests |
|
|
403
|
+
|------|-----------|
|
|
404
|
+
| session-manager.test.ts | 3 |
|
|
405
|
+
| handlers/models.test.ts | 18 |
|
|
406
|
+
| handlers/agents.test.ts | 10 |
|
|
407
|
+
| bot.test.ts | 4 |
|
|
408
|
+
| **Unit total** | **~35 new → ~222 total** |
|
|
409
|
+
| E2E phase-5.test.ts | 3 |
|
|
410
|
+
| **E2E total** | **3 new → ~20 total** |
|