@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,1349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Footer V2 - UI Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Single point of control for all terminal UI:
|
|
5
|
+
* - Owns all rendering decisions (when to clear, when to render)
|
|
6
|
+
* - Owns all UI state (agentRunning, todos, currentTool, etc.)
|
|
7
|
+
* - Provides output methods that handle clear/render automatically
|
|
8
|
+
* - REPL just emits events, doesn't manage rendering timing
|
|
9
|
+
*
|
|
10
|
+
* Key principles:
|
|
11
|
+
* - NO scroll regions (preserves terminal scrollback)
|
|
12
|
+
* - Deterministic render cycle
|
|
13
|
+
* - All output goes through footer (no external console.log)
|
|
14
|
+
*/
|
|
15
|
+
import { EventEmitter } from 'events';
|
|
16
|
+
import * as readline from 'readline';
|
|
17
|
+
import * as terminal from './terminal.js';
|
|
18
|
+
import { getPhysicalLineCount, getVisibleLength } from './line-utils.js';
|
|
19
|
+
import { getStyles } from '../themes/index.js';
|
|
20
|
+
import { MODE_INFO } from './types.js';
|
|
21
|
+
import { getAutocompleteCommands } from '../commands.js';
|
|
22
|
+
import { getFileMatches, extractAtMention, replaceAtMention, } from './file-autocomplete.js';
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// Constants
|
|
25
|
+
// =============================================================================
|
|
26
|
+
const MAX_VISIBLE_COMMANDS = 10;
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Mascot Expressions
|
|
29
|
+
// =============================================================================
|
|
30
|
+
/**
|
|
31
|
+
* Inline mascot expressions for agent identity and state feedback.
|
|
32
|
+
* Used to prefix agent messages and in spinner animations.
|
|
33
|
+
*/
|
|
34
|
+
export const MASCOT = {
|
|
35
|
+
// Core expressions
|
|
36
|
+
neutral: '[•_•]', // Default state - regular messages
|
|
37
|
+
thinking: '[°_°]', // Processing/thinking
|
|
38
|
+
searching: '[◐_◐]', // Searching/scanning files
|
|
39
|
+
success: '[^_^]', // Task completed successfully
|
|
40
|
+
error: '[×_×]', // Error occurred
|
|
41
|
+
confused: '[?_?]', // Needs clarification
|
|
42
|
+
working: '[•̀_•́]', // Actively working on task
|
|
43
|
+
// CRT monitor animation frames (subtle scanner effect)
|
|
44
|
+
crt: ['[░░░]', '[▒░░]', '[░▒░]', '[░░▒]', '[░▒░]', '[▒░░]'],
|
|
45
|
+
};
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// Fuzzy Matching
|
|
48
|
+
// =============================================================================
|
|
49
|
+
/**
|
|
50
|
+
* Calculate fuzzy match score for a query against a target string.
|
|
51
|
+
* Higher score = better match. Returns -1 if no match.
|
|
52
|
+
*/
|
|
53
|
+
function fuzzyMatchScore(query, target) {
|
|
54
|
+
const queryLower = query.toLowerCase();
|
|
55
|
+
const targetLower = target.toLowerCase();
|
|
56
|
+
// Exact prefix match - highest priority (score 1000+)
|
|
57
|
+
if (targetLower.startsWith(queryLower)) {
|
|
58
|
+
return 1000 + (100 - target.length); // Shorter commands rank higher
|
|
59
|
+
}
|
|
60
|
+
// Contiguous substring match - high priority (score 500+)
|
|
61
|
+
if (targetLower.includes(queryLower)) {
|
|
62
|
+
const index = targetLower.indexOf(queryLower);
|
|
63
|
+
return 500 + (100 - index); // Earlier matches rank higher
|
|
64
|
+
}
|
|
65
|
+
// Fuzzy match - characters appear in order (score 100+)
|
|
66
|
+
let queryIdx = 0;
|
|
67
|
+
let consecutiveBonus = 0;
|
|
68
|
+
let lastMatchIdx = -1;
|
|
69
|
+
for (let i = 0; i < targetLower.length && queryIdx < queryLower.length; i++) {
|
|
70
|
+
if (targetLower[i] === queryLower[queryIdx]) {
|
|
71
|
+
if (lastMatchIdx === i - 1) {
|
|
72
|
+
consecutiveBonus += 10;
|
|
73
|
+
}
|
|
74
|
+
lastMatchIdx = i;
|
|
75
|
+
queryIdx++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (queryIdx === queryLower.length) {
|
|
79
|
+
return 100 + consecutiveBonus + (100 - target.length);
|
|
80
|
+
}
|
|
81
|
+
return -1;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Filter and rank commands matching input using fuzzy matching
|
|
85
|
+
*/
|
|
86
|
+
function filterCommands(input, commands) {
|
|
87
|
+
const scored = commands
|
|
88
|
+
.map((cmd) => ({
|
|
89
|
+
cmd,
|
|
90
|
+
score: fuzzyMatchScore(input, cmd.command),
|
|
91
|
+
}))
|
|
92
|
+
.filter((item) => item.score >= 0);
|
|
93
|
+
scored.sort((a, b) => b.score - a.score);
|
|
94
|
+
return scored.map((item) => item.cmd);
|
|
95
|
+
}
|
|
96
|
+
// =============================================================================
|
|
97
|
+
// Footer V2 Class
|
|
98
|
+
// =============================================================================
|
|
99
|
+
export class FooterV2 extends EventEmitter {
|
|
100
|
+
// Configuration
|
|
101
|
+
promptPrefix;
|
|
102
|
+
promptPrefixLen;
|
|
103
|
+
// Input state (multiline)
|
|
104
|
+
lines = [''];
|
|
105
|
+
currentLine = 0;
|
|
106
|
+
cursorPos = 0;
|
|
107
|
+
mode;
|
|
108
|
+
projectName = null;
|
|
109
|
+
todos = [];
|
|
110
|
+
spinnerText = null;
|
|
111
|
+
spinnerFrame = 0;
|
|
112
|
+
agentRunning = false;
|
|
113
|
+
queuedInputs = [];
|
|
114
|
+
currentTool = null;
|
|
115
|
+
// Command autocomplete state (for /commands)
|
|
116
|
+
autocomplete = {
|
|
117
|
+
active: false,
|
|
118
|
+
matches: [],
|
|
119
|
+
selectedIndex: 0,
|
|
120
|
+
scrollOffset: 0,
|
|
121
|
+
};
|
|
122
|
+
// File autocomplete state (for @paths)
|
|
123
|
+
fileAutocomplete = {
|
|
124
|
+
active: false,
|
|
125
|
+
matches: [],
|
|
126
|
+
selectedIndex: 0,
|
|
127
|
+
scrollOffset: 0,
|
|
128
|
+
partial: '',
|
|
129
|
+
};
|
|
130
|
+
// History state
|
|
131
|
+
history = [];
|
|
132
|
+
historyIndex = -1;
|
|
133
|
+
savedInput = '';
|
|
134
|
+
// Render tracking
|
|
135
|
+
lastRenderHeight = 0;
|
|
136
|
+
cursorLineFromBottom = 0;
|
|
137
|
+
isRunning = false;
|
|
138
|
+
isPaused = false;
|
|
139
|
+
renderTimer = null;
|
|
140
|
+
needsRender = false;
|
|
141
|
+
// Double Esc detection
|
|
142
|
+
lastEscapeTime = 0;
|
|
143
|
+
// Ghost text suggestion
|
|
144
|
+
suggestion = null;
|
|
145
|
+
// Spinner animation - CRT monitor fill effect
|
|
146
|
+
spinnerFrames = MASCOT.crt;
|
|
147
|
+
spinnerTimer = null;
|
|
148
|
+
constructor(options = {}) {
|
|
149
|
+
super();
|
|
150
|
+
const s = getStyles();
|
|
151
|
+
this.promptPrefix = options.prompt ?? s.primaryBold('compilr>') + ' ';
|
|
152
|
+
this.promptPrefixLen = getVisibleLength(this.promptPrefix);
|
|
153
|
+
this.mode = options.initialMode ?? 'normal';
|
|
154
|
+
}
|
|
155
|
+
// ===========================================================================
|
|
156
|
+
// Input value helpers
|
|
157
|
+
// ===========================================================================
|
|
158
|
+
/** Get the full input value (all lines joined with newlines) */
|
|
159
|
+
getInputValue() {
|
|
160
|
+
return this.lines.join('\n');
|
|
161
|
+
}
|
|
162
|
+
/** Get the current line content */
|
|
163
|
+
getCurrentLineContent() {
|
|
164
|
+
return this.lines[this.currentLine];
|
|
165
|
+
}
|
|
166
|
+
/** Clear input and reset to single empty line */
|
|
167
|
+
clearInput() {
|
|
168
|
+
this.lines = [''];
|
|
169
|
+
this.currentLine = 0;
|
|
170
|
+
this.cursorPos = 0;
|
|
171
|
+
}
|
|
172
|
+
// ===========================================================================
|
|
173
|
+
// Lifecycle
|
|
174
|
+
// ===========================================================================
|
|
175
|
+
start() {
|
|
176
|
+
if (this.isRunning)
|
|
177
|
+
return;
|
|
178
|
+
this.isRunning = true;
|
|
179
|
+
this.isPaused = false;
|
|
180
|
+
// Start keyboard input handling
|
|
181
|
+
this.startKeyboardCapture();
|
|
182
|
+
// Initial render
|
|
183
|
+
this.render();
|
|
184
|
+
// Start render loop (60ms = ~16fps)
|
|
185
|
+
this.renderTimer = setInterval(() => {
|
|
186
|
+
if (this.needsRender && !this.isPaused) {
|
|
187
|
+
this.render();
|
|
188
|
+
this.needsRender = false;
|
|
189
|
+
}
|
|
190
|
+
}, 60);
|
|
191
|
+
}
|
|
192
|
+
stop() {
|
|
193
|
+
if (!this.isRunning)
|
|
194
|
+
return;
|
|
195
|
+
this.isRunning = false;
|
|
196
|
+
// Stop render loop
|
|
197
|
+
if (this.renderTimer) {
|
|
198
|
+
clearInterval(this.renderTimer);
|
|
199
|
+
this.renderTimer = null;
|
|
200
|
+
}
|
|
201
|
+
// Stop spinner
|
|
202
|
+
this.stopSpinnerAnimation();
|
|
203
|
+
// Stop keyboard capture
|
|
204
|
+
this.stopKeyboardCapture();
|
|
205
|
+
// Clear footer
|
|
206
|
+
this.clear();
|
|
207
|
+
}
|
|
208
|
+
// ===========================================================================
|
|
209
|
+
// Public API - Output (THE KEY METHODS)
|
|
210
|
+
// ===========================================================================
|
|
211
|
+
/**
|
|
212
|
+
* Print a semantic item to the scrolling zone.
|
|
213
|
+
* FooterV2 handles all formatting based on the item type.
|
|
214
|
+
*/
|
|
215
|
+
print(item) {
|
|
216
|
+
const s = getStyles();
|
|
217
|
+
this.clear();
|
|
218
|
+
switch (item.type) {
|
|
219
|
+
case 'user-message':
|
|
220
|
+
console.log('');
|
|
221
|
+
console.log(s.primaryBold('> ') + item.text);
|
|
222
|
+
console.log(''); // Trailing blank for separation
|
|
223
|
+
break;
|
|
224
|
+
case 'agent-text': {
|
|
225
|
+
// Prefix agent messages with mascot expression (accent colored) and separator
|
|
226
|
+
const expr = item.expression ? MASCOT[item.expression] : MASCOT.neutral;
|
|
227
|
+
console.log(s.primary(expr) + s.muted(' > ') + item.text);
|
|
228
|
+
console.log(''); // Trailing blank for separation
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case 'tool-start':
|
|
232
|
+
console.log(s.info(`● ${item.name}`) + s.muted(`(${item.params})`));
|
|
233
|
+
break;
|
|
234
|
+
case 'tool-result':
|
|
235
|
+
console.log(s.info(`● ${item.name}`) + s.muted(`(${item.params})`));
|
|
236
|
+
if (item.success === false) {
|
|
237
|
+
console.log(s.error(` ⎿ ${item.result}`));
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
console.log(s.muted(` ⎿ ${item.result}`));
|
|
241
|
+
}
|
|
242
|
+
console.log('');
|
|
243
|
+
break;
|
|
244
|
+
case 'tool-error':
|
|
245
|
+
console.log(s.info(`● ${item.name}`) + s.muted(`(${item.params})`));
|
|
246
|
+
console.log(s.error(` ⎿ Error: ${item.error}`));
|
|
247
|
+
console.log('');
|
|
248
|
+
break;
|
|
249
|
+
case 'interrupted': {
|
|
250
|
+
// Show what was ongoing (if provided)
|
|
251
|
+
if (item.action) {
|
|
252
|
+
console.log(s.info(`● ${item.action}`));
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// No action means interrupted right after user message
|
|
256
|
+
// Move cursor up to consume the trailing blank from user-message
|
|
257
|
+
process.stdout.write('\x1b[1A'); // Move up one line
|
|
258
|
+
}
|
|
259
|
+
// Show interrupted line with mascot
|
|
260
|
+
const suggestion = item.suggestion ?? 'What should I do instead?';
|
|
261
|
+
console.log(s.warning(` ⎿ ${MASCOT.confused} Interrupted - ${suggestion}`));
|
|
262
|
+
console.log('');
|
|
263
|
+
// Also set as ghost text suggestion
|
|
264
|
+
this.setSuggestion(suggestion);
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
case 'error':
|
|
268
|
+
console.log(s.error(`${MASCOT.error} ${item.message}`));
|
|
269
|
+
console.log('');
|
|
270
|
+
break;
|
|
271
|
+
case 'success':
|
|
272
|
+
console.log(s.success(`${MASCOT.success} ${item.message}`));
|
|
273
|
+
console.log('');
|
|
274
|
+
break;
|
|
275
|
+
case 'info':
|
|
276
|
+
console.log(s.info(item.message));
|
|
277
|
+
console.log('');
|
|
278
|
+
break;
|
|
279
|
+
case 'warning':
|
|
280
|
+
console.log(s.warning(item.message));
|
|
281
|
+
console.log('');
|
|
282
|
+
break;
|
|
283
|
+
case 'raw':
|
|
284
|
+
console.log(item.text);
|
|
285
|
+
break;
|
|
286
|
+
case 'raw-lines':
|
|
287
|
+
for (const line of item.lines) {
|
|
288
|
+
console.log(line);
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
this.render();
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Execute a callback that outputs to terminal.
|
|
296
|
+
* Handles clear → callback → render automatically.
|
|
297
|
+
* Use this for complex output that needs multiple console.log calls.
|
|
298
|
+
* @deprecated Prefer using print() with PrintableItem
|
|
299
|
+
*/
|
|
300
|
+
output(callback) {
|
|
301
|
+
this.clear();
|
|
302
|
+
callback();
|
|
303
|
+
this.render();
|
|
304
|
+
}
|
|
305
|
+
// ===========================================================================
|
|
306
|
+
// Public API - State setters
|
|
307
|
+
// ===========================================================================
|
|
308
|
+
setAgentRunning(running) {
|
|
309
|
+
const wasRunning = this.agentRunning;
|
|
310
|
+
this.agentRunning = running;
|
|
311
|
+
if (running && !wasRunning) {
|
|
312
|
+
this.startSpinnerAnimation();
|
|
313
|
+
}
|
|
314
|
+
else if (!running && wasRunning) {
|
|
315
|
+
this.stopSpinnerAnimation();
|
|
316
|
+
this.currentTool = null;
|
|
317
|
+
}
|
|
318
|
+
this.needsRender = true;
|
|
319
|
+
}
|
|
320
|
+
isAgentRunning() {
|
|
321
|
+
return this.agentRunning;
|
|
322
|
+
}
|
|
323
|
+
setTodos(todos) {
|
|
324
|
+
this.todos = todos;
|
|
325
|
+
this.needsRender = true;
|
|
326
|
+
}
|
|
327
|
+
getTodos() {
|
|
328
|
+
return [...this.todos];
|
|
329
|
+
}
|
|
330
|
+
setSpinnerText(text) {
|
|
331
|
+
this.spinnerText = text;
|
|
332
|
+
this.needsRender = true;
|
|
333
|
+
}
|
|
334
|
+
setCurrentTool(tool) {
|
|
335
|
+
this.currentTool = tool;
|
|
336
|
+
if (tool) {
|
|
337
|
+
this.spinnerText = tool;
|
|
338
|
+
}
|
|
339
|
+
this.needsRender = true;
|
|
340
|
+
}
|
|
341
|
+
setMode(mode) {
|
|
342
|
+
this.mode = mode;
|
|
343
|
+
this.needsRender = true;
|
|
344
|
+
}
|
|
345
|
+
getMode() {
|
|
346
|
+
return this.mode;
|
|
347
|
+
}
|
|
348
|
+
setProjectName(name) {
|
|
349
|
+
this.projectName = name;
|
|
350
|
+
this.needsRender = true;
|
|
351
|
+
}
|
|
352
|
+
getProjectName() {
|
|
353
|
+
return this.projectName;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Set ghost text suggestion (shown when input is empty)
|
|
357
|
+
*/
|
|
358
|
+
setSuggestion(text) {
|
|
359
|
+
this.suggestion = text;
|
|
360
|
+
this.needsRender = true;
|
|
361
|
+
}
|
|
362
|
+
getSuggestion() {
|
|
363
|
+
return this.suggestion;
|
|
364
|
+
}
|
|
365
|
+
clearSuggestion() {
|
|
366
|
+
this.suggestion = null;
|
|
367
|
+
this.needsRender = true;
|
|
368
|
+
}
|
|
369
|
+
// ===========================================================================
|
|
370
|
+
// Public API - Input queue
|
|
371
|
+
// ===========================================================================
|
|
372
|
+
getQueuedInputs() {
|
|
373
|
+
return [...this.queuedInputs];
|
|
374
|
+
}
|
|
375
|
+
popQueuedInput() {
|
|
376
|
+
if (this.queuedInputs.length === 0)
|
|
377
|
+
return null;
|
|
378
|
+
const input = this.queuedInputs.shift();
|
|
379
|
+
this.needsRender = true;
|
|
380
|
+
return input ?? null;
|
|
381
|
+
}
|
|
382
|
+
hasQueuedInput() {
|
|
383
|
+
return this.queuedInputs.length > 0;
|
|
384
|
+
}
|
|
385
|
+
clearQueue() {
|
|
386
|
+
this.queuedInputs = [];
|
|
387
|
+
this.needsRender = true;
|
|
388
|
+
}
|
|
389
|
+
// ===========================================================================
|
|
390
|
+
// Public API - Animation control (for overlays)
|
|
391
|
+
// ===========================================================================
|
|
392
|
+
/**
|
|
393
|
+
* Pause footer completely (for overlays)
|
|
394
|
+
*/
|
|
395
|
+
pauseAnimation() {
|
|
396
|
+
this.isPaused = true;
|
|
397
|
+
// Stop render loop
|
|
398
|
+
if (this.renderTimer) {
|
|
399
|
+
clearInterval(this.renderTimer);
|
|
400
|
+
this.renderTimer = null;
|
|
401
|
+
}
|
|
402
|
+
// Stop spinner
|
|
403
|
+
this.stopSpinnerAnimation();
|
|
404
|
+
// Stop keyboard capture
|
|
405
|
+
this.stopKeyboardCapture();
|
|
406
|
+
// Clear footer from screen
|
|
407
|
+
this.clear();
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Resume footer after pause
|
|
411
|
+
*/
|
|
412
|
+
resumeAnimation() {
|
|
413
|
+
this.isPaused = false;
|
|
414
|
+
// Resume spinner if agent running
|
|
415
|
+
if (this.agentRunning) {
|
|
416
|
+
this.startSpinnerAnimation();
|
|
417
|
+
}
|
|
418
|
+
// Restart keyboard capture
|
|
419
|
+
this.startKeyboardCapture();
|
|
420
|
+
// Restart render loop
|
|
421
|
+
this.render();
|
|
422
|
+
this.renderTimer = setInterval(() => {
|
|
423
|
+
if (this.needsRender && !this.isPaused) {
|
|
424
|
+
this.render();
|
|
425
|
+
this.needsRender = false;
|
|
426
|
+
}
|
|
427
|
+
}, 60);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Restart input after command
|
|
431
|
+
*/
|
|
432
|
+
restartInput() {
|
|
433
|
+
this.render();
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Refresh prompt with theme colors
|
|
437
|
+
*/
|
|
438
|
+
refreshPrompt() {
|
|
439
|
+
const s = getStyles();
|
|
440
|
+
this.promptPrefix = s.primaryBold('compilr>') + ' ';
|
|
441
|
+
this.promptPrefixLen = getVisibleLength(this.promptPrefix);
|
|
442
|
+
this.needsRender = true;
|
|
443
|
+
}
|
|
444
|
+
// ===========================================================================
|
|
445
|
+
// Public API - Legacy compatibility (for gradual migration)
|
|
446
|
+
// ===========================================================================
|
|
447
|
+
/**
|
|
448
|
+
* @deprecated Use print() or output() instead
|
|
449
|
+
*/
|
|
450
|
+
clearForOutput() {
|
|
451
|
+
this.clear();
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* @deprecated Use print() or output() instead
|
|
455
|
+
*/
|
|
456
|
+
forceRender() {
|
|
457
|
+
this.render();
|
|
458
|
+
this.needsRender = false;
|
|
459
|
+
}
|
|
460
|
+
// ===========================================================================
|
|
461
|
+
// Private - Rendering
|
|
462
|
+
// ===========================================================================
|
|
463
|
+
clear() {
|
|
464
|
+
if (this.lastRenderHeight === 0)
|
|
465
|
+
return;
|
|
466
|
+
terminal.moveCursorToLineStart();
|
|
467
|
+
const cursorRowFromTop = this.lastRenderHeight - this.cursorLineFromBottom;
|
|
468
|
+
const rowsToMoveUp = cursorRowFromTop - 1;
|
|
469
|
+
if (rowsToMoveUp > 0) {
|
|
470
|
+
terminal.moveCursorUp(rowsToMoveUp);
|
|
471
|
+
}
|
|
472
|
+
terminal.clearToEndOfScreen();
|
|
473
|
+
this.lastRenderHeight = 0;
|
|
474
|
+
this.cursorLineFromBottom = 0;
|
|
475
|
+
}
|
|
476
|
+
render() {
|
|
477
|
+
if (!this.isRunning || this.isPaused)
|
|
478
|
+
return;
|
|
479
|
+
const termWidth = terminal.getTerminalWidth();
|
|
480
|
+
const s = getStyles();
|
|
481
|
+
// Build all lines in order
|
|
482
|
+
const lines = [];
|
|
483
|
+
// 1. Header: Spinner (when running) or "Todos" (when idle with todos)
|
|
484
|
+
if (this.agentRunning) {
|
|
485
|
+
const spinnerLine = this.buildSpinnerLine();
|
|
486
|
+
lines.push({
|
|
487
|
+
content: spinnerLine,
|
|
488
|
+
physicalLines: getPhysicalLineCount(spinnerLine, termWidth),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
else if (this.todos.length > 0) {
|
|
492
|
+
const headerLine = s.muted('Todos');
|
|
493
|
+
lines.push({
|
|
494
|
+
content: headerLine,
|
|
495
|
+
physicalLines: 1,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
// 2. Todo list (always show if there are todos)
|
|
499
|
+
if (this.todos.length > 0) {
|
|
500
|
+
for (const todo of this.todos) {
|
|
501
|
+
const todoLine = this.buildTodoLine(todo);
|
|
502
|
+
lines.push({
|
|
503
|
+
content: todoLine,
|
|
504
|
+
physicalLines: getPhysicalLineCount(todoLine, termWidth),
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
// Blank line after todos for spacing
|
|
508
|
+
lines.push({ content: '', physicalLines: 1 });
|
|
509
|
+
}
|
|
510
|
+
// 3. Queued inputs (show multiline as single line with ↵ indicator)
|
|
511
|
+
for (const queued of this.queuedInputs) {
|
|
512
|
+
const displayText = queued.replace(/\n/g, ' ↵ ');
|
|
513
|
+
const queuedLine = s.muted(`queued: "${displayText}"`);
|
|
514
|
+
lines.push({
|
|
515
|
+
content: queuedLine,
|
|
516
|
+
physicalLines: getPhysicalLineCount(queuedLine, termWidth),
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
// 4. Top separator
|
|
520
|
+
const separator = '─'.repeat(termWidth);
|
|
521
|
+
lines.push({ content: separator, physicalLines: 1 });
|
|
522
|
+
// 5. Input prompt (multiline support)
|
|
523
|
+
const continuationPrompt = s.muted(' \\ ');
|
|
524
|
+
const continuationPromptLen = getVisibleLength(continuationPrompt);
|
|
525
|
+
const cursorLineIndex = lines.length + this.currentLine; // Index where cursor is
|
|
526
|
+
for (let i = 0; i < this.lines.length; i++) {
|
|
527
|
+
const linePrompt = i === 0 ? this.promptPrefix : continuationPrompt;
|
|
528
|
+
const linePromptLen = i === 0 ? this.promptPrefixLen : continuationPromptLen;
|
|
529
|
+
const lineContent = this.lines[i];
|
|
530
|
+
// Show ghost text on first line when input is empty and no autocomplete
|
|
531
|
+
let fullLine;
|
|
532
|
+
let totalLen;
|
|
533
|
+
if (i === 0 &&
|
|
534
|
+
lineContent === '' &&
|
|
535
|
+
this.lines.length === 1 &&
|
|
536
|
+
this.suggestion &&
|
|
537
|
+
!this.autocomplete.active &&
|
|
538
|
+
!this.fileAutocomplete.active) {
|
|
539
|
+
// Build: [prompt][suggestion]...[↵ send]
|
|
540
|
+
const rightHint = '↵ send';
|
|
541
|
+
const leftContent = linePrompt + s.muted(this.suggestion);
|
|
542
|
+
const leftLen = linePromptLen + this.suggestion.length;
|
|
543
|
+
const rightLen = rightHint.length;
|
|
544
|
+
const padding = Math.max(1, termWidth - leftLen - rightLen);
|
|
545
|
+
fullLine = leftContent + ' '.repeat(padding) + s.muted(rightHint);
|
|
546
|
+
totalLen = termWidth; // Full width
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
fullLine = linePrompt + lineContent;
|
|
550
|
+
totalLen = linePromptLen + getVisibleLength(lineContent);
|
|
551
|
+
}
|
|
552
|
+
const physicalLines = totalLen === 0 ? 1 : Math.ceil(totalLen / termWidth) || 1;
|
|
553
|
+
lines.push({
|
|
554
|
+
content: fullLine,
|
|
555
|
+
physicalLines,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
// 6. Bottom separator
|
|
559
|
+
lines.push({ content: separator, physicalLines: 1 });
|
|
560
|
+
// 7. Mode indicator
|
|
561
|
+
const modeLine = this.buildModeLine(termWidth);
|
|
562
|
+
lines.push({
|
|
563
|
+
content: modeLine,
|
|
564
|
+
physicalLines: getPhysicalLineCount(modeLine, termWidth),
|
|
565
|
+
});
|
|
566
|
+
// 8. Autocomplete dropdown (if active) - file autocomplete takes priority
|
|
567
|
+
const fileAutocompleteLines = this.buildFileAutocompleteLines(termWidth);
|
|
568
|
+
const autocompleteLines = fileAutocompleteLines.length > 0
|
|
569
|
+
? fileAutocompleteLines
|
|
570
|
+
: this.buildAutocompleteLines(termWidth);
|
|
571
|
+
for (const line of autocompleteLines) {
|
|
572
|
+
lines.push({
|
|
573
|
+
content: line,
|
|
574
|
+
physicalLines: getPhysicalLineCount(line, termWidth),
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
// Calculate total physical height
|
|
578
|
+
let totalPhysicalLines = 0;
|
|
579
|
+
for (const line of lines) {
|
|
580
|
+
totalPhysicalLines += line.physicalLines;
|
|
581
|
+
}
|
|
582
|
+
// Clear existing footer
|
|
583
|
+
this.clear();
|
|
584
|
+
// Write all lines
|
|
585
|
+
for (let i = 0; i < lines.length; i++) {
|
|
586
|
+
terminal.write(lines[i].content);
|
|
587
|
+
if (i < lines.length - 1) {
|
|
588
|
+
terminal.write('\n');
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// Calculate cursor position (for multiline, position on the line where cursor is)
|
|
592
|
+
let rowsAfterCursor = 0;
|
|
593
|
+
for (let i = cursorLineIndex + 1; i < lines.length; i++) {
|
|
594
|
+
rowsAfterCursor += lines[i].physicalLines;
|
|
595
|
+
}
|
|
596
|
+
if (rowsAfterCursor > 0) {
|
|
597
|
+
terminal.moveCursorUp(rowsAfterCursor);
|
|
598
|
+
}
|
|
599
|
+
// Calculate column within current line
|
|
600
|
+
const currentLinePromptLen = this.currentLine === 0 ? this.promptPrefixLen : continuationPromptLen;
|
|
601
|
+
const totalPos = currentLinePromptLen + this.cursorPos;
|
|
602
|
+
const cursorCol = (totalPos % termWidth) + 1;
|
|
603
|
+
terminal.moveCursorToColumn(cursorCol);
|
|
604
|
+
terminal.showCursor();
|
|
605
|
+
this.lastRenderHeight = totalPhysicalLines;
|
|
606
|
+
this.cursorLineFromBottom = rowsAfterCursor;
|
|
607
|
+
}
|
|
608
|
+
// ===========================================================================
|
|
609
|
+
// Private - Line builders
|
|
610
|
+
// ===========================================================================
|
|
611
|
+
buildSpinnerLine() {
|
|
612
|
+
const s = getStyles();
|
|
613
|
+
const frame = this.spinnerFrames[this.spinnerFrame % this.spinnerFrames.length];
|
|
614
|
+
const text = this.currentTool ?? this.spinnerText ?? 'Thinking...';
|
|
615
|
+
// Format: [CRT scanner] text (single monitor animation, no double mascot)
|
|
616
|
+
return s.muted(`${frame} ${text}`);
|
|
617
|
+
}
|
|
618
|
+
buildTodoLine(todo) {
|
|
619
|
+
const s = getStyles();
|
|
620
|
+
const icon = todo.status === 'completed' ? '✓' :
|
|
621
|
+
todo.status === 'in_progress' ? '→' : '☐';
|
|
622
|
+
const style = todo.status === 'completed' ? s.muted :
|
|
623
|
+
todo.status === 'in_progress' ? s.info : s.muted;
|
|
624
|
+
return style(`${icon} ${todo.content}`);
|
|
625
|
+
}
|
|
626
|
+
buildModeLine(termWidth) {
|
|
627
|
+
const s = getStyles();
|
|
628
|
+
const modeInfo = MODE_INFO[this.mode];
|
|
629
|
+
let leftPart;
|
|
630
|
+
switch (this.mode) {
|
|
631
|
+
case 'normal':
|
|
632
|
+
leftPart = s.muted(`mode: ${modeInfo.label} (Shift+Tab to change)`);
|
|
633
|
+
break;
|
|
634
|
+
case 'auto-accept':
|
|
635
|
+
leftPart = s.warning(`mode: ${modeInfo.label}`) + s.muted(' (Shift+Tab to change)');
|
|
636
|
+
break;
|
|
637
|
+
case 'plan':
|
|
638
|
+
leftPart = s.info(`mode: ${modeInfo.label}`) + s.muted(' (Shift+Tab to change)');
|
|
639
|
+
break;
|
|
640
|
+
default:
|
|
641
|
+
leftPart = s.muted(`mode: ${modeInfo.label} (Shift+Tab to change)`);
|
|
642
|
+
}
|
|
643
|
+
if (!this.projectName) {
|
|
644
|
+
return leftPart;
|
|
645
|
+
}
|
|
646
|
+
const projectText = `Project: ${this.projectName}`;
|
|
647
|
+
const rightPart = s.muted(projectText);
|
|
648
|
+
const leftVisible = getVisibleLength(leftPart);
|
|
649
|
+
const padding = Math.max(2, termWidth - leftVisible - projectText.length);
|
|
650
|
+
return leftPart + ' '.repeat(padding) + rightPart;
|
|
651
|
+
}
|
|
652
|
+
buildAutocompleteLines(termWidth) {
|
|
653
|
+
if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
|
|
654
|
+
return [];
|
|
655
|
+
}
|
|
656
|
+
const s = getStyles();
|
|
657
|
+
const lines = [];
|
|
658
|
+
const { matches, selectedIndex, scrollOffset } = this.autocomplete;
|
|
659
|
+
const visible = matches.slice(scrollOffset, scrollOffset + MAX_VISIBLE_COMMANDS);
|
|
660
|
+
const total = matches.length;
|
|
661
|
+
// Show scroll indicator if there are more items above
|
|
662
|
+
if (scrollOffset > 0) {
|
|
663
|
+
lines.push(s.muted(` ↑ ${String(scrollOffset)} more above`));
|
|
664
|
+
}
|
|
665
|
+
for (let i = 0; i < visible.length; i++) {
|
|
666
|
+
const cmd = visible[i];
|
|
667
|
+
const actualIndex = scrollOffset + i;
|
|
668
|
+
const isSelected = actualIndex === selectedIndex;
|
|
669
|
+
const prefix = isSelected ? s.primary('❯ ') : ' ';
|
|
670
|
+
const name = isSelected ? s.primaryBold(cmd.command) : cmd.command;
|
|
671
|
+
// Truncate if too long
|
|
672
|
+
const baseLen = getVisibleLength(prefix) + getVisibleLength(name) + 3; // 3 for ' - '
|
|
673
|
+
const maxDescLen = termWidth - baseLen - 2;
|
|
674
|
+
const truncatedDesc = cmd.description.length > maxDescLen
|
|
675
|
+
? cmd.description.slice(0, maxDescLen - 1) + '…'
|
|
676
|
+
: cmd.description;
|
|
677
|
+
lines.push(`${prefix}${name} ${s.muted('- ' + truncatedDesc)}`);
|
|
678
|
+
}
|
|
679
|
+
// Show scroll indicator if there are more items below
|
|
680
|
+
const belowCount = total - scrollOffset - visible.length;
|
|
681
|
+
if (belowCount > 0) {
|
|
682
|
+
lines.push(s.muted(` ↓ ${String(belowCount)} more below`));
|
|
683
|
+
}
|
|
684
|
+
return lines;
|
|
685
|
+
}
|
|
686
|
+
buildFileAutocompleteLines(termWidth) {
|
|
687
|
+
if (!this.fileAutocomplete.active || this.fileAutocomplete.matches.length === 0) {
|
|
688
|
+
return [];
|
|
689
|
+
}
|
|
690
|
+
const s = getStyles();
|
|
691
|
+
const lines = [];
|
|
692
|
+
const { matches, selectedIndex, scrollOffset } = this.fileAutocomplete;
|
|
693
|
+
const visible = matches.slice(scrollOffset, scrollOffset + MAX_VISIBLE_COMMANDS);
|
|
694
|
+
const total = matches.length;
|
|
695
|
+
// Show scroll indicator if there are more items above
|
|
696
|
+
if (scrollOffset > 0) {
|
|
697
|
+
lines.push(s.muted(` ↑ ${String(scrollOffset)} more above`));
|
|
698
|
+
}
|
|
699
|
+
for (let i = 0; i < visible.length; i++) {
|
|
700
|
+
const file = visible[i];
|
|
701
|
+
const actualIndex = scrollOffset + i;
|
|
702
|
+
const isSelected = actualIndex === selectedIndex;
|
|
703
|
+
const prefix = isSelected ? s.primary('❯ ') : ' ';
|
|
704
|
+
const icon = file.isDirectory ? '📁 ' : '📄 ';
|
|
705
|
+
const name = isSelected ? s.primaryBold(file.path) : file.path;
|
|
706
|
+
// Truncate if too long
|
|
707
|
+
const baseLen = getVisibleLength(prefix) + 3 + getVisibleLength(file.path); // 3 for icon
|
|
708
|
+
if (baseLen > termWidth - 2) {
|
|
709
|
+
const maxLen = termWidth - getVisibleLength(prefix) - 5; // 3 for icon, 2 for padding
|
|
710
|
+
const truncatedPath = file.path.length > maxLen
|
|
711
|
+
? '…' + file.path.slice(-(maxLen - 1))
|
|
712
|
+
: file.path;
|
|
713
|
+
const truncatedName = isSelected ? s.primaryBold(truncatedPath) : truncatedPath;
|
|
714
|
+
lines.push(`${prefix}${icon}${truncatedName}`);
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
lines.push(`${prefix}${icon}${name}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
// Show scroll indicator if there are more items below
|
|
721
|
+
const belowCount = total - scrollOffset - visible.length;
|
|
722
|
+
if (belowCount > 0) {
|
|
723
|
+
lines.push(s.muted(` ↓ ${String(belowCount)} more below`));
|
|
724
|
+
}
|
|
725
|
+
return lines;
|
|
726
|
+
}
|
|
727
|
+
// ===========================================================================
|
|
728
|
+
// Private - Cursor calculations
|
|
729
|
+
// ===========================================================================
|
|
730
|
+
// ===========================================================================
|
|
731
|
+
// Private - Autocomplete
|
|
732
|
+
// ===========================================================================
|
|
733
|
+
updateAutocomplete() {
|
|
734
|
+
const currentLine = this.getCurrentLineContent();
|
|
735
|
+
// Check for @ file path autocomplete first (works on any line)
|
|
736
|
+
const atMention = extractAtMention(currentLine, this.cursorPos);
|
|
737
|
+
if (atMention !== null) {
|
|
738
|
+
// File autocomplete mode - disable command autocomplete
|
|
739
|
+
this.autocomplete.active = false;
|
|
740
|
+
this.autocomplete.matches = [];
|
|
741
|
+
this.autocomplete.selectedIndex = 0;
|
|
742
|
+
this.autocomplete.scrollOffset = 0;
|
|
743
|
+
// Enable file autocomplete
|
|
744
|
+
this.fileAutocomplete.active = true;
|
|
745
|
+
this.fileAutocomplete.partial = atMention;
|
|
746
|
+
this.fileAutocomplete.matches = getFileMatches(atMention);
|
|
747
|
+
if (this.fileAutocomplete.selectedIndex >= this.fileAutocomplete.matches.length) {
|
|
748
|
+
this.fileAutocomplete.selectedIndex = Math.max(0, this.fileAutocomplete.matches.length - 1);
|
|
749
|
+
this.fileAutocomplete.scrollOffset = 0;
|
|
750
|
+
}
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
// Reset file autocomplete
|
|
754
|
+
this.fileAutocomplete.active = false;
|
|
755
|
+
this.fileAutocomplete.matches = [];
|
|
756
|
+
this.fileAutocomplete.selectedIndex = 0;
|
|
757
|
+
this.fileAutocomplete.scrollOffset = 0;
|
|
758
|
+
this.fileAutocomplete.partial = '';
|
|
759
|
+
// Activate command autocomplete when first line starts with /
|
|
760
|
+
const firstLine = this.lines[0];
|
|
761
|
+
if (this.currentLine === 0 && firstLine.startsWith('/')) {
|
|
762
|
+
this.autocomplete.active = true;
|
|
763
|
+
const freshCommands = getAutocompleteCommands();
|
|
764
|
+
this.autocomplete.matches = filterCommands(firstLine, freshCommands);
|
|
765
|
+
// Reset selection if it's out of bounds
|
|
766
|
+
if (this.autocomplete.selectedIndex >= this.autocomplete.matches.length) {
|
|
767
|
+
this.autocomplete.selectedIndex = Math.max(0, this.autocomplete.matches.length - 1);
|
|
768
|
+
this.autocomplete.scrollOffset = 0;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
this.autocomplete.active = false;
|
|
773
|
+
this.autocomplete.matches = [];
|
|
774
|
+
this.autocomplete.selectedIndex = 0;
|
|
775
|
+
this.autocomplete.scrollOffset = 0;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
acceptAutocomplete() {
|
|
779
|
+
// File autocomplete takes priority (works on current line)
|
|
780
|
+
if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
|
|
781
|
+
const selectedFile = this.fileAutocomplete.matches[this.fileAutocomplete.selectedIndex];
|
|
782
|
+
const currentLine = this.getCurrentLineContent();
|
|
783
|
+
const result = replaceAtMention(currentLine, this.cursorPos, selectedFile.path);
|
|
784
|
+
this.lines[this.currentLine] = result.input;
|
|
785
|
+
this.cursorPos = result.cursorPos;
|
|
786
|
+
this.fileAutocomplete.active = false;
|
|
787
|
+
this.fileAutocomplete.matches = [];
|
|
788
|
+
this.fileAutocomplete.selectedIndex = 0;
|
|
789
|
+
this.fileAutocomplete.scrollOffset = 0;
|
|
790
|
+
this.fileAutocomplete.partial = '';
|
|
791
|
+
// Check if still in @ context (e.g., directory selected, might want to continue)
|
|
792
|
+
this.updateAutocomplete();
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
// Command autocomplete (works on first line only)
|
|
796
|
+
if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
|
|
797
|
+
const selected = this.autocomplete.matches[this.autocomplete.selectedIndex];
|
|
798
|
+
this.lines[0] = selected.command;
|
|
799
|
+
this.currentLine = 0;
|
|
800
|
+
this.cursorPos = this.lines[0].length;
|
|
801
|
+
this.autocomplete.active = false;
|
|
802
|
+
this.autocomplete.matches = [];
|
|
803
|
+
this.autocomplete.selectedIndex = 0;
|
|
804
|
+
this.autocomplete.scrollOffset = 0;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
closeAutocomplete() {
|
|
808
|
+
// Close file autocomplete
|
|
809
|
+
this.fileAutocomplete.active = false;
|
|
810
|
+
this.fileAutocomplete.matches = [];
|
|
811
|
+
this.fileAutocomplete.selectedIndex = 0;
|
|
812
|
+
this.fileAutocomplete.scrollOffset = 0;
|
|
813
|
+
this.fileAutocomplete.partial = '';
|
|
814
|
+
// Close command autocomplete
|
|
815
|
+
this.autocomplete.active = false;
|
|
816
|
+
this.autocomplete.matches = [];
|
|
817
|
+
this.autocomplete.selectedIndex = 0;
|
|
818
|
+
this.autocomplete.scrollOffset = 0;
|
|
819
|
+
}
|
|
820
|
+
navigateAutocompleteUp() {
|
|
821
|
+
// File autocomplete navigation takes priority
|
|
822
|
+
if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
|
|
823
|
+
if (this.fileAutocomplete.selectedIndex > 0) {
|
|
824
|
+
this.fileAutocomplete.selectedIndex--;
|
|
825
|
+
if (this.fileAutocomplete.selectedIndex < this.fileAutocomplete.scrollOffset) {
|
|
826
|
+
this.fileAutocomplete.scrollOffset = this.fileAutocomplete.selectedIndex;
|
|
827
|
+
}
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
// Command autocomplete navigation
|
|
833
|
+
if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
if (this.autocomplete.selectedIndex > 0) {
|
|
837
|
+
this.autocomplete.selectedIndex--;
|
|
838
|
+
// Adjust scroll if selection goes above visible area
|
|
839
|
+
if (this.autocomplete.selectedIndex < this.autocomplete.scrollOffset) {
|
|
840
|
+
this.autocomplete.scrollOffset = this.autocomplete.selectedIndex;
|
|
841
|
+
}
|
|
842
|
+
return true;
|
|
843
|
+
}
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
navigateAutocompleteDown() {
|
|
847
|
+
// File autocomplete navigation takes priority
|
|
848
|
+
if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
|
|
849
|
+
if (this.fileAutocomplete.selectedIndex < this.fileAutocomplete.matches.length - 1) {
|
|
850
|
+
this.fileAutocomplete.selectedIndex++;
|
|
851
|
+
const maxVisibleIndex = this.fileAutocomplete.scrollOffset + MAX_VISIBLE_COMMANDS - 1;
|
|
852
|
+
if (this.fileAutocomplete.selectedIndex > maxVisibleIndex) {
|
|
853
|
+
this.fileAutocomplete.scrollOffset = this.fileAutocomplete.selectedIndex - MAX_VISIBLE_COMMANDS + 1;
|
|
854
|
+
}
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
// Command autocomplete navigation
|
|
860
|
+
if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
if (this.autocomplete.selectedIndex < this.autocomplete.matches.length - 1) {
|
|
864
|
+
this.autocomplete.selectedIndex++;
|
|
865
|
+
// Adjust scroll if selection goes below visible area
|
|
866
|
+
const maxVisibleIndex = this.autocomplete.scrollOffset + MAX_VISIBLE_COMMANDS - 1;
|
|
867
|
+
if (this.autocomplete.selectedIndex > maxVisibleIndex) {
|
|
868
|
+
this.autocomplete.scrollOffset = this.autocomplete.selectedIndex - MAX_VISIBLE_COMMANDS + 1;
|
|
869
|
+
}
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
// ===========================================================================
|
|
875
|
+
// Private - History
|
|
876
|
+
// ===========================================================================
|
|
877
|
+
addToHistory(input) {
|
|
878
|
+
const trimmed = input.trim();
|
|
879
|
+
// Don't add empty or duplicate consecutive entries
|
|
880
|
+
if (trimmed && (this.history.length === 0 || this.history[this.history.length - 1] !== trimmed)) {
|
|
881
|
+
this.history.push(trimmed);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
navigateHistoryUp() {
|
|
885
|
+
if (this.autocomplete.active)
|
|
886
|
+
return false;
|
|
887
|
+
if (this.history.length === 0)
|
|
888
|
+
return false;
|
|
889
|
+
// Save current input when starting to navigate
|
|
890
|
+
if (this.historyIndex === -1) {
|
|
891
|
+
this.savedInput = this.getInputValue();
|
|
892
|
+
}
|
|
893
|
+
// Navigate to older entry
|
|
894
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
895
|
+
this.historyIndex++;
|
|
896
|
+
const historyEntry = this.history[this.history.length - 1 - this.historyIndex];
|
|
897
|
+
// History entries are single-line (newlines stripped on save)
|
|
898
|
+
this.lines = [historyEntry];
|
|
899
|
+
this.currentLine = 0;
|
|
900
|
+
this.cursorPos = historyEntry.length;
|
|
901
|
+
return true;
|
|
902
|
+
}
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
navigateHistoryDown() {
|
|
906
|
+
if (this.autocomplete.active)
|
|
907
|
+
return false;
|
|
908
|
+
if (this.historyIndex < 0)
|
|
909
|
+
return false;
|
|
910
|
+
this.historyIndex--;
|
|
911
|
+
if (this.historyIndex === -1) {
|
|
912
|
+
// Restore saved input (may be multiline)
|
|
913
|
+
this.lines = this.savedInput.split('\n');
|
|
914
|
+
if (this.lines.length === 0)
|
|
915
|
+
this.lines = [''];
|
|
916
|
+
this.currentLine = this.lines.length - 1;
|
|
917
|
+
this.cursorPos = this.lines[this.currentLine].length;
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
// Navigate to newer entry
|
|
921
|
+
const historyEntry = this.history[this.history.length - 1 - this.historyIndex];
|
|
922
|
+
this.lines = [historyEntry];
|
|
923
|
+
this.currentLine = 0;
|
|
924
|
+
this.cursorPos = historyEntry.length;
|
|
925
|
+
}
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
resetHistoryNavigation() {
|
|
929
|
+
this.historyIndex = -1;
|
|
930
|
+
this.savedInput = '';
|
|
931
|
+
}
|
|
932
|
+
// ===========================================================================
|
|
933
|
+
// Private - Spinner animation
|
|
934
|
+
// ===========================================================================
|
|
935
|
+
startSpinnerAnimation() {
|
|
936
|
+
if (this.spinnerTimer)
|
|
937
|
+
return;
|
|
938
|
+
this.spinnerTimer = setInterval(() => {
|
|
939
|
+
this.spinnerFrame++;
|
|
940
|
+
this.needsRender = true;
|
|
941
|
+
}, 200); // Slower, more relaxed animation
|
|
942
|
+
}
|
|
943
|
+
stopSpinnerAnimation() {
|
|
944
|
+
if (this.spinnerTimer) {
|
|
945
|
+
clearInterval(this.spinnerTimer);
|
|
946
|
+
this.spinnerTimer = null;
|
|
947
|
+
}
|
|
948
|
+
this.spinnerFrame = 0;
|
|
949
|
+
}
|
|
950
|
+
// ===========================================================================
|
|
951
|
+
// Private - Keyboard handling
|
|
952
|
+
// ===========================================================================
|
|
953
|
+
keyHandler = null;
|
|
954
|
+
dataHandler = null;
|
|
955
|
+
startKeyboardCapture() {
|
|
956
|
+
if (this.keyHandler)
|
|
957
|
+
return; // Already capturing
|
|
958
|
+
readline.emitKeypressEvents(process.stdin);
|
|
959
|
+
if (process.stdin.isTTY) {
|
|
960
|
+
process.stdin.setRawMode(true);
|
|
961
|
+
}
|
|
962
|
+
// Handle raw data for reliable Escape and Option+Arrow detection
|
|
963
|
+
this.dataHandler = (data) => {
|
|
964
|
+
if (!this.isRunning || this.isPaused)
|
|
965
|
+
return;
|
|
966
|
+
// Pure Escape key is a single byte 0x1B
|
|
967
|
+
if (data.length === 1 && data[0] === 0x1b) {
|
|
968
|
+
this.handleEscape();
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
// Mac Option+Left (word left): ESC b or ESC [ 1 ; 9 D
|
|
972
|
+
const isOptionLeft = (data.length === 2 && data[0] === 0x1b && data[1] === 0x62) ||
|
|
973
|
+
(data.length === 6 &&
|
|
974
|
+
data[0] === 0x1b &&
|
|
975
|
+
data[1] === 0x5b &&
|
|
976
|
+
data[2] === 0x31 &&
|
|
977
|
+
data[3] === 0x3b &&
|
|
978
|
+
data[4] === 0x39 &&
|
|
979
|
+
data[5] === 0x44);
|
|
980
|
+
// Mac Option+Right (word right): ESC f or ESC [ 1 ; 9 C
|
|
981
|
+
const isOptionRight = (data.length === 2 && data[0] === 0x1b && data[1] === 0x66) ||
|
|
982
|
+
(data.length === 6 &&
|
|
983
|
+
data[0] === 0x1b &&
|
|
984
|
+
data[1] === 0x5b &&
|
|
985
|
+
data[2] === 0x31 &&
|
|
986
|
+
data[3] === 0x3b &&
|
|
987
|
+
data[4] === 0x39 &&
|
|
988
|
+
data[5] === 0x43);
|
|
989
|
+
if (isOptionLeft) {
|
|
990
|
+
this.handleWordLeft();
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (isOptionRight) {
|
|
994
|
+
this.handleWordRight();
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
// Home/Cmd+Left: Ctrl+A (\x01) or ESC [ H
|
|
998
|
+
const isHome = (data.length === 1 && data[0] === 0x01) ||
|
|
999
|
+
(data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x48);
|
|
1000
|
+
// End/Cmd+Right: Ctrl+E (\x05) or ESC [ F
|
|
1001
|
+
const isEnd = (data.length === 1 && data[0] === 0x05) ||
|
|
1002
|
+
(data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x46);
|
|
1003
|
+
if (isHome) {
|
|
1004
|
+
this.cursorPos = 0;
|
|
1005
|
+
this.needsRender = true;
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (isEnd) {
|
|
1009
|
+
this.cursorPos = this.lines[this.currentLine].length;
|
|
1010
|
+
this.needsRender = true;
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
this.keyHandler = (str, key) => {
|
|
1015
|
+
if (!this.isRunning || this.isPaused)
|
|
1016
|
+
return;
|
|
1017
|
+
// Skip escape here - handled by dataHandler
|
|
1018
|
+
if (key.name === 'escape')
|
|
1019
|
+
return;
|
|
1020
|
+
this.handleKeypress(str, key);
|
|
1021
|
+
};
|
|
1022
|
+
process.stdin.on('data', this.dataHandler);
|
|
1023
|
+
process.stdin.on('keypress', this.keyHandler);
|
|
1024
|
+
process.stdin.resume();
|
|
1025
|
+
}
|
|
1026
|
+
stopKeyboardCapture() {
|
|
1027
|
+
if (this.dataHandler) {
|
|
1028
|
+
process.stdin.removeListener('data', this.dataHandler);
|
|
1029
|
+
this.dataHandler = null;
|
|
1030
|
+
}
|
|
1031
|
+
if (this.keyHandler) {
|
|
1032
|
+
process.stdin.removeListener('keypress', this.keyHandler);
|
|
1033
|
+
this.keyHandler = null;
|
|
1034
|
+
}
|
|
1035
|
+
if (process.stdin.isTTY) {
|
|
1036
|
+
process.stdin.setRawMode(false);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Handle word left (Option+Left) - move cursor to previous word boundary
|
|
1041
|
+
*/
|
|
1042
|
+
handleWordLeft() {
|
|
1043
|
+
const line = this.lines[this.currentLine];
|
|
1044
|
+
if (this.cursorPos > 0) {
|
|
1045
|
+
let pos = this.cursorPos;
|
|
1046
|
+
// Skip spaces
|
|
1047
|
+
while (pos > 0 && line[pos - 1] === ' ')
|
|
1048
|
+
pos--;
|
|
1049
|
+
// Skip word
|
|
1050
|
+
while (pos > 0 && line[pos - 1] !== ' ')
|
|
1051
|
+
pos--;
|
|
1052
|
+
this.cursorPos = pos;
|
|
1053
|
+
this.needsRender = true;
|
|
1054
|
+
}
|
|
1055
|
+
else if (this.currentLine > 0) {
|
|
1056
|
+
// Move to end of previous line
|
|
1057
|
+
this.currentLine--;
|
|
1058
|
+
this.cursorPos = this.lines[this.currentLine].length;
|
|
1059
|
+
this.needsRender = true;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Handle word right (Option+Right) - move cursor to next word boundary
|
|
1064
|
+
*/
|
|
1065
|
+
handleWordRight() {
|
|
1066
|
+
const line = this.lines[this.currentLine];
|
|
1067
|
+
if (this.cursorPos < line.length) {
|
|
1068
|
+
let pos = this.cursorPos;
|
|
1069
|
+
// Skip word
|
|
1070
|
+
while (pos < line.length && line[pos] !== ' ')
|
|
1071
|
+
pos++;
|
|
1072
|
+
// Skip spaces
|
|
1073
|
+
while (pos < line.length && line[pos] === ' ')
|
|
1074
|
+
pos++;
|
|
1075
|
+
this.cursorPos = pos;
|
|
1076
|
+
this.needsRender = true;
|
|
1077
|
+
}
|
|
1078
|
+
else if (this.currentLine < this.lines.length - 1) {
|
|
1079
|
+
// Move to start of next line
|
|
1080
|
+
this.currentLine++;
|
|
1081
|
+
this.cursorPos = 0;
|
|
1082
|
+
this.needsRender = true;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Handle Escape key - called from raw data handler for reliable detection
|
|
1087
|
+
*/
|
|
1088
|
+
handleEscape() {
|
|
1089
|
+
const now = Date.now();
|
|
1090
|
+
const timeSinceLastEsc = now - this.lastEscapeTime;
|
|
1091
|
+
const isDoubleEsc = timeSinceLastEsc < 500;
|
|
1092
|
+
this.lastEscapeTime = now;
|
|
1093
|
+
if (this.fileAutocomplete.active) {
|
|
1094
|
+
// 1. Close file autocomplete first
|
|
1095
|
+
this.closeAutocomplete();
|
|
1096
|
+
this.needsRender = true;
|
|
1097
|
+
}
|
|
1098
|
+
else if (this.autocomplete.active) {
|
|
1099
|
+
// 2. Close command autocomplete
|
|
1100
|
+
this.closeAutocomplete();
|
|
1101
|
+
this.needsRender = true;
|
|
1102
|
+
}
|
|
1103
|
+
else if (isDoubleEsc && this.getInputValue().length > 0) {
|
|
1104
|
+
// 3. Double Esc clears input (if there's content)
|
|
1105
|
+
this.clearInput();
|
|
1106
|
+
this.resetHistoryNavigation();
|
|
1107
|
+
this.needsRender = true;
|
|
1108
|
+
}
|
|
1109
|
+
else if (this.agentRunning) {
|
|
1110
|
+
// 4. Single Esc cancels agent
|
|
1111
|
+
this.emit('cancel');
|
|
1112
|
+
}
|
|
1113
|
+
else {
|
|
1114
|
+
// 5. Single Esc emits escape
|
|
1115
|
+
this.emit('escape');
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
handleKeypress(str, key) {
|
|
1119
|
+
// Ctrl+C - interrupt/exit
|
|
1120
|
+
if (key.ctrl && key.name === 'c') {
|
|
1121
|
+
this.emit('interrupt');
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
// Tab - accept suggestion, or autocomplete
|
|
1125
|
+
if (key.name === 'tab' && !key.shift) {
|
|
1126
|
+
// Accept ghost text suggestion if input is empty
|
|
1127
|
+
if (this.suggestion && this.getInputValue() === '') {
|
|
1128
|
+
this.lines[0] = this.suggestion;
|
|
1129
|
+
this.cursorPos = this.suggestion.length;
|
|
1130
|
+
this.suggestion = null;
|
|
1131
|
+
this.needsRender = true;
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
|
|
1135
|
+
this.acceptAutocomplete();
|
|
1136
|
+
this.needsRender = true;
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
|
|
1140
|
+
this.acceptAutocomplete();
|
|
1141
|
+
this.needsRender = true;
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
// Otherwise ignore tab (or could insert spaces)
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
// Enter - handle multiline continuation, autocomplete, then execute
|
|
1148
|
+
if (key.name === 'return') {
|
|
1149
|
+
// Check for backslash continuation at end of current line
|
|
1150
|
+
const currentLine = this.getCurrentLineContent();
|
|
1151
|
+
if (currentLine.endsWith('\\')) {
|
|
1152
|
+
// Remove backslash and add new line
|
|
1153
|
+
this.lines[this.currentLine] = currentLine.slice(0, -1);
|
|
1154
|
+
this.lines.push('');
|
|
1155
|
+
this.currentLine++;
|
|
1156
|
+
this.cursorPos = 0;
|
|
1157
|
+
this.closeAutocomplete();
|
|
1158
|
+
this.resetHistoryNavigation();
|
|
1159
|
+
this.needsRender = true;
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
// If file autocomplete is active, accept the selection and submit
|
|
1163
|
+
if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
|
|
1164
|
+
this.acceptAutocomplete();
|
|
1165
|
+
// Fall through to submit the message
|
|
1166
|
+
}
|
|
1167
|
+
// If command autocomplete is active, accept the selection first
|
|
1168
|
+
if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
|
|
1169
|
+
this.acceptAutocomplete();
|
|
1170
|
+
// Fall through to execute the command
|
|
1171
|
+
}
|
|
1172
|
+
let input = this.getInputValue().trim();
|
|
1173
|
+
// If input is empty but we have a ghost text suggestion, accept it
|
|
1174
|
+
if (!input && this.suggestion) {
|
|
1175
|
+
input = this.suggestion;
|
|
1176
|
+
this.suggestion = null;
|
|
1177
|
+
}
|
|
1178
|
+
if (input) {
|
|
1179
|
+
// Add to history before processing (store as single line for history)
|
|
1180
|
+
this.addToHistory(input.replace(/\n/g, ' '));
|
|
1181
|
+
this.resetHistoryNavigation();
|
|
1182
|
+
if (this.agentRunning) {
|
|
1183
|
+
this.queuedInputs.push(input);
|
|
1184
|
+
this.needsRender = true;
|
|
1185
|
+
}
|
|
1186
|
+
else if (input.startsWith('/')) {
|
|
1187
|
+
const spaceIndex = input.indexOf(' ');
|
|
1188
|
+
const cmd = spaceIndex > 0 ? input.slice(1, spaceIndex) : input.slice(1);
|
|
1189
|
+
const args = spaceIndex > 0 ? input.slice(spaceIndex + 1) : '';
|
|
1190
|
+
this.closeAutocomplete();
|
|
1191
|
+
this.emit('command', cmd, args);
|
|
1192
|
+
}
|
|
1193
|
+
else {
|
|
1194
|
+
this.emit('submit', input);
|
|
1195
|
+
}
|
|
1196
|
+
this.clearInput();
|
|
1197
|
+
this.closeAutocomplete();
|
|
1198
|
+
this.needsRender = true;
|
|
1199
|
+
}
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
// Arrow Up - autocomplete > multiline navigation > history
|
|
1203
|
+
if (key.name === 'up') {
|
|
1204
|
+
// 1. Autocomplete navigation takes priority
|
|
1205
|
+
if (this.navigateAutocompleteUp()) {
|
|
1206
|
+
this.needsRender = true;
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
// 2. Navigate to previous line in multiline input
|
|
1210
|
+
if (this.currentLine > 0) {
|
|
1211
|
+
this.currentLine--;
|
|
1212
|
+
// Try to keep same cursor position, but clamp to line length
|
|
1213
|
+
this.cursorPos = Math.min(this.cursorPos, this.lines[this.currentLine].length);
|
|
1214
|
+
this.needsRender = true;
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
// 3. History navigation (only when at first line)
|
|
1218
|
+
if (this.navigateHistoryUp()) {
|
|
1219
|
+
this.closeAutocomplete();
|
|
1220
|
+
this.needsRender = true;
|
|
1221
|
+
}
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
// Arrow Down - autocomplete > multiline navigation > history
|
|
1225
|
+
if (key.name === 'down') {
|
|
1226
|
+
// 1. Autocomplete navigation takes priority
|
|
1227
|
+
if (this.navigateAutocompleteDown()) {
|
|
1228
|
+
this.needsRender = true;
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
// 2. Navigate to next line in multiline input
|
|
1232
|
+
if (this.currentLine < this.lines.length - 1) {
|
|
1233
|
+
this.currentLine++;
|
|
1234
|
+
// Try to keep same cursor position, but clamp to line length
|
|
1235
|
+
this.cursorPos = Math.min(this.cursorPos, this.lines[this.currentLine].length);
|
|
1236
|
+
this.needsRender = true;
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
// 3. History forward (only when at last line)
|
|
1240
|
+
if (this.historyIndex >= 0 && this.navigateHistoryDown()) {
|
|
1241
|
+
this.needsRender = true;
|
|
1242
|
+
}
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
// Backspace
|
|
1246
|
+
if (key.name === 'backspace') {
|
|
1247
|
+
if (this.cursorPos > 0) {
|
|
1248
|
+
// Delete character in current line
|
|
1249
|
+
const line = this.lines[this.currentLine];
|
|
1250
|
+
this.lines[this.currentLine] = line.slice(0, this.cursorPos - 1) + line.slice(this.cursorPos);
|
|
1251
|
+
this.cursorPos--;
|
|
1252
|
+
this.resetHistoryNavigation();
|
|
1253
|
+
this.updateAutocomplete();
|
|
1254
|
+
this.needsRender = true;
|
|
1255
|
+
}
|
|
1256
|
+
else if (this.currentLine > 0) {
|
|
1257
|
+
// At start of line - merge with previous line
|
|
1258
|
+
const currentLine = this.lines[this.currentLine];
|
|
1259
|
+
const prevLine = this.lines[this.currentLine - 1];
|
|
1260
|
+
this.lines[this.currentLine - 1] = prevLine + currentLine;
|
|
1261
|
+
this.lines.splice(this.currentLine, 1);
|
|
1262
|
+
this.currentLine--;
|
|
1263
|
+
this.cursorPos = prevLine.length;
|
|
1264
|
+
this.resetHistoryNavigation();
|
|
1265
|
+
this.updateAutocomplete();
|
|
1266
|
+
this.needsRender = true;
|
|
1267
|
+
}
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
// Delete
|
|
1271
|
+
if (key.name === 'delete') {
|
|
1272
|
+
const line = this.lines[this.currentLine];
|
|
1273
|
+
if (this.cursorPos < line.length) {
|
|
1274
|
+
// Delete character in current line
|
|
1275
|
+
this.lines[this.currentLine] = line.slice(0, this.cursorPos) + line.slice(this.cursorPos + 1);
|
|
1276
|
+
this.resetHistoryNavigation();
|
|
1277
|
+
this.updateAutocomplete();
|
|
1278
|
+
this.needsRender = true;
|
|
1279
|
+
}
|
|
1280
|
+
else if (this.currentLine < this.lines.length - 1) {
|
|
1281
|
+
// At end of line - merge with next line
|
|
1282
|
+
const nextLine = this.lines[this.currentLine + 1];
|
|
1283
|
+
this.lines[this.currentLine] = line + nextLine;
|
|
1284
|
+
this.lines.splice(this.currentLine + 1, 1);
|
|
1285
|
+
this.resetHistoryNavigation();
|
|
1286
|
+
this.updateAutocomplete();
|
|
1287
|
+
this.needsRender = true;
|
|
1288
|
+
}
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
// Arrow keys (left/right for cursor movement with multiline support)
|
|
1292
|
+
if (key.name === 'left') {
|
|
1293
|
+
if (this.cursorPos > 0) {
|
|
1294
|
+
this.cursorPos--;
|
|
1295
|
+
this.needsRender = true;
|
|
1296
|
+
}
|
|
1297
|
+
else if (this.currentLine > 0) {
|
|
1298
|
+
// Move to end of previous line
|
|
1299
|
+
this.currentLine--;
|
|
1300
|
+
this.cursorPos = this.lines[this.currentLine].length;
|
|
1301
|
+
this.needsRender = true;
|
|
1302
|
+
}
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (key.name === 'right') {
|
|
1306
|
+
const lineLen = this.lines[this.currentLine].length;
|
|
1307
|
+
if (this.cursorPos < lineLen) {
|
|
1308
|
+
this.cursorPos++;
|
|
1309
|
+
this.needsRender = true;
|
|
1310
|
+
}
|
|
1311
|
+
else if (this.currentLine < this.lines.length - 1) {
|
|
1312
|
+
// Move to start of next line
|
|
1313
|
+
this.currentLine++;
|
|
1314
|
+
this.cursorPos = 0;
|
|
1315
|
+
this.needsRender = true;
|
|
1316
|
+
}
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
if (key.name === 'home') {
|
|
1320
|
+
this.cursorPos = 0;
|
|
1321
|
+
this.needsRender = true;
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
if (key.name === 'end') {
|
|
1325
|
+
this.cursorPos = this.lines[this.currentLine].length;
|
|
1326
|
+
this.needsRender = true;
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
// Shift+Tab - mode change
|
|
1330
|
+
if (key.shift && key.name === 'tab') {
|
|
1331
|
+
this.emit('modeChange');
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
// Regular character
|
|
1335
|
+
if (str && str.length === 1 && str.charCodeAt(0) >= 32) {
|
|
1336
|
+
// Clear ghost text suggestion when user starts typing
|
|
1337
|
+
if (this.suggestion) {
|
|
1338
|
+
this.suggestion = null;
|
|
1339
|
+
}
|
|
1340
|
+
const line = this.lines[this.currentLine];
|
|
1341
|
+
this.lines[this.currentLine] =
|
|
1342
|
+
line.slice(0, this.cursorPos) + str + line.slice(this.cursorPos);
|
|
1343
|
+
this.cursorPos++;
|
|
1344
|
+
this.resetHistoryNavigation();
|
|
1345
|
+
this.updateAutocomplete();
|
|
1346
|
+
this.needsRender = true;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|