@njeybe/nativ 2.6.0 → 2.8.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/CHANGELOG.md +50 -0
- package/README.md +71 -1
- package/dist/commands/design-check.d.ts +4 -0
- package/dist/commands/design-check.js +76 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +53 -1
- package/dist/commands/update.js +46 -0
- package/dist/core/design-rules.d.ts +23 -0
- package/dist/core/design-rules.js +115 -0
- package/dist/core/harness-integrity.js +26 -7
- package/dist/core/setup/pm-playbook.d.ts +1 -1
- package/dist/core/setup/pm-playbook.js +1 -1
- package/dist/index.js +11 -0
- package/dist/server/studio-ui/pages/benchmarks/script.js +9 -8
- package/dist/server/studio-ui/pages/benchmarks/styles.js +11 -6
- package/dist/server/studio-ui/pages/database/browser.js +2 -2
- package/dist/server/studio-ui/pages/database/connection.js +6 -5
- package/dist/server/studio-ui/pages/database/data-shell.js +12 -17
- package/dist/server/studio-ui/pages/database/drift.js +2 -2
- package/dist/server/studio-ui/pages/database/events.js +1 -2
- package/dist/server/studio-ui/pages/database/explorer.js +6 -6
- package/dist/server/studio-ui/pages/database/styles.js +5 -5
- package/dist/server/studio-ui/pages/flow/layout.js +1 -1
- package/dist/server/studio-ui/pages/flow/styles.js +4 -4
- package/dist/server/studio-ui/pages/flow/timeline-view.js +1 -1
- package/dist/server/studio-ui/pages/home/script.js +108 -92
- package/dist/server/studio-ui/pages/home/styles.js +29 -45
- package/dist/server/studio-ui/pages/tasks/script.js +146 -14
- package/dist/server/studio-ui/pages/tasks/styles.js +63 -6
- package/dist/server/studio-ui/pages/team/script.js +69 -53
- package/dist/server/studio-ui/pages/team/styles.js +20 -20
- package/dist/server/studio-ui/pages/worktrees/script.js +47 -35
- package/dist/server/studio-ui/pages/worktrees/styles.js +12 -10
- package/dist/server/studio-ui/shell/brand.d.ts +4 -0
- package/dist/server/studio-ui/shell/brand.js +16 -0
- package/dist/server/studio-ui/shell/console.js +71 -6
- package/dist/server/studio-ui/shell/core.js +8 -8
- package/dist/server/studio-ui/shell/detail-view.js +4 -4
- package/dist/server/studio-ui/shell/dispatch.js +23 -4
- package/dist/server/studio-ui/shell/events-tools.js +16 -1
- package/dist/server/studio-ui/shell/format.js +36 -8
- package/dist/server/studio-ui/shell/icons.js +23 -15
- package/dist/server/studio-ui/shell/markup-head.js +11 -6
- package/dist/server/studio-ui/shell/markup-tools.js +29 -15
- package/dist/server/studio-ui/shell/model.js +8 -2
- package/dist/server/studio-ui/shell/paint.js +4 -3
- package/dist/server/studio-ui/shell/pipeline-state.js +2 -11
- package/dist/server/studio-ui/shell/proposals.js +6 -2
- package/dist/server/studio-ui/shell/pulse.d.ts +1 -0
- package/dist/server/studio-ui/shell/pulse.js +68 -0
- package/dist/server/studio-ui/shell/router.js +25 -0
- package/dist/server/studio-ui/shell/sidebar-view.js +7 -5
- package/dist/server/studio-ui/shell/toasts-format.js +4 -4
- package/dist/server/studio-ui/shell/triage.js +1 -1
- package/dist/server/studio-ui/styles/console.js +36 -13
- package/dist/server/studio-ui/styles/dialogs.js +4 -4
- package/dist/server/studio-ui/styles/responsive.js +8 -0
- package/dist/server/studio-ui/styles/shell.js +23 -6
- package/dist/server/studio-ui/styles/strategist.js +10 -10
- package/dist/server/studio-ui/styles/tokens-base.js +23 -23
- package/dist/server/studio-ui.js +2 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/agents/architect.md +11 -12
- package/plugin/agents/verifier.md +4 -1
- package/plugin/skills/nativ/SKILL.md +7 -7
- package/templates/AGENTS.md +7 -7
- package/templates/GEMINI.md +4 -3
- package/templates/claude-agents/architect.md +11 -12
- package/templates/claude-agents/verifier.md +4 -1
- package/templates/design/taste/LICENSE +21 -0
- package/templates/design/taste/SOURCE.md +28 -0
- package/templates/design/taste/v1/DIGEST.md +57 -0
- package/templates/design/taste/v1/SKILL.md +226 -0
- package/templates/design/taste/v2/DIGEST.md +62 -0
- package/templates/design/taste/v2/SKILL.md +1206 -0
- package/templates/dot-ai/subagents/architect.md +8 -2
- package/templates/dot-ai/subagents/flutter-developer.md +2 -11
- package/templates/dot-ai/subagents/frontend.md +47 -44
- package/templates/dot-ai/ui_specs.md +15 -36
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
// Home page client script: one pure render from the shared model.
|
|
1
|
+
// Home page client script: one pure render from the shared model, sections stacked top to bottom.
|
|
2
2
|
export const homeScript = String.raw ` var esc = Studio.ui.esc;
|
|
3
|
-
var
|
|
3
|
+
var UP_NEXT_CAP = 6;
|
|
4
|
+
var ACTIVITY_CAP = 8;
|
|
4
5
|
|
|
5
6
|
function head() {
|
|
6
|
-
return '<div class="page-head"><
|
|
7
|
+
return '<div class="page-head"><h1>Home</h1></div>';
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
function
|
|
10
|
-
return '<section class="
|
|
10
|
+
function section(key, title, right, body) {
|
|
11
|
+
return '<section class="home-sec" data-sec="' + key + '"><div class="sec-head"><h2>' + esc(title) +
|
|
12
|
+
'</h2>' + (right || '') + '</div>' + body + '</section>';
|
|
11
13
|
}
|
|
12
14
|
function rightText(text) { return '<span class="right">' + esc(text) + '</span>'; }
|
|
13
15
|
|
|
14
|
-
function actBtn(act, id, label, busy) {
|
|
16
|
+
function actBtn(act, id, label, busy, cls) {
|
|
15
17
|
var on = busy === act.slice(5);
|
|
16
|
-
return '<button type="button" class="btn
|
|
18
|
+
return '<button type="button" class="btn' + (cls ? ' ' + cls : '') + '" data-act="' + act +
|
|
19
|
+
'" data-id="' + esc(id) + '"' + (busy ? ' disabled' : '') + '>' +
|
|
17
20
|
(on ? '<span class="spinner" aria-hidden="true"></span>' : '') + esc(label) + '</button>';
|
|
18
21
|
}
|
|
19
22
|
|
|
@@ -21,130 +24,140 @@ export const homeScript = String.raw ` var esc = Studio.ui.esc;
|
|
|
21
24
|
var t = item.task;
|
|
22
25
|
if (t.status === 'blocked') return t.reason || 'Paused after 3 attempts.';
|
|
23
26
|
var e = item.escalation || {};
|
|
24
|
-
return e.reason || e.summary || e.details || '
|
|
27
|
+
return e.reason || e.summary || e.details || 'Needs your decision.';
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
function
|
|
30
|
+
function needActions(item) {
|
|
28
31
|
var t = item.task;
|
|
29
32
|
var busy = Studio.busyAction ? Studio.busyAction(t.id) : null;
|
|
30
33
|
var stuck = t.status === 'blocked';
|
|
31
|
-
var out = '<button type="button" class="btn
|
|
34
|
+
var out = '<button type="button" class="btn" data-task="' + esc(t.id) + '">' +
|
|
35
|
+
(stuck ? 'See what failed' : 'Open task') + '</button>';
|
|
32
36
|
if (stuck) out += actBtn('home-start', t.id, 'Try again', busy);
|
|
33
|
-
if (item.hasProposal) out += actBtn('home-proposal', t.id, 'Review proposal', busy);
|
|
34
|
-
else if (item.escalation) out += actBtn('home-ask', t.id, 'Ask the architect', busy);
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
if (item.hasProposal) out += actBtn('home-proposal', t.id, 'Review proposal', busy, 'primary');
|
|
38
|
+
else if (item.escalation) out += actBtn('home-ask', t.id, 'Ask the architect', busy, 'primary');
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function needItem(item) {
|
|
43
|
+
var t = item.task;
|
|
44
|
+
return '<div class="need" data-need="' + esc(t.id) + '"><div class="body">' +
|
|
45
|
+
'<div class="l1"><b>Human Decision Required</b><span class="mono">' + esc(t.id) + '</span></div>' +
|
|
46
|
+
'<span class="why">' + esc(needReason(item)) + '</span></div>' +
|
|
47
|
+
'<div class="acts">' + needActions(item) + '</div></div>';
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
function
|
|
50
|
+
function decisionQueue(model) {
|
|
42
51
|
var n = model.needsYou;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
if (!n.length) return '';
|
|
53
|
+
var count = n.length + (n.length === 1 ? ' item' : ' items');
|
|
54
|
+
return '<section class="home-sec decisions" data-sec="decisions"><div class="sec-head"><h2>Decisions</h2>' +
|
|
55
|
+
rightText(count) + '</div>' + n.map(needItem).join('') + '</section>';
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
var timed = done.filter(function (t) { return t.endT; }).sort(function (a, b) { return b.endT - a.endT; });
|
|
52
|
-
return timed[0] || done[done.length - 1] || null;
|
|
58
|
+
function readyTasks(model) {
|
|
59
|
+
return model.tasks.filter(Studio.status.isReady);
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
var when = last.endT ? ' ' + esc(Studio.fmt.ago(last.endT)) : '';
|
|
59
|
-
return 'No agent is working. Last finished <span class="mono">' + esc(last.id) + '</span>' + when + '.';
|
|
62
|
+
function dispatchBtn(id, label, cls) {
|
|
63
|
+
return '<button type="button" class="btn' + (cls ? ' ' + cls : '') + '" data-act="home-dispatch" data-id="' +
|
|
64
|
+
esc(id) + '">' + esc(label) + '</button>';
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
(t.latestLog ? '<div class="log">' + esc(t.latestLog) + '</div>' : '') + '</button>';
|
|
67
|
+
function taskCells(t) {
|
|
68
|
+
return '<span class="mono id">' + esc(t.id) + '</span>' +
|
|
69
|
+
'<button type="button" class="t link" data-task="' + esc(t.id) + '">' + esc(t.title) + '</button>' +
|
|
70
|
+
'<span class="agent">' + esc(Studio.agentInfo(t.agent).label) + '</span>';
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
function
|
|
70
|
-
var
|
|
71
|
-
var
|
|
72
|
-
|
|
73
|
+
function runRow(t) {
|
|
74
|
+
var since = t.startT ? ' data-since="' + t.startT + '">' + Studio.fmt.dur(Studio.now() - t.startT) : '>';
|
|
75
|
+
var row = '<div class="row run-row">' + taskCells(t) +
|
|
76
|
+
'<span class="state">Running</span>' +
|
|
77
|
+
'<span class="mono num timer"' + since + '</span></div>';
|
|
78
|
+
var log = '<span class="mono log">' + esc(t.latestLog || '') + '</span>';
|
|
79
|
+
var watch = t.run ? '<button type="button" class="btn" data-act="home-console" data-id="' +
|
|
80
|
+
esc(t.id) + '">Watch live</button>' : '';
|
|
81
|
+
return row + '<div class="row sub-row">' + log + watch + '</div>';
|
|
73
82
|
}
|
|
74
83
|
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
function nowRunning(model) {
|
|
85
|
+
var r = model.running;
|
|
86
|
+
if (r.length) return section('running', 'Now running', rightText(r.length + ' running'), r.map(runRow).join(''));
|
|
87
|
+
var next = readyTasks(model)[0];
|
|
88
|
+
var btn = next ? dispatchBtn(next.id, 'Dispatch ' + next.id, 'primary') : '';
|
|
89
|
+
return section('running', 'Now running', '', '<div class="row calm"><span>No agent is running.</span>' + btn + '</div>');
|
|
78
90
|
}
|
|
79
91
|
|
|
80
|
-
function
|
|
81
|
-
var
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return parts.length ? parts.join(', ') : 'no spend recorded';
|
|
92
|
+
function nextRow(t) {
|
|
93
|
+
var end = Studio.status.isReady(t) ? dispatchBtn(t.id, 'Dispatch') :
|
|
94
|
+
'<span class="wait">' + esc(Studio.status.waitText(t)) + '</span>';
|
|
95
|
+
return '<div class="row next-row">' + taskCells(t) + end + '</div>';
|
|
85
96
|
}
|
|
86
97
|
|
|
87
|
-
function
|
|
88
|
-
var
|
|
89
|
-
var
|
|
90
|
-
var
|
|
91
|
-
var
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
figure('Spend', Studio.fmt.money(s.spendUsd), null, spendNote(s)) +
|
|
96
|
-
figure('Plan files', have == null ? '–' : have, s.contractsTotal, plan) + '</section>';
|
|
98
|
+
function upNext(model) {
|
|
99
|
+
var list = model.tasks.filter(function (t) { return t.status === 'pending'; });
|
|
100
|
+
var ready = list.filter(Studio.status.isReady);
|
|
101
|
+
var waiting = list.filter(function (t) { return !Studio.status.isReady(t); });
|
|
102
|
+
var rows = ready.concat(waiting).slice(0, UP_NEXT_CAP);
|
|
103
|
+
var body = rows.length ? rows.map(nextRow).join('') :
|
|
104
|
+
'<div class="row calm">Nothing is waiting.</div>';
|
|
105
|
+
return section('next', 'Up next', '', body);
|
|
97
106
|
}
|
|
98
107
|
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
function milestoneProgress(model) {
|
|
109
|
+
var m = model.milestones.filter(function (x) { return x.id === model.activeMilestoneId; })[0];
|
|
110
|
+
if (!m) return '';
|
|
111
|
+
var pct = m.total ? Math.round((m.done / m.total) * 100) : 0;
|
|
112
|
+
var body = '<div class="row ms-row" data-ms="' + esc(m.id) + '"><span class="mono id">' + esc(m.id) + '</span>' +
|
|
113
|
+
'<span class="t">' + esc(m.name) + '</span><span class="mono num">' + m.done + ' of ' + m.total + '</span></div>' +
|
|
114
|
+
'<div class="progress"><i style="width:' + pct + '%"></i></div>';
|
|
115
|
+
return section('milestone', 'Milestone', '', body);
|
|
102
116
|
}
|
|
103
117
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
// One event per task that has started or finished; the state word comes from its status now.
|
|
119
|
+
function activityEvents(model) {
|
|
120
|
+
var out = [];
|
|
121
|
+
model.tasks.forEach(function (t) {
|
|
122
|
+
var at = t.status === 'completed' ? (t.endT || t.startT) : t.startT;
|
|
123
|
+
if (at) out.push({ at: at, task: t });
|
|
124
|
+
});
|
|
125
|
+
return out.sort(function (a, b) { return b.at - a.at; }).slice(0, ACTIVITY_CAP);
|
|
109
126
|
}
|
|
110
127
|
|
|
111
|
-
function
|
|
112
|
-
var
|
|
113
|
-
var
|
|
114
|
-
|
|
115
|
-
var btn = '<button type="button" class="btn ghost right" data-go="tasks">Open Tasks</button>';
|
|
116
|
-
return panel('Just finished', btn, body);
|
|
128
|
+
function activityState(t) {
|
|
129
|
+
var word = Studio.status.word(Studio.status.bucket(t));
|
|
130
|
+
var took = t.status === 'completed' && t.durationMs ? ', took ' + Studio.fmt.dur(t.durationMs) : '';
|
|
131
|
+
return word + took;
|
|
117
132
|
}
|
|
118
133
|
|
|
119
|
-
function
|
|
120
|
-
var
|
|
121
|
-
return '<
|
|
122
|
-
'
|
|
123
|
-
'
|
|
124
|
-
'<span class="num faint" style="font-size:12px;text-align:right">' + m.done + '/' + m.total + '</span></button>';
|
|
134
|
+
function activityRow(e) {
|
|
135
|
+
var t = e.task;
|
|
136
|
+
return '<div class="row act-row" data-event="' + esc(t.id) + '"><span class="mono num when">' +
|
|
137
|
+
Studio.ui.when(e.at) + '</span><button type="button" class="t link" data-task="' + esc(t.id) + '">' +
|
|
138
|
+
esc(t.title) + '</button><span class="state">' + esc(activityState(t)) + '</span></div>';
|
|
125
139
|
}
|
|
126
140
|
|
|
127
|
-
function
|
|
128
|
-
var list = model
|
|
129
|
-
var body = list.length ?
|
|
130
|
-
|
|
131
|
-
return panel('Milestones', rightText('newest first'), body);
|
|
141
|
+
function recentActivity(model) {
|
|
142
|
+
var list = activityEvents(model);
|
|
143
|
+
var body = list.length ? list.map(activityRow).join('') : '<div class="row calm">Nothing has happened yet.</div>';
|
|
144
|
+
return section('activity', 'Recent activity', '', body);
|
|
132
145
|
}
|
|
133
146
|
|
|
134
147
|
function skeleton() {
|
|
135
|
-
var
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
'<div class="skel-strip">' + block + block + block + block + '</div>' +
|
|
139
|
-
'<div class="home-top">' + block + block + '</div>';
|
|
148
|
+
var row = '<div class="row skel-row"><div class="skel"></div></div>';
|
|
149
|
+
return '<div class="home">' + head() + '<section class="home-sec" aria-busy="true">' +
|
|
150
|
+
row + row + row + row + '</section></div>';
|
|
140
151
|
}
|
|
141
152
|
|
|
142
153
|
function render(ctx) {
|
|
143
154
|
var model = ctx.model;
|
|
144
|
-
|
|
145
|
-
'<div class="home
|
|
146
|
-
|
|
147
|
-
|
|
155
|
+
if (!model.milestones.length) {
|
|
156
|
+
return '<div class="home">' + head() +
|
|
157
|
+
'<div class="row calm">No plan yet. Ask the architect to design one.</div></div>';
|
|
158
|
+
}
|
|
159
|
+
return '<div class="home">' + head() + decisionQueue(model) + nowRunning(model) + upNext(model) +
|
|
160
|
+
milestoneProgress(model) + recentActivity(model) + '</div>';
|
|
148
161
|
}
|
|
149
162
|
|
|
150
163
|
function forward(name) {
|
|
@@ -155,6 +168,9 @@ export const homeScript = String.raw ` var esc = Studio.ui.esc;
|
|
|
155
168
|
deps: ['status', 'tasks', 'telemetry', 'runs', 'escalations'],
|
|
156
169
|
render: render,
|
|
157
170
|
skeleton: skeleton,
|
|
158
|
-
actions: {
|
|
171
|
+
actions: {
|
|
172
|
+
'home-start': forward('start'), 'home-proposal': forward('proposal'), 'home-ask': forward('ask'),
|
|
173
|
+
'home-console': forward('logs'), 'home-dispatch': forward('dispatch')
|
|
174
|
+
}
|
|
159
175
|
});
|
|
160
176
|
`;
|
|
@@ -1,48 +1,32 @@
|
|
|
1
|
-
// Home page styles, all scoped under #panel-home.
|
|
2
|
-
export const homeCss = String.raw `#panel-home .
|
|
3
|
-
#panel-home .page-head h1 { font-size: 22px; font-weight: 600; margin: 0; letter-spacing: -.01em;
|
|
4
|
-
#panel-home .
|
|
5
|
-
#panel-home .
|
|
6
|
-
#panel-home .
|
|
7
|
-
#panel-home .
|
|
8
|
-
#panel-home .
|
|
9
|
-
#panel-home .
|
|
10
|
-
#panel-home .
|
|
11
|
-
#panel-home .
|
|
12
|
-
#panel-home .
|
|
13
|
-
#panel-home .
|
|
1
|
+
// Home page styles, all scoped under #panel-home. Ink: flat sections, one raised decision surface.
|
|
2
|
+
export const homeCss = String.raw `#panel-home .home { display: flex; flex-direction: column; }
|
|
3
|
+
#panel-home .page-head h1 { font-size: 22px; font-weight: 600; margin: 0 0 12px; letter-spacing: -.01em; }
|
|
4
|
+
#panel-home .home-sec { border-top: 1px solid var(--line); padding: 12px 0; min-width: 0; }
|
|
5
|
+
#panel-home .sec-head { display: flex; align-items: center; gap: 8px; min-height: 28px; }
|
|
6
|
+
#panel-home .sec-head h2 { font-size: 13px; font-weight: 600; margin: 0; color: var(--fg-2); }
|
|
7
|
+
#panel-home .sec-head .right { margin-left: auto; font-size: 12px; color: var(--fg-3); }
|
|
8
|
+
#panel-home .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
9
|
+
#panel-home .row { display: flex; align-items: center; gap: 12px; min-height: 36px; }
|
|
10
|
+
#panel-home .row .id { width: 72px; flex: none; color: var(--fg-2); font-size: 12px; }
|
|
11
|
+
#panel-home .row .t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
12
|
+
#panel-home .link { background: none; border: 0; padding: 0; text-align: left; color: var(--fg); cursor: pointer; font: inherit; }
|
|
13
|
+
#panel-home .agent { width: 96px; flex: none; color: var(--fg-2); font-size: 12.5px; }
|
|
14
|
+
#panel-home .state { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; }
|
|
15
|
+
#panel-home .timer { width: 64px; text-align: right; font-size: 12px; }
|
|
16
|
+
#panel-home .sub-row .log { flex: 1; min-width: 0; font-size: 12px; color: var(--fg-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
17
|
+
#panel-home .wait { color: var(--fg-3); font-size: 12.5px; }
|
|
18
|
+
#panel-home .calm { color: var(--fg-2); justify-content: space-between; }
|
|
19
|
+
#panel-home .decisions { background: var(--surface); border: 0; border-left: 3px solid var(--warn); border-radius: 6px; box-shadow: var(--shadow); padding: 8px 16px; margin-bottom: 12px; }
|
|
20
|
+
#panel-home .need { display: flex; align-items: center; gap: 12px; min-height: 36px; padding: 8px 0; border-top: 1px solid var(--line); flex-wrap: wrap; }
|
|
21
|
+
#panel-home .need .body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
22
|
+
#panel-home .need .l1 { display: flex; gap: 8px; align-items: baseline; }
|
|
14
23
|
#panel-home .need .why { font-size: 13px; color: var(--fg-2); overflow-wrap: anywhere; }
|
|
15
|
-
#panel-home .need .
|
|
16
|
-
#panel-home .
|
|
17
|
-
#panel-home .
|
|
18
|
-
#panel-home .
|
|
19
|
-
#panel-home .
|
|
20
|
-
|
|
21
|
-
#panel-home .
|
|
22
|
-
#panel-home .now .timer { margin-left: auto; font: 12px var(--mono); color: var(--run); }
|
|
23
|
-
#panel-home .now .log { font: 11.5px var(--mono); color: var(--fg-2); background: var(--surface-2); padding: 6px 8px; border-radius: 6px; overflow-wrap: anywhere; }
|
|
24
|
-
#panel-home .strip { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
25
|
-
#panel-home .strip > div { padding: 14px 16px; display: flex; flex-direction: column; gap: 2px; }
|
|
26
|
-
#panel-home .strip > div + div { border-left: 1px solid var(--line); }
|
|
27
|
-
#panel-home .strip .k { font-size: 12px; color: var(--fg-3); }
|
|
28
|
-
#panel-home .strip .v { font-size: 22px; font-weight: 600; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
|
|
29
|
-
#panel-home .strip .v small { font-size: 13px; font-weight: 400; color: var(--fg-3); }
|
|
30
|
-
#panel-home .strip .s { font-size: 12px; color: var(--fg-2); }
|
|
31
|
-
#panel-home .list .row-btn:first-child, #panel-home .list .ms-row:first-child { border-top: 0; }
|
|
32
|
-
#panel-home .ms-row { display: grid; grid-template-columns: 34px minmax(0, 1fr) 120px 64px; gap: 12px; align-items: center; padding: 9px 16px; border: 0; border-top: 1px solid var(--line); background: none; text-align: left; width: 100%; cursor: pointer; }
|
|
33
|
-
#panel-home .ms-row:hover { background: var(--surface-2); }
|
|
34
|
-
#panel-home .ms-row .t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
35
|
-
#panel-home .bar { height: 5px; border-radius: 3px; background: var(--surface-2); overflow: hidden; }
|
|
36
|
-
#panel-home .bar i { display: block; height: 100%; background: var(--ok); border-radius: 3px; }
|
|
37
|
-
#panel-home .row-right { text-align: right; font-size: 12px; }
|
|
38
|
-
#panel-home .skel-strip { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; }
|
|
39
|
-
@media (max-width: 900px) {
|
|
40
|
-
#panel-home .home-top { grid-template-columns: minmax(0, 1fr); }
|
|
41
|
-
#panel-home .strip, #panel-home .skel-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
42
|
-
#panel-home .strip > div:nth-child(3) { border-left: 0; }
|
|
43
|
-
}
|
|
44
|
-
@media (max-width: 760px) {
|
|
45
|
-
#panel-home .ms-row { grid-template-columns: 34px minmax(0, 1fr) 64px; }
|
|
46
|
-
#panel-home .ms-row .bar { display: none; }
|
|
24
|
+
#panel-home .need .acts { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
25
|
+
#panel-home .progress { height: 4px; border-bottom: 1px solid var(--line); }
|
|
26
|
+
#panel-home .progress i { display: block; height: 4px; background: var(--fg); border-radius: 2px; }
|
|
27
|
+
#panel-home .act-row .when { width: 72px; flex: none; font-size: 12px; color: var(--fg-3); }
|
|
28
|
+
#panel-home .skel-row .skel { height: 12px; width: 100%; border-radius: 6px; background: var(--surface-2); }
|
|
29
|
+
@media (max-width: 1024px) {
|
|
30
|
+
#panel-home .agent { display: none; }
|
|
47
31
|
}
|
|
48
32
|
`;
|
|
@@ -1,11 +1,119 @@
|
|
|
1
|
-
// Tasks page:
|
|
1
|
+
// Tasks page: a grouped list by default, or one milestone as four columns (Board).
|
|
2
2
|
export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
3
3
|
var CAP = 5;
|
|
4
|
-
var view = { lastMs: null, showAll: false, open: {} };
|
|
4
|
+
var view = { lastMs: null, showAll: false, open: {}, mode: null, filter: 'all', groups: {} };
|
|
5
|
+
var KEY = 'nativ-studio:tasks-view';
|
|
6
|
+
var FILTERS = [['all', 'All']].concat(Studio.status.buckets);
|
|
7
|
+
|
|
8
|
+
function readMode() {
|
|
9
|
+
try { return localStorage.getItem(KEY) === 'board' ? 'board' : 'list'; } catch (e) { return 'list'; }
|
|
10
|
+
}
|
|
11
|
+
function saveMode(mode) {
|
|
12
|
+
try { localStorage.setItem(KEY, mode); } catch (e) { /* private mode */ }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function matches(model, t, f) {
|
|
16
|
+
return f === 'all' || Studio.status.bucket(t) === f;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toolbar(model) {
|
|
20
|
+
var chips = FILTERS.map(function (f) {
|
|
21
|
+
var on = view.filter === f[0];
|
|
22
|
+
var n = model.tasks.filter(function (t) { return matches(model, t, f[0]); }).length;
|
|
23
|
+
if (!n && !on && f[0] !== 'all') return '';
|
|
24
|
+
return '<button type="button" class="tf' + (on ? ' on' : '') + '" data-act="tasks-filter" data-filter="' +
|
|
25
|
+
f[0] + '" aria-pressed="' + on + '">' + f[1] + ' <span class="num">' + n + '</span></button>';
|
|
26
|
+
}).join('');
|
|
27
|
+
var seg = ['list', 'board'].map(function (m) {
|
|
28
|
+
var on = view.mode === m;
|
|
29
|
+
return '<button type="button" class="seg' + (on ? ' on' : '') + '" data-act="tasks-view" data-mode="' +
|
|
30
|
+
m + '" aria-pressed="' + on + '">' + (m === 'list' ? 'List' : 'Board') + '</button>';
|
|
31
|
+
}).join('');
|
|
32
|
+
return '<div class="tbar"><div class="tfilters" role="group" aria-label="Filter tasks">' + chips +
|
|
33
|
+
'</div><div class="tseg" role="group" aria-label="View">' + seg + '</div></div>';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function timeOf(t, fmt) {
|
|
37
|
+
if (t.status === 'in_progress' && t.startT) return fmt.dur(Studio.now() - t.startT);
|
|
38
|
+
return t.durationMs ? fmt.dur(t.durationMs) : '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Rows use data-tid, not data-task, so the nested Dispatch button gets its own click.
|
|
42
|
+
function listRow(model, t, fmt) {
|
|
43
|
+
var bucket = Studio.status.bucket(t);
|
|
44
|
+
var wait = Studio.status.waitText(t);
|
|
45
|
+
var id = ui.esc(t.id);
|
|
46
|
+
var go = Studio.status.isReady(t) ? '<button type="button" class="tdispatch" data-act="tasks-dispatch" data-tid="' +
|
|
47
|
+
id + '">Dispatch</button>' : '';
|
|
48
|
+
return '<div class="trow" role="row" tabindex="0" data-act="tasks-open" data-tid="' + id + '">' +
|
|
49
|
+
'<span class="c-id mono" role="cell">' + id + '</span>' +
|
|
50
|
+
'<span class="c-title" role="cell" title="' + ui.esc(t.title) + '">' + ui.esc(t.title) + '</span>' +
|
|
51
|
+
'<span class="c-agent" role="cell">' + ui.esc(Studio.agentInfo(t.agent).label) + '</span>' +
|
|
52
|
+
'<span class="c-status st-' + bucket + '" role="cell">' + Studio.status.word(bucket) +
|
|
53
|
+
(wait ? ' <span class="faint">' + ui.esc(wait) + '</span>' : '') + go + '</span>' +
|
|
54
|
+
'<span class="c-time mono" role="cell">' + ui.esc(timeOf(t, fmt)) + '</span></div>';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isOpen(m) {
|
|
58
|
+
if (view.groups[m.id] != null) return view.groups[m.id];
|
|
59
|
+
return !(m.total > 0 && m.done === m.total);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
var THEAD = '<div class="trow thead" role="row"><span role="columnheader">Id</span>' +
|
|
63
|
+
'<span role="columnheader">Task</span><span class="c-agent" role="columnheader">Agent</span>' +
|
|
64
|
+
'<span role="columnheader">Status</span><span class="c-time" role="columnheader">Time</span></div>';
|
|
65
|
+
|
|
66
|
+
function group(model, m, rows, fmt) {
|
|
67
|
+
var open = isOpen(m);
|
|
68
|
+
var body = open ? THEAD + rows.map(function (t) { return listRow(model, t, fmt); }).join('') : '';
|
|
69
|
+
return '<section class="tgroup" role="table" aria-label="' + ui.esc(m.name) + '">' +
|
|
70
|
+
'<button type="button" class="tg-head" data-act="tasks-group" data-group="' + ui.esc(m.id) +
|
|
71
|
+
'" aria-expanded="' + open + '"><span class="chev">' + ui.icon('chev', 2) + '</span>' +
|
|
72
|
+
'<span class="mono faint">' + ui.esc(m.id) + '</span><span class="t" title="' + ui.esc(m.name) + '">' + ui.esc(m.name) + '</span>' +
|
|
73
|
+
'<span class="num faint">' + m.done + ' of ' + m.total + '</span></button>' + body + '</section>';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function list(model, fmt) {
|
|
77
|
+
var html = model.milestones.map(function (m) {
|
|
78
|
+
var rows = m.tasks.filter(function (t) { return matches(model, t, view.filter); });
|
|
79
|
+
return rows.length ? group(model, m, rows, fmt) : '';
|
|
80
|
+
}).join('');
|
|
81
|
+
if (html) return '<div class="tlist">' + html + '</div>';
|
|
82
|
+
return '<div class="tempty"><p>No tasks match this filter.</p>' +
|
|
83
|
+
'<button type="button" class="btn" data-act="tasks-clear">Clear filter</button></div>';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function skeleton() {
|
|
87
|
+
var row = '<div class="trow tskel"><span></span><span></span><span></span><span></span><span></span></div>';
|
|
88
|
+
return '<div class="panel-head"><div class="panel-title"><h1>Tasks</h1></div></div>' +
|
|
89
|
+
'<div class="tlist">' + row + row + row + row + row + '</div>';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The pulse strip leaves a filter on Studio.taskFilter; use it once.
|
|
93
|
+
function takeFilter() {
|
|
94
|
+
var f = Studio.taskFilter;
|
|
95
|
+
if (f && FILTERS.some(function (x) { return x[0] === f; })) view.filter = f;
|
|
96
|
+
Studio.taskFilter = null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function onlyFiltered(model, m) {
|
|
100
|
+
var copy = Object.assign({}, m);
|
|
101
|
+
copy.tasks = m.tasks.filter(function (t) { return matches(model, t, view.filter); });
|
|
102
|
+
return copy;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (typeof document !== 'undefined' && document.addEventListener) {
|
|
106
|
+
document.addEventListener('keydown', function (e) {
|
|
107
|
+
var el = e.target;
|
|
108
|
+
if (e.key !== 'Enter' || !el || !el.matches || !el.matches('.trow[data-tid]')) return;
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
Studio.openTask(el.getAttribute('data-tid'));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
5
113
|
|
|
6
114
|
function head(m) {
|
|
7
115
|
var lead = m ? ui.esc(m.name) : 'The work of one milestone, by state.';
|
|
8
|
-
return '<div class="panel-head"><div class="panel-title"><h1>Tasks</h1><p class="lead">' + lead + '</p></div>' +
|
|
116
|
+
return '<div class="panel-head"><div class="panel-title"><h1>Tasks</h1><p class="lead" title="' + lead + '">' + lead + '</p></div>' +
|
|
9
117
|
'<div class="panel-actions">' + ui.milestoneSelect() + '</div></div>';
|
|
10
118
|
}
|
|
11
119
|
|
|
@@ -13,7 +121,8 @@ export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
|
13
121
|
|
|
14
122
|
function metaHtml(t, fmt) {
|
|
15
123
|
if (t.status === 'completed') {
|
|
16
|
-
|
|
124
|
+
var took = t.durationMs ? '<span>took ' + ui.esc(fmt.dur(t.durationMs)) + '</span>' : '';
|
|
125
|
+
return '<span>' + ui.esc(fmt.ago(t.endT)) + '</span>' + took;
|
|
17
126
|
}
|
|
18
127
|
if (t.status === 'in_progress') {
|
|
19
128
|
var live = t.startT ? '<span data-since="' + t.startT + '">' + fmt.dur(Studio.now() - t.startT) + '</span>' : '';
|
|
@@ -22,7 +131,7 @@ export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
|
22
131
|
if (t.status === 'blocked') {
|
|
23
132
|
return '<span class="bad">Paused after ' + (t.attempt || t.maxAttempts) + ' of ' + t.maxAttempts + ' attempts</span>';
|
|
24
133
|
}
|
|
25
|
-
if (t.waitingOn.length) return '<span>
|
|
134
|
+
if (t.waitingOn.length && !t.stale) return '<span>Waits for ' + t.waitingOn.map(ui.esc).join(', ') + '</span>';
|
|
26
135
|
return '<span>Ready to start</span>';
|
|
27
136
|
}
|
|
28
137
|
|
|
@@ -32,8 +141,9 @@ export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
|
32
141
|
'<div class="title">' + ui.esc(t.title) + '</div><div class="meta">' + metaHtml(t, fmt) + '</div></button>';
|
|
33
142
|
}
|
|
34
143
|
|
|
35
|
-
function column(label, status, list, shown,
|
|
36
|
-
var body = shown.length ? shown.map(function (t) { return card(t, fmt); }).join('')
|
|
144
|
+
function column(label, status, list, shown, extra, fmt) {
|
|
145
|
+
var body = shown.length ? shown.map(function (t) { return card(t, fmt); }).join('')
|
|
146
|
+
: '<p class="col-empty">None</p>';
|
|
37
147
|
return '<div class="col" data-col="' + status + '"><div class="col-head">' + ui.statusIcon(status) + label +
|
|
38
148
|
'<span class="count">' + list.length + '</span></div>' + body + (extra || '') + '</div>';
|
|
39
149
|
}
|
|
@@ -44,12 +154,12 @@ export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
|
44
154
|
var shown = view.showAll ? done : done.slice(0, CAP);
|
|
45
155
|
var more = done.length > CAP
|
|
46
156
|
? '<button type="button" class="more" data-act="tasks-more">' + (view.showAll ? 'Show fewer' : 'Show all ' + done.length) + '</button>' : '';
|
|
47
|
-
var
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
column(
|
|
51
|
-
|
|
52
|
-
|
|
157
|
+
var lanes = [['Up next', 'pending', by('pending'), null], ['Running', 'in_progress', by('in_progress'), null],
|
|
158
|
+
['Done', 'completed', done, shown], ['Stuck', 'blocked', by('blocked'), null]];
|
|
159
|
+
var cols = lanes.map(function (l) {
|
|
160
|
+
return column(l[0], l[1], l[2], l[3] || l[2], l[1] === 'completed' ? more : '', fmt);
|
|
161
|
+
}).join('');
|
|
162
|
+
return '<div class="board">' + cols + '</div>';
|
|
53
163
|
}
|
|
54
164
|
|
|
55
165
|
function histRow(m, fmt) {
|
|
@@ -76,15 +186,37 @@ export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
|
76
186
|
|
|
77
187
|
Studio.registerPage('tasks', {
|
|
78
188
|
deps: ['tasks', 'runs', 'escalations'],
|
|
189
|
+
skeleton: skeleton,
|
|
79
190
|
render: function (ctx) {
|
|
80
191
|
var model = ctx.model;
|
|
192
|
+
if (!view.mode) view.mode = ctx.state.tasksView || readMode();
|
|
193
|
+
takeFilter();
|
|
194
|
+
if (!model.milestones.length) {
|
|
195
|
+
return head(null) + '<div class="tempty"><p>No plan yet. Ask the architect to design one.</p></div>';
|
|
196
|
+
}
|
|
197
|
+
if (view.mode === 'list') return head(null) + toolbar(model) + list(model, ctx.fmt);
|
|
81
198
|
var wanted = ctx.state.milestoneId || model.activeMilestoneId;
|
|
82
199
|
var m = model.milestones.filter(function (x) { return x.id === wanted; })[0];
|
|
83
200
|
if (!m) return head(null) + ui.empty('No milestones yet', 'Add tasks to the plan to see them here.');
|
|
84
201
|
if (view.lastMs !== m.id) { view.lastMs = m.id; view.showAll = false; }
|
|
85
|
-
return head(m) + board(m, ctx.fmt) + others(model, m, ctx.fmt);
|
|
202
|
+
return head(m) + toolbar(model) + board(onlyFiltered(model, m), ctx.fmt) + others(model, m, ctx.fmt);
|
|
86
203
|
},
|
|
87
204
|
actions: {
|
|
205
|
+
'tasks-view': function (el) {
|
|
206
|
+
view.mode = el.getAttribute('data-mode');
|
|
207
|
+
saveMode(view.mode);
|
|
208
|
+
Studio.repaint('tasks');
|
|
209
|
+
},
|
|
210
|
+
'tasks-filter': function (el) { view.filter = el.getAttribute('data-filter'); Studio.repaint('tasks'); },
|
|
211
|
+
'tasks-clear': function () { view.filter = 'all'; Studio.repaint('tasks'); },
|
|
212
|
+
'tasks-open': function (el) { Studio.openTask(el.getAttribute('data-tid')); },
|
|
213
|
+
'tasks-dispatch': function (el) { Studio.taskAction('dispatch', el.getAttribute('data-tid')); },
|
|
214
|
+
'tasks-group': function (el, ctx) {
|
|
215
|
+
var id = el.getAttribute('data-group');
|
|
216
|
+
var m = ctx.model.milestones.filter(function (x) { return x.id === id; })[0];
|
|
217
|
+
view.groups[id] = !(m && isOpen(m));
|
|
218
|
+
Studio.repaint('tasks');
|
|
219
|
+
},
|
|
88
220
|
'tasks-more': function () { view.showAll = !view.showAll; Studio.repaint('tasks'); },
|
|
89
221
|
'tasks-hist': function (el) {
|
|
90
222
|
var id = el.getAttribute('data-ms-id');
|