diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index 1f83359..11c309e 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -31,13 +31,17 @@ function formatTimestamp(ts) { /** * Return the priority label for a session object. * @param {object} session - * @returns {'bell'|'idle'} + * @returns {'bell'|'active'|'idle'} */ function sessionPriority(session) { const bell = session && session.bell; if (bell && bell.unseen_count > 0 && (bell.seen_at === null || bell.last_fired_at > bell.seen_at)) { return 'bell'; } + const activity = session && session.last_activity_at; + if (activity && (Date.now() / 1000 - activity) < 300) { + return 'active'; + } return 'idle'; } @@ -323,6 +327,17 @@ async function restoreState() { skipAnimation: true, remoteId: state.active_remote_id || '', }); + } else if (_serverSettings && _serverSettings.default_session) { + // Fallback to default_session setting if no active session + var defaultName = _serverSettings.default_session; + // Verify the session actually exists before opening + try { + var sessRes = await api('GET', '/api/sessions'); + var sessList = await sessRes.json(); + if (sessList.some(function(s) { return s.name === defaultName; })) { + await openSession(defaultName, { skipAnimation: true }); + } + } catch (e) { /* ignore */ } } } catch (err) { console.warn('[restoreState] could not restore previous session:', err); @@ -1528,7 +1543,23 @@ function renderGrid(sessions) { } } + // Search/filter bar integration + var filterBar = $('filter-bar'); + var filterQuery = filterBar && filterBar.query ? filterBar.query : ''; + var visible = getVisibleSessions(sessions); + var totalVisible = visible.length; + + if (filterQuery) { + var q = filterQuery.toLowerCase(); + visible = visible.filter(function(s) { return (s.name || '').toLowerCase().includes(q); }); + } + + if (filterBar && filterBar.tagName === 'SEARCH-FILTER') { + filterBar.total = totalVisible; + filterBar.count = visible.length; + } + var emptyState = $('empty-state'); if (visible.length === 0) { @@ -3934,6 +3965,14 @@ function bindStaticEventListeners() { }); } + // Search/filter bar — re-render grid when filter text changes + var filterBar = $('filter-bar'); + if (filterBar) { + filterBar.addEventListener('filter-change', function() { + renderGrid(_currentSessions || []); + }); + } + // Hover preview — component handles click internally; listen for preview-click var hoverPreview = $('hover-preview'); if (hoverPreview) { diff --git a/muxplex/frontend/components/search-filter.js b/muxplex/frontend/components/search-filter.js new file mode 100644 index 0000000..f47bd12 --- /dev/null +++ b/muxplex/frontend/components/search-filter.js @@ -0,0 +1,106 @@ +import { LitElement, html, css } from '/vendor/lit/lit.min.js'; + +export class SearchFilter extends LitElement { + static properties = { + query: { type: String, reflect: true }, + placeholder: { type: String }, + count: { type: Number }, + total: { type: Number }, + }; + + static styles = css` + :host { display: block; } + .filter-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 0 var(--grid-padding, 16px); + height: 40px; + } + .filter-input { + flex: 1; + max-width: 320px; + padding: 6px 12px; + font-size: 13px; + background: var(--bg-tile, #10131C); + color: var(--text, #F0F6FF); + border: 1px solid var(--border, #2A3040); + border-radius: 6px; + outline: none; + transition: border-color 150ms; + } + .filter-input:focus { + border-color: var(--accent, #00D9F5); + } + .filter-input::placeholder { + color: var(--text-muted, #8E95A3); + } + .filter-count { + font-size: 12px; + color: var(--text-muted, #8E95A3); + } + .filter-clear { + background: none; + border: none; + color: var(--text-muted, #8E95A3); + cursor: pointer; + padding: 4px; + font-size: 12px; + } + .filter-clear:hover { color: var(--text, #F0F6FF); } + `; + + constructor() { + super(); + this.query = ''; + this.placeholder = 'Filter sessions...'; + this.count = 0; + this.total = 0; + } + + _onInput(e) { + this.query = e.target.value; + this.dispatchEvent(new CustomEvent('filter-change', { + bubbles: true, composed: true, + detail: { query: this.query }, + })); + } + + _onClear() { + this.query = ''; + this.dispatchEvent(new CustomEvent('filter-change', { + bubbles: true, composed: true, + detail: { query: '' }, + })); + } + + _onKeydown(e) { + if (e.key === 'Escape') { + this._onClear(); + e.target.blur(); + } + } + + render() { + const showCount = this.query && this.query.length > 0; + return html` +
+ `; + } +} + +customElements.define('search-filter', SearchFilter); \ No newline at end of file diff --git a/muxplex/frontend/components/session-tile.js b/muxplex/frontend/components/session-tile.js index 5f7c873..69f0eda 100644 --- a/muxplex/frontend/components/session-tile.js +++ b/muxplex/frontend/components/session-tile.js @@ -64,15 +64,19 @@ export class SessionTile extends LitElement { get _classes() { let cls = 'session-tile'; const ind = this.activityIndicator; + const priority = this._priority; if (this._isBell && (ind === 'glow' || ind === 'both')) cls += ' session-tile--bell'; if (this._isBell && (ind === 'dot' || ind === 'both')) cls += ' session-tile--edge-bell'; - if (this.mobile) cls += ` session-tile--tier-${this._priority}`; + if (priority === 'active') cls += ' session-tile--active'; + const activity = this.session && this.session.last_activity_at; + if (!activity || (Date.now() / 1000 - activity) > 3600) cls += ' session-tile--stale'; + if (this.mobile) cls += ` session-tile--tier-${priority}`; return cls; } get _snapshotHtml() { const snapshot = this.session.snapshot || ''; - const lineCount = this.viewMode === 'fit' ? -80 : -20; + const lineCount = this.viewMode === 'fit' ? -80 : -12; // Trim trailing blank lines first (see buildTileHTML comment in app.js) const allLines = snapshot.split('\n'); while (allLines.length > 0 && allLines[allLines.length - 1].trim() === '') { diff --git a/muxplex/frontend/components/sidebar-item.js b/muxplex/frontend/components/sidebar-item.js index 8b876ee..3b503ce 100644 --- a/muxplex/frontend/components/sidebar-item.js +++ b/muxplex/frontend/components/sidebar-item.js @@ -1,6 +1,6 @@ import { LitElement, html, nothing } from '/vendor/lit/lit.min.js'; import { unsafeHTML } from '/vendor/lit/lit.min.js'; -import { escapeHtml, ansiToHtml } from './utils.js'; +import { escapeHtml, sessionPriority, ansiToHtml } from './utils.js'; import { icons } from './icons.js'; /** @@ -47,12 +47,20 @@ export class SidebarItem extends LitElement { return unseen && unseen > 0; } + get _priority() { + return sessionPriority(this.session); + } + get _classes() { let cls = 'sidebar-item'; if (this.active) cls += ' sidebar-item--active'; const ind = this.activityIndicator; + const priority = this._priority; if (this._isBell && (ind === 'glow' || ind === 'both')) cls += ' sidebar-item--bell'; if (this._isBell && (ind === 'dot' || ind === 'both')) cls += ' sidebar-item--edge-bell'; + if (priority === 'active') cls += ' sidebar-item--activity'; + const activity = this.session && this.session.last_activity_at; + if (!activity || (Date.now() / 1000 - activity) > 3600) cls += ' sidebar-item--stale'; return cls; } @@ -62,7 +70,7 @@ export class SidebarItem extends LitElement { while (allLines.length > 0 && allLines[allLines.length - 1].trim() === '') { allLines.pop(); } - return ansiToHtml(allLines.slice(-20).join('\n')); + return ansiToHtml(allLines.slice(-12).join('\n')); } _onClick(e) { diff --git a/muxplex/frontend/components/utils.js b/muxplex/frontend/components/utils.js index 0a50376..9333135 100644 --- a/muxplex/frontend/components/utils.js +++ b/muxplex/frontend/components/utils.js @@ -29,13 +29,17 @@ export function formatTimestamp(ts) { /** * Return the priority label for a session object. * @param {object} session - * @returns {'bell'|'idle'} + * @returns {'bell'|'active'|'idle'} */ export function sessionPriority(session) { const bell = session && session.bell; if (bell && bell.unseen_count > 0 && (bell.seen_at === null || bell.last_fired_at > bell.seen_at)) { return 'bell'; } + const activity = session && session.last_activity_at; + if (activity && (Date.now() / 1000 - activity) < 300) { + return 'active'; + } return 'idle'; } diff --git a/muxplex/frontend/index.html b/muxplex/frontend/index.html index 8c43dbc..cd4e396 100644 --- a/muxplex/frontend/index.html +++ b/muxplex/frontend/index.html @@ -27,7 +27,7 @@