@qodalis/vue-cli 2.0.0-beta.16 → 2.0.0-beta.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  h,
8
8
  inject
9
9
  } from "vue";
10
- import { CliEngine } from "@qodalis/cli";
10
+ import { createCliEngine, resolveCliHostConfig } from "@qodalis/cli";
11
11
 
12
12
  // src/cliInjection.ts
13
13
  var CliInjectionKey = /* @__PURE__ */ Symbol("qodalis-cli");
@@ -88,26 +88,10 @@ var Cli = defineComponent2({
88
88
  if (ctx) {
89
89
  return () => null;
90
90
  }
91
- const modules = props.modules ?? config?.modules;
92
- const processors = props.processors ?? config?.processors;
93
- const options = props.options ?? config?.options;
94
- const services = props.services ?? config?.services;
91
+ const hostConfig = resolveCliHostConfig(props, config);
95
92
  onMounted(async () => {
96
93
  if (!containerRef.value) return;
97
- const engineOptions = props.snapshot ? { ...options, snapshot: props.snapshot } : options;
98
- engine = new CliEngine(containerRef.value, engineOptions);
99
- engine.registerService("cli-framework", "Vue");
100
- if (services) {
101
- for (const [token, value] of Object.entries(services)) {
102
- engine.registerService(token, value);
103
- }
104
- }
105
- if (modules) {
106
- engine.registerModules(modules);
107
- }
108
- if (processors) {
109
- engine.registerProcessors(processors);
110
- }
94
+ engine = createCliEngine(containerRef.value, { ...hostConfig, snapshot: props.snapshot, framework: "Vue" });
111
95
  await engine.start();
112
96
  emit("ready", engine);
113
97
  });
@@ -124,139 +108,75 @@ var Cli = defineComponent2({
124
108
  });
125
109
 
126
110
  // src/CliPanel.ts
111
+ import { defineComponent as defineComponent3, computed, inject as inject2, watch, h as h2, onBeforeUnmount as onBeforeUnmount2, onMounted as onMounted2, shallowRef, Fragment } from "vue";
127
112
  import {
128
- defineComponent as defineComponent3,
129
- ref as ref2,
130
- computed,
131
- inject as inject2,
132
- watch,
133
- h as h2,
134
- onMounted as onMounted2,
135
- onBeforeUnmount as onBeforeUnmount2
136
- } from "vue";
137
- import { derivePanelThemeStyles, loadPanelPosition, savePanelPosition } from "@qodalis/cli-core";
138
- var MIN_PANE_WIDTH_PERCENT = 10;
139
- function normalizePanes(panes) {
140
- const total = panes.reduce((s, p) => s + p.widthPercent, 0);
141
- if (total === 0) return;
142
- const scale = 100 / total;
143
- panes.forEach((p) => {
144
- p.widthPercent *= scale;
145
- });
146
- }
147
- var svgAttrs = {
148
- width: "20",
149
- height: "20",
150
- viewBox: "0 0 24 24",
151
- fill: "none",
152
- stroke: "currentColor",
153
- "stroke-width": "1.8",
154
- "stroke-linecap": "round",
155
- "stroke-linejoin": "round"
156
- };
157
- var terminalIcon = () => h2("svg", { ...svgAttrs, width: "22", height: "22", "stroke-width": "2" }, [
158
- h2("polyline", { points: "4 17 10 11 4 5" }),
159
- h2("line", { x1: "12", y1: "19", x2: "20", y2: "19" })
160
- ]);
161
- var maximizeIcon = () => h2("svg", svgAttrs, [
162
- h2("polyline", { points: "15 3 21 3 21 9" }),
163
- h2("polyline", { points: "9 21 3 21 3 15" }),
164
- h2("line", { x1: "21", y1: "3", x2: "14", y2: "10" }),
165
- h2("line", { x1: "3", y1: "21", x2: "10", y2: "14" })
166
- ]);
167
- var restoreIcon = () => h2("svg", svgAttrs, [
168
- h2("polyline", { points: "4 14 10 14 10 20" }),
169
- h2("polyline", { points: "20 10 14 10 14 4" }),
170
- h2("line", { x1: "14", y1: "10", x2: "21", y2: "3" }),
171
- h2("line", { x1: "3", y1: "21", x2: "10", y2: "14" })
172
- ]);
173
- var closeIcon = () => h2("svg", svgAttrs, [
174
- h2("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
175
- h2("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
176
- ]);
177
- var hideIcon = () => h2("svg", svgAttrs, [
178
- h2("line", { x1: "5", y1: "18", x2: "19", y2: "18" }),
179
- h2("polyline", { points: "9 14 12 17 15 14" })
180
- ]);
181
- function collapseChevron(position, isCollapsed) {
182
- let points;
183
- switch (position) {
184
- case "top":
185
- points = isCollapsed ? "6 9 12 15 18 9" : "18 15 12 9 6 15";
186
- break;
187
- case "left":
188
- points = isCollapsed ? "9 18 15 12 9 6" : "15 6 9 12 15 18";
189
- break;
190
- case "right":
191
- points = isCollapsed ? "15 6 9 12 15 18" : "9 18 15 12 9 6";
192
- break;
193
- default:
194
- points = isCollapsed ? "18 15 12 9 6 15" : "6 9 12 15 18 9";
195
- break;
196
- }
197
- return h2("svg", svgAttrs, [h2("polyline", { points })]);
198
- }
199
- function hideTabChevron(position) {
200
- let points;
201
- switch (position) {
202
- case "top":
203
- points = "6 9 12 15 18 9";
204
- break;
205
- case "left":
206
- points = "9 18 15 12 9 6";
207
- break;
208
- case "right":
209
- points = "15 6 9 12 15 18";
210
- break;
211
- default:
212
- points = "18 15 12 9 6 15";
213
- break;
214
- }
215
- return h2("svg", { class: "cli-panel-hide-tab-arrow", ...svgAttrs, width: "14", height: "14", "stroke-width": "2.5" }, [
216
- h2("polyline", { points })
217
- ]);
218
- }
219
- function positionIcon(pos, size = 20) {
220
- let fillRect;
221
- switch (pos) {
222
- case "top":
223
- fillRect = h2("rect", { x: "4", y: "4", width: "16", height: "5", rx: "1", fill: "currentColor", stroke: "none", opacity: "0.5" });
224
- break;
225
- case "left":
226
- fillRect = h2("rect", { x: "4", y: "4", width: "5", height: "16", rx: "1", fill: "currentColor", stroke: "none", opacity: "0.5" });
227
- break;
228
- case "right":
229
- fillRect = h2("rect", { x: "15", y: "4", width: "5", height: "16", rx: "1", fill: "currentColor", stroke: "none", opacity: "0.5" });
230
- break;
231
- default:
232
- fillRect = h2("rect", { x: "4", y: "15", width: "16", height: "5", rx: "1", fill: "currentColor", stroke: "none", opacity: "0.5" });
233
- break;
234
- }
235
- return h2("svg", { ...svgAttrs, width: String(size), height: String(size) }, [
236
- h2("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
237
- fillRect
238
- ]);
113
+ CliPanelController,
114
+ CLOSE_GLYPH,
115
+ CLOSE_ICON,
116
+ HIDE_ICON,
117
+ HIDE_TAB_TERMINAL_ICON,
118
+ MAXIMIZE_ICON,
119
+ PANEL_LABELS,
120
+ RESTORE_ICON,
121
+ TERMINAL_ICON,
122
+ collapseChevronIcon,
123
+ contextMenuItems,
124
+ contextMenuStyle,
125
+ glowLineClass,
126
+ handleRenameKey,
127
+ hideTabChevronIcon,
128
+ joinClasses,
129
+ mergeCliOptions,
130
+ openStatusDropdown,
131
+ paneClass,
132
+ paneStyle,
133
+ panelTabClass,
134
+ panelTabDotClass,
135
+ panelWrapperClass,
136
+ panelWrapperStyle,
137
+ panesContainerClass,
138
+ panesContainerWidth,
139
+ positionIcon,
140
+ positionOptions,
141
+ resolveCliHostConfig as resolveCliHostConfig2,
142
+ runContextMenuAction,
143
+ statusIndicators,
144
+ svgAttrs
145
+ } from "@qodalis/cli";
146
+ var PANEL_REF_METHODS = [
147
+ "open",
148
+ "collapse",
149
+ "toggleCollapse",
150
+ "hide",
151
+ "unhide",
152
+ "toggleHide",
153
+ "close",
154
+ "maximize",
155
+ "restore",
156
+ "toggleMaximize",
157
+ "resize",
158
+ "setPosition",
159
+ "addTab",
160
+ "closeTab",
161
+ "selectTab",
162
+ "renameTab",
163
+ "splitPane",
164
+ "closePane",
165
+ "getEngine",
166
+ "getState"
167
+ ];
168
+ function icon(svg, className) {
169
+ return h2("svg", { class: className, ...svgAttrs(svg) }, svg.shapes.map((shape) => h2(shape.tag, shape.attrs)));
239
170
  }
171
+ var stop = (e) => e.stopPropagation();
240
172
  var CliPanel = defineComponent3({
241
173
  name: "CliPanel",
242
174
  props: {
243
- options: {
244
- type: Object,
245
- default: void 0
246
- },
175
+ options: { type: Object, default: void 0 },
247
176
  modules: { type: Array, default: void 0 },
248
- processors: {
249
- type: Array,
250
- default: void 0
251
- },
252
- services: {
253
- type: Object,
254
- default: void 0
255
- },
256
- onClose: {
257
- type: Function,
258
- default: void 0
259
- },
177
+ processors: { type: Array, default: void 0 },
178
+ services: { type: Object, default: void 0 },
179
+ onClose: { type: Function, default: void 0 },
260
180
  collapsed: { type: Boolean, default: void 0 },
261
181
  hidden: { type: Boolean, default: void 0 },
262
182
  maximized: { type: Boolean, default: void 0 },
@@ -264,10 +184,7 @@ var CliPanel = defineComponent3({
264
184
  position: { type: String, default: void 0 },
265
185
  height: { type: Number, default: void 0 },
266
186
  width: { type: Number, default: void 0 },
267
- style: {
268
- type: Object,
269
- default: void 0
270
- },
187
+ style: { type: Object, default: void 0 },
271
188
  class: { type: String, default: void 0 }
272
189
  },
273
190
  emits: [
@@ -286,1194 +203,378 @@ var CliPanel = defineComponent3({
286
203
  ],
287
204
  setup(props, { expose, emit }) {
288
205
  const config = inject2(CliConfigKey, null);
289
- const mergedOptions = computed(() => props.options ? { ...config?.options, ...props.options } : config?.options);
290
- const mergedModules = computed(() => props.modules ?? config?.modules);
291
- const mergedProcessors = computed(() => props.processors ?? config?.processors);
292
- const mergedServices = computed(() => props.services ?? config?.services);
293
- const visible = ref2(true);
294
- const internalCollapsed = ref2(mergedOptions.value?.isCollapsed ?? true);
295
- const internalMaximized = ref2(false);
296
- const internalHeight = ref2(600);
297
- const internalWidth = ref2(400);
298
- const prevHeight = ref2(600);
299
- const prevWidth = ref2(400);
300
- const initialized = ref2(false);
301
- const internalHidden = ref2(props.options?.isHidden ?? false);
302
- let preHideCollapsed = true;
303
- const internalActiveTabId = ref2(0);
304
- const internalPosition = ref2(
305
- loadPanelPosition() ?? mergedOptions.value?.position ?? "bottom"
306
- );
307
- const resolvedCollapsed = computed(() => props.collapsed !== void 0 ? props.collapsed : internalCollapsed.value);
308
- const resolvedHidden = computed(() => props.hidden !== void 0 ? props.hidden : internalHidden.value);
309
- const resolvedMaximized = computed(() => props.maximized !== void 0 ? props.maximized : internalMaximized.value);
310
- const resolvedActiveTabId = computed(() => props.activeTabId !== void 0 ? props.activeTabId : internalActiveTabId.value);
311
- const resolvedHeight = computed(() => props.height !== void 0 ? props.height : internalHeight.value);
312
- const resolvedWidth = computed(() => props.width !== void 0 ? props.width : internalWidth.value);
313
- const resolvedPosition = computed(() => props.position !== void 0 ? props.position : internalPosition.value);
314
- function updateCollapsed(value) {
315
- internalCollapsed.value = value;
316
- emit("update:collapsed", value);
317
- }
318
- function updateHidden(value) {
319
- internalHidden.value = value;
320
- emit("update:hidden", value);
321
- }
322
- function updateMaximized(value) {
323
- internalMaximized.value = value;
324
- emit("update:maximized", value);
325
- }
326
- function updateActiveTabId(value) {
327
- internalActiveTabId.value = value;
328
- emit("update:activeTabId", value);
329
- }
330
- function updateHeight(value) {
331
- internalHeight.value = value;
332
- emit("update:height", value);
333
- }
334
- function updateWidth(value) {
335
- internalWidth.value = value;
336
- emit("update:width", value);
337
- }
338
- function updatePosition(value) {
339
- internalPosition.value = value;
340
- savePanelPosition(value);
341
- emit("update:position", value);
342
- }
343
- const closable = computed(() => mergedOptions.value?.closable ?? true);
344
- const resizable = computed(() => mergedOptions.value?.resizable ?? true);
345
- const isHorizontal = computed(() => resolvedPosition.value === "left" || resolvedPosition.value === "right");
346
- const hideable = computed(() => mergedOptions.value?.hideable ?? true);
347
- const hideAlignment = computed(() => mergedOptions.value?.hideAlignment ?? "center");
348
- const syncTheme = computed(() => mergedOptions.value?.syncTheme ?? false);
349
- const themeStyles = ref2({});
350
- const tabs = ref2([]);
351
- const activePaneId = ref2(0);
352
- let nextTabId = 1;
353
- let nextPaneId = 1;
354
- const contextMenu = ref2({
355
- visible: false,
356
- x: 0,
357
- y: 0,
358
- tabId: 0
359
- });
360
- const engineMap = /* @__PURE__ */ new Map();
361
- const paneResizing = ref2(false);
362
- let paneResizeState = {
363
- tabId: 0,
364
- dividerIndex: 0,
365
- startX: 0,
366
- startWidths: [],
367
- containerWidth: 0
368
- };
369
- const panelResizing = ref2(false);
370
- let panelResizeState = { startY: 0, startHeight: 0, startX: 0, startWidth: 0 };
371
- const statusExecutionState = ref2("idle");
372
- const statusLastCommand = ref2(null);
373
- const statusServiceCount = ref2({ running: 0, total: 0 });
374
- const statusServiceDetails = ref2([]);
375
- const statusServerState = ref2("none");
376
- const statusServerDetails = ref2([]);
377
- const statusUptime = ref2(0);
378
- const notification = ref2(null);
379
- const servicesDropdownOpen = ref2(false);
380
- const serversDropdownOpen = ref2(false);
381
- const servicesDropdownTriggerRect = ref2(null);
382
- const serversDropdownTriggerRect = ref2(null);
383
- const formattedUptime = computed(() => {
384
- const mins = Math.floor(statusUptime.value / 6e4);
385
- if (mins < 60) return `${mins}m`;
386
- const hrs = Math.floor(mins / 60);
387
- return `${hrs}h${mins % 60}m`;
206
+ const mergedOptions = computed(() => mergeCliOptions(props.options, config?.options));
207
+ const hostConfig = computed(() => resolveCliHostConfig2(props, config));
208
+ const controlled = () => ({
209
+ collapsed: props.collapsed,
210
+ hidden: props.hidden,
211
+ maximized: props.maximized,
212
+ activeTabId: props.activeTabId,
213
+ position: props.position,
214
+ height: props.height,
215
+ width: props.width
388
216
  });
389
- let execPollTimer;
390
- let globalPollTimer;
391
- function startStatusPolling() {
392
- stopStatusPolling();
393
- execPollTimer = setInterval(() => {
394
- const tab = tabs.value.find((t) => t.id === resolvedActiveTabId.value);
395
- if (!tab) return;
396
- const engine = engineMap.get(tab.panes[0]?.id);
397
- if (!engine) return;
398
- const context = engine.getContext();
399
- if (!context) return;
400
- const running = !!context.isExecuting || !!context.contextProcessor;
401
- statusExecutionState.value = running ? "running" : "idle";
402
- const result = context.lastCommandResult;
403
- statusLastCommand.value = result ? { name: result.command, success: result.success } : null;
404
- const notif = context.notifier?.current;
405
- notification.value = notif || null;
406
- }, 300);
407
- globalPollTimer = setInterval(() => {
408
- const tab = tabs.value.find((t) => t.id === resolvedActiveTabId.value);
409
- if (!tab) return;
410
- const engine = engineMap.get(tab.panes[0]?.id);
411
- if (!engine) return;
412
- const context = engine.getContext();
413
- if (!context) return;
414
- try {
415
- const services = context.backgroundServices?.list() ?? [];
416
- statusServiceCount.value = { running: services.filter((s) => s.status === "running").length, total: services.length };
417
- statusServiceDetails.value = services.map((s) => ({ name: s.name, status: s.status, description: s.description }));
418
- } catch {
419
- }
420
- try {
421
- const serverManager = context.services?.get?.("cli-server-manager");
422
- const details = [];
423
- if (serverManager?.connections?.size > 0) {
424
- let anyConnected = false;
425
- for (const [name, conn] of serverManager.connections) {
426
- if (conn.connected) anyConnected = true;
427
- details.push({
428
- name,
429
- url: conn.config?.url ?? "",
430
- connected: !!conn.connected,
431
- apiVersion: conn.connected ? conn.apiVersion : void 0,
432
- commandCount: conn.connected ? conn.commands?.length : void 0
433
- });
434
- }
435
- statusServerState.value = anyConnected ? "connected" : "disconnected";
436
- } else {
437
- const configuredServers = engine.options?.servers;
438
- if (Array.isArray(configuredServers) && configuredServers.length > 0) {
439
- for (const srv of configuredServers) {
440
- if (srv.enabled === false) continue;
441
- details.push({
442
- name: srv.name,
443
- url: srv.url ?? "",
444
- connected: false
445
- });
446
- }
447
- statusServerState.value = "disconnected";
448
- } else {
449
- statusServerState.value = "none";
450
- }
451
- }
452
- statusServerDetails.value = details;
453
- } catch {
454
- }
455
- statusUptime.value = engine.startedAt ? Date.now() - engine.startedAt : 0;
456
- }, 2e3);
457
- }
458
- function stopStatusPolling() {
459
- if (execPollTimer) {
460
- clearInterval(execPollTimer);
461
- execPollTimer = void 0;
462
- }
463
- if (globalPollTimer) {
464
- clearInterval(globalPollTimer);
465
- globalPollTimer = void 0;
466
- }
467
- }
468
- watch(initialized, (val) => {
469
- if (val) startStatusPolling();
470
- else stopStatusPolling();
471
- });
472
- function addTab(title) {
473
- const tabId = nextTabId++;
474
- const paneId = nextPaneId++;
475
- tabs.value = [...tabs.value, {
476
- id: tabId,
477
- title: title ?? `Terminal ${tabId}`,
478
- isEditing: false,
479
- panes: [{ id: paneId, widthPercent: 100 }]
480
- }];
481
- updateActiveTabId(tabId);
482
- activePaneId.value = paneId;
483
- emit("tab-added", { tabId });
484
- return tabId;
485
- }
486
- let themeObserver;
487
- function syncThemeFromEngine() {
488
- const engine = engineMap.values().next().value;
489
- if (!engine) return;
490
- const theme = engine.getTerminal().options.theme;
491
- if (theme) themeStyles.value = derivePanelThemeStyles(theme);
492
- }
493
- function setupThemeSync() {
494
- if (!syncTheme.value) return;
495
- setTimeout(() => {
496
- syncThemeFromEngine();
497
- const container = document.querySelector(".cli-panel-wrapper .terminal-container");
498
- if (!container) return;
499
- themeObserver?.disconnect();
500
- themeObserver = new MutationObserver(syncThemeFromEngine);
501
- themeObserver.observe(container, { attributes: true, attributeFilter: ["style"] });
502
- }, 200);
503
- }
504
- onMounted2(() => {
505
- document.addEventListener("click", closePositionDropdown);
506
- });
507
- onBeforeUnmount2(() => {
508
- themeObserver?.disconnect();
509
- document.removeEventListener("click", closePositionDropdown);
510
- });
511
- function toggle() {
512
- const newVal = !resolvedCollapsed.value;
513
- updateCollapsed(newVal);
514
- if (!newVal && !initialized.value) {
515
- initialized.value = true;
516
- addTab();
517
- setupThemeSync();
518
- }
519
- }
520
- function closeTab(id) {
521
- const idx = tabs.value.findIndex((t) => t.id === id);
522
- if (idx === -1) return;
523
- tabs.value.splice(idx, 1);
524
- emit("tab-closed", { tabId: id });
525
- if (tabs.value.length === 0) {
526
- initialized.value = false;
527
- updateCollapsed(true);
528
- return;
529
- }
530
- if (resolvedActiveTabId.value === id) {
531
- const newIdx = Math.min(idx, tabs.value.length - 1);
532
- selectTab(tabs.value[newIdx].id);
533
- }
534
- }
535
- function selectTab(id) {
536
- updateActiveTabId(id);
537
- const tab = tabs.value.find((t) => t.id === id);
538
- if (tab && tab.panes.length > 0) {
539
- activePaneId.value = tab.panes[0].id;
540
- }
541
- }
542
- function splitPane(tabId) {
543
- const targetTabId = tabId ?? resolvedActiveTabId.value;
544
- const paneId = nextPaneId++;
545
- tabs.value = tabs.value.map((t) => {
546
- if (t.id !== targetTabId) return t;
547
- const newPanes = [...t.panes, { id: paneId, widthPercent: 0 }];
548
- const evenWidth = 100 / newPanes.length;
549
- newPanes.forEach((p) => {
550
- p.widthPercent = evenWidth;
551
- });
552
- normalizePanes(newPanes);
553
- return { ...t, panes: newPanes };
554
- });
555
- activePaneId.value = paneId;
556
- emit("pane-split", { paneId, tabId: targetTabId });
557
- return paneId;
558
- }
559
- function closePane(paneId) {
560
- for (const tab of tabs.value) {
561
- const idx = tab.panes.findIndex((p) => p.id === paneId);
562
- if (idx === -1) continue;
563
- if (tab.panes.length <= 1) {
564
- closeTab(tab.id);
565
- return;
566
- }
567
- engineMap.get(paneId)?.destroy();
568
- engineMap.delete(paneId);
569
- const newPanes = tab.panes.filter((p) => p.id !== paneId);
570
- normalizePanes(newPanes);
571
- if (activePaneId.value === paneId) {
572
- activePaneId.value = newPanes[Math.min(idx, newPanes.length - 1)].id;
573
- }
574
- emit("pane-closed", { paneId });
575
- tabs.value = tabs.value.map(
576
- (t) => t.id === tab.id ? { ...t, panes: newPanes } : t
577
- );
578
- return;
579
- }
580
- }
581
- function renameTab(tabId, title) {
582
- tabs.value = tabs.value.map((t) => t.id === tabId ? { ...t, title } : t);
583
- }
584
- function startRename(tabId) {
585
- tabs.value.forEach((t) => {
586
- t.isEditing = t.id === tabId;
587
- });
588
- }
589
- function commitRename(tabId, value) {
590
- const trimmed = value.trim();
591
- const tab = tabs.value.find((t) => t.id === tabId);
592
- if (tab) {
593
- if (trimmed) tab.title = trimmed;
594
- tab.isEditing = false;
595
- }
596
- }
597
- function closeContextMenu() {
598
- contextMenu.value = { ...contextMenu.value, visible: false };
599
- }
600
- function onDocClick() {
601
- if (contextMenu.value.visible) closeContextMenu();
602
- }
603
- onMounted2(() => document.addEventListener("click", onDocClick));
604
- onBeforeUnmount2(
605
- () => document.removeEventListener("click", onDocClick)
606
- );
607
- function onPanelResizeStart(e) {
608
- if (!resizable.value) return;
609
- e.preventDefault();
610
- if (resolvedCollapsed.value) toggle();
611
- panelResizing.value = true;
612
- panelResizeState = {
613
- startY: e.clientY,
614
- startHeight: resolvedHeight.value,
615
- startX: e.clientX,
616
- startWidth: resolvedWidth.value
617
- };
618
- }
619
- function onPanelResizeMove(e) {
620
- if (!panelResizing.value) return;
621
- if (isHorizontal.value) {
622
- const deltaX = resolvedPosition.value === "left" ? e.clientX - panelResizeState.startX : panelResizeState.startX - e.clientX;
623
- let next = Math.max(100, panelResizeState.startWidth + deltaX);
624
- if (next > window.innerWidth) next = window.innerWidth;
625
- updateWidth(next);
626
- } else {
627
- const deltaY = resolvedPosition.value === "top" ? e.clientY - panelResizeState.startY : panelResizeState.startY - e.clientY;
628
- let next = Math.max(100, panelResizeState.startHeight + deltaY);
629
- if (next > window.innerHeight) next = window.innerHeight;
630
- updateHeight(next);
217
+ const controller = new CliPanelController({
218
+ config: mergedOptions.value,
219
+ controlled: controlled(),
220
+ callbacks: {
221
+ onCollapsedChange: (v) => emit("update:collapsed", v),
222
+ onHiddenChange: (v) => emit("update:hidden", v),
223
+ onMaximizedChange: (v) => emit("update:maximized", v),
224
+ onActiveTabIdChange: (v) => emit("update:activeTabId", v),
225
+ onPositionChange: (v) => emit("update:position", v),
226
+ onHeightChange: (v) => emit("update:height", v),
227
+ onWidthChange: (v) => emit("update:width", v),
228
+ onClose: () => {
229
+ emit("close");
230
+ props.onClose?.();
231
+ },
232
+ onTabAdded: (e) => emit("tab-added", e),
233
+ onTabClosed: (e) => emit("tab-closed", e),
234
+ onPaneSplit: (e) => emit("pane-split", e),
235
+ onPaneClosed: (e) => emit("pane-closed", e)
631
236
  }
632
- }
633
- function onPanelResizeEnd() {
634
- panelResizing.value = false;
635
- }
636
- onMounted2(() => {
637
- document.addEventListener("mousemove", onPanelResizeMove);
638
- document.addEventListener("mouseup", onPanelResizeEnd);
639
- });
640
- onBeforeUnmount2(() => {
641
- document.removeEventListener("mousemove", onPanelResizeMove);
642
- document.removeEventListener("mouseup", onPanelResizeEnd);
643
237
  });
644
- function onPaneResizeStart(e, tabId, dividerIndex) {
645
- e.preventDefault();
646
- const tab = tabs.value.find((t) => t.id === tabId);
647
- if (!tab) return;
648
- paneResizing.value = true;
649
- const container = e.target.closest(
650
- ".cli-panel-panes-container"
651
- );
652
- paneResizeState = {
653
- tabId,
654
- dividerIndex,
655
- startX: e.clientX,
656
- startWidths: tab.panes.map((p) => p.widthPercent),
657
- containerWidth: container ? container.clientWidth : 1
658
- };
659
- document.body.classList.add("cli-pane-resizing");
660
- }
661
- function onPaneResizeMove(e) {
662
- if (!paneResizing.value) return;
663
- const s = paneResizeState;
664
- const deltaPct = (e.clientX - s.startX) / s.containerWidth * 100;
665
- const i = s.dividerIndex;
666
- let leftW = s.startWidths[i] + deltaPct;
667
- let rightW = s.startWidths[i + 1] - deltaPct;
668
- if (leftW < MIN_PANE_WIDTH_PERCENT) {
669
- leftW = MIN_PANE_WIDTH_PERCENT;
670
- rightW = s.startWidths[i] + s.startWidths[i + 1] - MIN_PANE_WIDTH_PERCENT;
671
- }
672
- if (rightW < MIN_PANE_WIDTH_PERCENT) {
673
- rightW = MIN_PANE_WIDTH_PERCENT;
674
- leftW = s.startWidths[i] + s.startWidths[i + 1] - MIN_PANE_WIDTH_PERCENT;
675
- }
676
- const tab = tabs.value.find((t) => t.id === s.tabId);
677
- if (tab) {
678
- tab.panes[i].widthPercent = leftW;
679
- tab.panes[i + 1].widthPercent = rightW;
680
- }
681
- }
682
- function onPaneResizeEnd() {
683
- if (!paneResizing.value) return;
684
- paneResizing.value = false;
685
- document.body.classList.remove("cli-pane-resizing");
686
- }
687
- onMounted2(() => {
688
- document.addEventListener("mousemove", onPaneResizeMove);
689
- document.addEventListener("mouseup", onPaneResizeEnd);
238
+ const state = shallowRef(controller.getSnapshot());
239
+ const unsubscribe = controller.subscribe(() => {
240
+ state.value = controller.getSnapshot();
690
241
  });
242
+ watch(controlled, (values) => controller.setControlled(values));
243
+ watch(mergedOptions, (options) => controller.setOptions(options));
244
+ const root = shallowRef(null);
245
+ onMounted2(() => controller.setRootElement(root.value));
691
246
  onBeforeUnmount2(() => {
692
- document.removeEventListener("mousemove", onPaneResizeMove);
693
- document.removeEventListener("mouseup", onPaneResizeEnd);
694
- stopStatusPolling();
247
+ unsubscribe();
248
+ controller.destroy();
695
249
  });
696
- function toggleMaximize() {
697
- if (!resolvedMaximized.value) {
698
- if (isHorizontal.value) {
699
- prevWidth.value = resolvedWidth.value;
700
- updateWidth(window.innerWidth);
701
- } else {
702
- prevHeight.value = resolvedHeight.value;
703
- updateHeight(window.innerHeight);
704
- }
705
- } else {
706
- if (isHorizontal.value) {
707
- updateWidth(prevWidth.value);
708
- } else {
709
- updateHeight(prevHeight.value);
710
- }
711
- }
712
- updateMaximized(!resolvedMaximized.value);
713
- }
714
- function handleClose() {
715
- visible.value = false;
716
- emit("close");
717
- props.onClose?.();
718
- }
719
- function handleHide() {
720
- preHideCollapsed = resolvedCollapsed.value;
721
- updateHidden(true);
722
- }
723
- function handleUnhide() {
724
- updateHidden(false);
725
- updateCollapsed(preHideCollapsed);
250
+ const exposed = {};
251
+ for (const method of PANEL_REF_METHODS) {
252
+ exposed[method] = controller[method].bind(controller);
726
253
  }
727
- const positionDropdownOpen = ref2(false);
728
- function closePositionDropdown() {
729
- positionDropdownOpen.value = false;
730
- servicesDropdownOpen.value = false;
731
- serversDropdownOpen.value = false;
732
- }
733
- function togglePositionDropdown(e) {
254
+ expose(exposed);
255
+ const openDropdown = (e, dropdown) => {
256
+ if (!dropdown) return;
734
257
  e.stopPropagation();
735
- positionDropdownOpen.value = !positionDropdownOpen.value;
736
- }
737
- function selectPosition(pos) {
738
- positionDropdownOpen.value = false;
739
- updatePosition(pos);
740
- }
741
- function getEngine(paneId) {
742
- const targetId = paneId ?? activePaneId.value;
743
- return engineMap.get(targetId);
744
- }
745
- function getState() {
746
- return {
747
- collapsed: resolvedCollapsed.value,
748
- hidden: resolvedHidden.value,
749
- maximized: resolvedMaximized.value,
750
- position: resolvedPosition.value,
751
- height: resolvedHeight.value,
752
- width: resolvedWidth.value,
753
- activeTabId: resolvedActiveTabId.value,
754
- activePaneId: activePaneId.value,
755
- tabs: tabs.value.map((t) => ({
756
- id: t.id,
757
- title: t.title,
758
- panes: t.panes.map((p) => ({ id: p.id, widthPercent: p.widthPercent }))
759
- }))
760
- };
761
- }
762
- expose({
763
- open: () => {
764
- if (!visible.value) {
765
- visible.value = true;
766
- }
767
- if (resolvedHidden.value) {
768
- updateHidden(false);
769
- }
770
- if (resolvedCollapsed.value) {
771
- updateCollapsed(false);
772
- if (!initialized.value) {
773
- initialized.value = true;
774
- addTab();
775
- setupThemeSync();
776
- }
777
- }
778
- },
779
- collapse: () => {
780
- if (!resolvedCollapsed.value) updateCollapsed(true);
781
- },
782
- toggleCollapse: () => {
783
- if (resolvedCollapsed.value) {
784
- updateCollapsed(false);
785
- if (!initialized.value) {
786
- initialized.value = true;
787
- addTab();
788
- setupThemeSync();
789
- }
790
- } else {
791
- updateCollapsed(true);
792
- }
793
- },
794
- hide: () => {
795
- if (!resolvedHidden.value) updateHidden(true);
796
- },
797
- unhide: () => {
798
- if (resolvedHidden.value) updateHidden(false);
799
- },
800
- toggleHide: () => {
801
- updateHidden(!resolvedHidden.value);
802
- },
803
- close: handleClose,
804
- maximize: () => {
805
- if (!resolvedMaximized.value) updateMaximized(true);
806
- },
807
- restore: () => {
808
- if (resolvedMaximized.value) updateMaximized(false);
809
- },
810
- toggleMaximize: () => {
811
- updateMaximized(!resolvedMaximized.value);
812
- },
813
- resize: (dims) => {
814
- if (dims.height !== void 0) updateHeight(dims.height);
815
- if (dims.width !== void 0) updateWidth(dims.width);
816
- },
817
- setPosition: updatePosition,
818
- addTab,
819
- closeTab,
820
- selectTab,
821
- renameTab,
822
- splitPane,
823
- closePane,
824
- getEngine,
825
- getState
826
- });
827
- return () => {
828
- if (!visible.value) return null;
829
- const wrapperClass = [
830
- "cli-panel-wrapper",
831
- resolvedCollapsed.value && "collapsed",
832
- resolvedMaximized.value && "maximized",
833
- panelResizing.value && "resizing",
834
- props.class
835
- ].filter(Boolean).join(" ");
836
- const wrapperStyle = {
837
- ...isHorizontal.value ? { width: `${resolvedWidth.value}px` } : { height: `${resolvedHeight.value}px` },
838
- ...resolvedHidden.value ? { display: "none" } : {},
839
- ...themeStyles.value,
840
- ...props.style
841
- };
842
- const headerEl = h2("div", { class: "cli-panel-header" }, [
258
+ const anchor = e.currentTarget;
259
+ if (dropdown === "services") controller.toggleServicesDropdown(anchor);
260
+ else controller.toggleServersDropdown(anchor);
261
+ };
262
+ const renderHeader = (s) => {
263
+ const indicators = statusIndicators(s);
264
+ return h2("div", { class: "cli-panel-header" }, [
843
265
  h2(
844
266
  "div",
845
267
  {
846
268
  class: "cli-panel-resize-bar",
847
- onMousedown: onPanelResizeStart
269
+ onMousedown: (e) => {
270
+ if (!s.resizable) return;
271
+ e.preventDefault();
272
+ controller.startPanelResize(e.clientX, e.clientY);
273
+ }
848
274
  },
849
275
  [h2("div", { class: "cli-panel-resize-grip" })]
850
276
  ),
851
277
  h2("div", { class: "cli-panel-header-content" }, [
852
- h2("p", { class: "cli-panel-title" }, [
853
- h2("span", { class: "cli-panel-title-icon" }, [
854
- terminalIcon()
855
- ]),
856
- "CLI"
857
- ]),
858
- // Status indicators (bottom/top only)
859
- resolvedPosition.value === "bottom" || resolvedPosition.value === "top" ? h2("div", { class: "cli-panel-status-indicators" }, [
860
- // Execution state
861
- h2("span", { class: ["cli-panel-status-item", statusExecutionState.value === "running" ? "status-running" : ""].filter(Boolean).join(" ") }, [
862
- h2("span", { class: ["cli-panel-status-dot", statusExecutionState.value === "running" ? "dot-running" : "dot-idle"].join(" ") }),
863
- h2("span", { class: "cli-panel-status-label" }, statusExecutionState.value)
864
- ]),
865
- // Background services
866
- statusServiceCount.value.total > 0 ? h2("span", {
867
- class: "cli-panel-status-item status-clickable",
868
- onClick: (e) => {
869
- e.stopPropagation();
870
- servicesDropdownTriggerRect.value = e.currentTarget.getBoundingClientRect();
871
- servicesDropdownOpen.value = !servicesDropdownOpen.value;
872
- serversDropdownOpen.value = false;
873
- }
874
- }, [
875
- h2("span", { class: "cli-panel-status-icon", innerHTML: "&#9881;" }),
876
- h2("span", { class: "cli-panel-status-label" }, `${statusServiceCount.value.running}/${statusServiceCount.value.total} services`)
877
- ]) : null,
878
- // Last command
879
- statusLastCommand.value ? h2("span", { class: "cli-panel-status-item" }, [
880
- h2("span", { class: ["cli-panel-status-icon", statusLastCommand.value.success ? "status-success" : "status-error"].join(" "), innerHTML: statusLastCommand.value.success ? "&#10003;" : "&#10005;" }),
881
- h2("span", { class: "cli-panel-status-label" }, statusLastCommand.value.name)
882
- ]) : null,
883
- // Server connection
884
- statusServerState.value !== "none" ? h2("span", {
885
- class: "cli-panel-status-item status-clickable",
886
- onClick: (e) => {
887
- e.stopPropagation();
888
- serversDropdownTriggerRect.value = e.currentTarget.getBoundingClientRect();
889
- serversDropdownOpen.value = !serversDropdownOpen.value;
890
- servicesDropdownOpen.value = false;
891
- }
892
- }, [
893
- h2("span", { class: ["cli-panel-status-dot", statusServerState.value === "connected" ? "dot-idle" : "dot-error"].join(" ") }),
894
- h2("span", { class: "cli-panel-status-label" }, `${statusServerDetails.value.filter((s) => s.connected).length}/${statusServerDetails.value.length} servers`)
895
- ]) : null,
896
- // Uptime
897
- statusUptime.value > 0 ? h2("span", { class: "cli-panel-status-item status-muted" }, [
898
- h2("span", { class: "cli-panel-status-icon", innerHTML: "&uarr;" }),
899
- h2("span", { class: "cli-panel-status-label" }, formattedUptime.value)
900
- ]) : null,
901
- // Custom notification
902
- notification.value ? h2("span", { class: `cli-panel-status-item status-text level-${notification.value.level}` }, [
903
- h2("span", { class: "cli-panel-status-label" }, notification.value.message)
904
- ]) : null
905
- ].filter(Boolean)) : null,
906
- // Compact status indicators (left/right positions — CSS controls visibility)
907
- h2("div", { class: "cli-panel-status-indicators-compact" }, [
908
- // Execution state
909
- h2("span", { class: "cli-panel-status-compact-item", title: statusExecutionState.value }, [
910
- h2("span", { class: ["cli-panel-status-dot", statusExecutionState.value === "running" ? "dot-running" : "dot-idle"].join(" ") })
911
- ]),
912
- // Background services
913
- statusServiceCount.value.total > 0 ? h2("span", {
914
- class: "cli-panel-status-compact-item status-clickable",
915
- title: `${statusServiceCount.value.running}/${statusServiceCount.value.total} services`,
916
- onClick: (e) => {
917
- e.stopPropagation();
918
- servicesDropdownTriggerRect.value = e.currentTarget.getBoundingClientRect();
919
- servicesDropdownOpen.value = !servicesDropdownOpen.value;
920
- serversDropdownOpen.value = false;
921
- }
922
- }, [
923
- h2("span", { class: "cli-panel-status-icon", innerHTML: "&#9881;" })
924
- ]) : null,
925
- // Last command
926
- statusLastCommand.value ? h2("span", {
927
- class: "cli-panel-status-compact-item",
928
- title: `${statusLastCommand.value.success ? "\u2713" : "\u2717"} ${statusLastCommand.value.name}`
929
- }, [
930
- h2("span", {
931
- class: ["cli-panel-status-icon", statusLastCommand.value.success ? "status-success" : "status-error"].join(" "),
932
- innerHTML: statusLastCommand.value.success ? "&#10003;" : "&#10005;"
933
- })
934
- ]) : null,
935
- // Server connection
936
- statusServerState.value !== "none" ? h2("span", {
937
- class: "cli-panel-status-compact-item status-clickable",
938
- title: `${statusServerDetails.value.filter((s) => s.connected).length}/${statusServerDetails.value.length} servers`,
939
- onClick: (e) => {
940
- e.stopPropagation();
941
- serversDropdownTriggerRect.value = e.currentTarget.getBoundingClientRect();
942
- serversDropdownOpen.value = !serversDropdownOpen.value;
943
- servicesDropdownOpen.value = false;
944
- }
945
- }, [
946
- h2("span", { class: ["cli-panel-status-dot", statusServerState.value === "connected" ? "dot-idle" : "dot-error"].join(" ") })
947
- ]) : null,
948
- // Uptime
949
- statusUptime.value > 0 ? h2("span", { class: "cli-panel-status-compact-item status-muted", title: formattedUptime.value }, [
950
- h2("span", { class: "cli-panel-status-icon", innerHTML: "&uarr;" })
951
- ]) : null,
952
- // Notification
953
- notification.value ? h2("span", { class: "cli-panel-status-compact-item", title: notification.value.message }, [
954
- h2("span", { class: `cli-panel-status-compact-dot level-${notification.value.level}` })
955
- ]) : null
956
- ].filter(Boolean)),
278
+ h2("p", { class: "cli-panel-title" }, [h2("span", { class: "cli-panel-title-icon" }, [icon(TERMINAL_ICON)]), PANEL_LABELS.title]),
279
+ s.showStatusIndicators ? h2(
280
+ "div",
281
+ { class: "cli-panel-status-indicators" },
282
+ indicators.map(
283
+ (item) => h2(
284
+ "span",
285
+ {
286
+ key: item.key,
287
+ class: joinClasses("cli-panel-status-item", item.itemClass),
288
+ onClick: item.dropdown ? (e) => openDropdown(e, item.dropdown) : void 0
289
+ },
290
+ [
291
+ item.dotClass ? h2("span", { class: `cli-panel-status-dot ${item.dotClass}` }) : null,
292
+ item.icon ? h2("span", { class: joinClasses("cli-panel-status-icon", item.iconClass) }, item.icon) : null,
293
+ h2("span", { class: "cli-panel-status-label" }, item.label)
294
+ ]
295
+ )
296
+ )
297
+ ) : null,
298
+ // Compact indicators for left/right positions (CSS controls visibility).
299
+ h2(
300
+ "div",
301
+ { class: "cli-panel-status-indicators-compact" },
302
+ indicators.map(
303
+ (item) => h2(
304
+ "span",
305
+ {
306
+ key: item.key,
307
+ class: joinClasses("cli-panel-status-compact-item", item.compactClass),
308
+ title: item.title,
309
+ onClick: item.dropdown ? (e) => openDropdown(e, item.dropdown) : void 0
310
+ },
311
+ [
312
+ item.compactDotClass ? h2("span", { class: item.compactDotClass }) : null,
313
+ item.dotClass ? h2("span", { class: `cli-panel-status-dot ${item.dotClass}` }) : null,
314
+ item.icon ? h2("span", { class: joinClasses("cli-panel-status-icon", item.iconClass) }, item.icon) : null
315
+ ]
316
+ )
317
+ )
318
+ ),
957
319
  h2("div", { class: "cli-panel-action-buttons" }, [
958
- h2("div", {
959
- class: "cli-panel-btn-position-wrapper",
960
- onClick: (e) => e.stopPropagation()
961
- }, [
962
- h2("button", {
963
- class: "cli-panel-btn cli-panel-btn-position",
964
- title: "Move panel",
965
- onClick: togglePositionDropdown
966
- }, [positionIcon(resolvedPosition.value)]),
967
- positionDropdownOpen.value ? h2(
320
+ h2("div", { class: "cli-panel-btn-position-wrapper", onClick: stop }, [
321
+ h2(
322
+ "button",
323
+ {
324
+ class: "cli-panel-btn cli-panel-btn-position",
325
+ title: PANEL_LABELS.movePanel,
326
+ onClick: () => controller.togglePositionDropdown()
327
+ },
328
+ [icon(positionIcon(s.position))]
329
+ ),
330
+ s.positionDropdownOpen ? h2(
968
331
  "div",
969
332
  { class: "cli-panel-position-dropdown" },
970
- ["bottom", "top", "left", "right"].map(
971
- (pos) => h2("button", {
972
- class: ["cli-panel-position-dropdown-item", resolvedPosition.value === pos ? "active" : ""].filter(Boolean).join(" "),
973
- onClick: () => selectPosition(pos)
974
- }, [
975
- positionIcon(pos, 16),
976
- pos.charAt(0).toUpperCase() + pos.slice(1)
977
- ])
333
+ positionOptions(s).map(
334
+ (opt) => h2(
335
+ "button",
336
+ { key: opt.position, class: opt.className, onClick: () => controller.setPosition(opt.position) },
337
+ [icon(positionIcon(opt.position, 16)), opt.label]
338
+ )
978
339
  )
979
340
  ) : null
980
341
  ]),
981
- hideable.value ? h2("button", {
982
- class: "cli-panel-btn cli-panel-btn-hide",
983
- title: "Hide",
984
- onClick: handleHide
985
- }, [hideIcon()]) : null,
342
+ s.hideable ? h2("button", { class: "cli-panel-btn cli-panel-btn-hide", title: PANEL_LABELS.hide, onClick: () => controller.hide() }, [icon(HIDE_ICON)]) : null,
986
343
  h2(
987
344
  "button",
988
345
  {
989
346
  class: "cli-panel-btn",
990
- title: resolvedMaximized.value ? "Restore" : "Maximize",
991
- disabled: resolvedCollapsed.value,
992
- onClick: toggleMaximize
347
+ title: s.maximized ? PANEL_LABELS.restore : PANEL_LABELS.maximize,
348
+ disabled: s.collapsed,
349
+ onClick: () => controller.toggleMaximize()
993
350
  },
994
- [resolvedMaximized.value ? restoreIcon() : maximizeIcon()]
351
+ [icon(s.maximized ? RESTORE_ICON : MAXIMIZE_ICON)]
995
352
  ),
996
353
  h2(
997
354
  "button",
998
355
  {
999
356
  class: "cli-panel-btn",
1000
- title: resolvedCollapsed.value ? "Expand" : "Collapse",
1001
- onClick: toggle
357
+ title: s.collapsed ? PANEL_LABELS.expand : PANEL_LABELS.collapse,
358
+ onClick: () => controller.toggle()
1002
359
  },
1003
- [collapseChevron(resolvedPosition.value, resolvedCollapsed.value)]
360
+ [icon(collapseChevronIcon(s.position, s.collapsed))]
1004
361
  ),
1005
- closable.value ? h2(
1006
- "button",
1007
- {
1008
- class: "cli-panel-btn cli-panel-btn-close",
1009
- title: "Close",
1010
- onClick: handleClose
1011
- },
1012
- [closeIcon()]
1013
- ) : null
362
+ s.closable ? h2("button", { class: "cli-panel-btn cli-panel-btn-close", title: PANEL_LABELS.close, onClick: () => controller.close() }, [icon(CLOSE_ICON)]) : null
1014
363
  ])
1015
364
  ]),
1016
- h2("div", { class: ["cli-panel-glow-line", statusExecutionState.value === "running" ? "glow-active" : ""].filter(Boolean).join(" ") })
365
+ h2("div", { class: glowLineClass(s) })
1017
366
  ]);
1018
- const contentEl = initialized.value ? h2("div", { class: "cli-panel-content", style: resolvedCollapsed.value ? { display: "none" } : void 0 }, [
1019
- // Tab bar
1020
- h2("div", { class: "cli-panel-tabs" }, [
367
+ };
368
+ const renderTabs = (s) => h2("div", { class: "cli-panel-tabs" }, [
369
+ h2(
370
+ "ul",
371
+ { class: "cli-panel-tab-list" },
372
+ s.tabs.map(
373
+ (tab) => h2(
374
+ "li",
375
+ {
376
+ key: tab.id,
377
+ class: panelTabClass(s, tab),
378
+ onClick: () => controller.selectTab(tab.id),
379
+ onDblclick: () => controller.startRename(tab.id),
380
+ onContextmenu: (e) => {
381
+ e.preventDefault();
382
+ e.stopPropagation();
383
+ controller.openContextMenu(tab.id, e.clientX, e.clientY);
384
+ }
385
+ },
386
+ tab.isEditing ? [
387
+ h2("input", {
388
+ class: "cli-panel-tab-rename-input",
389
+ type: "text",
390
+ value: tab.title,
391
+ onVnodeMounted: (vnode) => {
392
+ const input = vnode.el;
393
+ input.focus();
394
+ input.select();
395
+ },
396
+ onKeydown: (e) => handleRenameKey(controller, tab.id, e.key, e.target.value),
397
+ onBlur: (e) => controller.commitRename(tab.id, e.target.value),
398
+ onClick: stop,
399
+ onDblclick: stop
400
+ })
401
+ ] : [
402
+ h2("span", { class: panelTabDotClass(s, tab) }),
403
+ h2("span", { class: "cli-panel-tab-title" }, tab.title),
404
+ h2(
405
+ "button",
406
+ {
407
+ class: "cli-panel-tab-close-btn",
408
+ title: PANEL_LABELS.closeTab,
409
+ onClick: (e) => {
410
+ e.stopPropagation();
411
+ controller.closeTab(tab.id);
412
+ }
413
+ },
414
+ CLOSE_GLYPH
415
+ )
416
+ ]
417
+ )
418
+ )
419
+ ),
420
+ h2("button", { class: "cli-panel-add-tab", title: PANEL_LABELS.newTerminal, onClick: () => controller.addTab() }, "+")
421
+ ]);
422
+ const renderInstances = (s) => h2(
423
+ "div",
424
+ { class: "cli-panel-instances" },
425
+ s.tabs.map(
426
+ (tab) => h2("div", { key: tab.id, class: "cli-panel-instance", style: { display: tab.id === s.activeTabId ? void 0 : "none" } }, [
1021
427
  h2(
1022
- "ul",
1023
- { class: "cli-panel-tab-list" },
1024
- tabs.value.map(
1025
- (tab) => h2(
1026
- "li",
428
+ "div",
429
+ { class: panesContainerClass(s) },
430
+ tab.panes.flatMap((pane, i) => [
431
+ i > 0 ? h2(
432
+ "div",
1027
433
  {
1028
- key: tab.id,
1029
- class: [
1030
- "cli-panel-tab",
1031
- tab.id === resolvedActiveTabId.value && "active"
1032
- ].filter(Boolean).join(" "),
1033
- onClick: () => selectTab(tab.id),
1034
- onDblclick: () => startRename(tab.id),
1035
- onContextmenu: (e) => {
434
+ key: `divider-${pane.id}`,
435
+ class: "cli-panel-pane-divider",
436
+ onMousedown: (e) => {
1036
437
  e.preventDefault();
1037
- e.stopPropagation();
1038
- contextMenu.value = {
1039
- visible: true,
1040
- x: e.clientX,
1041
- y: e.clientY,
1042
- tabId: tab.id
1043
- };
438
+ controller.startPaneResize(tab.id, i - 1, e.clientX, panesContainerWidth(e.target));
1044
439
  }
1045
440
  },
1046
- tab.isEditing ? [
1047
- h2("input", {
1048
- class: "cli-panel-tab-rename-input",
1049
- type: "text",
1050
- value: tab.title,
1051
- onKeydown: (e) => {
1052
- if (e.key === "Enter")
1053
- commitRename(
1054
- tab.id,
1055
- e.target.value
1056
- );
1057
- else if (e.key === "Escape") {
1058
- tab.isEditing = false;
1059
- }
1060
- },
1061
- onBlur: (e) => commitRename(
1062
- tab.id,
1063
- e.target.value
1064
- ),
1065
- onClick: (e) => e.stopPropagation(),
1066
- onDblclick: (e) => e.stopPropagation()
1067
- })
1068
- ] : [
1069
- h2("span", { class: ["cli-panel-tab-dot", (() => {
1070
- if (tab.id !== resolvedActiveTabId.value) return "dot-idle";
1071
- if (statusExecutionState.value === "running") return "dot-running";
1072
- if (statusLastCommand.value && !statusLastCommand.value.success) return "dot-error";
1073
- return "dot-idle";
1074
- })()].join(" ") }),
1075
- h2(
1076
- "span",
1077
- {
1078
- class: "cli-panel-tab-title"
1079
- },
1080
- tab.title
1081
- ),
1082
- h2(
441
+ [h2("div", { class: "cli-panel-pane-divider-grip" })]
442
+ ) : null,
443
+ h2(
444
+ "div",
445
+ {
446
+ key: pane.id,
447
+ class: paneClass(s, tab.id, pane.id),
448
+ style: paneStyle(pane.widthPercent),
449
+ onClick: () => controller.activatePane(tab.id, pane.id)
450
+ },
451
+ [
452
+ tab.panes.length > 1 ? h2(
1083
453
  "button",
1084
454
  {
1085
- class: "cli-panel-tab-close-btn",
1086
- title: "Close tab",
455
+ class: "cli-panel-pane-close-btn",
456
+ title: PANEL_LABELS.closePane,
1087
457
  onClick: (e) => {
1088
458
  e.stopPropagation();
1089
- closeTab(
1090
- tab.id
1091
- );
459
+ controller.closePane(pane.id);
1092
460
  }
1093
461
  },
1094
- "\xD7"
1095
- )
462
+ CLOSE_GLYPH
463
+ ) : null,
464
+ h2(Cli, {
465
+ options: mergedOptions.value,
466
+ modules: hostConfig.value.modules,
467
+ processors: hostConfig.value.processors,
468
+ services: hostConfig.value.services,
469
+ snapshot: pane.snapshot,
470
+ style: { height: "100%" },
471
+ onReady: (engine) => controller.registerEngine(tab.id, pane.id, engine)
472
+ })
1096
473
  ]
1097
474
  )
1098
- )
1099
- ),
1100
- h2(
1101
- "button",
1102
- {
1103
- class: "cli-panel-add-tab",
1104
- title: "New terminal",
1105
- onClick: () => addTab()
1106
- },
1107
- "+"
475
+ ])
1108
476
  )
1109
- ]),
1110
- // Terminal instances
477
+ ])
478
+ )
479
+ );
480
+ const renderContextMenu = (s) => h2(
481
+ "div",
482
+ { class: "cli-panel-context-menu", style: contextMenuStyle(s), onClick: stop },
483
+ contextMenuItems(s).flatMap((item) => [
484
+ item.separatorBefore ? h2("div", { class: "cli-panel-context-menu-separator" }) : null,
1111
485
  h2(
1112
- "div",
1113
- { class: "cli-panel-instances" },
1114
- tabs.value.map(
1115
- (tab) => h2(
1116
- "div",
1117
- {
1118
- key: tab.id,
1119
- class: "cli-panel-instance",
1120
- style: {
1121
- display: tab.id === resolvedActiveTabId.value ? void 0 : "none"
1122
- }
1123
- },
1124
- [
1125
- h2(
1126
- "div",
1127
- {
1128
- class: [
1129
- "cli-panel-panes-container",
1130
- paneResizing.value && "resizing"
1131
- ].filter(Boolean).join(" ")
1132
- },
1133
- tab.panes.flatMap((pane, i) => {
1134
- const nodes = [];
1135
- if (i > 0) {
1136
- nodes.push(
1137
- h2(
1138
- "div",
1139
- {
1140
- class: "cli-panel-pane-divider",
1141
- onMousedown: (e) => onPaneResizeStart(
1142
- e,
1143
- tab.id,
1144
- i - 1
1145
- )
1146
- },
1147
- [
1148
- h2("div", {
1149
- class: "cli-panel-pane-divider-grip"
1150
- })
1151
- ]
1152
- )
1153
- );
1154
- }
1155
- nodes.push(
1156
- h2(
1157
- "div",
1158
- {
1159
- class: [
1160
- "cli-panel-pane",
1161
- pane.id === activePaneId.value && tab.id === resolvedActiveTabId.value && "active-pane"
1162
- ].filter(
1163
- Boolean
1164
- ).join(" "),
1165
- style: {
1166
- flex: `${pane.widthPercent} 1 0`
1167
- },
1168
- onClick: () => {
1169
- activePaneId.value = pane.id;
1170
- updateActiveTabId(tab.id);
1171
- }
1172
- },
1173
- [
1174
- tab.panes.length > 1 ? h2(
1175
- "button",
1176
- {
1177
- class: "cli-panel-pane-close-btn",
1178
- title: "Close pane",
1179
- onClick: (e) => {
1180
- e.stopPropagation();
1181
- closePane(
1182
- pane.id
1183
- );
1184
- }
1185
- },
1186
- "\xD7"
1187
- ) : null,
1188
- h2(Cli, {
1189
- options: mergedOptions.value,
1190
- modules: mergedModules.value,
1191
- processors: mergedProcessors.value,
1192
- services: mergedServices.value,
1193
- snapshot: pane.snapshot,
1194
- style: {
1195
- height: "100%"
1196
- },
1197
- onReady: (engine) => {
1198
- engineMap.set(pane.id, engine);
1199
- }
1200
- })
1201
- ]
1202
- )
1203
- );
1204
- return nodes;
1205
- })
1206
- )
1207
- ]
1208
- )
1209
- )
1210
- )
1211
- ]) : null;
1212
- const contextMenuEl = contextMenu.value.visible ? h2(
1213
- "div",
1214
- {
1215
- class: "cli-panel-context-menu",
1216
- style: {
1217
- left: `${contextMenu.value.x}px`,
1218
- top: `${contextMenu.value.y}px`
486
+ "button",
487
+ {
488
+ class: joinClasses("cli-panel-context-menu-item", item.destructive && "destructive"),
489
+ disabled: item.disabled,
490
+ onClick: () => runContextMenuAction(controller, item.action)
1219
491
  },
1220
- onClick: (e) => e.stopPropagation()
1221
- },
1222
- [
1223
- h2(
1224
- "button",
1225
- {
1226
- class: "cli-panel-context-menu-item",
1227
- onClick: () => {
1228
- closeContextMenu();
1229
- startRename(contextMenu.value.tabId);
1230
- }
1231
- },
1232
- "Rename"
1233
- ),
1234
- h2(
1235
- "button",
1236
- {
1237
- class: "cli-panel-context-menu-item",
1238
- onClick: () => {
1239
- const tab = tabs.value.find(
1240
- (t) => t.id === contextMenu.value.tabId
1241
- );
1242
- closeContextMenu();
1243
- if (!tab) return;
1244
- const sourceEngine = engineMap.get(tab.panes[0]?.id);
1245
- const snapshot = sourceEngine?.snapshot();
1246
- const paneId = nextPaneId++;
1247
- const tabId = nextTabId++;
1248
- const idx = tabs.value.indexOf(tab);
1249
- tabs.value.splice(idx + 1, 0, {
1250
- id: tabId,
1251
- title: `${tab.title} (copy)`,
1252
- isEditing: false,
1253
- panes: [
1254
- { id: paneId, widthPercent: 100, snapshot }
1255
- ]
1256
- });
1257
- updateActiveTabId(tabId);
1258
- activePaneId.value = paneId;
1259
- }
1260
- },
1261
- "Duplicate"
1262
- ),
1263
- h2(
1264
- "button",
1265
- {
1266
- class: "cli-panel-context-menu-item",
1267
- onClick: () => {
1268
- closeContextMenu();
1269
- splitPane(contextMenu.value.tabId);
1270
- }
1271
- },
1272
- "Split Right"
1273
- ),
1274
- h2("div", {
1275
- class: "cli-panel-context-menu-separator"
1276
- }),
1277
- h2(
1278
- "button",
1279
- {
1280
- class: "cli-panel-context-menu-item",
1281
- onClick: () => {
1282
- closeContextMenu();
1283
- closeTab(contextMenu.value.tabId);
1284
- }
1285
- },
1286
- "Close"
1287
- ),
1288
- h2(
1289
- "button",
1290
- {
1291
- class: "cli-panel-context-menu-item",
1292
- disabled: tabs.value.length <= 1,
1293
- onClick: () => {
1294
- const id = contextMenu.value.tabId;
1295
- closeContextMenu();
1296
- tabs.value = tabs.value.filter(
1297
- (t) => t.id === id
1298
- );
1299
- updateActiveTabId(id);
1300
- }
1301
- },
1302
- "Close Others"
1303
- ),
1304
- h2(
1305
- "button",
1306
- {
1307
- class: "cli-panel-context-menu-item",
1308
- onClick: () => {
1309
- const id = contextMenu.value.tabId;
1310
- closeContextMenu();
1311
- const idx = tabs.value.findIndex(
1312
- (t) => t.id === id
1313
- );
1314
- if (idx !== -1)
1315
- tabs.value = tabs.value.slice(
1316
- 0,
1317
- idx + 1
1318
- );
1319
- }
1320
- },
1321
- "Close to the Right"
1322
- ),
1323
- h2("div", {
1324
- class: "cli-panel-context-menu-separator"
1325
- }),
1326
- h2(
1327
- "button",
1328
- {
1329
- class: "cli-panel-context-menu-item destructive",
1330
- onClick: () => {
1331
- closeContextMenu();
1332
- tabs.value = [];
1333
- initialized.value = false;
1334
- updateCollapsed(true);
1335
- }
1336
- },
1337
- "Close All"
1338
- )
1339
- ]
1340
- ) : null;
1341
- const hideTabEl = resolvedHidden.value ? h2("button", {
1342
- class: "cli-panel-hide-tab",
1343
- "data-position": resolvedPosition.value,
1344
- "data-hide-align": hideAlignment.value,
1345
- style: themeStyles.value,
1346
- title: "Show CLI",
1347
- onClick: handleUnhide
1348
- }, [
1349
- h2("svg", { class: "cli-panel-hide-tab-icon", ...svgAttrs, width: "16", height: "16", "stroke-width": "2" }, [
1350
- h2("polyline", { points: "4 17 10 11 4 5" }),
1351
- h2("line", { x1: "12", y1: "19", x2: "20", y2: "19" })
1352
- ]),
1353
- h2("span", { class: "cli-panel-hide-tab-label" }, "CLI"),
1354
- hideTabChevron(resolvedPosition.value)
1355
- ]) : null;
1356
- const servicesDropdownEl = servicesDropdownOpen.value && statusServiceCount.value.total > 0 ? h2("div", {
1357
- class: "cli-panel-services-dropdown",
1358
- style: (() => {
1359
- const rect = servicesDropdownTriggerRect.value;
1360
- if (!rect) return {};
1361
- const base = { position: "fixed", zIndex: 1100 };
1362
- switch (resolvedPosition.value) {
1363
- case "top":
1364
- return { ...base, top: `${rect.bottom + 4}px`, left: `${rect.left}px` };
1365
- case "left":
1366
- return { ...base, top: `${rect.top}px`, left: `${rect.right + 4}px` };
1367
- case "right":
1368
- return { ...base, top: `${rect.top}px`, right: `${window.innerWidth - rect.left + 4}px` };
1369
- default:
1370
- return { ...base, bottom: `${window.innerHeight - rect.top + 4}px`, left: `${rect.left}px` };
1371
- }
1372
- })(),
1373
- onClick: (e) => e.stopPropagation()
1374
- }, [
1375
- h2("div", { class: "cli-panel-services-dropdown-header" }, "Background Services"),
1376
- h2(
1377
- "div",
1378
- { class: "cli-panel-services-dropdown-list" },
1379
- statusServiceDetails.value.map(
1380
- (svc, i) => h2("div", { key: i, class: "cli-panel-services-dropdown-item" }, [
1381
- h2("span", { class: ["cli-panel-svc-dot", svc.status === "running" ? "svc-running" : "svc-stopped"].join(" ") }),
1382
- h2("div", { class: "cli-panel-svc-info" }, [
1383
- h2("span", { class: "cli-panel-svc-name" }, svc.name),
1384
- svc.description ? h2("span", { class: "cli-panel-svc-desc" }, svc.description) : null
1385
- ].filter(Boolean)),
1386
- h2("span", { class: "cli-panel-svc-status" }, svc.status)
1387
- ])
1388
- )
1389
- ),
1390
- statusServiceDetails.value.length === 0 ? h2("div", { class: "cli-panel-services-dropdown-empty" }, "No services registered") : null
1391
- ].filter(Boolean)) : null;
1392
- const serversDropdownEl = serversDropdownOpen.value && statusServerState.value !== "none" ? h2("div", {
1393
- class: "cli-panel-services-dropdown",
1394
- style: (() => {
1395
- const rect = serversDropdownTriggerRect.value;
1396
- if (!rect) return {};
1397
- const base = { position: "fixed", zIndex: 1100 };
1398
- switch (resolvedPosition.value) {
1399
- case "top":
1400
- return { ...base, top: `${rect.bottom + 4}px`, left: `${rect.left}px` };
1401
- case "left":
1402
- return { ...base, top: `${rect.top}px`, left: `${rect.right + 4}px` };
1403
- case "right":
1404
- return { ...base, top: `${rect.top}px`, right: `${window.innerWidth - rect.left + 4}px` };
1405
- default:
1406
- return { ...base, bottom: `${window.innerHeight - rect.top + 4}px`, left: `${rect.left}px` };
1407
- }
1408
- })(),
1409
- onClick: (e) => e.stopPropagation()
1410
- }, [
1411
- h2("div", { class: "cli-panel-services-dropdown-header" }, "Server Connections"),
492
+ item.label
493
+ )
494
+ ])
495
+ );
496
+ const renderStatusDropdown = (s) => {
497
+ const dropdown = openStatusDropdown(s);
498
+ if (!dropdown) return null;
499
+ return h2("div", { class: "cli-panel-services-dropdown", style: dropdown.style, onClick: stop }, [
500
+ h2("div", { class: "cli-panel-services-dropdown-header" }, dropdown.header),
1412
501
  h2(
1413
502
  "div",
1414
503
  { class: "cli-panel-services-dropdown-list" },
1415
- statusServerDetails.value.map(
1416
- (srv, i) => h2("div", { key: i, class: "cli-panel-services-dropdown-item" }, [
1417
- h2("span", { class: ["cli-panel-svc-dot", srv.connected ? "svc-running" : "svc-stopped"].join(" ") }),
504
+ dropdown.rows.map(
505
+ (row) => h2("div", { key: row.key, class: "cli-panel-services-dropdown-item" }, [
506
+ h2("span", { class: row.dotClass }),
1418
507
  h2("div", { class: "cli-panel-svc-info" }, [
1419
- h2("span", { class: "cli-panel-svc-name" }, srv.name),
1420
- srv.url ? h2("span", { class: "cli-panel-svc-desc" }, srv.url) : null
1421
- ].filter(Boolean)),
1422
- h2("div", { class: "cli-panel-srv-meta" }, [
1423
- h2("span", { class: "cli-panel-svc-status" }, srv.connected ? "connected" : "disconnected"),
1424
- srv.apiVersion ? h2("span", { class: "cli-panel-svc-desc" }, `v${srv.apiVersion}`) : null,
1425
- srv.commandCount ? h2("span", { class: "cli-panel-svc-desc" }, `${srv.commandCount} cmds`) : null
1426
- ].filter(Boolean))
508
+ h2("span", { class: "cli-panel-svc-name" }, row.name),
509
+ row.description ? h2("span", { class: "cli-panel-svc-desc" }, row.description) : null
510
+ ]),
511
+ dropdown.kind === "servers" ? h2("div", { class: "cli-panel-srv-meta" }, [
512
+ h2("span", { class: "cli-panel-svc-status" }, row.status),
513
+ ...row.meta.map((m) => h2("span", { key: m, class: "cli-panel-svc-desc" }, m))
514
+ ]) : h2("span", { class: "cli-panel-svc-status" }, row.status)
1427
515
  ])
1428
516
  )
1429
517
  ),
1430
- statusServerDetails.value.length === 0 ? h2("div", { class: "cli-panel-services-dropdown-empty" }, "No servers configured") : null
1431
- ].filter(Boolean)) : null;
1432
- return h2("div", {}, [
1433
- hideTabEl,
518
+ dropdown.rows.length === 0 ? h2("div", { class: "cli-panel-services-dropdown-empty" }, dropdown.emptyText) : null
519
+ ]);
520
+ };
521
+ return () => {
522
+ const s = state.value;
523
+ if (!s.visible) return null;
524
+ return h2(Fragment, [
525
+ s.hidden ? h2(
526
+ "button",
527
+ {
528
+ class: "cli-panel-hide-tab",
529
+ "data-position": s.position,
530
+ "data-hide-align": s.hideAlignment,
531
+ style: s.themeStyles,
532
+ title: PANEL_LABELS.showCli,
533
+ onClick: () => controller.unhide()
534
+ },
535
+ [
536
+ icon(HIDE_TAB_TERMINAL_ICON, "cli-panel-hide-tab-icon"),
537
+ h2("span", { class: "cli-panel-hide-tab-label" }, PANEL_LABELS.title),
538
+ icon(hideTabChevronIcon(s.position), "cli-panel-hide-tab-arrow")
539
+ ]
540
+ ) : null,
1434
541
  h2(
1435
542
  "div",
1436
543
  {
1437
- class: wrapperClass,
1438
- style: wrapperStyle,
1439
- "data-position": resolvedPosition.value,
1440
- "data-resizable": String(resizable.value),
1441
- "data-closable": String(closable.value),
1442
- "data-hideable": String(hideable.value)
544
+ ref: root,
545
+ class: panelWrapperClass(s, props.class),
546
+ style: { ...panelWrapperStyle(s), ...props.style },
547
+ "data-position": s.position,
548
+ "data-resizable": String(s.resizable),
549
+ "data-closable": String(s.closable),
550
+ "data-hideable": String(s.hideable)
1443
551
  },
1444
- [headerEl, contentEl]
552
+ [
553
+ renderHeader(s),
554
+ s.initialized ? h2("div", { class: "cli-panel-content", style: s.collapsed ? { display: "none" } : void 0 }, [
555
+ renderTabs(s),
556
+ renderInstances(s)
557
+ ]) : null
558
+ ]
1445
559
  ),
1446
- contextMenuEl,
1447
- servicesDropdownEl,
1448
- serversDropdownEl
560
+ s.contextMenu.visible ? renderContextMenu(s) : null,
561
+ renderStatusDropdown(s)
1449
562
  ]);
1450
563
  };
1451
564
  }
1452
565
  });
1453
566
 
1454
567
  // src/CliProvider.ts
1455
- import { defineComponent as defineComponent4, ref as ref4, h as h3, provide as provide2 } from "vue";
568
+ import { defineComponent as defineComponent4, ref as ref2, h as h3, provide as provide2 } from "vue";
1456
569
 
1457
570
  // src/useCliEngine.ts
1458
- import { onMounted as onMounted3, onBeforeUnmount as onBeforeUnmount3, shallowRef } from "vue";
1459
- import { CliEngine as CliEngine2 } from "@qodalis/cli";
571
+ import { onMounted as onMounted3, onBeforeUnmount as onBeforeUnmount3, shallowRef as shallowRef2 } from "vue";
572
+ import { createCliEngine as createCliEngine2 } from "@qodalis/cli";
1460
573
  function useCliEngine(containerRef, config) {
1461
- const engine = shallowRef(null);
574
+ const engine = shallowRef2(null);
1462
575
  onMounted3(async () => {
1463
576
  if (!containerRef.value) return;
1464
- const e = new CliEngine2(containerRef.value, config?.options);
1465
- e.registerService("cli-framework", "Vue");
1466
- if (config?.services) {
1467
- for (const [token, value] of Object.entries(config.services)) {
1468
- e.registerService(token, value);
1469
- }
1470
- }
1471
- if (config?.modules) {
1472
- e.registerModules(config.modules);
1473
- }
1474
- if (config?.processors) {
1475
- e.registerProcessors(config.processors);
1476
- }
577
+ const e = createCliEngine2(containerRef.value, { ...config, framework: "Vue" });
1477
578
  await e.start();
1478
579
  engine.value = e;
1479
580
  });
@@ -1511,7 +612,7 @@ var CliProvider = defineComponent4({
1511
612
  },
1512
613
  emits: ["ready"],
1513
614
  setup(props, { slots, emit }) {
1514
- const containerRef = ref4(null);
615
+ const containerRef = ref2(null);
1515
616
  const engine = useCliEngine(containerRef, {
1516
617
  modules: props.modules,
1517
618
  processors: props.processors,
@@ -1542,12 +643,12 @@ function useCli() {
1542
643
  }
1543
644
 
1544
645
  // src/index.ts
1545
- import { CliEngine as CliEngine3 } from "@qodalis/cli";
646
+ import { CliEngine as CliEngine4 } from "@qodalis/cli";
1546
647
  export {
1547
648
  Cli,
1548
649
  CliConfigKey,
1549
650
  CliConfigProvider,
1550
- CliEngine3 as CliEngine,
651
+ CliEngine4 as CliEngine,
1551
652
  CliInjectionKey,
1552
653
  CliPanel,
1553
654
  CliProvider,