@tiagojct/subsub 0.11.4 → 0.12.1

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/web.ts CHANGED
@@ -22,7 +22,7 @@ import { homedir } from "node:os";
22
22
  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
- import { configPath, expand, loadConfig, RECOMMENDED_MODELS, readConfigFile, saveConfig, type SubsubConfig, withStarbuck } from "./config.ts";
25
+ import { configPath, expand, loadConfig, RECOMMENDED_MODELS, readConfigFile, saveConfig, type SubsubConfig, uiLanguage, withStarbuck } from "./config.ts";
26
26
  import { TESTED_MODELS } from "./init.ts";
27
27
  import { KEY_PROVIDERS, saveApiKey } from "./keys.ts";
28
28
  import { obsidianRoot } from "./layout.ts";
@@ -56,10 +56,7 @@ export function parseWebArgs(args: string[]): WebOptions {
56
56
  return o;
57
57
  }
58
58
 
59
- /** "pt" when the language setting is Portuguese, else "en". */
60
- export function uiLanguage(language: string | undefined): "pt" | "en" {
61
- return /portug|^pt\b|^pt-/i.test(language ?? "") ? "pt" : "en";
62
- }
59
+ export { uiLanguage } from "./config.ts";
63
60
 
64
61
  // ---------------------------------------------------------------- providers (API keys)
65
62
 
package/web/app.css CHANGED
@@ -883,3 +883,14 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
883
883
  @media (prefers-reduced-motion: reduce) {
884
884
  *, *::before, *::after { animation: none !important; transition: none !important; }
885
885
  }
886
+
887
+ /* First run: what is still missing */
888
+ .setup { margin-block: 1.4rem 1rem; padding: 0.9rem 1rem; border: 1px solid var(--rule); border-left: 3px solid var(--accent); border-radius: 6px; background: var(--raised); text-align: left; max-width: 40rem; }
889
+ .setup h2 { margin: 0 0 0.5rem; font-size: 0.95rem; }
890
+ .setup ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.55rem; }
891
+ .setup li { display: grid; grid-template-columns: 4.5rem 1fr auto; gap: 0.6rem; align-items: start; font-size: 0.88rem; line-height: 1.45; }
892
+ .setup li > * { min-width: 0; }
893
+ .setup .mark { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; padding-top: 0.15rem; color: var(--muted); }
894
+ .setup li.todo .mark { color: var(--rem); }
895
+ .setup li.ok .step-text { color: var(--muted); }
896
+ @media (max-width: 560px) { .setup li { grid-template-columns: 1fr; gap: 0.2rem; } }
package/web/app.js CHANGED
@@ -370,6 +370,7 @@ function scrollDown(force = false) {
370
370
 
371
371
  // A provider error in plain words (keep in step with providerTrouble in src/keys.ts), with the raw text below.
372
372
  function troubleOf(text) {
373
+ if (/requires Global regions|not available in your region|region.{0,40}not (supported|allowed)/i.test(text)) return "region";
373
374
  if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|usage limit/i.test(text)) return "quota";
374
375
  if (/\b(503|529)\b|UNAVAILABLE|high demand|overloaded/i.test(text)) return "busy";
375
376
  if (/RECITATION|SAFETY|PROHIBITED_CONTENT/.test(text)) return "blocked";
@@ -395,6 +396,36 @@ function clearEmpty() {
395
396
  log.querySelector(".empty")?.remove();
396
397
  }
397
398
 
399
+ /** First run: what is still missing (Zotero, a model, the servers), each with its fix. Hidden when all is ready. */
400
+ function setupList() {
401
+ const L = state?.library;
402
+ const m = state?.model ?? session?.model;
403
+ const steps = [
404
+ { ok: L?.zotero === "reachable", wait: !L || L.zotero === "checking", text: t("stepZotero") },
405
+ { ok: Boolean(m && m.id && m.id !== "unknown"), text: t("stepModel"), action: { label: t("connect"), run: openModels } },
406
+ { ok: !state?.down?.length, text: t("stepServers", { x: (state?.down ?? []).join(", ") }) },
407
+ ];
408
+ if (steps.every((x) => x.ok || x.wait)) return null;
409
+ return el(
410
+ "div",
411
+ { class: "setup" },
412
+ el("h2", { text: t("setupTitle") }),
413
+ el(
414
+ "ol",
415
+ {},
416
+ steps.map((x) =>
417
+ el(
418
+ "li",
419
+ { class: x.ok ? "ok" : x.wait ? "wait" : "todo" },
420
+ el("span", { class: "mark", text: x.ok ? t("stepDone") : x.wait ? "…" : t("stepTodo") }),
421
+ el("span", { class: "step-text", text: x.ok ? x.text.split(". ")[0] : x.text }),
422
+ !x.ok && x.action ? el("button", { type: "button", class: "primary small-btn", text: x.action.label, onclick: x.action.run }) : null,
423
+ ),
424
+ ),
425
+ ),
426
+ );
427
+ }
428
+
398
429
  function renderEmpty() {
399
430
  if (log.children.length) return;
400
431
  const s = SUGGESTIONS[mode()].map((x) => x[lang] ?? x.en);
@@ -405,6 +436,7 @@ function renderEmpty() {
405
436
  el("p", { class: "kicker", text: t(mode()) }),
406
437
  el("h1", { text: t("emptyTitle") }),
407
438
  el("p", { class: "lede", text: t(mode() === "librarian" ? "emptyLibrarian" : "emptyResearcher") }),
439
+ setupList(),
408
440
  el(
409
441
  "div",
410
442
  { class: "chips" },
@@ -505,6 +537,23 @@ function finishTool(id, result, isError) {
505
537
  row.querySelector("pre")?.remove();
506
538
  if (text) row.append(el("pre", { text: text.length > 6000 ? `${text.slice(0, 6000)}\n...` : text }));
507
539
  if (isError) row.querySelector(".label").textContent = `${toolLabel(row.dataset.name)} (${t("failed")})`;
540
+ // An applied library change gets an Undo button; it runs /undo with its id, which previews first.
541
+ const jid = !isError && !/undo/.test(row.dataset.name) && /"journal_id":\s*"(\d{8}-\d{6}-\d{2}-[a-z_]+)"/.exec(text ?? "")?.[1];
542
+ if (jid && !row.querySelector(".undo")) {
543
+ row.querySelector("summary").append(
544
+ el("button", {
545
+ type: "button",
546
+ class: "open undo",
547
+ text: t("undo"),
548
+ title: t("undoTitle"),
549
+ onclick: (e) => {
550
+ e.preventDefault();
551
+ if (busy) return note(t("busy"), "warning");
552
+ sendMessage(`/undo ${jid}`);
553
+ },
554
+ }),
555
+ );
556
+ }
508
557
  }
509
558
 
510
559
  function attachCopyButtons(root) {
@@ -925,6 +974,7 @@ async function openModels() {
925
974
  const free = { google: "freeGoogle", openrouter: "freeOpenRouter" }[sel.value];
926
975
  $("login-free").hidden = !free;
927
976
  $("login-free").textContent = free ? t(free) : "";
977
+ $("login-where").textContent = p?.where ? t(`where_${p.where}`) : "";
928
978
  };
929
979
  sel.onchange = setLink;
930
980
  setLink();
package/web/i18n.js CHANGED
@@ -53,6 +53,7 @@ export const STRINGS = {
53
53
  rec_tested: "tested default",
54
54
  rec_cheaper: "tested, cheaper",
55
55
  rec_free: "tested, free",
56
+ rec_eu: "tested, EU",
56
57
  needsKey: "needs a key from {x}",
57
58
  noModels: "No models yet. Connect a provider below.",
58
59
  connectProvider: "Connect a provider",
@@ -67,7 +68,20 @@ export const STRINGS = {
67
68
  trouble_busy: "The model provider is busy at the moment. Try again in a few minutes, or choose another model with the model button.",
68
69
  trouble_blocked: "The model provider stopped the reply (for example, because it repeated a source word for word). Ask again in other words, or choose another model.",
69
70
  trouble_key: "The model provider did not accept the API key. Enter it again with the model button.",
71
+ trouble_region: "The provider refuses this model with your account's region setting. In OpenCode, allow Global regions in the workspace's Privacy settings, or choose a model your region allows with the model button.",
70
72
  details: "Details",
73
+ undo: "Undo",
74
+ where_eu: "Where your data goes: servers in the European Union. Covered by the GDPR, the choice for material you would not send outside the EU.",
75
+ where_us: "Where your data goes: servers outside the EU, mostly in the United States, under the provider's terms for the EU. Read them before you send unpublished or sensitive material.",
76
+ where_cn: "Where your data goes: servers in China. Do not send unpublished or sensitive material.",
77
+ where_varies: "Where your data goes: it depends on the model. Each model runs at another company, in the United States, China or elsewhere. For material you would not send outside the EU, choose Mistral.",
78
+ setupTitle: "Before you start",
79
+ stepZotero: "Zotero is open and lets Sub-Sub in. If not: start Zotero, then in Settings > Advanced turn on \"Allow other applications on this computer to communicate with Zotero\".",
80
+ stepModel: "A model is connected. If not: select Connect and paste an API key. To try Sub-Sub for free, choose Google Gemini and get a free key there.",
81
+ stepServers: "The Zotero servers are running. If not ({x}): close Sub-Sub, open a terminal and type subsub doctor. Each line marked FIX says what to do.",
82
+ stepDone: "done",
83
+ stepTodo: "to do",
84
+ undoTitle: "Undo this change. Sub-Sub shows what it would restore before it changes anything.",
71
85
  oauthNote: "To sign in through a provider's website (for example with a subscription), open a terminal, type subsub, then /login.",
72
86
  saving: "Saving",
73
87
  saved: "Saved.",
@@ -164,6 +178,7 @@ export const STRINGS = {
164
178
  rec_tested: "testado, por omissão",
165
179
  rec_cheaper: "testado, mais barato",
166
180
  rec_free: "testado, gratuito",
181
+ rec_eu: "testado, UE",
167
182
  needsKey: "precisa de uma chave de {x}",
168
183
  noModels: "Ainda não há modelos. Ligue um fornecedor abaixo.",
169
184
  connectProvider: "Ligar um fornecedor",
@@ -178,7 +193,20 @@ export const STRINGS = {
178
193
  trouble_busy: "O fornecedor do modelo está sobrecarregado neste momento. Tente outra vez dentro de alguns minutos ou escolha outro modelo no botão do modelo.",
179
194
  trouble_blocked: "O fornecedor do modelo interrompeu a resposta (por exemplo, por repetir uma fonte palavra por palavra). Peça outra vez por outras palavras ou escolha outro modelo.",
180
195
  trouble_key: "O fornecedor do modelo não aceitou a chave da API. Volte a introduzi-la no botão do modelo.",
196
+ trouble_region: "O fornecedor recusa este modelo com a definição de região da sua conta. No OpenCode, permita as regiões globais (Global) nas definições de privacidade do workspace, ou escolha no botão do modelo um modelo que a sua região permita.",
181
197
  details: "Detalhes",
198
+ undo: "Anular",
199
+ where_eu: "Para onde vão os seus dados: servidores na União Europeia. Abrangidos pelo RGPD; a escolha para material que não enviaria para fora da UE.",
200
+ where_us: "Para onde vão os seus dados: servidores fora da UE, sobretudo nos Estados Unidos, com os termos do fornecedor para a UE. Leia-os antes de enviar material inédito ou sensível.",
201
+ where_cn: "Para onde vão os seus dados: servidores na China. Não envie material inédito ou sensível.",
202
+ where_varies: "Para onde vão os seus dados: depende do modelo. Cada modelo funciona noutra empresa, nos Estados Unidos, na China ou noutro país. Para material que não enviaria para fora da UE, escolha a Mistral.",
203
+ setupTitle: "Antes de começar",
204
+ stepZotero: "O Zotero está aberto e deixa o Sub-Sub entrar. Se não: abra o Zotero e, em Definições > Avançado, ative \"Allow other applications on this computer to communicate with Zotero\".",
205
+ stepModel: "Há um modelo ligado. Se não: selecione Ligar e cole uma chave de API. Para experimentar sem pagar, escolha Google Gemini e obtenha lá uma chave gratuita.",
206
+ stepServers: "Os servidores do Zotero estão a funcionar. Se não ({x}): feche o Sub-Sub, abra um terminal e escreva subsub doctor. Cada linha marcada FIX diz o que fazer.",
207
+ stepDone: "feito",
208
+ stepTodo: "por fazer",
209
+ undoTitle: "Anular esta alteração. O Sub-Sub mostra o que repõe antes de alterar alguma coisa.",
182
210
  oauthNote: "Para entrar através do site de um fornecedor (por exemplo, com uma subscrição), abra um terminal, escreva subsub e depois /login.",
183
211
  saving: "A guardar",
184
212
  saved: "Guardado.",
@@ -272,6 +300,10 @@ export const SUGGESTIONS = {
272
300
  export const TOOLS = {
273
301
  en: {
274
302
  find_items: "search the library",
303
+ standalone_items: "list files without a parent item",
304
+ set_parent_items: "give files a parent item",
305
+ find_reference: "look up a reference",
306
+ web_search: "search the web",
275
307
  get_item: "read an item",
276
308
  get_fulltext: "read the full text",
277
309
  library_overview: "library overview",
@@ -328,6 +360,10 @@ export const TOOLS = {
328
360
  },
329
361
  pt: {
330
362
  find_items: "pesquisar na biblioteca",
363
+ standalone_items: "listar ficheiros sem item principal",
364
+ set_parent_items: "dar um item principal aos ficheiros",
365
+ find_reference: "procurar uma referência",
366
+ web_search: "pesquisar na web",
331
367
  get_item: "ler um item",
332
368
  get_fulltext: "ler o texto completo",
333
369
  library_overview: "resumo da biblioteca",
package/web/index.html CHANGED
@@ -122,6 +122,7 @@
122
122
  <input type="password" id="login-key" autocomplete="off" spellcheck="false" required>
123
123
  </label>
124
124
  <p class="small"><a id="login-link" href="#" target="_blank" rel="noopener noreferrer" data-i18n="getKey">Get a key</a></p>
125
+ <p class="small muted" id="login-where"></p>
125
126
  <p class="small free-note" id="login-free" hidden></p>
126
127
  <p class="small muted" data-i18n="keyNote"></p>
127
128
  <div class="modal-buttons"><button type="submit" class="primary" id="login-save" data-i18n="save">Save</button></div>