@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +71 -1
  3. package/dist/commands/design-check.d.ts +4 -0
  4. package/dist/commands/design-check.js +76 -0
  5. package/dist/commands/init.d.ts +1 -0
  6. package/dist/commands/init.js +53 -1
  7. package/dist/commands/update.js +46 -0
  8. package/dist/core/design-rules.d.ts +23 -0
  9. package/dist/core/design-rules.js +115 -0
  10. package/dist/core/harness-integrity.js +26 -7
  11. package/dist/index.js +11 -0
  12. package/dist/server/studio-ui/pages/benchmarks/styles.js +8 -5
  13. package/dist/server/studio-ui/pages/database/browser.js +1 -1
  14. package/dist/server/studio-ui/pages/database/connection.js +1 -1
  15. package/dist/server/studio-ui/pages/database/data-shell.js +5 -5
  16. package/dist/server/studio-ui/pages/database/drift.js +1 -1
  17. package/dist/server/studio-ui/pages/database/explorer.js +2 -2
  18. package/dist/server/studio-ui/pages/database/styles.js +5 -5
  19. package/dist/server/studio-ui/pages/flow/styles.js +3 -3
  20. package/dist/server/studio-ui/pages/home/script.js +110 -91
  21. package/dist/server/studio-ui/pages/home/styles.js +33 -45
  22. package/dist/server/studio-ui/pages/tasks/script.js +150 -3
  23. package/dist/server/studio-ui/pages/tasks/styles.js +63 -6
  24. package/dist/server/studio-ui/pages/team/script.js +49 -52
  25. package/dist/server/studio-ui/pages/team/styles.js +21 -20
  26. package/dist/server/studio-ui/pages/worktrees/script.js +47 -35
  27. package/dist/server/studio-ui/pages/worktrees/styles.js +12 -10
  28. package/dist/server/studio-ui/shell/brand.d.ts +4 -0
  29. package/dist/server/studio-ui/shell/brand.js +16 -0
  30. package/dist/server/studio-ui/shell/console.js +61 -2
  31. package/dist/server/studio-ui/shell/core.js +8 -8
  32. package/dist/server/studio-ui/shell/detail-view.js +1 -1
  33. package/dist/server/studio-ui/shell/dispatch.js +18 -0
  34. package/dist/server/studio-ui/shell/events-tools.js +5 -1
  35. package/dist/server/studio-ui/shell/format.js +5 -6
  36. package/dist/server/studio-ui/shell/icons.js +23 -15
  37. package/dist/server/studio-ui/shell/markup-head.js +11 -6
  38. package/dist/server/studio-ui/shell/markup-tools.js +19 -10
  39. package/dist/server/studio-ui/shell/paint.js +3 -0
  40. package/dist/server/studio-ui/shell/pipeline-state.js +1 -1
  41. package/dist/server/studio-ui/shell/proposals.js +6 -2
  42. package/dist/server/studio-ui/shell/pulse.d.ts +1 -0
  43. package/dist/server/studio-ui/shell/pulse.js +67 -0
  44. package/dist/server/studio-ui/shell/router.js +25 -0
  45. package/dist/server/studio-ui/shell/sidebar-view.js +6 -3
  46. package/dist/server/studio-ui/shell/toasts-format.js +4 -4
  47. package/dist/server/studio-ui/styles/console.js +33 -13
  48. package/dist/server/studio-ui/styles/dialogs.js +4 -4
  49. package/dist/server/studio-ui/styles/responsive.js +8 -0
  50. package/dist/server/studio-ui/styles/shell.js +24 -5
  51. package/dist/server/studio-ui/styles/strategist.js +10 -10
  52. package/dist/server/studio-ui/styles/tokens-base.js +23 -23
  53. package/dist/server/studio-ui.js +2 -0
  54. package/package.json +1 -1
  55. package/plugin/.claude-plugin/plugin.json +1 -1
  56. package/plugin/agents/architect.md +10 -11
  57. package/templates/GEMINI.md +3 -2
  58. package/templates/claude-agents/architect.md +10 -11
  59. package/templates/design/taste/LICENSE +21 -0
  60. package/templates/design/taste/SOURCE.md +28 -0
  61. package/templates/design/taste/v1/DIGEST.md +57 -0
  62. package/templates/design/taste/v1/SKILL.md +226 -0
  63. package/templates/design/taste/v2/DIGEST.md +62 -0
  64. package/templates/design/taste/v2/SKILL.md +1206 -0
  65. package/templates/dot-ai/subagents/architect.md +6 -0
  66. package/templates/dot-ai/subagents/flutter-developer.md +2 -11
  67. package/templates/dot-ai/subagents/frontend.md +47 -44
  68. package/templates/dot-ai/ui_specs.md +15 -36
@@ -1,7 +1,132 @@
1
- // Tasks page: one milestone as four columns, then the other milestones as a history list.
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: 12px; padding: 8px; display: flex; flex-direction: column; gap: 8px; min-width: 0; }
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: 8px; }
7
- #panel-tasks .tcard { display: flex; flex-direction: column; gap: 6px; background: var(--surface); border: 1px solid var(--line); border-radius: 9px; padding: 10px; text-align: left; width: 100%; cursor: pointer; }
8
- #panel-tasks .tcard:hover { border-color: var(--line-2); box-shadow: var(--shadow); }
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: 12px; overflow: hidden; }
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 table row per agent.
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>Each agent in plain terms: what it did, how long it took, what it cost.</p></div></div>';
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 ? work : null,
24
- avg: timed.length ? work / timed.length : null,
25
- cost: costs.length ? sum(costs.map(function (t) { return t.costUsd; })) : null,
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 chip(task, label) {
45
- return '<button type="button" class="chip" data-task="' + esc(task.id) + '">' +
46
- Studio.ui.statusIcon(task.status) + esc(label) + '</button>';
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 rightNow(r) {
50
- if (r.running) return chip(r.running, r.running.id);
51
- if (r.stuck) return chip(r.stuck, 'Stuck on ' + r.stuck.id);
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, max) {
66
- return '<tr data-agent="' + esc(r.agent || 'unassigned') + '"><td>' + Studio.ui.agentChip(r.agent) + '</td>' +
67
- '<td>' + rightNow(r) + '</td>' +
68
- '<td class="r">' + r.done + '<span class="faint"> / ' + r.total + '</span></td>' +
69
- '<td class="share">' + shareBar(r, max) + '</td>' +
70
- '<td class="r">' + (r.avg == null ? DASH : esc(Studio.fmt.dur(r.avg))) + '</td>' +
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 faint">' + lastActive(r) + '</td></tr>';
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 max = Math.max.apply(null, rows.map(function (r) { return r.total; }));
78
- var cols = ['Agent', 'Right now', 'Tasks done', 'Share of work', 'Avg. time', 'Total time', 'Est. cost', 'Last active'];
79
- var th = cols.map(function (c, i) {
80
- var cls = i === 3 ? ' class="share"' : (i > 1 ? ' class="r"' : '');
81
- return '<th' + cls + '>' + c + '</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 '<section class="panel table-wrap"><table><thead><tr>' + th + '</tr></thead><tbody>' +
84
- rows.map(function (r) { return row(r, max); }).join('') + '</tbody></table></section>';
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
- return '<section class="panel"><div class="panel-head"><h2>What these columns mean</h2></div><div class="panel-body defs">' +
89
- '<div><b>Tasks done</b>: finished and checked, out of all tasks given to this agent.</div>' +
90
- '<div><b>Avg. time</b>: from start to a passing check, for tasks that recorded timing.</div>' +
91
- '<div><b>Est. cost</b>: estimated from token counts. The real bill is on Home under Spend.</div>' +
92
- '<div><b>Right now</b>: the task this agent is working on, or the one it is stuck on.</div></div></section>';
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 bar = function (w) { return '<div class="skel" style="width:' + w + '"></div>'; };
97
- var block = '<div class="skel-block">' + bar('100%') + bar('92%') + bar('74%') + '</div>';
98
- return head() + block + block;
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() + '<section class="panel">' + ctx.ui.empty('No tasks yet', 'Agents show up here once the plan has tasks.') + '</section>';
104
- return head() + table(rows) + defs();
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 { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; }
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 .panel { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; min-width: 0; margin-top: 16px; }
6
- #panel-team .panel-head { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-bottom: 1px solid var(--line); }
7
- #panel-team .panel-head h2 { font-size: 14px; font-weight: 600; margin: 0; }
8
- #panel-team .panel-body { padding: 14px 16px; }
9
- #panel-team .table-wrap { overflow-x: auto; }
10
- #panel-team table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
11
- #panel-team th { text-align: left; font-weight: 500; font-size: 12px; color: var(--fg-3); padding: 10px 16px; border-bottom: 1px solid var(--line); white-space: nowrap; }
12
- #panel-team td { padding: 11px 16px; border-bottom: 1px solid var(--line); white-space: nowrap; }
13
- #panel-team tr:last-child td { border-bottom: 0; }
14
- #panel-team td.r, #panel-team th.r { text-align: right; }
15
- #panel-team .share { width: 110px; }
16
- #panel-team .bar { height: 6px; border-radius: 3px; background: var(--surface-2); overflow: hidden; }
17
- #panel-team .bar i { display: block; height: 100%; border-radius: 3px; }
18
- #panel-team .chip { display: inline-flex; align-items: center; gap: 6px; padding: 2px 10px 2px 6px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface); font: 12px var(--mono); cursor: pointer; }
19
- #panel-team .chip:hover { background: var(--surface-2); }
20
- #panel-team .defs { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px 24px; font-size: 13px; color: var(--fg-2); }
21
- #panel-team .defs b { color: var(--fg); font-weight: 500; }
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 worktreeBadge(w, status) {
15
- if (!w.isAgentWorktree) return '<span class="badge badge-neutral">Main workspace</span>';
16
- if (status === 'completed') return '<span class="badge badge-success">Tests Passed (Ready to merge)</span>';
17
- if (status === 'in_progress') return '<span class="badge badge-active">Agent working</span>';
18
- if (status === 'blocked') return '<span class="badge badge-danger">Blocked</span>';
19
- if (status === 'pending') return '<span class="badge badge-neutral">Pending</span>';
20
- return '<span class="badge badge-neutral" title="No matching task in the master plan">Untracked</span>';
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
- // The server decides merge eligibility (and why not); fall back to the task status from the plan.
27
- var canMerge = w.mergeEligible != null ? !!w.mergeEligible : status === 'completed';
28
- var blockedWhy = w.mergeBlockedReason || 'Available once the task is completed';
29
- var busy = w.taskId && pipe.busy['wt:' + w.taskId];
30
- var actions = '<span class="muted">–</span>';
31
- if (w.isAgentWorktree && w.taskId) {
32
- actions = busy
33
- ? '<button class="btn small" type="button" disabled><span class="spinner" aria-hidden="true"></span>' + (busy === 'merge' ? 'Merging…' : 'Deleting…') + '</button>'
34
- : '<button class="btn small" type="button" data-act="wt-diff" data-key="' + esc(w.taskId) + '" aria-label="' + esc('Inspect diff for ' + w.taskId) + '">Inspect Diff</button>' +
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('Agent Worktrees', 'Isolated git workspaces for tasks. Changes are tested automatically before merging into your main branch.');
48
- if (!pipe.loaded.worktrees) return head + '<div class="card section" aria-hidden="true"><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton short"></div></div>';
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.slice().sort(function (a, b) { return (a.isAgentWorktree ? 1 : 0) - (b.isAgentWorktree ? 1 : 0); });
51
- if (!rows.some(function (w) { return w.isAgentWorktree; })) {
52
- return head + staleNote('worktrees') + '<div class="card state"><div class="state-icon">' + ICON.branch + '</div><h2>No agent worktrees</h2>' +
53
- '<p>Run <code>nativ worktree create &lt;taskId&gt;</code> to give an agent its own isolated workspace folder for an independent task.</p></div>';
54
- }
55
- return head + staleNote('worktrees') + '<div class="card wt-card"><table class="grid"><thead><tr>' +
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 `/* Agent worktrees */
3
- .wt-card { overflow: hidden; }
4
- table.grid { width: 100%; border-collapse: collapse; font-size: 13.5px; }
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 16px; border-bottom: 1px solid var(--border); vertical-align: middle; }
8
- table.grid tbody tr:last-child td { border-bottom: none; }
9
- table.grid tbody tr:hover td { background: var(--bg); }
10
- table.grid code { font-size: 12.5px; }
11
- .cell-sub { max-width: 280px; margin-top: 2px; font-size: 13px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
12
- .path { display: inline-block; max-width: 260px; vertical-align: bottom; font-size: 13px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
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
  `;