@qodalis/angular-cli
Version:
Angular wrapper for the @qodalis CLI terminal engine.
1,532 lines • 148 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, EventEmitter, ViewChild, Output, Input, Optional, Inject, ViewEncapsulation, Component, HostListener, ViewChildren, NgModule } from '@angular/core';
import * as i1 from '@angular/common';
import { CommonModule } from '@angular/common';
import { CliEngine } from '@qodalis/cli';
export { CliCommandExecutor, CliCommandHistory, CliEngine, CliPackageManagerService, CommandParser, ScriptLoaderService, getGreetingBasedOnTime, openLink } from '@qodalis/cli';
import { loadPanelPosition, savePanelPosition, derivePanelThemeStyles } from '@qodalis/cli-core';
import { BehaviorSubject, Subject, takeUntil, merge, interval, startWith, map, distinctUntilChanged } from 'rxjs';
const CliCommandProcessor_TOKEN = new InjectionToken('cli-processors');
const ICliUserSessionService_TOKEN = new InjectionToken('cli-user-session-service');
const ICliUsersStoreService_TOKEN = new InjectionToken('cli-users-store-service');
const ICliPingServerService_TOKEN = new InjectionToken('cli-ping-server-service');
const CliModule_TOKEN = new InjectionToken('cli-modules');
class CliComponent {
constructor(diProcessors, diModules, pingServerService) {
this.diProcessors = diProcessors;
this.diModules = diModules;
this.pingServerService = pingServerService;
this.engineReady = new EventEmitter();
}
ngAfterViewInit() {
const engineOptions = {
...(this.options ?? {}),
...(this.snapshot ? { snapshot: this.snapshot } : {}),
};
this.engine = new CliEngine(this.terminalDiv.nativeElement, engineOptions);
// Identify the serving framework
this.engine.registerService('cli-framework', 'Angular');
// Bridge Angular DI services into the engine's service container
if (this.pingServerService) {
this.engine.registerService('cli-ping-server-service', this.pingServerService);
}
// Register processors provided via Angular DI (from resolveCommandProcessorProvider).
// Exclude processors that already belong to a module — those will be
// initialized as part of the module boot (which registers services first).
if (this.diProcessors && this.diProcessors.length > 0) {
const moduleProcessors = new Set((this.diModules ?? []).flatMap(m => m.processors ?? []));
const standalone = this.diProcessors.filter(p => !moduleProcessors.has(p));
if (standalone.length > 0) {
this.engine.registerProcessors(standalone);
}
}
// Register processors provided via @Input
if (this.processors && this.processors.length > 0) {
this.engine.registerProcessors(this.processors);
}
// Register modules provided via Angular DI
if (this.diModules && this.diModules.length > 0) {
this.engine.registerModules(this.diModules);
}
// Register modules provided via @Input
if (this.modules && this.modules.length > 0) {
this.engine.registerModules(this.modules);
}
this.engine.start().then(() => {
this.engineReady.emit(this.engine);
});
}
ngOnDestroy() {
this.engine?.destroy();
}
focus() {
this.engine?.focus();
}
getEngine() {
return this.engine;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CliComponent, deps: [{ token: CliCommandProcessor_TOKEN, optional: true }, { token: CliModule_TOKEN, optional: true }, { token: ICliPingServerService_TOKEN, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: CliComponent, isStandalone: false, selector: "cli", inputs: { options: "options", processors: "processors", modules: "modules", height: "height", snapshot: "snapshot" }, outputs: { engineReady: "engineReady" }, viewQueries: [{ propertyName: "terminalDiv", first: true, predicate: ["terminal"], descendants: true, static: true }], ngImport: i0, template: `<div
#terminal
[style.height]="height || '100%'"
style="width: 100%;"
></div>`, isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CliComponent, decorators: [{
type: Component,
args: [{ standalone: false, selector: 'cli', template: `<div
#terminal
[style.height]="height || '100%'"
style="width: 100%;"
></div>`, encapsulation: ViewEncapsulation.None }]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Optional
}, {
type: Inject,
args: [CliCommandProcessor_TOKEN]
}] }, { type: undefined, decorators: [{
type: Optional
}, {
type: Inject,
args: [CliModule_TOKEN]
}] }, { type: undefined, decorators: [{
type: Optional
}, {
type: Inject,
args: [ICliPingServerService_TOKEN]
}] }], propDecorators: { options: [{
type: Input
}], processors: [{
type: Input
}], modules: [{
type: Input
}], height: [{
type: Input
}], snapshot: [{
type: Input
}], engineReady: [{
type: Output
}], terminalDiv: [{
type: ViewChild,
args: ['terminal', { static: true }]
}] } });
const HEADER_HEIGHT = 60;
class CollapsableContentComponent {
constructor() {
this.previousPanelHeight = 600;
this.panelHeight = 600;
this.panelWidth = 400;
this.previousPanelWidth = 400;
this.isResizing = false;
this.startY = 0;
this.startX = 0;
this.startHeight = 0;
this.startWidth = 0;
this.visible = true;
this.isCollapsed = true;
this.isMaximized = false;
this.position = 'bottom';
this.closable = true;
this.resizable = true;
this.hideable = true;
this.hideAlignment = 'center';
this.themeStyles = {};
this.onToggle = new EventEmitter();
this.onContentSizeChange = new EventEmitter();
this.onClose = new EventEmitter();
this.onHide = new EventEmitter();
this.onPositionChange = new EventEmitter();
this.isHidden = false;
// Status bar inputs
this.statusExecutionState = 'idle';
this.statusLastCommand = null;
this.statusServiceCount = { running: 0, total: 0 };
this.statusServiceDetails = [];
this.statusServerState = 'none';
this.statusServerDetails = [];
this.statusUptime = 0;
this.notification = null;
this.positionDropdownOpen = false;
this.dropdownStyle = {};
this.preHideCollapsed = true;
this.servicesDropdownOpen = false;
this.servicesDropdownStyle = {};
this.serversDropdownOpen = false;
this.serversDropdownStyle = {};
}
get isHorizontal() {
return this.position === 'left' || this.position === 'right';
}
get showStatusIndicators() {
return this.position === 'bottom' || this.position === 'top';
}
get connectedServerCount() {
return this.statusServerDetails.filter(s => s.connected).length;
}
get totalServerCount() {
return this.statusServerDetails.length;
}
toggleServicesDropdown(event) {
event.stopPropagation();
this.servicesDropdownOpen = !this.servicesDropdownOpen;
if (this.servicesDropdownOpen) {
const el = event.currentTarget;
const rect = el.getBoundingClientRect();
switch (this.position) {
case 'bottom':
this.servicesDropdownStyle = {
bottom: (window.innerHeight - rect.top + 4) + 'px',
left: rect.left + 'px',
};
break;
case 'top':
this.servicesDropdownStyle = {
top: (rect.bottom + 4) + 'px',
left: rect.left + 'px',
};
break;
case 'left':
this.servicesDropdownStyle = {
top: rect.top + 'px',
left: (rect.right + 4) + 'px',
};
break;
case 'right':
this.servicesDropdownStyle = {
top: rect.top + 'px',
right: (window.innerWidth - rect.left + 4) + 'px',
};
break;
default:
this.servicesDropdownStyle = {
bottom: (window.innerHeight - rect.top + 4) + 'px',
left: rect.left + 'px',
};
}
}
}
toggleServersDropdown(event) {
event.stopPropagation();
this.serversDropdownOpen = !this.serversDropdownOpen;
if (this.serversDropdownOpen) {
const el = event.currentTarget;
const rect = el.getBoundingClientRect();
switch (this.position) {
case 'bottom':
this.serversDropdownStyle = {
bottom: (window.innerHeight - rect.top + 4) + 'px',
left: rect.left + 'px',
};
break;
case 'top':
this.serversDropdownStyle = {
top: (rect.bottom + 4) + 'px',
left: rect.left + 'px',
};
break;
case 'left':
this.serversDropdownStyle = {
top: rect.top + 'px',
left: (rect.right + 4) + 'px',
};
break;
case 'right':
this.serversDropdownStyle = {
top: rect.top + 'px',
right: (window.innerWidth - rect.left + 4) + 'px',
};
break;
default:
this.serversDropdownStyle = {
bottom: (window.innerHeight - rect.top + 4) + 'px',
left: rect.left + 'px',
};
}
}
}
get formattedUptime() {
const mins = Math.floor(this.statusUptime / 60000);
if (mins < 60)
return `${mins}m`;
const hrs = Math.floor(mins / 60);
return `${hrs}h${mins % 60}m`;
}
get wrapperStyle() {
const size = this.isHorizontal
? { width: this.panelWidth + 'px' }
: { height: this.panelHeight + 'px' };
return { ...size, ...this.themeStyles };
}
toggleTerminal() {
this.isCollapsed = !this.isCollapsed;
this.onToggle.emit(this.isCollapsed);
}
closeTerminal() {
this.visible = false;
this.onClose.emit();
}
hideTerminal() {
this.preHideCollapsed = this.isCollapsed;
this.isHidden = true;
this.onHide.emit();
}
unhideTerminal() {
this.isHidden = false;
this.isCollapsed = this.preHideCollapsed;
this.onToggle.emit(this.isCollapsed);
}
togglePositionDropdown(event) {
event.stopPropagation();
this.positionDropdownOpen = !this.positionDropdownOpen;
if (this.positionDropdownOpen) {
const btn = event.currentTarget;
const rect = btn.getBoundingClientRect();
switch (this.position) {
case 'bottom':
this.dropdownStyle = {
bottom: (window.innerHeight - rect.top + 4) + 'px',
right: (window.innerWidth - rect.right) + 'px',
};
break;
case 'top':
this.dropdownStyle = {
top: (rect.bottom + 4) + 'px',
right: (window.innerWidth - rect.right) + 'px',
};
break;
case 'left':
this.dropdownStyle = {
top: rect.top + 'px',
left: (rect.right + 4) + 'px',
};
break;
case 'right':
this.dropdownStyle = {
top: rect.top + 'px',
right: (window.innerWidth - rect.left + 4) + 'px',
};
break;
}
}
}
selectPosition(pos) {
this.positionDropdownOpen = false;
this.onPositionChange.emit(pos);
}
closeDropdowns() {
this.positionDropdownOpen = false;
this.servicesDropdownOpen = false;
this.serversDropdownOpen = false;
}
toggleMaximizationTerminal() {
if (this.isHorizontal) {
if (!this.isMaximized) {
this.previousPanelWidth = this.panelWidth;
this.panelWidth = window.innerWidth;
}
else {
this.panelWidth = this.previousPanelWidth;
}
}
else {
if (!this.isMaximized) {
this.previousPanelHeight = this.panelHeight;
this.panelHeight = window.innerHeight;
}
else {
this.panelHeight = this.previousPanelHeight;
}
}
this.isMaximized = !this.isMaximized;
this.updateTerminalSize();
}
onResizeStart(event) {
if (!this.resizable)
return;
this.isResizing = true;
if (this.isCollapsed) {
this.toggleTerminal();
}
if (this.isHorizontal) {
this.startX = event.clientX;
this.startWidth = this.panelWidth;
}
else {
this.startY = event.clientY;
this.startHeight = this.panelHeight;
}
event.preventDefault();
}
onMouseMove(event) {
if (!this.isResizing)
return;
if (this.isHorizontal) {
const deltaX = this.position === 'left'
? event.clientX - this.startX
: this.startX - event.clientX;
let nextWidth = Math.max(100, this.startWidth + deltaX);
if (nextWidth > window.innerWidth) {
nextWidth = window.innerWidth;
}
this.panelWidth = nextWidth;
}
else {
const deltaY = this.position === 'top'
? event.clientY - this.startY
: this.startY - event.clientY;
let nextHeight = Math.max(100, this.startHeight + deltaY);
if (nextHeight > window.innerHeight) {
nextHeight = window.innerHeight;
}
this.panelHeight = nextHeight;
}
this.updateTerminalSize();
}
onMouseUp() {
this.isResizing = false;
}
/** Set collapsed state directly (called by parent CliPanelComponent). */
setCollapsed(value) {
this.isCollapsed = value;
this.onToggle.emit(value);
}
/** Set maximized state directly (called by parent CliPanelComponent). */
setMaximized(value) {
if (value && !this.isMaximized) {
if (this.isHorizontal) {
this.previousPanelWidth = this.panelWidth;
this.panelWidth = window.innerWidth;
}
else {
this.previousPanelHeight = this.panelHeight;
this.panelHeight = window.innerHeight;
}
}
else if (!value && this.isMaximized) {
if (this.isHorizontal) {
this.panelWidth = this.previousPanelWidth;
}
else {
this.panelHeight = this.previousPanelHeight;
}
}
this.isMaximized = value;
this.updateTerminalSize();
}
/** Programmatically set panel dimensions. */
setDimensions(dims) {
if (dims.height !== undefined) {
this.panelHeight = dims.height;
this.previousPanelHeight = dims.height;
}
if (dims.width !== undefined) {
this.panelWidth = dims.width;
this.previousPanelWidth = dims.width;
}
this.updateTerminalSize();
}
updateTerminalSize() {
if (this.isHorizontal) {
this.onContentSizeChange.emit(this.panelWidth - HEADER_HEIGHT);
}
else {
this.onContentSizeChange.emit(this.panelHeight - HEADER_HEIGHT);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CollapsableContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: CollapsableContentComponent, isStandalone: false, selector: "collapsable-content", inputs: { visible: "visible", isCollapsed: "isCollapsed", isMaximized: "isMaximized", position: "position", closable: "closable", resizable: "resizable", hideable: "hideable", hideAlignment: "hideAlignment", themeStyles: "themeStyles", isHidden: "isHidden", statusExecutionState: "statusExecutionState", statusLastCommand: "statusLastCommand", statusServiceCount: "statusServiceCount", statusServiceDetails: "statusServiceDetails", statusServerState: "statusServerState", statusServerDetails: "statusServerDetails", statusUptime: "statusUptime", notification: "notification" }, outputs: { onToggle: "onToggle", onContentSizeChange: "onContentSizeChange", onClose: "onClose", onHide: "onHide", onPositionChange: "onPositionChange" }, host: { listeners: { "document:click": "closeDropdowns()", "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp()" } }, ngImport: i0, template: "<!-- Hide tab (shown when panel is hidden) -->\n<button\n *ngIf=\"visible && isHidden\"\n class=\"cli-panel-hide-tab\"\n [attr.data-position]=\"position\"\n [attr.data-hide-align]=\"hideAlignment\"\n [ngStyle]=\"themeStyles\"\n title=\"Show CLI\"\n (click)=\"unhideTerminal()\"\n>\n <svg\n class=\"cli-panel-hide-tab-icon\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <polyline points=\"4 17 10 11 4 5\" />\n <line x1=\"12\" y1=\"19\" x2=\"20\" y2=\"19\" />\n </svg>\n <span class=\"cli-panel-hide-tab-label\">CLI</span>\n <svg\n class=\"cli-panel-hide-tab-arrow\"\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <!-- bottom: chevron up -->\n <polyline *ngIf=\"position === 'bottom'\" points=\"18 15 12 9 6 15\" />\n <!-- top: chevron down -->\n <polyline *ngIf=\"position === 'top'\" points=\"6 9 12 15 18 9\" />\n <!-- left: chevron right -->\n <polyline *ngIf=\"position === 'left'\" points=\"9 18 15 12 9 6\" />\n <!-- right: chevron left -->\n <polyline *ngIf=\"position === 'right'\" points=\"15 6 9 12 15 18\" />\n </svg>\n</button>\n\n<!-- Main panel (kept in DOM when hidden to preserve terminal state) -->\n<div\n *ngIf=\"visible\"\n class=\"terminal-wrapper\"\n [style.display]=\"isHidden ? 'none' : ''\"\n [class.collapsed]=\"isCollapsed\"\n [class.maximized]=\"isMaximized\"\n [class.resizing]=\"isResizing\"\n [attr.data-position]=\"position\"\n [attr.data-resizable]=\"resizable\"\n [attr.data-closable]=\"closable\"\n [attr.data-hideable]=\"hideable\"\n [ngStyle]=\"wrapperStyle\"\n>\n <div class=\"terminal-header\">\n <div class=\"resize-bar\" (mousedown)=\"onResizeStart($event)\">\n <div class=\"resize-grip\"></div>\n </div>\n <div class=\"header-content\">\n <p class=\"terminal-title\">\n <svg\n class=\"title-icon\"\n width=\"22\"\n height=\"22\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <polyline points=\"4 17 10 11 4 5\" />\n <line x1=\"12\" y1=\"19\" x2=\"20\" y2=\"19\" />\n </svg>\n CLI\n </p>\n <!-- Status indicators (bottom/top positions only) -->\n <div class=\"status-indicators\" *ngIf=\"showStatusIndicators\">\n <!-- Execution state -->\n <span class=\"status-item\" [class.status-running]=\"statusExecutionState === 'running'\">\n <span class=\"status-dot\" [class.dot-idle]=\"statusExecutionState === 'idle'\" [class.dot-running]=\"statusExecutionState === 'running'\"></span>\n <span class=\"status-label\">{{ statusExecutionState }}</span>\n </span>\n\n <!-- Background services (only if any exist) -->\n <span class=\"status-item status-clickable\" *ngIf=\"statusServiceCount.total > 0\" (click)=\"toggleServicesDropdown($event)\">\n <span class=\"status-icon\">⚙</span>\n <span class=\"status-label\">{{ statusServiceCount.running }}/{{ statusServiceCount.total }} services</span>\n </span>\n\n <!-- Last command -->\n <span class=\"status-item\" *ngIf=\"statusLastCommand\">\n <span class=\"status-icon\" [class.status-success]=\"statusLastCommand.success\" [class.status-error]=\"!statusLastCommand.success\">\n {{ statusLastCommand.success ? '✓' : '✕' }}\n </span>\n <span class=\"status-label\">{{ statusLastCommand.name }}</span>\n </span>\n\n <!-- Server connection -->\n <span class=\"status-item status-clickable\" *ngIf=\"statusServerState !== 'none'\" (click)=\"toggleServersDropdown($event)\">\n <span class=\"status-dot\" [class.dot-idle]=\"statusServerState === 'connected'\" [class.dot-error]=\"statusServerState === 'disconnected'\"></span>\n <span class=\"status-label\">{{ connectedServerCount }}/{{ totalServerCount }} servers</span>\n </span>\n\n <!-- Uptime -->\n <span class=\"status-item status-muted\" *ngIf=\"statusUptime > 0\">\n <span class=\"status-icon\">↑</span>\n <span class=\"status-label\">{{ formattedUptime }}</span>\n </span>\n\n <!-- Custom processor notification -->\n <span class=\"status-item status-text\" [ngClass]=\"'level-' + notification?.level\" *ngIf=\"notification\">\n <span class=\"status-label\">{{ notification.message }}</span>\n </span>\n </div>\n\n <!-- Compact status indicators (left/right positions \u2014 CSS controls visibility) -->\n <div class=\"status-indicators-compact\">\n <!-- Execution state -->\n <span class=\"compact-item\" [title]=\"statusExecutionState\">\n <span class=\"status-dot\" [class.dot-idle]=\"statusExecutionState === 'idle'\" [class.dot-running]=\"statusExecutionState === 'running'\"></span>\n </span>\n\n <!-- Background services -->\n <span class=\"compact-item status-clickable\" *ngIf=\"statusServiceCount.total > 0\" [title]=\"statusServiceCount.running + '/' + statusServiceCount.total + ' services'\" (click)=\"toggleServicesDropdown($event)\">\n <span class=\"status-icon\">⚙</span>\n </span>\n\n <!-- Last command -->\n <span class=\"compact-item\" *ngIf=\"statusLastCommand\" [title]=\"(statusLastCommand.success ? '\u2713 ' : '\u2717 ') + statusLastCommand.name\">\n <span class=\"status-icon\" [class.status-success]=\"statusLastCommand.success\" [class.status-error]=\"!statusLastCommand.success\">\n {{ statusLastCommand.success ? '✓' : '✕' }}\n </span>\n </span>\n\n <!-- Server connection -->\n <span class=\"compact-item status-clickable\" *ngIf=\"statusServerState !== 'none'\" [title]=\"connectedServerCount + '/' + totalServerCount + ' servers'\" (click)=\"toggleServersDropdown($event)\">\n <span class=\"status-dot\" [class.dot-idle]=\"statusServerState === 'connected'\" [class.dot-error]=\"statusServerState === 'disconnected'\"></span>\n </span>\n\n <!-- Uptime -->\n <span class=\"compact-item status-muted\" *ngIf=\"statusUptime > 0\" [title]=\"formattedUptime\">\n <span class=\"status-icon\">↑</span>\n </span>\n\n <!-- Notification -->\n <span class=\"compact-item\" *ngIf=\"notification\" [title]=\"notification.message\">\n <span class=\"compact-dot\" [ngClass]=\"'level-' + notification.level\"></span>\n </span>\n </div>\n\n <div class=\"action-buttons\">\n <!-- Position dropdown -->\n <div class=\"panel-btn-position-wrapper\" (click)=\"$event.stopPropagation()\">\n <button\n class=\"panel-btn panel-btn-position\"\n title=\"Move panel\"\n (click)=\"togglePositionDropdown($event)\"\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" />\n <!-- bottom -->\n <rect *ngIf=\"position === 'bottom'\" x=\"4\" y=\"15\" width=\"16\" height=\"5\" rx=\"1\" fill=\"currentColor\" stroke=\"none\" opacity=\"0.5\" />\n <!-- top -->\n <rect *ngIf=\"position === 'top'\" x=\"4\" y=\"4\" width=\"16\" height=\"5\" rx=\"1\" fill=\"currentColor\" stroke=\"none\" opacity=\"0.5\" />\n <!-- left -->\n <rect *ngIf=\"position === 'left'\" x=\"4\" y=\"4\" width=\"5\" height=\"16\" rx=\"1\" fill=\"currentColor\" stroke=\"none\" opacity=\"0.5\" />\n <!-- right -->\n <rect *ngIf=\"position === 'right'\" x=\"15\" y=\"4\" width=\"5\" height=\"16\" rx=\"1\" fill=\"currentColor\" stroke=\"none\" opacity=\"0.5\" />\n </svg>\n </button>\n </div>\n\n <!-- Hide button -->\n <button\n *ngIf=\"hideable\"\n class=\"panel-btn panel-btn-hide\"\n title=\"Hide\"\n (click)=\"hideTerminal()\"\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <line x1=\"5\" y1=\"18\" x2=\"19\" y2=\"18\" />\n <polyline points=\"9 14 12 17 15 14\" />\n </svg>\n </button>\n\n <button\n class=\"panel-btn\"\n [title]=\"!isMaximized ? 'Maximize' : 'Restore'\"\n [disabled]=\"isCollapsed\"\n (click)=\"toggleMaximizationTerminal()\"\n >\n <svg\n *ngIf=\"!isMaximized\"\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <polyline points=\"15 3 21 3 21 9\" />\n <polyline points=\"9 21 3 21 3 15\" />\n <line x1=\"21\" y1=\"3\" x2=\"14\" y2=\"10\" />\n <line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\" />\n </svg>\n <svg\n *ngIf=\"isMaximized\"\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <polyline points=\"4 14 10 14 10 20\" />\n <polyline points=\"20 10 14 10 14 4\" />\n <line x1=\"14\" y1=\"10\" x2=\"21\" y2=\"3\" />\n <line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\" />\n </svg>\n </button>\n\n <button\n class=\"panel-btn\"\n [title]=\"isCollapsed ? 'Expand' : 'Collapse'\"\n (click)=\"toggleTerminal()\"\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <polyline *ngIf=\"position === 'bottom' && isCollapsed\" points=\"18 15 12 9 6 15\" />\n <polyline *ngIf=\"position === 'bottom' && !isCollapsed\" points=\"6 9 12 15 18 9\" />\n <polyline *ngIf=\"position === 'top' && isCollapsed\" points=\"6 9 12 15 18 9\" />\n <polyline *ngIf=\"position === 'top' && !isCollapsed\" points=\"18 15 12 9 6 15\" />\n <polyline *ngIf=\"position === 'left' && isCollapsed\" points=\"9 18 15 12 9 6\" />\n <polyline *ngIf=\"position === 'left' && !isCollapsed\" points=\"15 6 9 12 15 18\" />\n <polyline *ngIf=\"position === 'right' && isCollapsed\" points=\"15 6 9 12 15 18\" />\n <polyline *ngIf=\"position === 'right' && !isCollapsed\" points=\"9 18 15 12 9 6\" />\n </svg>\n </button>\n\n <button class=\"panel-btn panel-btn-close\" title=\"Close\" (click)=\"closeTerminal()\">\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </div>\n </div>\n <!-- Ambient glow line -->\n <div class=\"glow-line\" [class.glow-active]=\"statusExecutionState === 'running'\"></div>\n </div>\n <div class=\"terminal-content\" [style.display]=\"isCollapsed ? 'none' : ''\">\n <ng-content></ng-content>\n </div>\n</div>\n\n<!-- Services dropdown portaled outside terminal-wrapper -->\n<div class=\"services-dropdown\" *ngIf=\"servicesDropdownOpen\" [ngStyle]=\"servicesDropdownStyle\" (click)=\"$event.stopPropagation()\">\n <div class=\"services-dropdown-header\">Background Services</div>\n <div class=\"services-dropdown-list\">\n <div class=\"services-dropdown-item\" *ngFor=\"let svc of statusServiceDetails\">\n <span class=\"svc-dot\" [class.svc-running]=\"svc.status === 'running'\" [class.svc-stopped]=\"svc.status !== 'running'\"></span>\n <div class=\"svc-info\">\n <span class=\"svc-name\">{{ svc.name }}</span>\n <span class=\"svc-desc\" *ngIf=\"svc.description\">{{ svc.description }}</span>\n </div>\n <span class=\"svc-status\">{{ svc.status }}</span>\n </div>\n </div>\n <div class=\"services-dropdown-empty\" *ngIf=\"statusServiceDetails.length === 0\">\n No services registered\n </div>\n</div>\n\n<!-- Servers dropdown portaled outside terminal-wrapper -->\n<div class=\"services-dropdown\" *ngIf=\"serversDropdownOpen\" [ngStyle]=\"serversDropdownStyle\" (click)=\"$event.stopPropagation()\">\n <div class=\"services-dropdown-header\">Server Connections</div>\n <div class=\"services-dropdown-list\">\n <div class=\"services-dropdown-item\" *ngFor=\"let srv of statusServerDetails\">\n <span class=\"svc-dot\" [class.svc-running]=\"srv.connected\" [class.svc-stopped]=\"!srv.connected\"></span>\n <div class=\"svc-info\">\n <span class=\"svc-name\">{{ srv.name }}</span>\n <span class=\"svc-desc\" *ngIf=\"srv.url\">{{ srv.url }}</span>\n </div>\n <div class=\"srv-meta\">\n <span class=\"svc-status\">{{ srv.connected ? 'connected' : 'disconnected' }}</span>\n <span class=\"svc-desc\" *ngIf=\"srv.apiVersion\">v{{ srv.apiVersion }}</span>\n <span class=\"svc-desc\" *ngIf=\"srv.commandCount\">{{ srv.commandCount }} cmds</span>\n </div>\n </div>\n </div>\n <div class=\"services-dropdown-empty\" *ngIf=\"statusServerDetails.length === 0\">\n No servers configured\n </div>\n</div>\n\n<!-- Position dropdown portaled outside terminal-wrapper to escape transform stacking context -->\n<div class=\"position-dropdown\" *ngIf=\"positionDropdownOpen\" [ngStyle]=\"dropdownStyle\" (click)=\"$event.stopPropagation()\">\n <button\n class=\"position-dropdown-item\"\n [class.active]=\"position === 'bottom'\"\n (click)=\"selectPosition('bottom')\"\n >\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" />\n <rect x=\"4\" y=\"15\" width=\"16\" height=\"5\" rx=\"1\" fill=\"currentColor\" stroke=\"none\" opacity=\"0.6\" />\n </svg>\n Bottom\n </button>\n <button\n class=\"position-dropdown-item\"\n [class.active]=\"position === 'top'\"\n (click)=\"selectPosition('top')\"\n >\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" />\n <rect x=\"4\" y=\"4\" width=\"16\" height=\"5\" rx=\"1\" fill=\"currentColor\" stroke=\"none\" opacity=\"0.6\" />\n </svg>\n Top\n </button>\n <button\n class=\"position-dropdown-item\"\n [class.active]=\"position === 'left'\"\n (click)=\"selectPosition('left')\"\n >\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" />\n <rect x=\"4\" y=\"4\" width=\"5\" height=\"16\" rx=\"1\" fill=\"currentColor\" stroke=\"none\" opacity=\"0.6\" />\n </svg>\n Left\n </button>\n <button\n class=\"position-dropdown-item\"\n [class.active]=\"position === 'right'\"\n (click)=\"selectPosition('right')\"\n >\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" />\n <rect x=\"15\" y=\"4\" width=\"5\" height=\"16\" rx=\"1\" fill=\"currentColor\" stroke=\"none\" opacity=\"0.6\" />\n </svg>\n Right\n </button>\n</div>\n", styles: [".terminal-wrapper{position:fixed;bottom:0;left:0;right:0;transform:translateY(0);transition:transform .3s cubic-bezier(.4,0,.2,1);background-color:var(--cli-panel-bg, #111827);color:var(--cli-panel-text, rgba(255, 255, 255, .87));border-top:1px solid var(--cli-panel-border, #374151);display:flex;flex-direction:column;z-index:1000;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;box-shadow:0 -4px 20px #00000040}.terminal-wrapper.collapsed{transform:translateY(calc(100% - 60px))}.terminal-wrapper.resizing{transition:none;-webkit-user-select:none;user-select:none}.terminal-header{background-color:var(--cli-panel-header-bg, #1f2937);display:flex;flex-direction:column;position:relative;flex-shrink:0}.terminal-header .resize-bar{position:absolute;top:-6px;left:0;right:0;height:12px;cursor:ns-resize;display:flex;align-items:center;justify-content:center;z-index:2}.terminal-header .resize-bar .resize-grip{width:32px;height:4px;border-radius:2px;background:var(--cli-panel-text-secondary, rgba(255, 255, 255, .3));opacity:0;transition:opacity .2s ease}.terminal-header .resize-bar:hover .resize-grip{opacity:1}.terminal-header .header-content{height:60px;display:flex;flex-direction:row;gap:1rem;align-items:center;border-bottom:1px solid var(--cli-panel-border, #374151);padding:0 1rem}.terminal-header .header-content .terminal-title{font-size:.95rem;display:flex;gap:.5rem;justify-content:center;align-items:center;margin:0;font-weight:600;color:var(--cli-panel-text, rgba(255, 255, 255, .87))}.terminal-header .header-content .terminal-title .title-icon{color:var(--cli-panel-accent, #818cf8)}.terminal-header .header-content .action-buttons{display:flex;flex-direction:row;gap:.35rem;margin-left:auto}.panel-btn{appearance:none;background:transparent;border:none;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .6));cursor:pointer;width:40px;height:40px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;padding:0;transition:background-color .2s ease,color .2s ease}.panel-btn svg{flex-shrink:0}.panel-btn:hover:not(:disabled){color:var(--cli-panel-text, rgba(255, 255, 255, .87));background-color:var(--cli-btn-hover-bg, rgba(129, 140, 248, .12))}.panel-btn:active:not(:disabled){background-color:var(--cli-btn-active-bg, rgba(129, 140, 248, .2))}.panel-btn:disabled{opacity:.3;cursor:not-allowed}.terminal-content{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column}.terminal-wrapper[data-position=top]{bottom:auto;top:0;border-top:none;border-bottom:1px solid var(--cli-panel-border, #374151);box-shadow:0 4px 20px #00000040;flex-direction:column-reverse}.terminal-wrapper[data-position=top].collapsed{transform:translateY(calc(-100% + 60px))}.terminal-wrapper[data-position=top] .terminal-header .header-content{border-bottom:none;border-top:1px solid var(--cli-panel-border, #374151)}.terminal-wrapper[data-position=top] .terminal-header .resize-bar{top:auto;bottom:-6px;cursor:ns-resize}.terminal-wrapper[data-position=left]{inset:0 auto 0 0;width:auto;height:100vh!important;border-top:none;border-right:1px solid var(--cli-panel-border, #374151);box-shadow:4px 0 20px #00000040;flex-direction:row-reverse}.terminal-wrapper[data-position=left].collapsed{transform:translate(calc(-100% + 60px))}.terminal-wrapper[data-position=left] .terminal-header{width:60px;flex-shrink:0;flex-direction:column}.terminal-wrapper[data-position=left] .terminal-header .header-content{height:auto;flex:1;flex-direction:column;padding:1rem 0;border-bottom:none;border-left:1px solid var(--cli-panel-border, #374151)}.terminal-wrapper[data-position=left] .terminal-header .header-content .terminal-title{writing-mode:vertical-rl;text-orientation:mixed}.terminal-wrapper[data-position=left] .terminal-header .header-content .action-buttons{flex-direction:column;margin-left:0;margin-top:auto}.terminal-wrapper[data-position=left] .terminal-header .resize-bar{position:absolute;inset:0 -6px 0 auto;width:12px;height:auto;cursor:ew-resize}.terminal-wrapper[data-position=left] .terminal-header .resize-bar .resize-grip{width:4px;height:32px}.terminal-wrapper[data-position=left] .terminal-content{flex:1;min-width:0}.terminal-wrapper[data-position=right]{inset:0 0 0 auto;width:auto;height:100vh!important;border-top:none;border-left:1px solid var(--cli-panel-border, #374151);box-shadow:-4px 0 20px #00000040;flex-direction:row}.terminal-wrapper[data-position=right].collapsed{transform:translate(calc(100% - 60px))}.terminal-wrapper[data-position=right] .terminal-header{width:60px;flex-shrink:0;flex-direction:column}.terminal-wrapper[data-position=right] .terminal-header .header-content{height:auto;flex:1;flex-direction:column;padding:1rem 0;border-bottom:none;border-right:1px solid var(--cli-panel-border, #374151)}.terminal-wrapper[data-position=right] .terminal-header .header-content .terminal-title{writing-mode:vertical-rl;text-orientation:mixed}.terminal-wrapper[data-position=right] .terminal-header .header-content .action-buttons{flex-direction:column;margin-left:0;margin-top:auto}.terminal-wrapper[data-position=right] .terminal-header .resize-bar{position:absolute;inset:0 auto 0 -6px;width:12px;height:auto;cursor:ew-resize}.terminal-wrapper[data-position=right] .terminal-header .resize-bar .resize-grip{width:4px;height:32px}.terminal-wrapper[data-position=right] .terminal-content{flex:1;min-width:0}.terminal-wrapper[data-resizable=false] .resize-bar,.terminal-wrapper[data-closable=false] .panel-btn-close{display:none}.cli-panel-hide-tab{position:fixed;z-index:1000;display:flex;align-items:center;gap:6px;background-color:var(--cli-panel-header-bg, #1f2937);border:1px solid var(--cli-panel-border, #374151);color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .6));cursor:pointer;padding:6px 12px;appearance:none;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:.8rem;font-weight:600;transition:transform .2s ease,opacity .2s ease,color .2s ease,background-color .2s ease,box-shadow .2s ease;box-shadow:0 2px 8px #0000004d;opacity:.85}.cli-panel-hide-tab:hover{transform:scale(1.05);opacity:1;color:var(--cli-panel-text, rgba(255, 255, 255, .87));background-color:var(--cli-panel-bg, #111827);box-shadow:0 4px 16px #0006}.cli-panel-hide-tab:active{transform:scale(.97)}.cli-panel-hide-tab svg{flex-shrink:0}.cli-panel-hide-tab .cli-panel-hide-tab-icon{color:var(--cli-panel-accent, #818cf8)}.cli-panel-hide-tab .cli-panel-hide-tab-label{letter-spacing:.03em}.cli-panel-hide-tab[data-position=bottom]{bottom:0;border-bottom:none;border-radius:8px 8px 0 0}.cli-panel-hide-tab[data-position=bottom][data-hide-align=start]{left:16px}.cli-panel-hide-tab[data-position=bottom][data-hide-align=center]{left:50%;transform:translate(-50%)}.cli-panel-hide-tab[data-position=bottom][data-hide-align=center]:hover{transform:translate(-50%) scale(1.05)}.cli-panel-hide-tab[data-position=bottom][data-hide-align=end]{right:16px}.cli-panel-hide-tab[data-position=top]{top:0;border-top:none;border-radius:0 0 8px 8px}.cli-panel-hide-tab[data-position=top][data-hide-align=start]{left:16px}.cli-panel-hide-tab[data-position=top][data-hide-align=center]{left:50%;transform:translate(-50%)}.cli-panel-hide-tab[data-position=top][data-hide-align=center]:hover{transform:translate(-50%) scale(1.05)}.cli-panel-hide-tab[data-position=top][data-hide-align=end]{right:16px}.cli-panel-hide-tab[data-position=left]{left:0;border-left:none;border-radius:0 8px 8px 0}.cli-panel-hide-tab[data-position=left][data-hide-align=start]{top:16px}.cli-panel-hide-tab[data-position=left][data-hide-align=center]{top:50%;transform:translateY(-50%)}.cli-panel-hide-tab[data-position=left][data-hide-align=center]:hover{transform:translateY(-50%) scale(1.05)}.cli-panel-hide-tab[data-position=left][data-hide-align=end]{bottom:16px}.cli-panel-hide-tab[data-position=right]{right:0;border-right:none;border-radius:8px 0 0 8px}.cli-panel-hide-tab[data-position=right][data-hide-align=start]{top:16px}.cli-panel-hide-tab[data-position=right][data-hide-align=center]{top:50%;transform:translateY(-50%)}.cli-panel-hide-tab[data-position=right][data-hide-align=center]:hover{transform:translateY(-50%) scale(1.05)}.cli-panel-hide-tab[data-position=right][data-hide-align=end]{bottom:16px}@keyframes cli-hide-tab-pulse{0%,to{transform:translate(0)}50%{transform:translate(3px)}}@keyframes cli-hide-tab-pulse-vertical{0%,to{transform:translateY(0)}50%{transform:translateY(3px)}}.cli-panel-hide-tab:hover .cli-panel-hide-tab-arrow{animation:cli-hide-tab-pulse .6s ease infinite}.cli-panel-hide-tab[data-position=left]:hover .cli-panel-hide-tab-arrow,.cli-panel-hide-tab[data-position=right]:hover .cli-panel-hide-tab-arrow{animation:cli-hide-tab-pulse-vertical .6s ease infinite}.panel-btn-position-wrapper{position:relative}.position-dropdown{position:fixed;background-color:var(--cli-panel-header-bg, #1f2937);border:1px solid var(--cli-panel-border, #374151);border-radius:6px;box-shadow:0 8px 24px #0006;z-index:1100;min-width:120px;overflow:hidden;display:flex;flex-direction:column}.position-dropdown-item{appearance:none;background:transparent;border:none;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .6));cursor:pointer;display:flex;align-items:center;gap:8px;padding:8px 12px;font-size:.85rem;font-family:inherit;width:100%;text-align:left;transition:background-color .15s ease,color .15s ease}.position-dropdown-item:hover{background-color:var(--cli-btn-hover-bg, rgba(129, 140, 248, .12));color:var(--cli-panel-text, rgba(255, 255, 255, .87))}.position-dropdown-item.active{color:var(--cli-panel-accent, #818cf8)}.position-dropdown-item svg{flex-shrink:0}.terminal-wrapper[data-hideable=false] .panel-btn-hide{display:none}.status-indicators{display:flex;align-items:center;gap:14px;margin-left:24px;font-size:12px;overflow:hidden;white-space:nowrap}.status-item{display:flex;align-items:center;gap:5px;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .6))}.status-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;background:var(--cli-status-neutral, #6b7280)}.status-dot.dot-idle{background:var(--cli-status-idle, #3fb950)}.status-dot.dot-running{background:var(--cli-status-running, #f0883e);animation:statusPulse 1.5s ease-in-out infinite}.status-dot.dot-error{background:var(--cli-status-error, #f85149)}.status-icon{flex-shrink:0}.status-icon.status-success{color:var(--cli-status-idle, #3fb950)}.status-icon.status-error{color:var(--cli-status-error, #f85149)}.status-label{overflow:hidden;text-overflow:ellipsis}.status-muted{color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .4))}.status-text{color:var(--cli-panel-accent, #818cf8);max-width:200px}.status-text .status-label{overflow:hidden;text-overflow:ellipsis}.status-running{color:var(--cli-status-running, #f0883e)}.status-clickable{cursor:pointer;border-radius:4px;padding:2px 6px;margin:-2px -6px;transition:background-color .15s ease}.status-clickable:hover{background-color:var(--cli-btn-hover-bg, rgba(129, 140, 248, .12))}.services-dropdown{position:fixed;background-color:var(--cli-panel-header-bg, #1f2937);border:1px solid var(--cli-panel-border, #374151);border-radius:8px;box-shadow:0 8px 24px #0006;z-index:1100;min-width:260px;max-width:380px;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif}.services-dropdown-header{padding:10px 14px 8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .5));border-bottom:1px solid var(--cli-panel-border, #374151)}.services-dropdown-list{max-height:240px;overflow-y:auto}.services-dropdown-list::-webkit-scrollbar{width:6px}.services-dropdown-list::-webkit-scrollbar-track{background:transparent}.services-dropdown-list::-webkit-scrollbar-thumb{background:var(--cli-panel-text-secondary, rgba(255, 255, 255, .2));border-radius:3px}.services-dropdown-list::-webkit-scrollbar-thumb:hover{background:var(--cli-panel-text-secondary, rgba(255, 255, 255, .35))}.services-dropdown-list{scrollbar-width:thin;scrollbar-color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .2)) transparent}.services-dropdown-item{display:flex;align-items:flex-start;gap:10px;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.04);transition:background-color .15s ease}.services-dropdown-item:last-child{border-bottom:none}.services-dropdown-item:hover{background-color:var(--cli-btn-hover-bg, rgba(129, 140, 248, .08))}.svc-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0;margin-top:5px}.svc-dot.svc-running{background:var(--cli-status-idle, #3fb950)}.svc-dot.svc-stopped{background:var(--cli-panel-text-secondary, rgba(255, 255, 255, .3))}.svc-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.svc-name{font-size:13px;font-weight:500;color:var(--cli-panel-text, rgba(255, 255, 255, .87));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.svc-desc{font-size:11px;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .5));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.svc-status{font-size:11px;flex-shrink:0;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .5));margin-top:1px}.srv-meta{display:flex;flex-direction:column;align-items:flex-end;gap:2px;flex-shrink:0}.services-dropdown-empty{padding:16px 14px;font-size:12px;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .4));text-align:center}.glow-line{position:absolute;bottom:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--cli-status-glow, var(--cli-panel-accent, #818cf8)),#a78bfa,var(--cli-status-glow, var(--cli-panel-accent, #818cf8)),transparent);background-size:200% 100%;opacity:.4;animation:glowShift 8s ease-in-out infinite;transition:opacity .6s ease;pointer-events:none}.glow-line.glow-active{opacity:.8;animation-duration:3s}.status-indicators-compact{display:none}.terminal-wrapper[data-position=left] .status-indicators-compact,.terminal-wrapper[data-position=right] .status-indicators-compact{display:flex;flex-direction:column;align-items:center;gap:10px;padding:12px 0}.compact-item{display:flex;align-items:center;justify-content:center;width:20px;height:20px;cursor:default}.compact-item.status-clickable{cursor:pointer;border-radius:4px}.compact-item.status-clickable:hover{background-color:var(--cli-btn-hover-bg, rgba(129, 140, 248, .12))}.compact-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.compact-dot.level-info{background:var(--cli-panel-accent, #818cf8)}.compact-dot.level-success{background:var(--cli-status-idle, #3fb950)}.compact-dot.level-warn{background:var(--cli-status-running, #f0883e)}.compact-dot.level-error{background:var(--cli-status-error, #f85149)}.terminal-wrapper[data-position=left] .glow-line{inset:0 0 auto auto;width:2px;height:100%;background:linear-gradient(180deg,transparent,var(--cli-status-glow, var(--cli-panel-accent, #818cf8)),#a78bfa,var(--cli-status-glow, var(--cli-panel-accent, #818cf8)),transparent);background-size:100% 200%}.terminal-wrapper[data-position=right] .glow-line{inset:0 auto auto 0;width:2px;height:100%;background:linear-gradient(180deg,transparent,var(--cli-status-glow, var(--cli-panel-accent, #818cf8)),#a78bfa,var(--cli-status-glow, var(--cli-panel-accent, #818cf8)),transparent);background-size:100% 200%}@keyframes glowShift{0%{background-position:200% center}to{background-position:-200% center}}@keyframes statusPulse{0%,to{opacity:1}50%{opacity:.3}}@media(prefers-reduced-motion:reduce){.glow-line,.status-dot{animation:none!important}}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CollapsableContentComponent, decorators: [{
type: Component,
args: [{ standalone: false, selector: 'collapsable-content', template: "<!-- Hide tab (shown when panel is hidden) -->\n<button\n *ngIf=\"visible && isHidden\"\n class=\"cli-panel-hide-tab\"\n [attr.data-position]=\"position\"\n [attr.data-hide-align]=\"hideAlignment\"\n [ngStyle]=\"themeStyles\"\n title=\"Show CLI\"\n (click)=\"unhideTerminal()\"\n>\n <svg\n class=\"cli-panel-hide-tab-icon\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <polyline points=\"4 17 10 11 4 5\" />\n <line x1=\"12\" y1=\"19\" x2=\"20\" y2=\"19\" />\n </svg>\n <span class=\"cli-panel-hide-tab-label\">CLI</span>\n <svg\n class=\"cli-panel-hide-tab-arrow\"\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <!-- bottom: chevron up -->\n <polyline *ngIf=\"position === 'bottom'\" points=\"18 15 12 9 6 15\" />\n <!-- top: chevron down -->\n <polyline *ngIf=\"position === 'top'\" points=\"6 9 12 15 18 9\" />\n <!-- left: chevron right -->\n <polyline *ngIf=\"position === 'left'\" points=\"9 18 15 12 9 6\" />\n <!-- right: chevron left -->\n <polyline *ngIf=\"position === 'right'\" points=\"15 6 9 12 15 18\" />\n </svg>\n</button>\n\n<!-- Main panel (kept in DOM when hidden to preserve terminal state) -->\n<div\n *ngIf=\"visible\"\n class=\"terminal-wrapper\"\n [style.display]=\"isHidden ? 'none' : ''\"\n [class.collapsed]=\"isCollapsed\"\n [class.maximized]=\"isMaximized\"\n [class.resizing]=\"isResizing\"\n [attr.data-position]=\"position\"\n [attr.data-resizable]=\"resizable\"\n [attr.data-closable]=\"closable\"\n [attr.data-hideable]=\"hideable\"\n [ngStyle]=\"wrapperStyle\"\n>\n <div class=\"terminal-header\">\n <div class=\"resize-bar\" (mousedown)=\"onResizeStart($event)\">\n <div class=\"resize-grip\"></div>\n </div>\n <div class=\"header-content\">\n <p class=\"terminal-title\">\n <svg\n class=\"title-icon\"\n width=\"22\"\n height=\"22\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <polyline points=\"4 17 10 11 4 5\" />\n <line x1=\"12\" y1=\"19\" x2=\"20\" y2=\"19\" />\n </svg>\n CLI\n </p>\n <!-- Status indicators (bottom/top positions only) -->\n <div class=\"status-indicators\" *ngIf=\"showStatusIndicators\">\n <!-- Execution state -->\n <span class=\"status-item\" [class.status-running]=\"statusExecutionState === 'running'\">\n <span class=\"status-dot\" [class.dot-idle]=\"statusExecutionState === 'idle'\" [class.dot-running]=\"statusExecutionState === 'running'\"></span>\n <span class=\"status-label\">{{ statusExecutionState }}</span>\n </span>\n\n <!-- Background services (only if any exist) -->\n <span class=\"status-item status-clickable\" *ngIf=\"statusServiceCount.total > 0\" (click)=\"toggleServicesDropdown($event)\">\n <span class=\"status-icon\">⚙</span>\n <span class=\"status-label\">{{ statusServiceCount.running }}/{{ statusServiceCount.total }} services</span>\n </span>\n\n <!-- Last command -->\n <span class=\"status-item\" *ngIf=\"statusLastCommand\">\n <span class=\"status-icon\" [class.status-success]=\"statusLastCommand.success\" [class.status-error]=\"!statusLastCommand.success\">\n {{ statusLastCommand.success ? '✓' : '✕' }}\n </span>\n <span class=\"status-label\">{{ statusLastCommand.name }}</span>\n </span>\n\n <!-- Server connection -->\n <span class=\"status-item status-clickable\" *ngIf=\"statusServerState !== 'none'\" (click)=\"toggleServersDropdown($event)\">\n <span class=\"status-dot\" [class.dot-idle]=\"statusServerState === 'connected'\" [class.dot-error]=\"statusServerState === 'disconnected'\"></span>\n <span class=\"status-label\">{{ connectedServerCount }}/{{ totalServerCount }} servers</span>\n </span>\n\n <!-- Uptime -->\n <span class=\"status-item status-muted\" *ngIf=\"statusUptime > 0\">\n <span class=\"status-icon\">↑</span>\n <span class=\"status-label\">{{ formattedUptime }}</span>\n </span>\n\n <!-- Custom processor notification -->\n <span class=\"status-item status-text\" [ngClass]=\"'level-' + notification?.level\" *ngIf=\"notification\">\n <span class=\"status-label\">{{ notification.message }}</span>\n </span>\n </div>\n\n <!-- Compact status indicators (left/right positions \u2014 CSS controls visibility) -->\n <div class=\"status-indicators-compact\">\n <!-- Execution state -->\n <span class=\"compact-item\" [title]=\"statusExecutionState\">\n <span class=\"status-dot\" [class.dot-idle]=\"statusExecutionState === 'idle'\" [class.dot-running]=\"statusExecutionState === 'running'\"></span>\n </span>\n\n <!-- Background services -->\n <span class=\"compact-item status-clickable\" *ngIf=\"statusServiceCount.total > 0\" [title]=\"statusServiceCount.running + '/' + statusServiceCount.total + ' services'\" (click)=\"toggleServicesDropdown($event)\">\n <span class=\"status-icon\">⚙</span>\n </span>\n\n <!-- Last command -->\n <span class=\"compact-item\" *ngIf=\"statusLastCommand\" [title]=\"(statusLastCommand.success ? '\u2713 ' : '\u2717 ') + statusLastCommand.name\">\n <span class=\"status-icon\" [class.status-success]=\"statusLastCommand.success\" [class.status-error]=\"!statusLastCommand.success\">\n {{ statusLastCommand.success ? '✓' : '✕' }}\n </span>\n </span>\n\n <!-- Server connection -->\n <span class=\"compact-item status-clickable\" *ngIf=\"statusServerState !== 'none'\" [title]=\"connectedServerCount + '/' + totalServerCount + ' servers'\" (click)=\"toggleServersDropdown($event)\">\n <span class=\"status-dot\" [class.dot-idle]=\"statusServerState === 'connected'\" [class.dot-error]=\"statusServerState === 'disconnected'\"></span>\n </span>\n\n <!-- Uptime -->\n <span class=\"compact-item status-muted\" *ngIf=\"statusUptime > 0\" [title]=\"formattedUptime\">\n <span class=\"status-icon\">↑</span>\n </span>\n\n <!-- Notification -->\n <span class=\"compact-item\" *ngIf=\"notification\" [title]=\"notification.message\">\n <span class=\"compact-dot\" [ngClass]=\"'level-' + notification.level\"></span>\n </span>\n </div>\n\n <div class=\"action-buttons\">\n <!-- Position dropdown -->\n <div class=\"panel-btn-position-wrapper\" (click)=\"$event.stopPropagation()\">\n <button\n class=\"panel-btn panel-btn-position\"\n title=\"Move panel\"\n (click)=\"togglePositionDropdown($event)\"\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" />\n <!-- bottom -->\n <rect *ngIf=\"position === 'bottom'\" x=\"4\" y=\"15\" width=\"16\" height=\"5\" rx=\"1\" fill=\"currentColor\" stroke=\"none\" opacity=\"0.5\" />\n <!-- top -->\n <rect *ngIf=\"position === 'top'\" x=\"4\" y=\"4\" width=\"16\" height=\"5\" rx=\"1\" fill=\"currentColor\" stroke=\"none\" opacity=\"0.5\" />\n <!-- left -->\n <rect *ngIf=\"position === 'left'\" x=\"4\" y=\"4\" width=\"5\" height=\"16\" rx=\"1\" fill=\"currentColor\" stroke=\"none\" opacity=\"0.5\" />\n <!-- right -->\n <rect *ngIf=\"position === 'right'\" x=\"15\" y=\"4\" width=\"5\" height=\"16\" rx=\"1\" fill=\"currentColor\" stroke=\"none\" opacity=\"0.5\" />\n </svg>\n </button>\n </div>\n\n <!-- Hide button -->\n <button\n *ngIf=\"hideable\"\n class=\"panel-btn panel-btn-hide\"\n title=\"Hide\"\n (click)=\"hideTerminal()\"\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <line x1=\"5\" y1=\"18\" x2=\"19\" y2=\"18\" />\n <polyline points=\"9 14 12 17 15 14\" />\n </svg>\n </button>\n\n <button\n class=\"panel-btn\"\n [title]=\"!isMaximized ? 'Maximize' : 'Restore'\"\n [disabled]=\"isCollapsed\"\n (click)=\"toggleMaximizationTerminal()\"\n >\n <svg\n *ngIf=\"!isMaximized\"\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <polyline points=\"15 3 21 3 21 9\" />\n <polyline points=\"9 21 3 21 3 15\" />\n <line x1=\"21\" y1=\"3\" x2=\"14\" y2=\"10\" />\n <line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\" />\n </svg>\n <svg\n *ngIf=\"isMaximized\"\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <polyline points=\"4 14 10 14 10 20\" />\n <polyline points=\"20 10 14 10 14 4\" />\n <line x1=\"14\" y1=\"10\" x2=\"21\" y2=\"3\" />\n <line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\" />\n </svg>\n </button>\n\n <button\n class=\"panel-btn\"\n [title]=\"isCollapsed ? 'Expand' : 'Collapse'\"\n (click)=\"toggleTerminal()\"\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <polyline *ngIf=\"position === 'bottom' && isCollapsed\" points=\"18 15 12 9 6 15\" />\n <polyline *ngIf=\"position === 'bottom' && !isCollapsed\" points=\"6 9 12 15 18 9\" />\n <polyline *ngIf=\"position === 'top' && isCollapsed\" points=\"6 9 12 15 18 9\" />\n <polyline *ngIf=\"position === 'top' && !isCollapsed\" points=\"18 15 12 9 6 15\" />\n <polyline *ngIf=\"position === 'left' && isCollapsed\" points=\"9 18 15 12 9 6\" />\n <polyline *ngIf=\"position === 'left' && !isCollapsed\" points=\"15 6 9 12 15 18\" />\n <polyline *ngIf=\"position === 'right' && isCollapsed\" points=\"15 6 9 12 15 18\" />\n <polyline *ngIf=\"position === 'right' && !isCollapsed\" points=\"9 18 15 12 9 6\" />\n </svg>\n </button>\n\n <button class=\"panel-btn panel-btn-close\" title=\"Close\" (click)=\"closeTerminal()\">\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </div>\n </div>\n <!-- Ambient glow line -->\n <div class=\"glow-line\" [class.glow-active]=\"statusExecutionState === 'running'\"></div>\n </div>\n <div class=\"terminal-content\" [style.display]=\"isCollapsed ? 'none' : ''\">\n <ng-content></ng-content>\n </div>\n</div>\n\n<!-- Services dropdown portaled outside terminal-wrapper -->\n<div class=\"services-dropdown\" *ngIf=\"servicesDropdownOpen\" [ngStyle]=\"servicesDropdownStyle\" (click)=\"$event.stopPropagation()\">\n <div class=\"services-dropdown-header\">Background Services</div>\n <div class=\"services-dropdown-list\">\n <div class=\"services-dropdown-item\" *ngFor=\"let svc of statusServiceDetails\">\n <span class=\"svc-dot\" [class.svc-running]=\"svc.status === 'running'\" [class.svc-stopped]=\"svc.status !== 'running'\"></span>\n <div class=\"svc-info\">\n <span class=\"svc-name\">{{ svc.name }}</span>\n <span class=\"svc-desc\" *ngIf=\"svc.description\">{{ svc.description }}</span>\n </div>\n <span class=\"svc-status\">{{ svc.status }}</span>\n </div>\n </div>\n <div class=\"services-dropdown-empty\" *ngIf=\"statusServiceDetails.length === 0\">\n No services registered\n </div>\n</div>\n\n<!-- Servers dropdown portaled outside terminal-wrapper -->\n<div class=\"services-dropdown\" *ngIf=\"serversDropdownOpen\" [ngStyle]=\"serversDropdownStyle\" (click)=\"$event.stopPropagation()\">\n <div class=\"services-dropdown-header\">Server Connections</div>\n <div class=\"services-dropdown-list\">\n <div class=\"services-dropdown-item\" *ngFor=\"let srv of statusServerDetails\">\n <span class=\"svc-dot\" [class.svc-running]=\"srv.connected\" [class.svc-stopped]=\"!srv.connected\"></span>\n <div class=\"svc-info\">\n <span class=\"svc-name\">{{ srv.name }}</span>\n <span class=\"svc-desc\" *ngIf=\"srv.url\">{{ srv.url }}</span>\n </div>\n <div class=\"srv-meta\">\n <span class=\"svc-status\">{{ srv.connected ? 'connected' : 'disconnected' }}</span>\n <span class=\"svc-desc\" *ngIf=\"srv.apiVersion\">v{{ srv.apiVersion }}</span>\n <span class=\"svc-desc\" *ngIf=\"srv.commandCount\">{{ srv.commandCount }} cmds</span>\n </div>\n </div>\n </div>\n <div class=\"services-dropdown-empty\" *ngIf=\"statusServerDetails.length === 0\">\n No servers configured\n </div>\n</div>\n\n<!-- Position dropdown portaled outside terminal-wrapper to escape transform stacking context -->\n<div class=\"position-dropdown\" *ngIf=\"positionDropdownOpen\" [ngStyle]=\"dropdownStyle\" (click)=\"$event.stopPropagation()\">\n <button\n class=\"position-dropdown-item\"\n [class.active]=\"position === 'bottom'\"\n (click)=\"selectPosition('bottom')\"\n >\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" />\n <rect x=\"4\" y=\"15\" width=\"16\" height=\"5\" rx=\"1\" fill=\"currentColor\" stroke=\"none\" opacity=\"0.6\" />\n </svg>\n Bottom\n </button>\n <button\n class=\"position-dropdown-item\"\n [class.active]=\"position === 'top'\"\n (click)=\"selectPosition('top')\"\n >\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" />\n <rect x=\"4\" y=\"4\" width=\"16\" height=\"5\" rx=\"1\" fill=\"currentColor\" stroke=\"none\" opacity=\"0.6\" />\n </svg>\n Top\n </button>\n <button\n class=\"position-dropdown-item\"\n [class.active]=\"position === 'left'\"\n (click)=\"selectPosition('left')\"\n >\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" />\n <rect x=\"4\" y=\"4\" width=\"5\" height=\"16\" rx=\"1\" fill=\"currentColor\" stroke=\"none\" opacity=\"0.6\" />\n </svg>\n Left\n </button>\n <button\n class=\"position-dropdown-item\"\n [class.active]=\"position === 'right'\"\n (click)=\"selectPosition('right')\"\n >\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" />\n <rect x=\"15\" y=\"4\" width=\"5\" height=\"16\" rx=\"1\" fill=\"currentColor\" stroke=\"none\" opacity=\"0.6\" />\n </svg>\n Right\n </button>\n</div>\n", styles: [".terminal-wrapper{position:fixed;bottom:0;left:0;right:0;transform:translateY(0);transition:transform .3s cubic-bezier(.4,0,.2,1);background-color:var(--cli-panel-bg, #111827);color:var(--cli-panel-text, rgba(255, 255, 255, .87));border-top:1px solid var(--cli-panel-border, #374151);display:flex;flex-direction:column;z-index:1000;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;box-shadow:0 -4px 20px #00000040}.terminal-wrapper.collapsed{transform:translateY(calc(100% - 60px))}.terminal-wrapper.resizing{transition:none;-webkit-user-select:none;user-select:none}.terminal-header{background-color:var(--cli-panel-header-bg, #1f2937);display:flex;flex-direction:column;position:relative;flex-shrink:0}.terminal-header .resize-bar{position:absolute;top:-6px;left:0;right:0;height:12px;cursor:ns-resize;display:flex;align-items:center;justify-content:center;z-index:2}.terminal-header .resize-bar .resize-grip{width:32px;height:4px;border-radius:2px;background:var(--cli-panel-text-secondary, rgba(255, 255, 255, .3));opacity:0;transition:opacity .2s ease}.terminal-header .resize-bar:hover .resize-grip{opacity:1}.terminal-header .header-content{height:60px;display:flex;flex-direction:row;gap:1rem;align-items:center;border-bottom:1px solid var(--cli-panel-border, #374151);padding:0 1rem}.terminal-header .header-content .terminal-title{font-size:.95rem;display:flex;gap:.5rem;justify-content:center;align-items:center;margin:0;font-weight:600;color:var(--cli-panel-text, rgba(255, 255, 255, .87))}.terminal-header .header-content .terminal-title .title-icon{color:var(--cli-panel-accent, #818cf8)}.terminal-header .header-content .action-buttons{display:flex;flex-direction:row;gap:.35rem;margin-left:auto}.panel-btn{appearance:none;background:transparent;border:none;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .6));cursor:pointer;width:40px;height:40px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;padding:0;transition:background-color .2s ease,color .2s ease}.panel-btn svg{flex-shrink:0}.panel-btn:hover:not(:disabled){color:var(--cli-panel-text, rgba(255, 255, 255, .87));background-color:var(--cli-btn-hover-bg, rgba(129, 140, 248, .12))}.panel-btn:active:not(:disabled){background-color:var(--cli-btn-active-bg, rgba(129, 140, 248, .2))}.panel-btn:disabled{opacity:.3;cursor:not-allowed}.terminal-content{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column}.terminal-wrapper[data-position=top]{bottom:auto;top:0;border-top:none;border-bottom:1px solid var(--cli-panel-border, #374151);box-shadow:0 4px 20px #00000040;flex-direction:column-reverse}.terminal-wrapper[data-position=top].collapsed{transform:translateY(calc(-100% + 60px))}.terminal-wrapper[data-position=top] .terminal-header .header-content{border-bottom:none;border-top:1px solid var(--cli-panel-border, #374151)}.terminal-wrapper[data-position=top] .terminal-header .resize-bar{top:auto;bottom:-6px;cursor:ns-resize}.terminal-wrapper[data-position=left]{inset:0 auto 0 0;width:auto;height:100vh!important;border-top:none;border-right:1px solid var(--cli-panel-border, #374151);box-shadow:4px 0 20px #00000040;flex-direction:row-reverse}.terminal-wrapper[data-position=left].collapsed{transform:translate(calc(-100% + 60px))}.terminal-wrapper[data-position=left] .terminal-header{width:60px;flex-shrink:0;flex-direction:column}.terminal-wrapper[data-position=left] .terminal-header .header-content{height:auto;flex:1;flex-direction:column;padding:1rem 0;border-bottom:none;border-left:1px solid var(--cli-panel-border, #374151)}.terminal-wrapper[data-position=left] .terminal-header .header-content .terminal-title{writing-mode:vertical-rl;text-orientation:mixed}.terminal-wrapper[data-position=left] .terminal-header .header-content .action-buttons{flex-direction:column;margin-left:0;margin-top:auto}.terminal-wrapper[data-position=left] .terminal-header .resize-bar{position:absolute;inset:0 -6px 0 auto;width:12px;height:auto;cursor:ew-resize}.terminal-wrapper[data-position=left] .terminal-header .resize-bar .resize-grip{width:4px;height:32px}.terminal-wrapper[data-position=left] .terminal-content{flex:1;min-width:0}.terminal-wrapper[data-position=right]{inset:0 0 0 auto;width:auto;height:100vh!important;border-top:none;border-left:1px solid var(--cli-panel-border, #374151);box-shadow:-4px 0 20px #00000040;flex-direction:row}.terminal-wrapper[data-position=right].collapsed{transform:translate(calc(100% - 60px))}.terminal-wrapper[data-position=right] .terminal-header{width:60px;flex-shrink:0;flex-direction:column}.terminal-wrapper[data-position=right] .terminal-header .header-content{height:auto;flex:1;flex-direction:column;padding:1rem 0;border-bottom:none;border-right:1px solid var(--cli-panel-border, #374151)}.terminal-wrapper[data-position=right] .terminal-header .header-content .terminal-title{writing-mode:vertical-rl;text-orientation:mixed}.terminal-wrapper[data-position=right] .terminal-header .header-content .action-buttons{flex-direction:column;margin-left:0;margin-top:auto}.terminal-wrapper[data-position=right] .terminal-header .resize-bar{position:absolute;inset:0 auto 0 -6px;width:12px;height:auto;cursor:ew-resize}.terminal-wrapper[data-position=right] .terminal-header .resize-bar .resize-grip{width:4px;height:32px}.terminal-wrapper[data-position=right] .terminal-content{flex:1;min-width:0}.terminal-wrapper[data-resizable=false] .resize-bar,.terminal-wrapper[data-closable=false] .panel-btn-close{display:none}.cli-panel-hide-tab{position:fixed;z-index:1000;display:flex;align-items:center;gap:6px;background-color:var(--cli-panel-header-bg, #1f2937);border:1px solid var(--cli-panel-border, #374151);color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .6));cursor:pointer;padding:6px 12px;appearance:none;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:.8rem;font-weight:600;transition:transform .2s ease,opacity .2s ease,color .2s ease,background-color .2s ease,box-shadow .2s ease;box-shadow:0 2px 8px #0000004d;opacity:.85}.cli-panel-hide-tab:hover{transform:scale(1.05);opacity:1;color:var(--cli-panel-text, rgba(255, 255, 255, .87));background-color:var(--cli-panel-bg, #111827);box-shadow:0 4px 16px #0006}.cli-panel-hide-tab:active{transform:scale(.97)}.cli-panel-hide-tab svg{flex-shrink:0}.cli-panel-hide-tab .cli-panel-hide-tab-icon{color:var(--cli-panel-accent, #818cf8)}.cli-panel-hide-tab .cli-panel-hide-tab-label{letter-spacing:.03em}.cli-panel-hide-tab[data-position=bottom]{bottom:0;border-bottom:none;border-radius:8px 8px 0 0}.cli-panel-hide-tab[data-position=bottom][data-hide-align=start]{left:16px}.cli-panel-hide-tab[data-position=bottom][data-hide-align=center]{left:50%;transform:translate(-50%)}.cli-panel-hide-tab[data-position=bottom][data-hide-align=center]:hover{transform:translate(-50%) scale(1.05)}.cli-panel-hide-tab[data-position=bottom][data-hide-align=end]{right:16px}.cli-panel-hide-tab[data-position=top]{top:0;border-top:none;border-radius:0 0 8px 8px}.cli-panel-hide-tab[data-position=top][data-hide-align=start]{left:16px}.cli-panel-hide-tab[data-position=top][data-hide-align=center]{left:50%;transform:translate(-50%)}.cli-panel-hide-tab[data-position=top][data-hide-align=center]:hover{transform:translate(-50%) scale(1.05)}.cli-panel-hide-tab[data-position=top][data-hide-align=end]{right:16px}.cli-panel-hide-tab[data-position=left]{left:0;border-left:none;border-radius:0 8px 8px 0}.cli-panel-hide-tab[data-position=left][data-hide-align=start]{top:16px}.cli-panel-hide-tab[data-position=left][data-hide-align=center]{top:50%;transform:translateY(-50%)}.cli-panel-hide-tab[data-position=left][data-hide-align=center]:hover{transform:translateY(-50%) scale(1.05)}.cli-panel-hide-tab[data-position=left][data-hide-align=end]{bottom:16px}.cli-panel-hide-tab[data-position=right]{right:0;border-right:none;border-radius:8px 0 0 8px}.cli-panel-hide-tab[data-position=right][data-hide-align=start]{top:16px}.cli-panel-hide-tab[data-position=right][data-hide-align=center]{top:50%;transform:translateY(-50%)}.cli-panel-hide-tab[data-position=right][data-hide-align=center]:hover{transform:translateY(-50%) scale(1.05)}.cli-panel-hide-tab[data-position=right][data-hide-align=end]{bottom:16px}@keyframes cli-hide-tab-pulse{0%,to{transform:translate(0)}50%{transform:translate(3px)}}@keyframes cli-hide-tab-pulse-vertical{0%,to{transform:translateY(0)}50%{transform:translateY(3px)}}.cli-panel-hide-tab:hover .cli-panel-hide-tab-arrow{animation:cli-hide-tab-pulse .6s ease infinite}.cli-panel-hide-tab[data-position=left]:hover .cli-panel-hide-tab-arrow,.cli-panel-hide-tab[data-position=right]:hover .cli-panel-hide-tab-arrow{animation:cli-hide-tab-pulse-vertical .6s ease infinite}.panel-btn-position-wrapper{position:relative}.position-dropdown{position:fixed;background-color:var(--cli-panel-header-bg, #1f2937);border:1px solid var(--cli-panel-border, #374151);border-radius:6px;box-shadow:0 8px 24px #0006;z-index:1100;min-width:120px;overflow:hidden;display:flex;flex-direction:column}.position-dropdown-item{appearance:none;background:transparent;border:none;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .6));cursor:pointer;display:flex;align-items:center;gap:8px;padding:8px 12px;font-size:.85rem;font-family:inherit;width:100%;text-align:left;transition:background-color .15s ease,color .15s ease}.position-dropdown-item:hover{background-color:var(--cli-btn-hover-bg, rgba(129, 140, 248, .12));color:var(--cli-panel-text, rgba(255, 255, 255, .87))}.position-dropdown-item.active{color:var(--cli-panel-accent, #818cf8)}.position-dropdown-item svg{flex-shrink:0}.terminal-wrapper[data-hideable=false] .panel-btn-hide{display:none}.status-indicators{display:flex;align-items:center;gap:14px;margin-left:24px;font-size:12px;overflow:hidden;white-space:nowrap}.status-item{display:flex;align-items:center;gap:5px;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .6))}.status-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;background:var(--cli-status-neutral, #6b7280)}.status-dot.dot-idle{background:var(--cli-status-idle, #3fb950)}.status-dot.dot-running{background:var(--cli-status-running, #f0883e);animation:statusPulse 1.5s ease-in-out infinite}.status-dot.dot-error{background:var(--cli-status-error, #f85149)}.status-icon{flex-shrink:0}.status-icon.status-success{color:var(--cli-status-idle, #3fb950)}.status-icon.status-error{color:var(--cli-status-error, #f85149)}.status-label{overflow:hidden;text-overflow:ellipsis}.status-muted{color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .4))}.status-text{color:var(--cli-panel-accent, #818cf8);max-width:200px}.status-text .status-label{overflow:hidden;text-overflow:ellipsis}.status-running{color:var(--cli-status-running, #f0883e)}.status-clickable{cursor:pointer;border-radius:4px;padding:2px 6px;margin:-2px -6px;transition:background-color .15s ease}.status-clickable:hover{background-color:var(--cli-btn-hover-bg, rgba(129, 140, 248, .12))}.services-dropdown{position:fixed;background-color:var(--cli-panel-header-bg, #1f2937);border:1px solid var(--cli-panel-border, #374151);border-radius:8px;box-shadow:0 8px 24px #0006;z-index:1100;min-width:260px;max-width:380px;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif}.services-dropdown-header{padding:10px 14px 8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .5));border-bottom:1px solid var(--cli-panel-border, #374151)}.services-dropdown-list{max-height:240px;overflow-y:auto}.services-dropdown-list::-webkit-scrollbar{width:6px}.services-dropdown-list::-webkit-scrollbar-track{background:transparent}.services-dropdown-list::-webkit-scrollbar-thumb{background:var(--cli-panel-text-secondary, rgba(255, 255, 255, .2));border-radius:3px}.services-dropdown-list::-webkit-scrollbar-thumb:hover{background:var(--cli-panel-text-secondary, rgba(255, 255, 255, .35))}.services-dropdown-list{scrollbar-width:thin;scrollbar-color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .2)) transparent}.services-dropdown-item{display:flex;align-items:flex-start;gap:10px;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.04);transition:background-color .15s ease}.services-dropdown-item:last-child{border-bottom:none}.services-dropdown-item:hover{background-color:var(--cli-btn-hover-bg, rgba(129, 140, 248, .08))}.svc-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0;margin-top:5px}.svc-dot.svc-running{background:var(--cli-status-idle, #3fb950)}.svc-dot.svc-stopped{background:var(--cli-panel-text-secondary, rgba(255, 255, 255, .3))}.svc-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.svc-name{font-size:13px;font-weight:500;color:var(--cli-panel-text, rgba(255, 255, 255, .87));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.svc-desc{font-size:11px;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .5));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.svc-status{font-size:11px;flex-shrink:0;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .5));margin-top:1px}.srv-meta{display:flex;flex-direction:column;align-items:flex-end;gap:2px;flex-shrink:0}.services-dropdown-empty{padding:16px 14px;font-size:12px;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .4));text-align:center}.glow-line{position:absolute;bottom:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--cli-status-glow, var(--cli-panel-accent, #818cf8)),#a78bfa,var(--cli-status-glow, var(--cli-panel-accent, #818cf8)),transparent);background-size:200% 100%;opacity:.4;animation:glowShift 8s ease-in-out infinite;transition:opacity .6s ease;pointer-events:none}.glow-line.glow-active{opacity:.8;animation-duration:3s}.status-indicators-compact{display:none}.terminal-wrapper[data-position=left] .status-indicators-compact,.terminal-wrapper[data-position=right] .status-indicators-compact{display:flex;flex-direction:column;align-items:center;gap:10px;padding:12px 0}.compact-item{display:flex;align-items:center;justify-content:center;width:20px;height:20px;cursor:default}.compact-item.status-clickable{cursor:pointer;border-radius:4px}.compact-item.status-clickable:hover{background-color:var(--cli-btn-hover-bg, rgba(129, 140, 248, .12))}.compact-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.compact-dot.level-info{background:var(--cli-panel-accent, #818cf8)}.compact-dot.level-success{background:var(--cli-status-idle, #3fb950)}.compact-dot.level-warn{background:var(--cli-status-running, #f0883e)}.compact-dot.level-error{background:var(--cli-status-error, #f85149)}.terminal-wrapper[data-position=left] .glow-line{inset:0 0 auto auto;width:2px;height:100%;background:linear-gradient(180deg,transparent,var(--cli-status-glow, var(--cli-panel-accent, #818cf8)),#a78bfa,var(--cli-status-glow, var(--cli-panel-accent, #818cf8)),transparent);background-size:100% 200%}.terminal-wrapper[data-position=right] .glow-line{inset:0 auto auto 0;width:2px;height:100%;background:linear-gradient(180deg,transparent,var(--cli-status-glow, var(--cli-panel-accent, #818cf8)),#a78bfa,var(--cli-status-glow, var(--cli-panel-accent, #818cf8)),transparent);background-size:100% 200%}@keyframes glowShift{0%{background-position:200% center}to{background-position:-200% center}}@keyframes statusPulse{0%,to{opacity:1}50%{opacity:.3}}@media(prefers-reduced-motion:reduce){.glow-line,.status-dot{animation:none!important}}\n"] }]
}], propDecorators: { visible: [{
type: Input
}], isCollapsed: [{
type: Input
}], isMaximized: [{
type: Input
}], position: [{
type: Input
}], closable: [{
type: Input
}], resizable: [{
type: Input
}], hideable: [{
type: Input
}], hideAlignment: [{
type: Input
}], themeStyles: [{
type: Input
}], onToggle: [{
type: Output
}], onContentSizeChange: [{
type: Output
}], onClose: [{
type: Output
}], onHide: [{
type: Output
}], onPositionChange: [{
type: Output
}], isHidden: [{
type: Input
}], statusExecutionState: [{
type: Input
}], statusLastCommand: [{
type: Input
}], statusServiceCount: [{
type: Input
}], statusServiceDetails: [{
type: Input
}], statusServerState: [{
type: Input
}], statusServerDetails: [{
type: Input
}], statusUptime: [{
type: Input
}], notification: [{
type: Input
}], closeDropdowns: [{
type: HostListener,
args: ['document:click']
}], onMouseMove: [{
type: HostListener,
args: ['document:mousemove', ['$event']]
}], onMouseUp: [{
type: HostListener,
args: ['document:mouseup']
}] } });
/** Slow poll for uptime display only. */
const UPTIME_POLL_MS = 2000;
/** Fast poll to catch execution state changes. */
const EXEC_POLL_MS = 300;
const DEFAULT_TAB_STATUS = {
executionState: 'idle',
lastCommandStatus: null,
lastCommandName: null,
notification: null,
};
const DEFAULT_GLOBAL_STATUS = {
runningServiceCount: 0,
totalServiceCount: 0,
serviceDetails: [],
serverConnectionState: 'none',
serverDetails: [],
uptime: 0,
};
class CliPanelStatusService {
constructor() {
this.tabs = new Map();
this.globalStatus$ = new BehaviorSubject(DEFAULT_GLOBAL_STATUS);
this.destroy$ = new Subject();
/** Push to trigger an immediate global status recalculation. */
this.globalRefresh$ = new Subject();
/** Cleanup functions for background-service event listeners. */
this.bgUnsubscribes = [];
this.globalPollingStarted = false;
}
/**
* Register an engine for a specific pane within a tab.
*/
registerEngine(tabId, paneId, engine) {
let entry = this.tabs.get(tabId);
if (!entry) {
entry = {
engines: new Map(),
status$: new BehaviorSubject(DEFAULT_TAB_STATUS),
destroy$: new Subject(),
refresh$: new Subject(),
};
this.tabs.set(tabId, entry);
this.startPollingTab(entry);
}
entry.engines.set(paneId, engine);
// Listen to notifier changes for immediate tab refresh
const context = engine.getContext();
if (context?.notifier?.change$) {
context.notifier.change$.pipe(takeUntil(entry.destroy$), takeUntil(this.destroy$)).subscribe(() => {
entry.refresh$.next();
});
}
// Listen to background service events for immediate global refresh
this.listenToBackgroundServices(engine);
if (!this.globalPollingStarted) {
this.globalPollingStarted = true;
this.startGlobalPolling();
}
}
/**
* Unregister all engines for a tab (called on tab close).
*/
unregisterTab(tabId) {
const entry = this.tabs.get(tabId);
if (entry) {
entry.destroy$.next();
entry.destroy$.complete();
this.tabs.delete(tabId);
}
}
/**
* Set the active tab for global status derivation.
*/
setActiveTab(tabId) {
this.activeTabId = tabId;
this.globalRefresh$.next();
}
/**
* Observable of per-tab status.
*/
getTabStatus$(tabId) {
const entry = this.tabs.get(tabId);
return entry ? entry.status$.asObservable() : new BehaviorSubject(DEFAULT_TAB_STATUS).asObservable();
}
/**
* Observable of global status (from active tab).
*/
getGlobalStatus$() {
return this.globalStatus$.asObservable();
}
/**
* Clean up all subscriptions.
*/
destroy() {
this.destroy$.next();
this.destroy$.complete();
for (const entry of this.tabs.values()) {
entry.destroy$.next();
entry.destroy$.complete();
}
this.tabs.clear();
for (const unsub of this.bgUnsubscribes) {
unsub();
}
this.bgUnsubscribes.length = 0;
}
/**
* Hook into an engine's background service events so that
* service start/stop/status-change triggers an immediate refresh.
*/
listenToBackgroundServices(engine) {
const context = engine.getContext();
if (!context?.backgroundServices)
return;
try {
const unsub = context.backgroundServices.on(() => {
this.globalRefresh$.next();
});
this.bgUnsubscribes.push(unsub);
}
catch { /* not available */ }
}
startPollingTab(entry) {
merge(interval(EXEC_POLL_MS).pipe(startWith(0)), entry.refresh$).pipe(map(() => this.computeTabStatus(entry)), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), takeUntil(entry.destroy$), takeUntil(this.destroy$)).subscribe(status => {
entry.status$.next(status);
});
}
startGlobalPolling() {
// Merge: slow timer for uptime ticking + immediate refresh signals
merge(interval(UPTIME_POLL_MS).pipe(startWith(0)), this.globalRefresh$).pipe(map(() => this.computeGlobalStatus()), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), takeUntil(this.destroy$)).subscribe(status => {
this.globalStatus$.next(status);
});
}
computeTabStatus(entry) {
let running = false;
let latestResult;
let notification = null;
for (const engine of entry.engines.values()) {
const context = engine.getContext();
if (!context)
continue;
// Check if a command is currently executing or a context processor
// (raw/fullscreen mode, e.g. games) is active
if (context.isExecuting || context.contextProcessor) {
running = true;
}
// Check last command result
const result = context.lastCommandResult;
if (result) {
latestResult = result;
}
// Pick up notification from processors
const notif = context.notifier?.current;
if (notif) {
notification = notif;
}
}
return {
executionState: running ? 'running' : 'idle',
lastCommandStatus: latestResult ? (latestResult.success ? 'success' : 'error') : null,
lastCommandName: latestResult?.command ?? null,
notification,
};
}
computeGlobalStatus() {
if (this.activeTabId == null)
return DEFAULT_GLOBAL_STATUS;
const entry = this.tabs.get(this.activeTabId);
if (!entry)
return DEFAULT_GLOBAL_STATUS;
// Use the first engine in the active tab for global data
const engine = entry.engines.values().next().value;
if (!engine)
return DEFAULT_GLOBAL_STATUS;
const context = engine.getContext();
if (!context)
return DEFAULT_GLOBAL_STATUS;
// Background services
let runningServiceCount = 0;
let totalServiceCount = 0;
let serviceDetails = [];
try {
const services = context.backgroundServices?.list() ?? [];
totalServiceCount = services.length;
runningServiceCount = services.filter((s) => s.status === 'running').length;
serviceDetails = services.map((s) => ({
name: s.name,
status: s.status,
description: s.description,
}));
}
catch { /* not available */ }
// Server connection
let serverConnectionState = 'none';
const serverDetails = [];
try {
const serverManager = context.services?.get?.('cli-server-manager');
if (serverManager?.connections?.size > 0) {
let anyConnected = false;
for (const [name, conn] of serverManager.connections) {
if (conn.connected) {
anyConnected = true;
}
serverDetails.push({
name,
url: conn.config?.url ?? '',
connected: !!conn.connected,
apiVersion: conn.connected ? conn.apiVersion : undefined,
commandCount: conn.connected ? conn.commands?.length : undefined,
});
}
serverConnectionState = anyConnected ? 'connected' : 'disconnected';
}
else {
// Fall back to configured servers from engine options
const configuredServers = engine.options?.servers;
if (Array.isArray(configuredServers) && configuredServers.length > 0) {
for (const srv of configuredServers) {
if (srv.enabled === false)
continue;
serverDetails.push({
name: srv.name,
url: srv.url ?? '',
connected: false,
});
}
serverConnectionState = 'disconnected';
}
}
}
catch { /* not available */ }
// Uptime
const uptime = engine.startedAt ? Date.now() - engine.startedAt : 0;
return { runningServiceCount, totalServiceCount, serviceDetails, serverConnectionState, serverDetails, uptime };
}
}
/**
* A component that displays the CLI on the bottom of page.
*/
class CliPanelComponent {
get resolvedCollapsed() {
return this.collapsed !== undefined ? this.collapsed : this._internalCollapsed;
}
get resolvedHidden() {
return this.hidden !== undefined ? this.hidden : this._internalHidden;
}
get resolvedMaximized() {
return this.maximized !== undefined ? this.maximized : this._internalMaximized;
}
get resolvedActiveTabId() {
return this.activeTabId !== undefined ? this.activeTabId : this._internalActiveTabId;
}
get resolvedHeight() {
return this.height !== undefined ? this.height : this._internalHeight;
}
get resolvedWidth() {
return this.width !== undefined ? this.width : this._internalWidth;
}
get resolvedPosition() {
return this.position !== undefined ? this.position : this.currentPosition;
}
static { this.MIN_PANE_WIDTH_PERCENT = 10; }
constructor(elementRef) {
this.elementRef = elementRef;
this.onClose = new EventEmitter();
this.collapsedChange = new EventEmitter();
this.hiddenChange = new EventEmitter();
this.maximizedChange = new EventEmitter();
this.activeTabIdChange = new EventEmitter();
this.positionChange = new EventEmitter();
this.heightChange = new EventEmitter();
this.widthChange = new EventEmitter();
// ── Structural events ──
this.onTabAdded = new EventEmitter();
this.onTabClosed = new EventEmitter();
this.onPaneSplit = new EventEmitter();
this.onPaneClosed = new EventEmitter();
this.currentPosition = 'bottom';
this.themeStyles = {};
this.visible = true;
this.tabs = [];
this.tabStatuses = {};
this.activePaneId = 0;
this._internalActiveTabId = 0;
this._internalCollapsed = true;
this._internalHidden = false;
this._internalMaximized = false;
this._internalHeight = 600;
this._internalWidth = 400;
this.nextTabId = 1;
this.nextPaneId = 1;
this.statusService = new CliPanelStatusService();
this.destroy$ = new Subject();
this.globalStatus = {
runningServiceCount: 0,
totalServiceCount: 0,
serviceDetails: [],
serverConnectionState: 'none',
serverDetails: [],
uptime: 0,
};
this.contextMenu = {
visible: false,
x: 0,
y: 0,
tabId: 0,
};
this.paneResizing = false;
this.paneResizeTabId = 0;
this.paneResizeDividerIndex = 0;
this.paneResizeStartX = 0;
this.paneResizeStartWidths = [];
this.paneResizeContainerWidth = 0;
this.terminalHeight = '100%';
this.initialized = false;
}
ngOnInit() {
this.currentPosition = loadPanelPosition() ?? this.options?.position ?? 'bottom';
if (this.options?.isHidden != null) {
this._internalHidden = this.options.isHidden;
}
if (this.options?.isCollapsed != null) {
this._internalCollapsed = this.options.isCollapsed;
}
this.statusService.getGlobalStatus$().pipe(takeUntil(this.destroy$)).subscribe(status => {
this.globalStatus = status;
});
}
ngOnDestroy() {
this.themeObserver?.disconnect();
this.statusService.destroy();
this.destroy$.next();
this.destroy$.complete();
}
onDocumentClick(event) {
if (this.contextMenu.visible) {
this.closeContextMenu();
}
}
onEscapeKey() {
if (this.contextMenu.visible) {
this.closeContextMenu();
}
this.cancelAllEditing();
}
onPositionChange(position) {
this.currentPosition = position;
savePanelPosition(this.currentPosition);
this.positionChange.emit(position);
}
onToggle($event) {
this._internalCollapsed = $event;
this.collapsedChange.emit($event);
if (!$event && !this.initialized) {
this.initialized = true;
this.addTab();
this.setupThemeSync();
}
}
onContentSizeChange(_event) {
// Terminal height is handled via CSS flex layout
}
// --- Tab management ---
addTab(title) {
const pane = { id: this.nextPaneId++, widthPercent: 100 };
const tabId = this.nextTabId++;
const tab = {
id: tabId,
title: title ?? `Terminal ${tabId}`,
isEditing: false,
panes: [pane],
};
this.tabs.push(tab);
this._internalActiveTabId = tabId;
this.activeTabIdChange.emit(tabId);
this.activePaneId = pane.id;
this.statusService.setActiveTab(tabId);
this.onTabAdded.emit({ tabId });
return tabId;
}
closeTab(id) {
const index = this.tabs.findIndex((t) => t.id === id);
if (index === -1)
return;
this.tabs.splice(index, 1);
this.onTabClosed.emit({ tabId: id });
this.statusService.unregisterTab(id);
delete this.tabStatuses[id];
if (this.tabs.length === 0) {
this.initialized = false;
this.collapsableContent?.toggleTerminal();
return;
}
if (this.resolvedActiveTabId === id) {
const nextIndex = Math.min(index, this.tabs.length - 1);
this.selectTab(this.tabs[nextIndex].id);
}
}
selectTab(id) {
this._internalActiveTabId = id;
this.activeTabIdChange.emit(id);
this.statusService.setActiveTab(id);
const tab = this.findTab(id);
if (tab && tab.panes.length > 0) {
this.activePaneId = tab.panes[0].id;
}
setTimeout(() => {
this.focusActiveTerminal();
});
}
trackByTabId(_index, tab) {
return tab.id;
}
getTabDotClass(tabId) {
const status = this.tabStatuses[tabId];
if (!status)
return 'dot-idle';
if (status.executionState === 'running')
return 'dot-running';
if (status.lastCommandStatus === 'error')
return 'dot-error';
return 'dot-idle';
}
onPaneEngineReady(tabId, paneId, engine) {
this.statusService.registerEngine(tabId, paneId, engine);
this.statusService.getTabStatus$(tabId).pipe(takeUntil(this.destroy$)).subscribe(status => {
this.tabStatuses[tabId] = status;
});
}
trackByPaneId(_index, pane) {
return pane.id;
}
// --- Inline rename ---
startRename(tab) {
this.cancelAllEditing();
tab.isEditing = true;
setTimeout(() => {
const input = this.elementRef.nativeElement.querySelector('.tab-rename-input');
if (input) {
input.focus();
input.select();
}
});
}
onTabDoubleClick(tab) {
this.startRename(tab);
}
commitRename(tab, value) {
const trimmed = value.trim();
if (trimmed) {
tab.title = trimmed;
}
tab.isEditing = false;
}
onRenameKeydown(event, tab) {
if (event.key === 'Enter') {
this.commitRename(tab, event.target.value);
}
else if (event.key === 'Escape') {
tab.isEditing = false;
}
}
onRenameBlur(event, tab) {
if (tab.isEditing) {
this.commitRename(tab, event.target.value);
}
}
// --- Context menu ---
onTabContextMenu(event, tab) {
event.preventDefault();
event.stopPropagation();
this.contextMenu = {
visible: true,
x: event.clientX,
y: event.clientY,
tabId: tab.id,
};
}
closeContextMenu() {
this.contextMenu = { ...this.contextMenu, visible: false };
}
contextMenuRename() {
const tab = this.findTab(this.contextMenu.tabId);
this.closeContextMenu();
if (tab) {
this.startRename(tab);
}
}
contextMenuDuplicate() {
const sourceTab = this.findTab(this.contextMenu.tabId);
this.closeContextMenu();
if (sourceTab) {
const snapshot = this.getEngineForTab(sourceTab)?.snapshot();
const pane = {
id: this.nextPaneId++,
widthPercent: 100,
snapshot,
};
const tab = {
id: this.nextTabId++,
title: `${sourceTab.title} (copy)`,
isEditing: false,
panes: [pane],
};
const sourceIndex = this.tabs.indexOf(sourceTab);
this.tabs.splice(sourceIndex + 1, 0, tab);
this._internalActiveTabId = tab.id;
this.activeTabIdChange.emit(tab.id);
this.activePaneId = pane.id;
}
}
contextMenuClose() {
const id = this.contextMenu.tabId;
this.closeContextMenu();
this.closeTab(id);
}
contextMenuCloseOthers() {
const id = this.contextMenu.tabId;
this.closeContextMenu();
this.tabs = this.tabs.filter((t) => t.id === id);
this._internalActiveTabId = id;
this.activeTabIdChange.emit(id);
}
contextMenuCloseToTheRight() {
const id = this.contextMenu.tabId;
this.closeContextMenu();
const index = this.tabs.findIndex((t) => t.id === id);
if (index === -1)
return;
this.tabs = this.tabs.slice(0, index + 1);
if (!this.tabs.find((t) => t.id === this.resolvedActiveTabId)) {
this._internalActiveTabId = id;
this.activeTabIdChange.emit(id);
}
}
contextMenuCloseAll() {
this.closeContextMenu();
this.tabs = [];
this.initialized = false;
this.collapsableContent?.toggleTerminal();
}
// --- Split / close pane ---
splitPane(tabId) {
const targetTabId = tabId ?? this.resolvedActiveTabId;
const tab = this.findTab(targetTabId);
if (!tab)
return -1;
const paneId = this.nextPaneId++;
const newPane = { id: paneId, widthPercent: 0 };
tab.panes.push(newPane);
const evenWidth = 100 / tab.panes.length;
tab.panes.forEach((p) => (p.widthPercent = evenWidth));
this.normalizePaneWidths(tab.panes);
this.activePaneId = paneId;
this.onPaneSplit.emit({ paneId, tabId: targetTabId });
return paneId;
}
closePane(paneId) {
for (const tab of this.tabs) {
const idx = tab.panes.findIndex((p) => p.id === paneId);
if (idx === -1)
continue;
if (tab.panes.length <= 1) {
this.closeTab(tab.id);
return;
}
// Destroy engine at the flat index
let flatIndex = 0;
for (const t of this.tabs) {
for (const p of t.panes) {
if (p.id === paneId) {
const engine = this.cliComponents?.toArray()[flatIndex]?.getEngine();
if (engine)
engine.destroy();
}
flatIndex++;
}
}
tab.panes.splice(idx, 1);
this.normalizePaneWidths(tab.panes);
if (this.activePaneId === paneId) {
this.activePaneId = tab.panes[Math.min(idx, tab.panes.length - 1)].id;
}
this.onPaneClosed.emit({ paneId });
setTimeout(() => this.focusActiveTerminal());
return;
}
}
contextMenuSplitRight() {
const tabId = this.contextMenu.tabId;
this.closeContextMenu();
this.splitPane(tabId);
}
// --- Pane resize ---
onPaneResizeStart(event, tabId, dividerIndex) {
event.preventDefault();
const tab = this.findTab(tabId);
if (!tab)
return;
this.paneResizing = true;
this.paneResizeTabId = tabId;
this.paneResizeDividerIndex = dividerIndex;
this.paneResizeStartX = event.clientX;
this.paneResizeStartWidths = tab.panes.map((p) => p.widthPercent);
const container = event.target.closest('.terminal-panes-container');
this.paneResizeContainerWidth = container ? container.clientWidth : 1;
document.body.classList.add('cli-pane-resizing');
}
onPaneResizeMove(event) {
if (!this.paneResizing)
return;
const tab = this.findTab(this.paneResizeTabId);
if (!tab)
return;
const deltaX = event.clientX - this.paneResizeStartX;
const deltaPct = (deltaX / this.paneResizeContainerWidth) * 100;
const i = this.paneResizeDividerIndex;
const minW = CliPanelComponent.MIN_PANE_WIDTH_PERCENT;
let leftWidth = this.paneResizeStartWidths[i] + deltaPct;
let rightWidth = this.paneResizeStartWidths[i + 1] - deltaPct;
if (leftWidth < minW) {
leftWidth = minW;
rightWidth =
this.paneResizeStartWidths[i] +
this.paneResizeStartWidths[i + 1] -
minW;
}
if (rightWidth < minW) {
rightWidth = minW;
leftWidth =
this.paneResizeStartWidths[i] +
this.paneResizeStartWidths[i + 1] -
minW;
}
tab.panes[i].widthPercent = leftWidth;
tab.panes[i + 1].widthPercent = rightWidth;
}
onPaneResizeEnd() {
if (!this.paneResizing)
return;
this.paneResizing = false;
document.body.classList.remove('cli-pane-resizing');
}
// --- Pane focus ---
onPaneClick(tabId, paneId) {
this.activePaneId = paneId;
if (this.resolvedActiveTabId !== tabId) {
this._internalActiveTabId = tabId;
this.activeTabIdChange.emit(tabId);
}
setTimeout(() => this.focusPane(tabId, paneId));
}
// --- ICliPanelRef methods ---
open() {
if (!this.visible) {
this.visible = true;
}
if (this.resolvedHidden) {
this._internalHidden = false;
this.hiddenChange.emit(false);
}
if (this.resolvedCollapsed) {
this._internalCollapsed = false;
this.collapsedChange.emit(false);
this.collapsableContent?.setCollapsed(false);
if (!this.initialized) {
this.initialized = true;
this.addTab();
this.setupThemeSync();
}
}
}
collapse() {
if (!this.resolvedCollapsed) {
this._internalCollapsed = true;
this.collapsedChange.emit(true);
this.collapsableContent?.setCollapsed(true);
}
}
toggleCollapse() {
if (this.resolvedCollapsed) {
this.open();
}
else {
this.collapse();
}
}
hide() {
if (!this.resolvedHidden) {
this._internalHidden = true;
this.hiddenChange.emit(true);
this.collapsableContent?.hideTerminal();
}
}
unhide() {
if (this.resolvedHidden) {
this._internalHidden = false;
this.hiddenChange.emit(false);
this.collapsableContent?.unhideTerminal();
}
}
toggleHide() {
if (this.resolvedHidden) {
this.unhide();
}
else {
this.hide();
}
}
close() {
this.visible = false;
this.onClose.emit();
}
maximize() {
if (!this.resolvedMaximized) {
this._internalMaximized = true;
this.maximizedChange.emit(true);
this.collapsableContent?.setMaximized(true);
}
}
restore() {
if (this.resolvedMaximized) {
this._internalMaximized = false;
this.maximizedChange.emit(false);
this.collapsableContent?.setMaximized(false);
}
}
toggleMaximize() {
if (this.resolvedMaximized) {
this.restore();
}
else {
this.maximize();
}
}
resize(dimensions) {
if (dimensions.height !== undefined) {
this._internalHeight = dimensions.height;
this.heightChange.emit(dimensions.height);
}
if (dimensions.width !== undefined) {
this._internalWidth = dimensions.width;
this.widthChange.emit(dimensions.width);
}
this.collapsableContent?.setDimensions(dimensions);
}
setPosition(pos) {
this.currentPosition = pos;
savePanelPosition(pos);
this.positionChange.emit(pos);
}
renameTab(tabId, title) {
const tab = this.findTab(tabId);
if (tab) {
tab.title = title;
}
}
getEngine(paneId) {
const targetPaneId = paneId ?? this.activePaneId;
let flatIndex = 0;
for (const tab of this.tabs) {
for (const pane of tab.panes) {
if (pane.id === targetPaneId) {
const components = this.cliComponents?.toArray();
return components?.[flatIndex]?.getEngine();
}
flatIndex++;
}
}
return undefined;
}
getState() {
return {
collapsed: this.resolvedCollapsed,
hidden: this.resolvedHidden,
maximized: this.resolvedMaximized,
position: this.resolvedPosition,
height: this.resolvedHeight,
width: this.resolvedWidth,
activeTabId: this.resolvedActiveTabId,
activePaneId: this.activePaneId,
tabs: this.tabs.map((t) => ({
id: t.id,
title: t.title,
panes: t.panes.map((p) => ({ id: p.id, widthPercent: p.widthPercent })),
})),
};
}
// --- Helpers ---
findTab(id) {
return this.tabs.find((t) => t.id === id);
}
getEngineForTab(tab) {
if (!this.cliComponents)
return undefined;
let flatIndex = 0;
for (const t of this.tabs) {
for (const _pane of t.panes) {
if (t.id === tab.id) {
const component = this.cliComponents.toArray()[flatIndex];
return component?.getEngine();
}
flatIndex++;
}
}
return undefined;
}
cancelAllEditing() {
this.tabs.forEach((t) => (t.isEditing = false));
}
focusActiveTerminal() {
this.focusPane(this.resolvedActiveTabId, this.activePaneId);
}
focusPane(tabId, paneId) {
if (!this.cliComponents)
return;
let flatIndex = 0;
for (const tab of this.tabs) {
for (const pane of tab.panes) {
if (tab.id === tabId && pane.id === paneId) {
const component = this.cliComponents.toArray()[flatIndex];
component?.focus();
return;
}
flatIndex++;
}
}
}
normalizePaneWidths(panes) {
const total = panes.reduce((s, p) => s + p.widthPercent, 0);
if (total === 0)
return;
const scale = 100 / total;
panes.forEach((p) => (p.widthPercent = p.widthPercent * scale));
}
setupThemeSync() {
if (!this.options?.syncTheme)
return;
// Wait for the first terminal to render, then observe style changes
setTimeout(() => {
this.syncThemeFromEngine();
const container = this.elementRef.nativeElement.querySelector('.terminal-container');
if (!container)
return;
this.themeObserver?.disconnect();
this.themeObserver = new MutationObserver(() => {
this.syncThemeFromEngine();
});
this.themeObserver.observe(container, {
attributes: true,
attributeFilter: ['style'],
});
}, 200);
}
syncThemeFromEngine() {
if (!this.cliComponents)
return;
const first = this.cliComponents.first;
if (!first)
return;
const engine = first.getEngine();
if (!engine)
return;
const theme = engine.getTerminal().options.theme;
if (!theme)
return;
this.themeStyles = derivePanelThemeStyles(theme);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CliPanelComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: CliPanelComponent, isStandalone: false, selector: "cli-panel", inputs: { options: "options", modules: "modules", processors: "processors", collapsed: "collapsed", hidden: "hidden", maximized: "maximized", activeTabId: "activeTabId", position: "position", height: "height", width: "width" }, outputs: { onClose: "onClose", collapsedChange: "collapsedChange", hiddenChange: "hiddenChange", maximizedChange: "maximizedChange", activeTabIdChange: "activeTabIdChange", positionChange: "positionChange", heightChange: "heightChange", widthChange: "widthChange", onTabAdded: "onTabAdded", onTabClosed: "onTabClosed", onPaneSplit: "onPaneSplit", onPaneClosed: "onPaneClosed" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscapeKey()", "document:mousemove": "onPaneResizeMove($event)", "document:mouseup": "onPaneResizeEnd()" } }, viewQueries: [{ propertyName: "collapsableContent", first: true, predicate: CollapsableContentComponent, descendants: true }, { propertyName: "cliComponents", predicate: CliComponent, descendants: true }], ngImport: i0, template: "<collapsable-content\n [isCollapsed]=\"resolvedCollapsed\"\n [isHidden]=\"resolvedHidden\"\n [isMaximized]=\"resolvedMaximized\"\n [position]=\"resolvedPosition\"\n [closable]=\"options?.closable ?? true\"\n [resizable]=\"options?.resizable ?? true\"\n [hideable]=\"options?.hideable ?? true\"\n [hideAlignment]=\"options?.hideAlignment ?? 'center'\"\n [themeStyles]=\"themeStyles\"\n [statusExecutionState]=\"tabStatuses[resolvedActiveTabId]?.executionState || 'idle'\"\n [statusLastCommand]=\"tabStatuses[resolvedActiveTabId]?.lastCommandStatus ? { name: tabStatuses[resolvedActiveTabId]?.lastCommandName || '', success: tabStatuses[resolvedActiveTabId]?.lastCommandStatus === 'success' } : null\"\n [statusServiceCount]=\"{ running: globalStatus.runningServiceCount, total: globalStatus.totalServiceCount }\"\n [statusServiceDetails]=\"globalStatus.serviceDetails\"\n [statusServerState]=\"globalStatus.serverConnectionState\"\n [statusServerDetails]=\"globalStatus.serverDetails\"\n [statusUptime]=\"globalStatus.uptime\"\n [notification]=\"tabStatuses[resolvedActiveTabId]?.notification || null\"\n (onToggle)=\"onToggle($event)\"\n (onContentSizeChange)=\"onContentSizeChange($event)\"\n (onPositionChange)=\"onPositionChange($event)\"\n (onClose)=\"onClose.emit()\"\n *ngIf=\"visible\"\n>\n <ng-container *ngIf=\"initialized\">\n <div class=\"terminal-tabs\">\n <ul class=\"tab-list\">\n <li\n *ngFor=\"let tab of tabs; trackBy: trackByTabId\"\n class=\"tab\"\n [class.active]=\"tab.id === resolvedActiveTabId\"\n (click)=\"selectTab(tab.id)\"\n (dblclick)=\"onTabDoubleClick(tab)\"\n (contextmenu)=\"onTabContextMenu($event, tab)\"\n >\n <input\n *ngIf=\"tab.isEditing\"\n class=\"tab-rename-input\"\n type=\"text\"\n [value]=\"tab.title\"\n (keydown)=\"onRenameKeydown($event, tab)\"\n (blur)=\"onRenameBlur($event, tab)\"\n (click)=\"$event.stopPropagation()\"\n (dblclick)=\"$event.stopPropagation()\"\n #renameInput\n />\n <span *ngIf=\"!tab.isEditing\" class=\"tab-dot\" [ngClass]=\"getTabDotClass(tab.id)\"></span>\n <span *ngIf=\"!tab.isEditing\" class=\"tab-title\">{{ tab.title }}</span>\n <button\n *ngIf=\"!tab.isEditing\"\n class=\"close-btn\"\n title=\"Close tab\"\n (click)=\"closeTab(tab.id); $event.stopPropagation()\"\n >\n ×\n </button>\n </li>\n </ul>\n <button class=\"add-tab\" title=\"New terminal\" (click)=\"addTab()\">+</button>\n </div>\n <div class=\"terminal-instances\">\n <div\n *ngFor=\"let tab of tabs; trackBy: trackByTabId\"\n class=\"terminal-instance\"\n [hidden]=\"tab.id !== resolvedActiveTabId\"\n >\n <div\n class=\"terminal-panes-container\"\n [attr.data-tab-id]=\"tab.id\"\n [class.resizing]=\"paneResizing\"\n >\n <ng-container\n *ngFor=\"\n let pane of tab.panes;\n let i = index;\n trackBy: trackByPaneId\n \"\n >\n <div\n *ngIf=\"i > 0\"\n class=\"pane-divider\"\n (mousedown)=\"onPaneResizeStart($event, tab.id, i - 1)\"\n >\n <div class=\"pane-divider-grip\"></div>\n </div>\n <div\n class=\"terminal-pane\"\n [class.active-pane]=\"\n pane.id === activePaneId && tab.id === resolvedActiveTabId\n \"\n [style.flex]=\"pane.widthPercent + ' 1 0'\"\n (click)=\"onPaneClick(tab.id, pane.id)\"\n >\n <button\n *ngIf=\"tab.panes.length > 1\"\n class=\"pane-close-btn\"\n (click)=\"closePane(pane.id); $event.stopPropagation()\"\n title=\"Close pane\"\n >\n ×\n </button>\n <cli\n [options]=\"options\"\n [height]=\"terminalHeight\"\n [modules]=\"modules\"\n [processors]=\"processors\"\n [snapshot]=\"pane.snapshot\"\n (engineReady)=\"onPaneEngineReady(tab.id, pane.id, $event)\"\n />\n </div>\n </ng-container>\n </div>\n </div>\n </div>\n </ng-container>\n</collapsable-content>\n\n<!-- Context menu rendered outside collapsable-content to escape transform/overflow clipping -->\n<div\n class=\"tab-context-menu\"\n *ngIf=\"contextMenu.visible\"\n [style.left.px]=\"contextMenu.x\"\n [style.top.px]=\"contextMenu.y\"\n (click)=\"$event.stopPropagation()\"\n>\n <button class=\"context-menu-item\" (click)=\"contextMenuRename()\">\n Rename\n </button>\n <button class=\"context-menu-item\" (click)=\"contextMenuDuplicate()\">\n Duplicate\n </button>\n <button class=\"context-menu-item\" (click)=\"contextMenuSplitRight()\">\n Split Right\n </button>\n <div class=\"context-menu-separator\"></div>\n <button class=\"context-menu-item\" (click)=\"contextMenuClose()\">Close</button>\n <button\n class=\"context-menu-item\"\n (click)=\"contextMenuCloseOthers()\"\n [disabled]=\"tabs.length <= 1\"\n >\n Close Others\n </button>\n <button class=\"context-menu-item\" (click)=\"contextMenuCloseToTheRight()\">\n Close to the Right\n </button>\n <div class=\"context-menu-separator\"></div>\n <button class=\"context-menu-item destructive\" (click)=\"contextMenuCloseAll()\">\n Close All\n </button>\n</div>\n", styles: [".terminal-tabs{background-color:var(--cli-panel-header-bg, #1f2937);display:flex;align-items:center;height:38px;padding:0 8px;gap:4px;border-bottom:1px solid var(--cli-panel-border, #374151);overflow-x:auto;overflow-y:hidden;flex-shrink:0;scrollbar-width:thin;scrollbar-color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .3)) transparent}.terminal-tabs::-webkit-scrollbar{height:2px}.terminal-tabs::-webkit-scrollbar-thumb{background:var(--cli-panel-text-secondary, rgba(255, 255, 255, .3));border-radius:1px}.terminal-tabs .tab-list{list-style:none;margin:0;padding:0;display:flex;gap:2px;align-items:stretch;height:100%}.terminal-tabs .tab{display:flex;align-items:center;gap:6px;padding:0 12px;cursor:pointer;background:transparent;border:none;border-top:2px solid transparent;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .6));font-size:.8rem;font-family:inherit;white-space:nowrap;transition:background-color .15s ease,color .15s ease,border-color .15s ease;height:100%}.terminal-tabs .tab:hover:not(.active){background-color:var(--cli-btn-hover-bg, rgba(129, 140, 248, .12));color:var(--cli-panel-text, rgba(255, 255, 255, .87))}.terminal-tabs .tab.active{background-color:var(--cli-panel-bg, #111827);color:var(--cli-panel-text, rgba(255, 255, 255, .87));border-top-color:var(--cli-panel-accent, #818cf8)}.terminal-tabs .tab .tab-title{pointer-events:none}.terminal-tabs .tab .tab-rename-input{appearance:none;background:var(--cli-panel-bg, #111827);border:1px solid var(--cli-panel-accent, #818cf8);color:var(--cli-panel-text, rgba(255, 255, 255, .87));font-size:.8rem;font-family:inherit;padding:2px 6px;border-radius:3px;outline:none;width:120px;height:22px}.terminal-tabs .tab .close-btn{appearance:none;background:none;border:none;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .4));cursor:pointer;font-size:1rem;line-height:1;padding:0 2px;border-radius:3px;transition:background-color .15s ease,color .15s ease}.terminal-tabs .tab .close-btn:hover{color:var(--cli-panel-text, rgba(255, 255, 255, .87));background-color:#ffffff1a}.terminal-tabs .add-tab{appearance:none;background:transparent;border:1px dashed var(--cli-panel-text-secondary, rgba(255, 255, 255, .3));color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .5));cursor:pointer;font-size:1rem;line-height:1;width:28px;height:24px;border-radius:4px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background-color .15s ease,color .15s ease,border-color .15s ease}.terminal-tabs .add-tab:hover{background-color:var(--cli-btn-hover-bg, rgba(129, 140, 248, .12));color:var(--cli-panel-text, rgba(255, 255, 255, .87));border-color:var(--cli-panel-text, rgba(255, 255, 255, .5))}.tab-context-menu{position:fixed;z-index:1100;background-color:var(--cli-panel-header-bg, #1f2937);border:1px solid var(--cli-panel-border, #374151);border-radius:6px;padding:4px 0;min-width:180px;box-shadow:0 4px 16px #0006;font-family:inherit}.tab-context-menu .context-menu-item{appearance:none;background:transparent;border:none;color:var(--cli-panel-text, rgba(255, 255, 255, .87));cursor:pointer;display:block;width:100%;text-align:left;padding:6px 14px;font-size:.82rem;font-family:inherit;transition:background-color .1s ease}.tab-context-menu .context-menu-item:hover:not(:disabled){background-color:var(--cli-btn-hover-bg, rgba(129, 140, 248, .12))}.tab-context-menu .context-menu-item:disabled{color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .3));cursor:not-allowed}.tab-context-menu .context-menu-item.destructive{color:#f87171}.tab-context-menu .context-menu-item.destructive:hover:not(:disabled){background-color:#f871711f}.tab-context-menu .context-menu-separator{height:1px;background-color:var(--cli-panel-border, #374151);margin:4px 0}.terminal-instances{flex:1;min-height:0;overflow:hidden}.terminal-instance{height:100%}.terminal-panes-container{display:flex;height:100%}.terminal-panes-container.resizing{-webkit-user-select:none;user-select:none;cursor:ew-resize}.terminal-pane{position:relative;height:100%;min-width:0;overflow:hidden}.terminal-pane cli{display:block;height:100%}.terminal-pane.active-pane{outline:1px solid var(--cli-panel-accent, #818cf8);outline-offset:-1px}.terminal-pane:hover .pane-close-btn{opacity:1}.pane-close-btn{appearance:none;position:absolute;top:4px;right:4px;z-index:10;background:var(--cli-panel-header-bg, #1f2937);border:1px solid var(--cli-panel-border, #374151);color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .6));cursor:pointer;font-size:.9rem;line-height:1;width:22px;height:22px;border-radius:3px;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .15s ease,background-color .15s ease,color .15s ease}.pane-close-btn:hover{color:var(--cli-panel-text, rgba(255, 255, 255, .87));background-color:#ffffff1a}.pane-divider{width:6px;cursor:ew-resize;display:flex;align-items:center;justify-content:center;flex-shrink:0;background:transparent;transition:background-color .15s ease}.pane-divider:hover{background-color:var(--cli-panel-accent, #818cf8)}.pane-divider .pane-divider-grip{width:2px;height:24px;border-radius:1px;background-color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .3));transition:background-color .15s ease}.pane-divider:hover .pane-divider-grip{background-color:var(--cli-panel-text, rgba(255, 255, 255, .87))}.tab-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0;background:var(--cli-status-neutral, #6b7280)}.tab-dot.dot-idle{background:var(--cli-status-idle, #3fb950)}.tab-dot.dot-running{background:var(--cli-status-running, #f0883e);animation:statusPulse 1.5s ease-in-out infinite}.tab-dot.dot-error{background:var(--cli-status-error, #f85149)}.tab-dot.dot-neutral{background:var(--cli-status-neutral, #6b7280)}@keyframes statusPulse{0%,to{opacity:1}50%{opacity:.3}}@media(prefers-reduced-motion:reduce){.tab-dot{animation:none!important}}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: CliComponent, selector: "cli", inputs: ["options", "processors", "modules", "height", "snapshot"], outputs: ["engineReady"] }, { kind: "component", type: CollapsableContentComponent, selector: "collapsable-content", inputs: ["visible", "isCollapsed", "isMaximized", "position", "closable", "resizable", "hideable", "hideAlignment", "themeStyles", "isHidden", "statusExecutionState", "statusLastCommand", "statusServiceCount", "statusServiceDetails", "statusServerState", "statusServerDetails", "statusUptime", "notification"], outputs: ["onToggle", "onContentSizeChange", "onClose", "onHide", "onPositionChange"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CliPanelComponent, decorators: [{
type: Component,
args: [{ standalone: false, selector: 'cli-panel', template: "<collapsable-content\n [isCollapsed]=\"resolvedCollapsed\"\n [isHidden]=\"resolvedHidden\"\n [isMaximized]=\"resolvedMaximized\"\n [position]=\"resolvedPosition\"\n [closable]=\"options?.closable ?? true\"\n [resizable]=\"options?.resizable ?? true\"\n [hideable]=\"options?.hideable ?? true\"\n [hideAlignment]=\"options?.hideAlignment ?? 'center'\"\n [themeStyles]=\"themeStyles\"\n [statusExecutionState]=\"tabStatuses[resolvedActiveTabId]?.executionState || 'idle'\"\n [statusLastCommand]=\"tabStatuses[resolvedActiveTabId]?.lastCommandStatus ? { name: tabStatuses[resolvedActiveTabId]?.lastCommandName || '', success: tabStatuses[resolvedActiveTabId]?.lastCommandStatus === 'success' } : null\"\n [statusServiceCount]=\"{ running: globalStatus.runningServiceCount, total: globalStatus.totalServiceCount }\"\n [statusServiceDetails]=\"globalStatus.serviceDetails\"\n [statusServerState]=\"globalStatus.serverConnectionState\"\n [statusServerDetails]=\"globalStatus.serverDetails\"\n [statusUptime]=\"globalStatus.uptime\"\n [notification]=\"tabStatuses[resolvedActiveTabId]?.notification || null\"\n (onToggle)=\"onToggle($event)\"\n (onContentSizeChange)=\"onContentSizeChange($event)\"\n (onPositionChange)=\"onPositionChange($event)\"\n (onClose)=\"onClose.emit()\"\n *ngIf=\"visible\"\n>\n <ng-container *ngIf=\"initialized\">\n <div class=\"terminal-tabs\">\n <ul class=\"tab-list\">\n <li\n *ngFor=\"let tab of tabs; trackBy: trackByTabId\"\n class=\"tab\"\n [class.active]=\"tab.id === resolvedActiveTabId\"\n (click)=\"selectTab(tab.id)\"\n (dblclick)=\"onTabDoubleClick(tab)\"\n (contextmenu)=\"onTabContextMenu($event, tab)\"\n >\n <input\n *ngIf=\"tab.isEditing\"\n class=\"tab-rename-input\"\n type=\"text\"\n [value]=\"tab.title\"\n (keydown)=\"onRenameKeydown($event, tab)\"\n (blur)=\"onRenameBlur($event, tab)\"\n (click)=\"$event.stopPropagation()\"\n (dblclick)=\"$event.stopPropagation()\"\n #renameInput\n />\n <span *ngIf=\"!tab.isEditing\" class=\"tab-dot\" [ngClass]=\"getTabDotClass(tab.id)\"></span>\n <span *ngIf=\"!tab.isEditing\" class=\"tab-title\">{{ tab.title }}</span>\n <button\n *ngIf=\"!tab.isEditing\"\n class=\"close-btn\"\n title=\"Close tab\"\n (click)=\"closeTab(tab.id); $event.stopPropagation()\"\n >\n ×\n </button>\n </li>\n </ul>\n <button class=\"add-tab\" title=\"New terminal\" (click)=\"addTab()\">+</button>\n </div>\n <div class=\"terminal-instances\">\n <div\n *ngFor=\"let tab of tabs; trackBy: trackByTabId\"\n class=\"terminal-instance\"\n [hidden]=\"tab.id !== resolvedActiveTabId\"\n >\n <div\n class=\"terminal-panes-container\"\n [attr.data-tab-id]=\"tab.id\"\n [class.resizing]=\"paneResizing\"\n >\n <ng-container\n *ngFor=\"\n let pane of tab.panes;\n let i = index;\n trackBy: trackByPaneId\n \"\n >\n <div\n *ngIf=\"i > 0\"\n class=\"pane-divider\"\n (mousedown)=\"onPaneResizeStart($event, tab.id, i - 1)\"\n >\n <div class=\"pane-divider-grip\"></div>\n </div>\n <div\n class=\"terminal-pane\"\n [class.active-pane]=\"\n pane.id === activePaneId && tab.id === resolvedActiveTabId\n \"\n [style.flex]=\"pane.widthPercent + ' 1 0'\"\n (click)=\"onPaneClick(tab.id, pane.id)\"\n >\n <button\n *ngIf=\"tab.panes.length > 1\"\n class=\"pane-close-btn\"\n (click)=\"closePane(pane.id); $event.stopPropagation()\"\n title=\"Close pane\"\n >\n ×\n </button>\n <cli\n [options]=\"options\"\n [height]=\"terminalHeight\"\n [modules]=\"modules\"\n [processors]=\"processors\"\n [snapshot]=\"pane.snapshot\"\n (engineReady)=\"onPaneEngineReady(tab.id, pane.id, $event)\"\n />\n </div>\n </ng-container>\n </div>\n </div>\n </div>\n </ng-container>\n</collapsable-content>\n\n<!-- Context menu rendered outside collapsable-content to escape transform/overflow clipping -->\n<div\n class=\"tab-context-menu\"\n *ngIf=\"contextMenu.visible\"\n [style.left.px]=\"contextMenu.x\"\n [style.top.px]=\"contextMenu.y\"\n (click)=\"$event.stopPropagation()\"\n>\n <button class=\"context-menu-item\" (click)=\"contextMenuRename()\">\n Rename\n </button>\n <button class=\"context-menu-item\" (click)=\"contextMenuDuplicate()\">\n Duplicate\n </button>\n <button class=\"context-menu-item\" (click)=\"contextMenuSplitRight()\">\n Split Right\n </button>\n <div class=\"context-menu-separator\"></div>\n <button class=\"context-menu-item\" (click)=\"contextMenuClose()\">Close</button>\n <button\n class=\"context-menu-item\"\n (click)=\"contextMenuCloseOthers()\"\n [disabled]=\"tabs.length <= 1\"\n >\n Close Others\n </button>\n <button class=\"context-menu-item\" (click)=\"contextMenuCloseToTheRight()\">\n Close to the Right\n </button>\n <div class=\"context-menu-separator\"></div>\n <button class=\"context-menu-item destructive\" (click)=\"contextMenuCloseAll()\">\n Close All\n </button>\n</div>\n", styles: [".terminal-tabs{background-color:var(--cli-panel-header-bg, #1f2937);display:flex;align-items:center;height:38px;padding:0 8px;gap:4px;border-bottom:1px solid var(--cli-panel-border, #374151);overflow-x:auto;overflow-y:hidden;flex-shrink:0;scrollbar-width:thin;scrollbar-color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .3)) transparent}.terminal-tabs::-webkit-scrollbar{height:2px}.terminal-tabs::-webkit-scrollbar-thumb{background:var(--cli-panel-text-secondary, rgba(255, 255, 255, .3));border-radius:1px}.terminal-tabs .tab-list{list-style:none;margin:0;padding:0;display:flex;gap:2px;align-items:stretch;height:100%}.terminal-tabs .tab{display:flex;align-items:center;gap:6px;padding:0 12px;cursor:pointer;background:transparent;border:none;border-top:2px solid transparent;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .6));font-size:.8rem;font-family:inherit;white-space:nowrap;transition:background-color .15s ease,color .15s ease,border-color .15s ease;height:100%}.terminal-tabs .tab:hover:not(.active){background-color:var(--cli-btn-hover-bg, rgba(129, 140, 248, .12));color:var(--cli-panel-text, rgba(255, 255, 255, .87))}.terminal-tabs .tab.active{background-color:var(--cli-panel-bg, #111827);color:var(--cli-panel-text, rgba(255, 255, 255, .87));border-top-color:var(--cli-panel-accent, #818cf8)}.terminal-tabs .tab .tab-title{pointer-events:none}.terminal-tabs .tab .tab-rename-input{appearance:none;background:var(--cli-panel-bg, #111827);border:1px solid var(--cli-panel-accent, #818cf8);color:var(--cli-panel-text, rgba(255, 255, 255, .87));font-size:.8rem;font-family:inherit;padding:2px 6px;border-radius:3px;outline:none;width:120px;height:22px}.terminal-tabs .tab .close-btn{appearance:none;background:none;border:none;color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .4));cursor:pointer;font-size:1rem;line-height:1;padding:0 2px;border-radius:3px;transition:background-color .15s ease,color .15s ease}.terminal-tabs .tab .close-btn:hover{color:var(--cli-panel-text, rgba(255, 255, 255, .87));background-color:#ffffff1a}.terminal-tabs .add-tab{appearance:none;background:transparent;border:1px dashed var(--cli-panel-text-secondary, rgba(255, 255, 255, .3));color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .5));cursor:pointer;font-size:1rem;line-height:1;width:28px;height:24px;border-radius:4px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background-color .15s ease,color .15s ease,border-color .15s ease}.terminal-tabs .add-tab:hover{background-color:var(--cli-btn-hover-bg, rgba(129, 140, 248, .12));color:var(--cli-panel-text, rgba(255, 255, 255, .87));border-color:var(--cli-panel-text, rgba(255, 255, 255, .5))}.tab-context-menu{position:fixed;z-index:1100;background-color:var(--cli-panel-header-bg, #1f2937);border:1px solid var(--cli-panel-border, #374151);border-radius:6px;padding:4px 0;min-width:180px;box-shadow:0 4px 16px #0006;font-family:inherit}.tab-context-menu .context-menu-item{appearance:none;background:transparent;border:none;color:var(--cli-panel-text, rgba(255, 255, 255, .87));cursor:pointer;display:block;width:100%;text-align:left;padding:6px 14px;font-size:.82rem;font-family:inherit;transition:background-color .1s ease}.tab-context-menu .context-menu-item:hover:not(:disabled){background-color:var(--cli-btn-hover-bg, rgba(129, 140, 248, .12))}.tab-context-menu .context-menu-item:disabled{color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .3));cursor:not-allowed}.tab-context-menu .context-menu-item.destructive{color:#f87171}.tab-context-menu .context-menu-item.destructive:hover:not(:disabled){background-color:#f871711f}.tab-context-menu .context-menu-separator{height:1px;background-color:var(--cli-panel-border, #374151);margin:4px 0}.terminal-instances{flex:1;min-height:0;overflow:hidden}.terminal-instance{height:100%}.terminal-panes-container{display:flex;height:100%}.terminal-panes-container.resizing{-webkit-user-select:none;user-select:none;cursor:ew-resize}.terminal-pane{position:relative;height:100%;min-width:0;overflow:hidden}.terminal-pane cli{display:block;height:100%}.terminal-pane.active-pane{outline:1px solid var(--cli-panel-accent, #818cf8);outline-offset:-1px}.terminal-pane:hover .pane-close-btn{opacity:1}.pane-close-btn{appearance:none;position:absolute;top:4px;right:4px;z-index:10;background:var(--cli-panel-header-bg, #1f2937);border:1px solid var(--cli-panel-border, #374151);color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .6));cursor:pointer;font-size:.9rem;line-height:1;width:22px;height:22px;border-radius:3px;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .15s ease,background-color .15s ease,color .15s ease}.pane-close-btn:hover{color:var(--cli-panel-text, rgba(255, 255, 255, .87));background-color:#ffffff1a}.pane-divider{width:6px;cursor:ew-resize;display:flex;align-items:center;justify-content:center;flex-shrink:0;background:transparent;transition:background-color .15s ease}.pane-divider:hover{background-color:var(--cli-panel-accent, #818cf8)}.pane-divider .pane-divider-grip{width:2px;height:24px;border-radius:1px;background-color:var(--cli-panel-text-secondary, rgba(255, 255, 255, .3));transition:background-color .15s ease}.pane-divider:hover .pane-divider-grip{background-color:var(--cli-panel-text, rgba(255, 255, 255, .87))}.tab-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0;background:var(--cli-status-neutral, #6b7280)}.tab-dot.dot-idle{background:var(--cli-status-idle, #3fb950)}.tab-dot.dot-running{background:var(--cli-status-running, #f0883e);animation:statusPulse 1.5s ease-in-out infinite}.tab-dot.dot-error{background:var(--cli-status-error, #f85149)}.tab-dot.dot-neutral{background:var(--cli-status-neutral, #6b7280)}@keyframes statusPulse{0%,to{opacity:1}50%{opacity:.3}}@media(prefers-reduced-motion:reduce){.tab-dot{animation:none!important}}\n"] }]
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { options: [{
type: Input
}], modules: [{
type: Input
}], processors: [{
type: Input
}], onClose: [{
type: Output
}], collapsed: [{
type: Input
}], collapsedChange: [{
type: Output
}], hidden: [{
type: Input
}], hiddenChange: [{
type: Output
}], maximized: [{
type: Input
}], maximizedChange: [{
type: Output
}], activeTabId: [{
type: Input
}], activeTabIdChange: [{
type: Output
}], position: [{
type: Input
}], positionChange: [{
type: Output
}], height: [{
type: Input
}], heightChange: [{
type: Output
}], width: [{
type: Input
}], widthChange: [{
type: Output
}], onTabAdded: [{
type: Output
}], onTabClosed: [{
type: Output
}], onPaneSplit: [{
type: Output
}], onPaneClosed: [{
type: Output
}], collapsableContent: [{
type: ViewChild,
args: [CollapsableContentComponent]
}], cliComponents: [{
type: ViewChildren,
args: [CliComponent]
}], onDocumentClick: [{
type: HostListener,
args: ['document:click', ['$event']]
}], onEscapeKey: [{
type: HostListener,
args: ['document:keydown.escape']
}], onPaneResizeMove: [{
type: HostListener,
args: ['document:mousemove', ['$event']]
}], onPaneResizeEnd: [{
type: HostListener,
args: ['document:mouseup']
}] } });
class CliDefaultPingServerService {
ping() {
// Simulate a server ping
return new Promise((resolve) => setTimeout(resolve, 2000));
}
}
/**
* Angular DI providers for services that the framework-agnostic
* processors in @qodalis/cli need. The CliComponent bridges these
* into the engine's service container automatically.
*/
const resolveCliProviders = () => {
return [
{
useClass: CliDefaultPingServerService,
provide: ICliPingServerService_TOKEN,
},
];
};
class CliModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CliModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.20", ngImport: i0, type: CliModule, declarations: [CliComponent,
CollapsableContentComponent,
CliPanelComponent], imports: [CommonModule], exports: [CliPanelComponent, CliComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CliModule, providers: [resolveCliProviders()], imports: [CommonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CliModule, decorators: [{
type: NgModule,
args: [{
declarations: [
CliComponent,
CollapsableContentComponent,
CliPanelComponent,
],
imports: [CommonModule],
providers: [resolveCliProviders()],
exports: [CliPanelComponent, CliComponent],
}]
}] });
const resolveCliProvider = (token, provider) => ({
provide: token,
useExisting: provider,
multi: true,
});
/**
* @deprecated Use resolveCliModuleProvider() instead.
*/
const resolveCommandProcessorProvider = (provider) => [
provider,
resolveCliProvider(CliCommandProcessor_TOKEN, provider),
];
/**
* Resolve an ICliModule into Angular providers.
* Registers the module via CliModule_TOKEN and also registers processors
* individually via CliCommandProcessor_TOKEN for backward compatibility.
*/
const resolveCliModuleProvider = (module) => {
const providers = [
{
provide: CliModule_TOKEN,
useValue: module,
multi: true,
},
];
if (module.processors) {
for (const processor of module.processors) {
providers.push({
provide: CliCommandProcessor_TOKEN,
useValue: processor,
multi: true,
});
}
}
return providers;
};
// Re-exports from @qodalis/cli for backwards compatibility
/*
* Public API Surface of @qodalis/angular-cli
*/
/**
* Generated bundle index. Do not edit.
*/
export { CliCommandProcessor_TOKEN, CliComponent, CliDefaultPingServerService, CliModule, CliModule_TOKEN, CliPanelComponent, ICliPingServerService_TOKEN, ICliUserSessionService_TOKEN, ICliUsersStoreService_TOKEN, resolveCliModuleProvider, resolveCliProvider, resolveCliProviders, resolveCommandProcessorProvider };
//# sourceMappingURL=qodalis-angular-cli.mjs.map