Refine scheduled task dashboard UX (#135)
* add gated codex goals support * sync README SDK versions * bump claude agent sdk * add codex goals settings toggle * reuse status dashboard message and simplify settings actions * refine settings page UX * fix settings nav hash routing * add dashboard UX verification * refine dashboard settings and inbox UX * remove inbox top-level navigation * remove dashboard health top-level navigation * fix: allow runtime image attachment paths * feat: configure attachment allowlist dirs * fix: clean duplicate dashboard status messages * fix: poll dashboard duplicate cleanup between status updates * fix: delete dashboard duplicates on create * Refine settings IA with tabbed sections * Add read-only runtime inventory settings * Fix runtime inventory MCP JSON parsing * Add room skill settings inventory * Add room skill setting mutations * Apply room skill overrides to runner spawn * Refine scheduled task dashboard UX
This commit is contained in:
688
apps/dashboard/src/TaskPanel.css
Normal file
688
apps/dashboard/src/TaskPanel.css
Normal file
@@ -0,0 +1,688 @@
|
|||||||
|
.task-board {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-command-center {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.2fr) minmax(210px, 0.75fr) minmax(
|
||||||
|
260px,
|
||||||
|
1fr
|
||||||
|
);
|
||||||
|
gap: 12px;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: 26px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 12% 0%, rgba(191, 95, 44, 0.18), transparent 34%),
|
||||||
|
linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 255, 255, 0.065),
|
||||||
|
rgba(255, 255, 255, 0.025)
|
||||||
|
);
|
||||||
|
box-shadow: 0 18px 48px rgba(16, 10, 6, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-command-copy,
|
||||||
|
.task-command-next,
|
||||||
|
.task-summary-metric {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-command-copy strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: clamp(23px, 4vw, 34px);
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-command-copy p {
|
||||||
|
max-width: 520px;
|
||||||
|
margin: 9px 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-command-next {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
align-content: center;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid rgba(191, 95, 44, 0.22);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(191, 95, 44, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-command-next small,
|
||||||
|
.task-command-next span,
|
||||||
|
.task-summary-metric small {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-command-next strong {
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 21px;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-command-next span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-command-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-summary-metric {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
align-content: center;
|
||||||
|
min-height: 70px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(255, 255, 255, 0.035);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-summary-metric strong {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-create-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(180px, 0.42fr) minmax(0, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: 24px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-edit-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns:
|
||||||
|
minmax(0, 1.4fr) minmax(110px, 0.6fr) minmax(0, 1fr)
|
||||||
|
auto;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: end;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 9px;
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-create-head {
|
||||||
|
display: grid;
|
||||||
|
align-content: center;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid rgba(191, 95, 44, 0.2);
|
||||||
|
border-radius: 20px;
|
||||||
|
background:
|
||||||
|
linear-gradient(145deg, rgba(191, 95, 44, 0.14), transparent),
|
||||||
|
rgba(255, 255, 255, 0.035);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-create-head strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1.06;
|
||||||
|
letter-spacing: -0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-create-head p {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-create-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-form-controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(160px, 1.2fr) repeat(3, minmax(110px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-form-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-form-footer > span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-create-form label,
|
||||||
|
.task-edit-form label {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 4px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-create-form input,
|
||||||
|
.task-create-form select,
|
||||||
|
.task-create-form textarea,
|
||||||
|
.task-edit-form input,
|
||||||
|
.task-edit-form select,
|
||||||
|
.task-edit-form textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
border: 1px solid var(--panel-border-strong);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: var(--ink);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-create-form textarea,
|
||||||
|
.task-edit-form textarea {
|
||||||
|
min-height: 82px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-edit-form textarea {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-create-form input:focus-visible,
|
||||||
|
.task-create-form select:focus-visible,
|
||||||
|
.task-create-form textarea:focus-visible,
|
||||||
|
.task-edit-form input:focus-visible,
|
||||||
|
.task-edit-form select:focus-visible,
|
||||||
|
.task-edit-form textarea:focus-visible {
|
||||||
|
outline: 3px solid rgba(191, 95, 44, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-create-form button,
|
||||||
|
.task-edit-form button {
|
||||||
|
min-height: 44px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0 18px;
|
||||||
|
color: var(--bg);
|
||||||
|
background: var(--accent);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-create-form button:disabled,
|
||||||
|
.task-edit-form button:disabled {
|
||||||
|
cursor: progress;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-form-wide {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-create-form .task-form-wide {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-lanes {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-group {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: 24px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent),
|
||||||
|
rgba(255, 255, 255, 0.018);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-group-completed {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-group-completed[open] {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-group-completed summary {
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-group-completed summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-group-head {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-group-head strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 18px;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: 20px;
|
||||||
|
background:
|
||||||
|
linear-gradient(160deg, rgba(255, 255, 255, 0.055), transparent 58%),
|
||||||
|
var(--bg-card);
|
||||||
|
box-shadow: 0 14px 34px rgba(15, 10, 7, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card-main,
|
||||||
|
.task-time-grid,
|
||||||
|
.task-suspended,
|
||||||
|
.task-result {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card-main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-title {
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-kind {
|
||||||
|
color: var(--accent-strong);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-title strong {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 16px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-meta-strip {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-meta-strip span {
|
||||||
|
display: inline-flex;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 26px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 9px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-status-line {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-action {
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: 1px solid var(--panel-border-strong);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--ink);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-action:hover,
|
||||||
|
.task-action:focus-visible {
|
||||||
|
border-color: rgba(191, 95, 44, 0.42);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(191, 95, 44, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-action:disabled {
|
||||||
|
cursor: progress;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-action.is-busy {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 7px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: progress;
|
||||||
|
opacity: 0.85;
|
||||||
|
background: rgba(191, 95, 44, 0.12);
|
||||||
|
border-color: rgba(191, 95, 44, 0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-action.is-busy::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
border-right-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: ej-spin 0.7s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ej-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-action-cancel {
|
||||||
|
color: #8f2f22;
|
||||||
|
border-color: rgba(183, 71, 52, 0.22);
|
||||||
|
background: rgba(183, 71, 52, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-time-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(150px, 1.2fr) repeat(2, minmax(0, 0.9fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-time-grid > span,
|
||||||
|
.task-suspended,
|
||||||
|
.task-result {
|
||||||
|
padding: 9px;
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(255, 255, 255, 0.025);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-next-run {
|
||||||
|
border-color: rgba(191, 95, 44, 0.25) !important;
|
||||||
|
background: rgba(191, 95, 44, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-time-grid small,
|
||||||
|
.task-suspended span,
|
||||||
|
.task-result span,
|
||||||
|
.task-time-grid em,
|
||||||
|
.task-suspended em {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-time-grid small,
|
||||||
|
.task-time-grid strong,
|
||||||
|
.task-time-grid em,
|
||||||
|
.task-suspended span,
|
||||||
|
.task-suspended strong,
|
||||||
|
.task-suspended em,
|
||||||
|
.task-result span,
|
||||||
|
.task-result strong {
|
||||||
|
display: block;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-time-grid strong,
|
||||||
|
.task-suspended strong,
|
||||||
|
.task-result strong {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-next-run strong {
|
||||||
|
font-size: 18px;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-suspended {
|
||||||
|
border-color: rgba(181, 138, 34, 0.22);
|
||||||
|
background: rgba(181, 138, 34, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-result {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
border-color: rgba(63, 127, 81, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-fail {
|
||||||
|
border-color: rgba(183, 71, 52, 0.28);
|
||||||
|
background: rgba(183, 71, 52, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-none {
|
||||||
|
border-color: rgba(109, 115, 95, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-prompt {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-edit summary,
|
||||||
|
.task-prompt summary {
|
||||||
|
min-height: 32px;
|
||||||
|
color: var(--accent-strong);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-prompt p {
|
||||||
|
margin: 4px 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-prompt small {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family:
|
||||||
|
'SF Mono', 'Cascadia Code', 'Roboto Mono', ui-monospace, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-group-empty {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
background: rgba(255, 255, 255, 0.025);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-watchers {
|
||||||
|
color: #173e23;
|
||||||
|
background: rgba(63, 127, 81, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-scheduled {
|
||||||
|
color: #6e4a05;
|
||||||
|
background: rgba(181, 138, 34, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-paused {
|
||||||
|
color: #6e4a05;
|
||||||
|
background: rgba(181, 138, 34, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-completed {
|
||||||
|
color: #625d52;
|
||||||
|
background: rgba(109, 115, 95, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card > p {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.task-command-center,
|
||||||
|
.task-create-form,
|
||||||
|
.task-form-controls,
|
||||||
|
.task-card-main {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-command-center {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-command-metrics {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-summary-metric {
|
||||||
|
min-height: 62px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-create-head {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-create-head strong {
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-form-footer {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-form-footer button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-group {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-edit-form {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-form-wide {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card {
|
||||||
|
gap: 7px;
|
||||||
|
padding: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-action {
|
||||||
|
min-height: 44px;
|
||||||
|
padding-inline: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card-main {
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-time-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-time-grid > span,
|
||||||
|
.task-suspended,
|
||||||
|
.task-result {
|
||||||
|
padding: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-prompt summary {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 380px) {
|
||||||
|
.task-command-metrics {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-time-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,9 @@ describe('TaskPanel', () => {
|
|||||||
|
|
||||||
expect(html).toContain(t.tasks.groups.scheduled);
|
expect(html).toContain(t.tasks.groups.scheduled);
|
||||||
expect(html).toContain('eyejokerdb-main');
|
expect(html).toContain('eyejokerdb-main');
|
||||||
|
expect(html).toContain(t.panels.scheduled);
|
||||||
|
expect(html).toContain(t.tasks.next);
|
||||||
|
expect(html).toContain('10m');
|
||||||
expect(html).toContain(t.tasks.actions.pause);
|
expect(html).toContain(t.tasks.actions.pause);
|
||||||
expect(html).toContain(t.tasks.actions.cancel);
|
expect(html).toContain(t.tasks.actions.cancel);
|
||||||
expect(html).toContain('Task completed');
|
expect(html).toContain('Task completed');
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { localeTags, type Locale, type Messages } from './i18n';
|
|||||||
import { redactSecretsForPreview } from './redaction';
|
import { redactSecretsForPreview } from './redaction';
|
||||||
import { statusLabel } from './dashboardHelpers';
|
import { statusLabel } from './dashboardHelpers';
|
||||||
import { TaskActionButtons, type TaskActionKey } from './TaskActionButtons';
|
import { TaskActionButtons, type TaskActionKey } from './TaskActionButtons';
|
||||||
|
import './TaskPanel.css';
|
||||||
|
|
||||||
export type { TaskActionKey } from './TaskActionButtons';
|
export type { TaskActionKey } from './TaskActionButtons';
|
||||||
|
|
||||||
@@ -41,6 +42,15 @@ interface TaskGroup {
|
|||||||
tasks: DashboardTask[];
|
tasks: DashboardTask[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TaskSummary {
|
||||||
|
completed: number;
|
||||||
|
nextTask: DashboardTask | null;
|
||||||
|
paused: number;
|
||||||
|
scheduled: number;
|
||||||
|
total: number;
|
||||||
|
watchers: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface TaskCreateFormProps {
|
interface TaskCreateFormProps {
|
||||||
rooms: RoomOption[];
|
rooms: RoomOption[];
|
||||||
onTaskCreate: (input: CreateScheduledTaskInput) => void;
|
onTaskCreate: (input: CreateScheduledTaskInput) => void;
|
||||||
@@ -170,6 +180,23 @@ function taskDisplayName(task: DashboardTask, t: Messages): string {
|
|||||||
return task.id;
|
return task.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function taskHeadline(task: DashboardTask, t: Messages): string {
|
||||||
|
if (task.isWatcher && task.ciProvider) {
|
||||||
|
return `${t.tasks.ciWatch} · ${task.ciProvider}`;
|
||||||
|
}
|
||||||
|
return safePreview(task.promptPreview, taskDisplayName(task, t));
|
||||||
|
}
|
||||||
|
|
||||||
|
function taskScheduleText(task: DashboardTask, t: Messages): string {
|
||||||
|
const scheduleType =
|
||||||
|
task.scheduleType === 'cron' ||
|
||||||
|
task.scheduleType === 'interval' ||
|
||||||
|
task.scheduleType === 'once'
|
||||||
|
? t.tasks.scheduleTypes[task.scheduleType]
|
||||||
|
: task.scheduleType;
|
||||||
|
return `${scheduleType} · ${task.scheduleValue || '-'}`;
|
||||||
|
}
|
||||||
|
|
||||||
function isTaskScheduleType(
|
function isTaskScheduleType(
|
||||||
value: FormDataEntryValue | null,
|
value: FormDataEntryValue | null,
|
||||||
): value is DashboardTaskScheduleType {
|
): value is DashboardTaskScheduleType {
|
||||||
@@ -259,6 +286,102 @@ function buildTaskGroups(tasks: DashboardTask[]): TaskGroup[] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildTaskSummary(tasks: DashboardTask[]): TaskSummary {
|
||||||
|
const summary: TaskSummary = {
|
||||||
|
completed: 0,
|
||||||
|
nextTask: null,
|
||||||
|
paused: 0,
|
||||||
|
scheduled: 0,
|
||||||
|
total: tasks.length,
|
||||||
|
watchers: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const task of tasks) {
|
||||||
|
const group = taskGroupKey(task);
|
||||||
|
if (group === 'watchers') summary.watchers += 1;
|
||||||
|
if (group === 'scheduled') summary.scheduled += 1;
|
||||||
|
if (group === 'paused') summary.paused += 1;
|
||||||
|
if (group === 'completed') summary.completed += 1;
|
||||||
|
|
||||||
|
if (task.status !== 'active' || !task.nextRun) continue;
|
||||||
|
const nextTime = new Date(task.nextRun).getTime();
|
||||||
|
if (Number.isNaN(nextTime)) continue;
|
||||||
|
const currentTime = summary.nextTask?.nextRun
|
||||||
|
? new Date(summary.nextTask.nextRun).getTime()
|
||||||
|
: Number.POSITIVE_INFINITY;
|
||||||
|
if (nextTime < currentTime) {
|
||||||
|
summary.nextTask = task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TaskSummaryMetric({ label, value }: { label: string; value: number }) {
|
||||||
|
return (
|
||||||
|
<span className="task-summary-metric">
|
||||||
|
<strong>{value}</strong>
|
||||||
|
<small>{label}</small>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TaskBoardSummary({
|
||||||
|
locale,
|
||||||
|
summary,
|
||||||
|
t,
|
||||||
|
}: {
|
||||||
|
locale: Locale;
|
||||||
|
summary: TaskSummary;
|
||||||
|
t: Messages;
|
||||||
|
}) {
|
||||||
|
const nextTask = summary.nextTask;
|
||||||
|
return (
|
||||||
|
<section className="task-command-center" aria-label={t.panels.scheduled}>
|
||||||
|
<div className="task-command-copy">
|
||||||
|
<span className="eyebrow">{t.panels.scheduled}</span>
|
||||||
|
<strong>
|
||||||
|
{summary.total} {t.tasks.count}
|
||||||
|
</strong>
|
||||||
|
<p>
|
||||||
|
{nextTask
|
||||||
|
? `${t.tasks.next}: ${taskHeadline(nextTask, t)}`
|
||||||
|
: t.tasks.empty}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="task-command-next">
|
||||||
|
<small>{t.tasks.next}</small>
|
||||||
|
<strong>
|
||||||
|
{nextTask ? formatRelativeDate(nextTask.nextRun, locale, t) : '-'}
|
||||||
|
</strong>
|
||||||
|
<span>
|
||||||
|
{nextTask
|
||||||
|
? `${nextTask.groupFolder} · ${taskScheduleText(nextTask, t)}`
|
||||||
|
: t.tasks.noTime}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="task-command-metrics">
|
||||||
|
<TaskSummaryMetric
|
||||||
|
label={t.tasks.groups.scheduled}
|
||||||
|
value={summary.scheduled}
|
||||||
|
/>
|
||||||
|
<TaskSummaryMetric
|
||||||
|
label={t.tasks.groups.watchers}
|
||||||
|
value={summary.watchers}
|
||||||
|
/>
|
||||||
|
<TaskSummaryMetric
|
||||||
|
label={t.tasks.groups.paused}
|
||||||
|
value={summary.paused}
|
||||||
|
/>
|
||||||
|
<TaskSummaryMetric
|
||||||
|
label={t.tasks.groups.completed}
|
||||||
|
value={summary.completed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TaskCreateForm({
|
function TaskCreateForm({
|
||||||
rooms,
|
rooms,
|
||||||
onTaskCreate,
|
onTaskCreate,
|
||||||
@@ -280,54 +403,62 @@ function TaskCreateForm({
|
|||||||
<div className="task-create-head">
|
<div className="task-create-head">
|
||||||
<span className="eyebrow">{t.tasks.createTitle}</span>
|
<span className="eyebrow">{t.tasks.createTitle}</span>
|
||||||
<strong>{t.tasks.createSubtitle}</strong>
|
<strong>{t.tasks.createSubtitle}</strong>
|
||||||
|
<p>{t.tasks.scheduleValueHint}</p>
|
||||||
|
</div>
|
||||||
|
<div className="task-create-body">
|
||||||
|
<label className="task-form-wide">
|
||||||
|
<span>{t.tasks.prompt}</span>
|
||||||
|
<textarea
|
||||||
|
name="prompt"
|
||||||
|
placeholder={t.tasks.promptPlaceholder}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="task-form-controls">
|
||||||
|
<label>
|
||||||
|
<span>{t.tasks.room}</span>
|
||||||
|
<select name="roomJid" required>
|
||||||
|
<option value="">{t.tasks.selectRoom}</option>
|
||||||
|
{rooms.map((room) => (
|
||||||
|
<option key={room.jid} value={room.jid}>
|
||||||
|
{room.name} · {room.folder}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>{t.tasks.scheduleType}</span>
|
||||||
|
<select name="scheduleType" required>
|
||||||
|
<option value="once">{t.tasks.scheduleTypes.once}</option>
|
||||||
|
<option value="interval">{t.tasks.scheduleTypes.interval}</option>
|
||||||
|
<option value="cron">{t.tasks.scheduleTypes.cron}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>{t.tasks.scheduleValue}</span>
|
||||||
|
<input
|
||||||
|
name="scheduleValue"
|
||||||
|
placeholder={t.tasks.scheduleValueHint}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>{t.tasks.context}</span>
|
||||||
|
<select name="contextMode" required>
|
||||||
|
<option value="isolated">{t.tasks.contextModes.isolated}</option>
|
||||||
|
<option value="group">{t.tasks.contextModes.group}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="task-form-footer">
|
||||||
|
<span>{t.tasks.schedule}</span>
|
||||||
|
<button disabled={taskActionKey === 'create'} type="submit">
|
||||||
|
{taskActionKey === 'create'
|
||||||
|
? t.tasks.actions.busy
|
||||||
|
: t.tasks.actions.create}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
|
||||||
<span>{t.tasks.room}</span>
|
|
||||||
<select name="roomJid" required>
|
|
||||||
<option value="">{t.tasks.selectRoom}</option>
|
|
||||||
{rooms.map((room) => (
|
|
||||||
<option key={room.jid} value={room.jid}>
|
|
||||||
{room.name} · {room.folder}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label className="task-form-wide">
|
|
||||||
<span>{t.tasks.prompt}</span>
|
|
||||||
<textarea
|
|
||||||
name="prompt"
|
|
||||||
placeholder={t.tasks.promptPlaceholder}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>{t.tasks.scheduleType}</span>
|
|
||||||
<select name="scheduleType" required>
|
|
||||||
<option value="once">{t.tasks.scheduleTypes.once}</option>
|
|
||||||
<option value="interval">{t.tasks.scheduleTypes.interval}</option>
|
|
||||||
<option value="cron">{t.tasks.scheduleTypes.cron}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>{t.tasks.scheduleValue}</span>
|
|
||||||
<input
|
|
||||||
name="scheduleValue"
|
|
||||||
placeholder={t.tasks.scheduleValueHint}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>{t.tasks.context}</span>
|
|
||||||
<select name="contextMode" required>
|
|
||||||
<option value="isolated">{t.tasks.contextModes.isolated}</option>
|
|
||||||
<option value="group">{t.tasks.contextModes.group}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<button disabled={taskActionKey === 'create'} type="submit">
|
|
||||||
{taskActionKey === 'create'
|
|
||||||
? t.tasks.actions.busy
|
|
||||||
: t.tasks.actions.create}
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -335,10 +466,10 @@ function TaskCreateForm({
|
|||||||
function TaskTimeGrid({ locale, t, task }: TaskDateProps) {
|
function TaskTimeGrid({ locale, t, task }: TaskDateProps) {
|
||||||
return (
|
return (
|
||||||
<div className="task-time-grid">
|
<div className="task-time-grid">
|
||||||
<span>
|
<span className="task-next-run">
|
||||||
<small>{t.tasks.next}</small>
|
<small>{t.tasks.next}</small>
|
||||||
<strong>{formatTaskDate(task.nextRun, locale)}</strong>
|
<strong>{formatRelativeDate(task.nextRun, locale, t)}</strong>
|
||||||
<em>{formatRelativeDate(task.nextRun, locale, t)}</em>
|
<em>{formatTaskDate(task.nextRun, locale)}</em>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<small>{t.tasks.last}</small>
|
<small>{t.tasks.last}</small>
|
||||||
@@ -347,8 +478,8 @@ function TaskTimeGrid({ locale, t, task }: TaskDateProps) {
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<small>{t.tasks.schedule}</small>
|
<small>{t.tasks.schedule}</small>
|
||||||
<strong>{task.scheduleType}</strong>
|
<strong>{taskScheduleText(task, t)}</strong>
|
||||||
<em>{task.scheduleValue}</em>
|
<em>{task.contextMode}</em>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -390,12 +521,23 @@ function TaskPromptDetails({ task, t }: { task: DashboardTask; t: Messages }) {
|
|||||||
<summary>{t.tasks.prompt}</summary>
|
<summary>{t.tasks.prompt}</summary>
|
||||||
<p>{safePreview(task.promptPreview, t.tasks.emptyPrompt)}</p>
|
<p>{safePreview(task.promptPreview, t.tasks.emptyPrompt)}</p>
|
||||||
<small>
|
<small>
|
||||||
{task.id} · {task.contextMode} · {task.promptLength} {t.units.chars}
|
{task.id} · {taskScheduleText(task, t)} · {task.promptLength}{' '}
|
||||||
|
{t.units.chars}
|
||||||
</small>
|
</small>
|
||||||
</details>
|
</details>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TaskMetaStrip({ task, t }: { task: DashboardTask; t: Messages }) {
|
||||||
|
return (
|
||||||
|
<div className="task-meta-strip">
|
||||||
|
<span>{task.groupFolder}</span>
|
||||||
|
<span>{task.agentType ?? t.tasks.task}</span>
|
||||||
|
{task.ciProvider ? <span>{task.ciProvider}</span> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TaskEditForm({
|
function TaskEditForm({
|
||||||
onTaskUpdate,
|
onTaskUpdate,
|
||||||
task,
|
task,
|
||||||
@@ -460,16 +602,14 @@ function TaskCard({
|
|||||||
<article className={`task-card task-card-${groupKey}`}>
|
<article className={`task-card task-card-${groupKey}`}>
|
||||||
<div className="task-card-main">
|
<div className="task-card-main">
|
||||||
<div className="task-title">
|
<div className="task-title">
|
||||||
<strong>{taskDisplayName(task, t)}</strong>
|
<span className="task-kind">{taskDisplayName(task, t)}</span>
|
||||||
<span className="mono-chip">{task.groupFolder}</span>
|
<strong>{taskHeadline(task, t)}</strong>
|
||||||
|
<TaskMetaStrip task={task} t={t} />
|
||||||
</div>
|
</div>
|
||||||
<div className="task-status-line">
|
<div className="task-status-line">
|
||||||
<span className={`pill pill-${task.status}`}>
|
<span className={`pill pill-${task.status}`}>
|
||||||
{statusLabel(task.status, t)}
|
{statusLabel(task.status, t)}
|
||||||
</span>
|
</span>
|
||||||
{task.ciProvider ? (
|
|
||||||
<span className="task-provider">{task.ciProvider}</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TaskActionButtons
|
<TaskActionButtons
|
||||||
@@ -546,6 +686,8 @@ function TaskGroupHead({
|
|||||||
|
|
||||||
function TaskGroupSection(props: TaskGroupSectionProps) {
|
function TaskGroupSection(props: TaskGroupSectionProps) {
|
||||||
const { group, t } = props;
|
const { group, t } = props;
|
||||||
|
if (group.tasks.length === 0) return null;
|
||||||
|
|
||||||
const label = t.tasks.groups[group.key];
|
const label = t.tasks.groups[group.key];
|
||||||
const body = <TaskGroupBody {...props} />;
|
const body = <TaskGroupBody {...props} />;
|
||||||
|
|
||||||
@@ -585,9 +727,11 @@ export function TaskPanel({
|
|||||||
t,
|
t,
|
||||||
}: TaskPanelProps) {
|
}: TaskPanelProps) {
|
||||||
const taskGroups = useMemo(() => buildTaskGroups(tasks), [tasks]);
|
const taskGroups = useMemo(() => buildTaskGroups(tasks), [tasks]);
|
||||||
|
const taskSummary = useMemo(() => buildTaskSummary(tasks), [tasks]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="task-board" aria-label={t.tasks.cardsAria}>
|
<div className="task-board" aria-label={t.tasks.cardsAria}>
|
||||||
|
<TaskBoardSummary locale={locale} summary={taskSummary} t={t} />
|
||||||
<TaskCreateForm
|
<TaskCreateForm
|
||||||
rooms={rooms}
|
rooms={rooms}
|
||||||
onTaskCreate={onTaskCreate}
|
onTaskCreate={onTaskCreate}
|
||||||
@@ -596,17 +740,19 @@ export function TaskPanel({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{tasks.length === 0 ? <EmptyState>{t.tasks.empty}</EmptyState> : null}
|
{tasks.length === 0 ? <EmptyState>{t.tasks.empty}</EmptyState> : null}
|
||||||
{taskGroups.map((group) => (
|
<div className="task-lanes">
|
||||||
<TaskGroupSection
|
{taskGroups.map((group) => (
|
||||||
group={group}
|
<TaskGroupSection
|
||||||
key={group.key}
|
group={group}
|
||||||
locale={locale}
|
key={group.key}
|
||||||
onTaskAction={onTaskAction}
|
locale={locale}
|
||||||
onTaskUpdate={onTaskUpdate}
|
onTaskAction={onTaskAction}
|
||||||
taskActionKey={taskActionKey}
|
onTaskUpdate={onTaskUpdate}
|
||||||
t={t}
|
taskActionKey={taskActionKey}
|
||||||
/>
|
t={t}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -490,33 +490,33 @@ export const messages = {
|
|||||||
noResult: '결과 없음',
|
noResult: '결과 없음',
|
||||||
noTime: '-',
|
noTime: '-',
|
||||||
now: '지금',
|
now: '지금',
|
||||||
createTitle: 'Create task',
|
createTitle: '작업 예약',
|
||||||
createSubtitle: 'Schedule work from web',
|
createSubtitle: '룸에 반복 작업 걸기',
|
||||||
room: 'Room',
|
room: '방',
|
||||||
selectRoom: 'Select room',
|
selectRoom: '방 선택',
|
||||||
scheduleType: 'Type',
|
scheduleType: '방식',
|
||||||
scheduleValue: 'Value',
|
scheduleValue: '시간값',
|
||||||
scheduleValueHint: 'ISO time, ms, or cron',
|
scheduleValueHint: 'ISO 시간, 10m 같은 간격, 또는 cron',
|
||||||
promptPlaceholder: 'What should the agent do?',
|
promptPlaceholder: '에이전트가 실행할 작업을 적어주세요.',
|
||||||
editPromptPlaceholder: 'Leave blank to keep prompt',
|
editPromptPlaceholder: '비우면 기존 프롬프트 유지',
|
||||||
scheduleTypes: {
|
scheduleTypes: {
|
||||||
once: 'Once',
|
once: '한 번',
|
||||||
interval: 'Interval',
|
interval: '간격',
|
||||||
cron: 'Cron',
|
cron: '크론',
|
||||||
},
|
},
|
||||||
contextModes: {
|
contextModes: {
|
||||||
isolated: 'Isolated',
|
isolated: '격리',
|
||||||
group: 'Group',
|
group: '방 맥락',
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
create: 'Create',
|
create: '생성',
|
||||||
edit: 'Edit',
|
edit: '수정',
|
||||||
save: 'Save',
|
save: '저장',
|
||||||
pause: 'Pause',
|
pause: '일시정지',
|
||||||
resume: 'Resume',
|
resume: '재개',
|
||||||
cancel: 'Cancel',
|
cancel: '취소',
|
||||||
busy: 'Working',
|
busy: '처리 중',
|
||||||
confirmCancel: 'Cancel this scheduled work?',
|
confirmCancel: '이 예약 작업을 취소할까요?',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
|
|||||||
@@ -2439,404 +2439,6 @@ progress::-moz-progress-bar {
|
|||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-board {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-create-form,
|
|
||||||
.task-edit-form {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: end;
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid var(--panel-border);
|
|
||||||
border-radius: 22px;
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-edit-form {
|
|
||||||
grid-template-columns:
|
|
||||||
minmax(0, 1.4fr) minmax(110px, 0.6fr) minmax(0, 1fr)
|
|
||||||
auto;
|
|
||||||
margin-top: 8px;
|
|
||||||
padding: 9px;
|
|
||||||
border-radius: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-create-head {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-create-head strong {
|
|
||||||
display: block;
|
|
||||||
margin-top: 2px;
|
|
||||||
font-size: 16px;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-create-form label,
|
|
||||||
.task-edit-form label {
|
|
||||||
display: grid;
|
|
||||||
min-width: 0;
|
|
||||||
gap: 4px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-create-form input,
|
|
||||||
.task-create-form select,
|
|
||||||
.task-create-form textarea,
|
|
||||||
.task-edit-form input,
|
|
||||||
.task-edit-form select,
|
|
||||||
.task-edit-form textarea {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 38px;
|
|
||||||
border: 1px solid var(--panel-border-strong);
|
|
||||||
border-radius: 13px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
color: var(--ink);
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
font: inherit;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-create-form textarea,
|
|
||||||
.task-edit-form textarea {
|
|
||||||
min-height: 38px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-create-form input:focus-visible,
|
|
||||||
.task-create-form select:focus-visible,
|
|
||||||
.task-create-form textarea:focus-visible,
|
|
||||||
.task-edit-form input:focus-visible,
|
|
||||||
.task-edit-form select:focus-visible,
|
|
||||||
.task-edit-form textarea:focus-visible {
|
|
||||||
outline: 3px solid rgba(191, 95, 44, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-create-form button,
|
|
||||||
.task-edit-form button {
|
|
||||||
min-height: 38px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0 14px;
|
|
||||||
color: var(--bg);
|
|
||||||
background: var(--accent);
|
|
||||||
font: inherit;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-create-form button:disabled,
|
|
||||||
.task-edit-form button:disabled {
|
|
||||||
cursor: progress;
|
|
||||||
opacity: 0.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-form-wide {
|
|
||||||
grid-column: span 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-group {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid var(--panel-border);
|
|
||||||
border-radius: 22px;
|
|
||||||
background: rgba(255, 255, 255, 0.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-group-completed {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-group-completed[open] {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-group-completed summary {
|
|
||||||
cursor: pointer;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-group-completed summary::-webkit-details-marker {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-group-head {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-group-head strong {
|
|
||||||
display: block;
|
|
||||||
margin-top: 2px;
|
|
||||||
font-size: 18px;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 360px));
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card {
|
|
||||||
display: grid;
|
|
||||||
gap: 9px;
|
|
||||||
min-width: 0;
|
|
||||||
padding: 14px;
|
|
||||||
border: 1px solid var(--panel-border);
|
|
||||||
border-radius: 14px;
|
|
||||||
background: var(--bg-card);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card-main,
|
|
||||||
.task-time-grid,
|
|
||||||
.task-suspended,
|
|
||||||
.task-result {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card-main {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-title {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-title strong {
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 15px;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-status-line {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-provider {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 26px;
|
|
||||||
padding: 0 9px;
|
|
||||||
border-radius: 999px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-action {
|
|
||||||
min-height: 40px;
|
|
||||||
padding: 0 14px;
|
|
||||||
border: 1px solid var(--panel-border-strong);
|
|
||||||
border-radius: 999px;
|
|
||||||
color: var(--ink);
|
|
||||||
font: inherit;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-action:hover,
|
|
||||||
.task-action:focus-visible {
|
|
||||||
border-color: rgba(191, 95, 44, 0.42);
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 3px rgba(191, 95, 44, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-action:disabled {
|
|
||||||
cursor: progress;
|
|
||||||
opacity: 0.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-action.is-busy {
|
|
||||||
display: inline-flex;
|
|
||||||
gap: 7px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: progress;
|
|
||||||
opacity: 0.85;
|
|
||||||
background: rgba(191, 95, 44, 0.12);
|
|
||||||
border-color: rgba(191, 95, 44, 0.32);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-action.is-busy::before {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border: 2px solid currentColor;
|
|
||||||
border-right-color: transparent;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: ej-spin 0.7s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes ej-spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-action-cancel {
|
|
||||||
color: #8f2f22;
|
|
||||||
border-color: rgba(183, 71, 52, 0.22);
|
|
||||||
background: rgba(183, 71, 52, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-time-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-time-grid > span,
|
|
||||||
.task-suspended,
|
|
||||||
.task-result {
|
|
||||||
padding: 9px;
|
|
||||||
border: 1px solid var(--panel-border);
|
|
||||||
border-radius: 14px;
|
|
||||||
background: rgba(255, 255, 255, 0.025);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-time-grid small,
|
|
||||||
.task-suspended span,
|
|
||||||
.task-result span,
|
|
||||||
.task-time-grid em,
|
|
||||||
.task-suspended em {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 11px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-time-grid small,
|
|
||||||
.task-time-grid strong,
|
|
||||||
.task-time-grid em,
|
|
||||||
.task-suspended span,
|
|
||||||
.task-suspended strong,
|
|
||||||
.task-suspended em,
|
|
||||||
.task-result span,
|
|
||||||
.task-result strong {
|
|
||||||
display: block;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-time-grid strong,
|
|
||||||
.task-suspended strong,
|
|
||||||
.task-result strong {
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-suspended {
|
|
||||||
border-color: rgba(181, 138, 34, 0.22);
|
|
||||||
background: rgba(181, 138, 34, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-result {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
border-color: rgba(63, 127, 81, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-fail {
|
|
||||||
border-color: rgba(183, 71, 52, 0.28);
|
|
||||||
background: rgba(183, 71, 52, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-none {
|
|
||||||
border-color: rgba(109, 115, 95, 0.14);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-prompt {
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-edit summary,
|
|
||||||
.task-prompt summary {
|
|
||||||
min-height: 32px;
|
|
||||||
color: var(--accent-strong);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-prompt p {
|
|
||||||
margin: 4px 0;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-prompt small {
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
font-family:
|
|
||||||
'SF Mono', 'Cascadia Code', 'Roboto Mono', ui-monospace, monospace;
|
|
||||||
font-size: 11px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-group-empty {
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 14px;
|
|
||||||
color: var(--muted);
|
|
||||||
background: rgba(255, 255, 255, 0.025);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-watchers {
|
|
||||||
color: #173e23;
|
|
||||||
background: rgba(63, 127, 81, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-scheduled {
|
|
||||||
color: #6e4a05;
|
|
||||||
background: rgba(181, 138, 34, 0.16);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-paused {
|
|
||||||
color: #6e4a05;
|
|
||||||
background: rgba(181, 138, 34, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-completed {
|
|
||||||
color: #625d52;
|
|
||||||
background: rgba(109, 115, 95, 0.16);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card > p {
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mono-chip {
|
.mono-chip {
|
||||||
font-family:
|
font-family:
|
||||||
'SF Mono', 'Cascadia Code', 'Roboto Mono', ui-monospace, monospace;
|
'SF Mono', 'Cascadia Code', 'Roboto Mono', ui-monospace, monospace;
|
||||||
@@ -3165,55 +2767,6 @@ progress::-moz-progress-bar {
|
|||||||
.health-service > div:nth-of-type(3) {
|
.health-service > div:nth-of-type(3) {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-group {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-create-form,
|
|
||||||
.task-edit-form {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 7px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-form-wide {
|
|
||||||
grid-column: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-list {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card {
|
|
||||||
gap: 7px;
|
|
||||||
padding: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-action {
|
|
||||||
min-height: 44px;
|
|
||||||
padding-inline: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card-main {
|
|
||||||
gap: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-time-grid {
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-time-grid > span,
|
|
||||||
.task-suspended,
|
|
||||||
.task-result {
|
|
||||||
padding: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-prompt summary {
|
|
||||||
min-height: 28px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 380px) {
|
@media (max-width: 380px) {
|
||||||
@@ -3270,14 +2823,6 @@ progress::-moz-progress-bar {
|
|||||||
.health-restart-record {
|
.health-restart-record {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-time-grid {
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-time-grid > span:last-child {
|
|
||||||
grid-column: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 420px) {
|
@media (max-width: 420px) {
|
||||||
|
|||||||
@@ -162,6 +162,8 @@ async function main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await runScheduledBoardScenario(browser, baseUrl);
|
||||||
|
|
||||||
await runScenario(
|
await runScenario(
|
||||||
'health route redirects to rooms and degraded state is conditional',
|
'health route redirects to rooms and degraded state is conditional',
|
||||||
browser,
|
browser,
|
||||||
@@ -196,6 +198,29 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runScheduledBoardScenario(browser: Browser, baseUrl: string) {
|
||||||
|
await runScenario(
|
||||||
|
'scheduled board surfaces next task without empty lanes',
|
||||||
|
browser,
|
||||||
|
baseUrl,
|
||||||
|
async (page) => {
|
||||||
|
await page.goto(new URL('/#/scheduled', baseUrl).toString(), {
|
||||||
|
waitUntil: 'networkidle',
|
||||||
|
});
|
||||||
|
|
||||||
|
await assertVisible(page.locator('#scheduled .task-command-center'));
|
||||||
|
await assertVisible(page.locator('#scheduled .task-create-form'));
|
||||||
|
await assertVisible(page.getByText('Nightly cleanup').first());
|
||||||
|
await assertVisible(page.getByText('*/15 * * * *').first());
|
||||||
|
assert.equal(await page.locator('#scheduled .task-card').count(), 3);
|
||||||
|
assert.equal(
|
||||||
|
await page.locator('#scheduled .task-group-empty').count(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function startDashboardServer(): Promise<ViteDevServer> {
|
async function startDashboardServer(): Promise<ViteDevServer> {
|
||||||
const port = Number(process.env.DASHBOARD_UX_PORT ?? 5175);
|
const port = Number(process.env.DASHBOARD_UX_PORT ?? 5175);
|
||||||
const server = await createServer({
|
const server = await createServer({
|
||||||
@@ -306,7 +331,7 @@ async function handleMockApi(route: Route, state: MockApiState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (method === 'GET' && url.pathname === '/api/tasks') {
|
if (method === 'GET' && url.pathname === '/api/tasks') {
|
||||||
await fulfillJson(route, []);
|
await fulfillJson(route, mockTasks());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,6 +557,52 @@ function mockStatusSnapshot() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mockTask(overrides: Record<string, unknown>) {
|
||||||
|
return {
|
||||||
|
agentType: 'codex',
|
||||||
|
chatJid: 'room@example',
|
||||||
|
ciMetadata: null,
|
||||||
|
ciProvider: null,
|
||||||
|
contextMode: 'group',
|
||||||
|
createdAt: MOCK_TIME,
|
||||||
|
groupFolder: 'ejclaw',
|
||||||
|
id: 'task-fixture',
|
||||||
|
isWatcher: false,
|
||||||
|
lastResult: 'ok',
|
||||||
|
lastRun: MOCK_TIME,
|
||||||
|
nextRun: new Date(Date.now() + 30 * 60_000).toISOString(),
|
||||||
|
promptLength: 28,
|
||||||
|
promptPreview: 'Nightly cleanup',
|
||||||
|
scheduleType: 'cron',
|
||||||
|
scheduleValue: '*/15 * * * *',
|
||||||
|
status: 'active',
|
||||||
|
suspendedUntil: null,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockTasks() {
|
||||||
|
return [
|
||||||
|
mockTask({ id: 'task-nightly-cleanup' }),
|
||||||
|
mockTask({
|
||||||
|
ciProvider: 'github',
|
||||||
|
id: 'task-ci-watch',
|
||||||
|
isWatcher: true,
|
||||||
|
promptPreview: 'Watch PR #133',
|
||||||
|
scheduleType: 'interval',
|
||||||
|
scheduleValue: '5m',
|
||||||
|
}),
|
||||||
|
mockTask({
|
||||||
|
id: 'task-paused-report',
|
||||||
|
nextRun: null,
|
||||||
|
promptPreview: 'Weekly report',
|
||||||
|
scheduleValue: '0 9 * * 1',
|
||||||
|
status: 'paused',
|
||||||
|
suspendedUntil: new Date(Date.now() + 2 * 3_600_000).toISOString(),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function runtimeSkill(name: string, description: string, skillPath: string) {
|
function runtimeSkill(name: string, description: string, skillPath: string) {
|
||||||
return { name, description, path: skillPath };
|
return { name, description, path: skillPath };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user