great-cto 3.70.0 → 3.70.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/board/.claude-plugin/plugin.json +1 -1
- package/board/packages/board/public/assets/admin-shell.css +63 -0
- package/board/packages/board/public/assets/admin-shell.js +39 -5
- package/board/packages/board/public/assets/work-view.js +180 -25
- package/board/packages/board/public/index.html +43 -1
- package/board/scripts/codex-pipeline.mjs +5 -1
- package/dist/main.js +7 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "great-cto",
|
|
3
3
|
"description": "You already have the agent. This is everything around it. great_cto runs Claude Code as a pipeline of 72 specialist agents \u2014 an independent model checks each stage before the next builds on it, spending caps refuse rather than warn, and three decisions stay yours: what gets built, how, and whether it ships.",
|
|
4
|
-
"version": "3.70.
|
|
4
|
+
"version": "3.70.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Alexander Velikiy",
|
|
7
7
|
"url": "https://hashnode.com/@Greatcto"
|
|
@@ -50,3 +50,66 @@
|
|
|
50
50
|
#panel-inbox .inbox-page{max-width:1500px}.nav-item:focus-visible,.admin-subtab:focus-visible,.knowledge-file:focus-visible,.work-filter:focus-visible,.work-tab:focus-visible,.work-task-row:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}
|
|
51
51
|
@media(max-width:1100px){.knowledge-layout,.work-cockpit{grid-template-columns:230px minmax(0,1fr)}.admin-metrics{grid-template-columns:1fr 1fr}.admin-metrics section{border-bottom:1px solid var(--border)}}
|
|
52
52
|
@media(max-width:768px){.admin-page,.work-page{padding:16px!important}.knowledge-layout,.work-cockpit{grid-template-columns:minmax(0,1fr)}#knowledge-index{border-right:0;border-bottom:1px solid var(--border);max-height:270px}.work-task-list{border-right:0;border-bottom:1px solid var(--border);max-height:240px;overflow:auto}#knowledge-document,.work-detail{padding:18px}.admin-page-heading{align-items:flex-start}.admin-metrics section{padding:15px 10px}.admin-metrics h2{font-size:23px}.admin-doc-heading{align-items:flex-start;flex-direction:column}#admin-subnav{padding:10px 16px}.admin-table{min-width:530px}.work-tabs{gap:4px}}
|
|
53
|
+
|
|
54
|
+
/* Canvas focus: the selected task owns the content area, not a nested master list. */
|
|
55
|
+
.canvas-task .work-page{max-width:none!important;padding:18px 24px!important}
|
|
56
|
+
.work-list-only .work-task-list{border:1px solid var(--border);border-radius:6px;background:var(--bg-card)}
|
|
57
|
+
.work-list-only .work-task-row{padding:16px 18px;display:grid;grid-template-columns:minmax(180px,2fr) minmax(130px,1fr) minmax(110px,1fr);gap:16px;align-items:center}
|
|
58
|
+
.work-list-only .work-task-row:last-child{border-bottom:0}
|
|
59
|
+
.work-task-header{display:block;margin:0 0 16px;min-width:0}
|
|
60
|
+
.work-task-header h2{font-size:23px;line-height:1.3;letter-spacing:-.5px;margin:12px 0 8px;overflow-wrap:anywhere}
|
|
61
|
+
.work-task-header p{margin:8px 0;font-size:12px;color:var(--text2)}
|
|
62
|
+
.work-task-header button{min-height:40px;padding:8px 12px;border:1px solid var(--border-strong);border-radius:5px;background:var(--bg-card);color:var(--text);font:inherit;cursor:pointer;flex-shrink:0}
|
|
63
|
+
.work-detail-only,.work-workspace{min-width:0}
|
|
64
|
+
.work-detail-only .work-detail{padding:0}
|
|
65
|
+
.canvas-task .work-tabs{margin-bottom:16px}
|
|
66
|
+
.work-execution-flat{padding:0!important;border:0!important;background:transparent!important}
|
|
67
|
+
.work-execution-flat>.execution-graph{padding:0;border:0;margin:12px 0}
|
|
68
|
+
.execution-graph-toolbar{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-bottom:10px}
|
|
69
|
+
.execution-graph-toolbar h3{font-size:15px;margin:0}
|
|
70
|
+
.execution-graph-toolbar .execution-graph-tools{margin:0;gap:6px}
|
|
71
|
+
.execution-graph-tools .gate-btn{width:auto;flex:0 0 auto;min-height:40px;padding:7px 12px;font-size:12px;border:1px solid var(--border-strong);border-radius:5px;background:var(--bg-card);color:var(--text)}
|
|
72
|
+
.canvas-execution .execution-graph-canvas{height:clamp(360px,calc(100dvh - 340px),850px);max-height:none;border:1px solid var(--border-strong);border-radius:6px;overscroll-behavior:contain;background:var(--bg-page);touch-action:pan-x pan-y}
|
|
73
|
+
.canvas-task .execution-node-detail{margin:8px 0;padding:10px}
|
|
74
|
+
.work-execution-flat>details{margin-top:16px;border-top:1px solid var(--border);padding-top:12px}
|
|
75
|
+
.work-execution-flat>details>summary{min-height:40px;cursor:pointer}
|
|
76
|
+
.work-switcher,.work-node-inspector{color:var(--text);background:var(--bg-card);border:1px solid var(--border-strong);border-radius:8px;padding:20px;box-shadow:0 8px 32px rgb(0 0 0 / .18);max-width:calc(100vw - 24px);max-height:calc(100dvh - 32px);overflow:auto;overflow-wrap:anywhere}
|
|
77
|
+
.work-switcher{width:590px}
|
|
78
|
+
.work-switcher::backdrop{background:rgb(0 0 0 / .35)}
|
|
79
|
+
.work-switcher input{display:block;box-sizing:border-box;width:100%;padding:10px;margin:10px 0;border:1px solid var(--border-strong);background:var(--bg-page);color:var(--text);font:inherit;border-radius:5px}
|
|
80
|
+
.work-switcher button,.work-node-inspector button{min-height:40px;padding:8px 12px;background:var(--bg-card);color:var(--text);border:1px solid var(--border-strong);border-radius:5px;font:inherit;cursor:pointer}
|
|
81
|
+
.work-switcher [data-work-select]{display:block;width:100%;text-align:left;margin-top:8px}
|
|
82
|
+
.work-switcher h2,.work-node-inspector h2{font-size:18px;margin:0 0 12px}
|
|
83
|
+
.work-node-inspector{position:fixed;inset:260px 14px 14px auto;margin:0;width:360px;max-height:calc(100dvh - 274px);z-index:25}
|
|
84
|
+
.work-node-inspector .execution-node-detail{border:0;padding:0}
|
|
85
|
+
.work-node-inspector .execution-node-detail summary{overflow-wrap:anywhere;line-height:1.6}
|
|
86
|
+
.work-node-inspector dd code{white-space:normal;overflow-wrap:anywhere}
|
|
87
|
+
.work-node-inspector [data-graph-close]{float:right}
|
|
88
|
+
.canvas-nav-toggle{display:none;align-items:center;justify-content:center;min-width:40px;min-height:40px;border:1px solid var(--border-strong);border-radius:5px;background:var(--bg-card);color:var(--text);cursor:pointer}
|
|
89
|
+
@media(min-width:769px){
|
|
90
|
+
.canvas-execution .canvas-nav-toggle{display:inline-flex}
|
|
91
|
+
.canvas-execution:not(.canvas-navigation-expanded) .sidebar{width:64px;overflow:visible}
|
|
92
|
+
.canvas-execution:not(.canvas-navigation-expanded) .sidebar-brand{justify-content:center;padding:14px 10px}
|
|
93
|
+
.canvas-execution:not(.canvas-navigation-expanded) .brand-name,.canvas-execution:not(.canvas-navigation-expanded) #build-version,.canvas-execution:not(.canvas-navigation-expanded) .sw-meta,.canvas-execution:not(.canvas-navigation-expanded) .proj-switch>.chev,.canvas-execution:not(.canvas-navigation-expanded) .nav-label,.canvas-execution:not(.canvas-navigation-expanded) .sidebar-footer #live-label,.canvas-execution:not(.canvas-navigation-expanded) .sidebar .nav-section,.canvas-execution:not(.canvas-navigation-expanded) #nav-views-label,.canvas-execution:not(.canvas-navigation-expanded) .nav-views{display:none!important}
|
|
94
|
+
.canvas-execution:not(.canvas-navigation-expanded) .proj-switch{justify-content:center;padding:14px 8px}
|
|
95
|
+
.canvas-execution:not(.canvas-navigation-expanded) .proj-menu{left:54px;width:320px;top:0}
|
|
96
|
+
.canvas-execution:not(.canvas-navigation-expanded) #nav-tablist{padding:8px}
|
|
97
|
+
.canvas-execution:not(.canvas-navigation-expanded) #nav-tablist .nav-item{justify-content:center;position:relative;min-height:44px;padding:10px;overflow:visible}
|
|
98
|
+
.canvas-execution:not(.canvas-navigation-expanded) #nav-tablist .count{display:none}
|
|
99
|
+
.canvas-execution:not(.canvas-navigation-expanded) #nav-tablist .nav-item:is(:hover,:focus-visible)::after{content:attr(data-nav-label);position:absolute;left:50px;top:4px;z-index:50;white-space:nowrap;padding:8px 12px;border:1px solid var(--border-strong);border-radius:5px;background:var(--bg-card);color:var(--text);box-shadow:0 3px 12px rgb(0 0 0 / .12);pointer-events:none}
|
|
100
|
+
.canvas-execution:not(.canvas-navigation-expanded) .sidebar-footer{justify-content:center;padding:12px 8px}
|
|
101
|
+
}
|
|
102
|
+
@media(max-width:768px){
|
|
103
|
+
.canvas-task .work-page{padding:16px!important}
|
|
104
|
+
.work-task-header h2{font-size:20px}
|
|
105
|
+
.work-list-only .work-task-row{display:block;line-height:1.6}
|
|
106
|
+
.work-list-only .work-task-row small{margin-top:6px}
|
|
107
|
+
.work-node-inspector{inset:auto 12px 12px 12px;width:auto;max-height:65dvh}
|
|
108
|
+
.canvas-execution .execution-graph-canvas{height:55dvh;min-height:300px}
|
|
109
|
+
.canvas-execution .topbar .search-box{display:none}
|
|
110
|
+
.execution-graph-tools .gate-btn{min-height:44px}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.work-task-navigation{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.work-task-navigation>.work-meta{margin-right:auto}.canvas-task .topbar .btn-new{display:none}
|
|
114
|
+
|
|
115
|
+
.work-page .execution-graph-tools .gate-btn{background:var(--bg-card);color:var(--text);width:auto;min-height:40px}.work-node-inspector header{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px}.work-node-inspector [data-dialog-close]{flex-shrink:0}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
4
4
|
const aliases = { docs:'knowledge', usage:'insights/consumption', ledger:'insights/ledger', budgets:'insights/ledger', dashboard:'insights/ledger', fleet:'settings/executors', agents:'settings/executors', harness:'settings/runtime', skills:'settings/capabilities', history:'work/completed', inbox:'decisions', notifications:'settings/general', share:'settings/general' };
|
|
5
5
|
function route(input) {
|
|
6
|
+
const taskRoute = window.GctoWork?.parseRoute?.(input);
|
|
7
|
+
if (taskRoute) return { canonical:taskRoute.canonical, panel:'work', primary:'work', workRoute:true };
|
|
6
8
|
if (/^(?:agents|fleet)\/[a-z0-9-]+$/i.test(input)) return {canonical:input,panel:'agents',primary:'settings'};
|
|
7
9
|
const canonical = aliases[input] || input;
|
|
8
10
|
const panels = { work:'work','work/completed':'history',decisions:'inbox',knowledge:'knowledge',insights:'admin-outcomes','insights/outcomes':'admin-outcomes','insights/consumption':'usage','insights/ledger':'budgets',settings:'agents','settings/executors':'agents','settings/runtime':'harness','settings/capabilities':'skills','settings/general':'settings' };
|
|
@@ -76,10 +78,31 @@
|
|
|
76
78
|
});
|
|
77
79
|
} catch(error) {if(current(seq,project)&&docTicket===request) panel('knowledge-document').textContent='Document could not be read: '+error.message;}
|
|
78
80
|
}
|
|
81
|
+
let expandedNavigation = false;
|
|
82
|
+
function setWorkView(view = {}) {
|
|
83
|
+
const detail = !!view.detail && selectedRoute.split('/')[0].split('?')[0] === 'work';
|
|
84
|
+
const execution = detail && view.tab === 'execution';
|
|
85
|
+
document.body.classList.toggle('canvas-task', detail);
|
|
86
|
+
document.body.classList.toggle('canvas-execution', execution);
|
|
87
|
+
document.body.classList.toggle('canvas-navigation-expanded', execution && expandedNavigation);
|
|
88
|
+
const toggle = panel('canvas-nav-toggle');
|
|
89
|
+
if (toggle) { toggle.hidden = !execution; toggle.setAttribute('aria-expanded', String(expandedNavigation)); toggle.setAttribute('aria-label', expandedNavigation ? 'Compact navigation' : 'Expand navigation'); toggle.title = expandedNavigation ? 'Compact navigation' : 'Expand navigation'; }
|
|
90
|
+
const composer = panel('work-compose')?.closest('.admin-compose');
|
|
91
|
+
if (composer) composer.hidden = detail;
|
|
92
|
+
const heading = document.querySelector('.work-page > h1');
|
|
93
|
+
if (heading) heading.hidden = detail;
|
|
94
|
+
}
|
|
95
|
+
function navigateWork(canonical) {
|
|
96
|
+
const parsed = window.GctoWork?.parseRoute?.(canonical);
|
|
97
|
+
if (!parsed) return;
|
|
98
|
+
const hash = '#/' + parsed.canonical;
|
|
99
|
+
if (location.hash !== hash) history.pushState(null, '', hash);
|
|
100
|
+
window.switchTab(parsed.canonical);
|
|
101
|
+
}
|
|
79
102
|
function setup() {
|
|
80
103
|
const nav=panel('nav-tablist'), existing={};
|
|
81
104
|
document.querySelectorAll('.nav-item[data-tab]').forEach(n=>existing[n.dataset.tab]=n.querySelector('svg')?.outerHTML||'');
|
|
82
|
-
nav.innerHTML=['work','decisions','knowledge','insights','settings'].map(key=>`<button type="button" class="nav-item${key==='work'?' active':''}" role="tab" aria-selected="${key==='work'}" data-tab="${key}" tabindex="0" onkeydown="navTabKeydown(event,this)">${existing[key]||existing[key==='knowledge'?'skills':key==='insights'?'usage':'harness']||''}<span class="nav-label">${key[0].toUpperCase()+key.slice(1)}</span>${key==='decisions'?'<span class="count" id="nav-inbox-count">0</span>':''}</button>`).join('');
|
|
105
|
+
nav.innerHTML=['work','decisions','knowledge','insights','settings'].map(key=>`<button type="button" class="nav-item${key==='work'?' active':''}" role="tab" aria-selected="${key==='work'}" data-tab="${key}" aria-label="${key[0].toUpperCase()+key.slice(1)}" data-nav-label="${key[0].toUpperCase()+key.slice(1)}" tabindex="0" onkeydown="navTabKeydown(event,this)">${existing[key]||existing[key==='knowledge'?'skills':key==='insights'?'usage':'harness']||''}<span class="nav-label">${key[0].toUpperCase()+key.slice(1)}</span>${key==='decisions'?'<span class="count" id="nav-inbox-count">0</span>':''}</button>`).join('');
|
|
83
106
|
nav.querySelectorAll('[data-tab]').forEach(n=>n.addEventListener('click',()=>window.switchTab(n.dataset.tab)));
|
|
84
107
|
const tools=panel('tools-nav');tools.hidden=true;
|
|
85
108
|
const workspace=document.querySelector('.workspace');
|
|
@@ -88,6 +111,14 @@
|
|
|
88
111
|
panel('knowledge-reload').addEventListener('click',()=>{generation++;loadKnowledge();});
|
|
89
112
|
const insights=document.createElement('div');insights.id='panel-admin-outcomes';insights.className='panel';insights.setAttribute('role','tabpanel');insights.innerHTML='<div class="admin-page"><header class="admin-page-heading"><div><h1>Insights</h1><p>Recorded outcomes are separate from token consumption.</p></div><button type="button" class="gate-btn" id="outcomes-reload">Reload</button></header><div id="admin-outcomes-body" aria-live="polite"></div></div>';workspace.append(insights);
|
|
90
113
|
panel('outcomes-reload').addEventListener('click',loadTaskOutcomes);
|
|
114
|
+
const toggle = document.createElement('button');
|
|
115
|
+
toggle.id = 'canvas-nav-toggle'; toggle.type = 'button'; toggle.className = 'canvas-nav-toggle'; toggle.hidden = true;
|
|
116
|
+
toggle.setAttribute('aria-controls', 'nav-tablist'); toggle.innerHTML = '<span aria-hidden="true">☰</span>';
|
|
117
|
+
workspace.querySelector('.topbar').prepend(toggle);
|
|
118
|
+
toggle.addEventListener('click', () => { expandedNavigation = !expandedNavigation; setWorkView({detail:true, tab:'execution'}); });
|
|
119
|
+
const projectSwitch = panel('proj-switch');
|
|
120
|
+
projectSwitch.setAttribute('role','button'); projectSwitch.tabIndex = 0; projectSwitch.setAttribute('aria-label','Choose project');
|
|
121
|
+
projectSwitch.addEventListener('keydown', event => { if (event.target === projectSwitch && ['Enter',' '].includes(event.key)) { event.preventDefault(); toggleProjMenu(event); } });
|
|
91
122
|
const original=window.switchTab;
|
|
92
123
|
window.switchTab=function(id,el){
|
|
93
124
|
const agentLink=String(id).match(/^(?:agents|fleet)\/([a-z0-9-]+)$/i);
|
|
@@ -98,16 +129,19 @@
|
|
|
98
129
|
open(0);return;
|
|
99
130
|
}
|
|
100
131
|
const r=route(id);
|
|
101
|
-
if(!r) {bar.hidden=true;return original(id,el);}
|
|
132
|
+
if(!r) {window.GctoWork.leaveView?.();setWorkView({detail:false});bar.hidden=true;return original(id,el);}
|
|
102
133
|
selectedRoute=r.canonical;
|
|
134
|
+
if (r.workRoute) { window.GctoWork.navigateRoute(r.canonical); const input = panel('search-input'); if(input) input.value = window.GctoWork.parseRoute(r.canonical).search; }
|
|
135
|
+
else { window.GctoWork.leaveView?.(); setWorkView({detail:false}); }
|
|
103
136
|
const old={inbox:'decisions',budgets:'ledger',agents:'fleet','admin-outcomes':'admin-outcomes',knowledge:'knowledge'}[r.panel]||r.panel;
|
|
104
|
-
|
|
137
|
+
if (r.workRoute && typeof currentTab === 'string' && currentTab === 'work') { closeMenu(false); window.GctoWork.refresh(); }
|
|
138
|
+
else original(old); // Keep established loaders and mutation guards; task inspection adds no view POST.
|
|
105
139
|
document.querySelectorAll('.nav-item[data-tab]').forEach(n=>{const active=n.closest('#nav-tablist')&&n.dataset.tab===r.primary;n.classList.toggle('active',!!active);n.setAttribute('aria-selected',String(!!active));});
|
|
106
140
|
panel('crumb-here').textContent=r.primary[0].toUpperCase()+r.primary.slice(1);
|
|
107
141
|
const items=r.primary==='settings'?[['settings/executors','Executors'],['settings/runtime','Runtime'],['settings/capabilities','Capabilities'],['settings/general','General']]:r.primary==='insights'?[['insights/outcomes','Outcomes'],['insights/consumption','Consumption'],['insights/ledger','Ledger & budgets']]:[];
|
|
108
142
|
bar.hidden=!items.length;bar.innerHTML=items.map(([key,label])=>`<button type="button" class="admin-subtab" data-route="${key}" aria-pressed="${r.canonical===key||(r.canonical===r.primary&&key===items[0][0])}">${label}</button>`).join('');
|
|
109
143
|
bar.querySelectorAll('[data-route]').forEach(b=>b.onclick=()=>window.switchTab(b.dataset.route));
|
|
110
|
-
history.replaceState(
|
|
144
|
+
history.replaceState(history.state,'','#/'+r.canonical);
|
|
111
145
|
if(r.primary==='knowledge')loadKnowledge();
|
|
112
146
|
if(r.panel==='admin-outcomes')loadTaskOutcomes();
|
|
113
147
|
// Restore a working route after projection errors; an error is not an empty list.
|
|
@@ -117,6 +151,6 @@
|
|
|
117
151
|
composer.querySelector('button[type="submit"]').textContent='Copy launch command';
|
|
118
152
|
window.switchTab((location.hash||'#/work').replace(/^#\/?/,''));
|
|
119
153
|
}
|
|
120
|
-
window.GctoAdmin={route,outcomes,reset,openDocument};
|
|
154
|
+
window.GctoAdmin={route,outcomes,reset,openDocument,setWorkView,navigateWork};
|
|
121
155
|
if(typeof document!=='undefined') setup();
|
|
122
156
|
})();
|
|
@@ -69,26 +69,32 @@
|
|
|
69
69
|
function cockpitEntries(data, category, search) {
|
|
70
70
|
return data.entries.filter(e => inCategory(e, category) && (!search || `${e.title} ${e.key}`.toLowerCase().includes(search.toLowerCase())));
|
|
71
71
|
}
|
|
72
|
-
function renderExecution(entry) {
|
|
72
|
+
function renderExecution(entry, options = {}) {
|
|
73
73
|
const roles = Array.isArray(entry.evidence) ? entry.evidence : [];
|
|
74
74
|
const execution = entry.execution, nodes = Array.isArray(execution?.nodes) ? execution.nodes.slice(0, 200) : [];
|
|
75
|
-
return '<section class="work-execution"><h3>Recorded observations</h3>'
|
|
76
|
-
+ renderExecutionGraph(execution?.graph)
|
|
75
|
+
return (options.canvas ? '<section class="work-execution work-execution-flat">' : '<section class="work-execution"><h3>Recorded observations</h3>')
|
|
76
|
+
+ renderExecutionGraph(execution?.graph, options)
|
|
77
77
|
+ (execution?.graph?.state === 'recorded' ? '' : '<p>Dependencies not recorded. This is an observation list, not an inferred execution graph. Event order does not establish dependencies.</p>')
|
|
78
78
|
+ `<p>Recorded stage: ${esc(entry.stage || 'not recorded')}</p>`
|
|
79
79
|
+ (execution?.reason ? `<p>${esc(execution.reason)}</p>` : '')
|
|
80
80
|
+ (execution?.truncated ? '<p class="work-warning">Showing the latest 200 recorded attempts; earlier observations are truncated.</p>' : execution?.partial ? '<p class="work-warning">Attempt observations are incomplete.</p>' : '')
|
|
81
|
+
+ (options.canvas && entry.inspector?.partial ? '<p class="work-warning">Activity window is incomplete.</p>' : '')
|
|
82
|
+
+ (options.canvas && entry.inspector?.reason ? `<p class="work-warning">${esc(entry.inspector.reason)}</p>` : '')
|
|
83
|
+
+ (options.canvas && !entry.inspector ? '<p class="work-meta">Linked activity unavailable. Absence of events is not proof of inactivity.</p>' : '')
|
|
84
|
+
+ (options.canvas ? '<details class="work-attempt-details"><summary>Recorded observations & technical details</summary>' : '')
|
|
81
85
|
+ (nodes.length ? '<ul class="work-observations" aria-label="Recorded execution attempts">' + nodes.map(n => `<li class="work-observation" data-attempt-id="${esc(n.attemptId)}"><strong>${esc(n.role)} · attempt ${esc(n.number || 'unknown')}</strong>`
|
|
82
86
|
+ `<p>${esc(n.host || 'Host not recorded')} · phase: ${esc(n.phase || 'not recorded')}</p><p>Controller recorded status: ${esc(n.status || 'unknown')}; not current verification or acceptance.</p>`
|
|
83
87
|
+ `<p>Run: ${esc(n.runId)} · Attempt: ${esc(n.attemptId)}</p><p>Started: ${esc(n.startedAt || 'not recorded')} · Finished: ${esc(n.finishedAt || 'not recorded')}</p></li>`).join('') + '</ul>' : '')
|
|
84
88
|
+ (roles.length ? '<ul class="work-observations">' + roles.map(role => `<li class="work-observation">${esc(role.label)} · recorded role observation; acceptance is not inferred</li>`).join('') + '</ul>' : '<p>No role observations recorded.</p>')
|
|
85
89
|
+ (entry.inspector ? renderInspector(entry.inspector).replace('<details ', '<details open ') : '<p>Linked activity unavailable. Absence of events is not proof of inactivity.</p>')
|
|
90
|
+
+ (options.canvas ? '</details>' : '')
|
|
86
91
|
+ '</section>';
|
|
87
92
|
}
|
|
88
|
-
function renderExecutionGraph(graph) {
|
|
93
|
+
function renderExecutionGraph(graph, options = {}) {
|
|
89
94
|
if (graph?.state !== 'recorded' || !Array.isArray(graph.nodes) || !graph.nodes.length)
|
|
90
95
|
return `<p class="work-meta">${esc(graph?.reason || 'Controller-recorded graph unavailable. Native and legacy observations remain partial.')}</p>`;
|
|
91
96
|
if (graph.nodes.length > 512 || !Array.isArray(graph.edges) || graph.edges.length > 2048) return '<p>Graph exceeds the display limit.</p>';
|
|
97
|
+
const horizontal = options.canvas && options.direction !== 'tb';
|
|
92
98
|
const nodes = graph.nodes, ids = new Map(nodes.map((n, i) => [n.id, i]));
|
|
93
99
|
if (ids.size !== nodes.length || graph.edges.some(e => !ids.has(e.from) || !ids.has(e.to) || ids.get(e.from) >= ids.get(e.to)))
|
|
94
100
|
return '<p>Graph dependencies cannot be displayed safely.</p>';
|
|
@@ -100,17 +106,18 @@
|
|
|
100
106
|
const level = Math.max(0, ...parents.get(n.id).map(e => levels.get(e.from) + 1));
|
|
101
107
|
levels.set(n.id, level);
|
|
102
108
|
const row = rows.get(level) || 0; rows.set(level, row + 1);
|
|
103
|
-
positions.set(n.id, { x: 24 + row * 236, y: 24 + level * 112 });
|
|
109
|
+
positions.set(n.id, horizontal ? { x: 24 + level * 272, y: 24 + row * 112 } : { x: 24 + row * 236, y: 24 + level * 112 });
|
|
104
110
|
}
|
|
105
|
-
const width = Math.max(500, Math.max(...rows.values()) * 236 + 24), height = (Math.max(...levels.values()) + 1) * 112 + 24;
|
|
111
|
+
const width = horizontal ? (Math.max(...levels.values()) + 1) * 272 : Math.max(500, Math.max(...rows.values()) * 236 + 24), height = horizontal ? Math.max(240, Math.max(...rows.values()) * 112 + 24) : (Math.max(...levels.values()) + 1) * 112 + 24;
|
|
106
112
|
const edges = graph.edges.map(e => {
|
|
107
113
|
const a = positions.get(e.from), b = positions.get(e.to);
|
|
114
|
+
if (horizontal) return `<path d="M${a.x + 208} ${a.y + 39} C${a.x + 240} ${a.y + 39},${b.x - 32} ${b.y + 39},${b.x} ${b.y + 39}" marker-end="url(#execution-arrow)"><title>${esc(e.kind)}: ${esc(e.from)} to ${esc(e.to)}</title></path>`;
|
|
108
115
|
return `<path d="M${a.x + 104} ${a.y + 78} C${a.x + 104} ${a.y + 98},${b.x + 104} ${b.y - 20},${b.x + 104} ${b.y}" marker-end="url(#execution-arrow)"><title>${esc(e.kind)}: ${esc(e.from)} to ${esc(e.to)}</title></path>`;
|
|
109
116
|
}).join('');
|
|
110
117
|
const short = value => String(value || '').length > 27 ? String(value).slice(0, 26) + '…' : String(value || '');
|
|
111
118
|
const boxes = nodes.map(n => {
|
|
112
119
|
const p = positions.get(n.id);
|
|
113
|
-
return `<g class="execution-node" role="button" tabindex="0" data-graph-node="${esc(n.id)}" aria-label="${esc(n.kind)} ${esc(n.role || '')}: ${esc(n.status)}. Open recorded details." transform="translate(${p.x},${p.y})"
|
|
120
|
+
return `<g class="execution-node" role="button" tabindex="0" data-graph-node="${esc(n.id)}" aria-label="${esc(n.kind)} ${esc(n.role || '')}: ${esc(n.status)}. Open recorded details." transform="translate(${p.x},${p.y})">${options.canvas ? `<title>${esc(n.kind)} · ${esc(n.role || 'controller')} · ${esc(n.status)}</title>` : ''}<rect width="208" height="78" rx="6"/><text x="12" y="23">${esc(short(n.role || n.kind))}</text><text x="12" y="45">${options.canvas ? esc(short(`${n.kind} · ${n.status}`)) : `${esc(n.kind)} · ${esc(n.status)}`}</text><text x="12" y="64">${esc(n.host || 'controller')}</text></g>`;
|
|
114
121
|
}).join('');
|
|
115
122
|
const details = nodes.map(n => `<details class="execution-node-detail" data-graph-detail="${esc(n.id)}"><summary>${esc(n.id)} · ${esc(n.kind)} · ${esc(n.role || 'controller')} · ${esc(n.status)}</summary><dl>`
|
|
116
123
|
+ [['Attempt', n.attemptId], ['Host', n.host], ['Recorded at', n.at], ['Verdict', n.verdict], ['Wave', n.waveId], ['Gates', (n.gates || []).join(', ')],
|
|
@@ -121,25 +128,30 @@
|
|
|
121
128
|
return `<section class="execution-graph" aria-label="Controller-recorded execution graph"><h3>Controller-recorded graph</h3><p>${nodes.length} nodes · ${graph.edges.length} recorded edges · ${graph.snapshots?.length || 0} workflow snapshots. ${graph.complete ? 'Controller history recorded from run creation.' : 'Partial history: missing edges are not inferred.'}</p>`
|
|
122
129
|
+ `<p class="work-meta">Dispatch, input, verification, gate and join edges describe controller observations. Intent is not proof of a live worker; recorded approval is not deployment or task acceptance. Historical reuse does not invoke a worker. Stage context digests exclude separately admitted domain packets.</p>`
|
|
123
130
|
+ (graph.reason ? `<p class="work-warning">${esc(graph.reason)}</p>` : '')
|
|
124
|
-
+ `<div class="execution-graph-tools" role="group" aria-label="Graph zoom"><button type="button" class="gate-btn" data-graph-scale="0.75">Zoom out</button><button type="button" class="gate-btn" data-graph-scale="1.333333">Zoom in</button><button type="button" class="gate-btn" data-graph-scale="reset">Reset zoom</button
|
|
125
|
-
+ `<div class="execution-graph-canvas" tabindex="0" aria-label="Scrollable graph. Tab to a node and press Enter for details."><svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="group" aria-label="Recorded execution dependencies"><defs><marker id="execution-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0 0 L8 4 L0 8"/></marker></defs><g class="execution-edges" fill="none">${edges}</g>${boxes}</svg></div>`
|
|
126
|
-
+
|
|
131
|
+
+ `<div class="execution-graph-tools execution-graph-toolbar" role="group" aria-label="Graph zoom"><button type="button" class="gate-btn" data-graph-scale="0.75">Zoom out</button><button type="button" class="gate-btn" data-graph-scale="1.333333">Zoom in</button><button type="button" class="gate-btn" data-graph-scale="reset">Reset zoom</button>${options.canvas ? `<button type="button" class="gate-btn" data-graph-scale="fit">Fit graph</button><button type="button" class="gate-btn" data-graph-direction="lr" aria-pressed="${horizontal}">Left to right</button><button type="button" class="gate-btn" data-graph-direction="tb" aria-pressed="${!horizontal}">Top to bottom</button>` : ''}</div>`
|
|
132
|
+
+ `<div class="execution-graph-canvas" tabindex="0" aria-label="Scrollable graph. Tab to a node and press Enter for details."><svg data-graph-layout="${horizontal ? 'lr' : 'tb'}" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="group" aria-label="Recorded execution dependencies"><defs><marker id="execution-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0 0 L8 4 L0 8"/></marker></defs><g class="execution-edges" fill="none">${edges}</g>${boxes}</svg></div>`
|
|
133
|
+
+ (options.canvas ? '<details class="execution-node-list"><summary>Recorded node details · keyboard list</summary>' : '<h3>Recorded node details</h3>') + `<p class="work-meta">Use the graph or expand a node below. Digests bind recorded inputs, not correctness.</p>${details}${options.canvas ? '</details>' : ''}</section>`;
|
|
127
134
|
}
|
|
128
135
|
function graphInteraction(target) {
|
|
136
|
+
const direction = target.closest('[data-graph-direction]');
|
|
137
|
+
if (config?.canvas && ['lr', 'tb'].includes(direction?.dataset.graphDirection)) { graphDirection = direction.dataset.graphDirection; closeWorkDialog(false); paint(); document.querySelector?.('[data-graph-direction][aria-pressed="true"]')?.focus(); return true; }
|
|
129
138
|
const node = target.closest('[data-graph-node]');
|
|
130
139
|
if (node?.dataset.graphNode) {
|
|
131
140
|
const detail = [...document.querySelectorAll('[data-graph-detail]')].find(el => el.dataset.graphDetail === node.dataset.graphNode);
|
|
132
|
-
if (detail
|
|
141
|
+
if (detail && config?.canvas) openNodeInspector(detail, node);
|
|
142
|
+
else if (detail) { detail.open = true; detail.querySelector('summary')?.focus(); detail.scrollIntoView({ block: 'nearest' }); }
|
|
133
143
|
return true;
|
|
134
144
|
}
|
|
135
145
|
const scale = target.closest('[data-graph-scale]');
|
|
136
146
|
if (!scale?.dataset.graphScale) return false;
|
|
137
147
|
const svg = scale.closest('.execution-graph').querySelector('svg'), bounds = svg.viewBox.baseVal;
|
|
138
|
-
const
|
|
148
|
+
const fit = scale.dataset.graphScale === 'fit' ? Math.min(1, Math.max(0.05, (scale.closest('.execution-graph').querySelector('.execution-graph-canvas').clientWidth - 2) / bounds.width)) : null;
|
|
149
|
+
const next = fit ?? (scale.dataset.graphScale === 'reset' ? 1 : Math.min(2, Math.max(0.25, Number(svg.dataset.zoom || 1) * Number(scale.dataset.graphScale))));
|
|
139
150
|
svg.dataset.zoom = String(next); svg.setAttribute('width', String(bounds.width * next)); svg.setAttribute('height', String(bounds.height * next));
|
|
140
151
|
return true;
|
|
141
152
|
}
|
|
142
153
|
function renderCockpit(data, options = {}) {
|
|
154
|
+
if (options.canvas) return renderWorkspace(data, options);
|
|
143
155
|
const category = categories.some(([key]) => key === options.category) ? options.category : 'active';
|
|
144
156
|
const tab = ['overview', 'execution', 'evidence'].includes(options.tab) ? options.tab : 'overview';
|
|
145
157
|
const rows = cockpitEntries(data, category, options.search || '');
|
|
@@ -159,6 +171,55 @@
|
|
|
159
171
|
+ `<div class="work-cockpit">${list}<section class="work-detail" aria-label="Selected task details">${tabs}<div id="work-detail-panel" role="tabpanel" aria-labelledby="work-tab-${tab}" tabindex="0">${detail}</div></section></div>`
|
|
160
172
|
+ (data.sessions.length ? `<details><summary>Claude sessions (${data.sessions.length}) · task linkage not recorded</summary>${data.sessions.map(s => `<p>${esc(s.session)} · ${esc(s.state)} · ${esc(s.reason || '')}</p>`).join('')}</details>` : '');
|
|
161
173
|
}
|
|
174
|
+
const taskTabs = ['overview', 'execution', 'evidence'];
|
|
175
|
+
function parseRoute(input) {
|
|
176
|
+
if (typeof input !== 'string' || input.length > 4096 || /[\u0000-\u001f\u007f]/.test(input)) return null;
|
|
177
|
+
const [path, query, extra] = input.split('?');
|
|
178
|
+
if (extra !== undefined) return null;
|
|
179
|
+
const params = new URLSearchParams(query || '');
|
|
180
|
+
if ([...params.keys()].some(key => !['category', 'search'].includes(key) || params.getAll(key).length !== 1)) return null;
|
|
181
|
+
const category = params.get('category') || (path === 'work/completed' ? 'completed' : 'active'), search = params.get('search') || '';
|
|
182
|
+
if (!categories.some(([key]) => key === category) || search.length > 1000 || /[\u0000-\u001f\u007f]/.test(search)) return null;
|
|
183
|
+
let selectedKey = null, tab = 'overview';
|
|
184
|
+
if (path !== 'work' && path !== 'work/completed') {
|
|
185
|
+
const match = path.match(/^work\/task\/([^/]+)\/(overview|execution|evidence)$/);
|
|
186
|
+
if (!match) return null;
|
|
187
|
+
try { selectedKey = decodeURIComponent(match[1]); } catch { return null; }
|
|
188
|
+
if (!selectedKey || selectedKey.length > 512 || /[\u0000-\u001f\u007f]/.test(selectedKey)) return null;
|
|
189
|
+
tab = match[2];
|
|
190
|
+
}
|
|
191
|
+
return { canonical: workspaceRoute(selectedKey, tab, category, search), selectedKey, tab, category, search };
|
|
192
|
+
}
|
|
193
|
+
function workspaceRoute(key, selectedTab, selectedCategory, search) {
|
|
194
|
+
const params = new URLSearchParams();
|
|
195
|
+
if (selectedCategory !== 'active') params.set('category', selectedCategory);
|
|
196
|
+
if (search) params.set('search', search);
|
|
197
|
+
return (key ? `work/task/${encodeURIComponent(key)}/${selectedTab}` : 'work') + (params.size ? '?' + params : '');
|
|
198
|
+
}
|
|
199
|
+
function taskRows(rows, selected = null) {
|
|
200
|
+
return rows.map(e => `<button type="button" class="work-task-row" data-work-select="${esc(e.key)}" aria-current="${e.key === selected}"><strong>${esc(e.title)}</strong><span>${esc(phaseLabel(e))}</span><small>${esc(e.host || 'Host not linked')} · ${esc(e.stage || 'Stage not recorded')}</small></button>`).join('');
|
|
201
|
+
}
|
|
202
|
+
function renderWorkspace(data, options) {
|
|
203
|
+
const category = categories.some(([key]) => key === options.category) ? options.category : 'active';
|
|
204
|
+
const tab = taskTabs.includes(options.tab) ? options.tab : 'overview';
|
|
205
|
+
const rows = cockpitEntries(data, category, options.search || '');
|
|
206
|
+
const feedback = data.sources.filter(s => ['degraded', 'unavailable'].includes(s.health)).map(s => `<p class="work-warning">${esc(s.id)}: ${esc(s.reason || s.health)}</p>`).join('');
|
|
207
|
+
const observation = `<p class="work-meta">Observed ${esc(data.observedAt)} · ${data.health === 'current' ? 'Sources read' : 'Sources incomplete'}. Recorded activity is not proof of current liveness or task acceptance.</p>`;
|
|
208
|
+
if (!options.selectedKey) {
|
|
209
|
+
const toolbar = '<div class="work-filters" role="group" aria-label="Task status filters">' + categories.map(([key, label]) => `<button type="button" class="work-filter" data-work-filter="${key}" aria-pressed="${category === key}">${label} <span>${cockpitEntries(data, key, options.search || '').length}</span></button>`).join('') + '</div>';
|
|
210
|
+
return `<section class="work-workspace work-list-only" aria-label="Project work list">${toolbar}${observation}${feedback}` + (rows.length ? `<div class="work-task-list" role="group" aria-label="Project tasks">${taskRows(rows)}</div>` : `<p>${data.health !== 'current' || feedback ? 'No readable entries match this view; sources are incomplete.' : options.search ? 'No entries match the search.' : 'No work records match this filter.'}</p>`) + '</section>';
|
|
211
|
+
}
|
|
212
|
+
const selected = data.entries.find(e => e.key === options.selectedKey);
|
|
213
|
+
const back = '<button type="button" class="work-back" data-work-back>← Work</button>';
|
|
214
|
+
if (!selected) return `<section class="work-workspace work-detail-only">${back}${observation}${feedback}<h2>Selected task is unavailable</h2><p>The requested task is not in the readable records for this project. Another task will not be substituted.</p></section>`;
|
|
215
|
+
const tabs = '<div class="work-tabs" role="tablist" aria-label="Selected task">' + taskTabs.map(name => `<button type="button" class="work-tab" id="work-tab-${name}" data-work-tab="${name}" role="tab" aria-selected="${tab === name}" aria-controls="work-detail-panel" tabindex="${tab === name ? 0 : -1}">${name[0].toUpperCase() + name.slice(1)}</button>`).join('') + '</div>';
|
|
216
|
+
const detail = tab === 'execution' ? renderExecution(selected, { canvas: true, direction: options.direction }) : tab === 'evidence'
|
|
217
|
+
? (Array.isArray(selected.evidenceSections) && selected.evidenceSections.length ? renderEvidence(selected.evidenceSections).replace('<details ', '<details open ') : '<p>Evidence unavailable. No verification or acceptance is inferred.</p>')
|
|
218
|
+
: render({ ...data, sources: [], sessions: [], entries: [{ ...selected, inspector: null, evidenceSections: null }] }, selected.terminal);
|
|
219
|
+
const runLabel = selected.title.match(/^Controlled run ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i);
|
|
220
|
+
const title = runLabel ? `Controlled run <small class="work-meta work-run-identifier">${esc(runLabel[1].slice(0, 8))}</small>` : esc(selected.title);
|
|
221
|
+
return `<section class="work-workspace work-detail-only" data-selected-work="${esc(selected.key)}"><header class="work-task-header"><div class="work-task-navigation">${back}<span class="work-meta">${esc(selected.kind)} · ${esc(selected.host || 'Host not linked')}</span><button type="button" data-work-switch>Switch task</button><button type="button" data-work-previous${rows.findIndex(e => e.key === selected.key) > 0 ? '' : ' disabled'} aria-label="Previous task in current filter">Previous</button><button type="button" data-work-next${rows.findIndex(e => e.key === selected.key) >= 0 && rows.findIndex(e => e.key === selected.key) < rows.length - 1 ? '' : ' disabled'} aria-label="Next task in current filter">Next</button></div><h2>${title}</h2><p>${esc(phaseLabel(selected))}</p></header>${tabs}${feedback}${tab === 'overview' ? '' : observation}<div id="work-detail-panel" role="tabpanel" aria-labelledby="work-tab-${tab}" tabindex="0">${detail}</div></section>`;
|
|
222
|
+
}
|
|
162
223
|
function renderEvidence(sections) {
|
|
163
224
|
if (!Array.isArray(sections)) return '';
|
|
164
225
|
return '<details data-work-detail="evidence"><summary>Task evidence · read-only</summary>'
|
|
@@ -207,40 +268,125 @@
|
|
|
207
268
|
status('Draft PR publication recorded. Merge and release remain separate decisions.'); await refresh();
|
|
208
269
|
} catch (error) { if (project === config.project()) { status(error.message); await refresh(); } }
|
|
209
270
|
}
|
|
210
|
-
let config, snapshot, generation = 0, filter = '', selectedKey = null, category = 'active', tab = 'overview';
|
|
271
|
+
let config, snapshot, snapshotProject = null, generation = 0, filter = '', selectedKey = null, category = 'active', tab = 'overview';
|
|
272
|
+
let graphDirection = 'lr';
|
|
273
|
+
let activeDialog = null, dialogOpener = null, dialogReturn = null, dialogEscape = null, pendingPaint = false, listPosition = null;
|
|
211
274
|
const status = message => { document.getElementById('work-feedback').textContent = message; };
|
|
275
|
+
function notifyView() { config?.onViewChange?.({ detail: !!selectedKey, tab }); }
|
|
276
|
+
function navigateRoute(input) {
|
|
277
|
+
const route = parseRoute(input);
|
|
278
|
+
if (!route) return false;
|
|
279
|
+
closeWorkDialog(false);
|
|
280
|
+
const wasDetail = !!selectedKey;
|
|
281
|
+
if (selectedKey !== route.selectedKey) graphDirection = 'lr';
|
|
282
|
+
selectedKey = route.selectedKey; tab = route.tab; category = route.category; filter = route.search;
|
|
283
|
+
if (snapshot && snapshotProject !== config?.project()) { snapshot = null; previews.clear(); document.getElementById('work-list').innerHTML = ''; document.getElementById('work-list').textContent = 'Loading selected project…'; }
|
|
284
|
+
notifyView(); paint();
|
|
285
|
+
if (wasDetail && !selectedKey) restoreListPosition();
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
function rememberListPosition() {
|
|
289
|
+
const list = document.getElementById('work-list'), scroller = list.closest?.('.panel');
|
|
290
|
+
listPosition = { project: config.project(), category, filter, key: selectedKey, top: scroller?.scrollTop || 0 };
|
|
291
|
+
}
|
|
292
|
+
function restoreListPosition() {
|
|
293
|
+
if (!listPosition || listPosition.project !== config.project() || listPosition.category !== category || listPosition.filter !== filter) return;
|
|
294
|
+
const list = document.getElementById('work-list'), scroller = list.closest?.('.panel');
|
|
295
|
+
if (scroller) scroller.scrollTop = listPosition.top;
|
|
296
|
+
[...document.querySelectorAll('[data-work-select]')].find(n => n.dataset.workSelect === listPosition.key)?.focus({ preventScroll: true });
|
|
297
|
+
}
|
|
298
|
+
function changeWorkspace() {
|
|
299
|
+
closeWorkDialog(false); notifyView(); paint();
|
|
300
|
+
config.navigate?.(workspaceRoute(selectedKey, tab, category, filter));
|
|
301
|
+
}
|
|
302
|
+
function closeWorkDialog(restore = true) {
|
|
303
|
+
if (!activeDialog) return;
|
|
304
|
+
const dialog = activeDialog, opener = dialogOpener, returnTo = dialogReturn;
|
|
305
|
+
activeDialog = null; dialogOpener = null; dialogReturn = null;
|
|
306
|
+
if (dialogEscape) { document.removeEventListener('keydown', dialogEscape, true); dialogEscape = null; }
|
|
307
|
+
if (dialog.open) dialog.close();
|
|
308
|
+
dialog.remove();
|
|
309
|
+
if (pendingPaint) { pendingPaint = false; paint(); }
|
|
310
|
+
if (!restore || !returnTo || returnTo.project !== config.project() || returnTo.task !== selectedKey) return;
|
|
311
|
+
const replacement = returnTo.node
|
|
312
|
+
? [...document.querySelectorAll('[data-graph-node]')].find(node => node.dataset.graphNode === returnTo.node)
|
|
313
|
+
: returnTo.switcher ? document.querySelectorAll('[data-work-switch]')[0] : null;
|
|
314
|
+
if (opener?.isConnected) opener.focus();
|
|
315
|
+
else replacement?.focus();
|
|
316
|
+
}
|
|
317
|
+
function leaveView() { pendingPaint = false; closeWorkDialog(false); }
|
|
318
|
+
function makeWorkDialog(className, title, opener, content, modal) {
|
|
319
|
+
closeWorkDialog(false);
|
|
320
|
+
const dialog = document.createElement('dialog'); dialog.className = className;
|
|
321
|
+
dialog.setAttribute('aria-label', title);
|
|
322
|
+
dialog.innerHTML = `<header><h2>${esc(title)}</h2><button type="button" data-dialog-close aria-label="Close ${esc(title)}">Close</button></header>${content}`;
|
|
323
|
+
document.body.append(dialog); activeDialog = dialog; dialogOpener = opener;
|
|
324
|
+
dialogReturn = { project: config.project(), task: selectedKey, node: opener?.dataset?.graphNode || null, switcher: Object.prototype.hasOwnProperty.call(opener?.dataset || {}, 'workSwitch') };
|
|
325
|
+
dialogEscape = event => { if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); closeWorkDialog(); } };
|
|
326
|
+
document.addEventListener('keydown', dialogEscape, true);
|
|
327
|
+
dialog.addEventListener('cancel', event => { event.preventDefault(); closeWorkDialog(); });
|
|
328
|
+
dialog.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); closeWorkDialog(); } });
|
|
329
|
+
dialog.addEventListener('click', event => { if (event.target.closest('[data-dialog-close]')) closeWorkDialog(); });
|
|
330
|
+
dialog.addEventListener('close', () => { if (activeDialog === dialog) closeWorkDialog(); });
|
|
331
|
+
if (modal) dialog.showModal(); else dialog.show();
|
|
332
|
+
return dialog;
|
|
333
|
+
}
|
|
334
|
+
function openTaskSwitcher(opener) {
|
|
335
|
+
const rows = cockpitEntries(snapshot, category, filter);
|
|
336
|
+
const dialog = makeWorkDialog('work-switcher', 'Switch task', opener, '<label>Find a task<input type="search" data-switch-search placeholder="Title or task ID"></label><p class="work-meta">Tasks in the current project and list filter.</p><div data-switch-rows>' + taskRows(rows, selectedKey) + '</div><p data-switch-empty hidden>No matching task.</p>', true);
|
|
337
|
+
const input = dialog.querySelector('[data-switch-search]'); input.focus();
|
|
338
|
+
input.addEventListener('input', () => { let count = 0; dialog.querySelectorAll('[data-work-select]').forEach(button => { const entry = rows.find(e => e.key === button.dataset.workSelect); button.hidden = !`${entry.title} ${entry.key}`.toLowerCase().includes(input.value.toLowerCase()); if (!button.hidden) count++; }); dialog.querySelector('[data-switch-empty]').hidden = count > 0; });
|
|
339
|
+
dialog.addEventListener('click', event => { const button = event.target.closest('[data-work-select]'); if (button && snapshot?.entries.some(e => e.key === button.dataset.workSelect) && snapshotProject === config.project()) { selectedKey = button.dataset.workSelect; tab = 'overview'; changeWorkspace(); document.querySelector('[data-work-tab]')?.focus(); } });
|
|
340
|
+
}
|
|
341
|
+
function openNodeInspector(detail, opener) {
|
|
342
|
+
document.querySelectorAll('[data-graph-detail][open]').forEach(node => { node.open = false; });
|
|
343
|
+
const content = detail.outerHTML.replace('<details ', '<details open ');
|
|
344
|
+
const dialog = makeWorkDialog('work-node-inspector', 'Recorded node details', opener, '<p class="work-meta">Recorded inputs, not proof of correctness, current activity or acceptance.</p>' + content, false);
|
|
345
|
+
dialog.querySelector('summary')?.focus();
|
|
346
|
+
}
|
|
212
347
|
async function refresh() {
|
|
213
348
|
const seq = ++generation, project = config.project();
|
|
214
349
|
try {
|
|
215
350
|
const data = await config.read(`/api/work${project ? '?project=' + encodeURIComponent(project) : ''}`);
|
|
216
351
|
if (seq !== generation || project !== config.project()) return;
|
|
217
352
|
if (!data || data.error || data.schemaVersion !== 1) throw Error(data?.error || 'Cannot read work state. Check the board connection and refresh.');
|
|
218
|
-
if (snapshot?.revision === data.revision) return;
|
|
219
|
-
snapshot = data;
|
|
353
|
+
if (snapshotProject === project && snapshot?.revision === data.revision) return;
|
|
354
|
+
snapshot = data; snapshotProject = project;
|
|
355
|
+
if (config.canvas && activeDialog?.open) { pendingPaint = true; previews.clear(); } else paint();
|
|
220
356
|
} catch (error) {
|
|
221
357
|
if (seq !== generation || project !== config.project()) return;
|
|
222
|
-
snapshot = null;
|
|
358
|
+
snapshot = null; snapshotProject = null; pendingPaint = false; closeWorkDialog(false);
|
|
223
359
|
for (const id of ['work-list', 'history-list']) document.getElementById(id).textContent = error.message;
|
|
224
360
|
}
|
|
225
361
|
}
|
|
226
362
|
function paint() {
|
|
227
|
-
if (!snapshot) return;
|
|
363
|
+
if (!snapshot || (config?.canvas && snapshotProject !== config.project())) return;
|
|
228
364
|
previews.clear(); // Repaint removes the reviewed diff and its confirmation controls.
|
|
229
365
|
const expanded = [...document.querySelectorAll('.work-card details[open]')].map(d => `${d.closest('[data-entry-key]').dataset.entryKey}:${d.dataset.workDetail}`);
|
|
230
366
|
const focused = document.activeElement;
|
|
231
367
|
const focusKey = focused?.dataset?.workKey, focusAction = focused?.dataset?.workAction;
|
|
232
368
|
const focusSelect = focused?.dataset?.workSelect, focusFilter = focused?.dataset?.workFilter, focusTab = focused?.dataset?.workTab;
|
|
233
|
-
|
|
369
|
+
const focusNode = focused?.dataset?.graphNode;
|
|
370
|
+
const oldCanvas = config.canvas ? document.querySelector?.('.execution-graph-canvas') : null;
|
|
371
|
+
const oldSvg = oldCanvas?.querySelector('svg'), oldZoom = oldSvg ? Number(oldSvg.dataset.zoom || 1) : 1;
|
|
372
|
+
const graphScroll = oldCanvas?.closest('[data-selected-work]')?.dataset.selectedWork === selectedKey && oldSvg?.dataset.graphLayout === graphDirection ? { left: oldCanvas.scrollLeft, top: oldCanvas.scrollTop } : null;
|
|
373
|
+
document.getElementById('work-list').innerHTML = renderCockpit(snapshot, { search: filter, selectedKey, category, tab, canvas: !!config.canvas, direction: graphDirection });
|
|
234
374
|
document.getElementById('history-list').innerHTML = render(snapshot, true, filter);
|
|
375
|
+
if (graphScroll) {
|
|
376
|
+
const canvas = document.querySelector?.('.execution-graph-canvas'), svg = canvas?.querySelector('svg');
|
|
377
|
+
if (svg && Number.isFinite(oldZoom) && oldZoom > 0) { const bounds = svg.viewBox.baseVal; svg.dataset.zoom = String(oldZoom); svg.setAttribute('width', String(bounds.width * oldZoom)); svg.setAttribute('height', String(bounds.height * oldZoom)); canvas.scrollLeft = graphScroll.left; canvas.scrollTop = graphScroll.top; }
|
|
378
|
+
}
|
|
235
379
|
for (const card of document.querySelectorAll('[data-entry-key]')) for (const detail of card.querySelectorAll('details'))
|
|
236
380
|
if (expanded.includes(`${card.dataset.entryKey}:${detail.dataset.workDetail}`)) detail.open = true;
|
|
237
381
|
if (focusKey) [...document.querySelectorAll('[data-work-key]')].find(b => b.dataset.workKey === focusKey && b.dataset.workAction === focusAction)?.focus();
|
|
238
382
|
else if (focusSelect) [...document.querySelectorAll('[data-work-select]')].find(b => b.dataset.workSelect === focusSelect)?.focus();
|
|
239
383
|
else if (focusFilter) [...document.querySelectorAll('[data-work-filter]')].find(b => b.dataset.workFilter === focusFilter)?.focus();
|
|
240
384
|
else if (focusTab) [...document.querySelectorAll('[data-work-tab]')].find(b => b.dataset.workTab === focusTab)?.focus();
|
|
385
|
+
else if (focusNode) [...document.querySelectorAll('[data-graph-node]')].find(n => n.dataset.graphNode === focusNode)?.focus({ preventScroll: true });
|
|
241
386
|
}
|
|
242
387
|
function reset() {
|
|
243
|
-
|
|
388
|
+
closeWorkDialog(false); pendingPaint = false; listPosition = null; graphDirection = 'lr';
|
|
389
|
+
generation++; snapshotProject = null; snapshot = null; previews.clear(); selectedKey = null; category = 'active'; tab = 'overview'; filter = ''; status(''); notifyView();
|
|
244
390
|
for (const id of ['work-list', 'history-list']) document.getElementById(id).textContent = 'Loading project work…';
|
|
245
391
|
refresh();
|
|
246
392
|
}
|
|
@@ -262,16 +408,24 @@
|
|
|
262
408
|
document.getElementById('work-command').textContent = ''; status('');
|
|
263
409
|
});
|
|
264
410
|
for (const id of ['work-list', 'history-list']) document.getElementById(id).addEventListener('click', event => {
|
|
411
|
+
if (config.canvas && snapshotProject !== config.project()) return;
|
|
265
412
|
if (graphInteraction(event.target)) return;
|
|
266
|
-
if (!snapshot) return;
|
|
413
|
+
if (!snapshot || (config.canvas && snapshotProject !== config.project())) return;
|
|
414
|
+
if (config.canvas) {
|
|
415
|
+
if (event.target.closest('[data-work-back]')) { selectedKey = null; tab = 'overview'; changeWorkspace(); restoreListPosition(); return; }
|
|
416
|
+
const switcher = event.target.closest('[data-work-switch]');
|
|
417
|
+
if (switcher) { openTaskSwitcher(switcher); return; }
|
|
418
|
+
const direction = event.target.closest('[data-work-previous]') ? -1 : event.target.closest('[data-work-next]') ? 1 : 0;
|
|
419
|
+
if (direction) { const rows = cockpitEntries(snapshot, category, filter), index = rows.findIndex(e => e.key === selectedKey), next = index >= 0 ? rows[index + direction] : null; if (next) { selectedKey = next.key; tab = 'overview'; changeWorkspace(); } return; }
|
|
420
|
+
}
|
|
267
421
|
const button = event.target.closest('[data-work-action]');
|
|
268
422
|
if (!button?.dataset.workAction) {
|
|
269
423
|
const selection = event.target.closest('[data-work-select]');
|
|
270
424
|
const filterButton = event.target.closest('[data-work-filter]');
|
|
271
425
|
const tabButton = event.target.closest('[data-work-tab]');
|
|
272
|
-
if (selection?.dataset.workSelect && snapshot.entries.some(e => e.key === selection.dataset.workSelect)) { selectedKey = selection.dataset.workSelect; tab = 'overview'; paint(); }
|
|
273
|
-
else if (categories.some(([key]) => key === filterButton?.dataset.workFilter)) { category = filterButton.dataset.workFilter; selectedKey = null; tab = 'overview'; paint(); }
|
|
274
|
-
else if (['overview', 'execution', 'evidence'].includes(tabButton?.dataset.workTab)) { tab = tabButton.dataset.workTab; paint(); }
|
|
426
|
+
if (selection?.dataset.workSelect && snapshot.entries.some(e => e.key === selection.dataset.workSelect)) { if (config.canvas) { rememberListPosition(); listPosition.key = selection.dataset.workSelect; } selectedKey = selection.dataset.workSelect; tab = 'overview'; if (config.canvas) changeWorkspace(); else paint(); }
|
|
427
|
+
else if (categories.some(([key]) => key === filterButton?.dataset.workFilter)) { category = filterButton.dataset.workFilter; selectedKey = null; tab = 'overview'; if (config.canvas) changeWorkspace(); else paint(); }
|
|
428
|
+
else if (['overview', 'execution', 'evidence'].includes(tabButton?.dataset.workTab)) { tab = tabButton.dataset.workTab; if (config.canvas) changeWorkspace(); else paint(); }
|
|
275
429
|
return;
|
|
276
430
|
}
|
|
277
431
|
const entry = snapshot.entries.find(e => e.key === button.dataset.workKey);
|
|
@@ -283,6 +437,7 @@
|
|
|
283
437
|
else options.openIssue(entry.issueIds[0], action.action === 'review_decision');
|
|
284
438
|
});
|
|
285
439
|
document.getElementById('work-list').addEventListener('keydown', event => {
|
|
440
|
+
if (config.canvas && snapshotProject !== config.project()) return;
|
|
286
441
|
if (['Enter', ' '].includes(event.key) && event.target.closest('[data-graph-node]')) { event.preventDefault(); graphInteraction(event.target); return; }
|
|
287
442
|
const button = event.target.closest('[data-work-tab]');
|
|
288
443
|
if (!button || !['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
|
@@ -290,10 +445,10 @@
|
|
|
290
445
|
if (current < 0) return;
|
|
291
446
|
event.preventDefault();
|
|
292
447
|
tab = event.key === 'Home' ? tabs[0] : event.key === 'End' ? tabs[2] : tabs[(current + (event.key === 'ArrowRight' ? 1 : 2)) % 3];
|
|
293
|
-
paint();
|
|
448
|
+
if (config.canvas) changeWorkspace(); else paint();
|
|
294
449
|
[...document.querySelectorAll('[data-work-tab]')].find(b => b.dataset.workTab === tab)?.focus();
|
|
295
450
|
});
|
|
296
451
|
setInterval(() => { if (!document.hidden && options.visible()) refresh(); }, 10000);
|
|
297
452
|
}
|
|
298
|
-
window.GctoWork = { bind, refresh, reset, render, renderCockpit, renderExecutionGraph, commandFor, publicationCommand, getSnapshot() { return snapshot; }, showCompleted() { category = 'completed'; selectedKey = null; tab = 'overview'; paint(); }, search(value) { filter = value; paint(); } };
|
|
453
|
+
window.GctoWork = { bind, refresh, reset, leaveView, parseRoute, navigateRoute, render, renderCockpit, renderExecutionGraph, commandFor, publicationCommand, getSnapshot() { return snapshot; }, showCompleted() { category = 'completed'; selectedKey = null; tab = 'overview'; notifyView(); paint(); }, search(value) { filter = value; if (config?.canvas) { if (!selectedKey) changeWorkspace(); } else paint(); } };
|
|
299
454
|
})();
|
|
@@ -4139,6 +4139,9 @@ let metrics = {};
|
|
|
4139
4139
|
let shareState = { enabled: false, url: null };
|
|
4140
4140
|
let currentTab = 'work';
|
|
4141
4141
|
window.GctoWork.bind({
|
|
4142
|
+
canvas: true,
|
|
4143
|
+
navigate: canonical => window.GctoAdmin?.navigateWork(canonical),
|
|
4144
|
+
onViewChange: view => window.GctoAdmin?.setWorkView(view),
|
|
4142
4145
|
project: () => new URLSearchParams(location.search).get('project') || '',
|
|
4143
4146
|
visible: () => currentTab === 'work' || currentTab === 'history',
|
|
4144
4147
|
read: url => api(url),
|
|
@@ -7815,6 +7818,7 @@ async function switchProject(slug) {
|
|
|
7815
7818
|
resetProjectUsage();
|
|
7816
7819
|
if (currentTab === 'usage') loadUsage();
|
|
7817
7820
|
if (currentTab === 'skills') loadSkillsInventory();
|
|
7821
|
+
if (window.GctoWork.parseRoute((location.hash || '').replace(/^#\/?/, ''))?.selectedKey) history.replaceState(history.state, '', '#/work');
|
|
7818
7822
|
window.GctoWork.reset();
|
|
7819
7823
|
// Optimistic UI clear
|
|
7820
7824
|
allTasks = [];
|
|
@@ -7882,6 +7886,7 @@ function filterTasks(q) {
|
|
|
7882
7886
|
function onSearch(q) {
|
|
7883
7887
|
window.GctoWork.search(q);
|
|
7884
7888
|
searchQuery = q;
|
|
7889
|
+
if (currentTab === 'work' || currentTab === 'history') return;
|
|
7885
7890
|
renderKanban(filterTasks(q));
|
|
7886
7891
|
document.getElementById('topbar-task-count').textContent =
|
|
7887
7892
|
`${filterTasks(q).length} ${q ? 'MATCH' : 'ISSUES'}`;
|
|
@@ -8025,6 +8030,14 @@ async function init() {
|
|
|
8025
8030
|
|| fromTaskId
|
|
8026
8031
|
|| 'project';
|
|
8027
8032
|
serverDefaultProject = !urlProject && !(allProjects || []).some(p => p.slug === proj);
|
|
8033
|
+
// Registered default projects get an explicit scope before history entries
|
|
8034
|
+
// are created. Keep the unregistered server-default fallback unqualified.
|
|
8035
|
+
if (!urlProject && !serverDefaultProject) {
|
|
8036
|
+
const scopedURL = new URL(location.href); scopedURL.searchParams.set('project', proj);
|
|
8037
|
+
history.replaceState(history.state, '', scopedURL.toString());
|
|
8038
|
+
const workRoute = (location.hash || '#/work').replace(/^#\/?/, '');
|
|
8039
|
+
if (window.GctoWork.parseRoute(workRoute)) window.GctoWork.navigateRoute(workRoute);
|
|
8040
|
+
}
|
|
8028
8041
|
currentProject = proj;
|
|
8029
8042
|
resetProjectUsage();
|
|
8030
8043
|
if (currentTab === 'usage') loadUsage();
|
|
@@ -9432,7 +9445,36 @@ function routeFromHash() {
|
|
|
9432
9445
|
}
|
|
9433
9446
|
return ROUTES[h] ? h : (LEGACY_ROUTES[h] || null);
|
|
9434
9447
|
}
|
|
9435
|
-
|
|
9448
|
+
let routeReplay = 0, lastRouteReplay = '';
|
|
9449
|
+
async function replayBoardRoute() {
|
|
9450
|
+
if (location.href === lastRouteReplay) return;
|
|
9451
|
+
lastRouteReplay = location.href;
|
|
9452
|
+
const ticket = ++routeReplay;
|
|
9453
|
+
let project = new URLSearchParams(location.search).get('project') || '';
|
|
9454
|
+
const expectedHash = location.hash, expectedURL = location.href;
|
|
9455
|
+
if (!project && typeof currentProject !== 'undefined' && currentProject) {
|
|
9456
|
+
// A legacy unqualified history entry means the server default, not the
|
|
9457
|
+
// project whose records happen to be in memory. Clear it before resolving.
|
|
9458
|
+
window.GctoWork.navigateRoute('work');
|
|
9459
|
+
const defaults = await api('/api/work');
|
|
9460
|
+
if (ticket !== routeReplay || location.href !== expectedURL) return;
|
|
9461
|
+
project = defaults?.projectName || '';
|
|
9462
|
+
if (!project || !(allProjects || []).some(item => item.slug === project)) { location.reload(); return; }
|
|
9463
|
+
const scopedURL = new URL(location.href); scopedURL.searchParams.set('project', project);
|
|
9464
|
+
history.replaceState(history.state, '', scopedURL.toString());
|
|
9465
|
+
}
|
|
9466
|
+
if (project && typeof currentProject !== 'undefined' && currentProject && project !== currentProject) {
|
|
9467
|
+
const loading = switchProject(project);
|
|
9468
|
+
history.replaceState(history.state, '', expectedHash || '#/work');
|
|
9469
|
+
await loading;
|
|
9470
|
+
if (ticket !== routeReplay || project !== new URLSearchParams(location.search).get('project')) return;
|
|
9471
|
+
history.replaceState(history.state, '', expectedHash || '#/work');
|
|
9472
|
+
}
|
|
9473
|
+
if (ticket !== routeReplay) return;
|
|
9474
|
+
const r = routeFromHash(); if (r) switchTab(r);
|
|
9475
|
+
}
|
|
9476
|
+
addEventListener('hashchange', replayBoardRoute);
|
|
9477
|
+
addEventListener('popstate', replayBoardRoute);
|
|
9436
9478
|
addEventListener('DOMContentLoaded', () => { const r = routeFromHash(); if (r && r !== 'work') switchTab(r); });
|
|
9437
9479
|
|
|
9438
9480
|
async function refreshLogs() {
|
|
@@ -50,7 +50,11 @@ try {
|
|
|
50
50
|
console.log(JSON.stringify(result, null, 2)); process.exit(result.state === 'ready' ? 0 : 2);
|
|
51
51
|
}
|
|
52
52
|
if (command === 'list') {
|
|
53
|
-
|
|
53
|
+
const output = JSON.stringify(listCodexRuns({ root: value('--dir') ? realpathSync(resolve(value('--dir'))) : null, store }), null, 2) + '\n';
|
|
54
|
+
// Piped stdout is asynchronous: exiting before the callback can truncate JSON.
|
|
55
|
+
await new Promise((resolve, reject) => {
|
|
56
|
+
process.stdout.write(output, error => error ? reject(error) : resolve());
|
|
57
|
+
});
|
|
54
58
|
process.exit(0);
|
|
55
59
|
}
|
|
56
60
|
let state;
|
package/dist/main.js
CHANGED
|
@@ -1680,7 +1680,13 @@ async function main() {
|
|
|
1680
1680
|
if (args.command === "run" || args.command === "status" || args.command === "resume") {
|
|
1681
1681
|
const { runDaily } = await import('./run.js');
|
|
1682
1682
|
const dailyArgs = [...(args.hostInput !== undefined ? ['--host', args.hostInput] : []), ...(args.taskArgs ?? [])];
|
|
1683
|
-
|
|
1683
|
+
const code = runDaily(args.command, dailyArgs, { cwd: args.dir });
|
|
1684
|
+
if (args.command === 'status') {
|
|
1685
|
+
await new Promise((resolve, reject) => {
|
|
1686
|
+
process.stdout.write('', error => error ? reject(error) : resolve());
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
await finish(code);
|
|
1684
1690
|
}
|
|
1685
1691
|
if (args.command === "serve") {
|
|
1686
1692
|
try {
|
package/package.json
CHANGED