@compilr-dev/cli 0.4.0 → 0.5.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/README.md +30 -12
- package/dist/agent.d.ts +74 -1
- package/dist/agent.js +259 -76
- package/dist/anchors/index.d.ts +9 -0
- package/dist/anchors/index.js +9 -0
- package/dist/anchors/project-anchors.d.ts +79 -0
- package/dist/anchors/project-anchors.js +202 -0
- package/dist/commands/handler-types.d.ts +68 -0
- package/dist/commands/handler-types.js +8 -0
- package/dist/commands/handlers/agent-commands.d.ts +13 -0
- package/dist/commands/handlers/agent-commands.js +305 -0
- package/dist/commands/handlers/design-commands.d.ts +15 -0
- package/dist/commands/handlers/design-commands.js +334 -0
- package/dist/commands/handlers/index.d.ts +20 -0
- package/dist/commands/handlers/index.js +43 -0
- package/dist/commands/handlers/overlay-commands.d.ts +21 -0
- package/dist/commands/handlers/overlay-commands.js +287 -0
- package/dist/commands/handlers/project-commands.d.ts +11 -0
- package/dist/commands/handlers/project-commands.js +167 -0
- package/dist/commands/handlers/simple-commands.d.ts +19 -0
- package/dist/commands/handlers/simple-commands.js +144 -0
- package/dist/commands/index.d.ts +2 -1
- package/dist/commands/registry.d.ts +50 -0
- package/dist/commands/registry.js +75 -0
- package/dist/commands-v2/handlers/context.d.ts +13 -0
- package/dist/commands-v2/handlers/context.js +348 -0
- package/dist/commands-v2/handlers/core.d.ts +13 -0
- package/dist/commands-v2/handlers/core.js +165 -0
- package/dist/commands-v2/handlers/debug.d.ts +11 -0
- package/dist/commands-v2/handlers/debug.js +159 -0
- package/dist/commands-v2/handlers/index.d.ts +12 -0
- package/dist/commands-v2/handlers/index.js +24 -0
- package/dist/commands-v2/handlers/project.d.ts +22 -0
- package/dist/commands-v2/handlers/project.js +814 -0
- package/dist/commands-v2/handlers/settings.d.ts +15 -0
- package/dist/commands-v2/handlers/settings.js +235 -0
- package/dist/commands-v2/index.d.ts +13 -0
- package/dist/commands-v2/index.js +15 -0
- package/dist/commands-v2/registry.d.ts +37 -0
- package/dist/commands-v2/registry.js +80 -0
- package/dist/commands-v2/types.d.ts +75 -0
- package/dist/commands-v2/types.js +7 -0
- package/dist/commands.js +110 -7
- package/dist/index.js +288 -29
- package/dist/input-handlers/index.d.ts +7 -0
- package/dist/input-handlers/index.js +7 -0
- package/dist/input-handlers/memory-handler.d.ts +26 -0
- package/dist/input-handlers/memory-handler.js +68 -0
- package/dist/repl-helpers.d.ts +63 -0
- package/dist/repl-helpers.js +318 -0
- package/dist/repl-v2.d.ts +155 -0
- package/dist/repl-v2.js +774 -0
- package/dist/repl.d.ts +32 -4
- package/dist/repl.js +250 -977
- package/dist/settings/index.d.ts +23 -0
- package/dist/settings/index.js +48 -0
- package/dist/settings/paths.d.ts +110 -0
- package/dist/settings/paths.js +264 -0
- package/dist/templates/compilr-md.js +7 -4
- package/dist/templates/index.js +3 -4
- package/dist/themes/colors.js +3 -1
- package/dist/themes/registry.d.ts +5 -36
- package/dist/themes/registry.js +11 -95
- package/dist/themes/types.d.ts +3 -38
- package/dist/themes/types.js +2 -2
- package/dist/tools/anchor-tools.d.ts +31 -0
- package/dist/tools/anchor-tools.js +255 -0
- package/dist/tools/backlog-wrappers.d.ts +54 -0
- package/dist/tools/backlog-wrappers.js +338 -0
- package/dist/tools/backlog.js +1 -1
- package/dist/tools/db-tools.d.ts +65 -0
- package/dist/tools/db-tools.js +19 -0
- package/dist/tools/document-db.d.ts +43 -0
- package/dist/tools/document-db.js +220 -0
- package/dist/tools/project-db.d.ts +102 -0
- package/dist/tools/project-db.js +370 -0
- package/dist/tools/workitem-db.d.ts +103 -0
- package/dist/tools/workitem-db.js +549 -0
- package/dist/tools.js +13 -3
- package/dist/ui/agents-overlay-v2.d.ts +43 -0
- package/dist/ui/agents-overlay-v2.js +809 -0
- package/dist/ui/agents-overlay.d.ts +5 -5
- package/dist/ui/agents-overlay.js +782 -420
- package/dist/ui/anchors-overlay.d.ts +12 -0
- package/dist/ui/anchors-overlay.js +775 -0
- package/dist/ui/arch-type-overlay.d.ts +1 -6
- package/dist/ui/arch-type-overlay.js +175 -203
- package/dist/ui/ask-user-overlay-v2.d.ts +26 -0
- package/dist/ui/ask-user-overlay-v2.js +555 -0
- package/dist/ui/ask-user-overlay.d.ts +2 -2
- package/dist/ui/ask-user-overlay.js +443 -535
- package/dist/ui/ask-user-simple-overlay-v2.d.ts +25 -0
- package/dist/ui/ask-user-simple-overlay-v2.js +215 -0
- package/dist/ui/ask-user-simple-overlay.d.ts +2 -2
- package/dist/ui/ask-user-simple-overlay.js +182 -209
- package/dist/ui/backlog-overlay.d.ts +16 -1
- package/dist/ui/backlog-overlay.js +525 -659
- package/dist/ui/base/index.d.ts +26 -0
- package/dist/ui/base/index.js +33 -0
- package/dist/ui/base/inline-overlay-utils.d.ts +217 -0
- package/dist/ui/base/inline-overlay-utils.js +320 -0
- package/dist/ui/base/inline-overlay.d.ts +159 -0
- package/dist/ui/base/inline-overlay.js +257 -0
- package/dist/ui/base/key-utils.d.ts +15 -0
- package/dist/ui/base/key-utils.js +30 -0
- package/dist/ui/base/overlay-base-v2.d.ts +193 -0
- package/dist/ui/base/overlay-base-v2.js +246 -0
- package/dist/ui/base/overlay-base.d.ts +156 -0
- package/dist/ui/base/overlay-base.js +238 -0
- package/dist/ui/base/overlay-lifecycle.d.ts +65 -0
- package/dist/ui/base/overlay-lifecycle.js +159 -0
- package/dist/ui/base/overlay-types.d.ts +185 -0
- package/dist/ui/base/overlay-types.js +7 -0
- package/dist/ui/base/render-utils.d.ts +8 -0
- package/dist/ui/base/render-utils.js +11 -0
- package/dist/ui/base/screen-stack.d.ts +148 -0
- package/dist/ui/base/screen-stack.js +184 -0
- package/dist/ui/base/tabbed-list-overlay-v2.d.ts +103 -0
- package/dist/ui/base/tabbed-list-overlay-v2.js +317 -0
- package/dist/ui/base/tabbed-list-overlay.d.ts +153 -0
- package/dist/ui/base/tabbed-list-overlay.js +369 -0
- package/dist/ui/commands-overlay-v2.d.ts +33 -0
- package/dist/ui/commands-overlay-v2.js +441 -0
- package/dist/ui/commands-overlay.d.ts +7 -2
- package/dist/ui/commands-overlay.js +384 -355
- package/dist/ui/config-overlay.d.ts +5 -4
- package/dist/ui/config-overlay.js +243 -513
- package/dist/ui/conversation.d.ts +75 -4
- package/dist/ui/conversation.js +374 -161
- package/dist/ui/docs-overlay.d.ts +17 -0
- package/dist/ui/docs-overlay.js +303 -0
- package/dist/ui/ephemeral.d.ts +1 -1
- package/dist/ui/ephemeral.js +1 -1
- package/dist/ui/features/index.d.ts +34 -0
- package/dist/ui/features/index.js +34 -0
- package/dist/ui/features/input-feature.d.ts +85 -0
- package/dist/ui/features/input-feature.js +238 -0
- package/dist/ui/features/list-feature.d.ts +155 -0
- package/dist/ui/features/list-feature.js +244 -0
- package/dist/ui/features/pagination-feature.d.ts +154 -0
- package/dist/ui/features/pagination-feature.js +238 -0
- package/dist/ui/features/search-feature.d.ts +148 -0
- package/dist/ui/features/search-feature.js +185 -0
- package/dist/ui/features/tab-feature.d.ts +194 -0
- package/dist/ui/features/tab-feature.js +307 -0
- package/dist/ui/footer-v2.d.ts +222 -0
- package/dist/ui/footer-v2.js +1349 -0
- package/dist/ui/footer.d.ts +107 -0
- package/dist/ui/footer.js +359 -67
- package/dist/ui/guardrail-overlay.d.ts +29 -0
- package/dist/ui/guardrail-overlay.js +145 -0
- package/dist/ui/help-overlay-v2.d.ts +34 -0
- package/dist/ui/help-overlay-v2.js +309 -0
- package/dist/ui/help-overlay.d.ts +16 -0
- package/dist/ui/help-overlay.js +316 -0
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -3
- package/dist/ui/init-overlay-v2.d.ts +34 -0
- package/dist/ui/init-overlay-v2.js +600 -0
- package/dist/ui/init-overlay.d.ts +12 -2
- package/dist/ui/init-overlay.js +349 -270
- package/dist/ui/input-prompt-v2.d.ts +1 -0
- package/dist/ui/input-prompt-v2.js +14 -6
- package/dist/ui/input-prompt.d.ts +116 -33
- package/dist/ui/input-prompt.js +536 -337
- package/dist/ui/iteration-limit-overlay-v2.d.ts +21 -0
- package/dist/ui/iteration-limit-overlay-v2.js +114 -0
- package/dist/ui/iteration-limit-overlay.d.ts +2 -2
- package/dist/ui/iteration-limit-overlay.js +92 -128
- package/dist/ui/keys-overlay-v2.d.ts +41 -0
- package/dist/ui/keys-overlay-v2.js +248 -0
- package/dist/ui/keys-overlay.d.ts +1 -0
- package/dist/ui/keys-overlay.js +203 -141
- package/dist/ui/line-utils.d.ts +88 -0
- package/dist/ui/line-utils.js +150 -0
- package/dist/ui/live-region.d.ts +161 -0
- package/dist/ui/live-region.js +387 -0
- package/dist/ui/mascot/expressions.d.ts +32 -0
- package/dist/ui/mascot/expressions.js +213 -0
- package/dist/ui/mascot/index.d.ts +8 -0
- package/dist/ui/mascot/index.js +8 -0
- package/dist/ui/mascot/renderer.d.ts +19 -0
- package/dist/ui/mascot/renderer.js +97 -0
- package/dist/ui/mascot-overlay-v2.d.ts +41 -0
- package/dist/ui/mascot-overlay-v2.js +138 -0
- package/dist/ui/mascot-overlay.d.ts +21 -0
- package/dist/ui/mascot-overlay.js +146 -0
- package/dist/ui/model-overlay-v2.d.ts +49 -0
- package/dist/ui/model-overlay-v2.js +118 -0
- package/dist/ui/model-overlay.d.ts +27 -0
- package/dist/ui/model-overlay.js +221 -0
- package/dist/ui/model-warning-overlay.js +3 -5
- package/dist/ui/new-overlay.d.ts +34 -0
- package/dist/ui/new-overlay.js +604 -0
- package/dist/ui/overlay/impl/agents-overlay-v2.d.ts +45 -0
- package/dist/ui/overlay/impl/agents-overlay-v2.js +825 -0
- package/dist/ui/overlay/impl/anchors-overlay-v2.d.ts +47 -0
- package/dist/ui/overlay/impl/anchors-overlay-v2.js +783 -0
- package/dist/ui/overlay/impl/arch-type-overlay-v2.d.ts +37 -0
- package/dist/ui/overlay/impl/arch-type-overlay-v2.js +240 -0
- package/dist/ui/overlay/impl/ask-user-overlay-v2.d.ts +72 -0
- package/dist/ui/overlay/impl/ask-user-overlay-v2.js +584 -0
- package/dist/ui/overlay/impl/ask-user-simple-overlay-v2.d.ts +46 -0
- package/dist/ui/overlay/impl/ask-user-simple-overlay-v2.js +204 -0
- package/dist/ui/overlay/impl/backlog-overlay-v2.d.ts +49 -0
- package/dist/ui/overlay/impl/backlog-overlay-v2.js +642 -0
- package/dist/ui/overlay/impl/commands-overlay-v2.d.ts +33 -0
- package/dist/ui/overlay/impl/commands-overlay-v2.js +441 -0
- package/dist/ui/overlay/impl/config-overlay-v2.d.ts +100 -0
- package/dist/ui/overlay/impl/config-overlay-v2.js +654 -0
- package/dist/ui/overlay/impl/dashboard-overlay-v2.d.ts +55 -0
- package/dist/ui/overlay/impl/dashboard-overlay-v2.js +359 -0
- package/dist/ui/overlay/impl/docs-overlay-v2.d.ts +45 -0
- package/dist/ui/overlay/impl/docs-overlay-v2.js +114 -0
- package/dist/ui/overlay/impl/document-detail-overlay-v2.d.ts +77 -0
- package/dist/ui/overlay/impl/document-detail-overlay-v2.js +1071 -0
- package/dist/ui/overlay/impl/guardrail-overlay-v2.d.ts +43 -0
- package/dist/ui/overlay/impl/guardrail-overlay-v2.js +114 -0
- package/dist/ui/overlay/impl/help-overlay-v2.d.ts +34 -0
- package/dist/ui/overlay/impl/help-overlay-v2.js +309 -0
- package/dist/ui/overlay/impl/init-overlay-v2.d.ts +77 -0
- package/dist/ui/overlay/impl/init-overlay-v2.js +593 -0
- package/dist/ui/overlay/impl/init-setup-overlay-v2.d.ts +25 -0
- package/dist/ui/overlay/impl/init-setup-overlay-v2.js +97 -0
- package/dist/ui/overlay/impl/iteration-limit-overlay-v2.d.ts +35 -0
- package/dist/ui/overlay/impl/iteration-limit-overlay-v2.js +105 -0
- package/dist/ui/overlay/impl/keys-overlay-v2.d.ts +41 -0
- package/dist/ui/overlay/impl/keys-overlay-v2.js +248 -0
- package/dist/ui/overlay/impl/mascot-overlay-v2.d.ts +41 -0
- package/dist/ui/overlay/impl/mascot-overlay-v2.js +138 -0
- package/dist/ui/overlay/impl/model-overlay-v2.d.ts +49 -0
- package/dist/ui/overlay/impl/model-overlay-v2.js +118 -0
- package/dist/ui/overlay/impl/model-warning-overlay-v2.d.ts +46 -0
- package/dist/ui/overlay/impl/model-warning-overlay-v2.js +132 -0
- package/dist/ui/overlay/impl/new-overlay-v2.d.ts +77 -0
- package/dist/ui/overlay/impl/new-overlay-v2.js +593 -0
- package/dist/ui/overlay/impl/permission-overlay-v2.d.ts +36 -0
- package/dist/ui/overlay/impl/permission-overlay-v2.js +380 -0
- package/dist/ui/overlay/impl/projects-overlay-v2.d.ts +36 -0
- package/dist/ui/overlay/impl/projects-overlay-v2.js +499 -0
- package/dist/ui/overlay/impl/theme-overlay-v2.d.ts +42 -0
- package/dist/ui/overlay/impl/theme-overlay-v2.js +135 -0
- package/dist/ui/overlay/impl/tools-overlay-v2.d.ts +47 -0
- package/dist/ui/overlay/impl/tools-overlay-v2.js +218 -0
- package/dist/ui/overlay/impl/tutorial-overlay-v2.d.ts +31 -0
- package/dist/ui/overlay/impl/tutorial-overlay-v2.js +1035 -0
- package/dist/ui/overlay/impl/workflow-overlay-v2.d.ts +80 -0
- package/dist/ui/overlay/impl/workflow-overlay-v2.js +637 -0
- package/dist/ui/overlay/index.d.ts +33 -0
- package/dist/ui/overlay/index.js +35 -0
- package/dist/ui/overlay/key-utils.d.ts +6 -0
- package/dist/ui/overlay/key-utils.js +6 -0
- package/dist/ui/overlay/overlay-types.d.ts +128 -0
- package/dist/ui/overlay/overlay-types.js +22 -0
- package/dist/ui/overlay/types.d.ts +135 -0
- package/dist/ui/overlay/types.js +22 -0
- package/dist/ui/overlays/help-overlay-v2.d.ts +28 -0
- package/dist/ui/overlays/help-overlay-v2.js +198 -0
- package/dist/ui/overlays/index.d.ts +11 -0
- package/dist/ui/overlays/index.js +11 -0
- package/dist/ui/overlays.d.ts +0 -4
- package/dist/ui/overlays.js +0 -444
- package/dist/ui/permission-overlay-v2.d.ts +36 -0
- package/dist/ui/permission-overlay-v2.js +380 -0
- package/dist/ui/permission-overlay.d.ts +1 -1
- package/dist/ui/permission-overlay.js +186 -298
- package/dist/ui/projects-overlay.d.ts +19 -0
- package/dist/ui/projects-overlay.js +484 -0
- package/dist/ui/providers/types.d.ts +178 -0
- package/dist/ui/providers/types.js +9 -0
- package/dist/ui/render-modes.d.ts +36 -0
- package/dist/ui/render-modes.js +44 -0
- package/dist/ui/startup-menu.d.ts +36 -0
- package/dist/ui/startup-menu.js +236 -0
- package/dist/ui/subagent-renderer.d.ts +117 -0
- package/dist/ui/subagent-renderer.js +334 -0
- package/dist/ui/terminal-codes.d.ts +94 -0
- package/dist/ui/terminal-codes.js +124 -0
- package/dist/ui/terminal-renderer.d.ts +221 -0
- package/dist/ui/terminal-renderer.js +751 -0
- package/dist/ui/terminal-ui.d.ts +463 -0
- package/dist/ui/terminal-ui.js +2296 -0
- package/dist/ui/terminal.d.ts +20 -0
- package/dist/ui/terminal.js +72 -0
- package/dist/ui/theme-overlay-v2.d.ts +42 -0
- package/dist/ui/theme-overlay-v2.js +135 -0
- package/dist/ui/theme-overlay.d.ts +24 -0
- package/dist/ui/theme-overlay.js +127 -0
- package/dist/ui/todo-zone.js +53 -25
- package/dist/ui/tool-formatters.d.ts +16 -0
- package/dist/ui/tool-formatters.js +516 -0
- package/dist/ui/tools-overlay-v2.d.ts +47 -0
- package/dist/ui/tools-overlay-v2.js +218 -0
- package/dist/ui/tools-overlay.d.ts +10 -2
- package/dist/ui/tools-overlay.js +172 -220
- package/dist/ui/tutorial-overlay-v2.d.ts +31 -0
- package/dist/ui/tutorial-overlay-v2.js +1035 -0
- package/dist/ui/tutorial-overlay.d.ts +1 -0
- package/dist/ui/tutorial-overlay.js +400 -302
- package/dist/ui/workflow-overlay.d.ts +22 -0
- package/dist/ui/workflow-overlay.js +636 -0
- package/dist/utils/debug-log.d.ts +28 -0
- package/dist/utils/debug-log.js +57 -0
- package/dist/utils/model-tiers.js +1 -1
- package/dist/utils/path-safety.d.ts +56 -0
- package/dist/utils/path-safety.js +239 -0
- package/dist/workflow/guided-mode-injector.d.ts +42 -0
- package/dist/workflow/guided-mode-injector.js +191 -0
- package/dist/workflow/index.d.ts +8 -0
- package/dist/workflow/index.js +8 -0
- package/dist/workflow/step-criteria.d.ts +62 -0
- package/dist/workflow/step-criteria.js +150 -0
- package/dist/workflow/step-tracker.d.ts +92 -0
- package/dist/workflow/step-tracker.js +141 -0
- package/package.json +12 -5
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal Renderer
|
|
3
|
+
*
|
|
4
|
+
* Single point of terminal output. All rendering goes through this class.
|
|
5
|
+
* Eliminates race conditions by ensuring only one writer.
|
|
6
|
+
*
|
|
7
|
+
* Key responsibilities:
|
|
8
|
+
* - Manage render modes (MENU, REPL, OVERLAY)
|
|
9
|
+
* - Set up and manage scroll regions (REPL mode)
|
|
10
|
+
* - Collect content from providers
|
|
11
|
+
* - Write to terminal (single writer)
|
|
12
|
+
* - Handle resize events
|
|
13
|
+
* - Route keyboard input
|
|
14
|
+
*/
|
|
15
|
+
import { EventEmitter } from 'events';
|
|
16
|
+
import { RenderMode, isValidTransition } from './render-modes.js';
|
|
17
|
+
// Re-export RenderMode for convenience
|
|
18
|
+
export { RenderMode } from './render-modes.js';
|
|
19
|
+
import * as codes from './terminal-codes.js';
|
|
20
|
+
import { getPhysicalLineCount } from './line-utils.js';
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// TerminalRenderer Class
|
|
23
|
+
// =============================================================================
|
|
24
|
+
export class TerminalRenderer extends EventEmitter {
|
|
25
|
+
// === Configuration ===
|
|
26
|
+
renderDebounceMs;
|
|
27
|
+
debug;
|
|
28
|
+
useScrollRegions;
|
|
29
|
+
// === State ===
|
|
30
|
+
mode;
|
|
31
|
+
terminalRows = 24;
|
|
32
|
+
terminalCols = 80;
|
|
33
|
+
footerHeight = 0;
|
|
34
|
+
scrollRegionBottom = 0;
|
|
35
|
+
isRendering = false;
|
|
36
|
+
renderPending = false;
|
|
37
|
+
renderTimer = null;
|
|
38
|
+
started = false;
|
|
39
|
+
simpleMode = false;
|
|
40
|
+
// === Content Queues ===
|
|
41
|
+
pendingScrollContent = [];
|
|
42
|
+
// === Scroll Position Tracking ===
|
|
43
|
+
// Track where the next output should go in scroll zone
|
|
44
|
+
scrollCursorRow = 1; // 1-indexed, starts at top
|
|
45
|
+
// === Providers ===
|
|
46
|
+
providers = {};
|
|
47
|
+
// === Footer (for scroll region mode) ===
|
|
48
|
+
footer = null;
|
|
49
|
+
// === Resize Handler ===
|
|
50
|
+
resizeHandler = null;
|
|
51
|
+
// === Render Loop (for scroll region mode) ===
|
|
52
|
+
renderLoopTimer = null;
|
|
53
|
+
renderLoopInterval = 60; // 60ms = ~16fps
|
|
54
|
+
constructor(options = {}) {
|
|
55
|
+
super();
|
|
56
|
+
this.mode = options.initialMode ?? RenderMode.REPL;
|
|
57
|
+
this.renderDebounceMs = options.renderDebounceMs ?? 16;
|
|
58
|
+
this.debug = options.debug ?? false;
|
|
59
|
+
this.useScrollRegions = options.useScrollRegions ?? false;
|
|
60
|
+
this.updateDimensions();
|
|
61
|
+
}
|
|
62
|
+
// ===========================================================================
|
|
63
|
+
// Lifecycle
|
|
64
|
+
// ===========================================================================
|
|
65
|
+
/**
|
|
66
|
+
* Start the renderer.
|
|
67
|
+
* Sets up terminal state and begins render loop.
|
|
68
|
+
*/
|
|
69
|
+
start() {
|
|
70
|
+
if (this.started) {
|
|
71
|
+
this.log('Already started, ignoring start() call');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.log(`Starting in ${this.mode} mode`);
|
|
75
|
+
// Check if stdout is a TTY
|
|
76
|
+
if (!process.stdout.isTTY) {
|
|
77
|
+
this.log('Not a TTY, using simple mode');
|
|
78
|
+
this.simpleMode = true;
|
|
79
|
+
this.started = true;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Update dimensions
|
|
83
|
+
this.updateDimensions();
|
|
84
|
+
// Set up resize handler
|
|
85
|
+
this.resizeHandler = () => {
|
|
86
|
+
this.handleResize();
|
|
87
|
+
};
|
|
88
|
+
process.stdout.on('resize', this.resizeHandler);
|
|
89
|
+
// Mark as started
|
|
90
|
+
this.started = true;
|
|
91
|
+
// Initial mode setup
|
|
92
|
+
this.setupMode(this.mode);
|
|
93
|
+
// Start render loop if using scroll regions
|
|
94
|
+
if (this.useScrollRegions) {
|
|
95
|
+
this.startRenderLoop();
|
|
96
|
+
}
|
|
97
|
+
this.log('Started successfully');
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Start the render loop (for scroll region mode)
|
|
101
|
+
* Polls footer for changes and re-renders
|
|
102
|
+
*/
|
|
103
|
+
startRenderLoop() {
|
|
104
|
+
if (this.renderLoopTimer)
|
|
105
|
+
return;
|
|
106
|
+
this.log('Starting render loop');
|
|
107
|
+
this.renderLoopTimer = setInterval(() => {
|
|
108
|
+
if (this.started && !this.simpleMode) {
|
|
109
|
+
// In scroll region mode, always re-render to pick up footer changes
|
|
110
|
+
this.forceRender();
|
|
111
|
+
}
|
|
112
|
+
}, this.renderLoopInterval);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Stop the render loop
|
|
116
|
+
*/
|
|
117
|
+
stopRenderLoop() {
|
|
118
|
+
if (this.renderLoopTimer) {
|
|
119
|
+
clearInterval(this.renderLoopTimer);
|
|
120
|
+
this.renderLoopTimer = null;
|
|
121
|
+
this.log('Render loop stopped');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Stop the renderer.
|
|
126
|
+
* Cleans up terminal state and stops render loop.
|
|
127
|
+
*/
|
|
128
|
+
stop() {
|
|
129
|
+
if (!this.started) {
|
|
130
|
+
this.log('Not started, ignoring stop() call');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.log('Stopping');
|
|
134
|
+
// Stop render loop
|
|
135
|
+
this.stopRenderLoop();
|
|
136
|
+
// Clear any pending render
|
|
137
|
+
if (this.renderTimer) {
|
|
138
|
+
clearTimeout(this.renderTimer);
|
|
139
|
+
this.renderTimer = null;
|
|
140
|
+
}
|
|
141
|
+
// Remove resize handler
|
|
142
|
+
if (this.resizeHandler) {
|
|
143
|
+
process.stdout.off('resize', this.resizeHandler);
|
|
144
|
+
this.resizeHandler = null;
|
|
145
|
+
}
|
|
146
|
+
// Reset terminal state (only if TTY)
|
|
147
|
+
if (!this.simpleMode) {
|
|
148
|
+
// Reset scroll region
|
|
149
|
+
this.write(codes.resetScrollRegion);
|
|
150
|
+
// Show cursor
|
|
151
|
+
this.write(codes.showCursor);
|
|
152
|
+
}
|
|
153
|
+
this.started = false;
|
|
154
|
+
this.log('Stopped');
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Check if renderer is running
|
|
158
|
+
*/
|
|
159
|
+
isStarted() {
|
|
160
|
+
return this.started;
|
|
161
|
+
}
|
|
162
|
+
// ===========================================================================
|
|
163
|
+
// Mode Management
|
|
164
|
+
// ===========================================================================
|
|
165
|
+
/**
|
|
166
|
+
* Get current render mode
|
|
167
|
+
*/
|
|
168
|
+
getMode() {
|
|
169
|
+
return this.mode;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Switch to a different render mode
|
|
173
|
+
*/
|
|
174
|
+
setMode(newMode, reason = 'programmatic') {
|
|
175
|
+
if (newMode === this.mode) {
|
|
176
|
+
this.log(`Already in ${newMode} mode, ignoring`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (!isValidTransition(this.mode, newMode)) {
|
|
180
|
+
this.log(`Invalid transition: ${this.mode} -> ${newMode}`);
|
|
181
|
+
this.emit('error', new Error(`Invalid mode transition: ${this.mode} -> ${newMode}`));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const transition = {
|
|
185
|
+
from: this.mode,
|
|
186
|
+
to: newMode,
|
|
187
|
+
reason,
|
|
188
|
+
timestamp: Date.now(),
|
|
189
|
+
};
|
|
190
|
+
this.log(`Mode transition: ${this.mode} -> ${newMode} (${reason})`);
|
|
191
|
+
// Cleanup current mode
|
|
192
|
+
this.cleanupMode(this.mode);
|
|
193
|
+
// Switch mode
|
|
194
|
+
this.mode = newMode;
|
|
195
|
+
// Setup new mode
|
|
196
|
+
this.setupMode(newMode);
|
|
197
|
+
// Emit event
|
|
198
|
+
this.emit('mode-change', transition);
|
|
199
|
+
// Force render
|
|
200
|
+
this.forceRender();
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Set up terminal for a specific mode
|
|
204
|
+
*/
|
|
205
|
+
setupMode(mode) {
|
|
206
|
+
if (this.simpleMode)
|
|
207
|
+
return;
|
|
208
|
+
switch (mode) {
|
|
209
|
+
case RenderMode.MENU:
|
|
210
|
+
// Full screen, no scroll region, hidden cursor
|
|
211
|
+
this.write(codes.resetScrollRegion);
|
|
212
|
+
this.write(codes.clearScreenAndHome);
|
|
213
|
+
this.write(codes.hideCursor);
|
|
214
|
+
break;
|
|
215
|
+
case RenderMode.REPL:
|
|
216
|
+
// Set up scroll region (will be adjusted on first render)
|
|
217
|
+
this.footerHeight = 0; // Will be calculated
|
|
218
|
+
this.write(codes.resetScrollRegion);
|
|
219
|
+
this.write(codes.showCursor);
|
|
220
|
+
break;
|
|
221
|
+
case RenderMode.OVERLAY:
|
|
222
|
+
// Full screen, no scroll region
|
|
223
|
+
this.write(codes.resetScrollRegion);
|
|
224
|
+
this.write(codes.clearScreenAndHome);
|
|
225
|
+
// Cursor visibility depends on overlay
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Clean up terminal state when leaving a mode
|
|
231
|
+
*/
|
|
232
|
+
cleanupMode(mode) {
|
|
233
|
+
if (this.simpleMode)
|
|
234
|
+
return;
|
|
235
|
+
switch (mode) {
|
|
236
|
+
case RenderMode.MENU:
|
|
237
|
+
// Nothing special to clean up
|
|
238
|
+
break;
|
|
239
|
+
case RenderMode.REPL:
|
|
240
|
+
// Reset scroll region before leaving
|
|
241
|
+
this.write(codes.resetScrollRegion);
|
|
242
|
+
break;
|
|
243
|
+
case RenderMode.OVERLAY:
|
|
244
|
+
// Nothing special to clean up
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// ===========================================================================
|
|
249
|
+
// Provider Management
|
|
250
|
+
// ===========================================================================
|
|
251
|
+
/**
|
|
252
|
+
* Register content providers
|
|
253
|
+
*/
|
|
254
|
+
setProviders(providers) {
|
|
255
|
+
this.providers = { ...this.providers, ...providers };
|
|
256
|
+
this.log(`Providers updated: ${Object.keys(providers).join(', ')}`);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Get a specific provider
|
|
260
|
+
*/
|
|
261
|
+
getProvider(key) {
|
|
262
|
+
return this.providers[key];
|
|
263
|
+
}
|
|
264
|
+
// ===========================================================================
|
|
265
|
+
// Footer Integration (for scroll region mode)
|
|
266
|
+
// ===========================================================================
|
|
267
|
+
/**
|
|
268
|
+
* Set the Footer instance for scroll region rendering
|
|
269
|
+
* When useScrollRegions is true, TerminalRenderer uses footer.getFooterLines()
|
|
270
|
+
* and manages the scroll region. Footer's own render loop is disabled.
|
|
271
|
+
*/
|
|
272
|
+
setFooter(footer) {
|
|
273
|
+
this.footer = footer;
|
|
274
|
+
this.log('Footer connected');
|
|
275
|
+
// When scroll regions are enabled, disable Footer's internal render loop
|
|
276
|
+
// TerminalRenderer will handle all rendering
|
|
277
|
+
if (this.useScrollRegions) {
|
|
278
|
+
footer.setExternalRenderer(true);
|
|
279
|
+
this.log('Footer external renderer enabled (scroll region mode)');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Get the connected Footer instance
|
|
284
|
+
*/
|
|
285
|
+
getFooter() {
|
|
286
|
+
return this.footer;
|
|
287
|
+
}
|
|
288
|
+
// ===========================================================================
|
|
289
|
+
// Scroll Zone (REPL mode)
|
|
290
|
+
// ===========================================================================
|
|
291
|
+
/**
|
|
292
|
+
* Queue content for scroll zone (REPL mode only)
|
|
293
|
+
* Content will be written on next render
|
|
294
|
+
*
|
|
295
|
+
* In legacy mode (useScrollRegions=false), writes immediately via console.log
|
|
296
|
+
* In scroll region mode, queues for next render cycle
|
|
297
|
+
*/
|
|
298
|
+
appendToScrollZone(content) {
|
|
299
|
+
if (this.mode !== RenderMode.REPL) {
|
|
300
|
+
this.log('appendToScrollZone called outside REPL mode, ignoring');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const lines = Array.isArray(content) ? content : content.split('\n');
|
|
304
|
+
if (!this.useScrollRegions) {
|
|
305
|
+
// Legacy mode: write immediately via console.log
|
|
306
|
+
// This works with existing Footer class that manages its own rendering
|
|
307
|
+
for (const line of lines) {
|
|
308
|
+
console.log(line);
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// Scroll region mode: queue for render
|
|
313
|
+
this.pendingScrollContent.push(...lines);
|
|
314
|
+
this.requestRender();
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Flush pending scroll content to terminal
|
|
318
|
+
*
|
|
319
|
+
* Uses tracked scroll position to append content naturally.
|
|
320
|
+
* When content would exceed scroll region, scrolling occurs automatically.
|
|
321
|
+
*/
|
|
322
|
+
flushScrollContent() {
|
|
323
|
+
if (this.pendingScrollContent.length === 0)
|
|
324
|
+
return;
|
|
325
|
+
this.log(`Flushing ${String(this.pendingScrollContent.length)} lines at row ${String(this.scrollCursorRow)}`);
|
|
326
|
+
// Move to current scroll position (within scroll region)
|
|
327
|
+
this.write(codes.cursorTo(this.scrollCursorRow, 1));
|
|
328
|
+
// Write each line
|
|
329
|
+
for (const line of this.pendingScrollContent) {
|
|
330
|
+
this.write(line);
|
|
331
|
+
this.write('\n');
|
|
332
|
+
// Track position (clamped to scroll region bottom)
|
|
333
|
+
this.scrollCursorRow++;
|
|
334
|
+
if (this.scrollCursorRow > this.scrollRegionBottom) {
|
|
335
|
+
// Content will scroll, cursor stays at bottom of scroll region
|
|
336
|
+
this.scrollCursorRow = this.scrollRegionBottom;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
this.pendingScrollContent = [];
|
|
340
|
+
}
|
|
341
|
+
// ===========================================================================
|
|
342
|
+
// Rendering
|
|
343
|
+
// ===========================================================================
|
|
344
|
+
/**
|
|
345
|
+
* Request a render (debounced)
|
|
346
|
+
*/
|
|
347
|
+
requestRender() {
|
|
348
|
+
if (!this.started || this.simpleMode)
|
|
349
|
+
return;
|
|
350
|
+
if (this.renderPending)
|
|
351
|
+
return;
|
|
352
|
+
this.renderPending = true;
|
|
353
|
+
if (this.renderTimer) {
|
|
354
|
+
clearTimeout(this.renderTimer);
|
|
355
|
+
}
|
|
356
|
+
this.renderTimer = setTimeout(() => {
|
|
357
|
+
this.doRender();
|
|
358
|
+
}, this.renderDebounceMs);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Force immediate render (bypasses debounce)
|
|
362
|
+
*/
|
|
363
|
+
forceRender() {
|
|
364
|
+
if (!this.started)
|
|
365
|
+
return;
|
|
366
|
+
// Clear any pending debounced render
|
|
367
|
+
if (this.renderTimer) {
|
|
368
|
+
clearTimeout(this.renderTimer);
|
|
369
|
+
this.renderTimer = null;
|
|
370
|
+
}
|
|
371
|
+
this.renderPending = false;
|
|
372
|
+
this.doRender();
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Main render method
|
|
376
|
+
*/
|
|
377
|
+
doRender() {
|
|
378
|
+
if (this.isRendering) {
|
|
379
|
+
// Already rendering, schedule another
|
|
380
|
+
this.renderPending = true;
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
this.isRendering = true;
|
|
384
|
+
this.renderPending = false;
|
|
385
|
+
try {
|
|
386
|
+
if (this.simpleMode) {
|
|
387
|
+
this.renderSimpleMode();
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
switch (this.mode) {
|
|
391
|
+
case RenderMode.MENU:
|
|
392
|
+
this.renderMenuMode();
|
|
393
|
+
break;
|
|
394
|
+
case RenderMode.REPL:
|
|
395
|
+
this.renderReplMode();
|
|
396
|
+
break;
|
|
397
|
+
case RenderMode.OVERLAY:
|
|
398
|
+
this.renderOverlayMode();
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
this.emit('render', this.mode);
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
this.emit('error', error instanceof Error ? error : new Error(String(error)));
|
|
406
|
+
}
|
|
407
|
+
finally {
|
|
408
|
+
this.isRendering = false;
|
|
409
|
+
// Check if another render was requested during this one
|
|
410
|
+
// (renderPending can be set by callbacks during render)
|
|
411
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
412
|
+
if (this.renderPending) {
|
|
413
|
+
this.requestRender();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Render in simple mode (non-TTY)
|
|
419
|
+
*/
|
|
420
|
+
renderSimpleMode() {
|
|
421
|
+
// In simple mode, just flush any pending content
|
|
422
|
+
for (const line of this.pendingScrollContent) {
|
|
423
|
+
console.log(line);
|
|
424
|
+
}
|
|
425
|
+
this.pendingScrollContent = [];
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Render MENU mode
|
|
429
|
+
*/
|
|
430
|
+
renderMenuMode() {
|
|
431
|
+
const menuProvider = this.providers.menu;
|
|
432
|
+
if (!menuProvider) {
|
|
433
|
+
this.log('No menu provider, skipping menu render');
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const lines = menuProvider.getLines();
|
|
437
|
+
// Clear and render from top
|
|
438
|
+
this.write(codes.cursorHome);
|
|
439
|
+
this.write(codes.clearToEndOfScreen);
|
|
440
|
+
for (const line of lines) {
|
|
441
|
+
this.write(line + '\n');
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Render REPL mode
|
|
446
|
+
*/
|
|
447
|
+
renderReplMode() {
|
|
448
|
+
// If scroll regions disabled (legacy mode), do nothing
|
|
449
|
+
// Footer manages its own rendering and scroll content goes via console.log
|
|
450
|
+
if (!this.useScrollRegions) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
// 1. Build footer content (from Footer or providers)
|
|
454
|
+
const footerLines = this.footer ? this.footer.getFooterLines() : this.buildFooterLines();
|
|
455
|
+
const newFooterHeight = this.calculatePhysicalHeight(footerLines);
|
|
456
|
+
this.log(`renderReplMode: footerHeight=${String(newFooterHeight)}, pending=${String(this.pendingScrollContent.length)}, scrollCursor=${String(this.scrollCursorRow)}`);
|
|
457
|
+
// 2. Adjust scroll region if footer height changed
|
|
458
|
+
if (newFooterHeight !== this.footerHeight) {
|
|
459
|
+
this.adjustScrollRegion(newFooterHeight);
|
|
460
|
+
}
|
|
461
|
+
// 3. Flush pending scroll content
|
|
462
|
+
if (this.pendingScrollContent.length > 0) {
|
|
463
|
+
this.flushScrollContent();
|
|
464
|
+
}
|
|
465
|
+
// 4. Render footer
|
|
466
|
+
this.renderFooter(footerLines);
|
|
467
|
+
// 5. Position cursor if footer is connected
|
|
468
|
+
if (this.footer) {
|
|
469
|
+
this.positionFooterCursor();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Build footer lines from providers
|
|
474
|
+
*/
|
|
475
|
+
buildFooterLines() {
|
|
476
|
+
const lines = [];
|
|
477
|
+
// 1. Subagent status (if any)
|
|
478
|
+
if (this.providers.subagent) {
|
|
479
|
+
lines.push(...this.providers.subagent.getLines());
|
|
480
|
+
}
|
|
481
|
+
// 2. Spinner OR Todo list (mutually exclusive)
|
|
482
|
+
if (this.providers.spinner?.isAgentRunning()) {
|
|
483
|
+
lines.push(...(this.providers.spinner.getLines()));
|
|
484
|
+
}
|
|
485
|
+
else if (this.providers.todo) {
|
|
486
|
+
lines.push(...this.providers.todo.getLines());
|
|
487
|
+
}
|
|
488
|
+
// 3. Input prompt (includes separator)
|
|
489
|
+
if (this.providers.input) {
|
|
490
|
+
lines.push(...this.providers.input.getLines());
|
|
491
|
+
}
|
|
492
|
+
// 4. Queued messages
|
|
493
|
+
if (this.providers.queue) {
|
|
494
|
+
lines.push(...this.providers.queue.getLines());
|
|
495
|
+
}
|
|
496
|
+
// 5. Mode indicator
|
|
497
|
+
if (this.providers.mode) {
|
|
498
|
+
lines.push(...this.providers.mode.getLines());
|
|
499
|
+
}
|
|
500
|
+
// Cap at 80% of terminal height
|
|
501
|
+
const maxHeight = Math.floor(this.terminalRows * 0.8);
|
|
502
|
+
const physicalHeight = this.calculatePhysicalHeight(lines);
|
|
503
|
+
if (physicalHeight > maxHeight) {
|
|
504
|
+
// TODO: Implement truncation (Phase 4)
|
|
505
|
+
this.log(`Footer too tall: ${String(physicalHeight)} > ${String(maxHeight)}`);
|
|
506
|
+
}
|
|
507
|
+
return lines;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Calculate physical height of lines (accounting for wrapping)
|
|
511
|
+
*/
|
|
512
|
+
calculatePhysicalHeight(lines) {
|
|
513
|
+
let height = 0;
|
|
514
|
+
for (const line of lines) {
|
|
515
|
+
height += getPhysicalLineCount(line, this.terminalCols);
|
|
516
|
+
}
|
|
517
|
+
return height;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Adjust scroll region when footer height changes
|
|
521
|
+
*/
|
|
522
|
+
adjustScrollRegion(newFooterHeight) {
|
|
523
|
+
if (newFooterHeight === this.footerHeight)
|
|
524
|
+
return;
|
|
525
|
+
const oldBottom = this.scrollRegionBottom;
|
|
526
|
+
this.footerHeight = newFooterHeight;
|
|
527
|
+
this.scrollRegionBottom = this.terminalRows - newFooterHeight;
|
|
528
|
+
this.log(`Scroll region: [1-${String(oldBottom)}] -> [1-${String(this.scrollRegionBottom)}] (footer: ${String(newFooterHeight)} lines)`);
|
|
529
|
+
// Set new scroll region (1-indexed)
|
|
530
|
+
this.write(codes.setScrollRegion(1, this.scrollRegionBottom));
|
|
531
|
+
// Keep scroll cursor within new bounds
|
|
532
|
+
if (this.scrollCursorRow > this.scrollRegionBottom) {
|
|
533
|
+
this.scrollCursorRow = this.scrollRegionBottom;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Render footer content
|
|
538
|
+
*/
|
|
539
|
+
renderFooter(lines) {
|
|
540
|
+
if (lines.length === 0)
|
|
541
|
+
return;
|
|
542
|
+
// Move to footer area (row after scroll region)
|
|
543
|
+
const footerStartRow = this.scrollRegionBottom + 1;
|
|
544
|
+
this.write(codes.cursorTo(footerStartRow, 1));
|
|
545
|
+
// Clear footer area
|
|
546
|
+
this.write(codes.clearToEndOfScreen);
|
|
547
|
+
// Write footer lines
|
|
548
|
+
for (let i = 0; i < lines.length; i++) {
|
|
549
|
+
if (i > 0)
|
|
550
|
+
this.write('\n');
|
|
551
|
+
this.write(lines[i]);
|
|
552
|
+
}
|
|
553
|
+
// Position cursor for input
|
|
554
|
+
this.positionCursor();
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Position cursor for input (provider-based)
|
|
558
|
+
*/
|
|
559
|
+
positionCursor() {
|
|
560
|
+
const inputProvider = this.providers.input;
|
|
561
|
+
if (!inputProvider)
|
|
562
|
+
return;
|
|
563
|
+
const cursorPos = inputProvider.getCursorPosition();
|
|
564
|
+
// Calculate absolute row: footer start + provider's relative row
|
|
565
|
+
const footerStartRow = this.scrollRegionBottom + 1;
|
|
566
|
+
// Find which line in footer is the input
|
|
567
|
+
// For now, assume input is after subagent + spinner/todo lines
|
|
568
|
+
let inputStartRow = footerStartRow;
|
|
569
|
+
if (this.providers.subagent) {
|
|
570
|
+
inputStartRow += this.providers.subagent.getLines().length;
|
|
571
|
+
}
|
|
572
|
+
if (this.providers.spinner?.isAgentRunning()) {
|
|
573
|
+
inputStartRow += this.providers.spinner.getLines().length;
|
|
574
|
+
}
|
|
575
|
+
else if (this.providers.todo) {
|
|
576
|
+
inputStartRow += this.providers.todo.getLines().length;
|
|
577
|
+
}
|
|
578
|
+
// Add provider's relative row
|
|
579
|
+
const absoluteRow = inputStartRow + cursorPos.row;
|
|
580
|
+
const absoluteCol = cursorPos.col + 1; // 1-indexed
|
|
581
|
+
this.write(codes.cursorTo(absoluteRow, absoluteCol));
|
|
582
|
+
this.write(codes.showCursor);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Position cursor for Footer integration (scroll region mode)
|
|
586
|
+
*/
|
|
587
|
+
positionFooterCursor() {
|
|
588
|
+
if (!this.footer)
|
|
589
|
+
return;
|
|
590
|
+
const cursorPos = this.footer.getFooterCursorPosition();
|
|
591
|
+
const footerStartRow = this.scrollRegionBottom + 1;
|
|
592
|
+
// cursorPos.row is 0-indexed from footer start
|
|
593
|
+
const absoluteRow = footerStartRow + cursorPos.row;
|
|
594
|
+
const absoluteCol = cursorPos.col + 1; // 1-indexed
|
|
595
|
+
this.write(codes.cursorTo(absoluteRow, absoluteCol));
|
|
596
|
+
this.write(codes.showCursor);
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Render OVERLAY mode
|
|
600
|
+
*/
|
|
601
|
+
renderOverlayMode() {
|
|
602
|
+
const overlayProvider = this.providers.overlay;
|
|
603
|
+
if (!overlayProvider || !overlayProvider.isActive()) {
|
|
604
|
+
this.log('No active overlay provider');
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const lines = overlayProvider.getLines();
|
|
608
|
+
// Clear and render from top
|
|
609
|
+
this.write(codes.cursorHome);
|
|
610
|
+
this.write(codes.clearToEndOfScreen);
|
|
611
|
+
for (let i = 0; i < lines.length; i++) {
|
|
612
|
+
if (i > 0)
|
|
613
|
+
this.write('\n');
|
|
614
|
+
this.write(lines[i]);
|
|
615
|
+
}
|
|
616
|
+
// Position cursor if overlay needs it
|
|
617
|
+
const cursorPos = overlayProvider.getCursorPosition();
|
|
618
|
+
if (cursorPos) {
|
|
619
|
+
this.write(codes.showCursor);
|
|
620
|
+
this.write(codes.cursorTo(cursorPos.row + 1, cursorPos.col + 1));
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
this.write(codes.hideCursor);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
// ===========================================================================
|
|
627
|
+
// Resize Handling
|
|
628
|
+
// ===========================================================================
|
|
629
|
+
/**
|
|
630
|
+
* Handle terminal resize
|
|
631
|
+
*/
|
|
632
|
+
handleResize() {
|
|
633
|
+
const oldCols = this.terminalCols;
|
|
634
|
+
const oldRows = this.terminalRows;
|
|
635
|
+
this.updateDimensions();
|
|
636
|
+
this.log(`Resize: ${String(oldCols)}x${String(oldRows)} -> ${String(this.terminalCols)}x${String(this.terminalRows)}`);
|
|
637
|
+
// Emit event
|
|
638
|
+
this.emit('resize', this.terminalCols, this.terminalRows);
|
|
639
|
+
// Mode-specific handling
|
|
640
|
+
switch (this.mode) {
|
|
641
|
+
case RenderMode.REPL:
|
|
642
|
+
// Recalculate scroll region
|
|
643
|
+
if (this.footerHeight > 0) {
|
|
644
|
+
this.scrollRegionBottom = this.terminalRows - this.footerHeight;
|
|
645
|
+
this.write(codes.setScrollRegion(1, this.scrollRegionBottom));
|
|
646
|
+
}
|
|
647
|
+
break;
|
|
648
|
+
case RenderMode.MENU:
|
|
649
|
+
case RenderMode.OVERLAY:
|
|
650
|
+
// Full screen modes just re-render
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
// Force full re-render
|
|
654
|
+
this.forceRender();
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Update terminal dimensions from stdout
|
|
658
|
+
*/
|
|
659
|
+
updateDimensions() {
|
|
660
|
+
this.terminalRows = process.stdout.rows || 24;
|
|
661
|
+
this.terminalCols = process.stdout.columns || 80;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Get current terminal dimensions
|
|
665
|
+
*/
|
|
666
|
+
getDimensions() {
|
|
667
|
+
return { rows: this.terminalRows, cols: this.terminalCols };
|
|
668
|
+
}
|
|
669
|
+
// ===========================================================================
|
|
670
|
+
// Low-Level Output
|
|
671
|
+
// ===========================================================================
|
|
672
|
+
/**
|
|
673
|
+
* Write to stdout (single output point)
|
|
674
|
+
*/
|
|
675
|
+
write(text) {
|
|
676
|
+
if (text.length === 0)
|
|
677
|
+
return;
|
|
678
|
+
process.stdout.write(text);
|
|
679
|
+
}
|
|
680
|
+
// ===========================================================================
|
|
681
|
+
// Keyboard Routing (stub for Phase 2+)
|
|
682
|
+
// ===========================================================================
|
|
683
|
+
/**
|
|
684
|
+
* Route keyboard input to appropriate handler
|
|
685
|
+
*/
|
|
686
|
+
handleKeyboard(key) {
|
|
687
|
+
switch (this.mode) {
|
|
688
|
+
case RenderMode.MENU:
|
|
689
|
+
if (this.providers.menu) {
|
|
690
|
+
const action = this.providers.menu.handleKey(key);
|
|
691
|
+
if (action === 'select') {
|
|
692
|
+
// TODO: Handle menu selection (Phase 2)
|
|
693
|
+
}
|
|
694
|
+
this.requestRender();
|
|
695
|
+
}
|
|
696
|
+
break;
|
|
697
|
+
case RenderMode.REPL:
|
|
698
|
+
if (this.providers.input) {
|
|
699
|
+
const action = this.providers.input.handleKey(key);
|
|
700
|
+
if (action) {
|
|
701
|
+
// TODO: Handle input action (Phase 4)
|
|
702
|
+
}
|
|
703
|
+
this.requestRender();
|
|
704
|
+
}
|
|
705
|
+
break;
|
|
706
|
+
case RenderMode.OVERLAY:
|
|
707
|
+
if (this.providers.overlay) {
|
|
708
|
+
const action = this.providers.overlay.handleKey(key);
|
|
709
|
+
if (action?.type === 'close') {
|
|
710
|
+
this.setMode(RenderMode.REPL, 'overlay-closed');
|
|
711
|
+
}
|
|
712
|
+
this.requestRender();
|
|
713
|
+
}
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// ===========================================================================
|
|
718
|
+
// Debug Logging
|
|
719
|
+
// ===========================================================================
|
|
720
|
+
/**
|
|
721
|
+
* Log debug message if debug mode enabled
|
|
722
|
+
*/
|
|
723
|
+
log(message) {
|
|
724
|
+
if (this.debug) {
|
|
725
|
+
const timestamp = new Date().toISOString().slice(11, 23);
|
|
726
|
+
console.error(`[TerminalRenderer ${timestamp}] ${message}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
// =============================================================================
|
|
731
|
+
// Singleton Export (optional)
|
|
732
|
+
// =============================================================================
|
|
733
|
+
let defaultRenderer = null;
|
|
734
|
+
/**
|
|
735
|
+
* Get or create the default renderer instance
|
|
736
|
+
*/
|
|
737
|
+
export function getRenderer(options) {
|
|
738
|
+
if (!defaultRenderer) {
|
|
739
|
+
defaultRenderer = new TerminalRenderer(options);
|
|
740
|
+
}
|
|
741
|
+
return defaultRenderer;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Reset the default renderer (for testing)
|
|
745
|
+
*/
|
|
746
|
+
export function resetRenderer() {
|
|
747
|
+
if (defaultRenderer) {
|
|
748
|
+
defaultRenderer.stop();
|
|
749
|
+
defaultRenderer = null;
|
|
750
|
+
}
|
|
751
|
+
}
|