@tiagojct/subsub 0.22.0 → 0.23.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 CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  Sub-Sub follows the version numbers of npm: a change in the second number (0.13) adds features or changes how you use it; a change in the third (0.13.1) only fixes.
4
4
 
5
+ ## 0.23.0 (2026-10-24)
6
+
7
+ - The browser view is set like the website. Replies and notes have the website's headings (a fine rule above each section), its numbered steps and a slightly larger text. A note opens on the lighter field of a page, and the list of notes is grouped by folder under small-capital heads, with the date on the right. The empty conversation has the website's running head and a larger heading. Notes and the model in the top bar are plain text buttons, as in the website's menu. A command you send (`/read ...`) is shown in the mono. The working line is drawn dotted, dashed and solid, as in the terminal, instead of a moving gradient. Scrollbars are thin. On a phone, Notes is no longer cut to "No..." (the theme button is in the side panel there).
8
+ - In the browser view, the sections of the side panel (Library, Actions, Conversations) and the two groups of actions (Research, Library) fold at their heading. With the others folded, the list of conversations takes the whole panel. The browser remembers which ones you folded.
9
+
5
10
  ## 0.22.0 (2026-10-24)
6
11
 
7
12
  - `subsub tui`: Sub-Sub on the whole terminal. It is a full-screen client of pi's RPC mode, like the browser view, with the look of `subsub`: a conversation that scrolls (mouse wheel, PgUp and PgDn), a side panel from 110 columns (mode, library, keys, earlier conversations), an editor that completes commands, and the footer. Alt+N, Alt+L, Alt+A and Alt+M open the notes, the library search and the actions, and switch the mode, as in `subsub`. `/sessions` and `/new` change the conversation, `/model` chooses the model, `/restart` starts pi again in the same conversation, `/quit` leaves. Ctrl+O shows the output of tool rows, Esc stops a running answer, and Ctrl+C twice quits. `--light` or `--dark` set the colours when the terminal does not say (the default is dark), and `--librarian` starts in the Librarian.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiagojct/subsub",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Sub-Sub: a research assistant that works from your own Zotero library, in the browser or the terminal, built on pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/web/app.css CHANGED
@@ -115,9 +115,13 @@ body {
115
115
  }
116
116
 
117
117
  button, input, select, textarea { font: inherit; color: inherit; }
118
- a { color: var(--accent); text-underline-offset: 0.18em; text-decoration-thickness: 1px; }
118
+ a { color: var(--accent); text-underline-offset: 0.2em; text-decoration-thickness: 1px; }
119
119
  a:hover { text-decoration-thickness: 2px; }
120
120
  svg { flex: none; }
121
+ /* Scrollbars as fine rules, not as grey bars. */
122
+ * { scrollbar-width: thin; }
123
+ /* The name breaks nowhere, least of all at its hyphen. */
124
+ .nobr { white-space: nowrap; }
121
125
 
122
126
  .visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
123
127
  [hidden] { display: none !important; }
@@ -164,20 +168,20 @@ button:disabled { opacity: 0.45; cursor: default; }
164
168
  .icon-btn:hover { color: var(--text); background: var(--surface); }
165
169
  .icon-btn[aria-pressed="true"] { color: var(--accent); background: var(--accent-soft); }
166
170
 
167
- .model-btn.needs { color: var(--warn); border-color: var(--warn); font-family: var(--sans); }
171
+ /* Top bar text buttons, as the website's navigation: muted, inked and underlined on hover or when open. */
168
172
  .chip-btn {
169
- max-width: 16rem;
170
- overflow: hidden;
171
- text-overflow: ellipsis;
172
- white-space: nowrap;
173
- font-family: var(--mono);
174
- font-size: 0.78rem;
173
+ flex: none;
174
+ padding: 0.35rem 0.5rem;
175
+ font-size: 0.9rem;
175
176
  color: var(--muted);
176
- border-color: var(--rule);
177
- background: var(--raised);
178
- padding: 0.35rem 0.65rem;
177
+ white-space: nowrap;
178
+ text-underline-offset: 0.35em;
179
+ text-decoration-thickness: 1px;
179
180
  }
180
- .chip-btn:hover { color: var(--text); border-color: var(--rule-strong); }
181
+ .chip-btn:hover { color: var(--text); text-decoration-line: underline; }
182
+ .chip-btn.model-btn { flex: 0 1 auto; max-width: 16rem; overflow: hidden; text-overflow: ellipsis; font-family: var(--mono); font-size: 0.8rem; }
183
+ /* No model yet: pending, so gamboge and dashed. */
184
+ .model-btn.needs { color: var(--warn); border-color: var(--warn); border-style: dashed; font-family: var(--sans); }
181
185
 
182
186
  select {
183
187
  appearance: none;
@@ -319,7 +323,7 @@ select:hover { border-color: var(--rule-strong); }
319
323
  .new-btn:hover { border-color: var(--text); }
320
324
  .new-btn svg { width: 16px; height: 16px; color: var(--accent); }
321
325
 
322
- .actions { display: flex; flex-direction: column; }
326
+ .actions, .actions > div { display: flex; flex-direction: column; }
323
327
  .actions button {
324
328
  text-align: left;
325
329
  padding: 0.32rem 0.5rem;
@@ -329,9 +333,35 @@ select:hover { border-color: var(--rule-strong); }
329
333
  }
330
334
  .actions button:hover { background: var(--bg); }
331
335
  .actions .group { margin: 0.6rem 0 0.15rem; padding: 0 0.5rem; font-family: var(--serif); font-style: italic; font-size: 0.86rem; color: var(--muted); }
332
- .actions .group:first-child { margin-top: 0; }
336
+ .actions > :first-child .group { margin-top: 0; }
337
+
338
+ /* Sections and action groups fold at their heading; a fine chevron points down when open, right when folded. */
339
+ .side .fold {
340
+ display: inline-flex;
341
+ align-items: center;
342
+ gap: 0.55em;
343
+ padding: 0;
344
+ border: 0;
345
+ border-radius: 0;
346
+ font-size: inherit;
347
+ letter-spacing: inherit;
348
+ color: inherit;
349
+ text-align: left;
350
+ }
351
+ .side .fold:hover { color: var(--text); background: none; }
352
+ .side .fold::after {
353
+ content: "";
354
+ width: 0.34em;
355
+ height: 0.34em;
356
+ border-right: 1px solid currentColor;
357
+ border-bottom: 1px solid currentColor;
358
+ transform: translateY(-0.15em) rotate(45deg);
359
+ }
360
+ .side .fold[aria-expanded="false"]::after { transform: rotate(-45deg); }
361
+ [data-fold].folded > :not(h2, .side-subhead, .group) { display: none; }
333
362
 
334
363
  .side .grow { flex: 1; display: flex; flex-direction: column; min-height: 8rem; }
364
+ .side .grow.folded { flex: none; min-height: 0; }
335
365
  .side-subhead { display: flex; justify-content: space-between; align-items: baseline; padding-right: 0.3rem; }
336
366
 
337
367
  .search {
@@ -423,7 +453,6 @@ body.layout-full { --measure: 72rem; }
423
453
  padding: 0.6rem 0.6rem 0.6rem 0.9rem;
424
454
  border: 1px solid var(--rule);
425
455
  border-top: 2px solid var(--warn);
426
- border-radius: var(--radius);
427
456
  background: var(--raised);
428
457
  display: flex;
429
458
  gap: 0.75rem;
@@ -454,14 +483,16 @@ body.layout-full { --measure: 72rem; }
454
483
  overflow-wrap: anywhere;
455
484
  font-size: 0.95rem;
456
485
  }
486
+ /* A command is code: it is set in the mono, as the website's plate draws it. */
487
+ .msg.user .bubble.cmd { font-family: var(--mono); font-size: 0.86rem; }
457
488
  .msg.user details.bubble summary { cursor: pointer; color: var(--muted); }
458
489
  .msg.user details.bubble[open] summary { margin-bottom: 0.4rem; }
459
490
 
460
491
  /* Replies read as text, in the serif. */
461
492
  .md {
462
493
  font-family: var(--serif);
463
- font-size: 1.02rem;
464
- line-height: 1.68;
494
+ font-size: 1.0625rem;
495
+ line-height: 1.65;
465
496
  font-variant-numeric: oldstyle-nums proportional-nums;
466
497
  overflow-wrap: anywhere;
467
498
  }
@@ -470,16 +501,20 @@ body.layout-full { --measure: 72rem; }
470
501
  .md p, .md ul, .md ol { margin: 0.7rem 0; }
471
502
  .md li { margin: 0.2rem 0; }
472
503
  .md li::marker { color: var(--faint); }
504
+ /* Numbered steps as on the website: the number in the accent, lining figures. */
505
+ .md ol > li::marker { color: var(--accent); font-weight: 600; font-variant-numeric: lining-nums tabular-nums; }
506
+ /* Headings as on the website: roman, weight 500 for the large ones, a fine rule above a section. */
473
507
  .md h1, .md h2, .md h3, .md h4 {
474
508
  font-family: var(--serif);
475
- line-height: 1.3;
476
- margin: 1.6rem 0 0.5rem;
509
+ line-height: 1.25;
510
+ margin: 1.8rem 0 0.6rem;
477
511
  font-weight: 600;
512
+ text-wrap: balance;
478
513
  }
479
- .md h1 { font-size: 1.3rem; }
480
- .md h2 { font-size: 1.12rem; }
481
- .md h3 { font-size: 1rem; }
482
- .md h4 { font-size: 0.92rem; color: var(--muted); }
514
+ .md h1 { font-size: 1.5rem; font-weight: 500; line-height: 1.15; letter-spacing: -0.012em; }
515
+ .md h2 { font-size: 1.25rem; font-weight: 500; letter-spacing: -0.008em; padding-top: 0.5rem; border-top: 1px solid var(--rule-strong); }
516
+ .md h3 { font-size: 1.04rem; }
517
+ .md h4 { font-size: 0.95rem; color: var(--muted); }
483
518
  .md strong { font-weight: 600; }
484
519
  .md hr { border: 0; border-top: 1px solid var(--rule); margin: 1.5rem 0; }
485
520
  .msg.assistant + .msg.assistant { margin-top: 0.9rem; }
@@ -568,7 +603,7 @@ body.layout-full { --measure: 72rem; }
568
603
 
569
604
  .msg-actions { display: flex; gap: 0.25rem; margin-top: 0.4rem; opacity: 0; transition: opacity 0.12s ease; }
570
605
  .msg.assistant:hover .msg-actions, .msg-actions:focus-within { opacity: 1; }
571
- .msg-action-btn { font-size: 0.75rem; padding: 0.15rem 0.45rem; color: var(--faint); }
606
+ .msg-action-btn { font-family: var(--sans); font-size: 0.75rem; padding: 0.15rem 0.45rem; color: var(--faint); }
572
607
  .msg-action-btn:hover { color: var(--text); background: var(--surface); }
573
608
  .msg-action-btn.copied { color: var(--add); }
574
609
  .table-actions { display: flex; justify-content: flex-end; margin: -0.35rem 0 0.9rem; }
@@ -633,7 +668,6 @@ body.layout-full { --measure: 72rem; }
633
668
  .note {
634
669
  margin-top: 1rem;
635
670
  padding: 0.6rem 0.9rem;
636
- border-radius: var(--radius);
637
671
  background: var(--surface);
638
672
  border: 1px solid var(--rule);
639
673
  font-size: 0.88rem;
@@ -659,44 +693,48 @@ body.layout-full { --measure: 72rem; }
659
693
  gap: 0.55rem;
660
694
  align-items: center;
661
695
  }
696
+ /* Working is drawn by line, as in the terminal: dotted, dashed, solid. Without motion it stays dashed: pending. */
662
697
  .working::before {
663
698
  content: "";
664
699
  width: 28px;
665
- height: 3px;
666
- border-radius: 2px;
667
- background: linear-gradient(90deg, var(--accent) 0 40%, var(--rule) 40% 100%);
668
- background-size: 250% 100%;
669
- animation: sweep 1.3s ease-in-out infinite alternate;
700
+ border-top: 2px dashed var(--accent);
701
+ animation: lines 1.5s steps(1, end) infinite;
670
702
  }
671
- @keyframes sweep { from { background-position: 100% 0; } to { background-position: 0 0; } }
703
+ @keyframes lines { 0% { border-top-style: dotted; } 33% { border-top-style: dashed; } 67% { border-top-style: solid; } }
672
704
  @keyframes pulse { 50% { opacity: 0.3; } }
673
705
 
674
706
  /* Empty conversation. */
675
707
  .empty { padding-top: 11vh; }
676
- .empty .kicker {
708
+ /* The running head of the website's plates: the name, an italic middle, the mode in its colour. */
709
+ .empty .runhead {
677
710
  display: flex;
678
711
  justify-content: space-between;
679
- margin: 0 0 1.5rem;
680
- padding-bottom: 0.3rem;
712
+ align-items: baseline;
713
+ gap: 0.5rem 1.5rem;
714
+ margin: 0 0 1.75rem;
715
+ padding-bottom: 0.35rem;
681
716
  border-bottom: 1px solid var(--text);
682
717
  font-family: var(--serif);
683
- font-size: 1rem;
718
+ font-size: 1.02rem;
719
+ line-height: 1.3;
684
720
  font-variant-caps: all-small-caps;
685
721
  letter-spacing: 0.08em;
686
- color: var(--accent);
722
+ color: var(--muted);
687
723
  }
688
- .empty .kicker::before { content: "Sub-Sub"; color: var(--muted); order: -1; }
724
+ .empty .runhead .mid { font-style: italic; font-variant-caps: normal; letter-spacing: 0; text-align: center; }
725
+ .empty .runhead .mode { color: var(--accent); }
689
726
  .empty h1 {
690
727
  font-family: var(--serif);
691
728
  font-weight: 500;
692
- font-size: clamp(1.6rem, 3.2vw, 2.15rem);
693
- line-height: 1.2;
694
- letter-spacing: -0.015em;
695
- margin: 0 0 0.6rem;
729
+ font-size: clamp(2rem, 3.6vw, 2.75rem);
730
+ line-height: 1.06;
731
+ letter-spacing: -0.018em;
732
+ margin: 0 0 0.9rem;
733
+ text-wrap: balance;
696
734
  }
697
- .empty .lede { color: var(--muted); max-width: 34rem; margin: 0 0 1.75rem; }
735
+ .empty .lede { font-family: var(--serif); font-size: 1.08rem; line-height: 1.55; color: var(--muted); max-width: 36rem; margin: 0 0 2rem; text-wrap: pretty; }
698
736
  /* Suggestions: a short list of lines to start from, not a row of cards. */
699
- .chips { display: grid; max-width: 34rem; border-bottom: 1px solid var(--rule-strong); }
737
+ .chips { display: grid; max-width: 36rem; border-bottom: 1px solid var(--rule-strong); }
700
738
  .chips button {
701
739
  text-align: left;
702
740
  border: 0;
@@ -792,7 +830,7 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
792
830
  }
793
831
  .modal-kicker::after { content: "Sub-Sub"; color: var(--muted); }
794
832
  .modal-kicker:empty { display: none; }
795
- .modal h2 { font-family: var(--serif); font-size: 1.25rem; font-weight: 600; line-height: 1.3; margin: 0 0 0.3rem; overflow-wrap: anywhere; }
833
+ .modal h2 { font-family: var(--serif); font-size: 1.4rem; font-weight: 500; line-height: 1.2; letter-spacing: -0.01em; margin: 0 0 0.35rem; overflow-wrap: anywhere; }
796
834
  .modal h3 { font-family: var(--serif); font-size: 0.98rem; font-weight: 400; font-variant-caps: all-small-caps; letter-spacing: 0.08em; color: var(--muted); margin: 1.4rem 0 0.5rem; }
797
835
  .modal .hint { color: var(--muted); margin: 0 0 1rem; font-size: 0.88rem; }
798
836
 
@@ -888,8 +926,7 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
888
926
 
889
927
  /* ---------------------------------------------------------------- library search and notes */
890
928
 
891
- #notes-btn { font-family: var(--sans); }
892
- #notes-btn[aria-expanded="true"] { color: var(--accent); border-color: var(--accent); }
929
+ #notes-btn[aria-expanded="true"] { color: var(--text); text-decoration-line: underline; }
893
930
 
894
931
  .items { list-style: none; margin: 0; padding: 0; max-height: 18rem; overflow-y: auto; }
895
932
  .items:empty { display: none; }
@@ -910,11 +947,12 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
910
947
  .session-tools button { width: auto; padding: 0.1rem 0.35rem; font-size: 0.7rem; color: var(--muted); }
911
948
  .session-tools button:hover { color: var(--text); background: var(--bg); }
912
949
 
950
+ /* A note is a page: the lighter plate field, as on the website. */
913
951
  .note-pane {
914
952
  grid-row: 2;
915
953
  grid-column: 3;
916
- border-left: 1px solid var(--rule);
917
- background: var(--surface);
954
+ border-left: 1px solid var(--rule-strong);
955
+ background: var(--raised);
918
956
  display: flex;
919
957
  flex-direction: column;
920
958
  min-height: 0;
@@ -923,7 +961,7 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
923
961
  .note-pane[hidden] { display: none; }
924
962
  .note-head { display: flex; align-items: center; gap: 0.6rem; padding: 0.6rem 0.9rem; border-bottom: 1px solid var(--rule); }
925
963
  .note-name { flex: 1; min-width: 0; font-family: var(--mono); font-size: 0.8rem; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
926
- .note-pane > .search { margin: 0.6rem 0.9rem 0; width: auto; }
964
+ .note-pane > .search { margin: 0.6rem 0.9rem 0; width: auto; background: var(--bg); }
927
965
  .note-body { flex: 1; overflow-y: auto; padding: 0.9rem 1.1rem 1.5rem; }
928
966
  .note-body .front { font-size: 0.74rem; color: var(--muted); margin: 0 0 1rem; }
929
967
  /* What a note was written from, drawn by line: solid for the full text, dashed for the abstract,
@@ -940,11 +978,25 @@ body > p.empty { text-align: center; color: var(--muted); padding: 30vh 1rem 0;
940
978
  #note-read[aria-pressed="true"] { color: var(--accent); text-decoration: underline; }
941
979
  .note-body .front summary { cursor: pointer; }
942
980
  .note-body .front pre { background: var(--bg); border: 1px solid var(--rule); border-radius: var(--radius); padding: 0.5rem 0.7rem; margin: 0.4rem 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }
943
- .note-list { list-style: none; margin: 0; padding: 0; }
944
- .note-list button { width: 100%; text-align: left; padding: 0.4rem 0.5rem; border-radius: 0; display: grid; gap: 0.1rem; }
981
+ /* The notes, one folder per section under a small-capitals head, rows ruled as in a table. */
982
+ .note-list section + section { margin-top: 1.4rem; }
983
+ .note-list h3 {
984
+ margin: 0;
985
+ padding: 0 0.5rem 0.25rem;
986
+ border-bottom: 1px solid var(--text);
987
+ font-family: var(--serif);
988
+ font-size: 1rem;
989
+ font-weight: 400;
990
+ font-variant-caps: all-small-caps;
991
+ letter-spacing: 0.08em;
992
+ color: var(--muted);
993
+ }
994
+ .note-list ul { list-style: none; margin: 0; padding: 0; }
995
+ .note-list li + li { border-top: 1px solid var(--rule); }
996
+ .note-list button { width: 100%; text-align: left; padding: 0.5rem; border-radius: 0; display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; }
945
997
  .note-list button:hover { background: var(--bg); }
946
- .note-list .rel { font-size: 0.84rem; overflow-wrap: anywhere; }
947
- .note-list .when { font-size: 0.72rem; color: var(--faint); }
998
+ .note-list .rel { min-width: 0; font-size: 0.9rem; overflow-wrap: anywhere; }
999
+ .note-list .when { flex: none; font-size: 0.74rem; color: var(--faint); font-variant-numeric: tabular-nums; }
948
1000
  .note-body img.note-img { display: block; max-width: 100%; height: auto; margin: 0.8rem 0; border: 1px solid var(--rule); border-radius: var(--radius); background: #fff; }
949
1001
  a[data-note] { color: var(--accent); text-decoration: underline; text-decoration-color: var(--accent-soft); text-underline-offset: 0.18em; cursor: pointer; }
950
1002
 
@@ -980,6 +1032,7 @@ a[data-note] { color: var(--accent); text-decoration: underline; text-decoration
980
1032
  @media (max-width: 30rem) {
981
1033
  .brand > span { display: none; }
982
1034
  .modes button { padding: 0.28rem 0.6rem; }
1035
+ .empty .runhead .mid { display: none; }
983
1036
  }
984
1037
 
985
1038
  @media (prefers-reduced-motion: reduce) {
package/web/app.js CHANGED
@@ -296,6 +296,16 @@ function hasCommand(name) {
296
296
  return commands.length === 0 || commands.some((c) => c.name === base);
297
297
  }
298
298
 
299
+ // Side panel sections and action groups fold at their heading; the folded ones are remembered on this computer.
300
+ const folded = new Set((localStorage.getItem("subsub-folded") || "").split(" ").filter(Boolean));
301
+ function setFold(box, on) {
302
+ box.classList.toggle("folded", on);
303
+ box.querySelector(".fold").setAttribute("aria-expanded", String(!on));
304
+ if (on) folded.add(box.dataset.fold);
305
+ else folded.delete(box.dataset.fold);
306
+ localStorage.setItem("subsub-folded", [...folded].join(" "));
307
+ }
308
+
299
309
  function renderActions() {
300
310
  const box = $("actions");
301
311
  box.replaceChildren();
@@ -305,8 +315,9 @@ function renderActions() {
305
315
  for (const [group, label] of groups) {
306
316
  const list = ACTIONS[group].filter(shown);
307
317
  if (!list.length) continue;
308
- box.append(el("p", { class: "group", text: t(label) }));
309
- for (const a of list) box.append(actionButton(a));
318
+ const g = el("div", { "data-fold": `actions-${group}` }, el("p", { class: "group" }, el("button", { type: "button", class: "fold", text: t(label) })), list.map(actionButton));
319
+ setFold(g, folded.has(g.dataset.fold));
320
+ box.append(g);
310
321
  }
311
322
  }
312
323
 
@@ -315,7 +326,10 @@ function actionButton(a) {
315
326
  type: "button",
316
327
  text: a[lang] ?? a.en,
317
328
  onclick: () => {
318
- if (a.item) return $("library-search").focus();
329
+ if (a.item) {
330
+ setFold($("library-search").closest("[data-fold]"), false);
331
+ return $("library-search").focus();
332
+ }
319
333
  closeSide();
320
334
  if (a.send) sendMessage(`/${a.cmd}`);
321
335
  else {
@@ -407,16 +421,34 @@ function renderNoteList() {
407
421
  const q = $("note-filter").value.trim().toLowerCase();
408
422
  const list = notesList.filter((n) => !q || n.rel.toLowerCase().includes(q));
409
423
  const fmt = new Intl.DateTimeFormat(lang === "pt" ? "pt-PT" : "en-GB", { day: "numeric", month: "short", year: "numeric" });
424
+ // One group per folder (Inbox, Literature, ...), newest first inside each, as the server sorts them.
425
+ const groups = new Map();
426
+ for (const n of list) {
427
+ const [folder, ...rest] = n.rel.split(/[\\/]/);
428
+ if (!groups.has(folder)) groups.set(folder, []);
429
+ groups.get(folder).push({ ...n, name: rest.join("/").replace(/\.md$/i, "") || folder });
430
+ }
410
431
  body.replaceChildren(
411
432
  list.length
412
433
  ? el(
413
- "ul",
434
+ "div",
414
435
  { class: "note-list" },
415
- list.map((n) =>
436
+ [...groups].map(([folder, notes]) =>
416
437
  el(
417
- "li",
438
+ "section",
418
439
  {},
419
- el("button", { type: "button", onclick: () => openNote(n.path) }, el("span", { class: "rel", text: n.rel }), el("span", { class: "when", text: fmt.format(new Date(n.modified)) })),
440
+ el("h3", { text: folder }),
441
+ el(
442
+ "ul",
443
+ {},
444
+ notes.map((n) =>
445
+ el(
446
+ "li",
447
+ {},
448
+ el("button", { type: "button", title: n.rel, onclick: () => openNote(n.path) }, el("span", { class: "rel", text: n.name }), el("span", { class: "when", text: fmt.format(new Date(n.modified)) })),
449
+ ),
450
+ ),
451
+ ),
420
452
  ),
421
453
  ),
422
454
  )
@@ -692,7 +724,7 @@ function renderEmpty() {
692
724
  el(
693
725
  "div",
694
726
  { class: "empty" },
695
- el("p", { class: "kicker", text: t(mode()) }),
727
+ el("p", { class: "runhead" }, el("span", { class: "nobr", text: "Sub-Sub" }), el("span", { class: "mid", text: t("tagline") }), el("span", { class: "mode", text: t(mode()) })),
696
728
  el("h1", { text: t("emptyTitle") }),
697
729
  el("p", { class: "lede", text: t(mode() === "librarian" ? "emptyLibrarian" : "emptyResearcher") }),
698
730
  setupList(),
@@ -732,7 +764,7 @@ function userMessage(text) {
732
764
  if (text.length > 700) {
733
765
  const first = text.split("\n").find((l) => l.trim()) ?? "";
734
766
  bubble = el("details", { class: "bubble" }, el("summary", { text: first.length > 120 ? `${first.slice(0, 117)}...` : first }), el("div", { text }));
735
- } else bubble = el("div", { class: "bubble", text });
767
+ } else bubble = el("div", { class: /^\/\S/.test(text) ? "bubble cmd" : "bubble", text });
736
768
  log.append(el("div", { class: "msg user" }, bubble));
737
769
  scrollDown(true);
738
770
  }
@@ -1517,6 +1549,11 @@ $("theme-btn-side")?.addEventListener("click", () => {
1517
1549
  closeSide();
1518
1550
  toggleTheme();
1519
1551
  });
1552
+ $("side").addEventListener("click", (e) => {
1553
+ const box = e.target.closest(".fold")?.closest("[data-fold]");
1554
+ if (box) setFold(box, !box.classList.contains("folded"));
1555
+ });
1556
+ for (const box of document.querySelectorAll(".side section[data-fold]")) setFold(box, folded.has(box.dataset.fold));
1520
1557
  $("session-search")?.addEventListener("input", (e) => renderSessionsList(e.target.value));
1521
1558
  $("library-search").addEventListener("input", searchLibrary);
1522
1559
  $("notes-btn").addEventListener("click", () => ($("note-pane").hidden ? showNoteList() : showPane(false)));
package/web/i18n.js CHANGED
@@ -84,6 +84,7 @@ export const STRINGS = {
84
84
  saved: "Saved.",
85
85
  busy: "Wait until Sub-Sub finishes.",
86
86
  emptyTitle: "What would you like to do?",
87
+ tagline: "A research assistant for Zotero",
87
88
  quit: "Close Sub-Sub",
88
89
  quitDone: "Sub-Sub is closed. You can close this tab.",
89
90
  earlier: "Earlier messages were summarised.",
@@ -226,6 +227,7 @@ export const STRINGS = {
226
227
  saved: "Guardado.",
227
228
  busy: "Espere que o Sub-Sub termine.",
228
229
  emptyTitle: "O que quer fazer?",
230
+ tagline: "Um assistente de investigação para o Zotero",
229
231
  quit: "Fechar o Sub-Sub",
230
232
  quitDone: "O Sub-Sub está fechado. Pode fechar este separador.",
231
233
  earlier: "As mensagens anteriores foram resumidas.",
package/web/index.html CHANGED
@@ -24,7 +24,7 @@
24
24
  <button type="button" class="chip-btn" id="notes-btn" aria-controls="note-pane" aria-expanded="false" data-i18n="notes" data-i18n-title="notesHint">Notes</button>
25
25
  <button type="button" class="chip-btn model-btn wide-only" id="model-btn"></button>
26
26
  <button type="button" class="icon-btn wide-only" id="width-btn" aria-pressed="false"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12h16M4 12l3.5-3.5M4 12l3.5 3.5M20 12l-3.5-3.5M20 12l-3.5 3.5"/></svg><span class="visually-hidden" id="width-btn-text">Wide</span></button>
27
- <button type="button" class="icon-btn" id="theme-btn"></button>
27
+ <button type="button" class="icon-btn wide-only" id="theme-btn"></button>
28
28
  </div>
29
29
  </header>
30
30
 
@@ -38,18 +38,18 @@
38
38
  <button type="button" class="secondary" id="theme-btn-side"></button>
39
39
  </label>
40
40
  </section>
41
- <section>
42
- <h2 data-i18n="groupLibrary">Library</h2>
41
+ <section data-fold="library">
42
+ <h2><button type="button" class="fold" aria-expanded="true" data-i18n="groupLibrary">Library</button></h2>
43
43
  <input type="search" id="library-search" class="search" data-i18n-placeholder="librarySearch" placeholder="Search your library" autocomplete="off">
44
44
  <ul class="items" id="items" aria-live="polite"></ul>
45
45
  </section>
46
- <section>
47
- <h2 data-i18n="actions">Actions</h2>
46
+ <section data-fold="actions">
47
+ <h2><button type="button" class="fold" aria-expanded="true" data-i18n="actions">Actions</button></h2>
48
48
  <div class="actions" id="actions"></div>
49
49
  </section>
50
- <section class="grow">
50
+ <section class="grow" data-fold="conversations">
51
51
  <div class="side-subhead">
52
- <h2 data-i18n="conversations">Conversations</h2>
52
+ <h2><button type="button" class="fold" aria-expanded="true" data-i18n="conversations">Conversations</button></h2>
53
53
  <button type="button" class="link-btn" id="export-btn" data-i18n="export" data-i18n-title="exportTitle">Export</button>
54
54
  </div>
55
55
  <input type="search" id="session-search" class="search" data-i18n-placeholder="searchConversations" placeholder="Search conversations" autocomplete="off">