moderado 0.3.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/LICENSE +21 -0
- package/README.md +208 -0
- package/dist/args.d.ts +26 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +108 -0
- package/dist/args.js.map +1 -0
- package/dist/commands/chat.d.ts +121 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +1484 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +61 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.d.ts +27 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +172 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/interactive_menu.d.ts +3 -0
- package/dist/commands/interactive_menu.d.ts.map +1 -0
- package/dist/commands/interactive_menu.js +98 -0
- package/dist/commands/interactive_menu.js.map +1 -0
- package/dist/commands/models.d.ts +8 -0
- package/dist/commands/models.d.ts.map +1 -0
- package/dist/commands/models.js +81 -0
- package/dist/commands/models.js.map +1 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +140 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +195 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +14 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +23 -0
- package/dist/credentials.js.map +1 -0
- package/dist/diagnostic_log.d.ts +2 -0
- package/dist/diagnostic_log.d.ts.map +1 -0
- package/dist/diagnostic_log.js +21 -0
- package/dist/diagnostic_log.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/dist/model_pricing.d.ts +4 -0
- package/dist/model_pricing.d.ts.map +1 -0
- package/dist/model_pricing.js +10 -0
- package/dist/model_pricing.js.map +1 -0
- package/dist/sessions.d.ts +203 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +82 -0
- package/dist/sessions.js.map +1 -0
- package/dist/ui/box.d.ts +14 -0
- package/dist/ui/box.d.ts.map +1 -0
- package/dist/ui/box.js +52 -0
- package/dist/ui/box.js.map +1 -0
- package/dist/ui/file_mentions.d.ts +38 -0
- package/dist/ui/file_mentions.d.ts.map +1 -0
- package/dist/ui/file_mentions.js +119 -0
- package/dist/ui/file_mentions.js.map +1 -0
- package/dist/ui/help_modal.d.ts +2 -0
- package/dist/ui/help_modal.d.ts.map +1 -0
- package/dist/ui/help_modal.js +101 -0
- package/dist/ui/help_modal.js.map +1 -0
- package/dist/ui/model_selector.d.ts +48 -0
- package/dist/ui/model_selector.d.ts.map +1 -0
- package/dist/ui/model_selector.js +829 -0
- package/dist/ui/model_selector.js.map +1 -0
- package/dist/ui/popup.d.ts +78 -0
- package/dist/ui/popup.d.ts.map +1 -0
- package/dist/ui/popup.js +274 -0
- package/dist/ui/popup.js.map +1 -0
- package/dist/ui/prompt.d.ts +21 -0
- package/dist/ui/prompt.d.ts.map +1 -0
- package/dist/ui/prompt.js +128 -0
- package/dist/ui/prompt.js.map +1 -0
- package/dist/ui/provider_connect.d.ts +33 -0
- package/dist/ui/provider_connect.d.ts.map +1 -0
- package/dist/ui/provider_connect.js +175 -0
- package/dist/ui/provider_connect.js.map +1 -0
- package/dist/ui/renderer.d.ts +19 -0
- package/dist/ui/renderer.d.ts.map +1 -0
- package/dist/ui/renderer.js +147 -0
- package/dist/ui/renderer.js.map +1 -0
- package/dist/ui/session_screen.d.ts +32 -0
- package/dist/ui/session_screen.d.ts.map +1 -0
- package/dist/ui/session_screen.js +148 -0
- package/dist/ui/session_screen.js.map +1 -0
- package/dist/ui/session_terminal.d.ts +46 -0
- package/dist/ui/session_terminal.d.ts.map +1 -0
- package/dist/ui/session_terminal.js +342 -0
- package/dist/ui/session_terminal.js.map +1 -0
- package/dist/ui/terminal_approval.d.ts +14 -0
- package/dist/ui/terminal_approval.d.ts.map +1 -0
- package/dist/ui/terminal_approval.js +98 -0
- package/dist/ui/terminal_approval.js.map +1 -0
- package/dist/ui/welcome.d.ts +130 -0
- package/dist/ui/welcome.d.ts.map +1 -0
- package/dist/ui/welcome.js +990 -0
- package/dist/ui/welcome.js.map +1 -0
- package/dist/vendor/contracts/approvals.d.ts +91 -0
- package/dist/vendor/contracts/approvals.d.ts.map +1 -0
- package/dist/vendor/contracts/approvals.js +22 -0
- package/dist/vendor/contracts/approvals.js.map +1 -0
- package/dist/vendor/contracts/events.d.ts +1215 -0
- package/dist/vendor/contracts/events.d.ts.map +1 -0
- package/dist/vendor/contracts/events.js +94 -0
- package/dist/vendor/contracts/events.js.map +1 -0
- package/dist/vendor/contracts/index.d.ts +7 -0
- package/dist/vendor/contracts/index.d.ts.map +1 -0
- package/dist/vendor/contracts/index.js +7 -0
- package/dist/vendor/contracts/index.js.map +1 -0
- package/dist/vendor/contracts/messages.d.ts +181 -0
- package/dist/vendor/contracts/messages.d.ts.map +1 -0
- package/dist/vendor/contracts/messages.js +40 -0
- package/dist/vendor/contracts/messages.js.map +1 -0
- package/dist/vendor/contracts/models.d.ts +117 -0
- package/dist/vendor/contracts/models.d.ts.map +1 -0
- package/dist/vendor/contracts/models.js +37 -0
- package/dist/vendor/contracts/models.js.map +1 -0
- package/dist/vendor/contracts/provider.d.ts +121 -0
- package/dist/vendor/contracts/provider.d.ts.map +1 -0
- package/dist/vendor/contracts/provider.js +64 -0
- package/dist/vendor/contracts/provider.js.map +1 -0
- package/dist/vendor/contracts/tools.d.ts +305 -0
- package/dist/vendor/contracts/tools.d.ts.map +1 -0
- package/dist/vendor/contracts/tools.js +65 -0
- package/dist/vendor/contracts/tools.js.map +1 -0
- package/dist/vendor/core/agent.d.ts +39 -0
- package/dist/vendor/core/agent.d.ts.map +1 -0
- package/dist/vendor/core/agent.js +710 -0
- package/dist/vendor/core/agent.js.map +1 -0
- package/dist/vendor/core/host_event_stream.d.ts +9 -0
- package/dist/vendor/core/host_event_stream.d.ts.map +1 -0
- package/dist/vendor/core/host_event_stream.js +12 -0
- package/dist/vendor/core/host_event_stream.js.map +1 -0
- package/dist/vendor/core/index.d.ts +7 -0
- package/dist/vendor/core/index.d.ts.map +1 -0
- package/dist/vendor/core/index.js +7 -0
- package/dist/vendor/core/index.js.map +1 -0
- package/dist/vendor/core/policy.d.ts +19 -0
- package/dist/vendor/core/policy.d.ts.map +1 -0
- package/dist/vendor/core/policy.js +31 -0
- package/dist/vendor/core/policy.js.map +1 -0
- package/dist/vendor/core/router.d.ts +26 -0
- package/dist/vendor/core/router.d.ts.map +1 -0
- package/dist/vendor/core/router.js +222 -0
- package/dist/vendor/core/router.js.map +1 -0
- package/dist/vendor/core/subagent.d.ts +29 -0
- package/dist/vendor/core/subagent.d.ts.map +1 -0
- package/dist/vendor/core/subagent.js +66 -0
- package/dist/vendor/core/subagent.js.map +1 -0
- package/dist/vendor/core/think_filter.d.ts +14 -0
- package/dist/vendor/core/think_filter.d.ts.map +1 -0
- package/dist/vendor/core/think_filter.js +136 -0
- package/dist/vendor/core/think_filter.js.map +1 -0
- package/dist/vendor/providers/fake/fake_provider.d.ts +16 -0
- package/dist/vendor/providers/fake/fake_provider.d.ts.map +1 -0
- package/dist/vendor/providers/fake/fake_provider.js +89 -0
- package/dist/vendor/providers/fake/fake_provider.js.map +1 -0
- package/dist/vendor/providers/index.d.ts +6 -0
- package/dist/vendor/providers/index.d.ts.map +1 -0
- package/dist/vendor/providers/index.js +6 -0
- package/dist/vendor/providers/index.js.map +1 -0
- package/dist/vendor/providers/model_discovery.d.ts +28 -0
- package/dist/vendor/providers/model_discovery.d.ts.map +1 -0
- package/dist/vendor/providers/model_discovery.js +106 -0
- package/dist/vendor/providers/model_discovery.js.map +1 -0
- package/dist/vendor/providers/nvidia/nvidia_adapter.d.ts +23 -0
- package/dist/vendor/providers/nvidia/nvidia_adapter.d.ts.map +1 -0
- package/dist/vendor/providers/nvidia/nvidia_adapter.js +197 -0
- package/dist/vendor/providers/nvidia/nvidia_adapter.js.map +1 -0
- package/dist/vendor/providers/nvidia/sse_parser.d.ts +3 -0
- package/dist/vendor/providers/nvidia/sse_parser.d.ts.map +1 -0
- package/dist/vendor/providers/nvidia/sse_parser.js +84 -0
- package/dist/vendor/providers/nvidia/sse_parser.js.map +1 -0
- package/dist/vendor/providers/openai_compatible_adapter.d.ts +10 -0
- package/dist/vendor/providers/openai_compatible_adapter.d.ts.map +1 -0
- package/dist/vendor/providers/openai_compatible_adapter.js +8 -0
- package/dist/vendor/providers/openai_compatible_adapter.js.map +1 -0
- package/dist/vendor/tools/checkpoints.d.ts +27 -0
- package/dist/vendor/tools/checkpoints.d.ts.map +1 -0
- package/dist/vendor/tools/checkpoints.js +145 -0
- package/dist/vendor/tools/checkpoints.js.map +1 -0
- package/dist/vendor/tools/diagnostics.d.ts +11 -0
- package/dist/vendor/tools/diagnostics.d.ts.map +1 -0
- package/dist/vendor/tools/diagnostics.js +16 -0
- package/dist/vendor/tools/diagnostics.js.map +1 -0
- package/dist/vendor/tools/diff.d.ts +7 -0
- package/dist/vendor/tools/diff.d.ts.map +1 -0
- package/dist/vendor/tools/diff.js +48 -0
- package/dist/vendor/tools/diff.js.map +1 -0
- package/dist/vendor/tools/errors.d.ts +17 -0
- package/dist/vendor/tools/errors.d.ts.map +1 -0
- package/dist/vendor/tools/errors.js +33 -0
- package/dist/vendor/tools/errors.js.map +1 -0
- package/dist/vendor/tools/git_workspace.d.ts +14 -0
- package/dist/vendor/tools/git_workspace.d.ts.map +1 -0
- package/dist/vendor/tools/git_workspace.js +42 -0
- package/dist/vendor/tools/git_workspace.js.map +1 -0
- package/dist/vendor/tools/index.d.ts +22 -0
- package/dist/vendor/tools/index.d.ts.map +1 -0
- package/dist/vendor/tools/index.js +22 -0
- package/dist/vendor/tools/index.js.map +1 -0
- package/dist/vendor/tools/jail.d.ts +17 -0
- package/dist/vendor/tools/jail.d.ts.map +1 -0
- package/dist/vendor/tools/jail.js +221 -0
- package/dist/vendor/tools/jail.js.map +1 -0
- package/dist/vendor/tools/lsp.d.ts +3 -0
- package/dist/vendor/tools/lsp.d.ts.map +1 -0
- package/dist/vendor/tools/lsp.js +43 -0
- package/dist/vendor/tools/lsp.js.map +1 -0
- package/dist/vendor/tools/mcp.d.ts +9 -0
- package/dist/vendor/tools/mcp.d.ts.map +1 -0
- package/dist/vendor/tools/mcp.js +54 -0
- package/dist/vendor/tools/mcp.js.map +1 -0
- package/dist/vendor/tools/mcp_adapter.d.ts +15 -0
- package/dist/vendor/tools/mcp_adapter.d.ts.map +1 -0
- package/dist/vendor/tools/mcp_adapter.js +29 -0
- package/dist/vendor/tools/mcp_adapter.js.map +1 -0
- package/dist/vendor/tools/registry.d.ts +10 -0
- package/dist/vendor/tools/registry.d.ts.map +1 -0
- package/dist/vendor/tools/registry.js +126 -0
- package/dist/vendor/tools/registry.js.map +1 -0
- package/dist/vendor/tools/tools/apply_patch.d.ts +3 -0
- package/dist/vendor/tools/tools/apply_patch.d.ts.map +1 -0
- package/dist/vendor/tools/tools/apply_patch.js +49 -0
- package/dist/vendor/tools/tools/apply_patch.js.map +1 -0
- package/dist/vendor/tools/tools/edit_file.d.ts +3 -0
- package/dist/vendor/tools/tools/edit_file.d.ts.map +1 -0
- package/dist/vendor/tools/tools/edit_file.js +71 -0
- package/dist/vendor/tools/tools/edit_file.js.map +1 -0
- package/dist/vendor/tools/tools/git_diff.d.ts +3 -0
- package/dist/vendor/tools/tools/git_diff.d.ts.map +1 -0
- package/dist/vendor/tools/tools/git_diff.js +91 -0
- package/dist/vendor/tools/tools/git_diff.js.map +1 -0
- package/dist/vendor/tools/tools/language_intelligence.d.ts +4 -0
- package/dist/vendor/tools/tools/language_intelligence.d.ts.map +1 -0
- package/dist/vendor/tools/tools/language_intelligence.js +13 -0
- package/dist/vendor/tools/tools/language_intelligence.js.map +1 -0
- package/dist/vendor/tools/tools/list_files.d.ts +9 -0
- package/dist/vendor/tools/tools/list_files.d.ts.map +1 -0
- package/dist/vendor/tools/tools/list_files.js +165 -0
- package/dist/vendor/tools/tools/list_files.js.map +1 -0
- package/dist/vendor/tools/tools/read_file.d.ts +3 -0
- package/dist/vendor/tools/tools/read_file.d.ts.map +1 -0
- package/dist/vendor/tools/tools/read_file.js +72 -0
- package/dist/vendor/tools/tools/read_file.js.map +1 -0
- package/dist/vendor/tools/tools/run_command.d.ts +12 -0
- package/dist/vendor/tools/tools/run_command.d.ts.map +1 -0
- package/dist/vendor/tools/tools/run_command.js +421 -0
- package/dist/vendor/tools/tools/run_command.js.map +1 -0
- package/dist/vendor/tools/tools/run_diagnostics.d.ts +3 -0
- package/dist/vendor/tools/tools/run_diagnostics.d.ts.map +1 -0
- package/dist/vendor/tools/tools/run_diagnostics.js +11 -0
- package/dist/vendor/tools/tools/run_diagnostics.js.map +1 -0
- package/dist/vendor/tools/tools/search_files.d.ts +3 -0
- package/dist/vendor/tools/tools/search_files.d.ts.map +1 -0
- package/dist/vendor/tools/tools/search_files.js +120 -0
- package/dist/vendor/tools/tools/search_files.js.map +1 -0
- package/dist/vendor/tools/tools/web_search.d.ts +46 -0
- package/dist/vendor/tools/tools/web_search.d.ts.map +1 -0
- package/dist/vendor/tools/tools/web_search.js +187 -0
- package/dist/vendor/tools/tools/web_search.js.map +1 -0
- package/dist/vendor/tools/tools/write_file.d.ts +3 -0
- package/dist/vendor/tools/tools/write_file.d.ts.map +1 -0
- package/dist/vendor/tools/tools/write_file.js +52 -0
- package/dist/vendor/tools/tools/write_file.js.map +1 -0
- package/dist/windows_credentials.d.ts +15 -0
- package/dist/windows_credentials.d.ts.map +1 -0
- package/dist/windows_credentials.js +23 -0
- package/dist/windows_credentials.js.map +1 -0
- package/package.json +28 -0
|
@@ -0,0 +1,990 @@
|
|
|
1
|
+
import { overlayCentered, dimLines, shadowUnder } from './popup.js';
|
|
2
|
+
import { activeMentionToken, filterMentionCandidates } from './file_mentions.js';
|
|
3
|
+
export const MODERADO_ASCII_LOGO = [
|
|
4
|
+
'\x1b[38;5;75m __ _______ ____ __________ ___ ____ ____ \x1b[0m',
|
|
5
|
+
'\x1b[38;5;75m / |/ / __ \\/ __ \\/ ____/ __ \\/ | / __ \\/ __ \\\x1b[0m',
|
|
6
|
+
'\x1b[38;5;81m / /|_/ / / / / / / / __/ / /_/ / /| | / / / / / / /\x1b[0m',
|
|
7
|
+
'\x1b[38;5;81m / / / / /_/ / /_/ / /___/ _, _/ ___ |/ /_/ / /_/ / \x1b[0m',
|
|
8
|
+
'\x1b[38;5;87m/_/ /_/\\____/_____/_____/_/ |_/_/ |_/_____/\\____/ \x1b[0m',
|
|
9
|
+
];
|
|
10
|
+
export const COMMAND_HINT = '\x1b[38;5;242mUse \x1b[38;5;75m/\x1b[38;5;242m for slash commands, \x1b[38;5;75m@\x1b[38;5;242m for file mentions\x1b[0m';
|
|
11
|
+
export function stripAnsi(str) {
|
|
12
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
13
|
+
}
|
|
14
|
+
/** Visible (ANSI-stripped) length of a line. */
|
|
15
|
+
function visibleLen(str) {
|
|
16
|
+
return stripAnsi(str).length;
|
|
17
|
+
}
|
|
18
|
+
/** Card width: nearly full terminal width, like OpenCode/Cline. */
|
|
19
|
+
export function getWelcomeCardWidth(width) {
|
|
20
|
+
const terminalWidth = width ?? (process.stdout.columns || 80);
|
|
21
|
+
return Math.max(70, Math.min(terminalWidth - 2, 120));
|
|
22
|
+
}
|
|
23
|
+
/** Left indent that centers a block of `blockWidth` visible columns on the terminal. */
|
|
24
|
+
export function getWelcomeIndent(blockWidth, width) {
|
|
25
|
+
const terminalWidth = width ?? (process.stdout.columns || 80);
|
|
26
|
+
return Math.max(0, Math.floor((terminalWidth - blockWidth) / 2));
|
|
27
|
+
}
|
|
28
|
+
/** Center each line of a block independently (used for the ASCII logo / hints). */
|
|
29
|
+
function centerBlock(lines, width) {
|
|
30
|
+
return lines.map((line) => {
|
|
31
|
+
const pad = getWelcomeIndent(visibleLen(line), width);
|
|
32
|
+
return ' '.repeat(pad) + line;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function renderQueuedCommandsBox(commands, width) {
|
|
36
|
+
if (!commands || commands.length === 0)
|
|
37
|
+
return [];
|
|
38
|
+
const title = `Queued Commands (${commands.length})`;
|
|
39
|
+
const remainingDashes = Math.max(0, width - title.length - 5);
|
|
40
|
+
const lines = [];
|
|
41
|
+
lines.push('\x1b[38;5;240m╭─ \x1b[1;38;5;221m' + title + '\x1b[0;38;5;240m ' + '─'.repeat(remainingDashes) + '╮\x1b[0m');
|
|
42
|
+
const maxItems = Math.min(commands.length, 4);
|
|
43
|
+
for (let i = 0; i < maxItems; i++) {
|
|
44
|
+
const cmd = commands[i];
|
|
45
|
+
const itemPrefix = `${i + 1}. `;
|
|
46
|
+
const availCmdWidth = Math.max(20, width - 4 - itemPrefix.length);
|
|
47
|
+
const shortCmd = cmd.length > availCmdWidth ? cmd.slice(0, availCmdWidth - 3) + '...' : cmd;
|
|
48
|
+
const content = `\x1b[38;5;221m${itemPrefix}\x1b[0m\x1b[38;5;252m${shortCmd}\x1b[0m`;
|
|
49
|
+
const plainLen = itemPrefix.length + shortCmd.length;
|
|
50
|
+
const padding = Math.max(0, width - 4 - plainLen);
|
|
51
|
+
lines.push(`\x1b[38;5;240m│\x1b[0m ${content}${' '.repeat(padding)}\x1b[38;5;240m│\x1b[0m`);
|
|
52
|
+
}
|
|
53
|
+
if (commands.length > 4) {
|
|
54
|
+
const moreText = `(+${commands.length - 4} more - type /queue to inspect)`;
|
|
55
|
+
const padding = Math.max(0, width - 4 - moreText.length);
|
|
56
|
+
lines.push(`\x1b[38;5;240m│\x1b[0m \x1b[38;5;244m${moreText}\x1b[0m${' '.repeat(padding)}\x1b[38;5;240m│\x1b[0m`);
|
|
57
|
+
}
|
|
58
|
+
lines.push('\x1b[38;5;240m╰' + '─'.repeat(Math.max(0, width - 2)) + '╯\x1b[0m');
|
|
59
|
+
return lines;
|
|
60
|
+
}
|
|
61
|
+
export function renderWelcomeCard(options) {
|
|
62
|
+
const width = getWelcomeCardWidth(options.width);
|
|
63
|
+
const indent = ' '.repeat(getWelcomeIndent(width, options.width));
|
|
64
|
+
const displayInput = options.input && options.input.length > 0
|
|
65
|
+
? options.input
|
|
66
|
+
: '\x1b[38;5;242mAsk anything, I am all ears...\x1b[0m';
|
|
67
|
+
const promptMarker = '\x1b[1;38;5;75m' + String.fromCodePoint(0x276F) + '\x1b[0;48;5;236m';
|
|
68
|
+
const textBox = `${promptMarker} ${displayInput}`;
|
|
69
|
+
const surface = '\x1b[48;5;236m';
|
|
70
|
+
const surfaceLine = (content = '') => {
|
|
71
|
+
const preserved = content.replace(/\x1b\[0m/g, '\x1b[0;48;5;236m');
|
|
72
|
+
return surface + preserved + ' '.repeat(Math.max(0, width - visibleLen(content))) + '\x1b[0m';
|
|
73
|
+
};
|
|
74
|
+
// Line 4: model & tokens / cost (left) ... Plan / Execute (Tab) (right)
|
|
75
|
+
const outputRate = options.outputTokenRate === undefined ? '' : ` · ${Math.round(options.outputTokenRate)} tok/s`;
|
|
76
|
+
const usageLabel = options.usageAvailable === false ? 'Usage unavailable' : `${options.tokens} tokens`;
|
|
77
|
+
const left4Raw = `${options.model} ${usageLabel} / ${options.cost}${outputRate}`;
|
|
78
|
+
const left4 = `\x1b[38;5;180m${options.model}\x1b[0m \x1b[38;5;244m${usageLabel} / ${options.cost}${outputRate}\x1b[0m`;
|
|
79
|
+
const right4 = options.mode === 'Plan'
|
|
80
|
+
? '\x1b[1;38;5;75m[Plan]\x1b[0m / Execute (Tab)'
|
|
81
|
+
: 'Plan / \x1b[1;38;5;75m[Execute]\x1b[0m (Tab)';
|
|
82
|
+
const right4Raw = options.mode === 'Plan' ? '[Plan] / Execute (Tab)' : 'Plan / [Execute] (Tab)';
|
|
83
|
+
const spaces4Count = Math.max(1, width - left4Raw.length - right4Raw.length);
|
|
84
|
+
const line4 = left4 + ' '.repeat(spaces4Count) + right4;
|
|
85
|
+
// Line 5: Working directory (left) ... Auto-approve (right)
|
|
86
|
+
const shortWs = options.workspace.length > 36
|
|
87
|
+
? '...' + options.workspace.slice(-33)
|
|
88
|
+
: options.workspace;
|
|
89
|
+
const left5 = `\x1b[38;5;245m${shortWs}\x1b[0m`;
|
|
90
|
+
const right5 = options.autoApprove
|
|
91
|
+
? '\x1b[38;5;114mAuto-approve enabled (Shift+Tab)\x1b[0m'
|
|
92
|
+
: '\x1b[38;5;242mAuto-approve off (Shift+Tab)\x1b[0m';
|
|
93
|
+
const right5Raw = options.autoApprove
|
|
94
|
+
? 'Auto-approve enabled (Shift+Tab)'
|
|
95
|
+
: 'Auto-approve off (Shift+Tab)';
|
|
96
|
+
const spaces5Count = Math.max(1, width - shortWs.length - right5Raw.length);
|
|
97
|
+
const line5 = left5 + ' '.repeat(spaces5Count) + right5;
|
|
98
|
+
const cardLines = [];
|
|
99
|
+
const queuedBox = renderQueuedCommandsBox(options.queuedCommands ?? [], width);
|
|
100
|
+
if (queuedBox.length > 0) {
|
|
101
|
+
cardLines.push(...queuedBox.map((line) => indent + line));
|
|
102
|
+
cardLines.push('');
|
|
103
|
+
}
|
|
104
|
+
cardLines.push(indent + surfaceLine(), indent + surfaceLine(textBox), indent + surfaceLine(), indent + line4, indent + line5);
|
|
105
|
+
if (options.mentionFiles && options.mentionFiles.length > 0) {
|
|
106
|
+
cardLines.push(...renderMentionSuggestionsBox(options.mentionFiles, options.mentionSelection ?? 0).map((line) => indent + line));
|
|
107
|
+
}
|
|
108
|
+
else if (options.input && options.input.startsWith('/')) {
|
|
109
|
+
const matching = getMatchingCommands(options.input);
|
|
110
|
+
if (matching.length > 0) {
|
|
111
|
+
cardLines.push(...renderSuggestionsBox(matching, width).map((line) => indent + line));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return cardLines.join('\n');
|
|
115
|
+
}
|
|
116
|
+
export const SLASH_COMMANDS = [
|
|
117
|
+
{ name: '/connect', desc: 'Connect a model provider' },
|
|
118
|
+
{ name: '/model', desc: 'Switch active AI model' },
|
|
119
|
+
{ name: '/init', desc: 'Scaffold AGENTS.md from workspace scan' },
|
|
120
|
+
{ name: '/btw', desc: 'Ask an ephemeral side question (no session pollution)' },
|
|
121
|
+
{ name: '/mcp', desc: 'Manage local MCP servers' },
|
|
122
|
+
{ name: '/session', desc: 'Create, resume, undo, redo, share, export, or compact sessions' },
|
|
123
|
+
{ name: '/queue', desc: 'Add, inspect, or clear queued follow-up commands' },
|
|
124
|
+
{ name: '/workflow', desc: 'Inspect Git, build plans, or undo agent changes' },
|
|
125
|
+
{ name: '/clear', desc: 'Reset conversation memory' },
|
|
126
|
+
{ name: '/help', desc: 'Display commands, shortcuts & version' },
|
|
127
|
+
{ name: '/exit', desc: 'Exit Moderado' },
|
|
128
|
+
];
|
|
129
|
+
export function getMatchingCommands(input) {
|
|
130
|
+
if (!input.startsWith('/'))
|
|
131
|
+
return [];
|
|
132
|
+
const q = input.toLowerCase();
|
|
133
|
+
return SLASH_COMMANDS.filter((cmd) => cmd.name.startsWith(q));
|
|
134
|
+
}
|
|
135
|
+
export function selectCommandCandidate(input, currentIndex, direction) {
|
|
136
|
+
const commands = getMatchingCommands(input);
|
|
137
|
+
if (commands.length === 0)
|
|
138
|
+
return undefined;
|
|
139
|
+
const index = ((currentIndex + direction) % commands.length + commands.length) % commands.length;
|
|
140
|
+
return commands[index];
|
|
141
|
+
}
|
|
142
|
+
export function renderSuggestionsBox(commands, width) {
|
|
143
|
+
if (commands.length === 0)
|
|
144
|
+
return [];
|
|
145
|
+
const terminalWidth = width ?? (process.stdout?.columns || 80);
|
|
146
|
+
const targetWidth = 82;
|
|
147
|
+
const boxWidth = Math.max(78, Math.min(terminalWidth, targetWidth));
|
|
148
|
+
const innerW = boxWidth - 4;
|
|
149
|
+
const title = 'Commands (Press Tab to autocomplete)';
|
|
150
|
+
const remainingDashes = Math.max(0, boxWidth - title.length - 5);
|
|
151
|
+
const nameColWidth = 10;
|
|
152
|
+
const availDescWidth = Math.max(20, innerW - nameColWidth - 2);
|
|
153
|
+
const lines = [];
|
|
154
|
+
lines.push('\x1b[38;5;240m╭─ \x1b[1;38;5;75m' + title + '\x1b[0;38;5;240m ' + '─'.repeat(remainingDashes) + '╮\x1b[0m');
|
|
155
|
+
for (const cmd of commands) {
|
|
156
|
+
const nameStr = `\x1b[1;38;5;75m${cmd.name.padEnd(nameColWidth)}\x1b[0m`;
|
|
157
|
+
let desc = cmd.desc;
|
|
158
|
+
if (desc.length > availDescWidth) {
|
|
159
|
+
desc = desc.slice(0, availDescWidth - 1) + '…';
|
|
160
|
+
}
|
|
161
|
+
const descStr = `\x1b[38;5;244m${desc.padEnd(availDescWidth)}\x1b[0m`;
|
|
162
|
+
lines.push(`\x1b[38;5;240m│\x1b[0m ${nameStr} ${descStr}\x1b[38;5;240m│\x1b[0m`);
|
|
163
|
+
}
|
|
164
|
+
lines.push('\x1b[38;5;240m╰' + '─'.repeat(boxWidth - 2) + '╯\x1b[0m');
|
|
165
|
+
return lines;
|
|
166
|
+
}
|
|
167
|
+
export function renderMentionSuggestionsBox(files, selectedIndex = 0) {
|
|
168
|
+
if (files.length === 0)
|
|
169
|
+
return [];
|
|
170
|
+
const lines = [];
|
|
171
|
+
lines.push('\x1b[38;5;240m ╭─ Files (@ to mention, Tab to insert) ──────────────╮\x1b[0m');
|
|
172
|
+
for (let i = 0; i < files.length; i++) {
|
|
173
|
+
const file = files[i];
|
|
174
|
+
const isSelected = i === selectedIndex;
|
|
175
|
+
const marker = isSelected ? '\x1b[1;38;5;75m❯ \x1b[0m' : ' ';
|
|
176
|
+
const nameStr = isSelected ? `\x1b[1;38;5;75m@${file}\x1b[0m` : `\x1b[38;5;250m@${file}\x1b[0m`;
|
|
177
|
+
const lineContent = `${marker}${nameStr}`;
|
|
178
|
+
const plainLen = visibleLen(lineContent);
|
|
179
|
+
const pad = Math.max(0, 52 - plainLen);
|
|
180
|
+
lines.push(`\x1b[38;5;240m │\x1b[0m ${lineContent}${' '.repeat(pad)}\x1b[38;5;240m│\x1b[0m`);
|
|
181
|
+
}
|
|
182
|
+
lines.push('\x1b[38;5;240m ╰─────────────────────────────────────────────────────╯\x1b[0m');
|
|
183
|
+
return lines;
|
|
184
|
+
}
|
|
185
|
+
export function renderModeradoHeader() {
|
|
186
|
+
return [
|
|
187
|
+
'',
|
|
188
|
+
...centerBlock(MODERADO_ASCII_LOGO),
|
|
189
|
+
'',
|
|
190
|
+
...centerBlock([COMMAND_HINT]),
|
|
191
|
+
].join('\n');
|
|
192
|
+
}
|
|
193
|
+
/** Word-wrap plain text to `maxWidth` visible columns. */
|
|
194
|
+
export function wrapText(text, maxWidth) {
|
|
195
|
+
const out = [];
|
|
196
|
+
for (const rawLine of text.split('\n')) {
|
|
197
|
+
if (rawLine.length <= maxWidth) {
|
|
198
|
+
out.push(rawLine);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
let current = '';
|
|
202
|
+
for (const word of rawLine.split(' ')) {
|
|
203
|
+
if (current.length === 0) {
|
|
204
|
+
current = word;
|
|
205
|
+
}
|
|
206
|
+
else if (current.length + 1 + word.length <= maxWidth) {
|
|
207
|
+
current += ' ' + word;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
out.push(current);
|
|
211
|
+
current = word;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (current.length > 0)
|
|
215
|
+
out.push(current);
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Full chat window (after a question is submitted): no logo — the question is
|
|
221
|
+
* displayed on the first row, the multi-line model answer below it, then the
|
|
222
|
+
* same input card as the welcome window at the bottom.
|
|
223
|
+
*/
|
|
224
|
+
export function renderChatScreen(options, height) {
|
|
225
|
+
const terminalWidth = options.width ?? (process.stdout.columns || 80);
|
|
226
|
+
const terminalHeight = height ?? process.stdout.rows ?? 24;
|
|
227
|
+
const maxWidth = Math.max(40, terminalWidth - 4);
|
|
228
|
+
const width = Math.max(40, terminalWidth - 2);
|
|
229
|
+
const lines = [];
|
|
230
|
+
lines.push(...renderModeradoHeader().split('\n'));
|
|
231
|
+
lines.push('');
|
|
232
|
+
// Row 1: the user's question
|
|
233
|
+
const questionMarker = '\x1b[1;38;5;75m' + String.fromCodePoint(0x276F) + '\x1b[0;48;5;236m';
|
|
234
|
+
const questionText = `${questionMarker} ${options.chatQuestion ?? ''}`;
|
|
235
|
+
lines.push(String.fromCharCode(27) + '[48;5;236m' + questionText + ' '.repeat(Math.max(0, width - visibleLen(questionText))) + String.fromCharCode(27) + '[0m');
|
|
236
|
+
const thoughtTime = options.chatThoughtTime ?? 0;
|
|
237
|
+
const thoughtTimeLabel = thoughtTime > 0 && thoughtTime < 1 ? '<1s' : `${Math.round(thoughtTime)}s`;
|
|
238
|
+
lines.push(options.chatAnswer?.trim() ? `\x1b[38;5;244mThought for ${thoughtTimeLabel}\x1b[0m` : '');
|
|
239
|
+
// Answer section: multi-line model answer
|
|
240
|
+
if (options.chatAnswer && options.chatAnswer.trim().length > 0) {
|
|
241
|
+
lines.push('');
|
|
242
|
+
for (const answerLine of wrapText(options.chatAnswer.trim(), maxWidth)) {
|
|
243
|
+
lines.push(`\x1b[38;5;253m${answerLine}\x1b[0m`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const composer = renderWelcomeCard(options).split('\n');
|
|
247
|
+
lines.push(...Array(Math.max(1, terminalHeight - lines.length - composer.length - 1)).fill(''));
|
|
248
|
+
lines.push(...composer);
|
|
249
|
+
return lines.join('\n') + '\n';
|
|
250
|
+
}
|
|
251
|
+
/** Place the terminal cursor on the first row of the bottom composer. */
|
|
252
|
+
export function renderChatComposerCursor(options, inputLength) {
|
|
253
|
+
const column = getWelcomeIndent(getWelcomeCardWidth(options.width), options.width) + 2 + inputLength;
|
|
254
|
+
return `\x1b[1 q\x1b[?25h\x1b[4A\r\x1b[${column}C`;
|
|
255
|
+
}
|
|
256
|
+
export function navigateQuestionHistory(history, direction, index, draft) {
|
|
257
|
+
const nextIndex = Math.max(0, Math.min(history.length, index + direction));
|
|
258
|
+
return { input: nextIndex === history.length ? draft : history[nextIndex], index: nextIndex, draft };
|
|
259
|
+
}
|
|
260
|
+
/** Update the fixed chat elapsed-time row without repainting the terminal. */
|
|
261
|
+
export function renderChatThoughtTimeUpdate(thoughtTime) {
|
|
262
|
+
const label = thoughtTime > 0 && thoughtTime < 1 ? '<1s' : `${Math.round(thoughtTime)}s`;
|
|
263
|
+
return `\x1b7\x1b[13;1H\r\x1b[K\x1b[38;5;244mThought for ${label}\x1b[0m\x1b8`;
|
|
264
|
+
}
|
|
265
|
+
/** Write one streamed fragment into the answer area without repainting the chat frame. */
|
|
266
|
+
export function renderChatAnswerDelta(delta, start, width) {
|
|
267
|
+
let row = start.row;
|
|
268
|
+
let column = start.column;
|
|
269
|
+
for (const character of delta) {
|
|
270
|
+
if (character === '\n') {
|
|
271
|
+
row++;
|
|
272
|
+
column = 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
column++;
|
|
276
|
+
if (column > width) {
|
|
277
|
+
row++;
|
|
278
|
+
column = 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return { sequence: `\x1b[${start.row};${start.column}H\x1b[38;5;253m${delta}\x1b[0m`, row, column };
|
|
282
|
+
}
|
|
283
|
+
export function renderFullWelcomeScreen(options, height) {
|
|
284
|
+
if (options.chatQuestion !== undefined) {
|
|
285
|
+
return renderChatScreen(options, height);
|
|
286
|
+
}
|
|
287
|
+
return [
|
|
288
|
+
renderModeradoHeader(),
|
|
289
|
+
'',
|
|
290
|
+
renderWelcomeCard(options),
|
|
291
|
+
].join('\n') + '\n';
|
|
292
|
+
}
|
|
293
|
+
/** Keep the initial welcome screen centered as the terminal viewport changes. */
|
|
294
|
+
export function getWelcomeBottomPadding(options, height) {
|
|
295
|
+
if (options.chatQuestion !== undefined)
|
|
296
|
+
return 0;
|
|
297
|
+
const terminalHeight = height ?? process.stdout.rows ?? 24;
|
|
298
|
+
const contentLines = renderFullWelcomeScreen(options).trimEnd().split('\n').length;
|
|
299
|
+
return Math.max(0, Math.ceil((terminalHeight - contentLines) / 2));
|
|
300
|
+
}
|
|
301
|
+
export function renderCenteredWelcomeScreen(options, height) {
|
|
302
|
+
if (options.chatQuestion !== undefined)
|
|
303
|
+
return renderFullWelcomeScreen(options, height);
|
|
304
|
+
const terminalHeight = height ?? process.stdout.rows ?? 24;
|
|
305
|
+
const content = renderFullWelcomeScreen(options).trimEnd();
|
|
306
|
+
const contentLines = content.split('\n').length;
|
|
307
|
+
const remainingRows = Math.max(0, terminalHeight - contentLines);
|
|
308
|
+
const topPadding = Math.floor(remainingRows / 2);
|
|
309
|
+
const bottomPadding = Math.ceil(remainingRows / 2);
|
|
310
|
+
return '\n'.repeat(topPadding) + content + '\n'.repeat(bottomPadding + 1);
|
|
311
|
+
}
|
|
312
|
+
/** Compose an OpenCode-style popup layer without losing the welcome background. */
|
|
313
|
+
export function renderWelcomePopupLayer(options, popupLines, width, height) {
|
|
314
|
+
const cols = width ?? process.stdout.columns ?? 80;
|
|
315
|
+
const rows = height ?? process.stdout.rows ?? 24;
|
|
316
|
+
const background = dimLines(renderCenteredWelcomeScreen(options, rows).split('\n')).join('\n');
|
|
317
|
+
const surface = '\x1b[48;5;236m';
|
|
318
|
+
const lightPopup = popupLines.map((line) => surface + line.replace(/\x1b\[0m/g, `\x1b[0m${surface}`) + '\x1b[0m');
|
|
319
|
+
return background + shadowUnder(lightPopup, cols, rows) + overlayCentered(lightPopup, cols, rows);
|
|
320
|
+
}
|
|
321
|
+
export function renderHelpPopupBox(version, workspace, width) {
|
|
322
|
+
const terminalWidth = width ?? (process.stdout.columns || 80);
|
|
323
|
+
const targetWidth = 82;
|
|
324
|
+
const boxWidth = Math.max(76, Math.min(terminalWidth, targetWidth));
|
|
325
|
+
const innerW = boxWidth - 4;
|
|
326
|
+
const shortWs = workspace.length > 38 ? '...' + workspace.slice(-35) : workspace;
|
|
327
|
+
const titleStr = 'Moderado Help & Shortcuts';
|
|
328
|
+
const remainingDashes = Math.max(0, boxWidth - titleStr.length - 5);
|
|
329
|
+
const content = [
|
|
330
|
+
'\x1b[1;38;5;75mSlash Commands:\x1b[0m',
|
|
331
|
+
'\x1b[1m/init\x1b[0m Scaffold AGENTS.md from workspace scan',
|
|
332
|
+
'\x1b[1m/model\x1b[0m Switch active AI model (Free, Paid, or Custom)',
|
|
333
|
+
'\x1b[1m/connect\x1b[0m Connect NVIDIA NIM or another compatible provider',
|
|
334
|
+
'\x1b[1m/btw\x1b[0m Ask an ephemeral side question (no session pollution)',
|
|
335
|
+
'\x1b[1m/mcp\x1b[0m Manage local MCP servers',
|
|
336
|
+
'\x1b[1m/session\x1b[0m Create, resume, undo, redo, share, export, or compact sessions',
|
|
337
|
+
'\x1b[1m/queue\x1b[0m Add, inspect, or clear queued follow-up commands',
|
|
338
|
+
'\x1b[1m/workflow\x1b[0m Inspect Git, build plans, or undo agent changes',
|
|
339
|
+
'\x1b[1m/clear\x1b[0m Reset conversation memory and context history',
|
|
340
|
+
'\x1b[1m/help\x1b[0m Display this commands, shortcuts & version guide',
|
|
341
|
+
'\x1b[1m/exit\x1b[0m Exit Moderado session cleanly',
|
|
342
|
+
'',
|
|
343
|
+
'\x1b[1;38;5;114mKeyboard Shortcuts:\x1b[0m',
|
|
344
|
+
'\x1b[1mTab\x1b[0m Toggle between [Plan] and [Execute] mode',
|
|
345
|
+
'\x1b[1mShift+Tab\x1b[0m Toggle Auto-approval on / off for actions',
|
|
346
|
+
'\x1b[1mCtrl+C\x1b[0m Cancel active inference or exit session',
|
|
347
|
+
'\x1b[1mLeft/Right\x1b[0m Move the caret inside the bottom input line',
|
|
348
|
+
'\x1b[1mMouse click\x1b[0m Place the caret on the input line',
|
|
349
|
+
'',
|
|
350
|
+
`\x1b[38;5;245mWorkspace:\x1b[0m \x1b[38;5;253m${shortWs}\x1b[0m`,
|
|
351
|
+
`\x1b[38;5;245mVersion:\x1b[0m \x1b[1;38;5;75m${version}\x1b[0m`,
|
|
352
|
+
'',
|
|
353
|
+
'\x1b[38;5;244mPress \x1b[1;38;5;75m[Esc]\x1b[0;38;5;244m or \x1b[1;38;5;75m[Enter]\x1b[0;38;5;244m to close\x1b[0m',
|
|
354
|
+
];
|
|
355
|
+
const lines = [];
|
|
356
|
+
lines.push('\x1b[38;5;240m╭─ \x1b[1;38;5;75m' + titleStr + '\x1b[0;38;5;240m ' + '─'.repeat(remainingDashes) + '╮\x1b[0m');
|
|
357
|
+
for (const item of content) {
|
|
358
|
+
const plain = item.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
359
|
+
let displayItem = item;
|
|
360
|
+
if (plain.length > innerW) {
|
|
361
|
+
displayItem = plain.slice(0, innerW - 1) + '…';
|
|
362
|
+
}
|
|
363
|
+
const displayPlain = displayItem.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
364
|
+
const spaces = Math.max(0, innerW - displayPlain.length);
|
|
365
|
+
lines.push('\x1b[38;5;240m│\x1b[0m ' + displayItem + ' '.repeat(spaces) + '\x1b[38;5;240m│\x1b[0m');
|
|
366
|
+
}
|
|
367
|
+
lines.push('\x1b[38;5;240m╰' + '─'.repeat(boxWidth - 2) + '╯\x1b[0m');
|
|
368
|
+
return lines;
|
|
369
|
+
}
|
|
370
|
+
export let terminalCleanExitDone = false;
|
|
371
|
+
/** Mark that a clean exit already restored + cleared the terminal (guards the process 'exit' hook). */
|
|
372
|
+
export function notifyCleanExit() {
|
|
373
|
+
terminalCleanExitDone = true;
|
|
374
|
+
}
|
|
375
|
+
export function renderExitMessage(message) {
|
|
376
|
+
return `${MODERADO_ASCII_LOGO.join('\n')}\n\n${message}\n\n`;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Exit Moderado cleanly: restore cursor visibility & style, leave the alternate
|
|
380
|
+
* screen buffer, disable raw mode, fully clear the OS terminal, print the
|
|
381
|
+
* farewell message, and terminate the process.
|
|
382
|
+
*/
|
|
383
|
+
export function exitCleanly(message) {
|
|
384
|
+
notifyCleanExit();
|
|
385
|
+
const stdout = process.stdout;
|
|
386
|
+
if (stdout.isTTY) {
|
|
387
|
+
stdout.write(MOUSE_REPORT_OFF + '\x1b[?25h\x1b[?1049l\x1b[0 q\x1b[0m\x1b[2J\x1b[3J\x1b[H' + renderExitMessage(message));
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
stdout.write(renderExitMessage(message));
|
|
391
|
+
}
|
|
392
|
+
const stdin = process.stdin;
|
|
393
|
+
if (stdin.isTTY) {
|
|
394
|
+
try {
|
|
395
|
+
stdin.setRawMode(false);
|
|
396
|
+
}
|
|
397
|
+
catch { /* ignore */ }
|
|
398
|
+
}
|
|
399
|
+
process.exit(0);
|
|
400
|
+
}
|
|
401
|
+
function isMcpCommandInput(value) {
|
|
402
|
+
const command = value.trim();
|
|
403
|
+
return command === '/mcp' || command.startsWith('/mcp ');
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* SGR mouse report, e.g. "\x1b[<0;12;5M" for a primary-button press on column 12
|
|
407
|
+
* of row 5. readline splits that escape into several keypress events ("0", ";",
|
|
408
|
+
* "5", "M"), so the raw data chunk is parsed instead of the derived keystrokes.
|
|
409
|
+
*/
|
|
410
|
+
const MOUSE_REPORT_PATTERN = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
411
|
+
const MOUSE_REPORT_ON = '\x1b[?1000h\x1b[?1006h';
|
|
412
|
+
const MOUSE_REPORT_OFF = '\x1b[?1000l\x1b[?1006l';
|
|
413
|
+
export async function promptInteractiveTurn(options) {
|
|
414
|
+
const stdin = process.stdin;
|
|
415
|
+
const stdout = process.stdout;
|
|
416
|
+
let currentModel = options.model;
|
|
417
|
+
let currentMode = options.initialMode ?? 'Execute';
|
|
418
|
+
let currentAutoApprove = options.initialAutoApprove ?? false;
|
|
419
|
+
let input = '';
|
|
420
|
+
/** Caret index inside `input` (0 .. input.length) - the composer is editable. */
|
|
421
|
+
let caret = 0;
|
|
422
|
+
/** True while keypress events derived from a mouse report are being dropped. */
|
|
423
|
+
let ignoreComposerKeypress = false;
|
|
424
|
+
/** Terminal height, with the same fallback the renderer uses. */
|
|
425
|
+
const getRows = () => stdout.rows || 24;
|
|
426
|
+
let commandSelection = 0;
|
|
427
|
+
const questionHistory = options.questionHistory ?? [];
|
|
428
|
+
let questionHistoryIndex = questionHistory.length;
|
|
429
|
+
let questionHistoryDraft = '';
|
|
430
|
+
let cachedFiles = [];
|
|
431
|
+
if (options.onMentionComplete) {
|
|
432
|
+
Promise.resolve(options.onMentionComplete('')).then((files) => {
|
|
433
|
+
cachedFiles = Array.isArray(files) ? files : [];
|
|
434
|
+
updateMentions();
|
|
435
|
+
}).catch(() => { cachedFiles = []; });
|
|
436
|
+
}
|
|
437
|
+
let mentionFiles = [];
|
|
438
|
+
let mentionSelection = 0;
|
|
439
|
+
const updateMentions = () => {
|
|
440
|
+
const mention = activeMentionToken(input, caret);
|
|
441
|
+
if (mention && cachedFiles.length > 0) {
|
|
442
|
+
mentionFiles = filterMentionCandidates(cachedFiles, mention.token, 5);
|
|
443
|
+
mentionSelection = Math.min(mentionSelection, Math.max(0, mentionFiles.length - 1));
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
mentionFiles = [];
|
|
447
|
+
mentionSelection = 0;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
/** Replace the composer text, leaving the caret at the end of the new value. */
|
|
451
|
+
const setInput = (value) => {
|
|
452
|
+
input = value;
|
|
453
|
+
caret = value.length;
|
|
454
|
+
};
|
|
455
|
+
const getOptions = () => ({
|
|
456
|
+
model: currentModel,
|
|
457
|
+
tokens: options.tokens,
|
|
458
|
+
cost: options.cost,
|
|
459
|
+
usageAvailable: options.usageAvailable,
|
|
460
|
+
workspace: options.workspace,
|
|
461
|
+
mode: currentMode,
|
|
462
|
+
autoApprove: currentAutoApprove,
|
|
463
|
+
input,
|
|
464
|
+
chatQuestion: options.chatQuestion,
|
|
465
|
+
chatAnswer: options.chatAnswer,
|
|
466
|
+
chatThoughtTime: options.chatThoughtTime,
|
|
467
|
+
outputTokenRate: options.outputTokenRate,
|
|
468
|
+
queuedCommands: options.queuedCommands,
|
|
469
|
+
isTurnSettled: true,
|
|
470
|
+
});
|
|
471
|
+
// ── Non-TTY fallback ──────────────────────────────────────────────────────
|
|
472
|
+
if (!stdin.isTTY) {
|
|
473
|
+
stdout.write('\x1b]0;Moderado\x07');
|
|
474
|
+
stdout.write(renderFullWelcomeScreen(getOptions()));
|
|
475
|
+
const readlineModule = await import('node:readline');
|
|
476
|
+
return new Promise((resolve) => {
|
|
477
|
+
const rl = readlineModule.createInterface({ input: stdin, output: stdout });
|
|
478
|
+
rl.question('\x1b[1;38;5;75m❯\x1b[0m ', (answer) => {
|
|
479
|
+
rl.close();
|
|
480
|
+
resolve({ text: answer.trim(), mode: currentMode, autoApprove: currentAutoApprove });
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
// ── TTY Interactive Loop ───────────────────────────────────────────────────
|
|
485
|
+
const readlineModule = await import('node:readline');
|
|
486
|
+
readlineModule.emitKeypressEvents(stdin);
|
|
487
|
+
// A previous turn's cleanup() pauses stdin — resume it so keystrokes keep
|
|
488
|
+
// flowing after an answer (the main app must never appear dead post-answer).
|
|
489
|
+
stdin.resume();
|
|
490
|
+
stdin.setRawMode(true);
|
|
491
|
+
const getExtraLines = () => {
|
|
492
|
+
if (mentionFiles.length > 0)
|
|
493
|
+
return mentionFiles.length + 2;
|
|
494
|
+
if (!input.startsWith('/'))
|
|
495
|
+
return 0;
|
|
496
|
+
const matching = getMatchingCommands(input);
|
|
497
|
+
return matching.length > 0 ? matching.length + 2 : 0;
|
|
498
|
+
};
|
|
499
|
+
/** Rows between the painted composer line and the terminal's last row. */
|
|
500
|
+
const getComposerOffset = () => getWelcomeBottomPadding(getOptions(), getRows()) + 4 + getExtraLines();
|
|
501
|
+
/** Screen row and 1-based column of the composer's first text cell. */
|
|
502
|
+
const getComposerAnchor = () => ({
|
|
503
|
+
row: getRows() - getComposerOffset(),
|
|
504
|
+
column: getWelcomeIndent(getWelcomeCardWidth()) + 3,
|
|
505
|
+
});
|
|
506
|
+
/**
|
|
507
|
+
* Move cursor to the ❯ input line (Line 2 inside the centered card).
|
|
508
|
+
* Absolute addressing (`row;column H`) works no matter where the cursor
|
|
509
|
+
* currently sits — relative "move up N" escapes the input line when
|
|
510
|
+
* pressed repeatedly, e.g. browsing history with Left/Right.
|
|
511
|
+
*/
|
|
512
|
+
const positionCursorOnInput = () => {
|
|
513
|
+
const { row, column } = getComposerAnchor();
|
|
514
|
+
stdout.write(`\x1b[1 q\x1b[?25h\x1b[${row};${column + caret}H`);
|
|
515
|
+
};
|
|
516
|
+
/** Full-screen redraw: clear everything, repaint welcome TUI, reposition cursor. */
|
|
517
|
+
const redrawFull = () => {
|
|
518
|
+
stdout.write('\x1b[H\x1b[J');
|
|
519
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
520
|
+
positionCursorOnInput();
|
|
521
|
+
};
|
|
522
|
+
/** Repaint the welcome view so it remains centered after every edit or resize. */
|
|
523
|
+
const redrawCard = () => {
|
|
524
|
+
redrawFull();
|
|
525
|
+
};
|
|
526
|
+
const onResize = () => redrawFull();
|
|
527
|
+
/**
|
|
528
|
+
* Place the caret where the user clicked on the composer line. Only a primary
|
|
529
|
+
* press is honoured; releases, drags, wheel and other buttons are ignored.
|
|
530
|
+
*/
|
|
531
|
+
const onMouseData = (chunk) => {
|
|
532
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
533
|
+
let reported = false;
|
|
534
|
+
for (const report of text.matchAll(MOUSE_REPORT_PATTERN)) {
|
|
535
|
+
reported = true;
|
|
536
|
+
if (report[4] !== 'M' || Number(report[1]) !== 0)
|
|
537
|
+
continue;
|
|
538
|
+
const { row, column } = getComposerAnchor();
|
|
539
|
+
if (Number(report[3]) !== row)
|
|
540
|
+
continue;
|
|
541
|
+
caret = Math.max(0, Math.min(input.length, Number(report[2]) - column));
|
|
542
|
+
positionCursorOnInput();
|
|
543
|
+
}
|
|
544
|
+
if (reported) {
|
|
545
|
+
// readline turns the same bytes into keypress events ("0", ";", "5", "M").
|
|
546
|
+
ignoreComposerKeypress = true;
|
|
547
|
+
setImmediate(() => { ignoreComposerKeypress = false; });
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
// Registered before readline's decoder so a report is consumed before it is
|
|
551
|
+
// mistaken for typed text.
|
|
552
|
+
stdin.prependListener('data', onMouseData);
|
|
553
|
+
stdout.on('resize', onResize);
|
|
554
|
+
// Initial paint
|
|
555
|
+
if (options.isFirstTurn) {
|
|
556
|
+
stdout.write('\x1b]0;Moderado\x07');
|
|
557
|
+
stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
// After an AI response the screen has content below — clear and repaint the full TUI.
|
|
561
|
+
stdout.write('\x1b[H\x1b[J');
|
|
562
|
+
}
|
|
563
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
564
|
+
positionCursorOnInput();
|
|
565
|
+
return new Promise((resolve) => {
|
|
566
|
+
const cleanup = () => {
|
|
567
|
+
unbindComposerInput();
|
|
568
|
+
stdin.removeListener('data', onMouseData);
|
|
569
|
+
stdout.removeListener('resize', onResize);
|
|
570
|
+
stdout.write(`${MOUSE_REPORT_OFF}\x1b[0 q\x1b[?25h`);
|
|
571
|
+
if (stdin.isTTY) {
|
|
572
|
+
try {
|
|
573
|
+
stdin.setRawMode(false);
|
|
574
|
+
}
|
|
575
|
+
catch { /* ignore */ }
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
stdin.pause();
|
|
579
|
+
}
|
|
580
|
+
catch { /* ignore */ }
|
|
581
|
+
};
|
|
582
|
+
const onAbort = () => {
|
|
583
|
+
cleanup();
|
|
584
|
+
resolve({ text: '', mode: currentMode, autoApprove: currentAutoApprove });
|
|
585
|
+
};
|
|
586
|
+
if (options.signal) {
|
|
587
|
+
options.signal.addEventListener('abort', onAbort, { once: true });
|
|
588
|
+
}
|
|
589
|
+
// ── Async keypress handler ────────────────────────────────────────────────
|
|
590
|
+
// Using a void async IIFE so we can await async operations (model selector)
|
|
591
|
+
// inside a synchronous event listener. For any async branch we first
|
|
592
|
+
// remove ourselves from stdin so no duplicate events fire during the await.
|
|
593
|
+
const onKeypress = (str, key) => {
|
|
594
|
+
void (async () => {
|
|
595
|
+
// Keystrokes decoded from a mouse report were already handled above.
|
|
596
|
+
if (ignoreComposerKeypress)
|
|
597
|
+
return;
|
|
598
|
+
if (options.signal?.aborted) {
|
|
599
|
+
cleanup();
|
|
600
|
+
resolve({ text: '', mode: currentMode, autoApprove: currentAutoApprove });
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
// ── Ctrl+C ────────────────────────────────────────────────────────────
|
|
604
|
+
if (key && key.ctrl && key.name === 'c') {
|
|
605
|
+
cleanup();
|
|
606
|
+
exitCleanly('\x1b[33mSession cancelled.\x1b[0m');
|
|
607
|
+
}
|
|
608
|
+
// ── /help overlay popup ───────────────────────────────────────────────
|
|
609
|
+
// Triggered when Enter is pressed with "/help" already in the input box.
|
|
610
|
+
// We keep the main TUI as background and layer the help box on top.
|
|
611
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim() === '/help') {
|
|
612
|
+
setInput('');
|
|
613
|
+
unbindComposerInput(); // pause main handler
|
|
614
|
+
// Paint: full welcome TUI (background) + help box on top
|
|
615
|
+
const helpLines = renderHelpPopupBox(options.version ?? 'v0.1', options.workspace);
|
|
616
|
+
stdout.write('\x1b[H\x1b[J');
|
|
617
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), helpLines, stdout.columns, stdout.rows));
|
|
618
|
+
// Wait for Esc / Enter / q to dismiss
|
|
619
|
+
await new Promise((dismissResolve) => {
|
|
620
|
+
const onDismiss = (s, k) => {
|
|
621
|
+
if ((k && (k.name === 'escape' || k.name === 'return' || k.name === 'enter')) ||
|
|
622
|
+
s === 'q' || s === 'Q' ||
|
|
623
|
+
(k && k.ctrl && k.name === 'c')) {
|
|
624
|
+
stdin.removeListener('keypress', onDismiss);
|
|
625
|
+
dismissResolve();
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
stdin.on('keypress', onDismiss);
|
|
629
|
+
});
|
|
630
|
+
// Restore: repaint the full welcome TUI, show cursor, position on input line
|
|
631
|
+
stdout.write('\x1b[H\x1b[J');
|
|
632
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
633
|
+
positionCursorOnInput();
|
|
634
|
+
bindComposerInput(); // re-attach main handler
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
// ── /model overlay popup ──────────────────────────────────────────────
|
|
638
|
+
// Triggered when Enter is pressed with "/model" in the input box.
|
|
639
|
+
// We temporarily detach the main keypress listener and hand the model
|
|
640
|
+
// selector a drawFrame callback. drawFrame repaints the background TUI
|
|
641
|
+
// exactly as-is and composites the popup content as a centered floating
|
|
642
|
+
// layer on top of it (Cline/OpenCode style) — the popup never scrolls
|
|
643
|
+
// the background because every step redraws the whole frame.
|
|
644
|
+
// After it returns, we repaint the full welcome TUI and reposition cursor.
|
|
645
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim() === '/model') {
|
|
646
|
+
setInput('');
|
|
647
|
+
unbindComposerInput(); // pause main handler
|
|
648
|
+
if (options.onModelSelect) {
|
|
649
|
+
const drawFrame = (popupLines) => {
|
|
650
|
+
stdout.write('\x1b[H\x1b[J');
|
|
651
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows));
|
|
652
|
+
};
|
|
653
|
+
const newId = await options.onModelSelect(drawFrame);
|
|
654
|
+
if (newId && newId !== currentModel) {
|
|
655
|
+
currentModel = newId;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Selection flow left stdin in non-raw mode — restore it for our TUI
|
|
659
|
+
readlineModule.emitKeypressEvents(stdin);
|
|
660
|
+
stdin.setRawMode(true);
|
|
661
|
+
stdout.write('\x1b[H\x1b[J');
|
|
662
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
663
|
+
positionCursorOnInput();
|
|
664
|
+
bindComposerInput(); // re-attach main handler
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim() === '/connect') {
|
|
668
|
+
setInput('');
|
|
669
|
+
unbindComposerInput();
|
|
670
|
+
const drawFrame = (popupLines) => {
|
|
671
|
+
stdout.write('\x1b[H\x1b[J');
|
|
672
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows));
|
|
673
|
+
};
|
|
674
|
+
if (options.onConnect) {
|
|
675
|
+
const newModel = await options.onConnect(drawFrame);
|
|
676
|
+
if (newModel)
|
|
677
|
+
currentModel = newModel;
|
|
678
|
+
}
|
|
679
|
+
readlineModule.emitKeypressEvents(stdin);
|
|
680
|
+
stdin.resume();
|
|
681
|
+
stdin.setRawMode(true);
|
|
682
|
+
stdout.write('\x1b[H\x1b[J');
|
|
683
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
684
|
+
positionCursorOnInput();
|
|
685
|
+
bindComposerInput();
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim().startsWith('/session')) {
|
|
689
|
+
const command = input.trim();
|
|
690
|
+
setInput('');
|
|
691
|
+
unbindComposerInput();
|
|
692
|
+
if (options.onSession) {
|
|
693
|
+
await options.onSession(command, (popupLines) => {
|
|
694
|
+
stdout.write('\x1b[H\x1b[J');
|
|
695
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows));
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
readlineModule.emitKeypressEvents(stdin);
|
|
699
|
+
stdin.resume();
|
|
700
|
+
stdin.setRawMode(true);
|
|
701
|
+
stdout.write('\x1b[H\x1b[J');
|
|
702
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
703
|
+
positionCursorOnInput();
|
|
704
|
+
bindComposerInput();
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim().startsWith('/queue')) {
|
|
708
|
+
const command = input.trim();
|
|
709
|
+
setInput('');
|
|
710
|
+
unbindComposerInput();
|
|
711
|
+
if (options.onQueue) {
|
|
712
|
+
await options.onQueue(command, async (popupLines) => {
|
|
713
|
+
stdout.write('\x1b[H\x1b[J');
|
|
714
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows));
|
|
715
|
+
await new Promise((dismissResolve) => {
|
|
716
|
+
const onDismiss = (s, k) => {
|
|
717
|
+
if ((k && (k.name === 'escape' || k.name === 'return' || k.name === 'enter')) ||
|
|
718
|
+
s === 'q' || s === 'Q' || s === '\x1b') {
|
|
719
|
+
stdin.removeListener('keypress', onDismiss);
|
|
720
|
+
dismissResolve();
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
stdin.on('keypress', onDismiss);
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
readlineModule.emitKeypressEvents(stdin);
|
|
728
|
+
stdin.resume();
|
|
729
|
+
stdin.setRawMode(true);
|
|
730
|
+
stdout.write('\x1b[H\x1b[J');
|
|
731
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
732
|
+
positionCursorOnInput();
|
|
733
|
+
bindComposerInput();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim().startsWith('/btw')) {
|
|
737
|
+
const command = input.trim();
|
|
738
|
+
setInput('');
|
|
739
|
+
unbindComposerInput();
|
|
740
|
+
if (options.onBtw) {
|
|
741
|
+
const ac = new AbortController();
|
|
742
|
+
const onBtwKey = (s, k) => {
|
|
743
|
+
if ((k && k.name === 'escape') || s === '\x1b') {
|
|
744
|
+
ac.abort();
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
stdin.on('keypress', onBtwKey);
|
|
748
|
+
try {
|
|
749
|
+
await options.onBtw(command, async (popupLines, frameOpts) => {
|
|
750
|
+
stdout.write('\x1b[H\x1b[J');
|
|
751
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows));
|
|
752
|
+
if (frameOpts?.waitDismiss !== false && !ac.signal.aborted) {
|
|
753
|
+
await new Promise((dismissResolve) => {
|
|
754
|
+
const onDismiss = (s, k) => {
|
|
755
|
+
if ((k && (k.name === 'escape' || k.name === 'return' || k.name === 'enter')) ||
|
|
756
|
+
s === 'q' || s === 'Q' || s === '\x1b') {
|
|
757
|
+
stdin.removeListener('keypress', onDismiss);
|
|
758
|
+
dismissResolve();
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
stdin.on('keypress', onDismiss);
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}, ac.signal);
|
|
765
|
+
}
|
|
766
|
+
finally {
|
|
767
|
+
stdin.removeListener('keypress', onBtwKey);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
readlineModule.emitKeypressEvents(stdin);
|
|
771
|
+
stdin.resume();
|
|
772
|
+
stdin.setRawMode(true);
|
|
773
|
+
stdout.write('\x1b[H\x1b[J');
|
|
774
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
775
|
+
positionCursorOnInput();
|
|
776
|
+
bindComposerInput();
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (key && (key.name === 'return' || key.name === 'enter') && isMcpCommandInput(input)) {
|
|
780
|
+
const command = input.trim();
|
|
781
|
+
setInput('');
|
|
782
|
+
unbindComposerInput();
|
|
783
|
+
if (options.onMcp) {
|
|
784
|
+
await options.onMcp(command, (popupLines) => {
|
|
785
|
+
stdout.write('\x1b[H\x1b[J');
|
|
786
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows));
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
if (options.signal?.aborted)
|
|
790
|
+
return;
|
|
791
|
+
readlineModule.emitKeypressEvents(stdin);
|
|
792
|
+
stdin.resume();
|
|
793
|
+
stdin.setRawMode(true);
|
|
794
|
+
redrawFull();
|
|
795
|
+
bindComposerInput();
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim() === '/init') {
|
|
799
|
+
setInput('');
|
|
800
|
+
unbindComposerInput();
|
|
801
|
+
if (options.onInit) {
|
|
802
|
+
await options.onInit((popupLines) => {
|
|
803
|
+
stdout.write('\x1b[H\x1b[J');
|
|
804
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows));
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
if (options.signal?.aborted)
|
|
808
|
+
return;
|
|
809
|
+
readlineModule.emitKeypressEvents(stdin);
|
|
810
|
+
stdin.resume();
|
|
811
|
+
stdin.setRawMode(true);
|
|
812
|
+
redrawFull();
|
|
813
|
+
bindComposerInput();
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim().startsWith('/workflow')) {
|
|
817
|
+
const command = input.trim();
|
|
818
|
+
setInput('');
|
|
819
|
+
unbindComposerInput();
|
|
820
|
+
const action = options.onWorkflow ? await options.onWorkflow(command, (popupLines) => { stdout.write('\x1b[H\x1b[J'); stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows)); }) : undefined;
|
|
821
|
+
if (action === 'build') {
|
|
822
|
+
cleanup();
|
|
823
|
+
resolve({ text: '', mode: 'Execute', autoApprove: currentAutoApprove, workflowAction: 'build' });
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
readlineModule.emitKeypressEvents(stdin);
|
|
827
|
+
stdin.resume();
|
|
828
|
+
stdin.setRawMode(true);
|
|
829
|
+
redrawFull();
|
|
830
|
+
bindComposerInput();
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
// ── /clear ────────────────────────────────────────────────────────────
|
|
834
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim() === '/clear') {
|
|
835
|
+
setInput('');
|
|
836
|
+
options.onClear?.();
|
|
837
|
+
redrawFull();
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
// ── /exit ─────────────────────────────────────────────────────────────
|
|
841
|
+
if (key && (key.name === 'return' || key.name === 'enter') &&
|
|
842
|
+
(input.trim() === '/exit' || input.trim() === '/quit')) {
|
|
843
|
+
cleanup();
|
|
844
|
+
if (options.signal)
|
|
845
|
+
options.signal.removeEventListener('abort', onAbort);
|
|
846
|
+
exitCleanly('\x1b[32mGoodbye! Stay Tuned with Moderado!\x1b[0m');
|
|
847
|
+
}
|
|
848
|
+
// ── Normal keys ───────────────────────────────────────────────────────
|
|
849
|
+
if (!key) {
|
|
850
|
+
if (str && str.length === 1 && str.charCodeAt(0) >= 32) {
|
|
851
|
+
input = input.slice(0, caret) + str + input.slice(caret);
|
|
852
|
+
caret += 1;
|
|
853
|
+
updateMentions();
|
|
854
|
+
redrawCard();
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
// Shift+Tab — toggle auto-approve
|
|
859
|
+
if ((key.name === 'tab' && key.shift) || key.sequence === '\x1b[Z') {
|
|
860
|
+
currentAutoApprove = !currentAutoApprove;
|
|
861
|
+
redrawCard();
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
// Tab — autocomplete mention, autocomplete slash command or toggle mode
|
|
865
|
+
if (key.name === 'tab' && !key.shift) {
|
|
866
|
+
const mention = activeMentionToken(input, caret);
|
|
867
|
+
if (mention && mentionFiles.length > 0) {
|
|
868
|
+
const chosen = mentionFiles[Math.min(mentionSelection, mentionFiles.length - 1)];
|
|
869
|
+
const before = input.slice(0, mention.start);
|
|
870
|
+
const after = input.slice(caret);
|
|
871
|
+
const insert = `@${chosen} `;
|
|
872
|
+
input = before + insert + after;
|
|
873
|
+
caret = before.length + insert.length;
|
|
874
|
+
updateMentions();
|
|
875
|
+
redrawCard();
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (input.startsWith('/')) {
|
|
879
|
+
const matching = getMatchingCommands(input);
|
|
880
|
+
if (matching.length > 0) {
|
|
881
|
+
setInput(matching[Math.min(commandSelection, matching.length - 1)].name);
|
|
882
|
+
commandSelection = 0;
|
|
883
|
+
redrawCard();
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
currentMode = currentMode === 'Plan' ? 'Execute' : 'Plan';
|
|
888
|
+
redrawCard();
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (mentionFiles.length > 0 && (key.name === 'up' || key.name === 'down')) {
|
|
892
|
+
mentionSelection = ((mentionSelection + (key.name === 'up' ? -1 : 1)) % mentionFiles.length + mentionFiles.length) % mentionFiles.length;
|
|
893
|
+
redrawCard();
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (input.startsWith('/') && (key.name === 'up' || key.name === 'down')) {
|
|
897
|
+
const matching = getMatchingCommands(input);
|
|
898
|
+
if (matching.length > 0) {
|
|
899
|
+
commandSelection = ((commandSelection + (key.name === 'up' ? -1 : 1)) % matching.length + matching.length) % matching.length;
|
|
900
|
+
redrawCard();
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (!input.startsWith('/') && (key.name === 'up' || key.name === 'down') && questionHistory.length > 0) {
|
|
905
|
+
if (questionHistoryIndex === questionHistory.length)
|
|
906
|
+
questionHistoryDraft = input;
|
|
907
|
+
const recalled = navigateQuestionHistory(questionHistory, key.name === 'up' ? -1 : 1, questionHistoryIndex, questionHistoryDraft);
|
|
908
|
+
setInput(recalled.input);
|
|
909
|
+
questionHistoryIndex = recalled.index;
|
|
910
|
+
redrawCard();
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
// Enter — submit real user message
|
|
914
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
915
|
+
const matching = getMatchingCommands(input);
|
|
916
|
+
if (input.startsWith('/') && matching.length > 0 && !matching.some((command) => command.name === input.trim())) {
|
|
917
|
+
setInput(matching[Math.min(commandSelection, matching.length - 1)].name);
|
|
918
|
+
commandSelection = 0;
|
|
919
|
+
redrawCard();
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const trimmed = input.trim();
|
|
923
|
+
cleanup();
|
|
924
|
+
if (options.signal)
|
|
925
|
+
options.signal.removeEventListener('abort', onAbort);
|
|
926
|
+
const moveDown = 4 + getExtraLines();
|
|
927
|
+
stdout.write(`\x1b[${moveDown}B\r\n\x1b[0 q\x1b[?25h`);
|
|
928
|
+
resolve({ text: trimmed, mode: currentMode, autoApprove: currentAutoApprove });
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
// Caret movement (cursor-only hop) and forward delete (repaint: the
|
|
932
|
+
// line shrank, so the old trailing character must be cleared).
|
|
933
|
+
if (key.name === 'left' || key.name === 'right' || key.name === 'delete') {
|
|
934
|
+
if (key.name === 'left') {
|
|
935
|
+
caret = Math.max(0, caret - 1);
|
|
936
|
+
updateMentions();
|
|
937
|
+
redrawCard();
|
|
938
|
+
}
|
|
939
|
+
else if (key.name === 'right') {
|
|
940
|
+
caret = Math.min(input.length, caret + 1);
|
|
941
|
+
updateMentions();
|
|
942
|
+
redrawCard();
|
|
943
|
+
}
|
|
944
|
+
else if (caret < input.length) {
|
|
945
|
+
input = input.slice(0, caret) + input.slice(caret + 1);
|
|
946
|
+
updateMentions();
|
|
947
|
+
redrawCard();
|
|
948
|
+
}
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
// Backspace
|
|
952
|
+
if (key.name === 'backspace') {
|
|
953
|
+
if (caret > 0) {
|
|
954
|
+
input = input.slice(0, caret - 1) + input.slice(caret);
|
|
955
|
+
caret = Math.max(0, caret - 1);
|
|
956
|
+
commandSelection = 0;
|
|
957
|
+
updateMentions();
|
|
958
|
+
redrawCard();
|
|
959
|
+
}
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
// Printable character
|
|
963
|
+
if (str && str.length === 1 && str.charCodeAt(0) >= 32 && !key.ctrl && !key.meta) {
|
|
964
|
+
input = input.slice(0, caret) + str + input.slice(caret);
|
|
965
|
+
caret += 1;
|
|
966
|
+
commandSelection = 0;
|
|
967
|
+
questionHistoryIndex = questionHistory.length;
|
|
968
|
+
updateMentions();
|
|
969
|
+
redrawCard();
|
|
970
|
+
}
|
|
971
|
+
})();
|
|
972
|
+
};
|
|
973
|
+
/**
|
|
974
|
+
* Mouse reporting is only useful while the composer owns the keyboard: a
|
|
975
|
+
* click on the composer line places the caret. Popups and sub-prompts take
|
|
976
|
+
* the keyboard over with mouse reporting off, so they keep the terminal's
|
|
977
|
+
* normal text selection, and clicks never leak into their filter fields.
|
|
978
|
+
*/
|
|
979
|
+
const bindComposerInput = () => {
|
|
980
|
+
stdout.write(MOUSE_REPORT_ON);
|
|
981
|
+
stdin.on('keypress', onKeypress);
|
|
982
|
+
};
|
|
983
|
+
const unbindComposerInput = () => {
|
|
984
|
+
stdin.removeListener('keypress', onKeypress);
|
|
985
|
+
stdout.write(MOUSE_REPORT_OFF);
|
|
986
|
+
};
|
|
987
|
+
bindComposerInput();
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
//# sourceMappingURL=welcome.js.map
|