pi-freeflow 1.3.3 → 1.3.7

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,14 @@ import {
17
17
  } from "./logger.ts";
18
18
  import {
19
19
  ensureRelay,
20
+ findRelay,
20
21
  getActiveRelayState,
22
+ getRelayHealth,
23
+ isRelayHealthy,
21
24
  removeRelay,
22
25
  saveRelayState,
23
26
  setActiveRelayState,
27
+ setRelayLabel,
24
28
  setStatusUi,
25
29
  shortRelayLabel,
26
30
  } from "./relay-state.ts";
@@ -48,7 +52,10 @@ export function updateStatusBar(ui?: ExtensionUIContext): void {
48
52
  relayState.relays.findIndex((r) => r.url === relayState.url) + 1,
49
53
  );
50
54
  const total = relayState.relays.length;
51
- ui.setStatus("freeflow", `relay: ON | ${label} ${idx}/${total}`);
55
+ const modeLabel = relayState.mode === "on" ? "ON" : "AUTO (ON)";
56
+ ui.setStatus("freeflow", `relay: ${modeLabel} | ${label} ${idx}/${total}`);
57
+ } else if (relayState.mode === "off" || !relayState.enabled) {
58
+ ui.setStatus("freeflow", "relay: OFF (direct)");
52
59
  } else {
53
60
  ui.setStatus("freeflow", undefined);
54
61
  }
@@ -60,17 +67,21 @@ export function createCommandSpec(
60
67
  ): Omit<RegisteredCommand, "name"> {
61
68
  return {
62
69
  description:
63
- "Relay egress: on | off | status | logs [level] [n] | debug on|off|status | url [URL] | deploy | list | use <URL> | remove <URL> | refresh",
70
+ "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
71
  getArgumentCompletions: (prefix: string) =>
65
72
  [
73
+ "auto",
66
74
  "on",
67
75
  "off",
68
76
  "status",
69
- "url",
70
- "deploy",
77
+ "add",
71
78
  "list",
72
79
  "use",
80
+ "label",
81
+ "rename",
73
82
  "remove",
83
+ "url",
84
+ "deploy",
74
85
  "refresh",
75
86
  "models",
76
87
  "logs",
@@ -88,37 +99,100 @@ export function createCommandSpec(
88
99
  const relayState = getActiveRelayState();
89
100
 
90
101
  const flash = () => {
91
- const activeLabel = shortRelayLabel(relayState.url);
102
+ const activeLabel = shortRelayLabel(relayState.url, relayState.relays);
92
103
  const activeIdx = Math.max(
93
104
  1,
94
105
  relayState.relays.findIndex((r) => r.url === relayState.url) + 1,
95
106
  );
96
107
  const total = relayState.relays.length || 1;
108
+ const modeStr = (relayState.mode || "auto").toUpperCase();
97
109
  ctx.ui.notify(
98
- `Relay ${relayState.enabled ? "ON" : "OFF"}${relayState.enabled ? ` → ${activeLabel} (${activeIdx}/${total})` : " (direct)"} | saved=${relayState.relays.length} (auto-fallback rolling)`,
110
+ `Relay mode: ${modeStr} (${relayState.enabled ? "ON" : "OFF"})${relayState.enabled ? ` → ${activeLabel} (${activeIdx}/${total})` : " (direct)"} | saved=${relayState.relays.length} (auto-fallback rolling)`,
99
111
  "info",
100
112
  );
101
113
  };
102
114
 
103
115
  const persist = () => {
104
- saveRelayState(relayState);
116
+ setActiveRelayState(relayState, true);
105
117
  setStatusUi(ctx.ui);
106
118
  updateStatusBar(ctx.ui);
107
119
  };
108
-
109
120
  const setRelay = (
110
121
  enabled: boolean,
111
122
  url: string,
112
123
  addLabel?: string,
113
124
  ) => {
114
125
  relayState.enabled = enabled;
115
- relayState.url = (url || "").trim() || DEFAULT_RELAY_URL;
116
- if (relayState.url) {
117
- ensureRelay(relayState, relayState.url, addLabel);
126
+ const cleanUrl = (url || "").trim() || DEFAULT_RELAY_URL;
127
+ relayState.url = cleanUrl;
128
+ if (cleanUrl) {
129
+ ensureRelay(relayState, cleanUrl, addLabel);
118
130
  }
119
131
  setActiveRelayState(relayState);
120
132
  };
121
133
 
134
+ const addRelayInteractive = async () => {
135
+ const inputUrl = (
136
+ await ctx.ui.input(
137
+ "Custom relay URL (e.g. https://my-relay.workers.dev):",
138
+ "",
139
+ )
140
+ )?.trim();
141
+ if (!inputUrl) {
142
+ ctx.ui.notify("Cancelled — no URL provided", "warning");
143
+ return;
144
+ }
145
+ const defaultLabel = shortRelayLabel(inputUrl, relayState.relays);
146
+ const inputLabel = (
147
+ await ctx.ui.input(
148
+ "Short name / alias for TUI (e.g. cf-sg, vercel-1):",
149
+ defaultLabel,
150
+ )
151
+ )?.trim() || defaultLabel;
152
+
153
+ const added = ensureRelay(relayState, inputUrl, inputLabel);
154
+ setRelay(true, added.url, added.label);
155
+ persist();
156
+ flash();
157
+ ctx.ui.notify(
158
+ `✓ Added & activated relay [${added.label || shortRelayLabel(added.url)}]: ${added.url}`,
159
+ "info",
160
+ );
161
+ };
162
+
163
+ const editRelayLabelMenu = async () => {
164
+ if (!relayState.relays.length) {
165
+ ctx.ui.notify("No saved relays to rename", "warning");
166
+ return;
167
+ }
168
+ const fmt = (r: KnownRelay, idx: number) =>
169
+ `[${idx + 1}] ${r.label ? `[${r.label}] ` : ""}${r.url}`;
170
+ const opts = relayState.relays.map(fmt);
171
+ const choice = await ctx.ui.select("Select relay to rename / set short name", opts);
172
+ if (!choice) return;
173
+ const match = relayState.relays.find((r, idx) => fmt(r, idx) === choice);
174
+ if (!match) return;
175
+
176
+ const curLabel = match.label || shortRelayLabel(match.url, relayState.relays);
177
+ const newLabel = (
178
+ await ctx.ui.input(
179
+ `New short name for ${match.url}:`,
180
+ curLabel,
181
+ )
182
+ )?.trim();
183
+ if (!newLabel) {
184
+ ctx.ui.notify("Cancelled — short name not changed", "warning");
185
+ return;
186
+ }
187
+ setRelayLabel(relayState, match.url, newLabel);
188
+ persist();
189
+ flash();
190
+ ctx.ui.notify(
191
+ `✓ Relay short name updated to [${newLabel}] for ${match.url}`,
192
+ "info",
193
+ );
194
+ };
195
+
122
196
  const doDeploy = async () => {
123
197
  const defaultName = `relay-${Date.now().toString(36)}`;
124
198
  const token = (
@@ -158,31 +232,46 @@ export function createCommandSpec(
158
232
  ctx.ui.notify("No saved relays yet", "warning");
159
233
  return;
160
234
  }
161
- const fmt = (r: KnownRelay) =>
162
- `${r.url === relayState.url ? "★ " : " "}${r.url}${r.label ? ` (${r.label})` : ""}`;
235
+ const fmt = (r: KnownRelay, idx: number) => {
236
+ const isAct = r.url === relayState.url ? "★ " : " ";
237
+ const lbl = r.label ? `[${r.label}] ` : `[${shortRelayLabel(r.url, relayState.relays)}] `;
238
+ return `${isAct}[${idx + 1}] ${lbl}→ ${r.url}`;
239
+ };
163
240
  const opts = relayState.relays.map(fmt);
164
- const choice = await ctx.ui.select("Switch relay", opts);
241
+ const choice = await ctx.ui.select("Switch active relay", opts);
165
242
  if (!choice) return;
166
- const match = relayState.relays.find((r) => fmt(r) === choice);
243
+ const match = relayState.relays.find((r, idx) => fmt(r, idx) === choice);
167
244
  if (!match) return;
168
- setRelay(true, match.url);
245
+ setRelay(true, match.url, match.label);
169
246
  persist();
170
247
  flash();
171
248
  };
172
249
 
173
250
  const showList = () => {
174
251
  if (!relayState.relays.length) {
175
- ctx.ui.notify("No saved relays", "info");
252
+ ctx.ui.notify("No saved relays (direct upstream mode)", "info");
176
253
  return;
177
254
  }
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",
255
+ const lines = relayState.relays.map((r, idx) => {
256
+ const star = r.url === relayState.url ? "★" : " ";
257
+ const shortName = r.label ? `[${r.label}]` : `[${shortRelayLabel(r.url, relayState.relays)}]`;
258
+ const paddedName = shortName.padEnd(16, " ");
259
+ const health = getRelayHealth(r.url);
260
+ const isCooling = health && Date.now() < health.cooldownUntil;
261
+ const remainingSec = isCooling ? Math.ceil((health.cooldownUntil - Date.now()) / 1000) : 0;
262
+ const healthBadge = isCooling
263
+ ? ` ⚠️ [cooling ${remainingSec}s: ${health.lastStatus ? `HTTP ${health.lastStatus}` : "error"}]`
264
+ : " ✓";
265
+ return `${star} [${idx + 1}] ${paddedName} → ${r.url}${healthBadge}`;
266
+ });
267
+ const activeLabel = shortRelayLabel(relayState.url, relayState.relays);
268
+ const activeIdx = Math.max(
269
+ 1,
270
+ relayState.relays.findIndex((r) => r.url === relayState.url) + 1,
185
271
  );
272
+ const modeStr = (relayState.mode || "auto").toUpperCase();
273
+ const header = `Saved Relays (${relayState.relays.length}):\n${lines.join("\n")}\n\nActive: [${activeIdx}] ${activeLabel} | Mode: ${modeStr} (${relayState.enabled ? "ON" : "OFF"})`;
274
+ ctx.ui.notify(header, "info");
186
275
  };
187
276
 
188
277
  const removeRelayMenu = async () => {
@@ -196,25 +285,35 @@ export function createCommandSpec(
196
285
  );
197
286
  return;
198
287
  }
199
- const fmt = (r: KnownRelay) =>
200
- `${r.url}${r.label ? ` (${r.label})` : ""}`;
288
+ const fmt = (r: KnownRelay, idx: number) => {
289
+ const lbl = r.label ? `[${r.label}] ` : `[${shortRelayLabel(r.url, relayState.relays)}] `;
290
+ return `[${idx + 1}] ${lbl}→ ${r.url}`;
291
+ };
201
292
  const choice = await ctx.ui.select(
202
293
  "Remove relay",
203
294
  removable.map(fmt),
204
295
  );
205
296
  if (!choice) return;
206
- const match = removable.find((r) => fmt(r) === choice);
297
+ const match = removable.find((r, idx) => fmt(r, idx) === choice);
207
298
  if (!match) return;
208
299
  removeRelay(relayState, match.url);
209
300
  persist();
210
- ctx.ui.notify(`Removed: ${match.url}`, "info");
301
+ ctx.ui.notify(`Removed: [${shortRelayLabel(match.url, relayState.relays)}] ${match.url}`, "info");
211
302
  };
212
303
 
213
- if (sub === "on") {
304
+ if (sub === "auto") {
305
+ relayState.mode = "auto";
306
+ relayState.enabled = true;
307
+ setRelay(true, relayState.url || DEFAULT_RELAY_URL);
308
+ persist();
309
+ flash();
310
+ } else if (sub === "on") {
311
+ relayState.mode = "on";
214
312
  setRelay(true, relayState.url || DEFAULT_RELAY_URL);
215
313
  persist();
216
314
  flash();
217
315
  } else if (sub === "off") {
316
+ relayState.mode = "off";
218
317
  relayState.enabled = false;
219
318
  setActiveRelayState(relayState);
220
319
  persist();
@@ -223,19 +322,74 @@ export function createCommandSpec(
223
322
  flash();
224
323
  } else if (sub === "list") {
225
324
  showList();
325
+ } else if (sub === "add") {
326
+ if (!rest.trim()) {
327
+ await addRelayInteractive();
328
+ } else {
329
+ const tokens = rest.trim().split(/\s+/);
330
+ const targetUrl = tokens[0];
331
+ const customLabel = tokens.slice(1).join(" ").trim() || undefined;
332
+ const added = ensureRelay(relayState, targetUrl, customLabel);
333
+ setRelay(true, added.url, added.label);
334
+ persist();
335
+ flash();
336
+ ctx.ui.notify(
337
+ `✓ Added & activated relay [${added.label || shortRelayLabel(added.url)}]: ${added.url}`,
338
+ "info",
339
+ );
340
+ }
226
341
  } 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;
342
+ if (!rest.trim()) {
343
+ await switchRelay();
344
+ } else {
345
+ const tokens = rest.trim().split(/\s+/);
346
+ const targetToken = tokens[0];
347
+ const customLabel = tokens.slice(1).join(" ").trim() || undefined;
348
+ const matched = findRelay(relayState, targetToken);
349
+ if (matched) {
350
+ if (customLabel) {
351
+ matched.label = customLabel;
352
+ }
353
+ setRelay(true, matched.url, matched.label);
354
+ persist();
355
+ flash();
356
+ } else {
357
+ // New URL
358
+ const added = ensureRelay(relayState, targetToken, customLabel);
359
+ setRelay(true, added.url, added.label);
360
+ persist();
361
+ flash();
362
+ }
363
+ }
364
+ } else if (sub === "label" || sub === "rename") {
365
+ if (!rest.trim()) {
366
+ await editRelayLabelMenu();
367
+ } else {
368
+ const tokens = rest.trim().split(/\s+/);
369
+ const targetToken = tokens[0];
370
+ const newLabel = tokens.slice(1).join(" ").trim();
371
+ const matched = findRelay(relayState, targetToken);
372
+ if (!matched) {
373
+ ctx.ui.notify(`Relay '${targetToken}' not found in saved list`, "warning");
374
+ return;
375
+ }
376
+ let finalLabel = newLabel;
377
+ if (!finalLabel) {
378
+ finalLabel = (
379
+ await ctx.ui.input(
380
+ `New short name / alias for ${matched.url}:`,
381
+ matched.label || shortRelayLabel(matched.url, relayState.relays),
382
+ )
383
+ )?.trim() || "";
384
+ }
385
+ setRelayLabel(relayState, matched.url, finalLabel);
386
+ persist();
387
+ flash();
388
+ ctx.ui.notify(
389
+ `✓ Relay short name updated to [${shortRelayLabel(matched.url, relayState.relays)}] for ${matched.url}`,
390
+ "info",
391
+ );
235
392
  }
236
- setRelay(true, url, "manual");
237
- persist();
238
- flash();
239
393
  } else if (sub === "debug") {
240
394
  const arg = rest.trim().toLowerCase();
241
395
  if (arg === "on" || arg === "enable" || arg === "true") {
@@ -362,29 +516,26 @@ export function createCommandSpec(
362
516
  "info",
363
517
  );
364
518
  } 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;
519
+ if (!rest.trim()) {
520
+ await removeRelayMenu();
521
+ } else {
522
+ const target = rest.trim();
523
+ const matched = findRelay(relayState, target);
524
+ if (!matched) {
525
+ ctx.ui.notify(`Relay '${target}' not in saved list`, "warning");
526
+ return;
527
+ }
528
+ if (matched.url === relayState.url) {
529
+ ctx.ui.notify(
530
+ "Cannot remove the active relay — switch to another relay first",
531
+ "warning",
532
+ );
533
+ return;
534
+ }
535
+ removeRelay(relayState, matched.url);
536
+ persist();
537
+ ctx.ui.notify(`Removed: [${shortRelayLabel(matched.url, relayState.relays)}] ${matched.url}`, "info");
384
538
  }
385
- removeRelay(relayState, url);
386
- persist();
387
- ctx.ui.notify(`Removed: ${url}`, "info");
388
539
  } else if (sub === "url") {
389
540
  const input =
390
541
  rest ||
@@ -392,55 +543,62 @@ export function createCommandSpec(
392
543
  "Relay URL (empty = default):",
393
544
  relayState.url || DEFAULT_RELAY_URL,
394
545
  ));
546
+ const cleanInput = (input || "").trim() || DEFAULT_RELAY_URL;
395
547
  setRelay(
396
548
  relayState.enabled,
397
- (input || "").trim() || DEFAULT_RELAY_URL,
398
- "manual",
549
+ cleanInput,
550
+ shortRelayLabel(cleanInput, relayState.relays),
399
551
  );
400
552
  persist();
401
553
  flash();
402
554
  } else if (sub === "deploy") {
403
555
  await doDeploy();
404
556
  } else {
557
+ const currentMode = (relayState.mode || "auto").toUpperCase();
558
+ const activeLabel = shortRelayLabel(relayState.url, relayState.relays);
405
559
  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…",
560
+ `Mode: ${currentMode} (${relayState.enabled ? "ON" : "OFF"}) → ${activeLabel}`,
561
+ "Add custom relay (URL + Short name)…",
562
+ "Switch active relay…",
563
+ "Rename / Set relay short name…",
410
564
  "Remove relay…",
411
- "Set URL",
412
- "Deploy Vercel relay…",
413
565
  "List saved relays",
566
+ "Deploy Vercel relay…",
567
+ "Mode: AUTO (auto-detect on model select)",
568
+ "Mode: ON (always relay)",
569
+ "Mode: OFF (always direct)",
414
570
  ]);
415
- if (choice === "Turn ON") {
571
+ if (choice === `Mode: ${currentMode} (${relayState.enabled ? "ON" : "OFF"}) → ${activeLabel}`) {
572
+ flash();
573
+ } else if (choice === "Add custom relay (URL + Short name)…") {
574
+ await addRelayInteractive();
575
+ } else if (choice === "Switch active relay…") {
576
+ await switchRelay();
577
+ } else if (choice === "Rename / Set relay short name…") {
578
+ await editRelayLabelMenu();
579
+ } else if (choice === "Remove relay…") {
580
+ await removeRelayMenu();
581
+ } else if (choice === "List saved relays") {
582
+ showList();
583
+ } else if (choice === "Deploy Vercel relay…") {
584
+ await doDeploy();
585
+ } else if (choice === "Mode: AUTO (auto-detect on model select)") {
586
+ relayState.mode = "auto";
587
+ relayState.enabled = true;
416
588
  setRelay(true, relayState.url || DEFAULT_RELAY_URL);
417
589
  persist();
418
590
  flash();
419
- } else if (choice === "Turn OFF") {
420
- relayState.enabled = false;
421
- setActiveRelayState(relayState);
591
+ } else if (choice === "Mode: ON (always relay)") {
592
+ relayState.mode = "on";
593
+ setRelay(true, relayState.url || DEFAULT_RELAY_URL);
422
594
  persist();
423
595
  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
- );
596
+ } else if (choice === "Mode: OFF (always direct)") {
597
+ relayState.mode = "off";
598
+ relayState.enabled = false;
599
+ setActiveRelayState(relayState);
438
600
  persist();
439
601
  flash();
440
- } else if (choice === "Deploy Vercel relay…") {
441
- await doDeploy();
442
- } else if (choice === "List saved relays") {
443
- showList();
444
602
  }
445
603
  }
446
604
  },
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...");