switchroom 0.16.5 → 0.16.6

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.
@@ -51674,8 +51674,8 @@ import { existsSync, readFileSync } from "node:fs";
51674
51674
  import { dirname, join } from "node:path";
51675
51675
 
51676
51676
  // src/build-info.ts
51677
- var VERSION = "0.16.5";
51678
- var COMMIT_SHA = "6821d14b";
51677
+ var VERSION = "0.16.6";
51678
+ var COMMIT_SHA = "925f5798";
51679
51679
 
51680
51680
  // src/cli/resolve-version.ts
51681
51681
  function readPackageVersion() {
@@ -69841,7 +69841,7 @@ x-litellm-tags: service:hindsight`);
69841
69841
  `--pids-limit=${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
69842
69842
  `--shm-size=${HINDSIGHT_DEFAULT_SHM_SIZE}`,
69843
69843
  "--health-cmd",
69844
- HINDSIGHT_HEALTHCHECK_CMD,
69844
+ litellm ? `python3 -c 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:${apiPort}/health",timeout=4).getcode()==200 else 1)'` : HINDSIGHT_HEALTHCHECK_CMD,
69845
69845
  "--health-interval",
69846
69846
  "30s",
69847
69847
  "--health-timeout",
@@ -22587,7 +22587,7 @@ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:f
22587
22587
  import { dirname as dirname4, join as join2 } from "node:path";
22588
22588
 
22589
22589
  // src/build-info.ts
22590
- var VERSION = "0.16.5";
22590
+ var VERSION = "0.16.6";
22591
22591
 
22592
22592
  // src/cli/resolve-version.ts
22593
22593
  function readPackageVersion() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.16.5",
3
+ "version": "0.16.6",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46101,6 +46101,7 @@ var MODEL_CALLBACK_PREFIX = "mdl:";
46101
46101
  var MODEL_CALLBACK_SELECT = "mdl:s:";
46102
46102
  var MODEL_CALLBACK_REFRESH = "mdl:r";
46103
46103
  var MODEL_CALLBACK_SR = "mdl:sr:";
46104
+ var MODEL_CALLBACK_HEADER = "mdl:h";
46104
46105
  var SR_MODEL_LABELS = {
46105
46106
  "sr-gemini-2.5-pro": "Gemini 2.5 Pro",
46106
46107
  "sr-gemini-2.5-flash": "Gemini 2.5 Flash",
@@ -46126,20 +46127,28 @@ function busyReply(deps) {
46126
46127
  html: true
46127
46128
  };
46128
46129
  }
46130
+ function headerRow(label) {
46131
+ return [{ text: label, callback_data: MODEL_CALLBACK_HEADER }];
46132
+ }
46129
46133
  function menuKeyboard(claudeOptions, srOptions) {
46130
- const rows = claudeOptions.map((o) => [
46131
- {
46134
+ const hasBothGroups = claudeOptions.length > 0 && srOptions.length > 0;
46135
+ const rows = [];
46136
+ if (hasBothGroups)
46137
+ rows.push(headerRow("\u2500\u2500 Claude (Max / Pro subscription) \u2500\u2500"));
46138
+ for (const o of claudeOptions) {
46139
+ rows.push([{
46132
46140
  text: o.current ? `\u2705 ${o.label}` : o.label,
46133
46141
  callback_data: modelSelectCallbackData(o.label)
46134
- }
46135
- ]);
46136
- for (const o of srOptions) {
46137
- rows.push([
46138
- {
46142
+ }]);
46143
+ }
46144
+ if (srOptions.length > 0) {
46145
+ rows.push(headerRow("\u2500\u2500 OpenRouter / external \u2500\u2500"));
46146
+ for (const o of srOptions) {
46147
+ rows.push([{
46139
46148
  text: `\uD83C\uDF10 ${srFriendlyLabel(o.label)}`,
46140
46149
  callback_data: `${MODEL_CALLBACK_SR}${o.label}`
46141
- }
46142
- ]);
46150
+ }]);
46151
+ }
46143
46152
  }
46144
46153
  rows.push([{ text: "\uD83D\uDD04 Refresh", callback_data: MODEL_CALLBACK_REFRESH }]);
46145
46154
  return rows;
@@ -46174,8 +46183,9 @@ async function buildModelMenu(deps) {
46174
46183
  if (quota)
46175
46184
  lines.push(`Quota: ${deps.escapeHtml(quota)}`);
46176
46185
  lines.push("", "Tap a model to switch the <b>live session</b>:");
46177
- if (srOptions.length > 0)
46178
- lines.push("\uD83C\uDF10 = non-Anthropic via LiteLLM (session only)");
46186
+ if (srOptions.length > 0) {
46187
+ lines.push("Claude models use your Max/Pro subscription. \uD83C\uDF10 models are billed separately via OpenRouter.");
46188
+ }
46179
46189
  lines.push(PERSIST_NOTE);
46180
46190
  return { text: lines.join(`
46181
46191
  `), html: true, keyboard: menuKeyboard(claudeOptions, srOptions) };
@@ -46184,6 +46194,9 @@ async function handleModelMenuCallback(data, deps) {
46184
46194
  if (data === MODEL_CALLBACK_REFRESH) {
46185
46195
  return { answer: "Refreshed", reply: await buildModelMenu(deps) };
46186
46196
  }
46197
+ if (data === MODEL_CALLBACK_HEADER) {
46198
+ return { answer: "Tap a model in this section to switch", reply: { text: "", html: true }, toastOnly: true };
46199
+ }
46187
46200
  if (data.startsWith(MODEL_CALLBACK_SR)) {
46188
46201
  const srName = data.slice(MODEL_CALLBACK_SR.length);
46189
46202
  if (!isValidModelArg(srName)) {
@@ -55998,10 +56011,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
55998
56011
  }
55999
56012
 
56000
56013
  // ../src/build-info.ts
56001
- var VERSION = "0.16.5";
56002
- var COMMIT_SHA = "6821d14b";
56003
- var COMMIT_DATE = "2026-06-28T02:45:06Z";
56004
- var LATEST_PR = 2608;
56014
+ var VERSION = "0.16.6";
56015
+ var COMMIT_SHA = "925f5798";
56016
+ var COMMIT_DATE = "2026-06-28T03:53:56Z";
56017
+ var LATEST_PR = 2611;
56005
56018
  var COMMITS_AHEAD_OF_TAG = 0;
56006
56019
 
56007
56020
  // gateway/boot-version.ts
@@ -68141,6 +68154,10 @@ bot.on("callback_query:data", async (ctx) => {
68141
68154
  await ctx.answerCallbackQuery({ text: "\u23F3 Agent is mid-turn \u2014 tap again when it\u2019s idle", show_alert: false }).catch(() => {});
68142
68155
  return;
68143
68156
  }
68157
+ if (data === MODEL_CALLBACK_HEADER) {
68158
+ await ctx.answerCallbackQuery({ text: "Tap a model in this section to switch" }).catch(() => {});
68159
+ return;
68160
+ }
68144
68161
  await ctx.answerCallbackQuery({ text: "Switching\u2026" }).catch(() => {});
68145
68162
  try {
68146
68163
  const outcome = await handleModelMenuCallback(data, modelDeps);
@@ -276,6 +276,7 @@ import {
276
276
  buildModelMenu,
277
277
  handleModelMenuCallback,
278
278
  MODEL_CALLBACK_PREFIX,
279
+ MODEL_CALLBACK_HEADER,
279
280
  type ModelMenuDeps,
280
281
  type ModelCommandDeps,
281
282
  type ModelMenuReply,
@@ -20721,6 +20722,11 @@ bot.on('callback_query:data', async ctx => {
20721
20722
  // drive behind the pane lock. A callback can only be answered once, so
20722
20723
  // the rich result (what was set / why it failed) is conveyed by the
20723
20724
  // message edit — which now ALWAYS keeps the menu buttons.
20725
+ // Header rows are section labels — informational, no model switch.
20726
+ if (data === MODEL_CALLBACK_HEADER) {
20727
+ await ctx.answerCallbackQuery({ text: 'Tap a model in this section to switch' }).catch(() => {})
20728
+ return
20729
+ }
20724
20730
  await ctx.answerCallbackQuery({ text: 'Switching…' }).catch(() => {})
20725
20731
  try {
20726
20732
  const outcome = await handleModelMenuCallback(data, modelDeps)
@@ -235,6 +235,8 @@ const MODEL_CALLBACK_SELECT = 'mdl:s:'
235
235
  export const MODEL_CALLBACK_REFRESH = 'mdl:r'
236
236
  /** Callback prefix for sr-* (LiteLLM non-Anthropic) model selection. */
237
237
  export const MODEL_CALLBACK_SR = 'mdl:sr:'
238
+ /** Callback for section-header rows — shows an informational toast, no action. */
239
+ export const MODEL_CALLBACK_HEADER = 'mdl:h'
238
240
 
239
241
  /**
240
242
  * Friendly display names for sr-* synthetic model names. An sr-* model in
@@ -296,29 +298,38 @@ function busyReply(deps: Pick<ModelMenuDeps, 'escapeHtml'>): ModelMenuReply {
296
298
  }
297
299
  }
298
300
 
301
+ function headerRow(label: string): ModelMenuKeyboardButton[] {
302
+ return [{ text: label, callback_data: MODEL_CALLBACK_HEADER }]
303
+ }
304
+
299
305
  function menuKeyboard(
300
306
  claudeOptions: ModelPickerOption[],
301
307
  srOptions: ModelPickerOption[],
302
308
  ): ModelMenuKeyboardButton[][] {
303
- // One option per row (labels + render cleanly at full width on
304
- // mobile), refresh on a trailing row.
305
- const rows: ModelMenuKeyboardButton[][] = claudeOptions.map((o) => [
306
- {
309
+ const hasBothGroups = claudeOptions.length > 0 && srOptions.length > 0
310
+ const rows: ModelMenuKeyboardButton[][] = []
311
+
312
+ if (hasBothGroups) rows.push(headerRow('── Claude (Max / Pro subscription) ──'))
313
+ for (const o of claudeOptions) {
314
+ rows.push([{
307
315
  text: o.current ? `✅ ${o.label}` : o.label,
308
316
  callback_data: modelSelectCallbackData(o.label),
309
- },
310
- ])
317
+ }])
318
+ }
319
+
311
320
  // sr-* models are non-Anthropic (routed via LiteLLM → OpenRouter).
312
321
  // Selection uses text-inject rather than cursor-nav — more reliable
313
322
  // when the picker has many models (GATEWAY_MODEL_DISCOVERY=1).
314
- for (const o of srOptions) {
315
- rows.push([
316
- {
323
+ if (srOptions.length > 0) {
324
+ rows.push(headerRow('── OpenRouter / external ──'))
325
+ for (const o of srOptions) {
326
+ rows.push([{
317
327
  text: `🌐 ${srFriendlyLabel(o.label)}`,
318
328
  callback_data: `${MODEL_CALLBACK_SR}${o.label}`,
319
- },
320
- ])
329
+ }])
330
+ }
321
331
  }
332
+
322
333
  rows.push([{ text: '🔄 Refresh', callback_data: MODEL_CALLBACK_REFRESH }])
323
334
  return rows
324
335
  }
@@ -367,7 +378,9 @@ export async function buildModelMenu(
367
378
  }
368
379
  if (quota) lines.push(`Quota: ${deps.escapeHtml(quota)}`)
369
380
  lines.push('', 'Tap a model to switch the <b>live session</b>:')
370
- if (srOptions.length > 0) lines.push('🌐 = non-Anthropic via LiteLLM (session only)')
381
+ if (srOptions.length > 0) {
382
+ lines.push('Claude models use your Max/Pro subscription. 🌐 models are billed separately via OpenRouter.')
383
+ }
371
384
  lines.push(PERSIST_NOTE)
372
385
 
373
386
  return { text: lines.join('\n'), html: true, keyboard: menuKeyboard(claudeOptions, srOptions) }
@@ -410,6 +423,13 @@ export async function handleModelMenuCallback(
410
423
  return { answer: 'Refreshed', reply: await buildModelMenu(deps) }
411
424
  }
412
425
 
426
+ if (data === MODEL_CALLBACK_HEADER) {
427
+ // Section-header row — the gateway handles this with a direct answerCallbackQuery
428
+ // before calling this function, so this branch is dead in practice. Guard
429
+ // for callers that skip gateway.ts (tests, future refactors).
430
+ return { answer: 'Tap a model in this section to switch', reply: { text: '', html: true }, toastOnly: true }
431
+ }
432
+
413
433
  // sr-* model tap: text-inject `/model sr-<name>` rather than cursor-nav.
414
434
  // Text-inject is more reliable when the picker has many models; sr-* names
415
435
  // are safe (no entry in model_group_settings → no OAuth forwarding). See I6.
@@ -255,6 +255,7 @@ import {
255
255
  sessionModelFromConfirmation,
256
256
  classifyDiscoveredOptions,
257
257
  MODEL_CALLBACK_REFRESH,
258
+ MODEL_CALLBACK_HEADER,
258
259
  MODEL_CALLBACK_SR,
259
260
  SR_MODEL_LABELS,
260
261
  type ModelMenuDeps,
@@ -506,16 +507,43 @@ describe("buildModelMenu — with sr-* models", () => {
506
507
  expect(srButton?.callback_data).toBe(`${MODEL_CALLBACK_SR}sr-gemini-2.5-pro`);
507
508
  });
508
509
 
509
- it("shows 🌐 = non-Anthropic legend when sr-* models are present", async () => {
510
+ it("shows section header rows when both claude and sr-* models present", async () => {
510
511
  const { deps } = makeMenuDepsWithSr();
511
512
  const menu = await buildModelMenu(deps);
512
- expect(menu.text).toContain("🌐 = non-Anthropic");
513
+ const allButtons = menu.keyboard!.flat();
514
+ const headers = allButtons.filter((b) => b.callback_data === MODEL_CALLBACK_HEADER);
515
+ expect(headers.length).toBe(2);
516
+ expect(headers[0].text).toContain("Claude");
517
+ expect(headers[1].text).toContain("OpenRouter");
518
+ });
519
+
520
+ it("no section headers when only claude models (no sr-*)", async () => {
521
+ const { deps } = makeMenuDeps();
522
+ const menu = await buildModelMenu(deps);
523
+ const allButtons = (menu.keyboard ?? []).flat();
524
+ const headers = allButtons.filter((b) => b.callback_data === MODEL_CALLBACK_HEADER);
525
+ expect(headers.length).toBe(0);
526
+ });
527
+
528
+ it("header-row tap returns toastOnly without inject or model change", async () => {
529
+ const { deps, injectCalls } = makeMenuDepsWithSr();
530
+ const out = await handleModelMenuCallback(MODEL_CALLBACK_HEADER, deps);
531
+ expect(out.toastOnly).toBe(true);
532
+ expect(out.selectedModel).toBeUndefined();
533
+ expect(injectCalls).toHaveLength(0);
534
+ });
535
+
536
+ it("shows subscription/OpenRouter legend when sr-* models are present", async () => {
537
+ const { deps } = makeMenuDepsWithSr();
538
+ const menu = await buildModelMenu(deps);
539
+ expect(menu.text).toContain("Max/Pro subscription");
540
+ expect(menu.text).toContain("OpenRouter");
513
541
  });
514
542
 
515
543
  it("no legend when no sr-* models in picker", async () => {
516
544
  const { deps } = makeMenuDeps();
517
545
  const menu = await buildModelMenu(deps);
518
- expect(menu.text).not.toContain("🌐 = non-Anthropic");
546
+ expect(menu.text).not.toContain("OpenRouter");
519
547
  });
520
548
  });
521
549