@researchcomputer/pista 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/agent-events.js +2 -6
- package/dist/agent-events.test.js +3 -0
- package/dist/app.js +142 -144
- package/dist/commands.js +200 -104
- package/dist/commands.test.js +37 -0
- package/dist/components/assistant-message-render.test.js +12 -2
- package/dist/components/assistant-message.js +1 -1
- package/dist/components/composer-panel.js +11 -0
- package/dist/components/editor.js +4 -2
- package/dist/components/editor.test.js +10 -2
- package/dist/components/frame.js +4 -5
- package/dist/components/header.js +3 -3
- package/dist/components/log-row.js +2 -2
- package/dist/components/scrollbar.js +1 -1
- package/dist/components/status-bar.js +7 -0
- package/dist/hooks/use-composer.js +89 -0
- package/dist/hooks/use-composer.test.js +24 -0
- package/dist/hooks/use-live-assistant.js +64 -0
- package/dist/hooks/use-live-assistant.test.js +30 -0
- package/dist/hooks/use-terminal-size.js +21 -0
- package/dist/transcript.test.js +22 -0
- package/dist/utils.js +24 -6
- package/dist/utils.test.js +53 -5
- package/package.json +13 -3
package/dist/commands.js
CHANGED
|
@@ -1,31 +1,100 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { describeConfig, parseCompatApiStyle, parsePermissionMode, parseThinkingLevel
|
|
2
|
+
import { describeConfig, parseCompatApiStyle, parsePermissionMode, parseThinkingLevel } from './config.js';
|
|
3
3
|
import { normalizeAgentName, normalizeEndpoint, errorMessage, saveApiKey, getApiKeyPath, getStoredApiKey, loadStoredPreferences, saveStoredPreferences, resolveConfiguredSkills, fetchGitHubSkill, fetchGitHubSkillIndex, } from './utils.js';
|
|
4
4
|
const SLASH_COMMANDS = [
|
|
5
5
|
{ id: 'help', usage: '/help', description: 'Show this help message', allowWhileRunning: true, section: 'commands' },
|
|
6
6
|
{ id: 'init', usage: '/init', description: 'Analyze codebase and generate AGENTS.md', section: 'commands' },
|
|
7
7
|
{ id: 'name', usage: '/name [name]', description: 'Show or change agent name', section: 'commands' },
|
|
8
|
-
{
|
|
8
|
+
{
|
|
9
|
+
id: 'abort',
|
|
10
|
+
usage: '/abort',
|
|
11
|
+
description: 'Stop current agent execution',
|
|
12
|
+
allowWhileRunning: true,
|
|
13
|
+
section: 'commands',
|
|
14
|
+
},
|
|
9
15
|
{ id: 'clear', usage: '/clear', description: 'Clear the terminal transcript', section: 'commands' },
|
|
10
16
|
{ id: 'reset', usage: '/reset', description: 'Reset agent conversation state', section: 'commands' },
|
|
11
17
|
{ id: 'cost', usage: '/cost', description: 'Show current session token usage', section: 'commands' },
|
|
12
18
|
{ id: 'key', usage: '/key', description: 'Update your stored API key', section: 'commands' },
|
|
13
|
-
{
|
|
14
|
-
|
|
19
|
+
{
|
|
20
|
+
id: 'jump',
|
|
21
|
+
usage: '/jump <target>',
|
|
22
|
+
description: 'Jump transcript to latest | error | tool',
|
|
23
|
+
insertText: '/jump ',
|
|
24
|
+
section: 'commands',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'permissions',
|
|
28
|
+
usage: '/permissions',
|
|
29
|
+
description: 'Change tool permission mode',
|
|
30
|
+
aliases: ['permission-mode'],
|
|
31
|
+
section: 'commands',
|
|
32
|
+
},
|
|
15
33
|
{ id: 'thinking', usage: '/thinking', description: 'Change reasoning intensity', section: 'commands' },
|
|
34
|
+
{
|
|
35
|
+
id: 'btw',
|
|
36
|
+
usage: '/btw [message]',
|
|
37
|
+
description: "Start a side conversation that won't affect history (Esc to exit)",
|
|
38
|
+
insertText: '/btw ',
|
|
39
|
+
section: 'commands',
|
|
40
|
+
},
|
|
16
41
|
{ id: 'quit', usage: '/quit', description: 'Exit the application', aliases: ['exit'], section: 'commands' },
|
|
17
42
|
{ id: 'plugins', usage: '/plugins', description: 'List installed plugins', aliases: ['skills'], section: 'plugins' },
|
|
18
|
-
{
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
43
|
+
{
|
|
44
|
+
id: 'plugins add',
|
|
45
|
+
usage: '/plugins add',
|
|
46
|
+
description: 'Add a new plugin (prompt or MCP)',
|
|
47
|
+
insertText: '/plugins add ',
|
|
48
|
+
section: 'plugins',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'plugins install',
|
|
52
|
+
usage: '/plugins install',
|
|
53
|
+
description: 'Install from GitHub (owner/repo/path)',
|
|
54
|
+
insertText: '/plugins install ',
|
|
55
|
+
aliases: ['plugins add-github', 'plugins github'],
|
|
56
|
+
section: 'plugins',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'plugins browse',
|
|
60
|
+
usage: '/plugins browse',
|
|
61
|
+
description: 'Browse a GitHub skill repo',
|
|
62
|
+
insertText: '/plugins browse ',
|
|
63
|
+
section: 'plugins',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'plugins remove',
|
|
67
|
+
usage: '/plugins remove',
|
|
68
|
+
description: 'Remove a plugin',
|
|
69
|
+
insertText: '/plugins remove ',
|
|
70
|
+
aliases: ['plugins rm'],
|
|
71
|
+
section: 'plugins',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'plugins enable',
|
|
75
|
+
usage: '/plugins enable',
|
|
76
|
+
description: 'Enable a disabled plugin',
|
|
77
|
+
insertText: '/plugins enable ',
|
|
78
|
+
section: 'plugins',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'plugins disable',
|
|
82
|
+
usage: '/plugins disable',
|
|
83
|
+
description: 'Disable a plugin',
|
|
84
|
+
insertText: '/plugins disable ',
|
|
85
|
+
section: 'plugins',
|
|
86
|
+
},
|
|
24
87
|
{ id: 'session', usage: '/session [new]', description: 'Show session info or start a fresh one', section: 'session' },
|
|
25
88
|
{ id: 'config', usage: '/config', description: 'Show current configuration', section: 'session' },
|
|
26
89
|
{ id: 'model', usage: '/model [id]', description: 'Change model id', section: 'model' },
|
|
27
90
|
{ id: 'endpoint', usage: '/endpoint [url]', description: 'Change API endpoint URL', section: 'model' },
|
|
28
|
-
{
|
|
91
|
+
{
|
|
92
|
+
id: 'api',
|
|
93
|
+
usage: '/api [style]',
|
|
94
|
+
description: 'Set API style (chat or responses)',
|
|
95
|
+
aliases: ['api-style'],
|
|
96
|
+
section: 'model',
|
|
97
|
+
},
|
|
29
98
|
];
|
|
30
99
|
const SLASH_COMMAND_SECTION_TITLES = {
|
|
31
100
|
commands: 'COMMANDS',
|
|
@@ -43,15 +112,10 @@ export function getSlashCommandSuggestions(input, running, limit = 6) {
|
|
|
43
112
|
if (query === null)
|
|
44
113
|
return [];
|
|
45
114
|
const normalizedQuery = query.toLowerCase();
|
|
46
|
-
const candidates = SLASH_COMMANDS
|
|
47
|
-
.filter((item) => !running || item.allowWhileRunning)
|
|
115
|
+
const candidates = SLASH_COMMANDS.filter((item) => !running || item.allowWhileRunning)
|
|
48
116
|
.map((item, index) => {
|
|
49
117
|
const aliases = item.aliases ?? [];
|
|
50
|
-
const searchTerms = [
|
|
51
|
-
item.id,
|
|
52
|
-
item.usage.replace(/^\//, ''),
|
|
53
|
-
...aliases,
|
|
54
|
-
].map((term) => term.toLowerCase());
|
|
118
|
+
const searchTerms = [item.id, item.usage.replace(/^\//, ''), ...aliases].map((term) => term.toLowerCase());
|
|
55
119
|
let rank = normalizedQuery ? 3 : 1;
|
|
56
120
|
if (normalizedQuery) {
|
|
57
121
|
if (searchTerms.some((term) => term === normalizedQuery)) {
|
|
@@ -166,16 +230,17 @@ export async function handleSlashCommand(input, ctx) {
|
|
|
166
230
|
}
|
|
167
231
|
case 'permissions':
|
|
168
232
|
case 'permission-mode': {
|
|
169
|
-
const nextMode = args[0] ??
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
233
|
+
const nextMode = args[0] ??
|
|
234
|
+
(await ctx.openPicker({
|
|
235
|
+
title: 'Permission mode',
|
|
236
|
+
description: 'Choose how aggressively the agent may run tools.',
|
|
237
|
+
items: [
|
|
238
|
+
{ id: 'default', label: 'default', description: 'Ask when the SDK needs approval.' },
|
|
239
|
+
{ id: 'allowAll', label: 'allowAll', description: 'Allow all tool calls without prompts.' },
|
|
240
|
+
{ id: 'rulesOnly', label: 'rulesOnly', description: 'Allow only rules-backed tool calls, deny the rest.' },
|
|
241
|
+
],
|
|
242
|
+
selectedId: config.permissionMode,
|
|
243
|
+
}));
|
|
179
244
|
if (!nextMode)
|
|
180
245
|
return;
|
|
181
246
|
const parsedMode = parsePermissionMode(nextMode);
|
|
@@ -187,19 +252,20 @@ export async function handleSlashCommand(input, ctx) {
|
|
|
187
252
|
return;
|
|
188
253
|
}
|
|
189
254
|
case 'thinking': {
|
|
190
|
-
const nextLevel = args[0] ??
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
255
|
+
const nextLevel = args[0] ??
|
|
256
|
+
(await ctx.openPicker({
|
|
257
|
+
title: 'Thinking level',
|
|
258
|
+
description: 'Higher levels may improve answers, but they usually cost more and respond slower.',
|
|
259
|
+
items: [
|
|
260
|
+
{ id: 'off', label: 'off', description: 'No extra reasoning budget.' },
|
|
261
|
+
{ id: 'minimal', label: 'minimal', description: 'Small reasoning budget.' },
|
|
262
|
+
{ id: 'low', label: 'low', description: 'Low reasoning budget.' },
|
|
263
|
+
{ id: 'medium', label: 'medium', description: 'Balanced reasoning budget.' },
|
|
264
|
+
{ id: 'high', label: 'high', description: 'More deliberate reasoning.' },
|
|
265
|
+
{ id: 'xhigh', label: 'xhigh', description: 'Maximum reasoning budget.' },
|
|
266
|
+
],
|
|
267
|
+
selectedId: config.thinkingLevel,
|
|
268
|
+
}));
|
|
203
269
|
if (!nextLevel)
|
|
204
270
|
return;
|
|
205
271
|
const parsedLevel = parseThinkingLevel(nextLevel);
|
|
@@ -211,12 +277,13 @@ export async function handleSlashCommand(input, ctx) {
|
|
|
211
277
|
return;
|
|
212
278
|
}
|
|
213
279
|
case 'name': {
|
|
214
|
-
const nextName = rest ||
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
280
|
+
const nextName = rest ||
|
|
281
|
+
(await ctx.openPrompt({
|
|
282
|
+
title: 'Agent name',
|
|
283
|
+
description: 'Pick a short and cute name for this coding agent.',
|
|
284
|
+
initialValue: config.agentName,
|
|
285
|
+
placeholder: 'Example: Pista',
|
|
286
|
+
}));
|
|
220
287
|
if (!nextName)
|
|
221
288
|
return;
|
|
222
289
|
const normalizedName = normalizeAgentName(nextName);
|
|
@@ -230,18 +297,19 @@ export async function handleSlashCommand(input, ctx) {
|
|
|
230
297
|
try {
|
|
231
298
|
const apiKey = getStoredApiKey();
|
|
232
299
|
const response = await fetch(`${config.selection.endpoint}/models`, {
|
|
233
|
-
headers: apiKey ? {
|
|
300
|
+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
234
301
|
});
|
|
235
302
|
if (response.ok) {
|
|
236
|
-
const data = await response.json();
|
|
237
|
-
const models = data.data.map(m => m.id);
|
|
303
|
+
const data = (await response.json());
|
|
304
|
+
const models = data.data.map((m) => m.id);
|
|
238
305
|
if (models.length > 0) {
|
|
239
|
-
nextModel =
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
306
|
+
nextModel =
|
|
307
|
+
(await ctx.openPicker({
|
|
308
|
+
title: 'Select Model',
|
|
309
|
+
items: models.map((id) => ({ id, label: id })),
|
|
310
|
+
selectedId: config.selection.model,
|
|
311
|
+
emptyMessage: 'No models returned from endpoint.',
|
|
312
|
+
})) || '';
|
|
245
313
|
}
|
|
246
314
|
}
|
|
247
315
|
}
|
|
@@ -269,11 +337,12 @@ export async function handleSlashCommand(input, ctx) {
|
|
|
269
337
|
return;
|
|
270
338
|
}
|
|
271
339
|
case 'endpoint': {
|
|
272
|
-
const nextEndpoint = rest ||
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
340
|
+
const nextEndpoint = rest ||
|
|
341
|
+
(await ctx.openPrompt({
|
|
342
|
+
title: 'API endpoint',
|
|
343
|
+
description: 'Example: https://api.research.computer/v1',
|
|
344
|
+
initialValue: config.selection.endpoint,
|
|
345
|
+
}));
|
|
277
346
|
if (!nextEndpoint)
|
|
278
347
|
return;
|
|
279
348
|
try {
|
|
@@ -291,14 +360,15 @@ export async function handleSlashCommand(input, ctx) {
|
|
|
291
360
|
}
|
|
292
361
|
case 'api':
|
|
293
362
|
case 'api-style': {
|
|
294
|
-
const nextStyle = args[0] ??
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
363
|
+
const nextStyle = args[0] ??
|
|
364
|
+
(await ctx.openPicker({
|
|
365
|
+
title: 'Pick API style',
|
|
366
|
+
items: [
|
|
367
|
+
{ id: 'chat', label: 'chat', description: 'OpenAI-compatible chat/completions API' },
|
|
368
|
+
{ id: 'responses', label: 'responses', description: 'OpenAI-compatible responses API' },
|
|
369
|
+
],
|
|
370
|
+
selectedId: config.selection.apiStyle,
|
|
371
|
+
}));
|
|
302
372
|
if (!nextStyle)
|
|
303
373
|
return;
|
|
304
374
|
const parsedStyle = parseCompatApiStyle(nextStyle);
|
|
@@ -379,11 +449,12 @@ export async function handleSlashCommand(input, ctx) {
|
|
|
379
449
|
return;
|
|
380
450
|
}
|
|
381
451
|
if (subcommand === 'add') {
|
|
382
|
-
const id = rest.slice(4).trim() ||
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
452
|
+
const id = rest.slice(4).trim() ||
|
|
453
|
+
(await ctx.openPrompt({
|
|
454
|
+
title: 'Plugin ID',
|
|
455
|
+
description: 'A unique identifier for this plugin (e.g. "typescript-style", "my-mcp-server").',
|
|
456
|
+
placeholder: 'my-plugin',
|
|
457
|
+
}));
|
|
387
458
|
if (!id)
|
|
388
459
|
return;
|
|
389
460
|
const description = await ctx.openPrompt({
|
|
@@ -411,7 +482,10 @@ export async function handleSlashCommand(input, ctx) {
|
|
|
411
482
|
if (description)
|
|
412
483
|
skill.description = description;
|
|
413
484
|
if (promptText) {
|
|
414
|
-
skill.promptSections = promptText
|
|
485
|
+
skill.promptSections = promptText
|
|
486
|
+
.split('\\n')
|
|
487
|
+
.map((s) => s.trim())
|
|
488
|
+
.filter(Boolean);
|
|
415
489
|
}
|
|
416
490
|
if (mcpChoice !== 'no') {
|
|
417
491
|
const transport = mcpChoice;
|
|
@@ -428,12 +502,14 @@ export async function handleSlashCommand(input, ctx) {
|
|
|
428
502
|
description: 'Space-separated arguments for the command.',
|
|
429
503
|
placeholder: '-y @modelcontextprotocol/server-filesystem /',
|
|
430
504
|
});
|
|
431
|
-
skill.mcpServers = [
|
|
505
|
+
skill.mcpServers = [
|
|
506
|
+
{
|
|
432
507
|
name: id.trim(),
|
|
433
508
|
transport,
|
|
434
509
|
command,
|
|
435
510
|
args: argsStr ? argsStr.split(/\s+/) : [],
|
|
436
|
-
}
|
|
511
|
+
},
|
|
512
|
+
];
|
|
437
513
|
}
|
|
438
514
|
else {
|
|
439
515
|
const url = await ctx.openPrompt({
|
|
@@ -464,11 +540,12 @@ export async function handleSlashCommand(input, ctx) {
|
|
|
464
540
|
return;
|
|
465
541
|
}
|
|
466
542
|
if (subcommand === 'add-github' || subcommand === 'github' || subcommand === 'install') {
|
|
467
|
-
const source = rest.slice(subcommand.length + 1).trim() ||
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
543
|
+
const source = rest.slice(subcommand.length + 1).trim() ||
|
|
544
|
+
(await ctx.openPrompt({
|
|
545
|
+
title: 'GitHub source',
|
|
546
|
+
description: 'Enter owner/repo or owner/repo/path/to/skill (e.g. "anthropics/skills/skills/pdf").',
|
|
547
|
+
placeholder: 'anthropics/skills/skills/pdf',
|
|
548
|
+
}));
|
|
472
549
|
if (!source)
|
|
473
550
|
return;
|
|
474
551
|
ctx.appendLog('system', 'Fetching plugin', `Downloading SKILL.md from ${source}...`);
|
|
@@ -503,11 +580,12 @@ export async function handleSlashCommand(input, ctx) {
|
|
|
503
580
|
return;
|
|
504
581
|
}
|
|
505
582
|
if (subcommand === 'browse') {
|
|
506
|
-
const source = rest.slice(7).trim() ||
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
583
|
+
const source = rest.slice(7).trim() ||
|
|
584
|
+
(await ctx.openPrompt({
|
|
585
|
+
title: 'GitHub repository',
|
|
586
|
+
description: 'Enter owner/repo to browse available skills (e.g. "anthropics/skills").',
|
|
587
|
+
placeholder: 'anthropics/skills',
|
|
588
|
+
}));
|
|
511
589
|
if (!source)
|
|
512
590
|
return;
|
|
513
591
|
const parts = source.replace(/https?:\/\/github\.com\//, '').split('/');
|
|
@@ -582,14 +660,15 @@ export async function handleSlashCommand(input, ctx) {
|
|
|
582
660
|
ctx.appendLog('system', 'Plugins', 'No plugins to remove.');
|
|
583
661
|
return;
|
|
584
662
|
}
|
|
585
|
-
const targetId = args[1] ||
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
663
|
+
const targetId = args[1] ||
|
|
664
|
+
(await ctx.openPicker({
|
|
665
|
+
title: 'Remove plugin',
|
|
666
|
+
items: allSkills.map((s) => ({
|
|
667
|
+
id: `${s.scope}:${s.id}`,
|
|
668
|
+
label: `${s.id} [${s.scope}]`,
|
|
669
|
+
description: s.description,
|
|
670
|
+
})),
|
|
671
|
+
}));
|
|
593
672
|
if (!targetId)
|
|
594
673
|
return;
|
|
595
674
|
const [targetScope, ...idParts] = targetId.split(':');
|
|
@@ -615,19 +694,20 @@ export async function handleSlashCommand(input, ctx) {
|
|
|
615
694
|
...(globalPrefs.skills ?? []).map((s) => ({ ...s, scope: 'global' })),
|
|
616
695
|
...(projectPrefs.skills ?? []).map((s) => ({ ...s, scope: 'project' })),
|
|
617
696
|
];
|
|
618
|
-
const filtered = allSkills.filter((s) => enabling ? s.enabled === false : s.enabled !== false);
|
|
697
|
+
const filtered = allSkills.filter((s) => (enabling ? s.enabled === false : s.enabled !== false));
|
|
619
698
|
if (filtered.length === 0) {
|
|
620
699
|
ctx.appendLog('system', 'Plugins', `No plugins to ${subcommand}.`);
|
|
621
700
|
return;
|
|
622
701
|
}
|
|
623
|
-
const targetId = args[1] ||
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
702
|
+
const targetId = args[1] ||
|
|
703
|
+
(await ctx.openPicker({
|
|
704
|
+
title: `${enabling ? 'Enable' : 'Disable'} plugin`,
|
|
705
|
+
items: filtered.map((s) => ({
|
|
706
|
+
id: `${s.scope}:${s.id}`,
|
|
707
|
+
label: `${s.id} [${s.scope}]`,
|
|
708
|
+
description: s.description,
|
|
709
|
+
})),
|
|
710
|
+
}));
|
|
631
711
|
if (!targetId)
|
|
632
712
|
return;
|
|
633
713
|
const [targetScope, ...idParts] = targetId.split(':');
|
|
@@ -648,6 +728,22 @@ export async function handleSlashCommand(input, ctx) {
|
|
|
648
728
|
ctx.appendLog('error', 'Unknown subcommand', `Usage: /plugins [list|add|install|browse|remove|enable|disable]`);
|
|
649
729
|
return;
|
|
650
730
|
}
|
|
731
|
+
case 'btw': {
|
|
732
|
+
if (ctx.isBtwActive()) {
|
|
733
|
+
ctx.appendLog('system', 'Already in btw mode', 'Press Esc to exit first.');
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (rest) {
|
|
737
|
+
const entered = ctx.enterBtw(true);
|
|
738
|
+
if (entered) {
|
|
739
|
+
await ctx.promptAgent(rest);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
ctx.enterBtw(false);
|
|
744
|
+
}
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
651
747
|
case 'quit':
|
|
652
748
|
case 'exit':
|
|
653
749
|
ctx.exit();
|
package/dist/commands.test.js
CHANGED
|
@@ -34,3 +34,40 @@ test('getSlashCommandSuggestions hides the popup once arguments are being typed'
|
|
|
34
34
|
test('getSlashCommandSuggestions hides the popup after a completion adds a trailing space', () => {
|
|
35
35
|
assert.deepEqual(getSlashCommandSuggestions('/plugins add ', false), []);
|
|
36
36
|
});
|
|
37
|
+
test('parseSlashCommand handles multi-word command', () => {
|
|
38
|
+
const result = parseSlashCommand('/plugins add my-plugin');
|
|
39
|
+
assert.equal(result.command, 'plugins');
|
|
40
|
+
assert.deepEqual(result.args, ['add', 'my-plugin']);
|
|
41
|
+
assert.equal(result.rest, 'add my-plugin');
|
|
42
|
+
});
|
|
43
|
+
test('parseSlashCommand lowercases command', () => {
|
|
44
|
+
const result = parseSlashCommand('/HELP');
|
|
45
|
+
assert.equal(result.command, 'help');
|
|
46
|
+
});
|
|
47
|
+
test('getSlashCommandSuggestions returns empty for non-slash input', () => {
|
|
48
|
+
const suggestions = getSlashCommandSuggestions('hello', false);
|
|
49
|
+
assert.equal(suggestions.length, 0);
|
|
50
|
+
});
|
|
51
|
+
test('getSlashCommandSuggestions filters by running state', () => {
|
|
52
|
+
const idle = getSlashCommandSuggestions('/h', false);
|
|
53
|
+
const running = getSlashCommandSuggestions('/h', true);
|
|
54
|
+
assert.ok(idle.length >= running.length);
|
|
55
|
+
});
|
|
56
|
+
test('getSlashCommandSuggestions respects limit', () => {
|
|
57
|
+
const suggestions = getSlashCommandSuggestions('/', false, 2);
|
|
58
|
+
assert.ok(suggestions.length <= 2);
|
|
59
|
+
});
|
|
60
|
+
test('getSlashCommandSuggestions includes btw', () => {
|
|
61
|
+
const suggestions = getSlashCommandSuggestions('/btw', false);
|
|
62
|
+
assert.ok(suggestions.some((s) => s.id === 'btw'));
|
|
63
|
+
});
|
|
64
|
+
test('parseSlashCommand parses /btw with message', () => {
|
|
65
|
+
const result = parseSlashCommand('/btw what is this function?');
|
|
66
|
+
assert.equal(result.command, 'btw');
|
|
67
|
+
assert.equal(result.rest, 'what is this function?');
|
|
68
|
+
});
|
|
69
|
+
test('parseSlashCommand parses /btw without message', () => {
|
|
70
|
+
const result = parseSlashCommand('/btw');
|
|
71
|
+
assert.equal(result.command, 'btw');
|
|
72
|
+
assert.equal(result.rest, '');
|
|
73
|
+
});
|
|
@@ -2,8 +2,18 @@ import test from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { renderMarkdownLines } from './assistant-message-render.js';
|
|
4
4
|
test('renderMarkdownLines flattens common markdown formatting for terminal output', () => {
|
|
5
|
-
assert.deepEqual(renderMarkdownLines('# Title\n\nSee [docs](https://example.com) and **bold** text.'), [
|
|
5
|
+
assert.deepEqual(renderMarkdownLines('# Title\n\nSee [docs](https://example.com) and **bold** text.'), [
|
|
6
|
+
'Title',
|
|
7
|
+
'',
|
|
8
|
+
'See docs (https://example.com) and bold text.',
|
|
9
|
+
]);
|
|
6
10
|
});
|
|
7
11
|
test('renderMarkdownLines preserves code blocks while normalizing block syntax', () => {
|
|
8
|
-
assert.deepEqual(renderMarkdownLines('> note\n- item\n1. step\n---\n```ts\nconst value = 1;\n```'), [
|
|
12
|
+
assert.deepEqual(renderMarkdownLines('> note\n- item\n1. step\n---\n```ts\nconst value = 1;\n```'), [
|
|
13
|
+
'| note',
|
|
14
|
+
'• item',
|
|
15
|
+
'1. step',
|
|
16
|
+
'────────',
|
|
17
|
+
'const value = 1;',
|
|
18
|
+
]);
|
|
9
19
|
});
|
|
@@ -5,5 +5,5 @@ import { renderMarkdownLines } from './assistant-message-render.js';
|
|
|
5
5
|
export const AssistantMessage = React.memo(function AssistantMessage({ content, showSpinner = false, spinner, }) {
|
|
6
6
|
const body = content.trimEnd();
|
|
7
7
|
const lines = renderMarkdownLines(body);
|
|
8
|
-
return (_jsxs(Box, { flexDirection: "column", children: [body ? lines.map((line, index) =>
|
|
8
|
+
return (_jsxs(Box, { flexDirection: "column", children: [body ? lines.map((line, index) => _jsx(Text, { children: line || ' ' }, index)) : _jsx(Text, { dimColor: true, children: "[no content]" }), showSpinner && spinner ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: " " }), spinner] })) : null] }));
|
|
9
9
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { Frame } from './frame.js';
|
|
5
|
+
import { Editor } from './editor.js';
|
|
6
|
+
import { SlashCommandMenu } from './slash-command-menu.js';
|
|
7
|
+
export const ComposerPanel = React.memo(function ComposerPanel({ value, cursor, agentName, running, btwActive = false, slashMenuVisible, slashCommandItems, slashMenuSelectedIndex, termWidth, }) {
|
|
8
|
+
return (_jsxs(_Fragment, { children: [slashMenuVisible ? (_jsx(SlashCommandMenu, { items: slashCommandItems, selectedIndex: Math.min(slashMenuSelectedIndex, Math.max(0, slashCommandItems.length - 1)) })) : null, _jsx(Frame, { borderColor: btwActive ? 'magenta' : running ? 'yellow' : 'cyan', bottomLeft: slashMenuVisible
|
|
9
|
+
? 'Enter send · ⇧Enter newline · Tab complete · /help'
|
|
10
|
+
: 'Enter send · ⇧Enter newline · /help', termWidth: termWidth, children: _jsxs(Box, { children: [_jsx(Text, { color: "greenBright", bold: true, children: '> ' }), _jsx(Editor, { value: value, cursor: cursor, placeholder: `Ask ${agentName} or type /help` })] }) })] }));
|
|
11
|
+
});
|
|
@@ -16,7 +16,9 @@ export function editBuffer(current, input, key) {
|
|
|
16
16
|
const chars = Array.from(current.value);
|
|
17
17
|
let cursor = Math.max(0, Math.min(current.cursor, chars.length));
|
|
18
18
|
const isBackspaceInput = input === '\x7f' || input === '\b' || input === '\x08' || (key.ctrl && input === 'h');
|
|
19
|
-
|
|
19
|
+
// Many terminals report Backspace as key.delete with empty input.
|
|
20
|
+
// When key.delete fires with no input string, treat it as backspace.
|
|
21
|
+
const shouldTreatDeleteAsBackspace = key.delete && !input;
|
|
20
22
|
const isBackspace = key.backspace || isBackspaceInput || shouldTreatDeleteAsBackspace;
|
|
21
23
|
if (isBackspace) {
|
|
22
24
|
if (cursor > 0) {
|
|
@@ -51,7 +53,7 @@ export function editBuffer(current, input, key) {
|
|
|
51
53
|
}
|
|
52
54
|
// Character insertion
|
|
53
55
|
if (input && input.length > 0) {
|
|
54
|
-
const inputChars = Array.from(input).filter(c => {
|
|
56
|
+
const inputChars = Array.from(input).filter((c) => {
|
|
55
57
|
const code = c.charCodeAt(0);
|
|
56
58
|
return code >= 32 && code !== 127;
|
|
57
59
|
});
|
|
@@ -30,10 +30,18 @@ test('delete at end of line behaves like backspace for terminals that map it tha
|
|
|
30
30
|
const result = editBuffer({ value: 'abc', cursor: 3 }, '', key({ delete: true }));
|
|
31
31
|
assert.deepEqual(result, { value: 'ab', cursor: 2 });
|
|
32
32
|
});
|
|
33
|
-
test('delete
|
|
34
|
-
const result = editBuffer({ value: 'abcd', cursor:
|
|
33
|
+
test('key.delete with no input in the middle of the line acts as backspace', () => {
|
|
34
|
+
const result = editBuffer({ value: 'abcd', cursor: 2 }, '', key({ delete: true }));
|
|
35
35
|
assert.deepEqual(result, { value: 'acd', cursor: 1 });
|
|
36
36
|
});
|
|
37
|
+
test('delete with no input inside the line acts as backspace', () => {
|
|
38
|
+
const result = editBuffer({ value: 'abcd', cursor: 1 }, '', key({ delete: true }));
|
|
39
|
+
assert.deepEqual(result, { value: 'bcd', cursor: 0 });
|
|
40
|
+
});
|
|
41
|
+
test('delete at cursor 0 with no input is a no-op', () => {
|
|
42
|
+
const result = editBuffer({ value: 'abcd', cursor: 0 }, '', key({ delete: true }));
|
|
43
|
+
assert.deepEqual(result, { value: 'abcd', cursor: 0 });
|
|
44
|
+
});
|
|
37
45
|
test('ctrl+key combinations are ignored by the editor buffer', () => {
|
|
38
46
|
const result = editBuffer({ value: 'abcd', cursor: 2 }, 'p', key({ ctrl: true }));
|
|
39
47
|
assert.deepEqual(result, { value: 'abcd', cursor: 2 });
|
package/dist/components/frame.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
export const Frame = React.memo(function Frame({ children, borderColor, topLeft, topRight, bottomLeft, bottomRight, paddingX = 1, }) {
|
|
5
|
-
const
|
|
6
|
-
const usableWidth = termWidth - 2; // account for outer padding in app
|
|
4
|
+
export const Frame = React.memo(function Frame({ children, borderColor, topLeft, topRight, bottomLeft, bottomRight, paddingX = 1, termWidth, }) {
|
|
5
|
+
const usableWidth = (termWidth ?? 80) - 2; // account for outer padding in app
|
|
7
6
|
return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsx(TopBorder, { width: usableWidth, color: borderColor, left: topLeft, right: topRight }), _jsx(Box, { flexDirection: "column", paddingX: paddingX, children: children }), _jsx(BottomBorder, { width: usableWidth, color: borderColor, left: bottomLeft, right: bottomRight })] }));
|
|
8
7
|
});
|
|
9
|
-
function TopBorder({ width, color, left, right }) {
|
|
8
|
+
function TopBorder({ width, color, left, right, }) {
|
|
10
9
|
const leftText = left ? ` ${left} ` : '';
|
|
11
10
|
const rightText = right ? ` ${right} ` : '';
|
|
12
11
|
const fillLen = Math.max(0, width - 3 - leftText.length - rightText.length);
|
|
13
12
|
const fill = '─'.repeat(fillLen);
|
|
14
13
|
return (_jsxs(Text, { color: color, children: ["\u256D\u2500", leftText, fill, rightText, "\u256E"] }));
|
|
15
14
|
}
|
|
16
|
-
function BottomBorder({ width, color, left, right }) {
|
|
15
|
+
function BottomBorder({ width, color, left, right, }) {
|
|
17
16
|
const leftText = left ? ` ${left} ` : '';
|
|
18
17
|
const rightText = right ? ` ${right} ` : '';
|
|
19
18
|
const fillLen = Math.max(0, width - 3 - leftText.length - rightText.length);
|
|
@@ -47,9 +47,9 @@ export function formatPendingToolsLine(pendingTools, termWidth) {
|
|
|
47
47
|
}
|
|
48
48
|
return `▸ ${shown.join(', ')}`;
|
|
49
49
|
}
|
|
50
|
-
export const Header = React.memo(function Header({ agentName, sessionId, model, apiStyle, endpoint, permissionMode, thinkingLevel, status, pendingTools, }) {
|
|
50
|
+
export const Header = React.memo(function Header({ agentName, sessionId, model, apiStyle, endpoint, permissionMode, thinkingLevel, status, pendingTools, termWidth: termWidthProp, }) {
|
|
51
51
|
const color = statusColor(status);
|
|
52
|
-
const termWidth = process.stdout.columns || 80;
|
|
52
|
+
const termWidth = termWidthProp ?? (process.stdout.columns || 80);
|
|
53
53
|
// Build config line with progressive truncation for narrow terminals
|
|
54
54
|
const configParts = [model, apiStyle];
|
|
55
55
|
if (termWidth >= 60 && permissionMode !== 'default')
|
|
@@ -59,5 +59,5 @@ export const Header = React.memo(function Header({ agentName, sessionId, model,
|
|
|
59
59
|
const host = truncateStr(stripEndpoint(endpoint), Math.max(10, termWidth - 30));
|
|
60
60
|
const toolsLine = formatPendingToolsLine(pendingTools, termWidth);
|
|
61
61
|
const toolsActive = pendingTools.length > 0;
|
|
62
|
-
return (_jsxs(Frame, { borderColor: "gray", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { bold: true, color: "cyanBright", children: ["\u25CF ", truncateStr(agentName, Math.max(4, termWidth - status.length - 8))] }), _jsxs(Text, { color: color, children: ["\u25CF ", status] })] }), _jsx(Text, { dimColor: true, wrap: "truncate", children: configParts.join(' · ') }), _jsxs(Text, { dimColor: true, wrap: "truncate", children: [host, "
|
|
62
|
+
return (_jsxs(Frame, { borderColor: "gray", termWidth: termWidth, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { bold: true, color: "cyanBright", children: ["\u25CF ", truncateStr(agentName, Math.max(4, termWidth - status.length - 8))] }), _jsxs(Text, { color: color, children: ["\u25CF ", status] })] }), _jsx(Text, { dimColor: true, wrap: "truncate", children: configParts.join(' · ') }), _jsxs(Text, { dimColor: true, wrap: "truncate", children: [host, " session:", sessionId.slice(0, 8)] }), _jsx(Text, { color: toolsActive ? 'yellow' : undefined, dimColor: !toolsActive, wrap: "truncate", children: toolsLine })] }));
|
|
63
63
|
});
|
|
@@ -17,12 +17,12 @@ function formatTime(timestamp) {
|
|
|
17
17
|
const s = String(d.getSeconds()).padStart(2, '0');
|
|
18
18
|
return `${h}:${m}:${s}`;
|
|
19
19
|
}
|
|
20
|
-
export const LogRow = React.memo(function LogRow({ entry }) {
|
|
20
|
+
export const LogRow = React.memo(function LogRow({ entry, termWidth: termWidthProp, }) {
|
|
21
21
|
const color = logColor(entry.kind);
|
|
22
22
|
const symbol = SYMBOLS[entry.kind] ?? '·';
|
|
23
23
|
const time = formatTime(entry.timestamp);
|
|
24
24
|
const bodyLines = (entry.body || '[no content]').split('\n');
|
|
25
|
-
const termWidth = process.stdout.columns || 80;
|
|
25
|
+
const termWidth = termWidthProp ?? (process.stdout.columns || 80);
|
|
26
26
|
const showTime = termWidth >= 50;
|
|
27
27
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: color, bold: true, children: [symbol, " ", entry.title] }), showTime ? _jsx(Text, { dimColor: true, children: time }) : null] }), _jsx(Box, { paddingLeft: 2, flexDirection: "column", children: entry.kind === 'assistant' ? (_jsx(AssistantMessage, { content: entry.body || '[no content]' })) : (bodyLines.map((line, index) => (_jsx(Text, { wrap: "truncate", children: line || ' ' }, `${entry.id}-${index}`)))) })] }));
|
|
28
28
|
});
|