mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-23 14:45:46 +00:00
feat(ui): Control UI polish — skills revamp, markdown preview, agent workspace, macOS config tree (#53411) thanks @BunsDev
Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com> Co-authored-by: Nova <nova@openknot.ai>
This commit is contained in:
@@ -5,3 +5,13 @@
|
||||
@import "./styles/chat.css";
|
||||
@import "./styles/config.css";
|
||||
@import "./styles/usage.css";
|
||||
@import "@create-markdown/preview/themes/system.css";
|
||||
|
||||
.cm-preview {
|
||||
--cm-mono: var(--mono);
|
||||
--cm-link: var(--accent);
|
||||
--cm-info: var(--info);
|
||||
--cm-warning: var(--warn);
|
||||
--cm-success: var(--ok);
|
||||
--cm-danger: var(--danger);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
/* Focus */
|
||||
--focus: rgba(255, 92, 92, 0.2);
|
||||
--focus-ring: 0 0 0 2px var(--bg), 0 0 0 3px color-mix(in srgb, var(--ring) 60%, transparent);
|
||||
--focus-ring: 0 0 0 2px var(--bg), 0 0 0 3px color-mix(in srgb, var(--ring) 80%, transparent);
|
||||
--focus-glow: 0 0 0 2px var(--bg), 0 0 0 3px var(--ring), 0 0 16px var(--accent-glow);
|
||||
|
||||
/* Grid */
|
||||
@@ -172,7 +172,7 @@
|
||||
--info: #2563eb;
|
||||
|
||||
--focus: rgba(220, 38, 38, 0.15);
|
||||
--focus-ring: 0 0 0 2px var(--bg), 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
|
||||
--focus-ring: 0 0 0 2px var(--bg), 0 0 0 3px color-mix(in srgb, var(--ring) 70%, transparent);
|
||||
--focus-glow: 0 0 0 2px var(--bg), 0 0 0 3px var(--ring), 0 0 12px var(--accent-glow);
|
||||
|
||||
--grid-line: rgba(0, 0, 0, 0.04);
|
||||
@@ -236,7 +236,7 @@
|
||||
--text-strong: #f5f5f7;
|
||||
--chat-text: #e0e0e2;
|
||||
--muted: #7a7a80;
|
||||
--muted-strong: #5a5a62;
|
||||
--muted-strong: #6e6e76;
|
||||
--muted-foreground: #7a7a80;
|
||||
|
||||
/* Borders — whisper-thin, barely visible */
|
||||
@@ -371,7 +371,7 @@
|
||||
--text-strong: #f0e4da;
|
||||
--chat-text: #d8c8b8;
|
||||
--muted: #9a8878;
|
||||
--muted-strong: #7a6858;
|
||||
--muted-strong: #8a7868;
|
||||
--muted-foreground: #9a8878;
|
||||
|
||||
--border: #302418;
|
||||
@@ -467,7 +467,7 @@ body {
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font: 400 13.5px/1.55 var(--font-body);
|
||||
font: 400 14px/1.55 var(--font-body);
|
||||
letter-spacing: -0.01em;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
@@ -475,6 +475,12 @@ body {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
body {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Theme transition */
|
||||
@keyframes theme-circle-transition {
|
||||
0% {
|
||||
@@ -517,11 +523,12 @@ openclaw-app {
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
|
||||
button,
|
||||
@@ -691,6 +698,17 @@ select {
|
||||
animation-delay: 250ms;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus visible styles */
|
||||
:focus-visible {
|
||||
outline: none;
|
||||
|
||||
@@ -66,7 +66,9 @@
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
padding: 4px;
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
color: var(--muted);
|
||||
opacity: 0;
|
||||
|
||||
@@ -227,8 +227,8 @@
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
@@ -702,8 +702,8 @@
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
|
||||
@@ -79,9 +79,207 @@
|
||||
|
||||
.sidebar-markdown {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
line-height: 1.6;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ── Headings ── */
|
||||
|
||||
.sidebar-markdown :where(h1) {
|
||||
font-size: 1.65em;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.025em;
|
||||
margin: 1.6em 0 0.6em;
|
||||
padding-bottom: 0.35em;
|
||||
border-bottom: 1px solid var(--border);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(h2) {
|
||||
font-size: 1.35em;
|
||||
font-weight: 650;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 1.4em 0 0.5em;
|
||||
padding-bottom: 0.25em;
|
||||
border-bottom: 1px solid var(--border);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(h3) {
|
||||
font-size: 1.15em;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
margin: 1.2em 0 0.4em;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(h4, h5, h6) {
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
margin: 1em 0 0.35em;
|
||||
line-height: 1.4;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
.sidebar-markdown > :where(h1, h2, h3, h4, h5, h6):first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* ── Paragraphs & spacing ── */
|
||||
|
||||
.sidebar-markdown :where(p, ul, ol, pre, blockquote, table, details) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(p + p, p + ul, p + ol, p + pre, p + blockquote, p + table, p + details) {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(h1 + p, h2 + p, h3 + p, h4 + p) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* ── Lists ── */
|
||||
|
||||
.sidebar-markdown :where(ul, ol) {
|
||||
padding-left: 1.6em;
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(li + li) {
|
||||
margin-top: 0.3em;
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(li > p) {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(li > ul, li > ol) {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
/* ── Links ── */
|
||||
|
||||
.sidebar-markdown :where(a) {
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent);
|
||||
transition: text-decoration-color var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(a:hover) {
|
||||
text-decoration-color: var(--accent);
|
||||
}
|
||||
|
||||
/* ── Inline code ── */
|
||||
|
||||
.sidebar-markdown code {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.88em;
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(:not(pre) > code) {
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
:root[data-theme-mode="light"] .sidebar-markdown :where(:not(pre) > code) {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* ── Code blocks ── */
|
||||
|
||||
.sidebar-markdown pre {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 14px 16px;
|
||||
overflow-x: auto;
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
:root[data-theme-mode="light"] .sidebar-markdown pre {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(pre code) {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
/* ── Blockquotes ── */
|
||||
|
||||
.sidebar-markdown :where(blockquote) {
|
||||
border-left: 3px solid var(--border-strong);
|
||||
padding: 8px 14px;
|
||||
margin-left: 0;
|
||||
margin-top: 0.75em;
|
||||
color: var(--muted);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||||
}
|
||||
|
||||
:root[data-theme-mode="light"] .sidebar-markdown :where(blockquote) {
|
||||
background: rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(blockquote blockquote) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* ── Tables ── */
|
||||
|
||||
.sidebar-markdown :where(table) {
|
||||
margin-top: 0.75em;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(th, td) {
|
||||
border: 1px solid var(--border);
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(th) {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
background: var(--secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(tbody tr:hover) {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
/* ── Horizontal rules ── */
|
||||
|
||||
.sidebar-markdown :where(hr) {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
|
||||
/* ── Bold / italic ── */
|
||||
|
||||
.sidebar-markdown :where(strong) {
|
||||
font-weight: 600;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
/* ── Images ── */
|
||||
|
||||
.sidebar-markdown .markdown-inline-image {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
@@ -92,18 +290,34 @@
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(in srgb, var(--secondary) 70%, transparent);
|
||||
object-fit: contain;
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
.sidebar-markdown pre {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
/* ── Details / summary ── */
|
||||
|
||||
.sidebar-markdown :where(details) {
|
||||
margin-top: 0.75em;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-elevated);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-markdown code {
|
||||
font-family: var(--mono);
|
||||
.sidebar-markdown :where(summary) {
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
transition: background var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(summary:hover) {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.sidebar-markdown :where(details[open] > :not(summary)) {
|
||||
padding: 0 12px 10px;
|
||||
}
|
||||
|
||||
/* Mobile: Full-screen modal */
|
||||
|
||||
@@ -165,7 +165,9 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 8px;
|
||||
padding: 2px;
|
||||
padding: 4px;
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
@@ -521,6 +523,69 @@
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.statusDot.muted {
|
||||
background: var(--muted);
|
||||
box-shadow: none;
|
||||
animation: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Skill Toggle Switch
|
||||
=========================================== */
|
||||
|
||||
.skill-toggle-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.skill-toggle {
|
||||
appearance: none;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--border);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
transition:
|
||||
background var(--duration-fast) var(--ease-out),
|
||||
box-shadow var(--duration-fast) var(--ease-out);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.skill-toggle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: var(--radius-full);
|
||||
background: white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
transition: transform var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.skill-toggle:checked {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.skill-toggle:checked::after {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
.skill-toggle:focus-visible {
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.skill-toggle:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Buttons - Tactile with personality
|
||||
=========================================== */
|
||||
@@ -906,9 +971,6 @@
|
||||
.field textarea[aria-invalid="true"],
|
||||
.field select[aria-invalid="true"] {
|
||||
border-color: var(--danger);
|
||||
box-shadow:
|
||||
inset 0 1px 0 var(--card-highlight),
|
||||
0 0 0 1px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.cron-form-status {
|
||||
@@ -2928,23 +2990,6 @@ td.data-table-key-col {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agents-toolbar-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agents-control-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agents-control-select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -2979,80 +3024,32 @@ td.data-table-key-col {
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.agents-control-actions {
|
||||
.agents-toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.agents-refresh-btn {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.agent-actions-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.agent-actions-toggle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--duration-fast) ease,
|
||||
border-color var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.agent-actions-toggle:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.agent-actions-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
min-width: 160px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-elevated);
|
||||
box-shadow: var(--shadow-md);
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.agent-actions-menu button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 7px 10px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
.btn--ghost {
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.agent-actions-menu button:hover:not(:disabled) {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.agent-actions-menu button:disabled {
|
||||
border-color: transparent;
|
||||
color: var(--muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn--ghost:hover:not(:disabled) {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn--ghost:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.agents-main {
|
||||
@@ -3197,6 +3194,19 @@ td.data-table-key-col {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.agent-tab--missing {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.agent-tab-badge {
|
||||
margin-left: 4px;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
color: var(--warn);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.agents-overview-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
@@ -3273,15 +3283,16 @@ td.data-table-key-col {
|
||||
|
||||
.agent-chip-input:focus-within {
|
||||
border-color: var(--accent);
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.agent-chip-input input {
|
||||
.agent-chip-input input,
|
||||
.agent-chip-input input:focus {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -3292,58 +3303,6 @@ td.data-table-key-col {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.agent-files-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 240px) minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.agent-files-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agent-file-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--card);
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
transition: border-color var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.agent-file-row:hover {
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.agent-file-row.active {
|
||||
border-color: var(--accent);
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.agent-file-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.agent-file-meta {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.agent-files-editor {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 16px;
|
||||
background: var(--card);
|
||||
}
|
||||
|
||||
.agent-file-field {
|
||||
min-height: clamp(320px, 56vh, 720px);
|
||||
}
|
||||
@@ -3365,10 +3324,6 @@ td.data-table-key-col {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.agent-file-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.agent-file-sub {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
@@ -3716,10 +3671,6 @@ td.data-table-key-col {
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.agent-files-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.agent-tools-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -3736,8 +3687,9 @@ td.data-table-key-col {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.agents-toolbar-label {
|
||||
display: none;
|
||||
.agents-toolbar-actions {
|
||||
margin-left: 0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4354,6 +4306,111 @@ details[open] > .ov-expandable-toggle::after {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Markdown Preview Dialog
|
||||
=========================================== */
|
||||
|
||||
.md-preview-dialog {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.md-preview-dialog::backdrop {
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.md-preview-dialog__panel {
|
||||
width: min(780px, calc(100vw - 48px));
|
||||
max-height: calc(100vh - 64px);
|
||||
margin: 32px auto;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
animation: scale-in 0.2s var(--ease-out);
|
||||
transition:
|
||||
width 0.2s var(--ease-out),
|
||||
max-height 0.2s var(--ease-out),
|
||||
margin 0.2s var(--ease-out);
|
||||
}
|
||||
|
||||
.md-preview-dialog__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--card);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.md-preview-dialog__title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.md-preview-dialog__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.md-preview-dialog__body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px 20px 24px;
|
||||
}
|
||||
|
||||
.md-preview-dialog__panel.fullscreen {
|
||||
width: calc(100vw - 32px);
|
||||
max-width: none;
|
||||
max-height: calc(100vh - 32px);
|
||||
margin: 16px auto;
|
||||
}
|
||||
|
||||
.md-preview-expand-btn .when-fullscreen {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.md-preview-expand-btn.is-fullscreen .when-normal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.md-preview-expand-btn.is-fullscreen .when-fullscreen {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.md-preview-dialog__panel {
|
||||
width: calc(100vw - 16px);
|
||||
max-height: calc(100vh - 32px);
|
||||
margin: 16px auto;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.md-preview-dialog__header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.md-preview-dialog__body {
|
||||
padding: 14px 12px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.ov-cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
@@ -103,8 +103,8 @@
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--bg-hover);
|
||||
@@ -869,6 +869,21 @@
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.cfg-field--error .cfg-input,
|
||||
.cfg-field--error .cfg-textarea,
|
||||
.cfg-field--error .cfg-select,
|
||||
.cfg-field--error .cfg-number {
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.cfg-field--error .cfg-input:focus,
|
||||
.cfg-field--error .cfg-textarea:focus,
|
||||
.cfg-field--error .cfg-select:focus {
|
||||
border-color: var(--border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.cfg-field__label {
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -28,16 +28,17 @@
|
||||
}
|
||||
|
||||
.usage-page-title {
|
||||
font-size: 24px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
letter-spacing: -0.025em;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
.usage-page-subtitle {
|
||||
max-width: 720px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.usage-section-title {
|
||||
@@ -70,13 +71,13 @@
|
||||
|
||||
.usage-header {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.usage-header-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
@@ -93,20 +94,21 @@
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.usage-refresh-indicator,
|
||||
.usage-loading-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--accent-subtle);
|
||||
background: color-mix(in srgb, var(--accent) 10%, var(--bg-muted));
|
||||
color: var(--accent);
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.usage-loading-badge {
|
||||
@@ -169,7 +171,7 @@
|
||||
flex: 1 1 100%;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.usage-presets,
|
||||
@@ -179,7 +181,35 @@
|
||||
.usage-filter-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.chart-toggle {
|
||||
padding: 3px;
|
||||
background: var(--bg-muted);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.chart-toggle .toggle-btn {
|
||||
border: none;
|
||||
border-radius: calc(var(--radius-md) - 3px);
|
||||
background: transparent;
|
||||
min-height: 28px;
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chart-toggle .toggle-btn:hover:not(.active) {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.chart-toggle .toggle-btn.active {
|
||||
background: var(--accent-subtle);
|
||||
border: none;
|
||||
box-shadow: 0 1px 2px color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
}
|
||||
|
||||
.usage-date-range {
|
||||
@@ -195,12 +225,13 @@
|
||||
.usage-query-input,
|
||||
.usage-filters-inline select,
|
||||
.usage-filters-inline input[type="text"] {
|
||||
min-height: 36px;
|
||||
min-height: 32px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
transition:
|
||||
border-color 0.18s var(--ease-out),
|
||||
box-shadow 0.18s var(--ease-out),
|
||||
@@ -209,7 +240,12 @@
|
||||
|
||||
.usage-date-input,
|
||||
.usage-select {
|
||||
padding: 7px 10px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.usage-date-input:hover,
|
||||
.usage-select:hover {
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.usage-date-input:focus,
|
||||
@@ -218,8 +254,8 @@
|
||||
.usage-filters-inline select:focus,
|
||||
.usage-filters-inline input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: color-mix(in srgb, var(--accent) 65%, var(--border));
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
|
||||
border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
}
|
||||
|
||||
.usage-separator,
|
||||
@@ -235,22 +271,41 @@
|
||||
.session-log-meta,
|
||||
.session-logs-header-count {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.usage-separator {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.usage-query-hint {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.usage-metric-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 11px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 12%, var(--border));
|
||||
gap: 5px;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-full);
|
||||
background: color-mix(in srgb, var(--accent) 8%, var(--card));
|
||||
font-size: 12px;
|
||||
color: var(--text);
|
||||
background: var(--bg-elevated);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.01em;
|
||||
transition: border-color 0.18s var(--ease-out);
|
||||
}
|
||||
|
||||
.usage-metric-badge:hover {
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.usage-metric-badge strong {
|
||||
color: var(--text-strong);
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Toggle-btn active state layers on .btn for segmented controls */
|
||||
@@ -259,11 +314,25 @@
|
||||
border-color: var(--accent);
|
||||
background: var(--accent);
|
||||
color: var(--primary-foreground);
|
||||
box-shadow: 0 8px 24px color-mix(in srgb, var(--accent) 28%, transparent);
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 3px color-mix(in srgb, var(--accent) 20%, transparent);
|
||||
}
|
||||
|
||||
.usage-action-btn.usage-primary-btn:hover:not(:disabled) {
|
||||
background: var(--accent-hover);
|
||||
border-color: var(--accent-hover);
|
||||
box-shadow: 0 2px 8px color-mix(in srgb, var(--accent) 25%, transparent);
|
||||
}
|
||||
|
||||
.chart-toggle .toggle-btn.active {
|
||||
border-color: color-mix(in srgb, var(--accent) 30%, var(--border));
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.usage-export-item:disabled {
|
||||
opacity: 0.55;
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -271,17 +340,25 @@
|
||||
.usage-query-section {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding-top: 2px;
|
||||
border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
|
||||
}
|
||||
|
||||
.usage-query-bar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.usage-query-input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.usage-query-input::placeholder {
|
||||
color: var(--muted);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.usage-query-actions {
|
||||
@@ -293,7 +370,7 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
details.usage-filter-select,
|
||||
@@ -314,39 +391,45 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
.usage-filter-select summary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 34px;
|
||||
padding: 8px 12px;
|
||||
gap: 6px;
|
||||
min-height: 30px;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition:
|
||||
border-color 0.18s var(--ease-out),
|
||||
box-shadow 0.18s var(--ease-out),
|
||||
background 0.18s var(--ease-out);
|
||||
}
|
||||
|
||||
.usage-filter-select[open] summary,
|
||||
.usage-filter-select summary:hover {
|
||||
border-color: color-mix(in srgb, var(--accent) 25%, var(--border));
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 10%, transparent);
|
||||
border-color: var(--border-strong);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.usage-filter-select[open] summary {
|
||||
border-color: color-mix(in srgb, var(--accent) 30%, var(--border));
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 8%, transparent);
|
||||
}
|
||||
|
||||
.usage-filter-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent);
|
||||
font-size: 11px;
|
||||
background: var(--bg-muted);
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.usage-filter-popover,
|
||||
@@ -383,26 +466,28 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
padding: 6px 8px;
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(in srgb, var(--bg-muted) 72%, transparent);
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
transition: background 0.12s ease;
|
||||
}
|
||||
|
||||
.usage-filter-option:hover {
|
||||
background: color-mix(in srgb, var(--accent) 10%, var(--bg-muted));
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.usage-export-item {
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
padding: 9px 10px;
|
||||
min-height: 32px;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.18s var(--ease-out),
|
||||
@@ -411,8 +496,8 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
}
|
||||
|
||||
.usage-export-item:hover:not(:disabled) {
|
||||
border-color: color-mix(in srgb, var(--accent) 24%, var(--border));
|
||||
background: color-mix(in srgb, var(--accent) 10%, var(--bg));
|
||||
border-color: var(--border-strong);
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
@@ -431,13 +516,14 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
.usage-empty-state__feature {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
gap: 5px;
|
||||
padding: 5px 10px;
|
||||
border-radius: var(--radius-full);
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 16%, var(--border));
|
||||
background: color-mix(in srgb, var(--accent) 8%, var(--card));
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.usage-query-chip button {
|
||||
@@ -453,15 +539,15 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
.usage-query-suggestion {
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.18s var(--ease-out),
|
||||
border-color 0.18s var(--ease-out),
|
||||
background 0.18s var(--ease-out);
|
||||
background 0.18s var(--ease-out),
|
||||
color 0.18s var(--ease-out);
|
||||
}
|
||||
|
||||
.usage-query-suggestion:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: color-mix(in srgb, var(--accent) 28%, var(--border));
|
||||
background: color-mix(in srgb, var(--accent) 12%, var(--card));
|
||||
border-color: var(--border-strong);
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
.usage-callout {
|
||||
@@ -499,28 +585,27 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
}
|
||||
|
||||
.usage-empty-block {
|
||||
padding: 20px 14px;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: color-mix(in srgb, var(--bg-muted) 62%, transparent);
|
||||
padding: 18px 14px;
|
||||
border: 1px dashed color-mix(in srgb, var(--border) 70%, transparent);
|
||||
border-radius: var(--radius-md);
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.usage-empty-block--compact {
|
||||
padding: 12px;
|
||||
padding: 10px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.usage-overview-card {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.usage-overview-layout {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.usage-summary-grid,
|
||||
@@ -529,7 +614,7 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
.usage-mosaic-grid,
|
||||
.context-breakdown-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.usage-summary-grid {
|
||||
@@ -540,13 +625,13 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
|
||||
.usage-summary-card,
|
||||
.session-summary-card {
|
||||
min-height: 118px;
|
||||
gap: 8px;
|
||||
min-height: 108px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.usage-summary-card.stat,
|
||||
.session-summary-card.stat {
|
||||
padding: 16px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.usage-summary-card {
|
||||
@@ -572,9 +657,9 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
}
|
||||
|
||||
.usage-summary-card--hero {
|
||||
min-height: 168px;
|
||||
gap: 12px;
|
||||
padding-block: 18px;
|
||||
min-height: 148px;
|
||||
gap: 10px;
|
||||
padding-block: 16px;
|
||||
}
|
||||
|
||||
.usage-summary-card--throughput {
|
||||
@@ -598,27 +683,28 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
.sessions-sort span,
|
||||
.cost-breakdown-header,
|
||||
.usage-mosaic-section-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.usage-summary-value,
|
||||
.session-summary-value {
|
||||
font-size: clamp(1.55rem, 2vw, 2rem);
|
||||
line-height: 1.05;
|
||||
font-size: clamp(1.4rem, 2vw, 1.85rem);
|
||||
line-height: 1.1;
|
||||
color: var(--text-strong);
|
||||
font-variant-numeric: tabular-nums;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.usage-summary-card--hero .usage-summary-value {
|
||||
font-size: clamp(2rem, 3vw, 2.8rem);
|
||||
font-size: clamp(1.8rem, 2.5vw, 2.4rem);
|
||||
}
|
||||
|
||||
.usage-summary-value--compact {
|
||||
font-size: clamp(1.2rem, 1.75vw, 1.7rem);
|
||||
font-size: clamp(1.15rem, 1.5vw, 1.5rem);
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
@@ -639,39 +725,55 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
.usage-list-sub,
|
||||
.usage-error-sub {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.usage-summary-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-left: 6px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 4px;
|
||||
border-radius: var(--radius-full);
|
||||
border: 1px solid var(--border);
|
||||
background: color-mix(in srgb, var(--bg-muted) 82%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
font-size: 9px;
|
||||
cursor: help;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.15s var(--ease-out);
|
||||
}
|
||||
|
||||
.usage-summary-hint:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.usage-insights-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
|
||||
}
|
||||
|
||||
.usage-insights-grid--tight {
|
||||
margin-top: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.usage-insight-card {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
min-height: 168px;
|
||||
gap: 12px;
|
||||
align-content: start;
|
||||
min-height: 148px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: color-mix(in srgb, var(--card) 82%, var(--bg-muted));
|
||||
background: var(--card);
|
||||
transition:
|
||||
border-color 0.18s var(--ease-out),
|
||||
box-shadow 0.18s var(--ease-out);
|
||||
}
|
||||
|
||||
.usage-insight-card:hover {
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.usage-insight-card--wide {
|
||||
@@ -682,7 +784,7 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
.usage-error-list,
|
||||
.context-breakdown-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.usage-list-item,
|
||||
@@ -690,8 +792,24 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
.context-breakdown-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
align-items: baseline;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.usage-list-item:last-child,
|
||||
.usage-error-row:last-child,
|
||||
.context-breakdown-item:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.usage-list-item:first-child,
|
||||
.usage-error-row:first-child,
|
||||
.context-breakdown-item:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.usage-list-value {
|
||||
@@ -700,17 +818,20 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
color: var(--text-strong);
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.usage-error-rate {
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--danger);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.usage-mosaic {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.usage-mosaic-header,
|
||||
@@ -725,7 +846,7 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.usage-mosaic-title,
|
||||
@@ -736,13 +857,14 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
|
||||
.usage-mosaic-title,
|
||||
.session-detail-title {
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.usage-mosaic-total {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.usage-mosaic-grid {
|
||||
@@ -751,14 +873,14 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
|
||||
.usage-mosaic-section {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.usage-mosaic-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.usage-daypart-grid {
|
||||
@@ -1586,10 +1708,13 @@ details.usage-filter-select summary::-webkit-details-marker,
|
||||
}
|
||||
|
||||
.usage-filter-popover,
|
||||
.usage-export-popover {
|
||||
width: min(320px, calc(100vw - 40px));
|
||||
}
|
||||
|
||||
.usage-export-popover {
|
||||
right: auto;
|
||||
left: 0;
|
||||
width: min(320px, calc(100vw - 40px));
|
||||
}
|
||||
|
||||
.usage-daypart-grid {
|
||||
|
||||
@@ -966,6 +966,7 @@ export function renderApp(state: AppViewState) {
|
||||
error: state.toolsCatalogError,
|
||||
result: state.toolsCatalogResult,
|
||||
},
|
||||
modelCatalog: state.chatModelCatalog ?? [],
|
||||
onRefresh: async () => {
|
||||
await loadAgents(state);
|
||||
const agentIds = state.agentsList?.agents?.map((entry) => entry.id) ?? [];
|
||||
@@ -1278,16 +1279,21 @@ export function renderApp(state: AppViewState) {
|
||||
report: state.skillsReport,
|
||||
error: state.skillsError,
|
||||
filter: state.skillsFilter,
|
||||
statusFilter: state.skillsStatusFilter,
|
||||
edits: state.skillEdits,
|
||||
messages: state.skillMessages,
|
||||
busyKey: state.skillsBusyKey,
|
||||
detailKey: state.skillsDetailKey,
|
||||
onFilterChange: (next) => (state.skillsFilter = next),
|
||||
onStatusFilterChange: (next) => (state.skillsStatusFilter = next),
|
||||
onRefresh: () => loadSkills(state, { clearMessages: true }),
|
||||
onToggle: (key, enabled) => updateSkillEnabled(state, key, enabled),
|
||||
onEdit: (key, value) => updateSkillEdit(state, key, value),
|
||||
onSaveKey: (key) => saveSkillApiKey(state, key),
|
||||
onInstall: (skillKey, name, installId) =>
|
||||
installSkill(state, skillKey, name, installId),
|
||||
onDetailOpen: (key) => (state.skillsDetailKey = key),
|
||||
onDetailClose: () => (state.skillsDetailKey = null),
|
||||
}),
|
||||
)
|
||||
: nothing
|
||||
|
||||
@@ -267,9 +267,11 @@ export type AppViewState = {
|
||||
skillsReport: SkillStatusReport | null;
|
||||
skillsError: string | null;
|
||||
skillsFilter: string;
|
||||
skillsStatusFilter: "all" | "ready" | "needs-setup" | "disabled";
|
||||
skillEdits: Record<string, string>;
|
||||
skillMessages: Record<string, SkillMessage>;
|
||||
skillsBusyKey: string | null;
|
||||
skillsDetailKey: string | null;
|
||||
healthLoading: boolean;
|
||||
healthResult: HealthSummary | null;
|
||||
healthError: string | null;
|
||||
|
||||
@@ -259,8 +259,7 @@ export class OpenClawApp extends LitElement {
|
||||
@state() toolsCatalogLoading = false;
|
||||
@state() toolsCatalogError: string | null = null;
|
||||
@state() toolsCatalogResult: ToolsCatalogResult | null = null;
|
||||
@state() agentsPanel: "overview" | "files" | "tools" | "skills" | "channels" | "cron" =
|
||||
"overview";
|
||||
@state() agentsPanel: "overview" | "files" | "tools" | "skills" | "channels" | "cron" = "files";
|
||||
@state() agentFilesLoading = false;
|
||||
@state() agentFilesError: string | null = null;
|
||||
@state() agentFilesList: AgentsFilesListResult | null = null;
|
||||
@@ -398,9 +397,11 @@ export class OpenClawApp extends LitElement {
|
||||
@state() skillsReport: SkillStatusReport | null = null;
|
||||
@state() skillsError: string | null = null;
|
||||
@state() skillsFilter = "";
|
||||
@state() skillsStatusFilter: "all" | "ready" | "needs-setup" | "disabled" = "all";
|
||||
@state() skillEdits: Record<string, string> = {};
|
||||
@state() skillsBusyKey: string | null = null;
|
||||
@state() skillMessages: Record<string, SkillMessage> = {};
|
||||
@state() skillsDetailKey: string | null = null;
|
||||
|
||||
@state() healthLoading = false;
|
||||
@state() healthResult: HealthSummary | null = null;
|
||||
|
||||
@@ -279,6 +279,9 @@ describe("executeSlashCommand directives", () => {
|
||||
if (method === "models.list") {
|
||||
return { models: createModelCatalog(OPENAI_GPT5_MINI_MODEL) };
|
||||
}
|
||||
if (method === "models.list") {
|
||||
return { models: [{ id: "gpt-5-mini", name: "gpt-5-mini", provider: "openai" }] };
|
||||
}
|
||||
throw new Error(`unexpected method: ${method}`);
|
||||
});
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ export async function saveSkillApiKey(state: SkillsState, skillKey: string) {
|
||||
await loadSkills(state);
|
||||
setSkillMessage(state, skillKey, {
|
||||
kind: "success",
|
||||
message: "API key saved",
|
||||
message: `API key saved — stored in openclaw.json (skills.entries.${skillKey})`,
|
||||
});
|
||||
} catch (err) {
|
||||
const message = getErrorMessage(err);
|
||||
|
||||
@@ -448,6 +448,22 @@ export const icons = {
|
||||
<path d="M10 10l-3 2 3 2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
`,
|
||||
maximize: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<polyline points="15 3 21 3 21 9" />
|
||||
<polyline points="9 21 3 21 3 15" />
|
||||
<line x1="21" x2="14" y1="3" y2="10" />
|
||||
<line x1="3" x2="10" y1="21" y2="14" />
|
||||
</svg>
|
||||
`,
|
||||
minimize: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<polyline points="4 14 10 14 10 20" />
|
||||
<polyline points="20 10 14 10 14 4" />
|
||||
<line x1="14" x2="21" y1="10" y2="3" />
|
||||
<line x1="3" x2="10" y1="21" y2="14" />
|
||||
</svg>
|
||||
`,
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof icons;
|
||||
|
||||
@@ -645,7 +645,7 @@ export type ModelCatalogEntry = {
|
||||
provider: string;
|
||||
contextWindow?: number;
|
||||
reasoning?: boolean;
|
||||
input?: Array<"text" | "image">;
|
||||
input?: Array<"text" | "image" | "document">;
|
||||
};
|
||||
|
||||
export type ToolCatalogProfile =
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { AgentIdentityResult, AgentsFilesListResult, AgentsListResult } from "../types.ts";
|
||||
import type {
|
||||
AgentIdentityResult,
|
||||
AgentsFilesListResult,
|
||||
AgentsListResult,
|
||||
ModelCatalogEntry,
|
||||
} from "../types.ts";
|
||||
import {
|
||||
buildModelOptions,
|
||||
normalizeModelValue,
|
||||
@@ -23,6 +28,7 @@ export function renderAgentOverview(params: {
|
||||
configLoading: boolean;
|
||||
configSaving: boolean;
|
||||
configDirty: boolean;
|
||||
modelCatalog: ModelCatalogEntry[];
|
||||
onConfigReload: () => void;
|
||||
onConfigSave: () => void;
|
||||
onModelChange: (agentId: string, modelId: string | null) => void;
|
||||
@@ -128,14 +134,16 @@ export function renderAgentOverview(params: {
|
||||
>
|
||||
${
|
||||
isDefault
|
||||
? nothing
|
||||
? html`
|
||||
<option value="">Not set</option>
|
||||
`
|
||||
: html`
|
||||
<option value="">
|
||||
${defaultPrimary ? `Inherit default (${defaultPrimary})` : "Inherit default"}
|
||||
</option>
|
||||
`
|
||||
}
|
||||
${buildModelOptions(configForm, effectivePrimary ?? undefined)}
|
||||
${buildModelOptions(configForm, effectivePrimary ?? undefined, params.modelCatalog)}
|
||||
</select>
|
||||
</label>
|
||||
<div class="field">
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { applyPreviewTheme } from "@create-markdown/preview";
|
||||
import DOMPurify from "dompurify";
|
||||
import { html, nothing } from "lit";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import { marked } from "marked";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import { icons } from "../icons.ts";
|
||||
import { toSanitizedMarkdownHtml } from "../markdown.ts";
|
||||
import {
|
||||
formatCronPayload,
|
||||
formatCronSchedule,
|
||||
@@ -10,14 +12,13 @@ import {
|
||||
formatNextRun,
|
||||
} from "../presenter.ts";
|
||||
import type {
|
||||
AgentFileEntry,
|
||||
AgentsFilesListResult,
|
||||
ChannelAccountSnapshot,
|
||||
ChannelsStatusSnapshot,
|
||||
CronJob,
|
||||
CronStatus,
|
||||
} from "../types.ts";
|
||||
import { formatBytes, type AgentContext } from "./agents-utils.ts";
|
||||
import { type AgentContext } from "./agents-utils.ts";
|
||||
import type { AgentsPanel } from "./agents.ts";
|
||||
import { resolveChannelExtras as resolveChannelExtrasFromConfig } from "./channel-config-extras.ts";
|
||||
|
||||
@@ -387,7 +388,10 @@ export function renderAgentFiles(params: {
|
||||
</div>
|
||||
${
|
||||
list
|
||||
? html`<div class="muted mono" style="margin-top: 8px;">Workspace: ${list.workspace}</div>`
|
||||
? html`<div class="muted mono" style="margin-top: 8px;">Workspace: <a
|
||||
href="file://${list.workspace}"
|
||||
class="workspace-link"
|
||||
>${list.workspace}</a></div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
@@ -402,96 +406,132 @@ export function renderAgentFiles(params: {
|
||||
Load the agent workspace files to edit core instructions.
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="agent-files-grid" style="margin-top: 16px;">
|
||||
<div class="agent-files-list">
|
||||
${
|
||||
files.length === 0
|
||||
? html`
|
||||
<div class="muted">No files found.</div>
|
||||
`
|
||||
: files.map((file) =>
|
||||
renderAgentFileRow(file, active, () => params.onSelectFile(file.name)),
|
||||
)
|
||||
}
|
||||
: files.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 16px">No files found.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="agent-tabs" style="margin-top: 14px;">
|
||||
${files.map((file) => {
|
||||
const isActive = active === file.name;
|
||||
const label = file.name.replace(/\.md$/i, "");
|
||||
return html`
|
||||
<button
|
||||
class="agent-tab ${isActive ? "active" : ""} ${file.missing ? "agent-tab--missing" : ""}"
|
||||
@click=${() => params.onSelectFile(file.name)}
|
||||
>${label}${
|
||||
file.missing
|
||||
? html`
|
||||
<span class="agent-tab-badge">missing</span>
|
||||
`
|
||||
: nothing
|
||||
}</button>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
<div class="agent-files-editor">
|
||||
${
|
||||
!activeEntry
|
||||
? html`
|
||||
<div class="muted">Select a file to edit.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="agent-file-header">
|
||||
<div>
|
||||
<div class="agent-file-title mono">${activeEntry.name}</div>
|
||||
<div class="agent-file-sub mono">${activeEntry.path}</div>
|
||||
</div>
|
||||
<div class="agent-file-actions">
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
title="Preview rendered markdown"
|
||||
@click=${(e: Event) => {
|
||||
const btn = e.currentTarget as HTMLElement;
|
||||
const dialog = btn
|
||||
.closest(".agent-files-editor")
|
||||
?.querySelector("dialog");
|
||||
if (dialog) {
|
||||
dialog.showModal();
|
||||
}
|
||||
}}
|
||||
>
|
||||
${icons.eye} Preview
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${!isDirty}
|
||||
@click=${() => params.onFileReset(activeEntry.name)}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm primary"
|
||||
?disabled=${params.agentFileSaving || !isDirty}
|
||||
@click=${() => params.onFileSave(activeEntry.name)}
|
||||
>
|
||||
${params.agentFileSaving ? "Saving…" : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
${
|
||||
!activeEntry
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 16px">Select a file to edit.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="agent-file-header" style="margin-top: 14px;">
|
||||
<div>
|
||||
<div class="agent-file-sub mono">${activeEntry.path}</div>
|
||||
</div>
|
||||
${
|
||||
activeEntry.missing
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 10px">
|
||||
This file is missing. Saving will create it in the agent workspace.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<label class="field agent-file-field" style="margin-top: 12px;">
|
||||
<span>Content</span>
|
||||
<textarea
|
||||
class="agent-file-textarea"
|
||||
.value=${draft}
|
||||
@input=${(e: Event) =>
|
||||
params.onFileDraftChange(
|
||||
activeEntry.name,
|
||||
(e.target as HTMLTextAreaElement).value,
|
||||
)}
|
||||
></textarea>
|
||||
</label>
|
||||
<dialog
|
||||
class="md-preview-dialog"
|
||||
@click=${(e: Event) => {
|
||||
const dialog = e.currentTarget as HTMLDialogElement;
|
||||
if (e.target === dialog) {
|
||||
dialog.close();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="md-preview-dialog__panel">
|
||||
<div class="md-preview-dialog__header">
|
||||
<div class="md-preview-dialog__title mono">${activeEntry.name}</div>
|
||||
<div class="agent-file-actions">
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
title="Preview rendered markdown"
|
||||
@click=${(e: Event) => {
|
||||
const btn = e.currentTarget as HTMLElement;
|
||||
const dialog = btn.closest(".card")?.querySelector("dialog");
|
||||
if (dialog) {
|
||||
dialog.showModal();
|
||||
}
|
||||
}}
|
||||
>
|
||||
${icons.eye} Preview
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${!isDirty}
|
||||
@click=${() => params.onFileReset(activeEntry.name)}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm primary"
|
||||
?disabled=${params.agentFileSaving || !isDirty}
|
||||
@click=${() => params.onFileSave(activeEntry.name)}
|
||||
>
|
||||
${params.agentFileSaving ? "Saving…" : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
activeEntry.missing
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 10px">
|
||||
This file is missing. Saving will create it in the agent workspace.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<label class="field agent-file-field" style="margin-top: 12px;">
|
||||
<span>Content</span>
|
||||
<textarea
|
||||
class="agent-file-textarea"
|
||||
.value=${draft}
|
||||
@input=${(e: Event) =>
|
||||
params.onFileDraftChange(
|
||||
activeEntry.name,
|
||||
(e.target as HTMLTextAreaElement).value,
|
||||
)}
|
||||
></textarea>
|
||||
</label>
|
||||
<dialog
|
||||
class="md-preview-dialog"
|
||||
@click=${(e: Event) => {
|
||||
const dialog = e.currentTarget as HTMLDialogElement;
|
||||
if (e.target === dialog) {
|
||||
dialog.close();
|
||||
}
|
||||
}}
|
||||
@close=${(e: Event) => {
|
||||
const dialog = e.currentTarget as HTMLElement;
|
||||
dialog
|
||||
.querySelector(".md-preview-dialog__panel")
|
||||
?.classList.remove("fullscreen");
|
||||
}}
|
||||
>
|
||||
<div class="md-preview-dialog__panel">
|
||||
<div class="md-preview-dialog__header">
|
||||
<div class="md-preview-dialog__title mono">${activeEntry.name}</div>
|
||||
<div class="md-preview-dialog__actions">
|
||||
<button
|
||||
class="btn btn--sm md-preview-expand-btn"
|
||||
title="Toggle fullscreen"
|
||||
@click=${(e: Event) => {
|
||||
const btn = e.currentTarget as HTMLElement;
|
||||
const panel = btn.closest(".md-preview-dialog__panel");
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
const isFullscreen = panel.classList.toggle("fullscreen");
|
||||
btn.classList.toggle("is-fullscreen", isFullscreen);
|
||||
}}
|
||||
><span class="when-normal">${icons.maximize} Expand</span><span class="when-fullscreen">${icons.minimize} Collapse</span></button>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
title="Edit file"
|
||||
@click=${(e: Event) => {
|
||||
(e.currentTarget as HTMLElement).closest("dialog")?.close();
|
||||
const textarea =
|
||||
document.querySelector<HTMLElement>(".agent-file-textarea");
|
||||
textarea?.focus();
|
||||
}}
|
||||
>${icons.edit} Editor</button>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
@click=${(e: Event) => {
|
||||
@@ -499,42 +539,16 @@ export function renderAgentFiles(params: {
|
||||
}}
|
||||
>${icons.x} Close</button>
|
||||
</div>
|
||||
<div class="md-preview-dialog__body sidebar-markdown">
|
||||
${unsafeHTML(toSanitizedMarkdownHtml(draft))}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
<div class="md-preview-dialog__body">
|
||||
${unsafeHTML(applyPreviewTheme(marked.parse(draft, { gfm: true, breaks: true }) as string, { sanitize: (h: string) => DOMPurify.sanitize(h) }))}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
`
|
||||
}
|
||||
`
|
||||
}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderAgentFileRow(file: AgentFileEntry, active: string | null, onSelect: () => void) {
|
||||
const status = file.missing
|
||||
? "Missing"
|
||||
: `${formatBytes(file.size)} · ${formatRelativeTimestamp(file.updatedAtMs ?? null)}`;
|
||||
return html`
|
||||
<button
|
||||
type="button"
|
||||
class="agent-file-row ${active === file.name ? "active" : ""}"
|
||||
@click=${onSelect}
|
||||
>
|
||||
<div>
|
||||
<div class="agent-file-name mono">${file.name}</div>
|
||||
<div class="agent-file-meta">${status}</div>
|
||||
</div>
|
||||
${
|
||||
file.missing
|
||||
? html`
|
||||
<span class="agent-pill warn">missing</span>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html } from "lit";
|
||||
import { html, nothing } from "lit";
|
||||
import {
|
||||
expandToolGroups,
|
||||
normalizeToolName,
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
AgentIdentityResult,
|
||||
AgentsFilesListResult,
|
||||
AgentsListResult,
|
||||
ModelCatalogEntry,
|
||||
ToolCatalogProfile,
|
||||
ToolsCatalogResult,
|
||||
} from "../types.ts";
|
||||
@@ -574,16 +575,38 @@ function resolveConfiguredModels(
|
||||
export function buildModelOptions(
|
||||
configForm: Record<string, unknown> | null,
|
||||
current?: string | null,
|
||||
catalog?: ModelCatalogEntry[],
|
||||
) {
|
||||
const options = resolveConfiguredModels(configForm);
|
||||
const hasCurrent = current ? options.some((option) => option.value === current) : false;
|
||||
if (current && !hasCurrent) {
|
||||
const seen = new Set<string>();
|
||||
const options: ConfiguredModelOption[] = [];
|
||||
const addOption = (value: string, label: string) => {
|
||||
const key = value.toLowerCase();
|
||||
if (seen.has(key)) {
|
||||
return;
|
||||
}
|
||||
seen.add(key);
|
||||
options.push({ value, label });
|
||||
};
|
||||
|
||||
for (const opt of resolveConfiguredModels(configForm)) {
|
||||
addOption(opt.value, opt.label);
|
||||
}
|
||||
|
||||
if (catalog) {
|
||||
for (const entry of catalog) {
|
||||
const provider = entry.provider?.trim();
|
||||
const value = provider ? `${provider}/${entry.id}` : entry.id;
|
||||
const label = provider ? `${entry.id} · ${provider}` : entry.id;
|
||||
addOption(value, label);
|
||||
}
|
||||
}
|
||||
|
||||
if (current && !seen.has(current.toLowerCase())) {
|
||||
options.unshift({ value: current, label: `Current (${current})` });
|
||||
}
|
||||
|
||||
if (options.length === 0) {
|
||||
return html`
|
||||
<option value="" disabled>No configured models</option>
|
||||
`;
|
||||
return nothing;
|
||||
}
|
||||
return options.map((option) => html`<option value=${option.value}>${option.label}</option>`);
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ function createProps(overrides: Partial<AgentsProps> = {}): AgentsProps {
|
||||
error: null,
|
||||
result: null,
|
||||
},
|
||||
modelCatalog: [],
|
||||
onRefresh: () => undefined,
|
||||
onSelectAgent: () => undefined,
|
||||
onSelectPanel: () => undefined,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
ChannelsStatusSnapshot,
|
||||
CronJob,
|
||||
CronStatus,
|
||||
ModelCatalogEntry,
|
||||
SkillStatusReport,
|
||||
ToolsCatalogResult,
|
||||
} from "../types.ts";
|
||||
@@ -81,6 +82,7 @@ export type AgentsProps = {
|
||||
agentIdentityById: Record<string, AgentIdentityResult>;
|
||||
agentSkills: AgentSkillsState;
|
||||
toolsCatalog: ToolsCatalogState;
|
||||
modelCatalog: ModelCatalogEntry[];
|
||||
onRefresh: () => void;
|
||||
onSelectAgent: (agentId: string) => void;
|
||||
onSelectPanel: (panel: AgentsPanel) => void;
|
||||
@@ -135,72 +137,51 @@ export function renderAgents(props: AgentsProps) {
|
||||
<div class="agents-layout">
|
||||
<section class="agents-toolbar">
|
||||
<div class="agents-toolbar-row">
|
||||
<span class="agents-toolbar-label">Agent</span>
|
||||
<div class="agents-control-row">
|
||||
<div class="agents-control-select">
|
||||
<select
|
||||
class="agents-select"
|
||||
.value=${selectedId ?? ""}
|
||||
?disabled=${props.loading || agents.length === 0}
|
||||
@change=${(e: Event) => props.onSelectAgent((e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
${
|
||||
agents.length === 0
|
||||
? html`
|
||||
<option value="">No agents</option>
|
||||
`
|
||||
: agents.map(
|
||||
(agent) => html`
|
||||
<div class="agents-control-select">
|
||||
<select
|
||||
class="agents-select"
|
||||
.value=${selectedId ?? ""}
|
||||
?disabled=${props.loading || agents.length === 0}
|
||||
@change=${(e: Event) => props.onSelectAgent((e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
${
|
||||
agents.length === 0
|
||||
? html`
|
||||
<option value="">No agents</option>
|
||||
`
|
||||
: agents.map(
|
||||
(agent) => html`
|
||||
<option value=${agent.id} ?selected=${agent.id === selectedId}>
|
||||
${normalizeAgentLabel(agent)}${agentBadgeText(agent.id, defaultId) ? ` (${agentBadgeText(agent.id, defaultId)})` : ""}
|
||||
</option>
|
||||
`,
|
||||
)
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="agents-control-actions">
|
||||
${
|
||||
selectedAgent
|
||||
? html`
|
||||
<div class="agent-actions-wrap">
|
||||
<button
|
||||
class="agent-actions-toggle"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
actionsMenuOpen = !actionsMenuOpen;
|
||||
}}
|
||||
>⋯</button>
|
||||
${
|
||||
actionsMenuOpen
|
||||
? html`
|
||||
<div class="agent-actions-menu">
|
||||
<button type="button" @click=${() => {
|
||||
void navigator.clipboard.writeText(selectedAgent.id);
|
||||
actionsMenuOpen = false;
|
||||
}}>Copy agent ID</button>
|
||||
<button
|
||||
type="button"
|
||||
?disabled=${Boolean(defaultId && selectedAgent.id === defaultId)}
|
||||
@click=${() => {
|
||||
props.onSetDefault(selectedAgent.id);
|
||||
actionsMenuOpen = false;
|
||||
}}
|
||||
>
|
||||
${defaultId && selectedAgent.id === defaultId ? "Already default" : "Set as default"}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
)
|
||||
}
|
||||
<button class="btn btn--sm agents-refresh-btn" ?disabled=${props.loading} @click=${props.onRefresh}>
|
||||
${props.loading ? "Loading…" : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
</select>
|
||||
</div>
|
||||
<div class="agents-toolbar-actions">
|
||||
${
|
||||
selectedAgent
|
||||
? html`
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--sm btn--ghost"
|
||||
@click=${() => void navigator.clipboard.writeText(selectedAgent.id)}
|
||||
title="Copy agent ID to clipboard"
|
||||
>Copy ID</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--sm btn--ghost"
|
||||
?disabled=${Boolean(defaultId && selectedAgent.id === defaultId)}
|
||||
@click=${() => props.onSetDefault(selectedAgent.id)}
|
||||
title=${defaultId && selectedAgent.id === defaultId ? "Already the default agent" : "Set as the default agent"}
|
||||
>${defaultId && selectedAgent.id === defaultId ? "Default" : "Set Default"}</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<button class="btn btn--sm agents-refresh-btn" ?disabled=${props.loading} @click=${props.onRefresh}>
|
||||
${props.loading ? "Loading…" : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
@@ -234,6 +215,7 @@ export function renderAgents(props: AgentsProps) {
|
||||
configLoading: props.config.loading,
|
||||
configSaving: props.config.saving,
|
||||
configDirty: props.config.dirty,
|
||||
modelCatalog: props.modelCatalog,
|
||||
onConfigReload: props.onConfigReload,
|
||||
onConfigSave: props.onConfigSave,
|
||||
onModelChange: props.onModelChange,
|
||||
@@ -350,8 +332,6 @@ export function renderAgents(props: AgentsProps) {
|
||||
`;
|
||||
}
|
||||
|
||||
let actionsMenuOpen = false;
|
||||
|
||||
function renderAgentTabs(
|
||||
active: AgentsPanel,
|
||||
onSelect: (panel: AgentsPanel) => void,
|
||||
|
||||
@@ -656,13 +656,14 @@ function renderSearchBar(requestUpdate: () => void): TemplateResult | typeof not
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search messages..."
|
||||
aria-label="Search messages"
|
||||
.value=${vs.searchQuery}
|
||||
@input=${(e: Event) => {
|
||||
vs.searchQuery = (e.target as HTMLInputElement).value;
|
||||
requestUpdate();
|
||||
}}
|
||||
/>
|
||||
<button class="btn btn--ghost" @click=${() => {
|
||||
<button class="btn btn--ghost" aria-label="Close search" @click=${() => {
|
||||
vs.searchOpen = false;
|
||||
vs.searchQuery = "";
|
||||
requestUpdate();
|
||||
@@ -739,13 +740,15 @@ function renderSlashMenu(
|
||||
// Arg-picker mode: show options for the selected command
|
||||
if (vs.slashMenuMode === "args" && vs.slashMenuCommand && vs.slashMenuArgItems.length > 0) {
|
||||
return html`
|
||||
<div class="slash-menu">
|
||||
<div class="slash-menu" role="listbox" aria-label="Command arguments">
|
||||
<div class="slash-menu-group">
|
||||
<div class="slash-menu-group__label">/${vs.slashMenuCommand.name} ${vs.slashMenuCommand.description}</div>
|
||||
${vs.slashMenuArgItems.map(
|
||||
(arg, i) => html`
|
||||
<div
|
||||
class="slash-menu-item ${i === vs.slashMenuIndex ? "slash-menu-item--active" : ""}"
|
||||
role="option"
|
||||
aria-selected=${i === vs.slashMenuIndex}
|
||||
@click=${() => selectSlashArg(arg, props, requestUpdate, true)}
|
||||
@mouseenter=${() => {
|
||||
vs.slashMenuIndex = i;
|
||||
@@ -798,6 +801,8 @@ function renderSlashMenu(
|
||||
({ cmd, globalIdx }) => html`
|
||||
<div
|
||||
class="slash-menu-item ${globalIdx === vs.slashMenuIndex ? "slash-menu-item--active" : ""}"
|
||||
role="option"
|
||||
aria-selected=${globalIdx === vs.slashMenuIndex}
|
||||
@click=${() => selectSlashCommand(cmd, props, requestUpdate)}
|
||||
@mouseenter=${() => {
|
||||
vs.slashMenuIndex = globalIdx;
|
||||
@@ -825,7 +830,7 @@ function renderSlashMenu(
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="slash-menu">
|
||||
<div class="slash-menu" role="listbox" aria-label="Slash commands">
|
||||
${sections}
|
||||
<div class="slash-menu-footer">
|
||||
<kbd>↑↓</kbd> navigate
|
||||
@@ -1254,6 +1259,7 @@ export function renderChat(props: ChatProps) {
|
||||
document.querySelector<HTMLInputElement>(".agent-chat__file-input")?.click();
|
||||
}}
|
||||
title="Attach file"
|
||||
aria-label="Attach file"
|
||||
?disabled=${!props.connected}
|
||||
>
|
||||
${icons.paperclip}
|
||||
@@ -1332,14 +1338,14 @@ export function renderChat(props: ChatProps) {
|
||||
</button>
|
||||
`
|
||||
}
|
||||
<button class="btn btn--ghost" @click=${() => exportMarkdown(props)} title="Export" ?disabled=${props.messages.length === 0}>
|
||||
<button class="btn btn--ghost" @click=${() => exportMarkdown(props)} title="Export" aria-label="Export chat" ?disabled=${props.messages.length === 0}>
|
||||
${icons.download}
|
||||
</button>
|
||||
|
||||
${
|
||||
canAbort && (isBusy || props.sending)
|
||||
? html`
|
||||
<button class="chat-send-btn chat-send-btn--stop" @click=${props.onAbort} title="Stop">
|
||||
<button class="chat-send-btn chat-send-btn--stop" @click=${props.onAbort} title="Stop" aria-label="Stop generating">
|
||||
${icons.stop}
|
||||
</button>
|
||||
`
|
||||
@@ -1354,6 +1360,7 @@ export function renderChat(props: ChatProps) {
|
||||
}}
|
||||
?disabled=${!props.connected || props.sending}
|
||||
title=${isBusy ? "Queue" : "Send"}
|
||||
aria-label=${isBusy ? "Queue message" : "Send message"}
|
||||
>
|
||||
${icons.send}
|
||||
</button>
|
||||
|
||||
@@ -887,6 +887,7 @@ export function renderConfig(props: ConfigProps) {
|
||||
type="text"
|
||||
class="config-search__input"
|
||||
placeholder="Search settings..."
|
||||
aria-label="Search settings"
|
||||
.value=${props.searchQuery}
|
||||
@input=${(e: Event) =>
|
||||
props.onSearchChange((e.target as HTMLInputElement).value)}
|
||||
@@ -896,6 +897,7 @@ export function renderConfig(props: ConfigProps) {
|
||||
? html`
|
||||
<button
|
||||
class="config-search__clear"
|
||||
aria-label="Clear search"
|
||||
@click=${() => props.onSearchChange("")}
|
||||
>
|
||||
×
|
||||
|
||||
@@ -9,33 +9,95 @@ import {
|
||||
renderSkillStatusChips,
|
||||
} from "./skills-shared.ts";
|
||||
|
||||
export type SkillsStatusFilter = "all" | "ready" | "needs-setup" | "disabled";
|
||||
|
||||
export type SkillsProps = {
|
||||
connected: boolean;
|
||||
loading: boolean;
|
||||
report: SkillStatusReport | null;
|
||||
error: string | null;
|
||||
filter: string;
|
||||
statusFilter: SkillsStatusFilter;
|
||||
edits: Record<string, string>;
|
||||
busyKey: string | null;
|
||||
messages: SkillMessageMap;
|
||||
detailKey: string | null;
|
||||
onFilterChange: (next: string) => void;
|
||||
onStatusFilterChange: (next: SkillsStatusFilter) => void;
|
||||
onRefresh: () => void;
|
||||
onToggle: (skillKey: string, enabled: boolean) => void;
|
||||
onEdit: (skillKey: string, value: string) => void;
|
||||
onSaveKey: (skillKey: string) => void;
|
||||
onInstall: (skillKey: string, name: string, installId: string) => void;
|
||||
onDetailOpen: (skillKey: string) => void;
|
||||
onDetailClose: () => void;
|
||||
};
|
||||
|
||||
type StatusTabDef = { id: SkillsStatusFilter; label: string };
|
||||
|
||||
const STATUS_TABS: StatusTabDef[] = [
|
||||
{ id: "all", label: "All" },
|
||||
{ id: "ready", label: "Ready" },
|
||||
{ id: "needs-setup", label: "Needs Setup" },
|
||||
{ id: "disabled", label: "Disabled" },
|
||||
];
|
||||
|
||||
function skillMatchesStatus(skill: SkillStatusEntry, status: SkillsStatusFilter): boolean {
|
||||
switch (status) {
|
||||
case "all":
|
||||
return true;
|
||||
case "ready":
|
||||
return !skill.disabled && skill.eligible;
|
||||
case "needs-setup":
|
||||
return !skill.disabled && !skill.eligible;
|
||||
case "disabled":
|
||||
return skill.disabled;
|
||||
}
|
||||
}
|
||||
|
||||
function skillStatusClass(skill: SkillStatusEntry): string {
|
||||
if (skill.disabled) {
|
||||
return "muted";
|
||||
}
|
||||
return skill.eligible ? "ok" : "warn";
|
||||
}
|
||||
|
||||
export function renderSkills(props: SkillsProps) {
|
||||
const skills = props.report?.skills ?? [];
|
||||
|
||||
const statusCounts: Record<SkillsStatusFilter, number> = {
|
||||
all: skills.length,
|
||||
ready: 0,
|
||||
"needs-setup": 0,
|
||||
disabled: 0,
|
||||
};
|
||||
for (const s of skills) {
|
||||
if (s.disabled) {
|
||||
statusCounts.disabled++;
|
||||
} else if (s.eligible) {
|
||||
statusCounts.ready++;
|
||||
} else {
|
||||
statusCounts["needs-setup"]++;
|
||||
}
|
||||
}
|
||||
|
||||
const afterStatus =
|
||||
props.statusFilter === "all"
|
||||
? skills
|
||||
: skills.filter((s) => skillMatchesStatus(s, props.statusFilter));
|
||||
|
||||
const filter = props.filter.trim().toLowerCase();
|
||||
const filtered = filter
|
||||
? skills.filter((skill) =>
|
||||
? afterStatus.filter((skill) =>
|
||||
[skill.name, skill.description, skill.source].join(" ").toLowerCase().includes(filter),
|
||||
)
|
||||
: skills;
|
||||
: afterStatus;
|
||||
const groups = groupSkills(filtered);
|
||||
|
||||
const detailSkill = props.detailKey
|
||||
? (skills.find((s) => s.skillKey === props.detailKey) ?? null)
|
||||
: null;
|
||||
|
||||
return html`
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content: space-between;">
|
||||
@@ -44,13 +106,26 @@ export function renderSkills(props: SkillsProps) {
|
||||
<div class="card-sub">Installed skills and their status.</div>
|
||||
</div>
|
||||
<button class="btn" ?disabled=${props.loading || !props.connected} @click=${props.onRefresh}>
|
||||
${props.loading ? "Loading…" : "Refresh"}
|
||||
${props.loading ? "Loading\u2026" : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="filters" style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 14px;">
|
||||
<div class="agent-tabs" style="margin-top: 14px;">
|
||||
${STATUS_TABS.map(
|
||||
(tab) => html`
|
||||
<button
|
||||
class="agent-tab ${props.statusFilter === tab.id ? "active" : ""}"
|
||||
@click=${() => props.onStatusFilterChange(tab.id)}
|
||||
>
|
||||
${tab.label}<span class="agent-tab-count">${statusCounts[tab.id]}</span>
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="filters" style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 12px;">
|
||||
<a
|
||||
class="btn"
|
||||
class="btn btn--sm"
|
||||
href="https://clawhub.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@@ -88,9 +163,8 @@ export function renderSkills(props: SkillsProps) {
|
||||
: html`
|
||||
<div class="agent-skills-groups" style="margin-top: 16px;">
|
||||
${groups.map((group) => {
|
||||
const collapsedByDefault = group.id === "workspace" || group.id === "built-in";
|
||||
return html`
|
||||
<details class="agent-skills-group" ?open=${!collapsedByDefault}>
|
||||
<details class="agent-skills-group" open>
|
||||
<summary class="agent-skills-header">
|
||||
<span>${group.label}</span>
|
||||
<span class="muted">${group.skills.length}</span>
|
||||
@@ -105,10 +179,50 @@ export function renderSkills(props: SkillsProps) {
|
||||
`
|
||||
}
|
||||
</section>
|
||||
|
||||
${detailSkill ? renderSkillDetail(detailSkill, props) : nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
|
||||
const busy = props.busyKey === skill.skillKey;
|
||||
const dotClass = skillStatusClass(skill);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="list-item list-item-clickable"
|
||||
@click=${() => props.onDetailOpen(skill.skillKey)}
|
||||
>
|
||||
<div class="list-main">
|
||||
<div class="list-title" style="display: flex; align-items: center; gap: 8px;">
|
||||
<span class="statusDot ${dotClass}"></span>
|
||||
${skill.emoji ? html`<span>${skill.emoji}</span>` : nothing}
|
||||
<span>${skill.name}</span>
|
||||
</div>
|
||||
<div class="list-sub">${clampText(skill.description, 140)}</div>
|
||||
</div>
|
||||
<div class="list-meta" style="display: flex; align-items: center; justify-content: flex-end; gap: 10px;">
|
||||
<label
|
||||
class="skill-toggle-wrap"
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="skill-toggle"
|
||||
.checked=${!skill.disabled}
|
||||
?disabled=${busy}
|
||||
@change=${(e: Event) => {
|
||||
e.stopPropagation();
|
||||
props.onToggle(skill.skillKey, skill.disabled);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSkillDetail(skill: SkillStatusEntry, props: SkillsProps) {
|
||||
const busy = props.busyKey === skill.skillKey;
|
||||
const apiKey = props.edits[skill.skillKey] ?? "";
|
||||
const message = props.messages[skill.skillKey] ?? null;
|
||||
@@ -116,92 +230,124 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
|
||||
const showBundledBadge = Boolean(skill.bundled && skill.source !== "openclaw-bundled");
|
||||
const missing = computeSkillMissing(skill);
|
||||
const reasons = computeSkillReasons(skill);
|
||||
|
||||
return html`
|
||||
<div class="list-item">
|
||||
<div class="list-main">
|
||||
<div class="list-title">
|
||||
${skill.emoji ? `${skill.emoji} ` : ""}${skill.name}
|
||||
<dialog class="md-preview-dialog" open @click=${(e: Event) => {
|
||||
if ((e.target as HTMLElement).classList.contains("md-preview-dialog")) {
|
||||
props.onDetailClose();
|
||||
}
|
||||
}}>
|
||||
<div class="md-preview-dialog__panel">
|
||||
<div class="md-preview-dialog__header">
|
||||
<div class="md-preview-dialog__title" style="display: flex; align-items: center; gap: 8px;">
|
||||
<span class="statusDot ${skillStatusClass(skill)}"></span>
|
||||
${skill.emoji ? html`<span style="font-size: 18px;">${skill.emoji}</span>` : nothing}
|
||||
<span>${skill.name}</span>
|
||||
</div>
|
||||
<button class="btn btn--sm" @click=${props.onDetailClose}>Close</button>
|
||||
</div>
|
||||
<div class="list-sub">${clampText(skill.description, 140)}</div>
|
||||
${renderSkillStatusChips({ skill, showBundledBadge })}
|
||||
${
|
||||
missing.length > 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 6px;">
|
||||
Missing: ${missing.join(", ")}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
reasons.length > 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 6px;">
|
||||
Reason: ${reasons.join(", ")}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
<div class="row" style="justify-content: flex-end; flex-wrap: wrap;">
|
||||
<button
|
||||
class="btn"
|
||||
?disabled=${busy}
|
||||
@click=${() => props.onToggle(skill.skillKey, skill.disabled)}
|
||||
>
|
||||
${skill.disabled ? "Enable" : "Disable"}
|
||||
</button>
|
||||
<div class="md-preview-dialog__body" style="display: grid; gap: 16px;">
|
||||
<div>
|
||||
<div style="font-size: 14px; line-height: 1.5; color: var(--text);">${skill.description}</div>
|
||||
${renderSkillStatusChips({ skill, showBundledBadge })}
|
||||
</div>
|
||||
|
||||
${
|
||||
canInstall
|
||||
? html`<button
|
||||
class="btn"
|
||||
?disabled=${busy}
|
||||
@click=${() => props.onInstall(skill.skillKey, skill.name, skill.install[0].id)}
|
||||
>
|
||||
${busy ? "Installing…" : skill.install[0].label}
|
||||
</button>`
|
||||
missing.length > 0
|
||||
? html`
|
||||
<div class="callout" style="border-color: var(--warn-subtle); background: var(--warn-subtle); color: var(--warn);">
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">Missing requirements</div>
|
||||
<div>${missing.join(", ")}</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${
|
||||
message
|
||||
? html`<div
|
||||
class="muted"
|
||||
style="margin-top: 8px; color: ${
|
||||
message.kind === "error"
|
||||
? "var(--danger-color, #d14343)"
|
||||
: "var(--success-color, #0a7f5a)"
|
||||
};"
|
||||
>
|
||||
${message.message}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
skill.primaryEnv
|
||||
? html`
|
||||
<div class="field" style="margin-top: 10px;">
|
||||
<span>API key</span>
|
||||
<input
|
||||
type="password"
|
||||
.value=${apiKey}
|
||||
@input=${(e: Event) =>
|
||||
props.onEdit(skill.skillKey, (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="btn primary"
|
||||
style="margin-top: 8px;"
|
||||
|
||||
${
|
||||
reasons.length > 0
|
||||
? html`
|
||||
<div class="muted" style="font-size: 13px;">
|
||||
Reason: ${reasons.join(", ")}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<label class="skill-toggle-wrap">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="skill-toggle"
|
||||
.checked=${!skill.disabled}
|
||||
?disabled=${busy}
|
||||
@click=${() => props.onSaveKey(skill.skillKey)}
|
||||
@change=${() => props.onToggle(skill.skillKey, skill.disabled)}
|
||||
/>
|
||||
</label>
|
||||
<span style="font-size: 13px; font-weight: 500;">
|
||||
${skill.disabled ? "Disabled" : "Enabled"}
|
||||
</span>
|
||||
${
|
||||
canInstall
|
||||
? html`<button
|
||||
class="btn"
|
||||
?disabled=${busy}
|
||||
@click=${() => props.onInstall(skill.skillKey, skill.name, skill.install[0].id)}
|
||||
>
|
||||
${busy ? "Installing\u2026" : skill.install[0].label}
|
||||
</button>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
|
||||
${
|
||||
message
|
||||
? html`<div
|
||||
class="callout ${message.kind === "error" ? "danger" : "success"}"
|
||||
>
|
||||
Save key
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${message.message}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${
|
||||
skill.primaryEnv
|
||||
? html`
|
||||
<div style="display: grid; gap: 8px;">
|
||||
<div class="field">
|
||||
<span>API key <span class="muted" style="font-weight: normal; font-size: 0.88em;">(${skill.primaryEnv})</span></span>
|
||||
<input
|
||||
type="password"
|
||||
.value=${apiKey}
|
||||
@input=${(e: Event) =>
|
||||
props.onEdit(skill.skillKey, (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
${
|
||||
skill.homepage
|
||||
? html`<div class="muted" style="font-size: 13px;">
|
||||
Get your key: <a href="${skill.homepage}" target="_blank" rel="noopener">${skill.homepage}</a>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
<button
|
||||
class="btn primary"
|
||||
?disabled=${busy}
|
||||
@click=${() => props.onSaveKey(skill.skillKey)}
|
||||
>
|
||||
Save key
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div style="border-top: 1px solid var(--border); padding-top: 12px; display: grid; gap: 6px; font-size: 12px; color: var(--muted);">
|
||||
<div><span style="font-weight: 600;">Source:</span> ${skill.source}</div>
|
||||
<div style="font-family: var(--mono); word-break: break-all;">${skill.filePath}</div>
|
||||
${skill.homepage ? html`<div><a href="${skill.homepage}" target="_blank" rel="noopener">${skill.homepage}</a></div>` : nothing}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -178,6 +178,22 @@ function renderDailyChartCompact(
|
||||
const values = daily.map((d) => (isTokenMode ? d.totalTokens : d.totalCost));
|
||||
const maxValue = Math.max(...values, isTokenMode ? 1 : 0.0001);
|
||||
|
||||
// Adaptive scaling: when the spread between largest and smallest non-zero
|
||||
// values is extreme (>50×), use square-root compression so small bars stay
|
||||
// visible instead of collapsing to a single pixel.
|
||||
const nonZero = values.filter((v) => v > 0);
|
||||
const minNonZero = nonZero.length > 0 ? Math.min(...nonZero) : maxValue;
|
||||
const spread = maxValue / minNonZero;
|
||||
const chartAreaPx = 200;
|
||||
const minBarPx = 6;
|
||||
const barHeights = values.map((v): number => {
|
||||
if (v <= 0) {
|
||||
return 0;
|
||||
}
|
||||
const ratio = spread > 50 ? Math.sqrt(v / maxValue) : v / maxValue;
|
||||
return Math.max(minBarPx, ratio * chartAreaPx);
|
||||
});
|
||||
|
||||
// Calculate bar width based on number of days
|
||||
const barMaxWidth = daily.length > 30 ? 12 : daily.length > 20 ? 18 : daily.length > 14 ? 24 : 32;
|
||||
const showTotals = daily.length <= 14;
|
||||
@@ -206,8 +222,7 @@ function renderDailyChartCompact(
|
||||
<div class="daily-chart">
|
||||
<div class="daily-chart-bars" style="--bar-max-width: ${barMaxWidth}px">
|
||||
${daily.map((d, idx) => {
|
||||
const value = values[idx];
|
||||
const heightPct = (value / maxValue) * 100;
|
||||
const heightPx = barHeights[idx];
|
||||
const isSelected = selectedDays.includes(d.date);
|
||||
const label = formatDayLabel(d.date);
|
||||
// Shorter label for many days (just day number)
|
||||
@@ -257,7 +272,7 @@ function renderDailyChartCompact(
|
||||
? html`
|
||||
<div
|
||||
class="daily-bar daily-bar--stacked"
|
||||
style="height: ${heightPct.toFixed(1)}%;"
|
||||
style="height: ${heightPx.toFixed(0)}px;"
|
||||
>
|
||||
${(() => {
|
||||
const total = segments.reduce((sum, seg) => sum + seg.value, 0) || 1;
|
||||
@@ -273,7 +288,7 @@ function renderDailyChartCompact(
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="daily-bar" style="height: ${heightPct.toFixed(1)}%"></div>
|
||||
<div class="daily-bar" style="height: ${heightPx.toFixed(0)}px"></div>
|
||||
`
|
||||
}
|
||||
${showTotals ? html`<div class="daily-bar-total">${totalLabel}</div>` : nothing}
|
||||
|
||||
@@ -584,6 +584,7 @@ export function renderUsage(props: UsageProps) {
|
||||
type="date"
|
||||
.value=${filters.startDate}
|
||||
title=${t("usage.filters.startDate")}
|
||||
aria-label=${t("usage.filters.startDate")}
|
||||
@change=${(e: Event) =>
|
||||
filterActions.onStartDateChange((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
@@ -593,6 +594,7 @@ export function renderUsage(props: UsageProps) {
|
||||
type="date"
|
||||
.value=${filters.endDate}
|
||||
title=${t("usage.filters.endDate")}
|
||||
aria-label=${t("usage.filters.endDate")}
|
||||
@change=${(e: Event) =>
|
||||
filterActions.onEndDateChange((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
@@ -600,6 +602,7 @@ export function renderUsage(props: UsageProps) {
|
||||
<select
|
||||
class="usage-select"
|
||||
title=${t("usage.filters.timeZone")}
|
||||
aria-label=${t("usage.filters.timeZone")}
|
||||
.value=${filters.timeZone}
|
||||
@change=${(e: Event) =>
|
||||
filterActions.onTimeZoneChange(
|
||||
|
||||
Reference in New Issue
Block a user