mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-08 16:25:10 +00:00
* feat(web-shell): aggregate scheduled tasks across all workspaces The Web Shell scheduled-tasks page was hard-wired to the primary workspace. On a multi-workspace daemon, creating a task while viewing another workspace stored it under (and ran it in) the primary, and tasks belonging to secondary workspaces never appeared on the page. Add a workspace-qualified `/workspaces/:workspace/scheduled-tasks` surface that shares the existing CRUD handlers, resolving each request to that workspace's own cron file and session bridge after a trust check — the same pattern every other qualified route uses. The page now aggregates every trusted workspace's tasks into one list, badges each card with its workspace, and lets the New-task form target a workspace. Keepalive and boot rehydration run per registered workspace so a bound task in any workspace fires and survives a restart, instead of only the primary's. * fix(web-shell): keep an untrusted primary in the scheduled-tasks aggregate The aggregated list and the New-task workspace picker were both built from the trusted workspaces only. When folder trust is enabled and the primary workspace itself is untrusted, that dropped the primary from the picker and skipped it during the fan-out — so its tasks disappeared from the list even though the primary's trust-free route can still read them, and the form's default (which targets the primary) no longer matched any option, letting the UI show a secondary while a create actually landed on the primary. Include the primary in the operable set regardless of its trust flag; it is always reachable through the unqualified route the single-workspace page has always used. Secondary workspaces stay gated on trust, since their qualified route rejects an untrusted read or write.
619 lines
11 KiB
CSS
619 lines
11 KiB
CSS
.root {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.intro {
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.count {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.toolbarActions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.primaryButton,
|
|
.secondaryButton {
|
|
appearance: none;
|
|
border-radius: 8px;
|
|
padding: 6px 14px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
border: 1px solid var(--border);
|
|
transition:
|
|
background 0.15s ease,
|
|
opacity 0.15s ease;
|
|
}
|
|
|
|
.primaryButton {
|
|
background: var(--primary);
|
|
color: var(--background);
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.secondaryButton {
|
|
background: transparent;
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.primaryButton:disabled,
|
|
.secondaryButton:disabled {
|
|
opacity: 0.55;
|
|
cursor: default;
|
|
}
|
|
|
|
.secondaryButton:hover:not(:disabled) {
|
|
background: var(--muted);
|
|
}
|
|
|
|
/* ── Create form ─────────────────────────────────────────────── */
|
|
|
|
/* Create form now lives inside a DialogShell modal, which supplies the panel
|
|
chrome (border/background/padding); this class only handles field layout. */
|
|
.formFields {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
}
|
|
|
|
.field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.fieldGrow {
|
|
flex: 1 1 220px;
|
|
}
|
|
|
|
.fieldLabel {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.required {
|
|
color: var(--error-color);
|
|
margin-left: 2px;
|
|
}
|
|
|
|
.fieldHint {
|
|
font-size: 11px;
|
|
line-height: 1.4;
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.input,
|
|
.textarea,
|
|
.select {
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 8px 10px;
|
|
font-size: 13px;
|
|
background: var(--background);
|
|
color: var(--foreground);
|
|
font-family: inherit;
|
|
}
|
|
|
|
.textarea {
|
|
resize: vertical;
|
|
min-height: 72px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.promptEditorWrap {
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
|
|
.promptEditor {
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 10px;
|
|
font-size: 13px;
|
|
background: var(--background);
|
|
color: var(--foreground);
|
|
font-family: inherit;
|
|
resize: vertical;
|
|
min-height: 72px;
|
|
max-height: 220px;
|
|
line-height: 1.5;
|
|
overflow: auto;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.input:focus,
|
|
.textarea:focus,
|
|
.promptEditor:focus,
|
|
.select:focus {
|
|
outline: 2px solid var(--primary);
|
|
outline-offset: -1px;
|
|
}
|
|
|
|
.promptPlaceholder {
|
|
position: absolute;
|
|
top: 11px;
|
|
right: 11px;
|
|
left: 11px;
|
|
overflow: hidden;
|
|
color: var(--muted-foreground);
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.promptTag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
max-width: min(36ch, 100%);
|
|
min-height: 22px;
|
|
margin: 0 0.25ch;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
background: var(--muted);
|
|
color: var(--foreground);
|
|
font-family: var(--font-mono, monospace);
|
|
font-size: 12px;
|
|
line-height: 1.2;
|
|
vertical-align: baseline;
|
|
}
|
|
|
|
.promptTagIcon {
|
|
display: block;
|
|
width: 12px;
|
|
height: 12px;
|
|
flex: 0 0 auto;
|
|
margin-left: 7px;
|
|
background: currentColor;
|
|
mask: var(--composer-tag-icon-url) center / contain no-repeat;
|
|
-webkit-mask: var(--composer-tag-icon-url) center / contain no-repeat;
|
|
}
|
|
|
|
.promptTagValue {
|
|
min-width: 0;
|
|
max-width: 30ch;
|
|
overflow: hidden;
|
|
padding: 3px 7px 3px 0.5ch;
|
|
color: var(--foreground);
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.referencePopover {
|
|
min-width: 0;
|
|
}
|
|
|
|
.referenceBar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin-top: -6px;
|
|
}
|
|
|
|
.referenceButton {
|
|
appearance: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 999px;
|
|
padding: 5px 12px;
|
|
background: var(--muted);
|
|
color: var(--muted-foreground);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.referenceButton:hover,
|
|
.referenceButtonActive {
|
|
color: var(--foreground);
|
|
background: var(--background);
|
|
}
|
|
|
|
.referenceButtonIcon {
|
|
display: block;
|
|
width: 13px;
|
|
height: 13px;
|
|
flex: 0 0 auto;
|
|
background: currentColor;
|
|
mask: var(--composer-tag-icon-url) center / contain no-repeat;
|
|
-webkit-mask: var(--composer-tag-icon-url) center / contain no-repeat;
|
|
}
|
|
|
|
.referencePicker {
|
|
position: fixed;
|
|
z-index: calc(var(--web-shell-dialog-backdrop-z-index, 1000) + 10);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: auto;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
background: var(--background);
|
|
box-shadow: 0 12px 32px rgb(0 0 0 / 18%);
|
|
}
|
|
|
|
.referenceItem {
|
|
appearance: none;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr);
|
|
gap: 2px;
|
|
border: 0;
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 9px 10px;
|
|
background: transparent;
|
|
color: var(--foreground);
|
|
font-family: inherit;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.referenceItem:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.referenceItem:hover {
|
|
background: var(--muted);
|
|
}
|
|
|
|
.referenceItemLabel,
|
|
.referenceItemDescription {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.referenceItemLabel {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.referenceItemDescription {
|
|
color: var(--muted-foreground);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.referenceEmpty,
|
|
.referenceError {
|
|
padding: 10px;
|
|
color: var(--muted-foreground);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.referenceError {
|
|
color: var(--error-color);
|
|
}
|
|
|
|
.scheduleRow {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
align-items: flex-end;
|
|
}
|
|
|
|
.scheduleRow .field {
|
|
flex: 0 0 auto;
|
|
min-width: 140px;
|
|
}
|
|
|
|
.preview {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.previewLabel {
|
|
font-weight: 600;
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.previewCron {
|
|
font-family: var(--font-mono, monospace);
|
|
font-size: 12px;
|
|
color: var(--muted-foreground);
|
|
background: var(--background);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 2px 6px;
|
|
}
|
|
|
|
.previewInvalid {
|
|
color: var(--warning-color);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.formError,
|
|
.loadError {
|
|
color: var(--error-color);
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.formActions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
}
|
|
|
|
/* ── List ────────────────────────────────────────────────────── */
|
|
|
|
.empty {
|
|
padding: 32px 16px;
|
|
text-align: center;
|
|
color: var(--muted-foreground);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.list {
|
|
/* One task per row (full width) — narrow grid columns clipped the prompt
|
|
* and made descriptions hard to read. */
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
padding: 14px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
background: var(--background);
|
|
min-width: 0;
|
|
}
|
|
|
|
.cardDisabled {
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.cardHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.cardTitle {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--foreground);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.cardMenu {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.iconAction {
|
|
appearance: none;
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--muted-foreground);
|
|
cursor: pointer;
|
|
border-radius: 6px;
|
|
width: 26px;
|
|
height: 26px;
|
|
font-size: 12px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.iconAction:hover:not(:disabled) {
|
|
background: var(--muted);
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.iconAction:disabled {
|
|
opacity: 0.5;
|
|
cursor: default;
|
|
}
|
|
|
|
/* Toggle switch */
|
|
.toggle {
|
|
appearance: none;
|
|
flex: 0 0 auto;
|
|
position: relative;
|
|
width: 36px;
|
|
height: 20px;
|
|
border-radius: 999px;
|
|
border: none;
|
|
background: var(--border);
|
|
cursor: pointer;
|
|
padding: 0;
|
|
transition: background 0.15s ease;
|
|
}
|
|
|
|
.toggle:disabled {
|
|
cursor: default;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.toggleOn {
|
|
background: var(--primary);
|
|
}
|
|
|
|
.toggleKnob {
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 2px;
|
|
width: 16px;
|
|
height: 16px;
|
|
border-radius: 50%;
|
|
background: #fff;
|
|
transition: transform 0.15s ease;
|
|
}
|
|
|
|
.toggleOn .toggleKnob {
|
|
transform: translateX(16px);
|
|
}
|
|
|
|
.cardPrompt {
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
color: var(--muted-foreground);
|
|
display: -webkit-box;
|
|
/* Full-width rows give room for more of the prompt before eliding. */
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.cardFooter {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
margin-top: auto;
|
|
}
|
|
|
|
.schedulePill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--foreground);
|
|
background: var(--muted);
|
|
border-radius: 999px;
|
|
padding: 3px 10px;
|
|
}
|
|
|
|
.clockIcon {
|
|
font-size: 12px;
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.recurringTag {
|
|
font-size: 11px;
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
/* Workspace tag on a card in the aggregated multi-workspace view. Bordered +
|
|
* accent-tinted so it reads as an origin label, distinct from the plain muted
|
|
* schedule pill. Truncates a long workspace name rather than wrapping the row. */
|
|
.workspacePill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
max-width: 180px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--primary);
|
|
background: color-mix(in srgb, var(--primary) 12%, transparent);
|
|
border: 1px solid color-mix(in srgb, var(--primary) 30%, transparent);
|
|
border-radius: 999px;
|
|
padding: 2px 9px;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.workspaceIcon {
|
|
font-size: 11px;
|
|
opacity: 0.85;
|
|
}
|
|
|
|
.countdown {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: var(--primary);
|
|
/* Fixed-width digits so the ticking countdown doesn't shift its neighbors. */
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.hourglassIcon {
|
|
font-size: 10px;
|
|
opacity: 0.85;
|
|
}
|
|
|
|
.lastFired {
|
|
font-size: 11px;
|
|
color: var(--muted-foreground);
|
|
margin-left: auto;
|
|
}
|
|
|
|
/* ── Run history ─────────────────────────────────────────────── */
|
|
|
|
.runsToggle {
|
|
appearance: none;
|
|
background: transparent;
|
|
border: none;
|
|
padding: 0;
|
|
font-size: 11px;
|
|
font-family: inherit;
|
|
color: var(--muted-foreground);
|
|
cursor: pointer;
|
|
text-decoration: underline;
|
|
text-underline-offset: 2px;
|
|
}
|
|
|
|
.runsToggle:hover {
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.runsList {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 8px 10px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
background: var(--muted);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
max-height: 160px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.runsItem {
|
|
font-size: 11px;
|
|
line-height: 1.4;
|
|
color: var(--muted-foreground);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|