@njeybe/nativ 2.6.0 → 2.7.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 +35 -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/index.js +11 -0
- package/dist/server/studio-ui/pages/benchmarks/styles.js +8 -5
- package/dist/server/studio-ui/pages/database/browser.js +1 -1
- package/dist/server/studio-ui/pages/database/connection.js +1 -1
- package/dist/server/studio-ui/pages/database/data-shell.js +5 -5
- package/dist/server/studio-ui/pages/database/drift.js +1 -1
- package/dist/server/studio-ui/pages/database/explorer.js +2 -2
- package/dist/server/studio-ui/pages/database/styles.js +5 -5
- package/dist/server/studio-ui/pages/flow/styles.js +3 -3
- package/dist/server/studio-ui/pages/home/script.js +110 -91
- package/dist/server/studio-ui/pages/home/styles.js +33 -45
- package/dist/server/studio-ui/pages/tasks/script.js +150 -3
- package/dist/server/studio-ui/pages/tasks/styles.js +63 -6
- package/dist/server/studio-ui/pages/team/script.js +49 -52
- package/dist/server/studio-ui/pages/team/styles.js +21 -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 +61 -2
- package/dist/server/studio-ui/shell/core.js +8 -8
- package/dist/server/studio-ui/shell/detail-view.js +1 -1
- package/dist/server/studio-ui/shell/dispatch.js +18 -0
- package/dist/server/studio-ui/shell/events-tools.js +5 -1
- package/dist/server/studio-ui/shell/format.js +5 -6
- 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 +19 -10
- package/dist/server/studio-ui/shell/paint.js +3 -0
- package/dist/server/studio-ui/shell/pipeline-state.js +1 -1
- 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 +67 -0
- package/dist/server/studio-ui/shell/router.js +25 -0
- package/dist/server/studio-ui/shell/sidebar-view.js +6 -3
- package/dist/server/studio-ui/shell/toasts-format.js +4 -4
- package/dist/server/studio-ui/styles/console.js +33 -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 +24 -5
- 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 +10 -11
- package/templates/GEMINI.md +3 -2
- package/templates/claude-agents/architect.md +10 -11
- 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 +6 -0
- 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,7 +1,132 @@
|
|
|
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'], ['running', 'Running'], ['ready', 'Ready'], ['blocked', 'Blocked'],
|
|
7
|
+
['done', 'Done']];
|
|
8
|
+
var DOT = { completed: 'ok', in_progress: 'run', blocked: 'bad', ready: 'fg', waiting: 'faint' };
|
|
9
|
+
var LABEL = { completed: 'Done', in_progress: 'Running', blocked: 'Blocked', ready: 'Ready',
|
|
10
|
+
waiting: 'Waiting' };
|
|
11
|
+
|
|
12
|
+
function readMode() {
|
|
13
|
+
try { return localStorage.getItem(KEY) === 'board' ? 'board' : 'list'; } catch (e) { return 'list'; }
|
|
14
|
+
}
|
|
15
|
+
function saveMode(mode) {
|
|
16
|
+
try { localStorage.setItem(KEY, mode); } catch (e) { /* private mode */ }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Same notion of ready as Home and the sidebar: pending and every dependency done.
|
|
20
|
+
function isReady(model, t) {
|
|
21
|
+
if (t.status !== 'pending') return false;
|
|
22
|
+
return t.isAvailable || t.deps.every(function (d) {
|
|
23
|
+
var dep = model.byId && model.byId[d];
|
|
24
|
+
return !dep || dep.status === 'completed';
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function kindOf(model, t) {
|
|
28
|
+
if (t.status === 'pending') return isReady(model, t) ? 'ready' : 'waiting';
|
|
29
|
+
return t.status;
|
|
30
|
+
}
|
|
31
|
+
function matches(model, t, f) {
|
|
32
|
+
if (f === 'all') return true;
|
|
33
|
+
if (f === 'running') return t.status === 'in_progress';
|
|
34
|
+
if (f === 'blocked') return t.status === 'blocked';
|
|
35
|
+
if (f === 'done') return t.status === 'completed';
|
|
36
|
+
return isReady(model, t);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toolbar(model) {
|
|
40
|
+
var chips = FILTERS.map(function (f) {
|
|
41
|
+
var on = view.filter === f[0];
|
|
42
|
+
var n = model.tasks.filter(function (t) { return matches(model, t, f[0]); }).length;
|
|
43
|
+
return '<button type="button" class="tf' + (on ? ' on' : '') + '" data-act="tasks-filter" data-filter="' +
|
|
44
|
+
f[0] + '" aria-pressed="' + on + '">' + f[1] + ' <span class="num">' + n + '</span></button>';
|
|
45
|
+
}).join('');
|
|
46
|
+
var seg = ['list', 'board'].map(function (m) {
|
|
47
|
+
var on = view.mode === m;
|
|
48
|
+
return '<button type="button" class="seg' + (on ? ' on' : '') + '" data-act="tasks-view" data-mode="' +
|
|
49
|
+
m + '" aria-pressed="' + on + '">' + (m === 'list' ? 'List' : 'Board') + '</button>';
|
|
50
|
+
}).join('');
|
|
51
|
+
return '<div class="tbar"><div class="tfilters" role="group" aria-label="Filter tasks">' + chips +
|
|
52
|
+
'</div><div class="tseg" role="group" aria-label="View">' + seg + '</div></div>';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function timeOf(t, fmt) {
|
|
56
|
+
if (t.status === 'in_progress' && t.startT) return fmt.dur(Studio.now() - t.startT);
|
|
57
|
+
return t.durationMs ? fmt.dur(t.durationMs) : '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Rows use data-tid, not data-task, so the nested Dispatch button gets its own click.
|
|
61
|
+
function listRow(model, t, fmt) {
|
|
62
|
+
var k = kindOf(model, t);
|
|
63
|
+
var id = ui.esc(t.id);
|
|
64
|
+
var go = k === 'ready' ? '<button type="button" class="tdispatch" data-act="tasks-dispatch" data-tid="' +
|
|
65
|
+
id + '">Dispatch</button>' : '';
|
|
66
|
+
return '<div class="trow" role="row" tabindex="0" data-act="tasks-open" data-tid="' + id + '">' +
|
|
67
|
+
'<span class="c-id mono" role="cell">' + id + '</span>' +
|
|
68
|
+
'<span class="c-title" role="cell" title="' + ui.esc(t.title) + '">' + ui.esc(t.title) + '</span>' +
|
|
69
|
+
'<span class="c-agent" role="cell">' + ui.esc(t.agent || '') + '</span>' +
|
|
70
|
+
'<span class="c-status st-' + DOT[k] + '" role="cell"><i class="dot"></i>' + LABEL[k] + go + '</span>' +
|
|
71
|
+
'<span class="c-time mono" role="cell">' + ui.esc(timeOf(t, fmt)) + '</span></div>';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isOpen(m) {
|
|
75
|
+
if (view.groups[m.id] != null) return view.groups[m.id];
|
|
76
|
+
return !(m.total > 0 && m.done === m.total);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
var THEAD = '<div class="trow thead" role="row"><span role="columnheader">Id</span>' +
|
|
80
|
+
'<span role="columnheader">Task</span><span class="c-agent" role="columnheader">Agent</span>' +
|
|
81
|
+
'<span role="columnheader">Status</span><span class="c-time" role="columnheader">Time</span></div>';
|
|
82
|
+
|
|
83
|
+
function group(model, m, rows, fmt) {
|
|
84
|
+
var open = isOpen(m);
|
|
85
|
+
var body = open ? THEAD + rows.map(function (t) { return listRow(model, t, fmt); }).join('') : '';
|
|
86
|
+
return '<section class="tgroup" role="table" aria-label="' + ui.esc(m.name) + '">' +
|
|
87
|
+
'<button type="button" class="tg-head" data-act="tasks-group" data-group="' + ui.esc(m.id) +
|
|
88
|
+
'" aria-expanded="' + open + '"><span class="chev">' + ui.icon('chev', 2) + '</span>' +
|
|
89
|
+
'<span class="mono faint">' + ui.esc(m.id) + '</span><span class="t">' + ui.esc(m.name) + '</span>' +
|
|
90
|
+
'<span class="num faint">' + m.done + ' of ' + m.total + '</span></button>' + body + '</section>';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function list(model, fmt) {
|
|
94
|
+
var html = model.milestones.map(function (m) {
|
|
95
|
+
var rows = m.tasks.filter(function (t) { return matches(model, t, view.filter); });
|
|
96
|
+
return rows.length ? group(model, m, rows, fmt) : '';
|
|
97
|
+
}).join('');
|
|
98
|
+
if (html) return '<div class="tlist">' + html + '</div>';
|
|
99
|
+
return '<div class="tempty"><p>No tasks match this filter.</p>' +
|
|
100
|
+
'<button type="button" class="btn" data-act="tasks-clear">Clear filter</button></div>';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function skeleton() {
|
|
104
|
+
var row = '<div class="trow tskel"><span></span><span></span><span></span><span></span><span></span></div>';
|
|
105
|
+
return '<div class="panel-head"><div class="panel-title"><h1>Tasks</h1></div></div>' +
|
|
106
|
+
'<div class="tlist">' + row + row + row + row + row + '</div>';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The pulse strip leaves a filter on Studio.taskFilter; use it once.
|
|
110
|
+
function takeFilter() {
|
|
111
|
+
var f = Studio.taskFilter;
|
|
112
|
+
if (f && FILTERS.some(function (x) { return x[0] === f; })) view.filter = f;
|
|
113
|
+
Studio.taskFilter = null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function onlyFiltered(model, m) {
|
|
117
|
+
var copy = Object.assign({}, m);
|
|
118
|
+
copy.tasks = m.tasks.filter(function (t) { return matches(model, t, view.filter); });
|
|
119
|
+
return copy;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (typeof document !== 'undefined' && document.addEventListener) {
|
|
123
|
+
document.addEventListener('keydown', function (e) {
|
|
124
|
+
var el = e.target;
|
|
125
|
+
if (e.key !== 'Enter' || !el || !el.matches || !el.matches('.trow[data-tid]')) return;
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
Studio.taskAction('logs', el.getAttribute('data-tid'));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
5
130
|
|
|
6
131
|
function head(m) {
|
|
7
132
|
var lead = m ? ui.esc(m.name) : 'The work of one milestone, by state.';
|
|
@@ -76,15 +201,37 @@ export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
|
76
201
|
|
|
77
202
|
Studio.registerPage('tasks', {
|
|
78
203
|
deps: ['tasks', 'runs', 'escalations'],
|
|
204
|
+
skeleton: skeleton,
|
|
79
205
|
render: function (ctx) {
|
|
80
206
|
var model = ctx.model;
|
|
207
|
+
if (!view.mode) view.mode = ctx.state.tasksView || readMode();
|
|
208
|
+
takeFilter();
|
|
209
|
+
if (!model.milestones.length) {
|
|
210
|
+
return head(null) + '<div class="tempty"><p>No plan yet. Ask the architect to design one.</p></div>';
|
|
211
|
+
}
|
|
212
|
+
if (view.mode === 'list') return head(null) + toolbar(model) + list(model, ctx.fmt);
|
|
81
213
|
var wanted = ctx.state.milestoneId || model.activeMilestoneId;
|
|
82
214
|
var m = model.milestones.filter(function (x) { return x.id === wanted; })[0];
|
|
83
215
|
if (!m) return head(null) + ui.empty('No milestones yet', 'Add tasks to the plan to see them here.');
|
|
84
216
|
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);
|
|
217
|
+
return head(m) + toolbar(model) + board(onlyFiltered(model, m), ctx.fmt) + others(model, m, ctx.fmt);
|
|
86
218
|
},
|
|
87
219
|
actions: {
|
|
220
|
+
'tasks-view': function (el) {
|
|
221
|
+
view.mode = el.getAttribute('data-mode');
|
|
222
|
+
saveMode(view.mode);
|
|
223
|
+
Studio.repaint('tasks');
|
|
224
|
+
},
|
|
225
|
+
'tasks-filter': function (el) { view.filter = el.getAttribute('data-filter'); Studio.repaint('tasks'); },
|
|
226
|
+
'tasks-clear': function () { view.filter = 'all'; Studio.repaint('tasks'); },
|
|
227
|
+
'tasks-open': function (el) { Studio.taskAction('logs', el.getAttribute('data-tid')); },
|
|
228
|
+
'tasks-dispatch': function (el) { Studio.taskAction('dispatch', el.getAttribute('data-tid')); },
|
|
229
|
+
'tasks-group': function (el, ctx) {
|
|
230
|
+
var id = el.getAttribute('data-group');
|
|
231
|
+
var m = ctx.model.milestones.filter(function (x) { return x.id === id; })[0];
|
|
232
|
+
view.groups[id] = !(m && isOpen(m));
|
|
233
|
+
Studio.repaint('tasks');
|
|
234
|
+
},
|
|
88
235
|
'tasks-more': function () { view.showAll = !view.showAll; Studio.repaint('tasks'); },
|
|
89
236
|
'tasks-hist': function (el) {
|
|
90
237
|
var id = el.getAttribute('data-ms-id');
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// Board and history styles, scoped under the Tasks panel.
|
|
1
|
+
// List, Board and history styles, scoped under the Tasks panel (Ink: 6px radius, 1px dividers).
|
|
2
2
|
export const tasksCss = String.raw `#panel-tasks .board { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; align-items: start; }
|
|
3
|
-
#panel-tasks .col { background: var(--surface-2); border-radius:
|
|
3
|
+
#panel-tasks .col { background: var(--surface-2); border-radius: 6px; padding: 8px; display: flex; flex-direction: column; gap: 8px; min-width: 0; }
|
|
4
4
|
#panel-tasks .col-head { display: flex; align-items: center; gap: 8px; padding: 4px 6px 2px; font-weight: 500; font-size: 13px; }
|
|
5
5
|
#panel-tasks .col-head .count { margin-left: auto; background: var(--surface); border-radius: 999px; padding: 0 8px; font-size: 12px; color: var(--fg-2); }
|
|
6
|
-
#panel-tasks .col-empty { font-size: 12.5px; color: var(--fg-3); padding: 14px 8px; text-align: center; border: 1px dashed var(--line-2); border-radius:
|
|
7
|
-
#panel-tasks .tcard { display: flex; flex-direction: column; gap: 6px; background: var(--surface); border: 1px solid var(--line); border-radius:
|
|
8
|
-
#panel-tasks .tcard:hover { border-color: var(--line-2);
|
|
6
|
+
#panel-tasks .col-empty { font-size: 12.5px; color: var(--fg-3); padding: 14px 8px; text-align: center; border: 1px dashed var(--line-2); border-radius: 6px; }
|
|
7
|
+
#panel-tasks .tcard { display: flex; flex-direction: column; gap: 6px; background: var(--surface); border: 1px solid var(--line); border-radius: 6px; padding: 10px; text-align: left; width: 100%; cursor: pointer; }
|
|
8
|
+
#panel-tasks .tcard:hover { border-color: var(--line-2); }
|
|
9
9
|
#panel-tasks .tcard.in_progress { border-color: color-mix(in srgb, var(--run) 45%, var(--line)); }
|
|
10
10
|
#panel-tasks .tcard.blocked { border-color: color-mix(in srgb, var(--bad) 45%, var(--line)); }
|
|
11
11
|
#panel-tasks .tcard .l1 { display: flex; align-items: center; gap: 6px; }
|
|
@@ -17,7 +17,7 @@ export const tasksCss = String.raw `#panel-tasks .board { display: grid; grid-te
|
|
|
17
17
|
#panel-tasks .more { border: 0; background: none; color: var(--accent); font-size: 12.5px; padding: 6px; cursor: pointer; text-align: center; }
|
|
18
18
|
#panel-tasks .more:hover { text-decoration: underline; }
|
|
19
19
|
#panel-tasks .section-title { margin: 22px 0 8px; font-size: 15px; font-weight: 600; }
|
|
20
|
-
#panel-tasks .hist { background: var(--surface); border: 1px solid var(--line); border-radius:
|
|
20
|
+
#panel-tasks .hist { background: var(--surface); border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
|
|
21
21
|
#panel-tasks .hist-row { display: grid; grid-template-columns: 16px 34px minmax(0, 1fr) auto 70px 90px 60px; 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; }
|
|
22
22
|
#panel-tasks .hist > div:first-child .hist-row { border-top: 0; }
|
|
23
23
|
#panel-tasks .hist-row:hover { background: var(--surface-2); }
|
|
@@ -28,6 +28,63 @@ export const tasksCss = String.raw `#panel-tasks .board { display: grid; grid-te
|
|
|
28
28
|
#panel-tasks .hist-row .small { font-size: 12px; }
|
|
29
29
|
#panel-tasks .hist-tasks { background: var(--surface-2); }
|
|
30
30
|
#panel-tasks .hist-tasks .row-btn { padding-left: 46px; }
|
|
31
|
+
#panel-tasks .tbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
32
|
+
#panel-tasks .tfilters, #panel-tasks .tseg { display: flex; gap: 4px; }
|
|
33
|
+
#panel-tasks .tseg { margin-left: auto; border: 1px solid var(--line); border-radius: 6px; padding: 2px; }
|
|
34
|
+
#panel-tasks .tf, #panel-tasks .seg {
|
|
35
|
+
border: 1px solid transparent; background: none; color: var(--fg-2); border-radius: 6px;
|
|
36
|
+
padding: 4px 10px; font-size: 12.5px; cursor: pointer;
|
|
37
|
+
}
|
|
38
|
+
#panel-tasks .tf { border-color: var(--line); }
|
|
39
|
+
#panel-tasks .tf .num { font-variant-numeric: tabular-nums; color: var(--fg-3); }
|
|
40
|
+
#panel-tasks .tf.on, #panel-tasks .seg.on { background: var(--surface-2); color: var(--fg); border-color: var(--line-2); }
|
|
41
|
+
#panel-tasks .tlist { border-top: 1px solid var(--line); }
|
|
42
|
+
#panel-tasks .tg-head {
|
|
43
|
+
display: flex; align-items: center; gap: 10px; width: 100%; height: 36px; padding: 0 8px; border: 0;
|
|
44
|
+
border-bottom: 1px solid var(--line); background: none; color: var(--fg); text-align: left;
|
|
45
|
+
cursor: pointer; font-weight: 600;
|
|
46
|
+
}
|
|
47
|
+
#panel-tasks .tg-head .t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
48
|
+
#panel-tasks .tg-head .num { font-variant-numeric: tabular-nums; font-weight: 400; font-size: 12px; }
|
|
49
|
+
#panel-tasks .tg-head .chev { display: inline-grid; transition: transform .15s; }
|
|
50
|
+
#panel-tasks .tg-head .chev svg { width: 14px; height: 14px; }
|
|
51
|
+
#panel-tasks .tg-head[aria-expanded="true"] .chev { transform: rotate(90deg); }
|
|
52
|
+
#panel-tasks .trow {
|
|
53
|
+
display: grid; grid-template-columns: 76px minmax(0, 1fr) 110px 150px 72px; gap: 12px;
|
|
54
|
+
align-items: center; height: 36px; padding: 0 8px; border-bottom: 1px solid var(--line);
|
|
55
|
+
font-size: 13px; cursor: pointer;
|
|
56
|
+
}
|
|
57
|
+
#panel-tasks .trow:hover, #panel-tasks .trow:focus-visible { background: var(--surface-2); }
|
|
58
|
+
#panel-tasks .trow:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
59
|
+
#panel-tasks .trow.thead { cursor: default; color: var(--fg-3); font-size: 12px; background: none; }
|
|
60
|
+
#panel-tasks .trow .mono { font-variant-numeric: tabular-nums; font-size: 12px; }
|
|
61
|
+
#panel-tasks .c-id { color: var(--fg-3); }
|
|
62
|
+
#panel-tasks .c-title, #panel-tasks .c-agent { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
63
|
+
#panel-tasks .c-agent { color: var(--fg-2); }
|
|
64
|
+
#panel-tasks .c-time { text-align: right; color: var(--fg-2); }
|
|
65
|
+
#panel-tasks .c-status { display: flex; align-items: center; gap: 6px; }
|
|
66
|
+
#panel-tasks .c-status .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
|
|
67
|
+
#panel-tasks .st-ok { color: var(--ok); }
|
|
68
|
+
#panel-tasks .st-run { color: var(--run); }
|
|
69
|
+
#panel-tasks .st-bad { color: var(--bad); }
|
|
70
|
+
#panel-tasks .st-fg { color: var(--fg); }
|
|
71
|
+
#panel-tasks .st-faint { color: var(--fg-3); }
|
|
72
|
+
#panel-tasks .tdispatch {
|
|
73
|
+
display: none; margin-left: auto; border: 1px solid var(--line-2); background: var(--surface);
|
|
74
|
+
color: var(--fg); border-radius: 6px; padding: 2px 8px; font-size: 12px; cursor: pointer;
|
|
75
|
+
}
|
|
76
|
+
#panel-tasks .trow:hover .tdispatch, #panel-tasks .trow:focus-within .tdispatch { display: inline-block; }
|
|
77
|
+
#panel-tasks .tskel span { height: 10px; border-radius: 6px; background: var(--surface-2); }
|
|
78
|
+
#panel-tasks .tempty { padding: 36px 16px; text-align: center; color: var(--fg-2); }
|
|
79
|
+
#panel-tasks .tempty .btn { margin-top: 8px; }
|
|
80
|
+
@media (max-width: 1024px) {
|
|
81
|
+
#panel-tasks .trow { grid-template-columns: 76px minmax(0, 1fr) 150px 72px; }
|
|
82
|
+
#panel-tasks .c-agent { display: none; }
|
|
83
|
+
}
|
|
84
|
+
@media (max-width: 768px) {
|
|
85
|
+
#panel-tasks .trow { grid-template-columns: 76px minmax(0, 1fr) 120px; }
|
|
86
|
+
#panel-tasks .c-time { display: none; }
|
|
87
|
+
}
|
|
31
88
|
@media (max-width: 1100px) { #panel-tasks .board { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
|
32
89
|
@media (max-width: 640px) {
|
|
33
90
|
#panel-tasks .board { grid-template-columns: minmax(0, 1fr); }
|
|
@@ -1,31 +1,25 @@
|
|
|
1
|
-
// Team page client script: one pure render, one
|
|
1
|
+
// Team page client script: one pure render, one roster row per agent.
|
|
2
2
|
export const teamScript = String.raw ` var esc = Studio.ui.esc;
|
|
3
|
-
var DASH = '
|
|
3
|
+
var DASH = '-';
|
|
4
4
|
|
|
5
5
|
function head() {
|
|
6
|
-
return '<div class="page-head"><div><h1>Team</h1><p>
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function sum(list) {
|
|
10
|
-
return list.reduce(function (total, n) { return total + n; }, 0);
|
|
6
|
+
return '<div class="page-head"><div><h1>Team</h1><p>Who is working on what, and how much each agent has finished.</p></div></div>';
|
|
11
7
|
}
|
|
12
8
|
|
|
13
9
|
function statsFor(key, tasks) {
|
|
14
10
|
var done = tasks.filter(function (t) { return t.status === 'completed'; });
|
|
15
11
|
var timed = done.filter(function (t) { return t.durationMs; });
|
|
16
|
-
var work = sum(timed.map(function (t) { return t.durationMs; }));
|
|
17
12
|
var costs = tasks.filter(function (t) { return t.costUsd != null; });
|
|
18
13
|
var stamps = tasks.map(function (t) { return t.status === 'in_progress' ? (t.startT || 0) : (t.endT || 0); });
|
|
19
14
|
return {
|
|
20
15
|
agent: key || null,
|
|
21
16
|
total: tasks.length,
|
|
22
17
|
done: done.length,
|
|
23
|
-
work: timed.length ?
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
work: timed.length ? timed.reduce(function (n, t) { return n + t.durationMs; }, 0) : null,
|
|
19
|
+
cost: costs.length ? costs.reduce(function (n, t) { return n + t.costUsd; }, 0) : null,
|
|
20
|
+
blocked: tasks.filter(function (t) { return t.status === 'blocked'; }).length,
|
|
26
21
|
last: Math.max.apply(null, [0].concat(stamps)) || null,
|
|
27
|
-
running: tasks.filter(function (t) { return t.status === 'in_progress'; })[0] || null
|
|
28
|
-
stuck: tasks.filter(function (t) { return t.status === 'blocked'; })[0] || null
|
|
22
|
+
running: tasks.filter(function (t) { return t.status === 'in_progress'; })[0] || null
|
|
29
23
|
};
|
|
30
24
|
}
|
|
31
25
|
|
|
@@ -41,71 +35,74 @@ export const teamScript = String.raw ` var esc = Studio.ui.esc;
|
|
|
41
35
|
.sort(function (a, b) { return b.total - a.total; });
|
|
42
36
|
}
|
|
43
37
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
function currentTask(r) {
|
|
39
|
+
var t = r.running;
|
|
40
|
+
if (!t) return '<span class="idle">Idle</span>';
|
|
41
|
+
return '<button type="button" class="cur" data-act="team-open" data-tid="' + esc(t.id) +
|
|
42
|
+
'" title="' + esc(t.title || t.id) + '"><span class="mono">' + esc(t.id) + '</span>' +
|
|
43
|
+
'<span class="ttl">' + esc(t.title || '') + '</span></button>';
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return '<span class="faint">Idle</span>';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function shareBar(r, max) {
|
|
56
|
-
var info = Studio.agentInfo(r.agent);
|
|
57
|
-
var pct = max ? Math.round((r.total / max) * 100) : 0;
|
|
58
|
-
return '<div class="bar"><i style="width:' + pct + '%;background:var(' + info.token + ')"></i></div>';
|
|
46
|
+
function rate(r) {
|
|
47
|
+
var fin = r.done + r.blocked;
|
|
48
|
+
return fin ? Math.round((r.done / fin) * 100) + '%' : DASH;
|
|
59
49
|
}
|
|
60
50
|
|
|
61
51
|
function lastActive(r) {
|
|
62
52
|
return r.last ? Studio.ui.when(r.last) : DASH;
|
|
63
53
|
}
|
|
64
54
|
|
|
65
|
-
function row(r
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
'<td
|
|
70
|
-
'<td class="r">' +
|
|
71
|
-
'<td class="r">' + (r.work == null ? DASH : esc(Studio.fmt.dur(r.work))) + '</td>' +
|
|
72
|
-
'<td class="r">' + (r.cost == null ? DASH : esc(Studio.fmt.money(r.cost))) + '</td>' +
|
|
73
|
-
'<td class="r
|
|
55
|
+
function row(r) {
|
|
56
|
+
var dot = r.running ? '<i class="run-dot"></i>' : '';
|
|
57
|
+
return '<tr data-agent="' + esc(r.agent || 'unassigned') + '"><td class="who">' + dot +
|
|
58
|
+
esc(r.agent || 'unassigned') + '</td>' +
|
|
59
|
+
'<td>' + currentTask(r) + '</td>' +
|
|
60
|
+
'<td class="mono r">' + r.done + '</td>' +
|
|
61
|
+
'<td class="mono r hide-sm">' + (r.work == null ? DASH : esc(Studio.fmt.dur(r.work))) + '</td>' +
|
|
62
|
+
'<td class="mono r hide-sm">' + (r.cost == null ? DASH : esc(Studio.fmt.money(r.cost))) + '</td>' +
|
|
63
|
+
'<td class="mono r hide-sm">' + rate(r) + '</td>' +
|
|
64
|
+
'<td class="mono r hide-sm">' + lastActive(r) + '</td></tr>';
|
|
74
65
|
}
|
|
75
66
|
|
|
76
67
|
function table(rows) {
|
|
77
|
-
var
|
|
78
|
-
|
|
79
|
-
var th = cols.map(function (c
|
|
80
|
-
var
|
|
81
|
-
return '<th' +
|
|
68
|
+
var cols = [['Agent', ''], ['Current task', ''], ['Tasks done', 'r'],
|
|
69
|
+
['Time', 'r hide-sm'], ['Cost', 'r hide-sm'], ['Success rate', 'r hide-sm'], ['Last active', 'r hide-sm']];
|
|
70
|
+
var th = cols.map(function (c) {
|
|
71
|
+
var tip = c[0] === 'Success rate' ? ' title="Completed / (completed + blocked)"' : '';
|
|
72
|
+
return '<th' + tip + (c[1] ? ' class="' + c[1] + '"' : '') + '>' + c[0] + '</th>';
|
|
82
73
|
}).join('');
|
|
83
|
-
return '<
|
|
84
|
-
rows.map(
|
|
74
|
+
return '<div class="roster"><table><thead><tr>' + th + '</tr></thead><tbody>' +
|
|
75
|
+
rows.map(row).join('') + '</tbody></table></div>' + defs();
|
|
85
76
|
}
|
|
86
77
|
|
|
87
78
|
function defs() {
|
|
88
|
-
|
|
89
|
-
'
|
|
90
|
-
'
|
|
91
|
-
'
|
|
92
|
-
|
|
79
|
+
var items = [['Tasks done', 'finished and checked by the gatekeeper.'],
|
|
80
|
+
['Time', 'total time on completed tasks that recorded timing.'],
|
|
81
|
+
['Cost', 'estimated from token counts; the real bill is on Home.'],
|
|
82
|
+
['Current task', 'the task this agent is working on right now.']];
|
|
83
|
+
return '<details class="defs"><summary>What these columns mean</summary>' + items.map(function (i) {
|
|
84
|
+
return '<div><b>' + i[0] + '</b><span>' + i[1] + '</span></div>';
|
|
85
|
+
}).join('') + '</details>';
|
|
93
86
|
}
|
|
94
87
|
|
|
95
88
|
function skeleton() {
|
|
96
|
-
var
|
|
97
|
-
|
|
98
|
-
|
|
89
|
+
var line = function (w) {
|
|
90
|
+
return '<tr><td colspan="6"><div class="skel" style="width:' + w + '"></div></td></tr>';
|
|
91
|
+
};
|
|
92
|
+
var rows = ['92%', '78%', '86%', '70%'].map(line).join('');
|
|
93
|
+
return head() + '<div class="roster"><table><thead>' + line('100%') + '</thead><tbody>' +
|
|
94
|
+
rows + '</tbody></table></div>';
|
|
99
95
|
}
|
|
100
96
|
|
|
101
97
|
function render(ctx) {
|
|
102
98
|
var rows = buildRows(ctx.model);
|
|
103
|
-
if (!rows.length) return head() + '<
|
|
104
|
-
return head() + table(rows)
|
|
99
|
+
if (!rows.length) return head() + '<p class="empty">No agents have worked on this project yet.</p>';
|
|
100
|
+
return head() + table(rows);
|
|
105
101
|
}
|
|
106
102
|
|
|
107
103
|
Studio.registerPage('team', {
|
|
108
104
|
deps: ['tasks', 'telemetry', 'runs'],
|
|
105
|
+
actions: { 'team-open': function (el) { Studio.taskAction('logs', el.getAttribute('data-tid')); } },
|
|
109
106
|
render: render,
|
|
110
107
|
skeleton: skeleton
|
|
111
108
|
});
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
// Team page styles, all scoped under #panel-team.
|
|
2
|
-
export const teamCss = String.raw `#panel-team .page-head {
|
|
3
|
-
#panel-team .page-head h1 { font-size: 22px; font-weight: 600; margin: 0; letter-spacing: -.01em; text-wrap: balance; }
|
|
1
|
+
// Team page styles, all scoped under #panel-team. Ink look: no cards, 1px dividers.
|
|
2
|
+
export const teamCss = String.raw `#panel-team .page-head h1 { font-size: 22px; font-weight: 600; margin: 0; letter-spacing: -.01em; text-wrap: balance; }
|
|
4
3
|
#panel-team .page-head p { margin: 4px 0 0; color: var(--fg-2); }
|
|
5
|
-
#panel-team .
|
|
6
|
-
#panel-team
|
|
7
|
-
#panel-team
|
|
8
|
-
#panel-team
|
|
9
|
-
#panel-team .
|
|
10
|
-
#panel-team
|
|
11
|
-
#panel-team
|
|
12
|
-
#panel-team
|
|
13
|
-
#panel-team
|
|
14
|
-
#panel-team
|
|
15
|
-
#panel-team .
|
|
16
|
-
#panel-team .
|
|
17
|
-
#panel-team .
|
|
18
|
-
#panel-team .
|
|
19
|
-
#panel-team .
|
|
20
|
-
#panel-team .defs { display:
|
|
21
|
-
#panel-team .defs b {
|
|
4
|
+
#panel-team .roster { margin-top: 16px; overflow-x: auto; }
|
|
5
|
+
#panel-team table { width: 100%; border-collapse: collapse; }
|
|
6
|
+
#panel-team th { text-align: left; font-weight: 500; font-size: 12px; color: var(--fg-3); height: 36px; padding: 0 12px; border-bottom: 1px solid var(--line); white-space: nowrap; }
|
|
7
|
+
#panel-team td { height: 36px; padding: 0 12px; border-bottom: 1px solid var(--line); white-space: nowrap; }
|
|
8
|
+
#panel-team .r { text-align: right; }
|
|
9
|
+
#panel-team .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
10
|
+
#panel-team .who { font-weight: 500; }
|
|
11
|
+
#panel-team .run-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--run); margin-right: 8px; }
|
|
12
|
+
#panel-team .idle { color: var(--fg-3); }
|
|
13
|
+
#panel-team .cur { display: inline-flex; gap: 8px; max-width: 360px; padding: 0; border: 0; background: none; color: var(--fg); font: inherit; cursor: pointer; }
|
|
14
|
+
#panel-team .cur:hover .ttl { text-decoration: underline; }
|
|
15
|
+
#panel-team .ttl { overflow: hidden; text-overflow: ellipsis; color: var(--fg-2); }
|
|
16
|
+
#panel-team .empty { margin: 24px 0; color: var(--fg-3); }
|
|
17
|
+
#panel-team .defs { margin-top: 24px; }
|
|
18
|
+
#panel-team .defs summary { cursor: pointer; color: var(--fg-2); height: 36px; line-height: 36px; }
|
|
19
|
+
#panel-team .defs div { display: flex; gap: 16px; height: 36px; align-items: center; border-bottom: 1px solid var(--line); }
|
|
20
|
+
#panel-team .defs b { width: 120px; font-weight: 500; }
|
|
21
|
+
#panel-team .defs span { color: var(--fg-2); }
|
|
22
|
+
@media (max-width: 1023px) { #panel-team .hide-sm { display: none; } }
|
|
22
23
|
`;
|
|
@@ -11,50 +11,62 @@ export const worktreesScript = String.raw ` // ─── View 3: Agent worktree
|
|
|
11
11
|
return task ? task.status : null;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function
|
|
15
|
-
if (!w.isAgentWorktree) return '
|
|
16
|
-
if (status === 'completed') return '
|
|
17
|
-
if (status === 'in_progress') return '
|
|
18
|
-
if (status === 'blocked') return '
|
|
19
|
-
if (status === 'pending') return '
|
|
20
|
-
return '
|
|
14
|
+
function wtStatus(w, status) {
|
|
15
|
+
if (!w.isAgentWorktree) return ['neutral', 'main'];
|
|
16
|
+
if (status === 'completed') return ['ok', 'ready to merge'];
|
|
17
|
+
if (status === 'in_progress') return ['run', 'agent working'];
|
|
18
|
+
if (status === 'blocked') return ['err', 'blocked'];
|
|
19
|
+
if (status === 'pending') return ['neutral', 'open'];
|
|
20
|
+
return ['neutral', 'stale'];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function wtChanges(w) {
|
|
24
|
+
var n = w.changedFiles != null ? w.changedFiles : w.files;
|
|
25
|
+
return n != null ? esc(String(n)) + ' files' : '<span class="muted">-</span>';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function wtActions(w, status) {
|
|
29
|
+
var id = esc(w.taskId);
|
|
30
|
+
var canMerge = w.mergeEligible != null ? !!w.mergeEligible : status === 'completed';
|
|
31
|
+
var why = w.mergeBlockedReason || 'Available once the task is completed';
|
|
32
|
+
var busy = pipe.busy['wt:' + w.taskId];
|
|
33
|
+
if (busy) {
|
|
34
|
+
return '<button class="btn small" type="button" disabled><span class="spinner" aria-hidden="true"></span>' +
|
|
35
|
+
(busy === 'merge' ? 'Merging...' : 'Deleting...') + '</button>';
|
|
36
|
+
}
|
|
37
|
+
return '<button class="btn small" type="button" data-act="wt-diff" data-key="' + id +
|
|
38
|
+
'" aria-label="Inspect diff for ' + id + '">View changes</button>' +
|
|
39
|
+
'<button class="btn small primary" type="button" data-act="wt-merge" data-key="' + id +
|
|
40
|
+
'" aria-label="Merge ' + id + ' to main"' + (canMerge ? '' : ' disabled title="' + esc(why) + '"') +
|
|
41
|
+
'>Merge</button>' +
|
|
42
|
+
'<button class="btn small danger" type="button" data-act="wt-remove" data-key="' + id +
|
|
43
|
+
'" aria-label="Delete workspace for ' + id + '">Remove</button>';
|
|
21
44
|
}
|
|
22
45
|
|
|
23
46
|
function worktreeRow(w) {
|
|
24
47
|
var status = worktreeTaskStatus(w);
|
|
25
48
|
var task = w.taskId ? taskById(w.taskId) : null;
|
|
26
|
-
|
|
27
|
-
var
|
|
28
|
-
var
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
'<button class="btn small primary" type="button" data-act="wt-merge" data-key="' + esc(w.taskId) + '"' + (canMerge ? '' : ' disabled title="' + esc(blockedWhy) + '"') + '>Merge to Main</button>' +
|
|
36
|
-
'<button class="btn small danger" type="button" data-act="wt-remove" data-key="' + esc(w.taskId) + '">Delete Workspace</button>';
|
|
37
|
-
}
|
|
38
|
-
return '<tr><td data-label="Branch"><code>' + esc(w.branch) + '</code></td>' +
|
|
39
|
-
'<td data-label="Task ID">' + (w.taskId ? '<div><code>' + esc(w.taskId) + '</code>' + (task ? '<div class="cell-sub" title="' + esc(task.title) + '">' + esc(task.title) + '</div>' : '') + '</div>' : '<span class="muted">–</span>') + '</td>' +
|
|
40
|
-
'<td data-label="Workspace Folder"><code class="path" title="' + esc(w.path) + '">' + esc(shortPath(w.path)) + '</code></td>' +
|
|
41
|
-
'<td data-label="Latest Commit"><code>' + esc(String(w.head || '').slice(0, 7) || '–') + '</code></td>' +
|
|
42
|
-
'<td data-label="Status">' + worktreeBadge(w, status) + '</td>' +
|
|
43
|
-
'<td data-label="Actions"><div class="row-actions">' + actions + '</div></td></tr>';
|
|
49
|
+
var st = wtStatus(w, status);
|
|
50
|
+
var actions = w.isAgentWorktree && w.taskId ? wtActions(w, status) : '<span class="muted">-</span>';
|
|
51
|
+
var title = task ? task.title : '';
|
|
52
|
+
return '<tr class="wt-row"><td class="wt-task">' + (w.taskId ? '<code>' + esc(w.taskId) + '</code>' : '') +
|
|
53
|
+
'<span class="wt-title" title="' + esc(title) + '">' + esc(title) + '</span></td>' +
|
|
54
|
+
'<td><code>' + esc(w.branch) + '</code></td>' +
|
|
55
|
+
'<td><span class="wt-dot" data-tone="' + st[0] + '"></span>' + esc(st[1]) + '</td>' +
|
|
56
|
+
'<td class="wt-num">' + wtChanges(w) + '</td>' +
|
|
57
|
+
'<td><div class="row-actions">' + actions + '</div></td></tr>';
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
function renderWorktrees() {
|
|
47
|
-
var head = panelHead('
|
|
48
|
-
if (!pipe.loaded.worktrees) return head + '<div class="
|
|
61
|
+
var head = panelHead('Worktrees', 'Isolated git workspaces for tasks.');
|
|
62
|
+
if (!pipe.loaded.worktrees) return head + '<div class="section" aria-hidden="true"><div class="skeleton"></div><div class="skeleton short"></div></div>';
|
|
49
63
|
if (pipe.errors.worktrees && !pipe.worktrees.length) return head + errorCard('worktrees', pipe.errors.worktrees, 'worktrees');
|
|
50
|
-
var rows = pipe.worktrees.
|
|
51
|
-
if (!rows.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
'<th scope="col">Branch Name</th><th scope="col">Task ID</th><th scope="col">Workspace Folder</th><th scope="col">Latest Commit</th><th scope="col">Status</th><th scope="col">Actions</th>' +
|
|
57
|
-
'</tr></thead><tbody>' + rows.map(worktreeRow).join('') + '</tbody></table></div>';
|
|
64
|
+
var rows = pipe.worktrees.filter(function (w) { return w.isAgentWorktree; });
|
|
65
|
+
if (!rows.length) return head + staleNote('worktrees') + '<div class="state"><p>No worktrees open.</p></div>';
|
|
66
|
+
return head + staleNote('worktrees') + '<table class="grid wt-table"><thead><tr>' +
|
|
67
|
+
'<th scope="col">Task</th><th scope="col">Branch</th><th scope="col">Status</th>' +
|
|
68
|
+
'<th scope="col">Changes</th><th scope="col">Actions</th>' +
|
|
69
|
+
'</tr></thead><tbody>' + rows.map(worktreeRow).join('') + '</tbody></table>';
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
function confirmWorktree(action, taskId) {
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
// Worktrees view styles.
|
|
2
|
-
export const worktreesCss = String.raw `/*
|
|
3
|
-
.
|
|
4
|
-
table.grid {
|
|
5
|
-
table.grid th { padding: 10px 16px; text-align: left; font-size: 12.5px; font-weight: 600; color: var(--muted); background: var(--bg); border-bottom: 1px solid var(--border); white-space: nowrap; }
|
|
2
|
+
export const worktreesCss = String.raw `/* Worktrees table */
|
|
3
|
+
table.grid { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
4
|
+
table.grid th { height: 32px; padding: 0 12px; text-align: left; font-size: 12px; font-weight: 500; color: var(--muted); border-bottom: 1px solid var(--border); white-space: nowrap; }
|
|
6
5
|
table.grid th:last-child { text-align: right; }
|
|
7
|
-
table.grid td { padding: 12px
|
|
8
|
-
table.grid
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
.
|
|
12
|
-
.
|
|
6
|
+
table.grid td { height: 36px; padding: 0 12px; border-bottom: 1px solid var(--border); vertical-align: middle; }
|
|
7
|
+
table.grid code { font-family: var(--mono); font-size: 12px; }
|
|
8
|
+
.wt-task { display: flex; align-items: center; gap: 8px; height: 36px; max-width: 320px; }
|
|
9
|
+
.wt-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }
|
|
10
|
+
.wt-num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
11
|
+
.wt-dot { display: inline-block; width: 7px; height: 7px; margin-right: 8px; border-radius: 50%; background: var(--muted); }
|
|
12
|
+
.wt-dot[data-tone="ok"] { background: var(--ok); }
|
|
13
|
+
.wt-dot[data-tone="err"] { background: var(--danger); }
|
|
14
|
+
.wt-dot[data-tone="run"] { background: var(--warn); }
|
|
13
15
|
.row-actions { display: flex; justify-content: flex-end; gap: 6px; }
|
|
14
16
|
|
|
15
17
|
`;
|