@tinyviber/a2h 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +229 -0
- package/bin/a2h.js +7 -0
- package/dist/actions/engine.d.ts +40 -0
- package/dist/actions/engine.d.ts.map +1 -0
- package/dist/actions/engine.js +227 -0
- package/dist/actions/engine.js.map +1 -0
- package/dist/actions/policy.d.ts +8 -0
- package/dist/actions/policy.d.ts.map +1 -0
- package/dist/actions/policy.js +55 -0
- package/dist/actions/policy.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +258 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +8 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +69 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/dist/ir/build.d.ts +8 -0
- package/dist/ir/build.d.ts.map +1 -0
- package/dist/ir/build.js +397 -0
- package/dist/ir/build.js.map +1 -0
- package/dist/ir/enrich.d.ts +21 -0
- package/dist/ir/enrich.d.ts.map +1 -0
- package/dist/ir/enrich.js +177 -0
- package/dist/ir/enrich.js.map +1 -0
- package/dist/ir/identity.d.ts +3 -0
- package/dist/ir/identity.d.ts.map +1 -0
- package/dist/ir/identity.js +91 -0
- package/dist/ir/identity.js.map +1 -0
- package/dist/ir/scoring.d.ts +7 -0
- package/dist/ir/scoring.d.ts.map +1 -0
- package/dist/ir/scoring.js +88 -0
- package/dist/ir/scoring.js.map +1 -0
- package/dist/parsers/code.d.ts +3 -0
- package/dist/parsers/code.d.ts.map +1 -0
- package/dist/parsers/code.js +17 -0
- package/dist/parsers/code.js.map +1 -0
- package/dist/parsers/diff.d.ts +11 -0
- package/dist/parsers/diff.d.ts.map +1 -0
- package/dist/parsers/diff.js +125 -0
- package/dist/parsers/diff.js.map +1 -0
- package/dist/parsers/json.d.ts +3 -0
- package/dist/parsers/json.d.ts.map +1 -0
- package/dist/parsers/json.js +16 -0
- package/dist/parsers/json.js.map +1 -0
- package/dist/parsers/log.d.ts +3 -0
- package/dist/parsers/log.d.ts.map +1 -0
- package/dist/parsers/log.js +105 -0
- package/dist/parsers/log.js.map +1 -0
- package/dist/parsers/markdown.d.ts +15 -0
- package/dist/parsers/markdown.d.ts.map +1 -0
- package/dist/parsers/markdown.js +89 -0
- package/dist/parsers/markdown.js.map +1 -0
- package/dist/parsers/text.d.ts +24 -0
- package/dist/parsers/text.d.ts.map +1 -0
- package/dist/parsers/text.js +98 -0
- package/dist/parsers/text.js.map +1 -0
- package/dist/presentation/blocks.d.ts +14 -0
- package/dist/presentation/blocks.d.ts.map +1 -0
- package/dist/presentation/blocks.js +87 -0
- package/dist/presentation/blocks.js.map +1 -0
- package/dist/presentation/present.d.ts +12 -0
- package/dist/presentation/present.d.ts.map +1 -0
- package/dist/presentation/present.js +115 -0
- package/dist/presentation/present.js.map +1 -0
- package/dist/providers/mock.d.ts +3 -0
- package/dist/providers/mock.d.ts.map +1 -0
- package/dist/providers/mock.js +171 -0
- package/dist/providers/mock.js.map +1 -0
- package/dist/providers/registry.d.ts +12 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +20 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/types.d.ts +64 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +40 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/renderer/assets/index.html +41 -0
- package/dist/renderer/assets/js/actions.js +205 -0
- package/dist/renderer/assets/js/api.js +75 -0
- package/dist/renderer/assets/js/artifacts.js +422 -0
- package/dist/renderer/assets/js/blocks.js +301 -0
- package/dist/renderer/assets/js/bus.js +21 -0
- package/dist/renderer/assets/js/cards.js +137 -0
- package/dist/renderer/assets/js/dom.js +61 -0
- package/dist/renderer/assets/js/format.js +118 -0
- package/dist/renderer/assets/js/main.js +125 -0
- package/dist/renderer/assets/js/nav.js +80 -0
- package/dist/renderer/assets/js/router.js +64 -0
- package/dist/renderer/assets/js/store.js +69 -0
- package/dist/renderer/assets/js/views.js +356 -0
- package/dist/renderer/assets/styles.css +856 -0
- package/dist/renderer/content.d.ts +11 -0
- package/dist/renderer/content.d.ts.map +1 -0
- package/dist/renderer/content.js +114 -0
- package/dist/renderer/content.js.map +1 -0
- package/dist/scanner/classify.d.ts +17 -0
- package/dist/scanner/classify.d.ts.map +1 -0
- package/dist/scanner/classify.js +117 -0
- package/dist/scanner/classify.js.map +1 -0
- package/dist/scanner/git.d.ts +3 -0
- package/dist/scanner/git.d.ts.map +1 -0
- package/dist/scanner/git.js +43 -0
- package/dist/scanner/git.js.map +1 -0
- package/dist/scanner/ignore.d.ts +22 -0
- package/dist/scanner/ignore.d.ts.map +1 -0
- package/dist/scanner/ignore.js +57 -0
- package/dist/scanner/ignore.js.map +1 -0
- package/dist/scanner/scan.d.ts +8 -0
- package/dist/scanner/scan.d.ts.map +1 -0
- package/dist/scanner/scan.js +108 -0
- package/dist/scanner/scan.js.map +1 -0
- package/dist/security/boundary.d.ts +19 -0
- package/dist/security/boundary.d.ts.map +1 -0
- package/dist/security/boundary.js +59 -0
- package/dist/security/boundary.js.map +1 -0
- package/dist/security/urlPolicy.d.ts +4 -0
- package/dist/security/urlPolicy.d.ts.map +1 -0
- package/dist/security/urlPolicy.js +56 -0
- package/dist/security/urlPolicy.js.map +1 -0
- package/dist/security/workspaceRead.d.ts +25 -0
- package/dist/security/workspaceRead.d.ts.map +1 -0
- package/dist/security/workspaceRead.js +103 -0
- package/dist/security/workspaceRead.js.map +1 -0
- package/dist/semantics/load.d.ts +11 -0
- package/dist/semantics/load.d.ts.map +1 -0
- package/dist/semantics/load.js +537 -0
- package/dist/semantics/load.js.map +1 -0
- package/dist/semantics/resolve.d.ts +37 -0
- package/dist/semantics/resolve.d.ts.map +1 -0
- package/dist/semantics/resolve.js +211 -0
- package/dist/semantics/resolve.js.map +1 -0
- package/dist/semantics/types.d.ts +158 -0
- package/dist/semantics/types.d.ts.map +1 -0
- package/dist/semantics/types.js +3 -0
- package/dist/semantics/types.js.map +1 -0
- package/dist/server/server.d.ts +17 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +510 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/workspace.d.ts +53 -0
- package/dist/server/workspace.d.ts.map +1 -0
- package/dist/server/workspace.js +211 -0
- package/dist/server/workspace.js.map +1 -0
- package/dist/types.d.ts +570 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/dist/util/path.d.ts +11 -0
- package/dist/util/path.d.ts.map +1 -0
- package/dist/util/path.js +35 -0
- package/dist/util/path.js.map +1 -0
- package/dist/util/safeRead.d.ts +38 -0
- package/dist/util/safeRead.d.ts.map +1 -0
- package/dist/util/safeRead.js +117 -0
- package/dist/util/safeRead.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Entry point: boot the viewer, own the chrome, dispatch routes.
|
|
2
|
+
//
|
|
3
|
+
// Note there is no bundler and no framework here on purpose — A2H ships as a
|
|
4
|
+
// local CLI, so the viewer is plain ES modules served from disk. Extensibility
|
|
5
|
+
// comes from the block registry, not from a build step.
|
|
6
|
+
|
|
7
|
+
import { el, byId } from './dom.js';
|
|
8
|
+
import { loadPresentation, invalidate, openEvents } from './api.js';
|
|
9
|
+
import { getData, setData, subscribe } from './store.js';
|
|
10
|
+
import { parseHash } from './router.js';
|
|
11
|
+
import { renderNav, highlightNav, renderFoot } from './nav.js';
|
|
12
|
+
import {
|
|
13
|
+
overviewView,
|
|
14
|
+
sectionView,
|
|
15
|
+
tasksView,
|
|
16
|
+
taskView,
|
|
17
|
+
runsView,
|
|
18
|
+
actionsView,
|
|
19
|
+
artifactContainer,
|
|
20
|
+
} from './views.js';
|
|
21
|
+
|
|
22
|
+
const nav = byId('nav');
|
|
23
|
+
const content = byId('content');
|
|
24
|
+
const foot = byId('sidebarFoot');
|
|
25
|
+
|
|
26
|
+
async function refresh() {
|
|
27
|
+
invalidate();
|
|
28
|
+
try {
|
|
29
|
+
const data = await loadPresentation(true);
|
|
30
|
+
setData(data);
|
|
31
|
+
renderNav(nav, data);
|
|
32
|
+
renderFoot(foot, data);
|
|
33
|
+
route();
|
|
34
|
+
} catch (err) {
|
|
35
|
+
content.textContent = '';
|
|
36
|
+
content.appendChild(
|
|
37
|
+
el('div', { class: 'notice', text: `Could not load the workspace: ${err.message}` }),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function route() {
|
|
43
|
+
const data = getData();
|
|
44
|
+
if (!data) return;
|
|
45
|
+
const { name, arg } = parseHash(location.hash);
|
|
46
|
+
|
|
47
|
+
let view;
|
|
48
|
+
switch (name) {
|
|
49
|
+
case 'section':
|
|
50
|
+
view = sectionView(arg);
|
|
51
|
+
break;
|
|
52
|
+
case 'artifact':
|
|
53
|
+
view = artifactContainer(arg);
|
|
54
|
+
break;
|
|
55
|
+
case 'tasks':
|
|
56
|
+
view = tasksView();
|
|
57
|
+
break;
|
|
58
|
+
case 'task':
|
|
59
|
+
view = taskView(arg);
|
|
60
|
+
break;
|
|
61
|
+
case 'runs':
|
|
62
|
+
view = runsView();
|
|
63
|
+
break;
|
|
64
|
+
case 'actions':
|
|
65
|
+
view = actionsView();
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
view = overviewView();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
content.textContent = '';
|
|
72
|
+
content.appendChild(view);
|
|
73
|
+
highlightNav(nav, location.hash || '#/');
|
|
74
|
+
closeDrawer();
|
|
75
|
+
window.scrollTo(0, 0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ------------------------------------------------------------------- chrome
|
|
79
|
+
|
|
80
|
+
function closeDrawer() {
|
|
81
|
+
const sidebar = byId('sidebar');
|
|
82
|
+
const scrim = byId('scrim');
|
|
83
|
+
const toggle = byId('navToggle');
|
|
84
|
+
sidebar.classList.remove('open');
|
|
85
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
86
|
+
scrim.hidden = true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function bindChrome() {
|
|
90
|
+
const toggle = byId('navToggle');
|
|
91
|
+
const sidebar = byId('sidebar');
|
|
92
|
+
const scrim = byId('scrim');
|
|
93
|
+
|
|
94
|
+
toggle.addEventListener('click', () => {
|
|
95
|
+
const open = sidebar.classList.toggle('open');
|
|
96
|
+
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
97
|
+
scrim.hidden = !open;
|
|
98
|
+
});
|
|
99
|
+
scrim.addEventListener('click', closeDrawer);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --------------------------------------------------------------------- boot
|
|
103
|
+
|
|
104
|
+
function boot() {
|
|
105
|
+
bindChrome();
|
|
106
|
+
|
|
107
|
+
window.addEventListener('hashchange', route);
|
|
108
|
+
subscribe('refresh', () => {
|
|
109
|
+
void refresh();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Watch mode: the server pushes a change event after a rescan.
|
|
113
|
+
openEvents({
|
|
114
|
+
onChange: () => void refresh(),
|
|
115
|
+
onAction: () => void refresh(),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
void refresh();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (document.readyState === 'loading') {
|
|
122
|
+
document.addEventListener('DOMContentLoaded', boot);
|
|
123
|
+
} else {
|
|
124
|
+
boot();
|
|
125
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Sidebar navigation. The nav mirrors the same information architecture the
|
|
2
|
+
// presentation layer produced: tasks first (they are what a human acts on),
|
|
3
|
+
// then the producer's groups, then inferred sections.
|
|
4
|
+
|
|
5
|
+
import { el } from './dom.js';
|
|
6
|
+
import { statusTone, statusLabel } from './format.js';
|
|
7
|
+
import { hrefFor } from './router.js';
|
|
8
|
+
|
|
9
|
+
export function renderNav(nav, data) {
|
|
10
|
+
nav.textContent = '';
|
|
11
|
+
|
|
12
|
+
nav.appendChild(navLink('Overview', '#/', 'nav-overview'));
|
|
13
|
+
|
|
14
|
+
if (data.tasks.length > 0) {
|
|
15
|
+
const group = el('div', { class: 'nav-section' });
|
|
16
|
+
group.appendChild(navLink('Tasks', '#/tasks', 'nav-section-title-link'));
|
|
17
|
+
const list = el('div', { class: 'nav-artifacts' });
|
|
18
|
+
for (const task of data.tasks) {
|
|
19
|
+
const link = navLink(task.title, hrefFor('task', task.id), 'nav-artifact', task.summary);
|
|
20
|
+
link.prepend(
|
|
21
|
+
el('span', { class: `nav-dot tone-${statusTone(task.status)}`, title: statusLabel(task.status) }),
|
|
22
|
+
);
|
|
23
|
+
if (task.runs.length) link.appendChild(el('span', { class: 'nav-count', text: String(task.runs.length) }));
|
|
24
|
+
list.appendChild(link);
|
|
25
|
+
}
|
|
26
|
+
group.appendChild(list);
|
|
27
|
+
nav.appendChild(group);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (data.runs.length > 0 || data.actions.length > 0) {
|
|
31
|
+
const group = el('div', { class: 'nav-section' });
|
|
32
|
+
if (data.runs.length > 0) group.appendChild(navLink('Runs', '#/runs', 'nav-section-title-link'));
|
|
33
|
+
if (data.actions.length > 0) {
|
|
34
|
+
const link = navLink('Actions', '#/actions', 'nav-section-title-link');
|
|
35
|
+
link.appendChild(el('span', { class: 'nav-count', text: String(data.actions.length) }));
|
|
36
|
+
group.appendChild(link);
|
|
37
|
+
}
|
|
38
|
+
nav.appendChild(group);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const section of data.sections) {
|
|
42
|
+
const group = el('div', { class: 'nav-section' });
|
|
43
|
+
// No "declared" flag here: it repeated on every producer section, crowded
|
|
44
|
+
// the labels into an ellipsis, and the section page already says it once.
|
|
45
|
+
group.appendChild(navLink(section.title, hrefFor('section', section.id), 'nav-section-title-link'));
|
|
46
|
+
|
|
47
|
+
const list = el('div', { class: 'nav-artifacts' });
|
|
48
|
+
for (const artifact of section.artifacts) {
|
|
49
|
+
list.appendChild(
|
|
50
|
+
navLink(artifact.title, hrefFor('artifact', artifact.id), 'nav-artifact', artifact.path || artifact.title),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
group.appendChild(list);
|
|
54
|
+
nav.appendChild(group);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function navLink(label, href, className, title) {
|
|
59
|
+
return el('a', {
|
|
60
|
+
class: `nav-item ${className || ''}`.trim(),
|
|
61
|
+
href,
|
|
62
|
+
title: title || label,
|
|
63
|
+
text: label,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function highlightNav(nav, hash) {
|
|
68
|
+
const current = hash || '#/';
|
|
69
|
+
for (const link of nav.querySelectorAll('.nav-item')) {
|
|
70
|
+
link.classList.toggle('is-active', link.getAttribute('href') === current);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function renderFoot(foot, data) {
|
|
75
|
+
const lines = [];
|
|
76
|
+
if (data.git && data.git.branch) lines.push(`branch ${data.git.branch}`);
|
|
77
|
+
lines.push(`${data.stats.files} file${data.stats.files === 1 ? '' : 's'}`);
|
|
78
|
+
if (data.semantics !== 'inferred') lines.push(`semantics: ${data.semantics}`);
|
|
79
|
+
foot.textContent = lines.join(' · ');
|
|
80
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Hash routing. Three shapes, all readable in a URL:
|
|
2
|
+
//
|
|
3
|
+
// #/ overview
|
|
4
|
+
// #/s/:sectionId a section
|
|
5
|
+
// #/a/:artifactPath an artifact (path is URL-encoded)
|
|
6
|
+
// #/tasks #/t/:id task list / a task
|
|
7
|
+
// #/runs #/actions runs / action surface
|
|
8
|
+
|
|
9
|
+
export function parseHash(hash) {
|
|
10
|
+
const raw = (hash || '#/').replace(/^#\/?/, '');
|
|
11
|
+
if (!raw) return { name: 'overview', arg: '' };
|
|
12
|
+
|
|
13
|
+
const slash = raw.indexOf('/');
|
|
14
|
+
const head = slash >= 0 ? raw.slice(0, slash) : raw;
|
|
15
|
+
const tail = slash >= 0 ? raw.slice(slash + 1) : '';
|
|
16
|
+
|
|
17
|
+
switch (head) {
|
|
18
|
+
case 's':
|
|
19
|
+
return { name: 'section', arg: decode(tail) };
|
|
20
|
+
case 'a':
|
|
21
|
+
return { name: 'artifact', arg: decode(tail) };
|
|
22
|
+
case 'tasks':
|
|
23
|
+
return { name: 'tasks', arg: '' };
|
|
24
|
+
case 't':
|
|
25
|
+
return { name: 'task', arg: decode(tail) };
|
|
26
|
+
case 'runs':
|
|
27
|
+
return { name: 'runs', arg: '' };
|
|
28
|
+
case 'actions':
|
|
29
|
+
return { name: 'actions', arg: '' };
|
|
30
|
+
default:
|
|
31
|
+
return { name: 'overview', arg: '' };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function hrefFor(name, arg) {
|
|
36
|
+
switch (name) {
|
|
37
|
+
case 'section':
|
|
38
|
+
return `#/s/${encodeURIComponent(arg)}`;
|
|
39
|
+
case 'artifact':
|
|
40
|
+
return `#/a/${encodeURIComponent(arg)}`;
|
|
41
|
+
case 'task':
|
|
42
|
+
return `#/t/${encodeURIComponent(arg)}`;
|
|
43
|
+
case 'tasks':
|
|
44
|
+
return '#/tasks';
|
|
45
|
+
case 'runs':
|
|
46
|
+
return '#/runs';
|
|
47
|
+
case 'actions':
|
|
48
|
+
return '#/actions';
|
|
49
|
+
default:
|
|
50
|
+
return '#/';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function navigate(name, arg) {
|
|
55
|
+
location.hash = hrefFor(name, arg);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function decode(value) {
|
|
59
|
+
try {
|
|
60
|
+
return decodeURIComponent(value);
|
|
61
|
+
} catch {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Application state. Small on purpose: one presentation payload, plus the
|
|
2
|
+
// transient state of actions the human just triggered.
|
|
3
|
+
|
|
4
|
+
import { subscribe as on, emit } from './bus.js';
|
|
5
|
+
|
|
6
|
+
const state = {
|
|
7
|
+
data: null,
|
|
8
|
+
/** actionId -> last ActionResult */
|
|
9
|
+
actionResults: new Map(),
|
|
10
|
+
/** actionIds currently in flight */
|
|
11
|
+
pending: new Set(),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function getData() {
|
|
15
|
+
return state.data;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function setData(data) {
|
|
19
|
+
state.data = data;
|
|
20
|
+
emit('data');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getPresentation() {
|
|
24
|
+
return fetchPresentationData();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getActionResult(id) {
|
|
28
|
+
return state.actionResults.get(id) || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function setActionResult(id, result) {
|
|
32
|
+
state.actionResults.set(id, result);
|
|
33
|
+
emit('action');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isPending(id) {
|
|
37
|
+
return state.pending.has(id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function setPending(id, value) {
|
|
41
|
+
if (value) state.pending.add(id);
|
|
42
|
+
else state.pending.delete(id);
|
|
43
|
+
emit('action');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function findTask(id) {
|
|
47
|
+
if (!state.data) return null;
|
|
48
|
+
return state.data.tasks.find((t) => t.id === id) || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function findAction(id) {
|
|
52
|
+
if (!state.data) return null;
|
|
53
|
+
return state.data.actions.find((a) => a.id === id) || null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function findArtifact(path) {
|
|
57
|
+
if (!state.data) return null;
|
|
58
|
+
for (const section of state.data.sections) {
|
|
59
|
+
for (const a of section.artifacts) if (a.id === path) return a;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function artifactViews() {
|
|
65
|
+
if (!state.data) return [];
|
|
66
|
+
return state.data.sections.flatMap((s) => s.artifacts);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { on as subscribe, emit };
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// Page-level views. Each returns a detached DOM subtree for #content.
|
|
2
|
+
//
|
|
3
|
+
// The visual language is fixed by design.md: warm, quiet, reading-first. Tasks
|
|
4
|
+
// and actions are added without turning the page into a dashboard — state is
|
|
5
|
+
// expressed in words first, colour second.
|
|
6
|
+
|
|
7
|
+
import { el, append, spacer } from './dom.js';
|
|
8
|
+
import { fmtBytes, fmtStamp, fmtDuration, statusLabel, statusTone, SOURCE_ORIGIN } from './format.js';
|
|
9
|
+
import { cardGrid, sectionHead, taskCard, runRow, progressBar } from './cards.js';
|
|
10
|
+
import { renderBlocks, statusPill } from './blocks.js';
|
|
11
|
+
import { renderActions, renderAudit } from './actions.js';
|
|
12
|
+
import { hrefFor } from './router.js';
|
|
13
|
+
import { renderArtifactView } from './artifacts.js';
|
|
14
|
+
import { loadArtifact } from './api.js';
|
|
15
|
+
import { getData, findTask } from './store.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* What to say about simulation, given that it is a per-action fact.
|
|
19
|
+
*
|
|
20
|
+
* A workspace may hand one action to a real provider and let the next one fall
|
|
21
|
+
* through to the built-in simulator, so "everything here is simulated" would
|
|
22
|
+
* be false in that case — and telling a human a simulated click is real is the
|
|
23
|
+
* failure this whole path exists to prevent.
|
|
24
|
+
*/
|
|
25
|
+
function simulatedNotice(data) {
|
|
26
|
+
const actions = data.actions || [];
|
|
27
|
+
const simulated = actions.filter((a) => a.simulated).length;
|
|
28
|
+
if (actions.length === 0 || simulated === 0) return '';
|
|
29
|
+
if (simulated === actions.length) {
|
|
30
|
+
return 'No external agent is connected. Every action here is simulated: it changes A2H state only, and says so when it runs.';
|
|
31
|
+
}
|
|
32
|
+
return `${simulated} of ${actions.length} actions fall through to the built-in simulator. Those are marked "simulated" where they appear; the rest run against a connected provider.`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function overviewView() {
|
|
36
|
+
const data = getData();
|
|
37
|
+
const wrap = el('div', {});
|
|
38
|
+
wrap.appendChild(headerBlock(data));
|
|
39
|
+
|
|
40
|
+
if (data.panels && data.panels.length) {
|
|
41
|
+
const panels = el('section', { class: 'panel-region' });
|
|
42
|
+
append(panels, renderBlocks(data.panels, ctx(), { card: true }));
|
|
43
|
+
wrap.appendChild(panels);
|
|
44
|
+
wrap.appendChild(spacer(32));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (data.tasks.length > 0) {
|
|
48
|
+
wrap.appendChild(sectionHead('Work in progress', data.tasks.length));
|
|
49
|
+
wrap.appendChild(
|
|
50
|
+
el('p', { class: 'section-desc', text: 'Tasks this workspace is tracking. Each one can be inspected and acted on.' }),
|
|
51
|
+
);
|
|
52
|
+
const grid = el('div', { class: 'card-grid' });
|
|
53
|
+
for (const task of data.tasks) grid.appendChild(taskCard(task));
|
|
54
|
+
wrap.appendChild(grid);
|
|
55
|
+
wrap.appendChild(spacer(32));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (data.highlights.length) {
|
|
59
|
+
wrap.appendChild(sectionHead('Start here', null));
|
|
60
|
+
wrap.appendChild(cardGrid(data.highlights));
|
|
61
|
+
wrap.appendChild(spacer(32));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const section of data.sections) {
|
|
65
|
+
wrap.appendChild(sectionHead(section.title, section.artifacts.length, { explicit: section.explicit }));
|
|
66
|
+
if (section.description) wrap.appendChild(el('p', { class: 'section-desc', text: section.description }));
|
|
67
|
+
wrap.appendChild(cardGrid(section.artifacts));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
append(wrap, warningsView(data.warnings));
|
|
71
|
+
return wrap;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function sectionView(id) {
|
|
75
|
+
const data = getData();
|
|
76
|
+
const section = data.sections.find((s) => s.id === id);
|
|
77
|
+
const wrap = el('div', {});
|
|
78
|
+
wrap.appendChild(backLink('Overview'));
|
|
79
|
+
|
|
80
|
+
if (!section) {
|
|
81
|
+
wrap.appendChild(el('div', { class: 'notice', text: 'Section not found.' }));
|
|
82
|
+
return wrap;
|
|
83
|
+
}
|
|
84
|
+
wrap.appendChild(el('h1', { class: 'artifact-title', text: section.title }));
|
|
85
|
+
if (section.description) wrap.appendChild(el('p', { class: 'artifact-meta', text: section.description }));
|
|
86
|
+
if (section.explicit) {
|
|
87
|
+
wrap.appendChild(el('p', { class: 'artifact-meta', text: 'Declared by the workspace manifest.' }));
|
|
88
|
+
}
|
|
89
|
+
wrap.appendChild(spacer(16));
|
|
90
|
+
wrap.appendChild(cardGrid(section.artifacts));
|
|
91
|
+
return wrap;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function tasksView() {
|
|
95
|
+
const data = getData();
|
|
96
|
+
const wrap = el('div', {});
|
|
97
|
+
wrap.appendChild(backLink('Overview'));
|
|
98
|
+
wrap.appendChild(el('h1', { class: 'artifact-title', text: 'Tasks' }));
|
|
99
|
+
wrap.appendChild(
|
|
100
|
+
el('p', { class: 'artifact-meta', text: `${data.tasks.length} task${data.tasks.length === 1 ? '' : 's'} declared by this workspace.` }),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (data.simulatedActions) {
|
|
104
|
+
wrap.appendChild(
|
|
105
|
+
el('div', { class: 'block-notice tone-info' }, [
|
|
106
|
+
el('span', { class: 'notice-label', text: 'Note' }),
|
|
107
|
+
el('span', { class: 'notice-body', text: simulatedNotice(data) }),
|
|
108
|
+
]),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
wrap.appendChild(spacer(16));
|
|
113
|
+
const grid = el('div', { class: 'card-grid' });
|
|
114
|
+
for (const task of data.tasks) grid.appendChild(taskCard(task));
|
|
115
|
+
wrap.appendChild(grid);
|
|
116
|
+
return wrap;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function taskView(id) {
|
|
120
|
+
const task = findTask(id);
|
|
121
|
+
const wrap = el('div', {});
|
|
122
|
+
wrap.appendChild(backLink('Tasks', hrefFor('tasks')));
|
|
123
|
+
|
|
124
|
+
if (!task) {
|
|
125
|
+
wrap.appendChild(el('div', { class: 'notice', text: 'Task not found.' }));
|
|
126
|
+
return wrap;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---- head
|
|
130
|
+
const head = el('div', { class: 'artifact-head' }, [
|
|
131
|
+
el('div', { class: 'task-title-row' }, [
|
|
132
|
+
el('h1', { class: 'artifact-title', text: task.title }),
|
|
133
|
+
statusPill(task.status),
|
|
134
|
+
]),
|
|
135
|
+
]);
|
|
136
|
+
if (task.summary) head.appendChild(el('p', { class: 'workspace-summary', text: task.summary }));
|
|
137
|
+
|
|
138
|
+
const meta = [];
|
|
139
|
+
if (task.owner) meta.push(task.owner);
|
|
140
|
+
if (task.updatedAt) meta.push(`updated ${fmtStamp(task.updatedAt)}`);
|
|
141
|
+
meta.push(`declared in ${SOURCE_ORIGIN[task.source] || task.source}`);
|
|
142
|
+
head.appendChild(el('p', { class: 'artifact-meta', text: meta.join(' · ') }));
|
|
143
|
+
wrap.appendChild(head);
|
|
144
|
+
|
|
145
|
+
if (task.progress) wrap.appendChild(progressBar(task.progress));
|
|
146
|
+
wrap.appendChild(spacer(16));
|
|
147
|
+
|
|
148
|
+
if (task.actions.length) {
|
|
149
|
+
const actions = renderActions(task.actions.map((a) => a.id));
|
|
150
|
+
if (actions) {
|
|
151
|
+
wrap.appendChild(el('section', { class: 'block-card' }, [
|
|
152
|
+
el('h3', { class: 'block-title', text: 'What you can do' }),
|
|
153
|
+
actions,
|
|
154
|
+
]));
|
|
155
|
+
wrap.appendChild(spacer(24));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (task.metrics && task.metrics.length) {
|
|
160
|
+
append(wrap, renderBlocks([{ type: 'metrics', items: task.metrics }], ctx(), { card: true }));
|
|
161
|
+
wrap.appendChild(spacer(24));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (task.blocks && task.blocks.length) {
|
|
165
|
+
append(wrap, renderBlocks(task.blocks, ctx(), { card: true }));
|
|
166
|
+
wrap.appendChild(spacer(24));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---- runs
|
|
170
|
+
wrap.appendChild(sectionHead('Runs', task.runs.length));
|
|
171
|
+
if (task.runs.length === 0) {
|
|
172
|
+
wrap.appendChild(el('p', { class: 'empty', text: 'No runs recorded for this task yet.' }));
|
|
173
|
+
} else {
|
|
174
|
+
const list = el('div', { class: 'run-list' });
|
|
175
|
+
for (const run of task.runs) list.appendChild(runCard(run));
|
|
176
|
+
wrap.appendChild(list);
|
|
177
|
+
}
|
|
178
|
+
wrap.appendChild(spacer(24));
|
|
179
|
+
|
|
180
|
+
// ---- artifacts
|
|
181
|
+
wrap.appendChild(sectionHead('Artifacts', task.artifacts.length));
|
|
182
|
+
const views = task.artifacts.map((p) => findArtifactView(p)).filter(Boolean);
|
|
183
|
+
if (views.length === 0) {
|
|
184
|
+
wrap.appendChild(el('p', { class: 'empty', text: 'No artifacts are attached to this task.' }));
|
|
185
|
+
} else {
|
|
186
|
+
wrap.appendChild(cardGrid(views));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return wrap;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function runsView() {
|
|
193
|
+
const data = getData();
|
|
194
|
+
const wrap = el('div', {});
|
|
195
|
+
wrap.appendChild(backLink('Overview'));
|
|
196
|
+
wrap.appendChild(el('h1', { class: 'artifact-title', text: 'Runs' }));
|
|
197
|
+
wrap.appendChild(
|
|
198
|
+
el('p', { class: 'artifact-meta', text: `${data.runs.length} recorded run${data.runs.length === 1 ? '' : 's'}, newest first.` }),
|
|
199
|
+
);
|
|
200
|
+
wrap.appendChild(spacer(16));
|
|
201
|
+
|
|
202
|
+
if (data.runs.length === 0) {
|
|
203
|
+
wrap.appendChild(el('p', { class: 'empty', text: 'No runs have been recorded in this workspace.' }));
|
|
204
|
+
return wrap;
|
|
205
|
+
}
|
|
206
|
+
const list = el('div', { class: 'run-list' });
|
|
207
|
+
for (const run of data.runs) list.appendChild(runCard(run));
|
|
208
|
+
wrap.appendChild(list);
|
|
209
|
+
return wrap;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function actionsView() {
|
|
213
|
+
const data = getData();
|
|
214
|
+
const wrap = el('div', {});
|
|
215
|
+
wrap.appendChild(backLink('Overview'));
|
|
216
|
+
wrap.appendChild(el('h1', { class: 'artifact-title', text: 'Actions' }));
|
|
217
|
+
wrap.appendChild(
|
|
218
|
+
el('p', { class: 'artifact-meta', text: 'Everything this workspace lets a human do, and what happened when it was tried.' }),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
if (data.simulatedActions) {
|
|
222
|
+
wrap.appendChild(
|
|
223
|
+
el('div', { class: 'block-notice tone-info' }, [
|
|
224
|
+
el('span', { class: 'notice-label', text: 'Note' }),
|
|
225
|
+
el('span', { class: 'notice-body', text: simulatedNotice(data) }),
|
|
226
|
+
]),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
wrap.appendChild(spacer(16));
|
|
231
|
+
|
|
232
|
+
if (data.actions.length === 0) {
|
|
233
|
+
wrap.appendChild(
|
|
234
|
+
el('p', { class: 'empty', text: 'This workspace declares no actions. Add an "actions" array to .a2h/manifest.json to give a human something to do.' }),
|
|
235
|
+
);
|
|
236
|
+
return wrap;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const byTask = new Map();
|
|
240
|
+
for (const action of data.actions) {
|
|
241
|
+
const key = action.taskId || '';
|
|
242
|
+
if (!byTask.has(key)) byTask.set(key, []);
|
|
243
|
+
byTask.get(key).push(action);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const [taskId, actions] of byTask) {
|
|
247
|
+
const task = taskId ? findTask(taskId) : null;
|
|
248
|
+
const card = el('section', { class: 'block-card' });
|
|
249
|
+
card.appendChild(el('h3', { class: 'block-title', text: task ? task.title : 'Workspace' }));
|
|
250
|
+
if (task) card.appendChild(el('p', { class: 'section-desc', text: `task ${task.id} · ${statusLabel(task.status)}` }));
|
|
251
|
+
card.appendChild(renderActions(actions.map((a) => a.id)));
|
|
252
|
+
wrap.appendChild(card);
|
|
253
|
+
wrap.appendChild(spacer(16));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
wrap.appendChild(sectionHead('Session audit', null));
|
|
257
|
+
wrap.appendChild(
|
|
258
|
+
el('p', { class: 'section-desc', text: 'Actions taken since this viewer started. Nothing is written to disk.' }),
|
|
259
|
+
);
|
|
260
|
+
wrap.appendChild(el('div', { class: 'artifact-body' }, [renderAudit(data.audit)]));
|
|
261
|
+
return wrap;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ------------------------------------------------------------------ artifact
|
|
265
|
+
|
|
266
|
+
export function artifactContainer(path) {
|
|
267
|
+
const wrap = el('div', {});
|
|
268
|
+
wrap.appendChild(el('div', { class: 'placeholder', text: 'Loading…' }));
|
|
269
|
+
|
|
270
|
+
loadArtifact(path)
|
|
271
|
+
.then((payload) => {
|
|
272
|
+
wrap.textContent = '';
|
|
273
|
+
wrap.appendChild(renderArtifactView(payload));
|
|
274
|
+
})
|
|
275
|
+
.catch(() => {
|
|
276
|
+
wrap.textContent = '';
|
|
277
|
+
wrap.appendChild(backLink('Overview'));
|
|
278
|
+
wrap.appendChild(el('div', { class: 'notice', text: 'Could not load this artifact.' }));
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return wrap;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ------------------------------------------------------------------ helpers
|
|
285
|
+
|
|
286
|
+
export function ctx() {
|
|
287
|
+
return { data: getData(), renderActions };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function runCard(run) {
|
|
291
|
+
const card = el('div', { class: 'run-card' });
|
|
292
|
+
card.appendChild(runRow(run));
|
|
293
|
+
if (run.artifacts && run.artifacts.length) {
|
|
294
|
+
const links = el('div', { class: 'run-artifacts' });
|
|
295
|
+
for (const path of run.artifacts) {
|
|
296
|
+
links.appendChild(el('a', { class: 'provenance-link', href: hrefFor('artifact', path), text: path }));
|
|
297
|
+
}
|
|
298
|
+
card.appendChild(links);
|
|
299
|
+
}
|
|
300
|
+
if (run.blocks && run.blocks.length) {
|
|
301
|
+
append(card, renderBlocks(run.blocks, ctx(), { card: false }));
|
|
302
|
+
}
|
|
303
|
+
return card;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function findArtifactView(path) {
|
|
307
|
+
const data = getData();
|
|
308
|
+
for (const section of data.sections) {
|
|
309
|
+
for (const artifact of section.artifacts) if (artifact.id === path) return artifact;
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function headerBlock(data) {
|
|
315
|
+
const head = el('header', { class: 'page-head' });
|
|
316
|
+
head.appendChild(el('h1', { class: 'workspace-name', text: data.identity.name }));
|
|
317
|
+
if (data.identity.summary) head.appendChild(el('p', { class: 'workspace-summary', text: data.identity.summary }));
|
|
318
|
+
|
|
319
|
+
const meta = el('p', { class: 'meta-line' });
|
|
320
|
+
const push = (text) => {
|
|
321
|
+
if (meta.childNodes.length) meta.appendChild(el('span', { class: 'dot', text: '·' }));
|
|
322
|
+
meta.appendChild(el('span', { text }));
|
|
323
|
+
};
|
|
324
|
+
if (data.git && data.git.branch) push(`branch ${data.git.branch}`);
|
|
325
|
+
if (data.git && data.git.lastCommit && data.git.lastCommit.hash) {
|
|
326
|
+
push(`${data.git.lastCommit.hash} ${data.git.lastCommit.subject}`);
|
|
327
|
+
}
|
|
328
|
+
push(`${data.stats.files} files`);
|
|
329
|
+
push(fmtBytes(data.stats.bytes));
|
|
330
|
+
if (data.semantics !== 'inferred') push(`semantics: ${data.semantics}`);
|
|
331
|
+
head.appendChild(meta);
|
|
332
|
+
return head;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function backLink(label, href) {
|
|
336
|
+
return el('button', {
|
|
337
|
+
class: 'back-link',
|
|
338
|
+
text: `← ${label}`,
|
|
339
|
+
onclick: () => {
|
|
340
|
+
location.hash = href || '#/';
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function warningsView(warnings) {
|
|
346
|
+
if (!warnings || warnings.length === 0) return null;
|
|
347
|
+
const box = el('div', { class: 'artifact-body' }, [
|
|
348
|
+
el('div', { class: 'bar' }, [el('span', { text: `${warnings.length} notice(s) from the scanner and manifest` })]),
|
|
349
|
+
]);
|
|
350
|
+
const list = el('div', { class: 'warning-list' });
|
|
351
|
+
for (const warning of warnings) list.appendChild(el('div', { text: warning }));
|
|
352
|
+
box.appendChild(list);
|
|
353
|
+
return box;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export { statusTone, fmtDuration };
|