@tiagojct/subsub 0.22.0 → 0.24.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/CHANGELOG.md +20 -0
- package/README.md +18 -4
- package/dist/config.js +2 -1
- package/dist/cost.js +17 -0
- package/dist/doctor.js +13 -2
- package/dist/init.js +38 -2
- package/dist/look.js +2 -2
- package/dist/prompt.js +6 -1
- package/dist/providers.js +78 -0
- package/dist/roles.js +11 -0
- package/dist/screens.js +8 -2
- package/dist/subsub.js +67 -13
- package/dist/tui-view.js +1 -1
- package/dist/tui.js +76 -4
- package/dist/web.js +8 -4
- package/package.json +1 -1
- package/prompts/check-library.md +12 -0
- package/prompts/compare.md +1 -1
- package/prompts/lit.md +9 -4
- package/roles/librarian.md +2 -1
- package/roles/researcher.md +3 -2
- package/src/config.ts +4 -1
- package/src/cost.ts +22 -0
- package/src/doctor.ts +12 -2
- package/src/init.ts +39 -2
- package/src/look.ts +2 -2
- package/src/prompt.ts +14 -1
- package/src/providers.ts +82 -0
- package/src/roles.ts +11 -0
- package/src/screens.ts +14 -2
- package/src/subsub.ts +67 -12
- package/src/tui-view.ts +1 -1
- package/src/tui.ts +79 -4
- package/src/web.ts +8 -3
- package/web/app.css +106 -53
- package/web/app.js +67 -16
- package/web/i18n.js +15 -2
- package/web/index.html +13 -7
package/src/subsub.ts
CHANGED
|
@@ -45,14 +45,15 @@ import { describeArgs, formatPreview, isEmptyPreview } from "./preview.ts";
|
|
|
45
45
|
import { perMinuteWait, providerTrouble, TROUBLE_TEXT, TROUBLE_TEXT_PT } from "./keys.ts";
|
|
46
46
|
import { headerLines, LIBRARIAN_THEME, type LibraryState, paintPreview, QUOTES, type Scheme, schemeFromAnsi, themeName } from "./look.ts";
|
|
47
47
|
import { footerLines, toolRenderers, usageTotals } from "./chrome.ts";
|
|
48
|
+
import { type CostLevel, costStatus, money, usesModel } from "./cost.ts";
|
|
48
49
|
import { ApprovalView } from "./dialogs.ts";
|
|
49
50
|
import { allowedFile, listNotes, OPENABLE, openFile } from "./files.ts";
|
|
50
|
-
import { LibraryView, NotesView, type LibraryItem } from "./screens.ts";
|
|
51
|
+
import { itemRef, LibraryView, NotesView, type LibraryItem } from "./screens.ts";
|
|
51
52
|
import { glyphs, paintFromTheme, workingFrames } from "./skin.ts";
|
|
52
53
|
import { loadWords, toolWord, type Words } from "./words.ts";
|
|
53
54
|
import { systemAddition } from "./prompt.ts";
|
|
54
55
|
import { logUsage } from "./usage.ts";
|
|
55
|
-
import { gateKind, SERVER_PREFIXES, toolsFor, unavailableReason, outboundProblem } from "./roles.ts";
|
|
56
|
+
import { activeTools, gateKind, SERVER_PREFIXES, toolsFor, unavailableReason, outboundProblem } from "./roles.ts";
|
|
56
57
|
|
|
57
58
|
const MODE_ENTRY = "subsub-mode";
|
|
58
59
|
|
|
@@ -181,6 +182,8 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
181
182
|
let resumeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
182
183
|
let turnStart = 0;
|
|
183
184
|
let turnTools = 0;
|
|
185
|
+
/** The cost level already noted in this session, so each note comes once. */
|
|
186
|
+
let costNoted: CostLevel | undefined;
|
|
184
187
|
let webCtx: ExtensionContext | undefined;
|
|
185
188
|
function emitWeb(): void {
|
|
186
189
|
const ctx = webCtx;
|
|
@@ -195,6 +198,8 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
195
198
|
userName: cfg.userName ?? null,
|
|
196
199
|
vault: cfg.vault ?? null,
|
|
197
200
|
starbuck: starbuckEnabled(cfg),
|
|
201
|
+
// Shown next to the model only when there is a limit.
|
|
202
|
+
cost: cfg.costLimit ? { spent: usageTotals(ctx.sessionManager.getEntries()).cost, limit: cfg.costLimit } : null,
|
|
198
203
|
};
|
|
199
204
|
try {
|
|
200
205
|
ctx.ui.setStatus("subsub.web", JSON.stringify(state));
|
|
@@ -277,15 +282,11 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
277
282
|
/** /library: search as you type; Enter puts /read <item> in the box (the Librarian gets the key), ctrl+n puts /lit-note <item>. */
|
|
278
283
|
async function openLibrary(ctx: ExtensionContext): Promise<void> {
|
|
279
284
|
if (!screenOk(ctx)) return;
|
|
280
|
-
const ref =
|
|
285
|
+
const ref = itemRef;
|
|
281
286
|
await ctx.ui.custom<void>(
|
|
282
287
|
(tui, theme, _keys, done) =>
|
|
283
288
|
new LibraryView({
|
|
284
|
-
search:
|
|
285
|
-
const r = await current().call("zotero_find_items", { query, limit: 25 });
|
|
286
|
-
if (r.isError) throw new Error(r.text);
|
|
287
|
-
return ((r.data as { items?: LibraryItem[] })?.items ?? []).filter((it) => it && it.key);
|
|
288
|
-
},
|
|
289
|
+
search: searchLibraries,
|
|
289
290
|
choose: (it) => {
|
|
290
291
|
ctx.ui.setEditorText(mode === "researcher" ? `/read ${ref(it)}` : ref(it));
|
|
291
292
|
done();
|
|
@@ -326,6 +327,33 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
326
327
|
: text;
|
|
327
328
|
}
|
|
328
329
|
|
|
330
|
+
/** The group libraries in Zotero, from the last library check (read-only; searched with the user's library). */
|
|
331
|
+
let groups: Array<{ library: string; name: string }> = [];
|
|
332
|
+
|
|
333
|
+
/** Search the user's library, then each group library; a group's items carry the group. */
|
|
334
|
+
async function searchLibraries(query: string): Promise<LibraryItem[]> {
|
|
335
|
+
const first = await current().call("zotero_find_items", { query, limit: 25 });
|
|
336
|
+
if (first.isError) throw new Error(first.text);
|
|
337
|
+
const items = ((first.data as { items?: LibraryItem[] })?.items ?? []).filter((it) => it && it.key);
|
|
338
|
+
for (const g of groups) {
|
|
339
|
+
const r = await current().call("zotero_find_items", { query, limit: 25, library: g.library });
|
|
340
|
+
if (r.isError) continue;
|
|
341
|
+
for (const it of (r.data as { items?: LibraryItem[] })?.items ?? []) if (it?.key) items.push({ ...it, library: g.library, group: g.name });
|
|
342
|
+
}
|
|
343
|
+
return items;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function refreshGroups(): Promise<void> {
|
|
347
|
+
if (!current().has("zotero_list_libraries")) return;
|
|
348
|
+
try {
|
|
349
|
+
const r = await current().call("zotero_list_libraries", {});
|
|
350
|
+
const all = ((r.data as { libraries?: Array<{ library: string; name?: string }> })?.libraries ?? []);
|
|
351
|
+
groups = r.isError ? [] : all.filter((l) => l.library?.startsWith("group:")).map((l) => ({ library: l.library, name: l.name || l.library }));
|
|
352
|
+
} catch {
|
|
353
|
+
groups = [];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
329
357
|
async function refreshLibrary(): Promise<void> {
|
|
330
358
|
try {
|
|
331
359
|
const st = await current().call("zotero_status", {});
|
|
@@ -336,6 +364,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
336
364
|
const ov = await current().call("zotero_library_overview", {});
|
|
337
365
|
const o = (ov.data ?? {}) as Record<string, unknown>;
|
|
338
366
|
Object.assign(library, { zotero: "reachable", items: o.items as number, toReview: o.awaiting_review as number });
|
|
367
|
+
await refreshGroups();
|
|
339
368
|
}
|
|
340
369
|
} catch {
|
|
341
370
|
library.zotero = "down";
|
|
@@ -410,7 +439,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
410
439
|
/** Set the mode's tools and, unless `switchModel` is false, the mode's model. */
|
|
411
440
|
async function applyMode(ctx: ExtensionContext, next: Mode, remember: boolean, switchModel = true): Promise<void> {
|
|
412
441
|
mode = next;
|
|
413
|
-
pi.setActiveTools(toolsFor(mode, registered(), toolOpts()));
|
|
442
|
+
pi.setActiveTools(activeTools(pi.getActiveTools(), toolsFor(mode, registered(), toolOpts()), standalone));
|
|
414
443
|
const spec = cfg.models[mode];
|
|
415
444
|
if (spec && switchModel) {
|
|
416
445
|
const i = spec.indexOf("/");
|
|
@@ -442,6 +471,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
442
471
|
}
|
|
443
472
|
|
|
444
473
|
pi.on("session_start", async (_event, ctx) => {
|
|
474
|
+
costNoted = undefined; // a new or another conversation: its own cost
|
|
445
475
|
if (!bridge) {
|
|
446
476
|
const b = new Bridge();
|
|
447
477
|
await b.start(deps.specs ?? serverSpecs(cfg), cfg.startTimeout * 1000);
|
|
@@ -531,6 +561,17 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
531
561
|
// A per-minute limit (free tiers): wait as long as the provider says, then continue the task,
|
|
532
562
|
// up to three times per request of the user. Anything the user types cancels the wait.
|
|
533
563
|
pi.on("agent_end", async (_event, ctx) => {
|
|
564
|
+
const spent = costStatus(usageTotals(ctx.sessionManager.getEntries()).cost, cfg.costLimit);
|
|
565
|
+
if (spent.level !== "ok" && costNoted !== spent.level) {
|
|
566
|
+
costNoted = spent.level;
|
|
567
|
+
ctx.ui.notify(
|
|
568
|
+
spent.level === "near"
|
|
569
|
+
? L(`Sub-Sub: this conversation has used 80% of your cost limit (${money(spent.spent)} of ${money(spent.limit!)}).`, `Sub-Sub: esta conversa já usou 80% do seu limite de custo (${money(spent.spent)} de ${money(spent.limit!)}).`)
|
|
570
|
+
: L(`Sub-Sub: this conversation has reached your cost limit (${money(spent.spent)} of ${money(spent.limit!)}). Sub-Sub will ask before each new request.`, `Sub-Sub: esta conversa atingiu o seu limite de custo (${money(spent.spent)} de ${money(spent.limit!)}). O Sub-Sub vai perguntar antes de cada pedido novo.`),
|
|
571
|
+
"warning",
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
if (cfg.costLimit) emitWeb(); // the cost next to the model in the browser view
|
|
534
575
|
if (turnStart) logUsage(cfg.usageLog, { ev: "turn", seconds: Math.round((Date.now() - turnStart) / 1000), tools: turnTools, error: providerTrouble(lastError) ?? (lastError ? "other" : undefined) });
|
|
535
576
|
turnStart = 0;
|
|
536
577
|
const wait = perMinuteWait(lastError);
|
|
@@ -558,6 +599,19 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
558
599
|
resumeTimer = undefined;
|
|
559
600
|
resumes = 0;
|
|
560
601
|
const cmd = /^\/([\w-]+)(?:\s|$)/.exec(event.text.trim())?.[1];
|
|
602
|
+
// Over the cost limit: ask before each new request to the model (a running answer is not cut).
|
|
603
|
+
const spent = costStatus(usageTotals(ctx.sessionManager.getEntries()).cost, cfg.costLimit);
|
|
604
|
+
if (spent.level === "over" && usesModel(event.text)) {
|
|
605
|
+
const text = L(
|
|
606
|
+
`This conversation has cost ${money(spent.spent)}, over your limit of ${money(spent.limit!)} (costLimit in the settings). Send this request to the model?`,
|
|
607
|
+
`Esta conversa já custou ${money(spent.spent)}, acima do seu limite de ${money(spent.limit!)} (costLimit nas definições). Enviar este pedido ao modelo?`,
|
|
608
|
+
);
|
|
609
|
+
const go = ctx.hasUI ? await ctx.ui.confirm(L("Sub-Sub: cost limit", "Sub-Sub: limite de custo"), text) : false;
|
|
610
|
+
if (!go) {
|
|
611
|
+
if (!ctx.hasUI) ctx.ui.notify(text, "warning");
|
|
612
|
+
return { action: "handled" };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
561
615
|
// The usage log keeps the kind of request and its length, never its text.
|
|
562
616
|
if (cmd !== "subsub-refresh") logUsage(cfg.usageLog, { ev: "ask", kind: cmd ? `/${cmd}` : "message", words: event.text.trim().split(/\s+/).filter(Boolean).length });
|
|
563
617
|
if (mode === "librarian" && cmd && RESEARCH_PROMPTS.includes(cmd)) {
|
|
@@ -578,7 +632,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
578
632
|
});
|
|
579
633
|
|
|
580
634
|
pi.on("before_agent_start", async (event, ctx) => ({
|
|
581
|
-
systemPrompt: `${event.systemPrompt}\n\n${systemAddition(mode, cfg, ctx.cwd)}`,
|
|
635
|
+
systemPrompt: `${event.systemPrompt}\n\n${systemAddition(mode, cfg, ctx.cwd, { zoteroDown: library.zotero === "down" })}`,
|
|
582
636
|
}));
|
|
583
637
|
|
|
584
638
|
/** Model test (subsub-bench): record every change instead of asking, and apply none. */
|
|
@@ -846,6 +900,8 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
846
900
|
? ov.text
|
|
847
901
|
: `Items: ${o.items}. Without tags: ${o.without_manual_tags}. Missing facets: ${JSON.stringify(o.missing_facet)}. Awaiting review (_agent): ${o.awaiting_review}.`,
|
|
848
902
|
);
|
|
903
|
+
await refreshGroups();
|
|
904
|
+
if (groups.length) lines.push(`Group libraries (read-only, searched with yours): ${groups.map((g) => g.name).join(", ")}.`);
|
|
849
905
|
}
|
|
850
906
|
}
|
|
851
907
|
redraw();
|
|
@@ -870,8 +926,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
870
926
|
const q = String(args ?? "").trim();
|
|
871
927
|
let out: Record<string, unknown>;
|
|
872
928
|
try {
|
|
873
|
-
|
|
874
|
-
out = r.isError ? { error: r.text } : { items: (r.data as { items?: unknown[] })?.items ?? [] };
|
|
929
|
+
out = { items: await searchLibraries(q) };
|
|
875
930
|
} catch (err) {
|
|
876
931
|
out = { error: (err as Error).message };
|
|
877
932
|
}
|
package/src/tui-view.ts
CHANGED
|
@@ -252,7 +252,7 @@ export function sideLines(s: SideState, width: number, rows: number, p: Paint, g
|
|
|
252
252
|
const w = Math.max(16, width);
|
|
253
253
|
const words = SIDE_WORDS[s.lang];
|
|
254
254
|
const mode = s.mode === "librarian" ? "Librarian" : "Researcher";
|
|
255
|
-
const lib = s.library.zotero === "reachable" ? `${s.library.items ?? "?"} ${s.lang === "pt" ? "itens" : "items"}` : s.library.zotero === "down" ? `${g.hollow} Zotero` : "Zotero...";
|
|
255
|
+
const lib = s.library.zotero === "reachable" ? `${s.library.items ?? "?"} ${s.lang === "pt" ? "itens" : "items"}` : s.library.zotero === "down" ? `${g.hollow} Zotero: ${s.lang === "pt" ? "só pesquisa" : "search only"}` : "Zotero...";
|
|
256
256
|
const lines = [p.mark(cut(` Sub-Sub ${s.version}`, w)), p.text(cut(` ${mode}`, w)), p.muted(cut(` ${lib}`, w)), p.dim(cut(` ${s.model}`, w)), rule(w, "solid", p.muted, g)];
|
|
257
257
|
for (const [key, word] of [["F2", words.notes], ["F3", words.library], ["F4", words.actions], ["F5", words.mode]] as const) lines.push(p.muted(cut(` ${key.padEnd(4)}${word}`, w)));
|
|
258
258
|
lines.push(rule(w, "solid", p.muted, g), p.text(cut(` ${words.conversations}`, w)));
|
package/src/tui.ts
CHANGED
|
@@ -12,12 +12,15 @@ import { CombinedAutocompleteProvider, Editor, HStack, Markdown, matchesKey, Pro
|
|
|
12
12
|
import { agentDirFor, chooseCwd, PACKAGE_DIR, packageVersion } from "./cli.ts";
|
|
13
13
|
import { fill, footerLines } from "./chrome.ts";
|
|
14
14
|
import { loadConfig, uiLanguage } from "./config.ts";
|
|
15
|
+
import { modelsAfterLogin } from "./init.ts";
|
|
15
16
|
import { type LibraryState, LIBRARIAN_THEME, DEFAULT_THEME, paintPreview, QUOTES } from "./look.ts";
|
|
16
17
|
import { PURPOSES } from "./read.ts";
|
|
17
18
|
import { allowedFile, listNotes, OPENABLE, openFile } from "./files.ts";
|
|
18
19
|
import { Agent, findLibrary, isDialog, Side, withHints } from "./rpc.ts";
|
|
20
|
+
import { KEY_PROVIDERS, saveApiKey } from "./keys.ts";
|
|
21
|
+
import { CUSTOM_DEFAULT_NAME, saveCustomProvider } from "./providers.ts";
|
|
19
22
|
import { ApprovalView } from "./dialogs.ts";
|
|
20
|
-
import { type Choice, ChoiceView, InputView, LibraryView, type LibraryItem, NotesView } from "./screens.ts";
|
|
23
|
+
import { itemRef, type Choice, ChoiceView, InputView, LibraryView, type LibraryItem, NotesView } from "./screens.ts";
|
|
21
24
|
import { glyphs as chooseGlyphs, type Glyphs, workingFrames } from "./skin.ts";
|
|
22
25
|
import { bannerLines, type Entry, entriesFromMessages, loadPalette, messageText, type Palette, schemeFromEnv, type Scheme, SIDE_WIDTH, sideLines, transcriptLines } from "./tui-view.ts";
|
|
23
26
|
import { loadWords, type Words } from "./words.ts";
|
|
@@ -58,6 +61,10 @@ export interface TuiDeps {
|
|
|
58
61
|
searchDelay?: number;
|
|
59
62
|
/** Called when the user quits or pi stops. */
|
|
60
63
|
onQuit?: (code: number) => void;
|
|
64
|
+
/** pi's agent folder, where /login saves keys (auth.json) and other providers (models.json). */
|
|
65
|
+
agentDir?: string;
|
|
66
|
+
/** After /login saved a provider: set the models when none are set (spec: the other provider's model). */
|
|
67
|
+
afterLogin?: (provider: string, spec?: string) => void;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
const SHORT = (s: string, n: number) => (s.length > n ? `${s.slice(0, n - 3)}...` : s);
|
|
@@ -213,6 +220,8 @@ export class TuiApp {
|
|
|
213
220
|
{ name: "library", description: pt ? "Pesquisar na biblioteca à medida que escreve" : "Search your library as you type" },
|
|
214
221
|
{ name: "actions", description: pt ? "As ações da vista no navegador" : "The actions of the browser view" },
|
|
215
222
|
{ name: "sessions", description: pt ? "Conversas anteriores" : "Earlier conversations" },
|
|
223
|
+
{ name: "resume", description: pt ? "Retomar uma conversa anterior" : "Resume an earlier conversation" },
|
|
224
|
+
{ name: "login", description: pt ? "Ligar um fornecedor do modelo" : "Connect a model provider" },
|
|
216
225
|
{ name: "new", description: pt ? "Uma conversa nova" : "A new conversation" },
|
|
217
226
|
{ name: "model", description: pt ? "Escolher o modelo" : "Choose the model" },
|
|
218
227
|
{ name: "restart", description: pt ? "Recomeçar o pi na mesma conversa" : "Start pi again in the same conversation" },
|
|
@@ -492,7 +501,7 @@ export class TuiApp {
|
|
|
492
501
|
/** Alt+L, /library: search as you type; Enter puts /read <item> in the editor (the Librarian gets the key), ctrl+n puts /lit-note <item>. */
|
|
493
502
|
async openLibrary(): Promise<void> {
|
|
494
503
|
if (this.overlay) return;
|
|
495
|
-
const ref =
|
|
504
|
+
const ref = itemRef;
|
|
496
505
|
this.open(
|
|
497
506
|
new LibraryView({
|
|
498
507
|
...this.common(),
|
|
@@ -558,6 +567,70 @@ export class TuiApp {
|
|
|
558
567
|
);
|
|
559
568
|
}
|
|
560
569
|
|
|
570
|
+
/** Ask one line on its own screen; undefined when the user presses Esc. */
|
|
571
|
+
private ask(title: string, opts: { placeholder?: string; secret?: boolean } = {}): Promise<string | undefined> {
|
|
572
|
+
return new Promise((resolve) => {
|
|
573
|
+
this.open(
|
|
574
|
+
new InputView({
|
|
575
|
+
...this.common(),
|
|
576
|
+
title,
|
|
577
|
+
...opts,
|
|
578
|
+
submit: (text) => (this.close(), resolve(text.trim())),
|
|
579
|
+
cancel: () => (this.close(), resolve(undefined)),
|
|
580
|
+
}),
|
|
581
|
+
true,
|
|
582
|
+
);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** /login: a provider that pi has (its API key) or another provider that speaks the OpenAI API; then pi starts again. */
|
|
587
|
+
async openLogin(): Promise<void> {
|
|
588
|
+
if (this.overlay) return;
|
|
589
|
+
const pt = this.lang === "pt";
|
|
590
|
+
const agentDir = this.d.agentDir;
|
|
591
|
+
if (!agentDir) return void this.notice("error", pt ? "Sem pasta do agente: use subsub web." : "No agent folder: use subsub web.");
|
|
592
|
+
const choices: Choice[] = [
|
|
593
|
+
...KEY_PROVIDERS.map((p) => ({ label: p.label, hint: p.url, value: p.id })),
|
|
594
|
+
{ label: pt ? "Outro fornecedor (API da OpenAI)" : "Another provider (OpenAI API)", hint: pt ? "servidor da instituição, Ollama, LM Studio" : "your institution's server, Ollama, LM Studio", value: CUSTOM_DEFAULT_NAME },
|
|
595
|
+
];
|
|
596
|
+
const provider = await new Promise<string | undefined>((resolve) => {
|
|
597
|
+
this.open(
|
|
598
|
+
new ChoiceView({
|
|
599
|
+
...this.common(),
|
|
600
|
+
title: pt ? "Ligar um fornecedor" : "Connect a provider",
|
|
601
|
+
choices,
|
|
602
|
+
choose: (v) => (this.close(), resolve(v)),
|
|
603
|
+
cancel: () => (this.close(), resolve(undefined)),
|
|
604
|
+
}),
|
|
605
|
+
true,
|
|
606
|
+
);
|
|
607
|
+
});
|
|
608
|
+
if (!provider) return;
|
|
609
|
+
let spec: string | undefined;
|
|
610
|
+
try {
|
|
611
|
+
if (provider === CUSTOM_DEFAULT_NAME) {
|
|
612
|
+
const baseUrl = await this.ask(pt ? "Endereço do servidor" : "Server address", { placeholder: "https://llm.example.org/v1" });
|
|
613
|
+
if (baseUrl === undefined) return;
|
|
614
|
+
const model = await this.ask(pt ? "Identificador do modelo" : "Model id");
|
|
615
|
+
if (model === undefined) return;
|
|
616
|
+
const key = await this.ask(pt ? "Chave de API (Enter sem chave, num servidor local)" : "API key (Enter for none, for a local server)", { secret: true });
|
|
617
|
+
if (key === undefined) return;
|
|
618
|
+
[spec] = saveCustomProvider(agentDir, { name: CUSTOM_DEFAULT_NAME, baseUrl, apiKey: key, models: [model] });
|
|
619
|
+
} else {
|
|
620
|
+
const label = KEY_PROVIDERS.find((p) => p.id === provider)?.label ?? provider;
|
|
621
|
+
const key = await this.ask(pt ? `Chave de API de ${label}` : `API key for ${label}`, { secret: true });
|
|
622
|
+
if (!key) return;
|
|
623
|
+
saveApiKey(agentDir, provider, key);
|
|
624
|
+
}
|
|
625
|
+
this.d.afterLogin?.(provider, spec);
|
|
626
|
+
} catch (err) {
|
|
627
|
+
this.notice("error", (err as Error).message);
|
|
628
|
+
return void this.tui.requestRender();
|
|
629
|
+
}
|
|
630
|
+
this.notice("info", pt ? "Guardado. O pi recomeça com o fornecedor." : "Saved. pi starts again with the provider.");
|
|
631
|
+
await this.restart();
|
|
632
|
+
}
|
|
633
|
+
|
|
561
634
|
/** /new: an empty conversation. */
|
|
562
635
|
async newConversation(): Promise<void> {
|
|
563
636
|
try {
|
|
@@ -633,12 +706,12 @@ export class TuiApp {
|
|
|
633
706
|
this.editor.addToHistory?.(line);
|
|
634
707
|
this.editor.setText("");
|
|
635
708
|
if (/^\/(quit|exit)\s*$/.test(line)) return void (await this.quit());
|
|
636
|
-
const unavailable = /^\/(
|
|
709
|
+
const unavailable = /^\/(logout|settings|tree|fork|share|export|import|hotkeys|reload)\b/.exec(line);
|
|
637
710
|
if (unavailable) {
|
|
638
711
|
this.notice("info", this.lang === "pt" ? `/${unavailable[1]} não existe em subsub tui. Use subsub no terminal ou subsub web.` : `/${unavailable[1]} is not in subsub tui. Use subsub in the terminal, or subsub web.`);
|
|
639
712
|
return void this.tui.requestRender();
|
|
640
713
|
}
|
|
641
|
-
const own: Record<string, () => Promise<void> | void> = { "/restart": () => this.restart(), "/model": () => this.openModels(), "/notes": () => this.openNotes(), "/library": () => this.openLibrary(), "/actions": () => this.openActions(), "/sessions": () => this.openSessions(), "/new": () => this.newConversation() };
|
|
714
|
+
const own: Record<string, () => Promise<void> | void> = { "/restart": () => this.restart(), "/model": () => this.openModels(), "/notes": () => this.openNotes(), "/library": () => this.openLibrary(), "/actions": () => this.openActions(), "/sessions": () => this.openSessions(), "/resume": () => this.openSessions(), "/login": () => this.openLogin(), "/new": () => this.newConversation() };
|
|
642
715
|
if (own[line]) return void (await own[line]());
|
|
643
716
|
try {
|
|
644
717
|
await this.d.agent.request({ type: "prompt", message: line, ...(this.busy ? { streamingBehavior: "followUp" } : {}) });
|
|
@@ -703,6 +776,8 @@ export async function runTui(args: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
703
776
|
themes: { researcher: typeof cfg.themes === "string" ? cfg.themes : undefined, librarian: cfg.themesLibrarian ?? (typeof cfg.themes === "string" ? cfg.themes : undefined) },
|
|
704
777
|
sessions: async () => (await SessionManager.list(cwd)).filter((s) => s.messageCount > 0).sort((a, b) => +new Date(b.modified) - +new Date(a.modified)).map((s) => ({ title: s.name ?? s.firstMessage.slice(0, 60), path: s.path })),
|
|
705
778
|
onQuit: resolve,
|
|
779
|
+
agentDir: env.PI_CODING_AGENT_DIR,
|
|
780
|
+
afterLogin: (provider, spec) => modelsAfterLogin(env, provider, spec),
|
|
706
781
|
});
|
|
707
782
|
void app.start();
|
|
708
783
|
});
|
package/src/web.ts
CHANGED
|
@@ -21,8 +21,9 @@ import { homedir } from "node:os";
|
|
|
21
21
|
import { basename, dirname, extname, join, normalize, relative, resolve, sep } from "node:path";
|
|
22
22
|
import { agentDirFor, chooseCwd, PACKAGE_DIR, packageVersion } from "./cli.ts";
|
|
23
23
|
import { configPath, loadConfig, RECOMMENDED_MODELS, readConfigFile, saveConfig, type SubsubConfig, uiLanguage, withStarbuck } from "./config.ts";
|
|
24
|
-
import {
|
|
24
|
+
import { modelsAfterLogin } from "./init.ts";
|
|
25
25
|
import { KEY_PROVIDERS, saveApiKey } from "./keys.ts";
|
|
26
|
+
import { CUSTOM_DEFAULT_NAME, saveCustomProvider } from "./providers.ts";
|
|
26
27
|
|
|
27
28
|
// ---------------------------------------------------------------- options
|
|
28
29
|
|
|
@@ -479,9 +480,13 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
479
480
|
if (busy || switching) return send(res, 409, { error: "busy" });
|
|
480
481
|
const body = await readJson(req);
|
|
481
482
|
return withRestart(res, () => {
|
|
483
|
+
if (body.provider === CUSTOM_DEFAULT_NAME) {
|
|
484
|
+
// Another provider (OpenAI API): models.json; its model becomes the default when none is set.
|
|
485
|
+
const [spec] = saveCustomProvider(agentDir, { name: CUSTOM_DEFAULT_NAME, baseUrl: String(body.baseUrl ?? ""), apiKey: String(body.key ?? ""), models: [String(body.model ?? "")] });
|
|
486
|
+
return modelsAfterLogin(env, CUSTOM_DEFAULT_NAME, spec);
|
|
487
|
+
}
|
|
482
488
|
saveApiKey(agentDir, String(body.provider ?? ""), String(body.key ?? ""));
|
|
483
|
-
|
|
484
|
-
if (body.provider === "opencode-go" && Object.keys(current.models).length === 0) saveConfig({ models: TESTED_MODELS, model: undefined }, env);
|
|
489
|
+
modelsAfterLogin(env, String(body.provider ?? ""));
|
|
485
490
|
});
|
|
486
491
|
}
|
|
487
492
|
case "GET /api/notes":
|