pi-freeflow 1.3.3 → 1.3.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.
package/src/commands.ts CHANGED
@@ -17,10 +17,12 @@ import {
17
17
  } from "./logger.ts";
18
18
  import {
19
19
  ensureRelay,
20
+ findRelay,
20
21
  getActiveRelayState,
21
22
  removeRelay,
22
23
  saveRelayState,
23
24
  setActiveRelayState,
25
+ setRelayLabel,
24
26
  setStatusUi,
25
27
  shortRelayLabel,
26
28
  } from "./relay-state.ts";
@@ -48,7 +50,10 @@ export function updateStatusBar(ui?: ExtensionUIContext): void {
48
50
  relayState.relays.findIndex((r) => r.url === relayState.url) + 1,
49
51
  );
50
52
  const total = relayState.relays.length;
51
- ui.setStatus("freeflow", `relay: ON | ${label} ${idx}/${total}`);
53
+ const modeLabel = relayState.mode === "on" ? "ON" : "AUTO (ON)";
54
+ ui.setStatus("freeflow", `relay: ${modeLabel} | ${label} ${idx}/${total}`);
55
+ } else if (relayState.mode === "off" || !relayState.enabled) {
56
+ ui.setStatus("freeflow", "relay: OFF (direct)");
52
57
  } else {
53
58
  ui.setStatus("freeflow", undefined);
54
59
  }
@@ -60,17 +65,21 @@ export function createCommandSpec(
60
65
  ): Omit<RegisteredCommand, "name"> {
61
66
  return {
62
67
  description:
63
- "Relay egress: on | off | status | logs [level] [n] | debug on|off|status | url [URL] | deploy | list | use <URL> | remove <URL> | refresh",
68
+ "Relay egress: auto | on | off | status | add <URL> [name] | list | use <URL|name|index> [name] | label <target> <name> | remove <target> | logs [level] [n] | debug on|off | refresh | deploy",
64
69
  getArgumentCompletions: (prefix: string) =>
65
70
  [
71
+ "auto",
66
72
  "on",
67
73
  "off",
68
74
  "status",
69
- "url",
70
- "deploy",
75
+ "add",
71
76
  "list",
72
77
  "use",
78
+ "label",
79
+ "rename",
73
80
  "remove",
81
+ "url",
82
+ "deploy",
74
83
  "refresh",
75
84
  "models",
76
85
  "logs",
@@ -88,37 +97,100 @@ export function createCommandSpec(
88
97
  const relayState = getActiveRelayState();
89
98
 
90
99
  const flash = () => {
91
- const activeLabel = shortRelayLabel(relayState.url);
100
+ const activeLabel = shortRelayLabel(relayState.url, relayState.relays);
92
101
  const activeIdx = Math.max(
93
102
  1,
94
103
  relayState.relays.findIndex((r) => r.url === relayState.url) + 1,
95
104
  );
96
105
  const total = relayState.relays.length || 1;
106
+ const modeStr = (relayState.mode || "auto").toUpperCase();
97
107
  ctx.ui.notify(
98
- `Relay ${relayState.enabled ? "ON" : "OFF"}${relayState.enabled ? ` → ${activeLabel} (${activeIdx}/${total})` : " (direct)"} | saved=${relayState.relays.length} (auto-fallback rolling)`,
108
+ `Relay mode: ${modeStr} (${relayState.enabled ? "ON" : "OFF"})${relayState.enabled ? ` → ${activeLabel} (${activeIdx}/${total})` : " (direct)"} | saved=${relayState.relays.length} (auto-fallback rolling)`,
99
109
  "info",
100
110
  );
101
111
  };
102
112
 
103
113
  const persist = () => {
104
- saveRelayState(relayState);
114
+ setActiveRelayState(relayState, true);
105
115
  setStatusUi(ctx.ui);
106
116
  updateStatusBar(ctx.ui);
107
117
  };
108
-
109
118
  const setRelay = (
110
119
  enabled: boolean,
111
120
  url: string,
112
121
  addLabel?: string,
113
122
  ) => {
114
123
  relayState.enabled = enabled;
115
- relayState.url = (url || "").trim() || DEFAULT_RELAY_URL;
116
- if (relayState.url) {
117
- ensureRelay(relayState, relayState.url, addLabel);
124
+ const cleanUrl = (url || "").trim() || DEFAULT_RELAY_URL;
125
+ relayState.url = cleanUrl;
126
+ if (cleanUrl) {
127
+ ensureRelay(relayState, cleanUrl, addLabel);
118
128
  }
119
129
  setActiveRelayState(relayState);
120
130
  };
121
131
 
132
+ const addRelayInteractive = async () => {
133
+ const inputUrl = (
134
+ await ctx.ui.input(
135
+ "Custom relay URL (e.g. https://my-relay.workers.dev):",
136
+ "",
137
+ )
138
+ )?.trim();
139
+ if (!inputUrl) {
140
+ ctx.ui.notify("Cancelled — no URL provided", "warning");
141
+ return;
142
+ }
143
+ const defaultLabel = shortRelayLabel(inputUrl, relayState.relays);
144
+ const inputLabel = (
145
+ await ctx.ui.input(
146
+ "Short name / alias for TUI (e.g. cf-sg, vercel-1):",
147
+ defaultLabel,
148
+ )
149
+ )?.trim() || defaultLabel;
150
+
151
+ const added = ensureRelay(relayState, inputUrl, inputLabel);
152
+ setRelay(true, added.url, added.label);
153
+ persist();
154
+ flash();
155
+ ctx.ui.notify(
156
+ `✓ Added & activated relay [${added.label || shortRelayLabel(added.url)}]: ${added.url}`,
157
+ "info",
158
+ );
159
+ };
160
+
161
+ const editRelayLabelMenu = async () => {
162
+ if (!relayState.relays.length) {
163
+ ctx.ui.notify("No saved relays to rename", "warning");
164
+ return;
165
+ }
166
+ const fmt = (r: KnownRelay, idx: number) =>
167
+ `[${idx + 1}] ${r.label ? `[${r.label}] ` : ""}${r.url}`;
168
+ const opts = relayState.relays.map(fmt);
169
+ const choice = await ctx.ui.select("Select relay to rename / set short name", opts);
170
+ if (!choice) return;
171
+ const match = relayState.relays.find((r, idx) => fmt(r, idx) === choice);
172
+ if (!match) return;
173
+
174
+ const curLabel = match.label || shortRelayLabel(match.url, relayState.relays);
175
+ const newLabel = (
176
+ await ctx.ui.input(
177
+ `New short name for ${match.url}:`,
178
+ curLabel,
179
+ )
180
+ )?.trim();
181
+ if (!newLabel) {
182
+ ctx.ui.notify("Cancelled — short name not changed", "warning");
183
+ return;
184
+ }
185
+ setRelayLabel(relayState, match.url, newLabel);
186
+ persist();
187
+ flash();
188
+ ctx.ui.notify(
189
+ `✓ Relay short name updated to [${newLabel}] for ${match.url}`,
190
+ "info",
191
+ );
192
+ };
193
+
122
194
  const doDeploy = async () => {
123
195
  const defaultName = `relay-${Date.now().toString(36)}`;
124
196
  const token = (
@@ -158,31 +230,40 @@ export function createCommandSpec(
158
230
  ctx.ui.notify("No saved relays yet", "warning");
159
231
  return;
160
232
  }
161
- const fmt = (r: KnownRelay) =>
162
- `${r.url === relayState.url ? "★ " : " "}${r.url}${r.label ? ` (${r.label})` : ""}`;
233
+ const fmt = (r: KnownRelay, idx: number) => {
234
+ const isAct = r.url === relayState.url ? "★ " : " ";
235
+ const lbl = r.label ? `[${r.label}] ` : `[${shortRelayLabel(r.url, relayState.relays)}] `;
236
+ return `${isAct}[${idx + 1}] ${lbl}→ ${r.url}`;
237
+ };
163
238
  const opts = relayState.relays.map(fmt);
164
- const choice = await ctx.ui.select("Switch relay", opts);
239
+ const choice = await ctx.ui.select("Switch active relay", opts);
165
240
  if (!choice) return;
166
- const match = relayState.relays.find((r) => fmt(r) === choice);
241
+ const match = relayState.relays.find((r, idx) => fmt(r, idx) === choice);
167
242
  if (!match) return;
168
- setRelay(true, match.url);
243
+ setRelay(true, match.url, match.label);
169
244
  persist();
170
245
  flash();
171
246
  };
172
247
 
173
248
  const showList = () => {
174
249
  if (!relayState.relays.length) {
175
- ctx.ui.notify("No saved relays", "info");
250
+ ctx.ui.notify("No saved relays (direct upstream mode)", "info");
176
251
  return;
177
252
  }
178
- const lines = relayState.relays.map(
179
- (r) =>
180
- `${r.url === relayState.url ? "★" : " "} ${r.url}${r.label ? ` [${r.label}]` : ""}`,
181
- );
182
- ctx.ui.notify(
183
- `Saved relays (${relayState.relays.length}):\n${lines.join("\n")}`,
184
- "info",
253
+ const lines = relayState.relays.map((r, idx) => {
254
+ const star = r.url === relayState.url ? "★" : " ";
255
+ const shortName = r.label ? `[${r.label}]` : `[${shortRelayLabel(r.url, relayState.relays)}]`;
256
+ const paddedName = shortName.padEnd(16, " ");
257
+ return `${star} [${idx + 1}] ${paddedName} → ${r.url}`;
258
+ });
259
+ const activeLabel = shortRelayLabel(relayState.url, relayState.relays);
260
+ const activeIdx = Math.max(
261
+ 1,
262
+ relayState.relays.findIndex((r) => r.url === relayState.url) + 1,
185
263
  );
264
+ const modeStr = (relayState.mode || "auto").toUpperCase();
265
+ const header = `Saved Relays (${relayState.relays.length}):\n${lines.join("\n")}\n\nActive: [${activeIdx}] ${activeLabel} | Mode: ${modeStr} (${relayState.enabled ? "ON" : "OFF"})`;
266
+ ctx.ui.notify(header, "info");
186
267
  };
187
268
 
188
269
  const removeRelayMenu = async () => {
@@ -196,25 +277,35 @@ export function createCommandSpec(
196
277
  );
197
278
  return;
198
279
  }
199
- const fmt = (r: KnownRelay) =>
200
- `${r.url}${r.label ? ` (${r.label})` : ""}`;
280
+ const fmt = (r: KnownRelay, idx: number) => {
281
+ const lbl = r.label ? `[${r.label}] ` : `[${shortRelayLabel(r.url, relayState.relays)}] `;
282
+ return `[${idx + 1}] ${lbl}→ ${r.url}`;
283
+ };
201
284
  const choice = await ctx.ui.select(
202
285
  "Remove relay",
203
286
  removable.map(fmt),
204
287
  );
205
288
  if (!choice) return;
206
- const match = removable.find((r) => fmt(r) === choice);
289
+ const match = removable.find((r, idx) => fmt(r, idx) === choice);
207
290
  if (!match) return;
208
291
  removeRelay(relayState, match.url);
209
292
  persist();
210
- ctx.ui.notify(`Removed: ${match.url}`, "info");
293
+ ctx.ui.notify(`Removed: [${shortRelayLabel(match.url, relayState.relays)}] ${match.url}`, "info");
211
294
  };
212
295
 
213
- if (sub === "on") {
296
+ if (sub === "auto") {
297
+ relayState.mode = "auto";
298
+ relayState.enabled = true;
299
+ setRelay(true, relayState.url || DEFAULT_RELAY_URL);
300
+ persist();
301
+ flash();
302
+ } else if (sub === "on") {
303
+ relayState.mode = "on";
214
304
  setRelay(true, relayState.url || DEFAULT_RELAY_URL);
215
305
  persist();
216
306
  flash();
217
307
  } else if (sub === "off") {
308
+ relayState.mode = "off";
218
309
  relayState.enabled = false;
219
310
  setActiveRelayState(relayState);
220
311
  persist();
@@ -223,19 +314,74 @@ export function createCommandSpec(
223
314
  flash();
224
315
  } else if (sub === "list") {
225
316
  showList();
317
+ } else if (sub === "add") {
318
+ if (!rest.trim()) {
319
+ await addRelayInteractive();
320
+ } else {
321
+ const tokens = rest.trim().split(/\s+/);
322
+ const targetUrl = tokens[0];
323
+ const customLabel = tokens.slice(1).join(" ").trim() || undefined;
324
+ const added = ensureRelay(relayState, targetUrl, customLabel);
325
+ setRelay(true, added.url, added.label);
326
+ persist();
327
+ flash();
328
+ ctx.ui.notify(
329
+ `✓ Added & activated relay [${added.label || shortRelayLabel(added.url)}]: ${added.url}`,
330
+ "info",
331
+ );
332
+ }
226
333
  } else if (sub === "use") {
227
- const url = (
228
- rest ||
229
- (await ctx.ui.input("Relay URL to activate:", "")) ||
230
- ""
231
- ).trim();
232
- if (!url) {
233
- ctx.ui.notify("No URL given", "warning");
234
- return;
334
+ if (!rest.trim()) {
335
+ await switchRelay();
336
+ } else {
337
+ const tokens = rest.trim().split(/\s+/);
338
+ const targetToken = tokens[0];
339
+ const customLabel = tokens.slice(1).join(" ").trim() || undefined;
340
+ const matched = findRelay(relayState, targetToken);
341
+ if (matched) {
342
+ if (customLabel) {
343
+ matched.label = customLabel;
344
+ }
345
+ setRelay(true, matched.url, matched.label);
346
+ persist();
347
+ flash();
348
+ } else {
349
+ // New URL
350
+ const added = ensureRelay(relayState, targetToken, customLabel);
351
+ setRelay(true, added.url, added.label);
352
+ persist();
353
+ flash();
354
+ }
355
+ }
356
+ } else if (sub === "label" || sub === "rename") {
357
+ if (!rest.trim()) {
358
+ await editRelayLabelMenu();
359
+ } else {
360
+ const tokens = rest.trim().split(/\s+/);
361
+ const targetToken = tokens[0];
362
+ const newLabel = tokens.slice(1).join(" ").trim();
363
+ const matched = findRelay(relayState, targetToken);
364
+ if (!matched) {
365
+ ctx.ui.notify(`Relay '${targetToken}' not found in saved list`, "warning");
366
+ return;
367
+ }
368
+ let finalLabel = newLabel;
369
+ if (!finalLabel) {
370
+ finalLabel = (
371
+ await ctx.ui.input(
372
+ `New short name / alias for ${matched.url}:`,
373
+ matched.label || shortRelayLabel(matched.url, relayState.relays),
374
+ )
375
+ )?.trim() || "";
376
+ }
377
+ setRelayLabel(relayState, matched.url, finalLabel);
378
+ persist();
379
+ flash();
380
+ ctx.ui.notify(
381
+ `✓ Relay short name updated to [${shortRelayLabel(matched.url, relayState.relays)}] for ${matched.url}`,
382
+ "info",
383
+ );
235
384
  }
236
- setRelay(true, url, "manual");
237
- persist();
238
- flash();
239
385
  } else if (sub === "debug") {
240
386
  const arg = rest.trim().toLowerCase();
241
387
  if (arg === "on" || arg === "enable" || arg === "true") {
@@ -362,29 +508,26 @@ export function createCommandSpec(
362
508
  "info",
363
509
  );
364
510
  } else if (sub === "remove") {
365
- const url = (
366
- rest ||
367
- (await ctx.ui.input("Relay URL to remove:", "")) ||
368
- ""
369
- ).trim();
370
- if (!url) {
371
- ctx.ui.notify("No URL given", "warning");
372
- return;
373
- }
374
- if (url === relayState.url) {
375
- ctx.ui.notify(
376
- "Cannot remove the active relay — switch first",
377
- "warning",
378
- );
379
- return;
380
- }
381
- if (!relayState.relays.some((r) => r.url === url)) {
382
- ctx.ui.notify("Not in saved list", "warning");
383
- return;
511
+ if (!rest.trim()) {
512
+ await removeRelayMenu();
513
+ } else {
514
+ const target = rest.trim();
515
+ const matched = findRelay(relayState, target);
516
+ if (!matched) {
517
+ ctx.ui.notify(`Relay '${target}' not in saved list`, "warning");
518
+ return;
519
+ }
520
+ if (matched.url === relayState.url) {
521
+ ctx.ui.notify(
522
+ "Cannot remove the active relay — switch to another relay first",
523
+ "warning",
524
+ );
525
+ return;
526
+ }
527
+ removeRelay(relayState, matched.url);
528
+ persist();
529
+ ctx.ui.notify(`Removed: [${shortRelayLabel(matched.url, relayState.relays)}] ${matched.url}`, "info");
384
530
  }
385
- removeRelay(relayState, url);
386
- persist();
387
- ctx.ui.notify(`Removed: ${url}`, "info");
388
531
  } else if (sub === "url") {
389
532
  const input =
390
533
  rest ||
@@ -392,55 +535,62 @@ export function createCommandSpec(
392
535
  "Relay URL (empty = default):",
393
536
  relayState.url || DEFAULT_RELAY_URL,
394
537
  ));
538
+ const cleanInput = (input || "").trim() || DEFAULT_RELAY_URL;
395
539
  setRelay(
396
540
  relayState.enabled,
397
- (input || "").trim() || DEFAULT_RELAY_URL,
398
- "manual",
541
+ cleanInput,
542
+ shortRelayLabel(cleanInput, relayState.relays),
399
543
  );
400
544
  persist();
401
545
  flash();
402
546
  } else if (sub === "deploy") {
403
547
  await doDeploy();
404
548
  } else {
549
+ const currentMode = (relayState.mode || "auto").toUpperCase();
550
+ const activeLabel = shortRelayLabel(relayState.url, relayState.relays);
405
551
  const choice = await ctx.ui.select("freeflow relay", [
406
- `Relay: ${relayState.enabled ? "ON" : "OFF"} → ${relayState.url || "direct"}`,
407
- "Turn ON",
408
- "Turn OFF",
409
- "Switch relay…",
552
+ `Mode: ${currentMode} (${relayState.enabled ? "ON" : "OFF"}) → ${activeLabel}`,
553
+ "Add custom relay (URL + Short name)…",
554
+ "Switch active relay…",
555
+ "Rename / Set relay short name…",
410
556
  "Remove relay…",
411
- "Set URL",
412
- "Deploy Vercel relay…",
413
557
  "List saved relays",
558
+ "Deploy Vercel relay…",
559
+ "Mode: AUTO (auto-detect on model select)",
560
+ "Mode: ON (always relay)",
561
+ "Mode: OFF (always direct)",
414
562
  ]);
415
- if (choice === "Turn ON") {
563
+ if (choice === `Mode: ${currentMode} (${relayState.enabled ? "ON" : "OFF"}) → ${activeLabel}`) {
564
+ flash();
565
+ } else if (choice === "Add custom relay (URL + Short name)…") {
566
+ await addRelayInteractive();
567
+ } else if (choice === "Switch active relay…") {
568
+ await switchRelay();
569
+ } else if (choice === "Rename / Set relay short name…") {
570
+ await editRelayLabelMenu();
571
+ } else if (choice === "Remove relay…") {
572
+ await removeRelayMenu();
573
+ } else if (choice === "List saved relays") {
574
+ showList();
575
+ } else if (choice === "Deploy Vercel relay…") {
576
+ await doDeploy();
577
+ } else if (choice === "Mode: AUTO (auto-detect on model select)") {
578
+ relayState.mode = "auto";
579
+ relayState.enabled = true;
416
580
  setRelay(true, relayState.url || DEFAULT_RELAY_URL);
417
581
  persist();
418
582
  flash();
419
- } else if (choice === "Turn OFF") {
420
- relayState.enabled = false;
421
- setActiveRelayState(relayState);
583
+ } else if (choice === "Mode: ON (always relay)") {
584
+ relayState.mode = "on";
585
+ setRelay(true, relayState.url || DEFAULT_RELAY_URL);
422
586
  persist();
423
587
  flash();
424
- } else if (choice === "Switch relay…") {
425
- await switchRelay();
426
- } else if (choice === "Remove relay…") {
427
- await removeRelayMenu();
428
- } else if (choice === "Set URL") {
429
- const input = await ctx.ui.input(
430
- "Relay URL (empty = default):",
431
- relayState.url || DEFAULT_RELAY_URL,
432
- );
433
- setRelay(
434
- relayState.enabled,
435
- (input || "").trim() || DEFAULT_RELAY_URL,
436
- "manual",
437
- );
588
+ } else if (choice === "Mode: OFF (always direct)") {
589
+ relayState.mode = "off";
590
+ relayState.enabled = false;
591
+ setActiveRelayState(relayState);
438
592
  persist();
439
593
  flash();
440
- } else if (choice === "Deploy Vercel relay…") {
441
- await doDeploy();
442
- } else if (choice === "List saved relays") {
443
- showList();
444
594
  }
445
595
  }
446
596
  },
package/src/index.ts CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  import { createCommandSpec, updateStatusBar } from "./commands.ts";
19
19
  import { DEFAULT_HOST, HOST, PORT } from "./config.ts";
20
20
  import { log, logInfo, logWarn } from "./logger.ts";
21
- import { ALL_MODELS, KILO_MODEL_IDS } from "./models.ts";
21
+ import { ALL_MODELS, KILO_MODEL_IDS, MODEL_MAP, getAllRegisteredModels, resolveCanonicalModelId } from "./models.ts";
22
22
  import { isProxyAlive, startProxy } from "./proxy.ts";
23
23
  import { resetRateLimits } from "./rate-limiter.ts";
24
24
  import {
@@ -26,6 +26,7 @@ import {
26
26
  resolveRelayState,
27
27
  setActiveRelayState,
28
28
  setStatusUi,
29
+ setFreeFlowModelActive,
29
30
  } from "./relay-state.ts";
30
31
  import type {
31
32
  ExtensionAPI,
@@ -138,14 +139,14 @@ export default async function (pi: ExtensionAPI): Promise<void> {
138
139
 
139
140
  // 3. Instant 0ms Static Catalog Registration
140
141
  // Register static models immediately on boot so Pi/OMP picker is populated with zero latency!
141
- const initialCatalog: RegisteredModel[] = ALL_MODELS.map((m) => ({
142
+ const registeredCatalog: RegisteredModel[] = getAllRegisteredModels().map((m) => ({
142
143
  ...m,
143
144
  source: KILO_MODEL_IDS.has(m.id) ? "kilo" : "opencode",
144
145
  }));
145
- setAliveCatalog(initialCatalog);
146
+ setAliveCatalog(ALL_MODELS.map((m) => ({ ...m, source: KILO_MODEL_IDS.has(m.id) ? "kilo" : "opencode" })));
146
147
  pi.registerProvider(
147
148
  "freeflow",
148
- buildProviderConfig(initialCatalog, actualPort),
149
+ buildProviderConfig(registeredCatalog, actualPort),
149
150
  );
150
151
 
151
152
  // 4. Background Catalog Refresh
@@ -156,7 +157,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
156
157
  setAliveCatalog(aliveModels);
157
158
  pi.registerProvider(
158
159
  "freeflow",
159
- buildProviderConfig(aliveModels, actualPort),
160
+ buildProviderConfig(registeredCatalog, actualPort),
160
161
  );
161
162
  logInfo(
162
163
  `Catalog refreshed: ${aliveModels.length} models verified active`,
@@ -179,9 +180,27 @@ export default async function (pi: ExtensionAPI): Promise<void> {
179
180
  pi.registerCommand("freeflow", commandSpec);
180
181
 
181
182
  // 6. Lifecycle Listeners
183
+ function isFreeFlowModelMatch(provider?: string, modelId?: string): boolean {
184
+ if (provider === "freeflow") return true;
185
+ if (typeof modelId === "string" && modelId.trim()) {
186
+ const clean = modelId.trim();
187
+ const canonical = resolveCanonicalModelId(clean);
188
+ return (
189
+ MODEL_MAP.has(clean) ||
190
+ MODEL_MAP.has(canonical) ||
191
+ getAliveCatalog().some((m) => m.id === clean || m.id === canonical)
192
+ );
193
+ }
194
+ // When provider and modelId are not yet populated on session_start,
195
+ // default to true so the widget is present when starting with freeflow
196
+ if (!provider && !modelId) {
197
+ return true;
198
+ }
199
+ return false;
200
+ }
201
+
182
202
  pi.on?.("session_start", async (_event, ctx: ExtensionContext) => {
183
203
  const freshRelayState = resolveRelayState();
184
- setActiveRelayState(freshRelayState, false);
185
204
  setStatusUi(ctx.ui);
186
205
 
187
206
  let provider: string | undefined;
@@ -195,9 +214,13 @@ export default async function (pi: ExtensionAPI): Promise<void> {
195
214
  modelId = m.id;
196
215
  }
197
216
  }
198
- const isFreeFlow =
199
- provider === "freeflow" ||
200
- Boolean(modelId && getAliveCatalog().some((m) => m.id === modelId));
217
+ const isFreeFlow = isFreeFlowModelMatch(provider, modelId);
218
+ setFreeFlowModelActive(isFreeFlow);
219
+
220
+ if (freshRelayState.mode !== "off") {
221
+ freshRelayState.enabled = freshRelayState.relays.length > 0;
222
+ setActiveRelayState(freshRelayState, false);
223
+ }
201
224
 
202
225
  if (isFreeFlow) {
203
226
  updateStatusBar(ctx.ui);
@@ -208,7 +231,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
208
231
 
209
232
  pi.on?.("model_select", async (event, ctx: ExtensionContext) => {
210
233
  const freshRelayState = resolveRelayState();
211
- setActiveRelayState(freshRelayState, false);
212
234
  setStatusUi(ctx.ui);
213
235
  let provider: string | undefined;
214
236
  let modelId: string | undefined;
@@ -220,10 +242,22 @@ export default async function (pi: ExtensionAPI): Promise<void> {
220
242
  if ("id" in m && typeof m.id === "string") {
221
243
  modelId = m.id;
222
244
  }
245
+ } else if (ctx && typeof ctx === "object" && "model" in ctx && ctx.model && typeof ctx.model === "object") {
246
+ const m = ctx.model;
247
+ if ("provider" in m && typeof m.provider === "string") {
248
+ provider = m.provider;
249
+ }
250
+ if ("id" in m && typeof m.id === "string") {
251
+ modelId = m.id;
252
+ }
253
+ }
254
+ const isFreeFlow = isFreeFlowModelMatch(provider, modelId);
255
+ setFreeFlowModelActive(isFreeFlow);
256
+
257
+ if (freshRelayState.mode !== "off") {
258
+ freshRelayState.enabled = freshRelayState.relays.length > 0;
259
+ setActiveRelayState(freshRelayState, false);
223
260
  }
224
- const isFreeFlow =
225
- provider === "freeflow" ||
226
- Boolean(modelId && getAliveCatalog().some((m) => m.id === modelId));
227
261
 
228
262
  if (isFreeFlow) {
229
263
  updateStatusBar(ctx.ui);
@@ -231,7 +265,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
231
265
  ctx.ui?.setStatus?.("freeflow", undefined);
232
266
  }
233
267
  });
234
-
235
268
  pi.on?.("session_shutdown", () => {
236
269
  if (server) {
237
270
  logInfo("shutting down proxy daemon...");