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/README.md +110 -93
- package/package.json +2 -2
- package/src/catalog.ts +8 -5
- package/src/commands.ts +250 -92
- package/src/index.ts +47 -14
- package/src/models.ts +72 -8
- package/src/proxy.ts +31 -10
- package/src/relay-state.ts +212 -29
- package/src/relay.ts +9 -1
- package/src/types.ts +2 -1
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
|
-
|
|
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 |
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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 === "
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
398
|
-
|
|
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
|
-
`
|
|
407
|
-
"
|
|
408
|
-
"
|
|
409
|
-
"
|
|
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 === "
|
|
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 === "
|
|
420
|
-
relayState.
|
|
421
|
-
|
|
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 === "
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
200
|
-
|
|
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...");
|