clay-server 5.4.0 → 5.5.0-beta.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.
Files changed (69) hide show
  1. package/lib/agent-behavior-gate.js +223 -0
  2. package/lib/agent-behavior-schema.js +184 -0
  3. package/lib/daemon.js +15 -0
  4. package/lib/issues-mcp-server.js +16 -3
  5. package/lib/migrate-single-user.js +2 -0
  6. package/lib/project-issues.js +9 -2
  7. package/lib/project-log-feedback-delivery.js +3 -0
  8. package/lib/project-logs-mcp-server.js +23 -2
  9. package/lib/project-logs.js +40 -6
  10. package/lib/project-session-document.js +9 -1
  11. package/lib/project-session-notes.js +19 -8
  12. package/lib/project-session-pair-prompt.js +5 -4
  13. package/lib/project-user-knowledge.js +21 -3
  14. package/lib/project-worker-proposal.js +8 -2
  15. package/lib/project.js +108 -61
  16. package/lib/public/app.js +1 -0
  17. package/lib/public/css/agent-behavior.css +101 -0
  18. package/lib/public/modules/agent-behavior-nav.js +90 -0
  19. package/lib/public/modules/agent-behavior-view.js +235 -0
  20. package/lib/public/modules/agent-behavior.js +200 -0
  21. package/lib/public/modules/settings-shell-registry.js +1 -1
  22. package/lib/public/modules/settings-shell-view.js +18 -0
  23. package/lib/public/modules/user-settings-layout.js +8 -1
  24. package/lib/public/modules/user-settings.js +5 -2
  25. package/lib/public/pane-assets/{app-BDYH6Y7R.js → app-4IZ6F6ND.js} +1 -1
  26. package/lib/public/pane-assets/{chunk-HWLBH6AL.js → chunk-3E6LDNXF.js} +1 -1
  27. package/lib/public/pane-assets/chunk-6KO36HMU.js +1 -0
  28. package/lib/public/pane-assets/{chunk-N5SIEIOG.js → chunk-6ZF2FJT3.js} +1 -1
  29. package/lib/public/pane-assets/{chunk-TT3NECKK.js → chunk-A4MLSYBJ.js} +1 -1
  30. package/lib/public/pane-assets/{chunk-KF3LZRJK.js → chunk-A7OB7HQS.js} +1 -1
  31. package/lib/public/pane-assets/{chunk-QRVKAVH7.js → chunk-ADGKPVFD.js} +1 -1
  32. package/lib/public/pane-assets/{chunk-UMIYQYGM.js → chunk-DJ5WXQKT.js} +1 -1
  33. package/lib/public/pane-assets/{chunk-KB4T5B2R.js → chunk-ELL4UBGY.js} +1 -1
  34. package/lib/public/pane-assets/{chunk-FQQMXMH7.js → chunk-FILRACQB.js} +97 -97
  35. package/lib/public/pane-assets/chunk-H6MU72QE.js +1 -0
  36. package/lib/public/pane-assets/{chunk-X452YCE5.js → chunk-I6X5J2V5.js} +1 -1
  37. package/lib/public/pane-assets/{chunk-U5GH2UJM.js → chunk-L3FCTD7U.js} +1 -1
  38. package/lib/public/pane-assets/{chunk-VCNLKOVT.js → chunk-OB3KKICE.js} +1 -1
  39. package/lib/public/pane-assets/chunk-PJURCIJQ.js +1 -0
  40. package/lib/public/pane-assets/chunk-Q7RFSS5Q.js +1 -0
  41. package/lib/public/pane-assets/{chunk-42C2CZEP.js → chunk-RRYWV5JT.js} +1 -1
  42. package/lib/public/pane-assets/{chunk-E6NRPGVH.js → chunk-SEELA2D2.js} +1 -1
  43. package/lib/public/pane-assets/chunk-UTLQWAKG.js +1 -0
  44. package/lib/public/pane-assets/chunk-WE4NWXNK.js +1 -0
  45. package/lib/public/pane-assets/{chunk-YE45RR7W.js → chunk-YFUORWA6.js} +1 -1
  46. package/lib/public/pane-assets/{chunk-TMUCEZ7C.js → chunk-YWHBNEB2.js} +1 -1
  47. package/lib/public/pane-assets/{chunk-XI2DJM4H.js → chunk-YXJ7ISEK.js} +1 -1
  48. package/lib/public/pane-assets/chunk-Z2MHS43B.js +1 -0
  49. package/lib/public/pane-assets/manifest.json +436 -432
  50. package/lib/public/pane-assets/{style-VDIGFFLR.css → style-DZX6JAMF.css} +1 -1
  51. package/lib/public/style.css +1 -0
  52. package/lib/sdk-bridge.js +3 -2
  53. package/lib/server-agent-behavior-settings.js +72 -0
  54. package/lib/server-settings.js +2 -0
  55. package/lib/server.js +9 -0
  56. package/lib/session-notes-mcp-server.js +27 -7
  57. package/lib/session-skill-scope.js +2 -2
  58. package/lib/users-agent-behavior-preferences.js +38 -0
  59. package/lib/users.js +6 -0
  60. package/lib/yoke/adapters/claude.js +8 -4
  61. package/lib/yoke/skill-discovery.js +46 -1
  62. package/package.json +1 -1
  63. package/lib/public/pane-assets/chunk-46OOGVHU.js +0 -1
  64. package/lib/public/pane-assets/chunk-4OJW4FF4.js +0 -1
  65. package/lib/public/pane-assets/chunk-F7VIE6FJ.js +0 -1
  66. package/lib/public/pane-assets/chunk-FCJYJLVZ.js +0 -1
  67. package/lib/public/pane-assets/chunk-FEJDEXOW.js +0 -1
  68. package/lib/public/pane-assets/chunk-HEMCIILG.js +0 -1
  69. package/lib/public/pane-assets/chunk-RV5O66TO.js +0 -1
@@ -0,0 +1,101 @@
1
+ /* Personal > Agent behavior: one scrolling page with a subordinate section
2
+ list. Shared settings-ui primitives supply headings, hints, buttons and
3
+ status; this sheet only places the page-specific pieces. */
4
+ #user-settings .us-section[data-section="us-agent"] { max-width: none; }
5
+ #us-agent-behavior { min-width: 0; }
6
+ #us-agent-behavior .ab-feedback { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
7
+ #us-agent-behavior .ab-feedback .settings-status { margin: 0; }
8
+ #us-agent-behavior .ab-feedback[hidden] { display: none; }
9
+
10
+ .ab-note { margin: 4px 0 20px; padding: 10px 12px; border: 1px solid var(--border-subtle); border-radius: 8px; background: var(--bg-alt); }
11
+ .ab-note p { margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--text-secondary, var(--text-muted)); }
12
+ .ab-note p + p { margin-top: 4px; }
13
+
14
+ .ab-layout { display: block; }
15
+ /* Section links under the active item of the shell's left navigation (desktop) */
16
+ .settings-shell-nav .ab-nav { margin: 2px 0 6px 12px; padding-left: 8px; border-left: 1px solid var(--border-subtle); }
17
+ .ab-nav-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 1px; }
18
+ .ab-nav-link {
19
+ display: flex; align-items: center; min-height: 28px; padding: 4px 10px; border-radius: 8px;
20
+ font-size: 12.5px; font-weight: 500; line-height: 1.3; color: var(--text-muted); text-decoration: none;
21
+ }
22
+ .ab-nav-link:hover { background: rgba(var(--overlay-rgb), .05); color: var(--text); }
23
+ .ab-nav-link.active { background: rgba(var(--overlay-rgb), .08); color: var(--text); }
24
+ .ab-nav-link:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
25
+
26
+ .ab-sections { min-width: 0; max-width: 760px; }
27
+ .ab-section { scroll-margin-top: 4px; padding-bottom: 8px; }
28
+ .ab-section + .ab-section { margin-top: 28px; }
29
+ #user-settings .ab-section .ab-section-title { margin: 0 0 2px; font-size: 15px; }
30
+ #user-settings .ab-section .ab-section-title:focus { outline: none; }
31
+ #user-settings .ab-section .ab-section-title:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
32
+ .ab-section-desc { margin: 0 0 4px; }
33
+
34
+ .ab-setting { padding: 12px 0; border-top: 1px solid var(--border-subtle); display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 24px; align-items: start; }
35
+ .ab-setting > .ab-label, .ab-setting > .ab-desc { grid-column: 1; }
36
+ .ab-setting > .ab-control-row { grid-column: 2; grid-row: 1 / span 2; margin-top: 0; justify-content: flex-end; flex-wrap: wrap; }
37
+ .ab-setting > .ab-control-row .ab-status { order: -1; }
38
+ .ab-setting > .ab-consequence, .ab-setting > .ab-example, .ab-setting > .ab-nested { grid-column: 1 / -1; }
39
+ .ab-section-desc + .ab-setting { margin-top: 10px; }
40
+ #user-settings .ab-setting .ab-label { margin: 0; font-size: 13px; font-weight: 600; line-height: 1.4; color: var(--text); }
41
+ #user-settings .ab-setting .ab-desc { margin: 2px 0 0; }
42
+ .ab-control-row { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; margin-top: 10px; min-height: 32px; }
43
+ .ab-status { display: flex; align-items: center; gap: 8px; min-width: 0; font-size: 12.5px; color: var(--text-muted); }
44
+ .ab-status:empty { display: none; }
45
+ .ab-status[data-state="saving"]::before { content: ""; width: 10px; height: 10px; flex: none; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: ab-spin .8s linear infinite; }
46
+ .ab-status[data-state="saved"] { color: var(--text); }
47
+ .ab-status[data-state="saved"]::before { content: ""; width: 7px; height: 7px; flex: none; border-radius: 50%; background: var(--success, #3fb950); }
48
+ .ab-status[data-state="error"] { color: var(--error); }
49
+ .ab-status-text { overflow-wrap: anywhere; }
50
+ #user-settings .ab-retry { height: 28px; padding: 0 10px; flex: none; }
51
+ @keyframes ab-spin { to { transform: rotate(360deg); } }
52
+
53
+ /* Inline segmented control (radiogroup) */
54
+ .ab-seg { display: inline-flex; gap: 2px; padding: 3px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-alt); }
55
+ .ab-seg-opt {
56
+ min-width: 64px; height: 28px; padding: 0 12px; border: 0; border-radius: 6px; background: none;
57
+ color: var(--text-muted); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; white-space: nowrap;
58
+ }
59
+ .ab-seg-opt:hover { color: var(--text); }
60
+ .ab-seg-opt.selected { background: var(--bg); color: var(--text); box-shadow: 0 1px 3px rgba(0, 0, 0, .12); }
61
+ .ab-seg-opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
62
+
63
+ .ab-consequence { margin: 8px 0 0; font-size: 13px; line-height: 1.5; color: var(--text); }
64
+ .ab-example { margin: 4px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--text-muted); }
65
+ .ab-example-label { display: inline-block; margin-right: 8px; font-size: 11.5px; font-weight: 600; color: var(--text-dimmer); }
66
+
67
+ /* Dependent checkbox nested under Sticky Notes */
68
+ .ab-nested { display: block; margin: 12px 0 0 4px; padding: 2px 0 2px 14px; border-top: 0; border-left: 2px solid var(--border-subtle); }
69
+ .ab-check { display: flex; align-items: flex-start; gap: 8px; font-size: 13px; font-weight: 600; line-height: 1.4; color: var(--text); cursor: pointer; }
70
+ .ab-check input { flex: none; width: 16px; height: 16px; margin: 2px 0 0; accent-color: var(--accent); cursor: pointer; }
71
+ .ab-check input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
72
+ #user-settings .ab-nested .ab-desc { margin: 2px 0 0 24px; }
73
+ .ab-nested .ab-control-row { margin-top: 4px; min-height: 0; margin-left: 24px; }
74
+ .ab-nested .ab-consequence, .ab-nested .ab-example { margin-left: 24px; }
75
+ .ab-nested .ab-consequence { margin-top: 8px; }
76
+ .ab-inactive { margin: 8px 0 0 24px; padding: 6px 10px; border-radius: 6px; background: rgba(var(--overlay-rgb), .05); font-size: 12.5px; line-height: 1.5; color: var(--text-secondary, var(--text-muted)); }
77
+ .ab-inactive[hidden] { display: none; }
78
+ .ab-inactive-setting .ab-check, .ab-inactive-setting .ab-consequence, .ab-inactive-setting .ab-example { opacity: .62; }
79
+
80
+ /* Narrow: the section list becomes a compact sticky row above the content */
81
+ .ab-layout.ab-narrow { display: block; }
82
+ .ab-layout.ab-narrow .ab-nav {
83
+ margin-left: -14px; border-left: 0;
84
+ position: sticky; top: -18px; z-index: 3; margin: 0 -14px 12px; padding: 8px 14px 8px; background: var(--bg);
85
+ border-bottom: 1px solid var(--border-subtle); overflow-x: auto; scrollbar-width: none;
86
+ }
87
+ .ab-layout.ab-narrow .ab-nav::-webkit-scrollbar { display: none; }
88
+ .ab-layout.ab-narrow .ab-nav-list { flex-direction: row; gap: 6px; width: max-content; }
89
+ .ab-layout.ab-narrow .ab-nav-link { min-height: 36px; padding: 6px 12px; border: 1px solid var(--border); white-space: nowrap; }
90
+ .ab-layout.ab-narrow .ab-nav-link.active { border-color: var(--accent); background: var(--accent-12); }
91
+ .ab-layout.ab-narrow .ab-section { scroll-margin-top: 60px; }
92
+ .ab-layout.ab-narrow .ab-setting { display: block; }
93
+ .ab-layout.ab-narrow .ab-setting > .ab-control-row { margin-top: 10px; justify-content: flex-start; }
94
+ .ab-layout.ab-narrow .ab-setting > .ab-control-row .ab-status { order: 0; }
95
+ .ab-layout.ab-narrow .ab-seg { display: flex; width: 100%; }
96
+ .ab-layout.ab-narrow .ab-seg-opt { flex: 1 1 0; min-width: 0; height: 36px; padding: 0 6px; }
97
+ .ab-layout.ab-narrow .ab-nested { margin-left: 0; }
98
+
99
+ @media (prefers-reduced-motion: reduce) {
100
+ .ab-status[data-state="saving"]::before { animation: none; }
101
+ }
@@ -0,0 +1,90 @@
1
+ // agent-behavior-nav.js - section links for the single scrolling Agent
2
+ // behavior page: smooth anchor scrolling, scroll-derived active state and the
3
+ // narrow (horizontal sticky row) layout flag.
4
+ var NARROW_QUERY = '(max-width: 800px)';
5
+ var SCROLL_KEYS = ['PageUp', 'PageDown', 'Home', 'End', ' '];
6
+
7
+ // opts: { layout, nav, scroller, sections: [{ id, el, heading }] }
8
+ export function attachSectionNav(opts) {
9
+ var layout = opts.layout; var nav = opts.nav; var scroller = opts.scroller;
10
+ var sections = opts.sections;
11
+ var links = Array.prototype.slice.call(nav.querySelectorAll('a[data-section]'));
12
+ var active = null; var locked = false; var frame = 0;
13
+ var narrowMq = window.matchMedia ? window.matchMedia(NARROW_QUERY) : null;
14
+
15
+ function applyNarrow() {
16
+ layout.classList.toggle('ab-narrow', !!(narrowMq && narrowMq.matches));
17
+ if (opts.onPlace) opts.onPlace(isNarrow());
18
+ }
19
+ function isNarrow() { return layout.classList.contains('ab-narrow'); }
20
+
21
+ function setActive(id) {
22
+ if (id === active) return;
23
+ active = id;
24
+ links.forEach(function (link) {
25
+ var on = link.dataset.section === id;
26
+ link.classList.toggle('active', on);
27
+ if (on) link.setAttribute('aria-current', 'location'); else link.removeAttribute('aria-current');
28
+ if (on && isNarrow() && nav.scrollWidth > nav.clientWidth) {
29
+ nav.scrollLeft = Math.max(0, link.offsetLeft - (nav.clientWidth - link.offsetWidth) / 2);
30
+ }
31
+ });
32
+ }
33
+
34
+ function compute() {
35
+ frame = 0;
36
+ if (locked || !sections.length) return;
37
+ var offset = (isNarrow() ? nav.offsetHeight : 0) + 24;
38
+ var line = scroller.getBoundingClientRect().top + offset;
39
+ var current = sections[0].id;
40
+ sections.forEach(function (section) { if (section.el.getBoundingClientRect().top <= line) current = section.id; });
41
+ if (scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 2 && scroller.scrollTop > 0) current = sections[sections.length - 1].id;
42
+ setActive(current);
43
+ }
44
+ function schedule() { if (!frame) frame = requestAnimationFrame(compute); }
45
+ function unlock() { locked = false; schedule(); }
46
+ function onKey(event) { if (SCROLL_KEYS.indexOf(event.key) >= 0 && !nav.contains(event.target)) unlock(); }
47
+
48
+ function onPointer(event) { if (event.target === scroller) unlock(); }
49
+
50
+ function go(id) {
51
+ var section = sections.filter(function (item) { return item.id === id; })[0];
52
+ if (!section) return;
53
+ locked = true; setActive(id);
54
+ var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
55
+ section.el.scrollIntoView({ behavior: reduce ? 'auto' : 'smooth', block: 'start' });
56
+ if (section.heading) section.heading.focus({ preventScroll: true });
57
+ }
58
+ function onClick(event) {
59
+ var link = event.target.closest ? event.target.closest('a[data-section]') : null;
60
+ if (!link || !nav.contains(link)) return;
61
+ event.preventDefault();
62
+ go(link.dataset.section);
63
+ }
64
+
65
+ nav.addEventListener('click', onClick);
66
+ scroller.addEventListener('scroll', schedule, { passive: true });
67
+ scroller.addEventListener('wheel', unlock, { passive: true });
68
+ scroller.addEventListener('touchstart', unlock, { passive: true });
69
+ scroller.addEventListener('keydown', onKey);
70
+ scroller.addEventListener('pointerdown', onPointer);
71
+ if (narrowMq && narrowMq.addEventListener) narrowMq.addEventListener('change', applyNarrow);
72
+ window.addEventListener('resize', schedule);
73
+ applyNarrow();
74
+ compute();
75
+
76
+ return {
77
+ go: go, refresh: compute, active: function () { return active; },
78
+ destroy: function () {
79
+ nav.removeEventListener('click', onClick);
80
+ scroller.removeEventListener('scroll', schedule);
81
+ scroller.removeEventListener('wheel', unlock);
82
+ scroller.removeEventListener('touchstart', unlock);
83
+ scroller.removeEventListener('keydown', onKey);
84
+ scroller.removeEventListener('pointerdown', onPointer);
85
+ if (narrowMq && narrowMq.removeEventListener) narrowMq.removeEventListener('change', applyNarrow);
86
+ window.removeEventListener('resize', schedule);
87
+ if (frame) cancelAnimationFrame(frame);
88
+ }
89
+ };
90
+ }
@@ -0,0 +1,235 @@
1
+ // agent-behavior-view.js - DOM for the Personal "Agent behavior" page. Built
2
+ // from the server catalog; later store changes update the existing nodes in
3
+ // place so focus, scroll position and the live status regions are stable.
4
+ import { store } from './store.js';
5
+ import {
6
+ getAgentBehavior, loadAgentBehavior, chooseValue, retryValue, displayedValue, statusFor, checkboxStatus
7
+ } from './agent-behavior.js';
8
+ import { attachSectionNav } from './agent-behavior-nav.js';
9
+ import { setPageSubnav } from './settings-shell-view.js';
10
+
11
+ var NOTE = 'Choices apply to your own Driver conversations and take effect from your next message in each conversation.';
12
+ var host = null; var unsubscribe = null; var built = null; var nav = null;
13
+
14
+ function make(tag, className, text) {
15
+ var node = document.createElement(tag);
16
+ if (className) node.className = className;
17
+ if (text !== undefined) node.textContent = text;
18
+ return node;
19
+ }
20
+ function setText(node, text) { if (node && node.textContent !== text) node.textContent = text; }
21
+
22
+ function statusBox(id) {
23
+ var box = make('div', 'ab-status'); box.dataset.status = id;
24
+ box.setAttribute('role', 'status'); box.setAttribute('aria-live', 'polite');
25
+ return box;
26
+ }
27
+
28
+ function buildMode(setting, state) {
29
+ var id = setting.id;
30
+ var wrap = make('div', 'ab-setting'); wrap.dataset.setting = id;
31
+ var label = make('h4', 'ab-label', setting.label); label.id = 'ab-l-' + id;
32
+ var desc = make('p', 'settings-hint ab-desc', setting.description); desc.id = 'ab-d-' + id;
33
+ var row = make('div', 'ab-control-row');
34
+ var group = make('div', 'ab-seg'); group.setAttribute('role', 'radiogroup');
35
+ group.setAttribute('aria-labelledby', 'ab-l-' + id);
36
+ group.setAttribute('aria-describedby', 'ab-d-' + id + ' ab-c-' + id);
37
+ state.catalog.modes.forEach(function (mode) {
38
+ var radio = make('button', 'ab-seg-opt', mode.label); radio.type = 'button';
39
+ radio.setAttribute('role', 'radio'); radio.dataset.value = mode.id;
40
+ group.appendChild(radio);
41
+ });
42
+ group.addEventListener('click', function (event) {
43
+ var radio = event.target.closest('[role="radio"]');
44
+ if (radio) chooseValue(id, radio.dataset.value);
45
+ });
46
+ group.addEventListener('keydown', function (event) { onRadioKey(event, group, id); });
47
+ row.appendChild(group); row.appendChild(statusBox(id));
48
+ var consequence = make('p', 'ab-consequence'); consequence.id = 'ab-c-' + id;
49
+ var example = make('p', 'ab-example');
50
+ example.appendChild(make('span', 'ab-example-label', 'Example'));
51
+ example.appendChild(make('span', 'ab-example-text'));
52
+ [label, desc, row, consequence, example].forEach(function (node) { wrap.appendChild(node); });
53
+ return wrap;
54
+ }
55
+
56
+ function onRadioKey(event, group, id) {
57
+ var keys = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 };
58
+ var radios = Array.prototype.slice.call(group.querySelectorAll('[role="radio"]'));
59
+ var index = radios.indexOf(document.activeElement);
60
+ if (index < 0) return;
61
+ var next = -1;
62
+ if (keys[event.key]) next = (index + keys[event.key] + radios.length) % radios.length;
63
+ else if (event.key === 'Home') next = 0;
64
+ else if (event.key === 'End') next = radios.length - 1;
65
+ if (next < 0) return;
66
+ event.preventDefault();
67
+ radios[next].focus();
68
+ chooseValue(id, radios[next].dataset.value);
69
+ }
70
+
71
+ function buildCheckbox(setting) {
72
+ var id = setting.id;
73
+ var wrap = make('div', 'ab-setting ab-nested'); wrap.dataset.setting = id;
74
+ var label = make('label', 'ab-check');
75
+ var input = document.createElement('input'); input.type = 'checkbox'; input.id = 'ab-i-' + id;
76
+ input.setAttribute('aria-describedby', 'ab-d-' + id + ' ab-c-' + id + ' ab-n-' + id);
77
+ input.addEventListener('change', function () { chooseValue(id, input.checked); });
78
+ label.appendChild(input); label.appendChild(make('span', 'ab-check-text', setting.label));
79
+ var desc = make('p', 'settings-hint ab-desc', setting.description); desc.id = 'ab-d-' + id;
80
+ var inactive = make('p', 'ab-inactive'); inactive.id = 'ab-n-' + id;
81
+ var row = make('div', 'ab-control-row'); row.appendChild(statusBox(id));
82
+ var consequence = make('p', 'ab-consequence'); consequence.id = 'ab-c-' + id;
83
+ var example = make('p', 'ab-example');
84
+ example.appendChild(make('span', 'ab-example-label', 'Example'));
85
+ example.appendChild(make('span', 'ab-example-text', setting.example));
86
+ [label, desc, inactive, row, consequence, example].forEach(function (node) { wrap.appendChild(node); });
87
+ return wrap;
88
+ }
89
+
90
+ function buildStatic(state, scroller) {
91
+ var frag = make('div', 'ab-ready');
92
+ var note = make('div', 'ab-note');
93
+ note.appendChild(make('p', '', NOTE));
94
+ var layout = make('div', 'ab-layout');
95
+ var navEl = make('nav', 'ab-nav'); navEl.setAttribute('aria-label', 'Agent behavior sections');
96
+ var list = make('ul', 'ab-nav-list');
97
+ var main = make('div', 'ab-sections');
98
+ var sections = [];
99
+ state.catalog.sections.forEach(function (section) {
100
+ var item = make('li'); var link = make('a', 'ab-nav-link', section.label);
101
+ link.href = '#ab-sec-' + section.id; link.dataset.section = section.id; item.appendChild(link); list.appendChild(item);
102
+ var el = make('section', 'ab-section'); el.id = 'ab-sec-' + section.id;
103
+ var heading = make('h3', 'ab-section-title', section.label); heading.id = 'ab-h-' + section.id; heading.tabIndex = -1;
104
+ el.setAttribute('aria-labelledby', heading.id);
105
+ el.appendChild(heading); el.appendChild(make('p', 'settings-hint ab-section-desc', section.description));
106
+ state.catalog.settings.forEach(function (setting) {
107
+ if (setting.section !== section.id || setting.type !== 'mode') return;
108
+ var block = buildMode(setting, state); el.appendChild(block);
109
+ state.catalog.settings.forEach(function (child) {
110
+ if (child.type === 'checkbox' && child.parent === setting.id) block.appendChild(buildCheckbox(child));
111
+ });
112
+ });
113
+ main.appendChild(el); sections.push({ id: section.id, el: el, heading: heading });
114
+ });
115
+ navEl.appendChild(list); layout.appendChild(main);
116
+ frag.appendChild(note); frag.appendChild(layout);
117
+ return { root: frag, layout: layout, nav: navEl, sections: sections, catalog: state.catalog, scroller: scroller };
118
+ }
119
+
120
+ // Desktop: the section links sit under the active page in the shell's left
121
+ // navigation. Narrow: they become a sticky row at the top of the content.
122
+ function placeNav(narrow) {
123
+ if (!built) return;
124
+ if (narrow) { setPageSubnav(null, null); built.layout.insertBefore(built.nav, built.layout.firstChild); }
125
+ else setPageSubnav('us-agent', built.nav);
126
+ }
127
+
128
+ function syncStatus(box, id, state, input) {
129
+ var status = statusFor(state, id);
130
+ box.dataset.state = status.kind;
131
+ if (status.kind === 'error') {
132
+ if (box.dataset.shown !== status.text) {
133
+ box.replaceChildren();
134
+ box.appendChild(make('span', 'ab-status-text', status.text));
135
+ var retry = make('button', 'us-btn us-btn-secondary ab-retry', 'Retry'); retry.type = 'button';
136
+ retry.addEventListener('click', function () {
137
+ retryValue(id);
138
+ var target = input || host.querySelector('[data-setting="' + id + '"] [role="radio"][tabindex="0"]');
139
+ if (target) target.focus();
140
+ });
141
+ box.appendChild(retry);
142
+ }
143
+ box.dataset.shown = status.text;
144
+ return;
145
+ }
146
+ delete box.dataset.shown;
147
+ if (box.querySelector('.ab-retry')) box.replaceChildren();
148
+ setText(box, status.text);
149
+ }
150
+
151
+ function syncMode(setting, state) {
152
+ var wrap = built.root.querySelector('[data-setting="' + setting.id + '"]');
153
+ var value = displayedValue(state, setting.id);
154
+ wrap.querySelectorAll('[role="radio"]').forEach(function (radio) {
155
+ var on = radio.dataset.value === value;
156
+ radio.setAttribute('aria-checked', on ? 'true' : 'false');
157
+ radio.tabIndex = on ? 0 : -1;
158
+ radio.classList.toggle('selected', on);
159
+ });
160
+ var copy = setting.modes[value] || { consequence: '', example: '' };
161
+ setText(wrap.querySelector('.ab-consequence'), copy.consequence);
162
+ setText(wrap.querySelector('.ab-example-text'), copy.example);
163
+ syncStatus(wrap.querySelector(':scope > .ab-control-row > .ab-status'), setting.id, state, null);
164
+ }
165
+
166
+ function syncCheckbox(setting, state) {
167
+ var wrap = built.root.querySelector('[data-setting="' + setting.id + '"]');
168
+ var info = checkboxStatus(state, setting);
169
+ var input = wrap.querySelector('input');
170
+ if (input.checked !== info.checked) input.checked = info.checked;
171
+ wrap.classList.toggle('ab-inactive-setting', info.inactive);
172
+ setText(wrap.querySelector('.ab-consequence'), info.checked ? setting.checked : setting.unchecked);
173
+ var note = wrap.querySelector('.ab-inactive');
174
+ setText(note, info.reason); note.hidden = !info.reason;
175
+ syncStatus(wrap.querySelector('.ab-status'), setting.id, state, input);
176
+ }
177
+
178
+ function sync(state) {
179
+ if (!host) return;
180
+ var box;
181
+ if (state.status === 'ready') {
182
+ if (!built || built.catalog !== state.catalog) {
183
+ if (nav) { nav.destroy(); nav = null; }
184
+ host.replaceChildren(make('div', 'ab-feedback')); host.appendChild((built = buildStatic(state, host.closest('.settings-shell-content') || host.parentElement)).root);
185
+ built.scroller.scrollTop = 0;
186
+ nav = attachSectionNav({ layout: built.layout, nav: built.nav, scroller: built.scroller, sections: built.sections, onPlace: placeNav });
187
+ }
188
+ delete host.dataset.abShown;
189
+ host.querySelector('.ab-feedback').hidden = true;
190
+ state.catalog.settings.forEach(function (setting) {
191
+ if (setting.type === 'mode') syncMode(setting, state); else syncCheckbox(setting, state);
192
+ });
193
+ return;
194
+ }
195
+ // Keep an already rendered loading or error state (and its Retry control).
196
+ var key = state.status + '|' + (state.error || '');
197
+ if (!built && host.dataset.abShown === key && host.firstChild) return;
198
+ host.dataset.abShown = key;
199
+ if (nav) { nav.destroy(); nav = null; }
200
+ setPageSubnav(null, null);
201
+ built = null; host.replaceChildren(); box = make('div', 'ab-feedback'); host.appendChild(box);
202
+ if (state.status === 'loading' || state.status === 'idle') {
203
+ box.appendChild(make('p', 'settings-status ab-loading', 'Loading agent behavior...'));
204
+ box.firstChild.dataset.state = 'loading'; box.firstChild.setAttribute('role', 'status');
205
+ } else {
206
+ box.appendChild(make('p', 'settings-status ab-load-error', state.error || 'Could not load agent behavior.'));
207
+ box.firstChild.dataset.state = 'error'; box.firstChild.setAttribute('role', 'alert');
208
+ var retry = make('button', 'us-btn us-btn-secondary ab-retry', 'Retry'); retry.type = 'button';
209
+ retry.addEventListener('click', function () { loadAgentBehavior(); });
210
+ box.appendChild(retry);
211
+ }
212
+ }
213
+
214
+ // Mounts into the page container and loads (every entry re-fetches).
215
+ export function mountAgentBehavior(container) {
216
+ // The shell may re-activate the page it is already showing (for example after
217
+ // it refreshes permissions). Keep the mounted view and its in-flight or
218
+ // finished load instead of rebuilding it, which would detach live controls.
219
+ if (host === container && unsubscribe) { sync(getAgentBehavior()); return Promise.resolve(); }
220
+ unmountAgentBehavior();
221
+ host = container; built = null;
222
+ unsubscribe = store.subscribe(function (state, prev) { if (state.agentBehavior !== prev.agentBehavior) sync(getAgentBehavior()); });
223
+ sync(getAgentBehavior());
224
+ return loadAgentBehavior();
225
+ }
226
+
227
+ export function unmountAgentBehavior() {
228
+ if (unsubscribe) { unsubscribe(); unsubscribe = null; }
229
+ setPageSubnav(null, null);
230
+ if (nav) { nav.destroy(); nav = null; }
231
+ if (host) { host.replaceChildren(); delete host.dataset.abShown; }
232
+ host = null; built = null;
233
+ }
234
+
235
+ export function agentBehaviorNav() { return nav; }
@@ -0,0 +1,200 @@
1
+ // agent-behavior.js - state, REST and save correlation for the Personal
2
+ // "Agent behavior" page. The catalog (labels, consequences, examples) always
3
+ // comes from the server; this module only decides what is acknowledged.
4
+ //
5
+ // Saving contract: a choice is shown as "Saving" until the PUT answers 200.
6
+ // A failed save reverts the displayed value to the last acknowledged server
7
+ // value and keeps the failed choice so Retry can resend it. Requests are
8
+ // serialized per setting (newest choice wins) and only the changed key of a
9
+ // reply is merged, so a late reply cannot overwrite a newer choice.
10
+ import { store } from './store.js';
11
+
12
+ var ROUTE = '/api/user/agent-behavior';
13
+ var SAVED_MS = 3500;
14
+ var inflight = {};
15
+ var loadToken = 0;
16
+ var requestCounter = 0;
17
+ var sendSeq = 0;
18
+
19
+ function emptyState() {
20
+ return { status: 'idle', error: '', catalog: null, policy: null, effective: null, edits: {}, saved: {} };
21
+ }
22
+
23
+ export function getAgentBehavior() { return store.get('agentBehavior') || emptyState(); }
24
+ function patch(values) { store.set({ agentBehavior: Object.assign({}, getAgentBehavior(), values) }); }
25
+
26
+ export function findSetting(state, id) {
27
+ var list = state && state.catalog ? state.catalog.settings : [];
28
+ for (var i = 0; i < list.length; i++) if (list[i].id === id) return list[i];
29
+ return null;
30
+ }
31
+
32
+ export function modeLabel(state, value) {
33
+ var modes = state && state.catalog ? state.catalog.modes : [];
34
+ for (var i = 0; i < modes.length; i++) if (modes[i].id === value) return modes[i].label;
35
+ return String(value);
36
+ }
37
+
38
+ // The value the control shows: a pending choice while saving, otherwise the
39
+ // last acknowledged value (a failed choice is never displayed as selected).
40
+ export function displayedValue(state, id) {
41
+ var edit = state.edits[id];
42
+ if (edit && edit.state === 'saving') return edit.value;
43
+ return state.policy ? state.policy[id] : undefined;
44
+ }
45
+
46
+ export function hasPendingEdits(state) {
47
+ return Object.keys(state.edits).some(function (id) { return state.edits[id].state === 'saving'; });
48
+ }
49
+
50
+ // Status line for one setting: { kind: '' | 'saving' | 'saved' | 'error', text }.
51
+ export function statusFor(state, id) {
52
+ var edit = state.edits[id];
53
+ if (edit && edit.state === 'saving') return { kind: 'saving', text: 'Saving...' };
54
+ if (edit && edit.state === 'failed') return { kind: 'error', text: edit.message };
55
+ if (state.saved[id]) return { kind: 'saved', text: 'Saved' };
56
+ return { kind: '', text: '' };
57
+ }
58
+
59
+ // The linked-note checkbox keeps its saved choice but is inactive unless Sticky
60
+ // Notes is Auto and Issues is not Off. Reasons use the displayed values.
61
+ export function checkboxStatus(state, setting) {
62
+ var parent = setting.parent || 'notes';
63
+ var notes = displayedValue(state, parent);
64
+ var issues = displayedValue(state, 'issues');
65
+ var reasons = [];
66
+ if (notes !== 'auto') reasons.push('Sticky Notes is ' + modeLabel(state, notes) + ' (it must be Auto)');
67
+ if (issues === 'off') reasons.push('Issues is Off');
68
+ var checked = displayedValue(state, setting.id) === true;
69
+ if (!reasons.length && checked && !hasPendingEdits(state) && state.effective && state.effective.issueLinkedNotes === false) {
70
+ reasons.push('your account does not currently apply it');
71
+ }
72
+ return {
73
+ checked: checked, inactive: reasons.length > 0,
74
+ reason: reasons.length ? 'Not in effect: ' + reasons.join(' and ') + '. Your choice is kept.' : ''
75
+ };
76
+ }
77
+
78
+ function validPayload(data) {
79
+ return !!(data && data.ok === true && data.policy && typeof data.policy === 'object' && data.catalog &&
80
+ Array.isArray(data.catalog.settings) && Array.isArray(data.catalog.modes) && Array.isArray(data.catalog.sections));
81
+ }
82
+
83
+ function errorText(response, data, fallback) {
84
+ var message = data && typeof data.error === 'string' && data.error ? data.error : '';
85
+ if (response && response.status === 401) return 'You are signed out. Sign in again to continue.';
86
+ return message || fallback;
87
+ }
88
+
89
+ export function loadAgentBehavior() {
90
+ var token = ++loadToken;
91
+ inflight = {};
92
+ patch({ status: 'loading', error: '', edits: {}, saved: {} });
93
+ return fetch(ROUTE, { headers: { Accept: 'application/json' }, cache: 'no-store' }).then(function (response) {
94
+ return response.json().catch(function () { return null; }).then(function (data) { return { response: response, data: data }; });
95
+ }).then(function (result) {
96
+ if (token !== loadToken) return;
97
+ if (!result.response.ok || !validPayload(result.data)) {
98
+ patch({ status: 'error', error: errorText(result.response, result.data, 'Could not load agent behavior.') });
99
+ return;
100
+ }
101
+ patch({ status: 'ready', error: '', catalog: result.data.catalog, policy: result.data.policy, effective: result.data.effective || null });
102
+ }).catch(function () {
103
+ if (token === loadToken) patch({ status: 'error', error: 'Could not reach the server. Check your connection and try again.' });
104
+ });
105
+ }
106
+
107
+ export function resetAgentBehavior() {
108
+ loadToken++;
109
+ inflight = {};
110
+ store.set({ agentBehavior: emptyState() });
111
+ }
112
+
113
+ function setEdit(id, edit) {
114
+ var state = getAgentBehavior();
115
+ var edits = Object.assign({}, state.edits);
116
+ if (edit) edits[id] = edit; else delete edits[id];
117
+ patch({ edits: edits });
118
+ }
119
+
120
+ function markSaved(id) {
121
+ var state = getAgentBehavior();
122
+ var saved = Object.assign({}, state.saved); saved[id] = true;
123
+ patch({ saved: saved });
124
+ var token = loadToken;
125
+ var timer = setTimeout(function () {
126
+ if (token !== loadToken) return;
127
+ var current = getAgentBehavior();
128
+ if (!current.saved[id]) return;
129
+ var next = Object.assign({}, current.saved); delete next[id];
130
+ patch({ saved: next });
131
+ }, SAVED_MS);
132
+ if (timer && timer.unref) timer.unref();
133
+ }
134
+
135
+ function flush(id) {
136
+ if (inflight[id]) return;
137
+ var state = getAgentBehavior();
138
+ var edit = state.edits[id];
139
+ if (!edit || edit.state !== 'saving') return;
140
+ if (state.policy && state.policy[id] === edit.value) { setEdit(id, null); return; }
141
+ var token = loadToken;
142
+ var sent = edit.value;
143
+ var requestId = 'ab-' + (++requestCounter) + '-' + Math.random().toString(36).slice(2, 8);
144
+ var seq = ++sendSeq;
145
+ inflight[id] = requestId;
146
+ var changes = {}; changes[id] = sent;
147
+ fetch(ROUTE, {
148
+ method: 'PUT', headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
149
+ body: JSON.stringify({ changes: changes, requestId: requestId })
150
+ }).then(function (response) {
151
+ return response.json().catch(function () { return null; }).then(function (data) { return { response: response, data: data }; });
152
+ }).then(function (result) {
153
+ var good = result.response.ok && validPayload(result.data) && (!result.data.requestId || result.data.requestId === requestId) &&
154
+ result.data.policy[id] === sent;
155
+ if (!good) throw new Error(errorText(result.response, result.data, 'Could not save this setting.'));
156
+ return result.data;
157
+ }).then(function (data) {
158
+ if (token !== loadToken) return;
159
+ delete inflight[id];
160
+ var current = getAgentBehavior();
161
+ var policy = Object.assign({}, current.policy); policy[id] = data.policy[id];
162
+ var values = { policy: policy };
163
+ // The server's effective flag is trusted only from the newest request when
164
+ // nothing else is in flight; otherwise it is unknown until the next load.
165
+ values.effective = seq === sendSeq && !Object.keys(inflight).length && data.effective ? data.effective : null;
166
+ patch(values);
167
+ var latest = getAgentBehavior().edits[id];
168
+ if (latest && latest.state === 'saving' && latest.value !== sent) { flush(id); return; }
169
+ setEdit(id, null);
170
+ markSaved(id);
171
+ }).catch(function (error) {
172
+ if (token !== loadToken) return;
173
+ delete inflight[id];
174
+ var latest = getAgentBehavior().edits[id];
175
+ if (latest && latest.state === 'saving' && latest.value !== sent) { flush(id); return; }
176
+ setEdit(id, { state: 'failed', value: sent, message: (error && error.message ? error.message : 'Could not save this setting.') });
177
+ });
178
+ }
179
+
180
+ // Choose a value for one setting. Returns false when it was ignored.
181
+ export function chooseValue(id, value) {
182
+ var state = getAgentBehavior();
183
+ var setting = findSetting(state, id);
184
+ if (state.status !== 'ready' || !setting) return false;
185
+ if (displayedValue(state, id) === value && !(state.edits[id] && state.edits[id].state === 'failed')) return false;
186
+ if (state.policy[id] === value && !inflight[id]) { setEdit(id, null); return false; }
187
+ var saved = Object.assign({}, state.saved); delete saved[id];
188
+ var edits = Object.assign({}, state.edits); edits[id] = { state: 'saving', value: value };
189
+ patch({ edits: edits, saved: saved });
190
+ flush(id);
191
+ return true;
192
+ }
193
+
194
+ export function retryValue(id) {
195
+ var edit = getAgentBehavior().edits[id];
196
+ if (!edit || edit.state !== 'failed') return false;
197
+ setEdit(id, { state: 'saving', value: edit.value });
198
+ flush(id);
199
+ return true;
200
+ }
@@ -11,7 +11,7 @@ export var SETTINGS_SCOPES = [
11
11
  export var PERSONAL_PAGES = [
12
12
  { id: 'us-account', label: 'Account' }, { id: 'us-appearance', label: 'Appearance' },
13
13
  { id: 'us-chat', label: 'Conversation' }, { id: 'us-notifications', label: 'Notifications' },
14
- { id: 'us-ai', label: 'AI preferences' }, { id: 'us-integrations', label: 'Integrations' }
14
+ { id: 'us-ai', label: 'AI preferences' }, { id: 'us-agent', label: 'Agent behavior' }, { id: 'us-integrations', label: 'Integrations' }
15
15
  ];
16
16
 
17
17
  export var PROJECT_PAGES = [