sideby 0.8.0 → 0.9.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/dist/build.json CHANGED
@@ -1 +1 @@
1
- {"id":"20261014T102906-2ab079"}
1
+ {"id":"20261015T091733-83e791"}
@@ -6,12 +6,14 @@ import { editConfigAccounts } from './config.js';
6
6
  import { UserError } from './errors.js';
7
7
  import { lineDiff } from './handoff-status.js';
8
8
  function pick(key, sideby, host, last) {
9
+ // What the Account really ran last beats the Host's default: subscription Accounts often share one settings.json,
10
+ // so its value would show the same for all of them.
9
11
  if (sideby[key])
10
12
  return { value: sideby[key], from: 'sideby' };
11
- if (host[key])
12
- return { value: host[key], from: 'host' };
13
13
  if (last[key])
14
14
  return { value: last[key], from: 'last' };
15
+ if (host[key])
16
+ return { value: host[key], from: 'host' };
15
17
  return undefined;
16
18
  }
17
19
  /** `last` is the model and effort of the newest session, from the usage reader when the caller already has it. */
@@ -120,6 +120,8 @@ declare const en: {
120
120
  'filter.clear': string;
121
121
  'group.expand': string;
122
122
  'group.collapse': string;
123
+ 'row.collapse': string;
124
+ 'row.expand': string;
123
125
  'group.toggle': string;
124
126
  'group.attention.one': string;
125
127
  'group.attention.other': string;
@@ -254,6 +256,7 @@ declare const en: {
254
256
  'md.from.sideby': string;
255
257
  'md.from.host': string;
256
258
  'md.from.last': string;
259
+ 'md.newDefault': string;
257
260
  'md.none': string;
258
261
  'md.click': string;
259
262
  'md.envNote': string;
@@ -122,6 +122,8 @@ const en = {
122
122
  'filter.clear': 'Clear filters',
123
123
  'group.expand': 'Show accounts',
124
124
  'group.collapse': 'Hide accounts',
125
+ 'row.collapse': 'Collapse {name}',
126
+ 'row.expand': 'Expand {name}',
125
127
  'group.toggle': '{title} ({count}): {action}',
126
128
  'group.attention.one': '{n} needs attention',
127
129
  'group.attention.other': '{n} need attention',
@@ -256,6 +258,7 @@ const en = {
256
258
  'md.from.sideby': 'set by sideby for this account',
257
259
  'md.from.host': 'from {host}’s settings',
258
260
  'md.from.last': 'used in the last session',
261
+ 'md.newDefault': 'new sessions start with {value} ({from})',
259
262
  'md.none': 'not known yet',
260
263
  'md.click': 'Click to change.',
261
264
  'md.envNote': 'ANTHROPIC_MODEL or CLAUDE_CODE_EFFORT_LEVEL in your shell, and settings from your organization, override this; sideby cannot see them.',
@@ -582,6 +585,8 @@ const zh = {
582
585
  'filter.clear': '清除筛选',
583
586
  'group.expand': '展开账号',
584
587
  'group.collapse': '收起账号',
588
+ 'row.collapse': '收起 {name}',
589
+ 'row.expand': '展开 {name}',
585
590
  'group.toggle': '{title}({count}):{action}',
586
591
  'group.attention.one': '{n} 个需要处理',
587
592
  'group.attention.other': '{n} 个需要处理',
@@ -716,6 +721,7 @@ const zh = {
716
721
  'md.from.sideby': 'sideby 为这个账号设置',
717
722
  'md.from.host': '来自 {host} 的设置',
718
723
  'md.from.last': '最近一次会话用的',
724
+ 'md.newDefault': '新会话默认是 {value}({from})',
719
725
  'md.none': '暂时不知道',
720
726
  'md.click': '点击修改。',
721
727
  'md.envNote': 'shell 里的 ANTHROPIC_MODEL、CLAUDE_CODE_EFFORT_LEVEL 和组织下发的设置会覆盖这里,sideby 看不到它们。',
@@ -65,13 +65,15 @@ const MAIN = String.raw `
65
65
  function loadPrefs() {
66
66
  let raw = {}
67
67
  try { raw = JSON.parse(localStorage.getItem(PREFS_KEY) || '{}') || {} } catch {}
68
- const p = { view: 'list', sort: 'pressure', quota: 'used', theme: 'system', lang: '', family: '', collapsed: {}, hideEmail: false }
68
+ const p = { view: 'list', sort: 'pressure', quota: 'used', theme: 'system', lang: '', family: '', collapsed: {}, rows: {}, hideEmail: false }
69
69
  for (const k of Object.keys(CHOICES)) if (CHOICES[k].includes(raw[k])) p[k] = raw[k]
70
70
  if (raw.hideEmail === true) p.hideEmail = true
71
71
  if (typeof raw.ahHintAt === 'number') p.ahHintAt = raw.ahHintAt
72
72
  if (typeof raw.family === 'string') p.family = raw.family
73
73
  if (raw.collapsed && typeof raw.collapsed === 'object')
74
74
  for (const k of Object.keys(raw.collapsed)) if (raw.collapsed[k] === true) p.collapsed[k] = true
75
+ if (raw.rows && typeof raw.rows === 'object')
76
+ for (const k of Object.keys(raw.rows)) if (raw.rows[k] === true) p.rows[k] = true
75
77
  return p
76
78
  }
77
79
  function savePrefs() { try { localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)) } catch {} }
@@ -954,12 +956,63 @@ const MAIN = String.raw `
954
956
  }
955
957
  // What a row or card shows depends on more than the account (health, view preferences, language); the
956
958
  // fingerprint covers what its click handlers use.
957
- function rowSig(a) { return sig([a, healthOf(a.ref), isNext(a), ui.desktopRef === a.ref]) }
959
+ function rowSig(a) { return sig([a, healthOf(a.ref), isNext(a), ui.desktopRef === a.ref, folded(a)]) }
960
+ // Each account can be folded to one line (its name) and opened again, like a family group; the choice is kept in
961
+ // this browser's preferences.
962
+ function folded(a) { return prefs.rows[a.ref] === true }
963
+ function rowToggle(a) {
964
+ const open = !folded(a)
965
+ const label = t(open ? 'row.collapse' : 'row.expand', { name: a.name })
966
+ return h('button', { class: 'icon-btn row-toggle', type: 'button', 'aria-expanded': String(open), 'aria-label': label, title: label, onclick: (e) => { e.stopPropagation(); toggleRow(a) } }, icon('chevron', 'chev'))
967
+ }
968
+ // The whole account cell (and a folded row anywhere) folds and opens it, not only the arrow; the buttons and links
969
+ // inside keep their own action, and a click that ends a text selection does nothing.
970
+ function foldClick(a) {
971
+ return (e) => {
972
+ if (e.target.closest('button, a, input, select, textarea, .tip')) return
973
+ if (window.getSelection && String(window.getSelection())) return
974
+ toggleRow(a)
975
+ }
976
+ }
977
+ // Folding animates: the details fade out, then the row's height eases to one line; opening eases the height out
978
+ // and fades the details in. The arrow turns with it. Reduced motion just switches.
979
+ const EASE = 'cubic-bezier(.2, 0, 0, 1)'
980
+ function rowDetails(el) { return Array.from(el.querySelectorAll(':scope > :not(.ident):not(.more):not(.card-head), .ident-email, .ident-sub')) }
981
+ function toggleRow(a) {
982
+ const closing = !folded(a)
983
+ const find = () => document.querySelector('[data-ref="' + CSS.escape(a.ref) + '"]')
984
+ const apply = () => {
985
+ if (closing) prefs.rows[a.ref] = true; else delete prefs.rows[a.ref]
986
+ savePrefs()
987
+ renderAccounts()
988
+ }
989
+ const old = find()
990
+ // A hidden page delays animation events; switch at once there too.
991
+ if (REDUCED || document.hidden || !old || !old.animate) { apply(); return }
992
+ const from = old.getBoundingClientRect().height
993
+ const fade = closing ? rowDetails(old).map((d) => d.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 110, easing: 'ease-out', fill: 'forwards' }).finished) : []
994
+ Promise.all(fade).catch(() => {}).then(() => {
995
+ apply()
996
+ const el = find()
997
+ if (!el) return
998
+ const to = el.getBoundingClientRect().height
999
+ el.style.overflow = 'hidden'
1000
+ const done = () => { el.style.overflow = '' }
1001
+ el.animate([{ height: from + 'px' }, { height: to + 'px' }], { duration: 220, easing: EASE }).finished.then(done, done)
1002
+ const chev = el.querySelector('.row-toggle .chev')
1003
+ if (chev) chev.animate([{ transform: closing ? 'rotate(90deg)' : 'rotate(0deg)' }, { transform: closing ? 'rotate(0deg)' : 'rotate(90deg)' }], { duration: 220, easing: EASE })
1004
+ if (!closing) for (const d of rowDetails(el)) d.animate([{ opacity: 0, transform: 'translateY(-4px)' }, { opacity: 1, transform: 'none' }], { duration: 200, delay: 60, easing: 'ease-out', fill: 'backwards' })
1005
+ })
1006
+ }
958
1007
  // The Account 'sideby next <family>' would pick; the server ranks it with the CLI's rule (spec §3.13).
959
1008
  function isNext(a) { return Boolean(S.state && S.state.handoff && S.state.handoff[a.family] === a.ref) }
960
1009
  function row(a, now, cols) {
1010
+ if (folded(a))
1011
+ return h('div', { class: 'row row-folded fold-hit' + (a.error ? ' row-error' : ''), role: 'listitem', 'data-ref': a.ref, 'data-key': a.ref, 'data-sig': rowSig(a), onclick: foldClick(a) },
1012
+ h('div', { class: 'ident' }, rowToggle(a), badge(a.family, 'sm'), h('div', { class: 'ident-text' }, nameLine(a))),
1013
+ moreButton(a))
961
1014
  return h('div', { class: 'row' + (a.error ? ' row-error' : ''), role: 'listitem', 'data-ref': a.ref, 'data-key': a.ref, 'data-sig': rowSig(a) },
962
- h('div', { class: 'ident' }, badge(a.family, 'sm'), h('div', { class: 'ident-text' }, nameLine(a), emailLine(a), subLine(a))),
1015
+ h('div', { class: 'ident fold-hit', onclick: foldClick(a) }, rowToggle(a), badge(a.family, 'sm'), h('div', { class: 'ident-text' }, nameLine(a), emailLine(a), subLine(a))),
963
1016
  quotaBlock(a, now, cols),
964
1017
  usageBlock(a),
965
1018
  lastUsed(a),
@@ -968,8 +1021,12 @@ const MAIN = String.raw `
968
1021
  notes(a))
969
1022
  }
970
1023
  function card(a, now, cols) {
1024
+ if (folded(a))
1025
+ return h('article', { class: 'card card-folded fold-hit' + (a.error ? ' card-error' : ''), 'data-ref': a.ref, 'data-key': a.ref, 'data-sig': rowSig(a), 'aria-label': a.ref, onclick: foldClick(a) },
1026
+ h('header', { class: 'card-head' }, rowToggle(a), badge(a.family, 'sm'), h('div', { class: 'ident-text' }, nameLine(a)), moreButton(a)))
971
1027
  return h('article', { class: 'card' + (a.error ? ' card-error' : ''), 'data-ref': a.ref, 'data-key': a.ref, 'data-sig': rowSig(a), 'aria-label': a.ref },
972
- h('header', { class: 'card-head' },
1028
+ h('header', { class: 'card-head fold-hit', onclick: foldClick(a) },
1029
+ rowToggle(a),
973
1030
  badge(a.family, 'sm'),
974
1031
  h('div', { class: 'ident-text' }, nameLine(a), emailLine(a), subLine(a)),
975
1032
  moreButton(a)),
@@ -2022,10 +2079,21 @@ const MAIN = String.raw `
2022
2079
  function modelChip(a) {
2023
2080
  const mi = a.modelInfo
2024
2081
  if (!mi || (!mi.model && !mi.effort)) return null
2025
- const text = [mi.model && mi.model.value, mi.effort && mi.effort.value].filter(Boolean).join(' · ')
2082
+ // The row has little room; the hover text keeps the full model id.
2083
+ const text = [mi.model && mi.model.value.replace(/^claude-/, ''), mi.effort && mi.effort.value].filter(Boolean).join(' · ')
2084
+ // A value from the last session may differ from what a new session starts with; say both.
2085
+ const line = (label, v, key) => {
2086
+ if (!v) return null
2087
+ const out = label + ': ' + v.value + ' (' + modelFrom(a, v.from) + ')'
2088
+ const host = mi.host && mi.host[key]
2089
+ // 'opus' and 'claude-opus-5-5' name the same model: only a real difference is worth a line.
2090
+ return v.from === 'last' && host && !v.value.includes(host)
2091
+ ? out + '; ' + t('md.newDefault', { value: host, from: modelFrom(a, 'host') })
2092
+ : out
2093
+ }
2026
2094
  const title = [
2027
- mi.model ? t('md.model') + ': ' + mi.model.value + ' (' + modelFrom(a, mi.model.from) + ')' : null,
2028
- mi.effort ? t('md.effort') + ': ' + mi.effort.value + ' (' + modelFrom(a, mi.effort.from) + ')' : null,
2095
+ line(t('md.model'), mi.model, 'model'),
2096
+ line(t('md.effort'), mi.effort, 'effort'),
2029
2097
  mi.settable ? t('md.click') : null,
2030
2098
  ].filter(Boolean).join('\n')
2031
2099
  if (!mi.settable) return h('span', { class: 'mono', title }, text)
@@ -159,6 +159,16 @@ code, .mono { font-family: var(--mono); font-size: 0.92em; }
159
159
  .row:first-child { border-top: 0; }
160
160
  .row:last-child { border-radius: 0 0 var(--radius) var(--radius); }
161
161
  .row-error .ident-name .name { color: var(--fail); }
162
+ /* One account folded to its name line, opened again with the arrow at its start. */
163
+ .row-toggle { width: 28px; height: 28px; margin: -4px -6px -4px -8px; }
164
+ .fold-hit { cursor: pointer; }
165
+ .row > .ident.fold-hit { align-self: stretch; margin: -11px 0 -11px -16px; padding: 11px 0 11px 16px; }
166
+ .fold-hit:hover .row-toggle { background: var(--hover); color: var(--text); }
167
+ .row-toggle[aria-expanded=true] .chev { transform: rotate(90deg); }
168
+ .row-toggle[aria-expanded=true]:not(:hover) { background: transparent; color: var(--muted); }
169
+ .fold-hit:hover .row-toggle[aria-expanded=true] { background: var(--hover); color: var(--text); }
170
+ .row-folded { grid-template-areas: "id id id id id more"; padding-top: 7px; padding-bottom: 7px; }
171
+ .card-folded .card-head { align-items: center; }
162
172
  .col-head { position: sticky; top: 0; z-index: 2; padding: 7px 10px 7px 16px; border-top: 1px solid var(--border); background: var(--surface); color: var(--faint); font-size: 11.5px; font-weight: var(--strong-weight); line-height: 1.3; white-space: nowrap; }
163
173
  .col-head > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
164
174
  .col-head .quota > span { overflow: hidden; text-overflow: ellipsis; }
@@ -515,6 +525,7 @@ select:is(:hover, :focus) { --chevron: var(--text); }
515
525
  @media (max-width: 1100px) {
516
526
  .col-head { display: none; }
517
527
  .row { grid-template-columns: minmax(0, 1fr) auto var(--m-w); grid-template-areas: "id health more" "quota quota quota" "usage usage last" "notes notes notes"; row-gap: 10px; }
528
+ .row-folded { grid-template-areas: "id id more"; }
518
529
  .row > .quota { grid-template-columns: repeat(var(--qn), minmax(0, 1fr)); max-width: 560px; }
519
530
  .rows .m-label, .rows .m-unit, .rows .u-unit { display: inline; }
520
531
  .rows .usage { justify-content: flex-start; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sideby",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Run work, personal and API-key accounts at the same time, without their configs drifting apart.",
5
5
  "license": "MIT",
6
6
  "author": "Adonis0123 (https://github.com/Adonis0123)",
@@ -2,7 +2,7 @@
2
2
  name: sideby
3
3
  description: Use when the user runs, or wants to run, several AI coding accounts (Claude Code, Codex, Grok Build, pi) side by side. Covers installing sideby and setting up a new account ("install sideby", "set up another Claude account", "add cc008", "配置一个 cc00x", "再加一个 Codex 账号"), explaining what accounts share and what stays separate ("what do my accounts share?", "这些账号共享了什么"), showing which account a session is ("show [cc001] in the status line", "加上账号标识"), seeing accounts, quota and usage, moving on when one hits its 5-hour or weekly limit, adding a short command (alias), and repairing drifted account directories (skills, hooks, rules or settings not shared, wrong symlinks, credential file permissions). Drives the `sideby` CLI with `--json`; the agent does every step except browser sign-in, filling in secrets and starting interactive sessions, which it hands to the user as one command.
4
4
  metadata:
5
- version: "0.8.0"
5
+ version: "0.9.1"
6
6
  requires:
7
7
  bins: [sideby, node]
8
8
  cliHelp: sideby --help