@tiagojct/subsub 0.9.1 → 0.10.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/README.md +14 -16
- package/dist/bench.js +8 -7
- package/dist/bridge.js +2 -2
- package/dist/config.js +16 -9
- package/dist/doctor.js +2 -2
- package/dist/init.js +25 -13
- package/dist/look.js +5 -5
- package/dist/profiles.js +6 -6
- package/dist/prompt.js +7 -4
- package/dist/roles.js +26 -17
- package/dist/subsub.js +69 -42
- package/dist/web.js +6 -10
- package/docs/Subsub.md +16 -16
- package/package.json +1 -1
- package/prompts/alert.md +1 -1
- package/prompts/clean-tags.md +1 -1
- package/prompts/compare.md +1 -1
- package/prompts/digest.md +2 -2
- package/prompts/gaps.md +2 -2
- package/prompts/import-queue.md +1 -1
- package/prompts/lit-note.md +1 -1
- package/prompts/lit.md +2 -2
- package/prompts/manuscript.md +1 -1
- package/prompts/request-copy.md +1 -1
- package/prompts/review.md +1 -1
- package/prompts/synthesis.md +1 -1
- package/prompts/tag-batch.md +1 -1
- package/prompts/verify.md +1 -1
- package/roles/assistant.md +95 -0
- package/src/bench.ts +12 -7
- package/src/bridge.ts +2 -2
- package/src/config.ts +24 -16
- package/src/doctor.ts +2 -2
- package/src/init.ts +32 -12
- package/src/look.ts +7 -7
- package/src/profiles.ts +6 -6
- package/src/prompt.ts +10 -5
- package/src/roles.ts +38 -20
- package/src/subsub.ts +70 -40
- package/src/web.ts +5 -8
- package/templates/Zotero agent.md +6 -6
- package/web/app.css +16 -22
- package/web/app.js +37 -26
- package/web/i18n.js +37 -35
- package/web/index.html +2 -6
- package/roles/librarian.md +0 -46
- package/roles/researcher.md +0 -50
package/src/subsub.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sub-Sub: a Zotero librarian and research assistant for pi.
|
|
3
3
|
*
|
|
4
|
-
* -
|
|
5
|
-
*
|
|
4
|
+
* - One assistant: it reads the library, searches PubMed, Europe PMC and
|
|
5
|
+
* OpenAlex, writes notes in the Sub-Sub folder and, with library changes on,
|
|
6
|
+
* changes the library. /library on|off is the switch (saved in the settings).
|
|
6
7
|
* - An approval gate: every library change is previewed by the server (dry run)
|
|
7
8
|
* and shown to the user; nothing is applied without a yes.
|
|
8
9
|
* - Profiles (reader, scholar, author, editor) set what Sub-Sub writes and which
|
|
9
10
|
* tools it offers; /profile changes it.
|
|
10
|
-
* - Commands: /
|
|
11
|
+
* - Commands: /library, /profile, /subsub, /history, /undo. /librarian and
|
|
12
|
+
* /researcher (the two modes before 0.10) turn library changes on and off.
|
|
11
13
|
* - Add-on: with "addons": ["starbuck"], Starbuck runs as a third server
|
|
12
|
-
* ("verify") for reference checks
|
|
14
|
+
* ("verify") for reference checks.
|
|
13
15
|
*
|
|
14
16
|
* The Python servers start in session_start (not in the factory) and stop in
|
|
15
17
|
* session_shutdown. Tools are registered once and use the current bridge.
|
|
@@ -25,7 +27,6 @@ import {
|
|
|
25
27
|
findUv,
|
|
26
28
|
isProfile,
|
|
27
29
|
loadConfig,
|
|
28
|
-
type Mode,
|
|
29
30
|
parseEnvFile,
|
|
30
31
|
type Profile,
|
|
31
32
|
saveConfig,
|
|
@@ -52,7 +53,6 @@ export interface SubsubDeps {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
const PACKAGE_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
55
|
-
const MODE_ENTRY = "subsub-mode";
|
|
56
56
|
const BENCH_ENTRY = "subsub-bench";
|
|
57
57
|
|
|
58
58
|
function ownVersion(): string {
|
|
@@ -127,9 +127,12 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
127
127
|
|
|
128
128
|
if (deps.bridge) registerTools(deps.bridge);
|
|
129
129
|
|
|
130
|
-
pi.registerFlag("
|
|
130
|
+
pi.registerFlag("read-only", { description: "Start Sub-Sub with library changes off (this run only)", type: "boolean", default: false });
|
|
131
|
+
pi.registerFlag("librarian", { description: "Start Sub-Sub with library changes on (this run only)", type: "boolean", default: false });
|
|
131
132
|
|
|
132
|
-
let
|
|
133
|
+
let libraryChanges = cfg.libraryChanges;
|
|
134
|
+
const benchRun = Boolean(process.env.SUBSUB_BENCH_OUT);
|
|
135
|
+
const toolOpts = () => ({ libraryChanges, profile, bench: benchRun });
|
|
133
136
|
let profile: Profile = cfg.profile;
|
|
134
137
|
const user = who(cfg);
|
|
135
138
|
/** True when started with the `subsub` command (not as a package inside plain pi). */
|
|
@@ -151,7 +154,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
151
154
|
if (!webView || !ctx || ctx.mode !== "rpc") return;
|
|
152
155
|
const down = bridge ? Object.keys(bridge.errors) : ["zotero", "scholar"];
|
|
153
156
|
const state = {
|
|
154
|
-
|
|
157
|
+
libraryChanges,
|
|
155
158
|
profile,
|
|
156
159
|
profileLabel: profileSpec(profile).label,
|
|
157
160
|
model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : null,
|
|
@@ -172,7 +175,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
172
175
|
function applyTheme(ctx: ExtensionContext): void {
|
|
173
176
|
if (!lookOn(ctx) || cfg.themes === false) return;
|
|
174
177
|
scheme ??= schemeFromAnsi(ctx.ui.theme.getFgAnsi("text"), ctx.ui.theme.name);
|
|
175
|
-
const name = themeName(
|
|
178
|
+
const name = themeName(scheme, cfg.themes || undefined);
|
|
176
179
|
const theme = name ? ctx.ui.getTheme(name) : undefined;
|
|
177
180
|
// A Theme object (not a name) changes the theme for this run only; the saved setting stays.
|
|
178
181
|
if (theme) ctx.ui.setTheme(theme);
|
|
@@ -218,7 +221,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
218
221
|
const th = ctx.ui.theme;
|
|
219
222
|
return headerLines({
|
|
220
223
|
version,
|
|
221
|
-
|
|
224
|
+
libraryChanges,
|
|
222
225
|
profile: profileSpec(profile).label,
|
|
223
226
|
model: modelId,
|
|
224
227
|
library,
|
|
@@ -241,28 +244,37 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
241
244
|
function status(ctx: ExtensionContext): void {
|
|
242
245
|
modelId = ctx.model ? `${ctx.model.id}` : "no model";
|
|
243
246
|
const down = bridge ? Object.keys(bridge.errors) : ["zotero", "scholar"];
|
|
244
|
-
|
|
245
|
-
|
|
247
|
+
const lib = libraryChanges ? "library changes on" : "read only";
|
|
248
|
+
ctx.ui.setStatus("subsub", `Sub-Sub: ${lib} | ${profileSpec(profile).label} | ${modelId}${down.length ? ` | not running: ${down.join(", ")}` : ""}`);
|
|
249
|
+
if (standalone) ctx.ui.setTitle(libraryChanges ? "Sub-Sub" : "Sub-Sub (read only)");
|
|
246
250
|
headerTui?.requestRender();
|
|
247
251
|
webCtx = ctx;
|
|
248
252
|
emitWeb();
|
|
249
253
|
}
|
|
250
254
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
255
|
+
/** Set the tools for the switch and the profile; save the switch when asked. */
|
|
256
|
+
function applyLibrary(ctx: ExtensionContext, on: boolean, save: boolean): void {
|
|
257
|
+
libraryChanges = on;
|
|
258
|
+
pi.setActiveTools(toolsFor(registered(), toolOpts()));
|
|
259
|
+
if (save) {
|
|
260
|
+
try {
|
|
261
|
+
saveConfig({ libraryChanges: on });
|
|
262
|
+
} catch (err) {
|
|
263
|
+
ctx.ui.notify(`Sub-Sub: the setting was not saved: ${(err as Error).message}`, "warning");
|
|
264
|
+
}
|
|
260
265
|
}
|
|
261
|
-
if (remember) pi.appendEntry(MODE_ENTRY, { mode });
|
|
262
|
-
applyTheme(ctx);
|
|
263
266
|
status(ctx);
|
|
264
267
|
}
|
|
265
268
|
|
|
269
|
+
async function applyModel(ctx: ExtensionContext): Promise<void> {
|
|
270
|
+
const spec = cfg.model;
|
|
271
|
+
if (!spec) return;
|
|
272
|
+
const i = spec.indexOf("/");
|
|
273
|
+
const model = i > 0 ? ctx.modelRegistry.find(spec.slice(0, i), spec.slice(i + 1)) : undefined;
|
|
274
|
+
const ok = model ? await pi.setModel(model) : false;
|
|
275
|
+
if (!ok) ctx.ui.notify(`Sub-Sub: cannot use ${spec} (unknown model or no key); keeping the current model.`, "warning");
|
|
276
|
+
}
|
|
277
|
+
|
|
266
278
|
pi.on("session_start", async (_event, ctx) => {
|
|
267
279
|
if (!bridge) {
|
|
268
280
|
const b = new Bridge();
|
|
@@ -270,14 +282,12 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
270
282
|
bridge = b;
|
|
271
283
|
registerTools(b);
|
|
272
284
|
}
|
|
273
|
-
let restored: Mode | undefined;
|
|
274
|
-
for (const e of ctx.sessionManager.getBranch() as Array<{ customType?: string; data?: { mode?: unknown } }>) {
|
|
275
|
-
const m = e.customType === MODE_ENTRY ? e.data?.mode : undefined;
|
|
276
|
-
if (m === "librarian" || m === "researcher") restored = m;
|
|
277
|
-
}
|
|
278
285
|
// Judge light or dark before Sub-Sub changes the theme.
|
|
279
286
|
if (lookOn(ctx)) scheme ??= schemeFromAnsi(ctx.ui.theme.getFgAnsi("text"), ctx.ui.theme.name);
|
|
280
|
-
await
|
|
287
|
+
await applyModel(ctx);
|
|
288
|
+
applyTheme(ctx);
|
|
289
|
+
// The flags change this run only; the saved setting stays.
|
|
290
|
+
applyLibrary(ctx, pi.getFlag("read-only") ? false : pi.getFlag("librarian") ? true : cfg.libraryChanges, false);
|
|
281
291
|
if (lookOn(ctx)) {
|
|
282
292
|
setHeader(ctx);
|
|
283
293
|
void refreshLibrary();
|
|
@@ -308,7 +318,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
308
318
|
});
|
|
309
319
|
|
|
310
320
|
pi.on("before_agent_start", async (event, ctx) => ({
|
|
311
|
-
systemPrompt: `${event.systemPrompt}\n\n${systemAddition(
|
|
321
|
+
systemPrompt: `${event.systemPrompt}\n\n${systemAddition({ ...cfg, profile }, ctx.cwd, libraryChanges)}`,
|
|
312
322
|
}));
|
|
313
323
|
|
|
314
324
|
/** Model test (subsub-bench): record every change instead of asking, and apply none. */
|
|
@@ -323,7 +333,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
323
333
|
if (schema) coerceArgs(schema, input);
|
|
324
334
|
resolvePathArgs(name, input, (p) => normalizeToolPath(p, ctx.cwd));
|
|
325
335
|
if (SERVER_PREFIXES.some((p) => name.startsWith(p))) {
|
|
326
|
-
const why = unavailableReason(name,
|
|
336
|
+
const why = unavailableReason(name, registered(), toolOpts());
|
|
327
337
|
if (why) {
|
|
328
338
|
if (benchOut) record({ kind: "wrong_mode", tool: name, input });
|
|
329
339
|
return { block: true, reason: why };
|
|
@@ -437,19 +447,39 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
437
447
|
};
|
|
438
448
|
}
|
|
439
449
|
|
|
450
|
+
const libraryNote = (on: boolean) =>
|
|
451
|
+
on
|
|
452
|
+
? "Sub-Sub: library changes on. Every change shows a preview first."
|
|
453
|
+
: "Sub-Sub: library changes off. Sub-Sub reads, searches and writes notes; it does not change the library.";
|
|
454
|
+
|
|
455
|
+
pi.registerCommand("library", {
|
|
456
|
+
description: "Sub-Sub: turn library changes on or off (/library on, /library off)",
|
|
457
|
+
handler: async (args, ctx) => {
|
|
458
|
+
const a = args.trim().toLowerCase();
|
|
459
|
+
const on = a === "on" ? true : a === "off" ? false : a === "" ? !libraryChanges : undefined;
|
|
460
|
+
if (on === undefined) {
|
|
461
|
+
ctx.ui.notify("Type /library on or /library off.", "warning");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
applyLibrary(ctx, on, true);
|
|
465
|
+
ctx.ui.notify(libraryNote(on), "info");
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// The two modes before 0.10, kept so that old habits and notes still work.
|
|
440
470
|
pi.registerCommand("librarian", {
|
|
441
|
-
description: "Sub-Sub:
|
|
471
|
+
description: "Sub-Sub: same as /library on",
|
|
442
472
|
handler: async (_args, ctx) => {
|
|
443
|
-
|
|
444
|
-
ctx.ui.notify(
|
|
473
|
+
applyLibrary(ctx, true, true);
|
|
474
|
+
ctx.ui.notify(`${libraryNote(true)} (/librarian is now /library on.)`, "info");
|
|
445
475
|
},
|
|
446
476
|
});
|
|
447
477
|
|
|
448
478
|
pi.registerCommand("researcher", {
|
|
449
|
-
description: "Sub-Sub:
|
|
479
|
+
description: "Sub-Sub: same as /library off",
|
|
450
480
|
handler: async (_args, ctx) => {
|
|
451
|
-
|
|
452
|
-
ctx.ui.notify(
|
|
481
|
+
applyLibrary(ctx, false, true);
|
|
482
|
+
ctx.ui.notify(`${libraryNote(false)} (/researcher is now /library off.)`, "info");
|
|
453
483
|
},
|
|
454
484
|
});
|
|
455
485
|
|
|
@@ -472,7 +502,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
472
502
|
} catch (err) {
|
|
473
503
|
saved = ` Not saved: ${(err as Error).message}`;
|
|
474
504
|
}
|
|
475
|
-
|
|
505
|
+
applyLibrary(ctx, libraryChanges, false);
|
|
476
506
|
ctx.ui.notify(`Sub-Sub: ${profileSpec(profile).label} profile (${profileSpec(profile).summary}).${saved}`, "info");
|
|
477
507
|
},
|
|
478
508
|
});
|
|
@@ -480,7 +510,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
480
510
|
pi.registerCommand("subsub", {
|
|
481
511
|
description: "Sub-Sub: Zotero connection and library overview",
|
|
482
512
|
handler: async (_args, ctx) => {
|
|
483
|
-
const lines = [`
|
|
513
|
+
const lines = [`Library changes: ${libraryChanges ? "on" : "off"}. Profile: ${profileSpec(profile).label}. Model: ${ctx.model?.id ?? "none"}.`];
|
|
484
514
|
if (!bridge) {
|
|
485
515
|
ctx.ui.notify(`${lines[0]}\nThe Zotero servers are not running. Try /reload.`, "warning");
|
|
486
516
|
return;
|
package/src/web.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { extname, join, normalize, resolve, sep } from "node:path";
|
|
|
23
23
|
import { StringDecoder } from "node:string_decoder";
|
|
24
24
|
import { agentDirFor, chooseCwd, PACKAGE_DIR, packageVersion } from "./cli.ts";
|
|
25
25
|
import { expand, loadConfig, saveConfig, type SubsubConfig, withStarbuck } from "./config.ts";
|
|
26
|
-
import {
|
|
26
|
+
import { TESTED_MODEL } from "./init.ts";
|
|
27
27
|
import { obsidianRoot } from "./layout.ts";
|
|
28
28
|
import { realish, within } from "./paths.ts";
|
|
29
29
|
|
|
@@ -568,12 +568,9 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
568
568
|
if (busy) return send(res, 409, { error: "busy" });
|
|
569
569
|
const body = await readJson(req);
|
|
570
570
|
const spec = `${String(body.provider)}/${String(body.id)}`;
|
|
571
|
-
//
|
|
572
|
-
// extension reads the new setting.
|
|
573
|
-
|
|
574
|
-
const models = { ...(loadConfig(env).models ?? {}), [mode]: spec };
|
|
575
|
-
if (body.both) models[mode === "librarian" ? "researcher" : "librarian"] = spec;
|
|
576
|
-
saveConfig({ models }, env);
|
|
571
|
+
// Remember the model (and drop the per-mode models of versions before 0.10), then restart pi
|
|
572
|
+
// so the extension reads the new setting.
|
|
573
|
+
saveConfig({ model: spec, models: undefined }, env);
|
|
577
574
|
await agent.request({ type: "set_model", provider: String(body.provider), modelId: String(body.id) });
|
|
578
575
|
await restartAgent();
|
|
579
576
|
return send(res, 200, { ok: true });
|
|
@@ -597,7 +594,7 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
597
594
|
const body = await readJson(req);
|
|
598
595
|
saveApiKey(agentDir, String(body.provider ?? ""), String(body.key ?? ""));
|
|
599
596
|
const current = loadConfig(env);
|
|
600
|
-
if (body.provider === "opencode-go" &&
|
|
597
|
+
if (body.provider === "opencode-go" && !current.model) saveConfig({ model: TESTED_MODEL, models: undefined }, env);
|
|
601
598
|
await restartAgent();
|
|
602
599
|
return send(res, 200, { ok: true });
|
|
603
600
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
Shared rules for
|
|
1
|
+
Shared rules for Sub-Sub. Sub-Sub reads this file at the start of every session. Edit it to change how notes look or where they go. Tag list: [[Zotero tags]].
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## What Sub-Sub does
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
5
|
+
- Sub-Sub reads the library, searches PubMed, Europe PMC and OpenAlex, and writes notes in this folder.
|
|
6
|
+
- With library changes on, it also changes the library: tags, imports, metadata, PDFs, collections, notes in Zotero. Every change shows a preview and needs the user's yes.
|
|
7
|
+
- Works found in a search are imported directly when the user chooses them. For a list to choose from later, Sub-Sub writes Inbox/Zotero import queue.md; the user ticks the lines and Sub-Sub imports them.
|
|
8
8
|
|
|
9
9
|
## Safety
|
|
10
10
|
|
|
@@ -85,5 +85,5 @@ created: YYYY-MM-DD
|
|
|
85
85
|
- Structure: scope and question; synthesis by theme; disagreements; gaps; a table of included items (citekey, year, design, main finding, source: full text or abstract).
|
|
86
86
|
- For a tag ("what does my library say about X"), include every item with the tag, one table row each. If an item is left out, say which and why.
|
|
87
87
|
- Cite with [@citekey] after each claim. Every claim must come from an included item.
|
|
88
|
-
- Works that are not in the library go in a separate section "Not in the library", with DOI or PMID, and are proposed for
|
|
88
|
+
- Works that are not in the library go in a separate section "Not in the library", with DOI or PMID, and are proposed for import.
|
|
89
89
|
- Link existing literature notes as [[citekey]].
|
package/web/app.css
CHANGED
|
@@ -87,9 +87,8 @@
|
|
|
87
87
|
--warn: #d6a84e;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/*
|
|
90
|
+
/* One accent, blue. Rust marks the switch that lets Sub-Sub change the library. */
|
|
91
91
|
body { --accent: var(--researcher); --accent-soft: var(--researcher-soft); }
|
|
92
|
-
body[data-mode="librarian"] { --accent: var(--librarian); --accent-soft: var(--librarian-soft); }
|
|
93
92
|
|
|
94
93
|
/* ---------------------------------------------------------------- base */
|
|
95
94
|
|
|
@@ -220,27 +219,20 @@ select:hover { border-color: var(--rule-strong); }
|
|
|
220
219
|
.brand .mark { color: var(--accent); }
|
|
221
220
|
.brand .hyphen { color: var(--accent); }
|
|
222
221
|
|
|
223
|
-
.
|
|
224
|
-
|
|
225
|
-
padding:
|
|
226
|
-
border-radius: 8px;
|
|
227
|
-
background: var(--surface);
|
|
222
|
+
/* The library switch in the top bar: rust when Sub-Sub may change the library. */
|
|
223
|
+
.lib-switch {
|
|
224
|
+
padding: 0.3rem 0.7rem 0.3rem 0.5rem;
|
|
228
225
|
border: 1px solid var(--rule);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
border-radius: 5px;
|
|
233
|
-
font-size: 0.86rem;
|
|
226
|
+
border-radius: 999px;
|
|
227
|
+
background: var(--surface);
|
|
228
|
+
font-size: 0.84rem;
|
|
234
229
|
color: var(--muted);
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
.modes button:hover { color: var(--text); }
|
|
238
|
-
.modes button[aria-pressed="true"] {
|
|
239
|
-
background: var(--raised);
|
|
240
|
-
color: var(--accent);
|
|
241
|
-
font-weight: 600;
|
|
242
|
-
box-shadow: 0 1px 2px rgb(0 0 0 / 0.08), 0 0 0 1px var(--rule);
|
|
230
|
+
white-space: nowrap;
|
|
243
231
|
}
|
|
232
|
+
.lib-switch:hover { border-color: var(--rule-strong); color: var(--text); }
|
|
233
|
+
.top .lib-switch input:checked + .track { background: var(--librarian); }
|
|
234
|
+
body[data-library="on"] .lib-switch { color: var(--text); }
|
|
235
|
+
body[data-library="off"] .top { border-top-color: var(--rule-strong); }
|
|
244
236
|
|
|
245
237
|
.status {
|
|
246
238
|
margin-left: auto;
|
|
@@ -316,7 +308,8 @@ select:hover { border-color: var(--rule-strong); }
|
|
|
316
308
|
border-radius: 5px;
|
|
317
309
|
}
|
|
318
310
|
.actions button:hover { background: var(--bg); }
|
|
319
|
-
.actions
|
|
311
|
+
.actions .group { margin: 0.55rem 0 0.15rem; padding: 0 0.5rem; font-size: 0.74rem; font-weight: 600; color: var(--muted); }
|
|
312
|
+
.actions .group:first-child { margin-top: 0; }
|
|
320
313
|
|
|
321
314
|
.side .grow { flex: 1; display: flex; flex-direction: column; min-height: 8rem; }
|
|
322
315
|
.side-subhead { display: flex; justify-content: space-between; align-items: baseline; padding-right: 0.3rem; }
|
|
@@ -862,7 +855,8 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
|
|
|
862
855
|
|
|
863
856
|
@media (max-width: 30rem) {
|
|
864
857
|
.brand > span { display: none; }
|
|
865
|
-
.
|
|
858
|
+
.lib-switch > span:last-child { display: none; }
|
|
859
|
+
.lib-switch { padding: 0.35rem 0.5rem; }
|
|
866
860
|
}
|
|
867
861
|
|
|
868
862
|
@media (prefers-reduced-motion: reduce) {
|
package/web/app.js
CHANGED
|
@@ -7,7 +7,7 @@ import { ACTIONS, STRINGS, SUGGESTIONS, TOOLS } from "/i18n.js";
|
|
|
7
7
|
// ---------------------------------------------------------------- state
|
|
8
8
|
|
|
9
9
|
let lang = "en";
|
|
10
|
-
let state = null; // Sub-Sub's own state:
|
|
10
|
+
let state = null; // Sub-Sub's own state: libraryChanges, profile, model, library, down
|
|
11
11
|
let session = null; // pi's session state
|
|
12
12
|
let busy = false;
|
|
13
13
|
let commands = [];
|
|
@@ -169,13 +169,15 @@ function applyLang() {
|
|
|
169
169
|
updateLayoutUI();
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
/** Library changes on (the default until the state arrives). */
|
|
173
|
+
function libOn() {
|
|
174
|
+
return state?.libraryChanges !== false;
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
function renderHeader() {
|
|
177
|
-
document.body.dataset.
|
|
178
|
-
|
|
178
|
+
document.body.dataset.library = libOn() ? "on" : "off";
|
|
179
|
+
$("library-switch").checked = libOn();
|
|
180
|
+
$("library-switch-label").title = t("libraryHint");
|
|
179
181
|
if (state?.profile) $("profile").value = $("profile-side").value = state.profile;
|
|
180
182
|
$("starbuck-toggle").checked = Boolean(state?.starbuck);
|
|
181
183
|
$("starbuck-label").title = t("starbuckHint");
|
|
@@ -203,7 +205,7 @@ function renderHeader() {
|
|
|
203
205
|
lib.classList.add("down");
|
|
204
206
|
}
|
|
205
207
|
lib.title = txt.textContent;
|
|
206
|
-
$("input").placeholder =
|
|
208
|
+
$("input").placeholder = libOn() ? t("placeholder") : t("placeholderReadOnly");
|
|
207
209
|
if (log.querySelector(".empty")) {
|
|
208
210
|
clearEmpty();
|
|
209
211
|
renderEmpty();
|
|
@@ -221,12 +223,17 @@ function hasCommand(name) {
|
|
|
221
223
|
function renderActions() {
|
|
222
224
|
const box = $("actions");
|
|
223
225
|
box.replaceChildren();
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
for (const
|
|
226
|
+
const shown = (a) =>
|
|
227
|
+
hasCommand(a.cmd) &&
|
|
228
|
+
(!a.changes || libOn()) &&
|
|
229
|
+
(!a.needs || state?.[a.needs]) &&
|
|
230
|
+
(!a.profiles || a.profiles.includes(state?.profile ?? "scholar"));
|
|
231
|
+
for (const [group, label] of [["research", "groupResearch"], ["library", "groupLibrary"]]) {
|
|
232
|
+
const list = ACTIONS[group].filter(shown);
|
|
233
|
+
if (!list.length) continue;
|
|
234
|
+
box.append(el("p", { class: "group", text: t(label) }));
|
|
235
|
+
for (const a of list) box.append(actionButton(a));
|
|
236
|
+
}
|
|
230
237
|
}
|
|
231
238
|
|
|
232
239
|
function actionButton(a) {
|
|
@@ -326,17 +333,17 @@ function exportConversation() {
|
|
|
326
333
|
if (text && text !== "/subsub-refresh") msgs.push(`### User\n\n${text}\n`);
|
|
327
334
|
} else if (m.classList.contains("assistant")) {
|
|
328
335
|
const text = Array.from(m.querySelectorAll(".md")).map((d) => d.innerText).join("\n\n");
|
|
329
|
-
if (text) msgs.push(`### Sub-Sub
|
|
336
|
+
if (text) msgs.push(`### Sub-Sub\n\n${text}\n`);
|
|
330
337
|
}
|
|
331
338
|
}
|
|
332
339
|
if (!msgs.length) return;
|
|
333
340
|
const date = new Date().toISOString().slice(0, 10);
|
|
334
|
-
const content = `# Sub-Sub Research Session (${date})\n\n**
|
|
341
|
+
const content = `# Sub-Sub Research Session (${date})\n\n**Profile**: ${state?.profile ?? "scholar"} \n\n---\n\n${msgs.join("\n---\n\n")}`;
|
|
335
342
|
const blob = new Blob([content], { type: "text/markdown;charset=utf-8" });
|
|
336
343
|
const url = URL.createObjectURL(blob);
|
|
337
344
|
const a = document.createElement("a");
|
|
338
345
|
a.href = url;
|
|
339
|
-
a.download = `Sub-Sub-${
|
|
346
|
+
a.download = `Sub-Sub-${date}.md`;
|
|
340
347
|
document.body.append(a);
|
|
341
348
|
a.click();
|
|
342
349
|
a.remove();
|
|
@@ -369,14 +376,14 @@ function clearEmpty() {
|
|
|
369
376
|
|
|
370
377
|
function renderEmpty() {
|
|
371
378
|
if (log.children.length) return;
|
|
372
|
-
const s = SUGGESTIONS
|
|
379
|
+
const s = SUGGESTIONS.filter((x) => !x.changes || libOn()).map((x) => x[lang] ?? x.en);
|
|
373
380
|
log.append(
|
|
374
381
|
el(
|
|
375
382
|
"div",
|
|
376
383
|
{ class: "empty" },
|
|
377
|
-
el("p", { class: "kicker", text: t(
|
|
384
|
+
el("p", { class: "kicker", text: libOn() ? t("assistant") : `${t("assistant")} · ${t("readOnly")}` }),
|
|
378
385
|
el("h1", { text: t("emptyTitle") }),
|
|
379
|
-
el("p", { class: "lede", text: t(
|
|
386
|
+
el("p", { class: "lede", text: t(libOn() ? "emptyOn" : "emptyOff") }),
|
|
380
387
|
el(
|
|
381
388
|
"div",
|
|
382
389
|
{ class: "chips" },
|
|
@@ -708,7 +715,8 @@ function onEvent(ev) {
|
|
|
708
715
|
break;
|
|
709
716
|
case "extension_ui_request":
|
|
710
717
|
if (ev.method === "notify") {
|
|
711
|
-
|
|
718
|
+
// The switch shows the state; these confirmations would only repeat it.
|
|
719
|
+
if (/^Sub-Sub: library changes (on|off)\./.test(ev.message)) break;
|
|
712
720
|
note(ev.message, ev.notifyType === "error" ? "error" : ev.notifyType === "warning" ? "warning" : "info", /\n/.test(ev.message));
|
|
713
721
|
} else {
|
|
714
722
|
dialogs.push(ev);
|
|
@@ -893,7 +901,7 @@ async function openModels() {
|
|
|
893
901
|
onclick: async () => {
|
|
894
902
|
if (busy) return note(t("busy"), "warning");
|
|
895
903
|
try {
|
|
896
|
-
await api("/api/model", { provider: m.provider, id: m.id
|
|
904
|
+
await api("/api/model", { provider: m.provider, id: m.id });
|
|
897
905
|
dlg.close();
|
|
898
906
|
} catch (err) {
|
|
899
907
|
note(err.message, "error");
|
|
@@ -935,15 +943,17 @@ function autosize() {
|
|
|
935
943
|
|
|
936
944
|
async function sendMessage(text) {
|
|
937
945
|
const message = text.trim();
|
|
938
|
-
if (!message) return;
|
|
946
|
+
if (!message) return false;
|
|
939
947
|
const name = message.startsWith("/") ? message.slice(1).split(/\s/)[0] : "";
|
|
940
948
|
const isTemplate = name && commands.some((c) => c.name === name && c.source === "prompt");
|
|
941
949
|
if (isTemplate) sentTemplates.push(message);
|
|
942
950
|
try {
|
|
943
951
|
await api("/api/prompt", { message });
|
|
952
|
+
return true;
|
|
944
953
|
} catch (err) {
|
|
945
954
|
if (isTemplate) sentTemplates.pop();
|
|
946
955
|
note(err.message, "error");
|
|
956
|
+
return false;
|
|
947
957
|
}
|
|
948
958
|
}
|
|
949
959
|
|
|
@@ -1032,11 +1042,12 @@ $("new-btn").addEventListener("click", async () => {
|
|
|
1032
1042
|
note(err.message, "error");
|
|
1033
1043
|
}
|
|
1034
1044
|
});
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
});
|
|
1039
|
-
|
|
1045
|
+
$("library-switch").addEventListener("change", async (e) => {
|
|
1046
|
+
const on = e.target.checked;
|
|
1047
|
+
if (on === libOn()) return;
|
|
1048
|
+
const ok = await sendMessage(`/library ${on ? "on" : "off"}`);
|
|
1049
|
+
if (!ok) e.target.checked = libOn();
|
|
1050
|
+
});
|
|
1040
1051
|
$("profile").addEventListener("change", (e) => sendMessage(`/profile ${e.target.value}`));
|
|
1041
1052
|
$("starbuck-toggle").addEventListener("change", async (e) => {
|
|
1042
1053
|
const on = e.target.checked;
|