feat(dashboard): add markdown rendering, project grouping, and visual refresh
Markdown & syntax highlighting: - Replace homegrown backtick parser with marked + DOMPurify + highlight.js - Register 10 language grammars (JS, TS, Bash, JSON, Python, Rust, CSS, HTML/XML, SQL, YAML) for fenced code block highlighting - Sanitize rendered HTML via DOMPurify to prevent XSS from message content Project-based session grouping: - Replace four status-based groups (attention/active/starting/done) with project-directory grouping via groupSessionsByProject() - Groups sort by most urgent status first, then most recent activity - Sessions within each group sort by urgency then recency - Group headers show project name, agent count, and per-status chips - Project directory path shown below group header Visual refinements: - Darken color palette across the board (bg, surface, scrollbar, glass panels) for deeper contrast - Add agent-header-codex and agent-header-claude CSS classes for color-coded card and modal headers (teal vs purple tints) - Increase agent badge visibility with higher opacity borders/backgrounds - Cards now fixed-width 600px with taller height (850px) in flex-wrap layout instead of responsive grid - Input focus border color now matches the session's status color instead of hardcoded active/attention colors - Send button background also matches status color for visual coherence - Lock body scroll when session modal is open - Add agent badge to modal header for consistent identification
This commit is contained in:
@@ -15,17 +15,17 @@
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
bg: '#060d1a',
|
bg: '#01040b',
|
||||||
surface: '#0e1728',
|
surface: '#070d18',
|
||||||
surface2: '#16233a',
|
surface2: '#0d1830',
|
||||||
selection: '#2a3c5c',
|
selection: '#223454',
|
||||||
fg: '#d9e3f7',
|
fg: '#e0ebff',
|
||||||
bright: '#f7faff',
|
bright: '#fbfdff',
|
||||||
dim: '#92a6c9',
|
dim: '#8ba3cc',
|
||||||
active: '#69c39a',
|
active: '#5fd0a4',
|
||||||
attention: '#d4ad5d',
|
attention: '#e0b45e',
|
||||||
starting: '#79a6ff',
|
starting: '#7cb2ff',
|
||||||
done: '#d19189',
|
done: '#e39a8c',
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
display: ['Space Grotesk', 'IBM Plex Sans', 'sans-serif'],
|
display: ['Space Grotesk', 'IBM Plex Sans', 'sans-serif'],
|
||||||
@@ -84,9 +84,15 @@
|
|||||||
'border-emerald-500/30',
|
'border-emerald-500/30',
|
||||||
'bg-emerald-500/10',
|
'bg-emerald-500/10',
|
||||||
'text-emerald-400',
|
'text-emerald-400',
|
||||||
|
'border-emerald-400/45',
|
||||||
|
'bg-emerald-500/14',
|
||||||
|
'text-emerald-300',
|
||||||
'border-violet-500/30',
|
'border-violet-500/30',
|
||||||
'bg-violet-500/10',
|
'bg-violet-500/10',
|
||||||
'text-violet-400',
|
'text-violet-400',
|
||||||
|
'border-violet-400/45',
|
||||||
|
'bg-violet-500/14',
|
||||||
|
'text-violet-300',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -97,13 +103,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-flat: #060d1a;
|
--bg-flat: #01040b;
|
||||||
--glass-border: rgba(137, 171, 226, 0.24);
|
--glass-border: rgba(116, 154, 214, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #4e658d #0f1a2d;
|
scrollbar-color: #445f8e #0a1222;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -111,7 +117,7 @@
|
|||||||
font-family: 'IBM Plex Sans', system-ui, sans-serif;
|
font-family: 'IBM Plex Sans', system-ui, sans-serif;
|
||||||
background: var(--bg-flat);
|
background: var(--bg-flat);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
color: #d9e3f7;
|
color: #e0ebff;
|
||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +126,7 @@
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app > * {
|
#app > *:not(.fixed) {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
@@ -131,16 +137,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #0f1a2d;
|
background: #0a1222;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #4e658d;
|
background: #445f8e;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #617ba8;
|
background: #5574aa;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-attention {
|
@keyframes pulse-attention {
|
||||||
@@ -161,8 +167,227 @@
|
|||||||
.glass-panel {
|
.glass-panel {
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
background: rgba(11, 20, 35, 0.94);
|
background: rgba(7, 13, 24, 0.95);
|
||||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(177, 204, 252, 0.06);
|
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.36), inset 0 1px 0 rgba(151, 185, 245, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-header-codex {
|
||||||
|
background: rgba(20, 60, 54, 0.4);
|
||||||
|
border-bottom-color: rgba(116, 227, 196, 0.34);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-header-claude {
|
||||||
|
background: rgba(45, 36, 78, 0.42);
|
||||||
|
border-bottom-color: rgba(179, 154, 255, 0.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown content styling */
|
||||||
|
.md-content {
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content > *:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content > *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content h1, .md-content h2, .md-content h3,
|
||||||
|
.md-content h4, .md-content h5, .md-content h6 {
|
||||||
|
font-family: 'Space Grotesk', 'IBM Plex Sans', sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fbfdff;
|
||||||
|
margin: 1.25em 0 0.5em;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content h1 { font-size: 1.4em; }
|
||||||
|
.md-content h2 { font-size: 1.25em; }
|
||||||
|
.md-content h3 { font-size: 1.1em; }
|
||||||
|
.md-content h4, .md-content h5, .md-content h6 { font-size: 1em; }
|
||||||
|
|
||||||
|
.md-content p {
|
||||||
|
margin: 0.75em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content strong {
|
||||||
|
color: #fbfdff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content em {
|
||||||
|
color: #c8d8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content a {
|
||||||
|
color: #7cb2ff;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content a:hover {
|
||||||
|
color: #a8ccff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content code {
|
||||||
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
background: rgba(1, 4, 11, 0.55);
|
||||||
|
border: 1px solid rgba(34, 52, 84, 0.8);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.15em 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content pre {
|
||||||
|
margin: 0.75em 0;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
background: rgba(1, 4, 11, 0.65);
|
||||||
|
border: 1px solid rgba(34, 52, 84, 0.75);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 0.85em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content pre code {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content ul, .md-content ol {
|
||||||
|
margin: 0.75em 0;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content li {
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content li > ul, .md-content li > ol {
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content blockquote {
|
||||||
|
margin: 0.75em 0;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
border-left: 3px solid rgba(124, 178, 255, 0.5);
|
||||||
|
background: rgba(34, 52, 84, 0.25);
|
||||||
|
border-radius: 0 0.5rem 0.5rem 0;
|
||||||
|
color: #c8d8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid rgba(34, 52, 84, 0.6);
|
||||||
|
margin: 1.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 0.75em 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content th, .md-content td {
|
||||||
|
border: 1px solid rgba(34, 52, 84, 0.6);
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content th {
|
||||||
|
background: rgba(34, 52, 84, 0.35);
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fbfdff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-content tr:nth-child(even) {
|
||||||
|
background: rgba(34, 52, 84, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlight.js syntax theme (dark) */
|
||||||
|
.hljs {
|
||||||
|
color: #e0ebff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-selector-tag,
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-tag {
|
||||||
|
color: #c792ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-title,
|
||||||
|
.hljs-section,
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-literal,
|
||||||
|
.hljs-template-tag,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-addition {
|
||||||
|
color: #c3e88d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote,
|
||||||
|
.hljs-deletion,
|
||||||
|
.hljs-meta {
|
||||||
|
color: #697098;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-selector-tag,
|
||||||
|
.hljs-literal,
|
||||||
|
.hljs-title,
|
||||||
|
.hljs-section,
|
||||||
|
.hljs-doctag,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-selector-class,
|
||||||
|
.hljs-quote,
|
||||||
|
.hljs-template-tag,
|
||||||
|
.hljs-deletion {
|
||||||
|
color: #f78c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-title.function_,
|
||||||
|
.hljs-subst,
|
||||||
|
.hljs-symbol,
|
||||||
|
.hljs-bullet,
|
||||||
|
.hljs-link {
|
||||||
|
color: #82aaff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-selector-attr,
|
||||||
|
.hljs-selector-pseudo,
|
||||||
|
.hljs-variable,
|
||||||
|
.hljs-template-variable {
|
||||||
|
color: #ffcb6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-attr {
|
||||||
|
color: #89ddff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-link {
|
||||||
|
color: #89ddff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -171,8 +396,52 @@
|
|||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { h, render } from 'https://esm.sh/preact@10.19.3';
|
import { h, render } from 'https://esm.sh/preact@10.19.3';
|
||||||
import { useState, useEffect, useRef, useCallback } from 'https://esm.sh/preact@10.19.3/hooks';
|
import { useState, useEffect, useRef, useCallback, useMemo } from 'https://esm.sh/preact@10.19.3/hooks';
|
||||||
import htm from 'https://esm.sh/htm@3.1.1';
|
import htm from 'https://esm.sh/htm@3.1.1';
|
||||||
|
import { marked } from 'https://esm.sh/marked@15.0.7';
|
||||||
|
import DOMPurify from 'https://esm.sh/dompurify@3.2.4';
|
||||||
|
import hljs from 'https://esm.sh/highlight.js@11.11.1/lib/core';
|
||||||
|
import langJavascript from 'https://esm.sh/highlight.js@11.11.1/lib/languages/javascript';
|
||||||
|
import langTypescript from 'https://esm.sh/highlight.js@11.11.1/lib/languages/typescript';
|
||||||
|
import langBash from 'https://esm.sh/highlight.js@11.11.1/lib/languages/bash';
|
||||||
|
import langJson from 'https://esm.sh/highlight.js@11.11.1/lib/languages/json';
|
||||||
|
import langPython from 'https://esm.sh/highlight.js@11.11.1/lib/languages/python';
|
||||||
|
import langRust from 'https://esm.sh/highlight.js@11.11.1/lib/languages/rust';
|
||||||
|
import langCss from 'https://esm.sh/highlight.js@11.11.1/lib/languages/css';
|
||||||
|
import langXml from 'https://esm.sh/highlight.js@11.11.1/lib/languages/xml';
|
||||||
|
import langSql from 'https://esm.sh/highlight.js@11.11.1/lib/languages/sql';
|
||||||
|
import langYaml from 'https://esm.sh/highlight.js@11.11.1/lib/languages/yaml';
|
||||||
|
|
||||||
|
// Register highlight.js languages
|
||||||
|
hljs.registerLanguage('javascript', langJavascript);
|
||||||
|
hljs.registerLanguage('js', langJavascript);
|
||||||
|
hljs.registerLanguage('typescript', langTypescript);
|
||||||
|
hljs.registerLanguage('ts', langTypescript);
|
||||||
|
hljs.registerLanguage('bash', langBash);
|
||||||
|
hljs.registerLanguage('sh', langBash);
|
||||||
|
hljs.registerLanguage('shell', langBash);
|
||||||
|
hljs.registerLanguage('json', langJson);
|
||||||
|
hljs.registerLanguage('python', langPython);
|
||||||
|
hljs.registerLanguage('py', langPython);
|
||||||
|
hljs.registerLanguage('rust', langRust);
|
||||||
|
hljs.registerLanguage('css', langCss);
|
||||||
|
hljs.registerLanguage('html', langXml);
|
||||||
|
hljs.registerLanguage('xml', langXml);
|
||||||
|
hljs.registerLanguage('sql', langSql);
|
||||||
|
hljs.registerLanguage('yaml', langYaml);
|
||||||
|
hljs.registerLanguage('yml', langYaml);
|
||||||
|
|
||||||
|
// Configure marked with highlight.js
|
||||||
|
marked.setOptions({
|
||||||
|
highlight(code, lang) {
|
||||||
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
|
return hljs.highlight(code, { language: lang }).value;
|
||||||
|
}
|
||||||
|
return hljs.highlightAuto(code).value;
|
||||||
|
},
|
||||||
|
breaks: true,
|
||||||
|
gfm: true,
|
||||||
|
});
|
||||||
|
|
||||||
const html = htm.bind(h);
|
const html = htm.bind(h);
|
||||||
|
|
||||||
@@ -207,28 +476,12 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render markdown-ish content with basic styling
|
// Render markdown content with syntax highlighting
|
||||||
function renderContent(content) {
|
function renderContent(content) {
|
||||||
if (!content) return '';
|
if (!content) return '';
|
||||||
|
const rawHtml = marked.parse(content);
|
||||||
// Split by code blocks first
|
const safeHtml = DOMPurify.sanitize(rawHtml);
|
||||||
const parts = content.split(/(```[\s\S]*?```|`[^`]+`)/g);
|
return html`<div class="md-content" dangerouslySetInnerHTML=${{ __html: safeHtml }} />`;
|
||||||
|
|
||||||
return parts.map((part, i) => {
|
|
||||||
// Multi-line code block
|
|
||||||
if (part.startsWith('```') && part.endsWith('```')) {
|
|
||||||
const code = part.slice(3, -3).replace(/^\w+\n/, ''); // Remove language identifier
|
|
||||||
return html`<pre key=${i} class="my-2 overflow-x-auto whitespace-pre-wrap rounded-xl border border-selection/75 bg-bg/65 p-3 font-mono text-xs leading-relaxed">${code}</pre>`;
|
|
||||||
}
|
|
||||||
// Inline code
|
|
||||||
if (part.startsWith('`') && part.endsWith('`')) {
|
|
||||||
return html`<code key=${i} class="rounded border border-selection/80 bg-bg/55 px-1.5 py-0.5 font-mono text-[0.9em]">${part.slice(1, -1)}</code>`;
|
|
||||||
}
|
|
||||||
// Regular text - preserve line breaks
|
|
||||||
return part.split('\n').map((line, j) =>
|
|
||||||
j === 0 ? line : html`<br key="${i}-${j}" />${line}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
@@ -254,39 +507,84 @@
|
|||||||
label: 'Needs attention',
|
label: 'Needs attention',
|
||||||
dot: 'bg-attention pulse-attention',
|
dot: 'bg-attention pulse-attention',
|
||||||
badge: 'bg-attention/18 text-attention border-attention/40',
|
badge: 'bg-attention/18 text-attention border-attention/40',
|
||||||
borderColor: '#d4ad5d',
|
borderColor: '#e0b45e',
|
||||||
};
|
};
|
||||||
case 'active':
|
case 'active':
|
||||||
return {
|
return {
|
||||||
label: 'Active',
|
label: 'Active',
|
||||||
dot: 'bg-active',
|
dot: 'bg-active',
|
||||||
badge: 'bg-active/18 text-active border-active/40',
|
badge: 'bg-active/18 text-active border-active/40',
|
||||||
borderColor: '#69c39a',
|
borderColor: '#5fd0a4',
|
||||||
};
|
};
|
||||||
case 'starting':
|
case 'starting':
|
||||||
return {
|
return {
|
||||||
label: 'Starting',
|
label: 'Starting',
|
||||||
dot: 'bg-starting',
|
dot: 'bg-starting',
|
||||||
badge: 'bg-starting/18 text-starting border-starting/40',
|
badge: 'bg-starting/18 text-starting border-starting/40',
|
||||||
borderColor: '#79a6ff',
|
borderColor: '#7cb2ff',
|
||||||
};
|
};
|
||||||
case 'done':
|
case 'done':
|
||||||
return {
|
return {
|
||||||
label: 'Done',
|
label: 'Done',
|
||||||
dot: 'bg-done',
|
dot: 'bg-done',
|
||||||
badge: 'bg-done/18 text-done border-done/40',
|
badge: 'bg-done/18 text-done border-done/40',
|
||||||
borderColor: '#d19189',
|
borderColor: '#e39a8c',
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
label: status || 'Unknown',
|
label: status || 'Unknown',
|
||||||
dot: 'bg-dim',
|
dot: 'bg-dim',
|
||||||
badge: 'bg-selection text-dim border-selection',
|
badge: 'bg-selection text-dim border-selection',
|
||||||
borderColor: '#2a3c5c',
|
borderColor: '#223454',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Grouping: project-based layout
|
||||||
|
// -----------------------------
|
||||||
|
const STATUS_PRIORITY = { needs_attention: 0, active: 1, starting: 2, done: 3 };
|
||||||
|
|
||||||
|
function groupSessionsByProject(sessions) {
|
||||||
|
const groups = new Map();
|
||||||
|
|
||||||
|
for (const session of sessions) {
|
||||||
|
const key = session.project_dir || session.cwd || 'unknown';
|
||||||
|
if (!groups.has(key)) {
|
||||||
|
groups.set(key, {
|
||||||
|
projectDir: key,
|
||||||
|
projectName: session.project || key.split('/').pop() || 'Unknown',
|
||||||
|
sessions: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
groups.get(key).sessions.push(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = Array.from(groups.values());
|
||||||
|
|
||||||
|
// Sort groups: most urgent status first, then most recent activity
|
||||||
|
result.sort((a, b) => {
|
||||||
|
const aWorst = Math.min(...a.sessions.map(s => STATUS_PRIORITY[s.status] ?? 99));
|
||||||
|
const bWorst = Math.min(...b.sessions.map(s => STATUS_PRIORITY[s.status] ?? 99));
|
||||||
|
if (aWorst !== bWorst) return aWorst - bWorst;
|
||||||
|
const aRecent = Math.max(...a.sessions.map(s => new Date(s.last_event_at || 0).getTime()));
|
||||||
|
const bRecent = Math.max(...b.sessions.map(s => new Date(s.last_event_at || 0).getTime()));
|
||||||
|
return bRecent - aRecent;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort sessions within each group: urgent first, then most recent
|
||||||
|
for (const group of result) {
|
||||||
|
group.sessions.sort((a, b) => {
|
||||||
|
const aPri = STATUS_PRIORITY[a.status] ?? 99;
|
||||||
|
const bPri = STATUS_PRIORITY[b.status] ?? 99;
|
||||||
|
if (aPri !== bPri) return aPri - bPri;
|
||||||
|
return (b.last_event_at || '').localeCompare(a.last_event_at || '');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
// ChatMessages Component
|
// ChatMessages Component
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
@@ -361,7 +659,7 @@
|
|||||||
<div class="mb-1 font-mono text-micro uppercase tracking-[0.14em] text-dim">
|
<div class="mb-1 font-mono text-micro uppercase tracking-[0.14em] text-dim">
|
||||||
${msg.role === 'user' ? 'Operator' : 'Agent'}
|
${msg.role === 'user' ? 'Operator' : 'Agent'}
|
||||||
</div>
|
</div>
|
||||||
${msg.content}
|
${renderContent(msg.content)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`)}
|
`)}
|
||||||
@@ -452,8 +750,10 @@
|
|||||||
// -----------------------------
|
// -----------------------------
|
||||||
// QuestionBlock Component
|
// QuestionBlock Component
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
function QuestionBlock({ questions, sessionId, onRespond }) {
|
function QuestionBlock({ questions, sessionId, status, onRespond }) {
|
||||||
const [freeformText, setFreeformText] = useState('');
|
const [freeformText, setFreeformText] = useState('');
|
||||||
|
const [focused, setFocused] = useState(false);
|
||||||
|
const meta = getStatusMeta(status);
|
||||||
|
|
||||||
if (!questions || questions.length === 0) return null;
|
if (!questions || questions.length === 0) return null;
|
||||||
|
|
||||||
@@ -479,7 +779,7 @@
|
|||||||
<div class="space-y-3" onClick=${(e) => e.stopPropagation()}>
|
<div class="space-y-3" onClick=${(e) => e.stopPropagation()}>
|
||||||
<!-- Question Header Badge -->
|
<!-- Question Header Badge -->
|
||||||
${question.header && html`
|
${question.header && html`
|
||||||
<span class="inline-flex rounded-full border border-attention/40 bg-attention/10 px-2 py-1 font-mono text-micro uppercase tracking-[0.15em] text-attention">
|
<span class="inline-flex rounded-full border px-2 py-1 font-mono text-micro uppercase tracking-[0.15em] ${meta.badge}">
|
||||||
${question.header}
|
${question.header}
|
||||||
</span>
|
</span>
|
||||||
`}
|
`}
|
||||||
@@ -517,14 +817,17 @@
|
|||||||
handleFreeformSubmit(e);
|
handleFreeformSubmit(e);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onFocus=${() => setFocused(true)}
|
||||||
|
onBlur=${() => setFocused(false)}
|
||||||
placeholder="Type a response..."
|
placeholder="Type a response..."
|
||||||
rows="1"
|
rows="1"
|
||||||
class="flex-1 resize-none overflow-hidden rounded-xl border border-selection/75 bg-bg/70 px-3 py-2 text-sm text-fg placeholder:text-dim focus:border-attention/70 focus:outline-none"
|
class="flex-1 resize-none overflow-hidden rounded-xl border border-selection/75 bg-bg/70 px-3 py-2 text-sm text-fg placeholder:text-dim focus:outline-none"
|
||||||
style="min-height: 38px; max-height: 150px;"
|
style=${{ minHeight: '38px', maxHeight: '150px', borderColor: focused ? meta.borderColor : undefined }}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="shrink-0 rounded-xl bg-attention px-3 py-2 text-sm font-medium text-[#16110a] transition-all hover:-translate-y-0.5 hover:brightness-110"
|
class="shrink-0 rounded-xl px-3 py-2 text-sm font-medium transition-all hover:-translate-y-0.5 hover:brightness-110"
|
||||||
|
style=${{ backgroundColor: meta.borderColor, color: '#0a0f18' }}
|
||||||
>
|
>
|
||||||
Send
|
Send
|
||||||
</button>
|
</button>
|
||||||
@@ -541,8 +844,10 @@
|
|||||||
// -----------------------------
|
// -----------------------------
|
||||||
// SimpleInput Component (for cards without pending questions)
|
// SimpleInput Component (for cards without pending questions)
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
function SimpleInput({ sessionId, onRespond }) {
|
function SimpleInput({ sessionId, status, onRespond }) {
|
||||||
const [text, setText] = useState('');
|
const [text, setText] = useState('');
|
||||||
|
const [focused, setFocused] = useState(false);
|
||||||
|
const meta = getStatusMeta(status);
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -568,14 +873,17 @@
|
|||||||
handleSubmit(e);
|
handleSubmit(e);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onFocus=${() => setFocused(true)}
|
||||||
|
onBlur=${() => setFocused(false)}
|
||||||
placeholder="Send a message..."
|
placeholder="Send a message..."
|
||||||
rows="1"
|
rows="1"
|
||||||
class="flex-1 resize-none overflow-hidden rounded-xl border border-selection/75 bg-bg/70 px-3 py-2 text-sm text-fg placeholder:text-dim focus:border-active/70 focus:outline-none"
|
class="flex-1 resize-none overflow-hidden rounded-xl border border-selection/75 bg-bg/70 px-3 py-2 text-sm text-fg placeholder:text-dim focus:outline-none"
|
||||||
style="min-height: 38px; max-height: 150px;"
|
style=${{ minHeight: '38px', maxHeight: '150px', borderColor: focused ? meta.borderColor : undefined }}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="shrink-0 rounded-xl bg-active px-3 py-2 text-sm font-medium text-[#08140f] transition-all hover:-translate-y-0.5 hover:brightness-110"
|
class="shrink-0 rounded-xl px-3 py-2 text-sm font-medium transition-all hover:-translate-y-0.5 hover:brightness-110"
|
||||||
|
style=${{ backgroundColor: meta.borderColor, color: '#0a0f18' }}
|
||||||
>
|
>
|
||||||
Send
|
Send
|
||||||
</button>
|
</button>
|
||||||
@@ -590,13 +898,15 @@
|
|||||||
const hasQuestions = session.pending_questions && session.pending_questions.length > 0;
|
const hasQuestions = session.pending_questions && session.pending_questions.length > 0;
|
||||||
const scrollRef = useRef(null);
|
const scrollRef = useRef(null);
|
||||||
const statusMeta = getStatusMeta(session.status);
|
const statusMeta = getStatusMeta(session.status);
|
||||||
|
const agent = session.agent === 'codex' ? 'codex' : 'claude';
|
||||||
|
const agentHeaderClass = agent === 'codex' ? 'agent-header-codex' : 'agent-header-claude';
|
||||||
|
|
||||||
// Fetch conversation when card mounts
|
// Fetch conversation when card mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!conversation && onFetchConversation) {
|
if (!conversation && onFetchConversation) {
|
||||||
onFetchConversation(session.session_id, session.project_dir, session.agent || 'claude');
|
onFetchConversation(session.session_id, session.project_dir, agent);
|
||||||
}
|
}
|
||||||
}, [session.session_id, session.project_dir, session.agent, conversation, onFetchConversation]);
|
}, [session.session_id, session.project_dir, agent, conversation, onFetchConversation]);
|
||||||
|
|
||||||
// Scroll chat to bottom when conversation loads or updates
|
// Scroll chat to bottom when conversation loads or updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -612,12 +922,12 @@
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="glass-panel flex h-[460px] w-full max-w-full cursor-pointer flex-col overflow-hidden rounded-xl border border-selection/70 transition-all duration-200 hover:border-starting/35 hover:shadow-panel"
|
class="glass-panel flex h-[850px] max-h-[850px] w-[600px] cursor-pointer flex-col overflow-hidden rounded-xl border border-selection/70 transition-all duration-200 hover:border-starting/35 hover:shadow-panel"
|
||||||
style=${{ borderLeftWidth: '3px', borderLeftColor: statusMeta.borderColor }}
|
style=${{ borderLeftWidth: '3px', borderLeftColor: statusMeta.borderColor }}
|
||||||
onClick=${() => onClick(session)}
|
onClick=${() => onClick(session)}
|
||||||
>
|
>
|
||||||
<!-- Card Header -->
|
<!-- Card Header -->
|
||||||
<div class="shrink-0 border-b border-selection/70 px-4 py-3">
|
<div class="shrink-0 border-b px-4 py-3 ${agentHeaderClass}">
|
||||||
<div class="flex items-start justify-between gap-2.5">
|
<div class="flex items-start justify-between gap-2.5">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
@@ -628,8 +938,8 @@
|
|||||||
<span class="rounded-full border px-2.5 py-1 font-mono text-micro uppercase tracking-[0.14em] ${statusMeta.badge}">
|
<span class="rounded-full border px-2.5 py-1 font-mono text-micro uppercase tracking-[0.14em] ${statusMeta.badge}">
|
||||||
${statusMeta.label}
|
${statusMeta.label}
|
||||||
</span>
|
</span>
|
||||||
<span class="rounded-full border px-2.5 py-1 font-mono text-micro uppercase tracking-[0.14em] ${session.agent === 'codex' ? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-400' : 'border-violet-500/30 bg-violet-500/10 text-violet-400'}">
|
<span class="rounded-full border px-2.5 py-1 font-mono text-micro uppercase tracking-[0.14em] ${agent === 'codex' ? 'border-emerald-400/45 bg-emerald-500/14 text-emerald-300' : 'border-violet-400/45 bg-violet-500/14 text-violet-300'}">
|
||||||
${session.agent === 'codex' ? 'codex' : 'claude'}
|
${agent}
|
||||||
</span>
|
</span>
|
||||||
${session.cwd && html`
|
${session.cwd && html`
|
||||||
<span class="truncate rounded-full border border-selection bg-bg/40 px-2.5 py-1 font-mono text-micro text-dim">
|
<span class="truncate rounded-full border border-selection bg-bg/40 px-2.5 py-1 font-mono text-micro text-dim">
|
||||||
@@ -666,11 +976,13 @@
|
|||||||
<${QuestionBlock}
|
<${QuestionBlock}
|
||||||
questions=${session.pending_questions}
|
questions=${session.pending_questions}
|
||||||
sessionId=${session.session_id}
|
sessionId=${session.session_id}
|
||||||
|
status=${session.status}
|
||||||
onRespond=${onRespond}
|
onRespond=${onRespond}
|
||||||
/>
|
/>
|
||||||
` : html`
|
` : html`
|
||||||
<${SimpleInput}
|
<${SimpleInput}
|
||||||
sessionId=${session.session_id}
|
sessionId=${session.session_id}
|
||||||
|
status=${session.status}
|
||||||
onRespond=${onRespond}
|
onRespond=${onRespond}
|
||||||
/>
|
/>
|
||||||
`}
|
`}
|
||||||
@@ -680,22 +992,45 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
// SessionGroup Component
|
// SessionGroup Component (project-based)
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
function SessionGroup({ title, sessions, onCardClick, statusColor, conversations, onFetchConversation, onRespond, onDismiss }) {
|
function SessionGroup({ projectName, projectDir, sessions, onCardClick, conversations, onFetchConversation, onRespond, onDismiss }) {
|
||||||
if (sessions.length === 0) return null;
|
if (sessions.length === 0) return null;
|
||||||
|
|
||||||
const textColor = statusColor.replace('bg-', 'text-');
|
// Status summary for chips
|
||||||
|
const statusCounts = {};
|
||||||
|
for (const s of sessions) {
|
||||||
|
statusCounts[s.status] = (statusCounts[s.status] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group header dot uses the most urgent status
|
||||||
|
const worstStatus = sessions.reduce((worst, s) => {
|
||||||
|
return (STATUS_PRIORITY[s.status] ?? 99) < (STATUS_PRIORITY[worst] ?? 99) ? s.status : worst;
|
||||||
|
}, 'done');
|
||||||
|
const worstMeta = getStatusMeta(worstStatus);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<section class="mb-12">
|
<section class="mb-12">
|
||||||
<div class="mb-5 flex items-center gap-2 border-b border-selection/50 pb-3">
|
<div class="mb-4 flex flex-wrap items-center gap-2.5 border-b border-selection/50 pb-3">
|
||||||
<span class="h-2.5 w-2.5 rounded-full ${statusColor} ${title.toLowerCase().includes('attention') ? 'pulse-attention' : ''}"></span>
|
<span class="h-2.5 w-2.5 rounded-full ${worstMeta.dot}"></span>
|
||||||
<h2 class="font-mono text-label font-medium uppercase tracking-[0.2em] ${textColor}">${title}</h2>
|
<h2 class="font-display text-body font-semibold text-bright">${projectName}</h2>
|
||||||
<span class="rounded-full border border-selection/80 bg-bg/55 px-2 py-0.5 font-mono text-micro text-dim">${sessions.length}</span>
|
<span class="rounded-full border border-selection/80 bg-bg/55 px-2 py-0.5 font-mono text-micro text-dim">
|
||||||
|
${sessions.length} agent${sessions.length === 1 ? '' : 's'}
|
||||||
|
</span>
|
||||||
|
${Object.entries(statusCounts).map(([status, count]) => {
|
||||||
|
const meta = getStatusMeta(status);
|
||||||
|
return html`
|
||||||
|
<span key=${status} class="rounded-full border px-2 py-0.5 font-mono text-micro ${meta.badge}">
|
||||||
|
${count} ${meta.label.toLowerCase()}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
${projectDir && projectDir !== 'unknown' && html`
|
||||||
|
<div class="-mt-2 mb-3 truncate font-mono text-micro text-dim/60">${projectDir}</div>
|
||||||
|
`}
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
<div class="flex flex-wrap gap-4">
|
||||||
${sessions.map(session => html`
|
${sessions.map(session => html`
|
||||||
<${SessionCard}
|
<${SessionCard}
|
||||||
key=${session.session_id}
|
key=${session.session_id}
|
||||||
@@ -726,6 +1061,8 @@
|
|||||||
const hasPendingQuestions = session.pending_questions && session.pending_questions.length > 0;
|
const hasPendingQuestions = session.pending_questions && session.pending_questions.length > 0;
|
||||||
const optionCount = hasPendingQuestions ? session.pending_questions[0]?.options?.length || 0 : 0;
|
const optionCount = hasPendingQuestions ? session.pending_questions[0]?.options?.length || 0 : 0;
|
||||||
const status = getStatusMeta(session.status);
|
const status = getStatusMeta(session.status);
|
||||||
|
const agent = session.agent === 'codex' ? 'codex' : 'claude';
|
||||||
|
const agentHeaderClass = agent === 'codex' ? 'agent-header-codex' : 'agent-header-claude';
|
||||||
|
|
||||||
// Track if user has scrolled away from bottom
|
// Track if user has scrolled away from bottom
|
||||||
const wasAtBottomRef = useRef(true);
|
const wasAtBottomRef = useRef(true);
|
||||||
@@ -773,6 +1110,14 @@
|
|||||||
}
|
}
|
||||||
}, [session]);
|
}, [session]);
|
||||||
|
|
||||||
|
// Lock body scroll when modal is open
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Handle keyboard events
|
// Handle keyboard events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e) => {
|
const handleKeyDown = (e) => {
|
||||||
@@ -808,7 +1153,7 @@
|
|||||||
setInputValue('');
|
setInputValue('');
|
||||||
// Refresh conversation after sending
|
// Refresh conversation after sending
|
||||||
if (onRefreshConversation) {
|
if (onRefreshConversation) {
|
||||||
await onRefreshConversation(session.session_id, session.project_dir, session.agent || 'claude');
|
await onRefreshConversation(session.session_id, session.project_dir, agent);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to send message:', err);
|
console.error('Failed to send message:', err);
|
||||||
@@ -828,7 +1173,7 @@
|
|||||||
onClick=${(e) => e.stopPropagation()}
|
onClick=${(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<!-- Modal Header -->
|
<!-- Modal Header -->
|
||||||
<div class="flex items-center justify-between border-b border-selection/70 px-5 py-4">
|
<div class="flex items-center justify-between border-b px-5 py-4 ${agentHeaderClass}">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="mb-1 flex items-center gap-3">
|
<div class="mb-1 flex items-center gap-3">
|
||||||
<h2 class="truncate font-display text-xl font-semibold text-bright">${session.project || session.name || session.session_id}</h2>
|
<h2 class="truncate font-display text-xl font-semibold text-bright">${session.project || session.name || session.session_id}</h2>
|
||||||
@@ -836,6 +1181,9 @@
|
|||||||
<span class="h-2 w-2 rounded-full ${status.dot}"></span>
|
<span class="h-2 w-2 rounded-full ${status.dot}"></span>
|
||||||
<span class="font-mono uppercase tracking-[0.14em]">${status.label}</span>
|
<span class="font-mono uppercase tracking-[0.14em]">${status.label}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="rounded-full border px-2.5 py-1 font-mono text-micro uppercase tracking-[0.14em] ${agent === 'codex' ? 'border-emerald-400/45 bg-emerald-500/14 text-emerald-300' : 'border-violet-400/45 bg-violet-500/14 text-violet-300'}">
|
||||||
|
${agent}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4 text-sm text-dim">
|
<div class="flex items-center gap-4 text-sm text-dim">
|
||||||
<span class="truncate font-mono">${session.cwd || 'No working directory'}</span>
|
<span class="truncate font-mono">${session.cwd || 'No working directory'}</span>
|
||||||
@@ -855,7 +1203,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal Content -->
|
<!-- Modal Content -->
|
||||||
<div ref=${chatContainerRef} class="flex-1 overflow-y-auto overflow-x-hidden bg-[linear-gradient(180deg,rgba(9,18,34,0.82),rgba(5,10,20,0.9))] p-5">
|
<div ref=${chatContainerRef} class="flex-1 overflow-y-auto overflow-x-hidden bg-surface p-5">
|
||||||
${conversationLoading ? html`
|
${conversationLoading ? html`
|
||||||
<div class="flex items-center justify-center py-12">
|
<div class="flex items-center justify-center py-12">
|
||||||
<div class="font-mono text-dim">Loading conversation...</div>
|
<div class="font-mono text-dim">Loading conversation...</div>
|
||||||
@@ -1101,10 +1449,7 @@
|
|||||||
}, [fetchState]);
|
}, [fetchState]);
|
||||||
|
|
||||||
// Group sessions by status
|
// Group sessions by status
|
||||||
const attentionSessions = sessions.filter(s => s.status === 'needs_attention');
|
const projectGroups = groupSessionsByProject(sessions);
|
||||||
const activeSessions = sessions.filter(s => s.status === 'active');
|
|
||||||
const startingSessions = sessions.filter(s => s.status === 'starting');
|
|
||||||
const doneSessions = sessions.filter(s => s.status === 'done');
|
|
||||||
|
|
||||||
// Handle card click - open modal and fetch conversation if not cached
|
// Handle card click - open modal and fetch conversation if not cached
|
||||||
const handleCardClick = useCallback(async (session) => {
|
const handleCardClick = useCallback(async (session) => {
|
||||||
@@ -1150,46 +1495,19 @@
|
|||||||
` : sessions.length === 0 ? html`
|
` : sessions.length === 0 ? html`
|
||||||
<${EmptyState} />
|
<${EmptyState} />
|
||||||
` : html`
|
` : html`
|
||||||
|
${projectGroups.map(group => html`
|
||||||
<${SessionGroup}
|
<${SessionGroup}
|
||||||
title="Needs Attention"
|
key=${group.projectDir}
|
||||||
sessions=${attentionSessions}
|
projectName=${group.projectName}
|
||||||
|
projectDir=${group.projectDir}
|
||||||
|
sessions=${group.sessions}
|
||||||
onCardClick=${handleCardClick}
|
onCardClick=${handleCardClick}
|
||||||
statusColor="bg-attention"
|
|
||||||
conversations=${conversations}
|
|
||||||
onFetchConversation=${fetchConversation}
|
|
||||||
onRespond=${respondToSession}
|
|
||||||
onDismiss=${dismissSession}
|
|
||||||
/>
|
|
||||||
<${SessionGroup}
|
|
||||||
title="Active"
|
|
||||||
sessions=${activeSessions}
|
|
||||||
onCardClick=${handleCardClick}
|
|
||||||
statusColor="bg-active"
|
|
||||||
conversations=${conversations}
|
|
||||||
onFetchConversation=${fetchConversation}
|
|
||||||
onRespond=${respondToSession}
|
|
||||||
onDismiss=${dismissSession}
|
|
||||||
/>
|
|
||||||
<${SessionGroup}
|
|
||||||
title="Starting"
|
|
||||||
sessions=${startingSessions}
|
|
||||||
onCardClick=${handleCardClick}
|
|
||||||
statusColor="bg-starting"
|
|
||||||
conversations=${conversations}
|
|
||||||
onFetchConversation=${fetchConversation}
|
|
||||||
onRespond=${respondToSession}
|
|
||||||
onDismiss=${dismissSession}
|
|
||||||
/>
|
|
||||||
<${SessionGroup}
|
|
||||||
title="Done"
|
|
||||||
sessions=${doneSessions}
|
|
||||||
onCardClick=${handleCardClick}
|
|
||||||
statusColor="bg-done"
|
|
||||||
conversations=${conversations}
|
conversations=${conversations}
|
||||||
onFetchConversation=${fetchConversation}
|
onFetchConversation=${fetchConversation}
|
||||||
onRespond=${respondToSession}
|
onRespond=${respondToSession}
|
||||||
onDismiss=${dismissSession}
|
onDismiss=${dismissSession}
|
||||||
/>
|
/>
|
||||||
|
`)}
|
||||||
`}
|
`}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user