@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
package/dist/ui/input-prompt.js
CHANGED
|
@@ -1,83 +1,141 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Input Prompt
|
|
2
|
+
* Input Prompt v2 (Event-Driven)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Refactored input handling with event-driven architecture.
|
|
5
|
+
* - Emits events instead of blocking with getInput()
|
|
6
|
+
* - Supports queue mode for capturing input during agent execution
|
|
7
|
+
* - Always captures keystrokes (no blocking)
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* Events:
|
|
10
|
+
* - 'submit' - User pressed Enter with input
|
|
11
|
+
* - 'command' - User submitted a slash command
|
|
12
|
+
* - 'cancel' - User pressed Ctrl+C
|
|
13
|
+
* - 'escape' - User pressed Esc (for aborting agent)
|
|
14
|
+
* - 'change' - Input text changed
|
|
10
15
|
*/
|
|
11
|
-
import
|
|
16
|
+
import { EventEmitter } from 'events';
|
|
17
|
+
import chalk from 'chalk';
|
|
18
|
+
import { getStyles } from '../themes/index.js';
|
|
12
19
|
import * as terminal from './terminal.js';
|
|
20
|
+
import { getAutocompleteCommands } from '../commands.js';
|
|
21
|
+
import { getFileMatches, extractAtMention, replaceAtMention, } from './file-autocomplete.js';
|
|
22
|
+
import { debugRender } from '../utils/debug-log.js';
|
|
23
|
+
import { stripAnsi as unifiedStripAnsi, getPhysicalLineCountWithOffset, } from './line-utils.js';
|
|
13
24
|
// =============================================================================
|
|
14
25
|
// Constants
|
|
15
26
|
// =============================================================================
|
|
16
27
|
const MAX_VISIBLE_COMMANDS = 10;
|
|
17
28
|
// =============================================================================
|
|
18
|
-
// Default Commands
|
|
29
|
+
// Default Commands (from central registry)
|
|
19
30
|
// =============================================================================
|
|
20
|
-
export const DEFAULT_COMMANDS =
|
|
21
|
-
{ command: '/help', description: 'Show available commands' },
|
|
22
|
-
{ command: '/exit', description: 'Quit the demo' },
|
|
23
|
-
{ command: '/clear', description: 'Clear conversation history' },
|
|
24
|
-
{ command: '/compact', description: 'Summarize old messages' },
|
|
25
|
-
{ command: '/tools', description: 'List available tools' },
|
|
26
|
-
{ command: '/tokens', description: 'Show session token usage' },
|
|
27
|
-
{ command: '/context', description: 'Show context window usage' },
|
|
28
|
-
];
|
|
31
|
+
export const DEFAULT_COMMANDS = getAutocompleteCommands();
|
|
29
32
|
// =============================================================================
|
|
30
33
|
// Helper Functions
|
|
31
34
|
// =============================================================================
|
|
32
35
|
/**
|
|
33
36
|
* Strip ANSI codes from string
|
|
37
|
+
* Uses unified line-utils for consistent behavior across components.
|
|
34
38
|
*/
|
|
35
39
|
export function stripAnsi(str) {
|
|
36
|
-
|
|
37
|
-
return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
|
|
40
|
+
return unifiedStripAnsi(str);
|
|
38
41
|
}
|
|
39
42
|
/**
|
|
40
43
|
* Calculate physical lines for wrapped text
|
|
44
|
+
* Uses unified line-utils for consistent behavior across components.
|
|
41
45
|
*/
|
|
42
46
|
function calcPhysicalLines(text, startCol, termWidth) {
|
|
43
|
-
|
|
44
|
-
return 1;
|
|
45
|
-
const totalLen = startCol + text.length;
|
|
46
|
-
return Math.ceil(totalLen / termWidth) || 1;
|
|
47
|
+
return getPhysicalLineCountWithOffset(text, startCol, termWidth);
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
49
|
-
*
|
|
50
|
+
* Calculate fuzzy match score for a query against a target string.
|
|
51
|
+
* Higher score = better match.
|
|
52
|
+
* Returns -1 if no match.
|
|
53
|
+
*/
|
|
54
|
+
function fuzzyMatchScore(query, target) {
|
|
55
|
+
const queryLower = query.toLowerCase();
|
|
56
|
+
const targetLower = target.toLowerCase();
|
|
57
|
+
// Exact prefix match - highest priority (score 1000+)
|
|
58
|
+
if (targetLower.startsWith(queryLower)) {
|
|
59
|
+
return 1000 + (100 - target.length); // Shorter commands rank higher
|
|
60
|
+
}
|
|
61
|
+
// Contiguous substring match - high priority (score 500+)
|
|
62
|
+
if (targetLower.includes(queryLower)) {
|
|
63
|
+
const index = targetLower.indexOf(queryLower);
|
|
64
|
+
return 500 + (100 - index); // Earlier matches rank higher
|
|
65
|
+
}
|
|
66
|
+
// Fuzzy match - characters appear in order (score 100+)
|
|
67
|
+
let queryIdx = 0;
|
|
68
|
+
let consecutiveBonus = 0;
|
|
69
|
+
let lastMatchIdx = -1;
|
|
70
|
+
for (let i = 0; i < targetLower.length && queryIdx < queryLower.length; i++) {
|
|
71
|
+
if (targetLower[i] === queryLower[queryIdx]) {
|
|
72
|
+
// Bonus for consecutive matches
|
|
73
|
+
if (lastMatchIdx === i - 1) {
|
|
74
|
+
consecutiveBonus += 10;
|
|
75
|
+
}
|
|
76
|
+
lastMatchIdx = i;
|
|
77
|
+
queryIdx++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// All query characters found in order
|
|
81
|
+
if (queryIdx === queryLower.length) {
|
|
82
|
+
return 100 + consecutiveBonus + (100 - target.length);
|
|
83
|
+
}
|
|
84
|
+
// No match
|
|
85
|
+
return -1;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Filter and rank commands matching input using fuzzy matching
|
|
50
89
|
*/
|
|
51
90
|
function filterCommands(input, commands) {
|
|
52
|
-
|
|
53
|
-
|
|
91
|
+
// Score all commands
|
|
92
|
+
const scored = commands
|
|
93
|
+
.map((cmd) => ({
|
|
94
|
+
cmd,
|
|
95
|
+
score: fuzzyMatchScore(input, cmd.command),
|
|
96
|
+
}))
|
|
97
|
+
.filter((item) => item.score >= 0);
|
|
98
|
+
// Sort by score (highest first)
|
|
99
|
+
scored.sort((a, b) => b.score - a.score);
|
|
100
|
+
return scored.map((item) => item.cmd);
|
|
54
101
|
}
|
|
55
102
|
// =============================================================================
|
|
56
103
|
// Input Prompt Class
|
|
57
104
|
// =============================================================================
|
|
58
|
-
export class InputPrompt {
|
|
105
|
+
export class InputPrompt extends EventEmitter {
|
|
59
106
|
// Configuration
|
|
60
107
|
prompt;
|
|
61
108
|
promptLen;
|
|
62
109
|
showSeparators;
|
|
63
110
|
commands;
|
|
64
|
-
getTodos;
|
|
65
111
|
// Input state
|
|
66
112
|
state = {
|
|
67
113
|
lines: [''],
|
|
68
114
|
currentLine: 0,
|
|
69
115
|
cursorPos: 0,
|
|
70
116
|
};
|
|
71
|
-
//
|
|
117
|
+
// Command autocomplete (for /commands)
|
|
72
118
|
autocomplete = {
|
|
73
119
|
active: false,
|
|
74
120
|
matches: [],
|
|
75
121
|
selectedIndex: 0,
|
|
122
|
+
scrollOffset: 0,
|
|
123
|
+
};
|
|
124
|
+
// File autocomplete (for @paths)
|
|
125
|
+
fileAutocomplete = {
|
|
126
|
+
active: false,
|
|
127
|
+
matches: [],
|
|
128
|
+
selectedIndex: 0,
|
|
129
|
+
scrollOffset: 0,
|
|
130
|
+
partial: '',
|
|
76
131
|
};
|
|
77
132
|
// History
|
|
78
133
|
history = [];
|
|
79
134
|
historyIndex = -1;
|
|
80
135
|
savedInput = '';
|
|
136
|
+
// Queue mode
|
|
137
|
+
queueMode = false;
|
|
138
|
+
queuedInputs = [];
|
|
81
139
|
// Rendering tracking
|
|
82
140
|
renderedLines = 0;
|
|
83
141
|
dropdownLines = 0;
|
|
@@ -85,29 +143,48 @@ export class InputPrompt {
|
|
|
85
143
|
hasSeparators = false;
|
|
86
144
|
// Control
|
|
87
145
|
isRunning = false;
|
|
88
|
-
|
|
146
|
+
// Double Esc detection
|
|
147
|
+
lastEscTime = 0;
|
|
148
|
+
DOUBLE_ESC_THRESHOLD_MS = 500; // 500ms window for double Esc
|
|
149
|
+
// Suggestion (ghost text for next action)
|
|
150
|
+
suggestion = null;
|
|
89
151
|
constructor(options = {}) {
|
|
90
|
-
|
|
152
|
+
super();
|
|
153
|
+
this.prompt = options.prompt ?? getStyles().primary('❯ ');
|
|
91
154
|
this.promptLen = stripAnsi(this.prompt).length;
|
|
92
155
|
this.showSeparators = options.showSeparators ?? true;
|
|
93
156
|
this.commands = options.commands ?? DEFAULT_COMMANDS;
|
|
94
|
-
this.getTodos = options.getTodos;
|
|
95
157
|
}
|
|
96
158
|
// ===========================================================================
|
|
97
159
|
// Public API
|
|
98
160
|
// ===========================================================================
|
|
99
161
|
/**
|
|
100
|
-
*
|
|
101
|
-
* Returns when user submits or cancels
|
|
162
|
+
* Update the prompt string (for dynamic theme changes)
|
|
102
163
|
*/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
164
|
+
setPrompt(prompt) {
|
|
165
|
+
this.prompt = prompt;
|
|
166
|
+
this.promptLen = stripAnsi(prompt).length;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Set a suggestion for the next action (ghost text)
|
|
170
|
+
*/
|
|
171
|
+
setSuggestion(action) {
|
|
172
|
+
this.suggestion = action;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get the current suggestion
|
|
176
|
+
*/
|
|
177
|
+
getSuggestion() {
|
|
178
|
+
return this.suggestion;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Clear the current suggestion
|
|
182
|
+
*/
|
|
183
|
+
clearSuggestion() {
|
|
184
|
+
this.suggestion = null;
|
|
108
185
|
}
|
|
109
186
|
/**
|
|
110
|
-
* Start input
|
|
187
|
+
* Start input capture (non-blocking, event-driven)
|
|
111
188
|
*/
|
|
112
189
|
start() {
|
|
113
190
|
if (this.isRunning)
|
|
@@ -115,17 +192,67 @@ export class InputPrompt {
|
|
|
115
192
|
this.isRunning = true;
|
|
116
193
|
terminal.enableRawMode();
|
|
117
194
|
this.resetState();
|
|
118
|
-
this.render();
|
|
119
195
|
process.stdin.on('data', this.handleData);
|
|
120
196
|
}
|
|
121
197
|
/**
|
|
122
|
-
* Stop input
|
|
198
|
+
* Stop input capture
|
|
123
199
|
*/
|
|
124
200
|
stop() {
|
|
201
|
+
if (!this.isRunning)
|
|
202
|
+
return;
|
|
125
203
|
this.isRunning = false;
|
|
126
204
|
terminal.disableRawMode();
|
|
127
205
|
process.stdin.removeListener('data', this.handleData);
|
|
128
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Check if running
|
|
209
|
+
*/
|
|
210
|
+
isActive() {
|
|
211
|
+
return this.isRunning;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Enable/disable queue mode
|
|
215
|
+
* In queue mode, Enter adds to queue instead of emitting 'submit'
|
|
216
|
+
*/
|
|
217
|
+
setQueueMode(enabled) {
|
|
218
|
+
this.queueMode = enabled;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Check if in queue mode
|
|
222
|
+
*/
|
|
223
|
+
isQueueMode() {
|
|
224
|
+
return this.queueMode;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Get all queued inputs (FIFO order)
|
|
228
|
+
*/
|
|
229
|
+
getQueuedInputs() {
|
|
230
|
+
return [...this.queuedInputs];
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Pop the first queued input
|
|
234
|
+
*/
|
|
235
|
+
popQueuedInput() {
|
|
236
|
+
return this.queuedInputs.shift() ?? null;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Check if there are queued inputs
|
|
240
|
+
*/
|
|
241
|
+
hasQueuedInput() {
|
|
242
|
+
return this.queuedInputs.length > 0;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Get number of queued inputs
|
|
246
|
+
*/
|
|
247
|
+
getQueueLength() {
|
|
248
|
+
return this.queuedInputs.length;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Clear the queue
|
|
252
|
+
*/
|
|
253
|
+
clearQueue() {
|
|
254
|
+
this.queuedInputs = [];
|
|
255
|
+
}
|
|
129
256
|
/**
|
|
130
257
|
* Get current buffer value
|
|
131
258
|
*/
|
|
@@ -139,37 +266,196 @@ export class InputPrompt {
|
|
|
139
266
|
this.state.lines = value.split('\n');
|
|
140
267
|
this.state.currentLine = this.state.lines.length - 1;
|
|
141
268
|
this.state.cursorPos = this.state.lines[this.state.currentLine].length;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
269
|
+
this.updateAutocomplete();
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Clear input buffer
|
|
273
|
+
*/
|
|
274
|
+
clearInput() {
|
|
275
|
+
this.state = {
|
|
276
|
+
lines: [''],
|
|
277
|
+
currentLine: 0,
|
|
278
|
+
cursorPos: 0,
|
|
279
|
+
};
|
|
280
|
+
this.autocomplete = {
|
|
281
|
+
active: false,
|
|
282
|
+
matches: [],
|
|
283
|
+
selectedIndex: 0,
|
|
284
|
+
scrollOffset: 0,
|
|
285
|
+
};
|
|
286
|
+
this.fileAutocomplete = {
|
|
287
|
+
active: false,
|
|
288
|
+
matches: [],
|
|
289
|
+
selectedIndex: 0,
|
|
290
|
+
scrollOffset: 0,
|
|
291
|
+
partial: '',
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Add command to history
|
|
296
|
+
*/
|
|
297
|
+
addToHistory(input) {
|
|
298
|
+
if (input.trim() && (this.history.length === 0 || this.history[this.history.length - 1] !== input)) {
|
|
299
|
+
this.history.push(input);
|
|
145
300
|
}
|
|
146
301
|
}
|
|
147
302
|
/**
|
|
148
|
-
*
|
|
303
|
+
* Render the input prompt - returns array of lines
|
|
304
|
+
* Does NOT write to terminal (Footer handles that)
|
|
149
305
|
*/
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
306
|
+
render() {
|
|
307
|
+
const lines = [];
|
|
308
|
+
// Top separator
|
|
309
|
+
if (this.showSeparators) {
|
|
310
|
+
lines.push(this.getSeparatorLine());
|
|
155
311
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
312
|
+
// Input lines
|
|
313
|
+
const s = getStyles();
|
|
314
|
+
for (let i = 0; i < this.state.lines.length; i++) {
|
|
315
|
+
const linePrompt = i === 0 ? this.prompt : s.muted(' \\ ');
|
|
316
|
+
const lineContent = this.state.lines[i];
|
|
317
|
+
// Show suggestion as ghost text on first line when input is empty
|
|
318
|
+
if (i === 0 && lineContent === '' && this.suggestion && !this.autocomplete.active && !this.fileAutocomplete.active) {
|
|
319
|
+
const hint = s.muted(' (tab to accept)');
|
|
320
|
+
lines.push(linePrompt + s.muted(this.suggestion) + hint);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
lines.push(linePrompt + lineContent);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Bottom separator
|
|
327
|
+
if (this.showSeparators) {
|
|
328
|
+
lines.push(this.getSeparatorLine());
|
|
329
|
+
}
|
|
330
|
+
return lines;
|
|
159
331
|
}
|
|
160
332
|
/**
|
|
161
|
-
* Get
|
|
333
|
+
* Get the cursor position info for rendering
|
|
162
334
|
*/
|
|
163
|
-
|
|
164
|
-
|
|
335
|
+
getCursorInfo() {
|
|
336
|
+
const termWidth = terminal.getTerminalWidth();
|
|
337
|
+
const currentLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
|
|
338
|
+
const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
|
|
339
|
+
// Calculate which row within input (accounting for separators)
|
|
340
|
+
let row = this.showSeparators ? 1 : 0; // Start after top separator
|
|
341
|
+
for (let i = 0; i < this.state.currentLine; i++) {
|
|
342
|
+
const lp = i === 0 ? this.promptLen : 5;
|
|
343
|
+
row += calcPhysicalLines(this.state.lines[i], lp, termWidth);
|
|
344
|
+
}
|
|
345
|
+
row += Math.floor(cursorAbsPos / termWidth);
|
|
346
|
+
const col = cursorAbsPos % termWidth;
|
|
347
|
+
debugRender('InputPrompt:getCursorInfo', `termWidth=${String(termWidth)} cursorPos=${String(this.state.cursorPos)} currentLine=${String(this.state.currentLine)} promptLen=${String(this.promptLen)}`);
|
|
348
|
+
debugRender('InputPrompt:getCursorInfo', `cursorAbsPos=${String(cursorAbsPos)} row=${String(row)} col=${String(col)}`);
|
|
349
|
+
return { row, col };
|
|
165
350
|
}
|
|
166
351
|
/**
|
|
167
|
-
*
|
|
352
|
+
* Get autocomplete dropdown lines (if active)
|
|
168
353
|
*/
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
354
|
+
getAutocompleteLines() {
|
|
355
|
+
const s = getStyles();
|
|
356
|
+
// File autocomplete takes priority
|
|
357
|
+
if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
|
|
358
|
+
const lines = [];
|
|
359
|
+
const offset = this.fileAutocomplete.scrollOffset;
|
|
360
|
+
const visible = this.fileAutocomplete.matches.slice(offset, offset + MAX_VISIBLE_COMMANDS);
|
|
361
|
+
const total = this.fileAutocomplete.matches.length;
|
|
362
|
+
// Show scroll indicator if there are more items above
|
|
363
|
+
if (offset > 0) {
|
|
364
|
+
lines.push(s.muted(` ↑ ${String(offset)} more above`));
|
|
365
|
+
}
|
|
366
|
+
for (let i = 0; i < visible.length; i++) {
|
|
367
|
+
const file = visible[i];
|
|
368
|
+
const actualIndex = offset + i;
|
|
369
|
+
const isSelected = actualIndex === this.fileAutocomplete.selectedIndex;
|
|
370
|
+
const prefix = isSelected ? s.primary('❯ ') : ' ';
|
|
371
|
+
const icon = file.isDirectory ? s.warning('📁 ') : s.info('📄 ');
|
|
372
|
+
const name = isSelected ? s.primary(chalk.bold(file.path)) : file.path;
|
|
373
|
+
lines.push(`${prefix}${icon}${name}`);
|
|
374
|
+
}
|
|
375
|
+
// Show scroll indicator if there are more items below
|
|
376
|
+
const remaining = total - offset - visible.length;
|
|
377
|
+
if (remaining > 0) {
|
|
378
|
+
lines.push(s.muted(` ↓ ${String(remaining)} more below`));
|
|
379
|
+
}
|
|
380
|
+
return lines;
|
|
381
|
+
}
|
|
382
|
+
// Command autocomplete
|
|
383
|
+
if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
|
|
384
|
+
return [];
|
|
172
385
|
}
|
|
386
|
+
const lines = [];
|
|
387
|
+
const offset = this.autocomplete.scrollOffset;
|
|
388
|
+
const visible = this.autocomplete.matches.slice(offset, offset + MAX_VISIBLE_COMMANDS);
|
|
389
|
+
const total = this.autocomplete.matches.length;
|
|
390
|
+
// Show scroll indicator if there are more items above
|
|
391
|
+
if (offset > 0) {
|
|
392
|
+
lines.push(s.muted(` ↑ ${String(offset)} more above`));
|
|
393
|
+
}
|
|
394
|
+
for (let i = 0; i < visible.length; i++) {
|
|
395
|
+
const cmd = visible[i];
|
|
396
|
+
const actualIndex = offset + i;
|
|
397
|
+
const isSelected = actualIndex === this.autocomplete.selectedIndex;
|
|
398
|
+
const prefix = isSelected ? s.primary('❯ ') : ' ';
|
|
399
|
+
const name = isSelected ? s.primary(chalk.bold(cmd.command)) : cmd.command;
|
|
400
|
+
const desc = s.muted(` - ${cmd.description}`);
|
|
401
|
+
lines.push(`${prefix}${name}${desc}`);
|
|
402
|
+
}
|
|
403
|
+
// Show scroll indicator if there are more items below
|
|
404
|
+
const remaining = total - offset - visible.length;
|
|
405
|
+
if (remaining > 0) {
|
|
406
|
+
lines.push(s.muted(` ↓ ${String(remaining)} more below`));
|
|
407
|
+
}
|
|
408
|
+
return lines;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Check if autocomplete is showing
|
|
412
|
+
*/
|
|
413
|
+
isAutocompleteActive() {
|
|
414
|
+
return ((this.autocomplete.active && this.autocomplete.matches.length > 0) ||
|
|
415
|
+
(this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0));
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Get height of rendered content
|
|
419
|
+
*/
|
|
420
|
+
getHeight() {
|
|
421
|
+
let height = this.state.lines.length;
|
|
422
|
+
if (this.showSeparators)
|
|
423
|
+
height += 2;
|
|
424
|
+
return height;
|
|
425
|
+
}
|
|
426
|
+
// ===========================================================================
|
|
427
|
+
// Legacy API (for backwards compatibility during migration)
|
|
428
|
+
// ===========================================================================
|
|
429
|
+
/**
|
|
430
|
+
* @deprecated Use event-driven start() instead
|
|
431
|
+
* Kept for backwards compatibility during migration
|
|
432
|
+
*/
|
|
433
|
+
async getInput() {
|
|
434
|
+
return new Promise((resolve) => {
|
|
435
|
+
const onSubmit = (input) => {
|
|
436
|
+
cleanup();
|
|
437
|
+
resolve({ action: 'submit', value: input });
|
|
438
|
+
};
|
|
439
|
+
const onCommand = (command, args) => {
|
|
440
|
+
cleanup();
|
|
441
|
+
resolve({ action: 'command', command, args });
|
|
442
|
+
};
|
|
443
|
+
const onCancel = () => {
|
|
444
|
+
cleanup();
|
|
445
|
+
resolve({ action: 'cancel' });
|
|
446
|
+
};
|
|
447
|
+
const cleanup = () => {
|
|
448
|
+
this.removeListener('submit', onSubmit);
|
|
449
|
+
this.removeListener('command', onCommand);
|
|
450
|
+
this.removeListener('cancel', onCancel);
|
|
451
|
+
};
|
|
452
|
+
this.on('submit', onSubmit);
|
|
453
|
+
this.on('command', onCommand);
|
|
454
|
+
this.on('cancel', onCancel);
|
|
455
|
+
if (!this.isRunning) {
|
|
456
|
+
this.start();
|
|
457
|
+
}
|
|
458
|
+
});
|
|
173
459
|
}
|
|
174
460
|
// ===========================================================================
|
|
175
461
|
// Private: State Management
|
|
@@ -184,6 +470,14 @@ export class InputPrompt {
|
|
|
184
470
|
active: false,
|
|
185
471
|
matches: [],
|
|
186
472
|
selectedIndex: 0,
|
|
473
|
+
scrollOffset: 0,
|
|
474
|
+
};
|
|
475
|
+
this.fileAutocomplete = {
|
|
476
|
+
active: false,
|
|
477
|
+
matches: [],
|
|
478
|
+
selectedIndex: 0,
|
|
479
|
+
scrollOffset: 0,
|
|
480
|
+
partial: '',
|
|
187
481
|
};
|
|
188
482
|
this.historyIndex = -1;
|
|
189
483
|
this.savedInput = '';
|
|
@@ -194,229 +488,52 @@ export class InputPrompt {
|
|
|
194
488
|
}
|
|
195
489
|
updateAutocomplete() {
|
|
196
490
|
const fullInput = this.getValue();
|
|
491
|
+
const currentLine = this.state.lines[this.state.currentLine];
|
|
492
|
+
const cursorPos = this.state.cursorPos;
|
|
493
|
+
// Check for @ file path autocomplete first
|
|
494
|
+
const atMention = extractAtMention(currentLine, cursorPos);
|
|
495
|
+
if (atMention !== null) {
|
|
496
|
+
// File autocomplete mode
|
|
497
|
+
this.autocomplete.active = false;
|
|
498
|
+
this.autocomplete.matches = [];
|
|
499
|
+
this.autocomplete.selectedIndex = 0;
|
|
500
|
+
this.autocomplete.scrollOffset = 0;
|
|
501
|
+
this.fileAutocomplete.active = true;
|
|
502
|
+
this.fileAutocomplete.partial = atMention;
|
|
503
|
+
this.fileAutocomplete.matches = getFileMatches(atMention);
|
|
504
|
+
if (this.fileAutocomplete.selectedIndex >= this.fileAutocomplete.matches.length) {
|
|
505
|
+
this.fileAutocomplete.selectedIndex = Math.max(0, this.fileAutocomplete.matches.length - 1);
|
|
506
|
+
this.fileAutocomplete.scrollOffset = 0;
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// Reset file autocomplete
|
|
511
|
+
this.fileAutocomplete.active = false;
|
|
512
|
+
this.fileAutocomplete.matches = [];
|
|
513
|
+
this.fileAutocomplete.selectedIndex = 0;
|
|
514
|
+
this.fileAutocomplete.scrollOffset = 0;
|
|
515
|
+
this.fileAutocomplete.partial = '';
|
|
516
|
+
// Check for / command autocomplete
|
|
197
517
|
if (fullInput.startsWith('/') && this.state.lines.length === 1) {
|
|
198
518
|
this.autocomplete.active = true;
|
|
199
|
-
|
|
519
|
+
// Refresh commands dynamically to include newly created custom commands
|
|
520
|
+
const freshCommands = getAutocompleteCommands();
|
|
521
|
+
this.autocomplete.matches = filterCommands(fullInput, freshCommands);
|
|
200
522
|
if (this.autocomplete.selectedIndex >= this.autocomplete.matches.length) {
|
|
201
523
|
this.autocomplete.selectedIndex = Math.max(0, this.autocomplete.matches.length - 1);
|
|
524
|
+
this.autocomplete.scrollOffset = 0;
|
|
202
525
|
}
|
|
203
526
|
}
|
|
204
527
|
else {
|
|
205
528
|
this.autocomplete.active = false;
|
|
206
529
|
this.autocomplete.matches = [];
|
|
207
530
|
this.autocomplete.selectedIndex = 0;
|
|
531
|
+
this.autocomplete.scrollOffset = 0;
|
|
208
532
|
}
|
|
209
533
|
}
|
|
210
|
-
// ===========================================================================
|
|
211
|
-
// Private: Layout Calculation
|
|
212
|
-
// ===========================================================================
|
|
213
|
-
/**
|
|
214
|
-
* Get physical layout of current buffer
|
|
215
|
-
*/
|
|
216
|
-
getPhysicalLayout() {
|
|
217
|
-
const termWidth = terminal.getTerminalWidth();
|
|
218
|
-
const lines = [];
|
|
219
|
-
let cursorRow = 0;
|
|
220
|
-
let cursorCol = 0;
|
|
221
|
-
let totalRows = 0;
|
|
222
|
-
for (let i = 0; i < this.state.lines.length; i++) {
|
|
223
|
-
const linePromptLen = i === 0 ? this.promptLen : 5; // " \ "
|
|
224
|
-
const lineText = this.state.lines[i];
|
|
225
|
-
const physicalLines = calcPhysicalLines(lineText, linePromptLen, termWidth);
|
|
226
|
-
for (let p = 0; p < physicalLines; p++) {
|
|
227
|
-
const start = p * termWidth - (p === 0 ? 0 : linePromptLen);
|
|
228
|
-
const end = start + termWidth;
|
|
229
|
-
lines.push(lineText.slice(Math.max(0, start), end));
|
|
230
|
-
}
|
|
231
|
-
if (i === this.state.currentLine) {
|
|
232
|
-
const cursorAbsPos = linePromptLen + this.state.cursorPos;
|
|
233
|
-
cursorRow = totalRows + Math.floor(cursorAbsPos / termWidth);
|
|
234
|
-
cursorCol = cursorAbsPos % termWidth;
|
|
235
|
-
}
|
|
236
|
-
totalRows += physicalLines;
|
|
237
|
-
}
|
|
238
|
-
return { lines, cursorRow, cursorCol, totalRows };
|
|
239
|
-
}
|
|
240
|
-
// ===========================================================================
|
|
241
|
-
// Private: Rendering
|
|
242
|
-
// ===========================================================================
|
|
243
534
|
getSeparatorLine() {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
renderTodos() {
|
|
247
|
-
if (!this.getTodos)
|
|
248
|
-
return 0;
|
|
249
|
-
const todos = this.getTodos();
|
|
250
|
-
if (todos.length === 0)
|
|
251
|
-
return 0;
|
|
252
|
-
let linesRendered = 0;
|
|
253
|
-
// Group todos by status for display
|
|
254
|
-
const inProgress = todos.filter(t => t.status === 'in_progress');
|
|
255
|
-
const pending = todos.filter(t => t.status === 'pending');
|
|
256
|
-
const completed = todos.filter(t => t.status === 'completed');
|
|
257
|
-
// Show in-progress task prominently
|
|
258
|
-
for (const todo of inProgress) {
|
|
259
|
-
const label = todo.activeForm || todo.content;
|
|
260
|
-
terminal.write(pc.cyan('⟳ ') + pc.bold(label) + '\n');
|
|
261
|
-
linesRendered++;
|
|
262
|
-
}
|
|
263
|
-
// Show pending tasks
|
|
264
|
-
for (const todo of pending) {
|
|
265
|
-
terminal.write(pc.dim('○ ') + pc.dim(todo.content) + '\n');
|
|
266
|
-
linesRendered++;
|
|
267
|
-
}
|
|
268
|
-
// Show completed tasks (dimmed)
|
|
269
|
-
for (const todo of completed) {
|
|
270
|
-
terminal.write(pc.green('✓ ') + pc.dim(pc.strikethrough(todo.content)) + '\n');
|
|
271
|
-
linesRendered++;
|
|
272
|
-
}
|
|
273
|
-
if (linesRendered > 0) {
|
|
274
|
-
terminal.write('\n');
|
|
275
|
-
linesRendered++;
|
|
276
|
-
}
|
|
277
|
-
return linesRendered;
|
|
278
|
-
}
|
|
279
|
-
render() {
|
|
280
|
-
const termWidth = terminal.getTerminalWidth();
|
|
281
|
-
// Clear previous render
|
|
282
|
-
this.clearDropdown();
|
|
283
|
-
if (this.linesAboveCursor > 0) {
|
|
284
|
-
terminal.moveCursorToLineStart();
|
|
285
|
-
terminal.moveCursorUp(this.linesAboveCursor);
|
|
286
|
-
}
|
|
287
|
-
else {
|
|
288
|
-
terminal.moveCursorToLineStart();
|
|
289
|
-
}
|
|
290
|
-
terminal.clearToEndOfScreen();
|
|
291
|
-
// Render todos (above input area)
|
|
292
|
-
let physicalLinesRendered = 0;
|
|
293
|
-
physicalLinesRendered += this.renderTodos();
|
|
294
|
-
// Render top separator
|
|
295
|
-
if (this.showSeparators) {
|
|
296
|
-
terminal.write(this.getSeparatorLine() + '\n');
|
|
297
|
-
physicalLinesRendered += 1;
|
|
298
|
-
}
|
|
299
|
-
// Render input lines
|
|
300
|
-
let physicalLinesBeforeCursor = 0;
|
|
301
|
-
for (let i = 0; i < this.state.lines.length; i++) {
|
|
302
|
-
const linePrompt = i === 0 ? this.prompt : pc.dim(' \\ ');
|
|
303
|
-
const linePromptLen = i === 0 ? this.promptLen : 5;
|
|
304
|
-
if (i < this.state.currentLine) {
|
|
305
|
-
physicalLinesBeforeCursor += calcPhysicalLines(this.state.lines[i], linePromptLen, termWidth);
|
|
306
|
-
}
|
|
307
|
-
terminal.write(linePrompt + this.state.lines[i]);
|
|
308
|
-
if (i < this.state.lines.length - 1) {
|
|
309
|
-
terminal.write('\n');
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
// Render bottom separator
|
|
313
|
-
if (this.showSeparators) {
|
|
314
|
-
terminal.write('\n' + this.getSeparatorLine());
|
|
315
|
-
}
|
|
316
|
-
// Track state
|
|
317
|
-
this.renderedLines = this.state.lines.length;
|
|
318
|
-
this.hasSeparators = this.showSeparators;
|
|
319
|
-
// Position cursor
|
|
320
|
-
const currentLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
|
|
321
|
-
const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
|
|
322
|
-
const currentLinePhysical = calcPhysicalLines(this.state.lines[this.state.currentLine], currentLinePromptLen, termWidth);
|
|
323
|
-
const cursorPhysicalRow = Math.floor(cursorAbsPos / termWidth);
|
|
324
|
-
// Move up from bottom to cursor position
|
|
325
|
-
let linesToMoveUp = this.showSeparators ? 1 : 0;
|
|
326
|
-
for (let i = this.state.currentLine + 1; i < this.state.lines.length; i++) {
|
|
327
|
-
const lp = i === 0 ? this.promptLen : 5;
|
|
328
|
-
linesToMoveUp += calcPhysicalLines(this.state.lines[i], lp, termWidth);
|
|
329
|
-
}
|
|
330
|
-
linesToMoveUp += currentLinePhysical - 1 - cursorPhysicalRow;
|
|
331
|
-
if (linesToMoveUp > 0) {
|
|
332
|
-
terminal.moveCursorUp(linesToMoveUp);
|
|
333
|
-
}
|
|
334
|
-
const cursorCol = (cursorAbsPos % termWidth) + 1;
|
|
335
|
-
terminal.moveCursorToColumn(cursorCol);
|
|
336
|
-
// Track cursor position for next render
|
|
337
|
-
this.linesAboveCursor = physicalLinesRendered + physicalLinesBeforeCursor + cursorPhysicalRow;
|
|
338
|
-
// Render dropdown
|
|
339
|
-
this.renderDropdown();
|
|
340
|
-
}
|
|
341
|
-
renderDropdown() {
|
|
342
|
-
if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
|
|
343
|
-
this.dropdownLines = 0;
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
const termWidth = terminal.getTerminalWidth();
|
|
347
|
-
const currentLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
|
|
348
|
-
// Move down past remaining input lines and bottom separator
|
|
349
|
-
let linesToMoveDown = this.state.lines.length - 1 - this.state.currentLine;
|
|
350
|
-
if (this.hasSeparators)
|
|
351
|
-
linesToMoveDown += 1;
|
|
352
|
-
// Account for wrapped lines
|
|
353
|
-
for (let i = this.state.currentLine; i < this.state.lines.length; i++) {
|
|
354
|
-
const lp = i === 0 ? this.promptLen : 5;
|
|
355
|
-
const physical = calcPhysicalLines(this.state.lines[i], lp, termWidth);
|
|
356
|
-
if (i === this.state.currentLine) {
|
|
357
|
-
const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
|
|
358
|
-
const cursorRow = Math.floor(cursorAbsPos / termWidth);
|
|
359
|
-
linesToMoveDown += physical - 1 - cursorRow;
|
|
360
|
-
}
|
|
361
|
-
else {
|
|
362
|
-
linesToMoveDown += physical - 1;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
if (linesToMoveDown > 0) {
|
|
366
|
-
terminal.moveCursorDown(linesToMoveDown);
|
|
367
|
-
}
|
|
368
|
-
terminal.write('\n');
|
|
369
|
-
const visible = this.autocomplete.matches.slice(0, MAX_VISIBLE_COMMANDS);
|
|
370
|
-
for (let i = 0; i < visible.length; i++) {
|
|
371
|
-
const cmd = visible[i];
|
|
372
|
-
const isSelected = i === this.autocomplete.selectedIndex;
|
|
373
|
-
const prefix = isSelected ? pc.cyan('❯ ') : ' ';
|
|
374
|
-
const name = isSelected ? pc.cyan(pc.bold(cmd.command)) : cmd.command;
|
|
375
|
-
const desc = pc.dim(` - ${cmd.description}`);
|
|
376
|
-
terminal.write(`${prefix}${name}${desc}\n`);
|
|
377
|
-
}
|
|
378
|
-
// Move back up to cursor position
|
|
379
|
-
this.dropdownLines = visible.length;
|
|
380
|
-
const linesToMoveUp = this.dropdownLines + linesToMoveDown + 1;
|
|
381
|
-
terminal.moveCursorUp(linesToMoveUp);
|
|
382
|
-
const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
|
|
383
|
-
const cursorCol = (cursorAbsPos % termWidth) + 1;
|
|
384
|
-
terminal.moveCursorToColumn(cursorCol);
|
|
385
|
-
}
|
|
386
|
-
clearDropdown() {
|
|
387
|
-
if (this.dropdownLines === 0)
|
|
388
|
-
return;
|
|
389
|
-
const termWidth = terminal.getTerminalWidth();
|
|
390
|
-
const currentLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
|
|
391
|
-
// Move down past remaining lines and separator
|
|
392
|
-
let linesToMoveDown = this.state.lines.length - 1 - this.state.currentLine;
|
|
393
|
-
if (this.hasSeparators)
|
|
394
|
-
linesToMoveDown += 1;
|
|
395
|
-
for (let i = this.state.currentLine; i < this.state.lines.length; i++) {
|
|
396
|
-
const lp = i === 0 ? this.promptLen : 5;
|
|
397
|
-
const physical = calcPhysicalLines(this.state.lines[i], lp, termWidth);
|
|
398
|
-
if (i === this.state.currentLine) {
|
|
399
|
-
const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
|
|
400
|
-
const cursorRow = Math.floor(cursorAbsPos / termWidth);
|
|
401
|
-
linesToMoveDown += physical - 1 - cursorRow;
|
|
402
|
-
}
|
|
403
|
-
else {
|
|
404
|
-
linesToMoveDown += physical - 1;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
if (linesToMoveDown > 0) {
|
|
408
|
-
terminal.moveCursorDown(linesToMoveDown);
|
|
409
|
-
}
|
|
410
|
-
terminal.write('\n');
|
|
411
|
-
// Clear dropdown lines
|
|
412
|
-
for (let i = 0; i < this.dropdownLines; i++) {
|
|
413
|
-
terminal.clearLine();
|
|
414
|
-
terminal.write('\n');
|
|
415
|
-
}
|
|
416
|
-
// Move back up
|
|
417
|
-
const linesToMoveUp = this.dropdownLines + linesToMoveDown + 1;
|
|
418
|
-
terminal.moveCursorUp(linesToMoveUp);
|
|
419
|
-
this.dropdownLines = 0;
|
|
535
|
+
const s = getStyles();
|
|
536
|
+
return s.muted('─'.repeat(terminal.getTerminalWidth()));
|
|
420
537
|
}
|
|
421
538
|
// ===========================================================================
|
|
422
539
|
// Private: Input Handling
|
|
@@ -433,7 +550,9 @@ export class InputPrompt {
|
|
|
433
550
|
const isEnter = key === '\r' || key === '\n';
|
|
434
551
|
const isBackspace = key === '\x7f' || key === '\b';
|
|
435
552
|
const isTab = key === '\t';
|
|
553
|
+
const isShiftTab = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x5a;
|
|
436
554
|
const isCtrlC = key === '\x03';
|
|
555
|
+
const isCtrlO = key === '\x0f'; // Ctrl+O for toggle subagent expand
|
|
437
556
|
const isUpArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x41;
|
|
438
557
|
const isDownArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x42;
|
|
439
558
|
const isLeftArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x44;
|
|
@@ -460,16 +579,39 @@ export class InputPrompt {
|
|
|
460
579
|
const isEnd = key === '\x05' || (data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x46);
|
|
461
580
|
// Ctrl+C - cancel
|
|
462
581
|
if (isCtrlC) {
|
|
463
|
-
this.
|
|
582
|
+
this.emit('cancel');
|
|
464
583
|
return;
|
|
465
584
|
}
|
|
466
|
-
//
|
|
585
|
+
// Ctrl+O - toggle subagent expand (only when agent is running)
|
|
586
|
+
if (isCtrlO) {
|
|
587
|
+
this.emit('toggleSubagentExpand');
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
// Escape - close autocomplete, double Esc clears input, or emit escape for agent abort
|
|
467
591
|
if (isEscape) {
|
|
468
|
-
|
|
469
|
-
|
|
592
|
+
const now = Date.now();
|
|
593
|
+
const isDoubleEsc = now - this.lastEscTime < this.DOUBLE_ESC_THRESHOLD_MS;
|
|
594
|
+
this.lastEscTime = now;
|
|
595
|
+
if (this.fileAutocomplete.active) {
|
|
596
|
+
// First priority: close file autocomplete
|
|
597
|
+
this.fileAutocomplete.active = false;
|
|
598
|
+
this.fileAutocomplete.matches = [];
|
|
599
|
+
this.emit('change', this.getValue());
|
|
600
|
+
}
|
|
601
|
+
else if (this.autocomplete.active) {
|
|
602
|
+
// Second priority: close command autocomplete
|
|
470
603
|
this.autocomplete.active = false;
|
|
471
604
|
this.autocomplete.matches = [];
|
|
472
|
-
this.
|
|
605
|
+
this.emit('change', this.getValue());
|
|
606
|
+
}
|
|
607
|
+
else if (isDoubleEsc && this.getValue().length > 0) {
|
|
608
|
+
// Third priority: double Esc clears input (if there's content)
|
|
609
|
+
this.clearInput();
|
|
610
|
+
this.emit('change', this.getValue());
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
// Fourth priority: emit escape for agent abort
|
|
614
|
+
this.emit('escape');
|
|
473
615
|
}
|
|
474
616
|
return;
|
|
475
617
|
}
|
|
@@ -478,6 +620,11 @@ export class InputPrompt {
|
|
|
478
620
|
this.handleEnter();
|
|
479
621
|
return;
|
|
480
622
|
}
|
|
623
|
+
// Shift+Tab - cycle modes
|
|
624
|
+
if (isShiftTab) {
|
|
625
|
+
this.emit('modeChange');
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
481
628
|
// Tab - accept autocomplete or insert spaces
|
|
482
629
|
if (isTab) {
|
|
483
630
|
this.handleTab();
|
|
@@ -512,12 +659,12 @@ export class InputPrompt {
|
|
|
512
659
|
// Home/End
|
|
513
660
|
if (isHome) {
|
|
514
661
|
this.state.cursorPos = 0;
|
|
515
|
-
this.
|
|
662
|
+
this.emit('change', this.getValue());
|
|
516
663
|
return;
|
|
517
664
|
}
|
|
518
665
|
if (isEnd) {
|
|
519
666
|
this.state.cursorPos = this.state.lines[this.state.currentLine].length;
|
|
520
|
-
this.
|
|
667
|
+
this.emit('change', this.getValue());
|
|
521
668
|
return;
|
|
522
669
|
}
|
|
523
670
|
// Backspace
|
|
@@ -532,13 +679,17 @@ export class InputPrompt {
|
|
|
532
679
|
.filter((c) => c.charCodeAt(0) >= 32)
|
|
533
680
|
.join('');
|
|
534
681
|
if (printable.length > 0) {
|
|
682
|
+
// Clear suggestion when user starts typing
|
|
683
|
+
if (this.suggestion) {
|
|
684
|
+
this.suggestion = null;
|
|
685
|
+
}
|
|
535
686
|
const line = this.state.lines[this.state.currentLine];
|
|
536
687
|
this.state.lines[this.state.currentLine] =
|
|
537
688
|
line.slice(0, this.state.cursorPos) + printable + line.slice(this.state.cursorPos);
|
|
538
689
|
this.state.cursorPos += printable.length;
|
|
539
690
|
this.historyIndex = -1;
|
|
540
691
|
this.updateAutocomplete();
|
|
541
|
-
this.
|
|
692
|
+
this.emit('change', this.getValue());
|
|
542
693
|
}
|
|
543
694
|
}
|
|
544
695
|
}
|
|
@@ -551,35 +702,82 @@ export class InputPrompt {
|
|
|
551
702
|
this.state.currentLine++;
|
|
552
703
|
this.state.cursorPos = 0;
|
|
553
704
|
this.autocomplete.active = false;
|
|
554
|
-
this.
|
|
555
|
-
this.render();
|
|
705
|
+
this.emit('change', this.getValue());
|
|
556
706
|
return;
|
|
557
707
|
}
|
|
558
|
-
//
|
|
708
|
+
// File autocomplete selection - accept the path and continue
|
|
709
|
+
if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
|
|
710
|
+
const selectedFile = this.fileAutocomplete.matches[this.fileAutocomplete.selectedIndex];
|
|
711
|
+
const result = replaceAtMention(currentLine, this.state.cursorPos, selectedFile.path);
|
|
712
|
+
this.state.lines[this.state.currentLine] = result.input;
|
|
713
|
+
this.state.cursorPos = result.cursorPos;
|
|
714
|
+
this.fileAutocomplete.active = false;
|
|
715
|
+
this.fileAutocomplete.matches = [];
|
|
716
|
+
// Don't return - fall through to submit
|
|
717
|
+
}
|
|
718
|
+
// Command autocomplete selection - accept the command and continue to execute it
|
|
559
719
|
if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
|
|
560
720
|
this.state.lines[0] = this.autocomplete.matches[this.autocomplete.selectedIndex].command;
|
|
561
721
|
this.state.cursorPos = this.state.lines[0].length;
|
|
722
|
+
this.autocomplete.active = false;
|
|
723
|
+
// Don't return - fall through to execute the command
|
|
562
724
|
}
|
|
563
725
|
const input = this.getValue();
|
|
726
|
+
// Empty input - ignore
|
|
727
|
+
if (!input.trim()) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
// Add to history
|
|
731
|
+
this.addToHistory(input);
|
|
732
|
+
// Queue mode - add to queue instead of emitting
|
|
733
|
+
if (this.queueMode) {
|
|
734
|
+
this.queuedInputs.push(input);
|
|
735
|
+
this.clearInput();
|
|
736
|
+
this.emit('change', this.getValue());
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
564
739
|
// Check if it's a command
|
|
565
740
|
if (input.startsWith('/')) {
|
|
566
741
|
const parts = input.slice(1).split(/\s+/);
|
|
567
742
|
const command = parts[0];
|
|
568
743
|
const args = parts.slice(1).join(' ');
|
|
569
|
-
this.
|
|
744
|
+
this.clearInput();
|
|
745
|
+
this.emit('command', command, args);
|
|
570
746
|
}
|
|
571
747
|
else {
|
|
572
|
-
this.
|
|
748
|
+
this.clearInput();
|
|
749
|
+
this.emit('submit', input);
|
|
573
750
|
}
|
|
574
751
|
}
|
|
575
752
|
handleTab() {
|
|
753
|
+
// Accept suggestion if present and input is empty
|
|
754
|
+
if (this.suggestion && this.getValue() === '') {
|
|
755
|
+
this.state.lines[0] = this.suggestion;
|
|
756
|
+
this.state.cursorPos = this.suggestion.length;
|
|
757
|
+
this.suggestion = null;
|
|
758
|
+
this.emit('change', this.getValue());
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
// File autocomplete completion
|
|
762
|
+
if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
|
|
763
|
+
const selectedFile = this.fileAutocomplete.matches[this.fileAutocomplete.selectedIndex];
|
|
764
|
+
const currentLine = this.state.lines[this.state.currentLine];
|
|
765
|
+
const result = replaceAtMention(currentLine, this.state.cursorPos, selectedFile.path);
|
|
766
|
+
this.state.lines[this.state.currentLine] = result.input;
|
|
767
|
+
this.state.cursorPos = result.cursorPos;
|
|
768
|
+
this.fileAutocomplete.active = false;
|
|
769
|
+
this.fileAutocomplete.matches = [];
|
|
770
|
+
this.updateAutocomplete(); // Check if still in @ context (e.g., directory selected)
|
|
771
|
+
this.emit('change', this.getValue());
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
// Command autocomplete completion
|
|
576
775
|
if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
|
|
577
776
|
this.state.lines[this.state.currentLine] =
|
|
578
777
|
this.autocomplete.matches[this.autocomplete.selectedIndex].command;
|
|
579
778
|
this.state.cursorPos = this.state.lines[this.state.currentLine].length;
|
|
580
779
|
this.autocomplete.active = false;
|
|
581
780
|
this.autocomplete.matches = [];
|
|
582
|
-
this.clearDropdown();
|
|
583
781
|
}
|
|
584
782
|
else {
|
|
585
783
|
// Insert 2 spaces
|
|
@@ -590,13 +788,27 @@ export class InputPrompt {
|
|
|
590
788
|
this.historyIndex = -1;
|
|
591
789
|
this.updateAutocomplete();
|
|
592
790
|
}
|
|
593
|
-
this.
|
|
791
|
+
this.emit('change', this.getValue());
|
|
594
792
|
}
|
|
595
793
|
handleArrowUp() {
|
|
596
|
-
//
|
|
794
|
+
// File autocomplete navigation
|
|
795
|
+
if (this.fileAutocomplete.active && this.fileAutocomplete.selectedIndex > 0) {
|
|
796
|
+
this.fileAutocomplete.selectedIndex--;
|
|
797
|
+
// Adjust scroll offset if selection goes above visible area
|
|
798
|
+
if (this.fileAutocomplete.selectedIndex < this.fileAutocomplete.scrollOffset) {
|
|
799
|
+
this.fileAutocomplete.scrollOffset = this.fileAutocomplete.selectedIndex;
|
|
800
|
+
}
|
|
801
|
+
this.emit('change', this.getValue());
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
// Command autocomplete navigation
|
|
597
805
|
if (this.autocomplete.active && this.autocomplete.selectedIndex > 0) {
|
|
598
806
|
this.autocomplete.selectedIndex--;
|
|
599
|
-
|
|
807
|
+
// Adjust scroll offset if selection goes above visible area
|
|
808
|
+
if (this.autocomplete.selectedIndex < this.autocomplete.scrollOffset) {
|
|
809
|
+
this.autocomplete.scrollOffset = this.autocomplete.selectedIndex;
|
|
810
|
+
}
|
|
811
|
+
this.emit('change', this.getValue());
|
|
600
812
|
return;
|
|
601
813
|
}
|
|
602
814
|
const termWidth = terminal.getTerminalWidth();
|
|
@@ -609,7 +821,7 @@ export class InputPrompt {
|
|
|
609
821
|
const newAbsPos = (currentPhysicalRow - 1) * termWidth + cursorColInRow;
|
|
610
822
|
this.state.cursorPos = Math.max(0, newAbsPos - currentLinePromptLen);
|
|
611
823
|
this.state.cursorPos = Math.min(this.state.cursorPos, this.state.lines[this.state.currentLine].length);
|
|
612
|
-
this.
|
|
824
|
+
this.emit('change', this.getValue());
|
|
613
825
|
return;
|
|
614
826
|
}
|
|
615
827
|
// Navigate to previous logical line
|
|
@@ -622,7 +834,7 @@ export class InputPrompt {
|
|
|
622
834
|
const targetAbsPos = lastRowStart + cursorColInRow;
|
|
623
835
|
this.state.cursorPos = Math.max(0, targetAbsPos - prevLinePromptLen);
|
|
624
836
|
this.state.cursorPos = Math.min(this.state.cursorPos, prevLineLen);
|
|
625
|
-
this.
|
|
837
|
+
this.emit('change', this.getValue());
|
|
626
838
|
return;
|
|
627
839
|
}
|
|
628
840
|
// History navigation (at top of input)
|
|
@@ -636,17 +848,33 @@ export class InputPrompt {
|
|
|
636
848
|
this.state.lines = [historyEntry];
|
|
637
849
|
this.state.currentLine = 0;
|
|
638
850
|
this.state.cursorPos = historyEntry.length;
|
|
639
|
-
this.
|
|
851
|
+
this.emit('change', this.getValue());
|
|
640
852
|
}
|
|
641
853
|
}
|
|
642
854
|
}
|
|
643
855
|
handleArrowDown() {
|
|
644
|
-
//
|
|
856
|
+
// File autocomplete navigation
|
|
857
|
+
if (this.fileAutocomplete.active &&
|
|
858
|
+
this.fileAutocomplete.selectedIndex < this.fileAutocomplete.matches.length - 1) {
|
|
859
|
+
this.fileAutocomplete.selectedIndex++;
|
|
860
|
+
// Adjust scroll offset if selection goes below visible area
|
|
861
|
+
const maxVisibleIndex = this.fileAutocomplete.scrollOffset + MAX_VISIBLE_COMMANDS - 1;
|
|
862
|
+
if (this.fileAutocomplete.selectedIndex > maxVisibleIndex) {
|
|
863
|
+
this.fileAutocomplete.scrollOffset = this.fileAutocomplete.selectedIndex - MAX_VISIBLE_COMMANDS + 1;
|
|
864
|
+
}
|
|
865
|
+
this.emit('change', this.getValue());
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
// Command autocomplete navigation
|
|
645
869
|
if (this.autocomplete.active &&
|
|
646
|
-
this.autocomplete.selectedIndex < this.autocomplete.matches.length - 1
|
|
647
|
-
this.autocomplete.selectedIndex < MAX_VISIBLE_COMMANDS - 1) {
|
|
870
|
+
this.autocomplete.selectedIndex < this.autocomplete.matches.length - 1) {
|
|
648
871
|
this.autocomplete.selectedIndex++;
|
|
649
|
-
|
|
872
|
+
// Adjust scroll offset if selection goes below visible area
|
|
873
|
+
const maxVisibleIndex = this.autocomplete.scrollOffset + MAX_VISIBLE_COMMANDS - 1;
|
|
874
|
+
if (this.autocomplete.selectedIndex > maxVisibleIndex) {
|
|
875
|
+
this.autocomplete.scrollOffset = this.autocomplete.selectedIndex - MAX_VISIBLE_COMMANDS + 1;
|
|
876
|
+
}
|
|
877
|
+
this.emit('change', this.getValue());
|
|
650
878
|
return;
|
|
651
879
|
}
|
|
652
880
|
const termWidth = terminal.getTerminalWidth();
|
|
@@ -660,7 +888,7 @@ export class InputPrompt {
|
|
|
660
888
|
const newAbsPos = (currentPhysicalRow + 1) * termWidth + cursorColInRow;
|
|
661
889
|
this.state.cursorPos = Math.max(0, newAbsPos - currentLinePromptLen);
|
|
662
890
|
this.state.cursorPos = Math.min(this.state.cursorPos, this.state.lines[this.state.currentLine].length);
|
|
663
|
-
this.
|
|
891
|
+
this.emit('change', this.getValue());
|
|
664
892
|
return;
|
|
665
893
|
}
|
|
666
894
|
// Navigate to next logical line
|
|
@@ -670,7 +898,7 @@ export class InputPrompt {
|
|
|
670
898
|
const nextLineLen = this.state.lines[this.state.currentLine].length;
|
|
671
899
|
this.state.cursorPos = Math.max(0, cursorColInRow - nextLinePromptLen);
|
|
672
900
|
this.state.cursorPos = Math.min(this.state.cursorPos, nextLineLen);
|
|
673
|
-
this.
|
|
901
|
+
this.emit('change', this.getValue());
|
|
674
902
|
return;
|
|
675
903
|
}
|
|
676
904
|
// History forward
|
|
@@ -688,29 +916,29 @@ export class InputPrompt {
|
|
|
688
916
|
this.state.cursorPos = historyEntry.length;
|
|
689
917
|
}
|
|
690
918
|
this.updateAutocomplete();
|
|
691
|
-
this.
|
|
919
|
+
this.emit('change', this.getValue());
|
|
692
920
|
}
|
|
693
921
|
}
|
|
694
922
|
handleArrowLeft() {
|
|
695
923
|
if (this.state.cursorPos > 0) {
|
|
696
924
|
this.state.cursorPos--;
|
|
697
|
-
|
|
925
|
+
this.emit('change', this.getValue());
|
|
698
926
|
}
|
|
699
927
|
else if (this.state.currentLine > 0) {
|
|
700
928
|
this.state.currentLine--;
|
|
701
929
|
this.state.cursorPos = this.state.lines[this.state.currentLine].length;
|
|
702
|
-
this.
|
|
930
|
+
this.emit('change', this.getValue());
|
|
703
931
|
}
|
|
704
932
|
}
|
|
705
933
|
handleArrowRight() {
|
|
706
934
|
if (this.state.cursorPos < this.state.lines[this.state.currentLine].length) {
|
|
707
935
|
this.state.cursorPos++;
|
|
708
|
-
|
|
936
|
+
this.emit('change', this.getValue());
|
|
709
937
|
}
|
|
710
938
|
else if (this.state.currentLine < this.state.lines.length - 1) {
|
|
711
939
|
this.state.currentLine++;
|
|
712
940
|
this.state.cursorPos = 0;
|
|
713
|
-
this.
|
|
941
|
+
this.emit('change', this.getValue());
|
|
714
942
|
}
|
|
715
943
|
}
|
|
716
944
|
handleWordLeft() {
|
|
@@ -722,12 +950,12 @@ export class InputPrompt {
|
|
|
722
950
|
while (pos > 0 && line[pos - 1] !== ' ')
|
|
723
951
|
pos--;
|
|
724
952
|
this.state.cursorPos = pos;
|
|
725
|
-
this.
|
|
953
|
+
this.emit('change', this.getValue());
|
|
726
954
|
}
|
|
727
955
|
else if (this.state.currentLine > 0) {
|
|
728
956
|
this.state.currentLine--;
|
|
729
957
|
this.state.cursorPos = this.state.lines[this.state.currentLine].length;
|
|
730
|
-
this.
|
|
958
|
+
this.emit('change', this.getValue());
|
|
731
959
|
}
|
|
732
960
|
}
|
|
733
961
|
handleWordRight() {
|
|
@@ -739,12 +967,12 @@ export class InputPrompt {
|
|
|
739
967
|
while (pos < line.length && line[pos] === ' ')
|
|
740
968
|
pos++;
|
|
741
969
|
this.state.cursorPos = pos;
|
|
742
|
-
this.
|
|
970
|
+
this.emit('change', this.getValue());
|
|
743
971
|
}
|
|
744
972
|
else if (this.state.currentLine < this.state.lines.length - 1) {
|
|
745
973
|
this.state.currentLine++;
|
|
746
974
|
this.state.cursorPos = 0;
|
|
747
|
-
this.
|
|
975
|
+
this.emit('change', this.getValue());
|
|
748
976
|
}
|
|
749
977
|
}
|
|
750
978
|
handleBackspace() {
|
|
@@ -755,7 +983,7 @@ export class InputPrompt {
|
|
|
755
983
|
line.slice(0, this.state.cursorPos - 1) + line.slice(this.state.cursorPos);
|
|
756
984
|
this.state.cursorPos--;
|
|
757
985
|
this.updateAutocomplete();
|
|
758
|
-
this.
|
|
986
|
+
this.emit('change', this.getValue());
|
|
759
987
|
}
|
|
760
988
|
else if (this.state.currentLine > 0) {
|
|
761
989
|
const currentLine = this.state.lines[this.state.currentLine];
|
|
@@ -765,36 +993,7 @@ export class InputPrompt {
|
|
|
765
993
|
this.state.currentLine--;
|
|
766
994
|
this.state.cursorPos = prevLine.length;
|
|
767
995
|
this.updateAutocomplete();
|
|
768
|
-
this.
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
finishInput(result) {
|
|
772
|
-
// Clear display
|
|
773
|
-
this.clearDropdown();
|
|
774
|
-
if (this.linesAboveCursor > 0) {
|
|
775
|
-
terminal.moveCursorToLineStart();
|
|
776
|
-
terminal.moveCursorUp(this.linesAboveCursor);
|
|
777
|
-
}
|
|
778
|
-
terminal.clearToEndOfScreen();
|
|
779
|
-
// Print clean input (without separators)
|
|
780
|
-
for (let i = 0; i < this.state.lines.length; i++) {
|
|
781
|
-
const linePrompt = i === 0 ? this.prompt : pc.dim(' \\ ');
|
|
782
|
-
terminal.write(linePrompt + this.state.lines[i]);
|
|
783
|
-
if (i < this.state.lines.length - 1) {
|
|
784
|
-
terminal.write('\n');
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
terminal.writeLine('');
|
|
788
|
-
// Save to history if submitting
|
|
789
|
-
if (result.action === 'submit' || result.action === 'command') {
|
|
790
|
-
const input = this.getValue();
|
|
791
|
-
this.addToHistory(input);
|
|
792
|
-
}
|
|
793
|
-
// Stop and resolve
|
|
794
|
-
this.stop();
|
|
795
|
-
if (this.resolveInput) {
|
|
796
|
-
this.resolveInput(result);
|
|
797
|
-
this.resolveInput = null;
|
|
996
|
+
this.emit('change', this.getValue());
|
|
798
997
|
}
|
|
799
998
|
}
|
|
800
999
|
}
|