taskchef 5.7.2 → 5.9.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/.codex-plugin/plugin.json +2 -2
- package/BACKLOG.md +4 -4
- package/README.md +125 -45
- package/SPEC.md +106 -89
- package/docs/delegation-design.md +158 -254
- package/hooks/hooks.json +18 -0
- package/hooks/taskchef-initial-prompt.js +17 -0
- package/index.js +13 -1
- package/package.json +3 -2
- package/skills/taskchef-bootstrap/SKILL.md +6 -3
- package/skills/taskchef-delegate/SKILL.md +49 -91
- package/skills/taskchef-report/SKILL.md +60 -27
- package/src/cli.js +52 -2
- package/src/dashboard/app.js +255 -0
- package/src/dashboard/index.html +86 -0
- package/src/dashboard/state.js +40 -0
- package/src/dashboard/styles.css +147 -0
- package/src/dashboard.js +597 -0
- package/src/delegation.js +109 -359
- package/src/hook.js +60 -0
- package/src/mcp.js +35 -2
- package/src/workspace-path.js +5 -1
- package/src/workspace.js +239 -23
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findCurrentTask,
|
|
3
|
+
reconcileNotifications,
|
|
4
|
+
} from "./state.js";
|
|
5
|
+
|
|
6
|
+
const state = {
|
|
7
|
+
tasks: [],
|
|
8
|
+
signatures: new Map(),
|
|
9
|
+
notifications: [],
|
|
10
|
+
initialized: false,
|
|
11
|
+
selectedTask: null,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const elements = {
|
|
15
|
+
clearNotifications: document.querySelector("#clear-notifications"),
|
|
16
|
+
closeDialog: document.querySelector("#close-dialog"),
|
|
17
|
+
connectionDot: document.querySelector("#connection-dot"),
|
|
18
|
+
connectionLabel: document.querySelector("#connection-label"),
|
|
19
|
+
copyThreadId: document.querySelector("#copy-thread-id"),
|
|
20
|
+
dashboardMessage: document.querySelector("#dashboard-message"),
|
|
21
|
+
dialog: document.querySelector("#task-dialog"),
|
|
22
|
+
dialogInstruction: document.querySelector("#dialog-instruction"),
|
|
23
|
+
dialogMetadata: document.querySelector("#dialog-metadata"),
|
|
24
|
+
dialogProject: document.querySelector("#dialog-project"),
|
|
25
|
+
dialogSummary: document.querySelector("#dialog-summary"),
|
|
26
|
+
dialogTitle: document.querySelector("#dialog-title"),
|
|
27
|
+
emptyState: document.querySelector("#empty-state"),
|
|
28
|
+
notifications: document.querySelector("#notifications"),
|
|
29
|
+
openProject: document.querySelector("#open-project"),
|
|
30
|
+
projectFilter: document.querySelector("#project-filter"),
|
|
31
|
+
statusFilter: document.querySelector("#status-filter"),
|
|
32
|
+
taskCount: document.querySelector("#task-count"),
|
|
33
|
+
taskList: document.querySelector("#task-list"),
|
|
34
|
+
toastList: document.querySelector("#toast-list"),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function statusLabel(task) {
|
|
38
|
+
return task.status === null ? "unresolved" : task.status.replaceAll("_", " ");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatTime(value) {
|
|
42
|
+
if (!value) return "—";
|
|
43
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
44
|
+
dateStyle: "medium",
|
|
45
|
+
timeStyle: "short",
|
|
46
|
+
}).format(new Date(value));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function setConnection(connected) {
|
|
50
|
+
elements.connectionDot.classList.toggle("connected", connected);
|
|
51
|
+
elements.connectionLabel.textContent = connected ? "Live" : "Reconnecting…";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function replaceOptions(select, values, allLabel) {
|
|
55
|
+
const selected = select.value;
|
|
56
|
+
select.replaceChildren();
|
|
57
|
+
const all = document.createElement("option");
|
|
58
|
+
all.value = "";
|
|
59
|
+
all.textContent = allLabel;
|
|
60
|
+
select.append(all);
|
|
61
|
+
for (const value of values) {
|
|
62
|
+
const option = document.createElement("option");
|
|
63
|
+
option.value = value;
|
|
64
|
+
option.textContent = value;
|
|
65
|
+
select.append(option);
|
|
66
|
+
}
|
|
67
|
+
if (values.includes(selected)) select.value = selected;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function notificationToast(notification) {
|
|
71
|
+
const task = findCurrentTask(state.tasks, notification.taskId);
|
|
72
|
+
if (!task) return null;
|
|
73
|
+
const toast = document.createElement("div");
|
|
74
|
+
toast.className = "toast";
|
|
75
|
+
const text = document.createElement("button");
|
|
76
|
+
text.type = "button";
|
|
77
|
+
text.className = "toast-content";
|
|
78
|
+
const title = document.createElement("strong");
|
|
79
|
+
title.textContent = notification.kind === "new" ? "New task" : "Task updated";
|
|
80
|
+
const description = document.createElement("span");
|
|
81
|
+
description.textContent = `${task.title} · ${statusLabel(task)}`;
|
|
82
|
+
text.append(title, description);
|
|
83
|
+
text.addEventListener("click", () => {
|
|
84
|
+
const current = findCurrentTask(state.tasks, notification.taskId);
|
|
85
|
+
if (current) openDialog(current);
|
|
86
|
+
else showMessage("This task is no longer present in the current snapshot.");
|
|
87
|
+
});
|
|
88
|
+
const dismiss = document.createElement("button");
|
|
89
|
+
dismiss.type = "button";
|
|
90
|
+
dismiss.className = "icon-button";
|
|
91
|
+
dismiss.setAttribute("aria-label", `Dismiss notification for ${task.title}`);
|
|
92
|
+
dismiss.textContent = "×";
|
|
93
|
+
dismiss.addEventListener("click", () => {
|
|
94
|
+
state.notifications = state.notifications.filter(({ id }) => id !== notification.id);
|
|
95
|
+
renderNotifications();
|
|
96
|
+
});
|
|
97
|
+
toast.append(text, dismiss);
|
|
98
|
+
return toast;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function renderNotifications() {
|
|
102
|
+
elements.toastList.replaceChildren(
|
|
103
|
+
...state.notifications.map(notificationToast).filter(Boolean),
|
|
104
|
+
);
|
|
105
|
+
elements.notifications.hidden = state.notifications.length === 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function detailRow(term, value) {
|
|
109
|
+
const dt = document.createElement("dt");
|
|
110
|
+
dt.textContent = term;
|
|
111
|
+
const dd = document.createElement("dd");
|
|
112
|
+
dd.textContent = value ?? "—";
|
|
113
|
+
return [dt, dd];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function openDialog(task) {
|
|
117
|
+
state.selectedTask = task;
|
|
118
|
+
elements.dialogProject.textContent = task.project.name;
|
|
119
|
+
elements.dialogTitle.textContent = task.title;
|
|
120
|
+
elements.dialogSummary.textContent = task.summary ?? "No semantic result has been reported yet.";
|
|
121
|
+
elements.dialogInstruction.textContent = task.instruction;
|
|
122
|
+
elements.copyThreadId.disabled = !task.threadId;
|
|
123
|
+
elements.dialogMetadata.replaceChildren(
|
|
124
|
+
...detailRow("Status", statusLabel(task)),
|
|
125
|
+
...detailRow("Task ID", task.id),
|
|
126
|
+
...detailRow("Thread ID", task.threadId),
|
|
127
|
+
...detailRow("Turn ID", task.turnId),
|
|
128
|
+
...detailRow("Project path", task.project.path),
|
|
129
|
+
...detailRow("Created", formatTime(task.createdAt)),
|
|
130
|
+
...detailRow("Updated", formatTime(task.updatedAt ?? task.createdAt)),
|
|
131
|
+
...detailRow("Updated by", task.updatedBy),
|
|
132
|
+
);
|
|
133
|
+
if (!elements.dialog.open) elements.dialog.showModal();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function taskCard(task) {
|
|
137
|
+
const article = document.createElement("article");
|
|
138
|
+
article.className = "task-card";
|
|
139
|
+
const heading = document.createElement("div");
|
|
140
|
+
heading.className = "task-heading";
|
|
141
|
+
const title = document.createElement("button");
|
|
142
|
+
title.type = "button";
|
|
143
|
+
title.className = "task-title";
|
|
144
|
+
title.textContent = task.title;
|
|
145
|
+
title.addEventListener("click", () => openDialog(task));
|
|
146
|
+
const badge = document.createElement("span");
|
|
147
|
+
badge.className = `status status-${task.status ?? "unresolved"}`;
|
|
148
|
+
badge.textContent = statusLabel(task);
|
|
149
|
+
heading.append(title, badge);
|
|
150
|
+
const project = document.createElement("p");
|
|
151
|
+
project.className = "task-project";
|
|
152
|
+
project.textContent = task.project.name;
|
|
153
|
+
const summary = document.createElement("p");
|
|
154
|
+
summary.className = "task-summary";
|
|
155
|
+
summary.textContent = task.summary ?? "No semantic result reported yet.";
|
|
156
|
+
const time = document.createElement("time");
|
|
157
|
+
time.dateTime = task.updatedAt ?? task.createdAt;
|
|
158
|
+
time.textContent = `Updated ${formatTime(task.updatedAt ?? task.createdAt)}`;
|
|
159
|
+
article.append(heading, project, summary, time);
|
|
160
|
+
return article;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function render() {
|
|
164
|
+
const project = elements.projectFilter.value;
|
|
165
|
+
const status = elements.statusFilter.value;
|
|
166
|
+
const visible = state.tasks.filter((task) =>
|
|
167
|
+
(!project || task.project.name === project)
|
|
168
|
+
&& (!status || statusLabel(task) === status));
|
|
169
|
+
elements.taskList.replaceChildren(...visible.map(taskCard));
|
|
170
|
+
elements.emptyState.hidden = visible.length > 0;
|
|
171
|
+
elements.taskCount.textContent = `${visible.length} of ${state.tasks.length} task${state.tasks.length === 1 ? "" : "s"}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function applySnapshot(snapshot) {
|
|
175
|
+
const reconciled = reconcileNotifications({
|
|
176
|
+
initialized: state.initialized,
|
|
177
|
+
notifications: state.notifications,
|
|
178
|
+
signatures: state.signatures,
|
|
179
|
+
}, snapshot.tasks, snapshot.revision);
|
|
180
|
+
state.tasks = snapshot.tasks;
|
|
181
|
+
state.signatures = reconciled.signatures;
|
|
182
|
+
state.notifications = reconciled.notifications;
|
|
183
|
+
state.initialized = true;
|
|
184
|
+
if (snapshot.healthy === false) {
|
|
185
|
+
showMessage("The task log is temporarily unavailable. Showing the last valid snapshot.");
|
|
186
|
+
} else {
|
|
187
|
+
elements.dashboardMessage.hidden = true;
|
|
188
|
+
}
|
|
189
|
+
replaceOptions(
|
|
190
|
+
elements.projectFilter,
|
|
191
|
+
[...new Set(state.tasks.map((task) => task.project.name))].sort(),
|
|
192
|
+
"All projects",
|
|
193
|
+
);
|
|
194
|
+
replaceOptions(
|
|
195
|
+
elements.statusFilter,
|
|
196
|
+
[...new Set(state.tasks.map(statusLabel))].sort(),
|
|
197
|
+
"All statuses",
|
|
198
|
+
);
|
|
199
|
+
if (state.selectedTask) {
|
|
200
|
+
const updated = state.tasks.find((task) => task.id === state.selectedTask.id);
|
|
201
|
+
if (updated && elements.dialog.open) openDialog(updated);
|
|
202
|
+
}
|
|
203
|
+
renderNotifications();
|
|
204
|
+
render();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function showMessage(message) {
|
|
208
|
+
elements.dashboardMessage.textContent = message;
|
|
209
|
+
elements.dashboardMessage.hidden = false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const events = new EventSource("/api/events");
|
|
213
|
+
events.addEventListener("open", () => setConnection(true));
|
|
214
|
+
events.addEventListener("error", () => setConnection(false));
|
|
215
|
+
events.addEventListener("snapshot", (event) => {
|
|
216
|
+
setConnection(true);
|
|
217
|
+
applySnapshot(JSON.parse(event.data));
|
|
218
|
+
});
|
|
219
|
+
events.addEventListener("dashboard-error", (event) => {
|
|
220
|
+
showMessage(JSON.parse(event.data).message);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
elements.projectFilter.addEventListener("change", render);
|
|
224
|
+
elements.statusFilter.addEventListener("change", render);
|
|
225
|
+
elements.clearNotifications.addEventListener("click", () => {
|
|
226
|
+
state.notifications = [];
|
|
227
|
+
renderNotifications();
|
|
228
|
+
});
|
|
229
|
+
elements.closeDialog.addEventListener("click", () => elements.dialog.close());
|
|
230
|
+
elements.dialog.addEventListener("click", (event) => {
|
|
231
|
+
if (event.target === elements.dialog) elements.dialog.close();
|
|
232
|
+
});
|
|
233
|
+
elements.copyThreadId.addEventListener("click", async () => {
|
|
234
|
+
if (!state.selectedTask?.threadId) return;
|
|
235
|
+
try {
|
|
236
|
+
await navigator.clipboard.writeText(state.selectedTask.threadId);
|
|
237
|
+
elements.copyThreadId.textContent = "Copied";
|
|
238
|
+
setTimeout(() => { elements.copyThreadId.textContent = "Copy thread ID"; }, 1_500);
|
|
239
|
+
} catch {
|
|
240
|
+
showMessage("Clipboard access is unavailable. Copy the thread ID from the metadata below.");
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
elements.openProject.addEventListener("click", async () => {
|
|
244
|
+
if (!state.selectedTask) return;
|
|
245
|
+
elements.openProject.disabled = true;
|
|
246
|
+
try {
|
|
247
|
+
const response = await fetch(`/api/tasks/${encodeURIComponent(state.selectedTask.id)}/open-project`, {
|
|
248
|
+
method: "POST",
|
|
249
|
+
});
|
|
250
|
+
const result = await response.json();
|
|
251
|
+
showMessage(result.message);
|
|
252
|
+
} finally {
|
|
253
|
+
elements.openProject.disabled = false;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="light dark">
|
|
7
|
+
<title>TaskChef dashboard</title>
|
|
8
|
+
<link rel="stylesheet" href="/styles.css">
|
|
9
|
+
<script src="/app.js" type="module"></script>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<header class="site-header">
|
|
13
|
+
<div>
|
|
14
|
+
<p class="eyebrow">TaskChef</p>
|
|
15
|
+
<h1>Task dashboard</h1>
|
|
16
|
+
<p class="subtitle">Latest delegated work, updated as TaskChef reports change.</p>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="connection" role="status" aria-live="polite">
|
|
19
|
+
<span id="connection-dot" class="connection-dot"></span>
|
|
20
|
+
<span id="connection-label">Connecting…</span>
|
|
21
|
+
</div>
|
|
22
|
+
</header>
|
|
23
|
+
|
|
24
|
+
<main>
|
|
25
|
+
<section class="toolbar" aria-label="Task filters">
|
|
26
|
+
<label>
|
|
27
|
+
Project
|
|
28
|
+
<select id="project-filter">
|
|
29
|
+
<option value="">All projects</option>
|
|
30
|
+
</select>
|
|
31
|
+
</label>
|
|
32
|
+
<label>
|
|
33
|
+
Status
|
|
34
|
+
<select id="status-filter">
|
|
35
|
+
<option value="">All statuses</option>
|
|
36
|
+
</select>
|
|
37
|
+
</label>
|
|
38
|
+
<p id="task-count" class="task-count" aria-live="polite"></p>
|
|
39
|
+
</section>
|
|
40
|
+
|
|
41
|
+
<div id="dashboard-message" class="dashboard-message" role="status" hidden></div>
|
|
42
|
+
<section aria-labelledby="tasks-heading">
|
|
43
|
+
<h2 id="tasks-heading" class="visually-hidden">Tasks</h2>
|
|
44
|
+
<div id="task-list" class="task-list"></div>
|
|
45
|
+
<div id="empty-state" class="empty-state" hidden>
|
|
46
|
+
<h2>No tasks match these filters</h2>
|
|
47
|
+
<p>Choose a different project or status to see more work.</p>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
</main>
|
|
51
|
+
|
|
52
|
+
<aside id="notifications" class="notifications" aria-label="Task notifications" hidden>
|
|
53
|
+
<div class="notifications-heading">
|
|
54
|
+
<strong>Updates</strong>
|
|
55
|
+
<button id="clear-notifications" class="text-button" type="button">Clear all</button>
|
|
56
|
+
</div>
|
|
57
|
+
<div id="toast-list" class="toast-list" aria-live="polite"></div>
|
|
58
|
+
</aside>
|
|
59
|
+
|
|
60
|
+
<dialog id="task-dialog" aria-labelledby="dialog-title">
|
|
61
|
+
<div class="dialog-header">
|
|
62
|
+
<div>
|
|
63
|
+
<p id="dialog-project" class="eyebrow"></p>
|
|
64
|
+
<h2 id="dialog-title"></h2>
|
|
65
|
+
</div>
|
|
66
|
+
<button id="close-dialog" class="icon-button" type="button" aria-label="Close task details">×</button>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="dialog-actions">
|
|
69
|
+
<button id="open-project" class="primary-button" type="button">Open project in Codex</button>
|
|
70
|
+
<button id="copy-thread-id" class="secondary-button" type="button">Copy thread ID</button>
|
|
71
|
+
</div>
|
|
72
|
+
<section>
|
|
73
|
+
<h3>Result</h3>
|
|
74
|
+
<p id="dialog-summary" class="preserve-lines"></p>
|
|
75
|
+
</section>
|
|
76
|
+
<section>
|
|
77
|
+
<h3>Original instruction</h3>
|
|
78
|
+
<pre id="dialog-instruction"></pre>
|
|
79
|
+
</section>
|
|
80
|
+
<section>
|
|
81
|
+
<h3>Metadata</h3>
|
|
82
|
+
<dl id="dialog-metadata" class="metadata"></dl>
|
|
83
|
+
</section>
|
|
84
|
+
</dialog>
|
|
85
|
+
</body>
|
|
86
|
+
</html>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const MAX_NOTIFICATIONS = 50;
|
|
2
|
+
|
|
3
|
+
export function taskSignature(task) {
|
|
4
|
+
return JSON.stringify([
|
|
5
|
+
task.threadId,
|
|
6
|
+
task.status,
|
|
7
|
+
task.summary,
|
|
8
|
+
task.turnId,
|
|
9
|
+
task.updatedAt,
|
|
10
|
+
task.updatedBy,
|
|
11
|
+
]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function findCurrentTask(tasks, taskId) {
|
|
15
|
+
return tasks.find((task) => task.id === taskId) ?? null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function reconcileNotifications(
|
|
19
|
+
{ initialized, notifications, signatures },
|
|
20
|
+
tasks,
|
|
21
|
+
revision,
|
|
22
|
+
) {
|
|
23
|
+
const nextSignatures = new Map(tasks.map((task) => [task.id, taskSignature(task)]));
|
|
24
|
+
if (!initialized) return { notifications, signatures: nextSignatures };
|
|
25
|
+
const additions = [];
|
|
26
|
+
for (const task of tasks) {
|
|
27
|
+
let kind = null;
|
|
28
|
+
if (!signatures.has(task.id)) kind = "new";
|
|
29
|
+
else if (signatures.get(task.id) !== nextSignatures.get(task.id)) kind = "changed";
|
|
30
|
+
if (kind) additions.push({
|
|
31
|
+
id: `${revision}:${task.id}`,
|
|
32
|
+
kind,
|
|
33
|
+
taskId: task.id,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
notifications: [...additions, ...notifications].slice(0, MAX_NOTIFICATIONS),
|
|
38
|
+
signatures: nextSignatures,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light;
|
|
3
|
+
--background: #f5f4f0;
|
|
4
|
+
--surface: #ffffff;
|
|
5
|
+
--surface-muted: #eeece6;
|
|
6
|
+
--text: #1d211f;
|
|
7
|
+
--muted: #626964;
|
|
8
|
+
--border: #d8d7d1;
|
|
9
|
+
--accent: #215f4a;
|
|
10
|
+
--accent-soft: #dcebe4;
|
|
11
|
+
--danger: #8b3a35;
|
|
12
|
+
--warning: #8a5c17;
|
|
13
|
+
--shadow: 0 12px 30px rgb(24 33 29 / 10%);
|
|
14
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
* { box-sizing: border-box; }
|
|
18
|
+
|
|
19
|
+
body {
|
|
20
|
+
margin: 0;
|
|
21
|
+
background: var(--background);
|
|
22
|
+
color: var(--text);
|
|
23
|
+
line-height: 1.5;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
button, select { font: inherit; }
|
|
27
|
+
button { color: inherit; }
|
|
28
|
+
|
|
29
|
+
.site-header, main {
|
|
30
|
+
width: min(1080px, calc(100% - 40px));
|
|
31
|
+
margin-inline: auto;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.site-header {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: flex-end;
|
|
37
|
+
justify-content: space-between;
|
|
38
|
+
gap: 24px;
|
|
39
|
+
padding-block: 48px 28px;
|
|
40
|
+
border-bottom: 1px solid var(--border);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
h1, h2, h3, p { margin-top: 0; }
|
|
44
|
+
h1 { margin-bottom: 6px; font-size: clamp(2rem, 5vw, 3.4rem); line-height: 1; letter-spacing: -0.04em; }
|
|
45
|
+
h2 { line-height: 1.2; }
|
|
46
|
+
h3 { margin-bottom: 8px; font-size: 0.84rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
|
|
47
|
+
|
|
48
|
+
.eyebrow { margin-bottom: 10px; color: var(--accent); font-size: 0.78rem; font-weight: 750; letter-spacing: 0.15em; text-transform: uppercase; }
|
|
49
|
+
.subtitle, .task-project, time { color: var(--muted); }
|
|
50
|
+
.subtitle { margin-bottom: 0; }
|
|
51
|
+
|
|
52
|
+
.connection { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 0.9rem; }
|
|
53
|
+
.connection-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--warning); }
|
|
54
|
+
.connection-dot.connected { background: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); }
|
|
55
|
+
|
|
56
|
+
main { padding-block: 28px 80px; }
|
|
57
|
+
|
|
58
|
+
.toolbar {
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: flex-end;
|
|
61
|
+
gap: 16px;
|
|
62
|
+
margin-bottom: 22px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
label { display: grid; gap: 6px; color: var(--muted); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
|
|
66
|
+
select { min-width: 180px; padding: 9px 34px 9px 11px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface); color: var(--text); }
|
|
67
|
+
.task-count { margin: 0 0 9px auto; color: var(--muted); font-size: 0.9rem; }
|
|
68
|
+
|
|
69
|
+
.task-list { display: grid; gap: 12px; }
|
|
70
|
+
.task-card { padding: 20px 22px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface); box-shadow: 0 1px 0 rgb(0 0 0 / 2%); }
|
|
71
|
+
.task-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; }
|
|
72
|
+
.task-title { padding: 0; border: 0; background: none; font-size: 1.08rem; font-weight: 720; text-align: left; cursor: pointer; }
|
|
73
|
+
.task-title:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
|
|
74
|
+
.task-project { margin: 3px 0 12px; font-size: 0.86rem; }
|
|
75
|
+
.task-summary { max-width: 78ch; margin-bottom: 14px; }
|
|
76
|
+
time { font-size: 0.78rem; }
|
|
77
|
+
|
|
78
|
+
.status { flex: none; padding: 4px 9px; border-radius: 999px; background: var(--surface-muted); color: var(--muted); font-size: 0.72rem; font-weight: 750; text-transform: capitalize; }
|
|
79
|
+
.status-completed { background: var(--accent-soft); color: var(--accent); }
|
|
80
|
+
.status-failed { background: #f3dfdd; color: var(--danger); }
|
|
81
|
+
.status-needs_input { background: #f4e9ce; color: var(--warning); }
|
|
82
|
+
|
|
83
|
+
.empty-state { padding: 60px 20px; border: 1px dashed var(--border); border-radius: 10px; text-align: center; color: var(--muted); }
|
|
84
|
+
.empty-state h2 { margin-bottom: 6px; color: var(--text); }
|
|
85
|
+
|
|
86
|
+
.dashboard-message { margin-bottom: 18px; padding: 11px 14px; border: 1px solid #d8c58e; border-radius: 7px; background: #fff7de; color: #604613; }
|
|
87
|
+
|
|
88
|
+
.notifications { position: fixed; right: 20px; bottom: 20px; z-index: 3; width: min(380px, calc(100vw - 40px)); padding: 14px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface); box-shadow: var(--shadow); }
|
|
89
|
+
.notifications-heading { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
|
90
|
+
.toast-list { display: grid; gap: 7px; max-height: 50vh; overflow: auto; }
|
|
91
|
+
.toast { display: flex; align-items: flex-start; gap: 8px; padding: 10px; border-radius: 7px; background: var(--surface-muted); }
|
|
92
|
+
.toast-content { display: grid; flex: 1; gap: 2px; padding: 0; border: 0; background: none; text-align: left; cursor: pointer; }
|
|
93
|
+
.toast-content span { color: var(--muted); font-size: 0.85rem; }
|
|
94
|
+
|
|
95
|
+
.text-button, .icon-button { border: 0; background: none; cursor: pointer; }
|
|
96
|
+
.text-button { color: var(--accent); font-size: 0.82rem; font-weight: 700; }
|
|
97
|
+
.icon-button { padding: 0 4px; color: var(--muted); font-size: 1.4rem; line-height: 1; }
|
|
98
|
+
|
|
99
|
+
dialog { width: min(760px, calc(100vw - 32px)); max-height: min(82vh, 900px); padding: 28px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); color: var(--text); box-shadow: var(--shadow); }
|
|
100
|
+
dialog::backdrop { background: rgb(18 23 21 / 50%); backdrop-filter: blur(2px); }
|
|
101
|
+
.dialog-header { display: flex; justify-content: space-between; gap: 20px; }
|
|
102
|
+
.dialog-header h2 { margin-bottom: 12px; font-size: 1.6rem; }
|
|
103
|
+
.dialog-actions { display: flex; gap: 8px; margin-bottom: 26px; }
|
|
104
|
+
.primary-button, .secondary-button { padding: 8px 12px; border-radius: 7px; font-weight: 700; cursor: pointer; }
|
|
105
|
+
.primary-button { border: 1px solid var(--accent); background: var(--accent); color: white; }
|
|
106
|
+
.secondary-button { border: 1px solid var(--border); background: var(--surface); }
|
|
107
|
+
.primary-button:disabled, .secondary-button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
108
|
+
dialog section + section { margin-top: 24px; }
|
|
109
|
+
.preserve-lines { white-space: pre-wrap; }
|
|
110
|
+
pre { max-height: 280px; margin: 0; padding: 14px; overflow: auto; border-radius: 7px; background: var(--surface-muted); white-space: pre-wrap; overflow-wrap: anywhere; font: 0.86rem/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
111
|
+
.metadata { display: grid; grid-template-columns: minmax(100px, 150px) 1fr; margin: 0; font-size: 0.88rem; }
|
|
112
|
+
.metadata dt, .metadata dd { padding: 7px 0; border-bottom: 1px solid var(--border); overflow-wrap: anywhere; }
|
|
113
|
+
.metadata dt { color: var(--muted); }
|
|
114
|
+
.metadata dd { margin: 0; }
|
|
115
|
+
|
|
116
|
+
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
117
|
+
:focus-visible { outline: 3px solid rgb(33 95 74 / 35%); outline-offset: 3px; }
|
|
118
|
+
|
|
119
|
+
@media (max-width: 650px) {
|
|
120
|
+
.site-header { align-items: flex-start; padding-top: 32px; }
|
|
121
|
+
.toolbar { align-items: stretch; flex-direction: column; }
|
|
122
|
+
.toolbar label, .toolbar select { width: 100%; }
|
|
123
|
+
.task-count { margin: 0; }
|
|
124
|
+
.task-heading { align-items: flex-start; }
|
|
125
|
+
.dialog-actions { align-items: stretch; flex-direction: column; }
|
|
126
|
+
.metadata { grid-template-columns: 1fr; }
|
|
127
|
+
.metadata dt { padding-bottom: 0; border-bottom: 0; }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@media (prefers-color-scheme: dark) {
|
|
131
|
+
:root {
|
|
132
|
+
color-scheme: dark;
|
|
133
|
+
--background: #181b19;
|
|
134
|
+
--surface: #222624;
|
|
135
|
+
--surface-muted: #2d322f;
|
|
136
|
+
--text: #edf1ee;
|
|
137
|
+
--muted: #aeb7b1;
|
|
138
|
+
--border: #3d4440;
|
|
139
|
+
--accent: #7dc1a4;
|
|
140
|
+
--accent-soft: #244436;
|
|
141
|
+
--danger: #ef9991;
|
|
142
|
+
--warning: #e4bd77;
|
|
143
|
+
}
|
|
144
|
+
.dashboard-message { border-color: #66592e; background: #38321d; color: #f1d98c; }
|
|
145
|
+
.status-failed { background: #4e2927; }
|
|
146
|
+
.status-needs_input { background: #4c3b20; }
|
|
147
|
+
}
|