mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
refactor(ui): streamline ephemeral state management in chat and config views
- Introduced interfaces for ephemeral state in chat and config views to encapsulate related variables. - Refactored state management to utilize a single object for better organization and maintainability. - Removed legacy state variables and updated related functions to reference the new state structure. - Enhanced readability and consistency across the codebase by standardizing state handling.
This commit is contained in:
@@ -3542,10 +3542,18 @@
|
||||
}
|
||||
|
||||
/* Stagger entrance */
|
||||
.ov-cards .ov-card:nth-child(1) { animation-delay: 0ms; }
|
||||
.ov-cards .ov-card:nth-child(2) { animation-delay: 50ms; }
|
||||
.ov-cards .ov-card:nth-child(3) { animation-delay: 100ms; }
|
||||
.ov-cards .ov-card:nth-child(4) { animation-delay: 150ms; }
|
||||
.ov-cards .ov-card:nth-child(1) {
|
||||
animation-delay: 0ms;
|
||||
}
|
||||
.ov-cards .ov-card:nth-child(2) {
|
||||
animation-delay: 50ms;
|
||||
}
|
||||
.ov-cards .ov-card:nth-child(3) {
|
||||
animation-delay: 100ms;
|
||||
}
|
||||
.ov-cards .ov-card:nth-child(4) {
|
||||
animation-delay: 150ms;
|
||||
}
|
||||
|
||||
/* Recent sessions widget */
|
||||
.ov-recent {
|
||||
|
||||
@@ -43,43 +43,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Sidebar
|
||||
=========================================== */
|
||||
|
||||
.config-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-accent);
|
||||
border-right: 1px solid var(--border);
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:root[data-theme-mode="light"] .config-sidebar {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.config-sidebar__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.config-sidebar__title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.config-sidebar__footer {
|
||||
margin-top: auto;
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.config-search {
|
||||
display: grid;
|
||||
@@ -162,208 +125,6 @@
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.config-search__hint {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.config-search__hint-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.config-search__tag-picker {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-elevated);
|
||||
transition:
|
||||
border-color var(--duration-fast) ease,
|
||||
box-shadow var(--duration-fast) ease,
|
||||
background var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.config-search__tag-picker[open] {
|
||||
border-color: var(--accent);
|
||||
box-shadow: var(--focus-ring);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
:root[data-theme-mode="light"] .config-search__tag-picker {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.config-search__tag-trigger {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-height: 30px;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.config-search__tag-trigger::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.config-search__tag-placeholder {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.config-search__tag-chips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-search__tag-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-full);
|
||||
padding: 2px 7px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.config-search__tag-chip--count {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.config-search__tag-caret {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.config-search__tag-picker[open] .config-search__tag-caret {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.config-search__tag-menu {
|
||||
max-height: 104px;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 6px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.config-search__tag-option {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 6px 8px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--duration-fast) ease,
|
||||
color var(--duration-fast) ease,
|
||||
border-color var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.config-search__tag-option:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.config-search__tag-option.active {
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent);
|
||||
border-color: color-mix(in srgb, var(--accent) 34%, transparent);
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.config-nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 6px 8px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.config-nav::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.config-nav__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-md);
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--duration-fast) ease,
|
||||
color var(--duration-fast) ease,
|
||||
border-color var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.config-nav__item:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
:root[data-theme-mode="light"] .config-nav__item:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.config-nav__item.active {
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent);
|
||||
border-color: color-mix(in srgb, var(--accent) 15%, transparent);
|
||||
}
|
||||
|
||||
.config-nav__icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
opacity: 0.5;
|
||||
transition: opacity var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.config-nav__item:hover .config-nav__icon,
|
||||
.config-nav__item.active .config-nav__icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.config-nav__icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.config-nav__label {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Mode Toggle */
|
||||
.config-mode-toggle {
|
||||
display: flex;
|
||||
@@ -688,56 +449,6 @@
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Subsection Nav */
|
||||
.config-subnav {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
padding: 10px 22px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-accent);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.config-subnav::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:root[data-theme-mode="light"] .config-subnav {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.config-subnav__item {
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-full);
|
||||
padding: 5px 13px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--duration-fast) ease,
|
||||
color var(--duration-fast) ease,
|
||||
border-color var(--duration-fast) ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.config-subnav__item:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
:root[data-theme-mode="light"] .config-subnav__item:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.config-subnav__item.active {
|
||||
color: var(--accent);
|
||||
border-color: color-mix(in srgb, var(--accent) 25%, transparent);
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
/* Content Area */
|
||||
.config-content {
|
||||
flex: 1;
|
||||
@@ -1891,38 +1602,6 @@
|
||||
=========================================== */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.config-sidebar {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.config-sidebar__header {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.config-nav {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 6px;
|
||||
padding: 10px 14px;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.config-nav__item {
|
||||
flex: 0 0 auto;
|
||||
padding: 9px 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.config-nav__label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.config-sidebar__footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
flex-wrap: wrap;
|
||||
padding: 14px 16px;
|
||||
@@ -1964,10 +1643,6 @@
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.config-subnav {
|
||||
padding: 10px 16px 12px;
|
||||
}
|
||||
|
||||
.config-content {
|
||||
padding: 16px;
|
||||
}
|
||||
@@ -2012,16 +1687,6 @@
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.config-nav__icon {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.config-nav__label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.config-section-card__icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
|
||||
@@ -333,13 +333,13 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 14px 14px 10px;
|
||||
padding: 14px 14px 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .sidebar-header {
|
||||
justify-content: center;
|
||||
padding: 12px 10px 8px;
|
||||
padding: 12px 10px 6px;
|
||||
}
|
||||
|
||||
/* Brand lockup */
|
||||
@@ -381,7 +381,7 @@
|
||||
}
|
||||
|
||||
.sidebar--collapsed .sidebar-nav {
|
||||
padding: 4px 8px 8px;
|
||||
padding: 4px 8px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -523,45 +523,6 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Legacy .nav kept for mobile breakpoints */
|
||||
.nav {
|
||||
grid-area: nav;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 12px 10px;
|
||||
background: var(--bg);
|
||||
border-right: 1px solid var(--border);
|
||||
scrollbar-width: none;
|
||||
transition:
|
||||
width 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;
|
||||
}
|
||||
|
||||
.nav::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.shell--chat-focus .nav {
|
||||
width: 0;
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.nav--collapsed {
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Nav collapse toggle */
|
||||
.nav-collapse-toggle {
|
||||
width: 28px;
|
||||
@@ -629,8 +590,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Nav label */
|
||||
.nav-label,
|
||||
.nav-group__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -654,29 +613,24 @@
|
||||
background var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.nav-label:hover,
|
||||
.nav-group__label:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.nav-label--static,
|
||||
.nav-group__label--static {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.nav-label--static:hover,
|
||||
.nav-group__label--static:hover {
|
||||
color: var(--muted);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.nav-label__text,
|
||||
.nav-group__label-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-label__chevron,
|
||||
.nav-group__chevron {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -686,7 +640,6 @@
|
||||
transition: transform var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.nav-label__chevron svg,
|
||||
.nav-group__chevron svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
@@ -697,7 +650,6 @@
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.nav-group--collapsed .nav-label__chevron,
|
||||
.nav-group--collapsed .nav-group__chevron {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
@@ -997,18 +949,6 @@
|
||||
"content";
|
||||
}
|
||||
|
||||
.nav {
|
||||
position: static;
|
||||
max-height: none;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
overflow-x: auto;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 10px 14px;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
|
||||
@@ -2,60 +2,8 @@
|
||||
Mobile Layout
|
||||
=========================================== */
|
||||
|
||||
/* Tablet: Horizontal nav */
|
||||
/* Tablet and smaller: collapse the left nav into a horizontal rail. */
|
||||
@media (max-width: 1100px) {
|
||||
.shell--nav-collapsed .shell-nav {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar--collapsed {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
gap: 4px;
|
||||
padding: 10px 14px;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.nav::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.nav-group__items {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-group--collapsed .nav-group__items {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: var(--radius-md);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.shell,
|
||||
.shell--nav-collapsed {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
@@ -70,12 +18,14 @@
|
||||
grid-template-rows: var(--shell-topbar-height) 0 minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.shell-nav,
|
||||
.shell--nav-collapsed .shell-nav {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.sidebar,
|
||||
.sidebar--collapsed {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
@@ -85,12 +35,18 @@
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.sidebar-header,
|
||||
.sidebar--collapsed .sidebar-header {
|
||||
justify-content: flex-start;
|
||||
padding: 8px 10px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-nav,
|
||||
.sidebar--collapsed .sidebar-nav {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
@@ -104,21 +60,33 @@
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.sidebar-nav::-webkit-scrollbar,
|
||||
.sidebar--collapsed .sidebar-nav::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-group,
|
||||
.nav-group__items,
|
||||
.sidebar--collapsed .nav-group,
|
||||
.sidebar--collapsed .nav-group__items {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .nav-group {
|
||||
.nav-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-group__label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-item,
|
||||
.sidebar--collapsed .nav-item {
|
||||
margin: 0;
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: var(--radius-md);
|
||||
white-space: nowrap;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@@ -127,6 +95,7 @@
|
||||
content: none;
|
||||
}
|
||||
|
||||
.sidebar-footer,
|
||||
.sidebar--collapsed .sidebar-footer {
|
||||
display: none;
|
||||
}
|
||||
@@ -134,17 +103,6 @@
|
||||
|
||||
/* Mobile-specific styles */
|
||||
@media (max-width: 600px) {
|
||||
.shell--nav-collapsed .shell-nav {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar--collapsed {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.shell {
|
||||
--shell-pad: 8px;
|
||||
--shell-gap: 8px;
|
||||
@@ -193,24 +151,17 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Nav */
|
||||
.nav {
|
||||
padding: 8px 10px;
|
||||
gap: 4px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
.shell-nav {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
.nav::-webkit-scrollbar {
|
||||
display: none;
|
||||
.sidebar-header {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
display: none;
|
||||
.sidebar-nav {
|
||||
gap: 6px;
|
||||
padding: 6px 8px 6px 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
@@ -428,10 +379,6 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 6px 8px;
|
||||
font-size: 11px;
|
||||
|
||||
@@ -85,6 +85,18 @@ describe("control UI routing", () => {
|
||||
|
||||
expect(window.matchMedia("(max-width: 768px)").matches).toBe(true);
|
||||
|
||||
const shellNav = app.querySelector<HTMLElement>(".shell-nav");
|
||||
const sidebarNav = app.querySelector<HTMLElement>(".sidebar-nav");
|
||||
expect(shellNav).not.toBeNull();
|
||||
expect(sidebarNav).not.toBeNull();
|
||||
if (shellNav) {
|
||||
expect(getComputedStyle(shellNav).width).toBe(`${window.innerWidth}px`);
|
||||
}
|
||||
if (sidebarNav) {
|
||||
expect(getComputedStyle(sidebarNav).display).toBe("flex");
|
||||
expect(getComputedStyle(sidebarNav).flexDirection).toBe("row");
|
||||
}
|
||||
|
||||
const split = app.querySelector(".chat-split-container");
|
||||
expect(split).not.toBeNull();
|
||||
if (split) {
|
||||
@@ -130,6 +142,8 @@ describe("control UI routing", () => {
|
||||
await nextFrame();
|
||||
}
|
||||
|
||||
expect(app.tab).toBe("chat");
|
||||
expect(app.connected).toBe(true);
|
||||
const container = app.querySelector(".chat-thread");
|
||||
expect(container).not.toBeNull();
|
||||
if (!container) {
|
||||
|
||||
@@ -138,39 +138,47 @@ function getDeletedMessages(sessionKey: string): DeletedMessages {
|
||||
);
|
||||
}
|
||||
|
||||
// Module-level ephemeral UI state (reset on navigation away)
|
||||
let sttRecording = false;
|
||||
let sttInterimText = "";
|
||||
let slashMenuOpen = false;
|
||||
let slashMenuItems: SlashCommandDef[] = [];
|
||||
let slashMenuIndex = 0;
|
||||
let slashMenuMode: "command" | "args" = "command";
|
||||
let slashMenuCommand: SlashCommandDef | null = null;
|
||||
let slashMenuArgItems: string[] = [];
|
||||
let searchOpen = false;
|
||||
let searchQuery = "";
|
||||
let pinnedExpanded = false;
|
||||
interface ChatEphemeralState {
|
||||
sttRecording: boolean;
|
||||
sttInterimText: string;
|
||||
slashMenuOpen: boolean;
|
||||
slashMenuItems: SlashCommandDef[];
|
||||
slashMenuIndex: number;
|
||||
slashMenuMode: "command" | "args";
|
||||
slashMenuCommand: SlashCommandDef | null;
|
||||
slashMenuArgItems: string[];
|
||||
searchOpen: boolean;
|
||||
searchQuery: string;
|
||||
pinnedExpanded: boolean;
|
||||
}
|
||||
|
||||
function createChatEphemeralState(): ChatEphemeralState {
|
||||
return {
|
||||
sttRecording: false,
|
||||
sttInterimText: "",
|
||||
slashMenuOpen: false,
|
||||
slashMenuItems: [],
|
||||
slashMenuIndex: 0,
|
||||
slashMenuMode: "command",
|
||||
slashMenuCommand: null,
|
||||
slashMenuArgItems: [],
|
||||
searchOpen: false,
|
||||
searchQuery: "",
|
||||
pinnedExpanded: false,
|
||||
};
|
||||
}
|
||||
|
||||
const vs = createChatEphemeralState();
|
||||
|
||||
/**
|
||||
* Reset module-level chat view state when navigating away from chat.
|
||||
* Prevents STT recording from continuing after a tab switch and clears
|
||||
* ephemeral search/slash UI that should not survive navigation.
|
||||
* Reset chat view ephemeral state when navigating away.
|
||||
* Stops STT recording and clears search/slash UI that should not survive navigation.
|
||||
*/
|
||||
export function resetChatViewState() {
|
||||
if (sttRecording) {
|
||||
if (vs.sttRecording) {
|
||||
stopStt();
|
||||
sttRecording = false;
|
||||
sttInterimText = "";
|
||||
}
|
||||
slashMenuOpen = false;
|
||||
slashMenuItems = [];
|
||||
slashMenuIndex = 0;
|
||||
slashMenuMode = "command";
|
||||
slashMenuCommand = null;
|
||||
slashMenuArgItems = [];
|
||||
searchOpen = false;
|
||||
searchQuery = "";
|
||||
pinnedExpanded = false;
|
||||
Object.assign(vs, createChatEphemeralState());
|
||||
}
|
||||
|
||||
export const cleanupChatModuleState = resetChatViewState;
|
||||
@@ -413,10 +421,10 @@ function renderAttachmentPreview(props: ChatProps): TemplateResult | typeof noth
|
||||
}
|
||||
|
||||
function resetSlashMenuState(): void {
|
||||
slashMenuMode = "command";
|
||||
slashMenuCommand = null;
|
||||
slashMenuArgItems = [];
|
||||
slashMenuItems = [];
|
||||
vs.slashMenuMode = "command";
|
||||
vs.slashMenuCommand = null;
|
||||
vs.slashMenuArgItems = [];
|
||||
vs.slashMenuItems = [];
|
||||
}
|
||||
|
||||
function updateSlashMenu(value: string, requestUpdate: () => void): void {
|
||||
@@ -431,17 +439,17 @@ function updateSlashMenu(value: string, requestUpdate: () => void): void {
|
||||
? cmd.argOptions.filter((opt) => opt.toLowerCase().startsWith(argFilter))
|
||||
: cmd.argOptions;
|
||||
if (filtered.length > 0) {
|
||||
slashMenuMode = "args";
|
||||
slashMenuCommand = cmd;
|
||||
slashMenuArgItems = filtered;
|
||||
slashMenuOpen = true;
|
||||
slashMenuIndex = 0;
|
||||
slashMenuItems = [];
|
||||
vs.slashMenuMode = "args";
|
||||
vs.slashMenuCommand = cmd;
|
||||
vs.slashMenuArgItems = filtered;
|
||||
vs.slashMenuOpen = true;
|
||||
vs.slashMenuIndex = 0;
|
||||
vs.slashMenuItems = [];
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
slashMenuOpen = false;
|
||||
vs.slashMenuOpen = false;
|
||||
resetSlashMenuState();
|
||||
requestUpdate();
|
||||
return;
|
||||
@@ -451,14 +459,14 @@ function updateSlashMenu(value: string, requestUpdate: () => void): void {
|
||||
const match = value.match(/^\/(\S*)$/);
|
||||
if (match) {
|
||||
const items = getSlashCommandCompletions(match[1]);
|
||||
slashMenuItems = items;
|
||||
slashMenuOpen = items.length > 0;
|
||||
slashMenuIndex = 0;
|
||||
slashMenuMode = "command";
|
||||
slashMenuCommand = null;
|
||||
slashMenuArgItems = [];
|
||||
vs.slashMenuItems = items;
|
||||
vs.slashMenuOpen = items.length > 0;
|
||||
vs.slashMenuIndex = 0;
|
||||
vs.slashMenuMode = "command";
|
||||
vs.slashMenuCommand = null;
|
||||
vs.slashMenuArgItems = [];
|
||||
} else {
|
||||
slashMenuOpen = false;
|
||||
vs.slashMenuOpen = false;
|
||||
resetSlashMenuState();
|
||||
}
|
||||
requestUpdate();
|
||||
@@ -472,17 +480,17 @@ function selectSlashCommand(
|
||||
// Transition to arg picker when the command has fixed options
|
||||
if (cmd.argOptions?.length) {
|
||||
props.onDraftChange(`/${cmd.name} `);
|
||||
slashMenuMode = "args";
|
||||
slashMenuCommand = cmd;
|
||||
slashMenuArgItems = cmd.argOptions;
|
||||
slashMenuOpen = true;
|
||||
slashMenuIndex = 0;
|
||||
slashMenuItems = [];
|
||||
vs.slashMenuMode = "args";
|
||||
vs.slashMenuCommand = cmd;
|
||||
vs.slashMenuArgItems = cmd.argOptions;
|
||||
vs.slashMenuOpen = true;
|
||||
vs.slashMenuIndex = 0;
|
||||
vs.slashMenuItems = [];
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
slashMenuOpen = false;
|
||||
vs.slashMenuOpen = false;
|
||||
resetSlashMenuState();
|
||||
|
||||
if (cmd.executeLocal && !cmd.args) {
|
||||
@@ -503,17 +511,17 @@ function tabCompleteSlashCommand(
|
||||
// Tab: fill in the command text without executing
|
||||
if (cmd.argOptions?.length) {
|
||||
props.onDraftChange(`/${cmd.name} `);
|
||||
slashMenuMode = "args";
|
||||
slashMenuCommand = cmd;
|
||||
slashMenuArgItems = cmd.argOptions;
|
||||
slashMenuOpen = true;
|
||||
slashMenuIndex = 0;
|
||||
slashMenuItems = [];
|
||||
vs.slashMenuMode = "args";
|
||||
vs.slashMenuCommand = cmd;
|
||||
vs.slashMenuArgItems = cmd.argOptions;
|
||||
vs.slashMenuOpen = true;
|
||||
vs.slashMenuIndex = 0;
|
||||
vs.slashMenuItems = [];
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
slashMenuOpen = false;
|
||||
vs.slashMenuOpen = false;
|
||||
resetSlashMenuState();
|
||||
props.onDraftChange(cmd.args ? `/${cmd.name} ` : `/${cmd.name}`);
|
||||
requestUpdate();
|
||||
@@ -525,8 +533,8 @@ function selectSlashArg(
|
||||
requestUpdate: () => void,
|
||||
execute: boolean,
|
||||
): void {
|
||||
const cmdName = slashMenuCommand?.name ?? "";
|
||||
slashMenuOpen = false;
|
||||
const cmdName = vs.slashMenuCommand?.name ?? "";
|
||||
vs.slashMenuOpen = false;
|
||||
resetSlashMenuState();
|
||||
props.onDraftChange(`/${cmdName} ${arg}`);
|
||||
requestUpdate();
|
||||
@@ -595,7 +603,7 @@ function renderWelcomeState(props: ChatProps): TemplateResult {
|
||||
}
|
||||
|
||||
function renderSearchBar(requestUpdate: () => void): TemplateResult | typeof nothing {
|
||||
if (!searchOpen) {
|
||||
if (!vs.searchOpen) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
@@ -604,15 +612,15 @@ function renderSearchBar(requestUpdate: () => void): TemplateResult | typeof not
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search messages..."
|
||||
.value=${searchQuery}
|
||||
.value=${vs.searchQuery}
|
||||
@input=${(e: Event) => {
|
||||
searchQuery = (e.target as HTMLInputElement).value;
|
||||
vs.searchQuery = (e.target as HTMLInputElement).value;
|
||||
requestUpdate();
|
||||
}}
|
||||
/>
|
||||
<button class="btn-ghost" @click=${() => {
|
||||
searchOpen = false;
|
||||
searchQuery = "";
|
||||
vs.searchOpen = false;
|
||||
vs.searchQuery = "";
|
||||
requestUpdate();
|
||||
}}>
|
||||
${icons.x}
|
||||
@@ -643,15 +651,15 @@ function renderPinnedSection(
|
||||
return html`
|
||||
<div class="agent-chat__pinned">
|
||||
<button class="agent-chat__pinned-toggle" @click=${() => {
|
||||
pinnedExpanded = !pinnedExpanded;
|
||||
vs.pinnedExpanded = !vs.pinnedExpanded;
|
||||
requestUpdate();
|
||||
}}>
|
||||
${icons.bookmark}
|
||||
${entries.length} pinned
|
||||
${pinnedExpanded ? icons.chevronDown : icons.chevronRight}
|
||||
${vs.pinnedExpanded ? icons.chevronDown : icons.chevronRight}
|
||||
</button>
|
||||
${
|
||||
pinnedExpanded
|
||||
vs.pinnedExpanded
|
||||
? html`
|
||||
<div class="agent-chat__pinned-list">
|
||||
${entries.map(
|
||||
@@ -680,29 +688,29 @@ function renderSlashMenu(
|
||||
requestUpdate: () => void,
|
||||
props: ChatProps,
|
||||
): TemplateResult | typeof nothing {
|
||||
if (!slashMenuOpen) {
|
||||
if (!vs.slashMenuOpen) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
// Arg-picker mode: show options for the selected command
|
||||
if (slashMenuMode === "args" && slashMenuCommand && slashMenuArgItems.length > 0) {
|
||||
if (vs.slashMenuMode === "args" && vs.slashMenuCommand && vs.slashMenuArgItems.length > 0) {
|
||||
return html`
|
||||
<div class="slash-menu">
|
||||
<div class="slash-menu-group">
|
||||
<div class="slash-menu-group__label">/${slashMenuCommand.name} ${slashMenuCommand.description}</div>
|
||||
${slashMenuArgItems.map(
|
||||
<div class="slash-menu-group__label">/${vs.slashMenuCommand.name} ${vs.slashMenuCommand.description}</div>
|
||||
${vs.slashMenuArgItems.map(
|
||||
(arg, i) => html`
|
||||
<div
|
||||
class="slash-menu-item ${i === slashMenuIndex ? "slash-menu-item--active" : ""}"
|
||||
class="slash-menu-item ${i === vs.slashMenuIndex ? "slash-menu-item--active" : ""}"
|
||||
@click=${() => selectSlashArg(arg, props, requestUpdate, true)}
|
||||
@mouseenter=${() => {
|
||||
slashMenuIndex = i;
|
||||
vs.slashMenuIndex = i;
|
||||
requestUpdate();
|
||||
}}
|
||||
>
|
||||
${slashMenuCommand?.icon ? html`<span class="slash-menu-icon">${icons[slashMenuCommand.icon]}</span>` : nothing}
|
||||
${vs.slashMenuCommand?.icon ? html`<span class="slash-menu-icon">${icons[vs.slashMenuCommand.icon]}</span>` : nothing}
|
||||
<span class="slash-menu-name">${arg}</span>
|
||||
<span class="slash-menu-desc">/${slashMenuCommand?.name} ${arg}</span>
|
||||
<span class="slash-menu-desc">/${vs.slashMenuCommand?.name} ${arg}</span>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
@@ -718,7 +726,7 @@ function renderSlashMenu(
|
||||
}
|
||||
|
||||
// Command mode: show grouped commands
|
||||
if (slashMenuItems.length === 0) {
|
||||
if (vs.slashMenuItems.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
@@ -726,8 +734,8 @@ function renderSlashMenu(
|
||||
SlashCommandCategory,
|
||||
Array<{ cmd: SlashCommandDef; globalIdx: number }>
|
||||
>();
|
||||
for (let i = 0; i < slashMenuItems.length; i++) {
|
||||
const cmd = slashMenuItems[i];
|
||||
for (let i = 0; i < vs.slashMenuItems.length; i++) {
|
||||
const cmd = vs.slashMenuItems[i];
|
||||
const cat = cmd.category ?? "session";
|
||||
let list = grouped.get(cat);
|
||||
if (!list) {
|
||||
@@ -745,10 +753,10 @@ function renderSlashMenu(
|
||||
${entries.map(
|
||||
({ cmd, globalIdx }) => html`
|
||||
<div
|
||||
class="slash-menu-item ${globalIdx === slashMenuIndex ? "slash-menu-item--active" : ""}"
|
||||
class="slash-menu-item ${globalIdx === vs.slashMenuIndex ? "slash-menu-item--active" : ""}"
|
||||
@click=${() => selectSlashCommand(cmd, props, requestUpdate)}
|
||||
@mouseenter=${() => {
|
||||
slashMenuIndex = globalIdx;
|
||||
vs.slashMenuIndex = globalIdx;
|
||||
requestUpdate();
|
||||
}}
|
||||
>
|
||||
@@ -873,9 +881,9 @@ export function renderChat(props: ChatProps) {
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${isEmpty && !searchOpen ? renderWelcomeState(props) : nothing}
|
||||
${isEmpty && !vs.searchOpen ? renderWelcomeState(props) : nothing}
|
||||
${
|
||||
isEmpty && searchOpen
|
||||
isEmpty && vs.searchOpen
|
||||
? html`
|
||||
<div class="agent-chat__empty">No matching messages</div>
|
||||
`
|
||||
@@ -933,30 +941,30 @@ export function renderChat(props: ChatProps) {
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Slash menu navigation — arg mode
|
||||
if (slashMenuOpen && slashMenuMode === "args" && slashMenuArgItems.length > 0) {
|
||||
const len = slashMenuArgItems.length;
|
||||
if (vs.slashMenuOpen && vs.slashMenuMode === "args" && vs.slashMenuArgItems.length > 0) {
|
||||
const len = vs.slashMenuArgItems.length;
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
slashMenuIndex = (slashMenuIndex + 1) % len;
|
||||
vs.slashMenuIndex = (vs.slashMenuIndex + 1) % len;
|
||||
requestUpdate();
|
||||
return;
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
slashMenuIndex = (slashMenuIndex - 1 + len) % len;
|
||||
vs.slashMenuIndex = (vs.slashMenuIndex - 1 + len) % len;
|
||||
requestUpdate();
|
||||
return;
|
||||
case "Tab":
|
||||
e.preventDefault();
|
||||
selectSlashArg(slashMenuArgItems[slashMenuIndex], props, requestUpdate, false);
|
||||
selectSlashArg(vs.slashMenuArgItems[vs.slashMenuIndex], props, requestUpdate, false);
|
||||
return;
|
||||
case "Enter":
|
||||
e.preventDefault();
|
||||
selectSlashArg(slashMenuArgItems[slashMenuIndex], props, requestUpdate, true);
|
||||
selectSlashArg(vs.slashMenuArgItems[vs.slashMenuIndex], props, requestUpdate, true);
|
||||
return;
|
||||
case "Escape":
|
||||
e.preventDefault();
|
||||
slashMenuOpen = false;
|
||||
vs.slashMenuOpen = false;
|
||||
resetSlashMenuState();
|
||||
requestUpdate();
|
||||
return;
|
||||
@@ -964,30 +972,30 @@ export function renderChat(props: ChatProps) {
|
||||
}
|
||||
|
||||
// Slash menu navigation — command mode
|
||||
if (slashMenuOpen && slashMenuItems.length > 0) {
|
||||
const len = slashMenuItems.length;
|
||||
if (vs.slashMenuOpen && vs.slashMenuItems.length > 0) {
|
||||
const len = vs.slashMenuItems.length;
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
slashMenuIndex = (slashMenuIndex + 1) % len;
|
||||
vs.slashMenuIndex = (vs.slashMenuIndex + 1) % len;
|
||||
requestUpdate();
|
||||
return;
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
slashMenuIndex = (slashMenuIndex - 1 + len) % len;
|
||||
vs.slashMenuIndex = (vs.slashMenuIndex - 1 + len) % len;
|
||||
requestUpdate();
|
||||
return;
|
||||
case "Tab":
|
||||
e.preventDefault();
|
||||
tabCompleteSlashCommand(slashMenuItems[slashMenuIndex], props, requestUpdate);
|
||||
tabCompleteSlashCommand(vs.slashMenuItems[vs.slashMenuIndex], props, requestUpdate);
|
||||
return;
|
||||
case "Enter":
|
||||
e.preventDefault();
|
||||
selectSlashCommand(slashMenuItems[slashMenuIndex], props, requestUpdate);
|
||||
selectSlashCommand(vs.slashMenuItems[vs.slashMenuIndex], props, requestUpdate);
|
||||
return;
|
||||
case "Escape":
|
||||
e.preventDefault();
|
||||
slashMenuOpen = false;
|
||||
vs.slashMenuOpen = false;
|
||||
resetSlashMenuState();
|
||||
requestUpdate();
|
||||
return;
|
||||
@@ -1015,9 +1023,9 @@ export function renderChat(props: ChatProps) {
|
||||
// Cmd+F for search
|
||||
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key === "f") {
|
||||
e.preventDefault();
|
||||
searchOpen = !searchOpen;
|
||||
if (!searchOpen) {
|
||||
searchQuery = "";
|
||||
vs.searchOpen = !vs.searchOpen;
|
||||
if (!vs.searchOpen) {
|
||||
vs.searchQuery = "";
|
||||
}
|
||||
requestUpdate();
|
||||
return;
|
||||
@@ -1173,7 +1181,7 @@ export function renderChat(props: ChatProps) {
|
||||
@change=${(e: Event) => handleFileSelect(e, props)}
|
||||
/>
|
||||
|
||||
${sttRecording && sttInterimText ? html`<div class="agent-chat__stt-interim">${sttInterimText}</div>` : nothing}
|
||||
${vs.sttRecording && vs.sttInterimText ? html`<div class="agent-chat__stt-interim">${vs.sttInterimText}</div>` : nothing}
|
||||
|
||||
<textarea
|
||||
${ref((el) => el && adjustTextareaHeight(el as HTMLTextAreaElement))}
|
||||
@@ -1183,7 +1191,7 @@ export function renderChat(props: ChatProps) {
|
||||
@keydown=${handleKeyDown}
|
||||
@input=${handleInput}
|
||||
@paste=${(e: ClipboardEvent) => handlePaste(e, props)}
|
||||
placeholder=${sttRecording ? "Listening..." : placeholder}
|
||||
placeholder=${vs.sttRecording ? "Listening..." : placeholder}
|
||||
rows="1"
|
||||
></textarea>
|
||||
|
||||
@@ -1204,12 +1212,12 @@ export function renderChat(props: ChatProps) {
|
||||
isSttSupported()
|
||||
? html`
|
||||
<button
|
||||
class="agent-chat__input-btn ${sttRecording ? "agent-chat__input-btn--recording" : ""}"
|
||||
class="agent-chat__input-btn ${vs.sttRecording ? "agent-chat__input-btn--recording" : ""}"
|
||||
@click=${() => {
|
||||
if (sttRecording) {
|
||||
if (vs.sttRecording) {
|
||||
stopStt();
|
||||
sttRecording = false;
|
||||
sttInterimText = "";
|
||||
vs.sttRecording = false;
|
||||
vs.sttInterimText = "";
|
||||
requestUpdate();
|
||||
} else {
|
||||
const started = startStt({
|
||||
@@ -1218,37 +1226,37 @@ export function renderChat(props: ChatProps) {
|
||||
const current = getDraft();
|
||||
const sep = current && !current.endsWith(" ") ? " " : "";
|
||||
props.onDraftChange(current + sep + text);
|
||||
sttInterimText = "";
|
||||
vs.sttInterimText = "";
|
||||
} else {
|
||||
sttInterimText = text;
|
||||
vs.sttInterimText = text;
|
||||
}
|
||||
requestUpdate();
|
||||
},
|
||||
onStart: () => {
|
||||
sttRecording = true;
|
||||
vs.sttRecording = true;
|
||||
requestUpdate();
|
||||
},
|
||||
onEnd: () => {
|
||||
sttRecording = false;
|
||||
sttInterimText = "";
|
||||
vs.sttRecording = false;
|
||||
vs.sttInterimText = "";
|
||||
requestUpdate();
|
||||
},
|
||||
onError: () => {
|
||||
sttRecording = false;
|
||||
sttInterimText = "";
|
||||
vs.sttRecording = false;
|
||||
vs.sttInterimText = "";
|
||||
requestUpdate();
|
||||
},
|
||||
});
|
||||
if (started) {
|
||||
sttRecording = true;
|
||||
vs.sttRecording = true;
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
}}
|
||||
title=${sttRecording ? "Stop recording" : "Voice input"}
|
||||
title=${vs.sttRecording ? "Stop recording" : "Voice input"}
|
||||
?disabled=${!props.connected}
|
||||
>
|
||||
${sttRecording ? icons.micOff : icons.mic}
|
||||
${vs.sttRecording ? icons.micOff : icons.mic}
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
|
||||
@@ -651,14 +651,27 @@ function renderAppearanceSection(props: ConfigProps) {
|
||||
`;
|
||||
}
|
||||
|
||||
let rawRevealed = false;
|
||||
let envRevealed = false;
|
||||
let validityDismissed = false;
|
||||
const revealedSensitivePaths = new Set<string>();
|
||||
interface ConfigEphemeralState {
|
||||
rawRevealed: boolean;
|
||||
envRevealed: boolean;
|
||||
validityDismissed: boolean;
|
||||
revealedSensitivePaths: Set<string>;
|
||||
}
|
||||
|
||||
function createConfigEphemeralState(): ConfigEphemeralState {
|
||||
return {
|
||||
rawRevealed: false,
|
||||
envRevealed: false,
|
||||
validityDismissed: false,
|
||||
revealedSensitivePaths: new Set(),
|
||||
};
|
||||
}
|
||||
|
||||
const cvs = createConfigEphemeralState();
|
||||
|
||||
function isSensitivePathRevealed(path: Array<string | number>): boolean {
|
||||
const key = pathKey(path);
|
||||
return key ? revealedSensitivePaths.has(key) : false;
|
||||
return key ? cvs.revealedSensitivePaths.has(key) : false;
|
||||
}
|
||||
|
||||
function toggleSensitivePathReveal(path: Array<string | number>) {
|
||||
@@ -666,18 +679,15 @@ function toggleSensitivePathReveal(path: Array<string | number>) {
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
if (revealedSensitivePaths.has(key)) {
|
||||
revealedSensitivePaths.delete(key);
|
||||
if (cvs.revealedSensitivePaths.has(key)) {
|
||||
cvs.revealedSensitivePaths.delete(key);
|
||||
} else {
|
||||
revealedSensitivePaths.add(key);
|
||||
cvs.revealedSensitivePaths.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
export function resetConfigViewStateForTests() {
|
||||
rawRevealed = false;
|
||||
envRevealed = false;
|
||||
validityDismissed = false;
|
||||
revealedSensitivePaths.clear();
|
||||
Object.assign(cvs, createConfigEphemeralState());
|
||||
}
|
||||
|
||||
export function renderConfig(props: ConfigProps) {
|
||||
@@ -697,7 +707,7 @@ export function renderConfig(props: ConfigProps) {
|
||||
if (formUnsafe && props.formMode === "form") {
|
||||
props.onFormModeChange("raw");
|
||||
}
|
||||
const envSensitiveVisible = !props.streamMode && envRevealed;
|
||||
const envSensitiveVisible = !props.streamMode && cvs.envRevealed;
|
||||
|
||||
// Build categorised nav from schema - only include sections that exist in the schema
|
||||
const schemaProps = analysis.schema?.properties ?? {};
|
||||
@@ -916,7 +926,7 @@ export function renderConfig(props: ConfigProps) {
|
||||
</div>
|
||||
|
||||
${
|
||||
validity === "invalid" && !validityDismissed
|
||||
validity === "invalid" && !cvs.validityDismissed
|
||||
? html`
|
||||
<div class="config-validity-warning">
|
||||
<svg class="config-validity-warning__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16">
|
||||
@@ -928,7 +938,7 @@ export function renderConfig(props: ConfigProps) {
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
@click=${() => {
|
||||
validityDismissed = true;
|
||||
cvs.validityDismissed = true;
|
||||
props.onRawChange(props.raw);
|
||||
}}
|
||||
>Don't remind again</button>
|
||||
@@ -1015,7 +1025,7 @@ export function renderConfig(props: ConfigProps) {
|
||||
if (props.streamMode) {
|
||||
return;
|
||||
}
|
||||
envRevealed = !envRevealed;
|
||||
cvs.envRevealed = !cvs.envRevealed;
|
||||
props.onRawChange(props.raw);
|
||||
}}
|
||||
>
|
||||
@@ -1083,7 +1093,7 @@ export function renderConfig(props: ConfigProps) {
|
||||
props.streamMode && containsSensitiveKeywords(props.raw);
|
||||
const blurred =
|
||||
(sensitiveCount > 0 || rawHasSensitiveKeywords) &&
|
||||
(props.streamMode || !rawRevealed);
|
||||
(props.streamMode || !cvs.rawRevealed);
|
||||
const canReveal =
|
||||
(sensitiveCount > 0 || rawHasSensitiveKeywords) && !props.streamMode;
|
||||
return html`
|
||||
@@ -1121,7 +1131,7 @@ export function renderConfig(props: ConfigProps) {
|
||||
if (!canReveal) {
|
||||
return;
|
||||
}
|
||||
rawRevealed = !rawRevealed;
|
||||
cvs.rawRevealed = !cvs.rawRevealed;
|
||||
props.onRawChange(props.raw);
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user