@ask-thane/thane-cli 0.1.2 → 0.1.6

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/dist/chat.js CHANGED
@@ -1,195 +1,526 @@
1
- import { createInterface } from "node:readline/promises";
2
1
  import { stdin as input, stdout as output } from "node:process";
3
- import { runSlashMenu } from "./menu.js";
4
- import { renderInbox, renderMessages, renderUsers, renderWorkspaces } from "./render.js";
2
+ import { emitKeypressEvents } from "node:readline";
5
3
  import { completeSlashCommand, renderSlashCommands, slashCommands } from "./slash-commands.js";
6
4
  import { ThaneStore } from "./store.js";
7
- async function selectChannel(store, target) {
5
+ const CLEAR = "\x1b[2J\x1b[H";
6
+ const HIDE_CURSOR = "\x1b[?25l";
7
+ const SHOW_CURSOR = "\x1b[?25h";
8
+ const RESET = "\x1b[0m";
9
+ const DIM = "\x1b[2m";
10
+ const INVERSE = "\x1b[7m";
11
+ const BOLD = "\x1b[1m";
12
+ function size() {
13
+ return {
14
+ columns: Math.max(60, Number(output.columns ?? 100)),
15
+ rows: Math.max(20, Number(output.rows ?? 30))
16
+ };
17
+ }
18
+ function stripAnsi(value) {
19
+ return value.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
20
+ }
21
+ function visibleLength(value) {
22
+ return stripAnsi(value).length;
23
+ }
24
+ function fit(value, width) {
25
+ const plain = stripAnsi(value);
26
+ if (plain.length <= width) {
27
+ return value + " ".repeat(width - plain.length);
28
+ }
29
+ return `${plain.slice(0, Math.max(0, width - 1))}…`;
30
+ }
31
+ function wrap(value, width) {
32
+ if (width <= 0) {
33
+ return [""];
34
+ }
35
+ const words = value.split(/\s+/);
36
+ const lines = [];
37
+ let line = "";
38
+ for (const word of words) {
39
+ if (!line) {
40
+ line = word;
41
+ }
42
+ else if (line.length + word.length + 1 <= width) {
43
+ line = `${line} ${word}`;
44
+ }
45
+ else {
46
+ lines.push(line);
47
+ line = word;
48
+ }
49
+ while (line.length > width) {
50
+ lines.push(line.slice(0, width));
51
+ line = line.slice(width);
52
+ }
53
+ }
54
+ if (line) {
55
+ lines.push(line);
56
+ }
57
+ return lines.length ? lines : [""];
58
+ }
59
+ function channelLabel(channel) {
60
+ return channel.kind === "dm" ? `@${channel.name}` : `#${channel.name}`;
61
+ }
62
+ function conversations(store, activeId) {
63
+ const activity = new Map(store.inbox({ includeQuiet: true }).map((summary) => [summary.conversationId, summary]));
64
+ const channels = store.listChannels().map((channel) => {
65
+ const summary = activity.get(channel.id);
66
+ return {
67
+ id: channel.id,
68
+ label: channelLabel(channel),
69
+ name: channel.name,
70
+ kind: channel.kind,
71
+ unreadCount: summary?.unreadCount ?? 0,
72
+ mentionCount: summary?.mentionCount ?? 0
73
+ };
74
+ });
75
+ const dms = store.listDms().map((dm) => {
76
+ const summary = activity.get(dm.id);
77
+ return {
78
+ id: dm.id,
79
+ label: channelLabel(dm),
80
+ name: dm.name,
81
+ kind: dm.kind,
82
+ unreadCount: summary?.unreadCount ?? 0,
83
+ mentionCount: summary?.mentionCount ?? 0
84
+ };
85
+ });
86
+ const all = [...channels, ...dms];
87
+ if (!all.some((item) => item.id === activeId)) {
88
+ const channel = store.findChannel(activeId);
89
+ if (channel) {
90
+ all.unshift({
91
+ id: channel.id,
92
+ label: channelLabel(channel),
93
+ name: channel.name,
94
+ kind: channel.kind,
95
+ unreadCount: 0,
96
+ mentionCount: 0
97
+ });
98
+ }
99
+ }
100
+ return all;
101
+ }
102
+ async function selectConversation(store, target) {
8
103
  if (target.startsWith("@")) {
9
- const dm = await store.findOrCreateDm(target.slice(1));
10
- return { channelId: dm.id, promptName: `@${dm.name}` };
104
+ return store.findOrCreateDm(target.slice(1));
105
+ }
106
+ return store.createChannel(target.replace(/^#/, ""));
107
+ }
108
+ function renderMessage(message, width) {
109
+ const date = new Date(message.createdAt);
110
+ const time = Number.isNaN(date.getTime())
111
+ ? message.createdAt
112
+ : date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
113
+ const prefix = `${DIM}${time}${RESET} ${BOLD}${message.author}${RESET}: `;
114
+ const bodyWidth = Math.max(10, width - visibleLength(prefix));
115
+ const lines = wrap(message.text, bodyWidth);
116
+ return lines.map((line, index) => (index === 0 ? `${prefix}${line}` : `${" ".repeat(visibleLength(prefix))}${line}`));
117
+ }
118
+ function commandMatches(inputText) {
119
+ if (!inputText.startsWith("/")) {
120
+ return [];
121
+ }
122
+ const [names] = completeSlashCommand(inputText);
123
+ return names
124
+ .map((name) => slashCommands.find((command) => command.name === name))
125
+ .filter((command) => Boolean(command));
126
+ }
127
+ function replaceTrailingToken(inputText, value) {
128
+ const match = inputText.match(/(^|\s)(\S*)$/);
129
+ if (!match || match.index === undefined) {
130
+ return value;
11
131
  }
12
- const channel = await store.createChannel(target);
13
- return { channelId: channel.id, promptName: `#${channel.name}` };
132
+ const start = match.index + (match[1]?.length ?? 0);
133
+ return `${inputText.slice(0, start)}${value}`;
134
+ }
135
+ function completionCandidates(store, inputText) {
136
+ if (inputText.startsWith("/join ")) {
137
+ const prefix = inputText.slice("/join ".length).replace(/^#/, "").toLowerCase();
138
+ return store
139
+ .listChannels()
140
+ .filter((channel) => channel.name.startsWith(prefix))
141
+ .map((channel) => ({ label: `#${channel.name}`, value: `/join ${channel.name}` }));
142
+ }
143
+ if (inputText.startsWith("/workspace ")) {
144
+ const prefix = inputText.slice("/workspace ".length).toLowerCase();
145
+ return store
146
+ .listWorkspaces()
147
+ .filter((workspace) => workspace.slug.startsWith(prefix))
148
+ .map((workspace) => ({ label: workspace.slug, value: `/workspace ${workspace.slug}` }));
149
+ }
150
+ if (inputText.startsWith("/dm ")) {
151
+ const prefix = inputText.slice("/dm ".length).replace(/^@/, "").toLowerCase();
152
+ return store
153
+ .listUsers()
154
+ .filter((user) => user.handle.startsWith(prefix) && user.id !== store.currentUser.id)
155
+ .map((user) => ({ label: `@${user.handle}`, value: `/dm ${user.handle}` }));
156
+ }
157
+ const trailingMention = inputText.match(/(^|\s)@([a-z0-9._-]*)$/i);
158
+ if (trailingMention) {
159
+ const prefix = trailingMention[2]?.toLowerCase() ?? "";
160
+ return store
161
+ .listUsers()
162
+ .filter((user) => user.handle.startsWith(prefix))
163
+ .map((user) => ({ label: `@${user.handle}`, value: replaceTrailingToken(inputText, `@${user.handle}`) }));
164
+ }
165
+ return commandMatches(inputText).map((command) => ({
166
+ label: command.usage,
167
+ value: commandInput(command)
168
+ }));
169
+ }
170
+ function renderMenuLines(selectedIndex, availableRows) {
171
+ const visibleRows = Math.max(1, availableRows - 4);
172
+ const start = Math.max(0, Math.min(selectedIndex - Math.floor(visibleRows / 2), slashCommands.length - visibleRows));
173
+ const visibleCommands = slashCommands.slice(start, start + visibleRows);
174
+ return [
175
+ `${BOLD}Command Menu${RESET}`,
176
+ `${DIM}Use arrows, Return to choose, Esc to close.${RESET}`,
177
+ "",
178
+ ...visibleCommands.map((command, index) => {
179
+ const actualIndex = start + index;
180
+ const pointer = actualIndex === selectedIndex ? "> " : " ";
181
+ const row = `${pointer}${command.usage.padEnd(26)} ${command.description}`;
182
+ return actualIndex === selectedIndex ? `${INVERSE}${row}${RESET}` : row;
183
+ })
184
+ ];
185
+ }
186
+ function commandInput(command) {
187
+ return command.needsArgument ? `${command.name} ` : command.name;
188
+ }
189
+ function renderScreen(inputText, state) {
190
+ const { columns, rows } = size();
191
+ const sidebarWidth = Math.min(32, Math.max(24, Math.floor(columns * 0.28)));
192
+ const mainWidth = columns - sidebarWidth - 1;
193
+ const contentRows = rows - 4;
194
+ const active = state.store.findChannel(state.activeChannelId);
195
+ const items = conversations(state.store, state.activeChannelId);
196
+ const messages = state.store.recent(state.activeChannelId, 200);
197
+ const lines = [];
198
+ lines.push(`${CLEAR}${HIDE_CURSOR}${BOLD}Thane Chat${RESET} ${DIM}${state.store.activeWorkspace.slug}${RESET}`);
199
+ lines.push(`${"─".repeat(columns)}`);
200
+ const renderedMessages = messages.flatMap((message) => renderMessage(message, mainWidth - 2));
201
+ const helpLines = state.showMenu
202
+ ? renderMenuLines(state.menuIndex, contentRows)
203
+ : state.showHelp
204
+ ? [
205
+ `${BOLD}Commands${RESET}`,
206
+ "/join <channel> /dm <handle> /workspace <slug>",
207
+ "/commands /help /quit",
208
+ "",
209
+ ...renderSlashCommands().split("\n").slice(0, Math.max(0, contentRows - 5))
210
+ ]
211
+ : renderedMessages.slice(-contentRows);
212
+ for (let row = 0; row < contentRows; row += 1) {
213
+ const item = items[row];
214
+ let left = "";
215
+ if (item) {
216
+ const unread = item.unreadCount > 0 ? ` ${item.unreadCount}` : "";
217
+ const mention = item.mentionCount > 0 ? " @" : "";
218
+ const marker = item.id === state.activeChannelId ? ">" : " ";
219
+ left = `${marker} ${item.label}${mention}${unread}`;
220
+ if (item.id === state.activeChannelId) {
221
+ left = `${INVERSE}${fit(left, sidebarWidth)}${RESET}`;
222
+ }
223
+ else {
224
+ left = fit(left, sidebarWidth);
225
+ }
226
+ }
227
+ else {
228
+ left = " ".repeat(sidebarWidth);
229
+ }
230
+ const right = fit(helpLines[row] ?? "", mainWidth - 1);
231
+ lines.push(`${left}│${right}`);
232
+ }
233
+ const matches = state.showMenu ? [] : completionCandidates(state.store, inputText).slice(0, 4);
234
+ const suggestionStatus = matches.length > 0
235
+ ? `${DIM}Tab completes:${RESET} ${matches.map((candidate) => candidate.label).join(" ")}`
236
+ : "";
237
+ const status = suggestionStatus || state.status || `${active ? channelLabel(active) : "conversation"} ${DIM}Enter sends. Up/down recalls input. Alt+up/down switches. /menu opens menu.${RESET}`;
238
+ lines.push(`${"─".repeat(columns)}`);
239
+ lines.push(fit(status, columns));
240
+ lines.push(fit(`> ${inputText}`, columns));
241
+ output.write(lines.join("\n"));
14
242
  }
15
243
  export async function runChat(initialChannel = "general") {
16
244
  let store = await ThaneStore.open();
17
- let { channelId: channel, promptName } = await selectChannel(store, initialChannel);
18
- let lastSeenAt = store.recent(channel, 50).at(-1)?.createdAt ?? "";
19
- let lastActivitySignature = "";
20
- const rl = createInterface({
21
- input,
22
- output,
23
- prompt: `${store.activeWorkspace.slug}${promptName}> `,
24
- completer: completeSlashCommand
25
- });
245
+ let activeChannel = await selectConversation(store, initialChannel);
246
+ let inputText = "";
247
+ let status = "";
248
+ let showHelp = false;
249
+ let showMenu = false;
250
+ let menuIndex = 0;
251
+ const inputHistory = [];
252
+ let historyIndex;
253
+ let draftInput = "";
26
254
  let isOpen = true;
27
- const poll = setInterval(() => {
28
- void (async () => {
29
- if (!isOpen) {
30
- return;
31
- }
32
- const freshStore = await ThaneStore.open();
33
- const freshMessages = freshStore.recent(channel, 50).filter((message) => message.createdAt > lastSeenAt);
34
- if (freshMessages.length === 0) {
35
- store = freshStore;
36
- const activity = freshStore
37
- .inbox({ allWorkspaces: true, onlyUnread: true })
38
- .filter((summary) => !(summary.workspaceId === freshStore.activeWorkspace.id && summary.conversationId === channel));
39
- const signature = JSON.stringify(activity.map((summary) => [summary.workspaceId, summary.conversationId, summary.unreadCount, summary.mentionCount]));
40
- if (activity.length > 0 && signature !== lastActivitySignature) {
41
- lastActivitySignature = signature;
42
- output.write(`\nActivity elsewhere:\n${renderInbox(activity)}\n`);
43
- rl.prompt();
44
- }
45
- return;
46
- }
47
- lastSeenAt = freshMessages.at(-1)?.createdAt ?? lastSeenAt;
48
- store = freshStore;
49
- output.write(`\n${renderMessages(freshMessages)}\n`);
50
- rl.prompt();
51
- })().catch((error) => {
52
- output.write(`\n${error.message}\n`);
53
- rl.prompt();
54
- });
55
- }, 2_000);
56
- output.write(`Thane chat - workspace ${store.activeWorkspace.slug}\n`);
57
- output.write("Type /commands to see commands, /menu for an arrow-key menu, or press Tab after / for completion.\n\n");
58
- output.write(`${renderMessages(store.recent(channel, 20))}\n\n`);
59
- rl.prompt();
60
- const runSlashLine = async (trimmed) => {
61
- if (trimmed === "/quit" || trimmed === "/exit") {
62
- return "quit";
63
- }
64
- if (trimmed === "/commands") {
65
- output.write(`${renderSlashCommands()}\n`);
66
- }
67
- else if (trimmed === "/menu") {
68
- rl.pause();
69
- const selected = await runSlashMenu(slashCommands);
70
- rl.resume();
71
- if (selected) {
72
- if (selected.needsArgument) {
73
- output.write(`${selected.usage} - ${selected.description}\n`);
74
- }
75
- else {
76
- return runSlashLine(selected.name);
77
- }
78
- }
255
+ const refresh = async () => {
256
+ store = await ThaneStore.open();
257
+ renderScreen(inputText, { store, activeChannelId: activeChannel.id, status, showHelp, showMenu, menuIndex });
258
+ };
259
+ const switchTo = async (conversationId) => {
260
+ const channel = store.findChannel(conversationId);
261
+ if (!channel) {
262
+ return;
79
263
  }
80
- else if (trimmed === "/inbox") {
81
- output.write(`${renderInbox(store.inbox({ onlyUnread: true }))}\n`);
264
+ activeChannel = channel;
265
+ showHelp = false;
266
+ showMenu = false;
267
+ status = `Switched to ${channelLabel(channel)}`;
268
+ await store.markReadConversation(activeChannel.id);
269
+ await refresh();
270
+ };
271
+ const switchConversation = async (direction) => {
272
+ const items = conversations(store, activeChannel.id);
273
+ const index = Math.max(0, items.findIndex((item) => item.id === activeChannel.id));
274
+ const next = items[(index + direction + items.length) % items.length];
275
+ if (next) {
276
+ await switchTo(next.id);
82
277
  }
83
- else if (trimmed === "/inbox all") {
84
- output.write(`${renderInbox(store.inbox({ allWorkspaces: true, onlyUnread: true }))}\n`);
278
+ };
279
+ const rememberInput = (line) => {
280
+ if (!line.trim()) {
281
+ return;
85
282
  }
86
- else if (trimmed === "/workspaces") {
87
- output.write(`${renderWorkspaces(store.listWorkspaces(), store.activeWorkspace.id)}\n`);
283
+ if (inputHistory[inputHistory.length - 1] !== line) {
284
+ inputHistory.push(line);
88
285
  }
89
- else if (trimmed.startsWith("/workspace ")) {
90
- const workspace = await store.useWorkspace(trimmed.slice("/workspace ".length).trim());
91
- ({ channelId: channel, promptName } = await selectChannel(store, "general"));
92
- lastSeenAt = store.recent(channel, 50).at(-1)?.createdAt ?? "";
93
- lastActivitySignature = "";
94
- rl.setPrompt(`${workspace.slug}${promptName}> `);
95
- output.write(`${renderMessages(store.recent(channel, 20))}\n`);
96
- }
97
- else if (trimmed === "/channels") {
98
- output.write(`${store.listChannels().map((item) => `#${item.name}`).join("\n")}\n`);
99
- }
100
- else if (trimmed.startsWith("/join ")) {
101
- ({ channelId: channel, promptName } = await selectChannel(store, trimmed.slice("/join ".length).trim()));
102
- lastSeenAt = store.recent(channel, 50).at(-1)?.createdAt ?? "";
103
- lastActivitySignature = "";
104
- rl.setPrompt(`${store.activeWorkspace.slug}${promptName}> `);
105
- output.write(`${renderMessages(store.recent(channel, 20))}\n`);
106
- }
107
- else if (trimmed === "/leave") {
108
- if (promptName.startsWith("@")) {
109
- output.write("DMs cannot be left in the MVP.\n");
110
- }
111
- else {
112
- const leftChannel = await store.leaveChannel(promptName.slice(1));
113
- ({ channelId: channel, promptName } = await selectChannel(store, "general"));
114
- lastSeenAt = store.recent(channel, 50).at(-1)?.createdAt ?? "";
115
- lastActivitySignature = "";
116
- rl.setPrompt(`${store.activeWorkspace.slug}${promptName}> `);
117
- output.write(`left #${leftChannel.name}\n${renderMessages(store.recent(channel, 20))}\n`);
118
- }
286
+ historyIndex = undefined;
287
+ draftInput = "";
288
+ };
289
+ const recallInput = (direction) => {
290
+ if (inputHistory.length === 0) {
291
+ return;
119
292
  }
120
- else if (trimmed === "/members") {
121
- if (promptName.startsWith("@")) {
122
- output.write("DM membership is just the two participants.\n");
123
- }
124
- else {
125
- output.write(`${renderUsers(store.channelMembers(promptName.slice(1)))}\n`);
126
- }
293
+ if (historyIndex === undefined) {
294
+ draftInput = inputText;
295
+ historyIndex = direction === -1 ? inputHistory.length - 1 : 0;
127
296
  }
128
- else if (trimmed.startsWith("/dm ")) {
129
- ({ channelId: channel, promptName } = await selectChannel(store, `@${trimmed.slice("/dm ".length).trim()}`));
130
- lastSeenAt = store.recent(channel, 50).at(-1)?.createdAt ?? "";
131
- lastActivitySignature = "";
132
- rl.setPrompt(`${store.activeWorkspace.slug}${promptName}> `);
133
- output.write(`${renderMessages(store.recent(channel, 20))}\n`);
297
+ else {
298
+ historyIndex += direction;
134
299
  }
135
- else if (trimmed === "/recent") {
136
- output.write(`${renderMessages(store.recent(channel, 20))}\n`);
300
+ if (historyIndex < 0) {
301
+ historyIndex = undefined;
302
+ inputText = draftInput;
303
+ draftInput = "";
304
+ return;
137
305
  }
138
- else if (trimmed.startsWith("/thread ")) {
139
- output.write(`${renderMessages(store.thread(trimmed.slice("/thread ".length).trim()))}\n`);
306
+ if (historyIndex >= inputHistory.length) {
307
+ historyIndex = undefined;
308
+ inputText = draftInput;
309
+ draftInput = "";
310
+ return;
140
311
  }
141
- else if (trimmed.startsWith("/reply ")) {
142
- const [messageId, ...textParts] = trimmed.slice("/reply ".length).trim().split(/\s+/);
143
- if (!messageId || textParts.length === 0) {
144
- output.write("Usage: /reply <message-id> <text>\n");
145
- }
146
- else {
147
- const message = await store.reply(messageId, textParts.join(" "));
148
- output.write(`sent ${message.id}\n`);
149
- }
312
+ inputText = inputHistory[historyIndex] ?? "";
313
+ };
314
+ const runLine = async (line) => {
315
+ const trimmed = line.trim();
316
+ if (!trimmed) {
317
+ return;
150
318
  }
151
- else if (trimmed.startsWith("/react ")) {
152
- const [messageId, emoji] = trimmed.slice("/react ".length).trim().split(/\s+/);
153
- if (!messageId || !emoji) {
154
- output.write("Usage: /react <message-id> <emoji>\n");
155
- }
156
- else {
157
- await store.react(messageId, emoji);
158
- output.write(`reacted to ${messageId}\n`);
319
+ if (trimmed === "/quit" || trimmed === "/exit") {
320
+ isOpen = false;
321
+ return;
322
+ }
323
+ if (trimmed === "/menu") {
324
+ showMenu = true;
325
+ showHelp = false;
326
+ status = "Command menu";
327
+ return;
328
+ }
329
+ if (trimmed === "/help" || trimmed === "/commands") {
330
+ showHelp = !showHelp;
331
+ showMenu = false;
332
+ status = showHelp ? "Command help" : "";
333
+ return;
334
+ }
335
+ if (trimmed.startsWith("/join ")) {
336
+ activeChannel = await selectConversation(store, trimmed.slice("/join ".length).trim());
337
+ await store.markReadConversation(activeChannel.id);
338
+ status = `Joined ${channelLabel(activeChannel)}`;
339
+ showHelp = false;
340
+ showMenu = false;
341
+ return;
342
+ }
343
+ if (trimmed.startsWith("/dm ")) {
344
+ activeChannel = await selectConversation(store, `@${trimmed.slice("/dm ".length).trim()}`);
345
+ await store.markReadConversation(activeChannel.id);
346
+ status = `Opened ${channelLabel(activeChannel)}`;
347
+ showHelp = false;
348
+ showMenu = false;
349
+ return;
350
+ }
351
+ if (trimmed.startsWith("/workspace ")) {
352
+ const workspace = await store.useWorkspace(trimmed.slice("/workspace ".length).trim());
353
+ activeChannel = await selectConversation(store, "general");
354
+ status = `Switched to workspace ${workspace.slug}`;
355
+ showHelp = false;
356
+ showMenu = false;
357
+ return;
358
+ }
359
+ if (trimmed.startsWith("/")) {
360
+ status = "Unknown command. Type /help.";
361
+ return;
362
+ }
363
+ const sent = await store.sendMessage(activeChannel.id, trimmed);
364
+ const messages = store.recent(activeChannel.id, 200);
365
+ status = messages.some((message) => message.id === sent.id) ? "" : "";
366
+ showHelp = false;
367
+ showMenu = false;
368
+ await store.markReadConversation(activeChannel.id);
369
+ };
370
+ const completeInput = () => {
371
+ const matches = completionCandidates(store, inputText);
372
+ if (matches.length === 0) {
373
+ return false;
374
+ }
375
+ if (matches.length === 1) {
376
+ const match = matches[0];
377
+ if (!match) {
378
+ return false;
159
379
  }
380
+ inputText = match.value;
381
+ return true;
160
382
  }
161
- else if (trimmed.startsWith("/search ")) {
162
- output.write(`${renderMessages(store.search(trimmed.slice("/search ".length).trim()))}\n`);
383
+ const values = matches.map((candidate) => candidate.value);
384
+ let prefix = values[0] ?? inputText;
385
+ for (const value of values.slice(1)) {
386
+ while (!value.startsWith(prefix) && prefix.length > 1) {
387
+ prefix = prefix.slice(0, -1);
388
+ }
163
389
  }
164
- else if (trimmed.startsWith("/")) {
165
- output.write(`Unknown command. Type /commands to see commands.\n`);
390
+ if (prefix.length > inputText.length) {
391
+ inputText = prefix;
166
392
  }
167
393
  else {
168
- const message = await store.sendMessage(channel, trimmed);
169
- lastSeenAt = message.createdAt;
170
- output.write(`sent ${message.id}\n`);
394
+ const firstCommand = commandMatches(inputText)[0];
395
+ if (firstCommand) {
396
+ menuIndex = Math.max(0, slashCommands.findIndex((command) => command.name === firstCommand.name));
397
+ showMenu = true;
398
+ showHelp = false;
399
+ status = "Command menu";
400
+ }
401
+ else {
402
+ status = `Matches: ${matches.slice(0, 4).map((candidate) => candidate.label).join(" ")}`;
403
+ }
171
404
  }
172
- return "continue";
405
+ return true;
173
406
  };
174
- for await (const line of rl) {
175
- const trimmed = line.trim();
176
- try {
177
- if (!trimmed) {
178
- rl.prompt();
179
- continue;
407
+ emitKeypressEvents(input);
408
+ if (input.isTTY) {
409
+ input.setRawMode?.(true);
410
+ }
411
+ input.resume();
412
+ const onKeypress = (_chunk, key) => {
413
+ void (async () => {
414
+ try {
415
+ if ((key.ctrl && key.name === "c") || key.sequence === "\u0003") {
416
+ isOpen = false;
417
+ return;
418
+ }
419
+ if (showMenu) {
420
+ if (key.name === "escape") {
421
+ showMenu = false;
422
+ status = "";
423
+ }
424
+ else if (key.name === "up") {
425
+ menuIndex = menuIndex === 0 ? slashCommands.length - 1 : menuIndex - 1;
426
+ }
427
+ else if (key.name === "down" || key.name === "tab") {
428
+ menuIndex = menuIndex === slashCommands.length - 1 ? 0 : menuIndex + 1;
429
+ }
430
+ else if (key.name === "return") {
431
+ const selected = slashCommands[menuIndex];
432
+ if (!selected) {
433
+ showMenu = false;
434
+ status = "";
435
+ await refresh();
436
+ return;
437
+ }
438
+ showMenu = false;
439
+ if (selected.needsArgument) {
440
+ inputText = commandInput(selected);
441
+ status = `${selected.usage} ${DIM}${selected.description}${RESET}`;
442
+ }
443
+ else {
444
+ inputText = "";
445
+ await runLine(selected.name);
446
+ }
447
+ }
448
+ if (!isOpen) {
449
+ return;
450
+ }
451
+ await refresh();
452
+ return;
453
+ }
454
+ if (key.name === "return") {
455
+ const submitted = inputText;
456
+ inputText = "";
457
+ rememberInput(submitted);
458
+ await runLine(submitted);
459
+ }
460
+ else if (key.name === "backspace") {
461
+ inputText = inputText.slice(0, -1);
462
+ historyIndex = undefined;
463
+ }
464
+ else if (key.name === "up") {
465
+ if (key.ctrl || key.meta || key.sequence === "\u001b[1;5A" || key.sequence === "\u001b[1;3A") {
466
+ await switchConversation(-1);
467
+ return;
468
+ }
469
+ recallInput(-1);
470
+ }
471
+ else if (key.name === "down") {
472
+ if (key.ctrl || key.meta || key.sequence === "\u001b[1;5B" || key.sequence === "\u001b[1;3B") {
473
+ await switchConversation(1);
474
+ return;
475
+ }
476
+ recallInput(1);
477
+ }
478
+ else if (key.name === "tab") {
479
+ if (completeInput()) {
480
+ await refresh();
481
+ return;
482
+ }
483
+ }
484
+ else if (key.name === "escape") {
485
+ showHelp = false;
486
+ showMenu = false;
487
+ status = "";
488
+ }
489
+ else if (key.sequence && key.sequence >= " " && !key.ctrl) {
490
+ inputText += key.sequence;
491
+ historyIndex = undefined;
492
+ }
493
+ if (!isOpen) {
494
+ return;
495
+ }
496
+ await refresh();
180
497
  }
181
- const result = await runSlashLine(trimmed);
182
- if (result === "quit") {
183
- break;
498
+ catch (error) {
499
+ status = error.message;
500
+ await refresh();
184
501
  }
502
+ })();
503
+ };
504
+ input.on("keypress", onKeypress);
505
+ const poll = setInterval(() => {
506
+ if (isOpen) {
507
+ void refresh();
508
+ }
509
+ }, 1_500);
510
+ try {
511
+ await store.markReadConversation(activeChannel.id);
512
+ await refresh();
513
+ while (isOpen) {
514
+ await new Promise((resolve) => setTimeout(resolve, 50));
185
515
  }
186
- catch (error) {
187
- output.write(`${error.message}\n`);
516
+ }
517
+ finally {
518
+ clearInterval(poll);
519
+ input.off("keypress", onKeypress);
520
+ if (input.isTTY) {
521
+ input.setRawMode?.(false);
188
522
  }
189
- rl.prompt();
523
+ output.write(`${SHOW_CURSOR}${RESET}\x1b[2J\x1b[H`);
190
524
  }
191
- isOpen = false;
192
- clearInterval(poll);
193
- rl.close();
194
525
  }
195
526
  //# sourceMappingURL=chat.js.map