@causyne/agent 1.10.5 → 1.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@causyne/agent",
3
- "version": "1.10.5",
3
+ "version": "1.10.6",
4
4
  "description": "Agentic AI for software, tools, and computer workflows",
5
5
  "main": "bin/causyne.js",
6
6
  "type": "module",
package/src/cliMode.js CHANGED
@@ -11,6 +11,7 @@ import { wrapBlock } from './tui/text.js';
11
11
  import { getPackageVersion } from './version.js';
12
12
  import { resolveCharPool } from './contextManager.js';
13
13
  import { PRIMARY_ACTIONS, homeStatusLine, homeHint, moreLayer, configureInPrompt, mcpPicker, PROVIDERS } from './tui/home.js';
14
+ import { SelectLayer } from './tui/dialogs.js';
14
15
  import { askSelect, askText, askConfirm } from './tui/ask.js';
15
16
  import { renderReport } from './reporting.js';
16
17
  import { grantAlways } from './approvalGrants.js';
@@ -352,8 +353,8 @@ export async function chatMode({ home = false } = {}) {
352
353
  root: process.cwd(),
353
354
  commands: listCommands('terminal'),
354
355
  // The rule for "is this a command" lives in commands.js and is handed
355
- // down rather than reimplemented, so the prompt draws a `/usage` as an
356
- // invocation instead of as a sentence the user supposedly said.
356
+ // down rather than reimplemented, so the prompt keeps `/usage` out of
357
+ // the conversation instead of drawing it as something the user said.
357
358
  isCommand: isCommandInput,
358
359
  surface,
359
360
  onInterrupt: () => loop.abort(),
@@ -458,6 +459,38 @@ export async function chatMode({ home = false } = {}) {
458
459
  continue;
459
460
  }
460
461
 
462
+ if (commandResult.effect === 'pick-session' && prompt) {
463
+ const active = commandResult.active;
464
+ prompt.openLayer(new SelectLayer({
465
+ title: 'Sessions',
466
+ emptyText: 'No sessions yet',
467
+ items: (commandResult.items || []).map((session) => ({
468
+ id: session.id,
469
+ label: `${session.id === active ? `${GLYPHS.bullet} ` : ''}${session.title}`,
470
+ description: `${session.messageCount} messages ${GLYPHS.dot} ${session.workspace || 'no workspace'}`,
471
+ value: session
472
+ })),
473
+ onPick: (item) => {
474
+ if (item) prompt.deliver(`/sessions switch ${item.value.id}`);
475
+ }
476
+ }));
477
+ continue;
478
+ }
479
+
480
+ if (commandResult.effect === 'pick-mcp' && prompt) {
481
+ const { empty } = await mcpPicker(prompt, (server) => {
482
+ const verb = server.granted ? 'revoke' : 'grant';
483
+ prompt.deliver(`/mcp ${verb} ${server.name}`);
484
+ });
485
+ if (empty) prompt.showText('MCP servers', commandResult.output);
486
+ continue;
487
+ }
488
+
489
+ if (commandResult.effect === 'dialog' && prompt) {
490
+ prompt.showText(commandResult.title, commandResult.output, { danger: !commandResult.ok });
491
+ continue;
492
+ }
493
+
461
494
  // A skill was invoked: it becomes part of the REQUEST, not terminal output.
462
495
  //
463
496
  // This is the whole point. `/skills <id>` used to print the file and call
package/src/commands.js CHANGED
@@ -47,6 +47,7 @@ export const COMMANDS = [
47
47
  scope: 'local',
48
48
  usage: '/help [command]',
49
49
  description: 'List the available commands',
50
+ dialogTitle: 'Commands',
50
51
  run: (argv, ctx) => {
51
52
  const [wanted] = argv;
52
53
  if (wanted) {
@@ -335,6 +336,7 @@ export const COMMANDS = [
335
336
  scope: 'local',
336
337
  usage: '/checkpoints [count]',
337
338
  description: 'List recent undo checkpoints',
339
+ dialogTitle: 'Checkpoints',
338
340
  run: (argv) => ok(buildCheckpointsText(parseInt(argv[0], 10) || 15))
339
341
  },
340
342
 
@@ -344,6 +346,7 @@ export const COMMANDS = [
344
346
  scope: 'local',
345
347
  usage: '/context',
346
348
  description: 'Show what fills the context window',
349
+ dialogTitle: 'Context',
347
350
  run: () => ok(buildContextText())
348
351
  },
349
352
 
@@ -353,6 +356,7 @@ export const COMMANDS = [
353
356
  scope: 'local',
354
357
  usage: '/usage',
355
358
  description: 'Show token usage and estimated cost',
359
+ dialogTitle: 'Usage',
356
360
  run: async () => ok(await buildUsageText())
357
361
  },
358
362
 
@@ -362,6 +366,7 @@ export const COMMANDS = [
362
366
  scope: 'local',
363
367
  usage: '/cortex [list | forget <id> | forget-here]',
364
368
  description: 'Inspect what the agent has learned, and forget any of it',
369
+ dialogTitle: 'Cortex',
365
370
  // A memory store you cannot read is not a memory, it is a black box that
366
371
  // occasionally changes the agent's behaviour. Section 4 asks for intelligent
367
372
  // safety rather than opacity, and section 10 layer 5 asks for a trail a
@@ -426,6 +431,7 @@ export const COMMANDS = [
426
431
  scope: 'local',
427
432
  usage: '/sessions [switch <id> | delete <id>]',
428
433
  description: 'List chat sessions, or switch to one',
434
+ dialogTitle: 'Sessions',
429
435
  // Sessions replaced ONE global chat_history.json that every directory read.
430
436
  // A session you cannot list or switch to is not a session, so this ships
431
437
  // with the store rather than after it.
@@ -462,7 +468,8 @@ export const COMMANDS = [
462
468
  return ok(
463
469
  `${rows.length} session${rows.length === 1 ? '' : 's'} (* = active):\n`
464
470
  + lines.join('\n')
465
- + '\n\nSwitch with /sessions switch <id>. A new directory always starts a new session.'
471
+ + '\n\nSwitch with /sessions switch <id>. A new directory always starts a new session.',
472
+ { effect: 'pick-session', items: rows, active }
466
473
  );
467
474
  }
468
475
  },
@@ -472,6 +479,7 @@ export const COMMANDS = [
472
479
  scope: 'local',
473
480
  usage: '/approvals [clear]',
474
481
  description: 'List tools granted "Allow always", or revoke them',
482
+ dialogTitle: 'Approvals',
475
483
  // A standing grant with no way to see or revoke it is a trap, not a
476
484
  // convenience. This is the way out, and the confirm dialog that grants one
477
485
  // names this command so the user is told about it at the moment it matters.
@@ -541,6 +549,7 @@ export const COMMANDS = [
541
549
  scope: 'local',
542
550
  usage: '/model [provider/model | model]',
543
551
  description: 'Show or change the model in use',
552
+ dialogTitle: 'Model',
544
553
  run: (argv) => {
545
554
  const config = loadConfig();
546
555
  if (argv.length === 0) {
@@ -567,6 +576,7 @@ export const COMMANDS = [
567
576
  scope: 'local',
568
577
  usage: '/mcp [add <name> <cmd…> | add <name> --url <url> | remove|grant|revoke <name>]',
569
578
  description: 'Manage MCP servers: list, add, remove, grant, revoke',
579
+ dialogTitle: 'MCP servers',
570
580
  run: (argv) => {
571
581
  const [action, serverName, ...rest] = argv;
572
582
 
@@ -622,7 +632,7 @@ export const COMMANDS = [
622
632
 
623
633
  const servers = serverStatus();
624
634
  if (servers.length === 0) {
625
- return ok('No MCP servers are configured.\n\nAdd one with:\n mcp add <name> <command> [args…]\n mcp add <name> --url <https://…>\n\n(prefix with / in chat, or `causyne-agent mcp …` in a terminal)');
635
+ return ok('No MCP servers are configured.\n\nAdd one with:\n mcp add <name> <command> [args…]\n mcp add <name> --url <https://…>\n\n(prefix with / in chat, or `causyne-agent mcp …` in a terminal)', { effect: 'pick-mcp' });
626
636
  }
627
637
  const lines = ['MCP servers', ''];
628
638
  for (const server of servers) {
@@ -632,7 +642,7 @@ export const COMMANDS = [
632
642
  }
633
643
  lines.push('');
634
644
  lines.push(' mcp grant <name> to enable, mcp remove <name> to delete.');
635
- return ok(lines.join('\n'));
645
+ return ok(lines.join('\n'), { effect: 'pick-mcp' });
636
646
  }
637
647
  },
638
648
 
@@ -703,6 +713,7 @@ export const COMMANDS = [
703
713
  scope: 'local',
704
714
  usage: '/status',
705
715
  description: 'Show the current provider, autonomy mode and workspace scope',
716
+ dialogTitle: 'Status',
706
717
  // One builder, shared with `causyne status`, so status output cannot drift.
707
718
  run: (argv, ctx) => ok(buildStatusText({ loop: ctx.loop || null }))
708
719
  },
@@ -791,7 +802,11 @@ export async function runCommand(name, argv = [], ctx = {}) {
791
802
  }
792
803
 
793
804
  try {
794
- return await command.run(argv, { ...ctx, surface });
805
+ const result = await command.run(argv, { ...ctx, surface });
806
+ if (command.dialogTitle && result && !result.effect) {
807
+ return { ...result, effect: 'dialog', title: command.dialogTitle };
808
+ }
809
+ return result;
795
810
  } catch (e) {
796
811
  return fail(`/${command.name} failed: ${e.message}`);
797
812
  }
package/src/tui/home.js CHANGED
@@ -207,7 +207,7 @@ export async function mcpPicker(prompt, onAction) {
207
207
  items: rows.map((server) => ({
208
208
  id: server.name,
209
209
  label: server.name,
210
- description: `${server.granted ? 'granted' : 'not granted'}${server.command ? ` ${GLYPHS.dot} ${server.command}` : ''}`,
210
+ description: `${server.granted ? 'granted' : 'not granted'}${server.target ? ` ${GLYPHS.dot} ${server.target}` : ''}`,
211
211
  category: server.granted ? 'granted' : 'available',
212
212
  value: server
213
213
  })),
package/src/tui/prompt.js CHANGED
@@ -23,10 +23,10 @@ import { PromptHistory } from './history.js';
23
23
  import { FileIndex, suggest, applyCompletion, renderPicker, MAX_VISIBLE } from './completion.js';
24
24
  import { paint, dim, bold, GLYPHS, terminalWidth, HAS_COLOR } from './theme.js';
25
25
  import { barBlock, GUTTER } from './block.js';
26
- import { visibleWidth, truncate, wrapBlock, shortenHome } from './text.js';
26
+ import { visibleWidth, truncate, wrapBlock, shortenHome, safeDisplay } from './text.js';
27
27
  import { pickExample, fitHints } from './banner.js';
28
28
  import { panelRows } from './overlay.js';
29
- import { commandPalette, skillPicker, ApprovalLayer, ConfirmLayer, HomeLayer } from './dialogs.js';
29
+ import { commandPalette, skillPicker, ApprovalLayer, ConfirmLayer, HomeLayer, TextLayer } from './dialogs.js';
30
30
  import { describeTool } from './toolDisplay.js';
31
31
 
32
32
  const CTRL_C_WINDOW_MS = 2000;
@@ -419,6 +419,19 @@ export class ChatPrompt {
419
419
  }));
420
420
  }
421
421
 
422
+ /** Show local command output inside the live region, never in chat scrollback. */
423
+ showText(title, text, options = {}) {
424
+ const panelWidth = Math.max(28, Math.min(this.width() - 2, options.width || 76));
425
+ const contentWidth = Math.max(12, panelWidth - 4);
426
+ const lines = wrapBlock(safeDisplay(String(text == null ? '' : text)), contentWidth);
427
+ return this.openLayer(new TextLayer({
428
+ title: String(title || 'Details'),
429
+ lines: options.danger ? lines.map((line) => paint('danger', line)) : lines,
430
+ width: panelWidth,
431
+ height: options.height || 20
432
+ }));
433
+ }
434
+
422
435
  /**
423
436
  * Ask for approval of a tool call: risk, arguments and a scrollable diff.
424
437
  *
@@ -549,19 +562,11 @@ export class ChatPrompt {
549
562
  return;
550
563
  }
551
564
 
552
- // A COMMAND IS NOT A MESSAGE, so it must not be drawn as one.
553
- //
554
- // This echoed every submitted line as a filled user block BEFORE the caller
555
- // had any chance to decide what the line was. Typing `/usage` therefore
556
- // rendered exactly like a sentence you had sent to the model — the command
557
- // did run, and its output appeared underneath, but the transcript showed a
558
- // conversation turn that never happened. opencode draws an invocation here,
559
- // not a message, and the difference is the whole reason a command line
560
- // exists.
565
+ // A command is control input, not conversation. Its handler decides whether
566
+ // to open a picker, show a text dialog, or perform an action. Keeping the
567
+ // invocation out of the transcript preserves the boundary between local UI
568
+ // and messages that were actually sent to the model.
561
569
  if (this.isCommand(text)) {
562
- this.commit('command', (width) => [
563
- `${paint('accent', GLYPHS.arrow)} ${bold(truncate(text, Math.max(8, width - 3), GLYPHS.ellipsis))}`
564
- ]);
565
570
  const settle = this.waiter;
566
571
  this.waiter = null;
567
572
  if (settle) settle(text);