eklavya 1.60.0 → 1.61.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.
@@ -813,7 +813,6 @@
813
813
  .set__ctl input[type="number"], .set__ctl textarea { font:inherit; font-size:13.5px; background:var(--bg); color:var(--ink);
814
814
  border:1px solid var(--line-2); border-radius:0; padding:7px var(--space-3); width:100%; }
815
815
  .set__ctl textarea { font-family:var(--font-mono); font-size:12.5px; resize:vertical; min-height:64px; }
816
- .set__ctl input[type="checkbox"] { width:18px; height:18px; accent-color:var(--spot); margin:4px 0; cursor:pointer; }
817
816
  .set__ctl :disabled { opacity:.55; cursor:not-allowed; }
818
817
  .set__rule { color:var(--faint); }
819
818
  .set__err { margin:var(--space-2) 0 0; color:var(--warning); font-size:12.5px; overflow-wrap:anywhere; }
@@ -821,6 +820,58 @@
821
820
  .set__btn { font-family:var(--font-mono); font-size:11px; letter-spacing:.06em; text-transform:uppercase;
822
821
  background:none; border:1px solid var(--line-2); color:var(--dim); padding:2px 8px; cursor:pointer; }
823
822
  .set__btn:hover { color:var(--ink); border-color:var(--spot); }
823
+ /* ---------- Settings switchboard: scopes on the right, one tab of settings at a time ---------- */
824
+ .sw { display:grid; grid-template-columns:minmax(0,1fr) 208px; gap:var(--space-5); align-items:stretch; min-height:calc(100vh - 168px); }
825
+ .sw__scopes { align-self:stretch; display:flex; flex-direction:column; gap:2px; align-content:flex-start;
826
+ background:var(--panel); border:1px solid var(--line-2); padding:var(--space-2); }
827
+ .sw__cap { margin:var(--space-3) var(--space-2) var(--space-1); font-family:var(--font-mono); font-size:10.5px;
828
+ letter-spacing:var(--tracking-caps); text-transform:uppercase; color:var(--faint); }
829
+ .sw__cap:first-child { margin-top:var(--space-1); }
830
+ .sw__projects { display:flex; flex-direction:column; gap:2px; overflow-y:auto; }
831
+ .sw__scope { display:flex; align-items:center; gap:10px; padding:8px 10px; color:var(--dim); text-decoration:none;
832
+ font-size:13.5px; min-width:0; transition:background var(--duration-fast) var(--ease), color var(--duration-fast) var(--ease); }
833
+ .sw__scope svg { width:16px; height:16px; flex:none; }
834
+ .sw__scope span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
835
+ .sw__scope:hover { background:var(--mass); color:var(--ink); }
836
+ .sw__scope[aria-current="page"] { background:var(--mass); color:var(--ink); box-shadow:inset 2px 0 0 var(--spot); }
837
+ .sw__scope[aria-current="page"] svg { color:var(--spot); }
838
+ .sw__none { margin:var(--space-1) var(--space-2) var(--space-2); color:var(--faint); font-size:12.5px; }
839
+ .sw__pane { min-width:0; display:flex; flex-direction:column; gap:var(--space-5); }
840
+ .sw__tabs { display:flex; gap:0; border-bottom:1px solid var(--line-2); overflow-x:auto; scrollbar-width:none; }
841
+ .sw__tabs::-webkit-scrollbar { display:none; }
842
+ .sw__tab { display:flex; align-items:center; gap:6px; padding:10px 9px; margin-bottom:-1px; white-space:nowrap;
843
+ font-family:var(--font-mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase;
844
+ color:var(--faint); text-decoration:none; border-bottom:2px solid transparent;
845
+ transition:color var(--duration-fast) var(--ease); }
846
+ .sw__tab:hover { color:var(--ink); }
847
+ .sw__tab[aria-current="page"] { color:var(--ink); border-bottom-color:var(--spot); }
848
+ .sw__tab i { font-style:normal; color:var(--spot); }
849
+ .card.sw__list { padding:0; }
850
+ .sw__list .set, .sw__list .set:first-child, .sw__list .set:last-child { padding:var(--space-4) var(--space-5); border-bottom:1px solid var(--line); }
851
+ .sw__list .set:last-child { border-bottom:none; }
852
+ .set--bool { align-items:center; flex-wrap:nowrap; }
853
+ .set--bool .set__ctl { flex:0 0 auto; }
854
+ /* A switch: a square track and thumb, the accent when on. It is still a checkbox underneath. */
855
+ .set__ctl input[type="checkbox"] { appearance:none; -webkit-appearance:none; width:44px; height:24px; margin:0; position:relative;
856
+ background:var(--mass); border:1px solid var(--line-2); border-radius:0; cursor:pointer;
857
+ transition:background var(--duration-fast) var(--ease), border-color var(--duration-fast) var(--ease); }
858
+ .set__ctl input[type="checkbox"]::after { content:""; position:absolute; top:2px; left:2px; width:18px; height:18px;
859
+ background:var(--dim); transition:transform var(--duration-fast) var(--ease), background var(--duration-fast) var(--ease); }
860
+ .set__ctl input[type="checkbox"]:checked { background:var(--spot); border-color:var(--spot); }
861
+ .set__ctl input[type="checkbox"]:checked::after { transform:translateX(20px); background:var(--spot-ink); }
862
+ @media (max-width:900px) {
863
+ .sw { grid-template-columns:minmax(0,1fr); gap:var(--space-4); min-height:0; }
864
+ .sw__scopes { order:-1; align-self:auto; flex-direction:row; align-items:center; overflow-x:auto; scrollbar-width:none; }
865
+ .sw__scopes::-webkit-scrollbar { display:none; }
866
+ .sw__cap { display:none; }
867
+ .sw__projects { flex-direction:row; max-height:none; overflow:visible; }
868
+ .sw__scope { white-space:nowrap; }
869
+ .sw__none { margin:0; white-space:nowrap; }
870
+ }
871
+ @media (max-width:560px) {
872
+ .sw__list .set, .sw__list .set:first-child, .sw__list .set:last-child { padding:var(--space-4); }
873
+ .set--bool { flex-wrap:wrap; }
874
+ }
824
875
  /* A config path is one unbroken word; on a phone it has to break anywhere. */
825
876
  .counts .mono, .lines .mono { overflow-wrap:anywhere; }
826
877
  /* ---------- feature tips: Driver.js hints restyled, vendor CSS untouched ---------- */
@@ -3118,8 +3169,8 @@ const TIPS = [
3118
3169
  when: () => (S.artifacts ?? []).some((a) => a.correction === 'open') },
3119
3170
  { id: 'artifact-tabs', where: ['artifacts/view'], el: '#view [role="tablist"]', title: 'Open pages',
3120
3171
  text: 'Pages you open stay here as tabs, up to eight. All artifacts takes you back.' },
3121
- { id: 'settings-scope', where: ['settings/dashboard'], el: '#nav', side: 'right', title: 'User or project',
3122
- text: 'User settings apply everywhere. Project settings override them for one project.' },
3172
+ { id: 'settings-scope', where: ['settings/dashboard'], el: '.sw__scopes', side: 'left', title: 'User or project',
3173
+ text: 'User settings apply everywhere. Pick a project to override them for that project only.' },
3123
3174
  { id: 'ground', where: ['settings/dashboard'], el: '.ground[aria-label="Colour ground"]', side: 'right', title: 'Ink or paper',
3124
3175
  text: 'Switch the dashboard between a dark and a light ground.' },
3125
3176
  ];
@@ -3492,7 +3543,7 @@ function settingRow(f, d, scope) {
3492
3543
  ? (Object.hasOwn(d.user.set, f.key) ? 'inherited from user settings' : 'default')
3493
3544
  : 'default'}</span>`;
3494
3545
  }
3495
- return `<div class="set">
3546
+ return `<div class="set${f.type === 'bool' ? ' set--bool' : ''}">
3496
3547
  <div class="set__main"><label for="${id}">${esc(f.label)}</label>
3497
3548
  <p id="${id}-h">${esc(f.help)}${ruleHint(f) ? ` <span class="set__rule">${esc(ruleHint(f))}</span>` : ''}</p>
3498
3549
  <p class="set__src"><code>${esc(f.key)}</code>${src}<span data-msg="${esc(f.key)}" role="status"></span></p></div>
@@ -3500,37 +3551,86 @@ function settingRow(f, d, scope) {
3500
3551
  </div>`;
3501
3552
  }
3502
3553
 
3554
+ /** A category's tab id: `This machine` is `this-machine`. */
3555
+ const swTab = (group) => group.toLowerCase().replace(/\W+/g, '-');
3556
+ const SW_ICON = {
3557
+ user: '<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>',
3558
+ project: '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
3559
+ };
3560
+
3561
+ /**
3562
+ * The switchboard. On the left, the scope: you (user settings, the default) or
3563
+ * one project. On the right, that scope's settings one category at a time, each
3564
+ * category a tab in the URL (`#/settings/dashboard/memory`). Picking another scope
3565
+ * keeps the tab. `Terminal` lists what stays a typed command; `Changed` lists
3566
+ * what this scope's file actually sets.
3567
+ */
3503
3568
  function drawSettings(d, scope) {
3504
3569
  SETS = d;
3505
- const W = scope === 'project' ? 'Project settings' : 'User settings';
3570
+ const tabParam = (id) => (id === swTab(d.fields[0].group) ? null : id);
3571
+ const page = scope === 'project' ? 'project' : 'dashboard';
3572
+ const file = scope === 'project' ? d.project : d.user;
3573
+ const groups = [...new Set(d.fields.map((f) => f.group))];
3574
+ const tabs = groups.map((g) => {
3575
+ const fields = d.fields.filter((f) => f.group === g);
3576
+ return { id: swTab(g), label: g, fields, n: file ? fields.filter((f) => Object.hasOwn(file.set, f.key)).length : 0 };
3577
+ });
3578
+ tabs.push({ id: 'terminal', label: 'Terminal', n: 0 }, { id: 'changed', label: 'Changed', n: file ? Object.keys(file.set).length : 0 });
3579
+ const here = tabs.find((t) => t.id === CUR.param) ?? tabs[0];
3580
+
3581
+ const scopeLink = (label, icon, to, current) =>
3582
+ `<a class="sw__scope" href="${esc(to)}"${current ? ' aria-current="page"' : ''}>${svg(icon)}<span>${esc(label)}</span></a>`;
3583
+ const rail = `<nav class="sw__scopes" aria-label="Settings scope">
3584
+ <p class="sw__cap">Everywhere</p>
3585
+ ${scopeLink('User', SW_ICON.user, SH('dashboard', tabParam(here.id)), scope === 'user')}
3586
+ <p class="sw__cap">Projects</p>
3587
+ <div class="sw__projects">${d.projects.length
3588
+ ? d.projects.map((p) => scopeLink(p.name, SW_ICON.project, SH('project', tabParam(here.id), { project: p.inventory }),
3589
+ scope === 'project' && d.project?.inventory === p.inventory)).join('')
3590
+ : '<p class="sw__none">No project with a checkout on this machine yet.</p>'}</div>
3591
+ </nav>`;
3592
+
3506
3593
  const about = `User settings apply everywhere; a project setting wins in that project only, and <b>Inherit</b>
3507
3594
  removes it so the user setting applies again. Every change here is the same write as
3508
3595
  <code>eklavya config set &lt;key&gt; &lt;value&gt;</code> (with <code>--project</code> for a project), and
3509
3596
  <b>Inherit</b> / <b>Reset</b> is <code>eklavya config unset</code>. Nothing is written inside a repository.`;
3510
- if (scope === 'project' && !d.project) {
3511
- const known = d.projects.filter((p) => projectById(p.inventory));
3512
- return [
3513
- pageHead(W, [plural(known.length, 'project')], about),
3597
+
3598
+ if (!file) {
3599
+ return `<div class="sw"><div class="sw__pane">${[
3600
+ pageHead('Project settings', [], about),
3514
3601
  nextStep(PROJECT
3515
- ? `<strong>${esc(scopeName())}</strong> has no git checkout on this machine, so there is no project file to change. Pick one below.`
3516
- : 'Pick a project to change its settings, or use the project selector.'),
3517
- card(null, null, lines(known.map((p) => ({ t: esc(p.name), s: `<span class="mono">${esc(p.id)}</span>`,
3518
- go: SH('project', null, { project: p.inventory }) })), 'No project with a checkout on this machine yet.')),
3519
- ].join('');
3602
+ ? `<strong>${esc(scopeName())}</strong> has no git checkout on this machine, so there is no project file to change. Pick another project on the right.`
3603
+ : 'Pick a project on the right to change its settings.'),
3604
+ ].join('')}</div>${rail}</div>`;
3520
3605
  }
3521
- const file = scope === 'project' ? d.project : d.user;
3522
- const groups = [...new Set(d.fields.map((f) => f.group))];
3523
- const setHere = Object.keys(file.set).length;
3524
- return [
3525
- pageHead(scope === 'project' ? `${esc(d.project.name)} settings` : W,
3526
- [`<span class="mono">${esc(file.path)}</span>`, `${setHere} set here`], about),
3527
- d.problem ? nextStep(`<strong>A config file is not being read.</strong> ${esc(d.problem)}`) : '',
3528
- ...groups.map((g) => card(g, null, d.fields.filter((f) => f.group === g).map((f) => settingRow(f, d, scope)).join(''))),
3529
- fold(`settings:${scope}:cli`, 'Changed from the terminal', plural(d.cli_only.length, 'setting'),
3606
+
3607
+ const tabBar = `<nav class="sw__tabs" aria-label="Setting categories">${tabs.map((t) =>
3608
+ `<a class="sw__tab" href="${esc(SH(page, tabParam(t.id)))}"${t.id === here.id ? ' aria-current="page"' : ''}>${esc(t.label)}${t.n ? `<i title="${t.n} set in this scope">${t.n}</i>` : ''}</a>`).join('')}</nav>`;
3609
+
3610
+ let body;
3611
+ if (here.id === 'terminal') {
3612
+ body = card('Changed from the terminal', 'These send work off this machine or run a command, so they stay a typed command: eklavya config set <key> <value>.',
3530
3613
  lines(d.cli_only.map((c) => ({ t: `<code>${esc(c.key)}</code>`, s: esc(c.why),
3531
- v: `<span class="mono">${esc(showValue(file.effective[c.key]))}</span>` }))),
3532
- { hint: 'These send work off this machine or run a command, so they stay a typed command: eklavya config set <key> <value>.' }),
3533
- ].join('');
3614
+ v: `<span class="mono">${esc(showValue(file.effective[c.key]))}</span>` }))));
3615
+ } else if (here.id === 'changed') {
3616
+ const tabOf = (k) => { const f = d.fields.find((x) => x.key === k); return f ? swTab(f.group) : 'terminal'; };
3617
+ body = card(`Set ${scope === 'project' ? 'for this project' : 'by you'}`,
3618
+ scope === 'project' ? 'Everything else inherits from user settings.' : 'Everything else is the default.',
3619
+ lines(Object.keys(file.set).map((k) => ({ t: `<code>${esc(k)}</code>`,
3620
+ s: scope === 'project' ? esc(`user: ${showValue(d.user.effective[k])}`) : '',
3621
+ v: `<span class="mono">${esc(showValue(file.set[k]))}</span>`, go: SH(page, tabParam(tabOf(k))) })),
3622
+ scope === 'project' ? 'This project overrides nothing; user settings apply.' : 'Nothing set: every setting is at its default.'));
3623
+ } else {
3624
+ body = `<section class="card sw__list" aria-label="${esc(here.label)} settings">${here.fields.map((f) => settingRow(f, d, scope)).join('')}</section>`;
3625
+ }
3626
+
3627
+ return `<div class="sw"><div class="sw__pane">${[
3628
+ pageHead(scope === 'project' ? `${esc(d.project.name)} settings` : 'User settings',
3629
+ [`<span class="mono">${esc(file.path)}</span>`, `${Object.keys(file.set).length} set here`], about),
3630
+ d.problem ? nextStep(`<strong>A config file is not being read.</strong> ${esc(d.problem)}`) : '',
3631
+ tabBar,
3632
+ body,
3633
+ ].join('')}</div>${rail}</div>`;
3534
3634
  }
3535
3635
 
3536
3636
  /* ============================================================
@@ -3736,30 +3836,7 @@ function fbDelete(btn) {
3736
3836
 
3737
3837
  function viewSettings(scope) {
3738
3838
  fill('settings', settingsUrl(), (d) => drawSettings(d, scope));
3739
- return `<div class="stack" id="settings">${loader('Reading your settings…')}</div>`;
3740
- }
3741
-
3742
- function viewSettingsHome() {
3743
- fill('settings', settingsUrl(), (d) => {
3744
- SETS = d;
3745
- const proj = d.project;
3746
- const overrides = proj ? Object.keys(proj.set).filter((k) => !d.global_only.includes(k)) : [];
3747
- return [
3748
- pageHead('Settings', [plural(Object.keys(d.user.set).length, 'user setting'),
3749
- proj ? `${plural(overrides.length, 'project setting')} in ${esc(proj.name)}` : 'no project selected'],
3750
- `Two files, one precedence: the project's file wins over yours, key by key, and anything neither sets is the
3751
- default. The terminal writes the same files — <code>eklavya config get</code> shows what is in force.`),
3752
- card(null, null, lines([
3753
- { t: 'User settings', s: `<span class="mono">${esc(d.user.path)}</span>`, v: esc(`${Object.keys(d.user.set).length} set`), go: SH('user') },
3754
- { t: 'Project settings', s: proj ? `<span class="mono">${esc(proj.path)}</span>` : 'choose a project',
3755
- v: proj ? esc(`${overrides.length} set`) : '', go: SH('project') },
3756
- ])),
3757
- proj && overrides.length ? card(`What ${proj.name} changes`, null, lines(overrides.map((k) => ({
3758
- t: `<code>${esc(k)}</code>`, s: esc(`user: ${showValue(d.user.effective[k])}`),
3759
- v: `<span class="mono">${esc(showValue(proj.set[k]))}</span>`, go: SH('project') })))) : '',
3760
- ].join('');
3761
- });
3762
- return `<div class="stack" id="settings">${loader('Reading your settings…')}</div>`;
3839
+ return `<div id="settings">${loader('Reading your settings…')}</div>`;
3763
3840
  }
3764
3841
 
3765
3842
  /** Reads a control back into the value its field stores. */
@@ -3914,13 +3991,11 @@ const WORKFLOWS = {
3914
3991
  },
3915
3992
  settings: {
3916
3993
  name: 'Settings',
3917
- groups: [
3918
- { id: 'scopes', label: 'Scopes', items: ['user', 'project'] },
3919
- ],
3994
+ groups: [],
3920
3995
  pages: {
3921
- dashboard: { label: 'Overview', view: () => viewSettingsHome() },
3922
- user: { label: 'User', view: () => viewSettings('user') },
3923
- project: { label: 'Project', view: () => viewSettings('project') },
3996
+ dashboard: { label: 'Preferences', view: () => viewSettings('user') },
3997
+ user: { parent: 'dashboard', view: () => viewSettings('user') },
3998
+ project: { parent: 'dashboard', view: () => viewSettings('project') },
3924
3999
  },
3925
4000
  },
3926
4001
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eklavya",
3
3
  "displayName": "Eklavya",
4
- "version": "1.60.0",
4
+ "version": "1.61.0",
5
5
  "description": "Learn while your agent works. Turns coding-agent generation time into adaptive, Socratic learning grounded in the code being written.",
6
6
  "author": {
7
7
  "name": "Ajay Kumar"
@@ -346,7 +346,7 @@ project; the ids are the ones `/api/projects` lists.
346
346
  | which explainers can I still correct | `/#/artifacts/dashboard/to-correct`; any page opens as a tab at `/#/artifacts/view/<id>` (the id from `eklavya artifacts list --json`, URL-encoded) |
347
347
  | which projects have pages | `/#/artifacts/projects` |
348
348
  | my prompt feedback, what is waiting, what I acknowledged | `/#/feedback/dashboard`, `/#/feedback/history`, `/#/feedback/item/<id>` |
349
- | change my settings in a page, see what a project overrides | `/#/settings/dashboard`, `/#/settings/user`, `/#/settings/project?project=<path>` |
349
+ | change my settings in a page, see what a project overrides | `/#/settings/dashboard`, `/#/settings/dashboard/<tab>` (`pacing`, `levels`, `memory`, `feedback`, `privacy`, `this-machine`, `terminal`, `changed`), `/#/settings/project/<tab>?project=<path>` |
350
350
 
351
351
  The older single-workflow links (`/#/overview`, `/#/concepts`, `/#/memory`,
352
352
  `/#/entry/<id>` and the rest) still open the right page, but hand back the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eklavya",
3
- "version": "1.60.0",
3
+ "version": "1.61.0",
4
4
  "description": "Learn while your agent works — local knowledge graph, spaced repetition, and quiz gating for agent-assisted development.",
5
5
  "license": "MIT",
6
6
  "repository": {