chore(ui): remove unused imports and variables
Clean up all unused imports, variables, and parameters across the UI codebase to comply with the new noUnusedLocals and noUnusedParameters TypeScript compiler options.
This commit is contained in:
parent
13661abbd8
commit
1e8681497c
@ -1,189 +1,61 @@
|
|||||||
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap");
|
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Unbounded:wght@400;500;600&family=Work+Sans:wght@400;500;600;700&display=swap");
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* Background - Warmer dark with depth */
|
--bg: #0a0f14;
|
||||||
--bg: #12141a;
|
--bg-accent: #111826;
|
||||||
--bg-accent: #14161d;
|
--bg-grad-1: #162031;
|
||||||
--bg-elevated: #1a1d25;
|
--bg-grad-2: #1f2a22;
|
||||||
--bg-hover: #262a35;
|
--bg-overlay: rgba(255, 255, 255, 0.05);
|
||||||
--bg-muted: #262a35;
|
--bg-glow: rgba(245, 159, 74, 0.12);
|
||||||
|
--panel: rgba(14, 20, 30, 0.88);
|
||||||
/* Card / Surface - More contrast between levels */
|
--panel-strong: rgba(18, 26, 38, 0.96);
|
||||||
--card: #181b22;
|
--chrome: rgba(9, 14, 20, 0.72);
|
||||||
--card-foreground: #f4f4f5;
|
--chrome-strong: rgba(9, 14, 20, 0.86);
|
||||||
--card-highlight: rgba(255, 255, 255, 0.05);
|
--text: rgba(244, 246, 251, 0.96);
|
||||||
--popover: #181b22;
|
--chat-text: rgba(231, 237, 244, 0.92);
|
||||||
--popover-foreground: #f4f4f5;
|
--muted: rgba(156, 169, 189, 0.72);
|
||||||
|
--border: rgba(255, 255, 255, 0.09);
|
||||||
/* Panel */
|
--border-strong: rgba(255, 255, 255, 0.16);
|
||||||
--panel: #12141a;
|
--accent: #f59f4a;
|
||||||
--panel-strong: #1a1d25;
|
--accent-2: #34c7b7;
|
||||||
--panel-hover: #262a35;
|
--ok: #2bd97f;
|
||||||
--chrome: rgba(18, 20, 26, 0.95);
|
--warn: #f2c94c;
|
||||||
--chrome-strong: rgba(18, 20, 26, 0.98);
|
--danger: #ff6b6b;
|
||||||
|
--focus: rgba(245, 159, 74, 0.35);
|
||||||
/* Text - Slightly warmer */
|
|
||||||
--text: #e4e4e7;
|
|
||||||
--text-strong: #fafafa;
|
|
||||||
--chat-text: #e4e4e7;
|
|
||||||
--muted: #71717a;
|
|
||||||
--muted-strong: #52525b;
|
|
||||||
--muted-foreground: #71717a;
|
|
||||||
|
|
||||||
/* Border - Subtle but defined */
|
|
||||||
--border: #27272a;
|
|
||||||
--border-strong: #3f3f46;
|
|
||||||
--border-hover: #52525b;
|
|
||||||
--input: #27272a;
|
|
||||||
--ring: #ff5c5c;
|
|
||||||
|
|
||||||
/* Accent - Punchy signature red */
|
|
||||||
--accent: #ff5c5c;
|
|
||||||
--accent-hover: #ff7070;
|
|
||||||
--accent-muted: #ff5c5c;
|
|
||||||
--accent-subtle: rgba(255, 92, 92, 0.15);
|
|
||||||
--accent-foreground: #fafafa;
|
|
||||||
--accent-glow: rgba(255, 92, 92, 0.25);
|
|
||||||
--primary: #ff5c5c;
|
|
||||||
--primary-foreground: #ffffff;
|
|
||||||
|
|
||||||
/* Secondary - Teal accent for variety */
|
|
||||||
--secondary: #1e2028;
|
|
||||||
--secondary-foreground: #f4f4f5;
|
|
||||||
--accent-2: #14b8a6;
|
|
||||||
--accent-2-muted: rgba(20, 184, 166, 0.7);
|
|
||||||
--accent-2-subtle: rgba(20, 184, 166, 0.15);
|
|
||||||
|
|
||||||
/* Semantic - More saturated */
|
|
||||||
--ok: #22c55e;
|
|
||||||
--ok-muted: rgba(34, 197, 94, 0.75);
|
|
||||||
--ok-subtle: rgba(34, 197, 94, 0.12);
|
|
||||||
--destructive: #ef4444;
|
|
||||||
--destructive-foreground: #fafafa;
|
|
||||||
--warn: #f59e0b;
|
|
||||||
--warn-muted: rgba(245, 158, 11, 0.75);
|
|
||||||
--warn-subtle: rgba(245, 158, 11, 0.12);
|
|
||||||
--danger: #ef4444;
|
|
||||||
--danger-muted: rgba(239, 68, 68, 0.75);
|
|
||||||
--danger-subtle: rgba(239, 68, 68, 0.12);
|
|
||||||
--info: #3b82f6;
|
|
||||||
|
|
||||||
/* Focus - With glow */
|
|
||||||
--focus: rgba(255, 92, 92, 0.25);
|
|
||||||
--focus-ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--ring);
|
|
||||||
--focus-glow: 0 0 0 2px var(--bg), 0 0 0 4px var(--ring), 0 0 20px var(--accent-glow);
|
|
||||||
|
|
||||||
/* Grid */
|
|
||||||
--grid-line: rgba(255, 255, 255, 0.04);
|
--grid-line: rgba(255, 255, 255, 0.04);
|
||||||
|
|
||||||
/* Theme transition */
|
|
||||||
--theme-switch-x: 50%;
|
--theme-switch-x: 50%;
|
||||||
--theme-switch-y: 50%;
|
--theme-switch-y: 50%;
|
||||||
|
--mono:
|
||||||
/* Typography - Space Grotesk for personality */
|
"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||||
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace;
|
"Courier New", monospace;
|
||||||
--font-body: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
--font-body: "Work Sans", system-ui, sans-serif;
|
||||||
--font-display: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
--font-display: "Unbounded", "Times New Roman", serif;
|
||||||
|
|
||||||
/* Shadows - Richer with subtle color */
|
|
||||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
||||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.03);
|
|
||||||
--shadow-lg: 0 12px 28px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.03);
|
|
||||||
--shadow-xl: 0 24px 48px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.03);
|
|
||||||
--shadow-glow: 0 0 30px var(--accent-glow);
|
|
||||||
|
|
||||||
/* Radii - Slightly larger for friendlier feel */
|
|
||||||
--radius-sm: 6px;
|
|
||||||
--radius-md: 8px;
|
|
||||||
--radius-lg: 12px;
|
|
||||||
--radius-xl: 16px;
|
|
||||||
--radius-full: 9999px;
|
|
||||||
--radius: 8px;
|
|
||||||
|
|
||||||
/* Transitions - Snappy but smooth */
|
|
||||||
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
||||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
||||||
--duration-fast: 120ms;
|
|
||||||
--duration-normal: 200ms;
|
|
||||||
--duration-slow: 350ms;
|
|
||||||
|
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light theme - Clean with subtle warmth */
|
|
||||||
:root[data-theme="light"] {
|
:root[data-theme="light"] {
|
||||||
--bg: #fafafa;
|
--bg: #f5f1ea;
|
||||||
--bg-accent: #f5f5f5;
|
--bg-accent: #ffffff;
|
||||||
--bg-elevated: #ffffff;
|
--bg-grad-1: #f1e6d6;
|
||||||
--bg-hover: #f0f0f0;
|
--bg-grad-2: #e5eef4;
|
||||||
--bg-muted: #f0f0f0;
|
--bg-overlay: rgba(28, 32, 46, 0.05);
|
||||||
--bg-content: #f5f5f5;
|
--bg-glow: rgba(52, 199, 183, 0.14);
|
||||||
|
--panel: rgba(255, 255, 255, 0.9);
|
||||||
--card: #ffffff;
|
--panel-strong: rgba(255, 255, 255, 0.97);
|
||||||
--card-foreground: #18181b;
|
--chrome: rgba(255, 255, 255, 0.75);
|
||||||
--card-highlight: rgba(0, 0, 0, 0.03);
|
--chrome-strong: rgba(255, 255, 255, 0.88);
|
||||||
--popover: #ffffff;
|
--text: rgba(27, 36, 50, 0.98);
|
||||||
--popover-foreground: #18181b;
|
--chat-text: rgba(36, 48, 66, 0.9);
|
||||||
|
--muted: rgba(80, 94, 114, 0.7);
|
||||||
--panel: #fafafa;
|
--border: rgba(18, 24, 40, 0.12);
|
||||||
--panel-strong: #f5f5f5;
|
--border-strong: rgba(18, 24, 40, 0.2);
|
||||||
--panel-hover: #ebebeb;
|
--accent: #e28a3f;
|
||||||
--chrome: rgba(250, 250, 250, 0.95);
|
--accent-2: #1ba99d;
|
||||||
--chrome-strong: rgba(250, 250, 250, 0.98);
|
--ok: #1aa86c;
|
||||||
|
--warn: #b3771c;
|
||||||
--text: #3f3f46;
|
--danger: #d44848;
|
||||||
--text-strong: #18181b;
|
--focus: rgba(226, 138, 63, 0.35);
|
||||||
--chat-text: #3f3f46;
|
--grid-line: rgba(18, 24, 40, 0.06);
|
||||||
--muted: #71717a;
|
|
||||||
--muted-strong: #52525b;
|
|
||||||
--muted-foreground: #71717a;
|
|
||||||
|
|
||||||
--border: #e4e4e7;
|
|
||||||
--border-strong: #d4d4d8;
|
|
||||||
--border-hover: #a1a1aa;
|
|
||||||
--input: #e4e4e7;
|
|
||||||
|
|
||||||
--accent: #dc2626;
|
|
||||||
--accent-hover: #ef4444;
|
|
||||||
--accent-muted: #dc2626;
|
|
||||||
--accent-subtle: rgba(220, 38, 38, 0.12);
|
|
||||||
--accent-foreground: #ffffff;
|
|
||||||
--accent-glow: rgba(220, 38, 38, 0.15);
|
|
||||||
--primary: #dc2626;
|
|
||||||
--primary-foreground: #ffffff;
|
|
||||||
|
|
||||||
--secondary: #f4f4f5;
|
|
||||||
--secondary-foreground: #3f3f46;
|
|
||||||
--accent-2: #0d9488;
|
|
||||||
--accent-2-muted: rgba(13, 148, 136, 0.75);
|
|
||||||
--accent-2-subtle: rgba(13, 148, 136, 0.12);
|
|
||||||
|
|
||||||
--ok: #16a34a;
|
|
||||||
--ok-muted: rgba(22, 163, 74, 0.75);
|
|
||||||
--ok-subtle: rgba(22, 163, 74, 0.1);
|
|
||||||
--destructive: #dc2626;
|
|
||||||
--destructive-foreground: #fafafa;
|
|
||||||
--warn: #d97706;
|
|
||||||
--warn-muted: rgba(217, 119, 6, 0.75);
|
|
||||||
--warn-subtle: rgba(217, 119, 6, 0.1);
|
|
||||||
--danger: #dc2626;
|
|
||||||
--danger-muted: rgba(220, 38, 38, 0.75);
|
|
||||||
--danger-subtle: rgba(220, 38, 38, 0.1);
|
|
||||||
--info: #2563eb;
|
|
||||||
|
|
||||||
--focus: rgba(220, 38, 38, 0.2);
|
|
||||||
--focus-glow: 0 0 0 2px var(--bg), 0 0 0 4px var(--ring), 0 0 16px var(--accent-glow);
|
|
||||||
|
|
||||||
--grid-line: rgba(0, 0, 0, 0.05);
|
|
||||||
|
|
||||||
/* Light shadows */
|
|
||||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
||||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04);
|
|
||||||
--shadow-lg: 0 12px 28px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.04);
|
|
||||||
--shadow-xl: 0 24px 48px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.04);
|
|
||||||
--shadow-glow: 0 0 24px var(--accent-glow);
|
|
||||||
|
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,19 +70,32 @@ body {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font: 400 14px/1.55 var(--font-body);
|
font: 15px/1.5 var(--font-body);
|
||||||
letter-spacing: -0.02em;
|
background:
|
||||||
background: var(--bg);
|
radial-gradient(1200px 900px at 15% -10%, var(--bg-grad-1) 0%, transparent 55%) fixed,
|
||||||
|
radial-gradient(900px 700px at 80% 10%, var(--bg-grad-2) 0%, transparent 60%) fixed,
|
||||||
|
linear-gradient(160deg, var(--bg) 0%, var(--bg-accent) 100%) fixed;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Theme transition */
|
body::before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(140deg, var(--bg-overlay) 0%, rgba(255, 255, 255, 0) 40%),
|
||||||
|
radial-gradient(620px 420px at 75% 75%, var(--bg-glow), transparent 60%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid overlay removed for cleaner look */
|
||||||
|
|
||||||
@keyframes theme-circle-transition {
|
@keyframes theme-circle-transition {
|
||||||
0% {
|
0% {
|
||||||
clip-path: circle(0% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%));
|
clip-path: circle(0% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%));
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
clip-path: circle(150% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%));
|
clip-path: circle(150% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%));
|
||||||
}
|
}
|
||||||
@ -229,7 +114,7 @@ html.theme-transition::view-transition-old(theme) {
|
|||||||
html.theme-transition::view-transition-new(theme) {
|
html.theme-transition::view-transition-new(theme) {
|
||||||
mix-blend-mode: normal;
|
mix-blend-mode: normal;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
animation: theme-circle-transition 0.4s var(--ease-out) forwards;
|
animation: theme-circle-transition 0.45s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
@ -247,12 +132,7 @@ clawdbot-app {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--accent);
|
color: inherit;
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
@ -263,35 +143,10 @@ select {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
|
||||||
background: var(--accent-subtle);
|
|
||||||
color: var(--text-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar styling */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--border);
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--border-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations - Polished with spring feel */
|
|
||||||
@keyframes rise {
|
@keyframes rise {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(8px);
|
transform: translateY(6px);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@ -299,26 +154,6 @@ select {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scale-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes dashboard-enter {
|
@keyframes dashboard-enter {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@ -329,44 +164,3 @@ select {
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
|
||||||
0% {
|
|
||||||
background-position: -200% 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 200% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-subtle {
|
|
||||||
0%, 100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glow-pulse {
|
|
||||||
0%, 100% {
|
|
||||||
box-shadow: 0 0 0 rgba(255, 92, 92, 0);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow: 0 0 20px var(--accent-glow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stagger animation delays for grouped elements */
|
|
||||||
.stagger-1 { animation-delay: 0ms; }
|
|
||||||
.stagger-2 { animation-delay: 50ms; }
|
|
||||||
.stagger-3 { animation-delay: 100ms; }
|
|
||||||
.stagger-4 { animation-delay: 150ms; }
|
|
||||||
.stagger-5 { animation-delay: 200ms; }
|
|
||||||
.stagger-6 { animation-delay: 250ms; }
|
|
||||||
|
|
||||||
/* Focus visible styles */
|
|
||||||
:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: var(--focus-ring);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -105,7 +105,9 @@ img.chat-avatar {
|
|||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
transition: background 150ms ease-out, border-color 150ms ease-out;
|
transition:
|
||||||
|
background 150ms ease-out,
|
||||||
|
border-color 150ms ease-out;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
@ -128,7 +130,9 @@ img.chat-avatar {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: opacity 120ms ease-out, background 120ms ease-out;
|
transition:
|
||||||
|
opacity 120ms ease-out,
|
||||||
|
background 120ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-copy-btn__icon {
|
.chat-copy-btn__icon {
|
||||||
@ -243,7 +247,8 @@ img.chat-avatar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulsing-border {
|
@keyframes pulsing-border {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
border-color: var(--border);
|
border-color: var(--border);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
|
|||||||
@ -77,7 +77,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: background 150ms ease-out, color 150ms ease-out, border-color 150ms ease-out;
|
transition:
|
||||||
|
background 150ms ease-out,
|
||||||
|
color 150ms ease-out,
|
||||||
|
border-color 150ms ease-out;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,17 +4,17 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
background: var(--card);
|
transition:
|
||||||
box-shadow: inset 0 1px 0 var(--card-highlight);
|
border-color 150ms ease-out,
|
||||||
transition: border-color 150ms ease-out, background 150ms ease-out;
|
background 150ms ease-out;
|
||||||
/* Fixed max-height to ensure cards don't expand too much */
|
/* Fixed max-height to ensure cards don't expand too much */
|
||||||
max-height: 120px;
|
max-height: 120px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-tool-card:hover {
|
.chat-tool-card:hover {
|
||||||
border-color: var(--border-strong);
|
border-color: var(--accent);
|
||||||
background: var(--bg-hover);
|
background: rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* First tool card in a group - no top margin */
|
/* First tool card in a group - no top margin */
|
||||||
@ -52,63 +52,33 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 16px;
|
width: 18px;
|
||||||
height: 16px;
|
height: 18px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||||
|
vertical-align: middle;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-tool-card__icon svg {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
stroke: currentColor;
|
|
||||||
fill: none;
|
|
||||||
stroke-width: 1.5px;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* "View >" action link */
|
/* "View >" action link */
|
||||||
.chat-tool-card__action {
|
.chat-tool-card__action {
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transition: opacity 150ms ease-out;
|
transition: opacity 150ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-tool-card__action svg {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
stroke: currentColor;
|
|
||||||
fill: none;
|
|
||||||
stroke-width: 1.5px;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-tool-card--clickable:hover .chat-tool-card__action {
|
.chat-tool-card--clickable:hover .chat-tool-card__action {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Status indicator for completed/empty results */
|
/* Status indicator for completed/empty results */
|
||||||
.chat-tool-card__status {
|
.chat-tool-card__status {
|
||||||
display: inline-flex;
|
font-size: 14px;
|
||||||
align-items: center;
|
|
||||||
color: var(--ok);
|
color: var(--ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-tool-card__status svg {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
stroke: currentColor;
|
|
||||||
fill: none;
|
|
||||||
stroke-width: 2px;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-tool-card__status-text {
|
.chat-tool-card__status-text {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
@ -126,18 +96,18 @@
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
background: var(--secondary);
|
background: rgba(0, 0, 0, 0.08);
|
||||||
border-radius: var(--radius-md);
|
border-radius: 6px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: 44px;
|
max-height: 44px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-tool-card--clickable:hover .chat-tool-card__preview {
|
.chat-tool-card--clickable:hover .chat-tool-card__preview {
|
||||||
background: var(--bg-hover);
|
background: rgba(0, 0, 0, 0.12);
|
||||||
border-color: var(--border-strong);
|
border-color: rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Short inline output */
|
/* Short inline output */
|
||||||
@ -146,8 +116,8 @@
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
background: var(--secondary);
|
background: rgba(0, 0, 0, 0.06);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: 4px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
@ -187,7 +157,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes reading-pulse {
|
@keyframes reading-pulse {
|
||||||
0%, 60%, 100% {
|
0%,
|
||||||
|
60%,
|
||||||
|
100% {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
transform: scale(0.8);
|
transform: scale(0.8);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,14 +1,10 @@
|
|||||||
/* ===========================================
|
|
||||||
Shell Layout
|
|
||||||
=========================================== */
|
|
||||||
|
|
||||||
.shell {
|
.shell {
|
||||||
--shell-pad: 16px;
|
--shell-pad: 16px;
|
||||||
--shell-gap: 16px;
|
--shell-gap: 16px;
|
||||||
--shell-nav-width: 220px;
|
--shell-nav-width: 220px;
|
||||||
--shell-topbar-height: 56px;
|
--shell-topbar-height: 56px;
|
||||||
--shell-focus-duration: 200ms;
|
--shell-focus-duration: 220ms;
|
||||||
--shell-focus-ease: var(--ease-out);
|
--shell-focus-ease: cubic-bezier(0.2, 0.85, 0.25, 1);
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--shell-nav-width) minmax(0, 1fr);
|
grid-template-columns: var(--shell-nav-width) minmax(0, 1fr);
|
||||||
@ -17,7 +13,7 @@
|
|||||||
"topbar topbar"
|
"topbar topbar"
|
||||||
"nav content";
|
"nav content";
|
||||||
gap: 0;
|
gap: 0;
|
||||||
animation: dashboard-enter 0.4s var(--ease-out);
|
animation: dashboard-enter 0.6s ease-out;
|
||||||
transition: grid-template-columns var(--shell-focus-duration) var(--shell-focus-ease);
|
transition: grid-template-columns var(--shell-focus-duration) var(--shell-focus-ease);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@ -65,10 +61,6 @@
|
|||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================
|
|
||||||
Topbar
|
|
||||||
=========================================== */
|
|
||||||
|
|
||||||
.topbar {
|
.topbar {
|
||||||
grid-area: topbar;
|
grid-area: topbar;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
@ -81,7 +73,8 @@
|
|||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
height: var(--shell-topbar-height);
|
height: var(--shell-topbar-height);
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
background: var(--bg);
|
background: var(--panel);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar-left {
|
.topbar-left {
|
||||||
@ -91,84 +84,49 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.topbar .nav-collapse-toggle {
|
.topbar .nav-collapse-toggle {
|
||||||
width: 36px;
|
width: 44px;
|
||||||
height: 36px;
|
height: 44px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar .nav-collapse-toggle__icon {
|
.topbar .nav-collapse-toggle__icon {
|
||||||
width: 20px;
|
font-size: 22px;
|
||||||
height: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar .nav-collapse-toggle__icon svg {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Brand */
|
|
||||||
.brand {
|
.brand {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-logo {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-logo img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-text {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-title {
|
.brand-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
letter-spacing: 1px;
|
||||||
letter-spacing: -0.03em;
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
color: var(--text-strong);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-sub {
|
.brand-sub {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 500;
|
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.8px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Topbar status */
|
|
||||||
.topbar-status {
|
.topbar-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Smaller pill and theme toggle in topbar */
|
||||||
.topbar-status .pill {
|
.topbar-status .pill {
|
||||||
padding: 6px 10px;
|
padding: 4px 10px;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
|
||||||
height: 32px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar-status .pill .mono {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
line-height: 1;
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar-status .statusDot {
|
.topbar-status .statusDot {
|
||||||
@ -177,9 +135,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.topbar-status .theme-toggle {
|
.topbar-status .theme-toggle {
|
||||||
--theme-item: 24px;
|
--theme-item: 22px;
|
||||||
--theme-gap: 2px;
|
--theme-gap: 4px;
|
||||||
--theme-pad: 3px;
|
--theme-pad: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar-status .theme-icon {
|
.topbar-status .theme-icon {
|
||||||
@ -187,26 +145,18 @@
|
|||||||
height: 12px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================
|
|
||||||
Navigation Sidebar
|
|
||||||
=========================================== */
|
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
grid-area: nav;
|
grid-area: nav;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding: 16px 12px;
|
padding: 16px;
|
||||||
background: var(--bg);
|
border-right: 1px solid var(--border);
|
||||||
scrollbar-width: none; /* Firefox */
|
background: var(--panel);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
transition:
|
transition:
|
||||||
width var(--shell-focus-duration) var(--shell-focus-ease),
|
width var(--shell-focus-duration) var(--shell-focus-ease),
|
||||||
padding var(--shell-focus-duration) var(--shell-focus-ease),
|
padding var(--shell-focus-duration) var(--shell-focus-ease);
|
||||||
opacity var(--shell-focus-duration) var(--shell-focus-ease);
|
min-height: 0; /* Allow grid item to shrink and enable scrolling */
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav::-webkit-scrollbar {
|
|
||||||
display: none; /* Chrome/Safari */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell--chat-focus .nav {
|
.shell--chat-focus .nav {
|
||||||
@ -215,9 +165,9 @@
|
|||||||
border-width: 0;
|
border-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Collapsed nav sidebar - completely hidden */
|
||||||
.nav--collapsed {
|
.nav--collapsed {
|
||||||
width: 0;
|
width: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@ -228,7 +178,7 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Nav collapse toggle */
|
/* Nav collapse toggle button */
|
||||||
.nav-collapse-toggle {
|
.nav-collapse-toggle {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@ -237,88 +187,73 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: var(--radius-md);
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
background var(--duration-fast) ease,
|
background 150ms ease,
|
||||||
border-color var(--duration-fast) ease;
|
border-color 150ms ease;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-collapse-toggle:hover {
|
.nav-collapse-toggle:hover {
|
||||||
background: var(--bg-hover);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
border-color: var(--border);
|
border-color: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] .nav-collapse-toggle:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
.nav-collapse-toggle__icon {
|
.nav-collapse-toggle__icon {
|
||||||
display: flex;
|
font-size: 16px;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
transition: color var(--duration-fast) ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-collapse-toggle__icon svg {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
stroke: currentColor;
|
|
||||||
fill: none;
|
|
||||||
stroke-width: 1.5px;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-collapse-toggle:hover .nav-collapse-toggle__icon {
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Nav groups */
|
|
||||||
.nav-group {
|
.nav-group {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 18px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 2px;
|
gap: 6px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px dashed rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-group:last-child {
|
.nav-group:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-group__items {
|
.nav-group__items {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-group--collapsed .nav-group__items {
|
.nav-group--collapsed .nav-group__items {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Nav label */
|
|
||||||
.nav-label {
|
.nav-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 6px 10px;
|
padding: 4px 0;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--muted);
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.4px;
|
||||||
|
color: var(--text);
|
||||||
|
opacity: 0.7;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
transition:
|
|
||||||
color var(--duration-fast) ease,
|
|
||||||
background var(--duration-fast) ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-label:hover {
|
.nav-label:hover {
|
||||||
color: var(--text);
|
opacity: 1;
|
||||||
background: var(--bg-hover);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-label--static {
|
.nav-label--static {
|
||||||
@ -326,8 +261,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-label--static:hover {
|
.nav-label--static:hover {
|
||||||
color: var(--muted);
|
opacity: 0.7;
|
||||||
background: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-label__text {
|
.nav-label__text {
|
||||||
@ -335,113 +269,99 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-label__chevron {
|
.nav-label__chevron {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
opacity: 0.5;
|
opacity: 0.6;
|
||||||
transition: transform var(--duration-fast) ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-group--collapsed .nav-label__chevron {
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Nav items */
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
padding: 8px 10px;
|
padding: 10px 12px 10px 14px;
|
||||||
border-radius: var(--radius-md);
|
border-radius: 12px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition:
|
transition:
|
||||||
border-color var(--duration-fast) ease,
|
border-color 160ms ease,
|
||||||
background var(--duration-fast) ease,
|
background 160ms ease,
|
||||||
color var(--duration-fast) ease;
|
color 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item__icon {
|
.nav-item__icon {
|
||||||
width: 16px;
|
font-size: 16px;
|
||||||
height: 16px;
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
opacity: 0.7;
|
|
||||||
transition: opacity var(--duration-fast) ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item__icon svg {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
stroke: currentColor;
|
|
||||||
fill: none;
|
|
||||||
stroke-width: 1.5px;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item__text {
|
.nav-item__text {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item:hover {
|
.nav-item:hover {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: var(--bg-hover);
|
border-color: rgba(255, 255, 255, 0.12);
|
||||||
text-decoration: none;
|
background: rgba(255, 255, 255, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item:hover .nav-item__icon {
|
.nav-item::before {
|
||||||
opacity: 1;
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
width: 4px;
|
||||||
|
height: 60%;
|
||||||
|
border-radius: 0 999px 999px 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item.active {
|
.nav-item.active {
|
||||||
color: var(--text-strong);
|
color: var(--text);
|
||||||
background: var(--accent-subtle);
|
border-color: rgba(245, 159, 74, 0.45);
|
||||||
|
background: rgba(245, 159, 74, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item.active .nav-item__icon {
|
.nav-item.active::before {
|
||||||
opacity: 1;
|
background: var(--accent);
|
||||||
color: var(--accent);
|
box-shadow: 0 0 12px rgba(245, 159, 74, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================
|
|
||||||
Content Area
|
|
||||||
=========================================== */
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
padding: 12px 16px 32px;
|
padding: 8px 6px 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24px;
|
gap: 20px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto; /* Enable vertical scrolling for pages with long content */
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="light"] .content {
|
/* Chat handles its own scrolling (chat-thread); avoid double scrollbars. */
|
||||||
background: var(--bg-content);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content--chat {
|
.content--chat {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Content header */
|
.shell--chat .content {
|
||||||
|
/* No-op: keep chat layout consistent with other tabs */
|
||||||
|
}
|
||||||
|
|
||||||
.content-header {
|
.content-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
padding: 4px 8px;
|
padding: 0 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transform-origin: top center;
|
transform-origin: top center;
|
||||||
transition:
|
transition:
|
||||||
@ -449,39 +369,35 @@
|
|||||||
transform var(--shell-focus-duration) var(--shell-focus-ease),
|
transform var(--shell-focus-duration) var(--shell-focus-ease),
|
||||||
max-height var(--shell-focus-duration) var(--shell-focus-ease),
|
max-height var(--shell-focus-duration) var(--shell-focus-ease),
|
||||||
padding var(--shell-focus-duration) var(--shell-focus-ease);
|
padding var(--shell-focus-duration) var(--shell-focus-ease);
|
||||||
max-height: 80px;
|
max-height: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell--chat-focus .content-header {
|
.shell--chat-focus .content-header {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-8px);
|
transform: translateY(-10px);
|
||||||
max-height: 0px;
|
max-height: 0px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
font-weight: 700;
|
letter-spacing: 0.6px;
|
||||||
letter-spacing: -0.035em;
|
|
||||||
line-height: 1.15;
|
|
||||||
color: var(--text-strong);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-sub {
|
.page-sub {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
font-weight: 400;
|
letter-spacing: 0.4px;
|
||||||
margin-top: 6px;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-meta {
|
.page-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chat view header adjustments */
|
/* Chat view: header and controls side by side */
|
||||||
.content--chat .content-header {
|
.content--chat .content-header {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -501,13 +417,9 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================
|
|
||||||
Grid Utilities
|
|
||||||
=========================================== */
|
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 20px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-cols-2 {
|
.grid-cols-2 {
|
||||||
@ -521,13 +433,13 @@
|
|||||||
.stat-grid {
|
.stat-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-grid {
|
.note-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 14px;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
@ -538,25 +450,21 @@
|
|||||||
|
|
||||||
.stack {
|
.stack {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 14px;
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters {
|
.filters {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================
|
|
||||||
Responsive - Tablet
|
|
||||||
=========================================== */
|
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.shell {
|
.shell {
|
||||||
--shell-pad: 12px;
|
--shell-pad: 12px;
|
||||||
--shell-gap: 12px;
|
--shell-gap: 12px;
|
||||||
|
--shell-nav-col: 1fr;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: auto auto 1fr;
|
grid-template-rows: auto auto 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
@ -569,18 +477,17 @@
|
|||||||
position: static;
|
position: static;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 16px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-bottom: 1px solid var(--border);
|
padding: 12px;
|
||||||
padding: 10px 14px;
|
|
||||||
background: var(--bg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-group {
|
.nav-group {
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
margin-bottom: 0;
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-cols-2,
|
.grid-cols-2,
|
||||||
@ -590,11 +497,13 @@
|
|||||||
|
|
||||||
.topbar {
|
.topbar {
|
||||||
position: static;
|
position: static;
|
||||||
padding: 12px 14px;
|
flex-direction: column;
|
||||||
gap: 10px;
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar-status {
|
.topbar-status {
|
||||||
|
width: 100%;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -122,9 +122,12 @@ export async function handleNostrProfileSave(host: ClawdbotApp) {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(state.values),
|
body: JSON.stringify(state.values),
|
||||||
});
|
});
|
||||||
const data = (await response.json().catch(() => null)) as
|
const data = (await response.json().catch(() => null)) as {
|
||||||
| { ok?: boolean; error?: string; details?: unknown; persisted?: boolean }
|
ok?: boolean;
|
||||||
| null;
|
error?: string;
|
||||||
|
details?: unknown;
|
||||||
|
persisted?: boolean;
|
||||||
|
} | null;
|
||||||
|
|
||||||
if (!response.ok || data?.ok === false || !data) {
|
if (!response.ok || data?.ok === false || !data) {
|
||||||
const errorMessage = data?.error ?? `Profile update failed (${response.status})`;
|
const errorMessage = data?.error ?? `Profile update failed (${response.status})`;
|
||||||
@ -187,9 +190,13 @@ export async function handleNostrProfileImport(host: ClawdbotApp) {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({ autoMerge: true }),
|
body: JSON.stringify({ autoMerge: true }),
|
||||||
});
|
});
|
||||||
const data = (await response.json().catch(() => null)) as
|
const data = (await response.json().catch(() => null)) as {
|
||||||
| { ok?: boolean; error?: string; imported?: NostrProfile; merged?: NostrProfile; saved?: boolean }
|
ok?: boolean;
|
||||||
| null;
|
error?: string;
|
||||||
|
imported?: NostrProfile;
|
||||||
|
merged?: NostrProfile;
|
||||||
|
saved?: boolean;
|
||||||
|
} | null;
|
||||||
|
|
||||||
if (!response.ok || data?.ok === false || !data) {
|
if (!response.ok || data?.ok === false || !data) {
|
||||||
const errorMessage = data?.error ?? `Profile import failed (${response.status})`;
|
const errorMessage = data?.error ?? `Profile import failed (${response.status})`;
|
||||||
|
|||||||
@ -82,7 +82,10 @@ async function sendChatMessageNow(
|
|||||||
host.chatAttachments = opts.previousAttachments;
|
host.chatAttachments = opts.previousAttachments;
|
||||||
}
|
}
|
||||||
if (ok) {
|
if (ok) {
|
||||||
setLastActiveSessionKey(host as unknown as Parameters<typeof setLastActiveSessionKey>[0], host.sessionKey);
|
setLastActiveSessionKey(
|
||||||
|
host as unknown as Parameters<typeof setLastActiveSessionKey>[0],
|
||||||
|
host.sessionKey,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (ok && opts?.restoreDraft && opts.previousDraft?.trim()) {
|
if (ok && opts?.restoreDraft && opts.previousDraft?.trim()) {
|
||||||
host.chatMessage = opts.previousDraft;
|
host.chatMessage = opts.previousDraft;
|
||||||
@ -170,7 +173,9 @@ type SessionDefaultsSnapshot = {
|
|||||||
function resolveAgentIdForSession(host: ChatHost): string | null {
|
function resolveAgentIdForSession(host: ChatHost): string | null {
|
||||||
const parsed = parseAgentSessionKey(host.sessionKey);
|
const parsed = parseAgentSessionKey(host.sessionKey);
|
||||||
if (parsed?.agentId) return parsed.agentId;
|
if (parsed?.agentId) return parsed.agentId;
|
||||||
const snapshot = host.hello?.snapshot as { sessionDefaults?: SessionDefaultsSnapshot } | undefined;
|
const snapshot = host.hello?.snapshot as
|
||||||
|
| { sessionDefaults?: SessionDefaultsSnapshot }
|
||||||
|
| undefined;
|
||||||
const fallback = snapshot?.sessionDefaults?.defaultAgentId?.trim();
|
const fallback = snapshot?.sessionDefaults?.defaultAgentId?.trim();
|
||||||
return fallback || "main";
|
return fallback || "main";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,12 +10,7 @@ import type { Tab } from "./navigation";
|
|||||||
import type { UiSettings } from "./storage";
|
import type { UiSettings } from "./storage";
|
||||||
import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream";
|
import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream";
|
||||||
import { flushChatQueueForEvent } from "./app-chat";
|
import { flushChatQueueForEvent } from "./app-chat";
|
||||||
import {
|
import { applySettings, loadCron, refreshActiveTab, setLastActiveSessionKey } from "./app-settings";
|
||||||
applySettings,
|
|
||||||
loadCron,
|
|
||||||
refreshActiveTab,
|
|
||||||
setLastActiveSessionKey,
|
|
||||||
} from "./app-settings";
|
|
||||||
import { handleChatEvent, type ChatEventPayload } from "./controllers/chat";
|
import { handleChatEvent, type ChatEventPayload } from "./controllers/chat";
|
||||||
import {
|
import {
|
||||||
addExecApproval,
|
addExecApproval,
|
||||||
@ -75,8 +70,7 @@ function normalizeSessionKeyForDefaults(
|
|||||||
raw === "main" ||
|
raw === "main" ||
|
||||||
raw === mainKey ||
|
raw === mainKey ||
|
||||||
(defaultAgentId &&
|
(defaultAgentId &&
|
||||||
(raw === `agent:${defaultAgentId}:main` ||
|
(raw === `agent:${defaultAgentId}:main` || raw === `agent:${defaultAgentId}:${mainKey}`));
|
||||||
raw === `agent:${defaultAgentId}:${mainKey}`));
|
|
||||||
return isAlias ? mainSessionKey : raw;
|
return isAlias ? mainSessionKey : raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,9 +179,7 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) {
|
|||||||
const state = handleChatEvent(host as unknown as ClawdbotApp, payload);
|
const state = handleChatEvent(host as unknown as ClawdbotApp, payload);
|
||||||
if (state === "final" || state === "error" || state === "aborted") {
|
if (state === "final" || state === "error" || state === "aborted") {
|
||||||
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
|
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
|
||||||
void flushChatQueueForEvent(
|
void flushChatQueueForEvent(host as unknown as Parameters<typeof flushChatQueueForEvent>[0]);
|
||||||
host as unknown as Parameters<typeof flushChatQueueForEvent>[0],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (state === "final") void loadChatHistory(host as unknown as ClawdbotApp);
|
if (state === "final") void loadChatHistory(host as unknown as ClawdbotApp);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -35,20 +35,11 @@ type LifecycleHost = {
|
|||||||
|
|
||||||
export function handleConnected(host: LifecycleHost) {
|
export function handleConnected(host: LifecycleHost) {
|
||||||
host.basePath = inferBasePath();
|
host.basePath = inferBasePath();
|
||||||
syncTabWithLocation(
|
syncTabWithLocation(host as unknown as Parameters<typeof syncTabWithLocation>[0], true);
|
||||||
host as unknown as Parameters<typeof syncTabWithLocation>[0],
|
syncThemeWithSettings(host as unknown as Parameters<typeof syncThemeWithSettings>[0]);
|
||||||
true,
|
attachThemeListener(host as unknown as Parameters<typeof attachThemeListener>[0]);
|
||||||
);
|
|
||||||
syncThemeWithSettings(
|
|
||||||
host as unknown as Parameters<typeof syncThemeWithSettings>[0],
|
|
||||||
);
|
|
||||||
attachThemeListener(
|
|
||||||
host as unknown as Parameters<typeof attachThemeListener>[0],
|
|
||||||
);
|
|
||||||
window.addEventListener("popstate", host.popStateHandler);
|
window.addEventListener("popstate", host.popStateHandler);
|
||||||
applySettingsFromUrl(
|
applySettingsFromUrl(host as unknown as Parameters<typeof applySettingsFromUrl>[0]);
|
||||||
host as unknown as Parameters<typeof applySettingsFromUrl>[0],
|
|
||||||
);
|
|
||||||
connectGateway(host as unknown as Parameters<typeof connectGateway>[0]);
|
connectGateway(host as unknown as Parameters<typeof connectGateway>[0]);
|
||||||
startNodesPolling(host as unknown as Parameters<typeof startNodesPolling>[0]);
|
startNodesPolling(host as unknown as Parameters<typeof startNodesPolling>[0]);
|
||||||
if (host.tab === "logs") {
|
if (host.tab === "logs") {
|
||||||
@ -68,17 +59,12 @@ export function handleDisconnected(host: LifecycleHost) {
|
|||||||
stopNodesPolling(host as unknown as Parameters<typeof stopNodesPolling>[0]);
|
stopNodesPolling(host as unknown as Parameters<typeof stopNodesPolling>[0]);
|
||||||
stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);
|
stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);
|
||||||
stopDebugPolling(host as unknown as Parameters<typeof stopDebugPolling>[0]);
|
stopDebugPolling(host as unknown as Parameters<typeof stopDebugPolling>[0]);
|
||||||
detachThemeListener(
|
detachThemeListener(host as unknown as Parameters<typeof detachThemeListener>[0]);
|
||||||
host as unknown as Parameters<typeof detachThemeListener>[0],
|
|
||||||
);
|
|
||||||
host.topbarObserver?.disconnect();
|
host.topbarObserver?.disconnect();
|
||||||
host.topbarObserver = null;
|
host.topbarObserver = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleUpdated(
|
export function handleUpdated(host: LifecycleHost, changed: Map<PropertyKey, unknown>) {
|
||||||
host: LifecycleHost,
|
|
||||||
changed: Map<PropertyKey, unknown>,
|
|
||||||
) {
|
|
||||||
if (
|
if (
|
||||||
host.tab === "chat" &&
|
host.tab === "chat" &&
|
||||||
(changed.has("chatMessages") ||
|
(changed.has("chatMessages") ||
|
||||||
|
|||||||
@ -45,8 +45,39 @@ export function renderChatControls(state: AppViewState) {
|
|||||||
const showThinking = state.onboarding ? false : state.settings.chatShowThinking;
|
const showThinking = state.onboarding ? false : state.settings.chatShowThinking;
|
||||||
const focusActive = state.onboarding ? true : state.settings.chatFocusMode;
|
const focusActive = state.onboarding ? true : state.settings.chatFocusMode;
|
||||||
// Refresh icon
|
// Refresh icon
|
||||||
const refreshIcon = html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path><path d="M21 3v5h-5"></path></svg>`;
|
const refreshIcon = html`
|
||||||
const focusIcon = html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V4h3"></path><path d="M20 7V4h-3"></path><path d="M4 17v3h3"></path><path d="M20 17v3h-3"></path><circle cx="12" cy="12" r="3"></circle></svg>`;
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
|
||||||
|
<path d="M21 3v5h-5"></path>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
const focusIcon = html`
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M4 7V4h3"></path>
|
||||||
|
<path d="M20 7V4h-3"></path>
|
||||||
|
<path d="M4 17v3h3"></path>
|
||||||
|
<path d="M20 17v3h-3"></path>
|
||||||
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
return html`
|
return html`
|
||||||
<div class="chat-controls">
|
<div class="chat-controls">
|
||||||
<label class="field chat-controls__session">
|
<label class="field chat-controls__session">
|
||||||
@ -105,9 +136,11 @@ export function renderChatControls(state: AppViewState) {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
aria-pressed=${showThinking}
|
aria-pressed=${showThinking}
|
||||||
title=${disableThinkingToggle
|
title=${
|
||||||
|
disableThinkingToggle
|
||||||
? "Disabled during onboarding"
|
? "Disabled during onboarding"
|
||||||
: "Toggle assistant thinking/working output"}
|
: "Toggle assistant thinking/working output"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
${icons.brain}
|
${icons.brain}
|
||||||
</button>
|
</button>
|
||||||
@ -122,9 +155,11 @@ export function renderChatControls(state: AppViewState) {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
aria-pressed=${focusActive}
|
aria-pressed=${focusActive}
|
||||||
title=${disableFocusToggle
|
title=${
|
||||||
|
disableFocusToggle
|
||||||
? "Disabled during onboarding"
|
? "Disabled during onboarding"
|
||||||
: "Toggle focus mode (hide sidebar + page header)"}
|
: "Toggle focus mode (hide sidebar + page header)"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
${focusIcon}
|
${focusIcon}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,35 +1,9 @@
|
|||||||
import { html, nothing } from "lit";
|
import { html, nothing } from "lit";
|
||||||
|
|
||||||
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway";
|
|
||||||
import type { AppViewState } from "./app-view-state";
|
import type { AppViewState } from "./app-view-state";
|
||||||
import { parseAgentSessionKey } from "../../../src/routing/session-key.js";
|
import { parseAgentSessionKey } from "../../../src/routing/session-key.js";
|
||||||
import {
|
import { TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation";
|
||||||
TAB_GROUPS,
|
|
||||||
iconForTab,
|
|
||||||
pathForTab,
|
|
||||||
subtitleForTab,
|
|
||||||
titleForTab,
|
|
||||||
type Tab,
|
|
||||||
} from "./navigation";
|
|
||||||
import { icons } from "./icons";
|
import { icons } from "./icons";
|
||||||
import type { UiSettings } from "./storage";
|
|
||||||
import type { ThemeMode } from "./theme";
|
|
||||||
import type { ThemeTransitionContext } from "./theme-transition";
|
|
||||||
import type {
|
|
||||||
ConfigSnapshot,
|
|
||||||
CronJob,
|
|
||||||
CronRunLogEntry,
|
|
||||||
CronStatus,
|
|
||||||
HealthSnapshot,
|
|
||||||
LogEntry,
|
|
||||||
LogLevel,
|
|
||||||
PresenceEntry,
|
|
||||||
ChannelsStatusSnapshot,
|
|
||||||
SessionsListResult,
|
|
||||||
SkillStatusReport,
|
|
||||||
StatusSummary,
|
|
||||||
} from "./types";
|
|
||||||
import type { ChatQueueItem, CronFormState } from "./ui-types";
|
|
||||||
import { refreshChatAvatar } from "./app-chat";
|
import { refreshChatAvatar } from "./app-chat";
|
||||||
import { renderChat } from "./views/chat";
|
import { renderChat } from "./views/chat";
|
||||||
import { renderConfig } from "./views/config";
|
import { renderConfig } from "./views/config";
|
||||||
@ -60,7 +34,6 @@ import {
|
|||||||
saveSkillApiKey,
|
saveSkillApiKey,
|
||||||
updateSkillEdit,
|
updateSkillEdit,
|
||||||
updateSkillEnabled,
|
updateSkillEnabled,
|
||||||
type SkillMessage,
|
|
||||||
} from "./controllers/skills";
|
} from "./controllers/skills";
|
||||||
import { loadNodes } from "./controllers/nodes";
|
import { loadNodes } from "./controllers/nodes";
|
||||||
import { loadChatHistory } from "./controllers/chat";
|
import { loadChatHistory } from "./controllers/chat";
|
||||||
@ -78,7 +51,13 @@ import {
|
|||||||
saveExecApprovals,
|
saveExecApprovals,
|
||||||
updateExecApprovalsFormValue,
|
updateExecApprovalsFormValue,
|
||||||
} from "./controllers/exec-approvals";
|
} from "./controllers/exec-approvals";
|
||||||
import { loadCronRuns, toggleCronJob, runCronJob, removeCronJob, addCronJob } from "./controllers/cron";
|
import {
|
||||||
|
loadCronRuns,
|
||||||
|
toggleCronJob,
|
||||||
|
runCronJob,
|
||||||
|
removeCronJob,
|
||||||
|
addCronJob,
|
||||||
|
} from "./controllers/cron";
|
||||||
import { loadDebug, callDebugMethod } from "./controllers/debug";
|
import { loadDebug, callDebugMethod } from "./controllers/debug";
|
||||||
import { loadLogs } from "./controllers/logs";
|
import { loadLogs } from "./controllers/logs";
|
||||||
|
|
||||||
@ -88,10 +67,7 @@ const AVATAR_HTTP_RE = /^https?:\/\//i;
|
|||||||
function resolveAssistantAvatarUrl(state: AppViewState): string | undefined {
|
function resolveAssistantAvatarUrl(state: AppViewState): string | undefined {
|
||||||
const list = state.agentsList?.agents ?? [];
|
const list = state.agentsList?.agents ?? [];
|
||||||
const parsed = parseAgentSessionKey(state.sessionKey);
|
const parsed = parseAgentSessionKey(state.sessionKey);
|
||||||
const agentId =
|
const agentId = parsed?.agentId ?? state.agentsList?.defaultId ?? "main";
|
||||||
parsed?.agentId ??
|
|
||||||
state.agentsList?.defaultId ??
|
|
||||||
"main";
|
|
||||||
const agent = list.find((entry) => entry.id === agentId);
|
const agent = list.find((entry) => entry.id === agentId);
|
||||||
const identity = agent?.identity;
|
const identity = agent?.identity;
|
||||||
const candidate = identity?.avatarUrl ?? identity?.avatar;
|
const candidate = identity?.avatarUrl ?? identity?.avatar;
|
||||||
@ -198,14 +174,13 @@ export function renderApp(state: AppViewState) {
|
|||||||
<div class="page-sub">${subtitleForTab(state.tab)}</div>
|
<div class="page-sub">${subtitleForTab(state.tab)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="page-meta">
|
<div class="page-meta">
|
||||||
${state.lastError
|
${state.lastError ? html`<div class="pill danger">${state.lastError}</div>` : nothing}
|
||||||
? html`<div class="pill danger">${state.lastError}</div>`
|
|
||||||
: nothing}
|
|
||||||
${isChat ? renderChatControls(state) : nothing}
|
${isChat ? renderChatControls(state) : nothing}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
${state.tab === "overview"
|
${
|
||||||
|
state.tab === "overview"
|
||||||
? renderOverview({
|
? renderOverview({
|
||||||
connected: state.connected,
|
connected: state.connected,
|
||||||
hello: state.hello,
|
hello: state.hello,
|
||||||
@ -233,9 +208,11 @@ export function renderApp(state: AppViewState) {
|
|||||||
onConnect: () => state.connect(),
|
onConnect: () => state.connect(),
|
||||||
onRefresh: () => state.loadOverview(),
|
onRefresh: () => state.loadOverview(),
|
||||||
})
|
})
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${state.tab === "channels"
|
${
|
||||||
|
state.tab === "channels"
|
||||||
? renderChannels({
|
? renderChannels({
|
||||||
connected: state.connected,
|
connected: state.connected,
|
||||||
loading: state.channelsLoading,
|
loading: state.channelsLoading,
|
||||||
@ -270,9 +247,11 @@ export function renderApp(state: AppViewState) {
|
|||||||
onNostrProfileImport: () => state.handleNostrProfileImport(),
|
onNostrProfileImport: () => state.handleNostrProfileImport(),
|
||||||
onNostrProfileToggleAdvanced: () => state.handleNostrProfileToggleAdvanced(),
|
onNostrProfileToggleAdvanced: () => state.handleNostrProfileToggleAdvanced(),
|
||||||
})
|
})
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${state.tab === "instances"
|
${
|
||||||
|
state.tab === "instances"
|
||||||
? renderInstances({
|
? renderInstances({
|
||||||
loading: state.presenceLoading,
|
loading: state.presenceLoading,
|
||||||
entries: state.presenceEntries,
|
entries: state.presenceEntries,
|
||||||
@ -280,9 +259,11 @@ export function renderApp(state: AppViewState) {
|
|||||||
statusMessage: state.presenceStatus,
|
statusMessage: state.presenceStatus,
|
||||||
onRefresh: () => loadPresence(state),
|
onRefresh: () => loadPresence(state),
|
||||||
})
|
})
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${state.tab === "sessions"
|
${
|
||||||
|
state.tab === "sessions"
|
||||||
? renderSessions({
|
? renderSessions({
|
||||||
loading: state.sessionsLoading,
|
loading: state.sessionsLoading,
|
||||||
result: state.sessionsResult,
|
result: state.sessionsResult,
|
||||||
@ -302,9 +283,11 @@ export function renderApp(state: AppViewState) {
|
|||||||
onPatch: (key, patch) => patchSession(state, key, patch),
|
onPatch: (key, patch) => patchSession(state, key, patch),
|
||||||
onDelete: (key) => deleteSession(state, key),
|
onDelete: (key) => deleteSession(state, key),
|
||||||
})
|
})
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${state.tab === "cron"
|
${
|
||||||
|
state.tab === "cron"
|
||||||
? renderCron({
|
? renderCron({
|
||||||
loading: state.cronLoading,
|
loading: state.cronLoading,
|
||||||
status: state.cronStatus,
|
status: state.cronStatus,
|
||||||
@ -314,7 +297,7 @@ export function renderApp(state: AppViewState) {
|
|||||||
form: state.cronForm,
|
form: state.cronForm,
|
||||||
channels: state.channelsSnapshot?.channelMeta?.length
|
channels: state.channelsSnapshot?.channelMeta?.length
|
||||||
? state.channelsSnapshot.channelMeta.map((entry) => entry.id)
|
? state.channelsSnapshot.channelMeta.map((entry) => entry.id)
|
||||||
: state.channelsSnapshot?.channelOrder ?? [],
|
: (state.channelsSnapshot?.channelOrder ?? []),
|
||||||
channelLabels: state.channelsSnapshot?.channelLabels ?? {},
|
channelLabels: state.channelsSnapshot?.channelLabels ?? {},
|
||||||
channelMeta: state.channelsSnapshot?.channelMeta ?? [],
|
channelMeta: state.channelsSnapshot?.channelMeta ?? [],
|
||||||
runsJobId: state.cronRunsJobId,
|
runsJobId: state.cronRunsJobId,
|
||||||
@ -327,9 +310,11 @@ export function renderApp(state: AppViewState) {
|
|||||||
onRemove: (job) => removeCronJob(state, job),
|
onRemove: (job) => removeCronJob(state, job),
|
||||||
onLoadRuns: (jobId) => loadCronRuns(state, jobId),
|
onLoadRuns: (jobId) => loadCronRuns(state, jobId),
|
||||||
})
|
})
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${state.tab === "skills"
|
${
|
||||||
|
state.tab === "skills"
|
||||||
? renderSkills({
|
? renderSkills({
|
||||||
loading: state.skillsLoading,
|
loading: state.skillsLoading,
|
||||||
report: state.skillsReport,
|
report: state.skillsReport,
|
||||||
@ -346,16 +331,20 @@ export function renderApp(state: AppViewState) {
|
|||||||
onInstall: (skillKey, name, installId) =>
|
onInstall: (skillKey, name, installId) =>
|
||||||
installSkill(state, skillKey, name, installId),
|
installSkill(state, skillKey, name, installId),
|
||||||
})
|
})
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${state.tab === "nodes"
|
${
|
||||||
|
state.tab === "nodes"
|
||||||
? renderNodes({
|
? renderNodes({
|
||||||
loading: state.nodesLoading,
|
loading: state.nodesLoading,
|
||||||
nodes: state.nodes,
|
nodes: state.nodes,
|
||||||
devicesLoading: state.devicesLoading,
|
devicesLoading: state.devicesLoading,
|
||||||
devicesError: state.devicesError,
|
devicesError: state.devicesError,
|
||||||
devicesList: state.devicesList,
|
devicesList: state.devicesList,
|
||||||
configForm: state.configForm ?? (state.configSnapshot?.config as Record<string, unknown> | null),
|
configForm:
|
||||||
|
state.configForm ??
|
||||||
|
(state.configSnapshot?.config as Record<string, unknown> | null),
|
||||||
configLoading: state.configLoading,
|
configLoading: state.configLoading,
|
||||||
configSaving: state.configSaving,
|
configSaving: state.configSaving,
|
||||||
configDirty: state.configFormDirty,
|
configDirty: state.configFormDirty,
|
||||||
@ -374,8 +363,7 @@ export function renderApp(state: AppViewState) {
|
|||||||
onDeviceReject: (requestId) => rejectDevicePairing(state, requestId),
|
onDeviceReject: (requestId) => rejectDevicePairing(state, requestId),
|
||||||
onDeviceRotate: (deviceId, role, scopes) =>
|
onDeviceRotate: (deviceId, role, scopes) =>
|
||||||
rotateDeviceToken(state, { deviceId, role, scopes }),
|
rotateDeviceToken(state, { deviceId, role, scopes }),
|
||||||
onDeviceRevoke: (deviceId, role) =>
|
onDeviceRevoke: (deviceId, role) => revokeDeviceToken(state, { deviceId, role }),
|
||||||
revokeDeviceToken(state, { deviceId, role }),
|
|
||||||
onLoadConfig: () => loadConfig(state),
|
onLoadConfig: () => loadConfig(state),
|
||||||
onLoadExecApprovals: () => {
|
onLoadExecApprovals: () => {
|
||||||
const target =
|
const target =
|
||||||
@ -413,8 +401,7 @@ export function renderApp(state: AppViewState) {
|
|||||||
},
|
},
|
||||||
onExecApprovalsPatch: (path, value) =>
|
onExecApprovalsPatch: (path, value) =>
|
||||||
updateExecApprovalsFormValue(state, path, value),
|
updateExecApprovalsFormValue(state, path, value),
|
||||||
onExecApprovalsRemove: (path) =>
|
onExecApprovalsRemove: (path) => removeExecApprovalsFormValue(state, path),
|
||||||
removeExecApprovalsFormValue(state, path),
|
|
||||||
onSaveExecApprovals: () => {
|
onSaveExecApprovals: () => {
|
||||||
const target =
|
const target =
|
||||||
state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId
|
state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId
|
||||||
@ -423,7 +410,8 @@ export function renderApp(state: AppViewState) {
|
|||||||
return saveExecApprovals(state, target);
|
return saveExecApprovals(state, target);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${state.tab === "chat"
|
${state.tab === "chat"
|
||||||
? renderChat({
|
? renderChat({
|
||||||
@ -499,7 +487,8 @@ export function renderApp(state: AppViewState) {
|
|||||||
})
|
})
|
||||||
: nothing}
|
: nothing}
|
||||||
|
|
||||||
${state.tab === "config"
|
${
|
||||||
|
state.tab === "config"
|
||||||
? renderConfig({
|
? renderConfig({
|
||||||
raw: state.configRaw,
|
raw: state.configRaw,
|
||||||
originalRaw: state.configRawOriginal,
|
originalRaw: state.configRawOriginal,
|
||||||
@ -535,9 +524,11 @@ export function renderApp(state: AppViewState) {
|
|||||||
onApply: () => applyConfig(state),
|
onApply: () => applyConfig(state),
|
||||||
onUpdate: () => runUpdate(state),
|
onUpdate: () => runUpdate(state),
|
||||||
})
|
})
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${state.tab === "debug"
|
${
|
||||||
|
state.tab === "debug"
|
||||||
? renderDebug({
|
? renderDebug({
|
||||||
loading: state.debugLoading,
|
loading: state.debugLoading,
|
||||||
status: state.debugStatus,
|
status: state.debugStatus,
|
||||||
@ -554,9 +545,11 @@ export function renderApp(state: AppViewState) {
|
|||||||
onRefresh: () => loadDebug(state),
|
onRefresh: () => loadDebug(state),
|
||||||
onCall: () => callDebugMethod(state),
|
onCall: () => callDebugMethod(state),
|
||||||
})
|
})
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${state.tab === "logs"
|
${
|
||||||
|
state.tab === "logs"
|
||||||
? renderLogs({
|
? renderLogs({
|
||||||
loading: state.logsLoading,
|
loading: state.logsLoading,
|
||||||
error: state.logsError,
|
error: state.logsError,
|
||||||
@ -575,7 +568,8 @@ export function renderApp(state: AppViewState) {
|
|||||||
onExport: (lines, label) => state.exportLogs(lines, label),
|
onExport: (lines, label) => state.exportLogs(lines, label),
|
||||||
onScroll: (event) => state.handleLogsScroll(event),
|
onScroll: (event) => state.handleLogsScroll(event),
|
||||||
})
|
})
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</main>
|
</main>
|
||||||
${renderExecApprovalPrompt(state)}
|
${renderExecApprovalPrompt(state)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -35,8 +35,7 @@ export function scheduleChatScroll(host: ScrollHost, force = false) {
|
|||||||
host.chatScrollFrame = null;
|
host.chatScrollFrame = null;
|
||||||
const target = pickScrollTarget();
|
const target = pickScrollTarget();
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
const distanceFromBottom =
|
const distanceFromBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
|
||||||
target.scrollHeight - target.scrollTop - target.clientHeight;
|
|
||||||
const shouldStick = force || host.chatUserNearBottom || distanceFromBottom < 200;
|
const shouldStick = force || host.chatUserNearBottom || distanceFromBottom < 200;
|
||||||
if (!shouldStick) return;
|
if (!shouldStick) return;
|
||||||
if (force) host.chatHasAutoScrolled = true;
|
if (force) host.chatHasAutoScrolled = true;
|
||||||
@ -49,8 +48,7 @@ export function scheduleChatScroll(host: ScrollHost, force = false) {
|
|||||||
if (!latest) return;
|
if (!latest) return;
|
||||||
const latestDistanceFromBottom =
|
const latestDistanceFromBottom =
|
||||||
latest.scrollHeight - latest.scrollTop - latest.clientHeight;
|
latest.scrollHeight - latest.scrollTop - latest.clientHeight;
|
||||||
const shouldStickRetry =
|
const shouldStickRetry = force || host.chatUserNearBottom || latestDistanceFromBottom < 200;
|
||||||
force || host.chatUserNearBottom || latestDistanceFromBottom < 200;
|
|
||||||
if (!shouldStickRetry) return;
|
if (!shouldStickRetry) return;
|
||||||
latest.scrollTop = latest.scrollHeight;
|
latest.scrollTop = latest.scrollHeight;
|
||||||
host.chatUserNearBottom = true;
|
host.chatUserNearBottom = true;
|
||||||
@ -78,16 +76,14 @@ export function scheduleLogsScroll(host: ScrollHost, force = false) {
|
|||||||
export function handleChatScroll(host: ScrollHost, event: Event) {
|
export function handleChatScroll(host: ScrollHost, event: Event) {
|
||||||
const container = event.currentTarget as HTMLElement | null;
|
const container = event.currentTarget as HTMLElement | null;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
const distanceFromBottom =
|
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||||
container.scrollHeight - container.scrollTop - container.clientHeight;
|
|
||||||
host.chatUserNearBottom = distanceFromBottom < 200;
|
host.chatUserNearBottom = distanceFromBottom < 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleLogsScroll(host: ScrollHost, event: Event) {
|
export function handleLogsScroll(host: ScrollHost, event: Event) {
|
||||||
const container = event.currentTarget as HTMLElement | null;
|
const container = event.currentTarget as HTMLElement | null;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
const distanceFromBottom =
|
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||||
container.scrollHeight - container.scrollTop - container.clientHeight;
|
|
||||||
host.logsAtBottom = distanceFromBottom < 80;
|
host.logsAtBottom = distanceFromBottom < 80;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,12 +9,24 @@ import { loadExecApprovals } from "./controllers/exec-approvals";
|
|||||||
import { loadPresence } from "./controllers/presence";
|
import { loadPresence } from "./controllers/presence";
|
||||||
import { loadSessions } from "./controllers/sessions";
|
import { loadSessions } from "./controllers/sessions";
|
||||||
import { loadSkills } from "./controllers/skills";
|
import { loadSkills } from "./controllers/skills";
|
||||||
import { inferBasePathFromPathname, normalizeBasePath, normalizePath, pathForTab, tabFromPath, type Tab } from "./navigation";
|
import {
|
||||||
|
inferBasePathFromPathname,
|
||||||
|
normalizeBasePath,
|
||||||
|
normalizePath,
|
||||||
|
pathForTab,
|
||||||
|
tabFromPath,
|
||||||
|
type Tab,
|
||||||
|
} from "./navigation";
|
||||||
import { saveSettings, type UiSettings } from "./storage";
|
import { saveSettings, type UiSettings } from "./storage";
|
||||||
import { resolveTheme, type ResolvedTheme, type ThemeMode } from "./theme";
|
import { resolveTheme, type ResolvedTheme, type ThemeMode } from "./theme";
|
||||||
import { startThemeTransition, type ThemeTransitionContext } from "./theme-transition";
|
import { startThemeTransition, type ThemeTransitionContext } from "./theme-transition";
|
||||||
import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll";
|
import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll";
|
||||||
import { startLogsPolling, stopLogsPolling, startDebugPolling, stopDebugPolling } from "./app-polling";
|
import {
|
||||||
|
startLogsPolling,
|
||||||
|
stopLogsPolling,
|
||||||
|
startDebugPolling,
|
||||||
|
stopDebugPolling,
|
||||||
|
} from "./app-polling";
|
||||||
import { refreshChat } from "./app-chat";
|
import { refreshChat } from "./app-chat";
|
||||||
import type { ClawdbotApp } from "./app";
|
import type { ClawdbotApp } from "./app";
|
||||||
|
|
||||||
@ -113,8 +125,7 @@ export function applySettingsFromUrl(host: SettingsHost) {
|
|||||||
export function setTab(host: SettingsHost, next: Tab) {
|
export function setTab(host: SettingsHost, next: Tab) {
|
||||||
if (host.tab !== next) host.tab = next;
|
if (host.tab !== next) host.tab = next;
|
||||||
if (next === "chat") host.chatHasAutoScrolled = false;
|
if (next === "chat") host.chatHasAutoScrolled = false;
|
||||||
if (next === "logs")
|
if (next === "logs") startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);
|
||||||
startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);
|
|
||||||
else stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);
|
else stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);
|
||||||
if (next === "debug")
|
if (next === "debug")
|
||||||
startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]);
|
startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]);
|
||||||
@ -123,11 +134,7 @@ export function setTab(host: SettingsHost, next: Tab) {
|
|||||||
syncUrlWithTab(host, next, false);
|
syncUrlWithTab(host, next, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setTheme(
|
export function setTheme(host: SettingsHost, next: ThemeMode, context?: ThemeTransitionContext) {
|
||||||
host: SettingsHost,
|
|
||||||
next: ThemeMode,
|
|
||||||
context?: ThemeTransitionContext,
|
|
||||||
) {
|
|
||||||
const applyTheme = () => {
|
const applyTheme = () => {
|
||||||
host.theme = next;
|
host.theme = next;
|
||||||
applySettings(host, { ...host.settings, theme: next });
|
applySettings(host, { ...host.settings, theme: next });
|
||||||
@ -172,10 +179,7 @@ export async function refreshActiveTab(host: SettingsHost) {
|
|||||||
if (host.tab === "logs") {
|
if (host.tab === "logs") {
|
||||||
host.logsAtBottom = true;
|
host.logsAtBottom = true;
|
||||||
await loadLogs(host as unknown as ClawdbotApp, { reset: true });
|
await loadLogs(host as unknown as ClawdbotApp, { reset: true });
|
||||||
scheduleLogsScroll(
|
scheduleLogsScroll(host as unknown as Parameters<typeof scheduleLogsScroll>[0], true);
|
||||||
host as unknown as Parameters<typeof scheduleLogsScroll>[0],
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,8 +265,7 @@ export function onPopState(host: SettingsHost) {
|
|||||||
export function setTabFromRoute(host: SettingsHost, next: Tab) {
|
export function setTabFromRoute(host: SettingsHost, next: Tab) {
|
||||||
if (host.tab !== next) host.tab = next;
|
if (host.tab !== next) host.tab = next;
|
||||||
if (next === "chat") host.chatHasAutoScrolled = false;
|
if (next === "chat") host.chatHasAutoScrolled = false;
|
||||||
if (next === "logs")
|
if (next === "logs") startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);
|
||||||
startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);
|
|
||||||
else stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);
|
else stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);
|
||||||
if (next === "debug")
|
if (next === "debug")
|
||||||
startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]);
|
startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]);
|
||||||
@ -293,11 +296,7 @@ export function syncUrlWithTab(host: SettingsHost, tab: Tab, replace: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function syncUrlWithSessionKey(
|
export function syncUrlWithSessionKey(host: SettingsHost, sessionKey: string, replace: boolean) {
|
||||||
host: SettingsHost,
|
|
||||||
sessionKey: string,
|
|
||||||
replace: boolean,
|
|
||||||
) {
|
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
url.searchParams.set("session", sessionKey);
|
url.searchParams.set("session", sessionKey);
|
||||||
|
|||||||
@ -191,8 +191,7 @@ export function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPaylo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (payload.stream !== "tool") return;
|
if (payload.stream !== "tool") return;
|
||||||
const sessionKey =
|
const sessionKey = typeof payload.sessionKey === "string" ? payload.sessionKey : undefined;
|
||||||
typeof payload.sessionKey === "string" ? payload.sessionKey : undefined;
|
|
||||||
if (sessionKey && sessionKey !== host.sessionKey) return;
|
if (sessionKey && sessionKey !== host.sessionKey) return;
|
||||||
// Fallback: only accept session-less events for the active run.
|
// Fallback: only accept session-less events for the active run.
|
||||||
if (!sessionKey && host.chatRunId && payload.runId !== host.chatRunId) return;
|
if (!sessionKey && host.chatRunId && payload.runId !== host.chatRunId) return;
|
||||||
|
|||||||
@ -22,10 +22,7 @@ import type {
|
|||||||
import type { ChatAttachment, ChatQueueItem, CronFormState } from "./ui-types";
|
import type { ChatAttachment, ChatQueueItem, CronFormState } from "./ui-types";
|
||||||
import type { EventLogEntry } from "./app-events";
|
import type { EventLogEntry } from "./app-events";
|
||||||
import type { SkillMessage } from "./controllers/skills";
|
import type { SkillMessage } from "./controllers/skills";
|
||||||
import type {
|
import type { ExecApprovalsFile, ExecApprovalsSnapshot } from "./controllers/exec-approvals";
|
||||||
ExecApprovalsFile,
|
|
||||||
ExecApprovalsSnapshot,
|
|
||||||
} from "./controllers/exec-approvals";
|
|
||||||
import type { DevicePairingList } from "./controllers/devices";
|
import type { DevicePairingList } from "./controllers/devices";
|
||||||
import type { ExecApprovalRequest } from "./controllers/exec-approval";
|
import type { ExecApprovalRequest } from "./controllers/exec-approval";
|
||||||
import type { NostrProfileFormState } from "./views/channels.nostr-profile-form";
|
import type { NostrProfileFormState } from "./views/channels.nostr-profile-form";
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { LitElement, html, nothing } from "lit";
|
import { LitElement } from "lit";
|
||||||
import { customElement, state } from "lit/decorators.js";
|
import { customElement, state } from "lit/decorators.js";
|
||||||
|
|
||||||
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway";
|
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway";
|
||||||
@ -27,10 +27,7 @@ import type {
|
|||||||
import { type ChatAttachment, type ChatQueueItem, type CronFormState } from "./ui-types";
|
import { type ChatAttachment, type ChatQueueItem, type CronFormState } from "./ui-types";
|
||||||
import type { EventLogEntry } from "./app-events";
|
import type { EventLogEntry } from "./app-events";
|
||||||
import { DEFAULT_CRON_FORM, DEFAULT_LOG_LEVEL_FILTERS } from "./app-defaults";
|
import { DEFAULT_CRON_FORM, DEFAULT_LOG_LEVEL_FILTERS } from "./app-defaults";
|
||||||
import type {
|
import type { ExecApprovalsFile, ExecApprovalsSnapshot } from "./controllers/exec-approvals";
|
||||||
ExecApprovalsFile,
|
|
||||||
ExecApprovalsSnapshot,
|
|
||||||
} from "./controllers/exec-approvals";
|
|
||||||
import type { DevicePairingList } from "./controllers/devices";
|
import type { DevicePairingList } from "./controllers/devices";
|
||||||
import type { ExecApprovalRequest } from "./controllers/exec-approval";
|
import type { ExecApprovalRequest } from "./controllers/exec-approval";
|
||||||
import {
|
import {
|
||||||
@ -259,9 +256,7 @@ export class ClawdbotApp extends LitElement {
|
|||||||
private toolStreamOrder: string[] = [];
|
private toolStreamOrder: string[] = [];
|
||||||
basePath = "";
|
basePath = "";
|
||||||
private popStateHandler = () =>
|
private popStateHandler = () =>
|
||||||
onPopStateInternal(
|
onPopStateInternal(this as unknown as Parameters<typeof onPopStateInternal>[0]);
|
||||||
this as unknown as Parameters<typeof onPopStateInternal>[0],
|
|
||||||
);
|
|
||||||
private themeMedia: MediaQueryList | null = null;
|
private themeMedia: MediaQueryList | null = null;
|
||||||
private themeMediaHandler: ((event: MediaQueryListEvent) => void) | null = null;
|
private themeMediaHandler: ((event: MediaQueryListEvent) => void) | null = null;
|
||||||
private topbarObserver: ResizeObserver | null = null;
|
private topbarObserver: ResizeObserver | null = null;
|
||||||
@ -285,16 +280,11 @@ export class ClawdbotApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changed: Map<PropertyKey, unknown>) {
|
protected updated(changed: Map<PropertyKey, unknown>) {
|
||||||
handleUpdated(
|
handleUpdated(this as unknown as Parameters<typeof handleUpdated>[0], changed);
|
||||||
this as unknown as Parameters<typeof handleUpdated>[0],
|
|
||||||
changed,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
connectGatewayInternal(
|
connectGatewayInternal(this as unknown as Parameters<typeof connectGatewayInternal>[0]);
|
||||||
this as unknown as Parameters<typeof connectGatewayInternal>[0],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChatScroll(event: Event) {
|
handleChatScroll(event: Event) {
|
||||||
@ -316,15 +306,11 @@ export class ClawdbotApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resetToolStream() {
|
resetToolStream() {
|
||||||
resetToolStreamInternal(
|
resetToolStreamInternal(this as unknown as Parameters<typeof resetToolStreamInternal>[0]);
|
||||||
this as unknown as Parameters<typeof resetToolStreamInternal>[0],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resetChatScroll() {
|
resetChatScroll() {
|
||||||
resetChatScrollInternal(
|
resetChatScrollInternal(this as unknown as Parameters<typeof resetChatScrollInternal>[0]);
|
||||||
this as unknown as Parameters<typeof resetChatScrollInternal>[0],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadAssistantIdentity() {
|
async loadAssistantIdentity() {
|
||||||
@ -332,10 +318,7 @@ export class ClawdbotApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
applySettings(next: UiSettings) {
|
applySettings(next: UiSettings) {
|
||||||
applySettingsInternal(
|
applySettingsInternal(this as unknown as Parameters<typeof applySettingsInternal>[0], next);
|
||||||
this as unknown as Parameters<typeof applySettingsInternal>[0],
|
|
||||||
next,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTab(next: Tab) {
|
setTab(next: Tab) {
|
||||||
@ -343,29 +326,19 @@ export class ClawdbotApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTheme(next: ThemeMode, context?: Parameters<typeof setThemeInternal>[2]) {
|
setTheme(next: ThemeMode, context?: Parameters<typeof setThemeInternal>[2]) {
|
||||||
setThemeInternal(
|
setThemeInternal(this as unknown as Parameters<typeof setThemeInternal>[0], next, context);
|
||||||
this as unknown as Parameters<typeof setThemeInternal>[0],
|
|
||||||
next,
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadOverview() {
|
async loadOverview() {
|
||||||
await loadOverviewInternal(
|
await loadOverviewInternal(this as unknown as Parameters<typeof loadOverviewInternal>[0]);
|
||||||
this as unknown as Parameters<typeof loadOverviewInternal>[0],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadCron() {
|
async loadCron() {
|
||||||
await loadCronInternal(
|
await loadCronInternal(this as unknown as Parameters<typeof loadCronInternal>[0]);
|
||||||
this as unknown as Parameters<typeof loadCronInternal>[0],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleAbortChat() {
|
async handleAbortChat() {
|
||||||
await handleAbortChatInternal(
|
await handleAbortChatInternal(this as unknown as Parameters<typeof handleAbortChatInternal>[0]);
|
||||||
this as unknown as Parameters<typeof handleAbortChatInternal>[0],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeQueuedMessage(id: string) {
|
removeQueuedMessage(id: string) {
|
||||||
|
|||||||
@ -28,13 +28,10 @@ function coerceIdentityValue(value: string | undefined, maxLength: number): stri
|
|||||||
export function normalizeAssistantIdentity(
|
export function normalizeAssistantIdentity(
|
||||||
input?: Partial<AssistantIdentity> | null,
|
input?: Partial<AssistantIdentity> | null,
|
||||||
): AssistantIdentity {
|
): AssistantIdentity {
|
||||||
const name =
|
const name = coerceIdentityValue(input?.name, MAX_ASSISTANT_NAME) ?? DEFAULT_ASSISTANT_NAME;
|
||||||
coerceIdentityValue(input?.name, MAX_ASSISTANT_NAME) ?? DEFAULT_ASSISTANT_NAME;
|
|
||||||
const avatar = coerceIdentityValue(input?.avatar ?? undefined, MAX_ASSISTANT_AVATAR) ?? null;
|
const avatar = coerceIdentityValue(input?.avatar ?? undefined, MAX_ASSISTANT_AVATAR) ?? null;
|
||||||
const agentId =
|
const agentId =
|
||||||
typeof input?.agentId === "string" && input.agentId.trim()
|
typeof input?.agentId === "string" && input.agentId.trim() ? input.agentId.trim() : null;
|
||||||
? input.agentId.trim()
|
|
||||||
: null;
|
|
||||||
return { agentId, name, avatar };
|
return { agentId, name, avatar };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,9 +46,7 @@ describe("chat markdown rendering", () => {
|
|||||||
|
|
||||||
await app.updateComplete;
|
await app.updateComplete;
|
||||||
|
|
||||||
const toolCards = Array.from(
|
const toolCards = Array.from(app.querySelectorAll<HTMLElement>(".chat-tool-card"));
|
||||||
app.querySelectorAll<HTMLElement>(".chat-tool-card"),
|
|
||||||
);
|
|
||||||
const toolCard = toolCards.find((card) =>
|
const toolCard = toolCards.find((card) =>
|
||||||
card.querySelector(".chat-tool-card__preview, .chat-tool-card__inline"),
|
card.querySelector(".chat-tool-card__preview, .chat-tool-card__inline"),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
import { html, type TemplateResult } from "lit";
|
import { html, type TemplateResult } from "lit";
|
||||||
import { icons } from "../icons";
|
import { renderEmojiIcon, setEmojiIcon } from "../icons";
|
||||||
|
|
||||||
const COPIED_FOR_MS = 1500;
|
const COPIED_FOR_MS = 1500;
|
||||||
const ERROR_FOR_MS = 2000;
|
const ERROR_FOR_MS = 2000;
|
||||||
const COPY_LABEL = "Copy as markdown";
|
const COPY_LABEL = "Copy as markdown";
|
||||||
const COPIED_LABEL = "Copied";
|
const COPIED_LABEL = "Copied";
|
||||||
const ERROR_LABEL = "Copy failed";
|
const ERROR_LABEL = "Copy failed";
|
||||||
|
const COPY_ICON = "📋";
|
||||||
|
const COPIED_ICON = "✓";
|
||||||
|
const ERROR_ICON = "!";
|
||||||
|
|
||||||
type CopyButtonOptions = {
|
type CopyButtonOptions = {
|
||||||
text: () => string;
|
text: () => string;
|
||||||
@ -38,9 +41,7 @@ function createCopyButton(options: CopyButtonOptions): TemplateResult {
|
|||||||
aria-label=${idleLabel}
|
aria-label=${idleLabel}
|
||||||
@click=${async (e: Event) => {
|
@click=${async (e: Event) => {
|
||||||
const btn = e.currentTarget as HTMLButtonElement | null;
|
const btn = e.currentTarget as HTMLButtonElement | null;
|
||||||
const iconContainer = btn?.querySelector(
|
const icon = btn?.querySelector(".chat-copy-btn__icon") as HTMLElement | null;
|
||||||
".chat-copy-btn__icon",
|
|
||||||
) as HTMLElement | null;
|
|
||||||
|
|
||||||
if (!btn || btn.dataset.copying === "1") return;
|
if (!btn || btn.dataset.copying === "1") return;
|
||||||
|
|
||||||
@ -58,29 +59,30 @@ function createCopyButton(options: CopyButtonOptions): TemplateResult {
|
|||||||
if (!copied) {
|
if (!copied) {
|
||||||
btn.dataset.error = "1";
|
btn.dataset.error = "1";
|
||||||
setButtonLabel(btn, ERROR_LABEL);
|
setButtonLabel(btn, ERROR_LABEL);
|
||||||
|
setEmojiIcon(icon, ERROR_ICON);
|
||||||
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
if (!btn.isConnected) return;
|
if (!btn.isConnected) return;
|
||||||
delete btn.dataset.error;
|
delete btn.dataset.error;
|
||||||
setButtonLabel(btn, idleLabel);
|
setButtonLabel(btn, idleLabel);
|
||||||
|
setEmojiIcon(icon, COPY_ICON);
|
||||||
}, ERROR_FOR_MS);
|
}, ERROR_FOR_MS);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.dataset.copied = "1";
|
btn.dataset.copied = "1";
|
||||||
setButtonLabel(btn, COPIED_LABEL);
|
setButtonLabel(btn, COPIED_LABEL);
|
||||||
|
setEmojiIcon(icon, COPIED_ICON);
|
||||||
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
if (!btn.isConnected) return;
|
if (!btn.isConnected) return;
|
||||||
delete btn.dataset.copied;
|
delete btn.dataset.copied;
|
||||||
setButtonLabel(btn, idleLabel);
|
setButtonLabel(btn, idleLabel);
|
||||||
|
setEmojiIcon(icon, COPY_ICON);
|
||||||
}, COPIED_FOR_MS);
|
}, COPIED_FOR_MS);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span class="chat-copy-btn__icon" aria-hidden="true">
|
${renderEmojiIcon(COPY_ICON, "chat-copy-btn__icon")}
|
||||||
<span class="chat-copy-btn__icon-copy">${icons.copy}</span>
|
|
||||||
<span class="chat-copy-btn__icon-check">${icons.check}</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -122,11 +122,7 @@ export function renderMessageGroup(
|
|||||||
? assistantName
|
? assistantName
|
||||||
: normalizedRole;
|
: normalizedRole;
|
||||||
const roleClass =
|
const roleClass =
|
||||||
normalizedRole === "user"
|
normalizedRole === "user" ? "user" : normalizedRole === "assistant" ? "assistant" : "other";
|
||||||
? "user"
|
|
||||||
: normalizedRole === "assistant"
|
|
||||||
? "assistant"
|
|
||||||
: "other";
|
|
||||||
const timestamp = new Date(group.timestamp).toLocaleTimeString([], {
|
const timestamp = new Date(group.timestamp).toLocaleTimeString([], {
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
@ -143,8 +139,7 @@ export function renderMessageGroup(
|
|||||||
renderGroupedMessage(
|
renderGroupedMessage(
|
||||||
item.message,
|
item.message,
|
||||||
{
|
{
|
||||||
isStreaming:
|
isStreaming: group.isStreaming && index === group.messages.length - 1,
|
||||||
group.isStreaming && index === group.messages.length - 1,
|
|
||||||
showReasoning: opts.showReasoning,
|
showReasoning: opts.showReasoning,
|
||||||
},
|
},
|
||||||
opts.onOpenSidebar,
|
opts.onOpenSidebar,
|
||||||
@ -159,10 +154,7 @@ export function renderMessageGroup(
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAvatar(
|
function renderAvatar(role: string, assistant?: Pick<AssistantIdentity, "name" | "avatar">) {
|
||||||
role: string,
|
|
||||||
assistant?: Pick<AssistantIdentity, "name" | "avatar">,
|
|
||||||
) {
|
|
||||||
const normalized = normalizeRoleForGrouping(role);
|
const normalized = normalizeRoleForGrouping(role);
|
||||||
const assistantName = assistant?.name?.trim() || "Assistant";
|
const assistantName = assistant?.name?.trim() || "Assistant";
|
||||||
const assistantAvatar = assistant?.avatar?.trim() || "";
|
const assistantAvatar = assistant?.avatar?.trim() || "";
|
||||||
@ -199,9 +191,7 @@ function renderAvatar(
|
|||||||
|
|
||||||
function isAvatarUrl(value: string): boolean {
|
function isAvatarUrl(value: string): boolean {
|
||||||
return (
|
return (
|
||||||
/^https?:\/\//i.test(value) ||
|
/^https?:\/\//i.test(value) || /^data:image\//i.test(value) || value.startsWith('/') // Relative paths from avatar endpoint
|
||||||
/^data:image\//i.test(value) ||
|
|
||||||
/^\//.test(value) // Relative paths from avatar endpoint
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,13 +235,9 @@ function renderGroupedMessage(
|
|||||||
|
|
||||||
const extractedText = extractTextCached(message);
|
const extractedText = extractTextCached(message);
|
||||||
const extractedThinking =
|
const extractedThinking =
|
||||||
opts.showReasoning && role === "assistant"
|
opts.showReasoning && role === "assistant" ? extractThinkingCached(message) : null;
|
||||||
? extractThinkingCached(message)
|
|
||||||
: null;
|
|
||||||
const markdownBase = extractedText?.trim() ? extractedText : null;
|
const markdownBase = extractedText?.trim() ? extractedText : null;
|
||||||
const reasoningMarkdown = extractedThinking
|
const reasoningMarkdown = extractedThinking ? formatReasoningMarkdown(extractedThinking) : null;
|
||||||
? formatReasoningMarkdown(extractedThinking)
|
|
||||||
: null;
|
|
||||||
const markdown = markdownBase;
|
const markdown = markdownBase;
|
||||||
const canCopyMarkdown = role === "assistant" && Boolean(markdown?.trim());
|
const canCopyMarkdown = role === "assistant" && Boolean(markdown?.trim());
|
||||||
|
|
||||||
@ -265,9 +251,7 @@ function renderGroupedMessage(
|
|||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
if (!markdown && hasToolCards && isToolResult) {
|
if (!markdown && hasToolCards && isToolResult) {
|
||||||
return html`${toolCards.map((card) =>
|
return html`${toolCards.map((card) => renderToolCardSidebar(card, onOpenSidebar))}`;
|
||||||
renderToolCardSidebar(card, onOpenSidebar),
|
|
||||||
)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!markdown && !hasToolCards && !hasImages) return nothing;
|
if (!markdown && !hasToolCards && !hasImages) return nothing;
|
||||||
|
|||||||
@ -90,13 +90,9 @@ export function extractThinking(message: unknown): string | null {
|
|||||||
const rawText = extractRawText(message);
|
const rawText = extractRawText(message);
|
||||||
if (!rawText) return null;
|
if (!rawText) return null;
|
||||||
const matches = [
|
const matches = [
|
||||||
...rawText.matchAll(
|
...rawText.matchAll(/<\s*think(?:ing)?\s*>([\s\S]*?)<\s*\/\s*think(?:ing)?\s*>/gi),
|
||||||
/<\s*think(?:ing)?\s*>([\s\S]*?)<\s*\/\s*think(?:ing)?\s*>/gi,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
const extracted = matches
|
const extracted = matches.map((m) => (m[1] ?? "").trim()).filter(Boolean);
|
||||||
.map((m) => (m[1] ?? "").trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
return extracted.length > 0 ? extracted.join("\n") : null;
|
return extracted.length > 0 ? extracted.join("\n") : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,8 +44,18 @@ describe("message-normalizer", () => {
|
|||||||
|
|
||||||
expect(result.role).toBe("assistant");
|
expect(result.role).toBe("assistant");
|
||||||
expect(result.content).toHaveLength(2);
|
expect(result.content).toHaveLength(2);
|
||||||
expect(result.content[0]).toEqual({ type: "text", text: "Here is the result", name: undefined, args: undefined });
|
expect(result.content[0]).toEqual({
|
||||||
expect(result.content[1]).toEqual({ type: "tool_use", text: undefined, name: "bash", args: { command: "ls" } });
|
type: "text",
|
||||||
|
text: "Here is the result",
|
||||||
|
name: undefined,
|
||||||
|
args: undefined,
|
||||||
|
});
|
||||||
|
expect(result.content[1]).toEqual({
|
||||||
|
type: "tool_use",
|
||||||
|
text: undefined,
|
||||||
|
name: "bash",
|
||||||
|
args: { command: "ls" },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("normalizes message with text field (alternative format)", () => {
|
it("normalizes message with text field (alternative format)", () => {
|
||||||
|
|||||||
@ -2,10 +2,7 @@
|
|||||||
* Message normalization utilities for chat rendering.
|
* Message normalization utilities for chat rendering.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type { NormalizedMessage, MessageContentItem } from "../types/chat-types";
|
||||||
NormalizedMessage,
|
|
||||||
MessageContentItem,
|
|
||||||
} from "../types/chat-types";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize a raw message object into a consistent structure.
|
* Normalize a raw message object into a consistent structure.
|
||||||
@ -16,8 +13,7 @@ export function normalizeMessage(message: unknown): NormalizedMessage {
|
|||||||
|
|
||||||
// Detect tool messages by common gateway shapes.
|
// Detect tool messages by common gateway shapes.
|
||||||
// Some tool events come through as assistant role with tool_* items in the content array.
|
// Some tool events come through as assistant role with tool_* items in the content array.
|
||||||
const hasToolId =
|
const hasToolId = typeof m.toolCallId === "string" || typeof m.tool_call_id === "string";
|
||||||
typeof m.toolCallId === "string" || typeof m.tool_call_id === "string";
|
|
||||||
|
|
||||||
const contentRaw = m.content;
|
const contentRaw = m.content;
|
||||||
const contentItems = Array.isArray(contentRaw) ? contentRaw : null;
|
const contentItems = Array.isArray(contentRaw) ? contentRaw : null;
|
||||||
|
|||||||
@ -1,13 +1,9 @@
|
|||||||
import { html, nothing } from "lit";
|
import { html, nothing } from "lit";
|
||||||
|
|
||||||
import { formatToolDetail, resolveToolDisplay } from "../tool-display";
|
import { formatToolDetail, resolveToolDisplay } from "../tool-display";
|
||||||
import { icons } from "../icons";
|
|
||||||
import type { ToolCard } from "../types/chat-types";
|
import type { ToolCard } from "../types/chat-types";
|
||||||
import { TOOL_INLINE_THRESHOLD } from "./constants";
|
import { TOOL_INLINE_THRESHOLD } from "./constants";
|
||||||
import {
|
import { formatToolOutputForSidebar, getTruncatedPreview } from "./tool-helpers";
|
||||||
formatToolOutputForSidebar,
|
|
||||||
getTruncatedPreview,
|
|
||||||
} from "./tool-helpers";
|
|
||||||
import { isToolResultMessage } from "./message-normalizer";
|
import { isToolResultMessage } from "./message-normalizer";
|
||||||
import { extractTextCached } from "./message-extract";
|
import { extractTextCached } from "./message-extract";
|
||||||
|
|
||||||
@ -38,10 +34,7 @@ export function extractToolCards(message: unknown): ToolCard[] {
|
|||||||
cards.push({ kind: "result", name, text });
|
cards.push({ kind: "result", name, text });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (isToolResultMessage(message) && !cards.some((card) => card.kind === "result")) {
|
||||||
isToolResultMessage(message) &&
|
|
||||||
!cards.some((card) => card.kind === "result")
|
|
||||||
) {
|
|
||||||
const name =
|
const name =
|
||||||
(typeof m.toolName === "string" && m.toolName) ||
|
(typeof m.toolName === "string" && m.toolName) ||
|
||||||
(typeof m.tool_name === "string" && m.tool_name) ||
|
(typeof m.tool_name === "string" && m.tool_name) ||
|
||||||
@ -53,10 +46,7 @@ export function extractToolCards(message: unknown): ToolCard[] {
|
|||||||
return cards;
|
return cards;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderToolCardSidebar(
|
export function renderToolCardSidebar(card: ToolCard, onOpenSidebar?: (content: string) => void) {
|
||||||
card: ToolCard,
|
|
||||||
onOpenSidebar?: (content: string) => void,
|
|
||||||
) {
|
|
||||||
const display = resolveToolDisplay({ name: card.name, args: card.args });
|
const display = resolveToolDisplay({ name: card.name, args: card.args });
|
||||||
const detail = formatToolDetail(display);
|
const detail = formatToolDetail(display);
|
||||||
const hasText = Boolean(card.text?.trim());
|
const hasText = Boolean(card.text?.trim());
|
||||||
@ -86,36 +76,48 @@ export function renderToolCardSidebar(
|
|||||||
@click=${handleClick}
|
@click=${handleClick}
|
||||||
role=${canClick ? "button" : nothing}
|
role=${canClick ? "button" : nothing}
|
||||||
tabindex=${canClick ? "0" : nothing}
|
tabindex=${canClick ? "0" : nothing}
|
||||||
@keydown=${canClick
|
@keydown=${
|
||||||
|
canClick
|
||||||
? (e: KeyboardEvent) => {
|
? (e: KeyboardEvent) => {
|
||||||
if (e.key !== "Enter" && e.key !== " ") return;
|
if (e.key !== "Enter" && e.key !== " ") return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleClick?.();
|
handleClick?.();
|
||||||
}
|
}
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div class="chat-tool-card__header">
|
<div class="chat-tool-card__header">
|
||||||
<div class="chat-tool-card__title">
|
<div class="chat-tool-card__title">
|
||||||
<span class="chat-tool-card__icon">${icons[display.icon]}</span>
|
<span class="chat-tool-card__icon">${display.emoji}</span>
|
||||||
<span>${display.label}</span>
|
<span>${display.label}</span>
|
||||||
</div>
|
</div>
|
||||||
${canClick
|
${
|
||||||
? html`<span class="chat-tool-card__action">${hasText ? "View" : ""} ${icons.check}</span>`
|
canClick
|
||||||
: nothing}
|
? html`<span class="chat-tool-card__action">${hasText ? "View ›" : "›"}</span>`
|
||||||
${isEmpty && !canClick ? html`<span class="chat-tool-card__status">${icons.check}</span>` : nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
${
|
||||||
|
isEmpty && !canClick
|
||||||
|
? html`
|
||||||
|
<span class="chat-tool-card__status">✓</span>
|
||||||
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
${detail
|
${detail ? html`<div class="chat-tool-card__detail">${detail}</div>` : nothing}
|
||||||
? html`<div class="chat-tool-card__detail">${detail}</div>`
|
${
|
||||||
: nothing}
|
isEmpty
|
||||||
${isEmpty
|
? html`
|
||||||
? html`<div class="chat-tool-card__status-text muted">Completed</div>`
|
<div class="chat-tool-card__status-text muted">Completed</div>
|
||||||
: nothing}
|
`
|
||||||
${showCollapsed
|
: nothing
|
||||||
|
}
|
||||||
|
${
|
||||||
|
showCollapsed
|
||||||
? html`<div class="chat-tool-card__preview mono">${getTruncatedPreview(card.text!)}</div>`
|
? html`<div class="chat-tool-card__preview mono">${getTruncatedPreview(card.text!)}</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
${showInline
|
}
|
||||||
? html`<div class="chat-tool-card__inline mono">${card.text}</div>`
|
${showInline ? html`<div class="chat-tool-card__inline mono">${card.text}</div>` : nothing}
|
||||||
: nothing}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ describe("tool-helpers", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("formats valid JSON array as code block", () => {
|
it("formats valid JSON array as code block", () => {
|
||||||
const input = '[1, 2, 3]';
|
const input = "[1, 2, 3]";
|
||||||
const result = formatToolOutputForSidebar(input);
|
const result = formatToolOutputForSidebar(input);
|
||||||
|
|
||||||
expect(result).toBe(`\`\`\`json
|
expect(result).toBe(`\`\`\`json
|
||||||
|
|||||||
@ -44,7 +44,9 @@ export class ResizableDivider extends LitElement {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html``;
|
return html`
|
||||||
|
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
@ -89,7 +91,7 @@ export class ResizableDivider extends LitElement {
|
|||||||
detail: { splitRatio: newRatio },
|
detail: { splitRatio: newRatio },
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -29,12 +29,7 @@ const rootSchema = {
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
},
|
},
|
||||||
bind: {
|
bind: {
|
||||||
anyOf: [
|
anyOf: [{ const: "auto" }, { const: "lan" }, { const: "tailnet" }, { const: "loopback" }],
|
||||||
{ const: "auto" },
|
|
||||||
{ const: "lan" },
|
|
||||||
{ const: "tailnet" },
|
|
||||||
{ const: "loopback" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -57,17 +52,12 @@ describe("config form renderer", () => {
|
|||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
|
|
||||||
const tokenInput = container.querySelector(
|
const tokenInput = container.querySelector("input[type='password']") as HTMLInputElement | null;
|
||||||
"input[type='password']",
|
|
||||||
) as HTMLInputElement | null;
|
|
||||||
expect(tokenInput).not.toBeNull();
|
expect(tokenInput).not.toBeNull();
|
||||||
if (!tokenInput) return;
|
if (!tokenInput) return;
|
||||||
tokenInput.value = "abc123";
|
tokenInput.value = "abc123";
|
||||||
tokenInput.dispatchEvent(new Event("input", { bubbles: true }));
|
tokenInput.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
expect(onPatch).toHaveBeenCalledWith(
|
expect(onPatch).toHaveBeenCalledWith(["gateway", "auth", "token"], "abc123");
|
||||||
["gateway", "auth", "token"],
|
|
||||||
"abc123",
|
|
||||||
);
|
|
||||||
|
|
||||||
const tokenButton = Array.from(
|
const tokenButton = Array.from(
|
||||||
container.querySelectorAll<HTMLButtonElement>(".cfg-segmented__btn"),
|
container.querySelectorAll<HTMLButtonElement>(".cfg-segmented__btn"),
|
||||||
@ -76,9 +66,7 @@ describe("config form renderer", () => {
|
|||||||
tokenButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
tokenButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(onPatch).toHaveBeenCalledWith(["mode"], "token");
|
expect(onPatch).toHaveBeenCalledWith(["mode"], "token");
|
||||||
|
|
||||||
const checkbox = container.querySelector(
|
const checkbox = container.querySelector("input[type='checkbox']") as HTMLInputElement | null;
|
||||||
"input[type='checkbox']",
|
|
||||||
) as HTMLInputElement | null;
|
|
||||||
expect(checkbox).not.toBeNull();
|
expect(checkbox).not.toBeNull();
|
||||||
if (!checkbox) return;
|
if (!checkbox) return;
|
||||||
checkbox.checked = true;
|
checkbox.checked = true;
|
||||||
@ -101,9 +89,7 @@ describe("config form renderer", () => {
|
|||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
|
|
||||||
const addButton = container.querySelector(
|
const addButton = container.querySelector(".cfg-array__add") as HTMLButtonElement | null;
|
||||||
".cfg-array__add",
|
|
||||||
) as HTMLButtonElement | null;
|
|
||||||
expect(addButton).not.toBeUndefined();
|
expect(addButton).not.toBeUndefined();
|
||||||
addButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
addButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(onPatch).toHaveBeenCalledWith(["allowFrom"], ["+1", ""]);
|
expect(onPatch).toHaveBeenCalledWith(["allowFrom"], ["+1", ""]);
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
import type { GatewayBrowserClient } from "../gateway";
|
import type { GatewayBrowserClient } from "../gateway";
|
||||||
import {
|
import { normalizeAssistantIdentity, type AssistantIdentity } from "../assistant-identity";
|
||||||
normalizeAssistantIdentity,
|
|
||||||
type AssistantIdentity,
|
|
||||||
} from "../assistant-identity";
|
|
||||||
|
|
||||||
export type AssistantIdentityState = {
|
export type AssistantIdentityState = {
|
||||||
client: GatewayBrowserClient | null;
|
client: GatewayBrowserClient | null;
|
||||||
|
|||||||
@ -144,9 +144,7 @@ export async function abortChatRun(state: ChatState): Promise<boolean> {
|
|||||||
try {
|
try {
|
||||||
await state.client.request(
|
await state.client.request(
|
||||||
"chat.abort",
|
"chat.abort",
|
||||||
runId
|
runId ? { sessionKey: state.sessionKey, runId } : { sessionKey: state.sessionKey },
|
||||||
? { sessionKey: state.sessionKey, runId }
|
|
||||||
: { sessionKey: state.sessionKey },
|
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -155,10 +153,7 @@ export async function abortChatRun(state: ChatState): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleChatEvent(
|
export function handleChatEvent(state: ChatState, payload?: ChatEventPayload) {
|
||||||
state: ChatState,
|
|
||||||
payload?: ChatEventPayload,
|
|
||||||
) {
|
|
||||||
if (!payload) return null;
|
if (!payload) return null;
|
||||||
if (payload.sessionKey !== state.sessionKey) return null;
|
if (payload.sessionKey !== state.sessionKey) return null;
|
||||||
|
|
||||||
|
|||||||
@ -46,11 +46,11 @@ describe("applyConfigSnapshot", () => {
|
|||||||
config: { gateway: { mode: "remote", port: 9999 } },
|
config: { gateway: { mode: "remote", port: 9999 } },
|
||||||
valid: true,
|
valid: true,
|
||||||
issues: [],
|
issues: [],
|
||||||
raw: "{\n \"gateway\": { \"mode\": \"remote\", \"port\": 9999 }\n}\n",
|
raw: '{\n "gateway": { "mode": "remote", "port": 9999 }\n}\n',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(state.configRaw).toBe(
|
expect(state.configRaw).toBe(
|
||||||
"{\n \"gateway\": {\n \"mode\": \"local\",\n \"port\": 18789\n }\n}\n",
|
'{\n "gateway": {\n "mode": "local",\n "port": 18789\n }\n}\n',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ describe("updateConfigFormValue", () => {
|
|||||||
updateConfigFormValue(state, ["gateway", "port"], 18789);
|
updateConfigFormValue(state, ["gateway", "port"], 18789);
|
||||||
|
|
||||||
expect(state.configRaw).toBe(
|
expect(state.configRaw).toBe(
|
||||||
"{\n \"gateway\": {\n \"mode\": \"local\",\n \"port\": 18789\n }\n}\n",
|
'{\n "gateway": {\n "mode": "local",\n "port": 18789\n }\n}\n',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -142,7 +142,7 @@ describe("applyConfig", () => {
|
|||||||
state.client = { request } as unknown as ConfigState["client"];
|
state.client = { request } as unknown as ConfigState["client"];
|
||||||
state.applySessionKey = "agent:main:whatsapp:dm:+15555550123";
|
state.applySessionKey = "agent:main:whatsapp:dm:+15555550123";
|
||||||
state.configFormMode = "raw";
|
state.configFormMode = "raw";
|
||||||
state.configRaw = "{\n agent: { workspace: \"~/clawd\" }\n}\n";
|
state.configRaw = '{\n agent: { workspace: "~/clawd" }\n}\n';
|
||||||
state.configSnapshot = {
|
state.configSnapshot = {
|
||||||
hash: "hash-123",
|
hash: "hash-123",
|
||||||
};
|
};
|
||||||
@ -150,7 +150,7 @@ describe("applyConfig", () => {
|
|||||||
await applyConfig(state);
|
await applyConfig(state);
|
||||||
|
|
||||||
expect(request).toHaveBeenCalledWith("config.apply", {
|
expect(request).toHaveBeenCalledWith("config.apply", {
|
||||||
raw: "{\n agent: { workspace: \"~/clawd\" }\n}\n",
|
raw: '{\n agent: { workspace: "~/clawd" }\n}\n',
|
||||||
baseHash: "hash-123",
|
baseHash: "hash-123",
|
||||||
sessionKey: "agent:main:whatsapp:dm:+15555550123",
|
sessionKey: "agent:main:whatsapp:dm:+15555550123",
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,9 +1,5 @@
|
|||||||
import type { GatewayBrowserClient } from "../gateway";
|
import type { GatewayBrowserClient } from "../gateway";
|
||||||
import type {
|
import type { ConfigSchemaResponse, ConfigSnapshot, ConfigUiHints } from "../types";
|
||||||
ConfigSchemaResponse,
|
|
||||||
ConfigSnapshot,
|
|
||||||
ConfigUiHints,
|
|
||||||
} from "../types";
|
|
||||||
import {
|
import {
|
||||||
cloneConfigObject,
|
cloneConfigObject,
|
||||||
removePathValue,
|
removePathValue,
|
||||||
@ -57,10 +53,7 @@ export async function loadConfigSchema(state: ConfigState) {
|
|||||||
if (state.configSchemaLoading) return;
|
if (state.configSchemaLoading) return;
|
||||||
state.configSchemaLoading = true;
|
state.configSchemaLoading = true;
|
||||||
try {
|
try {
|
||||||
const res = (await state.client.request(
|
const res = (await state.client.request("config.schema", {})) as ConfigSchemaResponse;
|
||||||
"config.schema",
|
|
||||||
{},
|
|
||||||
)) as ConfigSchemaResponse;
|
|
||||||
applyConfigSchema(state, res);
|
applyConfigSchema(state, res);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
state.lastError = String(err);
|
state.lastError = String(err);
|
||||||
@ -69,10 +62,7 @@ export async function loadConfigSchema(state: ConfigState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyConfigSchema(
|
export function applyConfigSchema(state: ConfigState, res: ConfigSchemaResponse) {
|
||||||
state: ConfigState,
|
|
||||||
res: ConfigSchemaResponse,
|
|
||||||
) {
|
|
||||||
state.configSchema = res.schema ?? null;
|
state.configSchema = res.schema ?? null;
|
||||||
state.configUiHints = res.uiHints ?? {};
|
state.configUiHints = res.uiHints ?? {};
|
||||||
state.configSchemaVersion = res.version ?? null;
|
state.configSchemaVersion = res.version ?? null;
|
||||||
@ -175,9 +165,7 @@ export function updateConfigFormValue(
|
|||||||
path: Array<string | number>,
|
path: Array<string | number>,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
) {
|
) {
|
||||||
const base = cloneConfigObject(
|
const base = cloneConfigObject(state.configForm ?? state.configSnapshot?.config ?? {});
|
||||||
state.configForm ?? state.configSnapshot?.config ?? {},
|
|
||||||
);
|
|
||||||
setPathValue(base, path, value);
|
setPathValue(base, path, value);
|
||||||
state.configForm = base;
|
state.configForm = base;
|
||||||
state.configFormDirty = true;
|
state.configFormDirty = true;
|
||||||
@ -186,13 +174,8 @@ export function updateConfigFormValue(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeConfigFormValue(
|
export function removeConfigFormValue(state: ConfigState, path: Array<string | number>) {
|
||||||
state: ConfigState,
|
const base = cloneConfigObject(state.configForm ?? state.configSnapshot?.config ?? {});
|
||||||
path: Array<string | number>,
|
|
||||||
) {
|
|
||||||
const base = cloneConfigObject(
|
|
||||||
state.configForm ?? state.configSnapshot?.config ?? {},
|
|
||||||
);
|
|
||||||
removePathValue(base, path);
|
removePathValue(base, path);
|
||||||
state.configForm = base;
|
state.configForm = base;
|
||||||
state.configFormDirty = true;
|
state.configFormDirty = true;
|
||||||
|
|||||||
@ -22,16 +22,14 @@ export function setPathValue(
|
|||||||
if (typeof key === "number") {
|
if (typeof key === "number") {
|
||||||
if (!Array.isArray(current)) return;
|
if (!Array.isArray(current)) return;
|
||||||
if (current[key] == null) {
|
if (current[key] == null) {
|
||||||
current[key] =
|
current[key] = typeof nextKey === "number" ? [] : ({} as Record<string, unknown>);
|
||||||
typeof nextKey === "number" ? [] : ({} as Record<string, unknown>);
|
|
||||||
}
|
}
|
||||||
current = current[key] as Record<string, unknown> | unknown[];
|
current = current[key] as Record<string, unknown> | unknown[];
|
||||||
} else {
|
} else {
|
||||||
if (typeof current !== "object" || current == null) return;
|
if (typeof current !== "object" || current == null) return;
|
||||||
const record = current as Record<string, unknown>;
|
const record = current as Record<string, unknown>;
|
||||||
if (record[key] == null) {
|
if (record[key] == null) {
|
||||||
record[key] =
|
record[key] = typeof nextKey === "number" ? [] : ({} as Record<string, unknown>);
|
||||||
typeof nextKey === "number" ? [] : ({} as Record<string, unknown>);
|
|
||||||
}
|
}
|
||||||
current = record[key] as Record<string, unknown> | unknown[];
|
current = record[key] as Record<string, unknown> | unknown[];
|
||||||
}
|
}
|
||||||
@ -59,9 +57,7 @@ export function removePathValue(
|
|||||||
current = current[key] as Record<string, unknown> | unknown[];
|
current = current[key] as Record<string, unknown> | unknown[];
|
||||||
} else {
|
} else {
|
||||||
if (typeof current !== "object" || current == null) return;
|
if (typeof current !== "object" || current == null) return;
|
||||||
current = (current as Record<string, unknown>)[key] as
|
current = (current as Record<string, unknown>)[key] as Record<string, unknown> | unknown[];
|
||||||
| Record<string, unknown>
|
|
||||||
| unknown[];
|
|
||||||
}
|
}
|
||||||
if (current == null) return;
|
if (current == null) return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -103,8 +103,7 @@ export async function addCronJob(state: CronState) {
|
|||||||
wakeMode: state.cronForm.wakeMode,
|
wakeMode: state.cronForm.wakeMode,
|
||||||
payload,
|
payload,
|
||||||
isolation:
|
isolation:
|
||||||
state.cronForm.postToMainPrefix.trim() &&
|
state.cronForm.postToMainPrefix.trim() && state.cronForm.sessionTarget === "isolated"
|
||||||
state.cronForm.sessionTarget === "isolated"
|
|
||||||
? { postToMainPrefix: state.cronForm.postToMainPrefix.trim() }
|
? { postToMainPrefix: state.cronForm.postToMainPrefix.trim() }
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
@ -125,11 +124,7 @@ export async function addCronJob(state: CronState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toggleCronJob(
|
export async function toggleCronJob(state: CronState, job: CronJob, enabled: boolean) {
|
||||||
state: CronState,
|
|
||||||
job: CronJob,
|
|
||||||
enabled: boolean,
|
|
||||||
) {
|
|
||||||
if (!state.client || !state.connected || state.cronBusy) return;
|
if (!state.client || !state.connected || state.cronBusy) return;
|
||||||
state.cronBusy = true;
|
state.cronBusy = true;
|
||||||
state.cronError = null;
|
state.cronError = null;
|
||||||
|
|||||||
@ -29,9 +29,7 @@ export async function loadDebug(state: DebugState) {
|
|||||||
state.debugStatus = status as StatusSummary;
|
state.debugStatus = status as StatusSummary;
|
||||||
state.debugHealth = health as HealthSnapshot;
|
state.debugHealth = health as HealthSnapshot;
|
||||||
const modelPayload = models as { models?: unknown[] } | undefined;
|
const modelPayload = models as { models?: unknown[] } | undefined;
|
||||||
state.debugModels = Array.isArray(modelPayload?.models)
|
state.debugModels = Array.isArray(modelPayload?.models) ? modelPayload?.models : [];
|
||||||
? modelPayload?.models
|
|
||||||
: [];
|
|
||||||
state.debugHeartbeat = heartbeat as unknown;
|
state.debugHeartbeat = heartbeat as unknown;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
state.debugCallError = String(err);
|
state.debugCallError = String(err);
|
||||||
|
|||||||
@ -118,9 +118,7 @@ export async function revokeDeviceToken(
|
|||||||
params: { deviceId: string; role: string },
|
params: { deviceId: string; role: string },
|
||||||
) {
|
) {
|
||||||
if (!state.client || !state.connected) return;
|
if (!state.client || !state.connected) return;
|
||||||
const confirmed = window.confirm(
|
const confirmed = window.confirm(`Revoke token for ${params.deviceId} (${params.role})?`);
|
||||||
`Revoke token for ${params.deviceId} (${params.role})?`,
|
|
||||||
);
|
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
try {
|
try {
|
||||||
await state.client.request("device.token.revoke", params);
|
await state.client.request("device.token.revoke", params);
|
||||||
|
|||||||
@ -80,6 +80,9 @@ export function addExecApproval(
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeExecApproval(queue: ExecApprovalRequest[], id: string): ExecApprovalRequest[] {
|
export function removeExecApproval(
|
||||||
|
queue: ExecApprovalRequest[],
|
||||||
|
id: string,
|
||||||
|
): ExecApprovalRequest[] {
|
||||||
return pruneExecApprovalQueue(queue).filter((entry) => entry.id !== id);
|
return pruneExecApprovalQueue(queue).filter((entry) => entry.id !== id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,9 +34,7 @@ export type ExecApprovalsSnapshot = {
|
|||||||
file: ExecApprovalsFile;
|
file: ExecApprovalsFile;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExecApprovalsTarget =
|
export type ExecApprovalsTarget = { kind: "gateway" } | { kind: "node"; nodeId: string };
|
||||||
| { kind: "gateway" }
|
|
||||||
| { kind: "node"; nodeId: string };
|
|
||||||
|
|
||||||
export type ExecApprovalsState = {
|
export type ExecApprovalsState = {
|
||||||
client: GatewayBrowserClient | null;
|
client: GatewayBrowserClient | null;
|
||||||
@ -120,10 +118,7 @@ export async function saveExecApprovals(
|
|||||||
state.lastError = "Exec approvals hash missing; reload and retry.";
|
state.lastError = "Exec approvals hash missing; reload and retry.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const file =
|
const file = state.execApprovalsForm ?? state.execApprovalsSnapshot?.file ?? {};
|
||||||
state.execApprovalsForm ??
|
|
||||||
state.execApprovalsSnapshot?.file ??
|
|
||||||
{};
|
|
||||||
const rpc = resolveExecApprovalsSaveRpc(target, { file, baseHash });
|
const rpc = resolveExecApprovalsSaveRpc(target, { file, baseHash });
|
||||||
if (!rpc) {
|
if (!rpc) {
|
||||||
state.lastError = "Select a node before saving exec approvals.";
|
state.lastError = "Select a node before saving exec approvals.";
|
||||||
|
|||||||
@ -16,14 +16,7 @@ export type LogsState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LOG_BUFFER_LIMIT = 2000;
|
const LOG_BUFFER_LIMIT = 2000;
|
||||||
const LEVELS = new Set<LogLevel>([
|
const LEVELS = new Set<LogLevel>(["trace", "debug", "info", "warn", "error", "fatal"]);
|
||||||
"trace",
|
|
||||||
"debug",
|
|
||||||
"info",
|
|
||||||
"warn",
|
|
||||||
"error",
|
|
||||||
"fatal",
|
|
||||||
]);
|
|
||||||
|
|
||||||
function parseMaybeJsonString(value: unknown) {
|
function parseMaybeJsonString(value: unknown) {
|
||||||
if (typeof value !== "string") return null;
|
if (typeof value !== "string") return null;
|
||||||
@ -53,11 +46,7 @@ export function parseLogLine(line: string): LogEntry {
|
|||||||
? (obj._meta as Record<string, unknown>)
|
? (obj._meta as Record<string, unknown>)
|
||||||
: null;
|
: null;
|
||||||
const time =
|
const time =
|
||||||
typeof obj.time === "string"
|
typeof obj.time === "string" ? obj.time : typeof meta?.date === "string" ? meta?.date : null;
|
||||||
? obj.time
|
|
||||||
: typeof meta?.date === "string"
|
|
||||||
? meta?.date
|
|
||||||
: null;
|
|
||||||
const level = normalizeLevel(meta?.logLevelName ?? meta?.level);
|
const level = normalizeLevel(meta?.logLevelName ?? meta?.level);
|
||||||
|
|
||||||
const contextCandidate =
|
const contextCandidate =
|
||||||
@ -94,17 +83,14 @@ export function parseLogLine(line: string): LogEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadLogs(
|
export async function loadLogs(state: LogsState, opts?: { reset?: boolean; quiet?: boolean }) {
|
||||||
state: LogsState,
|
|
||||||
opts?: { reset?: boolean; quiet?: boolean },
|
|
||||||
) {
|
|
||||||
if (!state.client || !state.connected) return;
|
if (!state.client || !state.connected) return;
|
||||||
if (state.logsLoading && !opts?.quiet) return;
|
if (state.logsLoading && !opts?.quiet) return;
|
||||||
if (!opts?.quiet) state.logsLoading = true;
|
if (!opts?.quiet) state.logsLoading = true;
|
||||||
state.logsError = null;
|
state.logsError = null;
|
||||||
try {
|
try {
|
||||||
const res = await state.client.request("logs.tail", {
|
const res = await state.client.request("logs.tail", {
|
||||||
cursor: opts?.reset ? undefined : state.logsCursor ?? undefined,
|
cursor: opts?.reset ? undefined : (state.logsCursor ?? undefined),
|
||||||
limit: state.logsLimit,
|
limit: state.logsLimit,
|
||||||
maxBytes: state.logsMaxBytes,
|
maxBytes: state.logsMaxBytes,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,10 +8,7 @@ export type NodesState = {
|
|||||||
lastError: string | null;
|
lastError: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function loadNodes(
|
export async function loadNodes(state: NodesState, opts?: { quiet?: boolean }) {
|
||||||
state: NodesState,
|
|
||||||
opts?: { quiet?: boolean },
|
|
||||||
) {
|
|
||||||
if (!state.client || !state.connected) return;
|
if (!state.client || !state.connected) return;
|
||||||
if (state.nodesLoading) return;
|
if (state.nodesLoading) return;
|
||||||
state.nodesLoading = true;
|
state.nodesLoading = true;
|
||||||
|
|||||||
@ -17,9 +17,7 @@ export async function loadPresence(state: PresenceState) {
|
|||||||
state.presenceError = null;
|
state.presenceError = null;
|
||||||
state.presenceStatus = null;
|
state.presenceStatus = null;
|
||||||
try {
|
try {
|
||||||
const res = (await state.client.request("system-presence", {})) as
|
const res = (await state.client.request("system-presence", {})) as PresenceEntry[] | undefined;
|
||||||
| PresenceEntry[]
|
|
||||||
| undefined;
|
|
||||||
if (Array.isArray(res)) {
|
if (Array.isArray(res)) {
|
||||||
state.presenceEntries = res;
|
state.presenceEntries = res;
|
||||||
state.presenceStatus = res.length === 0 ? "No instances yet." : null;
|
state.presenceStatus = res.length === 0 ? "No instances yet." : null;
|
||||||
|
|||||||
@ -45,9 +45,7 @@ export async function loadSkills(state: SkillsState, options?: LoadSkillsOptions
|
|||||||
state.skillsLoading = true;
|
state.skillsLoading = true;
|
||||||
state.skillsError = null;
|
state.skillsError = null;
|
||||||
try {
|
try {
|
||||||
const res = (await state.client.request("skills.status", {})) as
|
const res = (await state.client.request("skills.status", {})) as SkillStatusReport | undefined;
|
||||||
| SkillStatusReport
|
|
||||||
| undefined;
|
|
||||||
if (res) state.skillsReport = res;
|
if (res) state.skillsReport = res;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
state.skillsError = getErrorMessage(err);
|
state.skillsError = getErrorMessage(err);
|
||||||
@ -56,19 +54,11 @@ export async function loadSkills(state: SkillsState, options?: LoadSkillsOptions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateSkillEdit(
|
export function updateSkillEdit(state: SkillsState, skillKey: string, value: string) {
|
||||||
state: SkillsState,
|
|
||||||
skillKey: string,
|
|
||||||
value: string,
|
|
||||||
) {
|
|
||||||
state.skillEdits = { ...state.skillEdits, [skillKey]: value };
|
state.skillEdits = { ...state.skillEdits, [skillKey]: value };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSkillEnabled(
|
export async function updateSkillEnabled(state: SkillsState, skillKey: string, enabled: boolean) {
|
||||||
state: SkillsState,
|
|
||||||
skillKey: string,
|
|
||||||
enabled: boolean,
|
|
||||||
) {
|
|
||||||
if (!state.client || !state.connected) return;
|
if (!state.client || !state.connected) return;
|
||||||
state.skillsBusyKey = skillKey;
|
state.skillsBusyKey = skillKey;
|
||||||
state.skillsError = null;
|
state.skillsError = null;
|
||||||
|
|||||||
@ -36,9 +36,7 @@ describe("chat focus mode", () => {
|
|||||||
expect(shell).not.toBeNull();
|
expect(shell).not.toBeNull();
|
||||||
expect(shell?.classList.contains("shell--chat-focus")).toBe(false);
|
expect(shell?.classList.contains("shell--chat-focus")).toBe(false);
|
||||||
|
|
||||||
const toggle = app.querySelector<HTMLButtonElement>(
|
const toggle = app.querySelector<HTMLButtonElement>('button[title^="Toggle focus mode"]');
|
||||||
'button[title^="Toggle focus mode"]',
|
|
||||||
);
|
|
||||||
expect(toggle).not.toBeNull();
|
expect(toggle).not.toBeNull();
|
||||||
toggle?.click();
|
toggle?.click();
|
||||||
|
|
||||||
@ -47,9 +45,7 @@ describe("chat focus mode", () => {
|
|||||||
|
|
||||||
const link = app.querySelector<HTMLAnchorElement>('a.nav-item[href="/channels"]');
|
const link = app.querySelector<HTMLAnchorElement>('a.nav-item[href="/channels"]');
|
||||||
expect(link).not.toBeNull();
|
expect(link).not.toBeNull();
|
||||||
link?.dispatchEvent(
|
link?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }));
|
||||||
new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }),
|
|
||||||
);
|
|
||||||
|
|
||||||
await app.updateComplete;
|
await app.updateComplete;
|
||||||
expect(app.tab).toBe("channels");
|
expect(app.tab).toBe("channels");
|
||||||
|
|||||||
@ -42,7 +42,10 @@ export function clampText(value: string, max = 120): string {
|
|||||||
return `${value.slice(0, Math.max(0, max - 1))}…`;
|
return `${value.slice(0, Math.max(0, max - 1))}…`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function truncateText(value: string, max: number): {
|
export function truncateText(
|
||||||
|
value: string,
|
||||||
|
max: number,
|
||||||
|
): {
|
||||||
text: string;
|
text: string;
|
||||||
truncated: boolean;
|
truncated: boolean;
|
||||||
total: number;
|
total: number;
|
||||||
|
|||||||
@ -5,40 +5,223 @@ import { html, type TemplateResult } from "lit";
|
|||||||
|
|
||||||
export const icons = {
|
export const icons = {
|
||||||
// Navigation icons
|
// Navigation icons
|
||||||
messageSquare: html`<svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`,
|
messageSquare: html`
|
||||||
barChart: html`<svg viewBox="0 0 24 24"><line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/></svg>`,
|
<svg viewBox="0 0 24 24">
|
||||||
link: html`<svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`,
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||||
radio: html`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg>`,
|
</svg>
|
||||||
fileText: html`<svg viewBox="0 0 24 24"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="16" x2="8" y1="13" y2="13"/><line x1="16" x2="8" y1="17" y2="17"/><line x1="10" x2="8" y1="9" y2="9"/></svg>`,
|
`,
|
||||||
zap: html`<svg viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>`,
|
barChart: html`
|
||||||
monitor: html`<svg viewBox="0 0 24 24"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>`,
|
<svg viewBox="0 0 24 24">
|
||||||
settings: html`<svg viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>`,
|
<line x1="12" x2="12" y1="20" y2="10" />
|
||||||
bug: html`<svg viewBox="0 0 24 24"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>`,
|
<line x1="18" x2="18" y1="20" y2="4" />
|
||||||
scrollText: html`<svg viewBox="0 0 24 24"><path d="M8 21h12a2 2 0 0 0 2-2v-2H10v2a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v3h4"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M15 8h-5"/><path d="M15 12h-5"/></svg>`,
|
<line x1="6" x2="6" y1="20" y2="16" />
|
||||||
folder: html`<svg viewBox="0 0 24 24"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>`,
|
</svg>
|
||||||
|
`,
|
||||||
|
link: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||||
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
radio: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<circle cx="12" cy="12" r="2" />
|
||||||
|
<path
|
||||||
|
d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
fileText: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
||||||
|
<polyline points="14 2 14 8 20 8" />
|
||||||
|
<line x1="16" x2="8" y1="13" y2="13" />
|
||||||
|
<line x1="16" x2="8" y1="17" y2="17" />
|
||||||
|
<line x1="10" x2="8" y1="9" y2="9" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
zap: html`
|
||||||
|
<svg viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" /></svg>
|
||||||
|
`,
|
||||||
|
monitor: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<rect width="20" height="14" x="2" y="3" rx="2" />
|
||||||
|
<line x1="8" x2="16" y1="21" y2="21" />
|
||||||
|
<line x1="12" x2="12" y1="17" y2="21" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
settings: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
bug: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="m8 2 1.88 1.88" />
|
||||||
|
<path d="M14.12 3.88 16 2" />
|
||||||
|
<path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1" />
|
||||||
|
<path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6" />
|
||||||
|
<path d="M12 20v-9" />
|
||||||
|
<path d="M6.53 9C4.6 8.8 3 7.1 3 5" />
|
||||||
|
<path d="M6 13H2" />
|
||||||
|
<path d="M3 21c0-2.1 1.7-3.9 3.8-4" />
|
||||||
|
<path d="M20.97 5c0 2.1-1.6 3.8-3.5 4" />
|
||||||
|
<path d="M22 13h-4" />
|
||||||
|
<path d="M17.2 17c2.1.1 3.8 1.9 3.8 4" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
scrollText: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M8 21h12a2 2 0 0 0 2-2v-2H10v2a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v3h4" />
|
||||||
|
<path d="M19 17V5a2 2 0 0 0-2-2H4" />
|
||||||
|
<path d="M15 8h-5" />
|
||||||
|
<path d="M15 12h-5" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
folder: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
|
||||||
// UI icons
|
// UI icons
|
||||||
menu: html`<svg viewBox="0 0 24 24"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>`,
|
menu: html`
|
||||||
x: html`<svg viewBox="0 0 24 24"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`,
|
<svg viewBox="0 0 24 24">
|
||||||
check: html`<svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>`,
|
<line x1="4" x2="20" y1="12" y2="12" />
|
||||||
copy: html`<svg viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`,
|
<line x1="4" x2="20" y1="6" y2="6" />
|
||||||
search: html`<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>`,
|
<line x1="4" x2="20" y1="18" y2="18" />
|
||||||
brain: html`<svg viewBox="0 0 24 24"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M17.599 6.5a3 3 0 0 0 .399-1.375"/><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"/><path d="M3.477 10.896a4 4 0 0 1 .585-.396"/><path d="M19.938 10.5a4 4 0 0 1 .585.396"/><path d="M6 18a4 4 0 0 1-1.967-.516"/><path d="M19.967 17.484A4 4 0 0 1 18 18"/></svg>`,
|
</svg>
|
||||||
book: html`<svg viewBox="0 0 24 24"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>`,
|
`,
|
||||||
loader: html`<svg viewBox="0 0 24 24"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg>`,
|
x: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M18 6 6 18" />
|
||||||
|
<path d="m6 6 12 12" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
check: html`
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5" /></svg>
|
||||||
|
`,
|
||||||
|
copy: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
||||||
|
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
search: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<circle cx="11" cy="11" r="8" />
|
||||||
|
<path d="m21 21-4.3-4.3" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
brain: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" />
|
||||||
|
<path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z" />
|
||||||
|
<path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4" />
|
||||||
|
<path d="M17.599 6.5a3 3 0 0 0 .399-1.375" />
|
||||||
|
<path d="M6.003 5.125A3 3 0 0 0 6.401 6.5" />
|
||||||
|
<path d="M3.477 10.896a4 4 0 0 1 .585-.396" />
|
||||||
|
<path d="M19.938 10.5a4 4 0 0 1 .585.396" />
|
||||||
|
<path d="M6 18a4 4 0 0 1-1.967-.516" />
|
||||||
|
<path d="M19.967 17.484A4 4 0 0 1 18 18" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
book: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
loader: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M12 2v4" />
|
||||||
|
<path d="m16.2 7.8 2.9-2.9" />
|
||||||
|
<path d="M18 12h4" />
|
||||||
|
<path d="m16.2 16.2 2.9 2.9" />
|
||||||
|
<path d="M12 18v4" />
|
||||||
|
<path d="m4.9 19.1 2.9-2.9" />
|
||||||
|
<path d="M2 12h4" />
|
||||||
|
<path d="m4.9 4.9 2.9 2.9" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
|
||||||
// Tool icons
|
// Tool icons
|
||||||
wrench: html`<svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>`,
|
wrench: html`
|
||||||
fileCode: html`<svg viewBox="0 0 24 24"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><path d="m10 13-2 2 2 2"/><path d="m14 17 2-2-2-2"/></svg>`,
|
<svg viewBox="0 0 24 24">
|
||||||
edit: html`<svg viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`,
|
<path
|
||||||
penLine: html`<svg viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>`,
|
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
|
||||||
paperclip: html`<svg viewBox="0 0 24 24"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>`,
|
/>
|
||||||
globe: html`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>`,
|
</svg>
|
||||||
image: html`<svg viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`,
|
`,
|
||||||
smartphone: html`<svg viewBox="0 0 24 24"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"/><path d="M12 18h.01"/></svg>`,
|
fileCode: html`
|
||||||
plug: html`<svg viewBox="0 0 24 24"><path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"/></svg>`,
|
<svg viewBox="0 0 24 24">
|
||||||
circle: html`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>`,
|
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
||||||
puzzle: html`<svg viewBox="0 0 24 24"><path d="M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.23 8.77c.24-.24.581-.353.917-.303.515.076.874.54 1.02 1.02a2.5 2.5 0 1 0 3.237-3.237c-.48-.146-.944-.505-1.02-1.02a.98.98 0 0 1 .303-.917l1.526-1.526A2.402 2.402 0 0 1 11.998 2c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.877.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.236 3.236c-.464.18-.894.527-.967 1.02Z"/></svg>`,
|
<polyline points="14 2 14 8 20 8" />
|
||||||
|
<path d="m10 13-2 2 2 2" />
|
||||||
|
<path d="m14 17 2-2-2-2" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
edit: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||||
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
penLine: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M12 20h9" />
|
||||||
|
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
paperclip: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
globe: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
|
||||||
|
<path d="M2 12h20" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
image: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
|
||||||
|
<circle cx="9" cy="9" r="2" />
|
||||||
|
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
smartphone: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<rect width="14" height="20" x="5" y="2" rx="2" ry="2" />
|
||||||
|
<path d="M12 18h.01" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
plug: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M12 22v-5" />
|
||||||
|
<path d="M9 8V2" />
|
||||||
|
<path d="M15 8V2" />
|
||||||
|
<path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z" />
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
circle: html`
|
||||||
|
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /></svg>
|
||||||
|
`,
|
||||||
|
puzzle: html`
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.23 8.77c.24-.24.581-.353.917-.303.515.076.874.54 1.02 1.02a2.5 2.5 0 1 0 3.237-3.237c-.48-.146-.944-.505-1.02-1.02a.98.98 0 0 1 .303-.917l1.526-1.526A2.402 2.402 0 0 1 11.998 2c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.877.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.236 3.236c-.464.18-.894.527-.967 1.02Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type IconName = keyof typeof icons;
|
export type IconName = keyof typeof icons;
|
||||||
@ -52,7 +235,10 @@ export function renderIcon(name: IconName, className = "nav-item__icon"): Templa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Legacy function for compatibility
|
// Legacy function for compatibility
|
||||||
export function renderEmojiIcon(iconContent: string | TemplateResult, className: string): TemplateResult {
|
export function renderEmojiIcon(
|
||||||
|
iconContent: string | TemplateResult,
|
||||||
|
className: string,
|
||||||
|
): TemplateResult {
|
||||||
return html`<span class=${className} aria-hidden="true">${iconContent}</span>`;
|
return html`<span class=${className} aria-hidden="true">${iconContent}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -75,13 +75,9 @@ describe("control UI routing", () => {
|
|||||||
const app = mountApp("/chat");
|
const app = mountApp("/chat");
|
||||||
await app.updateComplete;
|
await app.updateComplete;
|
||||||
|
|
||||||
const link = app.querySelector<HTMLAnchorElement>(
|
const link = app.querySelector<HTMLAnchorElement>('a.nav-item[href="/channels"]');
|
||||||
'a.nav-item[href="/channels"]',
|
|
||||||
);
|
|
||||||
expect(link).not.toBeNull();
|
expect(link).not.toBeNull();
|
||||||
link?.dispatchEvent(
|
link?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }));
|
||||||
new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }),
|
|
||||||
);
|
|
||||||
|
|
||||||
await app.updateComplete;
|
await app.updateComplete;
|
||||||
expect(app.tab).toBe("channels");
|
expect(app.tab).toBe("channels");
|
||||||
|
|||||||
@ -37,9 +37,7 @@ const TAB_PATHS: Record<Tab, string> = {
|
|||||||
logs: "/logs",
|
logs: "/logs",
|
||||||
};
|
};
|
||||||
|
|
||||||
const PATH_TO_TAB = new Map(
|
const PATH_TO_TAB = new Map(Object.entries(TAB_PATHS).map(([tab, path]) => [path, tab as Tab]));
|
||||||
Object.entries(TAB_PATHS).map(([tab, path]) => [path, tab as Tab]),
|
|
||||||
);
|
|
||||||
|
|
||||||
export function normalizeBasePath(basePath: string): string {
|
export function normalizeBasePath(basePath: string): string {
|
||||||
if (!basePath) return "";
|
if (!basePath) return "";
|
||||||
|
|||||||
@ -49,22 +49,16 @@ export function loadSettings(): UiSettings {
|
|||||||
? parsed.sessionKey.trim()
|
? parsed.sessionKey.trim()
|
||||||
: defaults.sessionKey,
|
: defaults.sessionKey,
|
||||||
lastActiveSessionKey:
|
lastActiveSessionKey:
|
||||||
typeof parsed.lastActiveSessionKey === "string" &&
|
typeof parsed.lastActiveSessionKey === "string" && parsed.lastActiveSessionKey.trim()
|
||||||
parsed.lastActiveSessionKey.trim()
|
|
||||||
? parsed.lastActiveSessionKey.trim()
|
? parsed.lastActiveSessionKey.trim()
|
||||||
: (typeof parsed.sessionKey === "string" &&
|
: (typeof parsed.sessionKey === "string" && parsed.sessionKey.trim()) ||
|
||||||
parsed.sessionKey.trim()) ||
|
|
||||||
defaults.lastActiveSessionKey,
|
defaults.lastActiveSessionKey,
|
||||||
theme:
|
theme:
|
||||||
parsed.theme === "light" ||
|
parsed.theme === "light" || parsed.theme === "dark" || parsed.theme === "system"
|
||||||
parsed.theme === "dark" ||
|
|
||||||
parsed.theme === "system"
|
|
||||||
? parsed.theme
|
? parsed.theme
|
||||||
: defaults.theme,
|
: defaults.theme,
|
||||||
chatFocusMode:
|
chatFocusMode:
|
||||||
typeof parsed.chatFocusMode === "boolean"
|
typeof parsed.chatFocusMode === "boolean" ? parsed.chatFocusMode : defaults.chatFocusMode,
|
||||||
? parsed.chatFocusMode
|
|
||||||
: defaults.chatFocusMode,
|
|
||||||
chatShowThinking:
|
chatShowThinking:
|
||||||
typeof parsed.chatShowThinking === "boolean"
|
typeof parsed.chatShowThinking === "boolean"
|
||||||
? parsed.chatShowThinking
|
? parsed.chatShowThinking
|
||||||
@ -76,12 +70,9 @@ export function loadSettings(): UiSettings {
|
|||||||
? parsed.splitRatio
|
? parsed.splitRatio
|
||||||
: defaults.splitRatio,
|
: defaults.splitRatio,
|
||||||
navCollapsed:
|
navCollapsed:
|
||||||
typeof parsed.navCollapsed === "boolean"
|
typeof parsed.navCollapsed === "boolean" ? parsed.navCollapsed : defaults.navCollapsed,
|
||||||
? parsed.navCollapsed
|
|
||||||
: defaults.navCollapsed,
|
|
||||||
navGroupsCollapsed:
|
navGroupsCollapsed:
|
||||||
typeof parsed.navGroupsCollapsed === "object" &&
|
typeof parsed.navGroupsCollapsed === "object" && parsed.navGroupsCollapsed !== null
|
||||||
parsed.navGroupsCollapsed !== null
|
|
||||||
? parsed.navGroupsCollapsed
|
? parsed.navGroupsCollapsed
|
||||||
: defaults.navGroupsCollapsed,
|
: defaults.navGroupsCollapsed,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -55,8 +55,7 @@ export const startThemeTransition = ({
|
|||||||
const document_ = documentReference as DocumentWithViewTransition;
|
const document_ = documentReference as DocumentWithViewTransition;
|
||||||
const prefersReducedMotion = hasReducedMotionPreference();
|
const prefersReducedMotion = hasReducedMotionPreference();
|
||||||
|
|
||||||
const canUseViewTransition =
|
const canUseViewTransition = Boolean(document_.startViewTransition) && !prefersReducedMotion;
|
||||||
Boolean(document_.startViewTransition) && !prefersReducedMotion;
|
|
||||||
|
|
||||||
if (canUseViewTransition) {
|
if (canUseViewTransition) {
|
||||||
let xPercent = 0.5;
|
let xPercent = 0.5;
|
||||||
@ -71,11 +70,7 @@ export const startThemeTransition = ({
|
|||||||
yPercent = clamp01(context.pointerClientY / window.innerHeight);
|
yPercent = clamp01(context.pointerClientY / window.innerHeight);
|
||||||
} else if (context?.element) {
|
} else if (context?.element) {
|
||||||
const rect = context.element.getBoundingClientRect();
|
const rect = context.element.getBoundingClientRect();
|
||||||
if (
|
if (rect.width > 0 && rect.height > 0 && typeof window !== "undefined") {
|
||||||
rect.width > 0 &&
|
|
||||||
rect.height > 0 &&
|
|
||||||
typeof window !== "undefined"
|
|
||||||
) {
|
|
||||||
xPercent = clamp01((rect.left + rect.width / 2) / window.innerWidth);
|
xPercent = clamp01((rect.left + rect.width / 2) / window.innerWidth);
|
||||||
yPercent = clamp01((rect.top + rect.height / 2) / window.innerHeight);
|
yPercent = clamp01((rect.top + rect.height / 2) / window.innerHeight);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,9 +5,7 @@ export function getSystemTheme(): ResolvedTheme {
|
|||||||
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
||||||
return "dark";
|
return "dark";
|
||||||
}
|
}
|
||||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
? "dark"
|
|
||||||
: "light";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveTheme(mode: ThemeMode): ResolvedTheme {
|
export function resolveTheme(mode: ThemeMode): ResolvedTheme {
|
||||||
|
|||||||
@ -90,7 +90,13 @@
|
|||||||
},
|
},
|
||||||
"act": {
|
"act": {
|
||||||
"label": "act",
|
"label": "act",
|
||||||
"detailKeys": ["request.kind", "request.ref", "request.selector", "request.text", "request.value"]
|
"detailKeys": [
|
||||||
|
"request.kind",
|
||||||
|
"request.ref",
|
||||||
|
"request.selector",
|
||||||
|
"request.text",
|
||||||
|
"request.value"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -117,9 +123,15 @@
|
|||||||
"approve": { "label": "approve", "detailKeys": ["requestId"] },
|
"approve": { "label": "approve", "detailKeys": ["requestId"] },
|
||||||
"reject": { "label": "reject", "detailKeys": ["requestId"] },
|
"reject": { "label": "reject", "detailKeys": ["requestId"] },
|
||||||
"notify": { "label": "notify", "detailKeys": ["node", "nodeId", "title", "body"] },
|
"notify": { "label": "notify", "detailKeys": ["node", "nodeId", "title", "body"] },
|
||||||
"camera_snap": { "label": "camera snap", "detailKeys": ["node", "nodeId", "facing", "deviceId"] },
|
"camera_snap": {
|
||||||
|
"label": "camera snap",
|
||||||
|
"detailKeys": ["node", "nodeId", "facing", "deviceId"]
|
||||||
|
},
|
||||||
"camera_list": { "label": "camera list", "detailKeys": ["node", "nodeId"] },
|
"camera_list": { "label": "camera list", "detailKeys": ["node", "nodeId"] },
|
||||||
"camera_clip": { "label": "camera clip", "detailKeys": ["node", "nodeId", "facing", "duration", "durationMs"] },
|
"camera_clip": {
|
||||||
|
"label": "camera clip",
|
||||||
|
"detailKeys": ["node", "nodeId", "facing", "duration", "durationMs"]
|
||||||
|
},
|
||||||
"screen_record": {
|
"screen_record": {
|
||||||
"label": "screen record",
|
"label": "screen record",
|
||||||
"detailKeys": ["node", "nodeId", "duration", "durationMs", "fps", "screenIndex"]
|
"detailKeys": ["node", "nodeId", "duration", "durationMs", "fps", "screenIndex"]
|
||||||
|
|||||||
@ -153,8 +153,7 @@ export function resolveToolDisplay(params: {
|
|||||||
detail = resolveWriteDetail(params.args);
|
detail = resolveWriteDetail(params.args);
|
||||||
}
|
}
|
||||||
|
|
||||||
const detailKeys =
|
const detailKeys = actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? [];
|
||||||
actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? [];
|
|
||||||
if (!detail && detailKeys.length > 0) {
|
if (!detail && detailKeys.length > 0) {
|
||||||
detail = resolveDetailFromKeys(params.args, detailKeys);
|
detail = resolveDetailFromKeys(params.args, detailKeys);
|
||||||
}
|
}
|
||||||
@ -192,7 +191,5 @@ export function formatToolSummary(display: ToolDisplay): string {
|
|||||||
|
|
||||||
function shortenHomeInString(input: string): string {
|
function shortenHomeInString(input: string): string {
|
||||||
if (!input) return input;
|
if (!input) return input;
|
||||||
return input
|
return input.replace(/\/Users\/[^/]+/g, "~").replace(/\/home\/[^/]+/g, "~");
|
||||||
.replace(/\/Users\/[^/]+/g, "~")
|
|
||||||
.replace(/\/home\/[^/]+/g, "~");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -514,13 +514,7 @@ export type StatusSummary = Record<string, unknown>;
|
|||||||
|
|
||||||
export type HealthSnapshot = Record<string, unknown>;
|
export type HealthSnapshot = Record<string, unknown>;
|
||||||
|
|
||||||
export type LogLevel =
|
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
||||||
| "trace"
|
|
||||||
| "debug"
|
|
||||||
| "info"
|
|
||||||
| "warn"
|
|
||||||
| "error"
|
|
||||||
| "fatal";
|
|
||||||
|
|
||||||
export type LogEntry = {
|
export type LogEntry = {
|
||||||
raw: string;
|
raw: string;
|
||||||
|
|||||||
@ -2,12 +2,7 @@ import { html } from "lit";
|
|||||||
|
|
||||||
import type { ConfigUiHints } from "../types";
|
import type { ConfigUiHints } from "../types";
|
||||||
import type { ChannelsProps } from "./channels.types";
|
import type { ChannelsProps } from "./channels.types";
|
||||||
import {
|
import { analyzeConfigSchema, renderNode, schemaType, type JsonSchema } from "./config-form";
|
||||||
analyzeConfigSchema,
|
|
||||||
renderNode,
|
|
||||||
schemaType,
|
|
||||||
type JsonSchema,
|
|
||||||
} from "./config-form";
|
|
||||||
|
|
||||||
type ChannelConfigFormProps = {
|
type ChannelConfigFormProps = {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
@ -61,9 +56,7 @@ function resolveChannelValue(
|
|||||||
(fromChannels && typeof fromChannels === "object"
|
(fromChannels && typeof fromChannels === "object"
|
||||||
? (fromChannels as Record<string, unknown>)
|
? (fromChannels as Record<string, unknown>)
|
||||||
: null) ??
|
: null) ??
|
||||||
(fallback && typeof fallback === "object"
|
(fallback && typeof fallback === "object" ? (fallback as Record<string, unknown>) : null);
|
||||||
? (fallback as Record<string, unknown>)
|
|
||||||
: null);
|
|
||||||
return resolved ?? {};
|
return resolved ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,11 +64,15 @@ export function renderChannelConfigForm(props: ChannelConfigFormProps) {
|
|||||||
const analysis = analyzeConfigSchema(props.schema);
|
const analysis = analyzeConfigSchema(props.schema);
|
||||||
const normalized = analysis.schema;
|
const normalized = analysis.schema;
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return html`<div class="callout danger">Schema unavailable. Use Raw.</div>`;
|
return html`
|
||||||
|
<div class="callout danger">Schema unavailable. Use Raw.</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
const node = resolveSchemaNode(normalized, ["channels", props.channelId]);
|
const node = resolveSchemaNode(normalized, ["channels", props.channelId]);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return html`<div class="callout danger">Channel config schema unavailable.</div>`;
|
return html`
|
||||||
|
<div class="callout danger">Channel config schema unavailable.</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
const configValue = props.configValue ?? {};
|
const configValue = props.configValue ?? {};
|
||||||
const value = resolveChannelValue(configValue, props.channelId);
|
const value = resolveChannelValue(configValue, props.channelId);
|
||||||
@ -95,16 +92,16 @@ export function renderChannelConfigForm(props: ChannelConfigFormProps) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderChannelConfigSection(params: {
|
export function renderChannelConfigSection(params: { channelId: string; props: ChannelsProps }) {
|
||||||
channelId: string;
|
|
||||||
props: ChannelsProps;
|
|
||||||
}) {
|
|
||||||
const { channelId, props } = params;
|
const { channelId, props } = params;
|
||||||
const disabled = props.configSaving || props.configSchemaLoading;
|
const disabled = props.configSaving || props.configSchemaLoading;
|
||||||
return html`
|
return html`
|
||||||
<div style="margin-top: 16px;">
|
<div style="margin-top: 16px;">
|
||||||
${props.configSchemaLoading
|
${
|
||||||
? html`<div class="muted">Loading config schema…</div>`
|
props.configSchemaLoading
|
||||||
|
? html`
|
||||||
|
<div class="muted">Loading config schema…</div>
|
||||||
|
`
|
||||||
: renderChannelConfigForm({
|
: renderChannelConfigForm({
|
||||||
channelId,
|
channelId,
|
||||||
configValue: props.configForm,
|
configValue: props.configForm,
|
||||||
@ -112,7 +109,8 @@ export function renderChannelConfigSection(params: {
|
|||||||
uiHints: props.configUiHints,
|
uiHints: props.configUiHints,
|
||||||
disabled,
|
disabled,
|
||||||
onPatch: props.onConfigPatch,
|
onPatch: props.onConfigPatch,
|
||||||
})}
|
})
|
||||||
|
}
|
||||||
<div class="row" style="margin-top: 12px;">
|
<div class="row" style="margin-top: 12px;">
|
||||||
<button
|
<button
|
||||||
class="btn primary"
|
class="btn primary"
|
||||||
|
|||||||
@ -37,18 +37,22 @@ export function renderDiscordCard(params: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${discord?.lastError
|
${
|
||||||
|
discord?.lastError
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||||
${discord.lastError}
|
${discord.lastError}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${discord?.probe
|
${
|
||||||
|
discord?.probe
|
||||||
? html`<div class="callout" style="margin-top: 12px;">
|
? html`<div class="callout" style="margin-top: 12px;">
|
||||||
Probe ${discord.probe.ok ? "ok" : "failed"} ·
|
Probe ${discord.probe.ok ? "ok" : "failed"} ·
|
||||||
${discord.probe.status ?? ""} ${discord.probe.error ?? ""}
|
${discord.probe.status ?? ""} ${discord.probe.error ?? ""}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${renderChannelConfigSection({ channelId: "discord", props })}
|
${renderChannelConfigSection({ channelId: "discord", props })}
|
||||||
|
|
||||||
|
|||||||
@ -34,9 +34,11 @@ export function renderGoogleChatCard(params: {
|
|||||||
<div>
|
<div>
|
||||||
<span class="label">Audience</span>
|
<span class="label">Audience</span>
|
||||||
<span>
|
<span>
|
||||||
${googleChat?.audienceType
|
${
|
||||||
|
googleChat?.audienceType
|
||||||
? `${googleChat.audienceType}${googleChat.audience ? ` · ${googleChat.audience}` : ""}`
|
? `${googleChat.audienceType}${googleChat.audience ? ` · ${googleChat.audience}` : ""}`
|
||||||
: "n/a"}
|
: "n/a"
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -49,18 +51,22 @@ export function renderGoogleChatCard(params: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${googleChat?.lastError
|
${
|
||||||
|
googleChat?.lastError
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||||
${googleChat.lastError}
|
${googleChat.lastError}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${googleChat?.probe
|
${
|
||||||
|
googleChat?.probe
|
||||||
? html`<div class="callout" style="margin-top: 12px;">
|
? html`<div class="callout" style="margin-top: 12px;">
|
||||||
Probe ${googleChat.probe.ok ? "ok" : "failed"} ·
|
Probe ${googleChat.probe.ok ? "ok" : "failed"} ·
|
||||||
${googleChat.probe.status ?? ""} ${googleChat.probe.error ?? ""}
|
${googleChat.probe.status ?? ""} ${googleChat.probe.error ?? ""}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${renderChannelConfigSection({ channelId: "googlechat", props })}
|
${renderChannelConfigSection({ channelId: "googlechat", props })}
|
||||||
|
|
||||||
|
|||||||
@ -37,18 +37,22 @@ export function renderIMessageCard(params: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${imessage?.lastError
|
${
|
||||||
|
imessage?.lastError
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||||
${imessage.lastError}
|
${imessage.lastError}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${imessage?.probe
|
${
|
||||||
|
imessage?.probe
|
||||||
? html`<div class="callout" style="margin-top: 12px;">
|
? html`<div class="callout" style="margin-top: 12px;">
|
||||||
Probe ${imessage.probe.ok ? "ok" : "failed"} ·
|
Probe ${imessage.probe.ok ? "ok" : "failed"} ·
|
||||||
${imessage.probe.error ?? ""}
|
${imessage.probe.error ?? ""}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${renderChannelConfigSection({ channelId: "imessage", props })}
|
${renderChannelConfigSection({ channelId: "imessage", props })}
|
||||||
|
|
||||||
|
|||||||
@ -82,7 +82,7 @@ export function renderNostrProfileForm(params: {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
help?: string;
|
help?: string;
|
||||||
} = {}
|
} = {},
|
||||||
) => {
|
) => {
|
||||||
const { type = "text", placeholder, maxLength, help } = opts;
|
const { type = "text", placeholder, maxLength, help } = opts;
|
||||||
const value = state.values[field] ?? "";
|
const value = state.values[field] ?? "";
|
||||||
@ -169,13 +169,17 @@ export function renderNostrProfileForm(params: {
|
|||||||
<div style="font-size: 12px; color: var(--text-muted);">Account: ${accountId}</div>
|
<div style="font-size: 12px; color: var(--text-muted);">Account: ${accountId}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${state.error
|
${
|
||||||
|
state.error
|
||||||
? html`<div class="callout danger" style="margin-bottom: 12px;">${state.error}</div>`
|
? html`<div class="callout danger" style="margin-bottom: 12px;">${state.error}</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${state.success
|
${
|
||||||
|
state.success
|
||||||
? html`<div class="callout success" style="margin-bottom: 12px;">${state.success}</div>`
|
? html`<div class="callout success" style="margin-bottom: 12px;">${state.success}</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${renderPicturePreview()}
|
${renderPicturePreview()}
|
||||||
|
|
||||||
@ -204,7 +208,8 @@ export function renderNostrProfileForm(params: {
|
|||||||
help: "HTTPS URL to your profile picture",
|
help: "HTTPS URL to your profile picture",
|
||||||
})}
|
})}
|
||||||
|
|
||||||
${state.showAdvanced
|
${
|
||||||
|
state.showAdvanced
|
||||||
? html`
|
? html`
|
||||||
<div style="border-top: 1px solid var(--border-color); padding-top: 12px; margin-top: 12px;">
|
<div style="border-top: 1px solid var(--border-color); padding-top: 12px; margin-top: 12px;">
|
||||||
<div style="font-weight: 500; margin-bottom: 12px; color: var(--text-muted);">Advanced</div>
|
<div style="font-weight: 500; margin-bottom: 12px; color: var(--text-muted);">Advanced</div>
|
||||||
@ -232,7 +237,8 @@ export function renderNostrProfileForm(params: {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
<div style="display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap;">
|
<div style="display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap;">
|
||||||
<button
|
<button
|
||||||
@ -267,11 +273,15 @@ export function renderNostrProfileForm(params: {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${isDirty
|
${
|
||||||
? html`<div style="font-size: 12px; color: var(--warning-color); margin-top: 8px;">
|
isDirty
|
||||||
|
? html`
|
||||||
|
<div style="font-size: 12px; color: var(--warning-color); margin-top: 8px">
|
||||||
You have unsaved changes
|
You have unsaved changes
|
||||||
</div>`
|
</div>
|
||||||
: nothing}
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -284,7 +294,7 @@ export function renderNostrProfileForm(params: {
|
|||||||
* Create initial form state from existing profile
|
* Create initial form state from existing profile
|
||||||
*/
|
*/
|
||||||
export function createNostrProfileFormState(
|
export function createNostrProfileFormState(
|
||||||
profile: NostrProfileType | undefined
|
profile: NostrProfileType | undefined,
|
||||||
): NostrProfileFormState {
|
): NostrProfileFormState {
|
||||||
const values: NostrProfileType = {
|
const values: NostrProfileType = {
|
||||||
name: profile?.name ?? "",
|
name: profile?.name ?? "",
|
||||||
@ -305,8 +315,6 @@ export function createNostrProfileFormState(
|
|||||||
error: null,
|
error: null,
|
||||||
success: null,
|
success: null,
|
||||||
fieldErrors: {},
|
fieldErrors: {},
|
||||||
showAdvanced: Boolean(
|
showAdvanced: Boolean(profile?.banner || profile?.website || profile?.nip05 || profile?.lud16),
|
||||||
profile?.banner || profile?.website || profile?.nip05 || profile?.lud16
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,8 +44,7 @@ export function renderNostrCard(params: {
|
|||||||
const summaryConfigured = nostr?.configured ?? primaryAccount?.configured ?? false;
|
const summaryConfigured = nostr?.configured ?? primaryAccount?.configured ?? false;
|
||||||
const summaryRunning = nostr?.running ?? primaryAccount?.running ?? false;
|
const summaryRunning = nostr?.running ?? primaryAccount?.running ?? false;
|
||||||
const summaryPublicKey =
|
const summaryPublicKey =
|
||||||
nostr?.publicKey ??
|
nostr?.publicKey ?? (primaryAccount as { publicKey?: string } | undefined)?.publicKey;
|
||||||
(primaryAccount as { publicKey?: string } | undefined)?.publicKey;
|
|
||||||
const summaryLastStartAt = nostr?.lastStartAt ?? primaryAccount?.lastStartAt ?? null;
|
const summaryLastStartAt = nostr?.lastStartAt ?? primaryAccount?.lastStartAt ?? null;
|
||||||
const summaryLastError = nostr?.lastError ?? primaryAccount?.lastError ?? null;
|
const summaryLastError = nostr?.lastError ?? primaryAccount?.lastError ?? null;
|
||||||
const hasMultipleAccounts = nostrAccounts.length > 1;
|
const hasMultipleAccounts = nostrAccounts.length > 1;
|
||||||
@ -79,11 +78,13 @@ export function renderNostrCard(params: {
|
|||||||
<span class="label">Last inbound</span>
|
<span class="label">Last inbound</span>
|
||||||
<span>${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}</span>
|
<span>${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}</span>
|
||||||
</div>
|
</div>
|
||||||
${account.lastError
|
${
|
||||||
|
account.lastError
|
||||||
? html`
|
? html`
|
||||||
<div class="account-card-error">${account.lastError}</div>
|
<div class="account-card-error">${account.lastError}</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -100,7 +101,8 @@ export function renderNostrCard(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const profile =
|
const profile =
|
||||||
(primaryAccount as
|
(
|
||||||
|
primaryAccount as
|
||||||
| {
|
| {
|
||||||
profile?: {
|
profile?: {
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -110,7 +112,8 @@ export function renderNostrCard(params: {
|
|||||||
nip05?: string;
|
nip05?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| undefined)?.profile ?? nostr?.profile;
|
| undefined
|
||||||
|
)?.profile ?? nostr?.profile;
|
||||||
const { name, displayName, about, picture, nip05 } = profile ?? {};
|
const { name, displayName, about, picture, nip05 } = profile ?? {};
|
||||||
const hasAnyProfileData = name || displayName || about || picture || nip05;
|
const hasAnyProfileData = name || displayName || about || picture || nip05;
|
||||||
|
|
||||||
@ -118,7 +121,8 @@ export function renderNostrCard(params: {
|
|||||||
<div style="margin-top: 16px; padding: 12px; background: var(--bg-secondary); border-radius: 8px;">
|
<div style="margin-top: 16px; padding: 12px; background: var(--bg-secondary); border-radius: 8px;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||||
<div style="font-weight: 500;">Profile</div>
|
<div style="font-weight: 500;">Profile</div>
|
||||||
${summaryConfigured
|
${
|
||||||
|
summaryConfigured
|
||||||
? html`
|
? html`
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm"
|
class="btn btn-sm"
|
||||||
@ -128,12 +132,15 @@ export function renderNostrCard(params: {
|
|||||||
Edit Profile
|
Edit Profile
|
||||||
</button>
|
</button>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
${hasAnyProfileData
|
${
|
||||||
|
hasAnyProfileData
|
||||||
? html`
|
? html`
|
||||||
<div class="status-list">
|
<div class="status-list">
|
||||||
${picture
|
${
|
||||||
|
picture
|
||||||
? html`
|
? html`
|
||||||
<div style="margin-bottom: 8px;">
|
<div style="margin-bottom: 8px;">
|
||||||
<img
|
<img
|
||||||
@ -146,22 +153,28 @@ export function renderNostrCard(params: {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
${name ? html`<div><span class="label">Name</span><span>${name}</span></div>` : nothing}
|
${name ? html`<div><span class="label">Name</span><span>${name}</span></div>` : nothing}
|
||||||
${displayName
|
${
|
||||||
|
displayName
|
||||||
? html`<div><span class="label">Display Name</span><span>${displayName}</span></div>`
|
? html`<div><span class="label">Display Name</span><span>${displayName}</span></div>`
|
||||||
: nothing}
|
: nothing
|
||||||
${about
|
}
|
||||||
|
${
|
||||||
|
about
|
||||||
? html`<div><span class="label">About</span><span style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;">${about}</span></div>`
|
? html`<div><span class="label">About</span><span style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;">${about}</span></div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
${nip05 ? html`<div><span class="label">NIP-05</span><span>${nip05}</span></div>` : nothing}
|
${nip05 ? html`<div><span class="label">NIP-05</span><span>${nip05}</span></div>` : nothing}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<div style="color: var(--text-muted); font-size: 13px;">
|
<div style="color: var(--text-muted); font-size: 13px">
|
||||||
No profile set. Click "Edit Profile" to add your name, bio, and avatar.
|
No profile set. Click "Edit Profile" to add your name, bio, and avatar.
|
||||||
</div>
|
</div>
|
||||||
`}
|
`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
@ -172,7 +185,8 @@ export function renderNostrCard(params: {
|
|||||||
<div class="card-sub">Decentralized DMs via Nostr relays (NIP-04).</div>
|
<div class="card-sub">Decentralized DMs via Nostr relays (NIP-04).</div>
|
||||||
${accountCountLabel}
|
${accountCountLabel}
|
||||||
|
|
||||||
${hasMultipleAccounts
|
${
|
||||||
|
hasMultipleAccounts
|
||||||
? html`
|
? html`
|
||||||
<div class="account-card-list">
|
<div class="account-card-list">
|
||||||
${nostrAccounts.map((account) => renderAccountCard(account))}
|
${nostrAccounts.map((account) => renderAccountCard(account))}
|
||||||
@ -199,11 +213,14 @@ export function renderNostrCard(params: {
|
|||||||
<span>${summaryLastStartAt ? formatAgo(summaryLastStartAt) : "n/a"}</span>
|
<span>${summaryLastStartAt ? formatAgo(summaryLastStartAt) : "n/a"}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`}
|
`
|
||||||
|
}
|
||||||
|
|
||||||
${summaryLastError
|
${
|
||||||
|
summaryLastError
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">${summaryLastError}</div>`
|
? html`<div class="callout danger" style="margin-top: 12px;">${summaryLastError}</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${renderProfileSection()}
|
${renderProfileSection()}
|
||||||
|
|
||||||
|
|||||||
@ -41,18 +41,22 @@ export function renderSignalCard(params: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${signal?.lastError
|
${
|
||||||
|
signal?.lastError
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||||
${signal.lastError}
|
${signal.lastError}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${signal?.probe
|
${
|
||||||
|
signal?.probe
|
||||||
? html`<div class="callout" style="margin-top: 12px;">
|
? html`<div class="callout" style="margin-top: 12px;">
|
||||||
Probe ${signal.probe.ok ? "ok" : "failed"} ·
|
Probe ${signal.probe.ok ? "ok" : "failed"} ·
|
||||||
${signal.probe.status ?? ""} ${signal.probe.error ?? ""}
|
${signal.probe.status ?? ""} ${signal.probe.error ?? ""}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${renderChannelConfigSection({ channelId: "signal", props })}
|
${renderChannelConfigSection({ channelId: "signal", props })}
|
||||||
|
|
||||||
|
|||||||
@ -37,18 +37,22 @@ export function renderSlackCard(params: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${slack?.lastError
|
${
|
||||||
|
slack?.lastError
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||||
${slack.lastError}
|
${slack.lastError}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${slack?.probe
|
${
|
||||||
|
slack?.probe
|
||||||
? html`<div class="callout" style="margin-top: 12px;">
|
? html`<div class="callout" style="margin-top: 12px;">
|
||||||
Probe ${slack.probe.ok ? "ok" : "failed"} ·
|
Probe ${slack.probe.ok ? "ok" : "failed"} ·
|
||||||
${slack.probe.status ?? ""} ${slack.probe.error ?? ""}
|
${slack.probe.status ?? ""} ${slack.probe.error ?? ""}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${renderChannelConfigSection({ channelId: "slack", props })}
|
${renderChannelConfigSection({ channelId: "slack", props })}
|
||||||
|
|
||||||
|
|||||||
@ -39,13 +39,15 @@ export function renderTelegramCard(params: {
|
|||||||
<span class="label">Last inbound</span>
|
<span class="label">Last inbound</span>
|
||||||
<span>${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}</span>
|
<span>${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}</span>
|
||||||
</div>
|
</div>
|
||||||
${account.lastError
|
${
|
||||||
|
account.lastError
|
||||||
? html`
|
? html`
|
||||||
<div class="account-card-error">
|
<div class="account-card-error">
|
||||||
${account.lastError}
|
${account.lastError}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -57,7 +59,8 @@ export function renderTelegramCard(params: {
|
|||||||
<div class="card-sub">Bot status and channel configuration.</div>
|
<div class="card-sub">Bot status and channel configuration.</div>
|
||||||
${accountCountLabel}
|
${accountCountLabel}
|
||||||
|
|
||||||
${hasMultipleAccounts
|
${
|
||||||
|
hasMultipleAccounts
|
||||||
? html`
|
? html`
|
||||||
<div class="account-card-list">
|
<div class="account-card-list">
|
||||||
${telegramAccounts.map((account) => renderAccountCard(account))}
|
${telegramAccounts.map((account) => renderAccountCard(account))}
|
||||||
@ -86,20 +89,25 @@ export function renderTelegramCard(params: {
|
|||||||
<span>${telegram?.lastProbeAt ? formatAgo(telegram.lastProbeAt) : "n/a"}</span>
|
<span>${telegram?.lastProbeAt ? formatAgo(telegram.lastProbeAt) : "n/a"}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`}
|
`
|
||||||
|
}
|
||||||
|
|
||||||
${telegram?.lastError
|
${
|
||||||
|
telegram?.lastError
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||||
${telegram.lastError}
|
${telegram.lastError}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${telegram?.probe
|
${
|
||||||
|
telegram?.probe
|
||||||
? html`<div class="callout" style="margin-top: 12px;">
|
? html`<div class="callout" style="margin-top: 12px;">
|
||||||
Probe ${telegram.probe.ok ? "ok" : "failed"} ·
|
Probe ${telegram.probe.ok ? "ok" : "failed"} ·
|
||||||
${telegram.probe.status ?? ""} ${telegram.probe.error ?? ""}
|
${telegram.probe.status ?? ""} ${telegram.probe.error ?? ""}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${renderChannelConfigSection({ channelId: "telegram", props })}
|
${renderChannelConfigSection({ channelId: "telegram", props })}
|
||||||
|
|
||||||
|
|||||||
@ -15,11 +15,7 @@ import type {
|
|||||||
TelegramStatus,
|
TelegramStatus,
|
||||||
WhatsAppStatus,
|
WhatsAppStatus,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import type {
|
import type { ChannelKey, ChannelsChannelData, ChannelsProps } from "./channels.types";
|
||||||
ChannelKey,
|
|
||||||
ChannelsChannelData,
|
|
||||||
ChannelsProps,
|
|
||||||
} from "./channels.types";
|
|
||||||
import { channelEnabled, renderChannelAccountCount } from "./channels.shared";
|
import { channelEnabled, renderChannelAccountCount } from "./channels.shared";
|
||||||
import { renderChannelConfigSection } from "./channels.config";
|
import { renderChannelConfigSection } from "./channels.config";
|
||||||
import { renderDiscordCard } from "./channels.discord";
|
import { renderDiscordCard } from "./channels.discord";
|
||||||
@ -33,12 +29,8 @@ import { renderWhatsAppCard } from "./channels.whatsapp";
|
|||||||
|
|
||||||
export function renderChannels(props: ChannelsProps) {
|
export function renderChannels(props: ChannelsProps) {
|
||||||
const channels = props.snapshot?.channels as Record<string, unknown> | null;
|
const channels = props.snapshot?.channels as Record<string, unknown> | null;
|
||||||
const whatsapp = (channels?.whatsapp ?? undefined) as
|
const whatsapp = (channels?.whatsapp ?? undefined) as WhatsAppStatus | undefined;
|
||||||
| WhatsAppStatus
|
const telegram = (channels?.telegram ?? undefined) as TelegramStatus | undefined;
|
||||||
| undefined;
|
|
||||||
const telegram = (channels?.telegram ?? undefined) as
|
|
||||||
| TelegramStatus
|
|
||||||
| undefined;
|
|
||||||
const discord = (channels?.discord ?? null) as DiscordStatus | null;
|
const discord = (channels?.discord ?? null) as DiscordStatus | null;
|
||||||
const googlechat = (channels?.googlechat ?? null) as GoogleChatStatus | null;
|
const googlechat = (channels?.googlechat ?? null) as GoogleChatStatus | null;
|
||||||
const slack = (channels?.slack ?? null) as SlackStatus | null;
|
const slack = (channels?.slack ?? null) as SlackStatus | null;
|
||||||
@ -82,11 +74,13 @@ export function renderChannels(props: ChannelsProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="muted">${props.lastSuccessAt ? formatAgo(props.lastSuccessAt) : "n/a"}</div>
|
<div class="muted">${props.lastSuccessAt ? formatAgo(props.lastSuccessAt) : "n/a"}</div>
|
||||||
</div>
|
</div>
|
||||||
${props.lastError
|
${
|
||||||
|
props.lastError
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||||
${props.lastError}
|
${props.lastError}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
<pre class="code-block" style="margin-top: 12px;">
|
<pre class="code-block" style="margin-top: 12px;">
|
||||||
${props.snapshot ? JSON.stringify(props.snapshot, null, 2) : "No snapshot yet."}
|
${props.snapshot ? JSON.stringify(props.snapshot, null, 2) : "No snapshot yet."}
|
||||||
</pre>
|
</pre>
|
||||||
@ -101,27 +95,11 @@ function resolveChannelOrder(snapshot: ChannelsStatusSnapshot | null): ChannelKe
|
|||||||
if (snapshot?.channelOrder?.length) {
|
if (snapshot?.channelOrder?.length) {
|
||||||
return snapshot.channelOrder;
|
return snapshot.channelOrder;
|
||||||
}
|
}
|
||||||
return [
|
return ["whatsapp", "telegram", "discord", "googlechat", "slack", "signal", "imessage", "nostr"];
|
||||||
"whatsapp",
|
|
||||||
"telegram",
|
|
||||||
"discord",
|
|
||||||
"googlechat",
|
|
||||||
"slack",
|
|
||||||
"signal",
|
|
||||||
"imessage",
|
|
||||||
"nostr",
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderChannel(
|
function renderChannel(key: ChannelKey, props: ChannelsProps, data: ChannelsChannelData) {
|
||||||
key: ChannelKey,
|
const accountCountLabel = renderChannelAccountCount(key, data.channelAccounts);
|
||||||
props: ChannelsProps,
|
|
||||||
data: ChannelsChannelData,
|
|
||||||
) {
|
|
||||||
const accountCountLabel = renderChannelAccountCount(
|
|
||||||
key,
|
|
||||||
data.channelAccounts,
|
|
||||||
);
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "whatsapp":
|
case "whatsapp":
|
||||||
return renderWhatsAppCard({
|
return renderWhatsAppCard({
|
||||||
@ -218,7 +196,8 @@ function renderGenericChannelCard(
|
|||||||
<div class="card-sub">Channel status and configuration.</div>
|
<div class="card-sub">Channel status and configuration.</div>
|
||||||
${accountCountLabel}
|
${accountCountLabel}
|
||||||
|
|
||||||
${accounts.length > 0
|
${
|
||||||
|
accounts.length > 0
|
||||||
? html`
|
? html`
|
||||||
<div class="account-card-list">
|
<div class="account-card-list">
|
||||||
${accounts.map((account) => renderGenericAccount(account))}
|
${accounts.map((account) => renderGenericAccount(account))}
|
||||||
@ -239,13 +218,16 @@ function renderGenericChannelCard(
|
|||||||
<span>${connected == null ? "n/a" : connected ? "Yes" : "No"}</span>
|
<span>${connected == null ? "n/a" : connected ? "Yes" : "No"}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`}
|
`
|
||||||
|
}
|
||||||
|
|
||||||
${lastError
|
${
|
||||||
|
lastError
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||||
${lastError}
|
${lastError}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${renderChannelConfigSection({ channelId: key, props })}
|
${renderChannelConfigSection({ channelId: key, props })}
|
||||||
</div>
|
</div>
|
||||||
@ -259,10 +241,7 @@ function resolveChannelMetaMap(
|
|||||||
return Object.fromEntries(snapshot.channelMeta.map((entry) => [entry.id, entry]));
|
return Object.fromEntries(snapshot.channelMeta.map((entry) => [entry.id, entry]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveChannelLabel(
|
function resolveChannelLabel(snapshot: ChannelsStatusSnapshot | null, key: string): string {
|
||||||
snapshot: ChannelsStatusSnapshot | null,
|
|
||||||
key: string,
|
|
||||||
): string {
|
|
||||||
const meta = resolveChannelMetaMap(snapshot)[key];
|
const meta = resolveChannelMetaMap(snapshot)[key];
|
||||||
return meta?.label ?? snapshot?.channelLabels?.[key] ?? key;
|
return meta?.label ?? snapshot?.channelLabels?.[key] ?? key;
|
||||||
}
|
}
|
||||||
@ -316,13 +295,15 @@ function renderGenericAccount(account: ChannelAccountSnapshot) {
|
|||||||
<span class="label">Last inbound</span>
|
<span class="label">Last inbound</span>
|
||||||
<span>${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}</span>
|
<span>${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}</span>
|
||||||
</div>
|
</div>
|
||||||
${account.lastError
|
${
|
||||||
|
account.lastError
|
||||||
? html`
|
? html`
|
||||||
<div class="account-card-error">
|
<div class="account-card-error">
|
||||||
${account.lastError}
|
${account.lastError}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -39,9 +39,7 @@ export function renderWhatsAppCard(params: {
|
|||||||
<div>
|
<div>
|
||||||
<span class="label">Last connect</span>
|
<span class="label">Last connect</span>
|
||||||
<span>
|
<span>
|
||||||
${whatsapp?.lastConnectedAt
|
${whatsapp?.lastConnectedAt ? formatAgo(whatsapp.lastConnectedAt) : "n/a"}
|
||||||
? formatAgo(whatsapp.lastConnectedAt)
|
|
||||||
: "n/a"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -53,30 +51,34 @@ export function renderWhatsAppCard(params: {
|
|||||||
<div>
|
<div>
|
||||||
<span class="label">Auth age</span>
|
<span class="label">Auth age</span>
|
||||||
<span>
|
<span>
|
||||||
${whatsapp?.authAgeMs != null
|
${whatsapp?.authAgeMs != null ? formatDuration(whatsapp.authAgeMs) : "n/a"}
|
||||||
? formatDuration(whatsapp.authAgeMs)
|
|
||||||
: "n/a"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${whatsapp?.lastError
|
${
|
||||||
|
whatsapp?.lastError
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||||
${whatsapp.lastError}
|
${whatsapp.lastError}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${props.whatsappMessage
|
${
|
||||||
|
props.whatsappMessage
|
||||||
? html`<div class="callout" style="margin-top: 12px;">
|
? html`<div class="callout" style="margin-top: 12px;">
|
||||||
${props.whatsappMessage}
|
${props.whatsappMessage}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${props.whatsappQrDataUrl
|
${
|
||||||
|
props.whatsappQrDataUrl
|
||||||
? html`<div class="qr-wrap">
|
? html`<div class="qr-wrap">
|
||||||
<img src=${props.whatsappQrDataUrl} alt="WhatsApp QR" />
|
<img src=${props.whatsappQrDataUrl} alt="WhatsApp QR" />
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
<div class="row" style="margin-top: 14px; flex-wrap: wrap;">
|
<div class="row" style="margin-top: 14px; flex-wrap: wrap;">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -3,16 +3,13 @@ import { repeat } from "lit/directives/repeat.js";
|
|||||||
import type { SessionsListResult } from "../types";
|
import type { SessionsListResult } from "../types";
|
||||||
import type { ChatAttachment, ChatQueueItem } from "../ui-types";
|
import type { ChatAttachment, ChatQueueItem } from "../ui-types";
|
||||||
import type { ChatItem, MessageGroup } from "../types/chat-types";
|
import type { ChatItem, MessageGroup } from "../types/chat-types";
|
||||||
import { icons } from "../icons";
|
import { normalizeMessage, normalizeRoleForGrouping } from "../chat/message-normalizer";
|
||||||
import {
|
|
||||||
normalizeMessage,
|
|
||||||
normalizeRoleForGrouping,
|
|
||||||
} from "../chat/message-normalizer";
|
|
||||||
import {
|
import {
|
||||||
renderMessageGroup,
|
renderMessageGroup,
|
||||||
renderReadingIndicatorGroup,
|
renderReadingIndicatorGroup,
|
||||||
renderStreamingGroup,
|
renderStreamingGroup,
|
||||||
} from "../chat/grouped-render";
|
} from "../chat/grouped-render";
|
||||||
|
import { icons } from "../icons";
|
||||||
import { renderMarkdownSidebar } from "./markdown-sidebar";
|
import { renderMarkdownSidebar } from "./markdown-sidebar";
|
||||||
import "../components/resizable-divider";
|
import "../components/resizable-divider";
|
||||||
|
|
||||||
@ -78,7 +75,7 @@ function renderCompactionIndicator(status: CompactionIndicatorStatus | null | un
|
|||||||
if (status.active) {
|
if (status.active) {
|
||||||
return html`
|
return html`
|
||||||
<div class="callout info compaction-indicator compaction-indicator--active">
|
<div class="callout info compaction-indicator compaction-indicator--active">
|
||||||
${icons.loader} Compacting context...
|
🧹 Compacting context...
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -89,7 +86,7 @@ function renderCompactionIndicator(status: CompactionIndicatorStatus | null | un
|
|||||||
if (elapsed < COMPACTION_TOAST_DURATION_MS) {
|
if (elapsed < COMPACTION_TOAST_DURATION_MS) {
|
||||||
return html`
|
return html`
|
||||||
<div class="callout success compaction-indicator compaction-indicator--complete">
|
<div class="callout success compaction-indicator compaction-indicator--complete">
|
||||||
${icons.check} Context compacted
|
🧹 Context compacted
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -178,9 +175,7 @@ export function renderChat(props: ChatProps) {
|
|||||||
const canCompose = props.connected;
|
const canCompose = props.connected;
|
||||||
const isBusy = props.sending || props.stream !== null;
|
const isBusy = props.sending || props.stream !== null;
|
||||||
const canAbort = Boolean(props.canAbort && props.onAbort);
|
const canAbort = Boolean(props.canAbort && props.onAbort);
|
||||||
const activeSession = props.sessions?.sessions?.find(
|
const activeSession = props.sessions?.sessions?.find((row) => row.key === props.sessionKey);
|
||||||
(row) => row.key === props.sessionKey,
|
|
||||||
);
|
|
||||||
const reasoningLevel = activeSession?.reasoningLevel ?? "off";
|
const reasoningLevel = activeSession?.reasoningLevel ?? "off";
|
||||||
const showReasoning = props.showThinking && reasoningLevel !== "off";
|
const showReasoning = props.showThinking && reasoningLevel !== "off";
|
||||||
const assistantIdentity = {
|
const assistantIdentity = {
|
||||||
@ -204,8 +199,17 @@ export function renderChat(props: ChatProps) {
|
|||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
@scroll=${props.onChatScroll}
|
@scroll=${props.onChatScroll}
|
||||||
>
|
>
|
||||||
${props.loading ? html`<div class="muted">Loading chat…</div>` : nothing}
|
${
|
||||||
${repeat(buildChatItems(props), (item) => item.key, (item) => {
|
props.loading
|
||||||
|
? html`
|
||||||
|
<div class="muted">Loading chat…</div>
|
||||||
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
${repeat(
|
||||||
|
buildChatItems(props),
|
||||||
|
(item) => item.key,
|
||||||
|
(item) => {
|
||||||
if (item.kind === "reading-indicator") {
|
if (item.kind === "reading-indicator") {
|
||||||
return renderReadingIndicatorGroup(assistantIdentity);
|
return renderReadingIndicatorGroup(assistantIdentity);
|
||||||
}
|
}
|
||||||
@ -229,23 +233,21 @@ export function renderChat(props: ChatProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nothing;
|
return nothing;
|
||||||
})}
|
},
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<section class="card chat">
|
<section class="card chat">
|
||||||
${props.disabledReason
|
${props.disabledReason ? html`<div class="callout">${props.disabledReason}</div>` : nothing}
|
||||||
? html`<div class="callout">${props.disabledReason}</div>`
|
|
||||||
: nothing}
|
|
||||||
|
|
||||||
${props.error
|
${props.error ? html`<div class="callout danger">${props.error}</div>` : nothing}
|
||||||
? html`<div class="callout danger">${props.error}</div>`
|
|
||||||
: nothing}
|
|
||||||
|
|
||||||
${renderCompactionIndicator(props.compactionStatus)}
|
${renderCompactionIndicator(props.compactionStatus)}
|
||||||
|
|
||||||
${props.focusMode
|
${
|
||||||
|
props.focusMode
|
||||||
? html`
|
? html`
|
||||||
<button
|
<button
|
||||||
class="chat-focus-exit"
|
class="chat-focus-exit"
|
||||||
@ -254,10 +256,11 @@ export function renderChat(props: ChatProps) {
|
|||||||
aria-label="Exit focus mode"
|
aria-label="Exit focus mode"
|
||||||
title="Exit focus mode"
|
title="Exit focus mode"
|
||||||
>
|
>
|
||||||
${icons.x}
|
✕
|
||||||
</button>
|
</button>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="chat-split-container ${sidebarOpen ? "chat-split-container--open" : ""}"
|
class="chat-split-container ${sidebarOpen ? "chat-split-container--open" : ""}"
|
||||||
@ -269,12 +272,12 @@ export function renderChat(props: ChatProps) {
|
|||||||
${thread}
|
${thread}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${sidebarOpen
|
${
|
||||||
|
sidebarOpen
|
||||||
? html`
|
? html`
|
||||||
<resizable-divider
|
<resizable-divider
|
||||||
.splitRatio=${splitRatio}
|
.splitRatio=${splitRatio}
|
||||||
@resize=${(e: CustomEvent) =>
|
@resize=${(e: CustomEvent) => props.onSplitRatioChange?.(e.detail.splitRatio)}
|
||||||
props.onSplitRatioChange?.(e.detail.splitRatio)}
|
|
||||||
></resizable-divider>
|
></resizable-divider>
|
||||||
<div class="chat-sidebar">
|
<div class="chat-sidebar">
|
||||||
${renderMarkdownSidebar({
|
${renderMarkdownSidebar({
|
||||||
@ -288,10 +291,12 @@ export function renderChat(props: ChatProps) {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${props.queue.length
|
${
|
||||||
|
props.queue.length
|
||||||
? html`
|
? html`
|
||||||
<div class="chat-queue" role="status" aria-live="polite">
|
<div class="chat-queue" role="status" aria-live="polite">
|
||||||
<div class="chat-queue__title">Queued (${props.queue.length})</div>
|
<div class="chat-queue__title">Queued (${props.queue.length})</div>
|
||||||
@ -311,7 +316,7 @@ export function renderChat(props: ChatProps) {
|
|||||||
aria-label="Remove queued message"
|
aria-label="Remove queued message"
|
||||||
@click=${() => props.onQueueRemove(item.id)}
|
@click=${() => props.onQueueRemove(item.id)}
|
||||||
>
|
>
|
||||||
${icons.x}
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
@ -319,7 +324,8 @@ export function renderChat(props: ChatProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
<div class="chat-compose">
|
<div class="chat-compose">
|
||||||
${renderAttachmentPreview(props)}
|
${renderAttachmentPreview(props)}
|
||||||
|
|||||||
@ -47,8 +47,7 @@ function normalizeSchemaNode(
|
|||||||
|
|
||||||
const nullable = Array.isArray(schema.type) && schema.type.includes("null");
|
const nullable = Array.isArray(schema.type) && schema.type.includes("null");
|
||||||
const type =
|
const type =
|
||||||
schemaType(schema) ??
|
schemaType(schema) ?? (schema.properties || schema.additionalProperties ? "object" : undefined);
|
||||||
(schema.properties || schema.additionalProperties ? "object" : undefined);
|
|
||||||
normalized.type = type ?? schema.type;
|
normalized.type = type ?? schema.type;
|
||||||
normalized.nullable = nullable || schema.nullable;
|
normalized.nullable = nullable || schema.nullable;
|
||||||
|
|
||||||
@ -73,24 +72,15 @@ function normalizeSchemaNode(
|
|||||||
unsupported.add(pathLabel);
|
unsupported.add(pathLabel);
|
||||||
} else if (schema.additionalProperties === false) {
|
} else if (schema.additionalProperties === false) {
|
||||||
normalized.additionalProperties = false;
|
normalized.additionalProperties = false;
|
||||||
} else if (
|
} else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
||||||
schema.additionalProperties &&
|
|
||||||
typeof schema.additionalProperties === "object"
|
|
||||||
) {
|
|
||||||
if (!isAnySchema(schema.additionalProperties as JsonSchema)) {
|
if (!isAnySchema(schema.additionalProperties as JsonSchema)) {
|
||||||
const res = normalizeSchemaNode(
|
const res = normalizeSchemaNode(schema.additionalProperties as JsonSchema, [...path, "*"]);
|
||||||
schema.additionalProperties as JsonSchema,
|
normalized.additionalProperties = res.schema ?? (schema.additionalProperties as JsonSchema);
|
||||||
[...path, "*"],
|
|
||||||
);
|
|
||||||
normalized.additionalProperties =
|
|
||||||
res.schema ?? (schema.additionalProperties as JsonSchema);
|
|
||||||
if (res.unsupportedPaths.length > 0) unsupported.add(pathLabel);
|
if (res.unsupportedPaths.length > 0) unsupported.add(pathLabel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (type === "array") {
|
} else if (type === "array") {
|
||||||
const itemsSchema = Array.isArray(schema.items)
|
const itemsSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;
|
||||||
? schema.items[0]
|
|
||||||
: schema.items;
|
|
||||||
if (!itemsSchema) {
|
if (!itemsSchema) {
|
||||||
unsupported.add(pathLabel);
|
unsupported.add(pathLabel);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -28,11 +28,69 @@ function jsonValue(value: unknown): string {
|
|||||||
|
|
||||||
// SVG Icons as template literals
|
// SVG Icons as template literals
|
||||||
const icons = {
|
const icons = {
|
||||||
chevronDown: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>`,
|
chevronDown: html`
|
||||||
plus: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
|
<svg
|
||||||
minus: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
|
viewBox="0 0 24 24"
|
||||||
trash: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>`,
|
fill="none"
|
||||||
edit: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
plus: html`
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
minus: html`
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
trash: html`
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="3 6 5 6 21 6"></polyline>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
edit: html`
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
||||||
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function renderNode(params: {
|
export function renderNode(params: {
|
||||||
@ -64,7 +122,7 @@ export function renderNode(params: {
|
|||||||
if (schema.anyOf || schema.oneOf) {
|
if (schema.anyOf || schema.oneOf) {
|
||||||
const variants = schema.anyOf ?? schema.oneOf ?? [];
|
const variants = schema.anyOf ?? schema.oneOf ?? [];
|
||||||
const nonNull = variants.filter(
|
const nonNull = variants.filter(
|
||||||
(v) => !(v.type === "null" || (Array.isArray(v.type) && v.type.includes("null")))
|
(v) => !(v.type === "null" || (Array.isArray(v.type) && v.type.includes("null"))),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (nonNull.length === 1) {
|
if (nonNull.length === 1) {
|
||||||
@ -88,16 +146,18 @@ export function renderNode(params: {
|
|||||||
${showLabel ? html`<label class="cfg-field__label">${label}</label>` : nothing}
|
${showLabel ? html`<label class="cfg-field__label">${label}</label>` : nothing}
|
||||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing}
|
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing}
|
||||||
<div class="cfg-segmented">
|
<div class="cfg-segmented">
|
||||||
${literals.map((lit, idx) => html`
|
${literals.map(
|
||||||
|
(lit) => html`
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="cfg-segmented__btn ${lit === resolvedValue || String(lit) === String(resolvedValue) ? 'active' : ''}"
|
class="cfg-segmented__btn ${lit === resolvedValue || String(lit) === String(resolvedValue) ? "active" : ""}"
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
@click=${() => onPatch(path, lit)}
|
@click=${() => onPatch(path, lit)}
|
||||||
>
|
>
|
||||||
${String(lit)}
|
${String(lit)}
|
||||||
</button>
|
</button>
|
||||||
`)}
|
`,
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -109,11 +169,9 @@ export function renderNode(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle mixed primitive types
|
// Handle mixed primitive types
|
||||||
const primitiveTypes = new Set(
|
const primitiveTypes = new Set(nonNull.map((variant) => schemaType(variant)).filter(Boolean));
|
||||||
nonNull.map((variant) => schemaType(variant)).filter(Boolean)
|
|
||||||
);
|
|
||||||
const normalizedTypes = new Set(
|
const normalizedTypes = new Set(
|
||||||
[...primitiveTypes].map((v) => (v === "integer" ? "number" : v))
|
[...primitiveTypes].map((v) => (v === "integer" ? "number" : v)),
|
||||||
);
|
);
|
||||||
|
|
||||||
if ([...normalizedTypes].every((v) => ["string", "number", "boolean"].includes(v as string))) {
|
if ([...normalizedTypes].every((v) => ["string", "number", "boolean"].includes(v as string))) {
|
||||||
@ -147,16 +205,18 @@ export function renderNode(params: {
|
|||||||
${showLabel ? html`<label class="cfg-field__label">${label}</label>` : nothing}
|
${showLabel ? html`<label class="cfg-field__label">${label}</label>` : nothing}
|
||||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing}
|
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing}
|
||||||
<div class="cfg-segmented">
|
<div class="cfg-segmented">
|
||||||
${options.map((opt) => html`
|
${options.map(
|
||||||
|
(opt) => html`
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="cfg-segmented__btn ${opt === resolvedValue || String(opt) === String(resolvedValue) ? 'active' : ''}"
|
class="cfg-segmented__btn ${opt === resolvedValue || String(opt) === String(resolvedValue) ? "active" : ""}"
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
@click=${() => onPatch(path, opt)}
|
@click=${() => onPatch(path, opt)}
|
||||||
>
|
>
|
||||||
${String(opt)}
|
${String(opt)}
|
||||||
</button>
|
</button>
|
||||||
`)}
|
`,
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -176,9 +236,14 @@ export function renderNode(params: {
|
|||||||
|
|
||||||
// Boolean - toggle row
|
// Boolean - toggle row
|
||||||
if (type === "boolean") {
|
if (type === "boolean") {
|
||||||
const displayValue = typeof value === "boolean" ? value : typeof schema.default === "boolean" ? schema.default : false;
|
const displayValue =
|
||||||
|
typeof value === "boolean"
|
||||||
|
? value
|
||||||
|
: typeof schema.default === "boolean"
|
||||||
|
? schema.default
|
||||||
|
: false;
|
||||||
return html`
|
return html`
|
||||||
<label class="cfg-toggle-row ${disabled ? 'disabled' : ''}">
|
<label class="cfg-toggle-row ${disabled ? "disabled" : ""}">
|
||||||
<div class="cfg-toggle-row__content">
|
<div class="cfg-toggle-row__content">
|
||||||
<span class="cfg-toggle-row__label">${label}</span>
|
<span class="cfg-toggle-row__label">${label}</span>
|
||||||
${help ? html`<span class="cfg-toggle-row__help">${help}</span>` : nothing}
|
${help ? html`<span class="cfg-toggle-row__help">${help}</span>` : nothing}
|
||||||
@ -261,7 +326,9 @@ function renderTextInput(params: {
|
|||||||
onPatch(path, raw);
|
onPatch(path, raw);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
${schema.default !== undefined ? html`
|
${
|
||||||
|
schema.default !== undefined
|
||||||
|
? html`
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="cfg-input__reset"
|
class="cfg-input__reset"
|
||||||
@ -269,7 +336,9 @@ function renderTextInput(params: {
|
|||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
@click=${() => onPatch(path, schema.default)}
|
@click=${() => onPatch(path, schema.default)}
|
||||||
>↺</button>
|
>↺</button>
|
||||||
` : nothing}
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -360,9 +429,11 @@ function renderSelect(params: {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value=${unset}>Select...</option>
|
<option value=${unset}>Select...</option>
|
||||||
${options.map((opt, idx) => html`
|
${options.map(
|
||||||
|
(opt, idx) => html`
|
||||||
<option value=${String(idx)}>${String(opt)}</option>
|
<option value=${String(idx)}>${String(opt)}</option>
|
||||||
`)}
|
`,
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -379,13 +450,14 @@ function renderObject(params: {
|
|||||||
onPatch: (path: Array<string | number>, value: unknown) => void;
|
onPatch: (path: Array<string | number>, value: unknown) => void;
|
||||||
}): TemplateResult {
|
}): TemplateResult {
|
||||||
const { schema, value, path, hints, unsupported, disabled, onPatch } = params;
|
const { schema, value, path, hints, unsupported, disabled, onPatch } = params;
|
||||||
const showLabel = params.showLabel ?? true;
|
const _showLabel = params.showLabel ?? true;
|
||||||
const hint = hintForPath(path, hints);
|
const hint = hintForPath(path, hints);
|
||||||
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
||||||
const help = hint?.help ?? schema.description;
|
const help = hint?.help ?? schema.description;
|
||||||
|
|
||||||
const fallback = value ?? schema.default;
|
const fallback = value ?? schema.default;
|
||||||
const obj = fallback && typeof fallback === "object" && !Array.isArray(fallback)
|
const obj =
|
||||||
|
fallback && typeof fallback === "object" && !Array.isArray(fallback)
|
||||||
? (fallback as Record<string, unknown>)
|
? (fallback as Record<string, unknown>)
|
||||||
: {};
|
: {};
|
||||||
const props = schema.properties ?? {};
|
const props = schema.properties ?? {};
|
||||||
@ -416,9 +488,11 @@ function renderObject(params: {
|
|||||||
unsupported,
|
unsupported,
|
||||||
disabled,
|
disabled,
|
||||||
onPatch,
|
onPatch,
|
||||||
})
|
}),
|
||||||
)}
|
)}
|
||||||
${allowExtra ? renderMapField({
|
${
|
||||||
|
allowExtra
|
||||||
|
? renderMapField({
|
||||||
schema: additional as JsonSchema,
|
schema: additional as JsonSchema,
|
||||||
value: obj,
|
value: obj,
|
||||||
path,
|
path,
|
||||||
@ -427,7 +501,9 @@ function renderObject(params: {
|
|||||||
disabled,
|
disabled,
|
||||||
reservedKeys: reserved,
|
reservedKeys: reserved,
|
||||||
onPatch,
|
onPatch,
|
||||||
}) : nothing}
|
})
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -450,9 +526,11 @@ function renderObject(params: {
|
|||||||
unsupported,
|
unsupported,
|
||||||
disabled,
|
disabled,
|
||||||
onPatch,
|
onPatch,
|
||||||
})
|
}),
|
||||||
)}
|
)}
|
||||||
${allowExtra ? renderMapField({
|
${
|
||||||
|
allowExtra
|
||||||
|
? renderMapField({
|
||||||
schema: additional as JsonSchema,
|
schema: additional as JsonSchema,
|
||||||
value: obj,
|
value: obj,
|
||||||
path,
|
path,
|
||||||
@ -461,7 +539,9 @@ function renderObject(params: {
|
|||||||
disabled,
|
disabled,
|
||||||
reservedKeys: reserved,
|
reservedKeys: reserved,
|
||||||
onPatch,
|
onPatch,
|
||||||
}) : nothing}
|
})
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
`;
|
`;
|
||||||
@ -499,7 +579,7 @@ function renderArray(params: {
|
|||||||
<div class="cfg-array">
|
<div class="cfg-array">
|
||||||
<div class="cfg-array__header">
|
<div class="cfg-array__header">
|
||||||
${showLabel ? html`<span class="cfg-array__label">${label}</span>` : nothing}
|
${showLabel ? html`<span class="cfg-array__label">${label}</span>` : nothing}
|
||||||
<span class="cfg-array__count">${arr.length} item${arr.length !== 1 ? 's' : ''}</span>
|
<span class="cfg-array__count">${arr.length} item${arr.length !== 1 ? "s" : ""}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="cfg-array__add"
|
class="cfg-array__add"
|
||||||
@ -515,13 +595,15 @@ function renderArray(params: {
|
|||||||
</div>
|
</div>
|
||||||
${help ? html`<div class="cfg-array__help">${help}</div>` : nothing}
|
${help ? html`<div class="cfg-array__help">${help}</div>` : nothing}
|
||||||
|
|
||||||
${arr.length === 0 ? html`
|
${
|
||||||
<div class="cfg-array__empty">
|
arr.length === 0
|
||||||
No items yet. Click "Add" to create one.
|
? html`
|
||||||
</div>
|
<div class="cfg-array__empty">No items yet. Click "Add" to create one.</div>
|
||||||
` : html`
|
`
|
||||||
|
: html`
|
||||||
<div class="cfg-array__items">
|
<div class="cfg-array__items">
|
||||||
${arr.map((item, idx) => html`
|
${arr.map(
|
||||||
|
(item, idx) => html`
|
||||||
<div class="cfg-array__item">
|
<div class="cfg-array__item">
|
||||||
<div class="cfg-array__item-header">
|
<div class="cfg-array__item-header">
|
||||||
<span class="cfg-array__item-index">#${idx + 1}</span>
|
<span class="cfg-array__item-index">#${idx + 1}</span>
|
||||||
@ -552,9 +634,11 @@ function renderArray(params: {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`)}
|
`,
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
`}
|
`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -582,7 +666,7 @@ function renderMapField(params: {
|
|||||||
class="cfg-map__add"
|
class="cfg-map__add"
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
const next = { ...(value ?? {}) };
|
const next = { ...value };
|
||||||
let index = 1;
|
let index = 1;
|
||||||
let key = `custom-${index}`;
|
let key = `custom-${index}`;
|
||||||
while (key in next) {
|
while (key in next) {
|
||||||
@ -598,9 +682,12 @@ function renderMapField(params: {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${entries.length === 0 ? html`
|
${
|
||||||
|
entries.length === 0
|
||||||
|
? html`
|
||||||
<div class="cfg-map__empty">No custom entries.</div>
|
<div class="cfg-map__empty">No custom entries.</div>
|
||||||
` : html`
|
`
|
||||||
|
: html`
|
||||||
<div class="cfg-map__items">
|
<div class="cfg-map__items">
|
||||||
${entries.map(([key, entryValue]) => {
|
${entries.map(([key, entryValue]) => {
|
||||||
const valuePath = [...path, key];
|
const valuePath = [...path, key];
|
||||||
@ -617,7 +704,7 @@ function renderMapField(params: {
|
|||||||
@change=${(e: Event) => {
|
@change=${(e: Event) => {
|
||||||
const nextKey = (e.target as HTMLInputElement).value.trim();
|
const nextKey = (e.target as HTMLInputElement).value.trim();
|
||||||
if (!nextKey || nextKey === key) return;
|
if (!nextKey || nextKey === key) return;
|
||||||
const next = { ...(value ?? {}) };
|
const next = { ...value };
|
||||||
if (nextKey in next) return;
|
if (nextKey in next) return;
|
||||||
next[nextKey] = next[key];
|
next[nextKey] = next[key];
|
||||||
delete next[key];
|
delete next[key];
|
||||||
@ -626,7 +713,8 @@ function renderMapField(params: {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="cfg-map__item-value">
|
<div class="cfg-map__item-value">
|
||||||
${anySchema
|
${
|
||||||
|
anySchema
|
||||||
? html`
|
? html`
|
||||||
<textarea
|
<textarea
|
||||||
class="cfg-textarea cfg-textarea--sm"
|
class="cfg-textarea cfg-textarea--sm"
|
||||||
@ -658,7 +746,8 @@ function renderMapField(params: {
|
|||||||
disabled,
|
disabled,
|
||||||
showLabel: false,
|
showLabel: false,
|
||||||
onPatch,
|
onPatch,
|
||||||
})}
|
})
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -666,7 +755,7 @@ function renderMapField(params: {
|
|||||||
title="Remove entry"
|
title="Remove entry"
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
const next = { ...(value ?? {}) };
|
const next = { ...value };
|
||||||
delete next[key];
|
delete next[key];
|
||||||
onPatch(path, next);
|
onPatch(path, next);
|
||||||
}}
|
}}
|
||||||
@ -677,7 +766,8 @@ function renderMapField(params: {
|
|||||||
`;
|
`;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
`}
|
`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,6 @@
|
|||||||
import { html, nothing } from "lit";
|
import { html, nothing } from "lit";
|
||||||
import type { ConfigUiHints } from "../types";
|
import type { ConfigUiHints } from "../types";
|
||||||
import { icons } from "../icons";
|
import { hintForPath, humanize, schemaType, type JsonSchema } from "./config-form.shared";
|
||||||
import {
|
|
||||||
hintForPath,
|
|
||||||
humanize,
|
|
||||||
schemaType,
|
|
||||||
type JsonSchema,
|
|
||||||
} from "./config-form.shared";
|
|
||||||
import { renderNode } from "./config-form.node";
|
import { renderNode } from "./config-form.node";
|
||||||
|
|
||||||
export type ConfigFormProps = {
|
export type ConfigFormProps = {
|
||||||
@ -23,44 +17,237 @@ export type ConfigFormProps = {
|
|||||||
|
|
||||||
// SVG Icons for section cards (Lucide-style)
|
// SVG Icons for section cards (Lucide-style)
|
||||||
const sectionIcons = {
|
const sectionIcons = {
|
||||||
env: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>`,
|
env: html`
|
||||||
update: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`,
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
agents: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2z"></path><circle cx="8" cy="14" r="1"></circle><circle cx="16" cy="14" r="1"></circle></svg>`,
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
auth: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`,
|
<path
|
||||||
channels: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`,
|
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
|
||||||
messages: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>`,
|
></path>
|
||||||
commands: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>`,
|
</svg>
|
||||||
hooks: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>`,
|
`,
|
||||||
skills: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>`,
|
update: html`
|
||||||
tools: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>`,
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
gateway: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>`,
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
wizard: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M15 4V2"></path><path d="M15 16v-2"></path><path d="M8 9h2"></path><path d="M20 9h2"></path><path d="M17.8 11.8 19 13"></path><path d="M15 9h0"></path><path d="M17.8 6.2 19 5"></path><path d="m3 21 9-9"></path><path d="M12.2 6.2 11 5"></path></svg>`,
|
<polyline points="7 10 12 15 17 10"></polyline>
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
agents: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path
|
||||||
|
d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2z"
|
||||||
|
></path>
|
||||||
|
<circle cx="8" cy="14" r="1"></circle>
|
||||||
|
<circle cx="16" cy="14" r="1"></circle>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
auth: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||||
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
channels: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
messages: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
|
||||||
|
<polyline points="22,6 12,13 2,6"></polyline>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
commands: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<polyline points="4 17 10 11 4 5"></polyline>
|
||||||
|
<line x1="12" y1="19" x2="20" y2="19"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
hooks: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
|
||||||
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
skills: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<polygon
|
||||||
|
points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
|
||||||
|
></polygon>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
tools: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path
|
||||||
|
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
gateway: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<line x1="2" y1="12" x2="22" y2="12"></line>
|
||||||
|
<path
|
||||||
|
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
wizard: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M15 4V2"></path>
|
||||||
|
<path d="M15 16v-2"></path>
|
||||||
|
<path d="M8 9h2"></path>
|
||||||
|
<path d="M20 9h2"></path>
|
||||||
|
<path d="M17.8 11.8 19 13"></path>
|
||||||
|
<path d="M15 9h0"></path>
|
||||||
|
<path d="M17.8 6.2 19 5"></path>
|
||||||
|
<path d="m3 21 9-9"></path>
|
||||||
|
<path d="M12.2 6.2 11 5"></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
// Additional sections
|
// Additional sections
|
||||||
meta: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 20h9"></path><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg>`,
|
meta: html`
|
||||||
logging: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`,
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
browser: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="4"></circle><line x1="21.17" y1="8" x2="12" y2="8"></line><line x1="3.95" y1="6.06" x2="8.54" y2="14"></line><line x1="10.88" y1="21.94" x2="15.46" y2="14"></line></svg>`,
|
<path d="M12 20h9"></path>
|
||||||
ui: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>`,
|
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path>
|
||||||
models: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>`,
|
</svg>
|
||||||
bindings: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>`,
|
`,
|
||||||
broadcast: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"></path><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"></path><circle cx="12" cy="12" r="2"></circle><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"></path><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"></path></svg>`,
|
logging: html`
|
||||||
audio: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>`,
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
session: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>`,
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||||
cron: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>`,
|
<polyline points="14 2 14 8 20 8"></polyline>
|
||||||
web: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>`,
|
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||||
discovery: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>`,
|
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||||
canvasHost: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>`,
|
<polyline points="10 9 9 9 8 9"></polyline>
|
||||||
talk: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg>`,
|
</svg>
|
||||||
plugins: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2v6"></path><path d="m4.93 10.93 4.24 4.24"></path><path d="M2 12h6"></path><path d="m4.93 13.07 4.24-4.24"></path><path d="M12 22v-6"></path><path d="m19.07 13.07-4.24-4.24"></path><path d="M22 12h-6"></path><path d="m19.07 10.93-4.24 4.24"></path></svg>`,
|
`,
|
||||||
default: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>`,
|
browser: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<circle cx="12" cy="12" r="4"></circle>
|
||||||
|
<line x1="21.17" y1="8" x2="12" y2="8"></line>
|
||||||
|
<line x1="3.95" y1="6.06" x2="8.54" y2="14"></line>
|
||||||
|
<line x1="10.88" y1="21.94" x2="15.46" y2="14"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
ui: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
|
<line x1="3" y1="9" x2="21" y2="9"></line>
|
||||||
|
<line x1="9" y1="21" x2="9" y2="9"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
models: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path
|
||||||
|
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"
|
||||||
|
></path>
|
||||||
|
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||||
|
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
bindings: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
||||||
|
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
||||||
|
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
||||||
|
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
broadcast: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"></path>
|
||||||
|
<path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"></path>
|
||||||
|
<circle cx="12" cy="12" r="2"></circle>
|
||||||
|
<path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"></path>
|
||||||
|
<path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
audio: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M9 18V5l12-2v13"></path>
|
||||||
|
<circle cx="6" cy="18" r="3"></circle>
|
||||||
|
<circle cx="18" cy="16" r="3"></circle>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
session: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||||
|
<circle cx="9" cy="7" r="4"></circle>
|
||||||
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
cron: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<polyline points="12 6 12 12 16 14"></polyline>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
web: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<line x1="2" y1="12" x2="22" y2="12"></line>
|
||||||
|
<path
|
||||||
|
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
discovery: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
canvasHost: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
|
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||||
|
<polyline points="21 15 16 10 5 21"></polyline>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
talk: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
|
||||||
|
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
|
||||||
|
<line x1="12" y1="19" x2="12" y2="23"></line>
|
||||||
|
<line x1="8" y1="23" x2="16" y2="23"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
plugins: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M12 2v6"></path>
|
||||||
|
<path d="m4.93 10.93 4.24 4.24"></path>
|
||||||
|
<path d="M2 12h6"></path>
|
||||||
|
<path d="m4.93 13.07 4.24-4.24"></path>
|
||||||
|
<path d="M12 22v-6"></path>
|
||||||
|
<path d="m19.07 13.07-4.24-4.24"></path>
|
||||||
|
<path d="M22 12h-6"></path>
|
||||||
|
<path d="m19.07 10.93-4.24 4.24"></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
default: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||||
|
<polyline points="14 2 14 8 20 8"></polyline>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Section metadata
|
// Section metadata
|
||||||
export const SECTION_META: Record<string, { label: string; description: string }> = {
|
export const SECTION_META: Record<string, { label: string; description: string }> = {
|
||||||
env: { label: "Environment Variables", description: "Environment variables passed to the gateway process" },
|
env: {
|
||||||
|
label: "Environment Variables",
|
||||||
|
description: "Environment variables passed to the gateway process",
|
||||||
|
},
|
||||||
update: { label: "Updates", description: "Auto-update settings and release channel" },
|
update: { label: "Updates", description: "Auto-update settings and release channel" },
|
||||||
agents: { label: "Agents", description: "Agent configurations, models, and identities" },
|
agents: { label: "Agents", description: "Agent configurations, models, and identities" },
|
||||||
auth: { label: "Authentication", description: "API keys and authentication profiles" },
|
auth: { label: "Authentication", description: "API keys and authentication profiles" },
|
||||||
channels: { label: "Channels", description: "Messaging channels (Telegram, Discord, Slack, etc.)" },
|
channels: {
|
||||||
|
label: "Channels",
|
||||||
|
description: "Messaging channels (Telegram, Discord, Slack, etc.)",
|
||||||
|
},
|
||||||
messages: { label: "Messages", description: "Message handling and routing settings" },
|
messages: { label: "Messages", description: "Message handling and routing settings" },
|
||||||
commands: { label: "Commands", description: "Custom slash commands" },
|
commands: { label: "Commands", description: "Custom slash commands" },
|
||||||
hooks: { label: "Hooks", description: "Webhooks and event hooks" },
|
hooks: { label: "Hooks", description: "Webhooks and event hooks" },
|
||||||
@ -142,12 +329,16 @@ function schemaMatches(schema: JsonSchema, query: string): boolean {
|
|||||||
|
|
||||||
export function renderConfigForm(props: ConfigFormProps) {
|
export function renderConfigForm(props: ConfigFormProps) {
|
||||||
if (!props.schema) {
|
if (!props.schema) {
|
||||||
return html`<div class="muted">Schema unavailable.</div>`;
|
return html`
|
||||||
|
<div class="muted">Schema unavailable.</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
const schema = props.schema;
|
const schema = props.schema;
|
||||||
const value = props.value ?? {};
|
const value = props.value ?? {};
|
||||||
if (schemaType(schema) !== "object" || !schema.properties) {
|
if (schemaType(schema) !== "object" || !schema.properties) {
|
||||||
return html`<div class="callout danger">Unsupported schema. Use Raw.</div>`;
|
return html`
|
||||||
|
<div class="callout danger">Unsupported schema. Use Raw.</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
const unsupported = new Set(props.unsupportedPaths ?? []);
|
const unsupported = new Set(props.unsupportedPaths ?? []);
|
||||||
const properties = schema.properties;
|
const properties = schema.properties;
|
||||||
@ -168,9 +359,8 @@ export function renderConfigForm(props: ConfigFormProps) {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
let subsectionContext:
|
let subsectionContext: { sectionKey: string; subsectionKey: string; schema: JsonSchema } | null =
|
||||||
| { sectionKey: string; subsectionKey: string; schema: JsonSchema }
|
null;
|
||||||
| null = null;
|
|
||||||
if (activeSection && activeSubsection && filteredEntries.length === 1) {
|
if (activeSection && activeSubsection && filteredEntries.length === 1) {
|
||||||
const sectionSchema = filteredEntries[0]?.[1];
|
const sectionSchema = filteredEntries[0]?.[1];
|
||||||
if (
|
if (
|
||||||
@ -190,11 +380,9 @@ export function renderConfigForm(props: ConfigFormProps) {
|
|||||||
if (filteredEntries.length === 0) {
|
if (filteredEntries.length === 0) {
|
||||||
return html`
|
return html`
|
||||||
<div class="config-empty">
|
<div class="config-empty">
|
||||||
<div class="config-empty__icon">${icons.search}</div>
|
<div class="config-empty__icon">🔍</div>
|
||||||
<div class="config-empty__text">
|
<div class="config-empty__text">
|
||||||
${searchQuery
|
${searchQuery ? `No settings match "${searchQuery}"` : "No settings in this section"}
|
||||||
? `No settings match "${searchQuery}"`
|
|
||||||
: "No settings in this section"}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -202,7 +390,8 @@ export function renderConfigForm(props: ConfigFormProps) {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="config-form config-form--modern">
|
<div class="config-form config-form--modern">
|
||||||
${subsectionContext
|
${
|
||||||
|
subsectionContext
|
||||||
? (() => {
|
? (() => {
|
||||||
const { sectionKey, subsectionKey, schema: node } = subsectionContext;
|
const { sectionKey, subsectionKey, schema: node } = subsectionContext;
|
||||||
const hint = hintForPath([sectionKey, subsectionKey], props.uiHints);
|
const hint = hintForPath([sectionKey, subsectionKey], props.uiHints);
|
||||||
@ -220,9 +409,11 @@ export function renderConfigForm(props: ConfigFormProps) {
|
|||||||
<span class="config-section-card__icon">${getSectionIcon(sectionKey)}</span>
|
<span class="config-section-card__icon">${getSectionIcon(sectionKey)}</span>
|
||||||
<div class="config-section-card__titles">
|
<div class="config-section-card__titles">
|
||||||
<h3 class="config-section-card__title">${label}</h3>
|
<h3 class="config-section-card__title">${label}</h3>
|
||||||
${description
|
${
|
||||||
|
description
|
||||||
? html`<p class="config-section-card__desc">${description}</p>`
|
? html`<p class="config-section-card__desc">${description}</p>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-section-card__content">
|
<div class="config-section-card__content">
|
||||||
@ -252,9 +443,11 @@ export function renderConfigForm(props: ConfigFormProps) {
|
|||||||
<span class="config-section-card__icon">${getSectionIcon(key)}</span>
|
<span class="config-section-card__icon">${getSectionIcon(key)}</span>
|
||||||
<div class="config-section-card__titles">
|
<div class="config-section-card__titles">
|
||||||
<h3 class="config-section-card__title">${meta.label}</h3>
|
<h3 class="config-section-card__title">${meta.label}</h3>
|
||||||
${meta.description
|
${
|
||||||
|
meta.description
|
||||||
? html`<p class="config-section-card__desc">${meta.description}</p>`
|
? html`<p class="config-section-card__desc">${meta.description}</p>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-section-card__content">
|
<div class="config-section-card__content">
|
||||||
@ -271,7 +464,8 @@ export function renderConfigForm(props: ConfigFormProps) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
})}
|
})
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
export { renderConfigForm, type ConfigFormProps, SECTION_META } from "./config-form.render";
|
export { renderConfigForm, type ConfigFormProps, SECTION_META } from "./config-form.render";
|
||||||
export {
|
export { analyzeConfigSchema, type ConfigSchemaAnalysis } from "./config-form.analyze";
|
||||||
analyzeConfigSchema,
|
|
||||||
type ConfigSchemaAnalysis,
|
|
||||||
} from "./config-form.analyze";
|
|
||||||
export { renderNode } from "./config-form.node";
|
export { renderNode } from "./config-form.node";
|
||||||
export { schemaType, type JsonSchema } from "./config-form.shared";
|
export { schemaType, type JsonSchema } from "./config-form.shared";
|
||||||
|
|||||||
@ -59,11 +59,9 @@ describe("config view", () => {
|
|||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
|
|
||||||
const saveButton = Array.from(
|
const saveButton = Array.from(container.querySelectorAll("button")).find(
|
||||||
container.querySelectorAll("button"),
|
(btn) => btn.textContent?.trim() === "Save",
|
||||||
).find((btn) => btn.textContent?.trim() === "Save") as
|
) as HTMLButtonElement | undefined;
|
||||||
| HTMLButtonElement
|
|
||||||
| undefined;
|
|
||||||
expect(saveButton).not.toBeUndefined();
|
expect(saveButton).not.toBeUndefined();
|
||||||
expect(saveButton?.disabled).toBe(false);
|
expect(saveButton?.disabled).toBe(false);
|
||||||
});
|
});
|
||||||
@ -81,11 +79,9 @@ describe("config view", () => {
|
|||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
|
|
||||||
const saveButton = Array.from(
|
const saveButton = Array.from(container.querySelectorAll("button")).find(
|
||||||
container.querySelectorAll("button"),
|
(btn) => btn.textContent?.trim() === "Save",
|
||||||
).find((btn) => btn.textContent?.trim() === "Save") as
|
) as HTMLButtonElement | undefined;
|
||||||
| HTMLButtonElement
|
|
||||||
| undefined;
|
|
||||||
expect(saveButton).not.toBeUndefined();
|
expect(saveButton).not.toBeUndefined();
|
||||||
expect(saveButton?.disabled).toBe(true);
|
expect(saveButton?.disabled).toBe(true);
|
||||||
});
|
});
|
||||||
@ -102,16 +98,12 @@ describe("config view", () => {
|
|||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
|
|
||||||
const saveButton = Array.from(
|
const saveButton = Array.from(container.querySelectorAll("button")).find(
|
||||||
container.querySelectorAll("button"),
|
(btn) => btn.textContent?.trim() === "Save",
|
||||||
).find((btn) => btn.textContent?.trim() === "Save") as
|
) as HTMLButtonElement | undefined;
|
||||||
| HTMLButtonElement
|
const applyButton = Array.from(container.querySelectorAll("button")).find(
|
||||||
| undefined;
|
(btn) => btn.textContent?.trim() === "Apply",
|
||||||
const applyButton = Array.from(
|
) as HTMLButtonElement | undefined;
|
||||||
container.querySelectorAll("button"),
|
|
||||||
).find((btn) => btn.textContent?.trim() === "Apply") as
|
|
||||||
| HTMLButtonElement
|
|
||||||
| undefined;
|
|
||||||
expect(saveButton).not.toBeUndefined();
|
expect(saveButton).not.toBeUndefined();
|
||||||
expect(applyButton).not.toBeUndefined();
|
expect(applyButton).not.toBeUndefined();
|
||||||
expect(saveButton?.disabled).toBe(true);
|
expect(saveButton?.disabled).toBe(true);
|
||||||
@ -124,22 +116,18 @@ describe("config view", () => {
|
|||||||
renderConfig({
|
renderConfig({
|
||||||
...baseProps(),
|
...baseProps(),
|
||||||
formMode: "raw",
|
formMode: "raw",
|
||||||
raw: "{\n gateway: { mode: \"local\" }\n}\n",
|
raw: '{\n gateway: { mode: "local" }\n}\n',
|
||||||
originalRaw: "{\n}\n",
|
originalRaw: "{\n}\n",
|
||||||
}),
|
}),
|
||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
|
|
||||||
const saveButton = Array.from(
|
const saveButton = Array.from(container.querySelectorAll("button")).find(
|
||||||
container.querySelectorAll("button"),
|
(btn) => btn.textContent?.trim() === "Save",
|
||||||
).find((btn) => btn.textContent?.trim() === "Save") as
|
) as HTMLButtonElement | undefined;
|
||||||
| HTMLButtonElement
|
const applyButton = Array.from(container.querySelectorAll("button")).find(
|
||||||
| undefined;
|
(btn) => btn.textContent?.trim() === "Apply",
|
||||||
const applyButton = Array.from(
|
) as HTMLButtonElement | undefined;
|
||||||
container.querySelectorAll("button"),
|
|
||||||
).find((btn) => btn.textContent?.trim() === "Apply") as
|
|
||||||
| HTMLButtonElement
|
|
||||||
| undefined;
|
|
||||||
expect(saveButton).not.toBeUndefined();
|
expect(saveButton).not.toBeUndefined();
|
||||||
expect(applyButton).not.toBeUndefined();
|
expect(applyButton).not.toBeUndefined();
|
||||||
expect(saveButton?.disabled).toBe(false);
|
expect(saveButton?.disabled).toBe(false);
|
||||||
@ -157,8 +145,8 @@ describe("config view", () => {
|
|||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
|
|
||||||
const btn = Array.from(container.querySelectorAll("button")).find((b) =>
|
const btn = Array.from(container.querySelectorAll("button")).find(
|
||||||
b.textContent?.trim() === "Raw",
|
(b) => b.textContent?.trim() === "Raw",
|
||||||
) as HTMLButtonElement | undefined;
|
) as HTMLButtonElement | undefined;
|
||||||
expect(btn).toBeTruthy();
|
expect(btn).toBeTruthy();
|
||||||
btn?.click();
|
btn?.click();
|
||||||
@ -183,8 +171,8 @@ describe("config view", () => {
|
|||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
|
|
||||||
const btn = Array.from(container.querySelectorAll("button")).find((b) =>
|
const btn = Array.from(container.querySelectorAll("button")).find(
|
||||||
b.textContent?.trim() === "Gateway",
|
(b) => b.textContent?.trim() === "Gateway",
|
||||||
) as HTMLButtonElement | undefined;
|
) as HTMLButtonElement | undefined;
|
||||||
expect(btn).toBeTruthy();
|
expect(btn).toBeTruthy();
|
||||||
btn?.click();
|
btn?.click();
|
||||||
@ -202,9 +190,7 @@ describe("config view", () => {
|
|||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
|
|
||||||
const input = container.querySelector(
|
const input = container.querySelector(".config-search__input") as HTMLInputElement | null;
|
||||||
".config-search__input",
|
|
||||||
) as HTMLInputElement | null;
|
|
||||||
expect(input).not.toBeNull();
|
expect(input).not.toBeNull();
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
input.value = "gateway";
|
input.value = "gateway";
|
||||||
|
|||||||
@ -1,12 +1,7 @@
|
|||||||
import { html, nothing } from "lit";
|
import { html, nothing } from "lit";
|
||||||
import type { ConfigUiHints } from "../types";
|
import type { ConfigUiHints } from "../types";
|
||||||
import { analyzeConfigSchema, renderConfigForm, SECTION_META } from "./config-form";
|
import { analyzeConfigSchema, renderConfigForm, SECTION_META } from "./config-form";
|
||||||
import {
|
import { hintForPath, humanize, schemaType, type JsonSchema } from "./config-form.shared";
|
||||||
hintForPath,
|
|
||||||
humanize,
|
|
||||||
schemaType,
|
|
||||||
type JsonSchema,
|
|
||||||
} from "./config-form.shared";
|
|
||||||
|
|
||||||
export type ConfigProps = {
|
export type ConfigProps = {
|
||||||
raw: string;
|
raw: string;
|
||||||
@ -41,36 +36,230 @@ export type ConfigProps = {
|
|||||||
|
|
||||||
// SVG Icons for sidebar (Lucide-style)
|
// SVG Icons for sidebar (Lucide-style)
|
||||||
const sidebarIcons = {
|
const sidebarIcons = {
|
||||||
all: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>`,
|
all: html`
|
||||||
env: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>`,
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
update: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`,
|
<rect x="3" y="3" width="7" height="7"></rect>
|
||||||
agents: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2z"></path><circle cx="8" cy="14" r="1"></circle><circle cx="16" cy="14" r="1"></circle></svg>`,
|
<rect x="14" y="3" width="7" height="7"></rect>
|
||||||
auth: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`,
|
<rect x="14" y="14" width="7" height="7"></rect>
|
||||||
channels: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`,
|
<rect x="3" y="14" width="7" height="7"></rect>
|
||||||
messages: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>`,
|
</svg>
|
||||||
commands: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>`,
|
`,
|
||||||
hooks: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>`,
|
env: html`
|
||||||
skills: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>`,
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
tools: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>`,
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
gateway: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>`,
|
<path
|
||||||
wizard: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 4V2"></path><path d="M15 16v-2"></path><path d="M8 9h2"></path><path d="M20 9h2"></path><path d="M17.8 11.8 19 13"></path><path d="M15 9h0"></path><path d="M17.8 6.2 19 5"></path><path d="m3 21 9-9"></path><path d="M12.2 6.2 11 5"></path></svg>`,
|
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
update: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="7 10 12 15 17 10"></polyline>
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
agents: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path
|
||||||
|
d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2z"
|
||||||
|
></path>
|
||||||
|
<circle cx="8" cy="14" r="1"></circle>
|
||||||
|
<circle cx="16" cy="14" r="1"></circle>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
auth: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||||
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
channels: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
messages: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
|
||||||
|
<polyline points="22,6 12,13 2,6"></polyline>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
commands: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="4 17 10 11 4 5"></polyline>
|
||||||
|
<line x1="12" y1="19" x2="20" y2="19"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
hooks: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
|
||||||
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
skills: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polygon
|
||||||
|
points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
|
||||||
|
></polygon>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
tools: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path
|
||||||
|
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
gateway: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<line x1="2" y1="12" x2="22" y2="12"></line>
|
||||||
|
<path
|
||||||
|
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
wizard: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M15 4V2"></path>
|
||||||
|
<path d="M15 16v-2"></path>
|
||||||
|
<path d="M8 9h2"></path>
|
||||||
|
<path d="M20 9h2"></path>
|
||||||
|
<path d="M17.8 11.8 19 13"></path>
|
||||||
|
<path d="M15 9h0"></path>
|
||||||
|
<path d="M17.8 6.2 19 5"></path>
|
||||||
|
<path d="m3 21 9-9"></path>
|
||||||
|
<path d="M12.2 6.2 11 5"></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
// Additional sections
|
// Additional sections
|
||||||
meta: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"></path><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg>`,
|
meta: html`
|
||||||
logging: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`,
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
browser: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="4"></circle><line x1="21.17" y1="8" x2="12" y2="8"></line><line x1="3.95" y1="6.06" x2="8.54" y2="14"></line><line x1="10.88" y1="21.94" x2="15.46" y2="14"></line></svg>`,
|
<path d="M12 20h9"></path>
|
||||||
ui: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>`,
|
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path>
|
||||||
models: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>`,
|
</svg>
|
||||||
bindings: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>`,
|
`,
|
||||||
broadcast: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"></path><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"></path><circle cx="12" cy="12" r="2"></circle><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"></path><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"></path></svg>`,
|
logging: html`
|
||||||
audio: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>`,
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
session: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>`,
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||||
cron: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>`,
|
<polyline points="14 2 14 8 20 8"></polyline>
|
||||||
web: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>`,
|
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||||
discovery: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>`,
|
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||||
canvasHost: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>`,
|
<polyline points="10 9 9 9 8 9"></polyline>
|
||||||
talk: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg>`,
|
</svg>
|
||||||
plugins: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v6"></path><path d="m4.93 10.93 4.24 4.24"></path><path d="M2 12h6"></path><path d="m4.93 13.07 4.24-4.24"></path><path d="M12 22v-6"></path><path d="m19.07 13.07-4.24-4.24"></path><path d="M22 12h-6"></path><path d="m19.07 10.93-4.24 4.24"></path></svg>`,
|
`,
|
||||||
default: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>`,
|
browser: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<circle cx="12" cy="12" r="4"></circle>
|
||||||
|
<line x1="21.17" y1="8" x2="12" y2="8"></line>
|
||||||
|
<line x1="3.95" y1="6.06" x2="8.54" y2="14"></line>
|
||||||
|
<line x1="10.88" y1="21.94" x2="15.46" y2="14"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
ui: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
|
<line x1="3" y1="9" x2="21" y2="9"></line>
|
||||||
|
<line x1="9" y1="21" x2="9" y2="9"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
models: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path
|
||||||
|
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"
|
||||||
|
></path>
|
||||||
|
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||||
|
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
bindings: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
||||||
|
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
||||||
|
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
||||||
|
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
broadcast: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"></path>
|
||||||
|
<path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"></path>
|
||||||
|
<circle cx="12" cy="12" r="2"></circle>
|
||||||
|
<path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"></path>
|
||||||
|
<path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
audio: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M9 18V5l12-2v13"></path>
|
||||||
|
<circle cx="6" cy="18" r="3"></circle>
|
||||||
|
<circle cx="18" cy="16" r="3"></circle>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
session: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||||
|
<circle cx="9" cy="7" r="4"></circle>
|
||||||
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
cron: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<polyline points="12 6 12 12 16 14"></polyline>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
web: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<line x1="2" y1="12" x2="22" y2="12"></line>
|
||||||
|
<path
|
||||||
|
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
discovery: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
canvasHost: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
|
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||||
|
<polyline points="21 15 16 10 5 21"></polyline>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
talk: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
|
||||||
|
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
|
||||||
|
<line x1="12" y1="19" x2="12" y2="23"></line>
|
||||||
|
<line x1="8" y1="23" x2="16" y2="23"></line>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
plugins: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M12 2v6"></path>
|
||||||
|
<path d="m4.93 10.93 4.24 4.24"></path>
|
||||||
|
<path d="M2 12h6"></path>
|
||||||
|
<path d="m4.93 13.07 4.24-4.24"></path>
|
||||||
|
<path d="M12 22v-6"></path>
|
||||||
|
<path d="m19.07 13.07-4.24-4.24"></path>
|
||||||
|
<path d="M22 12h-6"></path>
|
||||||
|
<path d="m19.07 10.93-4.24 4.24"></path>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
default: html`
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||||
|
<polyline points="14 2 14 8 20 8"></polyline>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Section definitions
|
// Section definitions
|
||||||
@ -102,7 +291,10 @@ function getSectionIcon(key: string) {
|
|||||||
return sidebarIcons[key as keyof typeof sidebarIcons] ?? sidebarIcons.default;
|
return sidebarIcons[key as keyof typeof sidebarIcons] ?? sidebarIcons.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveSectionMeta(key: string, schema?: JsonSchema): {
|
function resolveSectionMeta(
|
||||||
|
key: string,
|
||||||
|
schema?: JsonSchema,
|
||||||
|
): {
|
||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
} {
|
} {
|
||||||
@ -134,7 +326,7 @@ function resolveSubsections(params: {
|
|||||||
|
|
||||||
function computeDiff(
|
function computeDiff(
|
||||||
original: Record<string, unknown> | null,
|
original: Record<string, unknown> | null,
|
||||||
current: Record<string, unknown> | null
|
current: Record<string, unknown> | null,
|
||||||
): Array<{ path: string; from: unknown; to: unknown }> {
|
): Array<{ path: string; from: unknown; to: unknown }> {
|
||||||
if (!original || !current) return [];
|
if (!original || !current) return [];
|
||||||
const changes: Array<{ path: string; from: unknown; to: unknown }> = [];
|
const changes: Array<{ path: string; from: unknown; to: unknown }> = [];
|
||||||
@ -182,22 +374,19 @@ function truncateValue(value: unknown, maxLen = 40): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function renderConfig(props: ConfigProps) {
|
export function renderConfig(props: ConfigProps) {
|
||||||
const validity =
|
const validity = props.valid == null ? "unknown" : props.valid ? "valid" : "invalid";
|
||||||
props.valid == null ? "unknown" : props.valid ? "valid" : "invalid";
|
|
||||||
const analysis = analyzeConfigSchema(props.schema);
|
const analysis = analyzeConfigSchema(props.schema);
|
||||||
const formUnsafe = analysis.schema
|
const formUnsafe = analysis.schema ? analysis.unsupportedPaths.length > 0 : false;
|
||||||
? analysis.unsupportedPaths.length > 0
|
|
||||||
: false;
|
|
||||||
|
|
||||||
// Get available sections from schema
|
// Get available sections from schema
|
||||||
const schemaProps = analysis.schema?.properties ?? {};
|
const schemaProps = analysis.schema?.properties ?? {};
|
||||||
const availableSections = SECTIONS.filter(s => s.key in schemaProps);
|
const availableSections = SECTIONS.filter((s) => s.key in schemaProps);
|
||||||
|
|
||||||
// Add any sections in schema but not in our list
|
// Add any sections in schema but not in our list
|
||||||
const knownKeys = new Set(SECTIONS.map(s => s.key));
|
const knownKeys = new Set(SECTIONS.map((s) => s.key));
|
||||||
const extraSections = Object.keys(schemaProps)
|
const extraSections = Object.keys(schemaProps)
|
||||||
.filter(k => !knownKeys.has(k))
|
.filter((k) => !knownKeys.has(k))
|
||||||
.map(k => ({ key: k, label: k.charAt(0).toUpperCase() + k.slice(1) }));
|
.map((k) => ({ key: k, label: k.charAt(0).toUpperCase() + k.slice(1) }));
|
||||||
|
|
||||||
const allSections = [...availableSections, ...extraSections];
|
const allSections = [...availableSections, ...extraSections];
|
||||||
|
|
||||||
@ -216,27 +405,22 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
const allowSubnav =
|
const allowSubnav =
|
||||||
props.formMode === "form" &&
|
props.formMode === "form" && Boolean(props.activeSection) && subsections.length > 0;
|
||||||
Boolean(props.activeSection) &&
|
|
||||||
subsections.length > 0;
|
|
||||||
const isAllSubsection = props.activeSubsection === ALL_SUBSECTION;
|
const isAllSubsection = props.activeSubsection === ALL_SUBSECTION;
|
||||||
const effectiveSubsection = props.searchQuery
|
const effectiveSubsection = props.searchQuery
|
||||||
? null
|
? null
|
||||||
: isAllSubsection
|
: isAllSubsection
|
||||||
? null
|
? null
|
||||||
: props.activeSubsection ?? (subsections[0]?.key ?? null);
|
: (props.activeSubsection ?? subsections[0]?.key ?? null);
|
||||||
|
|
||||||
// Compute diff for showing changes (works for both form and raw modes)
|
// Compute diff for showing changes (works for both form and raw modes)
|
||||||
const diff = props.formMode === "form"
|
const diff = props.formMode === "form" ? computeDiff(props.originalValue, props.formValue) : [];
|
||||||
? computeDiff(props.originalValue, props.formValue)
|
|
||||||
: [];
|
|
||||||
const hasRawChanges = props.formMode === "raw" && props.raw !== props.originalRaw;
|
const hasRawChanges = props.formMode === "raw" && props.raw !== props.originalRaw;
|
||||||
const hasChanges = props.formMode === "form" ? diff.length > 0 : hasRawChanges;
|
const hasChanges = props.formMode === "form" ? diff.length > 0 : hasRawChanges;
|
||||||
|
|
||||||
// Save/apply buttons require actual changes to be enabled.
|
// Save/apply buttons require actual changes to be enabled.
|
||||||
// Note: formUnsafe warns about unsupported schema paths but shouldn't block saving.
|
// Note: formUnsafe warns about unsupported schema paths but shouldn't block saving.
|
||||||
const canSaveForm =
|
const canSaveForm = Boolean(props.formValue) && !props.loading && Boolean(analysis.schema);
|
||||||
Boolean(props.formValue) && !props.loading && Boolean(analysis.schema);
|
|
||||||
const canSave =
|
const canSave =
|
||||||
props.connected &&
|
props.connected &&
|
||||||
!props.saving &&
|
!props.saving &&
|
||||||
@ -272,12 +456,16 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
.value=${props.searchQuery}
|
.value=${props.searchQuery}
|
||||||
@input=${(e: Event) => props.onSearchChange((e.target as HTMLInputElement).value)}
|
@input=${(e: Event) => props.onSearchChange((e.target as HTMLInputElement).value)}
|
||||||
/>
|
/>
|
||||||
${props.searchQuery ? html`
|
${
|
||||||
|
props.searchQuery
|
||||||
|
? html`
|
||||||
<button
|
<button
|
||||||
class="config-search__clear"
|
class="config-search__clear"
|
||||||
@click=${() => props.onSearchChange("")}
|
@click=${() => props.onSearchChange("")}
|
||||||
>×</button>
|
>×</button>
|
||||||
` : nothing}
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Section nav -->
|
<!-- Section nav -->
|
||||||
@ -289,7 +477,8 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
<span class="config-nav__icon">${sidebarIcons.all}</span>
|
<span class="config-nav__icon">${sidebarIcons.all}</span>
|
||||||
<span class="config-nav__label">All Settings</span>
|
<span class="config-nav__label">All Settings</span>
|
||||||
</button>
|
</button>
|
||||||
${allSections.map(section => html`
|
${allSections.map(
|
||||||
|
(section) => html`
|
||||||
<button
|
<button
|
||||||
class="config-nav__item ${props.activeSection === section.key ? "active" : ""}"
|
class="config-nav__item ${props.activeSection === section.key ? "active" : ""}"
|
||||||
@click=${() => props.onSectionChange(section.key)}
|
@click=${() => props.onSectionChange(section.key)}
|
||||||
@ -297,7 +486,8 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
<span class="config-nav__icon">${getSectionIcon(section.key)}</span>
|
<span class="config-nav__icon">${getSectionIcon(section.key)}</span>
|
||||||
<span class="config-nav__label">${section.label}</span>
|
<span class="config-nav__label">${section.label}</span>
|
||||||
</button>
|
</button>
|
||||||
`)}
|
`,
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Mode toggle at bottom -->
|
<!-- Mode toggle at bottom -->
|
||||||
@ -325,11 +515,15 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
<!-- Action bar -->
|
<!-- Action bar -->
|
||||||
<div class="config-actions">
|
<div class="config-actions">
|
||||||
<div class="config-actions__left">
|
<div class="config-actions__left">
|
||||||
${hasChanges ? html`
|
${
|
||||||
|
hasChanges
|
||||||
|
? html`
|
||||||
<span class="config-changes-badge">${props.formMode === "raw" ? "Unsaved changes" : `${diff.length} unsaved change${diff.length !== 1 ? "s" : ""}`}</span>
|
<span class="config-changes-badge">${props.formMode === "raw" ? "Unsaved changes" : `${diff.length} unsaved change${diff.length !== 1 ? "s" : ""}`}</span>
|
||||||
` : html`
|
`
|
||||||
|
: html`
|
||||||
<span class="config-status muted">No changes</span>
|
<span class="config-status muted">No changes</span>
|
||||||
`}
|
`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="config-actions__right">
|
<div class="config-actions__right">
|
||||||
<button class="btn btn--sm" ?disabled=${props.loading} @click=${props.onReload}>
|
<button class="btn btn--sm" ?disabled=${props.loading} @click=${props.onReload}>
|
||||||
@ -360,7 +554,9 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Diff panel (form mode only - raw mode doesn't have granular diff) -->
|
<!-- Diff panel (form mode only - raw mode doesn't have granular diff) -->
|
||||||
${hasChanges && props.formMode === "form" ? html`
|
${
|
||||||
|
hasChanges && props.formMode === "form"
|
||||||
|
? html`
|
||||||
<details class="config-diff">
|
<details class="config-diff">
|
||||||
<summary class="config-diff__summary">
|
<summary class="config-diff__summary">
|
||||||
<span>View ${diff.length} pending change${diff.length !== 1 ? "s" : ""}</span>
|
<span>View ${diff.length} pending change${diff.length !== 1 ? "s" : ""}</span>
|
||||||
@ -369,7 +565,8 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
</svg>
|
</svg>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="config-diff__content">
|
<div class="config-diff__content">
|
||||||
${diff.map(change => html`
|
${diff.map(
|
||||||
|
(change) => html`
|
||||||
<div class="config-diff__item">
|
<div class="config-diff__item">
|
||||||
<div class="config-diff__path">${change.path}</div>
|
<div class="config-diff__path">${change.path}</div>
|
||||||
<div class="config-diff__values">
|
<div class="config-diff__values">
|
||||||
@ -378,26 +575,34 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
<span class="config-diff__to">${truncateValue(change.to)}</span>
|
<span class="config-diff__to">${truncateValue(change.to)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`)}
|
`,
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
` : nothing}
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${activeSectionMeta && props.formMode === "form"
|
${
|
||||||
|
activeSectionMeta && props.formMode === "form"
|
||||||
? html`
|
? html`
|
||||||
<div class="config-section-hero">
|
<div class="config-section-hero">
|
||||||
<div class="config-section-hero__icon">${getSectionIcon(props.activeSection ?? "")}</div>
|
<div class="config-section-hero__icon">${getSectionIcon(props.activeSection ?? "")}</div>
|
||||||
<div class="config-section-hero__text">
|
<div class="config-section-hero__text">
|
||||||
<div class="config-section-hero__title">${activeSectionMeta.label}</div>
|
<div class="config-section-hero__title">${activeSectionMeta.label}</div>
|
||||||
${activeSectionMeta.description
|
${
|
||||||
|
activeSectionMeta.description
|
||||||
? html`<div class="config-section-hero__desc">${activeSectionMeta.description}</div>`
|
? html`<div class="config-section-hero__desc">${activeSectionMeta.description}</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${allowSubnav
|
${
|
||||||
|
allowSubnav
|
||||||
? html`
|
? html`
|
||||||
<div class="config-subnav">
|
<div class="config-subnav">
|
||||||
<button
|
<button
|
||||||
@ -421,17 +626,22 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Form content -->
|
<!-- Form content -->
|
||||||
<div class="config-content">
|
<div class="config-content">
|
||||||
${props.formMode === "form"
|
${
|
||||||
|
props.formMode === "form"
|
||||||
? html`
|
? html`
|
||||||
${props.schemaLoading
|
${
|
||||||
? html`<div class="config-loading">
|
props.schemaLoading
|
||||||
|
? html`
|
||||||
|
<div class="config-loading">
|
||||||
<div class="config-loading__spinner"></div>
|
<div class="config-loading__spinner"></div>
|
||||||
<span>Loading schema…</span>
|
<span>Loading schema…</span>
|
||||||
</div>`
|
</div>
|
||||||
|
`
|
||||||
: renderConfigForm({
|
: renderConfigForm({
|
||||||
schema: analysis.schema,
|
schema: analysis.schema,
|
||||||
uiHints: props.uiHints,
|
uiHints: props.uiHints,
|
||||||
@ -442,13 +652,17 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
searchQuery: props.searchQuery,
|
searchQuery: props.searchQuery,
|
||||||
activeSection: props.activeSection,
|
activeSection: props.activeSection,
|
||||||
activeSubsection: effectiveSubsection,
|
activeSubsection: effectiveSubsection,
|
||||||
})}
|
})
|
||||||
${formUnsafe
|
}
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
${
|
||||||
Form view can't safely edit some fields.
|
formUnsafe
|
||||||
Use Raw to avoid losing config entries.
|
? html`
|
||||||
</div>`
|
<div class="callout danger" style="margin-top: 12px">
|
||||||
: nothing}
|
Form view can't safely edit some fields. Use Raw to avoid losing config entries.
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<label class="field config-raw-field">
|
<label class="field config-raw-field">
|
||||||
@ -459,14 +673,17 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
props.onRawChange((e.target as HTMLTextAreaElement).value)}
|
props.onRawChange((e.target as HTMLTextAreaElement).value)}
|
||||||
></textarea>
|
></textarea>
|
||||||
</label>
|
</label>
|
||||||
`}
|
`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${props.issues.length > 0
|
${
|
||||||
|
props.issues.length > 0
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||||
<pre class="code-block">${JSON.stringify(props.issues, null, 2)}</pre>
|
<pre class="code-block">${JSON.stringify(props.issues, null, 2)}</pre>
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -63,11 +63,7 @@ export function renderCron(props: CronProps) {
|
|||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-label">Enabled</div>
|
<div class="stat-label">Enabled</div>
|
||||||
<div class="stat-value">
|
<div class="stat-value">
|
||||||
${props.status
|
${props.status ? (props.status.enabled ? "Yes" : "No") : "n/a"}
|
||||||
? props.status.enabled
|
|
||||||
? "Yes"
|
|
||||||
: "No"
|
|
||||||
: "n/a"}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
@ -131,7 +127,8 @@ export function renderCron(props: CronProps) {
|
|||||||
.value=${props.form.scheduleKind}
|
.value=${props.form.scheduleKind}
|
||||||
@change=${(e: Event) =>
|
@change=${(e: Event) =>
|
||||||
props.onFormChange({
|
props.onFormChange({
|
||||||
scheduleKind: (e.target as HTMLSelectElement).value as CronFormState["scheduleKind"],
|
scheduleKind: (e.target as HTMLSelectElement)
|
||||||
|
.value as CronFormState["scheduleKind"],
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<option value="every">Every</option>
|
<option value="every">Every</option>
|
||||||
@ -148,7 +145,8 @@ export function renderCron(props: CronProps) {
|
|||||||
.value=${props.form.sessionTarget}
|
.value=${props.form.sessionTarget}
|
||||||
@change=${(e: Event) =>
|
@change=${(e: Event) =>
|
||||||
props.onFormChange({
|
props.onFormChange({
|
||||||
sessionTarget: (e.target as HTMLSelectElement).value as CronFormState["sessionTarget"],
|
sessionTarget: (e.target as HTMLSelectElement)
|
||||||
|
.value as CronFormState["sessionTarget"],
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<option value="main">Main</option>
|
<option value="main">Main</option>
|
||||||
@ -174,7 +172,8 @@ export function renderCron(props: CronProps) {
|
|||||||
.value=${props.form.payloadKind}
|
.value=${props.form.payloadKind}
|
||||||
@change=${(e: Event) =>
|
@change=${(e: Event) =>
|
||||||
props.onFormChange({
|
props.onFormChange({
|
||||||
payloadKind: (e.target as HTMLSelectElement).value as CronFormState["payloadKind"],
|
payloadKind: (e.target as HTMLSelectElement)
|
||||||
|
.value as CronFormState["payloadKind"],
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<option value="systemEvent">System event</option>
|
<option value="systemEvent">System event</option>
|
||||||
@ -193,7 +192,8 @@ export function renderCron(props: CronProps) {
|
|||||||
rows="4"
|
rows="4"
|
||||||
></textarea>
|
></textarea>
|
||||||
</label>
|
</label>
|
||||||
${props.form.payloadKind === "agentTurn"
|
${
|
||||||
|
props.form.payloadKind === "agentTurn"
|
||||||
? html`
|
? html`
|
||||||
<div class="form-grid" style="margin-top: 12px;">
|
<div class="form-grid" style="margin-top: 12px;">
|
||||||
<label class="field checkbox">
|
<label class="field checkbox">
|
||||||
@ -213,7 +213,8 @@ export function renderCron(props: CronProps) {
|
|||||||
.value=${props.form.channel || "last"}
|
.value=${props.form.channel || "last"}
|
||||||
@change=${(e: Event) =>
|
@change=${(e: Event) =>
|
||||||
props.onFormChange({
|
props.onFormChange({
|
||||||
channel: (e.target as HTMLSelectElement).value as CronFormState["channel"],
|
channel: (e.target as HTMLSelectElement)
|
||||||
|
.value as CronFormState["channel"],
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
${channelOptions.map(
|
${channelOptions.map(
|
||||||
@ -243,7 +244,8 @@ export function renderCron(props: CronProps) {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
${props.form.sessionTarget === "isolated"
|
${
|
||||||
|
props.form.sessionTarget === "isolated"
|
||||||
? html`
|
? html`
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Post to main prefix</span>
|
<span>Post to main prefix</span>
|
||||||
@ -256,10 +258,12 @@ export function renderCron(props: CronProps) {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
<div class="row" style="margin-top: 14px;">
|
<div class="row" style="margin-top: 14px;">
|
||||||
<button class="btn primary" ?disabled=${props.busy} @click=${props.onAdd}>
|
<button class="btn primary" ?disabled=${props.busy} @click=${props.onAdd}>
|
||||||
${props.busy ? "Saving…" : "Add job"}
|
${props.busy ? "Saving…" : "Add job"}
|
||||||
@ -271,31 +275,37 @@ export function renderCron(props: CronProps) {
|
|||||||
<section class="card" style="margin-top: 18px;">
|
<section class="card" style="margin-top: 18px;">
|
||||||
<div class="card-title">Jobs</div>
|
<div class="card-title">Jobs</div>
|
||||||
<div class="card-sub">All scheduled jobs stored in the gateway.</div>
|
<div class="card-sub">All scheduled jobs stored in the gateway.</div>
|
||||||
${props.jobs.length === 0
|
${
|
||||||
? html`<div class="muted" style="margin-top: 12px;">No jobs yet.</div>`
|
props.jobs.length === 0
|
||||||
|
? html`
|
||||||
|
<div class="muted" style="margin-top: 12px">No jobs yet.</div>
|
||||||
|
`
|
||||||
: html`
|
: html`
|
||||||
<div class="list" style="margin-top: 12px;">
|
<div class="list" style="margin-top: 12px;">
|
||||||
${props.jobs.map((job) => renderJob(job, props))}
|
${props.jobs.map((job) => renderJob(job, props))}
|
||||||
</div>
|
</div>
|
||||||
`}
|
`
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card" style="margin-top: 18px;">
|
<section class="card" style="margin-top: 18px;">
|
||||||
<div class="card-title">Run history</div>
|
<div class="card-title">Run history</div>
|
||||||
<div class="card-sub">Latest runs for ${props.runsJobId ?? "(select a job)"}.</div>
|
<div class="card-sub">Latest runs for ${props.runsJobId ?? "(select a job)"}.</div>
|
||||||
${props.runsJobId == null
|
${
|
||||||
|
props.runsJobId == null
|
||||||
? html`
|
? html`
|
||||||
<div class="muted" style="margin-top: 12px;">
|
<div class="muted" style="margin-top: 12px">Select a job to inspect run history.</div>
|
||||||
Select a job to inspect run history.
|
|
||||||
</div>
|
|
||||||
`
|
`
|
||||||
: props.runs.length === 0
|
: props.runs.length === 0
|
||||||
? html`<div class="muted" style="margin-top: 12px;">No runs yet.</div>`
|
? html`
|
||||||
|
<div class="muted" style="margin-top: 12px">No runs yet.</div>
|
||||||
|
`
|
||||||
: html`
|
: html`
|
||||||
<div class="list" style="margin-top: 12px;">
|
<div class="list" style="margin-top: 12px;">
|
||||||
${props.runs.map((entry) => renderRun(entry))}
|
${props.runs.map((entry) => renderRun(entry))}
|
||||||
</div>
|
</div>
|
||||||
`}
|
`
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,8 +57,7 @@ export function renderDebug(props: DebugProps) {
|
|||||||
<span>Method</span>
|
<span>Method</span>
|
||||||
<input
|
<input
|
||||||
.value=${props.callMethod}
|
.value=${props.callMethod}
|
||||||
@input=${(e: Event) =>
|
@input=${(e: Event) => props.onCallMethodChange((e.target as HTMLInputElement).value)}
|
||||||
props.onCallMethodChange((e.target as HTMLInputElement).value)}
|
|
||||||
placeholder="system-presence"
|
placeholder="system-presence"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@ -75,14 +74,18 @@ export function renderDebug(props: DebugProps) {
|
|||||||
<div class="row" style="margin-top: 12px;">
|
<div class="row" style="margin-top: 12px;">
|
||||||
<button class="btn primary" @click=${props.onCall}>Call</button>
|
<button class="btn primary" @click=${props.onCall}>Call</button>
|
||||||
</div>
|
</div>
|
||||||
${props.callError
|
${
|
||||||
|
props.callError
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||||
${props.callError}
|
${props.callError}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
${props.callResult
|
}
|
||||||
|
${
|
||||||
|
props.callResult
|
||||||
? html`<pre class="code-block" style="margin-top: 12px;">${props.callResult}</pre>`
|
? html`<pre class="code-block" style="margin-top: 12px;">${props.callResult}</pre>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -99,8 +102,11 @@ export function renderDebug(props: DebugProps) {
|
|||||||
<section class="card" style="margin-top: 18px;">
|
<section class="card" style="margin-top: 18px;">
|
||||||
<div class="card-title">Event Log</div>
|
<div class="card-title">Event Log</div>
|
||||||
<div class="card-sub">Latest gateway events.</div>
|
<div class="card-sub">Latest gateway events.</div>
|
||||||
${props.eventLog.length === 0
|
${
|
||||||
? html`<div class="muted" style="margin-top: 12px;">No events yet.</div>`
|
props.eventLog.length === 0
|
||||||
|
? html`
|
||||||
|
<div class="muted" style="margin-top: 12px">No events yet.</div>
|
||||||
|
`
|
||||||
: html`
|
: html`
|
||||||
<div class="list" style="margin-top: 12px;">
|
<div class="list" style="margin-top: 12px;">
|
||||||
${props.eventLog.map(
|
${props.eventLog.map(
|
||||||
@ -117,7 +123,8 @@ export function renderDebug(props: DebugProps) {
|
|||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
`}
|
`
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,9 +32,11 @@ export function renderExecApprovalPrompt(state: AppViewState) {
|
|||||||
<div class="exec-approval-title">Exec approval needed</div>
|
<div class="exec-approval-title">Exec approval needed</div>
|
||||||
<div class="exec-approval-sub">${remaining}</div>
|
<div class="exec-approval-sub">${remaining}</div>
|
||||||
</div>
|
</div>
|
||||||
${queueCount > 1
|
${
|
||||||
|
queueCount > 1
|
||||||
? html`<div class="exec-approval-queue">${queueCount} pending</div>`
|
? html`<div class="exec-approval-queue">${queueCount} pending</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="exec-approval-command mono">${request.command}</div>
|
<div class="exec-approval-command mono">${request.command}</div>
|
||||||
<div class="exec-approval-meta">
|
<div class="exec-approval-meta">
|
||||||
@ -46,9 +48,11 @@ export function renderExecApprovalPrompt(state: AppViewState) {
|
|||||||
${renderMetaRow("Security", request.security)}
|
${renderMetaRow("Security", request.security)}
|
||||||
${renderMetaRow("Ask", request.ask)}
|
${renderMetaRow("Ask", request.ask)}
|
||||||
</div>
|
</div>
|
||||||
${state.execApprovalError
|
${
|
||||||
|
state.execApprovalError
|
||||||
? html`<div class="exec-approval-error">${state.execApprovalError}</div>`
|
? html`<div class="exec-approval-error">${state.execApprovalError}</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
<div class="exec-approval-actions">
|
<div class="exec-approval-actions">
|
||||||
<button
|
<button
|
||||||
class="btn primary"
|
class="btn primary"
|
||||||
|
|||||||
@ -23,30 +23,35 @@ export function renderInstances(props: InstancesProps) {
|
|||||||
${props.loading ? "Loading…" : "Refresh"}
|
${props.loading ? "Loading…" : "Refresh"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
${props.lastError
|
${
|
||||||
|
props.lastError
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||||
${props.lastError}
|
${props.lastError}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
${props.statusMessage
|
}
|
||||||
|
${
|
||||||
|
props.statusMessage
|
||||||
? html`<div class="callout" style="margin-top: 12px;">
|
? html`<div class="callout" style="margin-top: 12px;">
|
||||||
${props.statusMessage}
|
${props.statusMessage}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
<div class="list" style="margin-top: 16px;">
|
<div class="list" style="margin-top: 16px;">
|
||||||
${props.entries.length === 0
|
${
|
||||||
? html`<div class="muted">No instances reported yet.</div>`
|
props.entries.length === 0
|
||||||
: props.entries.map((entry) => renderEntry(entry))}
|
? html`
|
||||||
|
<div class="muted">No instances reported yet.</div>
|
||||||
|
`
|
||||||
|
: props.entries.map((entry) => renderEntry(entry))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEntry(entry: PresenceEntry) {
|
function renderEntry(entry: PresenceEntry) {
|
||||||
const lastInput =
|
const lastInput = entry.lastInputSeconds != null ? `${entry.lastInputSeconds}s ago` : "n/a";
|
||||||
entry.lastInputSeconds != null
|
|
||||||
? `${entry.lastInputSeconds}s ago`
|
|
||||||
: "n/a";
|
|
||||||
const mode = entry.mode ?? "unknown";
|
const mode = entry.mode ?? "unknown";
|
||||||
const roles = Array.isArray(entry.roles) ? entry.roles.filter(Boolean) : [];
|
const roles = Array.isArray(entry.roles) ? entry.roles.filter(Boolean) : [];
|
||||||
const scopes = Array.isArray(entry.scopes) ? entry.scopes.filter(Boolean) : [];
|
const scopes = Array.isArray(entry.scopes) ? entry.scopes.filter(Boolean) : [];
|
||||||
@ -66,12 +71,12 @@ function renderEntry(entry: PresenceEntry) {
|
|||||||
${roles.map((role) => html`<span class="chip">${role}</span>`)}
|
${roles.map((role) => html`<span class="chip">${role}</span>`)}
|
||||||
${scopesLabel ? html`<span class="chip">${scopesLabel}</span>` : nothing}
|
${scopesLabel ? html`<span class="chip">${scopesLabel}</span>` : nothing}
|
||||||
${entry.platform ? html`<span class="chip">${entry.platform}</span>` : nothing}
|
${entry.platform ? html`<span class="chip">${entry.platform}</span>` : nothing}
|
||||||
${entry.deviceFamily
|
${entry.deviceFamily ? html`<span class="chip">${entry.deviceFamily}</span>` : nothing}
|
||||||
? html`<span class="chip">${entry.deviceFamily}</span>`
|
${
|
||||||
: nothing}
|
entry.modelIdentifier
|
||||||
${entry.modelIdentifier
|
|
||||||
? html`<span class="chip">${entry.modelIdentifier}</span>`
|
? html`<span class="chip">${entry.modelIdentifier}</span>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
${entry.version ? html`<span class="chip">${entry.version}</span>` : nothing}
|
${entry.version ? html`<span class="chip">${entry.version}</span>` : nothing}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -60,7 +60,11 @@ export function renderLogs(props: LogsProps) {
|
|||||||
<button
|
<button
|
||||||
class="btn"
|
class="btn"
|
||||||
?disabled=${filtered.length === 0}
|
?disabled=${filtered.length === 0}
|
||||||
@click=${() => props.onExport(filtered.map((entry) => entry.raw), exportLabel)}
|
@click=${() =>
|
||||||
|
props.onExport(
|
||||||
|
filtered.map((entry) => entry.raw),
|
||||||
|
exportLabel,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
Export ${exportLabel}
|
Export ${exportLabel}
|
||||||
</button>
|
</button>
|
||||||
@ -72,8 +76,7 @@ export function renderLogs(props: LogsProps) {
|
|||||||
<span>Filter</span>
|
<span>Filter</span>
|
||||||
<input
|
<input
|
||||||
.value=${props.filterText}
|
.value=${props.filterText}
|
||||||
@input=${(e: Event) =>
|
@input=${(e: Event) => props.onFilterTextChange((e.target as HTMLInputElement).value)}
|
||||||
props.onFilterTextChange((e.target as HTMLInputElement).value)}
|
|
||||||
placeholder="Search logs"
|
placeholder="Search logs"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@ -104,21 +107,30 @@ export function renderLogs(props: LogsProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${props.file
|
${
|
||||||
|
props.file
|
||||||
? html`<div class="muted" style="margin-top: 10px;">File: ${props.file}</div>`
|
? html`<div class="muted" style="margin-top: 10px;">File: ${props.file}</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
${props.truncated
|
}
|
||||||
? html`<div class="callout" style="margin-top: 10px;">
|
${
|
||||||
Log output truncated; showing latest chunk.
|
props.truncated
|
||||||
</div>`
|
? html`
|
||||||
: nothing}
|
<div class="callout" style="margin-top: 10px">Log output truncated; showing latest chunk.</div>
|
||||||
${props.error
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
${
|
||||||
|
props.error
|
||||||
? html`<div class="callout danger" style="margin-top: 10px;">${props.error}</div>`
|
? html`<div class="callout danger" style="margin-top: 10px;">${props.error}</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
<div class="log-stream" style="margin-top: 12px;" @scroll=${props.onScroll}>
|
<div class="log-stream" style="margin-top: 12px;" @scroll=${props.onScroll}>
|
||||||
${filtered.length === 0
|
${
|
||||||
? html`<div class="muted" style="padding: 12px;">No log entries.</div>`
|
filtered.length === 0
|
||||||
|
? html`
|
||||||
|
<div class="muted" style="padding: 12px">No log entries.</div>
|
||||||
|
`
|
||||||
: filtered.map(
|
: filtered.map(
|
||||||
(entry) => html`
|
(entry) => html`
|
||||||
<div class="log-row">
|
<div class="log-row">
|
||||||
@ -128,7 +140,8 @@ export function renderLogs(props: LogsProps) {
|
|||||||
<div class="log-message mono">${entry.message ?? entry.raw}</div>
|
<div class="log-message mono">${entry.message ?? entry.raw}</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { html, nothing } from "lit";
|
import { html } from "lit";
|
||||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||||
|
|
||||||
import { icons } from "../icons";
|
import { icons } from "../icons";
|
||||||
@ -21,7 +21,8 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
${props.error
|
${
|
||||||
|
props.error
|
||||||
? html`
|
? html`
|
||||||
<div class="callout danger">${props.error}</div>
|
<div class="callout danger">${props.error}</div>
|
||||||
<button @click=${props.onViewRawText} class="btn" style="margin-top: 12px;">
|
<button @click=${props.onViewRawText} class="btn" style="margin-top: 12px;">
|
||||||
@ -30,7 +31,10 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
|
|||||||
`
|
`
|
||||||
: props.content
|
: props.content
|
||||||
? html`<div class="sidebar-markdown">${unsafeHTML(toSanitizedMarkdownHtml(props.content))}</div>`
|
? html`<div class="sidebar-markdown">${unsafeHTML(toSanitizedMarkdownHtml(props.content))}</div>`
|
||||||
: html`<div class="muted">No content available</div>`}
|
: html`
|
||||||
|
<div class="muted">No content available</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -68,9 +68,13 @@ export function renderNodes(props: NodesProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="list" style="margin-top: 16px;">
|
<div class="list" style="margin-top: 16px;">
|
||||||
${props.nodes.length === 0
|
${
|
||||||
? html`<div class="muted">No nodes found.</div>`
|
props.nodes.length === 0
|
||||||
: props.nodes.map((n) => renderNode(n))}
|
? html`
|
||||||
|
<div class="muted">No nodes found.</div>
|
||||||
|
`
|
||||||
|
: props.nodes.map((n) => renderNode(n))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
@ -91,25 +95,35 @@ function renderDevices(props: NodesProps) {
|
|||||||
${props.devicesLoading ? "Loading…" : "Refresh"}
|
${props.devicesLoading ? "Loading…" : "Refresh"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
${props.devicesError
|
${
|
||||||
|
props.devicesError
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.devicesError}</div>`
|
? html`<div class="callout danger" style="margin-top: 12px;">${props.devicesError}</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
<div class="list" style="margin-top: 16px;">
|
<div class="list" style="margin-top: 16px;">
|
||||||
${pending.length > 0
|
${
|
||||||
|
pending.length > 0
|
||||||
? html`
|
? html`
|
||||||
<div class="muted" style="margin-bottom: 8px;">Pending</div>
|
<div class="muted" style="margin-bottom: 8px;">Pending</div>
|
||||||
${pending.map((req) => renderPendingDevice(req, props))}
|
${pending.map((req) => renderPendingDevice(req, props))}
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
${paired.length > 0
|
}
|
||||||
|
${
|
||||||
|
paired.length > 0
|
||||||
? html`
|
? html`
|
||||||
<div class="muted" style="margin-top: 12px; margin-bottom: 8px;">Paired</div>
|
<div class="muted" style="margin-top: 12px; margin-bottom: 8px;">Paired</div>
|
||||||
${paired.map((device) => renderPairedDevice(device, props))}
|
${paired.map((device) => renderPairedDevice(device, props))}
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
${pending.length === 0 && paired.length === 0
|
}
|
||||||
? html`<div class="muted">No paired devices.</div>`
|
${
|
||||||
: nothing}
|
pending.length === 0 && paired.length === 0
|
||||||
|
? html`
|
||||||
|
<div class="muted">No paired devices.</div>
|
||||||
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
@ -156,14 +170,18 @@ function renderPairedDevice(device: PairedDevice, props: NodesProps) {
|
|||||||
<div class="list-title">${name}</div>
|
<div class="list-title">${name}</div>
|
||||||
<div class="list-sub">${device.deviceId}${ip}</div>
|
<div class="list-sub">${device.deviceId}${ip}</div>
|
||||||
<div class="muted" style="margin-top: 6px;">${roles} · ${scopes}</div>
|
<div class="muted" style="margin-top: 6px;">${roles} · ${scopes}</div>
|
||||||
${tokens.length === 0
|
${
|
||||||
? html`<div class="muted" style="margin-top: 6px;">Tokens: none</div>`
|
tokens.length === 0
|
||||||
|
? html`
|
||||||
|
<div class="muted" style="margin-top: 6px">Tokens: none</div>
|
||||||
|
`
|
||||||
: html`
|
: html`
|
||||||
<div class="muted" style="margin-top: 10px;">Tokens</div>
|
<div class="muted" style="margin-top: 10px;">Tokens</div>
|
||||||
<div style="display: flex; flex-direction: column; gap: 8px; margin-top: 6px;">
|
<div style="display: flex; flex-direction: column; gap: 8px; margin-top: 6px;">
|
||||||
${tokens.map((token) => renderTokenRow(device.deviceId, token, props))}
|
${tokens.map((token) => renderTokenRow(device.deviceId, token, props))}
|
||||||
</div>
|
</div>
|
||||||
`}
|
`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -183,7 +201,8 @@ function renderTokenRow(deviceId: string, token: DeviceTokenSummary, props: Node
|
|||||||
>
|
>
|
||||||
Rotate
|
Rotate
|
||||||
</button>
|
</button>
|
||||||
${token.revokedAtMs
|
${
|
||||||
|
token.revokedAtMs
|
||||||
? nothing
|
? nothing
|
||||||
: html`
|
: html`
|
||||||
<button
|
<button
|
||||||
@ -192,7 +211,8 @@ function renderTokenRow(deviceId: string, token: DeviceTokenSummary, props: Node
|
|||||||
>
|
>
|
||||||
Revoke
|
Revoke
|
||||||
</button>
|
</button>
|
||||||
`}
|
`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -389,21 +409,17 @@ function resolveExecApprovalsState(props: NodesProps): ExecApprovalsState {
|
|||||||
const targetNodes = resolveExecApprovalsNodes(props.nodes);
|
const targetNodes = resolveExecApprovalsNodes(props.nodes);
|
||||||
const target = props.execApprovalsTarget;
|
const target = props.execApprovalsTarget;
|
||||||
let targetNodeId =
|
let targetNodeId =
|
||||||
target === "node" && props.execApprovalsTargetNodeId
|
target === "node" && props.execApprovalsTargetNodeId ? props.execApprovalsTargetNodeId : null;
|
||||||
? props.execApprovalsTargetNodeId
|
|
||||||
: null;
|
|
||||||
if (target === "node" && targetNodeId && !targetNodes.some((node) => node.id === targetNodeId)) {
|
if (target === "node" && targetNodeId && !targetNodes.some((node) => node.id === targetNodeId)) {
|
||||||
targetNodeId = null;
|
targetNodeId = null;
|
||||||
}
|
}
|
||||||
const selectedScope = resolveExecApprovalsScope(props.execApprovalsSelectedAgent, agents);
|
const selectedScope = resolveExecApprovalsScope(props.execApprovalsSelectedAgent, agents);
|
||||||
const selectedAgent =
|
const selectedAgent =
|
||||||
selectedScope !== EXEC_APPROVALS_DEFAULT_SCOPE
|
selectedScope !== EXEC_APPROVALS_DEFAULT_SCOPE
|
||||||
? ((form?.agents ?? {})[selectedScope] as Record<string, unknown> | undefined) ??
|
? (((form?.agents ?? {})[selectedScope] as Record<string, unknown> | undefined) ?? null)
|
||||||
null
|
|
||||||
: null;
|
: null;
|
||||||
const allowlist = Array.isArray((selectedAgent as { allowlist?: unknown })?.allowlist)
|
const allowlist = Array.isArray((selectedAgent as { allowlist?: unknown })?.allowlist)
|
||||||
? ((selectedAgent as { allowlist?: ExecApprovalsAllowlistEntry[] }).allowlist ??
|
? ((selectedAgent as { allowlist?: ExecApprovalsAllowlistEntry[] }).allowlist ?? [])
|
||||||
[])
|
|
||||||
: [];
|
: [];
|
||||||
return {
|
return {
|
||||||
ready,
|
ready,
|
||||||
@ -450,13 +466,18 @@ function renderBindings(state: BindingState) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${state.formMode === "raw"
|
${
|
||||||
? html`<div class="callout warn" style="margin-top: 12px;">
|
state.formMode === "raw"
|
||||||
|
? html`
|
||||||
|
<div class="callout warn" style="margin-top: 12px">
|
||||||
Switch the Config tab to <strong>Form</strong> mode to edit bindings here.
|
Switch the Config tab to <strong>Form</strong> mode to edit bindings here.
|
||||||
</div>`
|
</div>
|
||||||
: nothing}
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${!state.ready
|
${
|
||||||
|
!state.ready
|
||||||
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
||||||
<div class="muted">Load config to edit bindings.</div>
|
<div class="muted">Load config to edit bindings.</div>
|
||||||
<button class="btn" ?disabled=${state.configLoading} @click=${state.onLoadConfig}>
|
<button class="btn" ?disabled=${state.configLoading} @click=${state.onLoadConfig}>
|
||||||
@ -493,19 +514,26 @@ function renderBindings(state: BindingState) {
|
|||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
${!supportsBinding
|
${
|
||||||
? html`<div class="muted">No nodes with system.run available.</div>`
|
!supportsBinding
|
||||||
: nothing}
|
? html`
|
||||||
|
<div class="muted">No nodes with system.run available.</div>
|
||||||
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${state.agents.length === 0
|
${
|
||||||
? html`<div class="muted">No agents found.</div>`
|
state.agents.length === 0
|
||||||
: state.agents.map((agent) =>
|
? html`
|
||||||
renderAgentBinding(agent, state),
|
<div class="muted">No agents found.</div>
|
||||||
)}
|
`
|
||||||
|
: state.agents.map((agent) => renderAgentBinding(agent, state))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
`}
|
`
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -533,7 +561,8 @@ function renderExecApprovals(state: ExecApprovalsState) {
|
|||||||
|
|
||||||
${renderExecApprovalsTarget(state)}
|
${renderExecApprovalsTarget(state)}
|
||||||
|
|
||||||
${!ready
|
${
|
||||||
|
!ready
|
||||||
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
||||||
<div class="muted">Load exec approvals to edit allowlists.</div>
|
<div class="muted">Load exec approvals to edit allowlists.</div>
|
||||||
<button class="btn" ?disabled=${state.loading || !targetReady} @click=${state.onLoad}>
|
<button class="btn" ?disabled=${state.loading || !targetReady} @click=${state.onLoad}>
|
||||||
@ -543,10 +572,13 @@ function renderExecApprovals(state: ExecApprovalsState) {
|
|||||||
: html`
|
: html`
|
||||||
${renderExecApprovalsTabs(state)}
|
${renderExecApprovalsTabs(state)}
|
||||||
${renderExecApprovalsPolicy(state)}
|
${renderExecApprovalsPolicy(state)}
|
||||||
${state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE
|
${
|
||||||
|
state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE
|
||||||
? nothing
|
? nothing
|
||||||
: renderExecApprovalsAllowlist(state)}
|
: renderExecApprovalsAllowlist(state)
|
||||||
`}
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -583,7 +615,8 @@ function renderExecApprovalsTarget(state: ExecApprovalsState) {
|
|||||||
<option value="node" ?selected=${state.target === "node"}>Node</option>
|
<option value="node" ?selected=${state.target === "node"}>Node</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
${state.target === "node"
|
${
|
||||||
|
state.target === "node"
|
||||||
? html`
|
? html`
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Node</span>
|
<span>Node</span>
|
||||||
@ -608,12 +641,17 @@ function renderExecApprovalsTarget(state: ExecApprovalsState) {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${state.target === "node" && !hasNodes
|
${
|
||||||
? html`<div class="muted">No nodes advertise exec approvals yet.</div>`
|
state.target === "node" && !hasNodes
|
||||||
: nothing}
|
? html`
|
||||||
|
<div class="muted">No nodes advertise exec approvals yet.</div>
|
||||||
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -652,13 +690,10 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
|||||||
const basePath = isDefaults ? ["defaults"] : ["agents", state.selectedScope];
|
const basePath = isDefaults ? ["defaults"] : ["agents", state.selectedScope];
|
||||||
const agentSecurity = typeof agent.security === "string" ? agent.security : undefined;
|
const agentSecurity = typeof agent.security === "string" ? agent.security : undefined;
|
||||||
const agentAsk = typeof agent.ask === "string" ? agent.ask : undefined;
|
const agentAsk = typeof agent.ask === "string" ? agent.ask : undefined;
|
||||||
const agentAskFallback =
|
const agentAskFallback = typeof agent.askFallback === "string" ? agent.askFallback : undefined;
|
||||||
typeof agent.askFallback === "string" ? agent.askFallback : undefined;
|
const securityValue = isDefaults ? defaults.security : (agentSecurity ?? "__default__");
|
||||||
const securityValue = isDefaults ? defaults.security : agentSecurity ?? "__default__";
|
const askValue = isDefaults ? defaults.ask : (agentAsk ?? "__default__");
|
||||||
const askValue = isDefaults ? defaults.ask : agentAsk ?? "__default__";
|
const askFallbackValue = isDefaults ? defaults.askFallback : (agentAskFallback ?? "__default__");
|
||||||
const askFallbackValue = isDefaults
|
|
||||||
? defaults.askFallback
|
|
||||||
: agentAskFallback ?? "__default__";
|
|
||||||
const autoOverride =
|
const autoOverride =
|
||||||
typeof agent.autoAllowSkills === "boolean" ? agent.autoAllowSkills : undefined;
|
typeof agent.autoAllowSkills === "boolean" ? agent.autoAllowSkills : undefined;
|
||||||
const autoEffective = autoOverride ?? defaults.autoAllowSkills;
|
const autoEffective = autoOverride ?? defaults.autoAllowSkills;
|
||||||
@ -670,9 +705,7 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
|||||||
<div class="list-main">
|
<div class="list-main">
|
||||||
<div class="list-title">Security</div>
|
<div class="list-title">Security</div>
|
||||||
<div class="list-sub">
|
<div class="list-sub">
|
||||||
${isDefaults
|
${isDefaults ? "Default security mode." : `Default: ${defaults.security}.`}
|
||||||
? "Default security mode."
|
|
||||||
: `Default: ${defaults.security}.`}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-meta">
|
<div class="list-meta">
|
||||||
@ -690,11 +723,13 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${!isDefaults
|
${
|
||||||
|
!isDefaults
|
||||||
? html`<option value="__default__" ?selected=${securityValue === "__default__"}>
|
? html`<option value="__default__" ?selected=${securityValue === "__default__"}>
|
||||||
Use default (${defaults.security})
|
Use default (${defaults.security})
|
||||||
</option>`
|
</option>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
${SECURITY_OPTIONS.map(
|
${SECURITY_OPTIONS.map(
|
||||||
(option) =>
|
(option) =>
|
||||||
html`<option
|
html`<option
|
||||||
@ -731,11 +766,13 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${!isDefaults
|
${
|
||||||
|
!isDefaults
|
||||||
? html`<option value="__default__" ?selected=${askValue === "__default__"}>
|
? html`<option value="__default__" ?selected=${askValue === "__default__"}>
|
||||||
Use default (${defaults.ask})
|
Use default (${defaults.ask})
|
||||||
</option>`
|
</option>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
${ASK_OPTIONS.map(
|
${ASK_OPTIONS.map(
|
||||||
(option) =>
|
(option) =>
|
||||||
html`<option
|
html`<option
|
||||||
@ -754,9 +791,11 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
|||||||
<div class="list-main">
|
<div class="list-main">
|
||||||
<div class="list-title">Ask fallback</div>
|
<div class="list-title">Ask fallback</div>
|
||||||
<div class="list-sub">
|
<div class="list-sub">
|
||||||
${isDefaults
|
${
|
||||||
|
isDefaults
|
||||||
? "Applied when the UI prompt is unavailable."
|
? "Applied when the UI prompt is unavailable."
|
||||||
: `Default: ${defaults.askFallback}.`}
|
: `Default: ${defaults.askFallback}.`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-meta">
|
<div class="list-meta">
|
||||||
@ -774,11 +813,13 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${!isDefaults
|
${
|
||||||
|
!isDefaults
|
||||||
? html`<option value="__default__" ?selected=${askFallbackValue === "__default__"}>
|
? html`<option value="__default__" ?selected=${askFallbackValue === "__default__"}>
|
||||||
Use default (${defaults.askFallback})
|
Use default (${defaults.askFallback})
|
||||||
</option>`
|
</option>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
${SECURITY_OPTIONS.map(
|
${SECURITY_OPTIONS.map(
|
||||||
(option) =>
|
(option) =>
|
||||||
html`<option
|
html`<option
|
||||||
@ -797,11 +838,13 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
|||||||
<div class="list-main">
|
<div class="list-main">
|
||||||
<div class="list-title">Auto-allow skill CLIs</div>
|
<div class="list-title">Auto-allow skill CLIs</div>
|
||||||
<div class="list-sub">
|
<div class="list-sub">
|
||||||
${isDefaults
|
${
|
||||||
|
isDefaults
|
||||||
? "Allow skill executables listed by the Gateway."
|
? "Allow skill executables listed by the Gateway."
|
||||||
: autoIsDefault
|
: autoIsDefault
|
||||||
? `Using default (${defaults.autoAllowSkills ? "on" : "off"}).`
|
? `Using default (${defaults.autoAllowSkills ? "on" : "off"}).`
|
||||||
: `Override (${autoEffective ? "on" : "off"}).`}
|
: `Override (${autoEffective ? "on" : "off"}).`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-meta">
|
<div class="list-meta">
|
||||||
@ -817,7 +860,8 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
${!isDefaults && !autoIsDefault
|
${
|
||||||
|
!isDefaults && !autoIsDefault
|
||||||
? html`<button
|
? html`<button
|
||||||
class="btn btn--sm"
|
class="btn btn--sm"
|
||||||
?disabled=${state.disabled}
|
?disabled=${state.disabled}
|
||||||
@ -825,7 +869,8 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
|||||||
>
|
>
|
||||||
Use default
|
Use default
|
||||||
</button>`
|
</button>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -853,11 +898,13 @@ function renderExecApprovalsAllowlist(state: ExecApprovalsState) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="list" style="margin-top: 12px;">
|
<div class="list" style="margin-top: 12px;">
|
||||||
${entries.length === 0
|
${
|
||||||
? html`<div class="muted">No allowlist entries yet.</div>`
|
entries.length === 0
|
||||||
: entries.map((entry, index) =>
|
? html`
|
||||||
renderAllowlistEntry(state, entry, index),
|
<div class="muted">No allowlist entries yet.</div>
|
||||||
)}
|
`
|
||||||
|
: entries.map((entry, index) => renderAllowlistEntry(state, entry, index))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -868,12 +915,8 @@ function renderAllowlistEntry(
|
|||||||
index: number,
|
index: number,
|
||||||
) {
|
) {
|
||||||
const lastUsed = entry.lastUsedAt ? formatAgo(entry.lastUsedAt) : "never";
|
const lastUsed = entry.lastUsedAt ? formatAgo(entry.lastUsedAt) : "never";
|
||||||
const lastCommand = entry.lastUsedCommand
|
const lastCommand = entry.lastUsedCommand ? clampText(entry.lastUsedCommand, 120) : null;
|
||||||
? clampText(entry.lastUsedCommand, 120)
|
const lastPath = entry.lastResolvedPath ? clampText(entry.lastResolvedPath, 120) : null;
|
||||||
: null;
|
|
||||||
const lastPath = entry.lastResolvedPath
|
|
||||||
? clampText(entry.lastResolvedPath, 120)
|
|
||||||
: null;
|
|
||||||
return html`
|
return html`
|
||||||
<div class="list-item">
|
<div class="list-item">
|
||||||
<div class="list-main">
|
<div class="list-main">
|
||||||
@ -926,9 +969,11 @@ function renderAgentBinding(agent: BindingAgent, state: BindingState) {
|
|||||||
<div class="list-title">${label}</div>
|
<div class="list-title">${label}</div>
|
||||||
<div class="list-sub">
|
<div class="list-sub">
|
||||||
${agent.isDefault ? "default agent" : "agent"} ·
|
${agent.isDefault ? "default agent" : "agent"} ·
|
||||||
${bindingValue === "__default__"
|
${
|
||||||
|
bindingValue === "__default__"
|
||||||
? `uses default (${state.defaultBinding ?? "any"})`
|
? `uses default (${state.defaultBinding ?? "any"})`
|
||||||
: `override: ${agent.binding}`}
|
: `override: ${agent.binding}`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-meta">
|
<div class="list-meta">
|
||||||
@ -973,18 +1018,24 @@ function resolveExecNodes(nodes: Array<Record<string, unknown>>): BindingNode[]
|
|||||||
typeof node.displayName === "string" && node.displayName.trim()
|
typeof node.displayName === "string" && node.displayName.trim()
|
||||||
? node.displayName.trim()
|
? node.displayName.trim()
|
||||||
: nodeId;
|
: nodeId;
|
||||||
list.push({ id: nodeId, label: displayName === nodeId ? nodeId : `${displayName} · ${nodeId}` });
|
list.push({
|
||||||
|
id: nodeId,
|
||||||
|
label: displayName === nodeId ? nodeId : `${displayName} · ${nodeId}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
list.sort((a, b) => a.label.localeCompare(b.label));
|
list.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveExecApprovalsNodes(nodes: Array<Record<string, unknown>>): ExecApprovalsTargetNode[] {
|
function resolveExecApprovalsNodes(
|
||||||
|
nodes: Array<Record<string, unknown>>,
|
||||||
|
): ExecApprovalsTargetNode[] {
|
||||||
const list: ExecApprovalsTargetNode[] = [];
|
const list: ExecApprovalsTargetNode[] = [];
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
const commands = Array.isArray(node.commands) ? node.commands : [];
|
const commands = Array.isArray(node.commands) ? node.commands : [];
|
||||||
const supports = commands.some(
|
const supports = commands.some(
|
||||||
(cmd) => String(cmd) === "system.execApprovals.get" || String(cmd) === "system.execApprovals.set",
|
(cmd) =>
|
||||||
|
String(cmd) === "system.execApprovals.get" || String(cmd) === "system.execApprovals.set",
|
||||||
);
|
);
|
||||||
if (!supports) continue;
|
if (!supports) continue;
|
||||||
const nodeId = typeof node.nodeId === "string" ? node.nodeId.trim() : "";
|
const nodeId = typeof node.nodeId === "string" ? node.nodeId.trim() : "";
|
||||||
@ -993,7 +1044,10 @@ function resolveExecApprovalsNodes(nodes: Array<Record<string, unknown>>): ExecA
|
|||||||
typeof node.displayName === "string" && node.displayName.trim()
|
typeof node.displayName === "string" && node.displayName.trim()
|
||||||
? node.displayName.trim()
|
? node.displayName.trim()
|
||||||
: nodeId;
|
: nodeId;
|
||||||
list.push({ id: nodeId, label: displayName === nodeId ? nodeId : `${displayName} · ${nodeId}` });
|
list.push({
|
||||||
|
id: nodeId,
|
||||||
|
label: displayName === nodeId ? nodeId : `${displayName} · ${nodeId}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
list.sort((a, b) => a.label.localeCompare(b.label));
|
list.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
return list;
|
return list;
|
||||||
@ -1035,9 +1089,7 @@ function resolveAgentBindings(config: Record<string, unknown> | null): {
|
|||||||
const toolsEntry = (record.tools ?? {}) as Record<string, unknown>;
|
const toolsEntry = (record.tools ?? {}) as Record<string, unknown>;
|
||||||
const execEntry = (toolsEntry.exec ?? {}) as Record<string, unknown>;
|
const execEntry = (toolsEntry.exec ?? {}) as Record<string, unknown>;
|
||||||
const binding =
|
const binding =
|
||||||
typeof execEntry.node === "string" && execEntry.node.trim()
|
typeof execEntry.node === "string" && execEntry.node.trim() ? execEntry.node.trim() : null;
|
||||||
? execEntry.node.trim()
|
|
||||||
: null;
|
|
||||||
agents.push({
|
agents.push({
|
||||||
id,
|
id,
|
||||||
name: name || undefined,
|
name: name || undefined,
|
||||||
@ -1077,9 +1129,7 @@ function renderNode(node: Record<string, unknown>) {
|
|||||||
${connected ? "connected" : "offline"}
|
${connected ? "connected" : "offline"}
|
||||||
</span>
|
</span>
|
||||||
${caps.slice(0, 12).map((c) => html`<span class="chip">${String(c)}</span>`)}
|
${caps.slice(0, 12).map((c) => html`<span class="chip">${String(c)}</span>`)}
|
||||||
${commands
|
${commands.slice(0, 8).map((c) => html`<span class="chip">${String(c)}</span>`)}
|
||||||
.slice(0, 8)
|
|
||||||
.map((c) => html`<span class="chip">${String(c)}</span>`)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -28,9 +28,7 @@ export function renderOverview(props: OverviewProps) {
|
|||||||
| { uptimeMs?: number; policy?: { tickIntervalMs?: number } }
|
| { uptimeMs?: number; policy?: { tickIntervalMs?: number } }
|
||||||
| undefined;
|
| undefined;
|
||||||
const uptime = snapshot?.uptimeMs ? formatDurationMs(snapshot.uptimeMs) : "n/a";
|
const uptime = snapshot?.uptimeMs ? formatDurationMs(snapshot.uptimeMs) : "n/a";
|
||||||
const tick = snapshot?.policy?.tickIntervalMs
|
const tick = snapshot?.policy?.tickIntervalMs ? `${snapshot.policy.tickIntervalMs}ms` : "n/a";
|
||||||
? `${snapshot.policy.tickIntervalMs}ms`
|
|
||||||
: "n/a";
|
|
||||||
const authHint = (() => {
|
const authHint = (() => {
|
||||||
if (props.connected || !props.lastError) return null;
|
if (props.connected || !props.lastError) return null;
|
||||||
const lower = props.lastError.toLowerCase();
|
const lower = props.lastError.toLowerCase();
|
||||||
@ -40,13 +38,13 @@ export function renderOverview(props: OverviewProps) {
|
|||||||
const hasPassword = Boolean(props.password.trim());
|
const hasPassword = Boolean(props.password.trim());
|
||||||
if (!hasToken && !hasPassword) {
|
if (!hasToken && !hasPassword) {
|
||||||
return html`
|
return html`
|
||||||
<div class="muted" style="margin-top: 8px;">
|
<div class="muted" style="margin-top: 8px">
|
||||||
This gateway requires auth. Add a token or password, then click Connect.
|
This gateway requires auth. Add a token or password, then click Connect.
|
||||||
<div style="margin-top: 6px;">
|
<div style="margin-top: 6px">
|
||||||
<span class="mono">clawdbot dashboard --no-open</span> → tokenized URL<br />
|
<span class="mono">clawdbot dashboard --no-open</span> → tokenized URL<br />
|
||||||
<span class="mono">clawdbot doctor --generate-gateway-token</span> → set token
|
<span class="mono">clawdbot doctor --generate-gateway-token</span> → set token
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 6px;">
|
<div style="margin-top: 6px">
|
||||||
<a
|
<a
|
||||||
class="session-link"
|
class="session-link"
|
||||||
href="https://docs.clawd.bot/web/dashboard"
|
href="https://docs.clawd.bot/web/dashboard"
|
||||||
@ -60,11 +58,10 @@ export function renderOverview(props: OverviewProps) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<div class="muted" style="margin-top: 8px;">
|
<div class="muted" style="margin-top: 8px">
|
||||||
Auth failed. Re-copy a tokenized URL with
|
Auth failed. Re-copy a tokenized URL with
|
||||||
<span class="mono">clawdbot dashboard --no-open</span>, or update the token,
|
<span class="mono">clawdbot dashboard --no-open</span>, or update the token, then click Connect.
|
||||||
then click Connect.
|
<div style="margin-top: 6px">
|
||||||
<div style="margin-top: 6px;">
|
|
||||||
<a
|
<a
|
||||||
class="session-link"
|
class="session-link"
|
||||||
href="https://docs.clawd.bot/web/dashboard"
|
href="https://docs.clawd.bot/web/dashboard"
|
||||||
@ -86,14 +83,14 @@ export function renderOverview(props: OverviewProps) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<div class="muted" style="margin-top: 8px;">
|
<div class="muted" style="margin-top: 8px">
|
||||||
This page is HTTP, so the browser blocks device identity. Use HTTPS (Tailscale Serve) or
|
This page is HTTP, so the browser blocks device identity. Use HTTPS (Tailscale Serve) or open
|
||||||
open <span class="mono">http://127.0.0.1:18789</span> on the gateway host.
|
<span class="mono">http://127.0.0.1:18789</span> on the gateway host.
|
||||||
<div style="margin-top: 6px;">
|
<div style="margin-top: 6px">
|
||||||
If you must stay on HTTP, set
|
If you must stay on HTTP, set
|
||||||
<span class="mono">gateway.controlUi.allowInsecureAuth: true</span> (token-only).
|
<span class="mono">gateway.controlUi.allowInsecureAuth: true</span> (token-only).
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 6px;">
|
<div style="margin-top: 6px">
|
||||||
<a
|
<a
|
||||||
class="session-link"
|
class="session-link"
|
||||||
href="https://docs.clawd.bot/gateway/tailscale"
|
href="https://docs.clawd.bot/gateway/tailscale"
|
||||||
@ -195,21 +192,23 @@ export function renderOverview(props: OverviewProps) {
|
|||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-label">Last Channels Refresh</div>
|
<div class="stat-label">Last Channels Refresh</div>
|
||||||
<div class="stat-value">
|
<div class="stat-value">
|
||||||
${props.lastChannelsRefresh
|
${props.lastChannelsRefresh ? formatAgo(props.lastChannelsRefresh) : "n/a"}
|
||||||
? formatAgo(props.lastChannelsRefresh)
|
|
||||||
: "n/a"}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${props.lastError
|
${
|
||||||
|
props.lastError
|
||||||
? html`<div class="callout danger" style="margin-top: 14px;">
|
? html`<div class="callout danger" style="margin-top: 14px;">
|
||||||
<div>${props.lastError}</div>
|
<div>${props.lastError}</div>
|
||||||
${authHint ?? ""}
|
${authHint ?? ""}
|
||||||
${insecureContextHint ?? ""}
|
${insecureContextHint ?? ""}
|
||||||
</div>`
|
</div>`
|
||||||
: html`<div class="callout" style="margin-top: 14px;">
|
: html`
|
||||||
|
<div class="callout" style="margin-top: 14px">
|
||||||
Use Channels to link WhatsApp, Telegram, Discord, Signal, or iMessage.
|
Use Channels to link WhatsApp, Telegram, Discord, Signal, or iMessage.
|
||||||
</div>`}
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -227,11 +226,7 @@ export function renderOverview(props: OverviewProps) {
|
|||||||
<div class="card stat-card">
|
<div class="card stat-card">
|
||||||
<div class="stat-label">Cron</div>
|
<div class="stat-label">Cron</div>
|
||||||
<div class="stat-value">
|
<div class="stat-value">
|
||||||
${props.cronEnabled == null
|
${props.cronEnabled == null ? "n/a" : props.cronEnabled ? "Enabled" : "Disabled"}
|
||||||
? "n/a"
|
|
||||||
: props.cronEnabled
|
|
||||||
? "Enabled"
|
|
||||||
: "Disabled"}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="muted">Next wake ${formatNextRun(props.cronNext)}</div>
|
<div class="muted">Next wake ${formatNextRun(props.cronNext)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -141,9 +141,11 @@ export function renderSessions(props: SessionsProps) {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${props.error
|
${
|
||||||
|
props.error
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.error}</div>`
|
? html`<div class="callout danger" style="margin-top: 12px;">${props.error}</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
<div class="muted" style="margin-top: 12px;">
|
<div class="muted" style="margin-top: 12px;">
|
||||||
${props.result ? `Store: ${props.result.path}` : ""}
|
${props.result ? `Store: ${props.result.path}` : ""}
|
||||||
@ -161,11 +163,15 @@ export function renderSessions(props: SessionsProps) {
|
|||||||
<div>Reasoning</div>
|
<div>Reasoning</div>
|
||||||
<div>Actions</div>
|
<div>Actions</div>
|
||||||
</div>
|
</div>
|
||||||
${rows.length === 0
|
${
|
||||||
? html`<div class="muted">No sessions found.</div>`
|
rows.length === 0
|
||||||
|
? html`
|
||||||
|
<div class="muted">No sessions found.</div>
|
||||||
|
`
|
||||||
: rows.map((row) =>
|
: rows.map((row) =>
|
||||||
renderRow(row, props.basePath, props.onPatch, props.onDelete, props.loading),
|
renderRow(row, props.basePath, props.onPatch, props.onDelete, props.loading),
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
@ -193,9 +199,9 @@ function renderRow(
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="table-row">
|
<div class="table-row">
|
||||||
<div class="mono">${canLink
|
<div class="mono">${
|
||||||
? html`<a href=${chatUrl} class="session-link">${displayName}</a>`
|
canLink ? html`<a href=${chatUrl} class="session-link">${displayName}</a>` : displayName
|
||||||
: displayName}</div>
|
}</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
.value=${row.label ?? ""}
|
.value=${row.label ?? ""}
|
||||||
@ -221,9 +227,7 @@ function renderRow(
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${thinkLevels.map((level) =>
|
${thinkLevels.map((level) => html`<option value=${level}>${level || "inherit"}</option>`)}
|
||||||
html`<option value=${level}>${level || "inherit"}</option>`,
|
|
||||||
)}
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -249,8 +253,8 @@ function renderRow(
|
|||||||
onPatch(row.key, { reasoningLevel: value || null });
|
onPatch(row.key, { reasoningLevel: value || null });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${REASONING_LEVELS.map((level) =>
|
${REASONING_LEVELS.map(
|
||||||
html`<option value=${level}>${level || "inherit"}</option>`,
|
(level) => html`<option value=${level}>${level || "inherit"}</option>`,
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -25,10 +25,7 @@ export function renderSkills(props: SkillsProps) {
|
|||||||
const filter = props.filter.trim().toLowerCase();
|
const filter = props.filter.trim().toLowerCase();
|
||||||
const filtered = filter
|
const filtered = filter
|
||||||
? skills.filter((skill) =>
|
? skills.filter((skill) =>
|
||||||
[skill.name, skill.description, skill.source]
|
[skill.name, skill.description, skill.source].join(" ").toLowerCase().includes(filter),
|
||||||
.join(" ")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(filter),
|
|
||||||
)
|
)
|
||||||
: skills;
|
: skills;
|
||||||
|
|
||||||
@ -49,25 +46,30 @@ export function renderSkills(props: SkillsProps) {
|
|||||||
<span>Filter</span>
|
<span>Filter</span>
|
||||||
<input
|
<input
|
||||||
.value=${props.filter}
|
.value=${props.filter}
|
||||||
@input=${(e: Event) =>
|
@input=${(e: Event) => props.onFilterChange((e.target as HTMLInputElement).value)}
|
||||||
props.onFilterChange((e.target as HTMLInputElement).value)}
|
|
||||||
placeholder="Search skills"
|
placeholder="Search skills"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div class="muted">${filtered.length} shown</div>
|
<div class="muted">${filtered.length} shown</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${props.error
|
${
|
||||||
|
props.error
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.error}</div>`
|
? html`<div class="callout danger" style="margin-top: 12px;">${props.error}</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${filtered.length === 0
|
${
|
||||||
? html`<div class="muted" style="margin-top: 16px;">No skills found.</div>`
|
filtered.length === 0
|
||||||
|
? html`
|
||||||
|
<div class="muted" style="margin-top: 16px">No skills found.</div>
|
||||||
|
`
|
||||||
: html`
|
: html`
|
||||||
<div class="list" style="margin-top: 16px;">
|
<div class="list" style="margin-top: 16px;">
|
||||||
${filtered.map((skill) => renderSkill(skill, props))}
|
${filtered.map((skill) => renderSkill(skill, props))}
|
||||||
</div>
|
</div>
|
||||||
`}
|
`
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -76,8 +78,7 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
|
|||||||
const busy = props.busyKey === skill.skillKey;
|
const busy = props.busyKey === skill.skillKey;
|
||||||
const apiKey = props.edits[skill.skillKey] ?? "";
|
const apiKey = props.edits[skill.skillKey] ?? "";
|
||||||
const message = props.messages[skill.skillKey] ?? null;
|
const message = props.messages[skill.skillKey] ?? null;
|
||||||
const canInstall =
|
const canInstall = skill.install.length > 0 && skill.missing.bins.length > 0;
|
||||||
skill.install.length > 0 && skill.missing.bins.length > 0;
|
|
||||||
const missing = [
|
const missing = [
|
||||||
...skill.missing.bins.map((b) => `bin:${b}`),
|
...skill.missing.bins.map((b) => `bin:${b}`),
|
||||||
...skill.missing.env.map((e) => `env:${e}`),
|
...skill.missing.env.map((e) => `env:${e}`),
|
||||||
@ -99,22 +100,32 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
|
|||||||
<span class="chip ${skill.eligible ? "chip-ok" : "chip-warn"}">
|
<span class="chip ${skill.eligible ? "chip-ok" : "chip-warn"}">
|
||||||
${skill.eligible ? "eligible" : "blocked"}
|
${skill.eligible ? "eligible" : "blocked"}
|
||||||
</span>
|
</span>
|
||||||
${skill.disabled ? html`<span class="chip chip-warn">disabled</span>` : nothing}
|
${
|
||||||
|
skill.disabled
|
||||||
|
? html`
|
||||||
|
<span class="chip chip-warn">disabled</span>
|
||||||
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
${missing.length > 0
|
${
|
||||||
|
missing.length > 0
|
||||||
? html`
|
? html`
|
||||||
<div class="muted" style="margin-top: 6px;">
|
<div class="muted" style="margin-top: 6px;">
|
||||||
Missing: ${missing.join(", ")}
|
Missing: ${missing.join(", ")}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
${reasons.length > 0
|
}
|
||||||
|
${
|
||||||
|
reasons.length > 0
|
||||||
? html`
|
? html`
|
||||||
<div class="muted" style="margin-top: 6px;">
|
<div class="muted" style="margin-top: 6px;">
|
||||||
Reason: ${reasons.join(", ")}
|
Reason: ${reasons.join(", ")}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="list-meta">
|
<div class="list-meta">
|
||||||
<div class="row" style="justify-content: flex-end; flex-wrap: wrap;">
|
<div class="row" style="justify-content: flex-end; flex-wrap: wrap;">
|
||||||
@ -125,18 +136,20 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
|
|||||||
>
|
>
|
||||||
${skill.disabled ? "Enable" : "Disable"}
|
${skill.disabled ? "Enable" : "Disable"}
|
||||||
</button>
|
</button>
|
||||||
${canInstall
|
${
|
||||||
|
canInstall
|
||||||
? html`<button
|
? html`<button
|
||||||
class="btn"
|
class="btn"
|
||||||
?disabled=${busy}
|
?disabled=${busy}
|
||||||
@click=${() =>
|
@click=${() => props.onInstall(skill.skillKey, skill.name, skill.install[0].id)}
|
||||||
props.onInstall(skill.skillKey, skill.name, skill.install[0].id)}
|
|
||||||
>
|
>
|
||||||
${busy ? "Installing…" : skill.install[0].label}
|
${busy ? "Installing…" : skill.install[0].label}
|
||||||
</button>`
|
</button>`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
${message
|
${
|
||||||
|
message
|
||||||
? html`<div
|
? html`<div
|
||||||
class="muted"
|
class="muted"
|
||||||
style="margin-top: 8px; color: ${
|
style="margin-top: 8px; color: ${
|
||||||
@ -147,8 +160,10 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
|
|||||||
>
|
>
|
||||||
${message.message}
|
${message.message}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing
|
||||||
${skill.primaryEnv
|
}
|
||||||
|
${
|
||||||
|
skill.primaryEnv
|
||||||
? html`
|
? html`
|
||||||
<div class="field" style="margin-top: 10px;">
|
<div class="field" style="margin-top: 10px;">
|
||||||
<span>API key</span>
|
<span>API key</span>
|
||||||
@ -168,7 +183,8 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
|
|||||||
Save key
|
Save key
|
||||||
</button>
|
</button>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user