botholomew 0.8.5 → 0.8.7
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/package.json +1 -1
- package/src/chat/session.ts +19 -0
- package/src/commands/mcpx.ts +47 -146
- package/src/skills/commands.ts +12 -0
- package/src/tui/App.tsx +46 -2
- package/src/tui/components/InputBar.tsx +24 -7
- package/src/tui/slashCompletion.ts +16 -2
package/package.json
CHANGED
package/src/chat/session.ts
CHANGED
|
@@ -140,3 +140,22 @@ export async function endChatSession(session: ChatSession): Promise<void> {
|
|
|
140
140
|
await withDb(session.dbPath, (conn) => endThread(conn, session.threadId));
|
|
141
141
|
await session.cleanup();
|
|
142
142
|
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* End the current thread and start a fresh one on the same session.
|
|
146
|
+
* The old thread is persisted (marked ended) and can still be resumed
|
|
147
|
+
* via `botholomew chat --thread-id <id>`. Returns the previous thread
|
|
148
|
+
* ID so callers can display it to the user.
|
|
149
|
+
*/
|
|
150
|
+
export async function clearChatSession(
|
|
151
|
+
session: ChatSession,
|
|
152
|
+
): Promise<{ previousThreadId: string; newThreadId: string }> {
|
|
153
|
+
const previousThreadId = session.threadId;
|
|
154
|
+
const newThreadId = await withDb(session.dbPath, async (conn) => {
|
|
155
|
+
await endThread(conn, previousThreadId);
|
|
156
|
+
return createThread(conn, "chat_session", undefined, "New chat");
|
|
157
|
+
});
|
|
158
|
+
session.threadId = newThreadId;
|
|
159
|
+
session.messages.length = 0;
|
|
160
|
+
return { previousThreadId, newThreadId };
|
|
161
|
+
}
|
package/src/commands/mcpx.ts
CHANGED
|
@@ -51,155 +51,64 @@ function getDir(program: Command): string {
|
|
|
51
51
|
return program.opts().dir;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Slice process.argv from the token after "mcpx" so flags (including --help)
|
|
55
|
+
// and positional args flow through to upstream mcpx verbatim.
|
|
56
|
+
function getRawMcpxArgs(): string[] {
|
|
57
|
+
const idx = process.argv.indexOf("mcpx");
|
|
58
|
+
return idx === -1 ? [] : process.argv.slice(idx + 1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const PASSTHROUGH_SUBCOMMANDS: ReadonlyArray<[name: string, desc: string]> = [
|
|
62
|
+
["servers", "List configured MCP server names"],
|
|
63
|
+
["info", "Show server overview or schema for a specific tool"],
|
|
64
|
+
["search", "Search tools by keyword and/or semantic similarity"],
|
|
65
|
+
["exec", "Execute a tool call"],
|
|
66
|
+
["add", "Add an MCP server"],
|
|
67
|
+
["remove", "Remove an MCP server"],
|
|
68
|
+
["ping", "Check connectivity to MCP servers"],
|
|
69
|
+
["auth", "Authenticate with an HTTP MCP server"],
|
|
70
|
+
["deauth", "Remove stored authentication for a server"],
|
|
71
|
+
["resource", "List resources for a server, or read a specific resource"],
|
|
72
|
+
["prompt", "List prompts for a server, or get a specific prompt"],
|
|
73
|
+
["task", "Manage async tool tasks (list, get, result, cancel)"],
|
|
74
|
+
["index", "Build the search index from all configured servers"],
|
|
75
|
+
];
|
|
76
|
+
|
|
54
77
|
export function registerMcpxCommand(program: Command) {
|
|
55
78
|
const mcpx = program
|
|
56
79
|
.command("mcpx")
|
|
57
80
|
.description("Manage MCP servers via MCPX");
|
|
58
81
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.command("info <first> [second]")
|
|
71
|
-
.description(
|
|
72
|
-
"Show server overview, or schema for a specific tool (server is optional if tool name is unambiguous)",
|
|
73
|
-
)
|
|
74
|
-
.action(async (first: string, second?: string) => {
|
|
75
|
-
const out = await runMcpx(getDir(program), ["info", first, second]);
|
|
76
|
-
process.stdout.write(out);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// --- search ---
|
|
80
|
-
mcpx
|
|
81
|
-
.command("search <terms...>")
|
|
82
|
-
.description("Search tools by keyword and/or semantic similarity")
|
|
83
|
-
.action(async (terms: string[]) => {
|
|
84
|
-
const out = await runMcpx(getDir(program), ["search", ...terms]);
|
|
85
|
-
process.stdout.write(out);
|
|
86
|
-
});
|
|
82
|
+
for (const [name, description] of PASSTHROUGH_SUBCOMMANDS) {
|
|
83
|
+
mcpx
|
|
84
|
+
.command(name)
|
|
85
|
+
.description(description)
|
|
86
|
+
.allowUnknownOption(true)
|
|
87
|
+
.helpOption(false)
|
|
88
|
+
.argument("[args...]", "arguments forwarded to mcpx")
|
|
89
|
+
.action(async () => {
|
|
90
|
+
await runMcpx(getDir(program), getRawMcpxArgs(), { inherit: true });
|
|
91
|
+
});
|
|
92
|
+
}
|
|
87
93
|
|
|
88
|
-
//
|
|
94
|
+
// Upstream mcpx's "list" is the default action when invoked with no
|
|
95
|
+
// subcommand — not a registered subcommand — so we strip the "list"
|
|
96
|
+
// token before forwarding.
|
|
89
97
|
mcpx
|
|
90
|
-
.command("
|
|
98
|
+
.command("list")
|
|
91
99
|
.description(
|
|
92
|
-
"
|
|
100
|
+
"List all tools, resources, and prompts across all configured servers",
|
|
93
101
|
)
|
|
94
|
-
.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
process.stdout.write(out);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// --- add ---
|
|
105
|
-
mcpx
|
|
106
|
-
.command("add <name>")
|
|
107
|
-
.description("Add an MCP server")
|
|
108
|
-
.option("--command <cmd>", "Stdio server command")
|
|
109
|
-
.option("--args <args...>", "Stdio server arguments")
|
|
110
|
-
.option("--url <url>", "HTTP server URL")
|
|
111
|
-
.option("--transport <type>", "HTTP transport: sse or streamable-http")
|
|
112
|
-
.option("--env <pairs...>", "Environment variables as KEY=VALUE pairs")
|
|
113
|
-
.action(
|
|
114
|
-
async (
|
|
115
|
-
name: string,
|
|
116
|
-
opts: {
|
|
117
|
-
command?: string;
|
|
118
|
-
args?: string[];
|
|
119
|
-
url?: string;
|
|
120
|
-
transport?: string;
|
|
121
|
-
env?: string[];
|
|
122
|
-
},
|
|
123
|
-
) => {
|
|
124
|
-
const cliArgs: string[] = ["add", name];
|
|
125
|
-
if (opts.command) cliArgs.push("--command", opts.command);
|
|
126
|
-
if (opts.args) {
|
|
127
|
-
for (const a of opts.args) cliArgs.push("--args", a);
|
|
128
|
-
}
|
|
129
|
-
if (opts.url) cliArgs.push("--url", opts.url);
|
|
130
|
-
if (opts.transport) cliArgs.push("--transport", opts.transport);
|
|
131
|
-
if (opts.env) {
|
|
132
|
-
for (const e of opts.env) cliArgs.push("--env", e);
|
|
133
|
-
}
|
|
134
|
-
const out = await runMcpx(getDir(program), cliArgs);
|
|
135
|
-
process.stdout.write(out);
|
|
136
|
-
},
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
// --- remove ---
|
|
140
|
-
mcpx
|
|
141
|
-
.command("remove <name>")
|
|
142
|
-
.description("Remove an MCP server")
|
|
143
|
-
.action(async (name: string) => {
|
|
144
|
-
const out = await runMcpx(getDir(program), ["remove", name]);
|
|
145
|
-
process.stdout.write(out);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// --- ping ---
|
|
149
|
-
mcpx
|
|
150
|
-
.command("ping [servers...]")
|
|
151
|
-
.description("Check connectivity to MCP servers")
|
|
152
|
-
.action(async (servers: string[]) => {
|
|
153
|
-
const out = await runMcpx(getDir(program), ["ping", ...servers]);
|
|
154
|
-
process.stdout.write(out);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// --- auth ---
|
|
158
|
-
mcpx
|
|
159
|
-
.command("auth <server>")
|
|
160
|
-
.description("Authenticate with an HTTP MCP server")
|
|
161
|
-
.action(async (server: string) => {
|
|
162
|
-
await runMcpx(getDir(program), ["auth", server], { inherit: true });
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// --- resource ---
|
|
166
|
-
mcpx
|
|
167
|
-
.command("resource [server] [uri]")
|
|
168
|
-
.description("List resources for a server, or read a specific resource")
|
|
169
|
-
.action(async (server?: string, uri?: string) => {
|
|
170
|
-
const out = await runMcpx(getDir(program), ["resource", server, uri]);
|
|
171
|
-
process.stdout.write(out);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// --- prompt ---
|
|
175
|
-
mcpx
|
|
176
|
-
.command("prompt [server] [name] [args]")
|
|
177
|
-
.description("List prompts for a server, or get a specific prompt")
|
|
178
|
-
.action(async (server?: string, name?: string, argsJson?: string) => {
|
|
179
|
-
const out = await runMcpx(getDir(program), [
|
|
180
|
-
"prompt",
|
|
181
|
-
server,
|
|
182
|
-
name,
|
|
183
|
-
argsJson,
|
|
184
|
-
]);
|
|
185
|
-
process.stdout.write(out);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
// --- task ---
|
|
189
|
-
mcpx
|
|
190
|
-
.command("task <action> <server> [taskId]")
|
|
191
|
-
.description("Manage async tasks (actions: list, get, result, cancel)")
|
|
192
|
-
.action(async (action: string, server: string, taskId?: string) => {
|
|
193
|
-
const out = await runMcpx(getDir(program), [
|
|
194
|
-
"task",
|
|
195
|
-
action,
|
|
196
|
-
server,
|
|
197
|
-
taskId,
|
|
198
|
-
]);
|
|
199
|
-
process.stdout.write(out);
|
|
102
|
+
.allowUnknownOption(true)
|
|
103
|
+
.helpOption(false)
|
|
104
|
+
.argument("[args...]", "arguments forwarded to mcpx")
|
|
105
|
+
.action(async () => {
|
|
106
|
+
const raw = getRawMcpxArgs();
|
|
107
|
+
const args = raw[0] === "list" ? raw.slice(1) : raw;
|
|
108
|
+
await runMcpx(getDir(program), args, { inherit: true });
|
|
200
109
|
});
|
|
201
110
|
|
|
202
|
-
//
|
|
111
|
+
// Botholomew-specific: copy system-wide MCPX settings into this project.
|
|
203
112
|
mcpx
|
|
204
113
|
.command("import-global")
|
|
205
114
|
.description("Copy system-wide MCPX settings (~/.mcpx) into this project")
|
|
@@ -234,12 +143,4 @@ export function registerMcpxCommand(program: Command) {
|
|
|
234
143
|
);
|
|
235
144
|
}
|
|
236
145
|
});
|
|
237
|
-
|
|
238
|
-
// --- index ---
|
|
239
|
-
mcpx
|
|
240
|
-
.command("index")
|
|
241
|
-
.description("Build the search index from all configured servers")
|
|
242
|
-
.action(async () => {
|
|
243
|
-
await runMcpx(getDir(program), ["index"], { inherit: true });
|
|
244
|
-
});
|
|
245
146
|
}
|
package/src/skills/commands.ts
CHANGED
|
@@ -4,11 +4,13 @@ import { renderSkill } from "./parser.ts";
|
|
|
4
4
|
export interface SlashCommand {
|
|
5
5
|
name: string;
|
|
6
6
|
description: string;
|
|
7
|
+
takesArgs?: boolean;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export const BUILTIN_SLASH_COMMANDS: SlashCommand[] = [
|
|
10
11
|
{ name: "help", description: "Show command reference and shortcuts" },
|
|
11
12
|
{ name: "skills", description: "List available skills" },
|
|
13
|
+
{ name: "clear", description: "End current thread and start a new one" },
|
|
12
14
|
{ name: "exit", description: "End the chat session" },
|
|
13
15
|
];
|
|
14
16
|
|
|
@@ -17,6 +19,7 @@ export interface SlashCommandContext {
|
|
|
17
19
|
addSystemMessage: (content: string) => void;
|
|
18
20
|
queueUserMessage: (content: string) => void;
|
|
19
21
|
exit: () => void;
|
|
22
|
+
clearChat?: () => void;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/**
|
|
@@ -38,6 +41,15 @@ export function handleSlashCommand(
|
|
|
38
41
|
return true;
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
if (name === "clear") {
|
|
45
|
+
if (ctx.clearChat) {
|
|
46
|
+
ctx.clearChat();
|
|
47
|
+
} else {
|
|
48
|
+
ctx.addSystemMessage("/clear is only available in the chat TUI.");
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
if (name === "skills") {
|
|
42
54
|
if (ctx.skills.size === 0) {
|
|
43
55
|
ctx.addSystemMessage(
|
package/src/tui/App.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { Box, Static, Text, useApp, useInput } from "ink";
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import {
|
|
4
4
|
type ChatSession,
|
|
5
|
+
clearChatSession,
|
|
5
6
|
endChatSession,
|
|
6
7
|
sendMessage,
|
|
7
8
|
startChatSession,
|
|
@@ -126,6 +127,7 @@ export function App({
|
|
|
126
127
|
}: AppProps) {
|
|
127
128
|
const { exit } = useApp();
|
|
128
129
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
130
|
+
const [messagesEpoch, setMessagesEpoch] = useState(0);
|
|
129
131
|
const [inputValue, setInputValue] = useState("");
|
|
130
132
|
const [inputHistory, setInputHistory] = useState<string[]>([]);
|
|
131
133
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -490,7 +492,8 @@ export function App({
|
|
|
490
492
|
" ⌥+Enter Insert newline",
|
|
491
493
|
" ↑/↓ Browse input history",
|
|
492
494
|
" / Open slash-command autocomplete",
|
|
493
|
-
"
|
|
495
|
+
" Enter Run highlighted command / insert if it takes args (popup open)",
|
|
496
|
+
" Tab Insert highlighted command without submitting (popup open)",
|
|
494
497
|
" ↑/↓ Move highlight (popup open)",
|
|
495
498
|
" Esc Close popup",
|
|
496
499
|
"",
|
|
@@ -534,6 +537,7 @@ export function App({
|
|
|
534
537
|
"Commands:",
|
|
535
538
|
" /help Show this help",
|
|
536
539
|
" /skills List available skills",
|
|
540
|
+
" /clear End current thread and start a new one",
|
|
537
541
|
" /exit End the chat session",
|
|
538
542
|
...skillLines,
|
|
539
543
|
].join("\n"),
|
|
@@ -563,6 +567,42 @@ export function App({
|
|
|
563
567
|
processQueue();
|
|
564
568
|
},
|
|
565
569
|
exit,
|
|
570
|
+
clearChat: () => {
|
|
571
|
+
const session = sessionRef.current;
|
|
572
|
+
if (!session) return;
|
|
573
|
+
// Drain any queued messages so they don't leak into the new thread.
|
|
574
|
+
queueRef.current.length = 0;
|
|
575
|
+
syncQueue();
|
|
576
|
+
clearChatSession(session)
|
|
577
|
+
.then(({ previousThreadId, newThreadId }) => {
|
|
578
|
+
// Ink's <Static> writes messages to terminal scrollback and
|
|
579
|
+
// can't un-write them, so setMessages alone leaves the old
|
|
580
|
+
// lines visible. Clear the terminal (including scrollback)
|
|
581
|
+
// and bump the epoch key on <Static> to force a fresh mount.
|
|
582
|
+
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
583
|
+
setMessages([
|
|
584
|
+
{
|
|
585
|
+
id: msgId(),
|
|
586
|
+
role: "system",
|
|
587
|
+
content: `Started a new chat thread (${newThreadId}). Previous thread saved — resume with: botholomew chat --thread-id ${previousThreadId}`,
|
|
588
|
+
timestamp: new Date(),
|
|
589
|
+
},
|
|
590
|
+
]);
|
|
591
|
+
setMessagesEpoch((n) => n + 1);
|
|
592
|
+
setChatTitle(undefined);
|
|
593
|
+
})
|
|
594
|
+
.catch((err) => {
|
|
595
|
+
setMessages((prev) => [
|
|
596
|
+
...prev,
|
|
597
|
+
{
|
|
598
|
+
id: msgId(),
|
|
599
|
+
role: "system",
|
|
600
|
+
content: `Failed to clear chat: ${err}`,
|
|
601
|
+
timestamp: new Date(),
|
|
602
|
+
},
|
|
603
|
+
]);
|
|
604
|
+
});
|
|
605
|
+
},
|
|
566
606
|
});
|
|
567
607
|
if (handled) return;
|
|
568
608
|
}
|
|
@@ -595,6 +635,10 @@ export function App({
|
|
|
595
635
|
? Array.from(sessionSkills.values()).map((s) => ({
|
|
596
636
|
name: s.name,
|
|
597
637
|
description: s.description,
|
|
638
|
+
takesArgs:
|
|
639
|
+
s.arguments.length > 0 ||
|
|
640
|
+
/\$ARGUMENTS\b/.test(s.body) ||
|
|
641
|
+
/\$[1-9]\b/.test(s.body),
|
|
598
642
|
}))
|
|
599
643
|
: [];
|
|
600
644
|
return buildSlashCommands(BUILTIN_SLASH_COMMANDS, skillList);
|
|
@@ -640,7 +684,7 @@ export function App({
|
|
|
640
684
|
node always has proper terminal width in its Yoga layout.
|
|
641
685
|
Otherwise Ink's border renderer crashes with a negative
|
|
642
686
|
contentWidth when tool-call boxes are rendered at width 0. */}
|
|
643
|
-
<Static items={messages}>
|
|
687
|
+
<Static key={messagesEpoch} items={messages}>
|
|
644
688
|
{(msg) => <MessageBubble key={msg.id} message={msg} />}
|
|
645
689
|
</Static>
|
|
646
690
|
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
useState,
|
|
10
10
|
} from "react";
|
|
11
11
|
import type { SlashCommand } from "../../skills/commands.ts";
|
|
12
|
-
import { getSlashMatches } from "../slashCompletion.ts";
|
|
12
|
+
import { getSlashMatches, shouldSubmitOnEnter } from "../slashCompletion.ts";
|
|
13
13
|
import { SlashCommandPopup } from "./SlashCommandPopup.tsx";
|
|
14
14
|
|
|
15
15
|
interface InputBarProps {
|
|
@@ -128,11 +128,23 @@ export const InputBar = memo(function InputBar({
|
|
|
128
128
|
? getSlashMatches(val, slashCommandsRef.current ?? [])
|
|
129
129
|
: null;
|
|
130
130
|
|
|
131
|
-
const acceptSelection = () => {
|
|
131
|
+
const acceptSelection = (mode: "insert" | "submit") => {
|
|
132
132
|
if (!popupOpen) return false;
|
|
133
133
|
const chosen =
|
|
134
134
|
popupOpen[Math.min(selectedIndexRef.current, popupOpen.length - 1)];
|
|
135
135
|
if (!chosen) return false;
|
|
136
|
+
if (mode === "submit") {
|
|
137
|
+
const completed = `/${chosen.name}`;
|
|
138
|
+
valueRef.current = completed;
|
|
139
|
+
cursorPosRef.current = 0;
|
|
140
|
+
onChangeRef.current(completed);
|
|
141
|
+
setCursorPos(0);
|
|
142
|
+
historyIndexRef.current = -1;
|
|
143
|
+
setHistoryIndex(-1);
|
|
144
|
+
savedInput.current = "";
|
|
145
|
+
onSubmitRef.current(completed);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
136
148
|
const completed = `/${chosen.name} `;
|
|
137
149
|
valueRef.current = completed;
|
|
138
150
|
cursorPosRef.current = completed.length;
|
|
@@ -152,11 +164,16 @@ export const InputBar = memo(function InputBar({
|
|
|
152
164
|
return;
|
|
153
165
|
}
|
|
154
166
|
|
|
155
|
-
// Enter: if popup is open, accept
|
|
156
|
-
//
|
|
167
|
+
// Enter: if popup is open, accept the highlighted entry. No-arg
|
|
168
|
+
// commands submit in one keystroke; commands that take args insert
|
|
169
|
+
// `/<name> ` and wait for the user to finish typing.
|
|
157
170
|
if (key.return) {
|
|
158
171
|
if (popupOpen && !key.shift && !key.meta) {
|
|
159
|
-
|
|
172
|
+
const chosen =
|
|
173
|
+
popupOpen[Math.min(selectedIndexRef.current, popupOpen.length - 1)];
|
|
174
|
+
acceptSelection(
|
|
175
|
+
chosen && shouldSubmitOnEnter(chosen) ? "submit" : "insert",
|
|
176
|
+
);
|
|
160
177
|
return;
|
|
161
178
|
}
|
|
162
179
|
if (key.shift || key.meta) {
|
|
@@ -179,10 +196,10 @@ export const InputBar = memo(function InputBar({
|
|
|
179
196
|
return;
|
|
180
197
|
}
|
|
181
198
|
|
|
182
|
-
// Tab:
|
|
199
|
+
// Tab: insert the highlighted completion so the user can keep editing.
|
|
183
200
|
if (key.tab) {
|
|
184
201
|
if (popupOpen) {
|
|
185
|
-
acceptSelection();
|
|
202
|
+
acceptSelection("insert");
|
|
186
203
|
}
|
|
187
204
|
return;
|
|
188
205
|
}
|
|
@@ -28,11 +28,25 @@ export function getSlashMatches(
|
|
|
28
28
|
|
|
29
29
|
export function buildSlashCommands(
|
|
30
30
|
builtins: SlashCommand[],
|
|
31
|
-
skills: Iterable<{ name: string; description: string }>,
|
|
31
|
+
skills: Iterable<{ name: string; description: string; takesArgs?: boolean }>,
|
|
32
32
|
): SlashCommand[] {
|
|
33
33
|
const out: SlashCommand[] = [...builtins];
|
|
34
34
|
for (const s of skills) {
|
|
35
|
-
out.push({
|
|
35
|
+
out.push({
|
|
36
|
+
name: s.name,
|
|
37
|
+
description: s.description,
|
|
38
|
+
takesArgs: s.takesArgs,
|
|
39
|
+
});
|
|
36
40
|
}
|
|
37
41
|
return out;
|
|
38
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Decide whether pressing Enter on a highlighted popup entry should both
|
|
46
|
+
* accept the completion and immediately submit. True for no-argument
|
|
47
|
+
* commands (single-Enter runs them); false for commands that take args,
|
|
48
|
+
* where we insert `/<name> ` and wait for the user to finish typing.
|
|
49
|
+
*/
|
|
50
|
+
export function shouldSubmitOnEnter(cmd: SlashCommand): boolean {
|
|
51
|
+
return !cmd.takesArgs;
|
|
52
|
+
}
|