@njeybe/nativ 2.7.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 +15 -0
- package/dist/core/setup/pm-playbook.d.ts +1 -1
- package/dist/core/setup/pm-playbook.js +1 -1
- package/dist/server/studio-ui/pages/benchmarks/script.js +9 -8
- package/dist/server/studio-ui/pages/benchmarks/styles.js +3 -1
- package/dist/server/studio-ui/pages/database/browser.js +1 -1
- package/dist/server/studio-ui/pages/database/connection.js +5 -4
- package/dist/server/studio-ui/pages/database/data-shell.js +8 -13
- package/dist/server/studio-ui/pages/database/drift.js +1 -1
- package/dist/server/studio-ui/pages/database/events.js +1 -2
- package/dist/server/studio-ui/pages/database/explorer.js +4 -4
- package/dist/server/studio-ui/pages/flow/layout.js +1 -1
- package/dist/server/studio-ui/pages/flow/styles.js +1 -1
- package/dist/server/studio-ui/pages/flow/timeline-view.js +1 -1
- package/dist/server/studio-ui/pages/home/script.js +26 -29
- package/dist/server/studio-ui/pages/home/styles.js +2 -6
- package/dist/server/studio-ui/pages/tasks/script.js +25 -40
- package/dist/server/studio-ui/pages/tasks/styles.js +9 -9
- package/dist/server/studio-ui/pages/team/script.js +27 -8
- package/dist/server/studio-ui/pages/team/styles.js +0 -1
- package/dist/server/studio-ui/shell/console.js +13 -7
- package/dist/server/studio-ui/shell/detail-view.js +3 -3
- package/dist/server/studio-ui/shell/dispatch.js +9 -8
- package/dist/server/studio-ui/shell/events-tools.js +11 -0
- package/dist/server/studio-ui/shell/format.js +31 -2
- package/dist/server/studio-ui/shell/markup-head.js +1 -1
- package/dist/server/studio-ui/shell/markup-tools.js +13 -8
- package/dist/server/studio-ui/shell/model.js +8 -2
- package/dist/server/studio-ui/shell/paint.js +1 -3
- package/dist/server/studio-ui/shell/pipeline-state.js +2 -11
- package/dist/server/studio-ui/shell/pulse.js +8 -7
- package/dist/server/studio-ui/shell/sidebar-view.js +2 -3
- package/dist/server/studio-ui/shell/triage.js +1 -1
- package/dist/server/studio-ui/styles/console.js +3 -0
- package/dist/server/studio-ui/styles/shell.js +4 -6
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/agents/architect.md +2 -2
- 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 +1 -1
- package/templates/claude-agents/architect.md +2 -2
- package/templates/claude-agents/verifier.md +4 -1
- package/templates/dot-ai/subagents/architect.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.8.0
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **One set of Studio state words.** Every page says Up next, Running, Done or Stuck. "Ready", "Blocked", "Waiting", "Just finished", "Idle" and "completed / started" are gone. A failed run shows as Stuck. A stopped run, or a claim older than 4 hours with no active run, shows as Up next.
|
|
8
|
+
- **Plain agent names.** Home, Tasks and Team show names such as "QA tester" instead of `qa-tester`. Team merges alias rows of the same agent.
|
|
9
|
+
- **Quieter shell.** The pulse strip shows only non-zero counts and hides when everything is zero. The dispatch button hides when nothing is ready instead of showing a grey "Nothing ready". "Abort" is now "Stop". The Benchmarks nav badge is gone. The workspace path no longer breaks mid-word.
|
|
10
|
+
- **Slimmer runner console.** Stop shows only while a run is active. Open diff, Clear and Auto-scroll sit in a More menu. An idle console hides its empty stepper and zero totals.
|
|
11
|
+
- **Tasks page.** A list row opens the task drawer, the same as a Board card. Filters with no tasks are hidden. The Board always shows its four lanes, and an empty lane says "None". The drawer has "Watch live", and "How it is checked" wraps.
|
|
12
|
+
- **Home.** Recent activity shows one row per task with its title and state, for example "Done, took 4m 00s".
|
|
13
|
+
- **Flow.** Plan cards show titles on up to two lines.
|
|
14
|
+
- **Database and Benchmarks.** "Not set up" is neutral, and red means only a configured connection that is offline. The header Connection Settings button is the single way in. Benchmarks lists scenarios that were not run on one line.
|
|
15
|
+
- **Option trade-offs.** The architect, the project manager and the shared directive add one line on when the non-recommended option would be the better choice.
|
|
16
|
+
- **Verifier report.** The verifier reports contract findings and code style findings in separate parts.
|
|
17
|
+
|
|
3
18
|
## 2.7.0
|
|
4
19
|
|
|
5
20
|
### Added
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* The project manager's playbook. The SessionStart hook prints it into the main session only:
|
|
3
3
|
* subagents load CLAUDE.md and AGENTS.md on every turn, so anything kept there is paid for by every worker.
|
|
4
4
|
*/
|
|
5
|
-
export declare const PM_PLAYBOOK = "## Working as Project Manager\n\n- Start each session with `nativ task next --json` (or the `nativ_task_next` MCP tool).\n- Delegate, do not implement. Hand each task to the `worker` agent by task id, passing the task's `recommendedModel` (haiku, sonnet or opus) as the Agent tool's `model`; with none, the worker's default applies. Ask the `verifier` agent for an independent check before you mark a milestone done. Ask the `architect` agent for anything that changes a contract.\n- Before the architect designs on existing code, run the `explorer` when `.ai/codebase_map.md` is missing or its commit is well behind `HEAD`, and pass its report to the architect. Give the explorer the current commit (`git rev-parse --short HEAD`); it cannot look it up. Pass `model: haiku` for a single lookup such as \"where is the auth middleware?\".\n- Keep usage low. Run this session on Sonnet. Agent defaults: architect Opus, worker Sonnet, verifier Haiku. Pass `haiku` for mechanical tasks (wording, docs, renames) and `sonnet` for the verifier at the end of a milestone. Never pass a model the human did not choose.\n- Independent tasks can run in parallel in worktrees (`nativ worktree create <taskId>`). On a Claude subscription every agent draws from the same usage limit, so run two or three at a time, not ten.\n- You may not edit `.ai/`. If the enforcement hook warns about a file, that is a scope or contract gap: escalate with `nativ task escalate`, do not edit around it.\n- `nativ doctor` confirms the integration is intact. Run it after updating Claude Code or nativ, and whenever hooks or MCP tools seem to be missing.\n\n## Autonomous execution\n\nWhen told to run autonomously (\"run the queue\", \"execute milestone\", AFK mode):\n1. `nativ_task_next`, then `nativ_task_start`.\n2. Delegate to the `worker` agent with the task id and recommended model.\n3. The worker completes the task itself; `nativ task complete` already ran the check. Do not run `nativ_verify` or `nativ_task_complete` again for it.\n4. Loop to the next task immediately, without asking the human.\n5. Stop only when the milestone is done (ask the `verifier` for a milestone check, then tell the human) or when a task is blocked or escalated (present the four-part summary).\n\n## Talking to the human\n\nThe human is the product owner, not a log reader. For a blocker, trade-off or escalation use four parts in plain words: what the user would see; why it happens, as simple cause and effect; what is affected and what is safe; two or three options with the best marked `(Recommended)`, then one direct question. Say \"the app tried to link a record to an item that does not exist yet\", not \"foreign key constraint violation\". Never paste stack traces; put raw output in a collapsed `<details>` block.";
|
|
5
|
+
export declare const PM_PLAYBOOK = "## Working as Project Manager\n\n- Start each session with `nativ task next --json` (or the `nativ_task_next` MCP tool).\n- Delegate, do not implement. Hand each task to the `worker` agent by task id, passing the task's `recommendedModel` (haiku, sonnet or opus) as the Agent tool's `model`; with none, the worker's default applies. Ask the `verifier` agent for an independent check before you mark a milestone done. Ask the `architect` agent for anything that changes a contract.\n- Before the architect designs on existing code, run the `explorer` when `.ai/codebase_map.md` is missing or its commit is well behind `HEAD`, and pass its report to the architect. Give the explorer the current commit (`git rev-parse --short HEAD`); it cannot look it up. Pass `model: haiku` for a single lookup such as \"where is the auth middleware?\".\n- Keep usage low. Run this session on Sonnet. Agent defaults: architect Opus, worker Sonnet, verifier Haiku. Pass `haiku` for mechanical tasks (wording, docs, renames) and `sonnet` for the verifier at the end of a milestone. Never pass a model the human did not choose.\n- Independent tasks can run in parallel in worktrees (`nativ worktree create <taskId>`). On a Claude subscription every agent draws from the same usage limit, so run two or three at a time, not ten.\n- You may not edit `.ai/`. If the enforcement hook warns about a file, that is a scope or contract gap: escalate with `nativ task escalate`, do not edit around it.\n- `nativ doctor` confirms the integration is intact. Run it after updating Claude Code or nativ, and whenever hooks or MCP tools seem to be missing.\n\n## Autonomous execution\n\nWhen told to run autonomously (\"run the queue\", \"execute milestone\", AFK mode):\n1. `nativ_task_next`, then `nativ_task_start`.\n2. Delegate to the `worker` agent with the task id and recommended model.\n3. The worker completes the task itself; `nativ task complete` already ran the check. Do not run `nativ_verify` or `nativ_task_complete` again for it.\n4. Loop to the next task immediately, without asking the human.\n5. Stop only when the milestone is done (ask the `verifier` for a milestone check, then tell the human) or when a task is blocked or escalated (present the four-part summary).\n\n## Talking to the human\n\nThe human is the product owner, not a log reader. For a blocker, trade-off or escalation use four parts in plain words: what the user would see; why it happens, as simple cause and effect; what is affected and what is safe; two or three options with the best marked `(Recommended)`, plus one line on when the other option would be the better choice, then one direct question. Say \"the app tried to link a record to an item that does not exist yet\", not \"foreign key constraint violation\". Never paste stack traces; put raw output in a collapsed `<details>` block.";
|
|
@@ -23,4 +23,4 @@ When told to run autonomously ("run the queue", "execute milestone", AFK mode):
|
|
|
23
23
|
|
|
24
24
|
## Talking to the human
|
|
25
25
|
|
|
26
|
-
The human is the product owner, not a log reader. For a blocker, trade-off or escalation use four parts in plain words: what the user would see; why it happens, as simple cause and effect; what is affected and what is safe; two or three options with the best marked \`(Recommended)\`, then one direct question. Say "the app tried to link a record to an item that does not exist yet", not "foreign key constraint violation". Never paste stack traces; put raw output in a collapsed \`<details>\` block.`;
|
|
26
|
+
The human is the product owner, not a log reader. For a blocker, trade-off or escalation use four parts in plain words: what the user would see; why it happens, as simple cause and effect; what is affected and what is safe; two or three options with the best marked \`(Recommended)\`, plus one line on when the other option would be the better choice, then one direct question. Say "the app tried to link a record to an item that does not exist yet", not "foreign key constraint violation". Never paste stack traces; put raw output in a collapsed \`<details>\` block.`;
|
|
@@ -34,9 +34,9 @@ export const benchmarksScript = String.raw ` // ─── View 4: Synthetic ben
|
|
|
34
34
|
if (env.cpuCount) meta.push(env.cpuCount + ' CPUs');
|
|
35
35
|
var score = clampPct(s.score);
|
|
36
36
|
return '<section class="card hero' + (running ? ' is-running' : '') + '" aria-labelledby="hero-h" aria-busy="' + running + '">' +
|
|
37
|
-
'<div><h2 class="
|
|
37
|
+
'<div><h2 class="hero-title" id="hero-h">Average throughput</h2>' +
|
|
38
38
|
'<div class="hero-value">' + num(Math.round(s.averageThroughputOpsPerSec || 0)) + '<span class="badge badge-accent">ops/sec</span></div>' +
|
|
39
|
-
'<p class="hero-meta">' + meta.join('
|
|
39
|
+
'<p class="hero-meta">' + meta.join(', ') + '</p></div>' +
|
|
40
40
|
'<dl class="hero-stats">' +
|
|
41
41
|
'<div><dt>Score</dt><dd class="' + (score < 100 ? 'text-danger' : '') + '">' + score + '%</dd></div>' +
|
|
42
42
|
'<div><dt>Scenarios passed</dt><dd>' + num(s.passedScenarios) + '/' + num(s.totalScenarios) + '</dd></div>' +
|
|
@@ -48,16 +48,17 @@ export const benchmarksScript = String.raw ` // ─── View 4: Synthetic ben
|
|
|
48
48
|
function scenarioCards(r) {
|
|
49
49
|
var byId = {}, known = {};
|
|
50
50
|
(r.scenarios || []).forEach(function (s) { byId[s.id] = s; });
|
|
51
|
-
var cards =
|
|
51
|
+
var cards = [], idle = [];
|
|
52
|
+
SCENARIOS.forEach(function (meta) {
|
|
53
|
+
known[meta.id] = true;
|
|
54
|
+
if (byId[meta.id]) cards.push(scenarioCard(meta.label, byId[meta.id])); else idle.push(meta.label);
|
|
55
|
+
});
|
|
52
56
|
(r.scenarios || []).forEach(function (s) { if (!known[s.id]) cards.push(scenarioCard(s.name || s.id, s)); });
|
|
57
|
+
if (idle.length) cards.push('<p class="muted not-run">Not run: ' + idle.map(esc).join(', ') + '.</p>');
|
|
53
58
|
return cards.join('');
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
function scenarioCard(label, s) {
|
|
57
|
-
if (!s) {
|
|
58
|
-
return '<article class="card scenario is-idle"><header class="scenario-head"><h3>' + esc(label) + '</h3><span class="badge badge-neutral">Not run</span></header>' +
|
|
59
|
-
'<p class="muted">Not part of the last run. Run the full matrix to score it.</p></article>';
|
|
60
|
-
}
|
|
61
62
|
var total = (s.assertionsPassed || 0) + (s.assertionsFailed || 0);
|
|
62
63
|
var passed = s.status === 'passed';
|
|
63
64
|
var score = total ? Math.round((s.assertionsPassed || 0) / total * 100) : (passed ? 100 : 0);
|
|
@@ -76,7 +77,7 @@ export const benchmarksScript = String.raw ` // ─── View 4: Synthetic ben
|
|
|
76
77
|
'<div class="metrics"><div><span class="metric-v">' + score + '%</span><span class="metric-l">Score</span></div>' +
|
|
77
78
|
'<div><span class="metric-v">' + fmtMs(s.durationMs) + '</span><span class="metric-l">Latency</span></div>' +
|
|
78
79
|
'<div><span class="metric-v">' + num(Math.round(s.throughputOpsPerSec || 0)) + '</span><span class="metric-l">ops/sec</span></div></div>' +
|
|
79
|
-
progressBar(score, 'sc-' + s.id, label + ' score', passed ? '
|
|
80
|
+
progressBar(score, 'sc-' + s.id, label + ' score', passed ? '' : 'danger') +
|
|
80
81
|
(details ? '<dl class="details">' + details + '</dl>' : '') + errors + '</article>';
|
|
81
82
|
}
|
|
82
83
|
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
export const benchmarksCss = String.raw `/* Benchmarks */
|
|
3
3
|
.hero { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 20px 32px; padding: 22px 0; margin-bottom: 0; background: none; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; box-shadow: none; transition: opacity .2s; }
|
|
4
4
|
.hero.is-running { opacity: .6; }
|
|
5
|
-
.
|
|
5
|
+
.hero-title { margin: 0; font-size: 14px; font-weight: 600; color: var(--muted); }
|
|
6
|
+
.not-run { margin: 0; padding: 12px 0; font-size: 13px; }
|
|
7
|
+
.scenario .progress { height: 4px; }
|
|
6
8
|
.hero-value { display: flex; align-items: center; gap: 10px; margin: 6px 0; font-size: 40px; font-weight: 650; letter-spacing: -0.03em; line-height: 1.1; font-variant-numeric: tabular-nums; }
|
|
7
9
|
.hero-value .badge { font-size: 12px; letter-spacing: 0; }
|
|
8
10
|
.hero-meta { margin: 0; font-size: 13.5px; color: var(--muted); }
|
|
@@ -168,7 +168,7 @@ export const databaseBrowserScript = String.raw ` // ─── Database Studio:
|
|
|
168
168
|
table.columns.forEach(function (col) {
|
|
169
169
|
var isPk = col.primaryKey;
|
|
170
170
|
var fieldLabel = esc(col.name) + (isPk ? ' (Primary Key - Optional/Auto)' : '');
|
|
171
|
-
var placeholder = col.type + (col.default ? '
|
|
171
|
+
var placeholder = col.type + (col.default ? ', default: ' + col.default : '');
|
|
172
172
|
var isLong = /json|map|array|text/i.test(col.type);
|
|
173
173
|
if (isLong) {
|
|
174
174
|
html += '<div class="field field-full"><label><b>' + fieldLabel + '</b></label><textarea class="input" name="' + esc(col.name) + '" placeholder="' + esc(placeholder) + '"></textarea></div>';
|
|
@@ -3,10 +3,11 @@ export const databaseConnectionScript = String.raw ` // ─── Database Stud
|
|
|
3
3
|
function renderModalStatus() {
|
|
4
4
|
['dev', 'prod'].forEach(function (env) {
|
|
5
5
|
var s = state.status && state.status[env];
|
|
6
|
-
|
|
6
|
+
var unset = !s || /No (Dev|Prod) database configured/.test(s.error || '');
|
|
7
|
+
$('mdot-' + env).className = 'dot ' + (s && s.connected ? 'on' : unset ? '' : 'off');
|
|
7
8
|
$('current-' + env).innerHTML = s && s.maskedUrl
|
|
8
9
|
? 'Current: <code>' + esc(s.maskedUrl) + '</code><span class="copy-wrap"><span class="tooltip">Copied!</span><button class="btn small" type="button" data-copy-masked="' + env + '">Copy</button></span>'
|
|
9
|
-
: 'Current: <span class="muted">
|
|
10
|
+
: 'Current: <span class="muted">Not set up</span>';
|
|
10
11
|
});
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -139,8 +140,8 @@ export const databaseConnectionScript = String.raw ` // ─── Database Stud
|
|
|
139
140
|
if (r.success) {
|
|
140
141
|
var count = r.entityCount != null ? r.entityCount : r.tableCount;
|
|
141
142
|
out.className = 'result ok';
|
|
142
|
-
out.textContent = 'Connected to ' + (ENGINE_LABEL[r.engine] || r.engine) + ' in ' + Math.round(r.pingMs) + 'ms
|
|
143
|
-
entityNoun({ engine: r.engine }, true).toLowerCase() + '
|
|
143
|
+
out.textContent = 'Connected to ' + (ENGINE_LABEL[r.engine] || r.engine) + ' in ' + Math.round(r.pingMs) + 'ms, ' + count + ' ' +
|
|
144
|
+
entityNoun({ engine: r.engine }, true).toLowerCase() + ', saved to session as ' + r.maskedUrl;
|
|
144
145
|
$('url-' + env).value = '';
|
|
145
146
|
$('ppass-' + env).value = '';
|
|
146
147
|
return true;
|
|
@@ -53,9 +53,12 @@ export const databaseDataScript = String.raw ` // ─── View 5: Database St
|
|
|
53
53
|
var label = env.toUpperCase();
|
|
54
54
|
if (!s) return '<span class="pill"><span class="dot"></span><b>' + label + ':</b> loading…</span>';
|
|
55
55
|
if (!s.connected) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
if (/No (Dev|Prod) database configured/.test(s.error || '')) {
|
|
57
|
+
return '<span class="pill"><span class="dot"></span><b>' + label + ':</b> ' +
|
|
58
|
+
'<span class="badge badge-neutral">Not set up</span></span>';
|
|
59
|
+
}
|
|
60
|
+
return '<span class="pill" title="' + esc(s.error || '') + '"><span class="dot off"></span><b>' + label + ':</b> offline' +
|
|
61
|
+
' <span class="badge badge-offline">Offline</span></span>';
|
|
59
62
|
}
|
|
60
63
|
var sourceBadge = s.synthesized && s.sourceKey
|
|
61
64
|
? ' <span class="badge badge-synth" title="Assembled in memory from fragmented .env keys">synthesized from ' + esc(keyFamily(s.sourceKey)) + '</span>'
|
|
@@ -63,7 +66,7 @@ export const databaseDataScript = String.raw ` // ─── View 5: Database St
|
|
|
63
66
|
? ' <span class="badge badge-source" title="Detected in ' + esc(s.exampleFile || '.env.example') + '">via ' + esc(s.sourceKey) + '</span>'
|
|
64
67
|
: (s.sourceKey ? ' <span class="badge badge-source">via ' + esc(s.sourceKey) + '</span>' : '');
|
|
65
68
|
return '<span class="pill" title="' + esc(s.maskedUrl) + '"><span class="dot on"></span><b>' + label + ':</b> ' +
|
|
66
|
-
esc(ENGINE_LABEL[s.engine] || s.engine) + (s.database ? '
|
|
69
|
+
esc(ENGINE_LABEL[s.engine] || s.engine) + (s.database ? ', ' + esc(s.database) : '') + ' (' + Math.round(s.pingMs) + 'ms): ' +
|
|
67
70
|
entityCount(s) + ' ' + entityNoun(s, true) + sourceBadge + ' <span class="badge badge-online">ONLINE</span></span>';
|
|
68
71
|
}
|
|
69
72
|
|
|
@@ -97,15 +100,8 @@ export const databaseDataScript = String.raw ` // ─── View 5: Database St
|
|
|
97
100
|
'<span class="diagnostic-tag">[DIAGNOSTIC]</span> ' +
|
|
98
101
|
'<span class="diagnostic-msg">' + esc(suggestion) + '</span>' +
|
|
99
102
|
'</div>' +
|
|
100
|
-
'<div class="diagnostic-actions">' +
|
|
101
|
-
'<button class="btn small" type="button" id="btn-diag-settings">Open Connection Settings</button>' +
|
|
102
|
-
'</div>' +
|
|
103
103
|
'</div>';
|
|
104
104
|
bannerEl.hidden = false;
|
|
105
|
-
var btnDiag = $('btn-diag-settings');
|
|
106
|
-
if (btnDiag) {
|
|
107
|
-
btnDiag.onclick = function () { openSettings(); };
|
|
108
|
-
}
|
|
109
105
|
} else {
|
|
110
106
|
bannerEl.innerHTML = '';
|
|
111
107
|
bannerEl.hidden = true;
|
|
@@ -158,8 +154,7 @@ export const databaseDataScript = String.raw ` // ─── View 5: Database St
|
|
|
158
154
|
'<path d="M128,24C74.17,24,32,48.6,32,80v96c0,31.4,42.17,56,96,56s96-24.6,96-56V80C224,48.6,181.83,24,128,24Zm80,104c0,9.62-7.88,19.43-21.61,26.92C170.93,163.35,150.19,168,128,168s-42.93-4.65-58.39-13.08C55.88,147.43,48,137.62,48,128V111.36c17.06,15,46.23,24.64,80,24.64s62.94-9.68,80-24.64ZM69.61,53.08C85.07,44.65,105.81,40,128,40s42.93,4.65,58.39,13.08C200.12,60.57,208,70.38,208,80s-7.88,19.43-21.61,26.92C170.93,115.35,150.19,120,128,120s-42.93-4.65-58.39-13.08C55.88,99.43,48,89.62,48,80S55.88,60.57,69.61,53.08ZM186.39,202.92C170.93,211.35,150.19,216,128,216s-42.93-4.65-58.39-13.08C55.88,195.43,48,185.62,48,176V159.36c17.06,15,46.23,24.64,80,24.64s62.94-9.68,80-24.64V176C208,185.62,200.12,195.43,186.39,202.92Z"/></svg>';
|
|
159
155
|
|
|
160
156
|
function emptyState(title, text) {
|
|
161
|
-
return '<div class="empty card">' + DB_ICON + '<h3>' + esc(title) + '</h3><p>' + esc(text) + '</p>'
|
|
162
|
-
'<button class="btn primary" type="button" data-action="open-settings">Connection Settings</button></div>';
|
|
157
|
+
return '<div class="empty card">' + DB_ICON + '<h3>' + esc(title) + '</h3><p>' + esc(text) + '</p></div>';
|
|
163
158
|
}
|
|
164
159
|
|
|
165
160
|
`;
|
|
@@ -292,7 +292,7 @@ export const databaseDriftScript = String.raw ` // ─── Database Studio: d
|
|
|
292
292
|
var lang = state.scriptLang;
|
|
293
293
|
var unit = lang === 'json' ? ' change(s)' : ' statement(s)';
|
|
294
294
|
return '<div class="sql-wrap card"><div class="sql-head"><span class="muted"><span class="lang">' + LANG_LABEL[lang] + '</span>' +
|
|
295
|
-
(destructive ? '<span class="badge badge-dropped">DESTRUCTIVE</span> ' : '') + state.scriptCount + unit + '
|
|
295
|
+
(destructive ? '<span class="badge badge-dropped">DESTRUCTIVE</span> ' : '') + state.scriptCount + unit + ', read-only preview</span>' +
|
|
296
296
|
'<span class="copy-wrap"><span class="tooltip">Copied!</span><button class="btn small" type="button" data-action="copy-sql">Copy ' +
|
|
297
297
|
(lang === 'sql' ? 'SQL' : lang === 'js' ? 'Script' : 'JSON') + '</button></span></div>' +
|
|
298
298
|
'<pre class="sql" tabindex="0" aria-label="Migration script (' + LANG_LABEL[lang] + ')"><code>' + highlightScript(state.sql, lang) + '</code></pre></div>';
|
|
@@ -123,8 +123,7 @@ export const databaseEventsScript = String.raw ` // ─── Events: Database
|
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
125
|
var action = el.getAttribute('data-action');
|
|
126
|
-
if (action === '
|
|
127
|
-
else if (action === 'copy-sql') copyText(state.sql, el);
|
|
126
|
+
if (action === 'copy-sql') copyText(state.sql, el);
|
|
128
127
|
else if (action === 'edit-record') openEditRecordModal(Number(el.getAttribute('data-row')));
|
|
129
128
|
else if (action === 'delete-record') handleDeleteRecord(Number(el.getAttribute('data-row')));
|
|
130
129
|
else if (action === 'preview-url') {
|
|
@@ -45,7 +45,7 @@ export const databaseExplorerScript = String.raw ` // ─── Database Studio
|
|
|
45
45
|
: '';
|
|
46
46
|
var docMeta = coll && (t.documentCount != null || t.sampledDocuments != null)
|
|
47
47
|
? '<p class="desc">' + (t.documentCount != null ? '~' + t.documentCount + ' documents' : 'Document count unavailable') +
|
|
48
|
-
(t.sampledDocuments != null ? '
|
|
48
|
+
(t.sampledDocuments != null ? ', fields inferred from ' + t.sampledDocuments + ' sampled' : '') + '</p>'
|
|
49
49
|
: '';
|
|
50
50
|
return '<details class="table-card"><summary><span class="tname">' + esc(t.name) + '</span>' +
|
|
51
51
|
(coll ? '<span class="badge badge-collection">COLLECTION</span>' : '') +
|
|
@@ -87,7 +87,7 @@ export const databaseExplorerScript = String.raw ` // ─── Database Studio
|
|
|
87
87
|
var parts = [m.mimeType || 'unknown type'];
|
|
88
88
|
if (m.width && m.height) parts.push(m.width + '×' + m.height);
|
|
89
89
|
if (m.sizeBytes != null) parts.push(formatBytes(m.sizeBytes));
|
|
90
|
-
return parts.join('
|
|
90
|
+
return parts.join(', ');
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
function showThumb(el) {
|
|
@@ -117,7 +117,7 @@ export const databaseExplorerScript = String.raw ` // ─── Database Studio
|
|
|
117
117
|
hideThumb();
|
|
118
118
|
var m = entry.media;
|
|
119
119
|
$('media-title').textContent = entry.table + '.' + entry.field;
|
|
120
|
-
$('media-sub').textContent = (m ? SOURCE_LABEL[m.source] || m.source : 'Image field') + '
|
|
120
|
+
$('media-sub').textContent = (m ? SOURCE_LABEL[m.source] || m.source : 'Image field') + ', ' + entry.type;
|
|
121
121
|
$('media-thumb').innerHTML = thumbHtml(m);
|
|
122
122
|
var rows = [];
|
|
123
123
|
if (m) {
|
|
@@ -144,7 +144,7 @@ export const databaseExplorerScript = String.raw ` // ─── Database Studio
|
|
|
144
144
|
var body;
|
|
145
145
|
if (!s || !s.connected) {
|
|
146
146
|
body = '<div class="empty" style="padding:28px 12px"><p>' + esc(s && s.error ? s.error : 'No database connected.') + '</p>' +
|
|
147
|
-
'<
|
|
147
|
+
'<p class="muted">Use Connection Settings at the top.</p></div>';
|
|
148
148
|
} else if (!tables.length) {
|
|
149
149
|
body = '<p class="muted">Connected, but this database has no ' + entityNoun(s, true).toLowerCase() + ' yet.</p>';
|
|
150
150
|
} else if (!filtered.length) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Plan layout: rounds, lanes, links and the highlight set. Pure, no DOM.
|
|
2
|
-
export const flowLayoutScript = String.raw ` var FLOW = { W: 206, H:
|
|
2
|
+
export const flowLayoutScript = String.raw ` var FLOW = { W: 206, H: 74, GX: 52, GY: 10, PAD: 14, LX: 128, TOP: 30 };
|
|
3
3
|
|
|
4
4
|
function flowAgentKey(t) { return t.agent || 'unassigned'; }
|
|
5
5
|
|
|
@@ -25,7 +25,7 @@ export const flowCss = String.raw `#panel-flow .seg { display: inline-flex; bord
|
|
|
25
25
|
#panel-flow .node .l1 { display: flex; align-items: center; gap: 6px; }
|
|
26
26
|
#panel-flow .node .l1 .d { margin-left: auto; font: 11px var(--mono); color: var(--fg-3); }
|
|
27
27
|
#panel-flow .node .l1 .mono { font-size: 11px; }
|
|
28
|
-
#panel-flow .node .title { font-size: 12.5px;
|
|
28
|
+
#panel-flow .node .title { font-size: 12.5px; line-height: 1.3; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
|
29
29
|
#panel-flow .canvas.focus .node:not(.hot) { opacity: .3; }
|
|
30
30
|
#panel-flow .node.sel { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
31
31
|
#panel-flow .legend { display: flex; gap: 14px; flex-wrap: wrap; font-size: 12px; color: var(--fg-2); align-items: center; }
|
|
@@ -51,7 +51,7 @@ export const flowTimelineScript = String.raw ` function flowStat(k, v, s) {
|
|
|
51
51
|
var cw = t.checkMs && t.status === 'completed' ? Math.min(w, t.checkMs * tm.sg.k) : 0;
|
|
52
52
|
var lx = w > 64 ? x1 + 6 : x2 + 4;
|
|
53
53
|
var took = (t.status === 'in_progress' ? 'running' : 'took ' + fmt.dur(t.durationMs)) + (t.checkMs ? ', check ' + fmt.dur(t.checkMs) : '');
|
|
54
|
-
var tip = ui.esc(t.id + '
|
|
54
|
+
var tip = ui.esc(t.id + ', ' + t.title) + ' ' + ui.esc(took);
|
|
55
55
|
return '<g><rect class="bar-w ' + ui.esc(t.status) + '" data-task="' + ui.esc(t.id) + '" data-row="' + row + '" x="' + x1 + '" y="' + by +
|
|
56
56
|
'" width="' + w + '" height="21" rx="4"><title>' + tip + '</title></rect>' +
|
|
57
57
|
(cw > 1 ? '<rect class="bar-c" x="' + (x2 - cw) + '" y="' + (by + 17) + '" width="' + cw + '" height="4" rx="1"/>' : '') +
|
|
@@ -24,7 +24,7 @@ export const homeScript = String.raw ` var esc = Studio.ui.esc;
|
|
|
24
24
|
var t = item.task;
|
|
25
25
|
if (t.status === 'blocked') return t.reason || 'Paused after 3 attempts.';
|
|
26
26
|
var e = item.escalation || {};
|
|
27
|
-
return e.reason || e.summary || e.details || '
|
|
27
|
+
return e.reason || e.summary || e.details || 'Needs your decision.';
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function needActions(item) {
|
|
@@ -55,16 +55,8 @@ export const homeScript = String.raw ` var esc = Studio.ui.esc;
|
|
|
55
55
|
rightText(count) + '</div>' + n.map(needItem).join('') + '</section>';
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function isReady(model, t) {
|
|
59
|
-
if (t.status !== 'pending') return false;
|
|
60
|
-
return t.isAvailable || t.deps.every(function (d) {
|
|
61
|
-
var dep = model.byId[d];
|
|
62
|
-
return !dep || dep.status === 'completed';
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
58
|
function readyTasks(model) {
|
|
67
|
-
return model.tasks.filter(
|
|
59
|
+
return model.tasks.filter(Studio.status.isReady);
|
|
68
60
|
}
|
|
69
61
|
|
|
70
62
|
function dispatchBtn(id, label, cls) {
|
|
@@ -75,17 +67,18 @@ export const homeScript = String.raw ` var esc = Studio.ui.esc;
|
|
|
75
67
|
function taskCells(t) {
|
|
76
68
|
return '<span class="mono id">' + esc(t.id) + '</span>' +
|
|
77
69
|
'<button type="button" class="t link" data-task="' + esc(t.id) + '">' + esc(t.title) + '</button>' +
|
|
78
|
-
'<span class="agent">' + esc(t.agent
|
|
70
|
+
'<span class="agent">' + esc(Studio.agentInfo(t.agent).label) + '</span>';
|
|
79
71
|
}
|
|
80
72
|
|
|
81
73
|
function runRow(t) {
|
|
82
74
|
var since = t.startT ? ' data-since="' + t.startT + '">' + Studio.fmt.dur(Studio.now() - t.startT) : '>';
|
|
83
75
|
var row = '<div class="row run-row">' + taskCells(t) +
|
|
84
|
-
'<span class="state"
|
|
76
|
+
'<span class="state">Running</span>' +
|
|
85
77
|
'<span class="mono num timer"' + since + '</span></div>';
|
|
86
78
|
var log = '<span class="mono log">' + esc(t.latestLog || '') + '</span>';
|
|
87
|
-
|
|
88
|
-
|
|
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>';
|
|
89
82
|
}
|
|
90
83
|
|
|
91
84
|
function nowRunning(model) {
|
|
@@ -96,23 +89,18 @@ export const homeScript = String.raw ` var esc = Studio.ui.esc;
|
|
|
96
89
|
return section('running', 'Now running', '', '<div class="row calm"><span>No agent is running.</span>' + btn + '</div>');
|
|
97
90
|
}
|
|
98
91
|
|
|
99
|
-
function
|
|
100
|
-
var
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function nextRow(model, t) {
|
|
105
|
-
var end = isReady(model, t) ? dispatchBtn(t.id, 'Dispatch') :
|
|
106
|
-
'<span class="wait">waits <span class="mono">' + esc(waitOn(model, t)) + '</span></span>';
|
|
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>';
|
|
107
95
|
return '<div class="row next-row">' + taskCells(t) + end + '</div>';
|
|
108
96
|
}
|
|
109
97
|
|
|
110
98
|
function upNext(model) {
|
|
111
99
|
var list = model.tasks.filter(function (t) { return t.status === 'pending'; });
|
|
112
|
-
var ready = list.filter(
|
|
113
|
-
var waiting = list.filter(function (t) { return !isReady(
|
|
100
|
+
var ready = list.filter(Studio.status.isReady);
|
|
101
|
+
var waiting = list.filter(function (t) { return !Studio.status.isReady(t); });
|
|
114
102
|
var rows = ready.concat(waiting).slice(0, UP_NEXT_CAP);
|
|
115
|
-
var body = rows.length ? rows.map(
|
|
103
|
+
var body = rows.length ? rows.map(nextRow).join('') :
|
|
116
104
|
'<div class="row calm">Nothing is waiting.</div>';
|
|
117
105
|
return section('next', 'Up next', '', body);
|
|
118
106
|
}
|
|
@@ -127,18 +115,27 @@ export const homeScript = String.raw ` var esc = Studio.ui.esc;
|
|
|
127
115
|
return section('milestone', 'Milestone', '', body);
|
|
128
116
|
}
|
|
129
117
|
|
|
118
|
+
// One event per task that has started or finished; the state word comes from its status now.
|
|
130
119
|
function activityEvents(model) {
|
|
131
120
|
var out = [];
|
|
132
121
|
model.tasks.forEach(function (t) {
|
|
133
|
-
|
|
134
|
-
if (
|
|
122
|
+
var at = t.status === 'completed' ? (t.endT || t.startT) : t.startT;
|
|
123
|
+
if (at) out.push({ at: at, task: t });
|
|
135
124
|
});
|
|
136
125
|
return out.sort(function (a, b) { return b.at - a.at; }).slice(0, ACTIVITY_CAP);
|
|
137
126
|
}
|
|
138
127
|
|
|
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;
|
|
132
|
+
}
|
|
133
|
+
|
|
139
134
|
function activityRow(e) {
|
|
140
|
-
|
|
141
|
-
|
|
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>';
|
|
142
139
|
}
|
|
143
140
|
|
|
144
141
|
function recentActivity(model) {
|
|
@@ -1,20 +1,17 @@
|
|
|
1
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 {
|
|
2
|
+
export const homeCss = String.raw `#panel-home .home { display: flex; flex-direction: column; }
|
|
3
3
|
#panel-home .page-head h1 { font-size: 22px; font-weight: 600; margin: 0 0 12px; letter-spacing: -.01em; }
|
|
4
4
|
#panel-home .home-sec { border-top: 1px solid var(--line); padding: 12px 0; min-width: 0; }
|
|
5
5
|
#panel-home .sec-head { display: flex; align-items: center; gap: 8px; min-height: 28px; }
|
|
6
6
|
#panel-home .sec-head h2 { font-size: 13px; font-weight: 600; margin: 0; color: var(--fg-2); }
|
|
7
7
|
#panel-home .sec-head .right { margin-left: auto; font-size: 12px; color: var(--fg-3); }
|
|
8
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:last-child { border-bottom: 0; }
|
|
9
|
+
#panel-home .row { display: flex; align-items: center; gap: 12px; min-height: 36px; }
|
|
11
10
|
#panel-home .row .id { width: 72px; flex: none; color: var(--fg-2); font-size: 12px; }
|
|
12
11
|
#panel-home .row .t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
13
12
|
#panel-home .link { background: none; border: 0; padding: 0; text-align: left; color: var(--fg); cursor: pointer; font: inherit; }
|
|
14
13
|
#panel-home .agent { width: 96px; flex: none; color: var(--fg-2); font-size: 12.5px; }
|
|
15
14
|
#panel-home .state { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; }
|
|
16
|
-
#panel-home .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--fg-3); }
|
|
17
|
-
#panel-home .dot.run { background: var(--run); }
|
|
18
15
|
#panel-home .timer { width: 64px; text-align: right; font-size: 12px; }
|
|
19
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; }
|
|
20
17
|
#panel-home .wait { color: var(--fg-3); font-size: 12.5px; }
|
|
@@ -30,7 +27,6 @@ export const homeCss = String.raw `#panel-home .home { max-width: 960px; display
|
|
|
30
27
|
#panel-home .act-row .when { width: 72px; flex: none; font-size: 12px; color: var(--fg-3); }
|
|
31
28
|
#panel-home .skel-row .skel { height: 12px; width: 100%; border-radius: 6px; background: var(--surface-2); }
|
|
32
29
|
@media (max-width: 1024px) {
|
|
33
|
-
#panel-home .home { max-width: none; }
|
|
34
30
|
#panel-home .agent { display: none; }
|
|
35
31
|
}
|
|
36
32
|
`;
|
|
@@ -3,11 +3,7 @@ export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
|
3
3
|
var CAP = 5;
|
|
4
4
|
var view = { lastMs: null, showAll: false, open: {}, mode: null, filter: 'all', groups: {} };
|
|
5
5
|
var KEY = 'nativ-studio:tasks-view';
|
|
6
|
-
var FILTERS = [['all', 'All']
|
|
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' };
|
|
6
|
+
var FILTERS = [['all', 'All']].concat(Studio.status.buckets);
|
|
11
7
|
|
|
12
8
|
function readMode() {
|
|
13
9
|
try { return localStorage.getItem(KEY) === 'board' ? 'board' : 'list'; } catch (e) { return 'list'; }
|
|
@@ -16,30 +12,15 @@ export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
|
16
12
|
try { localStorage.setItem(KEY, mode); } catch (e) { /* private mode */ }
|
|
17
13
|
}
|
|
18
14
|
|
|
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
15
|
function matches(model, t, f) {
|
|
32
|
-
|
|
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);
|
|
16
|
+
return f === 'all' || Studio.status.bucket(t) === f;
|
|
37
17
|
}
|
|
38
18
|
|
|
39
19
|
function toolbar(model) {
|
|
40
20
|
var chips = FILTERS.map(function (f) {
|
|
41
21
|
var on = view.filter === f[0];
|
|
42
22
|
var n = model.tasks.filter(function (t) { return matches(model, t, f[0]); }).length;
|
|
23
|
+
if (!n && !on && f[0] !== 'all') return '';
|
|
43
24
|
return '<button type="button" class="tf' + (on ? ' on' : '') + '" data-act="tasks-filter" data-filter="' +
|
|
44
25
|
f[0] + '" aria-pressed="' + on + '">' + f[1] + ' <span class="num">' + n + '</span></button>';
|
|
45
26
|
}).join('');
|
|
@@ -59,15 +40,17 @@ export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
|
59
40
|
|
|
60
41
|
// Rows use data-tid, not data-task, so the nested Dispatch button gets its own click.
|
|
61
42
|
function listRow(model, t, fmt) {
|
|
62
|
-
var
|
|
43
|
+
var bucket = Studio.status.bucket(t);
|
|
44
|
+
var wait = Studio.status.waitText(t);
|
|
63
45
|
var id = ui.esc(t.id);
|
|
64
|
-
var go =
|
|
46
|
+
var go = Studio.status.isReady(t) ? '<button type="button" class="tdispatch" data-act="tasks-dispatch" data-tid="' +
|
|
65
47
|
id + '">Dispatch</button>' : '';
|
|
66
48
|
return '<div class="trow" role="row" tabindex="0" data-act="tasks-open" data-tid="' + id + '">' +
|
|
67
49
|
'<span class="c-id mono" role="cell">' + id + '</span>' +
|
|
68
50
|
'<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
|
|
70
|
-
'<span class="c-status st-' +
|
|
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>' +
|
|
71
54
|
'<span class="c-time mono" role="cell">' + ui.esc(timeOf(t, fmt)) + '</span></div>';
|
|
72
55
|
}
|
|
73
56
|
|
|
@@ -86,7 +69,7 @@ export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
|
86
69
|
return '<section class="tgroup" role="table" aria-label="' + ui.esc(m.name) + '">' +
|
|
87
70
|
'<button type="button" class="tg-head" data-act="tasks-group" data-group="' + ui.esc(m.id) +
|
|
88
71
|
'" 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>' +
|
|
72
|
+
'<span class="mono faint">' + ui.esc(m.id) + '</span><span class="t" title="' + ui.esc(m.name) + '">' + ui.esc(m.name) + '</span>' +
|
|
90
73
|
'<span class="num faint">' + m.done + ' of ' + m.total + '</span></button>' + body + '</section>';
|
|
91
74
|
}
|
|
92
75
|
|
|
@@ -124,13 +107,13 @@ export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
|
124
107
|
var el = e.target;
|
|
125
108
|
if (e.key !== 'Enter' || !el || !el.matches || !el.matches('.trow[data-tid]')) return;
|
|
126
109
|
e.preventDefault();
|
|
127
|
-
Studio.
|
|
110
|
+
Studio.openTask(el.getAttribute('data-tid'));
|
|
128
111
|
});
|
|
129
112
|
}
|
|
130
113
|
|
|
131
114
|
function head(m) {
|
|
132
115
|
var lead = m ? ui.esc(m.name) : 'The work of one milestone, by state.';
|
|
133
|
-
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>' +
|
|
134
117
|
'<div class="panel-actions">' + ui.milestoneSelect() + '</div></div>';
|
|
135
118
|
}
|
|
136
119
|
|
|
@@ -138,7 +121,8 @@ export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
|
138
121
|
|
|
139
122
|
function metaHtml(t, fmt) {
|
|
140
123
|
if (t.status === 'completed') {
|
|
141
|
-
|
|
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;
|
|
142
126
|
}
|
|
143
127
|
if (t.status === 'in_progress') {
|
|
144
128
|
var live = t.startT ? '<span data-since="' + t.startT + '">' + fmt.dur(Studio.now() - t.startT) + '</span>' : '';
|
|
@@ -147,7 +131,7 @@ export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
|
147
131
|
if (t.status === 'blocked') {
|
|
148
132
|
return '<span class="bad">Paused after ' + (t.attempt || t.maxAttempts) + ' of ' + t.maxAttempts + ' attempts</span>';
|
|
149
133
|
}
|
|
150
|
-
if (t.waitingOn.length) return '<span>
|
|
134
|
+
if (t.waitingOn.length && !t.stale) return '<span>Waits for ' + t.waitingOn.map(ui.esc).join(', ') + '</span>';
|
|
151
135
|
return '<span>Ready to start</span>';
|
|
152
136
|
}
|
|
153
137
|
|
|
@@ -157,8 +141,9 @@ export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
|
157
141
|
'<div class="title">' + ui.esc(t.title) + '</div><div class="meta">' + metaHtml(t, fmt) + '</div></button>';
|
|
158
142
|
}
|
|
159
143
|
|
|
160
|
-
function column(label, status, list, shown,
|
|
161
|
-
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>';
|
|
162
147
|
return '<div class="col" data-col="' + status + '"><div class="col-head">' + ui.statusIcon(status) + label +
|
|
163
148
|
'<span class="count">' + list.length + '</span></div>' + body + (extra || '') + '</div>';
|
|
164
149
|
}
|
|
@@ -169,12 +154,12 @@ export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
|
169
154
|
var shown = view.showAll ? done : done.slice(0, CAP);
|
|
170
155
|
var more = done.length > CAP
|
|
171
156
|
? '<button type="button" class="more" data-act="tasks-more">' + (view.showAll ? 'Show fewer' : 'Show all ' + done.length) + '</button>' : '';
|
|
172
|
-
var
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
column(
|
|
176
|
-
|
|
177
|
-
|
|
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>';
|
|
178
163
|
}
|
|
179
164
|
|
|
180
165
|
function histRow(m, fmt) {
|
|
@@ -224,7 +209,7 @@ export const tasksScript = String.raw ` var ui = Studio.ui;
|
|
|
224
209
|
},
|
|
225
210
|
'tasks-filter': function (el) { view.filter = el.getAttribute('data-filter'); Studio.repaint('tasks'); },
|
|
226
211
|
'tasks-clear': function () { view.filter = 'all'; Studio.repaint('tasks'); },
|
|
227
|
-
'tasks-open': function (el) { Studio.
|
|
212
|
+
'tasks-open': function (el) { Studio.openTask(el.getAttribute('data-tid')); },
|
|
228
213
|
'tasks-dispatch': function (el) { Studio.taskAction('dispatch', el.getAttribute('data-tid')); },
|
|
229
214
|
'tasks-group': function (el, ctx) {
|
|
230
215
|
var id = el.getAttribute('data-group');
|