@pellux/goodvibes-tui 1.1.0 → 1.9.0
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/CHANGELOG.md +85 -21
- package/README.md +28 -18
- package/docs/foundation-artifacts/operator-contract.json +4045 -1763
- package/package.json +2 -2
- package/src/audio/spoken-turn-controller.ts +12 -2
- package/src/audio/spoken-turn-wiring.ts +2 -1
- package/src/cli/help.ts +8 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/cli/status.ts +2 -1
- package/src/cli/surface-command.ts +2 -2
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/composer-state.ts +11 -3
- package/src/core/conversation-line-cache.ts +27 -3
- package/src/core/conversation-rendering.ts +90 -14
- package/src/core/conversation.ts +18 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/stream-event-wiring.ts +20 -1
- package/src/core/system-message-noise.ts +87 -0
- package/src/core/system-message-router.ts +104 -51
- package/src/core/turn-cancellation.ts +25 -0
- package/src/core/turn-event-wiring.ts +72 -3
- package/src/daemon/cli.ts +29 -2
- package/src/daemon/handlers/register.ts +8 -1
- package/src/daemon/service-commands.ts +329 -0
- package/src/export/cost-utils.ts +36 -0
- package/src/input/autocomplete.ts +27 -1
- package/src/input/command-registry.ts +84 -5
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +232 -0
- package/src/input/commands/config.ts +43 -3
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +10 -2
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +73 -35
- package/src/input/commands/operator-panel-runtime.ts +71 -31
- package/src/input/commands/planning-runtime.ts +116 -4
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +26 -6
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime-setup.ts +5 -3
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-content.ts +20 -9
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +16 -4
- package/src/input/commands/shell-core.ts +25 -8
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +488 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +175 -0
- package/src/input/config-modal.ts +592 -0
- package/src/input/feed-context-factory.ts +10 -8
- package/src/input/handler-command-route.ts +37 -20
- package/src/input/handler-content-actions.ts +17 -2
- package/src/input/handler-feed-routes.ts +74 -114
- package/src/input/handler-feed.ts +79 -27
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +136 -6
- package/src/input/handler-modal-stack.ts +14 -7
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
- package/src/input/handler-onboarding.ts +71 -59
- package/src/input/handler-picker-routes.ts +26 -102
- package/src/input/handler-shortcuts.ts +74 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +12 -29
- package/src/input/keybindings.ts +23 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +77 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/panel-mouse-geometry.ts +97 -0
- package/src/input/panel-paste-flood-guard.ts +86 -0
- package/src/input/selection-modal.ts +11 -0
- package/src/input/session-picker-modal.ts +44 -4
- package/src/input/settings-modal-data.ts +146 -2
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/input/settings-modal-types.ts +4 -2
- package/src/main.ts +49 -51
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +18 -46
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +36 -10
- package/src/panels/diff-panel.ts +12 -43
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-deep-link.ts +31 -0
- package/src/panels/fleet-panel-format.ts +62 -0
- package/src/panels/fleet-panel-worktree-detail.ts +48 -0
- package/src/panels/fleet-panel.ts +758 -0
- package/src/panels/fleet-read-model.ts +520 -0
- package/src/panels/fleet-steer.ts +125 -0
- package/src/panels/fleet-stop.ts +153 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/index.ts +7 -31
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +166 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +42 -0
- package/src/panels/modals/pairing-modal.ts +163 -0
- package/src/panels/modals/planning-modal.ts +304 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +177 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +123 -9
- package/src/panels/polish-core.ts +38 -25
- package/src/panels/project-planning-answer-actions.ts +8 -11
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +37 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +216 -13
- package/src/renderer/autocomplete-overlay.ts +28 -2
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/compositor.ts +2 -3
- package/src/renderer/config-modal.ts +95 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +15 -3
- package/src/renderer/fullscreen-primitives.ts +32 -22
- package/src/renderer/git-status.ts +43 -1
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/layout.ts +0 -4
- package/src/renderer/markdown.ts +7 -3
- package/src/renderer/modal-factory.ts +25 -20
- package/src/renderer/model-workspace.ts +62 -4
- package/src/renderer/overlay-box.ts +21 -17
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/process-indicator.ts +14 -3
- package/src/renderer/selection-modal-overlay.ts +6 -1
- package/src/renderer/session-picker-modal.ts +196 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +7 -0
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/status-glyphs.ts +14 -15
- package/src/renderer/system-message.ts +15 -3
- package/src/renderer/terminal-bg-probe.ts +339 -0
- package/src/renderer/terminal-escapes.ts +20 -0
- package/src/renderer/theme-mode-config.ts +67 -0
- package/src/renderer/theme.ts +91 -1
- package/src/renderer/thinking.ts +11 -3
- package/src/renderer/tool-call.ts +15 -9
- package/src/renderer/tool-result-summary.ts +148 -0
- package/src/renderer/turn-injection.ts +133 -0
- package/src/renderer/ui-factory.ts +154 -85
- package/src/renderer/ui-primitives.ts +30 -129
- package/src/runtime/bootstrap-command-context.ts +27 -1
- package/src/runtime/bootstrap-command-parts.ts +41 -10
- package/src/runtime/bootstrap-core.ts +47 -15
- package/src/runtime/bootstrap-hook-bridge.ts +7 -0
- package/src/runtime/bootstrap-shell.ts +48 -17
- package/src/runtime/bootstrap.ts +129 -22
- package/src/runtime/code-index-services.ts +135 -0
- package/src/runtime/legacy-daemon-migration.ts +516 -0
- package/src/runtime/memory-fold.ts +26 -0
- package/src/runtime/onboarding/derivation.ts +7 -2
- package/src/runtime/onboarding/snapshot.ts +27 -1
- package/src/runtime/onboarding/types.ts +31 -1
- package/src/runtime/operator-token-cleanup.ts +82 -1
- package/src/runtime/orchestrator-core-services.ts +59 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/resume-notice.ts +209 -0
- package/src/runtime/services.ts +98 -46
- package/src/runtime/session-inbound-inputs.ts +252 -0
- package/src/runtime/session-spine-transport.ts +64 -0
- package/src/runtime/terminal-output-guard.ts +15 -8
- package/src/runtime/ui-services.ts +27 -3
- package/src/runtime/workstream-services.ts +327 -0
- package/src/runtime/wrfc-persistence.ts +124 -17
- package/src/shell/blocking-input.ts +68 -4
- package/src/shell/recovery-input-helpers.ts +170 -1
- package/src/shell/ui-openers.ts +171 -26
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/utils/terminal-width.ts +52 -0
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -515
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -566
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -496
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -1,534 +0,0 @@
|
|
|
1
|
-
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
-
import { BasePanel } from './base-panel.ts';
|
|
4
|
-
import type { PanelIntegrationContext } from './types.ts';
|
|
5
|
-
import type { UiReadModel, UiRemoteSnapshot } from '../runtime/ui-read-models.ts';
|
|
6
|
-
import {
|
|
7
|
-
buildDetailBlock,
|
|
8
|
-
buildEmptyState,
|
|
9
|
-
buildPanelListRow,
|
|
10
|
-
buildPanelLine,
|
|
11
|
-
buildSummaryBlock,
|
|
12
|
-
buildPanelWorkspace,
|
|
13
|
-
DEFAULT_PANEL_PALETTE,
|
|
14
|
-
resolvePrimaryScrollableSection,
|
|
15
|
-
type PanelWorkspaceSection,
|
|
16
|
-
} from './polish.ts';
|
|
17
|
-
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
18
|
-
import { getTrackedVisibleWindow } from '../renderer/surface-layout.ts';
|
|
19
|
-
|
|
20
|
-
// Base chrome only — title band, state colors, and text tokens all come
|
|
21
|
-
// straight from DEFAULT_PANEL_PALETTE (WO-002).
|
|
22
|
-
const C = DEFAULT_PANEL_PALETTE;
|
|
23
|
-
|
|
24
|
-
function stateColor(state: string): string {
|
|
25
|
-
switch (state) {
|
|
26
|
-
case 'connected':
|
|
27
|
-
case 'syncing':
|
|
28
|
-
return C.good;
|
|
29
|
-
case 'degraded':
|
|
30
|
-
case 'reconnecting':
|
|
31
|
-
case 'authenticating':
|
|
32
|
-
case 'initializing':
|
|
33
|
-
return C.warn;
|
|
34
|
-
case 'terminal_failure':
|
|
35
|
-
return C.bad;
|
|
36
|
-
default:
|
|
37
|
-
return C.dim;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function formatTimestamp(value?: number): string {
|
|
42
|
-
return value ? new Date(value).toLocaleTimeString() : 'n/a';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function truncate(text: string, width: number): string {
|
|
46
|
-
return truncateDisplay(text, width);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Splits a machine-generated slash-command string (e.g. `/remote show foo`)
|
|
50
|
-
// into the { name, args } shape ctx.executeCommand expects — the same
|
|
51
|
-
// leading-slash-stripping approach panel-integration-actions.ts uses for
|
|
52
|
-
// ApprovalPanel's dispatched command.
|
|
53
|
-
function parseCommand(command: string): { name: string; args: string[] } | null {
|
|
54
|
-
const parts = command.replace(/^\//, '').split(/\s+/).filter(Boolean);
|
|
55
|
-
const [name, ...args] = parts;
|
|
56
|
-
return name ? { name, args } : null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Mirrors RemoteSupervisor's own stateIsDegraded()/degradedConnections
|
|
60
|
-
// definition (sdk/platform/runtime/remote/supervisor.js) so "degraded/stale"
|
|
61
|
-
// means the same thing here as it does in the posture summary above.
|
|
62
|
-
function isSupervisorEntryDegraded(entry: { transportState: string; heartbeat: { status: string } }): boolean {
|
|
63
|
-
const degradedTransport = entry.transportState === 'degraded'
|
|
64
|
-
|| entry.transportState === 'reconnecting'
|
|
65
|
-
|| entry.transportState === 'terminal_failure';
|
|
66
|
-
return degradedTransport || entry.heartbeat.status !== 'fresh';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export class RemotePanel extends BasePanel {
|
|
70
|
-
private readonly readModel?: UiReadModel<UiRemoteSnapshot>;
|
|
71
|
-
private readonly unsub: (() => void) | null;
|
|
72
|
-
private selectedIndex = 0;
|
|
73
|
-
private scrollOffset = 0;
|
|
74
|
-
private browseMode: 'connections' | 'contracts' = 'connections';
|
|
75
|
-
// Set by handleInput (enter/r) and consumed on the very next
|
|
76
|
-
// handlePanelIntegrationAction dispatch of that same key, mirroring the
|
|
77
|
-
// token-budget-panel pattern — handleInput has no ctx.executeCommand.
|
|
78
|
-
private pendingCommand: { name: string; args: string[] } | null = null;
|
|
79
|
-
|
|
80
|
-
public constructor(readModel?: UiReadModel<UiRemoteSnapshot>) {
|
|
81
|
-
super('remote', 'Remote', '▰', 'providers');
|
|
82
|
-
this.readModel = readModel;
|
|
83
|
-
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
public override onDestroy(): void {
|
|
87
|
-
this.unsub?.();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
public handleInput(key: string): boolean {
|
|
91
|
-
// r = dispatch /remote recover for whatever is currently selected (or
|
|
92
|
-
// the runtime's first non-fresh session when nothing is selected).
|
|
93
|
-
if (this.readModel && key === 'r') {
|
|
94
|
-
const runnerId = this.getSelectedRunnerId();
|
|
95
|
-
const parsed = parseCommand(runnerId ? `/remote recover ${runnerId}` : '/remote recover');
|
|
96
|
-
if (parsed) {
|
|
97
|
-
this.pendingCommand = parsed;
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// Enter on a degraded/stale supervisor selection dispatches its
|
|
102
|
-
// top recovery action (already machine-readable via .recovery[0].command)
|
|
103
|
-
// instead of requiring the operator to retype it.
|
|
104
|
-
if (this.readModel && (key === 'enter' || key === 'return')) {
|
|
105
|
-
const entry = this.getSelectedSupervisorEntry();
|
|
106
|
-
if (entry && isSupervisorEntryDegraded(entry) && entry.recovery.length > 0) {
|
|
107
|
-
const parsed = parseCommand(entry.recovery[0].command);
|
|
108
|
-
if (parsed) {
|
|
109
|
-
this.pendingCommand = parsed;
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
const activeConnections = this.getActiveConnections();
|
|
115
|
-
const contracts = this.readModel?.getSnapshot().contracts ?? [];
|
|
116
|
-
const browseCount = this.browseMode === 'connections' && activeConnections.length > 0
|
|
117
|
-
? activeConnections.length
|
|
118
|
-
: contracts.length;
|
|
119
|
-
if (key === 'tab' && contracts.length > 0) {
|
|
120
|
-
this.browseMode = this.browseMode === 'connections' ? 'contracts' : 'connections';
|
|
121
|
-
this.selectedIndex = 0;
|
|
122
|
-
this.markDirty();
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
if (browseCount === 0) return false;
|
|
126
|
-
if (key === 'up' || key === 'k') {
|
|
127
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
128
|
-
this.markDirty();
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
if (key === 'down' || key === 'j') {
|
|
132
|
-
this.selectedIndex = Math.min(browseCount - 1, this.selectedIndex + 1);
|
|
133
|
-
this.markDirty();
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
if (key === 'home') {
|
|
137
|
-
this.selectedIndex = 0;
|
|
138
|
-
this.markDirty();
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
if (key === 'end') {
|
|
142
|
-
this.selectedIndex = Math.max(0, browseCount - 1);
|
|
143
|
-
this.markDirty();
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
private getActiveConnections() {
|
|
150
|
-
return this.readModel?.getSnapshot().acp.activeConnections ?? [];
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Runner id backing the currently browsed row, whichever browse mode is
|
|
154
|
-
// active — shared by the Enter/r dispatch logic and render()'s own
|
|
155
|
-
// selection lookups so they never drift apart.
|
|
156
|
-
private getSelectedRunnerId(): string | null {
|
|
157
|
-
const snapshot = this.readModel?.getSnapshot();
|
|
158
|
-
if (!snapshot) return null;
|
|
159
|
-
const activeConnections = this.getActiveConnections();
|
|
160
|
-
const contracts = snapshot.contracts;
|
|
161
|
-
const viewingConnections = this.browseMode === 'connections' && activeConnections.length > 0;
|
|
162
|
-
// selectedIndex indexes only the mode's active list (activeConnections when
|
|
163
|
-
// viewing connections, otherwise contracts); .at() keeps that read off any
|
|
164
|
-
// raw source array (no-raw-selectedindex-read rule).
|
|
165
|
-
if (viewingConnections) {
|
|
166
|
-
return activeConnections.at(this.selectedIndex)?.agentId ?? null;
|
|
167
|
-
}
|
|
168
|
-
return contracts.at(this.selectedIndex)?.runnerId ?? null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
private getSelectedSupervisorEntry() {
|
|
172
|
-
const snapshot = this.readModel?.getSnapshot();
|
|
173
|
-
if (!snapshot) return null;
|
|
174
|
-
const runnerId = this.getSelectedRunnerId();
|
|
175
|
-
if (!runnerId) return null;
|
|
176
|
-
return snapshot.supervisor.sessions.find((entry) => entry.runnerId === runnerId) ?? null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
public handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
|
|
180
|
-
if (!this.pendingCommand) return false;
|
|
181
|
-
const { name, args } = this.pendingCommand;
|
|
182
|
-
this.pendingCommand = null;
|
|
183
|
-
void ctx.executeCommand?.(name, args);
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
public render(width: number, height: number): Line[] {
|
|
188
|
-
this.needsRender = false;
|
|
189
|
-
const intro = 'Bridge, ACP, runner-contract, and artifact posture for self-hosted remote work.';
|
|
190
|
-
|
|
191
|
-
if (!this.readModel) {
|
|
192
|
-
const sectionLines = buildEmptyState(
|
|
193
|
-
width,
|
|
194
|
-
' Runtime store not wired into this panel yet.',
|
|
195
|
-
'The remote control room needs the shell read model so it can display ACP state, runner contracts, and replay artifacts.',
|
|
196
|
-
[
|
|
197
|
-
{ command: '/remote setup', summary: 'review bootstrap, env, tunnel, and bridge guidance' },
|
|
198
|
-
{ command: '/remote panel', summary: 'reopen the panel from the shell-owned runtime' },
|
|
199
|
-
],
|
|
200
|
-
C,
|
|
201
|
-
);
|
|
202
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
203
|
-
title: 'Remote Control Room',
|
|
204
|
-
intro,
|
|
205
|
-
sections: [{ lines: sectionLines }],
|
|
206
|
-
palette: C,
|
|
207
|
-
});
|
|
208
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
209
|
-
return lines;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const snapshot = this.readModel.getSnapshot();
|
|
213
|
-
const daemon = snapshot.daemon;
|
|
214
|
-
const acp = snapshot.acp;
|
|
215
|
-
const activeConnections = this.getActiveConnections();
|
|
216
|
-
const artifactCount = snapshot.artifacts.length;
|
|
217
|
-
const pools = snapshot.pools;
|
|
218
|
-
const contracts = snapshot.contracts;
|
|
219
|
-
const supervisor = snapshot.supervisor;
|
|
220
|
-
const distributed = {
|
|
221
|
-
pairRequests: { pending: snapshot.distributed.pairRequests.length },
|
|
222
|
-
peers: {
|
|
223
|
-
total: snapshot.distributed.peers.length,
|
|
224
|
-
connected: snapshot.distributed.peers.filter((peer) => peer.status === 'connected').length,
|
|
225
|
-
nodes: snapshot.distributed.peers.filter((peer) => peer.kind === 'node').length,
|
|
226
|
-
devices: snapshot.distributed.peers.filter((peer) => peer.kind === 'device').length,
|
|
227
|
-
},
|
|
228
|
-
work: {
|
|
229
|
-
queued: snapshot.distributed.work.filter((item) => item.status === 'queued').length,
|
|
230
|
-
claimed: snapshot.distributed.work.filter((item) => item.status === 'claimed').length,
|
|
231
|
-
},
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
const postureLines: Line[] = [
|
|
235
|
-
buildPanelLine(width, [
|
|
236
|
-
[' daemon ', C.label],
|
|
237
|
-
[daemon.transportState.toUpperCase(), stateColor(daemon.transportState)],
|
|
238
|
-
[' running ', C.label],
|
|
239
|
-
[daemon.isRunning ? 'yes' : 'no', daemon.isRunning ? C.good : C.dim],
|
|
240
|
-
[' reconnects ', C.label],
|
|
241
|
-
[String(daemon.reconnectAttempts), daemon.reconnectAttempts > 0 ? C.warn : C.good],
|
|
242
|
-
[' jobs ', C.label],
|
|
243
|
-
[String(daemon.runningJobCount), daemon.runningJobCount > 0 ? C.info : C.dim],
|
|
244
|
-
]),
|
|
245
|
-
buildPanelLine(width, [
|
|
246
|
-
[' ACP manager ', C.label],
|
|
247
|
-
[acp.transportState.toUpperCase(), stateColor(acp.transportState)],
|
|
248
|
-
[' active connections ', C.label],
|
|
249
|
-
[String(acp.activeConnections.length), acp.activeConnections.length > 0 ? C.info : C.dim],
|
|
250
|
-
[' total messages ', C.label],
|
|
251
|
-
[String(acp.totalMessages), acp.totalMessages > 0 ? C.value : C.dim],
|
|
252
|
-
]),
|
|
253
|
-
buildPanelLine(width, [
|
|
254
|
-
[' runner contracts ', C.label],
|
|
255
|
-
[String(contracts.length), C.info],
|
|
256
|
-
[' pools ', C.label],
|
|
257
|
-
[String(pools.length), pools.length > 0 ? C.info : C.dim],
|
|
258
|
-
[' review artifacts ', C.label],
|
|
259
|
-
[String(artifactCount), artifactCount > 0 ? C.good : C.dim],
|
|
260
|
-
]),
|
|
261
|
-
buildPanelLine(width, [
|
|
262
|
-
[' supervisor ', C.label],
|
|
263
|
-
[String(supervisor.sessions.length), C.info],
|
|
264
|
-
[' degraded ', C.label],
|
|
265
|
-
[String(supervisor.degradedConnections), supervisor.degradedConnections > 0 ? C.warn : C.good],
|
|
266
|
-
[' distributed peers ', C.label],
|
|
267
|
-
[String(distributed.peers?.total ?? 0), C.info],
|
|
268
|
-
[' connected ', C.label],
|
|
269
|
-
[String(distributed.peers?.connected ?? 0), (distributed.peers?.connected ?? 0) > 0 ? C.good : C.dim],
|
|
270
|
-
[' queued work ', C.label],
|
|
271
|
-
[String(distributed.work?.queued ?? 0), (distributed.work?.queued ?? 0) > 0 ? C.info : C.dim],
|
|
272
|
-
]),
|
|
273
|
-
];
|
|
274
|
-
|
|
275
|
-
if (daemon.lastError) {
|
|
276
|
-
postureLines.push(buildPanelLine(width, [
|
|
277
|
-
[' daemon error ', C.label],
|
|
278
|
-
[truncateDisplay(daemon.lastError, Math.max(0, width - 14)), C.bad],
|
|
279
|
-
]));
|
|
280
|
-
}
|
|
281
|
-
const canBrowse = activeConnections.length > 0 || contracts.length > 0;
|
|
282
|
-
const canSwitch = contracts.length > 0 && activeConnections.length > 0;
|
|
283
|
-
const navHint = !canBrowse
|
|
284
|
-
? ` focus=${this.browseMode} idle - no connections or contracts to browse`
|
|
285
|
-
: canSwitch
|
|
286
|
-
? ` focus=${this.browseMode} Up/Down move Tab switch connections/contracts r=recover Enter=recover selection`
|
|
287
|
-
: ` focus=${this.browseMode} Up/Down move r=recover Enter=recover selection`;
|
|
288
|
-
const footerLines = [
|
|
289
|
-
buildPanelLine(width, [[navHint, C.dim]]),
|
|
290
|
-
] as const;
|
|
291
|
-
|
|
292
|
-
if (activeConnections.length === 0 && contracts.length === 0) {
|
|
293
|
-
const idleLines = [
|
|
294
|
-
...postureLines,
|
|
295
|
-
...buildEmptyState(
|
|
296
|
-
width,
|
|
297
|
-
' No active ACP or remote subagent connections.',
|
|
298
|
-
'The remote control room is healthy but idle. Runner contracts, session bundles, and bridge pools will appear here once remote work starts.',
|
|
299
|
-
[
|
|
300
|
-
{ command: '/remote setup', summary: 'review remote bootstrap and environment export' },
|
|
301
|
-
{ command: '/remote env', summary: 'emit a reusable remote shell snippet' },
|
|
302
|
-
{ command: '/bridge status', summary: 'inspect runner pools and existing remote artifacts' },
|
|
303
|
-
],
|
|
304
|
-
C,
|
|
305
|
-
),
|
|
306
|
-
];
|
|
307
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
308
|
-
title: 'Remote Control Room',
|
|
309
|
-
intro,
|
|
310
|
-
sections: [{ lines: buildSummaryBlock(width, 'Remote posture', idleLines, C) }],
|
|
311
|
-
footerLines,
|
|
312
|
-
palette: C,
|
|
313
|
-
});
|
|
314
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
315
|
-
return lines.slice(0, height);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const viewingConnections = this.browseMode === 'connections' && activeConnections.length > 0;
|
|
319
|
-
this.selectedIndex = Math.min(
|
|
320
|
-
this.selectedIndex,
|
|
321
|
-
Math.max(0, (viewingConnections ? activeConnections.length : contracts.length) - 1),
|
|
322
|
-
);
|
|
323
|
-
const browseCount = viewingConnections ? activeConnections.length : contracts.length;
|
|
324
|
-
const selected = viewingConnections ? activeConnections.at(this.selectedIndex) ?? null : null;
|
|
325
|
-
const selectedContract = !viewingConnections ? contracts.at(this.selectedIndex) ?? null : null;
|
|
326
|
-
const detailRows: Line[] = [];
|
|
327
|
-
|
|
328
|
-
if (selected) {
|
|
329
|
-
detailRows.push(buildPanelLine(width, [
|
|
330
|
-
[' Agent: ', C.label],
|
|
331
|
-
[selected.agentId, C.value],
|
|
332
|
-
[' State: ', C.label],
|
|
333
|
-
[selected.transportState, stateColor(selected.transportState)],
|
|
334
|
-
[' Completing: ', C.label],
|
|
335
|
-
[selected.completing ? 'yes' : 'no', selected.completing ? C.warn : C.dim],
|
|
336
|
-
]));
|
|
337
|
-
detailRows.push(buildPanelLine(width, [
|
|
338
|
-
[' Connected: ', C.label],
|
|
339
|
-
[formatTimestamp(selected.connectedAt), C.dim],
|
|
340
|
-
[' Messages: ', C.label],
|
|
341
|
-
[String(selected.messageCount), selected.messageCount > 0 ? C.info : C.dim],
|
|
342
|
-
[' Errors: ', C.label],
|
|
343
|
-
[String(selected.errorCount), selected.errorCount > 0 ? C.warn : C.dim],
|
|
344
|
-
]));
|
|
345
|
-
if (selected.lastError) {
|
|
346
|
-
detailRows.push(buildPanelLine(width, [
|
|
347
|
-
[' Last error: ', C.label],
|
|
348
|
-
[selected.lastError.slice(0, Math.max(0, width - 13)), C.bad],
|
|
349
|
-
]));
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const contract = contracts.find((entry) => entry.runnerId === selected.agentId);
|
|
353
|
-
if (contract) {
|
|
354
|
-
detailRows.push(buildPanelLine(width, [
|
|
355
|
-
[' Contract: ', C.label],
|
|
356
|
-
[`${contract.template} / ${contract.trustClass}`, C.info],
|
|
357
|
-
[' Pool: ', C.label],
|
|
358
|
-
[contract.poolId ?? '(none)', contract.poolId ? C.info : C.dim],
|
|
359
|
-
]));
|
|
360
|
-
detailRows.push(buildPanelLine(width, [
|
|
361
|
-
[' Depth: ', C.label],
|
|
362
|
-
[String(contract.capabilityCeiling.orchestrationDepth), C.value],
|
|
363
|
-
[' Pools: ', C.label],
|
|
364
|
-
[String(pools.length), pools.length > 0 ? C.info : C.dim],
|
|
365
|
-
]));
|
|
366
|
-
detailRows.push(buildPanelLine(width, [
|
|
367
|
-
[' Protocol: ', C.label],
|
|
368
|
-
[contract.capabilityCeiling.executionProtocol, C.value],
|
|
369
|
-
[' Review: ', C.label],
|
|
370
|
-
[contract.capabilityCeiling.reviewMode, C.value],
|
|
371
|
-
[' Lane: ', C.label],
|
|
372
|
-
[contract.capabilityCeiling.communicationLane, C.value],
|
|
373
|
-
]));
|
|
374
|
-
detailRows.push(buildPanelLine(width, [
|
|
375
|
-
[' Tools: ', C.label],
|
|
376
|
-
[truncate(contract.capabilityCeiling.allowedTools.join(', ') || '(none)', Math.max(0, width - 10)), C.dim],
|
|
377
|
-
]));
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (selected.taskId) {
|
|
381
|
-
detailRows.push(buildPanelLine(width, [
|
|
382
|
-
[' Task: ', C.label],
|
|
383
|
-
[selected.taskId, C.value],
|
|
384
|
-
]));
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const supervisorEntry = supervisor.sessions.find((entry) => entry.runnerId === selected.agentId);
|
|
388
|
-
if (supervisorEntry) {
|
|
389
|
-
detailRows.push(buildPanelLine(width, [
|
|
390
|
-
[' Heartbeat: ', C.label],
|
|
391
|
-
[supervisorEntry.heartbeat.status, supervisorEntry.heartbeat.status === 'fresh' ? C.good : supervisorEntry.heartbeat.status === 'stale' ? C.warn : C.bad],
|
|
392
|
-
[' Protocol: ', C.label],
|
|
393
|
-
[supervisorEntry.negotiation.executionProtocol, C.value],
|
|
394
|
-
[' Review: ', C.label],
|
|
395
|
-
[supervisorEntry.negotiation.reviewMode, supervisorEntry.negotiation.reviewMode === 'wrfc' ? C.good : C.dim],
|
|
396
|
-
]));
|
|
397
|
-
detailRows.push(buildPanelLine(width, [[truncateDisplay(` ${supervisorEntry.heartbeat.detail}`, width), C.dim]]));
|
|
398
|
-
if (isSupervisorEntryDegraded(supervisorEntry) && supervisorEntry.recovery.length > 0) {
|
|
399
|
-
detailRows.push(buildPanelLine(width, [
|
|
400
|
-
[' Enter recovers: ', C.label],
|
|
401
|
-
[supervisorEntry.recovery[0].reason, C.warn],
|
|
402
|
-
]));
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const recentArtifact = snapshot.artifacts.find((artifact) => artifact.runnerId === selected.agentId);
|
|
407
|
-
if (recentArtifact) {
|
|
408
|
-
detailRows.push(buildPanelLine(width, [[' Recent Review Artifact', C.label]]));
|
|
409
|
-
detailRows.push(buildPanelLine(width, [
|
|
410
|
-
[' Artifact: ', C.label],
|
|
411
|
-
[recentArtifact.id, C.value],
|
|
412
|
-
[' Status: ', C.label],
|
|
413
|
-
[recentArtifact.task.status, stateColor(recentArtifact.evidence.transportState)],
|
|
414
|
-
]));
|
|
415
|
-
detailRows.push(buildPanelLine(width, [
|
|
416
|
-
[' Summary: ', C.label],
|
|
417
|
-
[truncate(recentArtifact.task.summary, Math.max(0, width - 12)), C.dim],
|
|
418
|
-
]));
|
|
419
|
-
}
|
|
420
|
-
detailRows.push(buildPanelLine(width, [
|
|
421
|
-
[' Tip: ', C.label],
|
|
422
|
-
['Use Up/Down or j/k to inspect another connection.', C.dim],
|
|
423
|
-
]));
|
|
424
|
-
} else if (selectedContract) {
|
|
425
|
-
detailRows.push(buildPanelLine(width, [
|
|
426
|
-
[' Runner: ', C.label],
|
|
427
|
-
[selectedContract.runnerId, C.value],
|
|
428
|
-
[' Template: ', C.label],
|
|
429
|
-
[selectedContract.template, C.info],
|
|
430
|
-
[' Trust: ', C.label],
|
|
431
|
-
[selectedContract.trustClass, C.value],
|
|
432
|
-
]));
|
|
433
|
-
detailRows.push(buildPanelLine(width, [
|
|
434
|
-
[' Pool: ', C.label],
|
|
435
|
-
[selectedContract.poolId ?? '(none)', selectedContract.poolId ? C.info : C.dim],
|
|
436
|
-
[' Transport: ', C.label],
|
|
437
|
-
[selectedContract.transport.state, stateColor(selectedContract.transport.state)],
|
|
438
|
-
]));
|
|
439
|
-
detailRows.push(buildPanelLine(width, [
|
|
440
|
-
[' Protocol: ', C.label],
|
|
441
|
-
[selectedContract.capabilityCeiling.executionProtocol, C.value],
|
|
442
|
-
[' Review: ', C.label],
|
|
443
|
-
[selectedContract.capabilityCeiling.reviewMode, C.value],
|
|
444
|
-
[' Lane: ', C.label],
|
|
445
|
-
[selectedContract.capabilityCeiling.communicationLane, C.value],
|
|
446
|
-
]));
|
|
447
|
-
detailRows.push(buildPanelLine(width, [
|
|
448
|
-
[' Tools: ', C.label],
|
|
449
|
-
[truncate(selectedContract.capabilityCeiling.allowedTools.join(', ') || '(none)', Math.max(0, width - 10)), C.dim],
|
|
450
|
-
]));
|
|
451
|
-
const supervisorEntry = supervisor.sessions.find((entry) => entry.runnerId === selectedContract.runnerId);
|
|
452
|
-
if (supervisorEntry) {
|
|
453
|
-
detailRows.push(buildPanelLine(width, [
|
|
454
|
-
[' Heartbeat: ', C.label],
|
|
455
|
-
[supervisorEntry.heartbeat.status, supervisorEntry.heartbeat.status === 'fresh' ? C.good : supervisorEntry.heartbeat.status === 'stale' ? C.warn : C.bad],
|
|
456
|
-
[' Lane: ', C.label],
|
|
457
|
-
[supervisorEntry.negotiation.communicationLane, C.info],
|
|
458
|
-
]));
|
|
459
|
-
if (isSupervisorEntryDegraded(supervisorEntry) && supervisorEntry.recovery.length > 0) {
|
|
460
|
-
detailRows.push(buildPanelLine(width, [
|
|
461
|
-
[' Enter recovers: ', C.label],
|
|
462
|
-
[supervisorEntry.recovery[0].reason, C.warn],
|
|
463
|
-
]));
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
const recentArtifact = snapshot.artifacts.find((artifact) => artifact.runnerId === selectedContract.runnerId);
|
|
467
|
-
if (recentArtifact) {
|
|
468
|
-
detailRows.push(buildPanelLine(width, [
|
|
469
|
-
[' Recent artifact: ', C.label],
|
|
470
|
-
[recentArtifact.id, C.good],
|
|
471
|
-
[' Status: ', C.label],
|
|
472
|
-
[recentArtifact.task.status, stateColor(recentArtifact.evidence.transportState)],
|
|
473
|
-
]));
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'Remote posture', postureLines, C) };
|
|
477
|
-
const detailSection: PanelWorkspaceSection = {
|
|
478
|
-
lines: buildDetailBlock(width, selected ? 'Selected connection' : 'Selected contract', detailRows, C),
|
|
479
|
-
};
|
|
480
|
-
const browseTitle = viewingConnections ? 'Active Connections' : 'Registered Remote Runner Contracts';
|
|
481
|
-
const rawBrowseLines: Line[] = viewingConnections
|
|
482
|
-
? activeConnections.map((connection, absolute) => {
|
|
483
|
-
return buildPanelListRow(width, [
|
|
484
|
-
{ text: connection.agentId.padEnd(18), fg: C.value },
|
|
485
|
-
{ text: ` ${connection.transportState.padEnd(18)}`, fg: stateColor(connection.transportState) },
|
|
486
|
-
{ text: ` msgs=${String(connection.messageCount).padEnd(6)}`, fg: C.info },
|
|
487
|
-
{ text: ` errs=${String(connection.errorCount).padEnd(4)}`, fg: connection.errorCount > 0 ? C.warn : C.dim },
|
|
488
|
-
{ text: truncateDisplay(` ${connection.label}`, Math.max(0, width - 56)), fg: C.dim },
|
|
489
|
-
], C, { selected: absolute === this.selectedIndex, selectedBg: C.headerBg });
|
|
490
|
-
})
|
|
491
|
-
: [
|
|
492
|
-
...contracts.map((contract, absolute) => {
|
|
493
|
-
return buildPanelListRow(width, [
|
|
494
|
-
{ text: contract.runnerId.padEnd(18), fg: C.value },
|
|
495
|
-
{ text: ` ${contract.transport.state.padEnd(18)}`, fg: stateColor(contract.transport.state) },
|
|
496
|
-
{ text: truncateDisplay(` ${contract.template}`, Math.max(0, width - 42)), fg: C.dim },
|
|
497
|
-
], C, { selected: absolute === this.selectedIndex, selectedBg: C.headerBg });
|
|
498
|
-
}),
|
|
499
|
-
buildPanelLine(width, [[' No active connection is currently attached to these contracts.', C.dim]]),
|
|
500
|
-
];
|
|
501
|
-
const resolvedBrowseSection = resolvePrimaryScrollableSection(width, height, {
|
|
502
|
-
intro,
|
|
503
|
-
footerLines,
|
|
504
|
-
palette: C,
|
|
505
|
-
beforeSections: [postureSection],
|
|
506
|
-
section: {
|
|
507
|
-
title: browseTitle,
|
|
508
|
-
scrollableLines: rawBrowseLines,
|
|
509
|
-
selectedIndex: this.selectedIndex,
|
|
510
|
-
scrollOffset: this.scrollOffset,
|
|
511
|
-
guardRows: 1,
|
|
512
|
-
minRows: 4,
|
|
513
|
-
appendWindowSummary: viewingConnections ? { dimColor: C.dim } : undefined,
|
|
514
|
-
},
|
|
515
|
-
afterSections: [detailSection],
|
|
516
|
-
});
|
|
517
|
-
this.scrollOffset = resolvedBrowseSection.scrollOffset;
|
|
518
|
-
|
|
519
|
-
const sections: PanelWorkspaceSection[] = [
|
|
520
|
-
postureSection,
|
|
521
|
-
resolvedBrowseSection.section,
|
|
522
|
-
detailSection,
|
|
523
|
-
];
|
|
524
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
525
|
-
title: 'Remote Control Room',
|
|
526
|
-
intro,
|
|
527
|
-
sections,
|
|
528
|
-
footerLines,
|
|
529
|
-
palette: C,
|
|
530
|
-
});
|
|
531
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
532
|
-
return lines.slice(0, height);
|
|
533
|
-
}
|
|
534
|
-
}
|